UTA DevHub

Session Tracking Headers

Implementation guide for session tracking and correlation headers in API requests

Session Tracking Headers

Overview

This guide details the implementation of session tracking headers, a key component of our API header customization architecture. Session headers create continuity across API requests, enabling request correlation, user journey tracking, and performance monitoring. These headers are essential for maintaining context across distributed systems, debugging complex interactions, and analyzing user behavior patterns.

Quick Start

If you need to quickly implement session tracking in your application, follow these three steps:

  1. Create a session header provider:

    // core/shared/api/headers/providers/sessionHeaderProvider.ts
    export class SessionHeaderProvider implements HeaderProvider {
      id = 'session';
      priority = 80;
      
      async getHeaders() {
        const sessionId = await this.getOrCreateSessionId();
        const requestId = `req-${uuidv4()}`;
        
        return {
          [API_HEADERS.SESSION_ID]: sessionId,
          [API_HEADERS.REQUEST_ID]: requestId,
          [API_HEADERS.REQUEST_START_TIME]: new Date().toISOString(),
        };
      }
      
      private async getOrCreateSessionId() {
        let sessionId = await storage.getItem('current_session_id');
        if (!sessionId) {
          sessionId = `sess-${uuidv4()}`;
          await storage.setItem('current_session_id', sessionId);
        }
        return sessionId;
      }
    }
  2. Register with your API client:

    // core/shared/api/client.ts
    const apiClient = new AuthenticatedApiClient(config);
    apiClient.registerHeaderProvider(new SessionHeaderProvider());
  3. Add logging integration:

    // core/shared/logger/index.ts
    logger.addInterceptor((logEntry) => {
      // Add current session context to all log entries
      const sessionId = apiClient.getLastRequestHeader(API_HEADERS.SESSION_ID);
      const requestId = apiClient.getLastRequestHeader(API_HEADERS.REQUEST_ID);
      return {
        ...logEntry,
        sessionId,
        requestId,
      };
    });

For a complete implementation with more features, continue reading the sections below.

Purpose & Scope

This implementation guide covers:

  • Creating and maintaining unique session identifiers
  • Generating per-request correlation IDs
  • Implementing the appropriate header provider pattern
  • Best practices for session management
  • Integration with logging and analytics systems

This document builds upon the foundation established in the Header Customization Architecture guide, with specific focus on implementing session tracking and correlation headers.

Prerequisites

To effectively implement session tracking headers, you should be familiar with:

Key Session Headers

The following session headers provide essential context for request tracking and correlation:

HeaderDescriptionPurposeExample Value
X-SESSION-IDPersistent session identifierSession continuity & user journey tracking"sess-4f58a782-1b8c-4932-8998-5a3c987541e2"
X-REQUEST-IDUnique identifier for each requestRequest identification & correlation"req-c7d2b51f-e1b5-487e-9a34-95f893c428bf"
X-CORRELATION-IDID for tracking across servicesDistributed tracing"corr-3a7bf5e1-009d-4648-a2c5-f0d1eb7f3f87"
X-REQUEST-START-TIMETimestamp when request was initiatedPerformance timing"2023-07-15T14:25:32.145Z"

Implementation

1. Session Header Provider

First, let's implement a complete SessionHeaderProvider that creates and maintains session information:

// core/shared/api/headers/providers/sessionHeaderProvider.ts
import { HeaderProvider } from '../types';
import { API_HEADERS } from '@/core/shared/constants/headers';
import { storage } from '@/core/shared/utils/storage';
import { v4 as uuidv4 } from 'uuid';
 
export class SessionHeaderProvider implements HeaderProvider {
  id = 'session';
  priority = 80; // High priority but below device headers
  private sessionId: string | null = null;
  
