-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implement EventTracker abstraction and Amplitude implementation #3650
base: main
Are you sure you want to change the base?
Changes from all commits
95dccda
9f549ef
8f189a6
6e309e5
3069c0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import * as amplitude from '@amplitude/analytics-browser' | ||
|
||
import { providerToInternalProvider } from 'shared/utils/provider' | ||
|
||
import { Event, EventContext, EventTracker, Identity } from '../types' | ||
|
||
const AMPLITUDE_API_KEY = process.env.REACT_APP_AMPLITUDE_API_KEY | ||
|
||
export function initAmplitude() { | ||
if (!AMPLITUDE_API_KEY) { | ||
throw new Error( | ||
'AMPLITUDE_API_KEY is not defined. Amplitude events will not be tracked.' | ||
) | ||
} | ||
amplitude.init(AMPLITUDE_API_KEY, { | ||
// Disable all autocapture - may change this in the future | ||
autocapture: false, | ||
minIdLength: 1, // Necessary to accommodate our owner ids | ||
}) | ||
} | ||
|
||
export class AmplitudeEventTracker implements EventTracker { | ||
context: EventContext = {} | ||
identity?: Identity | ||
|
||
identify(identity: Identity) { | ||
if (JSON.stringify(this.identity) === JSON.stringify(identity)) { | ||
// Don't identify this user again this session. | ||
return | ||
} | ||
|
||
amplitude.setUserId(identity.userOwnerId.toString()) | ||
const identifyEvent = new amplitude.Identify() | ||
identifyEvent.set('provider', providerToInternalProvider(identity.provider)) | ||
amplitude.identify(identifyEvent) | ||
|
||
this.identity = identity | ||
} | ||
|
||
track(event: Event) { | ||
amplitude.track({ | ||
// eslint-disable-next-line camelcase | ||
event_type: event.type, | ||
// eslint-disable-next-line camelcase | ||
event_properties: { | ||
...event.properties, | ||
...this.context, | ||
}, | ||
// This attaches the event to the owner's user group (org) as well | ||
groups: this.context.owner?.id | ||
? { | ||
org: this.context.owner.id, | ||
} | ||
: undefined, | ||
}) | ||
} | ||
|
||
setContext(context: EventContext) { | ||
this.context = context | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { captureException } from '@sentry/react' | ||
import { useEffect } from 'react' | ||
import { useParams } from 'react-router' | ||
|
||
import { useRepo } from 'services/repo' | ||
import { useOwner } from 'services/user' | ||
|
||
import { AmplitudeEventTracker, initAmplitude } from './amplitude/amplitude' | ||
import { StubbedEventTracker } from './stub' | ||
import { EventTracker } from './types' | ||
|
||
// EventTracker singleton | ||
let EVENT_TRACKER: EventTracker = new StubbedEventTracker() | ||
|
||
export function initEventTracker(): void { | ||
// Sets the global EventTracker singleton and calls necessary init functions | ||
try { | ||
initAmplitude() | ||
EVENT_TRACKER = new AmplitudeEventTracker() | ||
} catch (e) { | ||
if (process.env.REACT_APP_ENV === 'production') { | ||
// If in production, we need to know this has occured. | ||
captureException(e) | ||
} | ||
} | ||
} | ||
|
||
// Returns the global EventTracker instance. | ||
export function eventTracker(): EventTracker { | ||
return EVENT_TRACKER | ||
} | ||
|
||
// Hook to keep the global EventTracker's context up-to-date. | ||
export function useEventContext() { | ||
const { provider, owner, repo } = useParams<{ | ||
provider?: string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This can be typed to "Provider" |
||
owner?: string | ||
repo?: string | ||
}>() | ||
|
||
const { data: ownerData } = useOwner({ username: owner }) | ||
const { data: repoData } = useRepo({ | ||
provider: provider || '', | ||
owner: owner || '', | ||
repo: repo || '', | ||
opts: { enabled: !!(provider && owner && repo) }, | ||
}) | ||
|
||
useEffect(() => { | ||
EVENT_TRACKER.setContext({ | ||
owner: ownerData?.ownerid | ||
? { | ||
id: ownerData?.ownerid, | ||
} | ||
: undefined, | ||
repo: repoData?.repository | ||
? { | ||
id: repoData?.repository?.repoid, | ||
isPrivate: repoData?.repository.private || undefined, | ||
} | ||
: undefined, | ||
}) | ||
}, [ownerData, repoData]) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Event, EventContext, EventTracker, Identity } from './types' | ||
|
||
export class StubbedEventTracker implements EventTracker { | ||
identify(_identity: Identity): void {} | ||
track(_event: Event): void {} | ||
setContext(_context: EventContext): void {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { Provider } from 'shared/api/helpers' | ||
|
||
// | ||
// Add new events to the the Event union type below! | ||
// | ||
// Adding event types in this way provides type safety for names and event | ||
// properties. | ||
// E.g., every 'Button Clicked' event must have the buttonType property. | ||
// | ||
// Guidelines: | ||
// - Event names should: | ||
// - be of the form "[Noun] [Past-tense verb]", | ||
// - have each word capitalized, | ||
// - describe an action taken by the user. | ||
// - Keep the values of `type` very generic as we have a limited number of | ||
// them. Instead, add more detail in `properties` where possible. | ||
// - Try to keep event property names unique to the event type to avoid | ||
// accidental correlation of unrelated events. | ||
// - Never include names, only use ids. E.g., use repoid instead of repo name. | ||
// | ||
|
||
export type Event = | ||
| { | ||
type: 'Button Clicked' | ||
properties: { | ||
buttonType: 'Install GitHub App' | 'Configure Repo' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wondering if we should do a "Type of" and have these buttonTypes (and other event properties that will have a range of set values) live somewhere else for easier parsing and clarity |
||
buttonLocation?: string | ||
} | ||
} | ||
| { | ||
type: 'Page Viewed' | ||
properties: { | ||
pageName: 'Owner Page' | ||
} | ||
} | ||
|
||
export type Identity = { | ||
userOwnerId: number | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this named as such just cuz owners can be orgs and users? |
||
provider: Provider | ||
} | ||
|
||
// Describes the context to be attached to events. We can extend this as needs | ||
// arise in the future. | ||
export type EventContext = { | ||
// owner the event is being performed ON, not BY. | ||
owner?: { | ||
id: number | ||
} | ||
repo?: { | ||
id: number | ||
isPrivate?: boolean | ||
} | ||
} | ||
|
||
export abstract class EventTracker { | ||
// Identifies the user this session belongs to. | ||
identify(_identity: Identity): void { | ||
throw new Error( | ||
'EventTracker is abstract. Method identify must be implemented.' | ||
) | ||
} | ||
|
||
// Tracks an event | ||
track(_event: Event): void { | ||
throw new Error( | ||
'EventTracker is abstract. Method track must be implemented.' | ||
) | ||
} | ||
|
||
// Sets the current EventContext | ||
setContext(_context: EventContext): void { | ||
throw new Error( | ||
'EventTracker is abstract. Method setContext must be implemented.' | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: occurred