Skip to content

Commit c8902a9

Browse files
Add tests for EventTracker code
1 parent ddeab9a commit c8902a9

File tree

8 files changed

+625
-54
lines changed

8 files changed

+625
-54
lines changed

src/layouts/BaseLayout/BaseLayout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +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'
13+
import { useEventContext } from 'services/events/hooks'
1414
import { useImpersonate } from 'services/impersonate'
1515
import { useTracking } from 'services/tracking'
1616
import GlobalBanners from 'shared/GlobalBanners'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import config from 'config'
2+
3+
import { AmplitudeEventTracker, initAmplitude } from './amplitude'
4+
5+
const mockIdentifySet = vi.hoisted(() => vi.fn())
6+
const mockIdentifyConstructor = vi.hoisted(() => vi.fn())
7+
const mockAmplitude = vi.hoisted(() => {
8+
class MockIdentify {
9+
constructor() {
10+
mockIdentifyConstructor()
11+
}
12+
set(key: string, value: any) {
13+
mockIdentifySet(key, value)
14+
}
15+
}
16+
return {
17+
init: vi.fn(),
18+
track: vi.fn(),
19+
identify: vi.fn(),
20+
setUserId: vi.fn(),
21+
Identify: MockIdentify,
22+
}
23+
})
24+
vi.mock('@amplitude/analytics-browser', () => mockAmplitude)
25+
26+
afterEach(() => {
27+
vi.resetAllMocks()
28+
})
29+
30+
describe('when initAmplitude is called', () => {
31+
describe('and AMPLITUDE_API_KEY is not defined', () => {
32+
it('throws an error', () => {
33+
config.AMPLITUDE_API_KEY = undefined
34+
try {
35+
initAmplitude()
36+
} catch (e) {
37+
expect(e).toEqual(
38+
new Error(
39+
'AMPLITUDE_API_KEY is not defined. Amplitude events will not be tracked.'
40+
)
41+
)
42+
}
43+
})
44+
})
45+
46+
describe('and AMPLITUDE_API_KEY is defined', () => {
47+
it('calls amplitude.init() with api key', () => {
48+
config.AMPLITUDE_API_KEY = 'asdf1234'
49+
initAmplitude()
50+
expect(mockAmplitude.init).toHaveBeenCalled()
51+
})
52+
})
53+
})
54+
55+
describe('AmplitudeEventTracker', () => {
56+
describe('identify', () => {
57+
describe('when identify is called', () => {
58+
it('calls appropriate sdk functions', () => {
59+
const tracker = new AmplitudeEventTracker()
60+
tracker.identify({
61+
provider: 'gh',
62+
userOwnerId: 123,
63+
})
64+
expect(mockAmplitude.setUserId).toHaveBeenCalledWith('123')
65+
expect(mockIdentifyConstructor).toHaveBeenCalled()
66+
expect(mockIdentifySet).toHaveBeenCalledWith('provider', 'github')
67+
expect(mockAmplitude.identify).toHaveBeenCalled()
68+
expect(tracker.identity).toEqual({
69+
userOwnerId: 123,
70+
provider: 'gh',
71+
})
72+
})
73+
})
74+
75+
describe('when identify is called multiple times with the same identity', () => {
76+
it('does not make any amplitude calls', () => {
77+
const tracker = new AmplitudeEventTracker()
78+
tracker.identify({
79+
provider: 'gh',
80+
userOwnerId: 123,
81+
})
82+
83+
vi.resetAllMocks()
84+
85+
tracker.identify({
86+
provider: 'gh',
87+
userOwnerId: 123,
88+
})
89+
90+
expect(mockAmplitude.setUserId).not.toHaveBeenCalled()
91+
92+
expect(tracker.identity).toEqual({
93+
userOwnerId: 123,
94+
provider: 'gh',
95+
})
96+
})
97+
})
98+
})
99+
100+
describe('track', () => {
101+
describe('when track is called with no context', () => {
102+
it('does not populate any context', () => {
103+
const tracker = new AmplitudeEventTracker()
104+
tracker.track({
105+
type: 'Button Clicked',
106+
properties: {
107+
buttonType: 'Configure Repo',
108+
},
109+
})
110+
111+
expect(mockAmplitude.track).toHaveBeenCalledWith({
112+
event_type: 'Button Clicked',
113+
event_properties: {
114+
buttonType: 'Configure Repo',
115+
},
116+
})
117+
})
118+
})
119+
120+
describe('when track is called with context', () => {
121+
it('populates context as event properties', () => {
122+
const tracker = new AmplitudeEventTracker()
123+
tracker.setContext({
124+
owner: {
125+
id: 123,
126+
},
127+
repo: {
128+
id: 321,
129+
isPrivate: false,
130+
},
131+
})
132+
133+
tracker.track({
134+
type: 'Button Clicked',
135+
properties: {
136+
buttonType: 'Configure Repo',
137+
},
138+
})
139+
140+
expect(mockAmplitude.track).toHaveBeenCalledWith({
141+
event_type: 'Button Clicked',
142+
event_properties: {
143+
buttonType: 'Configure Repo',
144+
owner: {
145+
id: 123,
146+
},
147+
repo: {
148+
id: 321,
149+
isPrivate: false,
150+
},
151+
},
152+
groups: {
153+
org: 123,
154+
},
155+
})
156+
})
157+
})
158+
})
159+
})

