Skip to content

Commit

Permalink
feat: replace old matomo tracking HP-2500
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkojamG committed Aug 19, 2024
1 parent c0c41c3 commit 4e38591
Show file tree
Hide file tree
Showing 22 changed files with 380 additions and 74 deletions.
6 changes: 5 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ REACT_APP_PROFILE_GRAPHQL=
REACT_APP_OIDC_SCOPE="openid profile https://api.hel.fi/auth/helsinkiprofile"
REACT_APP_OIDC_RESPONSE_TYPE="code"
REACT_APP_SENTRY_DSN=
REACT_APP_MATOMO_ENABLED=false
REACT_APP_ENVIRONMENT=
REACT_APP_MATOMO_SRC_URL=
REACT_APP_MATOMO_URL_BASE="<Matomo url base>"
REACT_APP_MATOMO_SITE_ID="<Matomo site id>"
REACT_APP_MATOMO_ENABLED=false
REACT_APP_VERSION=$npm_package_version
TRANSLATION_LANGUAGES=en,fi,sv
TRANSLATIONS_SHEET_ID=1Ky-E1nJ_pRUYMoORobahOJ_IWucfL7kirBBLiA6r8zs
TRANSLATION_PROJECT_NAME=open-city-profile
GENERATE_SOURCEMAP=false

