diff --git a/CHANGELOG.md b/CHANGELOG.md index d6eea879d3..22cbc9ab5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixes - Resolve relative `SOURCEMAP_FILE` paths against the project root in the Xcode build script ([#5730](https://github.com/getsentry/sentry-react-native/pull/5730)) +- Fixes the issue with unit mismatch in `adjustTransactionDuration` ([#5740](https://github.com/getsentry/sentry-react-native/pull/5740)) - Handle `inactive` state for spans ([#5742](https://github.com/getsentry/sentry-react-native/pull/5742)) ### Dependencies diff --git a/packages/core/src/js/tracing/onSpanEndUtils.ts b/packages/core/src/js/tracing/onSpanEndUtils.ts index 758560ad06..45a12a04bb 100644 --- a/packages/core/src/js/tracing/onSpanEndUtils.ts +++ b/packages/core/src/js/tracing/onSpanEndUtils.ts @@ -39,8 +39,9 @@ export const adjustTransactionDuration = (client: Client, span: Span, maxDuratio return; } - const diff = endTimestamp - startTimestamp; - const isOutdatedTransaction = endTimestamp && (diff > maxDurationMs || diff < 0); + const diff = endTimestamp - startTimestamp; // a diff in *seconds* + const isOutdatedTransaction = diff > maxDurationMs / 1000 || diff < 0; + if (isOutdatedTransaction) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); // TODO: check where was used, might be possible to delete diff --git a/packages/core/src/js/tracing/span.ts b/packages/core/src/js/tracing/span.ts index 90f2b401b8..7f0aa62622 100644 --- a/packages/core/src/js/tracing/span.ts +++ b/packages/core/src/js/tracing/span.ts @@ -30,18 +30,18 @@ export const defaultIdleOptions: { * * @default 1_000 (ms) */ - finalTimeout: number; + idleTimeout: number; /** * The max. time an idle span may run. * If this time is exceeded, the idle span will finish no matter what. * - * @default 60_0000 (ms) + * @default 600_000 (ms) */ - idleTimeout: number; + finalTimeout: number; } = { idleTimeout: 1_000, - finalTimeout: 60_0000, + finalTimeout: 600_000, }; export const startIdleNavigationSpan = ( diff --git a/packages/core/test/tracing/adjustTransactionDuration.test.ts b/packages/core/test/tracing/adjustTransactionDuration.test.ts new file mode 100644 index 0000000000..95cef9da80 --- /dev/null +++ b/packages/core/test/tracing/adjustTransactionDuration.test.ts @@ -0,0 +1,157 @@ +import { getClient, spanToJSON, startSpanManual } from '@sentry/core'; +import { adjustTransactionDuration } from '../../src/js/tracing/onSpanEndUtils'; +import { setupTestClient } from '../mocks/client'; + +jest.mock('react-native', () => { + return { + AppState: { + isAvailable: true, + currentState: 'active', + addEventListener: jest.fn(() => ({ remove: jest.fn() })), + }, + Platform: { OS: 'ios' }, + NativeModules: { + RNSentry: {}, + }, + }; +}); + +describe('adjustTransactionDuration', () => { + beforeEach(() => { + setupTestClient(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('marks span as deadline_exceeded when duration exceeds maxDurationMs', () => { + const client = getClient()!; + const span = startSpanManual({ name: 'Test Transaction', forceTransaction: true }, span => span); + + const maxDurationMs = 60_000; // 60 seconds + adjustTransactionDuration(client, span, maxDurationMs); + + // End the span 120 seconds after it started (exceeds 60s max) + const startTimestamp = spanToJSON(span).start_timestamp; + span.end(startTimestamp + 120); + + expect(spanToJSON(span).status).toBe('deadline_exceeded'); + expect(spanToJSON(span).data).toMatchObject({ maxTransactionDurationExceeded: 'true' }); + }); + + it('does not mark span as deadline_exceeded when duration is within maxDurationMs', () => { + const client = getClient()!; + const span = startSpanManual({ name: 'Test Transaction', forceTransaction: true }, span => span); + + const maxDurationMs = 60_000; // 60 seconds + adjustTransactionDuration(client, span, maxDurationMs); + + // End the span 30 seconds after it started (within 60s max) + const startTimestamp = spanToJSON(span).start_timestamp; + span.end(startTimestamp + 30); + + expect(spanToJSON(span).status).not.toBe('deadline_exceeded'); + expect(spanToJSON(span).data).not.toMatchObject({ maxTransactionDurationExceeded: 'true' }); + }); + + it('does not mark span as deadline_exceeded when duration equals maxDurationMs exactly', () => { + const client = getClient()!; + const span = startSpanManual({ name: 'Test Transaction', forceTransaction: true }, span => span); + + const maxDurationMs = 60_000; // 60 seconds + adjustTransactionDuration(client, span, maxDurationMs); + + // End the span exactly 60 seconds after it started + const startTimestamp = spanToJSON(span).start_timestamp; + span.end(startTimestamp + 60); + + expect(spanToJSON(span).status).not.toBe('deadline_exceeded'); + expect(spanToJSON(span).data).not.toMatchObject({ maxTransactionDurationExceeded: 'true' }); + }); + + it('marks span as deadline_exceeded when duration is negative', () => { + const client = getClient()!; + const span = startSpanManual({ name: 'Test Transaction', forceTransaction: true }, span => span); + + const maxDurationMs = 60_000; + adjustTransactionDuration(client, span, maxDurationMs); + + // End the span before it started (negative duration) + const startTimestamp = spanToJSON(span).start_timestamp; + span.end(startTimestamp - 10); + + expect(spanToJSON(span).status).toBe('deadline_exceeded'); + expect(spanToJSON(span).data).toMatchObject({ maxTransactionDurationExceeded: 'true' }); + }); + + it('correctly handles maxDurationMs in milliseconds not seconds', () => { + const client = getClient()!; + const span = startSpanManual({ name: 'Test Transaction', forceTransaction: true }, span => span); + + // maxDurationMs = 600_000 ms = 600 seconds = 10 minutes + // A span lasting 601 seconds should exceed this limit + const maxDurationMs = 600_000; + adjustTransactionDuration(client, span, maxDurationMs); + + const startTimestamp = spanToJSON(span).start_timestamp; + span.end(startTimestamp + 601); + + expect(spanToJSON(span).status).toBe('deadline_exceeded'); + expect(spanToJSON(span).data).toMatchObject({ maxTransactionDurationExceeded: 'true' }); + }); + + it('does not mark span when duration is 599 seconds with 600_000ms max', () => { + const client = getClient()!; + const span = startSpanManual({ name: 'Test Transaction', forceTransaction: true }, span => span); + + // maxDurationMs = 600_000 ms = 600 seconds + // A span lasting 599 seconds should NOT exceed this limit + const maxDurationMs = 600_000; + adjustTransactionDuration(client, span, maxDurationMs); + + const startTimestamp = spanToJSON(span).start_timestamp; + span.end(startTimestamp + 599); + + expect(spanToJSON(span).status).not.toBe('deadline_exceeded'); + expect(spanToJSON(span).data).not.toMatchObject({ maxTransactionDurationExceeded: 'true' }); + }); + + it('does not affect spans from other transactions', () => { + const client = getClient()!; + const trackedSpan = startSpanManual({ name: 'Tracked Transaction', forceTransaction: true }, span => span); + const otherSpan = startSpanManual({ name: 'Other Transaction', forceTransaction: true }, span => span); + + const maxDurationMs = 60_000; + adjustTransactionDuration(client, trackedSpan, maxDurationMs); + + // End the other span with a duration that exceeds the limit + const otherStartTimestamp = spanToJSON(otherSpan).start_timestamp; + otherSpan.end(otherStartTimestamp + 120); + + // Other span should not be affected by adjustTransactionDuration + expect(spanToJSON(otherSpan).status).not.toBe('deadline_exceeded'); + + // End tracked span within limits + const trackedStartTimestamp = spanToJSON(trackedSpan).start_timestamp; + trackedSpan.end(trackedStartTimestamp + 30); + + expect(spanToJSON(trackedSpan).status).not.toBe('deadline_exceeded'); + }); + + it('handles very short maxDurationMs values', () => { + const client = getClient()!; + const span = startSpanManual({ name: 'Test Transaction', forceTransaction: true }, span => span); + + // 100ms max duration + const maxDurationMs = 100; + adjustTransactionDuration(client, span, maxDurationMs); + + // End after 1 second (1000ms > 100ms) + const startTimestamp = spanToJSON(span).start_timestamp; + span.end(startTimestamp + 1); + + expect(spanToJSON(span).status).toBe('deadline_exceeded'); + expect(spanToJSON(span).data).toMatchObject({ maxTransactionDurationExceeded: 'true' }); + }); +});