ngx-matomo-client
Version:
Matomo (fka. Piwik) client for Angular applications
498 lines (483 loc) • 20.9 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, runInInjectionContext, INJECTOR, Injectable, PLATFORM_ID, Inject, Optional, ENVIRONMENT_INITIALIZER, NgModule } from '@angular/core';
import * as i2 from 'ngx-matomo-client/core';
import { ɵINTERNAL_MATOMO_CONFIGURATION as _INTERNAL_MATOMO_CONFIGURATION, MatomoTracker, ɵrunOnce as _runOnce, ɵcreateMatomoFeature as _createMatomoFeature, ɵMATOMO_ROUTER_ENABLED as _MATOMO_ROUTER_ENABLED } from 'ngx-matomo-client/core';
import * as i1$1 from '@angular/router';
import { Router, PRIMARY_OUTLET, NavigationEnd } from '@angular/router';
import { APP_BASE_HREF, LocationStrategy, isPlatformBrowser } from '@angular/common';
import { of, identity, forkJoin, from, combineLatest } from 'rxjs';
import { delay, filter, distinctUntilChanged, switchMap, map, concatMap, tap, take, defaultIfEmpty, mapTo } from 'rxjs/operators';
import * as i1 from '@angular/platform-browser';
const MATOMO_ROUTER_CONFIGURATION = new InjectionToken('MATOMO_ROUTER_CONFIGURATION');
const DEFAULT_ROUTER_CONFIGURATION = {
prependBaseHref: true,
trackPageTitle: true,
delay: 0,
exclude: [],
navigationEndComparator: 'fullUrl',
};
const INTERNAL_ROUTER_CONFIGURATION = new InjectionToken('INTERNAL_ROUTER_CONFIGURATION');
function createInternalRouterConfiguration() {
const { disabled, enableLinkTracking } = inject(_INTERNAL_MATOMO_CONFIGURATION);
const routerConfig = inject(MATOMO_ROUTER_CONFIGURATION, { optional: true }) || {};
return { ...DEFAULT_ROUTER_CONFIGURATION, ...routerConfig, enableLinkTracking, disabled };
}
const MATOMO_ROUTER_INTERCEPTORS = new InjectionToken('MATOMO_ROUTER_INTERCEPTORS');
/**
* This is not an ideal implementation, because there exist no easy way to differentiate between a class constructor and a function.
*/
function isInterceptorFn(interceptor) {
return typeof interceptor.prototype?.beforePageTrack !== 'function';
}
class InterceptorFnAdapter {
constructor(fn, injector) {
this.fn = fn;
this.injector = injector;
}
beforePageTrack(event) {
return runInInjectionContext(this.injector, () => this.fn(event));
}
}
function provideInterceptor(typeOrFn) {
if (isInterceptorFn(typeOrFn)) {
return {
provide: MATOMO_ROUTER_INTERCEPTORS,
multi: true,
useFactory: () => new InterceptorFnAdapter(typeOrFn, inject(INJECTOR)),
};
}
else {
return {
provide: MATOMO_ROUTER_INTERCEPTORS,
multi: true,
useClass: typeOrFn,
};
}
}
function provideInterceptors(types) {
if (!types) {
return [];
}
return types.map(provideInterceptor);
}
function findChildRoute(route, outlet) {
return route.children.find(child => child.outlet === outlet);
}
function getLeafRoute(route, outlet) {
const child = findChildRoute(route, outlet);
return child ? getLeafRoute(child, outlet) : route;
}
/**
* Simple interceptor base looking into route's data for tracking
*
* @see MatomoRouteDataInterceptor
*/
class MatomoRouteInterceptorBase {
constructor() {
this.router = inject(Router);
}
beforePageTrack(event) {
const route = this.getRoute(event);
const data = this.extractRouteData(route);
return this.processRouteData(data);
}
getRoute(_) {
return getLeafRoute(this.router.routerState.snapshot.root, PRIMARY_OUTLET);
}
}
const DEFAULT_DATA_KEY = 'matomo';
/** Token to define the route's data key to be looked-up by `MatomoRouteDataInterceptor` */
const MATOMO_ROUTE_DATA_KEY = new InjectionToken('MATOMO_ROUTE_DATA_KEY', {
providedIn: 'root',
factory: () => DEFAULT_DATA_KEY,
});
/**
* Simple interceptor looking at 'matomo' key of route's data for tracking.
*
* It is possible to extend this class or {@link MatomoRouteInterceptorBase}
* for custom behavior (to use another data key, etc.)
*
* @example
* // Using provided MatomoRouteDataInterceptor (looks into 'matomo' data key)
* const routes: Routes = [
* {
* path: '/hello',
* component: HelloComponent,
* data: {
* matomo: {
* title: 'Page title',
* } as MatomoRouteData
* }
* },
* ];
*
* NgxMatomoRouterModule.forRoot({
* interceptors: [MatomoRouteDataInterceptor],
* }),
*
* @example
* // Using custom 'myCustomAnalyticsKey' data key
* const routes: Routes = [
* {
* path: '/hello',
* component: HelloComponent,
* data: {
* myCustomAnalyticsKey: {
* title: 'Page title',
* } as MatomoRouteData
* }
* },
* ];
*
* @Injectable()
* export class MyCustomInterceptor extends MatomoRouteDataInterceptor {
* readonly dataKey = 'myCustomAnalyticsKey';
* }
*
* NgxMatomoRouterModule.forRoot({
* interceptors: [MyCustomInterceptor],
* }),
*
* @see MatomoRouteInterceptorBase
* @see MatomoRouteData
*/
class MatomoRouteDataInterceptor extends MatomoRouteInterceptorBase {
constructor() {
super(...arguments);
this.tracker = inject(MatomoTracker);
this.dataKey = inject(MATOMO_ROUTE_DATA_KEY);
}
extractRouteData(route) {
return route.data[this.dataKey];
}
processRouteData(data) {
if (!data) {
return;
}
if (data.title) {
this.tracker.setDocumentTitle(data.title);
}
if (data.ecommerce) {
this.tracker.setEcommerceView(data.ecommerce);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouteDataInterceptor, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouteDataInterceptor }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouteDataInterceptor, decorators: [{
type: Injectable
}] });
const ROUTER_ALREADY_INITIALIZED_ERROR = 'MatomoRouter has already been initialized';
function invalidInterceptorsProviderError() {
return new Error('An invalid MATOMO_ROUTER_INTERCEPTORS provider was configured. Did you forget to set "multi: true" ?');
}
/**
* @deprecated Use an interceptor calling `setDocumentTitle()` instead
* @see MatomoRouterInterceptor
* @see MATOMO_ROUTER_INTERCEPTORS
*/
const MATOMO_PAGE_TITLE_PROVIDER = new InjectionToken('MATOMO_PAGE_TITLE_PROVIDER');
class DefaultPageTitleProvider {
constructor(title) {
this.title = title;
}
getCurrentPageTitle(_) {
return of(this.title.getTitle());
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DefaultPageTitleProvider, deps: [{ token: i1.Title }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DefaultPageTitleProvider }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: DefaultPageTitleProvider, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.Title }] });
const MATOMO_PAGE_URL_PROVIDER = new InjectionToken('MATOMO_PAGE_URL_PROVIDER');
function createDefaultPageUrlProvider() {
return new DefaultPageUrlProvider(inject(INTERNAL_ROUTER_CONFIGURATION), inject(APP_BASE_HREF, { optional: true }), inject(LocationStrategy));
}
function isPageUrlProviderFn(type) {
return typeof type.prototype?.getCurrentPageUrl !== 'function';
}
class PageUrlProviderFnAdapter {
constructor(fn, injector) {
this.fn = fn;
this.injector = injector;
}
getCurrentPageUrl(event) {
return runInInjectionContext(this.injector, () => this.fn(event));
}
}
function providePageUrlProvider(typeOrFn) {
if (isPageUrlProviderFn(typeOrFn)) {
return {
provide: MATOMO_PAGE_URL_PROVIDER,
useFactory: () => new PageUrlProviderFnAdapter(typeOrFn, inject(INJECTOR)),
};
}
else {
return {
provide: MATOMO_PAGE_URL_PROVIDER,
useClass: typeOrFn,
};
}
}
function trimTrailingSlash(str) {
return str.endsWith('/') ? str.slice(0, -1) : str;
}
class DefaultPageUrlProvider {
constructor(config, baseHref, locationStrategy) {
this.config = config;
this.baseHref = baseHref;
this.locationStrategy = locationStrategy;
}
getCurrentPageUrl(event) {
const url = this.config.prependBaseHref
? this.getBaseHrefWithoutTrailingSlash() + event.urlAfterRedirects
: event.urlAfterRedirects;
return of(url);
}
getBaseHrefWithoutTrailingSlash() {
return trimTrailingSlash(this.baseHref ?? this.locationStrategy.getBaseHref() ?? '');
}
}
function isNavigationEnd(event) {
return event instanceof NavigationEnd;
}
function coerceRegExp(input) {
return typeof input === 'string' ? new RegExp(input) : input;
}
function coerceRegExpArray(input) {
if (!input) {
return [];
}
return Array.isArray(input) ? input.map(coerceRegExp) : [coerceRegExp(input)];
}
function isNotExcluded(excludeConfig) {
const exclusions = coerceRegExpArray(excludeConfig);
return (event) => !exclusions.some(rx => event.urlAfterRedirects.match(rx));
}
function stripQueryParams(url) {
return url.split('?')[0];
}
function defaultNavigationEndComparator(urlExtractor) {
return (eventA, eventB) => urlExtractor(eventA) === urlExtractor(eventB);
}
function getNavigationEndComparator(config) {
switch (config.navigationEndComparator) {
case 'fullUrl':
return defaultNavigationEndComparator(event => event.urlAfterRedirects);
case 'ignoreQueryParams':
return defaultNavigationEndComparator(event => stripQueryParams(event.urlAfterRedirects));
default:
return config.navigationEndComparator;
}
}
class MatomoRouter {
constructor(router, platformId, config, pageTitleProvider, pageUrlProvider, tracker, interceptors) {
this.router = router;
this.platformId = platformId;
this.config = config;
this.pageTitleProvider = pageTitleProvider;
this.pageUrlProvider = pageUrlProvider;
this.tracker = tracker;
this.interceptors = interceptors;
this.initialize = _runOnce(() => {
if (this.config.disabled || !isPlatformBrowser(this.platformId)) {
// Do not set-up router if globally disabled or running on server
return;
}
const delayOp = this.config.delay === -1 ? identity : delay(this.config.delay);
const navigationEndComparator = getNavigationEndComparator(this.config);
this.router.events
.pipe(
// Take only NavigationEnd events
filter(isNavigationEnd),
// Filter out excluded urls
filter(isNotExcluded(this.config.exclude)),
// Filter out NavigationEnd events to ignore, e.g. when url does not actually change (component reload)
distinctUntilChanged(navigationEndComparator),
// Optionally add some delay
delayOp,
// Set default page title & url
switchMap(event => this.presetPageTitleAndUrl(event).pipe(map(({ pageUrl }) => ({ pageUrl, event })))),
// Run interceptors then track page view
concatMap(({ event, pageUrl }) => this.callInterceptors(event).pipe(tap(() => this.trackPageView(pageUrl)))))
.subscribe();
}, ROUTER_ALREADY_INITIALIZED_ERROR);
if (interceptors && !Array.isArray(interceptors)) {
throw invalidInterceptorsProviderError();
}
}
/** @deprecated use {@link initialize initialize()} instead */
init() {
this.initialize();
}
callInterceptors(event) {
if (this.interceptors) {
return forkJoin(this.interceptors.map(interceptor => {
const result = interceptor.beforePageTrack(event);
const result$ = result == null ? of(undefined) : from(result);
// Must not be an empty observable (otherwise forkJoin would complete without waiting others)
return result$.pipe(take(1), defaultIfEmpty(undefined));
})).pipe(mapTo(undefined), defaultIfEmpty(undefined));
}
else {
return of(undefined);
}
}
presetPageTitleAndUrl(event) {
const title$ = this.config.trackPageTitle
? this.pageTitleProvider
.getCurrentPageTitle(event)
.pipe(tap(pageTitle => this.tracker.setDocumentTitle(pageTitle)))
: of(undefined);
const url$ = from(this.pageUrlProvider.getCurrentPageUrl(event)).pipe(tap(pageUrl => this.tracker.setCustomUrl(pageUrl)));
return combineLatest([title$, url$]).pipe(map(([_, pageUrl]) => ({ pageUrl })));
}
trackPageView(pageUrl) {
this.tracker.trackPageView();
if (this.config.enableLinkTracking) {
this.tracker.enableLinkTracking(this.config.enableLinkTracking === 'enable-pseudo');
}
// Set referrer for next page view
this.tracker.setReferrerUrl(pageUrl);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouter, deps: [{ token: i1$1.Router }, { token: PLATFORM_ID }, { token: INTERNAL_ROUTER_CONFIGURATION }, { token: MATOMO_PAGE_TITLE_PROVIDER }, { token: MATOMO_PAGE_URL_PROVIDER }, { token: i2.MatomoTracker }, { token: MATOMO_ROUTER_INTERCEPTORS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouter }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouter, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1$1.Router }, { type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [INTERNAL_ROUTER_CONFIGURATION]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [MATOMO_PAGE_TITLE_PROVIDER]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [MATOMO_PAGE_URL_PROVIDER]
}] }, { type: i2.MatomoTracker }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [MATOMO_ROUTER_INTERCEPTORS]
}] }] });
/** Enable automatic page views tracking */
function withRouter(config) {
const providers = buildInternalRouterProviders(config);
return _createMatomoFeature("Router" /* RouterMatomoFeatureKind.Router */, providers);
}
function buildInternalRouterProviders(config) {
return [
MatomoRouter,
{ provide: _MATOMO_ROUTER_ENABLED, useValue: true },
{ provide: MATOMO_ROUTER_CONFIGURATION, useValue: config },
{
provide: INTERNAL_ROUTER_CONFIGURATION,
useFactory: createInternalRouterConfiguration,
},
{
provide: MATOMO_PAGE_URL_PROVIDER,
useFactory: createDefaultPageUrlProvider,
},
{
provide: MATOMO_PAGE_TITLE_PROVIDER,
useClass: DefaultPageTitleProvider,
},
MatomoRouter,
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue() {
inject(MatomoRouter).initialize();
},
},
];
}
function requireRouterFeature(featureName) {
return (featuresKind) => {
if (!featuresKind.includes("Router" /* RouterMatomoFeatureKind.Router */)) {
throw new Error(`Matomo feature ${featureName} cannot be used without router feature! Did you forget to call withRouter()?`);
}
};
}
/** Add some matomo router interceptors */
function withRouterInterceptors(interceptors) {
return _createMatomoFeature("RouterInterceptors" /* RouterMatomoFeatureKind.RouterInterceptors */, provideInterceptors(interceptors), requireRouterFeature('withRouterInterceptors()'));
}
/**
* Enable retrieval of tracking information from route data
*
* @see MatomoRouteData
* @param key A custom key to get lookup route data - default is 'matomo'
*/
function withRouteData(key) {
const providers = [provideInterceptor(MatomoRouteDataInterceptor)];
if (key) {
providers.push({ provide: MATOMO_ROUTE_DATA_KEY, useValue: key });
}
return _createMatomoFeature("BuiltInRouteDataInterceptor" /* RouterMatomoFeatureKind.BuiltInRouteDataInterceptor */, providers, requireRouterFeature('withRouteData()'));
}
function withPageUrlProvider(provider) {
return _createMatomoFeature("PageUrlProvider" /* RouterMatomoFeatureKind.PageUrlProvider */, [providePageUrlProvider(provider)], requireRouterFeature('withPageUrlProvider()'));
}
class MatomoRouterModule {
static forRoot(configWithInterceptors = {}) {
// Note: not using "rest" syntax here, in order to avoid any dependency on tslib (and reduce package size)
// The only drawback of this is that MATOMO_ROUTER_CONFIGURATION will actually
// contain a reference to provided interceptors
return {
ngModule: MatomoRouterModule,
providers: [
buildInternalRouterProviders(configWithInterceptors),
provideInterceptors(configWithInterceptors.interceptors),
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouterModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouterModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouterModule, providers: [{ provide: _MATOMO_ROUTER_ENABLED, useValue: true }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: MatomoRouterModule, decorators: [{
type: NgModule,
args: [{
providers: [{ provide: _MATOMO_ROUTER_ENABLED, useValue: true }],
}]
}] });
/**
* @deprecated use MatomoRouterModule instead
* @breaking-change 8.0.0
*/
class NgxMatomoRouterModule {
static forRoot(configWithInterceptors = {}) {
// Note: not using "rest" syntax here, in order to avoid any dependency on tslib (and reduce package size)
// The only drawback of this is that MATOMO_ROUTER_CONFIGURATION will actually
// contain a reference to provided interceptors
return {
ngModule: NgxMatomoRouterModule,
providers: [
buildInternalRouterProviders(configWithInterceptors),
provideInterceptors(configWithInterceptors.interceptors),
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: NgxMatomoRouterModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: NgxMatomoRouterModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: NgxMatomoRouterModule, providers: [{ provide: _MATOMO_ROUTER_ENABLED, useValue: true }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: NgxMatomoRouterModule, decorators: [{
type: NgModule,
args: [{
providers: [{ provide: _MATOMO_ROUTER_ENABLED, useValue: true }],
}]
}] });
/*
* Public API Surface of tracker
*/
/**
* Generated bundle index. Do not edit.
*/
export { MATOMO_PAGE_TITLE_PROVIDER, MATOMO_PAGE_URL_PROVIDER, MATOMO_ROUTER_CONFIGURATION, MATOMO_ROUTER_INTERCEPTORS, MATOMO_ROUTE_DATA_KEY, MatomoRouteDataInterceptor, MatomoRouteInterceptorBase, MatomoRouterModule, NgxMatomoRouterModule, provideInterceptor, provideInterceptors, providePageUrlProvider, withPageUrlProvider, withRouteData, withRouter, withRouterInterceptors };
//# sourceMappingURL=ngx-matomo-client-router.mjs.map