|
6 | 6 | SEMANTIC_ATTRIBUTE_SENTRY_OP, |
7 | 7 | SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, |
8 | 8 | setCurrentClient, |
| 9 | + startInactiveSpan, |
9 | 10 | timestampInSeconds, |
10 | 11 | } from '@sentry/core'; |
11 | 12 | import { |
@@ -385,6 +386,69 @@ describe('App Start Integration', () => { |
385 | 386 | expect(actualEvent).toStrictEqual(undefined); |
386 | 387 | expect(NATIVE.fetchNativeAppStart).toHaveBeenCalledTimes(1); |
387 | 388 | }); |
| 389 | + |
| 390 | + it('Attaches app start to standalone transaction even when navigation transaction starts first', async () => { |
| 391 | + // This test simulates the Android scenario where React Navigation auto-instrumentation |
| 392 | + // starts a navigation transaction before the standalone app start transaction is created. |
| 393 | + // The fix ensures that when standalone: true, the span ID check is skipped so app start |
| 394 | + // can be attached to the standalone transaction even if a navigation transaction started first. |
| 395 | + getCurrentScope().clear(); |
| 396 | + getIsolationScope().clear(); |
| 397 | + getGlobalScope().clear(); |
| 398 | + |
| 399 | + mockAppStart({ cold: true }); |
| 400 | + |
| 401 | + const integration = appStartIntegration({ |
| 402 | + standalone: true, |
| 403 | + }); |
| 404 | + const client = new TestClient({ |
| 405 | + ...getDefaultTestClientOptions(), |
| 406 | + enableAppStartTracking: true, |
| 407 | + tracesSampleRate: 1.0, |
| 408 | + }); |
| 409 | + setCurrentClient(client); |
| 410 | + integration.setup(client); |
| 411 | + |
| 412 | + // Simulate a navigation transaction starting first (like React Navigation auto-instrumentation) |
| 413 | + // This will set firstStartedActiveRootSpanId to the navigation span's ID |
| 414 | + const navigationSpan = startInactiveSpan({ |
| 415 | + name: 'calendar/home', |
| 416 | + op: 'navigation', |
| 417 | + forceTransaction: true, |
| 418 | + }); |
| 419 | + const navigationSpanId = navigationSpan?.spanContext().spanId; |
| 420 | + if (navigationSpan) { |
| 421 | + navigationSpan.end(); |
| 422 | + } |
| 423 | + |
| 424 | + // Now capture standalone app start - it should still work even though navigation span started first |
| 425 | + // The standalone transaction will have a different span ID, but the fix skips the check |
| 426 | + await integration.captureStandaloneAppStart(); |
| 427 | + |
| 428 | + const actualEvent = client.event as TransactionEvent | undefined; |
| 429 | + expect(actualEvent).toBeDefined(); |
| 430 | + expect(actualEvent?.spans).toBeDefined(); |
| 431 | + expect(actualEvent?.spans?.length).toBeGreaterThan(0); |
| 432 | + |
| 433 | + // Verify that app start was attached successfully |
| 434 | + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); |
| 435 | + expect(appStartSpan).toBeDefined(); |
| 436 | + expect(appStartSpan).toEqual( |
| 437 | + expect.objectContaining<Partial<SpanJSON>>({ |
| 438 | + description: 'Cold Start', |
| 439 | + op: APP_START_COLD_OP, |
| 440 | + }), |
| 441 | + ); |
| 442 | + |
| 443 | + // Verify the standalone transaction has a different span ID than the navigation transaction |
| 444 | + // This confirms that the span ID check was skipped (otherwise app start wouldn't be attached) |
| 445 | + expect(actualEvent?.contexts?.trace?.span_id).toBeDefined(); |
| 446 | + if (navigationSpanId) { |
| 447 | + expect(actualEvent?.contexts?.trace?.span_id).not.toBe(navigationSpanId); |
| 448 | + } |
| 449 | + |
| 450 | + expect(actualEvent?.measurements?.[APP_START_COLD_MEASUREMENT]).toBeDefined(); |
| 451 | + }); |
388 | 452 | }); |
389 | 453 |
|
390 | 454 | describe('App Start Attached to the First Root Span', () => { |
|
0 commit comments