  /**
   * Returns all session-related headers for API requests
   */
  async getHeaders(): Promise<Record<string, string | undefined>> {
    // Ensure we have a session ID
    if (!this.sessionId) {
      this.sessionId = await this.getOrCreateSessionId();
    }
    
    // Generate a unique request ID for this specific API call
    const requestId = this.generateRequestId();
    
    // Generate correlation ID (or use request ID if no existing correlation context)
    const correlationId = this.getCorrelationId() || requestId;
    
    // Capture request start time
    const requestStartTime = new Date().toISOString();
    
    return {
      [API_HEADERS.SESSION_ID]: this.sessionId,
      [API_HEADERS.REQUEST_ID]: requestId,
      [API_HEADERS.CORRELATION_ID]: correlationId,
      [API_HEADERS.REQUEST_START_TIME]: requestStartTime,
    };
  }
  
  /**
   * Retrieves the existing session ID or creates a new one
   */
  private async getOrCreateSessionId(): Promise<string> {
    // Try to get stored session ID
    let sessionId = await storage.getItem('current_session_id');
    
    if (!sessionId) {
      // Generate new session ID with sess- prefix
      sessionId = `sess-${uuidv4()}`;
      
      // Store for future use within this session
      await storage.setItem('current_session_id', sessionId);
      
      // Record session start time
      const sessionStartTime = new Date().toISOString();
      await storage.setItem('session_start_time', sessionStartTime);
    }
    
    return sessionId;
  }
  
  /**
   * Generate a unique request ID with req- prefix
   */
  private generateRequestId(): string {
    return `req-${uuidv4()}`;
  }
  
  /**
   * Get correlation ID from current context if available
   * This implementation uses a simple in-memory approach, but
   * a more robust implementation might use React context or another
   * method to propagate correlation IDs
   */
  private getCorrelationId(): string | null {
    // Simple implementation - in a real app, this might pull from
    // a more sophisticated correlation context system
    return null;
  }
  
  /**
   * Reset the session ID to force a new session
   */
  public async resetSession(): Promise<string> {
    // Generate new session ID
    const newSessionId = `sess-${uuidv4()}`;
    
    // Update stored session ID
    await storage.setItem('current_session_id', newSessionId);
    
    // Record new session start time
    const sessionStartTime = new Date().toISOString();
    await storage.setItem('session_start_time', sessionStartTime);
    
    // Update instance variable
    this.sessionId = newSessionId;
    
    return newSessionId;
  }
  
  /**
   * Get the current session duration in milliseconds
   */
  public async getSessionDuration(): Promise<number | null> {
    const sessionStartTime = await storage.getItem('session_start_time');
    
    if (!sessionStartTime) {
      return null;
    }
    
    const startTime = new Date(sessionStartTime).getTime();
    const currentTime = new Date().getTime();
    
    return currentTime - startTime;
  }
}

2. Distributed Tracing Enhancement

For applications requiring distributed tracing, we can extend the basic provider with more sophisticated correlation:

// core/shared/api/headers/providers/tracingHeaderProvider.ts
import { SessionHeaderProvider } from './sessionHeaderProvider';
import { API_HEADERS } from '@/core/shared/constants/headers';
 
/**
 * Storage for the current trace context throughout the app lifecycle
 */
class TraceContextManager {
  private static instance: TraceContextManager;
  private correlationId: string | null = null;
  private traceState: Record<string, string> = {};
  
  public static getInstance(): TraceContextManager {
    if (!TraceContextManager.instance) {
      TraceContextManager.instance = new TraceContextManager();
    }
    return TraceContextManager.instance;
  }
  
  /**
   * Set the current correlation ID
   */
  setCorrelationId(id: string): void {
    this.correlationId = id;
  }
  
  /**
   * Get the current correlation ID
   */
  getCorrelationId(): string | null {
    return this.correlationId;
  }
  
  /**
   * Clear the current trace context
   */
  clearContext(): void {
    this.correlationId = null;
    this.traceState = {};
  }
  
  /**
   * Set a trace state value
   */
  setTraceState(key: string, value: string): void {
    this.traceState[key] = value;
  }
  
