Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(router): subsequent navigation to the same url should not trigger… #89

Merged
merged 2 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
75 changes: 75 additions & 0 deletions 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,6 +296,80 @@ 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(
{ navigationEndComparator: 'ignoreQueryParams' },
{ 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(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
28 changes: 25 additions & 3 deletions projects/ngx-matomo-client/router/matomo-router.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
concatMap,
defaultIfEmpty,
delay,
distinctUntilKeyChanged,
distinctUntilChanged,
filter,
map,
mapTo,
Expand All @@ -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,6 +55,26 @@ function isNotExcluded(excludeConfig: ExclusionConfig): (event: NavigationEnd) =
return (event: NavigationEnd) => !exclusions.some(rx => event.urlAfterRedirects.match(rx));
}

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' })
export class MatomoRouter {
constructor(
Expand Down Expand Up @@ -87,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)),
// Distinct urls
distinctUntilKeyChanged('urlAfterRedirects'),
// 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
Loading