Skip to content
Merged
2 changes: 2 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Router } from 'react-router-dom'
import { CompatRouter } from 'react-router-dom-v5-compat'

import ErrorBoundary from 'layouts/shared/ErrorBoundary'
import { initEventTracker } from 'services/events/events'
import { withFeatureFlagProvider } from 'shared/featureFlags'

import App from './App'
Expand All @@ -38,6 +39,7 @@ const history = createBrowserHistory()

const TOO_MANY_REQUESTS_ERROR_CODE = 429

initEventTracker()
setupSentry({ history })

const queryClient = new QueryClient({
Expand Down
24 changes: 24 additions & 0 deletions src/layouts/BaseLayout/BaseLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,22 @@ const mockNavigatorData = {
},
}

const mockOwnerContext = {
owner: {
ownerid: 123,
},
}

const mockRepoContext = {
owner: {
repository: {
__typename: 'Repository',
repoid: 321,
private: false,
},
},
}

const server = setupServer()
const queryClient = new QueryClient({
defaultOptions: {
Expand Down Expand Up @@ -288,6 +304,14 @@ describe('BaseLayout', () => {
graphql.query('NavigatorData', () => {
return HttpResponse.json({ data: mockNavigatorData })
}),
graphql.query('OwnerContext', () => {
return HttpResponse.json({ data: mockOwnerContext })
}),
graphql.query('RepoContext', () => {
return HttpResponse.json({
data: mockRepoContext,
})
}),
http.get('/internal/users/current', () => {
return HttpResponse.json({})
})
Expand Down
2 changes: 2 additions & 0 deletions src/layouts/BaseLayout/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import NetworkErrorBoundary from 'layouts/shared/NetworkErrorBoundary'
import SilentNetworkErrorWrapper from 'layouts/shared/SilentNetworkErrorWrapper'
import ToastNotifications from 'layouts/ToastNotifications'
import { RepoBreadcrumbProvider } from 'pages/RepoPage/context'
import { useEventContext } from 'services/events/hooks'
import { useImpersonate } from 'services/impersonate'
import { useTracking } from 'services/tracking'
import GlobalBanners from 'shared/GlobalBanners'
Expand Down Expand Up @@ -77,6 +78,7 @@ interface URLParams {
function BaseLayout({ children }: React.PropsWithChildren) {
const { provider, owner, repo } = useParams<URLParams>()
useTracking()
useEventContext()
const { isImpersonating } = useImpersonate()
const {
isFullExperience,
Expand Down
35 changes: 35 additions & 0 deletions src/layouts/Header/components/UserDropdown/UserDropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { type Mock } from 'vitest'

import config from 'config'

import { eventTracker } from 'services/events/events'
import { useImage } from 'services/image'
import { Plans } from 'shared/utils/billing'

Expand Down Expand Up @@ -60,6 +61,7 @@ const mockUser = {
vi.mock('services/image')
vi.mock('config')
vi.mock('js-cookie')
vi.mock('services/events/events')

const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
Expand Down Expand Up @@ -214,6 +216,39 @@ describe('UserDropdown', () => {
'https://github.com/apps/codecov/installations/new'
)
})

describe('when app access link is clicked', () => {
it('tracks a Button Clicked event', async () => {
const { user } = setup()
render(<UserDropdown />, {
wrapper: wrapper(),
})

expect(
screen.queryByText('Install Codecov app')
).not.toBeInTheDocument()

const openSelect = await screen.findByTestId('user-dropdown-trigger')
await user.click(openSelect)

const link = screen.getByText('Install Codecov app')
expect(link).toBeVisible()
expect(link).toHaveAttribute(
'href',
'https://github.com/apps/codecov/installations/new'
)

await user.click(link)

expect(eventTracker().track).toHaveBeenCalledWith({
type: 'Button Clicked',
properties: {
buttonName: 'Install GitHub App',
buttonLocation: 'User dropdown',
},
})
})
})
})
})
describe('when not on GitHub', () => {
Expand Down
9 changes: 9 additions & 0 deletions src/layouts/Header/components/UserDropdown/UserDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useHistory, useParams } from 'react-router-dom'

import config from 'config'

import { eventTracker } from 'services/events/events'
import { useUser } from 'services/user'
import { Provider } from 'shared/api/helpers'
import { providerToName } from 'shared/utils/provider'
Expand Down Expand Up @@ -42,6 +43,14 @@ function UserDropdown() {
{
to: { pageName: 'codecovAppInstallation' },
children: 'Install Codecov app',
onClick: () =>
eventTracker().track({
type: 'Button Clicked',
properties: {
buttonName: 'Install GitHub App',
buttonLocation: 'User dropdown',
},
}),
} as DropdownItem,
]
: []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useParams } from 'react-router-dom'

import { eventTracker } from 'services/events/events'
import { providerToName } from 'shared/utils/provider'
import A from 'ui/A'
import Banner from 'ui/Banner'
Expand All @@ -21,6 +22,15 @@ const GithubConfigBanner = () => {
<A
data-testid="codecovGithubApp-link"
to={{ pageName: 'codecovGithubAppSelectTarget' }}
onClick={() =>
eventTracker().track({
type: 'Button Clicked',
properties: {
buttonName: 'Install GitHub App',
buttonLocation: 'Configure GitHub app banner',
},
})
}
>
Codecov&apos;s GitHub app
</A>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { render, screen } from '@testing-library/react'
import { act, render, screen } from '@testing-library/react'
import { MemoryRouter, Route, Switch } from 'react-router-dom'

import { eventTracker } from 'services/events/events'

import GithubConfigBanner from './GithubConfigBanner'

vi.mock('services/events/events')

const wrapper =
({ provider = 'gh' }) =>
({ children }) => {
Expand Down Expand Up @@ -34,6 +38,27 @@ describe('GithubConfigBanner', () => {
)
expect(body).toBeInTheDocument()
})

describe('and button is clicked', () => {
it('tracks a Button Clicked event', async () => {
render(<GithubConfigBanner />, {
wrapper: wrapper({ provider: 'gh' }),
})

const title = screen.getByText(/Codecov's GitHub app/)
expect(title).toBeInTheDocument()

act(() => title.click())

expect(eventTracker().track).toHaveBeenCalledWith({
type: 'Button Clicked',
properties: {
buttonName: 'Install GitHub App',
buttonLocation: 'Configure GitHub app banner',
},
})
})
})
})

describe('when rendered with other providers', () => {
Expand Down
18 changes: 18 additions & 0 deletions src/services/events/__mocks__/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EventTracker } from '../types'

//
// Use this mock by
// vi.mock('services/events/events')
// and
// expect(eventTracker().track).toHaveBeenCalledWith()
//

const MOCK_EVENT_TRACKER: EventTracker = {
identify: vi.fn(),
track: vi.fn(),
setContext: vi.fn(),
}

export function eventTracker() {
return MOCK_EVENT_TRACKER
}
159 changes: 159 additions & 0 deletions src/services/events/amplitude/amplitude.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import config from 'config'

import { AmplitudeEventTracker, initAmplitude } from './amplitude'

const mockIdentifySet = vi.hoisted(() => vi.fn())
const mockIdentifyConstructor = vi.hoisted(() => vi.fn())
const mockAmplitude = vi.hoisted(() => {
class MockIdentify {
constructor() {
mockIdentifyConstructor()
}
set(key: string, value: any) {
mockIdentifySet(key, value)
}
}
return {
init: vi.fn(),
track: vi.fn(),
identify: vi.fn(),
setUserId: vi.fn(),
Identify: MockIdentify,
}
})
vi.mock('@amplitude/analytics-browser', () => mockAmplitude)

afterEach(() => {
vi.resetAllMocks()
})

describe('when initAmplitude is called', () => {
describe('and AMPLITUDE_API_KEY is not defined', () => {
it('throws an error', () => {
config.AMPLITUDE_API_KEY = undefined
try {
initAmplitude()
} catch (e) {
expect(e).toEqual(
new Error(
'AMPLITUDE_API_KEY is not defined. Amplitude events will not be tracked.'
)
)
}
})
})

describe('and AMPLITUDE_API_KEY is defined', () => {
it('calls amplitude.init() with api key', () => {
config.AMPLITUDE_API_KEY = 'asdf1234'
initAmplitude()
expect(mockAmplitude.init).toHaveBeenCalled()
})
})
})

describe('AmplitudeEventTracker', () => {
describe('identify', () => {
describe('when identify is called', () => {
it('calls appropriate sdk functions', () => {
const tracker = new AmplitudeEventTracker()
tracker.identify({
provider: 'gh',
userOwnerId: 123,
})
expect(mockAmplitude.setUserId).toHaveBeenCalledWith('123')
expect(mockIdentifyConstructor).toHaveBeenCalled()
expect(mockIdentifySet).toHaveBeenCalledWith('provider', 'github')
expect(mockAmplitude.identify).toHaveBeenCalled()
expect(tracker.identity).toEqual({
userOwnerId: 123,
provider: 'gh',
})
})
})

describe('when identify is called multiple times with the same identity', () => {
it('does not make any amplitude calls', () => {
const tracker = new AmplitudeEventTracker()
tracker.identify({
provider: 'gh',
userOwnerId: 123,
})

vi.resetAllMocks()

tracker.identify({
provider: 'gh',
userOwnerId: 123,
})

expect(mockAmplitude.setUserId).not.toHaveBeenCalled()

expect(tracker.identity).toEqual({
userOwnerId: 123,
provider: 'gh',
})
})
})
})

describe('track', () => {
describe('when track is called with no context', () => {
it('does not populate any context', () => {
const tracker = new AmplitudeEventTracker()
tracker.track({
type: 'Button Clicked',
properties: {
buttonName: 'Configure Repo',
},
})

expect(mockAmplitude.track).toHaveBeenCalledWith({
event_type: 'Button Clicked',
event_properties: {
buttonName: 'Configure Repo',
},
})
})
})

describe('when track is called with context', () => {
it('populates context as event properties', () => {
const tracker = new AmplitudeEventTracker()
tracker.setContext({
owner: {
id: 123,
},
repo: {
id: 321,
isPrivate: false,
},
})

tracker.track({
type: 'Button Clicked',
properties: {
buttonName: 'Configure Repo',
},
})

expect(mockAmplitude.track).toHaveBeenCalledWith({
event_type: 'Button Clicked',
event_properties: {
buttonName: 'Configure Repo',
owner: {
id: 123,
},
repo: {
id: 321,
isPrivate: false,
},
},
groups: {
org: 123,
},
})
})
})
})
})
Loading
Loading