From 5f6b58c0a3eedd4faa4c9308762c3d3bbca75e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Gonz=C3=A1lez?= Date: Tue, 18 Jan 2022 11:11:40 +0100 Subject: [PATCH] Add enableTracking function and allow lazy loading --- CHANGELOG.md | 1 + packages/js/src/MatomoTracker.test.ts | 42 +++++++++ packages/js/src/MatomoTracker.ts | 123 ++++++++++++++------------ packages/react/README.md | 33 +++++++ packages/react/src/types.ts | 1 + packages/react/src/useMatomo.ts | 11 ++- 6 files changed, 150 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51a8fbb2..82b11d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Prefix the change with one of these keywords: ## [Unreleased] - Fixed: Made `trackPageView` `params` argument optional +- Added: Allow delaying library loading when `disabled` is set to true with a new `enableTracking` function exposed in `useMatomo` ## [0.5.1] diff --git a/packages/js/src/MatomoTracker.test.ts b/packages/js/src/MatomoTracker.test.ts index 90f4b3a6..19ac0e17 100644 --- a/packages/js/src/MatomoTracker.test.ts +++ b/packages/js/src/MatomoTracker.test.ts @@ -45,4 +45,46 @@ describe('MatomoTracker', () => { expect(window._paq).toEqual([['foo', 'bar', 1]]) }) }) + + describe('delay enabling tracking', () => { + it('should allow to delay enabling tracking', () => { + window._paq = [] + + const fakeScriptElement = {} as HTMLScriptElement + + const createElementSpy = jest + .spyOn(document, 'createElement') + .mockReturnValue(fakeScriptElement) + + const matomo = new MatomoTracker({ + disabled: true, + siteId: 1, + urlBase: 'https://foo.bar', + configurations: { setCustomDimension: [1, 'someValue'], foo: 'bar' }, + }) + + expect(window._paq).toEqual([]) + expect(createElementSpy).not.toHaveBeenCalled() + + matomo.enableTracking() + + expect(window._paq).toEqual([ + ['setTrackerUrl', 'https://foo.bar/matomo.php'], + ['setSiteId', 1], + ['setCustomDimension', 1, 'someValue'], + ['foo', 'bar'], + ['enableHeartBeatTimer', 15], + ['enableLinkTracking', true], + ]) + + expect(createElementSpy).toHaveBeenCalledWith('script') + + expect(fakeScriptElement).toEqual({ + type: 'text/javascript', + async: true, + defer: true, + src: 'https://foo.bar/matomo.js', + }) + }) + }) }) diff --git a/packages/js/src/MatomoTracker.ts b/packages/js/src/MatomoTracker.ts index b12c7418..6d52772d 100644 --- a/packages/js/src/MatomoTracker.ts +++ b/packages/js/src/MatomoTracker.ts @@ -16,6 +16,8 @@ import { class MatomoTracker { mutationObserver?: MutationObserver + userOptions: UserOptions + constructor(userOptions: UserOptions) { if (!userOptions.urlBase) { throw new Error('Matomo urlBase is required.') @@ -24,22 +26,7 @@ class MatomoTracker { throw new Error('Matomo siteId is required.') } - this.initialize(userOptions) - } - - private initialize({ - urlBase, - siteId, - userId, - trackerUrl, - srcUrl, - disabled, - heartBeat, - linkTracking = true, - configurations = {}, - }: UserOptions) { - const normalizedUrlBase = - urlBase[urlBase.length - 1] !== '/' ? `${urlBase}/` : urlBase + this.userOptions = userOptions if (typeof window === 'undefined') { return @@ -51,51 +38,11 @@ class MatomoTracker { return } - if (disabled) { + if (userOptions.disabled) { return } - this.pushInstruction( - 'setTrackerUrl', - trackerUrl ?? `${normalizedUrlBase}matomo.php`, - ) - - this.pushInstruction('setSiteId', siteId) - - if (userId) { - this.pushInstruction('setUserId', userId) - } - - Object.entries(configurations).forEach(([name, instructions]) => { - if (instructions instanceof Array) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - this.pushInstruction(name, ...instructions) - } else { - this.pushInstruction(name, instructions) - } - }) - - // accurately measure the time spent on the last pageview of a visit - if (!heartBeat || (heartBeat && heartBeat.active)) { - this.enableHeartBeatTimer((heartBeat && heartBeat.seconds) ?? 15) - } - - // // measure outbound links and downloads - // // might not work accurately on SPAs because new links (dom elements) are created dynamically without a server-side page reload. - this.enableLinkTracking(linkTracking) - - const doc = document - const scriptElement = doc.createElement('script') - const scripts = doc.getElementsByTagName('script')[0] - - scriptElement.type = 'text/javascript' - scriptElement.async = true - scriptElement.defer = true - scriptElement.src = srcUrl || `${normalizedUrlBase}matomo.js` - - if (scripts && scripts.parentNode) { - scripts.parentNode.insertBefore(scriptElement, scripts) - } + this.enableTracking() } enableHeartBeatTimer(seconds: number): void { @@ -358,6 +305,66 @@ class MatomoTracker { return this } + + enableTracking(): MatomoTracker { + const { + urlBase, + siteId, + userId, + trackerUrl, + srcUrl, + heartBeat, + linkTracking = true, + configurations = {}, + } = this.userOptions + + const normalizedUrlBase = + urlBase[urlBase.length - 1] !== '/' ? `${urlBase}/` : urlBase + + this.pushInstruction( + 'setTrackerUrl', + trackerUrl ?? `${normalizedUrlBase}matomo.php`, + ) + + this.pushInstruction('setSiteId', siteId) + + if (userId) { + this.pushInstruction('setUserId', userId) + } + + Object.entries(configurations).forEach(([name, instructions]) => { + if (instructions instanceof Array) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.pushInstruction(name, ...instructions) + } else { + this.pushInstruction(name, instructions) + } + }) + + // accurately measure the time spent on the last pageview of a visit + if (!heartBeat || (heartBeat && heartBeat.active)) { + this.enableHeartBeatTimer((heartBeat && heartBeat.seconds) ?? 15) + } + + // // measure outbound links and downloads + // // might not work accurately on SPAs because new links (dom elements) are created dynamically without a server-side page reload. + this.enableLinkTracking(linkTracking) + + const doc = document + const scriptElement = doc.createElement('script') + const scripts = doc.getElementsByTagName('script')[0] + + scriptElement.type = 'text/javascript' + scriptElement.async = true + scriptElement.defer = true + scriptElement.src = srcUrl || `${normalizedUrlBase}matomo.js` + + if (scripts && scripts.parentNode) { + scripts.parentNode.insertBefore(scriptElement, scripts) + } + + return this + } } export default MatomoTracker diff --git a/packages/react/README.md b/packages/react/README.md index 0f147ee6..5ef7bed3 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -184,7 +184,40 @@ const MyApp = () => { // Render components ) } +``` + +## Delay loading Matomo +In order to be GDPR compliant, users must give consent before behavioural tracking is applied to them. To achieve this you should pass `disabled` set to true when initializing your Matomo instance and then call `enableTracking` once you get their approval: + +```tsx +import { MatomoProvider, createInstance, useMatomo } from '@datapunt/matomo-tracker-react' +import { userHasConsented } from 'your-own-tracking-acceptance-library' + +const instance = createInstance({ + urlBase: "https://LINK.TO.DOMAIN", + disabled: true, // Prevents the matomo provided library from loading on instantiation +}); + +ReactDOM.render( + + + +) + +const MyApp = () => { + const { enableTracking } = useMatomo() + + React.useEffect(() => { + if(userHasConsented()) { + enableTracking() + } + }, []) + + return ( + // Render components + ) +} ``` ## References diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 2b9d1309..f1de79fa 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -7,6 +7,7 @@ export interface MatomoInstance { trackSiteSearch: MatomoTracker['trackSiteSearch'] trackLink: MatomoTracker['trackLink'] pushInstruction: MatomoTracker['pushInstruction'] + enableTracking: MatomoTracker['enableTracking'] } export type InstanceParams = types.UserOptions diff --git a/packages/react/src/useMatomo.ts b/packages/react/src/useMatomo.ts index f54703e8..ddcd33aa 100644 --- a/packages/react/src/useMatomo.ts +++ b/packages/react/src/useMatomo.ts @@ -47,14 +47,19 @@ function useMatomo() { [instance], ) + const enableTracking = useCallback(() => { + instance?.enableTracking() + }, [instance]) + return { + enableLinkTracking, + enableTracking, + pushInstruction, trackEvent, trackEvents, + trackLink, trackPageView, trackSiteSearch, - trackLink, - enableLinkTracking, - pushInstruction, } }