Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
- You can now choose which logs are captured: 'native' for logs from native code only, 'js' for logs from the JavaScript layer only, or 'all' for both layers.
- Takes effect only if `enableLogs` is `true` and defaults to 'all', preserving previous behavior.

### Fixes

- Preserves interaction span context during app restart to allow proper replay capture ([#5386](https://github.com/getsentry/sentry-react-native/pull/5386))

### Dependencies

- Bump JavaScript SDK from v10.24.0 to v10.25.0 ([#5362](https://github.com/getsentry/sentry-react-native/pull/5362))
Expand Down
9 changes: 6 additions & 3 deletions packages/core/src/js/tracing/reactnavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export const reactNavigationIntegration = ({
// This ensures runApplication calls after the initial start are correctly traced.
// This is used for example when Activity is (re)started on Android.
debug.log('[ReactNavigationIntegration] Starting new idle navigation span based on runApplication call.');
startIdleNavigationSpan();
startIdleNavigationSpan(undefined, true);
}
});

Expand Down Expand Up @@ -209,8 +209,11 @@ export const reactNavigationIntegration = ({
* To be called on every React-Navigation action dispatch.
* It does not name the transaction or populate it with route information. Instead, it waits for the state to fully change
* and gets the route information from there, @see updateLatestNavigationSpanWithCurrentRoute
*
* @param unknownEvent - The event object that contains navigation action data
* @param isAppRestart - Whether this span is being started due to an app restart rather than a normal navigation action
*/
const startIdleNavigationSpan = (unknownEvent?: unknown): void => {
const startIdleNavigationSpan = (unknownEvent?: unknown, isAppRestart = false): void => {
const event = unknownEvent as UnsafeAction | undefined;
if (useDispatchedActionData && event?.data.noop) {
debug.log(`${INTEGRATION_NAME} Navigation action is a noop, not starting navigation span.`);
Expand Down Expand Up @@ -245,7 +248,7 @@ export const reactNavigationIntegration = ({
tracing?.options.beforeStartSpan
? tracing.options.beforeStartSpan(getDefaultIdleNavigationSpanOptions())
: getDefaultIdleNavigationSpanOptions(),
idleSpanOptions,
{ ...idleSpanOptions, isAppRestart },
);
latestNavigationSpan?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION);
latestNavigationSpan?.setAttribute(SEMANTIC_ATTRIBUTE_NAVIGATION_ACTION_TYPE, navigationActionType);
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/js/tracing/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export const startIdleNavigationSpan = (
{
finalTimeout = defaultIdleOptions.finalTimeout,
idleTimeout = defaultIdleOptions.idleTimeout,
}: Partial<typeof defaultIdleOptions> = {},
isAppRestart = false,
}: Partial<typeof defaultIdleOptions> & { isAppRestart?: boolean } = {},
): Span | undefined => {
const client = getClient();
if (!client) {
Expand All @@ -58,8 +59,20 @@ export const startIdleNavigationSpan = (
}

const activeSpan = getActiveSpan();
const isActiveSpanInteraction = activeSpan && isRootSpan(activeSpan) && isSentryInteractionSpan(activeSpan);

clearActiveSpanFromScope(getCurrentScope());
if (activeSpan && isRootSpan(activeSpan) && isSentryInteractionSpan(activeSpan)) {

// Don't cancel user interaction spans when starting from runApplication (app restart/reload).
// This preserves the span context for error capture and replay recording.
if (isActiveSpanInteraction && isAppRestart) {
debug.log(
`[startIdleNavigationSpan] Not canceling ${
spanToJSON(activeSpan).op
} transaction because navigation is from app restart - preserving error context.`,
);
// Don't end the span - it will timeout naturally and remains available for error/replay processing
} else if (isActiveSpanInteraction) {
debug.log(
`[startIdleNavigationSpan] Canceling ${
spanToJSON(activeSpan).op
Expand Down
52 changes: 52 additions & 0 deletions packages/core/test/tracing/idleNavigationSpan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,58 @@ describe('startIdleNavigationSpan', () => {
expect(newSpan).toBe(getActiveSpan());
expect(spanToJSON(newSpan!).parent_span_id).toBeUndefined();
});

it('Cancels user interaction span during normal navigation', () => {
const userInteractionSpan = startSpanManual(
{
name: 'ui.action.touch',
op: 'ui.action.touch',
attributes: {
'sentry.origin': 'auto.interaction',
},
},
(span: Span) => span,
);
setActiveSpanOnScope(getCurrentScope(), userInteractionSpan);

const navigationSpan = startIdleNavigationSpan({
name: 'test',
});

expect(spanToJSON(userInteractionSpan).timestamp).toBeDefined();
expect(spanToJSON(userInteractionSpan).status).toBe('cancelled');

expect(navigationSpan).toBe(getActiveSpan());
});

it('Does NOT cancel user interaction span when navigation starts from runApplication (app restart)', () => {
const userInteractionSpan = startSpanManual(
{
name: 'ui.action.touch',
op: 'ui.action.touch',
attributes: {
'sentry.origin': 'auto.interaction',
},
},
(span: Span) => span,
);
setActiveSpanOnScope(getCurrentScope(), userInteractionSpan);

// Start navigation span from runApplication (app restart/reload - e.g. after error)
const navigationSpan = startIdleNavigationSpan(
{
name: 'test',
},
{ isAppRestart: true },
);

// User interaction span should NOT be cancelled/ended - preserving it for replay capture
expect(spanToJSON(userInteractionSpan).timestamp).toBeUndefined();
expect(spanToJSON(userInteractionSpan).status).not.toBe('cancelled');

expect(navigationSpan).toBeDefined();
expect(getActiveSpan()).toBe(navigationSpan);
});
});
});

Expand Down
Loading