  /**
   * Get the full trace state
   */
  getTraceState(): Record<string, string> {
    return { ...this.traceState };
  }
  
  /**
   * Get the trace state as a W3C-compatible string
   */
  getTraceStateString(): string {
    return Object.entries(this.traceState)
      .map(([key, value]) => `${key}=${value}`)
      .join(',');
  }
}
 
/**
 * Enhanced session provider with distributed tracing capabilities
 */
export class TracingHeaderProvider extends SessionHeaderProvider {
  id = 'tracing';
  private traceContext: TraceContextManager;
  
  constructor() {
    super();
    this.traceContext = TraceContextManager.getInstance();
  }
  
  /**
   * Override to add trace context headers
   */
  async getHeaders(): Promise<Record<string, string | undefined>> {
    // Get base session headers
    const baseHeaders = await super.getHeaders();
    
    // Get trace context
    const correlationId = this.traceContext.getCorrelationId() || baseHeaders[API_HEADERS.CORRELATION_ID];
    const traceState = this.traceContext.getTraceStateString();
    
    // If this is a new correlation ID, store it
    if (correlationId && correlationId !== this.traceContext.getCorrelationId()) {
      this.traceContext.setCorrelationId(correlationId);
    }
    
    // Add tracing headers
    return {
      ...baseHeaders,
      [API_HEADERS.CORRELATION_ID]: correlationId,
      ...(traceState ? { 'traceparent': traceState } : {}),
    };
  }
  
  /**
   * Process incoming trace context from responses
   * @param headers Response headers containing trace info
   */
  processResponseTraceContext(headers: Record<string, string>): void {
    // Extract server-side correlation ID if present
    const serverCorrelationId = headers[API_HEADERS.CORRELATION_ID.toLowerCase()];
    
    if (serverCorrelationId) {
      this.traceContext.setCorrelationId(serverCorrelationId);
    }
    
    // Extract trace state if present
    const traceParent = headers['traceparent'];
    if (traceParent) {
      // Parse W3C trace context format
      // In a real implementation, we would properly parse the traceparent format
      this.traceContext.setTraceState('server-trace', traceParent);
    }
  }
}

3. Session Events Integration

To enhance analytics and monitoring, we can implement session event handling:

// core/shared/api/headers/providers/sessionEventsProvider.ts
import { SessionHeaderProvider } from './sessionHeaderProvider';
import { EventEmitter } from '@/core/shared/utils/events';
 
/**
 * Events emitted by the session provider
 */
export type SessionEvents = {
  'session:created': { sessionId: string, timestamp: string };
  'session:ended': { sessionId: string, duration: number, timestamp: string };
  'request:sent': { requestId: string, sessionId: string, url: string, timestamp: string };
  'request:completed': { requestId: string, sessionId: string, duration: number, status: number, timestamp: string };
};
 
/**
 * Event emitter for session-related events
 */
export const sessionEvents = new EventEmitter<SessionEvents>();
 
/**
 * Session provider with event tracking
 */
export class SessionEventsProvider extends SessionHeaderProvider {
  id = 'session-events';
  private requestTimestamps: Map<string, number> = new Map();
  
  constructor() {
    super();
    this.setupResponseInterceptors();
  }
  
  /**
   * Override getHeaders to add event tracking
   */
  async getHeaders(): Promise<Record<string, string | undefined>> {
    const headers = await super.getHeaders();
    const requestId = headers[API_HEADERS.REQUEST_ID];
    const sessionId = headers[API_HEADERS.SESSION_ID];
    
    if (requestId && sessionId) {
      // Record request start time
      this.requestTimestamps.set(requestId, Date.now());
      
      // Emit request sent event
      sessionEvents.emit('request:sent', {
        requestId,
        sessionId,
        url: this.getCurrentUrl() || 'unknown',
        timestamp: new Date().toISOString(),
      });
    }
    
    return headers;
  }
  
