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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### Features

- Add breadcrumbs for dispatched React Navigation events ([#6218](https://github.com/getsentry/sentry-react-native/pull/6218))
- Add `Sentry.NavigationContainer` drop-in wrapper for React Navigation ([#6199](https://github.com/getsentry/sentry-react-native/pull/6199))
- Opt-in: consume sentry-cocoa via Swift Package Manager. Set `SENTRY_USE_SPM=1` before `pod install` to pull `Sentry` from sentry-cocoa's SPM package as a binary xcframework instead of the CocoaPods source build ([#6182](https://github.com/getsentry/sentry-react-native/pull/6182))
- Add `disableAutoUpload` option to Expo plugin to disable source map and debug symbol uploads ([#6195](https://github.com/getsentry/sentry-react-native/pull/6195))
Expand Down
22 changes: 20 additions & 2 deletions packages/core/src/js/tracing/reactnavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,12 +347,30 @@ export const reactNavigationIntegration = ({
// oxlint-disable-next-line eslint(complexity)
const startIdleNavigationSpan = (unknownEvent?: unknown, isAppRestart = false): void => {
const event = unknownEvent as UnsafeAction | undefined;
const actionType = event?.data?.action?.type;
const targetRouteName = getRouteNameFromAction(event);

if (event && !isAppRestart && !event.data?.noop) {
addBreadcrumb({
category: 'navigation.dispatch',
type: 'navigation',
message: targetRouteName
? `Dispatched ${actionType ?? 'NAVIGATE'} to ${targetRouteName}`
: `Dispatched ${actionType ?? 'NAVIGATE'}`,
data: {
...(actionType ? { action_type: actionType } : undefined),
...(targetRouteName ? { to: targetRouteName } : undefined),
},
level: 'info',
});
}
Comment thread
antonis marked this conversation as resolved.

if (useDispatchedActionData && event?.data.noop) {
debug.log(`${INTEGRATION_NAME} Navigation action is a noop, not starting navigation span.`);
return;
}

const navigationActionType = useDispatchedActionData ? event?.data.action.type : undefined;
const navigationActionType = useDispatchedActionData ? actionType : undefined;

// Handle PRELOAD actions separately if prefetch tracking is enabled
if (enablePrefetchTracking && navigationActionType === 'PRELOAD') {
Expand Down Expand Up @@ -407,7 +425,7 @@ export const reactNavigationIntegration = ({
}

// Extract route name from dispatch action payload when available
const dispatchedRouteName = useDispatchedActionData ? getRouteNameFromAction(event) : undefined;
const dispatchedRouteName = useDispatchedActionData ? targetRouteName : undefined;
if (useDispatchedActionData && event && !dispatchedRouteName && !isAppRestart) {
debug.log(`${INTEGRATION_NAME} Navigation action has no route name in payload, not starting navigation span.`);
return;
Expand Down
182 changes: 182 additions & 0 deletions packages/core/test/tracing/reactnavigation.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Event, Measurements, SentrySpan, StartSpanOptions } from '@sentry/core';

import * as core from '@sentry/core';
import {
getActiveSpan,
getCurrentScope,
Expand Down Expand Up @@ -1641,6 +1642,187 @@ describe('ReactNavigationInstrumentation', () => {
});
});

describe('dispatch breadcrumbs', () => {
let addBreadcrumbSpy: jest.SpyInstance;

beforeEach(() => {
addBreadcrumbSpy = jest.spyOn(core, 'addBreadcrumb');
});

afterEach(() => {
addBreadcrumbSpy.mockRestore();
});

it('includes action type and route name even when useDispatchedActionData is disabled', async () => {
setupTestClient();
mockNavigation.navigateToNewScreenWithPayload();
await jest.advanceTimersByTimeAsync(500);

expect(addBreadcrumbSpy).toHaveBeenCalledWith(
expect.objectContaining({
category: 'navigation.dispatch',
type: 'navigation',
message: 'Dispatched NAVIGATE to New Screen',
data: expect.objectContaining({
action_type: 'NAVIGATE',
to: 'New Screen',
}),
}),
);
});

it('includes action type and route name when useDispatchedActionData is enabled', async () => {
setupTestClient({ useDispatchedActionData: true });
mockNavigation.navigateToNewScreenWithPayload();
await jest.advanceTimersByTimeAsync(500);

expect(addBreadcrumbSpy).toHaveBeenCalledWith(
expect.objectContaining({
category: 'navigation.dispatch',
type: 'navigation',
message: 'Dispatched NAVIGATE to New Screen',
data: expect.objectContaining({
action_type: 'NAVIGATE',
to: 'New Screen',
}),
}),
);
});

it('creates dispatch breadcrumb without route name for GO_BACK', async () => {
setupTestClient({ useDispatchedActionData: true });
mockNavigation.emitGoBackWithStateChange();
await jest.advanceTimersByTimeAsync(500);

expect(addBreadcrumbSpy).toHaveBeenCalledWith(
expect.objectContaining({
category: 'navigation.dispatch',
type: 'navigation',
message: 'Dispatched GO_BACK',
data: expect.objectContaining({
action_type: 'GO_BACK',
}),
}),
);
});

it('creates dispatch breadcrumb for filtered actions like SET_PARAMS', async () => {
setupTestClient({ useDispatchedActionData: true });
mockNavigation.emitWithoutStateChange({
data: {
action: { type: 'SET_PARAMS' },
noop: false,
stack: undefined,
},
});
await jest.advanceTimersByTimeAsync(500);

expect(addBreadcrumbSpy).toHaveBeenCalledWith(
expect.objectContaining({
category: 'navigation.dispatch',
message: 'Dispatched SET_PARAMS',
data: expect.objectContaining({
action_type: 'SET_PARAMS',
}),
}),
);
});

it('creates dispatch breadcrumb for drawer actions', async () => {
setupTestClient({ useDispatchedActionData: true });
mockNavigation.emitWithoutStateChange({
data: {
action: { type: 'OPEN_DRAWER' },
noop: false,
stack: undefined,
},
});
await jest.advanceTimersByTimeAsync(500);

expect(addBreadcrumbSpy).toHaveBeenCalledWith(
expect.objectContaining({
category: 'navigation.dispatch',
message: 'Dispatched OPEN_DRAWER',
data: expect.objectContaining({
action_type: 'OPEN_DRAWER',
}),
}),
);
});

it('does not create dispatch breadcrumb for noop actions when useDispatchedActionData is enabled', async () => {
setupTestClient({ useDispatchedActionData: true });
mockNavigation.emitWithoutStateChange({
data: {
action: { type: 'NAVIGATE' },
noop: true,
stack: undefined,
},
});
await jest.advanceTimersByTimeAsync(500);

const dispatchCall = addBreadcrumbSpy.mock.calls.find(
(call: unknown[]) => (call[0] as { category?: string }).category === 'navigation.dispatch',
);
expect(dispatchCall).toBeUndefined();
});

it('does not create dispatch breadcrumb for noop actions when useDispatchedActionData is disabled', async () => {
setupTestClient();
mockNavigation.emitWithoutStateChange({
data: {
action: { type: 'NAVIGATE' },
noop: true,
stack: undefined,
},
});
await jest.advanceTimersByTimeAsync(500);

const dispatchCall = addBreadcrumbSpy.mock.calls.find(
(call: unknown[]) => (call[0] as { category?: string }).category === 'navigation.dispatch',
);
expect(dispatchCall).toBeUndefined();
});

it('still creates navigation breadcrumb on completed navigation', async () => {
setupTestClient();
mockNavigation.navigateToNewScreen();
await jest.advanceTimersByTimeAsync(500);

expect(addBreadcrumbSpy).toHaveBeenCalledWith(
expect.objectContaining({
category: 'navigation',
type: 'navigation',
message: 'Navigation to New Screen',
}),
);
});

it('does not create dispatch breadcrumb for app restart', async () => {
const rNavigation = reactNavigationIntegration({ routeChangeTimeoutMs: 200 });
const rnTracing = reactNativeTracingIntegration();
const options = getDefaultTestClientOptions({
enableNativeFramesTracking: false,
enableStallTracking: false,
tracesSampleRate: 1.0,
integrations: [rNavigation, rnTracing],
enableAppStartTracking: false,
});
client = new TestClient(options);
setCurrentClient(client);
client.init();
mockNavigation = createMockNavigationAndAttachTo(rNavigation);

mockNavigation.finishAppStartNavigation();
await jest.advanceTimersByTimeAsync(500);

const dispatchCall = addBreadcrumbSpy.mock.calls.find(
(call: unknown[]) => (call[0] as { category?: string }).category === 'navigation.dispatch',
);
expect(dispatchCall).toBeUndefined();
});
});

function setupTestClient(
setupOptions: {
beforeSpanStart?: (options: StartSpanOptions) => StartSpanOptions;
Expand Down
Loading