// Polyfill for crypto.getRandomValues() required by ULID import 'react-native-get-random-values'; import { MpDataLayerHelper } from './eedl/eedl'; import type { AppCustomerInfo, AppPageLoad, MapLike, MpDeviceInfo, SdkInitOptions, } from './common/app-types'; import { DataStore } from './common/data-store'; import { Logger } from './common/logger'; import { Reporter } from './common/reporter'; import { DataElementProcessor } from './processors/data-element.processor'; import { TagProcessor } from './processors/tag.processor'; import { Utils } from './common/utils'; import { QcProcessor } from './processors/qc.processor'; import { NetworkService } from './common/network-service'; import { EventBus } from './common/event-bus'; import { VisitIdProcessor } from './processors/visit-id.processor'; import { GeoLocationProcessor } from './processors/geo-location.processor'; import { DeviceInfoHelper } from './common/device-info-helper'; import { DeepLinkHelper } from './common/deeplink-helper'; import { StorageHelper } from './common/storage-helper'; const DL_INIT_EVENT = 'page_load'; // Maximum number of events to buffer before SDK is ready const MAX_BUFFERED_EVENTS = 500; // Type for buffered events interface BufferedEvent { type: 'pageLoad' | 'event'; name?: string; // event name for recordEvent data: any; } class MagicPixelImpl { private static dl: MpDataLayerHelper = new MpDataLayerHelper( 'mpDlEvent', 'manual', DL_INIT_EVENT, '_mpRnDataLayer' ); private static customerInfo: AppCustomerInfo; private static customerIdentifiers: MapLike = {}; private static deepLinkUrl: string | undefined = undefined; private static orgId: string | undefined = undefined; private static firstAppLaunch = true; // SDK readiness state and event buffer private static isReady = false; private static isInitializing = false; private static eventBuffer: BufferedEvent[] = []; private static onInitFailureCallback: ((error: Error) => void) | undefined; /** * Initialize the MagicPixel SDK * This method is synchronous - it stores config immediately and runs async setup in background * Events called before async setup completes are buffered and processed once ready * @param options SDK initialization options */ static init(options: SdkInitOptions): void { // Guard against multiple init() calls if (this.isReady) { Logger.logDbg( 'MagicPixel SDK already initialized. Ignoring duplicate init() call.' ); return; } if (this.isInitializing) { Logger.logDbg( 'MagicPixel SDK initialization in progress. Ignoring duplicate init() call.' ); return; } this.isInitializing = true; // Sync setup - happens immediately Logger.setLogLevel(options.logLevel ?? 'none'); this.orgId = options.orgId; this.onInitFailureCallback = options.onInitFailure; Logger.logDbg('MagicPixel SDK init started'); // Async setup - runs in background, events are buffered until complete this.initAsync(options).catch((err) => { Logger.logError('MagicPixel SDK initialization failed:', err); Reporter.reportError('sdk_init', err); // Reset initializing flag so developer can retry if needed this.isInitializing = false; // Call failure callback if provided if (this.onInitFailureCallback) { const error = err instanceof Error ? err : new Error(String(err)); this.onInitFailureCallback(error); } }); } /** * Shutdown the SDK and release all resources * Call this when the app is closing or when you need to reinitialize the SDK * After shutdown, you must call init() again to use the SDK */ static shutdown(): void { Logger.logDbg('MagicPixel SDK shutting down...'); // Clean up deeplink listener and callback DeepLinkHelper.cleanup(); // Clear all EventBus listeners to prevent duplicates on reinit EventBus.clearAll(); // Clear session-scoped data element values (in-memory store) StorageHelper.clearSessionStore(); // Clear event buffer this.eventBuffer = []; // Reset state flags this.isReady = false; this.isInitializing = false; this.onInitFailureCallback = undefined; // Reset customer data this.customerInfo = undefined as any; this.customerIdentifiers = {}; this.deepLinkUrl = undefined; this.firstAppLaunch = true; // Shutdown EEDL this.dl.shutdown(); Logger.logDbg('MagicPixel SDK shutdown complete'); } /** * Async initialization - runs in background */ private static async initAsync(options: SdkInitOptions): Promise { await NetworkService.refreshClientSdkJson(options); if (!DataStore.isDataStoreReady()) { throw new Error( 'MagicPixel SDK: DataStore not ready after config fetch. Initialization failed.' ); } // Initialize StorageHelper for data element storage duration // This loads visitor-scoped values from AsyncStorage and clears expired ones (>30 days) await StorageHelper.initialize(); // Auto-detect device info - always use detected values to prevent hardcoded values const autoDetectedInfo = await DeviceInfoHelper.getAppInfo(); Logger.logDbg('Auto-detected device info:', autoDetectedInfo); // Set device type from auto-detection (Expo/react-native-device-info) // This is more reliable than dimension-based detection which fails on high-res phones const detectedDeviceType = autoDetectedInfo.is_tablet ? 'tablet' : 'mobile'; DataStore.overrideDeviceType(detectedDeviceType); Logger.logDbg('Device type set from auto-detection:', detectedDeviceType); // Always use auto-detected app_version (ignore options.app_version) this.setAppVersion(autoDetectedInfo.app_version); // Set device info with auto-detected values (cannot be overridden) await this.setDeviceInfo(); await VisitIdProcessor.init(options?.orgId); const fbp = await VisitIdProcessor.getFacebookFBP(); if (fbp) { Logger.logDbg('Setting facebook client id', fbp); this.setFacebookClientId(fbp); } else { Logger.logDbg('No facebook client id found. not setting'); } // Auto-detect deeplink if app was opened via deeplink (e.g., ad click) // Supports: custom schemes (myapp://), universal links (https://), app links, HTTP links const initialDeepLink = await DeepLinkHelper.initialize((url, linkType) => { try { Logger.logDbg(`Deeplink detected (${linkType}):`, url); // Store deeplink URL - will be attached to next page_load event this.setDeepLinkUrl(url); // Fire deeplink opened event // URL params will be parsed and attached to page_load event automatically this.recordEvent('deeplink_opened', { deep_link_url: url, link_type: linkType, }); } catch (err) { Logger.logError('Error processing deeplink:', err); Reporter.reportError('deeplink_callback', err); } }); if (initialDeepLink) { Logger.logDbg('App opened with initial deeplink:', initialDeepLink); } // Initialize EEDL with any global event listeners await this.dl.init({}); // Set event deduplication window if specified (default is 5000ms) if (typeof options.eventDeduplicationWindowMs !== 'undefined') { this.dl.setDeduplicationWindow(options.eventDeduplicationWindowMs); } // Make geo location API call after initialization try { await GeoLocationProcessor.makeGeoLocationApiCall(); } catch (err) { Logger.logError('Error making geo location API call:', err); } MagicPixelEventBus.on('mpDlEvent', async (evtName, payload) => { Logger.logDbg('mpDlEvent:: ', evtName, JSON.stringify(payload)); const eventName: string = payload.eventName; const eventDataModel: MapLike = payload.model ?? {}; const eventId = eventDataModel['ev._id']; Logger.logDbg('Tracking Event:: ', eventName, 'with id:: ', eventId); if (DataStore.shouldExecuteTMForEvent(eventName)) { // Call runTM directly - no queuing, EEDL manages sequential processing await this.runTM( false, `custom_event_${eventName}`, eventName, eventId, eventDataModel ); } }); const isFirstOpen = await VisitIdProcessor.isFirstOpenAfterInstall(); if (isFirstOpen) { Logger.logDbg('First open event fired'); this.recordEvent('app_first_open', {}); } this.ready(); // Mark SDK as ready and flush any buffered events this.isReady = true; this.isInitializing = false; Logger.logDbg('SDK is ready, flushing event buffer'); this.flushEventBuffer(); } static recordEvent(eventName: string, payload: MapLike): void { // Buffer event if SDK is not ready yet if (!this.isReady) { Logger.logDbg(`SDK not ready, buffering event: ${eventName}`); // Evict oldest event if buffer at capacity if (this.eventBuffer.length >= MAX_BUFFERED_EVENTS) { const evicted = this.eventBuffer.shift(); Logger.logDbg( `Event buffer at capacity (${MAX_BUFFERED_EVENTS}), evicted oldest:`, evicted?.type === 'event' ? evicted.name : 'pageLoad' ); } this.eventBuffer.push({ type: 'event', name: eventName, data: payload, }); return; } this.processRecordEvent(eventName, payload); } private static processRecordEvent(eventName: string, payload: MapLike): void { if (this.customerInfo) { const newPayload: MapLike = { ...payload, ...this.customerInfo, ...(this.customerIdentifiers ?? {}), }; this.dl.pushEvent(eventName, newPayload); } else { const newPayload: MapLike = { ...payload, ...(this.customerIdentifiers ?? {}), }; this.dl.pushEvent(eventName, newPayload); } } static ready(): void { this.dl.ready(); } /** * Flush buffered events after SDK is ready * Events are processed in the order they were received */ private static flushEventBuffer(): void { if (this.eventBuffer.length === 0) { Logger.logDbg('No buffered events to flush'); return; } Logger.logDbg(`Flushing ${this.eventBuffer.length} buffered event(s)`); // Process all buffered events in order while (this.eventBuffer.length > 0) { const event = this.eventBuffer.shift()!; if (event.type === 'pageLoad') { Logger.logDbg(`Processing buffered page load: ${event.data.page_name}`); this.processRecordPageLoad(event.data); } else if (event.type === 'event') { Logger.logDbg(`Processing buffered event: ${event.name}`); this.processRecordEvent(event.name!, event.data); } } Logger.logDbg('Event buffer flushed'); } static getDebugId(): string { return DataStore.getDebugId(); } private static async runTM( sseOnly: boolean, triggerName: string, evtName: string, evtId: string, eventData?: Record ): Promise { try { Logger.logDbg( 'Running:: ', sseOnly, triggerName, evtName, evtId, eventData ); // process all the data elements and cache them for this run await DataElementProcessor.processDataElements( DataStore.getSdkDataElements(), eventData ); const sdk = DataStore.getClientSdk(); const pcKey = DataStore.getSdkPcKey(); if (pcKey) { const pcVal = DataStore.getDataElementValue(pcKey); if (typeof pcVal !== 'undefined' && pcVal === true) { Logger.logDbg('Set: PR Comp: ', true); DataStore.setPrivacyCompliance(true); } else { Logger.logDbg('Set: PR Comp: ', false); DataStore.setPrivacyCompliance(false); } } else { // No privacy manager configured - default to true (tracking allowed) Logger.logDbg( 'Set: PR Comp: ', true, '(no privacy manager configured)' ); DataStore.setPrivacyCompliance(true); } if (DataStore.getSdkPageLangKey()) { DataStore.setDataElement( 'page_lang_key', DataStore.getSdkPageLangKey() ); DataStore.setPageLang( DataStore.getDataElementValue('page_lang_key') as string ); } else { DataStore.setDataElement('page_lang_key', null); } //set page_browser default data element with page_os because backend expects this DataStore.setDataElement( 'page_browser', DataStore.getOperatingSystem().toLowerCase() ); Reporter.initReporter( sdk.s.ev, sdk.s.ev_id, DataStore.getDeviceType(), DataStore.getPageLang(), DataStore.getDeviceOs(), DataStore.getPageName(), evtName, evtId, DataStore.getClientDownStream(), DataStore.getCoreVersion(), [] ); // Store developer payload (base64 encoded) for report if (eventData) { Reporter.reportDevPayload(eventData, evtId); } const validQCList = QcProcessor.processQc( DataStore.getSdkQC(), evtName, evtId ); if (validQCList?.length > 0) { Logger.logDbg('Found Valid QCs to process', DataStore.getValidQcInfo()); TagProcessor.processTags(evtName, evtId) .then(() => { this._fireTM(sdk.s.ev, sdk.s.ev_id, evtName, evtId, eventData); }) .catch((err) => { Logger.logError('Error processing tag lists.', err); Reporter.reportError('i::processTags', err); this._fireTM(sdk.s.ev, sdk.s.ev_id, evtName, evtId, eventData); }); } else { this._fireTM(sdk.s.ev, sdk.s.ev_id, evtName, evtId, eventData); } } catch (runTMErr) { Logger.logError('Error in runTM', runTMErr); Reporter.reportError('m::runTM', runTMErr); } } private static _fireTM( envName: string, envId: string, evtName: string, evtId: string, eventData?: Record ): void { // increment visit_depth if event name is page_load because that's the only way we can track page views in an app for now if (evtName === DL_INIT_EVENT) { // Synchronous increment - no race condition possible VisitIdProcessor.incrementVisitDepth(); } // Continue immediately after sync increment this._fireTMPrivate(envName, envId, evtName, evtId, eventData); } private static _fireTMPrivate( envName: string, envId: string, evtName: string, evtId: string, eventData?: Record ): void { if (DataStore.hasOneSSTTag() && DataStore.shouldFireSstForEvent(evtName)) { Reporter.postSST( DataStore.getDataElements(), envName, envId, DataStore.getSSTDownStream(), evtName, evtId, eventData ).catch((err) => { Reporter.reportError('l::postSST', err); }); } // Note: EEDL handles queue progression via its own .finally() block // No need to call processNext() here - it would be redundant } static setCustomerInfo(customerInfo: AppCustomerInfo): void { this.customerInfo = customerInfo; this.recordEvent('idl_attribution_link', customerInfo); } static setFirebaseAppInstanceId(instanceId: string): void { this.customerIdentifiers.firebase_instance_id = instanceId; } static setDeepLinkUrl(deepLinkUrl: string): void { this.deepLinkUrl = deepLinkUrl; } /** * Set device info with auto-detected values * Standard fields (os_version, device_model_name, package_name) are always auto-detected * Optional customFields parameter allows adding custom tracking fields only * @param customFields Optional custom fields to add (cannot override standard fields) */ static async setDeviceInfo( customFields?: Record ): Promise { // Always auto-detect standard device info (cannot be overridden) const autoDetectedInfo = await DeviceInfoHelper.getAppInfo(); // Standard fields are always auto-detected to prevent hardcoded values const deviceInfo: MpDeviceInfo = { os_version: autoDetectedInfo.os_version, device_model_name: autoDetectedInfo.device_model_name, package_name: autoDetectedInfo.package_name, }; // Add any custom fields (if provided) if (customFields) { // Filter out attempts to override standard fields const allowedCustomFields = { ...customFields }; delete allowedCustomFields.os_version; delete allowedCustomFields.device_model_name; delete allowedCustomFields.package_name; Object.assign(deviceInfo, allowedCustomFields); if ( Object.keys(allowedCustomFields).length !== Object.keys(customFields).length ) { Logger.logDbg( 'Warning: Attempted to override standard device fields. Using auto-detected values instead.' ); } } Logger.logDbg('Setting device info:', deviceInfo); DataStore.setDeviceInfo(deviceInfo); } static setFacebookClientId(fbp: string): void { this.customerIdentifiers.fbp = fbp; } private static setAppVersion(appVersion?: string): void { if (appVersion && appVersion?.trim()?.length > 0) { this.customerIdentifiers.app_version = appVersion; } } static recordPageLoad(pageLoadInfo: AppPageLoad): void { // Buffer page load if SDK is not ready yet if (!this.isReady) { Logger.logDbg( `SDK not ready, buffering page load: ${pageLoadInfo.page_name}` ); // Evict oldest event if buffer at capacity if (this.eventBuffer.length >= MAX_BUFFERED_EVENTS) { const evicted = this.eventBuffer.shift(); Logger.logDbg( `Event buffer at capacity (${MAX_BUFFERED_EVENTS}), evicted oldest:`, evicted?.type === 'event' ? evicted.name : 'pageLoad' ); } this.eventBuffer.push({ type: 'pageLoad', data: { ...pageLoadInfo }, // Clone to preserve original data }); return; } this.processRecordPageLoad(pageLoadInfo); } private static processRecordPageLoad(pageLoadInfo: AppPageLoad): void { // Retry visitor ID fetch if it failed during init (fire and forget) VisitIdProcessor.retryVisitorIdIfNeeded().catch((err) => { Logger.logError('Error retrying visitor ID fetch:', err); }); pageLoadInfo.is_entry = this.firstAppLaunch ? 1 : 0; // Use stored deepLinkUrl if it exists AND either: // - deep_link_url wasn't provided in pageLoadInfo, OR // - deep_link_url is empty/falsy (defensive programming for developer mistakes) if (this.deepLinkUrl && !pageLoadInfo.deep_link_url) { pageLoadInfo.deep_link_url = this.deepLinkUrl; } // Set page_url for attribution framework // Priority: deep_link_url (if present) > constructed URL (v_://) // Using deep_link_url allows urlp data elements to extract attribution params (gclid, fbclid, etc.) if (pageLoadInfo.deep_link_url) { pageLoadInfo.page_url = pageLoadInfo.deep_link_url; Logger.logDbg( 'Setting page_url from deep_link_url:', pageLoadInfo.page_url ); } else if (this.orgId && pageLoadInfo.page_name) { pageLoadInfo.page_url = `v_${this.orgId}://${pageLoadInfo.page_name}`; Logger.logDbg( 'Setting page_url from orgId/page_name:', pageLoadInfo.page_url ); } else { Logger.logDbg( 'Not setting page_url. No deep_link_url and orgId or page_name is missing. Org Id is: ', this.orgId, 'page_name is: ', pageLoadInfo.page_name ); } const deviceInfo = DataStore.getDeviceInfo(); if (this.customerInfo) { const newPayload: MapLike = { ...pageLoadInfo, ...this.customerInfo, ...(this.customerIdentifiers ?? {}), page_params: Utils.parseQueryParamsToObject( pageLoadInfo?.deep_link_url ), }; if (deviceInfo) { newPayload.device_info = deviceInfo; } this.dl.pushEvent('page_load', newPayload); } else { const newPayload: MapLike = { ...pageLoadInfo, ...(this.customerIdentifiers ?? {}), page_params: Utils.parseQueryParamsToObject( pageLoadInfo?.deep_link_url ), }; if (deviceInfo) { newPayload.device_info = deviceInfo; } this.dl.pushEvent('page_load', newPayload); } // only set for first page load event and remove after that this.deepLinkUrl = undefined; this.firstAppLaunch = false; } /** * Set user information for tracking * @param userInfo User information object */ static setUserInfo(userInfo: { pid?: string; email?: string; phone?: string; country?: string; city?: string; state?: string; fName?: string; lName?: string; zip?: string; }): void { this.dl.pushEvent('user_info', userInfo); } /** * Persist custom data to storage * @param key Storage key * @param value Value to store */ static persistData(key: string, value: any): void { this.dl.pushEvent('persist', { key, value }); } /** * Clear specific persisted data * @param keys Array of keys to clear */ static clearPersistedData(keys: string[]): void { this.dl.pushEvent('clear', keys); } /** * Set data layer variables * @param data Data to set in the data layer */ static setDataLayer(data: MapLike): void { this.dl.pushEvent('set', data); } /** * Register a custom event processor * @param eventType Event type to listen for * @param processor Function to process the event */ static registerEventProcessor( eventType: string, processor: (payload: any) => void ): void { this.dl.registerProcessor(eventType, processor); } /** * Get current data layer state * @returns Current data layer object */ static getDataLayer(): MapLike { return this.dl.getDl(); } /** * Get event state tracker * @returns Array of all tracked events */ static getEventState(): any[] { return this.dl.getState(); } /** * Check if a specific event has happened * @param eventName Event name to check * @returns True if event has occurred */ static hasEventHappened(eventName: string): boolean { return this.dl.hasEventHappened(eventName); } } export const MagicPixelEventBus = EventBus; export const MagicPixel = MagicPixelImpl; export { DeepLinkHelper, DeepLinkType } from './common/deeplink-helper';