  /**
   * Setup response interceptors to track request completion
   */
  private setupResponseInterceptors(): void {
    // This would be integrated with the API client in a real implementation
    // For demonstration purposes, here's the logic that would be used
    
    /*
    apiClient.client.interceptors.response.use(
      // Success interceptor
      (response) => {
        this.trackRequestCompletion(
          response.config.headers[API_HEADERS.REQUEST_ID],
          response.config.headers[API_HEADERS.SESSION_ID],
          response.status
        );
        return response;
      },
      // Error interceptor
      (error) => {
        if (error.response) {
          this.trackRequestCompletion(
            error.config.headers[API_HEADERS.REQUEST_ID],
            error.config.headers[API_HEADERS.SESSION_ID],
            error.response.status
          );
        }
        return Promise.reject(error);
      }
    );
    */
  }
  
  /**
   * Track request completion and emit events
   */
  private trackRequestCompletion(requestId: string, sessionId: string, status: number): void {
    if (!requestId || !sessionId) return;
    
    const startTime = this.requestTimestamps.get(requestId);
    if (!startTime) return;
    
    // Calculate duration
    const duration = Date.now() - startTime;
    
    // Clear from tracking map
    this.requestTimestamps.delete(requestId);
    
    // Emit completion event
    sessionEvents.emit('request:completed', {
      requestId,
      sessionId,
      duration,
      status,
      timestamp: new Date().toISOString(),
    });
  }
  
  /**
   * Get the current URL (simplified implementation)
   */
  private getCurrentUrl(): string | null {
    // In a real app, this might come from navigation state or another source
    return null;
  }
  
  /**
   * Override resetSession to emit session events
   */
  async resetSession(): Promise<string> {
    // Get current session ID before reset
    const oldSessionId = await this.getOrCreateSessionId();
    const duration = await this.getSessionDuration() || 0;
    
    // Emit session ended event
    if (oldSessionId) {
      sessionEvents.emit('session:ended', {
        sessionId: oldSessionId,
        duration,
        timestamp: new Date().toISOString(),
      });
    }
    
    // Create new session
    const newSessionId = await super.resetSession();
    
    // Emit session created event
    sessionEvents.emit('session:created', {
      sessionId: newSessionId,
      timestamp: new Date().toISOString(),
    });
    
    return newSessionId;
  }
}

Integration with Application State

To enhance session management with application state awareness:

// core/domains/session/store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { SessionHeaderProvider } from '@/core/shared/api/headers/providers/sessionHeaderProvider';
 
interface SessionState {
  isActive: boolean;
  lastActiveTime: string | null;
  inactivityTimeout: number; // milliseconds
  
  markActive: () => void;
  checkSessionTimeout: () => boolean;
  resetInactivityTimer: () => void;
  setInactivityTimeout: (timeout: number) => void;
}
 
export const useSessionStore = create<SessionState>()(
  persist(
    (set, get) => ({
      isActive: true,
      lastActiveTime: new Date().toISOString(),
      inactivityTimeout: 30 * 60 * 1000, // 30 minutes by default
      
      markActive: () => set({ 
        isActive: true, 
        lastActiveTime: new Date().toISOString() 
      }),
      
      checkSessionTimeout: () => {
        const { lastActiveTime, inactivityTimeout } = get();
        if (!lastActiveTime) return false;
        
        const lastActive = new Date(lastActiveTime).getTime();
        const now = Date.now();
        
        return (now - lastActive) > inactivityTimeout;
      },
      
      resetInactivityTimer: () => set({ 
        lastActiveTime: new Date().toISOString() 
      }),
      
      setInactivityTimeout: (timeout) => set({ 
        inactivityTimeout: timeout 
      }),
    }),
    {
      name: 'session-state',
      partialize: (state) => ({ 
        inactivityTimeout: state.inactivityTimeout
      }),
    }
  )
);
 
/**
 * Custom hook for session timeout management
 */
