Skip to content

Commit

Permalink
feature: Enable Matomo configuration to be provided as a Promise
Browse files Browse the repository at this point in the history
  • Loading branch information
Arnaud73 committed Jul 7, 2023
1 parent 8323f19 commit 30c64d4
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 74 deletions.
10 changes: 9 additions & 1 deletion projects/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@ import { EventComponent } from './event/event.component';
imports: [
BrowserModule,
ReactiveFormsModule,
MatomoModule.forRoot(environment.matomoConfig),
MatomoModule.forRoot(
// Delay configuration of the Matomo module by 25 seconds.
new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Matomo configuration retrieved');
resolve(environment.matomoConfig);
}, 25000);
})
),
AppRoutingModule,
],
providers: [],
Expand Down
2 changes: 1 addition & 1 deletion projects/demo/src/environments/environment.no-tracking.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const environment = {
production: true,
matomoConfig: {},
matomoConfig: { mode: <'INACTIVE'>'INACTIVE' },
};
1 change: 1 addition & 0 deletions projects/demo/src/environments/environment.prod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const environment = {
production: true,
matomoConfig: {
mode: <'ACTIVE'>'ACTIVE',
scriptUrl: '//cdn.matomo.cloud/ngx.matomo.cloud/matomo.js',
trackers: [
{
Expand Down
1 change: 1 addition & 0 deletions projects/demo/src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export const environment = {
production: false,
matomoConfig: {
mode: <'ACTIVE'>'ACTIVE',
scriptUrl: '//cdn.matomo.cloud/ngx.matomo.cloud/matomo.js',
trackers: [
{
Expand Down
26 changes: 22 additions & 4 deletions projects/ngx-matomo/src/lib/matomo-configuration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { InjectionToken } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

export type Mode = 'ACTIVE' | 'INACTIVE' | 'PRELOADED';

export interface SanitizedMatomoConfiguration {
/**
* Mode for the Matomo tracker initialization.
*/
mode: Mode;
/**
* URL of the Matomo JS script to execute.
*/
Expand Down Expand Up @@ -62,12 +69,16 @@ export interface MatomoConfiguration extends SanitizedMatomoConfiguration {

export function sanitizeConfiguration(
configuration: Partial<MatomoConfiguration>
): Partial<SanitizedMatomoConfiguration> {
const sanitizedConfiguration: Partial<SanitizedMatomoConfiguration> = {
): SanitizedMatomoConfiguration {
const sanitizedConfiguration = {
...defaultConfiguration,
...configuration,
};

if (configuration.mode === undefined) {
sanitizedConfiguration.mode = !!configuration.scriptUrl ? 'ACTIVE' : 'INACTIVE';
}

if (configuration.trackAppStart !== undefined && configuration.trackAppStart !== null) {
sanitizedConfiguration.skipTrackingInitialPageView = !configuration.trackAppStart;
}
Expand All @@ -82,12 +93,19 @@ export function sanitizeConfiguration(
/**
* Injection token for Matomo configuration.
*/
export const MATOMO_CONFIGURATION = new InjectionToken<MatomoConfiguration>('MATOMO_CONFIGURATION');
export const MATOMO_CONFIGURATION = new InjectionToken<Promise<Partial<MatomoConfiguration>>>(
'MATOMO_CONFIGURATION'
);

export const SANITIZED_MATOMO_CONFIGURATION = new InjectionToken<
BehaviorSubject<SanitizedMatomoConfiguration>
>('SANITIZED_MATOMO_CONFIGURATION');

/**
* Default configuration for the Matomo module.
*/
const defaultConfiguration: Partial<SanitizedMatomoConfiguration> = {
const defaultConfiguration = {
mode: <Mode>'ACTIVE',
scriptVersion: 4,
trackers: [],
requireConsent: false,
Expand Down
67 changes: 12 additions & 55 deletions projects/ngx-matomo/src/lib/matomo-injector.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';

import { MatomoConfiguration, MATOMO_CONFIGURATION } from './matomo-configuration';
import { SanitizedMatomoConfiguration } from './matomo-configuration';

declare global {
/**
Expand All @@ -19,64 +19,21 @@ declare global {
export class MatomoInjector {
/**
* Creates an instance of MatomoInjector.
*
* @param configuration Matomo configuration provided by DI.
*/
constructor(@Inject(MATOMO_CONFIGURATION) private readonly configuration: MatomoConfiguration) {
try {
window['_paq'] = window['_paq'] || (!!this.configuration.scriptUrl ? [] : { push: () => {} });
} catch (e) {
if (!(e instanceof ReferenceError)) {
throw e;
}
}
}
constructor() {}

/**
* Configures and injects the Matomo tracker in the DOM.
*/
init(): void {
try {
if (this.configuration?.requireConsent === true) {
window['_paq'].push(['requireConsent']);
} else if (this.configuration?.requireCookieConsent === true) {
window['_paq'].push(['requireCookieConsent']);
}
if (this.configuration?.skipTrackingInitialPageView === false) {
window['_paq'].push(['trackPageView']);
if (
this.configuration?.trackLinks === true &&
this.configuration?.routeTracking?.enable === false
) {
setTimeout(() => {
window['_paq'].push([
'enableLinkTracking',
this.configuration?.trackLinkValue ?? false,
]);
}, 0);
}
}
if (this.configuration.trackers?.length) {
const [mainTracker, ...otherTrackers] = this.configuration.trackers;
window['_paq'].push(['setTrackerUrl', mainTracker.trackerUrl]);
window['_paq'].push(['setSiteId', mainTracker.siteId.toString()]);
otherTrackers.forEach((tracker) =>
window['_paq'].push(['addTracker', tracker.trackerUrl, tracker.siteId.toString()])
);
}
if (!!this.configuration.scriptUrl) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.defer = true;
script.src = this.configuration.scriptUrl;
const firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode?.insertBefore(script, firstScript);
}
} catch (e) {
if (!(e instanceof ReferenceError)) {
throw e;
}
init(configuration: SanitizedMatomoConfiguration): void {
if (!!configuration.scriptUrl) {
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.defer = true;
script.src = configuration.scriptUrl;
const firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode?.insertBefore(script, firstScript);
}
}
}
23 changes: 20 additions & 3 deletions projects/ngx-matomo/src/lib/matomo-tracker.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

import { MATOMO_CONFIGURATION, MatomoConfiguration } from './matomo-configuration';
import {
SANITIZED_MATOMO_CONFIGURATION,
SanitizedMatomoConfiguration,
} from './matomo-configuration';

declare global {
/**
Expand Down Expand Up @@ -28,7 +32,10 @@ export class MatomoTracker {
*
* @param configuration Matomo configuration provided by DI.
*/
constructor(@Inject(MATOMO_CONFIGURATION) private readonly configuration: MatomoConfiguration) {
constructor(
@Inject(SANITIZED_MATOMO_CONFIGURATION)
private readonly $configuration$: BehaviorSubject<SanitizedMatomoConfiguration | null>
) {
try {
if (typeof window['_paq'] === 'undefined') {
console.warn('Matomo has not yet been initialized!');
Expand Down Expand Up @@ -701,7 +708,7 @@ export class MatomoTracker {
* @param generationTime Time, in milliseconds, of the page generation.
*/
setGenerationTimeMs(generationTime: number): void {
if (this.configuration.scriptVersion < 4) {
if ((this.$configuration$.getValue()?.scriptVersion || 4) < 4) {
try {
window['_paq'].push(['setGenerationTimeMs', generationTime]);
} catch (e) {
Expand Down Expand Up @@ -1752,3 +1759,13 @@ export class MatomoTracker {
}
}
}

function filterOutreferenceErrors(f: () => void): void {
try {
f();
} catch (e) {
if (!(e instanceof ReferenceError)) {
throw e;
}
}
}
88 changes: 78 additions & 10 deletions projects/ngx-matomo/src/lib/matomo.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import { NgModule, ModuleWithProviders, Inject, PLATFORM_ID, Injector } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

import { MatomoConfiguration, MATOMO_CONFIGURATION } from './matomo-configuration';
import {
MatomoConfiguration,
MATOMO_CONFIGURATION,
sanitizeConfiguration,
SanitizedMatomoConfiguration,
SANITIZED_MATOMO_CONFIGURATION,
} from './matomo-configuration';
import { MatomoInjector } from './matomo-injector.service';
import { MatomoTracker } from './matomo-tracker.service';
import { MatomoRouteTracker } from './matomo-route-tracker.service';
import { BehaviorSubject } from 'rxjs';

declare global {
/**
* Extend Window interface in order to introduce the Matomo _paq attribute
*/
interface Window {
_paq: any;
}
}

/**
* Angular module encapsulating Matomo features.
Expand All @@ -27,32 +43,84 @@ export class MatomoModule {
constructor(
@Inject(PLATFORM_ID) private readonly platformId: Object,
private readonly injector: Injector,
@Inject(MATOMO_CONFIGURATION) private readonly configuration: MatomoConfiguration,
@Inject(MATOMO_CONFIGURATION)
private readonly configuration: Promise<Partial<MatomoConfiguration>>,
private readonly matomoInjector: MatomoInjector
) {
// Warn if module is not being loaded by a browser.
if (!isPlatformBrowser(this.platformId)) {
console.warn('ngx-Matomo does not support server platform');
}
// Inject the Matomo script and create trackers.
this.matomoInjector.init();
// Enable route tracking if requested.
if (this.configuration?.routeTracking?.enable === true) {
// Using Injector instead of DI in order to allow use in routerless apps.
this.injector.get(MatomoRouteTracker).startTracking();
if (window['_paq'] === undefined) {
window['_paq'] = [];
}
this.configuration.then(sanitizeConfiguration).then(
(config) => {
switch (config?.mode) {
// @ts-expect-error
case 'ACTIVE':
window['_paq'] = [];
// Inject the Matomo script and create trackers.
this.matomoInjector.init(config);
case 'PRELOADED':
try {
if (config?.requireConsent === true) {
window['_paq'].push(['requireConsent']);
} else if (config?.requireCookieConsent === true) {
window['_paq'].push(['requireCookieConsent']);
}
if (config?.skipTrackingInitialPageView === false) {
window['_paq'].push(['trackPageView']);
if (config?.trackLinks === true && config?.routeTracking?.enable === false) {
setTimeout(() => {
window['_paq'].push(['enableLinkTracking', config?.trackLinkValue ?? false]);
}, 0);
}
}
if (config.trackers?.length) {
const [mainTracker, ...otherTrackers] = config.trackers;
window['_paq'].push(['setTrackerUrl', mainTracker.trackerUrl]);
window['_paq'].push(['setSiteId', mainTracker.siteId.toString()]);
otherTrackers.forEach((tracker) =>
window['_paq'].push(['addTracker', tracker.trackerUrl, tracker.siteId.toString()])
);
}
// Enable route tracking if requested.
if (config?.routeTracking?.enable === true) {
// Using Injector instead of DI in order to allow use in routerless apps.
this.injector.get(MatomoRouteTracker).startTracking();
}
} catch (e) {
if (!(e instanceof ReferenceError)) {
throw e;
}
}
break;
case 'INACTIVE':
window['_paq'] = { push: () => {} };
break;
}
},
(err) => {}
);
}

/**
* Use this method in your root module to provide the MatomoTracker service.
*/
static forRoot(configuration?: Partial<MatomoConfiguration>): ModuleWithProviders<MatomoModule> {
static forRoot(
configuration?: Promise<Partial<MatomoConfiguration>> | Partial<MatomoConfiguration>
): ModuleWithProviders<MatomoModule> {
return {
ngModule: MatomoModule,
providers: [
{
provide: MATOMO_CONFIGURATION,
useValue: configuration,
useValue: Promise.resolve(configuration),
},
{
provide: SANITIZED_MATOMO_CONFIGURATION,
useValue: new BehaviorSubject<SanitizedMatomoConfiguration | null>(null),
},
MatomoTracker,
MatomoRouteTracker,
Expand Down

0 comments on commit 30c64d4

Please sign in to comment.