3 changes: 3 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
REACT_APP_PROFILE_GRAPHQL="https://helsinkiprofile.test.kuva.hel.ninja/graphql/"
REACT_APP_HELSINKI_ACCOUNT_AMR="helsinki_tunnus"
REACT_APP_KEYCLOAK_AUTHORITY="https://tunnistus.test.hel.ninja/auth/realms/helsinki-tunnistus/"
REACT_APP_MATOMO_URL_BASE="test"
REACT_APP_MATOMO_SITE_ID="test123"
REACT_APP_MATOMO_ENABLED=false
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
"private": true,
"dependencies": {
"@apollo/client": "^3.9.6",
"@datapunt/matomo-tracker-react": "^0.5.1",
"@react-aria/visually-hidden": "^3.2.1",
"@sentry/react": "^7.103.0",
"@sinonjs/fake-timers": "^8.1.0",
Expand Down
19 changes: 14 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React from 'react';
import { Switch, Route } from 'react-router';
import { ApolloProvider } from '@apollo/client';
import { MatomoProvider } from '@datapunt/matomo-tracker-react';
import countries from 'i18n-iso-countries';
import fi from 'i18n-iso-countries/langs/fi.json';
import en from 'i18n-iso-countries/langs/en.json';
import sv from 'i18n-iso-countries/langs/sv.json';
import { MatomoInstance } from '@datapunt/matomo-tracker-react/lib/types';

import graphqlClient from './graphql/client';
import Login from './auth/components/login/Login';
Expand All @@ -26,19 +24,30 @@ import { useHistoryListener } from './profile/hooks/useHistoryListener';
import WithAuthCheck from './profile/components/withAuthCheck/WithAuthCheck';
import CookieConsentPage from './cookieConsents/CookieConsentPage';
import LoginSSO from './auth/components/loginsso/LoginSSO';
import { useTrackingInstance } from './common/helpers/tracking/matomoTracking';
import MatomoTracker from './common/matomo/MatomoTracker';
import { MatomoProvider } from './common/matomo/matomo-context';

countries.registerLocale(fi);
countries.registerLocale(en);
countries.registerLocale(sv);

function App(): React.ReactElement {
useHistoryListener();
const instance = useTrackingInstance();

const matomoTracker = new MatomoTracker({
urlBase: window._env_.REACT_APP_MATOMO_URL_BASE,
siteId: window._env_.REACT_APP_MATOMO_SITE_ID,
srcUrl: window._env_.REACT_APP_MATOMO_SRC_URL,
enabled: window._env_.REACT_APP_MATOMO_ENABLED === 'true',
configurations: {
setDoNotTrack: true,
},
});

return (
<ApolloProvider client={graphqlClient}>
<ToastProvider>
<MatomoProvider value={instance as MatomoInstance}>
<MatomoProvider value={matomoTracker}>
<ProfileProvider>
<Switch>
<Route path="/callback" component={OidcCallback} />
Expand Down
2 changes: 0 additions & 2 deletions src/BrowserApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import { I18nextProvider } from 'react-i18next';
import App from './App';
import i18n from './i18n/i18nInit';
import CookieConsentModal from './cookieConsents/CookieConsentModal';
import { disableTrackingCookiesUntilConsentGiven } from './common/helpers/tracking/matomoTracking';

function BrowserApp(): React.ReactElement {
disableTrackingCookiesUntilConsentGiven();
return (
<I18nextProvider i18n={i18n}>
<BrowserRouter>
Expand Down
12 changes: 1 addition & 11 deletions src/__tests__/BrowserApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { render } from '@testing-library/react';
import { Mock } from 'vitest';

import BrowserApp from '../BrowserApp';
import { getMockCallArgs } from '../common/test/mockHelper';

describe('BrowserApp', () => {
const pushTracker = vi.fn();
Expand All @@ -21,16 +20,7 @@ describe('BrowserApp', () => {
}));
});

it('renders without crashing and commands tracker to wait for consent', () => {
it('renders without crashing', () => {
render(<BrowserApp />);

const calls = getMockCallArgs(pushTracker) as string[];

expect(calls).toEqual([
['requireCookieConsent'],
['requireConsent'],
['forgetCookieConsentGiven'],
['forgetConsentGiven'],
]);
});
});
2 changes: 1 addition & 1 deletion src/auth/components/login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useMatomo } from '@datapunt/matomo-tracker-react';
import classNames from 'classnames';
import { Button } from 'hds-react';

Expand All @@ -10,6 +9,7 @@ import PageLayout from '../../../common/pageLayout/PageLayout';
import commonContentStyles from '../../../common/cssHelpers/content.module.css';
import authService from '../../authService';
import FocusableH1 from '../../../common/focusableH1/FocusableH1';
import useMatomo from '../../../common/matomo/hooks/useMatomo';

function Login(): React.ReactElement {
const { t } = useTranslation();
Expand Down
2 changes: 1 addition & 1 deletion src/common/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {
logoSv,
LanguageSelectorProps,
} from 'hds-react';
import { useMatomo } from '@datapunt/matomo-tracker-react';

import { MAIN_CONTENT_ID } from '../constants';
import { ProfileContext } from '../../profile/context/ProfileContext';
import UserDropdown from './userDropdown/UserDropdown';
import useMatomo from '../matomo/hooks/useMatomo';

function Header(): React.ReactElement {
const { t, i18n } = useTranslation();
Expand Down
2 changes: 1 addition & 1 deletion src/common/header/userDropdown/UserDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Header as HDSHeader, IconUser } from 'hds-react';
import { useMatomo } from '@datapunt/matomo-tracker-react';

import authService from '../../../auth/authService';
import { ProfileContext } from '../../../profile/context/ProfileContext';
import useMatomo from '../../matomo/hooks/useMatomo';

type UserDataWithActions = {
userName: string;
Expand Down
36 changes: 0 additions & 36 deletions src/common/helpers/tracking/matomoTracking.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { createInstance } from '@datapunt/matomo-tracker-react';
import { useMemo } from 'react';

import { trackingCookieId } from '../../../cookieConsents/cookieContentSource';

type TrackingEvent = string[];
Expand Down Expand Up @@ -58,36 +55,3 @@ export function handleCookieConsentChange(
disableTrackingCookies();
}
}

export function useTrackingInstance():
| ReturnType<typeof createInstance>
| undefined {
return useMemo(() => {
if (import.meta.env.REACT_APP_MATOMO_ENABLED !== 'true') {
return undefined;
}

// matomo.js is not loaded, if window._paq.length > 0
// so clearing it before creating the instance.
// events are pushed back below
const existingTrackingEvents = Array.isArray(window._paq)
? [...window._paq]
: [];

const hadExistingEvents = existingTrackingEvents.length;
if (hadExistingEvents) {
window._paq.length = 0;
}

const matomo = createInstance({
urlBase: 'https://analytics.hel.ninja/',
siteId: 60,
});

if (hadExistingEvents) {
existingTrackingEvents.forEach(pushEvent);
}

return matomo;
}, []);
}
152 changes: 152 additions & 0 deletions src/common/matomo/MatomoTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* eslint-disable no-underscore-dangle */
import { TRACK_TYPES } from './constants';

export type MatomoTrackerOptions = {
urlBase: string;
siteId: string;
srcUrl: string;
trackerUrl?: string;
enabled: boolean;
linkTracking?: boolean;
configurations?: {
[key: string]: string | string[] | boolean | undefined;
};
};

export type CustomDimension = {
id: number;
value: string;
};

export type TrackPageViewParams = {
documentTitle?: string;
href?: string | Location;
customDimensions?: boolean | CustomDimension[];
};

export interface TrackEventParams extends TrackPageViewParams {
category: string;
action: string;
name?: string;
value?: number;
}

export type TrackParams = {
data: unknown[];
} & TrackPageViewParams;

class MatomoTracker {
constructor(userOptions: MatomoTrackerOptions) {
if (!userOptions.urlBase) {
throw new Error('Matomo urlBase is required');
}

if (!userOptions.siteId) {
throw new Error('Matomo siteId is required.');
}

this.initialize(userOptions);
}

enableLinkTracking(active: boolean): void {
this.pushInstruction('enableLinkTracking', active);
}

pushInstruction(name: string, ...args: unknown[]): this {
if (typeof window !== 'undefined') {
window._paq.push([name, ...args]);
}

return this;
}

trackPageView(params?: TrackPageViewParams): void {
this.track({ data: [TRACK_TYPES.TRACK_VIEW], ...params });
}

// Tracks events
// https://matomo.org/docs/event-tracking/#tracking-events
trackEvent({
category,
action,
name,
value,
...otherParams
}: TrackEventParams): void {
if (category && action) {
this.track({
data: [TRACK_TYPES.TRACK_EVENT, category, action, name, value],
...otherParams,
});
} else {
throw new Error(`Error: category and action are required.`);
}
}

track({
data = [],
documentTitle = document.title,
href,
}: TrackParams): void {
if (data.length) {
this.pushInstruction('setCustomUrl', href ?? window.location.href);
this.pushInstruction('setDocumentTitle', documentTitle);

this.pushInstruction(...(data as [string, ...unknown[]]));
}
}

private initialize({
urlBase,
siteId,
srcUrl,
trackerUrl = 'matomo.php',
enabled = true,
linkTracking = true,
configurations = {},
}: MatomoTrackerOptions) {
if (typeof window === 'undefined') {
return;
}

window._paq = window._paq || [];

if (window._paq.length !== 0) {
return;
}

if (!enabled) {
return;
}

this.pushInstruction('setTrackerUrl', `${urlBase}${trackerUrl}`);
this.pushInstruction('setSiteId', siteId);

Object.entries(configurations).forEach(([name, instructions]) => {
if (instructions instanceof Array) {
this.pushInstruction(name, ...instructions);
} else if (instructions === undefined) {
this.pushInstruction(name);
} else {
this.pushInstruction(name, instructions);
}
});

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 = `${urlBase}${srcUrl}`;

if (scripts?.parentNode) {
scripts?.parentNode.insertBefore(scriptElement, scripts);
}
}
}

export default MatomoTracker;
45 changes: 45 additions & 0 deletions src/common/matomo/__tests__/MatomoTracker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable no-underscore-dangle */
import MatomoTracker, { MatomoTrackerOptions } from '../MatomoTracker';

describe('MatomoTracker', () => {
it('should initialise window._paq', () => {
window._paq = [];

const intance = new MatomoTracker({
urlBase: 'https://www.test.fi/',
siteId: 'test123',
srcUrl: 'test.js',
enabled: true,
configurations: {
foo: 'bar',
testArray: ['testArrayItem1', 'testArrayItem2'],
testNoValue: undefined,
},
});

expect(intance).toBeTruthy();
expect(window._paq).toEqual([
['setTrackerUrl', 'https://www.test.fi/matomo.php'],
['setSiteId', 'test123'],
['foo', 'bar'],
['testArray', 'testArrayItem1', 'testArrayItem2'],
['testNoValue'],
['enableLinkTracking', true],
]);
});

it('should throw error if urlBase missing', () => {
expect(
() => new MatomoTracker({ siteId: 'test123' } as MatomoTrackerOptions)
).toThrowError();
});

it('should throw error if siteId missing', () => {
expect(
() =>
new MatomoTracker({
urlBase: 'https://www.test.fi',
} as MatomoTrackerOptions)
).toThrowError();
});
});
Loading

0 comments on commit 4e38591

Please sign in to comment.