diff --git a/src/with-tracker/index.ts b/src/with-tracker/index.ts new file mode 100644 index 0000000..4a5673e --- /dev/null +++ b/src/with-tracker/index.ts @@ -0,0 +1,2 @@ +export { withTrackerContext } from './with-tracker' +export type { TrackModule, SubtractEventProperties } from './with-tracker-types' diff --git a/src/with-tracker/with-tracker-types.ts b/src/with-tracker/with-tracker-types.ts new file mode 100644 index 0000000..359a5bc --- /dev/null +++ b/src/with-tracker/with-tracker-types.ts @@ -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 = Omit< + OriginalProps, + keyof PropsToSubtract +> & + EventProperties + +/** + * Functions responsible for triggering track events. + * @public + */ +export type TrackModule = { + /** + * 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: RepeatedProps + ) => ( + remainingProps: SubtractEventProperties< + CustomEventProperties, + RepeatedProps + > + ) => void +} diff --git a/src/with-tracker/with-tracker.test.ts b/src/with-tracker/with-tracker.test.ts new file mode 100644 index 0000000..068e18d --- /dev/null +++ b/src/with-tracker/with-tracker.test.ts @@ -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) +}) diff --git a/src/with-tracker/with-tracker.ts b/src/with-tracker/with-tracker.ts new file mode 100644 index 0000000..369f18b --- /dev/null +++ b/src/with-tracker/with-tracker.ts @@ -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({ + context, +}: TrackerContext): TrackModule { + function trackEvent(eventProps: Properties) { + dataLayer.assertIsAvailable() + dataLayer.addEvent({ ...context.value, ...eventProps }) + } + + function setRepeatedProps>(defaultProps: T) { + return (remainingProps: SubtractEventProperties) => { + trackEvent({ + ...context.value, + ...defaultProps, + ...remainingProps, + } as unknown as Properties) + } + } + + return { trackEvent, setRepeatedProps } +}