UNPKG

@convivainc/conviva-react-native-appanalytics

Version:

Conviva React Native Application Analytics Library

1,412 lines (1,390 loc) 199 kB
import { NativeModules, TurboModuleRegistry, AppState } from 'react-native'; import * as React from 'react'; import React__default from 'react'; import hoistNonReactStatic from 'hoist-non-react-statics'; import * as _ from 'lodash'; /* * Copyright (c) 2020-2023 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, * software distributed under the Apache License Version 2.0 is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ /** * Returns a function that accepts a side-effect function as its argument and subscribes * that function to aPromise's fullfillment, * and errHandle to aPromise's rejection. * * @param aPromise - A void Promise * @param errHandle - A function to handle the promise being rejected * @returns - A function subscribed to the Promise's fullfillment */ function safeWait(aPromise, errHandle) { return ((func) => { return (...args) => { return aPromise.then(() => func(...args)).catch((err) => errHandle(err)); }; }); } /** * Returns a function that accepts a callback function as its argument and subscribes * that function to aPromise's fullfillment, * and errHandle to aPromise's rejection. * * @param aPromise - A void Promise * @param errHandle - A function to handle the promise being rejected * @returns - A function subscribed to the Promise's fullfillment */ function safeWaitCallback(callPromise, errHandle) { return ((func) => { return (...args) => { return callPromise.then(() => func(...args)).catch((err) => errHandle(err)); }; }); } /** * Handles an error. * * @param err - The error to be handled. * @param alwaysLog - When true, the error is logged regardless of the __DEV__ flag. */ function errorHandler(err, alwaysLog = false) { if (__DEV__ || alwaysLog) { console.warn('ConvivaTracker:' + err.message); } return undefined; } /** * Helper to check whether its argument is of object type * * @param x - The argument to check. * @returns - A boolean */ function isObject$1(x) { return Object.prototype.toString.call(x) === '[object Object]'; } /* * Copyright (c) 2020-2023 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, * software distributed under the Apache License Version 2.0 is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ const isAvailable = NativeModules.RNConvivaTracker != null; if (!isAvailable) { errorHandler(new Error('Unable to access the native iOS/Android Conviva tracker, a tracker implementation with very limited functionality is used.')); } const RNConvivaTracker = NativeModules.RNConvivaTracker; /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Payload size limits aligned with the Conviva JS web tracker * (JS_DPI_ERROR_REPORTING_AND_CONFIG §4). Cross-platform parity ensures * consistent backend schema validation and payload cap enforcement. */ /** Max error message length. Matches the Conviva JS tracker limit. */ const MAX_MESSAGE_LENGTH = 2048; /** * Max stack trace length. Matches the Conviva JS tracker limit. * Minified RN bundles can produce longer stacks, but parity simplifies backend * schema validation. */ const MAX_STACK_TRACE_LENGTH = 8192; const MAX_COMPONENT_STACK_LENGTH = 4096; /** * Default rate limiter: 20 events per 1-second window, matching the Conviva JS * horizontal tracker defaults (JS_DPI_ERROR_REPORTING_AND_CONFIG §2.2). */ const DEFAULT_MAX_EVENTS_PER_WINDOW = 20; const DEFAULT_RATE_LIMIT_WINDOW_MS = 1000; /** * Circuit-breaker cooldown. After maxEvents is exceeded within the window, * the circuit OPENS and stays open for this duration. Matches the Conviva JS * horizontal tracker `disconnectDuration` parameter. */ const DEFAULT_DISCONNECT_DURATION_MS = 2000; /** * Schema URI for the Conviva application-error self-describing event. * Used for fatal JS errors via TrackerController.track(SelfDescribing(...)). * Mirrors the native ExceptionHandler pipeline (sp/ae/1-0-2). */ const APPLICATION_ERROR_SCHEMA = 'sp/ae/1-0-2'; /** * Event name for non-fatal RN errors (custom event, own metric surface). * Fatal RN errors use APPLICATION_ERROR_SCHEMA via track() instead. * Web / Horizontal JS errors remain `conviva_application_error` — unchanged. */ const NON_FATAL_ERROR_EVENT_NAME = 'conviva_non_fatal_error'; /** * Installed-state sentinel placed on `globalThis`. The slot's VALUE is a * `TrackerInstallState` record (`{ cleanups: Array<() => void> }`) rather * than a bare boolean — colocating the plugin cleanup handles with the * flag is required so a JS hot-reload / Fast Refresh cycle that replaces * the `ConvivaErrorTracker` singleton can still find and run the prior * cleanups (the new instance starts with `pluginCleanups: []`). Without * this, every reload chains a fresh Conviva handler on top of the old * one, causing duplicate reporting and unbounded handler-chain growth. * * `_applyHookState` is the single authority that reads/writes the slot; * `teardown()` clears it. Any truthy value is treated as "installed" so * an older boolean shape (from a prior tracker version still resident * after upgrade) is recognised but its cleanups are dropped — the new * install will simply replace the previously-registered handlers. */ const INSTALL_FLAG = '__conviva_error_tracker_installed__'; /** * Attribute keys that are unconditionally dropped at every consumer * attribute merge point inside the error-tracking pipeline: * - `errorTracker._dispatch` (globalAttributes + extraAttributes merge), * - `errorTracker.addAttribute` (per-key admission), * - `NativeBridgeAdapter.buildWirePayload` (wire flatten), * - `trackError` non-fatal-fallback custom-event payload. * * `Object.assign(target, source)` invokes `[[Set]]` on the target, so an * own `"__proto__"` property on a JSON-parsed source (e.g. * `JSON.parse('{"__proto__":{"polluted":true}}')`) would otherwise hit * the `__proto__` setter and pollute `Object.prototype` for the entire * realm. `constructor` and `prototype` are dropped for defence-in-depth * even on `Object.create(null)` targets so reserved prototype-chain * names cannot end up in the wire payload. */ const BLOCKED_ATTRIBUTE_KEYS = new Set([ '__proto__', 'constructor', 'prototype', ]); /* * Copyright (c) 2020-2023 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, * software distributed under the Apache License Version 2.0 is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ const logMessages = { // configuration errors customerKey: 'customerKey parameter is required to be set', appName: 'appName parameter is required to be set', namespace: 'namespace parameter is required to be set fail', endpoint: 'endpoint parameter is required to be set', network: 'networkConfig is invalid', tracker: 'trackerConfig is invalid', session: 'sessionConfig is invalid', emitter: 'emitterConfig is invalid', subject: 'subjectConfig is invalid', gdpr: 'gdprConfig is invalid', gc: 'gcConfig is invalid', remote: 'remoteConfig is invalid', clidSync: 'clidSyncConfig is invalid', // event errors context: 'invalid contexts parameter', selfDesc: 'selfDescribing event requires schema and data parameters to be set', evType: 'event argument can only be an object', screenViewReq: 'screenView event requires name as string parameter to be set', structuredReq: 'structured event requires category and action parameters to be set', pageviewReq: 'pageView event requires pageUrl parameter to be set', timingReq: 'timing event requires category, variable and timing parameters to be set', consentGReq: 'consentGranted event requires expiry, documentId and version parameters to be set', consentWReq: 'consentWithdrawn event requires all, documentId and version parameters to be set', ecomReq: 'ecommerceTransaction event requires orderId, totalValue to be set and items to be an array of valid ecommerceItems', deepLinkReq: 'deepLinkReceived event requires the url parameter to be set', messageNotificationReq: 'messageNotification event requires title, body, and trigger parameters to be set', trackCustomEvent: 'trackCustomEvent event requires name and data', revenueEventNullEvent: 'event is null. Event not sent.', revenueEventInvalidTotalOrderAmount: 'Must be a finite number. Event not sent.', revenueEventInvalidTransactionId: 'Must be a non-empty string. Event not sent.', revenueEventInvalidCurrency: 'Must be a non-empty string. Event not sent.', trackClickEvent: 'click event requires atleast one attribute', trackError: 'trackError event requires message as string parameter to be set', // custom tags contexts setCustomTags: 'setCustomTags requires tags', clearCustomTags: 'clearCustomTags requires tag keys', clearAllCustomTags: 'clearAllCustomTags requires earlier set tags', // global contexts errors gcTagType: 'tag argument is required to be a string', gcType: 'global context argument is invalid', // api error prefix createTracker: 'createTracker:', removeTracker: 'removeTracker: trackerNamespace can only be a string', getClientId: 'getClientId: failed fetching client id', setClientId: 'setClientId: clientId is invalid', // methods trackSelfDesc: 'trackSelfDescribingEvent:', trackScreenView: 'trackScreenViewEvent:', trackStructured: 'trackStructuredEvent:', trackPageView: 'trackPageView:', trackTiming: 'trackTimingEvent:', trackConsentGranted: 'trackConsentGranted:', trackConsentWithdrawn: 'trackConsentWithdrawn:', trackEcommerceTransaction: 'trackEcommerceTransaction:', trackDeepLinkReceived: 'trackDeepLinkReceivedEvent:', trackMessageNotification: 'trackMessageNotificationEvent:', trackRevenueEvent: 'trackRevenueEvent:', removeGlobalContexts: 'removeGlobalContexts:', addGlobalContexts: 'addGlobalContexts:', // setters setUserId: 'setUserId: userId can only be a string or null', setNetworkUserId: 'setNetworkUserId: networkUserId can only be a string(UUID) or null', setDomainUserId: 'setDomainUserId: domainUserId can only be a string(UUID) or null', setIpAddress: 'setIpAddress: ipAddress can only be a string or null', setUseragent: 'setUseragent: useragent can only be a string or null', setTimezone: 'setTimezone: timezone can only be a string or null', setLanguage: 'setLanguage: language can only be a string or null', setScreenResolution: 'setScreenResolution: screenResolution can only be of ScreenSize type or null', setScreenViewport: 'setScreenViewport: screenViewport can only be of ScreenSize type or null', setColorDepth: 'setColorDepth: colorDepth can only be a number(integer) or null', setSubjectData: 'setSubjectData:', createTrackerNotSet: 'createTracker not invoked prior:', // error tracking trackErrorEvent: 'trackError:' }; /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Returns a rejected Promise when `argmap` is not an object, or null when it * is. Centralises the repeated isObject guard that opens every validate* * function — kept as a shared helper so validate* functions stay one-line. */ function rejectIfNotObject(argmap) { return !isObject$1(argmap) ? Promise.reject(new Error(logMessages.evType)) : null; } /** * Validates a manual error event. * * The only required field is `message`. All others are optional: the tracker * fills in sensible defaults (isFatal=false, isHandled=true) when omitted. */ function validateErrorEvent(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } if (typeof argmap.message !== 'string' || argmap.message.length === 0) { return Promise.reject(new Error(logMessages.trackError)); } return Promise.resolve(true); } /* * Copyright (c) 2020-2023 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, * software distributed under the Apache License Version 2.0 is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ /** * Validates whether an object is valid self-describing * * @param sd {Object} - the object to validate * @returns - boolean */ function isValidSD(sd) { return isObject$1(sd) && typeof sd.schema === 'string' && isObject$1(sd.data); } /** * Validates whether an object is a valid array of contexts * * @param contexts {Object} - the object to validate * @returns - boolean promise */ function validateContexts(contexts) { const isValid = Object.prototype.toString.call(contexts) === '[object Array]' && contexts .map((c) => isValidSD(c)) .reduce((acc, curr) => acc !== false && curr, true); if (!isValid) { return Promise.reject(new Error(logMessages.context)); } return Promise.resolve(true); } /** * Validates whether an object is valid self describing * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateSelfDesc(argmap) { if (!isValidSD(argmap)) { return Promise.reject(new Error(logMessages.selfDesc)); } return Promise.resolve(true); } /** * Validates a screen view event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateScreenView(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr !== null) { return typeErr; } // validate required props if (typeof argmap.name !== 'string') { return Promise.reject(new Error(logMessages.screenViewReq)); } return Promise.resolve(true); } /** * Validates a structured event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateStructured(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } // validate required props if (typeof argmap.category !== 'string' || typeof argmap.action !== 'string') { return Promise.reject(new Error(logMessages.structuredReq)); } return Promise.resolve(true); } /** * Validates a page-view event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validatePageView(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } // validate required props if (typeof argmap.pageUrl !== 'string') { return Promise.reject(new Error(logMessages.pageviewReq)); } return Promise.resolve(true); } /** * Validates a timing event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateTiming(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } // validate required props if (typeof argmap.category !== 'string' || typeof argmap.variable !== 'string' || typeof argmap.timing !== 'number') { return Promise.reject(new Error(logMessages.timingReq)); } return Promise.resolve(true); } /** * Validates a consent-granted event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateConsentGranted(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } // validate required props if (typeof argmap.expiry !== 'string' || typeof argmap.documentId !== 'string' || typeof argmap.version !== 'string') { return Promise.reject(new Error(logMessages.consentGReq)); } return Promise.resolve(true); } /** * Validates a consent-withdrawn event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateConsentWithdrawn(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } // validate required props if (typeof argmap.all !== 'boolean' || typeof argmap.documentId !== 'string' || typeof argmap.version !== 'string') { return Promise.reject(new Error(logMessages.consentWReq)); } return Promise.resolve(true); } /** * Validates a deep link received event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateDeepLinkReceived(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } // validate required props if (typeof argmap.url !== 'string') { return Promise.reject(new Error(logMessages.deepLinkReq)); } return Promise.resolve(true); } /** * Validates a message notification event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateMessageNotification(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } // validate required props if (typeof argmap.title !== 'string' || typeof argmap.body !== 'string' || typeof argmap.trigger !== 'string' || !['push', 'location', 'calendar', 'timeInterval', 'other'].includes(argmap.trigger)) { return Promise.reject(new Error(logMessages.messageNotificationReq)); } return Promise.resolve(true); } /** * Validates whether an object is valid ecommerce-item * * @param item {Object} - the object to validate * @returns - boolean */ function isValidEcomItem(item) { if (isObject$1(item) && typeof item.sku === 'string' && typeof item.price === 'number' && typeof item.quantity === 'number') { return true; } return false; } /** * Validates an array of ecommerce-items * * @param items {Object} - the object to validate * @returns - boolean promise */ function validItemsArg(items) { return Object.prototype.toString.call(items) === '[object Array]' && items .map((i) => isValidEcomItem(i)) .reduce((acc, curr) => acc !== false && curr, true); } /** * Validates an ecommerce-transaction event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateEcommerceTransaction(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } // validate required props if (typeof argmap.orderId !== 'string' || typeof argmap.totalValue !== 'number' || !validItemsArg(argmap.items)) { return Promise.reject(new Error(logMessages.ecomReq)); } return Promise.resolve(true); } /** * Validates a revenue event * * @param argmap {Object} - the object to validate * @returns - boolean promise */ function validateRevenueEvent(argmap) { if (!isObject$1(argmap)) { return Promise.reject(new Error(logMessages.revenueEventNullEvent)); } if (typeof argmap.totalOrderAmount !== 'number' || !Number.isFinite(argmap.totalOrderAmount)) { return Promise.reject(new Error(`invalid totalOrderAmount "${argmap.totalOrderAmount}". ${logMessages.revenueEventInvalidTotalOrderAmount}`)); } if (typeof argmap.transactionId !== 'string' || argmap.transactionId.trim() === '') { return Promise.reject(new Error(`invalid transactionId "${argmap.transactionId}". ${logMessages.revenueEventInvalidTransactionId}`)); } if (typeof argmap.currency !== 'string' || argmap.currency.trim() === '') { return Promise.reject(new Error(`invalid currency "${argmap.currency}". ${logMessages.revenueEventInvalidCurrency}`)); } return Promise.resolve(true); } function validateCustomEvent(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } return Promise.resolve(true); } function validateCustomTags(argmap) { const typeErr = rejectIfNotObject(argmap); if (typeErr) { return typeErr; } return Promise.resolve(true); } function validateClearCustomTags(tagKeys) { // validate type if (Object.prototype.toString.call(tagKeys) !== '[object Array]') { return Promise.reject(new Error(logMessages.evType)); } return Promise.resolve(true); } /* * Copyright (c) 2020-2023 Snowplow Analytics Ltd. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, * software distributed under the Apache License Version 2.0 is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ /** * Configuration properties */ const networkProps = [ 'endpoint', 'method', 'customPostPath', 'requestHeaders', ]; const trackerProps = [ 'devicePlatform', 'base64Encoding', 'logLevel', 'applicationContext', 'platformContext', 'geoLocationContext', 'sessionContext', 'deepLinkContext', 'screenContext', 'screenViewAutotracking', 'lifecycleAutotracking', 'installAutotracking', 'exceptionAutotracking', 'diagnosticAutotracking', 'userAnonymisation' // TODO: add all the features included by Conviva ]; const sessionProps = [ 'foregroundTimeout', 'backgroundTimeout' ]; const emitterProps = [ 'bufferOption', 'emitRange', 'threadPoolSize', 'byteLimitPost', 'byteLimitGet', 'serverAnonymisation', ]; const subjectProps = [ 'userId', 'networkUserId', 'domainUserId', 'useragent', 'ipAddress', 'timezone', 'language', 'screenResolution', 'screenViewport', 'colorDepth' ]; const gdprProps = [ 'basisForProcessing', 'documentId', 'documentVersion', 'documentDescription' ]; const gcProps = [ 'tag', 'globalContexts' ]; const remoteProps = [ 'endpoint', 'method' ]; const clidSyncProps = [ 'webViewCookie', 'webViewBridge' ]; /** * Validates whether an object is of valid configuration given its default keys * * @param config {Object} - the object to validate * @param defaultKeys {Array} - the default keys to validate against * @returns - boolean */ function isValidConfig(config, defaultKeys) { return Object.keys(config).every(key => defaultKeys.includes(key)); } /** * Validates the networkConfig * * @param config {Object} - the config to validate * @returns - boolean */ function isValidNetworkConf(config) { if (!isObject$1(config) || !isValidConfig(config, networkProps) || typeof config.endpoint !== 'string' || !config.endpoint) { return false; } return true; } /** * Validates the trackerConfig * * @param config {Object} - the config to validate * @returns - boolean */ function isValidTrackerConf(config) { if (!isObject$1(config) || !isValidConfig(config, trackerProps)) { return false; } return true; } /** * Validates the sessionConfig * * @param config {Object} - the config to validate * @returns - boolean */ function isValidSessionConf(config) { if (!isObject$1(config) || !isValidConfig(config, sessionProps) || !sessionProps.every(key => Object.keys(config).includes(key))) { return false; } return true; } /** * Validates the emitterConfig * * @param config {Object} - the config to validate * @returns - boolean */ function isValidEmitterConf(config) { if (!isObject$1(config) || !isValidConfig(config, emitterProps)) { return false; } return true; } /** * Validates whether an object is of ScreenSize type * * @param arr {Object} - the object to validate * @returns - boolean */ function isScreenSize(arr) { return Array.isArray(arr) && arr.length === 2 && arr.every((n) => typeof n === 'number'); } /** * Validates the subjectConfig * * @param config {Object} - the config to validate * @returns - boolean */ function isValidSubjectConf(config) { if (!isObject$1(config) || !isValidConfig(config, subjectProps)) { return false; } // validating ScreenSize here to simplify array handling in bridge if (Object.prototype.hasOwnProperty.call(config, 'screenResolution') && config.screenResolution !== null && !isScreenSize(config.screenResolution)) { return false; } if (Object.prototype.hasOwnProperty.call(config, 'screenViewport') && config.screenViewport !== null && !isScreenSize(config.screenViewport)) { return false; } return true; } /** * Validates the gdprConfig * * @param config {Object} - the config to validate * @returns - boolean */ function isValidGdprConf(config) { if (!isObject$1(config) || !isValidConfig(config, gdprProps) || !gdprProps.every(key => Object.keys(config).includes(key)) || !['consent', 'contract', 'legal_obligation', 'legitimate_interests', 'public_task', 'vital_interests'].includes(config.basisForProcessing)) { return false; } return true; } /** * Validates whether an object is of GlobalContext type * * @param gc {Object} - the object to validate * @returns - boolean */ function isValidGC(gc) { return isObject$1(gc) && isValidConfig(gc, gcProps) && typeof gc.tag === 'string' && Array.isArray(gc.globalContexts) && gc.globalContexts.every(c => isValidSD(c)); } /** * Validates the GCConfig (global contexts) * * @param config {Object} - the config to validate * @returns - boolean */ function isValidGCConf(config) { if (!Array.isArray(config)) { return false; } if (!config.every(gc => isValidGC(gc))) { return false; } return true; } /** * Validates the ClidSyncConfig * * @param config {Object} - the config to validate * @returns - boolean */ function isValidClidSyncConf(config) { if (!isObject$1(config) || !isValidConfig(config, clidSyncProps)) { return false; } if (Object.prototype.hasOwnProperty.call(config, 'webViewCookie') && config.webViewCookie != null) { if (!isObject$1(config.webViewCookie)) { return false; } if (Object.prototype.hasOwnProperty.call(config.webViewCookie, 'domains') && config.webViewCookie.domains != null && (!Array.isArray(config.webViewCookie.domains) || !config.webViewCookie.domains.every(d => typeof d === 'string'))) { return false; } } return true; } /** * Validates the RemoteConfig (remote config) * * @param config {Object} - the config to validate * @returns - boolean */ function isValidRemoteConf(config) { if (!isObject$1(config) || !isValidConfig(config, remoteProps) || typeof config.endpoint !== 'string' || !config.endpoint) { return false; } return true; } /** * Validates the initTrackerConfiguration * * @param init {Object} - the config to validate * @returns - boolean promise */ function initValidate(init) { if (typeof init.customerKey !== 'string' || !init.customerKey || init.customerKey === "") { return Promise.reject(new Error(logMessages.customerKey)); } if (typeof init.appName !== 'string' || !init.appName || init.appName === "") { return Promise.reject(new Error(logMessages.appName)); } if (Object.prototype.hasOwnProperty.call(init, 'networkConfig') && !isValidNetworkConf(init.networkConfig)) { return Promise.reject(new Error(logMessages.network)); } if (Object.prototype.hasOwnProperty.call(init, 'trackerConfig') && !isValidTrackerConf(init.trackerConfig)) { return Promise.reject(new Error(logMessages.tracker)); } if (Object.prototype.hasOwnProperty.call(init, 'sessionConfig') && (!isValidSessionConf(init.sessionConfig))) { return Promise.reject(new Error(logMessages.session)); } if (Object.prototype.hasOwnProperty.call(init, 'emitterConfig') && !isValidEmitterConf(init.emitterConfig)) { return Promise.reject(new Error(logMessages.emitter)); } if (Object.prototype.hasOwnProperty.call(init, 'subjectConfig') && !isValidSubjectConf(init.subjectConfig)) { return Promise.reject(new Error(logMessages.subject)); } if (Object.prototype.hasOwnProperty.call(init, 'gdprConfig') && !isValidGdprConf(init.gdprConfig)) { return Promise.reject(new Error(logMessages.gdpr)); } if (Object.prototype.hasOwnProperty.call(init, 'gcConfig') && !isValidGCConf(init.gcConfig)) { return Promise.reject(new Error(logMessages.gc)); } if (Object.prototype.hasOwnProperty.call(init, 'remoteConfig') && !isValidRemoteConf(init.remoteConfig)) { return Promise.reject(new Error(logMessages.remote)); } if (Object.prototype.hasOwnProperty.call(init, 'clidSyncConfig') && !isValidClidSyncConf(init.clidSyncConfig)) { return Promise.reject(new Error(logMessages.clidSync)); } return Promise.resolve(true); } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** Copies non-blocked, defined entries from `src` into `target`. */ function copyInto(target, src) { for (const k of Object.keys(src)) { if (BLOCKED_ATTRIBUTE_KEYS.has(k)) { continue; } const v = src[k]; if (v !== undefined) { target[k] = v; } } } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Owns the tracker's global attribute bag plus the propagated userId, and * builds the per-dispatch merged attribute set. * * Prototype-pollution defence: the backing object uses a null prototype so a * consumer-supplied `__proto__` key cannot reach `Object.prototype`'s setter, * and BLOCKED_ATTRIBUTE_KEYS are dropped on every read/write path as * defence-in-depth so they never leak into the wire payload. */ class AttributeStore { globalAttributes = Object.create(null); userId = null; add(key, value) { if (key.length === 0 || BLOCKED_ATTRIBUTE_KEYS.has(key)) { return; } this.globalAttributes[key] = value; } remove(key) { if (BLOCKED_ATTRIBUTE_KEYS.has(key)) { return; } delete this.globalAttributes[key]; } setUserId(id) { this.userId = typeof id === 'string' ? id : null; } /** * Builds the merged attribute bag from globalAttributes, `extraAttributes`, * and userId. Returns undefined when the resulting bag is empty. */ build(extraAttributes) { const attrs = Object.create(null); copyInto(attrs, this.globalAttributes); if (extraAttributes !== undefined) { copyInto(attrs, extraAttributes); } if (this.userId !== null) { attrs.userId = this.userId; } return Object.keys(attrs).length > 0 ? attrs : undefined; } /** Resets to a null-prototype object and clears the userId. */ reset() { this.globalAttributes = Object.create(null); this.userId = null; } } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Fail-silent execution helpers. * * The error-tracking subsystem must NEVER throw out of its own reporting paths * (design constraint D7). Historically every such path wrapped its body in an * inline `try { ... } catch { /* fail-silent *\/ }`. Routing those through these * two helpers keeps the exact same fail-silent behaviour while removing the * per-call `catch` branch from each caller — concentrating it in one place. */ /** * Runs `fn`, swallowing any thrown error. Drop-in replacement for an inline * `try { fn(); } catch { /* fail-silent *\/ }` void block. */ function runSafely(fn) { try { fn(); } catch { /* fail-silent */ } } /** * Runs `fn` and returns its result; returns `fallback` if `fn` throws. * Drop-in replacement for a `try { return fn(); } catch { return fallback; }` * block whose catch yields a fixed value. */ function safeCall(fn, fallback) { try { return fn(); } catch { return fallback; } } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** Returns `v` when it is a positive finite number, otherwise `fallback`. */ function positiveOr(v, fallback) { return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : fallback; } /** Reports whether the bundle is running in a dev build. Never throws. */ function isDevMode() { return safeCall(() => typeof __DEV__ !== 'undefined' && !!__DEV__, false); } /** * Attempts to locate a build-time-injected bundle hash. Consumers can set * `global.__CONVIVA_BUNDLE_ID__` via a Metro transformer or Babel plugin. * Runtime override via ErrorTrackingConfiguration.bundleId wins over this. */ function readBundleIdFromGlobal$1() { const g = globalThis; const v = g.__CONVIVA_BUNDLE_ID__; return typeof v === 'string' && v.length > 0 ? v : undefined; } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** Boolean flags whose default is `true` — set false only if the consumer * explicitly passed `false`. */ const DEFAULT_TRUE_FLAGS = [ 'enabled', 'captureGlobalErrors', 'captureUnhandledRejections', 'enableRateLimiting', ]; /** Boolean flags whose default is `false` — set true only if the consumer * explicitly passed `true`. */ const DEFAULT_FALSE_FLAGS = [ 'suppressInDev', 'promiseRejectionsAsHandled', ]; /** Numeric rate-limit fields and their fallbacks; applied via positiveOr. */ const NUMERIC_DEFAULTS = [ ['maxEventsPerWindow', DEFAULT_MAX_EVENTS_PER_WINDOW], ['rateLimitWindowMs', DEFAULT_RATE_LIMIT_WINDOW_MS], ['disconnectDurationMs', DEFAULT_DISCONNECT_DURATION_MS], ]; /** Applies the four DEFAULT_TRUE_FLAGS — anything not strictly false wins. */ function applyTrueFlags(c, out) { for (const key of DEFAULT_TRUE_FLAGS) { out[key] = c[key] !== false; } } /** Applies the two DEFAULT_FALSE_FLAGS — only strictly true enables them. */ function applyFalseFlags(c, out) { for (const key of DEFAULT_FALSE_FLAGS) { out[key] = c[key] === true; } } /** Applies the three NUMERIC_DEFAULTS via positiveOr. */ function applyNumericDefaults(c, out) { for (const [key, fallback] of NUMERIC_DEFAULTS) { out[key] = positiveOr(c[key], fallback); } } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Applies the per-payload defaults that `_initFromTracker` lifts out of the * consumer-facing ErrorTrackingConfiguration. Flag tables avoid repeating one * ternary per field — see ./flagTables. */ function applyConfigDefaults(cfg) { const c = cfg ?? {}; const out = {}; applyTrueFlags(c, out); applyFalseFlags(c, out); applyNumericDefaults(c, out); out.beforeCapture = c.beforeCapture; out.bundleId = c.bundleId ?? readBundleIdFromGlobal$1(); out.bridgeAdapter = c.bridgeAdapter; return out; } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * True when `err` is a non-null object or function — the kinds of values that * can be tracked via WeakSet. Acts as a type guard so callers don't need an * `err as object` cast on the WeakSet add/has side. */ function isObjectKey(err) { return err !== null && (typeof err === 'object' || typeof err === 'function'); } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** Stable key for a non-object thrown value (primitives, undefined, null). */ function primitiveKey(err) { if (err === undefined) { return '__undef__'; } if (err === null) { return '__null__'; } return `${typeof err}:${String(err)}`; } /** * Bounded TTL store for thrown primitives. Primitives cannot be WeakSet keys, * so we key by their string serialisation and expire entries after `ttlMs`, * dropping the oldest insertion when the store is full. */ class PrimitiveDedupStore { ttlMs; max; seen = new Map(); constructor(ttlMs, max) { this.ttlMs = ttlMs; this.max = max; } isDuplicate(err) { this.evictExpired(); const prev = this.seen.get(primitiveKey(err)); return prev !== undefined && Date.now() - prev < this.ttlMs; } markSeen(err) { if (this.seen.size >= this.max) { const oldest = this.seen.keys().next().value; if (oldest !== undefined) { this.seen.delete(oldest); } } this.seen.set(primitiveKey(err), Date.now()); } reset() { this.seen.clear(); } evictExpired() { const now = Date.now(); for (const [k, v] of this.seen) { if (now - v >= this.ttlMs) { this.seen.delete(k); } } } } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Prevents the same Error object from being reported twice when captured by * multiple hooks (e.g. ErrorUtils.setGlobalHandler runs, then a parent * ErrorBoundary's componentDidCatch runs for the same throw). * * Strategy: * - Error instances tracked via WeakSet so GC is automatic (no leak). * - Non-Error thrown primitives delegated to PrimitiveDedupStore, since * primitives cannot be WeakSet keys. */ class DedupGuard { seenObjects = new WeakSet(); primitives; constructor(primitiveTtlMs = 500, maxPrimitives = 256) { this.primitives = new PrimitiveDedupStore(primitiveTtlMs, maxPrimitives); } /** true if this thrown value was already seen within the TTL window. */ isDuplicate(err) { return isObjectKey(err) ? this.seenObjects.has(err) : this.primitives.isDuplicate(err); } /** Call after dispatching so subsequent captures of the same throw are suppressed. */ markSeen(err) { if (isObjectKey(err)) { this.seenObjects.add(err); return; } this.primitives.markSeen(err); } /** Test hook — clears all seen state. Not part of the public API. */ _reset() { this.primitives.reset(); // WeakSet has no .clear() method; reassigning is the only way to reset it. this.seenObjects = new WeakSet(); } } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Reads the current install state from the global slot. Returns the modern * `{ cleanups }` shape, the sentinel `'legacy'` when a truthy-but-unknown value * is present, or `null` when nothing is installed. */ function readInstallState() { const g = globalThis; const v = g[INSTALL_FLAG]; if (v === undefined || v === null || v === false) { return null; } if (typeof v === 'object' && Array.isArray(v.cleanups)) { return v; } // Truthy but not the modern shape (e.g. legacy `true`). Treat as installed // for "previous install detected" purposes, but no recoverable cleanups. return 'legacy'; } /** Writes (or, when passed null, clears) the install state on the global slot. */ function writeInstallState(state) { const g = globalThis; if (state === null) { delete g[INSTALL_FLAG]; } else { g[INSTALL_FLAG] = state; } } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Detects the active JavaScript engine. Hermes exposes the marker global * `HermesInternal`; JSC / browser do not. */ function detectJsEngine() { const g = globalThis; if (g.HermesInternal != null) { return 'hermes'; } // If a browser-style `window` is present but no Hermes, treat as JSC/browser. if (typeof g.navigator !== 'undefined') { return 'jsc'; } return 'unknown'; } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Stack-frame patterns probed in order; the first match wins. Driven as a * table so `matchFrame` loops over them instead of cascading near-identical * `if (m === null)` blocks. * Pattern 1 (V8/Hermes): "at ... (FILE:LINE:COL)" * Pattern 2 (V8 no-parens / anon): "at FILE:LINE:COL" * Pattern 3 (JSC/Safari): "funcName@FILE:LINE:COL" */ const FRAME_PATTERNS = [ /\(([^()]+):(\d+):(\d+)\)\s*$/, /at\s+([^\s]+):(\d+):(\d+)\s*$/, /@([^@\s]+):(\d+):(\d+)\s*$/, ]; /** Returns the first regex match against `line`, or null if none match. */ function matchFrame(line) { for (const re of FRAME_PATTERNS) { const m = line.match(re); if (m !== null) { return m; } } return null; } /** * True when `line` is the top "ErrorType: message" summary line that should be * skipped before searching for stack frames. Only relevant at index 0 — interior * lines are always treated as candidate frames. */ function isSummaryLine(i, line) { return i === 0 && !/^\s*at\s/.test(line) && !/@/.test(line); } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** * Reads the first frame of an Error.stack string and extracts file/line/col. * Handles the common V8/Hermes/JSC shapes: * "at funcName (file:///path/to/index.bundle:123:45)" * "funcName@file:///path/to/index.bundle:123:45" * "at file:///path/to/index.bundle:123:45" */ function parseFirstFrame(stack) { if (stack == null) { return {}; } const lines = stack.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line == null) { continue; } if (isSummaryLine(i, line)) { continue; } const m = matchFrame(line.trim()); if (m !== null) { return { fileName: m[1], lineNumber: parseInt(m[2], 10), lineColumn: parseInt(m[3], 10), }; } } return {}; } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. */ /** Truncates `s` to at most `max` characters. */ function truncate(s, max) { if (s.length <= max) { return s; } return s.slice(0, max); } /** Renders a non-Error primitive into a string suitable for `message`. */ function primitiveMessage(error) { if (error === undefined) { return 'undefined'; } if (error === null) { return 'null'; } return String(error); } /** * Extracts message, errorType, and stackTrace from a thrown value. Handles both * Error instances and arbitrary primitives. */ function normalizeError(error, errorSource) { if (error instanceof Error) { return { message: error.message || String(error), errorType: error.name || 'Error', stackTrace: typeof error.stack === 'string' ? error.stack : '', }; } return { message: primitiveMessage(error), errorType: errorSource === 'unhandledRejection' ? 'UnhandledRejection' : 'Error', stackTrace: '', }; } /* * Copyright (c) 2020-2026 Conviva Inc. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.