@convivainc/conviva-react-native-appanalytics
Version:
Conviva React Native Application Analytics Library
1,412 lines (1,390 loc) • 199 kB
JavaScript
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.