@sentry/react-native
Version:
Official Sentry SDK for react-native
259 lines • 11.7 kB
JavaScript
import { debug, getSpanDescendants, SPAN_STATUS_ERROR, spanToJSON, timestampInSeconds } from '@sentry/core';
import { AppState, Platform } from 'react-native';
import { isRootSpan, isSentrySpan } from '../utils/span';
/**
* Attribute used to mark root spans whose corresponding transaction event
* should be dropped by the React Native tracing event processor instead of
* being treated as sampled-out by `@sentry/core`.
*
* The value is a stable string identifier describing why the SDK chose to
* discard the transaction (e.g. an empty back-navigation, a route-change
* transaction that never received route information, or a childless idle
* transaction).
*/
export const SENTRY_DISCARD_REASON_ATTRIBUTE = 'sentry.rn.discard_reason';
/**
* Marks a root span so its transaction event will be filtered out by the
* tracing integration's event processor. The span itself is left with its
* original sampling decision so debug logs no longer report "dropped due to
* sampling" for SDK-side discards.
*/
export function markRootSpanForDiscard(span, reason) {
span.setAttribute(SENTRY_DISCARD_REASON_ATTRIBUTE, reason);
}
/**
* Returns the SDK-side discard reason recorded on a root span, if any.
*/
export function getRootSpanDiscardReason(span) {
var _a;
const value = (_a = spanToJSON(span).data) === null || _a === void 0 ? void 0 : _a[SENTRY_DISCARD_REASON_ATTRIBUTE];
return typeof value === 'string' ? value : undefined;
}
/**
* Returns the SDK-side discard reason from a transaction event, if any.
*/
export function getTransactionEventDiscardReason(event) {
var _a, _b, _c;
const value = (_c = (_b = (_a = event.contexts) === null || _a === void 0 ? void 0 : _a.trace) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c[SENTRY_DISCARD_REASON_ATTRIBUTE];
return typeof value === 'string' ? value : undefined;
}
/**
* The time to wait after the app enters the `inactive` state on iOS before
* cancelling the span.
*/
const IOS_INACTIVE_CANCEL_DELAY_MS = 5000;
/**
* Hooks on span end event to execute a callback when the span ends.
*/
export function onThisSpanEnd(client, span, callback) {
const unsubscribe = client.on('spanEnd', (endedSpan) => {
if (span !== endedSpan) {
return;
}
unsubscribe();
callback(endedSpan);
});
}
export const adjustTransactionDuration = (client, span, maxDurationMs) => {
if (!isRootSpan(span)) {
debug.warn('Not sampling empty back spans only works for Sentry Transactions (Root Spans).');
return;
}
const unsubscribe = client.on('spanEnd', (endedSpan) => {
if (endedSpan !== span) {
return;
}
unsubscribe();
const endTimestamp = spanToJSON(span).timestamp;
const startTimestamp = spanToJSON(span).start_timestamp;
if (!endTimestamp || !startTimestamp) {
return;
}
const diff = endTimestamp - startTimestamp; // a diff in *seconds*
const isOutdatedTransaction = diff > maxDurationMs / 1000 || diff < 0;
if (isOutdatedTransaction) {
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' });
// TODO: check where was used, might be possible to delete
span.setAttribute('maxTransactionDurationExceeded', 'true');
}
});
};
/**
* Helper function to filter out auto-instrumentation child spans.
*/
function getMeaningfulChildSpans(span) {
const children = getSpanDescendants(span);
return children.filter(child => child.spanContext().spanId !== span.spanContext().spanId &&
spanToJSON(child).op !== 'ui.load.initial_display' &&
spanToJSON(child).op !== 'navigation.processing');
}
/**
* Generic helper to discard empty navigation spans based on a condition.
*/
function discardEmptyNavigationSpan(client, span, shouldDiscardFn, onDiscardFn) {
if (!client) {
debug.warn('Could not hook on spanEnd event because client is not defined.');
return;
}
if (!span) {
debug.warn('Could not hook on spanEnd event because span is not defined.');
return;
}
if (!isRootSpan(span) || !isSentrySpan(span)) {
debug.warn('Not sampling empty navigation spans only works for Sentry Transactions (Root Spans).');
return;
}
const unsubscribe = client.on('spanEnd', (endedSpan) => {
if (endedSpan !== span) {
return;
}
unsubscribe();
if (!shouldDiscardFn(span)) {
return;
}
const meaningfulChildren = getMeaningfulChildSpans(span);
if (meaningfulChildren.length <= 0) {
onDiscardFn(span);
}
});
}
export const ignoreEmptyBackNavigation = (client, span) => {
discardEmptyNavigationSpan(client, span,
// Only discard if route has been seen before
span => { var _a; return ((_a = spanToJSON(span).data) === null || _a === void 0 ? void 0 : _a['route.has_been_seen']) === true; },
// Log message and mark the span for discard via the event processor.
span => {
debug.log('Not sampling transaction as route has been seen before. Pass ignoreEmptyBackNavigationTransactions = false to disable this feature.');
markRootSpanForDiscard(span, 'empty_back_navigation');
});
};
/**
* Discards empty "Route Change" transactions that never received route information.
* This happens when navigation library emits a route change event but getCurrentRoute() returns undefined.
* Such transactions don't contain any useful information and should not be sent to Sentry.
*
* This function must be called with a reference tracker function that can check if the span
* was cleared from the integration's tracking (indicating it went through the state listener).
*/
export const ignoreEmptyRouteChangeTransactions = (client, span, defaultNavigationSpanName, isSpanStillTracked) => {
discardEmptyNavigationSpan(client, span,
// Only discard if:
// 1. Still has default name
// 2. No route information was set
// 3. Still being tracked (state listener never called)
span => {
var _a;
const spanJSON = spanToJSON(span);
return (spanJSON.description === defaultNavigationSpanName && !((_a = spanJSON.data) === null || _a === void 0 ? void 0 : _a['route.name']) && isSpanStillTracked());
},
// Log and mark the span for discard. The actual `recordDroppedEvent` call
// happens in the tracing integration's event processor with the correct
// `event_processor` reason, so we no longer record it here as a sampling
// drop.
span => {
debug.log(`Discarding empty "${defaultNavigationSpanName}" transaction that never received route information.`);
markRootSpanForDiscard(span, 'no_route_info');
});
};
/**
* Idle Transaction callback to only sample transactions with child spans.
* To avoid side effects of other callbacks this should be hooked as the last callback.
*/
export const onlySampleIfChildSpans = (client, span) => {
if (!isRootSpan(span) || !isSentrySpan(span)) {
debug.warn('Not sampling childless spans only works for Sentry Transactions (Root Spans).');
return;
}
const unsubscribe = client.on('spanEnd', (endedSpan) => {
if (endedSpan !== span) {
return;
}
unsubscribe();
const children = getSpanDescendants(span);
if (children.length <= 1) {
// Span always has at lest one child, itself
debug.log(`Not sampling as ${spanToJSON(span).op} transaction has no child spans.`);
markRootSpanForDiscard(span, 'no_child_spans');
}
});
};
/**
* Hooks on AppState change to cancel the span if the app goes background.
*
* On iOS the JS thread can be suspended between the `inactive` and
* `background` transitions, which means the `background` event may never
* reach JS in time. To handle this we schedule a deferred cancellation when
* the app becomes `inactive`. If the app returns to `active` before the
* timeout fires, the cancellation is cleared. If it transitions to
* `background` first, we cancel immediately and clear the timeout.
*/
export const cancelInBackground = (client, span) => {
let inactiveTimeout;
// The timestamp when the app actually left the foreground. Used to end
// http.client child spans at the right time instead of whenever the
// deferred cancellation timer fires (which can be much later if the JS
// thread was suspended on iOS).
let leftForegroundTimestamp;
const cancelSpan = () => {
if (inactiveTimeout !== undefined) {
clearTimeout(inactiveTimeout);
inactiveTimeout = undefined;
}
debug.log(`Setting ${spanToJSON(span).op} transaction to cancelled because the app is in the background.`);
// End still-recording http.client children at the time the app left
// the foreground, not when the deferred timer fires. On iOS, the JS
// thread can be suspended after the `inactive` event, so the 5-second
// timer may fire long after the app backgrounded. Using the original
// timestamp prevents inflated span durations.
const childEndTimestamp = leftForegroundTimestamp !== null && leftForegroundTimestamp !== void 0 ? leftForegroundTimestamp : timestampInSeconds();
const children = getSpanDescendants(span);
for (const child of children) {
if (child !== span && child.isRecording() && spanToJSON(child).op === 'http.client') {
child.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' });
child.end(childEndTimestamp);
}
}
span.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' });
span.end();
};
const subscription = AppState.addEventListener('change', (newState) => {
if (newState === 'background') {
leftForegroundTimestamp = leftForegroundTimestamp !== null && leftForegroundTimestamp !== void 0 ? leftForegroundTimestamp : timestampInSeconds();
cancelSpan();
}
else if (Platform.OS === 'ios' && newState === 'inactive') {
// Record when the app actually left the foreground.
leftForegroundTimestamp = timestampInSeconds();
// Schedule a deferred cancellation — if the JS thread is suspended
// before the 'background' event fires, this timer will execute when
// the app is eventually resumed and end the span.
if (inactiveTimeout === undefined) {
inactiveTimeout = setTimeout(cancelSpan, IOS_INACTIVE_CANCEL_DELAY_MS);
}
}
else if (newState === 'active') {
// App returned to foreground — clear any pending inactive cancellation.
leftForegroundTimestamp = undefined;
if (inactiveTimeout !== undefined) {
clearTimeout(inactiveTimeout);
inactiveTimeout = undefined;
}
}
});
if (subscription) {
const unsubscribe = client.on('spanEnd', (endedSpan) => {
var _a;
if (endedSpan !== span) {
return;
}
unsubscribe();
debug.log(`Removing AppState listener for ${spanToJSON(span).op} transaction.`);
if (inactiveTimeout !== undefined) {
clearTimeout(inactiveTimeout);
inactiveTimeout = undefined;
}
(_a = subscription.remove) === null || _a === void 0 ? void 0 : _a.call(subscription);
});
}
};
//# sourceMappingURL=onSpanEndUtils.js.map