UTA DevHub

Device Identification Headers

Implementation guide for device identification and tracking headers across API requests

Device Identification Headers

Overview

This guide provides a detailed implementation approach for device identification headers, a critical component of our API header customization architecture. Device headers enable backend services to identify, track, and optimize experiences based on the specific device making API requests. They form the foundation for device-specific features, analytics, and security auditing.

Purpose & Scope

This implementation guide covers:

  • Implementing device identification headers using our header provider pattern
  • Collecting essential device information in React Native applications
  • Persisting device identifiers across app sessions
  • Security considerations for device tracking
  • Integration with analytics and monitoring systems

This document extends the core concepts established in the Header Customization Architecture guide, focusing specifically on device identification implementation patterns.

Prerequisites

To effectively implement device identification headers, you should be familiar with:

Key Device Headers

The following device headers provide essential context about the requesting device:

HeaderDescriptionPurposeExample Value
X-DEVICE-UUIDUnique device identifierDevice tracking & user experience"550e8400-e29b-41d4-a716-446655440000"
X-DEVICE-TYPEPhysical device typeResponse optimization"Handset" or "Tablet"
X-APP-VERSIONApp version + build numberVersion-specific handling"2.4.1+42"
X-OS-VERSIONOperating system versionOS-specific optimizations"iOS 16.2" or "Android 13"
X-DEVICE-MODELDevice modelDevice-specific optimizations"iPhone 14 Pro"
X-CARRIERNetwork carrierNetwork-specific optimizations"AT&T"

Implementation

1. Base Device Header Provider

First, we'll implement a complete DeviceHeaderProvider that collects essential device information:

// core/shared/api/headers/providers/deviceHeaderProvider.ts
import { HeaderProvider } from '../types';
import { API_HEADERS } from '@/core/shared/constants/headers';
import DeviceInfo from 'react-native-device-info';
import { Platform } from 'react-native';
import { storage } from '@/core/shared/utils/storage';
 
export class DeviceHeaderProvider implements HeaderProvider {
  id = 'device';
  priority = 90; // High priority to ensure device context is available to other providers
  private deviceId: string | null = null;
  
  /**
   * Returns all device-related headers for API requests
   */
  async getHeaders(): Promise<Record<string, string | undefined>> {
    // Ensure we have a device ID
    if (!this.deviceId) {
      this.deviceId = await this.getOrCreateDeviceId();
    }
    
    // Collect basic device information
    const appVersion = DeviceInfo.getVersion();
    const buildNumber = DeviceInfo.getBuildNumber();
    const deviceType = DeviceInfo.getDeviceType();
    const deviceModel = await DeviceInfo.getModel();
    const systemVersion = DeviceInfo.getSystemVersion();
    const carrier = await DeviceInfo.getCarrierSync();
    
    return {
      [API_HEADERS.DEVICE_UUID]: this.deviceId,
      [API_HEADERS.DEVICE_TYPE]: deviceType,
      [API_HEADERS.APP_VERSION]: `${appVersion}+${buildNumber}`,
      [API_HEADERS.OS_VERSION]: `${Platform.OS} ${systemVersion}`,
      [API_HEADERS.DEVICE_MODEL]: deviceModel,
      ...(carrier ? { [API_HEADERS.CARRIER]: carrier } : {}),
    };
  }
  
  /**
   * Retrieves a stored device ID or creates a new one.
   * We persist this ID to provide consistent device identification
   * across app sessions.
   */
  private async getOrCreateDeviceId(): Promise<string> {
    // Try to get stored device ID
    let deviceId = await storage.getSecureItem('device_uuid');
    
    if (!deviceId) {
      // Create new device ID if none exists
      try {
        // First try to use the device's unique ID if available
        deviceId = await DeviceInfo.getUniqueId();
      } catch (error) {
        // Fallback to a generated UUID
        deviceId = this.generateUuidV4();
      }
      
      // Store for future use
      await storage.setSecureItem('device_uuid', deviceId);
    }
    
    return deviceId;
  }
  
  /**
   * Generate a UUID v4 as fallback device identifier
   */
  private generateUuidV4(): string {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
      const r = Math.random() * 16 | 0, 
          v = c == 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }
}

2. Registering the Provider

Add the device header provider to your API clients as outlined in the API Header Customization guide:

// Example: Setting up API clients with device headers
import { PublicApiClient, AuthenticatedApiClient } from '@/core/shared/api/client';
import { DeviceHeaderProvider } from '@/core/shared/api/headers/providers/deviceHeaderProvider';
 
// Client setup
const apiConfig = { baseURL: process.env.API_BASE_URL };
 
