@sentry/react-native
Version:
Official Sentry SDK for react-native
226 lines • 10.7 kB
JavaScript
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