Skip to content

Commit

Permalink
feat: add with-tracker
Browse files Browse the repository at this point in the history
  • Loading branch information
emkis committed Apr 5, 2022
1 parent 17a6d9f commit 0e178c3
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/with-tracker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { withTrackerContext } from './with-tracker'
export type { TrackModule, SubtractEventProperties } from './with-tracker-types'
60 changes: 60 additions & 0 deletions src/with-tracker/with-tracker-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { EventProperties } from '../data-layer'

/**
* Will subtract `PropsToSubtract` type from `OriginalProps` type. And ensure
* that those properties are compatible with a track event type.
* @public
*/
export type SubtractEventProperties<OriginalProps, PropsToSubtract> = Omit<
OriginalProps,
keyof PropsToSubtract
> &
EventProperties

/**
* Functions responsible for triggering track events.
* @public
*/
export type TrackModule<CustomEventProperties extends EventProperties> = {
/**
* Triggers a track event.
*
* Use this function if you don't need to trigger multiple events
* with repeated properties.
*
* @param eventProps - Properties of your event.
* @example
* trackEvent({
* action: 'new account',
* category: 'pro plan',
* foo: 'bar',
* userId: '...',
* })
*/
trackEvent: (eventProps: CustomEventProperties & EventProperties) => void

/**
* A partial function that returns the `trackEvent` function with the
* repeated properties injected as props.
*
* Use this function when you need to track multiple events with
* the same properties.
*
* @param repeatedProps - Properties that you need in multiple events.
* @example
* const trackNewAccount = setRepeatedProps({
* action: 'new account'
* })
*
* trackNewAccount({ category: 'pro plan', lorem: 'ipsum' })
* trackNewAccount({ category: 'business plan', sit: 'dolor' })
*/
setRepeatedProps: <RepeatedProps extends Partial<CustomEventProperties>>(
repeatedProps: RepeatedProps
) => (
remainingProps: SubtractEventProperties<
CustomEventProperties,
RepeatedProps
>
) => void
}
106 changes: 106 additions & 0 deletions src/with-tracker/with-tracker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { createTrackerContext } from '../tracker-context'
import { withTrackerContext } from './with-tracker'
import { WarningError } from '../error'
import type { EventProperties } from '../data-layer'

function makeTracker(props?: EventProperties) {
const context = createTrackerContext(props)
return withTrackerContext(context)
}

beforeEach(() => {
window.dataLayer = []
})

it('should throw error if window.dataLayer is not available', () => {
// @ts-expect-error deleting for purposes of this test
delete window.dataLayer

const tracker = makeTracker()
const trackEmptyEvent = () => tracker.trackEvent({})

expect(trackEmptyEvent).toThrowError(WarningError)
expect(trackEmptyEvent).toThrowError('window.dataLayer is not defined')
})

it('should throw error if window.dataLayer is not an array', () => {
// @ts-expect-error changing for purposes of this test
window.dataLayer = {}
const tracker = makeTracker()
const trackEmptyEvent = () => tracker.trackEvent({})

expect(trackEmptyEvent).toThrowError(WarningError)
expect(trackEmptyEvent).toThrowError('window.dataLayer is not an array')
})

it('should contain all composed properties in the events', () => {
const contextProps = { foo: 'bar', baz: 'quz' }
const { trackEvent } = makeTracker(contextProps)

const eventPropertiesA = {
action: 'new subscription',
plan: 'business',
lorem: 'ipsum',
}
const eventPropertiesB = {
action: 'new subscription',
plan: 'pro',
random: 'something',
}

trackEvent(eventPropertiesA)
trackEvent(eventPropertiesB)
trackEvent({})

const [eventPayloadA, eventPayloadB, eventPayloadC] = window.dataLayer

expect(window.dataLayer).toHaveLength(3)
expect(eventPayloadA).toStrictEqual({ ...contextProps, ...eventPropertiesA })
expect(eventPayloadB).toStrictEqual({ ...contextProps, ...eventPropertiesB })
expect(eventPayloadC).toStrictEqual(contextProps)
})

it('should be able to overwrite context properties in track event', () => {
const contextProps = { static: 'prop', from: 'context' }
const { trackEvent } = makeTracker(contextProps)

trackEvent({ foo: 'bar', from: 'trackEvent' })
const [eventPayload] = window.dataLayer

expect(window.dataLayer).toHaveLength(1)
expect(eventPayload).toStrictEqual({
static: 'prop',
from: 'trackEvent',
foo: 'bar',
})
})

it('should return a partial function when setRepeatedProps is called', () => {
const { setRepeatedProps } = makeTracker()
const result = setRepeatedProps({ greeting: 'eai' })
expect(typeof result).toBe('function')
})

it('should inject repeated props in the events', () => {
const contextProps = { from: 'context' }
const tracker = makeTracker(contextProps)
const trackCoolEvent = tracker.setRepeatedProps({ isRepeatedProp: 'yes' })

trackCoolEvent({ isFromPartial: 'oh yes' })
trackCoolEvent({ soCool: 'true' })
tracker.trackEvent({ isAlone: 'true' })
const [coolPayloadA, coolPayloadB, alonePayload] = window.dataLayer

expect(coolPayloadA).toStrictEqual({
from: 'context',
isRepeatedProp: 'yes',
isFromPartial: 'oh yes',
})
expect(coolPayloadB).toStrictEqual({
from: 'context',
isRepeatedProp: 'yes',
soCool: 'true',
})
expect(alonePayload).toStrictEqual({ from: 'context', isAlone: 'true' })
expect(window.dataLayer).toHaveLength(3)
})
47 changes: 47 additions & 0 deletions src/with-tracker/with-tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { dataLayer, EventProperties } from '../data-layer'
import type { TrackerContext } from '../tracker-context'

import type { TrackModule, SubtractEventProperties } from './with-tracker-types'

/**
* Accepts a context object returned from `createTrackerContext` function,
* and returns the functions responsible for triggering the track events.
*
* @param trackerContext - The tracker context object.
*
* @public
* @example
* const appTrackerContext = createTrackerContext({
* someProperty: 'i need to have in every single event'
* appName: 'awesome-app',
* })
*
* // using the tracker context to track events
* const Tracker = withTrackerContext(appTrackerContext)
*
* Tracker.trackEvent({ foo: 'bar', baz: 'qux', })
*
* const trackMenuEvent = Tracker.setRepeatedProps({ category: 'menu' })
* trackMenuEvent({ action: 'log in' })
* trackMenuEvent({ action: 'log out' })
*/
export function withTrackerContext<Properties extends EventProperties>({
context,
}: TrackerContext): TrackModule<Properties> {
function trackEvent(eventProps: Properties) {
dataLayer.assertIsAvailable()
dataLayer.addEvent({ ...context.value, ...eventProps })
}

function setRepeatedProps<T extends Partial<Properties>>(defaultProps: T) {
return (remainingProps: SubtractEventProperties<Properties, T>) => {
trackEvent({
...context.value,
...defaultProps,
...remainingProps,
} as unknown as Properties)
}
}

return { trackEvent, setRepeatedProps }
}

0 comments on commit 0e178c3

Please sign in to comment.