Skip to content

Commit

Permalink
feat: allow providing Matomo in lazy-loaded components/modules (#98)
Browse files Browse the repository at this point in the history
fixes #97
  • Loading branch information
raphael22 authored and EmmanuelRoux committed Jan 14, 2025
1 parent 3d002d8 commit 2982fc4
Show file tree
Hide file tree
Showing 28 changed files with 828 additions and 860 deletions.
3 changes: 2 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"options": {
"main": "projects/ngx-matomo-client/test.ts",
"tsConfig": "projects/ngx-matomo-client/tsconfig.spec.json",
"karmaConfig": "projects/ngx-matomo-client/karma.conf.js"
"karmaConfig": "projects/ngx-matomo-client/karma.conf.js",
"codeCoverageExclude": ["projects/ngx-matomo-client/core/testing/**"]
}
},
"lint": {
Expand Down
6 changes: 3 additions & 3 deletions projects/demo/src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { MatomoConfiguration, MatomoRouterModule, MatomoModule } from 'ngx-matomo-client';
import { MatomoModule, MatomoRouterModule } from 'ngx-matomo-client';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
Expand All @@ -12,8 +12,8 @@ describe('AppComponent', () => {
MatomoModule.forRoot({
trackerUrl: '',
siteId: '',
} as MatomoConfiguration),
MatomoRouterModule,
}),
MatomoRouterModule.forRoot(),
],
declarations: [AppComponent],
schemas: [NO_ERRORS_SCHEMA],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Component, LOCALE_ID } from '@angular/core';
import { fakeAsync, flush, TestBed } from '@angular/core/testing';
import { By, DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { provideMatomo } from '../providers';
import { provideTestingTracker } from '../testing/testing-tracker';
import {
ASYNC_INTERNAL_MATOMO_CONFIGURATION,
INTERNAL_MATOMO_CONFIGURATION,
InternalMatomoConfiguration,
MATOMO_CONFIGURATION,
MatomoConfiguration,
} from '../tracker/configuration';
import { MatomoInitializerService } from '../tracker/matomo-initializer.service';
import { MatomoOptOutFormComponent } from './matomo-opt-out-form.component';

@Component({
Expand Down Expand Up @@ -102,18 +101,14 @@ describe('MatomoOptOutFormComponent', () => {
HostWithoutLocaleComponent,
],
providers: [
{
provide: MATOMO_CONFIGURATION,
useValue: { siteId: 1, trackerUrl: 'http://localhost' } as MatomoConfiguration,
},
provideMatomo({ siteId: 1, trackerUrl: 'http://localhost' }),
provideTestingTracker(),
{
provide: LOCALE_ID,
useValue: 'en',
},
],
}).compileComponents();

TestBed.inject(MatomoInitializerService).initialize();
});

