} from '@microsoft/applicationinsights-web';
import { ENVIRONMENT } from '../../environments/environment.token';
@Injectable({ providedIn: 'root' })
export class TelemetryBridgeService {
private readonly aiInstance: ApplicationInsights;
private readonly env = inject(ENVIRONMENT);
constructor() {
this.aiInstance = new ApplicationInsights({
config: {
connectionString: this.env.appInsightsConnectionString,
enableAutoRouteTracking: false,
disableAjaxTracking: false,
maxBatchIntervalMs: 1500,
enableCorsCorrelation: true
}
});
this.aiInstance.loadAppInsights();
}
emitPageView(payload: IPageViewTelemetry): void {
this.aiInstance.trackPageView(payload);
}
setUserIdentity(userId: string): void {
this.aiInstance.setAuthenticatedUserContext(userId);
}
}
**Architecture Rationale:**
- `connectionString` replaces legacy `instrumentationKey` for improved regional routing and security.
- `enableAutoRouteTracking: false` prevents the SDK from hijacking the History API, which conflicts with Angular's router.
- `maxBatchIntervalMs` controls network flush frequency, balancing latency vs. payload size.
- Encapsulation ensures a single SDK instance across the application, preventing memory leaks and duplicate initialization warnings.
### Step 2: Router Event Listener
Angular's router emits a stream of events during navigation. Capturing every event causes telemetry duplication. We must isolate `NavigationEnd`, which fires only after guards resolve, redirects complete, and the route is fully activated.
```typescript
// src/app/core/telemetry/route-telemetry-listener.service.ts
import { Injectable, inject, DestroyRef } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TelemetryBridgeService } from './telemetry-bridge.service';
@Injectable({ providedIn: 'root' })
export class RouteTelemetryListenerService {
private readonly router = inject(Router);
private readonly telemetry = inject(TelemetryBridgeService);
private readonly destroyRef = inject(DestroyRef);
initialize(): void {
this.router.events
.pipe(
filter((evt): evt is NavigationEnd => evt instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((navEnd) => {
this.telemetry.emitPageView({
name: navEnd.urlAfterRedirects,
uri: window.location.href,
properties: {
navigationTrigger: 'router',
timestamp: new Date().toISOString()
}
});
});
}
}
Architecture Rationale:
takeUntilDestroyed prevents memory leaks when the injector is destroyed.
- Type narrowing (
evt is NavigationEnd) improves TypeScript safety and eliminates runtime type checks.
urlAfterRedirects ensures we track the final resolved route, not intermediate guard paths.
- Separating the listener from the bridge service allows independent testing and future extension (e.g., adding performance marks or custom dimensions).
Step 3: Application Bootstrap Integration
The listener must activate once the router is ready. In modern Angular, this is typically handled in the root component or an APP_INITIALIZER.
// src/app/app.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { RouteTelemetryListenerService } from './core/telemetry/route-telemetry-listener.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: `<router-outlet />`
})
export class AppComponent implements OnInit {
private readonly routeTelemetry = inject(RouteTelemetryListenerService);
ngOnInit(): void {
this.routeTelemetry.initialize();
}
}
Why this structure?
- Standalone components eliminate module boilerplate while preserving lifecycle hooks.
- Initialization occurs after Angular's dependency injection tree is fully constructed.
- The router is guaranteed to be active, preventing race conditions during bootstrap.
Pitfall Guide
1. Tracking on RoutesRecognized or GuardsCheckStart
Explanation: These events fire before navigation completes. If a guard cancels the route or triggers a redirect, you'll log phantom page views that never actually render.
Fix: Always filter for NavigationEnd. It guarantees the route is fully resolved and the view is mounted.
2. Hardcoding Connection Strings in Source Control
Explanation: Embedding telemetry credentials directly in TypeScript files exposes them to client-side inspection and violates security best practices.
Fix: Use Angular's environment files or a build-time configuration service. Inject credentials via tokens or APP_INITIALIZER to keep them out of version control.
3. Ignoring Hash vs. Path Routing Differences
Explanation: Hash-based routing (#/dashboard) stores the route in window.location.hash, while path routing uses window.location.pathname. Blindly using window.location.href can produce inconsistent URIs across deployment environments.
Fix: Normalize the URI before sending. Extract the base path and append the router URL, or rely exclusively on navEnd.urlAfterRedirects for consistency.
4. Missing Authenticated User Context Setup
Explanation: Application Insights treats all anonymous sessions as separate users. Without setAuthenticatedUserContext, you cannot correlate telemetry across devices or filter by customer tier.
Fix: Call setAuthenticatedUserContext immediately after authentication succeeds. Pass a stable, non-PII identifier (e.g., user_12345) and clear it on logout.
5. Overloading trackPageView with Synchronous Operations
Explanation: Attaching heavy computations or blocking API calls to the telemetry payload delays navigation feedback and degrades perceived performance.
Fix: Keep page view payloads lightweight. Defer expensive context gathering to background observables or attach it via addTelemetryInitializer instead of inline computation.
6. Failing to Correlate Frontend and Backend Telemetry
Explanation: Client-side page views and server-side API logs appear as disconnected traces in Application Insights, making it impossible to trace a single user request across the stack.
Fix: Propagate the operationId from the frontend to backend headers. Use addTelemetryInitializer to inject a correlation ID into outgoing HTTP requests, and ensure the backend SDK reads and attaches it to server telemetry.
7. Multiple SDK Instances via Lazy-Loaded Modules
Explanation: Importing the telemetry service in lazy-loaded modules without providedIn: 'root' creates duplicate Application Insights instances, causing duplicate telemetry and inflated billing.
Fix: Always use providedIn: 'root' for singleton services. If module-level scoping is required, explicitly manage instance lifecycles and disable auto-instrumentation in secondary instances.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small internal tool | SDK Auto-Tracking | Minimal setup, acceptable metadata loss | Baseline |
| Enterprise SPA with RBAC | Manual Router Binding | Full custom dimensions, audit compliance | +5% ingestion |
| Multi-tenant SaaS | Manual + Telemetry Initializer | Tenant isolation, dynamic context injection | +10% ingestion |
| High-traffic marketing site | Hybrid (Auto + Overrides) | Balance setup speed with conversion tracking | +8% ingestion |
Configuration Template
// src/app/core/telemetry/telemetry.config.ts
import { IConfig } from '@microsoft/applicationinsights-web';
export function buildAppInsightsConfig(connectionString: string): IConfig {
return {
connectionString,
enableAutoRouteTracking: false,
disableAjaxTracking: false,
maxBatchIntervalMs: 1500,
enableCorsCorrelation: true,
correlationHeaderExcludedDomains: ['localhost', '127.0.0.1'],
enableAutoRouteTracking: false,
disableExceptionTracking: false,
enableDebug: false
};
}
// src/app/core/telemetry/telemetry-initializer.ts
import { ITelemetryItem } from '@microsoft/applicationinsights-web';
export function attachTenantContext(item: ITelemetryItem): boolean {
const tenantId = localStorage.getItem('activeTenant');
if (tenantId && item.baseData) {
item.baseData.properties = {
...item.baseData.properties,
tenantId,
environment: process.env.NODE_ENV || 'production'
};
}
return true;
}
Quick Start Guide
- Install dependencies: Run
npm install @microsoft/applicationinsights-web in your Angular workspace.
- Create the bridge service: Copy the
TelemetryBridgeService implementation, inject your connection string via environment configuration, and call loadAppInsights() in the constructor.
- Wire the router listener: Implement
RouteTelemetryListenerService, filter for NavigationEnd, and invoke emitPageView with the resolved URL.
- Activate on bootstrap: Call
initialize() in AppComponent.ngOnInit() or via an APP_INITIALIZER provider.
- Verify ingestion: Open Application Insights in Azure Portal, navigate to Logs, and run
pageViews | take 10. Confirm name, uri, and custom properties appear correctly.
By decoupling telemetry initialization from routing logic and enforcing strict event filtering, you gain deterministic page view tracking that scales with complex Angular architectures. This pattern eliminates duplicate signals, preserves business context, and aligns client-side telemetry with backend observability standards.