import { NativeModules, Platform } from 'react-native'; /** * Environment constants */ const ENVIRONMENT = { PRODUCTION: 'Production', SANDBOX: 'Sandbox', } as const; /** * Debug logging flag - disabled by default */ let debugEnabled = false; /** * Log levels for debug logging */ enum LogLevel { DEBUG = 'DEBUG', INFO = 'INFO', ERROR = 'ERROR', } /** * Log entry structure for storage */ export interface LogEntry { id: string; timestamp: number; level: 'DEBUG' | 'INFO' | 'ERROR'; message: string; data?: any; } /** * Maximum number of logs to store (FIFO when limit reached) */ const MAX_LOG_ENTRIES = 1000; /** * In-memory log storage array */ const logStorage: LogEntry[] = []; /** * Generates a unique ID for log entries */ function generateLogId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Adds a log entry to storage, removing oldest if limit reached */ function addLogToStorage(level: LogLevel, message: string, data?: any): void { const logEntry: LogEntry = Object.freeze({ id: generateLogId(), timestamp: Date.now(), level, message, data, }); logStorage.push(logEntry); // Remove oldest logs if limit exceeded (FIFO) if (logStorage.length > MAX_LOG_ENTRIES) { logStorage.shift(); } } /** * Internal logger utility that checks debug flag before logging */ const logger = { debug: (message: string, ...args: any[]) => { if (debugEnabled) { console.debug(`[BureauSDK] [${LogLevel.DEBUG}] ${message}`, ...args); addLogToStorage( LogLevel.DEBUG, message, args.length > 0 ? args : undefined ); } }, info: (message: string, ...args: any[]) => { if (debugEnabled) { console.log(`[BureauSDK] [${LogLevel.INFO}] ${message}`, ...args); addLogToStorage( LogLevel.INFO, message, args.length > 0 ? args : undefined ); } }, error: (message: string, ...args: any[]) => { if (debugEnabled) { console.error(`[BureauSDK] [${LogLevel.ERROR}] ${message}`, ...args); addLogToStorage( LogLevel.ERROR, message, args.length > 0 ? args : undefined ); } }, }; /** * Masks sensitive data for logging (shows first 4 and last 4 characters) */ function maskSensitiveData(value: string): string { if (!value || value.length <= 8) { return '***'; } return `${value.substring(0, 4)}...${value.substring(value.length - 4)}`; } /** * Default timeout in milliseconds (15 seconds) */ const DEFAULT_TIMEOUT_MS = 15000; /** * Result keys for authentication response */ const RESULT_KEYS = { STATUS: 'status', MESSAGE: 'message', } as const; /** * Error messages */ const ERROR_MESSAGES = { INVALID_AUTHENTICATION_RESULT: 'Invalid authentication result from native module', } as const; /** * Validates that the authentication result from native module has the expected structure * * @param result - The result object from native module * @returns True if result is valid, false otherwise */ function validateAuthenticationResult(result: any): boolean { return ( result && typeof result[RESULT_KEYS.STATUS] === 'string' && typeof result[RESULT_KEYS.MESSAGE] === 'string' ); } /** * Detects if a timeout occurred based on result status and elapsed time * * @param result - The authentication result object * @param timeOutInMs - Configured timeout duration in milliseconds * @param startTime - Timestamp when authentication started * @returns True if timeout is detected, false otherwise */ function detectTimeoutInResult( result: any, timeOutInMs: number, startTime: number ): boolean { if (!result || !result[RESULT_KEYS.STATUS]) { return false; } const elapsedTime = Date.now() - startTime; const status = result[RESULT_KEYS.STATUS] as string; // Timeout detected if: // 1. Status is 'awaitingProviderAck' and elapsed time >= timeout duration // 2. Status is 'unknown' and elapsed time >= timeout duration // 3. Status is explicitly 'timeout' and elapsed time >= timeout duration const isTimeoutCandidate = status === 'awaitingProviderAck' || status === 'unknown' || status === 'timeout'; return isTimeoutCandidate && elapsedTime >= timeOutInMs; } /** * Creates a standardized timeout error log context object * * @param clientId - Bureau client ID (will be masked) * @param sessionId - Session ID (will be masked) * @param timeOutInMs - Configured timeout duration in milliseconds * @param elapsedTime - Actual elapsed time in milliseconds * @param env - Environment (Production/Sandbox) * @returns Object with timeout logging context */ function createTimeoutErrorLogContext( clientId: string, sessionId: string, timeOutInMs: number, elapsedTime: number, env: string ): Record { const percentage = timeOutInMs > 0 ? ((elapsedTime / timeOutInMs) * 100).toFixed(1) : 'N/A'; return { event: 'Timeout error detected', duration: `${timeOutInMs}ms`, elapsed: `${elapsedTime}ms (${percentage}% of timeout)`, sessionId: maskSensitiveData(sessionId), clientId: maskSensitiveData(clientId), env, platform: Platform.OS, }; } /** * Error message shown when the native module is not properly linked */ const LINKING_ERROR = `The package 'react-native-bureauid-fraud-sdk' doesn't seem to be linked. Make sure: \n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n'; /** * Native module bridge to BureauOtlModule * Provides a proxy that throws a helpful error if the module is not linked */ const BureauOtlModule = NativeModules.BureauOtlModule ? NativeModules.BureauOtlModule : new Proxy( {}, { get() { throw new Error(LINKING_ERROR); }, } ); /** * AuthenticationStatus enum values matching the native SDK * This represents all possible authentication status cases from Bureau SDK */ export type AuthenticationStatus = | 'networkAndOperatorMismatch' | 'authFailure' | 'networkNotSupported' | 'operatorNotSupported' | 'operatorAndNetworkNotSupported' | 'awaitingProviderAck' | 'authValidationError' | 'duplicateCorrelationId' | 'integrationFailure' | 'authStateExpired' | 'unauthorized' | 'countryNotSupported' | 'rateLimitExceeded' | 'internalServerError' | 'networkUnavailable' | 'wifiDetectedAndNoDataNetwork' | 'completed' | 'timeout' | 'unknown'; /** * Authentication result containing the full AuthenticationStatus enum and all properties * This is a structured object returned from the native SDK, matching the SDK's AuthenticationStatus */ export interface AuthenticationResult { /** * Authentication status enum case name from SDK * This matches the AuthenticationStatus enum from iOS/Android SDKs */ readonly status: AuthenticationStatus; /** * Human-readable message from SDK (associated value from enum) */ readonly message: string; /** * Total latency in milliseconds (from start to finish) */ totalLatency?: number; /** * SDK initialization latency in milliseconds */ initLatency?: number; /** * Authentication call latency in milliseconds */ authLatency?: number; } /** * Creates a properly typed AuthenticationResult object * This ensures type safety and proper object structure */ function createAuthenticationResult( status: AuthenticationStatus, message: string, latency?: { totalLatency?: number; initLatency?: number; authLatency?: number; } ): AuthenticationResult { const result: AuthenticationResult = { status, message, }; // Add latency fields if provided if (latency) { if (latency.totalLatency !== undefined) { result.totalLatency = latency.totalLatency; } if (latency.initLatency !== undefined) { result.initLatency = latency.initLatency; } if (latency.authLatency !== undefined) { result.authLatency = latency.authLatency; } } return Object.freeze(result); } /** * Enables or disables debug logging * * @param enabled - Whether to enable debug logging */ export function setDebugEnabled(enabled: boolean): void { // Store the previous state for logging purposes const previousState = debugEnabled; // If we are turning logging off, log this event before disabling it if (!enabled && previousState) { // Log the disable event while logging is still enabled console.log(`[BureauSDK] [INFO] Debug logging disabled`); addLogToStorage(LogLevel.INFO, 'Debug logging disabled'); } // Update the debug enabled state debugEnabled = enabled; // If we are turning logging on, log this event after enabling it if (enabled) { console.log(`[BureauSDK] [INFO] Debug logging enabled`); addLogToStorage(LogLevel.INFO, 'Debug logging enabled'); } // Also set debug flag on native modules (use proxy so missing linkage throws a clear error) const nativeModule = BureauOtlModule as any; if (nativeModule && typeof nativeModule.setDebugEnabled === 'function') { try { nativeModule.setDebugEnabled(enabled); } catch (error) { // Log warning if we fail to set native debug state, but only if logging is enabled if (enabled) { console.warn( '[BureauSDK] Failed to enable debug logging on native module:', error ); } } } } /** * Fetches native SDK logs from the native modules (iOS/Android) * Merges them with React Native logs and returns combined list * @returns Promise resolving to array of log entries (most recent first) */ export async function fetchNativeLogs(): Promise { try { const nativeModule = BureauOtlModule as any; if (!nativeModule || typeof nativeModule.getNativeLogs !== 'function') { return []; } const nativeLogs = await nativeModule.getNativeLogs(); if (!Array.isArray(nativeLogs)) { return []; } // Convert native log format to LogEntry format const convertedLogs: LogEntry[] = nativeLogs.map((log: any) => ({ id: log.id || generateLogId(), timestamp: log.timestamp || Date.now(), level: (log.level || 'INFO').toUpperCase() as 'DEBUG' | 'INFO' | 'ERROR', message: log.message || '', data: log.data, })); return convertedLogs; } catch (error) { if (debugEnabled) { logger.error('Failed to fetch native logs', { error }); } return []; } } /** * Retrieves all stored log entries, including native logs * Fetches native logs and merges with React Native logs * @returns Promise resolving to array of log entries (most recent first) */ export async function getLogs(): Promise { const reactNativeLogs = [...logStorage]; // Fetch native logs if debug is enabled let nativeLogs: LogEntry[] = []; if (debugEnabled) { try { nativeLogs = await fetchNativeLogs(); } catch (error) { // Silently fail if native logs can't be fetched if (debugEnabled) { logger.debug('Could not fetch native logs', { error }); } } } // Merge logs and sort by timestamp (most recent first) const allLogs = [...reactNativeLogs, ...nativeLogs]; allLogs.sort((a, b) => b.timestamp - a.timestamp); return allLogs; } /** * Retrieves all stored log entries synchronously (React Native logs only) * For backward compatibility - use getLogs() for native logs * @returns Array of log entries (most recent first) */ export function getLogsSync(): LogEntry[] { return [...logStorage].reverse(); // Return reversed to show most recent first } /** * Retrieves log entries filtered by level, including native logs * @param level - Log level to filter by * @returns Promise resolving to array of log entries matching the level (most recent first) */ export async function getLogsByLevel( level: 'DEBUG' | 'INFO' | 'ERROR' ): Promise { const allLogs = await getLogs(); return allLogs.filter((log) => log.level === level); } /** * Retrieves log entries filtered by level synchronously (React Native logs only) * For backward compatibility - use getLogsByLevel() for native logs * @param level - Log level to filter by * @returns Array of log entries matching the level (most recent first) */ export function getLogsByLevelSync( level: 'DEBUG' | 'INFO' | 'ERROR' ): LogEntry[] { return logStorage.filter((log) => log.level === level).reverse(); // Return reversed to show most recent first } /** * Clears all stored log entries */ export function clearLogs(): void { logStorage.length = 0; } /** * Gets the current debug enabled state * @returns True if debug logging is enabled, false otherwise */ export function isDebugEnabled(): boolean { return debugEnabled; } /** * Validates MSISDN format against allowed country codes * * @param msisdn - Mobile number as string * @param allowedCodes - Array of allowed country code prefixes (e.g., ["91", "1"]) * @returns True if MSISDN starts with an allowed country code, false otherwise */ function validateMSISDNFormat(msisdn: string, allowedCodes: string[]): boolean { if (debugEnabled) { logger.debug('[validateMSISDNFormat] Entry', { msisdn: maskSensitiveData(msisdn), allowedCodesCount: allowedCodes.length, allowedCodes: allowedCodes, }); } if (allowedCodes.length === 0) { // If no codes specified, allow all (backward compatibility) if (debugEnabled) { logger.debug( '[validateMSISDNFormat] No codes specified - allowing all (backward compatibility)' ); } return true; } // Remove any non-digit characters for validation const digitsOnly = msisdn.replace(/\D/g, ''); if (debugEnabled) { logger.debug('[validateMSISDNFormat] Step 1: Extracted digits', { originalLength: msisdn.length, digitsOnlyLength: digitsOnly.length, digitsOnly: maskSensitiveData(digitsOnly), }); } const isValid = allowedCodes.some((code) => { const matches = digitsOnly.startsWith(code); if (debugEnabled) { logger.debug('[validateMSISDNFormat] Step 2: Checking code', { code, matches, digitsStart: maskSensitiveData(digitsOnly.substring(0, code.length)), }); } return matches; }); if (debugEnabled) { logger.debug('[validateMSISDNFormat] Result', { isValid, checkedCodes: allowedCodes, }); } return isValid; } /** * Authenticates a user using Bureau's One-Tap Login * * @param clientId - Bureau client ID * @param sessionId - Unique session/correlation ID * @param msisdn - Mobile number with country code * @param env - Environment: 'Production' or 'Sandbox' (default: 'Production') * @param timeOutInMs - Timeout in milliseconds (default: 15000) * @param allowedCountryCodes - Optional array of allowed country codes (e.g., ["91", "1"]). Defaults to ["91"] if not provided * @param pspCallbacks - Optional PSP callback keys for the Initiate request (iOS BureauSMV 1.2.1+) * @returns Promise resolving to AuthenticationResult with status enum and message from SDK */ export function authenticate( clientId: String, sessionId: String, msisdn: String, env: String = ENVIRONMENT.PRODUCTION, timeOutInMs: number = DEFAULT_TIMEOUT_MS, allowedCountryCodes?: string[], pspCallbacks?: string[] ): Promise { const startTime = Date.now(); const functionName = '[authenticate]'; const pspCallbackKeys = (pspCallbacks ?? []) .map((key) => String(key).trim()) .filter((key) => key.length > 0); // Step 1: Log function entry with all parameters if (debugEnabled) { logger.debug(`${functionName} Entry - Starting authentication`, { clientId: maskSensitiveData(String(clientId)), sessionId: maskSensitiveData(String(sessionId)), msisdn: maskSensitiveData(String(msisdn)), env: String(env), timeOutInMs, allowedCountryCodesProvided: allowedCountryCodes !== undefined, allowedCountryCodesCount: allowedCountryCodes?.length || 0, allowedCountryCodes: allowedCountryCodes || [], pspCallbacksCount: pspCallbackKeys.length, platform: Platform.OS, timestamp: startTime, }); } // Step 2: Parameter normalization and transformation const paramNormalizationStart = Date.now(); const clientIdStr = String(clientId); const sessionIdStr = String(sessionId); const msisdnStr = String(msisdn); const envStr = String(env); const paramNormalizationTime = Date.now() - paramNormalizationStart; if (debugEnabled) { logger.debug(`${functionName} Step 1: Parameter normalization`, { clientIdType: typeof clientId, sessionIdType: typeof sessionId, msisdnType: typeof msisdn, envType: typeof env, timeOutInMsType: typeof timeOutInMs, normalizationTimeMs: paramNormalizationTime, normalizedValues: { clientId: maskSensitiveData(clientIdStr), sessionId: maskSensitiveData(sessionIdStr), msisdn: maskSensitiveData(msisdnStr), env: envStr, timeOutInMs, }, }); } // Step 3: Country code default handling const countryCodeHandlingStart = Date.now(); const countryCodes = allowedCountryCodes && allowedCountryCodes.length > 0 ? allowedCountryCodes : ['91']; // Default to ["91"] for backward compatibility const countryCodeHandlingTime = Date.now() - countryCodeHandlingStart; if (debugEnabled) { logger.debug(`${functionName} Step 2: Country code handling`, { provided: allowedCountryCodes !== undefined, providedCount: allowedCountryCodes?.length || 0, usingDefault: !allowedCountryCodes || allowedCountryCodes.length === 0, finalCountryCodes: countryCodes, handlingTimeMs: countryCodeHandlingTime, }); } // Step 4: Country code validation const validationStart = Date.now(); const validationResult = validateMSISDNFormat(msisdnStr, countryCodes); const validationTime = Date.now() - validationStart; if (debugEnabled) { logger.debug(`${functionName} Step 3: Country code validation`, { msisdn: maskSensitiveData(msisdnStr), allowedCodes: countryCodes, validationResult, validationTimeMs: validationTime, elapsedFromStart: Date.now() - startTime, }); } if (!validationResult) { const codesStr = countryCodes.join(', '); const errorMessage = `Invalid country code. Supported codes: ${codesStr}`; const totalTime = Date.now() - startTime; if (debugEnabled) { logger.error(`${functionName} Step 3: Validation failed`, { msisdn: maskSensitiveData(msisdnStr), allowedCodes: countryCodes, errorMessage, totalExecutionTimeMs: totalTime, }); } const authResult = createAuthenticationResult( 'authValidationError', errorMessage ); if (debugEnabled) { logger.debug( `${functionName} Step 3: Returning validation error result`, { result: authResult, totalExecutionTimeMs: totalTime, } ); } return Promise.resolve(authResult); } if (debugEnabled) { logger.debug( `${functionName} Step 3: Validation passed - proceeding to native call` ); } // Step 5: Prepare native module call const nativeCallPrepStart = Date.now(); const nativeCallParams = { clientId: clientIdStr, sessionId: sessionIdStr, msisdn: msisdnStr, env: envStr.toLowerCase(), timeout: timeOutInMs, allowedCountryCodes: countryCodes, pspCallbacks: pspCallbackKeys, }; const nativeCallPrepTime = Date.now() - nativeCallPrepStart; if (debugEnabled) { logger.debug(`${functionName} Step 4: Native module call preparation`, { params: { clientId: maskSensitiveData(nativeCallParams.clientId), sessionId: maskSensitiveData(nativeCallParams.sessionId), msisdn: maskSensitiveData(nativeCallParams.msisdn), env: nativeCallParams.env, timeout: nativeCallParams.timeout, allowedCountryCodes: nativeCallParams.allowedCountryCodes, pspCallbacksCount: nativeCallParams.pspCallbacks.length, }, prepTimeMs: nativeCallPrepTime, elapsedFromStart: Date.now() - startTime, }); } // Step 6: Make native module call const nativeCallStart = Date.now(); if (debugEnabled) { logger.debug(`${functionName} Step 5: Calling native module`, { moduleAvailable: !!BureauOtlModule, callStartTime: nativeCallStart, }); } return BureauOtlModule.authenticate( clientIdStr, sessionIdStr, msisdnStr, envStr.toLowerCase(), timeOutInMs, countryCodes, pspCallbackKeys ) .then((result: any) => { const nativeCallTime = Date.now() - nativeCallStart; const responseReceivedTime = Date.now(); // Step 7: Log native module response received if (debugEnabled) { logger.debug( `${functionName} Step 6: Native module response received`, { hasResult: !!result, resultType: typeof result, resultIsNull: result === null, resultIsUndefined: result === undefined, resultKeys: result ? Object.keys(result) : [], nativeCallDurationMs: nativeCallTime, elapsedFromStart: responseReceivedTime - startTime, rawResult: result, } ); } // Step 8: Validate result structure const resultValidationStart = Date.now(); const isValidResult = validateAuthenticationResult(result); const resultValidationTime = Date.now() - resultValidationStart; if (debugEnabled) { logger.debug(`${functionName} Step 7: Result structure validation`, { isValid: isValidResult, hasResult: !!result, hasStatus: result && result[RESULT_KEYS.STATUS] !== undefined, hasMessage: result && result[RESULT_KEYS.MESSAGE] !== undefined, statusType: result && typeof result[RESULT_KEYS.STATUS], messageType: result && typeof result[RESULT_KEYS.MESSAGE], validationTimeMs: resultValidationTime, elapsedFromStart: Date.now() - startTime, }); } if (!isValidResult) { const totalTime = Date.now() - startTime; logger.error( `${functionName} Step 7: Validation failed - Invalid result format`, { result, expectedStructure: { status: 'string', message: 'string', }, actualStructure: result ? { status: typeof result[RESULT_KEYS.STATUS], message: typeof result[RESULT_KEYS.MESSAGE], } : 'null/undefined', totalExecutionTimeMs: totalTime, } ); throw new Error(ERROR_MESSAGES.INVALID_AUTHENTICATION_RESULT); } // Step 9: Extract status, message, and latency const extractionStart = Date.now(); const rawStatus = result[RESULT_KEYS.STATUS] as string; const rawMessage = result[RESULT_KEYS.MESSAGE] as string; // Extract latency fields from native result const latency = { totalLatency: result.totalLatency !== undefined ? Number(result.totalLatency) : undefined, initLatency: result.initLatency !== undefined ? Number(result.initLatency) : undefined, authLatency: result.authLatency !== undefined ? Number(result.authLatency) : undefined, }; const extractionTime = Date.now() - extractionStart; if (debugEnabled) { logger.debug(`${functionName} Step 8: Status, message, and latency extraction`, { rawStatus, rawMessage, statusLength: rawStatus?.length, messageLength: rawMessage?.length, latency, extractionTimeMs: extractionTime, elapsedFromStart: Date.now() - startTime, }); } // Step 10: Detect timeout scenarios const timeoutDetectionStart = Date.now(); const elapsedTime = Date.now() - startTime; const isTimeout = detectTimeoutInResult(result, timeOutInMs, startTime); const timeoutDetectionTime = Date.now() - timeoutDetectionStart; if (debugEnabled) { logger.debug(`${functionName} Step 9: Timeout detection`, { elapsedTimeMs: elapsedTime, timeoutThresholdMs: timeOutInMs, status: rawStatus, isTimeoutCandidate: rawStatus === 'awaitingProviderAck' || rawStatus === 'unknown' || rawStatus === 'timeout', isTimeout, timeoutPercentage: timeOutInMs > 0 ? ((elapsedTime / timeOutInMs) * 100).toFixed(1) + '%' : 'N/A', detectionTimeMs: timeoutDetectionTime, elapsedFromStart: Date.now() - startTime, }); } // Step 11: Determine final status and message let finalStatus = rawStatus as AuthenticationStatus; let finalMessage = rawMessage; if (isTimeout) { finalStatus = 'timeout'; finalMessage = `Authentication timeout after ${timeOutInMs}ms`; const timeoutContext = createTimeoutErrorLogContext( clientIdStr, sessionIdStr, timeOutInMs, elapsedTime, envStr ); if (debugEnabled) { logger.error( `${functionName} Step 10: Timeout detected - updating status`, { originalStatus: rawStatus, originalMessage: rawMessage, finalStatus, finalMessage, timeoutContext, elapsedFromStart: Date.now() - startTime, } ); } } else { if (debugEnabled) { logger.debug( `${functionName} Step 10: No timeout - using original status`, { finalStatus, finalMessage, elapsedFromStart: Date.now() - startTime, } ); } } // Step 12: Create authentication result with latency const resultCreationStart = Date.now(); const authResult = createAuthenticationResult(finalStatus, finalMessage, latency); const resultCreationTime = Date.now() - resultCreationStart; if (debugEnabled) { logger.debug( `${functionName} Step 11: Creating authentication result`, { result: authResult, creationTimeMs: resultCreationTime, elapsedFromStart: Date.now() - startTime, } ); } // Step 13: Log final outcome const totalTime = Date.now() - startTime; if (isTimeout) { logger.error(`${functionName} Step 12: Authentication timed out`, { status: authResult.status, message: authResult.message, totalExecutionTimeMs: totalTime, nativeCallDurationMs: nativeCallTime, breakdown: { paramNormalization: paramNormalizationTime, countryCodeHandling: countryCodeHandlingTime, validation: validationTime, nativeCallPrep: nativeCallPrepTime, nativeCall: nativeCallTime, resultValidation: validationTime, statusExtraction: extractionTime, timeoutDetection: timeoutDetectionTime, resultCreation: resultCreationTime, }, }); } else { logger.info(`${functionName} Step 12: Authentication completed`, { status: authResult.status, message: authResult.message, totalExecutionTimeMs: totalTime, nativeCallDurationMs: nativeCallTime, breakdown: { paramNormalization: paramNormalizationTime, countryCodeHandling: countryCodeHandlingTime, validation: validationTime, nativeCallPrep: nativeCallPrepTime, nativeCall: nativeCallTime, resultValidation: validationTime, statusExtraction: extractionTime, timeoutDetection: timeoutDetectionTime, resultCreation: resultCreationTime, }, }); } return authResult; }) .catch((error: any) => { const errorTime = Date.now(); const totalTime = errorTime - startTime; const nativeCallTime = errorTime - nativeCallStart; if (debugEnabled) { logger.error( `${functionName} Step 13: Authentication failed with error`, { errorType: error?.constructor?.name || typeof error, errorMessage: error?.message || String(error), errorStack: error?.stack, errorName: error?.name, errorCode: error?.code, errorDetails: error, totalExecutionTimeMs: totalTime, nativeCallDurationMs: nativeCallTime, context: { clientId: maskSensitiveData(clientIdStr), sessionId: maskSensitiveData(sessionIdStr), msisdn: maskSensitiveData(msisdnStr), env: envStr, timeout: timeOutInMs, countryCodes, platform: Platform.OS, }, timingBreakdown: { paramNormalization: paramNormalizationTime, countryCodeHandling: countryCodeHandlingTime, validation: validationTime, nativeCallPrep: nativeCallPrepTime, nativeCall: nativeCallTime, }, } ); } return Promise.reject(error); }); }