it('should create', async () => {
Expand Down
4 changes: 4 additions & 0 deletions projects/ngx-matomo-client/core/private-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ export {
isAutoConfigurationMode as ɵisAutoConfigurationMode,
} from './tracker/configuration';
export { InternalMatomoTracker as ɵInternalMatomoTracker } from './tracker/internal-matomo-tracker.service';
export {
provideTestingTracker as ɵprovideTestingTracker,
MatomoTestingTracker as ɵMatomoTestingTracker,
} from './testing/testing-tracker';
export { createMatomoFeature as ɵcreateMatomoFeature } from './providers';
4 changes: 2 additions & 2 deletions projects/ngx-matomo-client/core/providers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ describe('providers', () => {
const config: MatomoConfiguration = { trackerUrl: 'my-tracker', siteId: 42 };

await setUp([
provideMatomo(config),
{
provide: MatomoInitializerService,
useValue: fakeInitializer,
},
provideMatomo(config),
]);

expect(TestBed.inject(MatomoTracker)).toEqual(jasmine.any(MatomoTracker));
Expand All @@ -40,6 +40,7 @@ describe('providers', () => {
const trackerUrlToken = new InjectionToken<string>('trackerUrl');

await setUp([
provideMatomo(() => ({ trackerUrl: TestBed.inject(trackerUrlToken), siteId: 42 })),
{
provide: MatomoInitializerService,
useValue: fakeInitializer,
Expand All @@ -48,7 +49,6 @@ describe('providers', () => {
provide: trackerUrlToken,
useValue: trackerUrl,
},
provideMatomo(() => ({ trackerUrl: TestBed.inject(trackerUrlToken), siteId: 42 })),
]);

expect(TestBed.inject(MatomoTracker)).toEqual(jasmine.any(MatomoTracker));
Expand Down
43 changes: 41 additions & 2 deletions projects/ngx-matomo-client/core/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,26 @@ import {
makeEnvironmentProviders,
Provider,
} from '@angular/core';
import { MATOMO_CONFIGURATION, MatomoConfiguration } from './tracker/configuration';
import { MatomoInitializerService } from './tracker/matomo-initializer.service';
import {
ASYNC_INTERNAL_MATOMO_CONFIGURATION,
createDeferredInternalMatomoConfiguration,
createInternalMatomoConfiguration,
DEFERRED_INTERNAL_MATOMO_CONFIGURATION,
INTERNAL_MATOMO_CONFIGURATION,
MATOMO_CONFIGURATION,
MatomoConfiguration,
} from './tracker/configuration';
import {
createInternalMatomoTracker,
InternalMatomoTracker,
} from './tracker/internal-matomo-tracker.service';
import {
createMatomoInitializer,
MatomoInitializerService,
} from './tracker/matomo-initializer.service';
import { MatomoTracker } from './tracker/matomo-tracker.service';
import { MATOMO_SCRIPT_FACTORY, MatomoScriptFactory } from './tracker/script-factory';
import { ScriptInjector } from './utils/script-injector';

const PRIVATE_MATOMO_PROVIDERS = Symbol('MATOMO_PROVIDERS');
const PRIVATE_MATOMO_CHECKS = Symbol('MATOMO_CHECKS');
Expand Down Expand Up @@ -74,6 +91,28 @@ export function provideMatomo(
...features: MatomoFeature[]
): EnvironmentProviders {
const providers: Provider[] = [
MatomoTracker,
ScriptInjector,
{
provide: InternalMatomoTracker,
useFactory: createInternalMatomoTracker,
},
{
provide: MatomoInitializerService,
useFactory: createMatomoInitializer,
},
{
provide: INTERNAL_MATOMO_CONFIGURATION,
useFactory: createInternalMatomoConfiguration,
},
{
provide: DEFERRED_INTERNAL_MATOMO_CONFIGURATION,
useFactory: createDeferredInternalMatomoConfiguration,
},
{
provide: ASYNC_INTERNAL_MATOMO_CONFIGURATION,
useFactory: () => inject(DEFERRED_INTERNAL_MATOMO_CONFIGURATION).configuration,
},
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
Expand Down
59 changes: 59 additions & 0 deletions projects/ngx-matomo-client/core/testing/testing-tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ApplicationInitStatus, inject, Injectable, Provider } from '@angular/core';
import {
InternalMatomoTracker,
InternalMatomoTrackerType,
} from '../tracker/internal-matomo-tracker.service';
import { PrefixedType } from '../utils/types';

export function provideTestingTracker(): Provider[] {
return [
MatomoTestingTracker,
{
provide: InternalMatomoTracker,
useExisting: MatomoTestingTracker,
},
];
}

@Injectable()
export class MatomoTestingTracker<MATOMO = unknown, PREFIX extends string = ''>
implements InternalMatomoTrackerType
{
private readonly initStatus = inject(ApplicationInitStatus);

/** Get list of all calls until initialization */
callsOnInit: unknown[][] = [];
/** Get list of all calls after initialization */
callsAfterInit: unknown[][] = [];

/** Get a copy of all calls since application startup */
get calls(): unknown[] {
return [...this.callsOnInit, ...this.callsAfterInit];
}

countCallsAfterInit(command: string): number {
return this.callsAfterInit.filter(call => call[0] === command).length;
}

reset() {
this.callsOnInit = [];
this.callsAfterInit = [];
}

/** Asynchronously call provided method name on matomo tracker instance */
async get<K extends keyof PrefixedType<MATOMO, PREFIX>>(_: K): Promise<never> {
return Promise.reject('MatomoTracker is disabled');
}

push(arg: unknown[]): void {
if (this.initStatus.done) {
this.callsAfterInit.push(arg);
} else {
this.callsOnInit.push(arg);
}
}

async pushFn<T>(_: (matomo: PrefixedType<MATOMO, PREFIX>) => T): Promise<T> {
return Promise.reject('MatomoTracker is disabled');
}
}
90 changes: 41 additions & 49 deletions projects/ngx-matomo-client/core/tracker/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,69 +20,61 @@ export const MATOMO_CONFIGURATION = new InjectionToken<MatomoConfiguration>('MAT
*/
export const INTERNAL_MATOMO_CONFIGURATION = new InjectionToken<InternalMatomoConfiguration>(
'INTERNAL_MATOMO_CONFIGURATION',
{
factory(): InternalMatomoConfiguration {
const { mode, requireConsent, ...restConfig } = requireNonNull(
inject(MATOMO_CONFIGURATION, { optional: true }),
CONFIG_NOT_FOUND,
);

return {
mode: mode ? coerceInitializationMode(mode) : undefined,
disabled: false,
enableLinkTracking: true,
trackAppInitialLoad: !inject(MATOMO_ROUTER_ENABLED),
requireConsent: requireConsent ? coerceConsentRequirement(requireConsent) : 'none',
enableJSErrorTracking: false,
runOutsideAngularZone: false,
disableCampaignParameters: false,
acceptDoNotTrack: false,
...restConfig,
};
},
},
);

export function createInternalMatomoConfiguration(): InternalMatomoConfiguration {
const { mode, requireConsent, ...restConfig } = requireNonNull(
inject(MATOMO_CONFIGURATION, { optional: true }),
CONFIG_NOT_FOUND,
);

return {
mode: mode ? coerceInitializationMode(mode) : undefined,
disabled: false,
enableLinkTracking: true,
trackAppInitialLoad: !inject(MATOMO_ROUTER_ENABLED),
requireConsent: requireConsent ? coerceConsentRequirement(requireConsent) : 'none',
enableJSErrorTracking: false,
runOutsideAngularZone: false,
disableCampaignParameters: false,
acceptDoNotTrack: false,
...restConfig,
};
}

/**
* For internal use only. Injection token for deferred {@link InternalMatomoConfiguration}.
*
*/
export const DEFERRED_INTERNAL_MATOMO_CONFIGURATION =
new InjectionToken<DeferredInternalMatomoConfiguration>(
'DEFERRED_INTERNAL_MATOMO_CONFIGURATION',
{
factory: () => {
const base = inject(INTERNAL_MATOMO_CONFIGURATION);
let resolveFn: ((configuration: InternalMatomoConfiguration) => void) | undefined;
const configuration = new Promise<InternalMatomoConfiguration>(
resolve => (resolveFn = resolve),
);

return {
configuration,
markReady(configuration) {
requireNonNull(
resolveFn,
'resolveFn',
)({
...base,
...configuration,
} as InternalMatomoConfiguration);
},
};
},
new InjectionToken<DeferredInternalMatomoConfiguration>('DEFERRED_INTERNAL_MATOMO_CONFIGURATION');

export function createDeferredInternalMatomoConfiguration(): DeferredInternalMatomoConfiguration {
const base = inject(INTERNAL_MATOMO_CONFIGURATION);
let resolveFn: ((configuration: InternalMatomoConfiguration) => void) | undefined;
const configuration = new Promise<InternalMatomoConfiguration>(resolve => (resolveFn = resolve));

return {
configuration,
markReady(configuration) {
requireNonNull(
resolveFn,
'resolveFn',
)({
...base,
...configuration,
});
},
);
};
}

/**
* For internal use only. Injection token for fully loaded async {@link InternalMatomoConfiguration}.
*
*/
export const ASYNC_INTERNAL_MATOMO_CONFIGURATION = new InjectionToken<
Promise<InternalMatomoConfiguration>
>('ASYNC_INTERNAL_MATOMO_CONFIGURATION', {
factory: () => inject(DEFERRED_INTERNAL_MATOMO_CONFIGURATION).configuration,
});
>('ASYNC_INTERNAL_MATOMO_CONFIGURATION');

/**
* For internal use only. Module configuration merged with default values.
Expand All @@ -97,7 +89,7 @@ export type InternalMatomoConfiguration = Omit<MatomoConfiguration, 'mode' | 're
export interface DeferredInternalMatomoConfiguration {
readonly configuration: Promise<InternalMatomoConfiguration>;

markReady(configuration: AutoMatomoConfiguration<'auto' | 'deferred'>): void;
markReady(configuration: InternalMatomoConfiguration): void;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function trimTrailingUndefinedElements<T>(array: T[]): T[] {
return trimmed;
}

type InternalMatomoTrackerType = Pick<
export type InternalMatomoTrackerType = Pick<
InternalMatomoTracker<unknown, string>,
'get' | 'push' | 'pushFn'
>;
Expand All @@ -30,10 +30,7 @@ export function createInternalMatomoTracker(): InternalMatomoTrackerType {
return disabled || !isBrowser ? new NoopMatomoTracker() : new InternalMatomoTracker();
}

@Injectable({
providedIn: 'root',
useFactory: createInternalMatomoTracker,
})
@Injectable()
export class InternalMatomoTracker<MATOMO, PREFIX extends string = ''> {
private readonly ngZone = inject(NgZone);
private readonly config = inject(INTERNAL_MATOMO_CONFIGURATION);
Expand Down Expand Up @@ -70,6 +67,7 @@ export class InternalMatomoTracker<MATOMO, PREFIX extends string = ''> {
}
}

@Injectable()
export class NoopMatomoTracker<MATOMO = unknown, PREFIX extends string = ''>
implements InternalMatomoTrackerType
{
Expand Down
Loading

0 comments on commit 2982fc4

Please sign in to comment.