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 @@ -23,6 +23,10 @@
```
- Adds tags with Expo Updates context variables to make them searchable and filterable ([#5788](https://github.com/getsentry/sentry-react-native/pull/5788))

### Fixes

- Defer initial navigation span creation until navigation container is registered ([#5789](https://github.com/getsentry/sentry-react-native/pull/5789))

## 8.3.0

### Features
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/js/tracing/reactnavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export const reactNavigationIntegration = ({
let navigationProcessingSpan: Span | undefined;

let initialStateHandled: boolean = false;
let isSetupComplete: boolean = false;
let stateChangeTimeout: ReturnType<typeof setTimeout> | undefined;
let recentRouteKeys: string[] = [];

Expand Down Expand Up @@ -233,14 +234,15 @@ export const reactNavigationIntegration = ({
}
});

startIdleNavigationSpan();
isSetupComplete = true;

if (!navigationContainer) {
// This is expected as navigation container is registered after the root component is mounted.
return undefined;
}

// Navigation container already registered, just populate with route state
// Navigation container already registered, create and populate initial span
startIdleNavigationSpan();
updateLatestNavigationSpanWithCurrentRoute();
initialStateHandled = true;
};
Expand Down Expand Up @@ -282,8 +284,13 @@ export const reactNavigationIntegration = ({
}

if (!latestNavigationSpan) {
debug.log(`${INTEGRATION_NAME} Navigation container registered, but integration has not been setup yet.`);
return undefined;
if (!isSetupComplete) {
debug.log(
`${INTEGRATION_NAME} Navigation container registered before integration setup. Initial span will be created when setup completes.`,
);
return undefined;
}
startIdleNavigationSpan();
}

// Navigation Container is registered after the first navigation
Expand Down
170 changes: 170 additions & 0 deletions packages/core/test/tracing/reactnavigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,176 @@ describe('ReactNavigationInstrumentation', () => {
});
});

describe('deferred initial span creation', () => {
test('initial span is created in registerNavigationContainer when called after afterAllSetup', async () => {
const instrumentation = reactNavigationIntegration({
routeChangeTimeoutMs: 200,
});

const options = getDefaultTestClientOptions({
enableNativeFramesTracking: false,
enableStallTracking: false,
tracesSampleRate: 1.0,
integrations: [instrumentation],
enableAppStartTracking: false,
});
client = new TestClient(options);
setCurrentClient(client);
client.init();

// No span created yet โ€” no navigation container registered
expect(getActiveSpan()).toBeUndefined();

// Register navigation container after init (production flow)
const mockNavigationContainer = new MockNavigationContainer();
instrumentation.registerNavigationContainer({ current: mockNavigationContainer });

const span = getActiveSpan();
expect(span).toBeDefined();
expect(spanToJSON(span!).description).toBe('Route');

await jest.runOnlyPendingTimersAsync();
await client.flush();

expect(client.event).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'Route',
}),
);
});

test('initial span is not discarded by idle timeout when container registers after init', async () => {
const instrumentation = reactNavigationIntegration({
routeChangeTimeoutMs: 200,
});

const options = getDefaultTestClientOptions({
enableNativeFramesTracking: false,
enableStallTracking: false,
tracesSampleRate: 1.0,
integrations: [instrumentation],
enableAppStartTracking: false,
});
client = new TestClient(options);
setCurrentClient(client);
client.init();

// Simulate a delay before container registration (e.g. auth gate, slow render)
jest.advanceTimersByTime(500);
expect(getActiveSpan()).toBeUndefined();

// Register after 500ms โ€” should still create the span successfully
const mockNavigationContainer = new MockNavigationContainer();
instrumentation.registerNavigationContainer({ current: mockNavigationContainer });

const span = getActiveSpan();
expect(span).toBeDefined();

await jest.runOnlyPendingTimersAsync();
await client.flush();

expect(client.event).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'Route',
}),
);
});

test('afterAllSetup does not create span when navigation container is not registered', () => {
const instrumentation = reactNavigationIntegration();

const options = getDefaultTestClientOptions({
enableNativeFramesTracking: false,
enableStallTracking: false,
tracesSampleRate: 1.0,
integrations: [instrumentation],
enableAppStartTracking: false,
});
client = new TestClient(options);
setCurrentClient(client);
client.init();

// No navigation container registered โ€” no span should be created
expect(getActiveSpan()).toBeUndefined();
});

test('subsequent navigations work after deferred initial span', async () => {
const instrumentation = reactNavigationIntegration({
routeChangeTimeoutMs: 200,
});

const options = getDefaultTestClientOptions({
enableNativeFramesTracking: false,
enableStallTracking: false,
tracesSampleRate: 1.0,
integrations: [instrumentation],
enableAppStartTracking: false,
});
client = new TestClient(options);
setCurrentClient(client);
client.init();

mockNavigation = createMockNavigationAndAttachTo(instrumentation);

// Flush the initial span
jest.runOnlyPendingTimers();
await client.flush();

expect(client.event).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'Initial Screen',
}),
);

// Navigate to a new screen
mockNavigation.navigateToNewScreen();
jest.runOnlyPendingTimers();
await client.flush();

expect(client.event).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'New Screen',
}),
);
});

test('runApplication creates span after deferred initial state is handled', async () => {
const { mockedOnRunApplication } = mockAppRegistryIntegration();

const instrumentation = reactNavigationIntegration();

const options = getDefaultTestClientOptions({
enableNativeFramesTracking: false,
enableStallTracking: false,
tracesSampleRate: 1.0,
integrations: [instrumentation],
enableAppStartTracking: false,
});
client = new TestClient(options);
setCurrentClient(client);
client.init();

// Register container after init
const mockNavigationContainer = new MockNavigationContainer();
instrumentation.registerNavigationContainer({ current: mockNavigationContainer });

// Flush initial span
await jest.runOnlyPendingTimersAsync();

// Simulate app restart via runApplication
const runApplicationCallback = mockedOnRunApplication.mock.calls[0][0];
runApplicationCallback();

const span = getActiveSpan();
expect(span).toBeDefined();
expect(spanToJSON(span!).op).toBe('navigation');
});
});

describe('options', () => {
test('waits until routeChangeTimeoutMs', () => {
const instrumentation = reactNavigationIntegration({
Expand Down
Loading