// Create client instances
export const publicApi = new PublicApiClient(apiConfig);
export const authenticatedApi = new AuthenticatedApiClient(apiConfig);
 
// Register device header provider with both clients
const deviceHeaderProvider = new DeviceHeaderProvider();
publicApi.registerHeaderProvider(deviceHeaderProvider);
authenticatedApi.registerHeaderProvider(deviceHeaderProvider);

3. Enhanced Implementation with Device Fingerprinting

For applications requiring stronger device identification, you can extend the basic provider with fingerprinting:

// core/shared/api/headers/providers/enhancedDeviceHeaderProvider.ts
import { DeviceHeaderProvider } from './deviceHeaderProvider';
import DeviceInfo from 'react-native-device-info';
import { Platform } from 'react-native';
import { API_HEADERS } from '@/core/shared/constants/headers';
 
export class EnhancedDeviceHeaderProvider extends DeviceHeaderProvider {
  id = 'enhanced-device';
  
  /**
   * Creates a device fingerprint from multiple device characteristics
   */
  private async createDeviceFingerprint(): Promise<string> {
    // Collect various device properties
    const brand = DeviceInfo.getBrand();
    const model = await DeviceInfo.getModel();
    const systemName = DeviceInfo.getSystemName();
    const systemVersion = DeviceInfo.getSystemVersion();
    const buildId = await DeviceInfo.getBuildId();
    const deviceName = await DeviceInfo.getDeviceName();
    const timezone = DeviceInfo.getTimezone();
    const hasNotch = DeviceInfo.hasNotch();
    
    // Combine properties into fingerprint string
    const fingerprintData = [
      brand,
      model,
      systemName,
      systemVersion,
      buildId,
      deviceName,
      timezone,
      hasNotch ? 'notch' : 'no-notch',
      Platform.constants?.reactNativeVersion || '',
    ].join('|');
    
    // Create hash of the fingerprint
    const fingerprintHash = await this.hashString(fingerprintData);
    return fingerprintHash;
  }
  
  /**
   * Simple hash function for fingerprint
   */
  private async hashString(str: string): Promise<string> {
    // This is a simple hash for demonstration purposes
    // In a real implementation, use a cryptographic hash function
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return hash.toString(16);
  }
  
  /**
   * Override getHeaders to include fingerprint
   */
  async getHeaders(): Promise<Record<string, string | undefined>> {
    // Get base headers from parent class
    const baseHeaders = await super.getHeaders();
    
    // Add fingerprint
    const fingerprint = await this.createDeviceFingerprint();
    
    return {
      ...baseHeaders,
      'X-DEVICE-FINGERPRINT': fingerprint,
    };
  }
}

Advanced Use Cases

1. Device Change Detection

In some applications, detecting when a user's device changes can be important for security. Here's how to implement device change detection:

// core/domains/security/deviceChangeDetector.ts
import { HeaderProvider } from '@/core/shared/api/headers/types';
import { API_HEADERS } from '@/core/shared/constants/headers';
import { useAuthStore } from '@/core/domains/auth/store';
import { storage } from '@/core/shared/utils/storage';
 
export class DeviceChangeDetector {
  /**
   * Checks if the current device differs from the last authenticated device
   * @param userId The ID of the current user
   * @param currentDeviceId The current device UUID
   */
  async checkDeviceChanged(userId: string, currentDeviceId: string): Promise<boolean> {
    const storageKey = `last_device_${userId}`;
    const lastDeviceId = await storage.getSecureItem(storageKey);
    
    // If no previous device ID or same device, no change detected
    if (!lastDeviceId || lastDeviceId === currentDeviceId) {
      // Update the last device ID
      await storage.setSecureItem(storageKey, currentDeviceId);
      return false;
    }
    
    // Device has changed
    // Update the stored device ID
    await storage.setSecureItem(storageKey, currentDeviceId);
    return true;
  }
}
 
/**
 * Hook to check device changes on login
 */
export function useDeviceChangeDetection() {
  const deviceChangeDetector = new DeviceChangeDetector();
  
  const checkDeviceChange = async (deviceId: string): Promise<boolean> => {
    const { user } = useAuthStore.getState();
    
    if (!user) {
      return false;
    }
    
    return deviceChangeDetector.checkDeviceChanged(user.id, deviceId);
  };
  
  return { checkDeviceChange };
}

2. Network Quality Headers

Extending device headers with network information can help backends optimize responses based on connection quality:

// core/shared/api/headers/providers/networkQualityHeaderProvider.ts
import { HeaderProvider } from '../types';
import { API_HEADERS } from '@/core/shared/constants/headers';
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
 
