UNPKG

ngx-matomo-client

Version:

Matomo (fka. Piwik) client for Angular applications

498 lines (483 loc) 20.9 kB
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