Skip to content

Commit

Permalink
feat: push SPA router changes to matomo analytics (when appropriate)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmijatovic committed Aug 30, 2022
1 parent 049028d commit 57e7564
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 36 deletions.
64 changes: 64 additions & 0 deletions frontend/components/cookies/setMatomoPage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 dv4all
//
// SPDX-License-Identifier: Apache-2.0

import {initMatomoCustomUrl} from './setMatomoPage'

let setCustomUrl:Function

// mock push fn
const mockPush = jest.fn()
// mock matomo object _paq
Object.defineProperty(window, '_paq', {
value: {
push: mockPush,
}
})

beforeEach(() => {
setCustomUrl = initMatomoCustomUrl()
})

it('does not trigger action when matomo.id is null', () => {
const matomo = {
id: null,
consent:null
}
setCustomUrl(matomo)
expect(mockPush).not.toBeCalled()
})

it('does not trigger action when matomo.consent is false', () => {
const matomo = {
id: 'test-id',
consent: false
}
setCustomUrl(matomo)
expect(mockPush).not.toBeCalled()
})

// This test ensures that we push custom page only after inital
// page load. At initial page load piwik.js script will report that
// page automatically. After that we need to push customUrl change on our
// because we use SPA router (next.router) for navigation
it('push custom page to matomo ONLY after initial url changed', () => {
const matomo = {
id: 'test-id',
consent: true
}
setCustomUrl(matomo)
// this will not trigger call to _paq
// because previousUrl is null unchanged
expect(mockPush).not.toBeCalled()
// move to another location
Object.defineProperty(window, 'location', {
value: {
href: 'http://test.com/page1',
}
})
setCustomUrl(matomo)
// calls push 4 times to setReferrerUrl,
// setCustomUrl, setDocumentTitle, trackPageView
expect(mockPush).toBeCalledTimes(4)
})
57 changes: 57 additions & 0 deletions frontend/components/cookies/setMatomoPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 dv4all
//
// SPDX-License-Identifier: Apache-2.0

import {Matomo} from './nodeCookies'

export function initMatomoCustomUrl() {
// keep previousPath in memory
let previousUrl: string | null = null
/**
* This function is used to register SPA router changes.
* These route changes are not registerd by matomo automatically.
* The function should be called after navigation is completed in order
* to extract correct (dynamic) page title
* https://matomo.org/faq/how-to/how-do-i-set-a-custom-url-using-the-matomo-javascript-tracker/
* @param param0
*/
function setCustomUrl({id, consent}: Matomo) {
// console.group('setMatomoPage')
// console.log('id...', id)
// console.log('consent...', consent)
// console.log('previousUrl...', previousUrl)
if (id && consent &&
typeof window != 'undefined' &&
typeof location != 'undefined'
) {
// extract push function from matomo/piwik
const setValue = (window as any)?._paq?.push
// console.log('typeof setValue...', typeof setValue)
if (typeof setValue === 'function') {
// extract current location
const href = location.href
// console.log('href...', href)
// we use this method ONLY after initial landing
// at initial landing we only save previousUrl
if (previousUrl !== null && previousUrl !== href) {
// set previous page/url
setValue(['setReferrerUrl', previousUrl])
// console.log('setReferrerUrl...', previousUrl)
// set current page/url
setValue(['setCustomUrl', href])
// console.log('setCustomUrl...', href)
// set page title
setValue(['setDocumentTitle', document.title])
// console.log('setDocumentTitle...', document.title)
// this push triggers piwik.php script to track new page
setValue(['trackPageView'])
}
// save current url as previuos url
previousUrl=href
}
}
// console.groupEnd()
}
return setCustomUrl
}
56 changes: 42 additions & 14 deletions frontend/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// SPDX-License-Identifier: Apache-2.0

import {useEffect} from 'react'
import {useRouter} from 'next/router'
import Router, {useRouter} from 'next/router'
import App, {AppContext, AppProps} from 'next/app'
import Head from 'next/head'
import {ThemeProvider} from '@mui/material/styles'
Expand All @@ -32,6 +32,7 @@ import {RsdSettingsProvider} from '~/config/RsdSettingsContext'
import {RsdSettingsState} from '~/config/rsdSettingsReducer'
import {getPageLinks} from '~/components/page/useMarkdownPages'
import {getMatomoConsent,Matomo} from '~/components/cookies/nodeCookies'
import {initMatomoCustomUrl} from '~/components/cookies/setMatomoPage'

// extend Next app props interface with emotion cache
export interface MuiAppProps extends AppProps {
Expand All @@ -55,8 +56,23 @@ const matomo: Matomo = {
id: process.env.MATOMO_ID || null,
consent: null
}
const setCustomUrl = initMatomoCustomUrl()
// init session
let session: Session | null = null
// ProgressBar at the top
// listen to route change and drive nprogress status
// it's taken out of RsdApp to be initialized only once
Router.events.on('routeChangeStart', (props) => {
// console.log('routeChangeStart...props...', props)
nprogress.start()
})
Router.events.on('routeChangeComplete', (path) => {
// console.log('routeChangeComplete...path...', path)
nprogress.done()
})
Router.events.on('routeChangeError', ()=>{
nprogress.done()
})

function RsdApp(props: MuiAppProps) {
const {
Expand All @@ -67,21 +83,13 @@ function RsdApp(props: MuiAppProps) {
const muiTheme = loadMuiTheme(settings.theme.mode as RsdThemes)
const router = useRouter()

// Matomo customUrl method
// to register SPA route changes
useEffect(()=>{
router.events.on('routeChangeStart', ()=>{
nprogress.start()
})
router.events.on('routeChangeComplete', ()=>{
nprogress.done()
})
router.events.on('routeChangeError', ()=>{
nprogress.done()
})
return () => {
// on effect teardown
nprogress.done()
if (matomo?.id) {
setCustomUrl(matomo)
}
})
},[router.asPath,matomo])

// save location cookie
if (typeof document != 'undefined') {
Expand Down Expand Up @@ -124,6 +132,26 @@ function RsdApp(props: MuiAppProps) {
* NOTE! initially it runs server side but moves to client side
* when Link or router.push is used.
* see https://nextjs.org/docs/api-reference/data-fetching/get-initial-props
* Resolution order
On the server:
1. app.getInitialProps
2. page.getInitialProps
3. document.getInitialProps
4. app.render
5. page.render
6. document.render
On the server with error:
1. document.getInitialProps
2. app.render
3. page.render
4. document.render
On the client
1. app.getInitialProps
2. page.getInitialProps
3. app.render
4. page.render
* @param appContext
* @returns
*/
Expand Down
42 changes: 20 additions & 22 deletions frontend/pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,29 +90,27 @@ export default class MyDocument extends Document {

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with static-site generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
// Resolution order
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render

// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render

// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
MyDocument.getInitialProps = async (ctx) => {
const originalRenderPage = ctx.renderPage

// You can consider sharing the same emotion cache between all the SSR requests to speed up performance.
Expand Down

0 comments on commit 57e7564

Please sign in to comment.