export class NetworkQualityHeaderProvider implements HeaderProvider {
  id = 'network-quality';
  priority = 85; // Just below device headers
  
  async getHeaders(): Promise<Record<string, string | undefined>> {
    try {
      const netInfo = await NetInfo.fetch();
      
      return {
        'X-CONNECTION-TYPE': netInfo.type,
        'X-CONNECTION-EFFECTIVE-TYPE': netInfo.details?.effectiveType,
        ...(netInfo.isConnected !== null ? { 'X-CONNECTED': netInfo.isConnected.toString() } : {}),
        ...(typeof netInfo.details?.isConnectionExpensive === 'boolean' 
            ? { 'X-EXPENSIVE-CONNECTION': netInfo.details.isConnectionExpensive.toString() } 
            : {}),
      };
    } catch (error) {
      console.warn('Failed to get network information:', error);
      return {};
    }
  }
  
  /**
   * Subscribe to network changes and update network quality metrics
   */
  subscribeToNetworkChanges(callback?: (state: NetInfoState) => void): () => void {
    return NetInfo.addEventListener(state => {
      if (callback) {
        callback(state);
      }
    });
  }
}

Testing Strategies

1. Unit Testing the Provider

// __tests__/core/shared/api/headers/providers/deviceHeaderProvider.test.ts
import { DeviceHeaderProvider } from '@/core/shared/api/headers/providers/deviceHeaderProvider';
import DeviceInfo from 'react-native-device-info';
import { API_HEADERS } from '@/core/shared/constants/headers';
import { storage } from '@/core/shared/utils/storage';
 
// Mock dependencies
jest.mock('react-native-device-info');
jest.mock('@/core/shared/utils/storage');
 
describe('DeviceHeaderProvider', () => {
  let provider: DeviceHeaderProvider;
  
  beforeEach(() => {
    // Reset mocks
    jest.clearAllMocks();
    
    // Setup default mock values
    (DeviceInfo.getVersion as jest.Mock).mockReturnValue('1.0.0');
    (DeviceInfo.getBuildNumber as jest.Mock).mockReturnValue('42');
    (DeviceInfo.getDeviceType as jest.Mock).mockReturnValue('Handset');
    (DeviceInfo.getModel as jest.Mock).mockResolvedValue('iPhone 12');
    (DeviceInfo.getSystemVersion as jest.Mock).mockReturnValue('14.5');
    (DeviceInfo.getCarrierSync as jest.Mock).mockReturnValue('T-Mobile');
    (DeviceInfo.getUniqueId as jest.Mock).mockResolvedValue('device-123');
    
    // Mock storage
    (storage.getSecureItem as jest.Mock).mockResolvedValue(null);
    (storage.setSecureItem as jest.Mock).mockResolvedValue(undefined);
    
    // Create provider instance
    provider = new DeviceHeaderProvider();
  });
  
  it('should retrieve device UUID from storage if it exists', async () => {
    // Setup storage mock to return existing device ID
    (storage.getSecureItem as jest.Mock).mockResolvedValue('stored-device-id');
    
    const headers = await provider.getHeaders();
    
    // Assert stored device ID is used
    expect(headers[API_HEADERS.DEVICE_UUID]).toBe('stored-device-id');
    
    // Verify storage was checked but not set
    expect(storage.getSecureItem).toHaveBeenCalledWith('device_uuid');
    expect(storage.setSecureItem).not.toHaveBeenCalled();
  });
  
  it('should create and store new device UUID if none exists', async () => {
    // Ensure storage returns null (no existing ID)
    (storage.getSecureItem as jest.Mock).mockResolvedValue(null);
    
    const headers = await provider.getHeaders();
    
    // Assert device ID is retrieved from DeviceInfo and stored
    expect(headers[API_HEADERS.DEVICE_UUID]).toBe('device-123');
    expect(storage.setSecureItem).toHaveBeenCalledWith('device_uuid', 'device-123');
  });
  
  it('should include all required device headers', async () => {
    const headers = await provider.getHeaders();
    
    // Verify all expected headers are present
    expect(headers[API_HEADERS.DEVICE_UUID]).toBeDefined();
    expect(headers[API_HEADERS.DEVICE_TYPE]).toBe('Handset');
    expect(headers[API_HEADERS.APP_VERSION]).toBe('1.0.0+42');
    expect(headers[API_HEADERS.OS_VERSION]).toContain('14.5');
    expect(headers[API_HEADERS.DEVICE_MODEL]).toBe('iPhone 12');
    expect(headers[API_HEADERS.CARRIER]).toBe('T-Mobile');
  });
  
  it('should handle errors from DeviceInfo gracefully', async () => {
    // Make DeviceInfo.getUniqueId throw an error
    (DeviceInfo.getUniqueId as jest.Mock).mockRejectedValue(new Error('Failed to get unique ID'));
    
    // Mock the UUID generation
    const mockUuid = 'generated-uuid-123';
    jest.spyOn(provider as any, 'generateUuidV4').mockReturnValue(mockUuid);
    
    const headers = await provider.getHeaders();
    
    // Verify fallback UUID was used
    expect(headers[API_HEADERS.DEVICE_UUID]).toBe(mockUuid);
    expect(storage.setSecureItem).toHaveBeenCalledWith('device_uuid', mockUuid);
  });
});