src/services/events/amplitude/amplitude.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@ import { providerToInternalProvider } from 'shared/utils/provider'
66

77
import { Event, EventContext, EventTracker, Identity } from '../types'
88

9-
const AMPLITUDE_API_KEY = config.AMPLITUDE_API_KEY
10-
119
export function initAmplitude() {
12-
if (!AMPLITUDE_API_KEY) {
10+
const apiKey = config.AMPLITUDE_API_KEY
11+
if (!apiKey) {
1312
throw new Error(
1413
'AMPLITUDE_API_KEY is not defined. Amplitude events will not be tracked.'
1514
)
1615
}
17-
amplitude.init(AMPLITUDE_API_KEY, {
16+
amplitude.init(apiKey, {
1817
// Disable all autocapture - may change this in the future
1918
autocapture: false,
2019
minIdLength: 1, // Necessary to accommodate our owner ids

src/services/events/events.test.tsx

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import config from 'config'
2+
3+
import { eventTracker, initEventTracker, StubbedEventTracker } from './events'
4+
5+
vi.mock('config')
6+
const mockCaptureException = vi.hoisted(() => vi.fn())
7+
vi.mock('@sentry/react', () => ({
8+
captureException: mockCaptureException,
9+
}))
10+
const mockInitAmplitude = vi.hoisted(() => vi.fn())
11+
vi.mock('./amplitude/amplitude', () => ({
12+
initAmplitude: mockInitAmplitude,
13+
AmplitudeEventTracker: vi.fn(),
14+
}))
15+
16+
afterEach(() => {
17+
vi.resetAllMocks()
18+
})
19+
20+
describe('EventTracker', () => {
21+
describe('StubbedEventTracker', () => {
22+
it('should accept calls without error', () => {
23+
const stubbedEventTracker = eventTracker()
24+
expect(stubbedEventTracker).toBeInstanceOf(StubbedEventTracker)
25+
26+
stubbedEventTracker.setContext({})
27+
stubbedEventTracker.identify({
28+
userOwnerId: 1,
29+
provider: 'gh',
30+
})
31+
stubbedEventTracker.track({
32+
type: 'Button Clicked',
33+
properties: {
34+
buttonType: 'Install GitHub App',
35+
buttonLocation: 'test',
36+
},
37+
})
38+
})
39+
})
40+
41+
describe('when initEventTracker is called', () => {
42+
describe('and initAmplitude() throws an error', () => {
43+
beforeEach(() => {
44+
mockInitAmplitude.mockImplementationOnce(() => {
45+
throw new Error('oopsie')
46+
})
47+
})
48+
49+
describe('and ENV is production', () => {
50+
beforeEach(() => {
51+
config.ENV = 'production'
52+
})
53+
54+
it('calls sentry.captureException()', () => {
55+
initEventTracker()
56+
expect(mockCaptureException).toHaveBeenCalledOnce()
57+
})
58+
59+
it('does not set EVENT_TRACKER to Amplitude instance', () => {
60+
const initialTracker = eventTracker()
61+
initEventTracker()
62+
const afterTracker = eventTracker()
63+
expect(afterTracker).toBe(initialTracker)
64+
})
65+
})
66+
67+
describe('and ENV is not production', () => {
68+
beforeEach(() => {
69+
config.ENV = 'development'
70+
})
71+
72+
it('does not call sentry.captureException()', () => {
73+
initEventTracker()
74+
expect(mockCaptureException).not.toHaveBeenCalled()
75+
})
76+
77+
it('does not set EVENT_TRACKER to Amplitude instance', () => {
78+
const initialTracker = eventTracker()
79+
initEventTracker()
80+
const afterTracker = eventTracker()
81+
expect(afterTracker).toBe(initialTracker)
82+
})
83+
})
84+
})
85+
86+
describe('and initAmplitude() does not throw', () => {
87+
it('sets EVENT_TRACKER to an Amplitude instance', () => {
88+
const initialTracker = eventTracker()
89+
initEventTracker()
90+
const afterTracker = eventTracker()
91+
expect(afterTracker).not.toBe(initialTracker)
92+
})
93+
})
94+
})
95+
96+
describe('when eventTracker is called', () => {
97+
it('should always return the same EventTracker instance', () => {
98+
initEventTracker()
99+
const eventTrackerA = eventTracker()
100+
const eventTrackerB = eventTracker()
101+
expect(eventTrackerA).toBe(eventTrackerB)
102+
})
103+
})
104+
})

src/services/events/events.ts

+3-46
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import { captureException } from '@sentry/react'
2-
import { useQuery as useQueryV5 } from '@tanstack/react-queryV5'
3-
import { useRef } from 'react'
4-
import { useParams } from 'react-router'
52

6-
import { Provider } from 'shared/api/helpers'
3+
import config from 'config'
74

85
import { AmplitudeEventTracker, initAmplitude } from './amplitude/amplitude'
9-
import { OwnerContextQueryOpts, RepoContextQueryOpts } from './hooks'
106
import { Event, EventContext, EventTracker, Identity } from './types'
117

12-
class StubbedEventTracker implements EventTracker {
8+
export class StubbedEventTracker implements EventTracker {
139
identify(_identity: Identity): void {}
1410
track(_event: Event): void {}
1511
setContext(_context: EventContext): void {}
@@ -24,7 +20,7 @@ export function initEventTracker(): void {
2420
initAmplitude()
2521
EVENT_TRACKER = new AmplitudeEventTracker()
2622
} catch (e) {
27-
if (process.env.REACT_APP_ENV === 'production') {
23+
if (config.ENV === 'production') {
2824
// If in production, we need to know this has occurred.
2925
captureException(e)
3026
}
@@ -35,42 +31,3 @@ export function initEventTracker(): void {
3531
export function eventTracker(): EventTracker {
3632
return EVENT_TRACKER
3733
}
38-
39-
// Hook to keep the global EventTracker's context up-to-date.
40-
export function useEventContext() {
41-
const { provider, owner, repo } = useParams<{
42-
provider: Provider
43-
owner?: string
44-
repo?: string
45-
}>()
46-
const context = useRef<EventContext>({})
47-
48-
const { data: ownerData } = useQueryV5(
49-
OwnerContextQueryOpts({ provider, owner })
50-
)
51-
const { data: repoData } = useQueryV5(
52-
RepoContextQueryOpts({ provider, owner, repo })
53-
)
54-
55-
if (
56-
ownerData?.ownerid !== context.current.owner?.id ||
57-
repoData?.repoid !== context.current.repo?.id
58-
) {
59-
// only update if this is a new owner or repo
60-
const newContext: EventContext = {
61-
owner: ownerData?.ownerid
62-
? {
63-
id: ownerData?.ownerid,
64-
}
65-
: undefined,
66-
repo: repoData?.repoid
67-
? {
68-
id: repoData.repoid,
69-
isPrivate: repoData.private === null ? undefined : repoData.private,
70-
}
71-
: undefined,
72-
}
73-
EVENT_TRACKER.setContext(newContext)
74-
context.current = newContext
75-
}
76-
}

0 commit comments

Comments
 (0)