Skip to content

Commit 2ff6d50

Browse files
authored
Fix duplicate page navigation tracking in Application Insights (#773)
### Summary & Motivation Fix duplicate page view tracking that occurred when navigating between pages in the single-page application. Application Insights' enableAutoRouteTracking feature was causing duplicate tracking with TanStack Router because both systems independently detected navigation events. - Disable enableAutoRouteTracking in Application Insights configuration - Create custom PageTracker component that integrates properly with TanStack Router - Track initial page load and subsequent navigations without duplicates - Maintain existing JavaScript error tracking functionality The enableAutoRouteTracking feature is designed for React Router and listens to browser history changes (popstate, pushState/replaceState, hashchange). Since TanStack Router also manipulates browser history, both were detecting the same navigation event and sending duplicate page views. The custom PageTracker component uses TanStack Router's onLoad event and tracks pathname changes to ensure each navigation is tracked exactly once. ### Downstream projects 1. Add the PageTracker component to `your-self-contained-system/__root.tsx`: ```diff import { queryClient } from "@/shared/lib/api/client"; +import { PageTracker } from "@repo/infrastructure/applicationInsights/PageTracker"; import { AuthenticationProvider } from "@repo/infrastructure/auth/AuthenticationProvider"; function Root() { return ( <QueryClientProvider client={queryClient}> <ThemeModeProvider> <ReactAriaRouterProvider> <AuthenticationProvider navigate={(options) => navigate(options)}> + <PageTracker /> <Outlet /> </AuthenticationProvider> </ReactAriaRouterProvider> </ThemeModeProvider> </QueryClientProvider> ); } ``` ### Checklist - [x] I have added tests, or done manual regression tests - [x] I have updated the documentation, if necessary
2 parents eacb0ff + f80c2e0 commit 2ff6d50

File tree

6 files changed

+68
-8
lines changed

6 files changed

+68
-8
lines changed

application/account-management/WebApp/routes/__root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { queryClient } from "@/shared/lib/api/client";
2+
import { PageTracker } from "@repo/infrastructure/applicationInsights/PageTracker";
23
import { AuthenticationProvider } from "@repo/infrastructure/auth/AuthenticationProvider";
34
import { ErrorPage } from "@repo/infrastructure/errorComponents/ErrorPage";
45
import { NotFound } from "@repo/infrastructure/errorComponents/NotFoundPage";
@@ -23,6 +24,7 @@ function Root() {
2324
<ThemeModeProvider>
2425
<ReactAriaRouterProvider>
2526
<AuthenticationProvider navigate={(options) => navigate(options)}>
27+
<PageTracker />
2628
<Outlet />
2729
</AuthenticationProvider>
2830
</ReactAriaRouterProvider>

application/back-office/WebApp/routes/__root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { queryClient } from "@/shared/lib/api/client";
2+
import { PageTracker } from "@repo/infrastructure/applicationInsights/PageTracker";
23
import { AuthenticationProvider } from "@repo/infrastructure/auth/AuthenticationProvider";
34
import { ErrorPage } from "@repo/infrastructure/errorComponents/ErrorPage";
45
import { NotFound } from "@repo/infrastructure/errorComponents/NotFoundPage";
@@ -23,6 +24,7 @@ function Root() {
2324
<ThemeModeProvider>
2425
<ReactAriaRouterProvider>
2526
<AuthenticationProvider navigate={(options) => navigate(options)}>
27+
<PageTracker />
2628
<Outlet />
2729
</AuthenticationProvider>
2830
</ReactAriaRouterProvider>

application/shared-kernel/SharedKernel/Endpoints/TrackEndpoints.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ ILogger<string> logger
4141
var telemetry = new PageViewTelemetry
4242
{
4343
Name = trackRequest.Data.BaseData.Name,
44-
Url = new Uri(trackRequest.Data.BaseData.Url),
44+
Url = Uri.TryCreate(trackRequest.Data.BaseData.Url, UriKind.Absolute, out var pageViewUri) ? pageViewUri : null,
4545
Duration = trackRequest.Data.BaseData.Duration,
4646
Timestamp = trackRequest.Time,
4747
Id = trackRequest.Data.BaseData.Id
@@ -59,7 +59,7 @@ ILogger<string> logger
5959
var telemetry = new PageViewPerformanceTelemetry
6060
{
6161
Name = trackRequest.Data.BaseData.Name,
62-
Url = new Uri(trackRequest.Data.BaseData.Url),
62+
Url = Uri.TryCreate(trackRequest.Data.BaseData.Url, UriKind.Absolute, out var perfUri) ? perfUri : null,
6363
Duration = trackRequest.Data.BaseData.Duration,
6464
Timestamp = trackRequest.Time,
6565
Id = trackRequest.Data.BaseData.Id,

application/shared-webapp/infrastructure/applicationInsights/ApplicationInsightsProvider.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ const applicationInsights = new ApplicationInsights({
3434
disableInstrumentationKeyValidation: true,
3535
// Set the endpoint URL to our custom endpoint
3636
endpointUrl: "/api/track",
37-
// Enable auto route tracking for React Router
38-
enableAutoRouteTracking: true,
37+
// Disable auto route tracking (not compatible with TanStack Router)
38+
enableAutoRouteTracking: false,
3939
// Instrument error tracking
4040
autoExceptionInstrumented: true,
4141
autoUnhandledPromiseInstrumented: true,
@@ -60,5 +60,6 @@ const applicationInsights = new ApplicationInsights({
6060

6161
// Load the Application Insights script
6262
applicationInsights.loadAppInsights();
63-
// Track the initial page view
64-
applicationInsights.trackPageView();
63+
64+
// Export for error tracking
65+
export { applicationInsights };
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useRouter } from "@tanstack/react-router";
2+
import { useEffect, useRef } from "react";
3+
import { applicationInsights } from "./ApplicationInsightsProvider";
4+
5+
export function PageTracker() {
6+
const router = useRouter();
7+
const lastPathname = useRef<string>("");
8+
9+
useEffect(() => {
10+
// Track initial page view
11+
const pathname = router.state.location.pathname;
12+
if (pathname !== lastPathname.current) {
13+
applicationInsights.trackPageView({
14+
name: pathname,
15+
uri: window.location.href
16+
});
17+
lastPathname.current = pathname;
18+
}
19+
20+
// Subscribe to navigation events
21+
const unsubscribe = router.subscribe("onLoad", ({ toLocation }) => {
22+
if (toLocation.pathname !== lastPathname.current) {
23+
applicationInsights.trackPageView({
24+
name: toLocation.pathname,
25+
uri: toLocation.href
26+
});
27+
lastPathname.current = toLocation.pathname;
28+
}
29+
});
30+
31+
return unsubscribe;
32+
}, [router]);
33+
34+
return null;
35+
}

application/shared-webapp/infrastructure/http/errorHandler.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* 4. Used by both httpClient.ts and queryClient.ts to ensure consistent error handling
99
* 5. Shows toast notifications to the user for unhandled errors
1010
*/
11+
import { applicationInsights } from "@repo/infrastructure/applicationInsights/ApplicationInsightsProvider";
1112
import { toastQueue } from "@repo/ui/components/Toast";
1213

1314
// RFC 7807 Problem Details format
@@ -98,6 +99,9 @@ function showTimeoutToast(): void {
9899
}
99100

100101
function showUnknownErrorToast(error: Error) {
102+
// Track the error in Application Insights
103+
applicationInsights.trackException({ exception: error });
104+
101105
toastQueue.add({
102106
title: "Unknown Error",
103107
description: `An unknown error occured (${error})`,
@@ -270,7 +274,15 @@ export function setupGlobalErrorHandlers() {
270274
}
271275

272276
processedErrors.add(event.reason);
273-
showErrorToast(event.reason);
277+
278+
// Check if it's an HttpError or regular Error
279+
if (event.reason instanceof Error && !("kind" in event.reason)) {
280+
// Regular JavaScript error - track it and show toast
281+
showUnknownErrorToast(event.reason);
282+
} else {
283+
// HttpError - use existing error handling
284+
showErrorToast(event.reason);
285+
}
274286
});
275287

276288
// Handle uncaught exceptions
@@ -285,7 +297,15 @@ export function setupGlobalErrorHandlers() {
285297
}
286298

287299
processedErrors.add(event.error);
288-
showErrorToast(event.error);
300+
301+
// Track JavaScript errors in Application Insights and show toast
302+
if (event.error instanceof Error) {
303+
showUnknownErrorToast(event.error);
304+
} else {
305+
// Create an Error object for non-Error exceptions
306+
const error = new Error(String(event.error));
307+
showUnknownErrorToast(error);
308+
}
289309

290310
return true; // Stop error propagation
291311
});

0 commit comments

Comments
 (0)