UNPKG

@sentry/react-native

Version:
226 lines 10.7 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { debug } from '@sentry/core'; import { isHardCrash } from '../misc'; import { hasHooks } from '../utils/clientutils'; import { isExpoGo, notMobileOs } from '../utils/environment'; import { registerFeatureMarker } from '../utils/featureMarkers'; import { NATIVE } from '../wrapper'; import { makeEnrichXhrBreadcrumbsForMobileReplay } from './xhrUtils'; const MOBILE_REPLAY_NETWORK_DETAILS_INTEGRATION_NAME = 'MobileReplayNetworkDetails'; const MOBILE_REPLAY_NETWORK_BODIES_INTEGRATION_NAME = 'MobileReplayNetworkBodies'; export const MOBILE_REPLAY_INTEGRATION_NAME = 'MobileReplay'; const defaultOptions = { maskAllText: true, maskAllImages: true, maskAllVectors: true, enableExperimentalViewRenderer: false, enableViewRendererV2: true, enableFastViewRendering: false, screenshotStrategy: 'pixelCopy', networkDetailAllowUrls: [], networkDetailDenyUrls: [], networkCaptureBodies: true, networkRequestHeaders: [], networkResponseHeaders: [], }; function mergeOptions(initOptions) { const merged = Object.assign(Object.assign({}, defaultOptions), initOptions); if (initOptions.enableViewRendererV2 === undefined && initOptions.enableExperimentalViewRenderer !== undefined) { merged.enableViewRendererV2 = initOptions.enableExperimentalViewRenderer; } return merged; } /** * Network detail allow/deny lists accept `RegExp` in JS, but the native bridge * can only serialize strings (a `RegExp` becomes `{}` when crossing the bridge). * * Convert `RegExp` entries to their `source` string so the native SDK can * populate its `SentryReplayOptions`, which is what emits the rrweb options * event that tells the Sentry frontend to render captured request/response * details. The JS-side matching in `xhrUtils` keeps using the original * `RegExp` values, so this normalization only affects native signaling. */ export function serializeNetworkDetailUrlsForNative(urls) { if (!urls) { return []; } return urls.map(url => (typeof url === 'string' ? url : url.source)).filter(url => url.length > 0); } /** * The Mobile Replay Integration, let's you adjust the default mobile replay options. * To be passed to `Sentry.init` with `replaysOnErrorSampleRate` or `replaysSessionSampleRate`. * * ```javascript * Sentry.init({ * replaysOnErrorSampleRate: 1.0, * replaysSessionSampleRate: 1.0, * integrations: [mobileReplayIntegration({ * // Adjust the default options * })], * }); * ``` * * @experimental */ export const mobileReplayIntegration = (initOptions = defaultOptions) => { if (isExpoGo()) { debug.warn(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`); } if (notMobileOs()) { debug.warn(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} is not supported on this platform.`); } if (isExpoGo() || notMobileOs()) { return mobileReplayIntegrationNoop(); } const options = mergeOptions(initOptions); // Cache the replay ID in JavaScript to avoid excessive bridge calls // This will be updated when we know the replay ID changes (e.g., after captureReplay) let cachedReplayId = null; function updateCachedReplayId(replayId) { cachedReplayId = replayId; } function getCachedReplayId() { if (cachedReplayId !== null) { return cachedReplayId; } const nativeReplayId = NATIVE.getCurrentReplayId(); if (nativeReplayId) { cachedReplayId = nativeReplayId; } return nativeReplayId; } function processEvent(event, hint) { return __awaiter(this, void 0, void 0, function* () { var _a; const hasException = ((_a = event.exception) === null || _a === void 0 ? void 0 : _a.values) && event.exception.values.length > 0; if (!hasException) { // Event is not an error, will not capture replay return event; } // Check if beforeErrorSampling callback filters out this error if (initOptions.beforeErrorSampling) { try { if (initOptions.beforeErrorSampling(event, hint) === false) { debug.log(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} not sent; beforeErrorSampling conditions not met for event ${event.event_id}.`); return event; } } catch (error) { debug.error(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} beforeErrorSampling callback threw an error, proceeding with replay capture`, error); // Continue with replay capture if callback throws } } const replayId = yield NATIVE.captureReplay(isHardCrash(event)); if (replayId) { updateCachedReplayId(replayId); debug.log(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} Captured recording replay ${replayId} for event ${event.event_id}.`); // Add replay_id to error event contexts to link replays to events/traces event.contexts = event.contexts || {}; event.contexts.replay = Object.assign(Object.assign({}, event.contexts.replay), { replay_id: replayId }); } else { // Check if there's an ongoing recording and update cache if found const recordingReplayId = NATIVE.getCurrentReplayId(); if (recordingReplayId) { updateCachedReplayId(recordingReplayId); debug.log(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`); // Add replay_id to error event contexts to link replays to events/traces event.contexts = event.contexts || {}; event.contexts.replay = Object.assign(Object.assign({}, event.contexts.replay), { replay_id: recordingReplayId }); } else { updateCachedReplayId(null); debug.log(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} not sampled for event ${event.event_id}.`); } } return event; }); } function setup(client) { var _a, _b, _c, _d, _e, _f, _g, _h; if (!hasHooks(client)) { return; } if (((_b = (_a = options.networkDetailAllowUrls) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0) { registerFeatureMarker(MOBILE_REPLAY_NETWORK_DETAILS_INTEGRATION_NAME, client); if ((_c = options.networkCaptureBodies) !== null && _c !== void 0 ? _c : true) { registerFeatureMarker(MOBILE_REPLAY_NETWORK_BODIES_INTEGRATION_NAME, client); } } // Initialize the cached replay ID on setup cachedReplayId = NATIVE.getCurrentReplayId(); client.on('createDsc', (dsc) => { if (dsc.replay_id) { return; } // Use cached replay ID to avoid bridge calls const currentReplayId = getCachedReplayId(); if (currentReplayId) { dsc.replay_id = currentReplayId; } }); client.on('processMetric', (metric) => { // Add replay_id to metric attributes to link metrics to replays const currentReplayId = getCachedReplayId(); if (currentReplayId) { metric.attributes = metric.attributes || {}; metric.attributes.replay_id = currentReplayId; } }); const networkOptions = { allowUrls: (_d = options.networkDetailAllowUrls) !== null && _d !== void 0 ? _d : [], denyUrls: (_e = options.networkDetailDenyUrls) !== null && _e !== void 0 ? _e : [], captureBodies: (_f = options.networkCaptureBodies) !== null && _f !== void 0 ? _f : true, requestHeaders: (_g = options.networkRequestHeaders) !== null && _g !== void 0 ? _g : [], responseHeaders: (_h = options.networkResponseHeaders) !== null && _h !== void 0 ? _h : [], }; client.on('beforeAddBreadcrumb', makeEnrichXhrBreadcrumbsForMobileReplay(networkOptions)); // Wrap beforeSend to run processEvent after user's beforeSend const clientOptions = client.getOptions(); const originalBeforeSend = clientOptions.beforeSend; clientOptions.beforeSend = (event, hint) => __awaiter(this, void 0, void 0, function* () { let result = event; if (originalBeforeSend) { result = yield originalBeforeSend(event, hint); if (result === null) { // Event was dropped by user's beforeSend, don't capture replay return null; } } try { return yield processEvent(result, hint); } catch (error) { debug.error(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} Failed to process event for replay`, error); return result; } }); } function getReplayId() { return getCachedReplayId(); } // TODO: When adding manual API, ensure overlap with the web replay so users can use the same API interchangeably // https://github.com/getsentry/sentry-javascript/blob/develop/packages/replay-internal/src/integration.ts#L45 return { name: MOBILE_REPLAY_INTEGRATION_NAME, setup, options: options, getReplayId: getReplayId, }; }; const mobileReplayIntegrationNoop = () => { return { name: MOBILE_REPLAY_INTEGRATION_NAME, options: defaultOptions, getReplayId: () => null, // Mock implementation for noop version }; }; //# sourceMappingURL=mobilereplay.js.map