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 @@ -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
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/js/tracing/onSpanEndUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/js/tracing/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
157 changes: 157 additions & 0 deletions packages/core/test/tracing/adjustTransactionDuration.test.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lint issue here. A yarn fix should correct that.

  /home/admin/actions-runner/_work/sentry-react-native/sentry-react-native/packages/core/test/tracing/adjustTransactionDuration.test.ts
     39:28  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
     57:28  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
     75:28  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
     93:28  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
    112:28  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
    131:28  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
    153:33  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
    160:35  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion
    178:28  error  This assertion is unnecessary since it does not change the type of the expression  @typescript-eslint/no-unnecessary-type-assertion

Original file line number Diff line number Diff line change
@@ -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' });
});
});
Loading