Skip to content

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:

typescript
// 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:

typescript
// 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):

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
@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 beforeunload and pagehide for 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 milliseconds
  • routeSnapshot - Optional ActivatedRouteSnapshot for route parameter extraction (auto-detected if omitted)

Example:

typescript
// 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 URL
  • toPage - Destination page URL
  • loadTime - Optional load time in milliseconds

Example:

typescript
// 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:

typescript
// 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 instances

When to use:

  • βœ… For custom navigation flows outside Angular Router
  • βœ… When you need explicit from/to page tracking
  • ❌ Usually not needed if trackPageView is 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:

typescript
// 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:

typescript
// 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 identifier
  • properties - Optional additional data (e.g., error messages, field values)

Example:

typescript
// 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:

html
<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:

typescript
// 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:

typescript
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 identifier
  • step - Step where drop-off occurred
  • reason - Reason for drop-off (e.g., 'cancel_clicked', 'validation_failed', 'timeout')

Example:

typescript
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 to
    • metricId - Success metric identifier (e.g., 'task_created', 'purchase_complete')
    • value - Optional conversion value (e.g., revenue amount)
    • properties - Optional additional data

Example:

typescript
// 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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
interface UserFlowPattern {
  mostVisitedPages: Array<{ page: string; visits: number }>;
  commonPaths: Array<{ path: string[]; count: number }>;
  avgPageDepth: number;
  bounceRate: number;
}

Example:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
// 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 ​

typescript
// 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 ​

typescript
// 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 ​

typescript
// 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;
  }
}
html
<!-- 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 ​

typescript
// βœ… 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 ​

typescript
// βœ… 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:

typescript
// 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 ​

typescript
// βœ… 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 ​

typescript
// βœ… 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 ​

typescript
// βœ… 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 ​

typescript
// βœ… 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 ​

typescript
// βœ… 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 ​

typescript
// βœ… 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.

typescript
@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.

typescript
@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.

typescript
@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.

typescript
@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.

typescript
@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:

typescript
// βœ… 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:

  • trackPageView not firing on navigation
  • Missing page view events

Solutions:

typescript
// βœ… 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:

typescript
// βœ… 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:

typescript
// βœ… 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:

typescript
// βœ… 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 ABTestModule in app module
  • [ ] Configure AB_TEST_AUTH_SERVICE provider
  • [ ] Configure AB_TEST_ANALYTICS_SERVICE provider
  • [ ] Set up Router event subscription in root component
  • [ ] Call startJourney(userId) on user login
  • [ ] Set up window.beforeunload and window.pagehide listeners
  • [ ] Call endJourney() on logout

When to Use Each Method ​

MethodWhen to UseExample
startJourneyOnce per user session, after loginUser authentication
endJourneyOn logout or session endUser logs out
trackPageViewAutomatically via Router (set up once)Every navigation
trackUserInteractionImportant user actionsCTA clicks, menu interactions
trackFormInteractionForm events, validation errorsForm submission, field errors
trackFunnelStepMajor milestones in multi-step processWizard step completion
trackFunnelDropoffUser abandons funnelCancel click, navigation away
trackConversionEventGoal achieved, API successPurchase complete, signup success
getFunnelMetricsAnalyzing funnel performanceAnalytics dashboard
onPageUnloadBefore page closesWindow unload event

Summary ​

The User Journey Tracker Service provides comprehensive tracking capabilities for understanding user behavior in your Angular application. Key takeaways:

  1. Set up once - Initialize tracking in your root component
  2. Be selective - Track important actions, not everything
  3. Track after success - Wait for API confirmation before tracking conversions
  4. Use consistent naming - Follow naming conventions for clarity
  5. Handle errors gracefully - Service has built-in error handling
  6. Clean up properly - Unsubscribe and end journeys appropriately