Skip to content

Commit

Permalink
feat(router): allow to ignore subsequent navigation to the same url a…
Browse files Browse the repository at this point in the history
…ccording to custom url comparator

EmmanuelRoux#72
  • Loading branch information
EmmanuelRoux committed May 28, 2024
1 parent 5737ce6 commit b636f78
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 5 deletions.
27 changes: 27 additions & 0 deletions projects/ngx-matomo-client/router/configuration.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -7,6 +8,10 @@ export const MATOMO_ROUTER_CONFIGURATION = new InjectionToken<MatomoRouterConfig
);

export type ExclusionConfig = string | RegExp | (string | RegExp)[];
export type NavigationEndComparator = (
previousNavigationEnd: NavigationEnd,
currentNavigationEnd: NavigationEnd,
) => boolean;

export interface MatomoRouterConfiguration {
/**
Expand Down Expand Up @@ -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 {
Expand All @@ -60,6 +86,7 @@ export const DEFAULT_ROUTER_CONFIGURATION: Required<MatomoRouterConfiguration> =
trackPageTitle: true,
delay: 0,
exclude: [],
navigationEndComparator: 'fullUrl',
};

export type InternalGlobalConfiguration = Pick<
Expand Down
62 changes: 61 additions & 1 deletion projects/ngx-matomo-client/router/matomo-router.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
InternalGlobalConfiguration,
MATOMO_ROUTER_CONFIGURATION,
MatomoRouterConfiguration,
NavigationEndComparator,
} from './configuration';
import { invalidInterceptorsProviderError } from './errors';
import { MATOMO_ROUTER_INTERCEPTORS, MatomoRouterInterceptor } from './interceptor';
Expand Down Expand Up @@ -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<MatomoTracker>;

// 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<MatomoTracker>;

// When
Expand All @@ -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<MatomoTracker>;

// 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<MatomoRouterInterceptor>('interceptor1', [
Expand Down
26 changes: 22 additions & 4 deletions projects/ngx-matomo-client/router/matomo-router.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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' })
Expand Down Expand Up @@ -91,15 +108,16 @@ export class MatomoRouter {

const delayOp: MonoTypeOperatorFunction<NavigationEnd> =
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)),
// 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
Expand Down
1 change: 1 addition & 0 deletions projects/ngx-matomo-client/router/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit b636f78

Please sign in to comment.