diff --git a/projects/ngx-matomo-client/router/configuration.ts b/projects/ngx-matomo-client/router/configuration.ts index 1f49e8e..ae2df62 100644 --- a/projects/ngx-matomo-client/router/configuration.ts +++ b/projects/ngx-matomo-client/router/configuration.ts @@ -1,4 +1,5 @@ import { inject, InjectionToken, Type } from '@angular/core'; +import { NavigationEnd } from '@angular/router'; import { INTERNAL_MATOMO_CONFIGURATION, InternalMatomoConfiguration } from 'ngx-matomo-client/core'; import { MatomoRouterInterceptor } from './interceptor'; @@ -7,6 +8,10 @@ export const MATOMO_ROUTER_CONFIGURATION = new InjectionToken boolean; export interface MatomoRouterConfiguration { /** @@ -43,6 +48,27 @@ export interface MatomoRouterConfiguration { * Optional, default is no url excluded */ exclude?: ExclusionConfig; + + /** + * Custom url comparator to detect url change between Angular route navigations. + * + * This may be useful, because by default all `NavigationEnd` events will trigger a page track and this may happen + * after query params change only (without url actually changing). + * + * You can define a custom comparator here to compare url by ignoring query params. + * + * Note: this is different from providing the url sent to Matomo for actual tracking. The url sent to Matomo will be + * the full page url, including any base href, and is configured using a {@link PageUrlProvider} (see + * `MATOMO_PAGE_URL_PROVIDER` token). + * + * Optional, default is to compare `NavigationEnd.urlAfterRedirects` + * + * Possible values: + * - `'fullUrl'` (or undefined): default value, compare using `NavigationEnd.urlAfterRedirects` + * - `'ignoreQueryParams'`: compare using `NavigationEnd.urlAfterRedirects` but ignoring query params + * - `NavigationEndComparator`: compare using a custom `NavigationEndComparator` function + */ + navigationEndComparator?: NavigationEndComparator | 'ignoreQueryParams' | 'fullUrl'; } export interface MatomoRouterConfigurationWithInterceptors extends MatomoRouterConfiguration { @@ -60,6 +86,7 @@ export const DEFAULT_ROUTER_CONFIGURATION: Required = trackPageTitle: true, delay: 0, exclude: [], + navigationEndComparator: 'fullUrl', }; export type InternalGlobalConfiguration = Pick< diff --git a/projects/ngx-matomo-client/router/matomo-router.service.spec.ts b/projects/ngx-matomo-client/router/matomo-router.service.spec.ts index 176c3d5..972898e 100644 --- a/projects/ngx-matomo-client/router/matomo-router.service.spec.ts +++ b/projects/ngx-matomo-client/router/matomo-router.service.spec.ts @@ -7,6 +7,7 @@ import { InternalGlobalConfiguration, MATOMO_ROUTER_CONFIGURATION, MatomoRouterConfiguration, + NavigationEndComparator, } from './configuration'; import { invalidInterceptorsProviderError } from './errors'; import { MATOMO_ROUTER_INTERCEPTORS, MatomoRouterInterceptor } from './interceptor'; @@ -295,9 +296,32 @@ describe('MatomoRouter', () => { expect(tracker.setReferrerUrl).not.toHaveBeenCalled(); })); + it('should track page view if navigated to the same url with different query params', fakeAsync(() => { + // Given + const service = instantiate( + { + navigationEndComparator: 'fullUrl', + }, + { enableLinkTracking: false }, + ); + const tracker = TestBed.inject(MatomoTracker) as jasmine.SpyObj; + + // When + service.initialize(); + triggerEvent('/test'); + triggerEvent('/test?page=1'); + tick(); // Tracking is asynchronous by default + + // Then + expect(tracker.trackPageView).toHaveBeenCalledTimes(2); + })); + it('should not track page view if navigated to the same url with query params', fakeAsync(() => { // Given - const service = instantiate({}, { enableLinkTracking: false }); + const service = instantiate( + { navigationEndComparator: 'ignoreQueryParams' }, + { enableLinkTracking: false }, + ); const tracker = TestBed.inject(MatomoTracker) as jasmine.SpyObj; // When @@ -310,6 +334,42 @@ describe('MatomoRouter', () => { expect(tracker.trackPageView).toHaveBeenCalledTimes(1); })); + it('should not track page view if navigated to the "same" url, as configured from custom NavigationEndComparator', fakeAsync(() => { + // Given + const isEvenPageParam = (url: string) => { + const params = new URL(url, 'http://localhost').searchParams; + const page = Number(params.get('page') ?? 0); + + return page % 2 === 0; + }; + const myCustomComparator: NavigationEndComparator = ( + previousNavigationEnd, + currentNavigationEnd, + ) => { + return ( + isEvenPageParam(previousNavigationEnd.urlAfterRedirects) === + isEvenPageParam(currentNavigationEnd.urlAfterRedirects) + ); + }; + const service = instantiate( + { + navigationEndComparator: myCustomComparator, + }, + { enableLinkTracking: false }, + ); + const tracker = TestBed.inject(MatomoTracker) as jasmine.SpyObj; + + // When + service.initialize(); + triggerEvent('/test?page=1'); + triggerEvent('/test?page=2'); + triggerEvent('/test?page=4'); + tick(); // Tracking is asynchronous by default + + // Then + expect(tracker.trackPageView).toHaveBeenCalledTimes(2); + })); + it('should call interceptors if any and wait for them to resolve', fakeAsync(() => { // Given const interceptor1 = jasmine.createSpyObj('interceptor1', [ diff --git a/projects/ngx-matomo-client/router/matomo-router.service.ts b/projects/ngx-matomo-client/router/matomo-router.service.ts index 12720e3..b87f005 100644 --- a/projects/ngx-matomo-client/router/matomo-router.service.ts +++ b/projects/ngx-matomo-client/router/matomo-router.service.ts @@ -26,6 +26,7 @@ import { ExclusionConfig, INTERNAL_ROUTER_CONFIGURATION, InternalRouterConfiguration, + NavigationEndComparator, } from './configuration'; import { invalidInterceptorsProviderError, ROUTER_ALREADY_INITIALIZED_ERROR } from './errors'; import { MATOMO_ROUTER_INTERCEPTORS, MatomoRouterInterceptor } from './interceptor'; @@ -54,8 +55,24 @@ function isNotExcluded(excludeConfig: ExclusionConfig): (event: NavigationEnd) = return (event: NavigationEnd) => !exclusions.some(rx => event.urlAfterRedirects.match(rx)); } -function pathName(event: NavigationEnd) { - return event.urlAfterRedirects.split('?')[0]; +function stripQueryParams(url: string): string { + return url.split('?')[0]; +} + +function defaultNavigationEndComparator(urlExtractor: (event: NavigationEnd) => string) { + return (eventA: NavigationEnd, eventB: NavigationEnd) => + urlExtractor(eventA) === urlExtractor(eventB); +} + +function getNavigationEndComparator(config: InternalRouterConfiguration): NavigationEndComparator { + switch (config.navigationEndComparator) { + case 'fullUrl': + return defaultNavigationEndComparator(event => event.urlAfterRedirects); + case 'ignoreQueryParams': + return defaultNavigationEndComparator(event => stripQueryParams(event.urlAfterRedirects)); + default: + return config.navigationEndComparator; + } } @Injectable({ providedIn: 'root' }) @@ -91,6 +108,7 @@ export class MatomoRouter { const delayOp: MonoTypeOperatorFunction = this.config.delay === -1 ? identity : delay(this.config.delay); + const navigationEndComparator = getNavigationEndComparator(this.config); this.router.events .pipe( @@ -98,8 +116,8 @@ export class MatomoRouter { filter(isNavigationEnd), // Filter out excluded urls filter(isNotExcluded(this.config.exclude)), - // Query param changes also trigger isNavigationEnd events filtering out those - distinctUntilChanged((prevEvent, currEvent) => pathName(prevEvent) === pathName(currEvent)), + // 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 diff --git a/projects/ngx-matomo-client/router/public-api.ts b/projects/ngx-matomo-client/router/public-api.ts index b70b15f..92145a9 100644 --- a/projects/ngx-matomo-client/router/public-api.ts +++ b/projects/ngx-matomo-client/router/public-api.ts @@ -9,6 +9,7 @@ export { MATOMO_ROUTER_CONFIGURATION, ExclusionConfig, MatomoRouterConfigurationWithInterceptors, + NavigationEndComparator, } from './configuration'; export { PageTitleProvider, MATOMO_PAGE_TITLE_PROVIDER } from './page-title-providers'; export { PageUrlProvider, MATOMO_PAGE_URL_PROVIDER } from './page-url-provider';