diff --git a/src/index.ts b/src/index.ts index 43287ca..81ffa8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -interface Fathom { +export interface Fathom { blockTrackingForMe: () => void; enableTrackingForMe: () => void; trackPageview: (opts?: PageViewOptions) => void; @@ -7,17 +7,25 @@ interface Fathom { setSite: (id: string) => void; } +/** + * @see https://usefathom.com/docs/script/script-advanced#api + */ export type PageViewOptions = { url?: string; referrer?: string; }; +/** + * @see https://usefathom.com/docs/features/events + */ export type EventOptions = { _value?: number; _site_id?: string; }; -// refer to https://usefathom.com/support/tracking-advanced +/** + * @see https://usefathom.com/support/tracking-advanced + **/ export type LoadOptions = { url?: string; auto?: boolean; @@ -39,7 +47,11 @@ type FathomCommand = declare global { interface Window { fathom?: Fathom; + + /** @internal */ __fathomClientQueue: FathomCommand[]; + /** @internal */ + __fathomIsLoading?: boolean; } } @@ -57,7 +69,9 @@ const enqueue = (command: FathomCommand): void => { * Flushes the command queue. */ const flushQueue = (): void => { + window.__fathomIsLoading = false; window.__fathomClientQueue = window.__fathomClientQueue || []; + window.__fathomClientQueue.forEach(command => { switch (command.type) { case 'trackPageview': @@ -85,13 +99,13 @@ const flushQueue = (): void => { return; } }); + window.__fathomClientQueue = []; }; /** - * Loops through list of domains and warns if they start with - * http, https, http://, etc... as this does not work with the - * Fathom script. + * Loops through list of domains and warns if they start with http, https, + * http://, etc... as this does not work with the Fathom script. * * @param domains - List of domains to check */ @@ -106,7 +120,20 @@ const checkDomainsAndWarn = (domains: string[]): void => { }); }; +/** + * Loads the Fathom script. + * + * @param siteId - the id for the Fathom site. + * @param opts - advanced tracking options (https://usefathom.com/support/tracking-advanced) + */ export const load = (siteId: string, opts?: LoadOptions): void => { + if (window.__fathomIsLoading || window.fathom) return; + + // Mark that fathom is loading, so that we can prevent a race condition if + // `load` is called again BEFORE the fathom script finishes initializing (and + // so before `window.fathom` is actually defined) + window.__fathomIsLoading = true; + let tracker = document.createElement('script'); let firstScript = @@ -116,8 +143,7 @@ export const load = (siteId: string, opts?: LoadOptions): void => { tracker.id = 'fathom-script'; tracker.async = true; tracker.setAttribute('data-site', siteId); - tracker.src = - opts && opts.url ? opts.url : 'https://cdn.usefathom.com/script.js'; + tracker.src = opts?.url || 'https://cdn.usefathom.com/script.js'; if (opts) { if (opts.auto !== undefined) @@ -199,7 +225,7 @@ export const trackEvent = (eventName: string, opts?: EventOptions) => { /** * Blocks tracking for the current visitor. * - * See https://usefathom.com/docs/features/exclude + * @see https://usefathom.com/docs/features/exclude */ export const blockTrackingForMe = (): void => { if (window.fathom) { @@ -212,7 +238,7 @@ export const blockTrackingForMe = (): void => { /** * Enables tracking for the current visitor. * - * See https://usefathom.com/docs/features/exclude + * @see https://usefathom.com/docs/features/exclude */ export const enableTrackingForMe = (): void => { if (window.fathom) { diff --git a/test/index.test.js b/test/index.test.js index 48dcbe8..30ba55a 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -4,8 +4,17 @@ import * as Fathom from '../src'; +const fathomStub = () => { + return { + trackPageview: jest.fn(), + trackGoal: jest.fn(), + trackEvent: jest.fn() + }; +}; + beforeEach(() => { window.fathom = undefined; + delete window.__fathomIsLoading; delete window.__fathomClientQueue; }); @@ -25,6 +34,31 @@ describe('load', () => { expect(fathomScript.src).toBe('https://cdn.usefathom.com/script.js'); }); + it('skips injecting the script if already loaded or currently loading', () => { + // simulate the script already being loaded + Fathom.load(); + // ↓ + window.fathom = fathomStub(); + + const firstScript = document.createElement('script'); + document.body.appendChild(firstScript); + Fathom.load(); + + const fathomScripts = Array.from( + document.getElementsByTagName('script') + ).filter(s => { + return s.src == 'https://cdn.usefathom.com/script.js'; + }); + + expect(fathomScripts.length).toBe(1); + + // simulate 'onload' firing + const fathomScript = document.getElementById('fathom-script'); + fathomScript.dispatchEvent(new Event('load')); + + expect(window.__fathomIsLoading).toBe(false); + }); + it('injects the Fathom script with options', () => { const firstScript = document.createElement('script'); document.body.appendChild(firstScript); @@ -46,15 +80,12 @@ describe('load', () => { it('runs the queue after load', () => { Fathom.trackPageview(); - window.fathom = { - trackPageview: jest.fn(), - trackGoal: jest.fn(), - trackEvent: jest.fn() - }; - const firstScript = document.createElement('script'); document.body.appendChild(firstScript); + Fathom.load(); + // ↓ + window.fathom = fathomStub(); // simulate 'onload' firing const fathomScript = document.getElementById('fathom-script');