Skip to content

Commit 6e309e5

Browse files
Iterate on EventTracker
1 parent 8f189a6 commit 6e309e5

File tree

12 files changed

+157
-116
lines changed

12 files changed

+157
-116
lines changed

src/layouts/BaseLayout/BaseLayout.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import NetworkErrorBoundary from 'layouts/shared/NetworkErrorBoundary'
1010
import SilentNetworkErrorWrapper from 'layouts/shared/SilentNetworkErrorWrapper'
1111
import ToastNotifications from 'layouts/ToastNotifications'
1212
import { RepoBreadcrumbProvider } from 'pages/RepoPage/context'
13+
import { useEventContext } from 'services/events/events'
1314
import { useImpersonate } from 'services/impersonate'
1415
import { useTracking } from 'services/tracking'
1516
import GlobalBanners from 'shared/GlobalBanners'
@@ -77,6 +78,7 @@ interface URLParams {
7778
function BaseLayout({ children }: React.PropsWithChildren) {
7879
const { provider, owner, repo } = useParams<URLParams>()
7980
useTracking()
81+
useEventContext()
8082
const { isImpersonating } = useImpersonate()
8183
const {
8284
isFullExperience,

src/layouts/Header/components/UserDropdown/UserDropdown.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function UserDropdown() {
3535
},
3636
})
3737

38-
const { provider, owner, repo } = useParams<URLParams>()
38+
const { provider } = useParams<URLParams>()
3939
const isGh = providerToName(provider) === 'GitHub'
4040
const history = useHistory()
4141

@@ -46,11 +46,11 @@ function UserDropdown() {
4646
to: { pageName: 'codecovAppInstallation' },
4747
children: 'Install Codecov app',
4848
onClick: () =>
49-
eventTracker(provider, owner, repo).track({
49+
eventTracker().track({
5050
type: 'Button Clicked',
5151
properties: {
52-
buttonType: 'Install Github App',
53-
buttonLocation: 'UserDropdown',
52+
buttonType: 'Install GitHub App',
53+
buttonLocation: 'User dropdown',
5454
},
5555
}),
5656
} as DropdownItem,

src/pages/OwnerPage/HeaderBanners/GithubConfigBanner/GithubConfigBanner.jsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const GithubConfigBanner = () => {
2626
eventTracker(provider, owner).track({
2727
type: 'Button Clicked',
2828
properties: {
29-
buttonType: 'Install Github App',
30-
buttonLocation: 'GithubConfigBanner',
29+
buttonType: 'Install GitHub App',
30+
buttonLocation: 'Configure GitHub app banner',
3131
},
3232
})
3333
}
+21-47
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import * as amplitude from '@amplitude/analytics-browser'
22

3-
import { Provider } from 'shared/api/helpers'
4-
import {
5-
InternalProvider,
6-
providerToInternalProvider,
7-
} from 'shared/utils/provider'
3+
import { providerToInternalProvider } from 'shared/utils/provider'
84

9-
import { EventTracker } from '../events'
10-
import { Event } from '../types'
5+
import { Event, EventContext, EventTracker, Identity } from '../types'
116

127
const AMPLITUDE_API_KEY = process.env.REACT_APP_AMPLITUDE_API_KEY
138

149
export function initAmplitude() {
1510
if (!AMPLITUDE_API_KEY) {
16-
return
11+
throw new Error(
12+
'AMPLITUDE_API_KEY is not defined. Amplitude events will not be tracked.'
13+
)
1714
}
1815
amplitude.init(AMPLITUDE_API_KEY, {
1916
// Disable all autocapture - may change this in the future
@@ -23,40 +20,21 @@ export function initAmplitude() {
2320
}
2421

2522
export class AmplitudeEventTracker implements EventTracker {
26-
#provider?: InternalProvider
27-
#owner?: string
28-
#providerOwner?: string
29-
#repo?: string
23+
context: EventContext = {}
24+
identity?: Identity
3025

31-
constructor(provider?: Provider, owner?: string, repo?: string) {
32-
if (!AMPLITUDE_API_KEY) {
33-
throw new Error(
34-
'AMPLITUDE_API_KEY is not defined. Ensure the environment variable is defined before attempting to initialize AmplitudeEventTracker.'
35-
)
26+
identify(identity: Identity) {
27+
if (JSON.stringify(this.identity) === JSON.stringify(identity)) {
28+
// Don't identify this user again this session.
29+
return
3630
}
37-
this.#provider = provider ? providerToInternalProvider(provider) : undefined
38-
this.#owner = owner
39-
this.#providerOwner =
40-
this.#provider && this.#owner
41-
? formatProviderOwner(this.#provider, this.#owner)
42-
: undefined
43-
this.#repo = repo
44-
}
4531

46-
identify({
47-
userOwnerId,
48-
username,
49-
}: {
50-
userOwnerId: number
51-
username: string
52-
}) {
53-
amplitude.setUserId(userOwnerId.toString())
32+
amplitude.setUserId(identity.userOwnerId.toString())
5433
const identifyEvent = new amplitude.Identify()
55-
if (this.#provider) {
56-
identifyEvent.set('provider', this.#provider)
57-
}
58-
identifyEvent.set('username', username)
34+
identifyEvent.set('provider', providerToInternalProvider(identity.provider))
5935
amplitude.identify(identifyEvent)
36+
37+
this.identity = identity
6038
}
6139

6240
track(event: Event) {
@@ -65,23 +43,19 @@ export class AmplitudeEventTracker implements EventTracker {
6543
event_type: event.type,
6644
// eslint-disable-next-line camelcase
6745
event_properties: {
68-
owner: this.#providerOwner,
69-
repo: this.#repo,
7046
...event.properties,
47+
...this.context,
7148
},
7249
// This attaches the event to the owner's user group as well
73-
groups: this.#providerOwner
50+
groups: this.context.owner?.id
7451
? {
75-
owner: this.#providerOwner,
52+
owner: this.context.owner.id,
7653
}
7754
: undefined,
7855
})
7956
}
80-
}
8157

82-
function formatProviderOwner(provider: InternalProvider, ownerName: string) {
83-
// Helper function to format the owner group name.
84-
// The reason for this is owner names are not unique on their own, but
85-
// provider/owner names are.
86-
return `${provider}/${ownerName}`
58+
setContext(context: EventContext) {
59+
this.context = context
60+
}
8761
}

src/services/events/events.ts

+54-38
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,64 @@
1-
import { Provider } from 'shared/api/helpers'
1+
import { captureException } from '@sentry/react'
2+
import { useEffect } from 'react'
3+
import { useParams } from 'react-router'
4+
5+
import { useRepo } from 'services/repo'
6+
import { useOwner } from 'services/user'
27

38
import { AmplitudeEventTracker, initAmplitude } from './amplitude/amplitude'
49
import { StubbedEventTracker } from './stub'
5-
import { Event } from './types'
6-
7-
const AMPLITUDE_API_KEY = process.env.REACT_APP_AMPLITUDE_API_KEY
8-
9-
export abstract class EventTracker {
10-
// Identifies the user this session belongs to.
11-
identify({
12-
userOwnerId: _userOwnerId,
13-
username: _username,
14-
}: {
15-
userOwnerId: number
16-
username: string
17-
}): void {
18-
throw new Error(
19-
'EventTracker is abstract. Method identify must be implemented.'
20-
)
21-
}
10+
import { EventTracker } from './types'
2211

23-
track(_event: Event): void {
24-
throw new Error(
25-
'EventTracker is abstract. Method track must be implemented.'
26-
)
12+
// EventTracker singleton
13+
let EVENT_TRACKER: EventTracker = new StubbedEventTracker()
14+
15+
export function initEventTracker(): void {
16+
// Sets the global EventTracker singleton and calls necessary init functions
17+
try {
18+
initAmplitude()
19+
EVENT_TRACKER = new AmplitudeEventTracker()
20+
} catch (e) {
21+
if (process.env.REACT_APP_ENV === 'production') {
22+
// If in production, we need to know this has occured.
23+
captureException(e)
24+
}
2725
}
2826
}
2927

30-
export function initEventTracker(): void {
31-
// Calls any init functions for EventTracker implementations
32-
initAmplitude()
28+
// Returns the global EventTracker instance.
29+
export function eventTracker(): EventTracker {
30+
return EVENT_TRACKER
3331
}
3432

35-
// Returns an EventTracker. Provide any of provider, owner, repo that are
36-
// relevant to the event you're going to track. They will be attached to
37-
// any events you send with the returned EventTracker instance.
38-
export function eventTracker(
39-
provider?: Provider,
40-
owner?: string,
41-
repo?: string
42-
): EventTracker {
43-
if (!AMPLITUDE_API_KEY) {
44-
// If the API key is not defined, we'll just return a stubbed EventTracker.
45-
return new StubbedEventTracker()
46-
}
47-
return new AmplitudeEventTracker(provider, owner, repo)
33+
// Hook to keep the global EventTracker's context up-to-date.
34+
export function useEventContext() {
35+
const { provider, owner, repo } = useParams<{
36+
provider?: string
37+
owner?: string
38+
repo?: string
39+
}>()
40+
41+
const { data: ownerData } = useOwner({ username: owner })
42+
const { data: repoData } = useRepo({
43+
provider: provider || '',
44+
owner: owner || '',
45+
repo: repo || '',
46+
opts: { enabled: !!(provider && owner && repo) },
47+
})
48+
49+
useEffect(() => {
50+
EVENT_TRACKER.setContext({
51+
owner: ownerData?.ownerid
52+
? {
53+
id: ownerData?.ownerid,
54+
}
55+
: undefined,
56+
repo: repoData?.repository
57+
? {
58+
id: repoData?.repository?.repoid,
59+
isPrivate: repoData?.repository.private || undefined,
60+
}
61+
: undefined,
62+
})
63+
}, [ownerData, repoData])
4864
}

src/services/events/stub.ts

+3-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
1-
import { EventTracker } from './events'
2-
import { Event } from './types'
1+
import { Event, EventContext, EventTracker, Identity } from './types'
32

43
export class StubbedEventTracker implements EventTracker {
5-
identify({
6-
userOwnerId: _userOwnerId,
7-
username: _username,
8-
}: {
9-
userOwnerId: number
10-
username: string
11-
}): void {}
4+
identify(_identity: Identity): void {}
125
track(_event: Event): void {}
6+
setContext(_context: EventContext): void {}
137
}

src/services/events/types.ts

+60-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
// Add new events to the union type here.
2-
// Please keep the values of `type` very generic as we have a limited number of
3-
// them. Instead, add more detail in `properties` where possible.
4-
// Adding event types this way provides type safety for names and event
1+
import { Provider } from 'shared/api/helpers'
2+
3+
//
4+
// Add new events to the the Event union type below!
5+
//
6+
// Adding event types in this way provides type safety for names and event
57
// properties.
68
// E.g., every 'Button Clicked' event must have the buttonType property.
9+
//
10+
// Guidelines:
11+
// - Event names should:
12+
// - be of the form "[Noun] [Past-tense verb]",
13+
// - have each word capitalized,
14+
// - describe an action taken by the user.
15+
// - Keep the values of `type` very generic as we have a limited number of
16+
// them. Instead, add more detail in `properties` where possible.
17+
// - Try to keep event property names unique to the event type to avoid
18+
// accidental correlation of unrelated events.
19+
// - Never include names, only use ids. E.g., use repoid instead of repo name.
20+
//
721

822
export type Event =
923
| {
1024
type: 'Button Clicked'
1125
properties: {
12-
buttonType: 'Install Github App' | 'Configure Repo'
26+
buttonType: 'Install GitHub App' | 'Configure Repo'
1327
buttonLocation?: string
1428
}
1529
}
@@ -19,3 +33,44 @@ export type Event =
1933
pageName: 'OwnerPage'
2034
}
2135
}
36+
37+
export type Identity = {
38+
userOwnerId: number
39+
provider: Provider
40+
}
41+
42+
// Describes the context to be attached to events. We can extend this as needs
43+
// arise in the future.
44+
export type EventContext = {
45+
// owner the event is being performed ON, not BY.
46+
owner?: {
47+
id: number
48+
}
49+
repo?: {
50+
id: number
51+
isPrivate?: boolean
52+
}
53+
}
54+
55+
export abstract class EventTracker {
56+
// Identifies the user this session belongs to.
57+
identify(_identity: Identity): void {
58+
throw new Error(
59+
'EventTracker is abstract. Method identify must be implemented.'
60+
)
61+
}
62+
63+
// Tracks an event
64+
track(_event: Event): void {
65+
throw new Error(
66+
'EventTracker is abstract. Method track must be implemented.'
67+
)
68+
}
69+
70+
// Sets the current EventContext
71+
setContext(_context: EventContext): void {
72+
throw new Error(
73+
'EventTracker is abstract. Method setContext must be implemented.'
74+
)
75+
}
76+
}

src/services/repo/useRepo.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111

1212
const RepositorySchema = z.object({
1313
__typename: z.literal('Repository'),
14+
repoid: z.number(),
1415
private: z.boolean().nullable(),
1516
uploadToken: z.string().nullable(),
1617
defaultBranch: z.string().nullable(),
@@ -47,6 +48,7 @@ const query = `
4748
repository(name: $repo) {
4849
__typename
4950
... on Repository {
51+
repoid
5052
private
5153
uploadToken
5254
defaultBranch
@@ -73,6 +75,7 @@ type UseRepoArgs = {
7375
repo: string
7476
opts?: {
7577
refetchOnWindowFocus?: boolean
78+
enabled?: boolean
7679
}
7780
}
7881

src/services/user/useOwner.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function useOwner({
7373
})
7474
}
7575

76-
return res?.data?.owner
76+
return parsedRes?.data?.owner
7777
}),
7878
...opts,
7979
})

0 commit comments

Comments
 (0)