export function useSessionTimeout(provider: SessionHeaderProvider) {
  const { 
    checkSessionTimeout, 
    resetInactivityTimer, 
    setInactivityTimeout 
  } = useSessionStore();
  
  /**
   * Check for session timeout and reset if needed
   * @returns true if session was reset, false otherwise
   */
  const checkAndResetSession = async (): Promise<boolean> => {
    if (checkSessionTimeout()) {
      // Session timed out, reset
      await provider.resetSession();
      resetInactivityTimer();
      return true;
    }
    
    // Session still valid
    resetInactivityTimer();
    return false;
  };
  
  return {
    checkAndResetSession,
    setInactivityTimeout,
  };
}

Testing Session Headers

1. Unit Testing the Provider

// __tests__/core/shared/api/headers/providers/sessionHeaderProvider.test.ts
import { SessionHeaderProvider } from '@/core/shared/api/headers/providers/sessionHeaderProvider';
import { API_HEADERS } from '@/core/shared/constants/headers';
import { storage } from '@/core/shared/utils/storage';
 
// Mock dependencies
jest.mock('@/core/shared/utils/storage');
 
describe('SessionHeaderProvider', () => {
  let provider: SessionHeaderProvider;
  
  beforeEach(() => {
    // Reset mocks
    jest.clearAllMocks();
    
    // Reset storage mock
    (storage.getItem as jest.Mock).mockReset();
    (storage.setItem as jest.Mock).mockReset();
    
    // Create provider instance
    provider = new SessionHeaderProvider();
  });
  
  it('should create a new session ID if none exists', async () => {
    // Setup storage to return no existing session
    (storage.getItem as jest.Mock).mockResolvedValue(null);
    
    const headers = await provider.getHeaders();
    
    // Verify session ID was created and stored
    expect(headers[API_HEADERS.SESSION_ID]).toMatch(/^sess-/);
    expect(storage.setItem).toHaveBeenCalledWith(
      'current_session_id',
      headers[API_HEADERS.SESSION_ID]
    );
    expect(storage.setItem).toHaveBeenCalledWith(
      'session_start_time',
      expect.any(String)
    );
  });
  
  it('should reuse existing session ID', async () => {
    // Setup storage to return existing session
    const existingSessionId = 'sess-existing-id';
    (storage.getItem as jest.Mock).mockResolvedValue(existingSessionId);
    
    const headers = await provider.getHeaders();
    
    // Verify existing session ID was used
    expect(headers[API_HEADERS.SESSION_ID]).toBe(existingSessionId);
    expect(storage.setItem).not.toHaveBeenCalledWith(
      'current_session_id',
      expect.any(String)
    );
  });
  
  it('should generate unique request ID for each call', async () => {
    // Setup storage to return existing session
    (storage.getItem as jest.Mock).mockResolvedValue('sess-existing-id');
    
    const headers1 = await provider.getHeaders();
    const headers2 = await provider.getHeaders();
    
    // Verify request IDs are different
    expect(headers1[API_HEADERS.REQUEST_ID]).not.toBe(headers2[API_HEADERS.REQUEST_ID]);
    expect(headers1[API_HEADERS.REQUEST_ID]).toMatch(/^req-/);
    expect(headers2[API_HEADERS.REQUEST_ID]).toMatch(/^req-/);
  });
  
  it('should reset session and create new ID', async () => {
    // Setup existing session
    const existingSessionId = 'sess-existing-id';
    (storage.getItem as jest.Mock).mockResolvedValue(existingSessionId);
    
    // First get headers to establish session
    await provider.getHeaders();
    
    // Clear mock calls
    (storage.setItem as jest.Mock).mockClear();
    
    // Reset session
    const newSessionId = await provider.resetSession();
    
    // Verify new session ID
    expect(newSessionId).not.toBe(existingSessionId);
    expect(newSessionId).toMatch(/^sess-/);
    expect(storage.setItem).toHaveBeenCalledWith(
      'current_session_id',
      newSessionId
    );
    
    // Get headers and verify new session used
    const headers = await provider.getHeaders();
    expect(headers[API_HEADERS.SESSION_ID]).toBe(newSessionId);
  });
  
  it('should calculate session duration', async () => {
    // Setup mock for session start time (10 seconds ago)
    const now = Date.now();
    const startTime = new Date(now - 10000).toISOString();
    (storage.getItem as jest.Mock).mockResolvedValue(startTime);
    
    // Mock Date.now to return consistent value
    const originalDateNow = Date.now;
    Date.now = jest.fn(() => now);
    
    // Get session duration
    const duration = await provider.getSessionDuration();
    
    // Verify duration is approximately 10 seconds
    expect(duration).toBeCloseTo(10000);
    
    // Restore Date.now
    Date.now = originalDateNow;
  });
});