2. Integration Testing with API Client

// __tests__/integration/api/deviceHeadersIntegration.test.ts
import { authenticatedApi } from '@/core/shared/api/client';
import { DeviceHeaderProvider } from '@/core/shared/api/headers/providers/deviceHeaderProvider';
import { API_HEADERS } from '@/core/shared/constants/headers';
import MockAdapter from 'axios-mock-adapter';
 
describe('Device Headers Integration', () => {
  let mockAxios: MockAdapter;
  
  beforeEach(() => {
    // Setup mock for the Axios instance
    mockAxios = new MockAdapter((authenticatedApi as any).client);
    
    // Register device header provider for testing
    authenticatedApi.registerHeaderProvider(new DeviceHeaderProvider());
  });
  
  afterEach(() => {
    mockAxios.restore();
  });
  
  it('should include device headers in API requests', async () => {
    // Setup mock response
    mockAxios.onGet('/test-endpoint').reply((config) => {
      // Check that device headers are present
      expect(config.headers[API_HEADERS.DEVICE_UUID]).toBeDefined();
      expect(config.headers[API_HEADERS.DEVICE_TYPE]).toBeDefined();
      expect(config.headers[API_HEADERS.APP_VERSION]).toBeDefined();
      
      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);
  });
});

Security Considerations

When implementing device headers, consider these security best practices:

  1. Storage Security: Always store device identifiers in secure storage like Keychain (iOS) or EncryptedSharedPreferences (Android).

  2. Identifier Persistence:

    • (Do ✅) Use non-resettable identifiers for fraud prevention and security-critical features.
    • (Do ✅) Use resettable identifiers for analytics and user experience features.
    • (Consider 🤔) Supporting "reset tracking identifiers" functionality for privacy-conscious users.
  3. Header Transport:

    • (Do ✅) Always send device headers over HTTPS connections.
    • (Don't ❌) Include sensitive device information that could compromise user privacy.
  4. PII Considerations:

    • (Don't ❌) Include personally identifiable information in device headers.
    • (Don't ❌) Use actual device names that may contain user names.
    • (Do ✅) Consider hashing any potentially user-identifying values.
  5. Compliance:

    • (Do ✅) Ensure device tracking complies with relevant privacy regulations (GDPR, CCPA, etc.).
    • (Do ✅) Disclose device tracking in your privacy policy.
    • (Do ✅) Allow users to opt-out of non-essential tracking when required by regulations.

Best Practices

  1. Initialization:

    • Initialize device header providers early in the application lifecycle to ensure consistent tracking.
    • Consider deferring fingerprinting to a background task if it's computationally expensive.
  2. Header Retention:

    • (Do ✅) Keep device headers consistent across the application session.
    • (Do ✅) Cache device information to avoid repeated expensive device queries.
    • (Consider 🤔) Updating device information periodically (e.g., daily) for long-running apps.
  3. Performance:

    • Cache device information that doesn't change frequently.
    • Use lazy loading for expensive device information operations.
    • Consider batching device information updates.
  4. Troubleshooting:

    • Include device headers in application logs for easier debugging.
    • Develop a system to correlate backend and client-side logs using device identifiers.

Summary

Device identification headers provide critical context for backend services to understand the client environment. By implementing robust device tracking using the HeaderProvider pattern, we can enable device-specific optimizations, enhance security, and improve analytics across our application. The implementation patterns provided in this guide work seamlessly with our established API client architecture while maintaining appropriate security and privacy safeguards.

Key takeaways:

  • Use the DeviceHeaderProvider to consistently attach device context to all API requests
  • Securely persist device identifiers across app sessions
  • Consider enhanced implementations for more sophisticated device fingerprinting
  • Always balance device tracking needs with privacy and security considerations
  • Integrate with analytics systems for better user experience monitoring