Skip to content
This repository has been archived by the owner on Jan 30, 2023. It is now read-only.

Add enableTracking function and allow lazy loading #736

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
42 changes: 42 additions & 0 deletions packages/js/src/MatomoTracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
})
})
})
123 changes: 65 additions & 58 deletions packages/js/src/MatomoTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
class MatomoTracker {
mutationObserver?: MutationObserver

userOptions: UserOptions

constructor(userOptions: UserOptions) {
if (!userOptions.urlBase) {
throw new Error('Matomo urlBase is required.')
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
33 changes: 33 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<MatomoProvider value={instance}>
<MyApp />
</MatomoProvider>
)

const MyApp = () => {
const { enableTracking } = useMatomo()

React.useEffect(() => {
if(userHasConsented()) {
enableTracking()
}
}, [])

return (
// Render components
)
}
```

## References
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface MatomoInstance {
trackSiteSearch: MatomoTracker['trackSiteSearch']
trackLink: MatomoTracker['trackLink']
pushInstruction: MatomoTracker['pushInstruction']
enableTracking: MatomoTracker['enableTracking']
}

export type InstanceParams = types.UserOptions
Expand Down
11 changes: 8 additions & 3 deletions packages/react/src/useMatomo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down