2. Integration Testing with API Client

// __tests__/integration/api/sessionHeadersIntegration.test.ts
import { authenticatedApi } from '@/core/shared/api/client';
import { SessionHeaderProvider } from '@/core/shared/api/headers/providers/sessionHeaderProvider';
import { API_HEADERS } from '@/core/shared/constants/headers';
import MockAdapter from 'axios-mock-adapter';
 
describe('Session Headers Integration', () => {
  let mockAxios: MockAdapter;
  let sessionProvider: SessionHeaderProvider;
  
  beforeEach(() => {
    // Setup mock for the Axios instance
    mockAxios = new MockAdapter((authenticatedApi as any).client);
    
    // Create session provider instance
    sessionProvider = new SessionHeaderProvider();
    
    // Register session header provider for testing
    authenticatedApi.registerHeaderProvider(sessionProvider);
  });
  
  afterEach(() => {
    mockAxios.restore();
  });
  
  it('should include session headers in API requests', async () => {
    // Setup mock response
    mockAxios.onGet('/test-endpoint').reply((config) => {
      // Check that session headers are present
      expect(config.headers[API_HEADERS.SESSION_ID]).toBeDefined();
      expect(config.headers[API_HEADERS.SESSION_ID]).toMatch(/^sess-/);
      expect(config.headers[API_HEADERS.REQUEST_ID]).toBeDefined();
      expect(config.headers[API_HEADERS.REQUEST_ID]).toMatch(/^req-/);
      
      return [200, { success: true }];
    });
    
    // Make request
    await authenticatedApi.get('/test-endpoint');
    
    // Verification is done in the mock response
    expect(mockAxios.history.get.length).toBe(1);
  });
  
  it('should maintain session ID across multiple requests', async () => {
    let sessionId: string | undefined;
    
    // First request
    mockAxios.onGet('/first-request').reply((config) => {
      sessionId = config.headers[API_HEADERS.SESSION_ID];
      return [200, { success: true }];
    });
    
    await authenticatedApi.get('/first-request');
    
    // Second request
    mockAxios.onGet('/second-request').reply((config) => {
      expect(config.headers[API_HEADERS.SESSION_ID]).toBe(sessionId);
      return [200, { success: true }];
    });
    
    await authenticatedApi.get('/second-request');
    
    // Verification
    expect(mockAxios.history.get.length).toBe(2);
  });
  
  it('should use new session ID after reset', async () => {
    let sessionId: string | undefined;
    
    // First request
    mockAxios.onGet('/first-request').reply((config) => {
      sessionId = config.headers[API_HEADERS.SESSION_ID];
      return [200, { success: true }];
    });
    
    await authenticatedApi.get('/first-request');
    
    // Reset session
    await sessionProvider.resetSession();
    
    // Second request
    mockAxios.onGet('/second-request').reply((config) => {
      expect(config.headers[API_HEADERS.SESSION_ID]).not.toBe(sessionId);
      expect(config.headers[API_HEADERS.SESSION_ID]).toMatch(/^sess-/);
      return [200, { success: true }];
    });
    
    await authenticatedApi.get('/second-request');
  });
});

Security and Privacy Considerations

