User Journey Tracking Documentation β
Overview β
The User Journey Tracker Service provides comprehensive tracking of user behavior throughout your Angular application. It automatically captures page views, navigation patterns, user interactions, funnel progression, and conversion events while maintaining user privacy and performance.
What it Tracks β
- Page Views & Navigation: Automatic tracking of all route changes with load times
- Active/Inactive Time: Distinguishes between active engagement and idle time
- User Interactions: Click events, form submissions, and custom actions
- Funnel Progression: Multi-step process tracking with drop-off analysis
- Conversion Events: Goal completion with attribution to AB test variants
- Session Context: Device type, referrer, and session continuity
Key Features β
- β Automatic Page Tracking - Integrates with Angular Router
- β Activity Detection - Tracks active vs inactive time on pages
- β URL Normalization - Removes dynamic IDs for cleaner analytics
- β Funnel Analytics - Built-in funnel metrics and drop-off detection
- β AB Test Integration - Links all events to AB test variant assignments
- β Privacy-First - Supports user opt-out and data anonymization
- β Performance Optimized - Event batching and efficient tracking
- β Mobile & Desktop - Device detection and responsive tracking
Setup Guide β
1. Module Import β
Import the ABTestModule in your app module:
// app.module.ts
import { ABTestModule } from '@campus/ab-test';
@NgModule({
imports: [
// ... other imports
ABTestModule,
],
})
export class AppModule {}2. Dependency Configuration β
Configure the required dependency tokens in your app-token.module.ts:
// app-token.module.ts
import {
AB_TEST_AUTH_SERVICE,
AB_TEST_ANALYTICS_SERVICE
} from '@campus/ab-test';
import {
AuthService,
AnalyticsService,
ANALYTICS_SERVICE_TOKEN
} from '@campus/dal';
@NgModule({
providers: [
// Authentication service for user context
{
provide: AB_TEST_AUTH_SERVICE,
useExisting: AuthService,
},
// Analytics service for event batching
{
provide: ANALYTICS_SERVICE_TOKEN,
useClass: AnalyticsService
},
{
provide: AB_TEST_ANALYTICS_SERVICE,
useExisting: ANALYTICS_SERVICE_TOKEN
},
],
})
export class AppTokenModule {}3. Initialize Journey Tracking β
Set up journey tracking in your root component (e.g., app.component.ts or wrapper.component.ts):
// wrapper.component.ts
import { Component, HostListener, inject, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { UserJourneyTrackerService } from '@campus/ab-test';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`,
})
export class AppWrapperComponent implements OnInit, OnDestroy {
private router = inject(Router);
private userJourneyTracker = inject(UserJourneyTrackerService);
private destroy$ = new Subject<void>();
private navigationStartTime: number | null = null;
ngOnInit(): void {
this.initializeUserJourneyTracking();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
private initializeUserJourneyTracking(): void {
// Start journey when user logs in
this.authService.currentUser$
.pipe(
filter(user => !!user),
takeUntil(this.destroy$)
)
.subscribe(user => {
this.userJourneyTracker.startJourney(user.id.toString());
});
// Track navigation start time for performance metrics
this.router.events
.pipe(
filter(event => event instanceof NavigationStart),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.navigationStartTime = performance.now();
});
// Track page views on navigation end
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.destroy$)
)
.subscribe((event: NavigationEnd) => {
const loadTime = this.navigationStartTime
? Math.round(performance.now() - this.navigationStartTime)
: undefined;
this.userJourneyTracker.trackPageView(
event.urlAfterRedirects,
loadTime
);
this.navigationStartTime = null;
});
}
// Handle page unload to save journey data
@HostListener('window:beforeunload', ['$event'])
onBeforeUnload(event: Event): void {
this.userJourneyTracker.onPageUnload();
}
// More reliable on mobile devices
@HostListener('window:pagehide', ['$event'])
onPageHide(event: Event): void {
this.userJourneyTracker.onPageUnload();
}
}API Reference β
Journey Lifecycle β
startJourney(userId: string): void β
Initializes a new user journey session. This should be called once when the user logs in or the app initializes.
Parameters:
userId- Unique identifier for the user (will be anonymized if configured)
Example:
// In your authentication success handler
this.authService.currentUser$
.pipe(filter(user => !!user))
.subscribe(user => {
this.userJourneyTracker.startJourney(user.id.toString());
});When to use:
- β After successful login
- β On app initialization for logged-in users
- β Don't call multiple times per session
- β Don't call before user authentication
endJourney(): void β
Explicitly ends the current journey session and flushes all pending events.
Example:
// In your logout handler
logout(): void {
this.userJourneyTracker.endJourney();
this.authService.logout();
}When to use:
- β On user logout
- β When session expires
- β On app destroy (ngOnDestroy)
- β Don't call on every route change
onPageUnload(): void β
Handles page unload events to ensure journey data is saved before the user leaves.
Example:
@HostListener('window:beforeunload', ['$event'])
onBeforeUnload(event: Event): void {
this.userJourneyTracker.onPageUnload();
}
@HostListener('window:pagehide', ['$event'])
onPageHide(event: Event): void {
this.userJourneyTracker.onPageUnload();
}When to use:
- β Always set up in your root component
- β
Use both
beforeunloadandpagehidefor better mobile support
Page Tracking β
trackPageView(page: string, loadTime?: number, routeSnapshot?: ActivatedRouteSnapshot | null): void β
Tracks a page view event with optional load time and automatic route parameter extraction.
Parameters:
page- URL path (e.g.,/methods/overview)loadTime- Optional page load time in millisecondsrouteSnapshot- Optional ActivatedRouteSnapshot for route parameter extraction (auto-detected if omitted)
Example:
// Automatic tracking (recommended - set up once in root component)
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
const loadTime = performance.now() - navigationStartTime;
this.userJourneyTracker.trackPageView(
event.urlAfterRedirects,
Math.round(loadTime)
);
});
// Manual tracking in a component
ngOnInit(): void {
this.userJourneyTracker.trackPageView('/dashboard', undefined, this.route.snapshot);
}What it captures:
- Raw URL and normalized URL (without dynamic IDs, with normalized query params)
- Route parameters and query parameters
- Page load time
- Previous page
- Active/Inactive time on previous page
URL Normalization: The service automatically normalizes URLs to group similar pages together in analytics:
- Route parameters are replaced with their names:
/methods/book-123β/methods/:book - Query parameters are also normalized:
?tab=overview&filter=activeβ?filter=:filter&tab=:tab - Combined result:
/v2/methods/book-123/toc-456?tab=contentβ/v2/methods/:book/:toc?tab=:tab
This allows you to analyze user behavior across all instances of a page template, rather than treating each unique ID as a separate page.
When to use:
- β Automatically via Router events (recommended)
- β For manual SPA page changes
- β Don't call for every small view state change
trackPageNavigation(fromPage: string, toPage: string, loadTime?: number): void β
Tracks explicit navigation between pages. Less commonly used since trackPageView handles this automatically.
Parameters:
fromPage- Source page URLtoPage- Destination page URLloadTime- Optional load time in milliseconds
Example:
// When navigating programmatically with custom tracking
navigateToPage(targetUrl: string): void {
const currentUrl = this.router.url;
const startTime = performance.now();
this.router.navigate([targetUrl]).then(() => {
const loadTime = Math.round(performance.now() - startTime);
this.userJourneyTracker.trackPageNavigation(
currentUrl,
targetUrl,
loadTime
);
});
}URL Normalization Example:
// Raw URLs with different IDs and query params
'/v2/methods/book-123/toc-456?tab=overview'
'/v2/methods/book-789/toc-012?tab=exercises'
'/v2/methods/book-345/toc-678?tab=content&view=grid'
// All normalized to the same pattern for analytics
'/v2/methods/:book/:toc?tab=:tab'
'/v2/methods/:book/:toc?tab=:tab'
'/v2/methods/:book/:toc?tab=:tab&view=:view'
// This allows grouping by page template, not individual instancesWhen to use:
- β For custom navigation flows outside Angular Router
- β When you need explicit from/to page tracking
- β Usually not needed if
trackPageViewis set up
User Interactions β
trackUserInteraction(action: string, elementId: string, properties?: Record<string, unknown>): void β
Tracks user interactions like clicks, taps, or custom actions.
Parameters:
action- Action type (e.g.,'click','tap','hover')elementId- Identifier for the element (e.g.,'btn-submit','nav-methods')properties- Optional additional data
Example:
// In your component template
<button
(click)="onMethodAdd()"
data-tracking-id="btn-add-method">
Add Method
</button>
// In your component class
onMethodAdd(): void {
this.userJourneyTracker.trackUserInteraction(
'click',
'btn-add-method',
{
section: 'methods-overview',
methodsCount: this.methods.length
}
);
// ... your business logic
}More Examples:
// Track help icon click
onHelpClick(topic: string): void {
this.userJourneyTracker.trackUserInteraction(
'click_help',
`help-${topic}`,
{ topic, fromPage: this.router.url }
);
}
// Track navigation menu interaction
onMenuItemClick(menuItem: string): void {
this.userJourneyTracker.trackUserInteraction(
'menu_click',
`nav-${menuItem}`,
{ destination: menuItem }
);
}
// Track card expansion
onCardExpand(cardId: string): void {
this.userJourneyTracker.trackUserInteraction(
'expand',
`card-${cardId}`,
{ expanded: true }
);
}When to use:
- β Important buttons (CTAs, submit, cancel)
- β Navigation elements
- β Help/documentation interactions
- β Feature toggles or switches
- β Every mouse movement or minor UI change
- β Repetitive actions (use throttling if needed)
trackFormInteraction(formId: string, action: string, elementId: string, properties?: Record<string, unknown>): void β
Tracks form-specific interactions like field focus, blur, validation errors, and submissions.
Parameters:
formId- Form identifier (e.g.,'signup-form','task-create-form')action- Action type (e.g.,'focus','blur','error','submit')elementId- Field or button identifierproperties- Optional additional data (e.g., error messages, field values)
Example:
// In your component
export class TaskCreateComponent {
formId = 'task-create-form';
// Track form submission
onSubmit(): void {
if (this.taskForm.invalid) {
this.userJourneyTracker.trackFormInteraction(
this.formId,
'submit_failed',
'btn-submit',
{
errors: this.getFormErrors(),
errorCount: Object.keys(this.taskForm.errors || {}).length
}
);
return;
}
this.userJourneyTracker.trackFormInteraction(
this.formId,
'submit_success',
'btn-submit'
);
// ... save logic
}
// Track field validation errors
onFieldBlur(fieldName: string): void {
const field = this.taskForm.get(fieldName);
if (field?.invalid && field?.touched) {
this.userJourneyTracker.trackFormInteraction(
this.formId,
'validation_error',
`field-${fieldName}`,
{
errors: field.errors,
value: field.value?.length || 0
}
);
}
}
// Track field focus (for engagement analysis)
onFieldFocus(fieldName: string): void {
this.userJourneyTracker.trackFormInteraction(
this.formId,
'focus',
`field-${fieldName}`
);
}
}Template Example:
<form [formGroup]="taskForm" (ngSubmit)="onSubmit()">
<input
type="text"
formControlName="title"
(focus)="onFieldFocus('title')"
(blur)="onFieldBlur('title')"
placeholder="Task Title"
/>
<textarea
formControlName="description"
(focus)="onFieldFocus('description')"
(blur)="onFieldBlur('description')"
></textarea>
<button type="submit">Create Task</button>
</form>When to use:
- β Form submission (success and failure)
- β Validation errors
- β Critical field interactions (focus on key fields)
- β Form abandonment detection
- β Every keystroke
- β Non-critical field focus events
Funnel Tracking β
trackFunnelStep(funnelId: string, step: string): void β
Tracks completion of a step in a multi-step funnel (e.g., onboarding, checkout, task creation).
Parameters:
funnelId- Unique funnel identifier (e.g.,'task_creation_funnel','onboarding_funnel')step- Step identifier (e.g.,'1-start','2-configure','3-complete')
Example:
// Multi-step task creation funnel
export class TaskCreateWizardComponent {
private funnelId = 'task_creation_funnel';
ngOnInit(): void {
// Track funnel start
this.userJourneyTracker.trackFunnelStep(this.funnelId, '1-start');
}
onConfigureStep(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, '2-configure');
this.currentStep = 2;
}
onReviewStep(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, '3-review');
this.currentStep = 3;
}
onComplete(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, '4-complete');
// Completion logic...
}
}Onboarding Example:
export class OnboardingComponent {
private funnelId = 'user_onboarding';
ngOnInit(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'welcome');
}
onProfileComplete(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'profile_complete');
}
onPreferencesComplete(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'preferences_complete');
}
onOnboardingComplete(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'complete');
}
}What it captures:
- Time between funnel steps
- Active vs inactive time during each step
- Total time from funnel start to current step
- AB test variant assignments
When to use:
- β Multi-step workflows (wizards, checkouts)
- β Onboarding processes
- β Task creation flows
- β Purchase funnels
- β Single-page actions
trackFunnelDropoff(funnelId: string, step: string, reason: string): void β
Tracks when a user drops off from a funnel before completion.
Parameters:
funnelId- Unique funnel identifierstep- Step where drop-off occurredreason- Reason for drop-off (e.g.,'cancel_clicked','validation_failed','timeout')
Example:
export class TaskCreateWizardComponent {
private funnelId = 'task_creation_funnel';
private currentStep = '1-start';
onCancel(): void {
this.userJourneyTracker.trackFunnelDropoff(
this.funnelId,
this.currentStep,
'cancel_clicked'
);
this.router.navigate(['/tasks']);
}
onValidationError(): void {
this.userJourneyTracker.trackFunnelDropoff(
this.funnelId,
this.currentStep,
'validation_failed'
);
}
onTimeout(): void {
this.userJourneyTracker.trackFunnelDropoff(
this.funnelId,
this.currentStep,
'session_timeout'
);
}
}When to use:
- β User clicks cancel/back
- β Validation prevents progress
- β Session timeout during funnel
- β Navigation away from funnel
- β Successful funnel completion
Conversion Tracking β
trackConversionEvent(event: ConversionEvent): void β
Tracks goal completion events with attribution to AB test variants.
Parameters:
event- Conversion event object with the following properties:timestamp- Event timestamp (Date)testId- AB test identifier (if applicable)variantId- Variant user was assigned tometricId- Success metric identifier (e.g.,'task_created','purchase_complete')value- Optional conversion value (e.g., revenue amount)properties- Optional additional data
Example:
// In your service/component
export class TaskService {
private userJourneyTracker = inject(UserJourneyTrackerService);
createTask(task: Task): Observable<Task> {
return this.http.post<Task>('/api/tasks', task).pipe(
tap(createdTask => {
// Track conversion after successful API call
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: 'task_creation_ab_test',
variantId: 'B', // Get from AB test service
metricId: 'task_created',
value: 1,
properties: {
taskType: createdTask.type,
hasDueDate: !!createdTask.dueDate,
assignedUsers: createdTask.assignedUsers.length
}
});
})
);
}
}E-commerce Example:
onPurchaseComplete(order: Order): void {
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: 'checkout_flow_test',
variantId: this.abTestService.getVariant('checkout_flow_test'),
metricId: 'purchase_complete',
value: order.totalAmount,
properties: {
orderId: order.id,
itemCount: order.items.length,
paymentMethod: order.paymentMethod
}
});
}When to use:
- β After successful API calls for key actions
- β Goal completions (signups, purchases, submissions)
- β Feature adoptions
- β Tutorial completions
- β Before API confirmation
- β For every minor action
Analytics & Metrics β
getFunnelMetrics(funnelId: string): FunnelMetrics | null β
Retrieves comprehensive metrics for a specific funnel including completion rate, drop-off points, and step timings.
Returns:
interface FunnelMetrics {
funnelId: string;
started: number; // Number of users who started
completed: number; // Number who completed
completionRate: number; // Percentage (0-100)
avgTimeToComplete?: number; // Average time in ms
dropoffPoints: FunnelDropoffPoint[];
stepTimings: FunnelStepTiming[];
}Example:
// In an analytics dashboard component
export class FunnelAnalyticsComponent {
funnelMetrics: FunnelMetrics | null;
ngOnInit(): void {
this.funnelMetrics = this.userJourneyTracker.getFunnelMetrics('task_creation_funnel');
if (this.funnelMetrics) {
console.log('Funnel completion rate:', this.funnelMetrics.completionRate);
console.log('Average time to complete:', this.funnelMetrics.avgTimeToComplete);
console.log('Drop-off points:', this.funnelMetrics.dropoffPoints);
}
}
}When to use:
- β Analytics dashboards
- β Funnel optimization analysis
- β Performance monitoring
- β Not for real-time user-facing features
getUserFlowPattern(): UserFlowPattern | null β
Analyzes the user's navigation pattern to identify common paths and behaviors.
Returns:
interface UserFlowPattern {
mostVisitedPages: Array<{ page: string; visits: number }>;
commonPaths: Array<{ path: string[]; count: number }>;
avgPageDepth: number;
bounceRate: number;
}Example:
// Analyze user behavior patterns
const flowPattern = this.userJourneyTracker.getUserFlowPattern();
if (flowPattern) {
console.log('Most visited:', flowPattern.mostVisitedPages);
console.log('Common paths:', flowPattern.commonPaths);
console.log('Average depth:', flowPattern.avgPageDepth);
console.log('Bounce rate:', flowPattern.bounceRate);
}When to use:
- β UX research and analysis
- β User behavior studies
- β Navigation optimization
- β Not for individual user profiling
getCurrentJourney(): UserJourney | null β
Returns the current journey object with all tracked events.
Returns:
interface UserJourney {
userId: string;
sessionId: string;
startTime: Date;
endTime?: Date;
events: JourneyEvent[];
conversionEvents: ConversionEvent[];
testAssignments: Record<string, VariantType>;
deviceType: 'mobile' | 'tablet' | 'desktop';
}
interface JourneyEvent {
timestamp: Date;
eventType: 'page_view' | 'page_exit' | 'interaction' | 'form_interaction' | 'funnel_step';
page: string; // Raw URL
normalizedPage: string; // Normalized URL (e.g., /methods/:book/:toc?tab=:tab)
action: string;
properties?: Record<string, unknown>;
}Example:
// Check current journey status
const journey = this.userJourneyTracker.getCurrentJourney();
if (journey) {
console.log('Journey started:', journey.startTime);
console.log('Events tracked:', journey.events.length);
console.log('Conversions:', journey.conversionEvents.length);
console.log('Test assignments:', journey.testAssignments);
}When to use:
- β Debugging tracking implementation
- β Building custom analytics
- β Not for modifying journey data
configureBehavior(config: Partial<UserJourneyConfig>): void β
Configures tracking behavior like activity timeout and event batching.
Parameters:
interface UserJourneyConfig {
inactivityThresholdMs: number; // Time before marking user as inactive
autoTrackClicks: boolean; // Auto-track all click events
batchEvents: boolean; // Enable event batching
flushIntervalMs: number; // How often to flush batched events
}Example:
// Configure during app initialization
ngOnInit(): void {
this.userJourneyTracker.configureBehavior({
inactivityThresholdMs: 30000, // 30 seconds
autoTrackClicks: false, // Manual click tracking
batchEvents: true,
flushIntervalMs: 5000 // Flush every 5 seconds
});
}When to use:
- β During app initialization
- β For performance optimization
- β To adjust to specific use cases
- β Don't change frequently during runtime
updateTestAssignments(testAssignments: Record<string, VariantType>): void β
Updates the AB test variant assignments for the current journey.
Parameters:
testAssignments- Object mapping test IDs to variant IDs
Example:
// Update assignments when new tests are loaded
this.abTestService.getAllVariantAssignments().subscribe(assignments => {
const assignmentMap = assignments.reduce((acc, assignment) => {
acc[assignment.testId] = assignment.variantId;
return acc;
}, {} as Record<string, VariantType>);
this.userJourneyTracker.updateTestAssignments(assignmentMap);
});When to use:
- β When AB test assignments are loaded
- β When new tests are activated
- β Usually handled automatically by ABTestService
Angular Implementation Examples β
Example 1: Complete App Setup β
// app.component.ts or wrapper.component.ts
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { UserJourneyTrackerService, ABTestService } from '@campus/ab-test';
import { AuthService } from '@campus/dal';
import { Subject } from 'rxjs';
import { filter, takeUntil, distinctUntilKeyChanged } from 'rxjs/operators';
@Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>',
})
export class AppComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
private navigationStartTime: number | null = null;
constructor(
private router: Router,
private authService: AuthService,
private userJourneyTracker: UserJourneyTrackerService,
private abTestService: ABTestService
) {}
ngOnInit(): void {
this.initializeTracking();
}
ngOnDestroy(): void {
this.userJourneyTracker.endJourney();
this.destroy$.next();
this.destroy$.complete();
}
private initializeTracking(): void {
// Start journey on user login
this.authService.currentUser$
.pipe(
filter(user => !!user),
distinctUntilKeyChanged('id'),
takeUntil(this.destroy$)
)
.subscribe(user => {
this.userJourneyTracker.startJourney(user.id.toString());
this.loadABTestAssignments();
});
// Track page load start time
this.router.events
.pipe(
filter(event => event instanceof NavigationStart),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.navigationStartTime = performance.now();
});
// Track page views
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.destroy$)
)
.subscribe((event: NavigationEnd) => {
const loadTime = this.navigationStartTime
? Math.round(performance.now() - this.navigationStartTime)
: undefined;
this.userJourneyTracker.trackPageView(
event.urlAfterRedirects,
loadTime
);
this.navigationStartTime = null;
});
}
private loadABTestAssignments(): void {
this.abTestService.getAllVariantAssignments().subscribe(assignments => {
const assignmentMap = assignments.reduce((acc, assignment) => {
acc[assignment.testId] = assignment.variantId;
return acc;
}, {} as Record<string, VariantType>);
this.userJourneyTracker.updateTestAssignments(assignmentMap);
});
}
}Example 2: Feature Component with Funnel Tracking β
// task-creation-wizard.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { UserJourneyTrackerService } from '@campus/ab-test';
import { TaskService } from './task.service';
@Component({
selector: 'app-task-creation-wizard',
templateUrl: './task-creation-wizard.component.html',
})
export class TaskCreationWizardComponent implements OnInit {
private funnelId = 'task_creation_funnel';
currentStep = 1;
taskData: any = {};
constructor(
private router: Router,
private taskService: TaskService,
private userJourneyTracker: UserJourneyTrackerService
) {}
ngOnInit(): void {
// Track funnel start
this.userJourneyTracker.trackFunnelStep(this.funnelId, '1-start');
}
onBasicInfoComplete(data: any): void {
this.taskData = { ...this.taskData, ...data };
this.userJourneyTracker.trackFunnelStep(this.funnelId, '2-basic-info');
this.currentStep = 2;
}
onDetailsComplete(data: any): void {
this.taskData = { ...this.taskData, ...data };
this.userJourneyTracker.trackFunnelStep(this.funnelId, '3-details');
this.currentStep = 3;
}
onReviewComplete(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, '4-review');
this.submitTask();
}
submitTask(): void {
this.taskService.createTask(this.taskData).subscribe(
(task) => {
// Track successful completion
this.userJourneyTracker.trackFunnelStep(this.funnelId, '5-complete');
// Track conversion
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: 'task-creation-ui',
variantId: 'A', // Get from AB test service
metricId: 'task_created',
value: 1,
properties: {
taskType: task.type,
stepsCompleted: 5,
},
});
this.router.navigate(['/tasks', task.id]);
},
(error) => {
// Track dropoff on error
this.userJourneyTracker.trackFunnelDropoff(
this.funnelId,
`${this.currentStep}-${this.getStepName()}`,
'api_error'
);
}
);
}
onCancel(): void {
this.userJourneyTracker.trackFunnelDropoff(
this.funnelId,
`${this.currentStep}-${this.getStepName()}`,
'user_cancelled'
);
this.router.navigate(['/tasks']);
}
private getStepName(): string {
const steps = ['start', 'basic-info', 'details', 'review', 'complete'];
return steps[this.currentStep - 1] || 'unknown';
}
}Example 3: Form Component with Interaction Tracking β
// signup-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UserJourneyTrackerService } from '@campus/ab-test';
import { AuthService } from './auth.service';
@Component({
selector: 'app-signup-form',
templateUrl: './signup-form.component.html',
})
export class SignupFormComponent implements OnInit {
signupForm: FormGroup;
formId = 'user_signup_form';
constructor(
private fb: FormBuilder,
private authService: AuthService,
private userJourneyTracker: UserJourneyTrackerService
) {}
ngOnInit(): void {
this.signupForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
role: ['', Validators.required],
});
// Track form load
this.userJourneyTracker.trackUserInteraction(
'form_loaded',
this.formId
);
}
onFieldFocus(fieldName: string): void {
this.userJourneyTracker.trackFormInteraction(
this.formId,
'field_focus',
`field-${fieldName}`
);
}
onFieldBlur(fieldName: string): void {
const control = this.signupForm.get(fieldName);
if (control?.invalid && control?.touched) {
this.userJourneyTracker.trackFormInteraction(
this.formId,
'validation_error',
`field-${fieldName}`,
{
errors: Object.keys(control.errors || {}),
value: control.value?.length || 0,
}
);
}
}
onSubmit(): void {
if (this.signupForm.invalid) {
const errorCount = Object.keys(this.signupForm.errors || {}).length;
this.userJourneyTracker.trackFormInteraction(
this.formId,
'submit_failed',
'btn-submit',
{
errors: this.getFormErrors(),
errorCount,
}
);
return;
}
this.userJourneyTracker.trackFormInteraction(
this.formId,
'submit_attempt',
'btn-submit'
);
this.authService.signup(this.signupForm.value).subscribe(
(user) => {
this.userJourneyTracker.trackFormInteraction(
this.formId,
'submit_success',
'btn-submit'
);
// Track conversion
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: 'signup-form-test',
variantId: 'A',
metricId: 'signup_complete',
value: 1,
});
},
(error) => {
this.userJourneyTracker.trackFormInteraction(
this.formId,
'submit_error',
'btn-submit',
{ errorMessage: error.message }
);
}
);
}
private getFormErrors(): string[] {
const errors: string[] = [];
Object.keys(this.signupForm.controls).forEach((key) => {
const control = this.signupForm.get(key);
if (control?.errors) {
errors.push(...Object.keys(control.errors));
}
});
return errors;
}
}<!-- signup-form.component.html -->
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<input
type="text"
formControlName="name"
(focus)="onFieldFocus('name')"
(blur)="onFieldBlur('name')"
placeholder="Full Name"
/>
<input
type="email"
formControlName="email"
(focus)="onFieldFocus('email')"
(blur)="onFieldBlur('email')"
placeholder="Email"
/>
<input
type="password"
formControlName="password"
(focus)="onFieldFocus('password')"
(blur)="onFieldBlur('password')"
placeholder="Password"
/>
<select
formControlName="role"
(focus)="onFieldFocus('role')"
(blur)="onFieldBlur('role')">
<option value="">Select Role</option>
<option value="teacher">Teacher</option>
<option value="student">Student</option>
</select>
<button type="submit">Sign Up</button>
</form>Best Practices β
1. Journey Initialization β
// β
CORRECT: Start journey once on login
this.authService.currentUser$
.pipe(
filter(user => !!user),
distinctUntilKeyChanged('id'),
takeUntil(this.destroy$)
)
.subscribe(user => {
this.userJourneyTracker.startJourney(user.id.toString());
});
// β WRONG: Starting journey multiple times
ngOnInit(): void {
this.userJourneyTracker.startJourney(this.userId); // Don't call on every component init
}When to start a journey:
- β Once after successful login
- β Once on app initialization for authenticated users
- β Not on every component initialization
- β Not on every route change
2. Page View Tracking β
// β
CORRECT: Automatic tracking via Router (set up once)
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.userJourneyTracker.trackPageView(event.urlAfterRedirects, loadTime);
});
// β WRONG: Manual tracking in every component
ngOnInit(): void {
this.userJourneyTracker.trackPageView('/my-page'); // Don't do this!
}Best approach:
- β Set up once in root component
- β Captures all route changes automatically
- β Includes load time metrics
- β Automatically normalizes URLs including query parameters
- β Don't duplicate in individual components
URL Normalization Benefits:
// Users visit different books with different query params:
'/methods/book-abc?tab=overview'
'/methods/book-xyz?tab=exercises'
'/methods/book-123?tab=content&filter=new'
// All tracked as the same page template:
'/methods/:book?filter=:filter&tab=:tab'
// Makes analytics meaningful - you can see:
// - How many users visit the "methods book page" (regardless of which book)
// - Which query param combinations are most common
// - Funnel drop-off at "book page with tab filter" vs "book page without"3. Interaction Tracking - Be Selective β
// β
GOOD: Track important actions
onMethodAdd(): void {
this.userJourneyTracker.trackUserInteraction('click', 'btn-add-method');
// ... business logic
}
onHelpClick(): void {
this.userJourneyTracker.trackUserInteraction('click_help', 'icon-help');
}
// β BAD: Tracking everything
onMouseMove(event: MouseEvent): void {
this.userJourneyTracker.trackUserInteraction('mouse_move', 'page'); // Too much!
}
onEveryKeyPress(): void {
this.userJourneyTracker.trackUserInteraction('keypress', 'input'); // Too much!
}What to track:
- β CTA button clicks
- β Navigation interactions
- β Help/support interactions
- β Feature usage
- β Mouse movements
- β Every keystroke
- β Minor UI interactions
4. Funnel Step Tracking β
// β
CORRECT: Track meaningful funnel steps
class CheckoutComponent {
private funnelId = 'purchase_funnel';
ngOnInit(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'cart_review');
}
onShippingComplete(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'shipping_complete');
}
onPaymentComplete(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'payment_complete');
}
}
// β WRONG: Too many or too few steps
ngOnInit(): void {
this.userJourneyTracker.trackFunnelStep('funnel', 'step1');
this.userJourneyTracker.trackFunnelStep('funnel', 'step2'); // Don't track multiple immediately
this.userJourneyTracker.trackFunnelStep('funnel', 'step3');
}Funnel step guidelines:
- β Track major milestones (page changes, form submissions)
- β Use clear, descriptive step names
- β Keep 3-7 steps per funnel
- β Don't track minor UI state changes
- β Don't track multiple steps at once
5. Conversion Tracking - After API Success β
// β
CORRECT: Track after confirmed success
createTask(task: Task): void {
this.taskService.create(task).subscribe(
(created) => {
// API succeeded, now track conversion
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: 'task-ui',
variantId: 'B',
metricId: 'task_created',
value: 1,
});
}
);
}
// β WRONG: Track before API confirmation
createTask(task: Task): void {
// Don't track here - API might fail!
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
metricId: 'task_created',
});
this.taskService.create(task).subscribe();
}Conversion timing:
- β After successful API response
- β After confirmed action completion
- β When user reaches success state
- β Before API call
- β On button click (before confirmation)
- β On optimistic updates
6. Form Tracking - Focus on Errors β
// β
GOOD: Track validation errors and submissions
onFieldBlur(fieldName: string): void {
const field = this.form.get(fieldName);
if (field?.invalid && field?.touched) {
this.userJourneyTracker.trackFormInteraction(
'signup_form',
'validation_error',
`field-${fieldName}`,
{ errors: field.errors }
);
}
}
onSubmit(): void {
if (this.form.invalid) {
this.userJourneyTracker.trackFormInteraction(
'signup_form',
'submit_failed',
'btn-submit',
{ errorCount: this.getErrorCount() }
);
}
}
// β BAD: Tracking every focus/blur
onFieldFocus(fieldName: string): void {
// Only track focus for critical fields, not every field
this.userJourneyTracker.trackFormInteraction('form', 'focus', fieldName); // Too much
}Form tracking guidelines:
- β Track validation errors
- β Track submission attempts (success/failure)
- β Track abandonment (if applicable)
- β Don't track every focus/blur
- β Don't track every field value change
7. Naming Conventions β
// β
GOOD: Clear, consistent naming
// Funnel IDs: snake_case, descriptive
'task_creation_funnel'
'user_onboarding_funnel'
'purchase_checkout_funnel'
// Funnel steps: numbered or descriptive
'1-start'
'2-basic-info'
'3-details'
'4-review'
'5-complete'
// Action names: action_object format
'click_button'
'click_help'
'submit_form'
'open_menu'
// Element IDs: component-element format
'btn-submit'
'icon-help'
'nav-methods'
'field-email'
// β BAD: Unclear naming
'funnel1'
'step'
'action'
'element'8. Error Handling β
// β
CORRECT: Defensive tracking
trackSafely(): void {
try {
this.userJourneyTracker.trackUserInteraction('click', 'btn-save');
} catch (error) {
console.error('Tracking failed:', error);
// Don't let tracking break user experience
}
}
// β
BETTER: Service handles errors internally
// Just call the methods - they won't throw
this.userJourneyTracker.trackUserInteraction('click', 'btn-save');Error handling approach:
- β Service has built-in error handling
- β Failed tracking won't break user flow
- β Errors are logged for debugging
- β Don't wrap every call in try-catch (service handles it)
Common Use Cases β
Use Case 1: Multi-Step Wizard β
Track user progress through a multi-step process with drop-off points.
@Component({
selector: 'app-product-wizard',
template: `
<div *ngIf="currentStep === 1">
<h2>Step 1: Product Info</h2>
<button (click)="nextStep()">Next</button>
<button (click)="cancel()">Cancel</button>
</div>
<!-- More steps... -->
`
})
export class ProductWizardComponent implements OnInit {
private funnelId = 'product_creation_funnel';
currentStep = 1;
ngOnInit(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'start');
}
nextStep(): void {
this.currentStep++;
this.userJourneyTracker.trackFunnelStep(
this.funnelId,
`step_${this.currentStep}`
);
}
cancel(): void {
this.userJourneyTracker.trackFunnelDropoff(
this.funnelId,
`step_${this.currentStep}`,
'user_cancelled'
);
this.router.navigate(['/products']);
}
complete(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'complete');
// Save product...
}
}Use Case 2: Feature Adoption Tracking β
Track when users discover and use new features.
@Component({
selector: 'app-dashboard',
})
export class DashboardComponent {
onNewFeatureClick(): void {
// Track feature discovery
this.userJourneyTracker.trackUserInteraction(
'feature_discovered',
'ai-assistant-button',
{ feature: 'ai_assistant' }
);
this.showAIAssistant();
}
onFeatureUsed(): void {
// Track actual usage
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: 'ai-assistant-launch',
variantId: 'A',
metricId: 'ai_assistant_used',
value: 1,
});
}
}Use Case 3: E-commerce Purchase Funnel β
Complete purchase funnel with cart, checkout, and payment.
@Component({
selector: 'app-checkout',
})
export class CheckoutComponent implements OnInit {
private funnelId = 'purchase_funnel';
ngOnInit(): void {
// User arrived at checkout
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'checkout_start');
}
onShippingSubmit(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'shipping_complete');
}
onPaymentSubmit(): void {
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'payment_submitted');
this.orderService.submitOrder().subscribe(
(order) => {
// Track successful purchase
this.userJourneyTracker.trackFunnelStep(this.funnelId, 'purchase_complete');
this.userJourneyTracker.trackConversionEvent({
timestamp: new Date(),
testId: 'checkout-flow',
variantId: 'B',
metricId: 'purchase_complete',
value: order.total,
properties: {
orderId: order.id,
itemCount: order.items.length,
},
});
},
(error) => {
this.userJourneyTracker.trackFunnelDropoff(
this.funnelId,
'payment_submitted',
'payment_failed'
);
}
);
}
}Use Case 4: Help/Support Interaction β
Track when users need help, indicating friction points.
@Component({
selector: 'app-task-form',
})
export class TaskFormComponent {
onHelpClick(topic: string): void {
// Track help request - indicates confusion
this.userJourneyTracker.trackUserInteraction(
'help_requested',
`help-${topic}`,
{
topic,
formSection: this.getCurrentSection(),
timeSpentOnPage: this.getTimeOnPage(),
}
);
this.showHelp(topic);
}
onTooltipHover(fieldName: string): void {
// Track tooltip usage
this.userJourneyTracker.trackUserInteraction(
'tooltip_viewed',
`tooltip-${fieldName}`,
{ field: fieldName }
);
}
}Use Case 5: Form Abandonment Detection β
Detect when users start but don't complete forms.
@Component({
selector: 'app-registration-form',
})
export class RegistrationFormComponent implements OnInit, OnDestroy {
private formId = 'user_registration';
private formStarted = false;
private formCompleted = false;
ngOnInit(): void {
// Track form load
this.userJourneyTracker.trackUserInteraction('form_loaded', this.formId);
}
onFirstFieldInteraction(): void {
if (!this.formStarted) {
this.formStarted = true;
this.userJourneyTracker.trackFormInteraction(
this.formId,
'form_started',
'first_field'
);
}
}
onSubmitSuccess(): void {
this.formCompleted = true;
this.userJourneyTracker.trackFormInteraction(
this.formId,
'form_completed',
'btn-submit'
);
}
ngOnDestroy(): void {
// Track abandonment if started but not completed
if (this.formStarted && !this.formCompleted) {
this.userJourneyTracker.trackFormInteraction(
this.formId,
'form_abandoned',
'page_exit'
);
}
}
}Troubleshooting β
Issue 1: Events Not Being Tracked β
Symptoms:
- No events appear in analytics
getCurrentJourney()returns null
Solutions:
// β
Ensure journey is started
this.authService.currentUser$.subscribe(user => {
if (user) {
this.userJourneyTracker.startJourney(user.id.toString());
}
});
// β
Check analytics service is configured
{
provide: AB_TEST_ANALYTICS_SERVICE,
useExisting: ANALYTICS_SERVICE_TOKEN
}
// β
Verify user is not opted out
// Check ABTestAuthService.hasUserOptedOut()Issue 2: Page Views Not Captured β
Symptoms:
trackPageViewnot firing on navigation- Missing page view events
Solutions:
// β
Set up Router event subscription in root component
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.destroy$)
)
.subscribe((event: NavigationEnd) => {
this.userJourneyTracker.trackPageView(event.urlAfterRedirects);
});
// β
Ensure subscription is not destroyed early
// Use takeUntil(this.destroy$) and clean up in ngOnDestroy
// β Don't unsubscribe too early
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}Issue 3: Duplicate Events β
Symptoms:
- Same event tracked multiple times
- Funnel steps counted multiple times
Solutions:
// β
Track only once per action
onButtonClick(): void {
if (!this.hasTracked) {
this.userJourneyTracker.trackUserInteraction('click', 'btn-submit');
this.hasTracked = true;
}
}
// β
Use distinctUntilChanged for observables
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
distinctUntilChanged((a, b) => a.url === b.url)
)
.subscribe(event => {
this.userJourneyTracker.trackPageView(event.url);
});Issue 4: Funnel Metrics Not Calculating β
Symptoms:
getFunnelMetrics()returns null- Incomplete funnel data
Solutions:
// β
Ensure consistent funnel ID
private funnelId = 'task_creation_funnel'; // Use same ID everywhere
// β
Track all funnel steps
this.userJourneyTracker.trackFunnelStep(this.funnelId, '1-start');
this.userJourneyTracker.trackFunnelStep(this.funnelId, '2-config');
this.userJourneyTracker.trackFunnelStep(this.funnelId, '3-complete');
// β
Check journey is active
const journey = this.userJourneyTracker.getCurrentJourney();
if (!journey) {
console.warn('No active journey');
}Issue 5: Memory Leaks β
Symptoms:
- Performance degradation over time
- Growing memory usage
Solutions:
// β
Always unsubscribe in ngOnDestroy
private destroy$ = new Subject<void>();
ngOnInit(): void {
this.router.events
.pipe(takeUntil(this.destroy$))
.subscribe(/* ... */);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
// β
End journey on logout
onLogout(): void {
this.userJourneyTracker.endJourney();
this.authService.logout();
}
// β
Call onPageUnload on window unload
@HostListener('window:beforeunload')
onBeforeUnload(): void {
this.userJourneyTracker.onPageUnload();
}Quick Reference β
Initialization Checklist β
- [ ] Import
ABTestModulein app module - [ ] Configure
AB_TEST_AUTH_SERVICEprovider - [ ] Configure
AB_TEST_ANALYTICS_SERVICEprovider - [ ] Set up Router event subscription in root component
- [ ] Call
startJourney(userId)on user login - [ ] Set up
window.beforeunloadandwindow.pagehidelisteners - [ ] Call
endJourney()on logout
When to Use Each Method β
| Method | When to Use | Example |
|---|---|---|
startJourney | Once per user session, after login | User authentication |
endJourney | On logout or session end | User logs out |
trackPageView | Automatically via Router (set up once) | Every navigation |
trackUserInteraction | Important user actions | CTA clicks, menu interactions |
trackFormInteraction | Form events, validation errors | Form submission, field errors |
trackFunnelStep | Major milestones in multi-step process | Wizard step completion |
trackFunnelDropoff | User abandons funnel | Cancel click, navigation away |
trackConversionEvent | Goal achieved, API success | Purchase complete, signup success |
getFunnelMetrics | Analyzing funnel performance | Analytics dashboard |
onPageUnload | Before page closes | Window unload event |
Summary β
The User Journey Tracker Service provides comprehensive tracking capabilities for understanding user behavior in your Angular application. Key takeaways:
- Set up once - Initialize tracking in your root component
- Be selective - Track important actions, not everything
- Track after success - Wait for API confirmation before tracking conversions
- Use consistent naming - Follow naming conventions for clarity
- Handle errors gracefully - Service has built-in error handling
- Clean up properly - Unsubscribe and end journeys appropriately