Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 3 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 @@ -210,7 +210,7 @@ export const reactNavigationIntegration = ({
* 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
*/
const startIdleNavigationSpan = (unknownEvent?: unknown): void => {
const startIdleNavigationSpan = (unknownEvent?: unknown, isFromRunApplication = 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 +245,7 @@ export const reactNavigationIntegration = ({
tracing?.options.beforeStartSpan
? tracing.options.beforeStartSpan(getDefaultIdleNavigationSpanOptions())
: getDefaultIdleNavigationSpanOptions(),
idleSpanOptions,
{ ...idleSpanOptions, isFromRunApplication },
);
latestNavigationSpan?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION);
latestNavigationSpan?.setAttribute(SEMANTIC_ATTRIBUTE_NAVIGATION_ACTION_TYPE, navigationActionType);
Expand Down
19 changes: 16 additions & 3 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> = {},
isFromRunApplication = false,
}: Partial<typeof defaultIdleOptions> & { isFromRunApplication?: boolean } = {},
): Span | undefined => {
const client = getClient();
if (!client) {
Expand All @@ -58,15 +59,27 @@ export const startIdleNavigationSpan = (
}

const activeSpan = getActiveSpan();
clearActiveSpanFromScope(getCurrentScope());
if (activeSpan && isRootSpan(activeSpan) && isSentryInteractionSpan(activeSpan)) {
const isActiveSpanInteraction = 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 && isFromRunApplication) {
debug.log(
`[startIdleNavigationSpan] Not canceling ${
spanToJSON(activeSpan).op
} transaction because navigation is from app restart - preserving error context.`,
);
} else if (isActiveSpanInteraction) {
debug.log(
`[startIdleNavigationSpan] Canceling ${
spanToJSON(activeSpan).op
} transaction because of a new navigation root span.`,
);
clearActiveSpanFromScope(getCurrentScope());
activeSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' });
activeSpan.end();
} else {
clearActiveSpanFromScope(getCurrentScope());
}

const finalStartSpanOptions = {
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',
},
{ isFromRunApplication: 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