When implementing session tracking, consider these security and privacy best practices:

  1. Session IDs:

    • (Do ✅) Use cryptographically secure random values for session IDs
    • (Don't ❌) Include sensitive information in session IDs
    • (Do ✅) Consider session expiration based on inactivity and absolute time
  2. Correlation Context:

    • (Do ✅) Keep correlation IDs free of sensitive data
    • (Do ✅) Validate correlation IDs received from external sources
    • (Consider 🤔) Implementing length and format restrictions
  3. Logging:

    • (Do ✅) Include session and request IDs in all application logs
    • (Do ✅) Ensure logs with session IDs don't contain PII
    • (Consider 🤔) Implementing log retention policies for session data
  4. Privacy:

    • (Do ✅) Disclose session tracking in privacy policies
    • (Do ✅) Ensure compliance with relevant privacy regulations
    • (Consider 🤔) Providing options to opt out of non-essential tracking

Performance Considerations

  1. Header Size:

    • Keep session and request IDs reasonably sized to minimize header overhead
    • Consider the cumulative impact of all tracking headers on request size
  2. Storage Impact:

    • Session data storage is minimal, but still implement proper cleanup
    • Monitor storage usage if tracking large volumes of request data
  3. Overhead:

    • The overhead of generating and processing session headers is negligible
    • The benefits for debugging and monitoring outweigh the minimal performance cost

Best Practices

  1. Session Lifecycle:

    • (Do ✅) Create new sessions on login or authentication changes
    • (Do ✅) Reset sessions after prolonged inactivity
    • (Consider 🤔) Aligning session timeouts with authentication token lifetimes
  2. Request Correlation:

    • (Do ✅) Use consistent header formats across all services
    • (Do ✅) Propagate correlation IDs through all system components
    • (Consider 🤔) Adopting an established standard like W3C Trace Context
  3. Analytics Integration:

    • (Do ✅) Include session IDs in analytics events for journey mapping
    • (Do ✅) Measure session duration and request counts
    • (Consider 🤔) Implementing session stitching across devices
  4. Debugging:

    • (Do ✅) Make session and request IDs easily accessible in debugging tools
    • (Do ✅) Include correlation IDs in error reports
    • (Consider 🤔) Creating developer tools to track request flows

Migration Considerations

If you're transitioning from another session tracking approach, follow these guidelines to migrate successfully:

From Manual Header Addition

If you've been manually adding session headers in each API call:

  1. Identify existing headers - Document all session-related headers currently in use
  2. Create a compatible provider - Implement a SessionHeaderProvider that produces the same header names and formats
  3. Gradual transition - First add the provider to your API client while maintaining the manual code
  4. Validate - Verify that both methods produce the same results
  5. Remove manual code - Once validated, remove all manual session header code

From Server-Side Sessions Only

If your backend manages sessions without client-side tracking:

  1. Coordination - Work with backend teams to align on header naming and format
  2. Compatibility layer - Create a provider that works with your current backend expectations
  3. Enhanced tracking - Gradually add client-side features like request timing

From a Different Header Format

If you need to change header formats or names:

  1. Compatibility period - Configure your backend to accept both old and new header formats
  2. Transition clients - Update client applications to use the new header provider
  3. Monitoring - Track usage of old header formats to ensure complete transition
  4. Finalize - Remove support for old header formats once all clients have migrated

Summary

Session tracking headers provide essential context for maintaining continuity across API requests, enabling powerful debugging, monitoring, and analytics capabilities. By implementing robust session tracking using the HeaderProvider pattern, you can create a consistent context across your application's interactions with backend services, enhancing both development workflows and user experience.

Key takeaways:

  • Use SessionHeaderProvider to attach session context to all API requests
  • Generate unique request IDs for each API call while maintaining session continuity
  • Consider enhanced implementations for distributed tracing and event tracking
  • Integrate with application state for more sophisticated session management
  • Balance comprehensive tracking with performance and privacy considerations

Implementing these patterns will create a consistent foundation for request correlation, enabling more effective debugging, monitoring, and user journey analysis across your entire application ecosystem.