From c0f2daae00001a1cf8879aef0760a8b7087a1e4b Mon Sep 17 00:00:00 2001 From: Matt Quinn Date: Mon, 29 Jun 2026 11:45:07 -0400 Subject: [PATCH 1/3] feat(replays): Record segment names that occur during replay --- packages/core/src/types/replay.ts | 1 + .../coreHandlers/handleAfterSegmentSpanEnd.ts | 19 ----- .../src/coreHandlers/handleAfterSendEvent.ts | 6 +- .../coreHandlers/handleProcessSegmentSpan.ts | 19 +++++ .../util/addSegmentDetailsToContext.ts | 13 ++++ .../coreHandlers/util/addTraceIdToContext.ts | 10 --- packages/replay-internal/src/replay.ts | 3 + packages/replay-internal/src/types/replay.ts | 10 +++ .../src/util/addGlobalListeners.ts | 4 +- .../src/util/sendReplayRequest.ts | 3 +- ...st.ts => handleProcessSegmentSpan.test.ts} | 76 +++++++++++++------ .../test/integration/sampling.test.ts | 2 + .../test/integration/session.test.ts | 1 + 13 files changed, 110 insertions(+), 57 deletions(-) delete mode 100644 packages/replay-internal/src/coreHandlers/handleAfterSegmentSpanEnd.ts create mode 100644 packages/replay-internal/src/coreHandlers/handleProcessSegmentSpan.ts create mode 100644 packages/replay-internal/src/coreHandlers/util/addSegmentDetailsToContext.ts delete mode 100644 packages/replay-internal/src/coreHandlers/util/addTraceIdToContext.ts rename packages/replay-internal/test/integration/coreHandlers/{handleAfterSegmentSpanEnd.test.ts => handleProcessSegmentSpan.test.ts} (51%) diff --git a/packages/core/src/types/replay.ts b/packages/core/src/types/replay.ts index a23f548aa357..862be2ff07ef 100644 --- a/packages/core/src/types/replay.ts +++ b/packages/core/src/types/replay.ts @@ -9,6 +9,7 @@ export interface ReplayEvent extends Event { replay_start_timestamp?: number; error_ids: string[]; trace_ids: string[]; + segment_names: string[]; replay_id: string; segment_id: number; replay_type: ReplayRecordingMode; diff --git a/packages/replay-internal/src/coreHandlers/handleAfterSegmentSpanEnd.ts b/packages/replay-internal/src/coreHandlers/handleAfterSegmentSpanEnd.ts deleted file mode 100644 index 6d98b94559ec..000000000000 --- a/packages/replay-internal/src/coreHandlers/handleAfterSegmentSpanEnd.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Span } from '@sentry/core'; -import { spanIsSampled } from '@sentry/core'; -import type { ReplayContainer } from '../types'; -import { addTraceIdToContext } from './util/addTraceIdToContext'; - -type AfterSegmentSpanEndCallback = (segmentSpan: Span) => void; - -export function handleAfterSegmentSpanEnd(replay: ReplayContainer): AfterSegmentSpanEndCallback { - return (segmentSpan: Span) => { - if (!replay.isEnabled() || !spanIsSampled(segmentSpan)) { - return; - } - - const traceId = segmentSpan.spanContext().traceId; - if (traceId) { - addTraceIdToContext(replay, traceId); - } - }; -} diff --git a/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts b/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts index 3e78959580f0..d7247dd47d92 100644 --- a/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts +++ b/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts @@ -2,7 +2,7 @@ import type { ErrorEvent, Event, TransactionEvent, TransportMakeRequestResponse import { setTimeout } from '@sentry/browser-utils'; import type { ReplayContainer } from '../types'; import { isErrorEvent, isTransactionEvent } from '../util/eventUtils'; -import { addTraceIdToContext } from './util/addTraceIdToContext'; +import { addSegmentDetailsToContext } from './util/addSegmentDetailsToContext'; type AfterSendEventCallback = (event: Event, sendResponse: TransportMakeRequestResponse) => void; @@ -34,8 +34,8 @@ export function handleAfterSendEvent(replay: ReplayContainer): AfterSendEventCal function handleTransactionEvent(replay: ReplayContainer, event: TransactionEvent): void { const traceId = event.contexts?.trace?.trace_id; - if (traceId) { - addTraceIdToContext(replay, traceId); + if (traceId && event.transaction) { + addSegmentDetailsToContext(replay, traceId, event.transaction); } } diff --git a/packages/replay-internal/src/coreHandlers/handleProcessSegmentSpan.ts b/packages/replay-internal/src/coreHandlers/handleProcessSegmentSpan.ts new file mode 100644 index 000000000000..f6ad31b8c5f2 --- /dev/null +++ b/packages/replay-internal/src/coreHandlers/handleProcessSegmentSpan.ts @@ -0,0 +1,19 @@ +import type { StreamedSpanJSON } from '@sentry/core'; +import type { ReplayContainer } from '../types'; +import { addSegmentDetailsToContext } from './util/addSegmentDetailsToContext'; + +type ProcessSegmentSpanCallback = (spanJSON: StreamedSpanJSON) => void; + +export function handleProcessSegmentSpan(replay: ReplayContainer): ProcessSegmentSpanCallback { + return (spanJSON: StreamedSpanJSON) => { + if (!replay.isEnabled()) { + return; + } + + const traceId = spanJSON.trace_id; + const segmentName = spanJSON.name; + if (traceId && segmentName) { + addSegmentDetailsToContext(replay, traceId, segmentName); + } + }; +} diff --git a/packages/replay-internal/src/coreHandlers/util/addSegmentDetailsToContext.ts b/packages/replay-internal/src/coreHandlers/util/addSegmentDetailsToContext.ts new file mode 100644 index 000000000000..cb5b1a3f2d0e --- /dev/null +++ b/packages/replay-internal/src/coreHandlers/util/addSegmentDetailsToContext.ts @@ -0,0 +1,13 @@ +import type { ReplayContainer } from '../../types'; + +const MAX_CONTEXT_VALUES = 100; + +export function addSegmentDetailsToContext(replay: ReplayContainer, traceId: string, segmentName: string): void { + const replayContext = replay.getContext(); + if (replayContext.traceIds.size < MAX_CONTEXT_VALUES) { + replayContext.traceIds.add(traceId); + } + if (replayContext.segmentNames.size < MAX_CONTEXT_VALUES) { + replayContext.segmentNames.add(segmentName); + } +} diff --git a/packages/replay-internal/src/coreHandlers/util/addTraceIdToContext.ts b/packages/replay-internal/src/coreHandlers/util/addTraceIdToContext.ts deleted file mode 100644 index a3302c1ba661..000000000000 --- a/packages/replay-internal/src/coreHandlers/util/addTraceIdToContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ReplayContainer } from '../../types'; - -const MAX_TRACE_IDS = 100; - -export function addTraceIdToContext(replay: ReplayContainer, traceId: string): void { - const replayContext = replay.getContext(); - if (replayContext.traceIds.size < MAX_TRACE_IDS) { - replayContext.traceIds.add(traceId); - } -} diff --git a/packages/replay-internal/src/replay.ts b/packages/replay-internal/src/replay.ts index 947d0485ed3d..439ae124f4a5 100644 --- a/packages/replay-internal/src/replay.ts +++ b/packages/replay-internal/src/replay.ts @@ -195,6 +195,7 @@ export class ReplayContainer implements ReplayContainerInterface { this._context = { errorIds: new Set(), traceIds: new Set(), + segmentNames: new Set(), urls: [], initialTimestamp: Date.now(), initialUrl: '', @@ -1122,6 +1123,7 @@ export class ReplayContainer implements ReplayContainerInterface { // XXX: `initialTimestamp` and `initialUrl` do not get cleared this._context.errorIds.clear(); this._context.traceIds.clear(); + this._context.segmentNames.clear(); this._context.urls = []; } @@ -1154,6 +1156,7 @@ export class ReplayContainer implements ReplayContainerInterface { initialUrl: this._context.initialUrl, errorIds: Array.from(this._context.errorIds), traceIds: Array.from(this._context.traceIds), + segmentNames: Array.from(this._context.segmentNames), urls: this._context.urls, }; diff --git a/packages/replay-internal/src/types/replay.ts b/packages/replay-internal/src/types/replay.ts index 95cfbdd849bf..955393c2f225 100644 --- a/packages/replay-internal/src/types/replay.ts +++ b/packages/replay-internal/src/types/replay.ts @@ -360,6 +360,11 @@ export interface PopEventContext extends CommonEventContext { * List of Sentry trace ids that have occurred during a replay segment */ traceIds: Array; + + /** + * List of Sentry segment names that have occurred during a replay segment + */ + segmentNames: Array; } /** @@ -375,6 +380,11 @@ export interface InternalEventContext extends CommonEventContext { * Set of Sentry trace ids that have occurred during a replay segment */ traceIds: Set; + + /** + * Set of Sentry segment names that have occurred during a replay segment + */ + segmentNames: Set; } export type Sampled = false | 'session' | 'buffer'; diff --git a/packages/replay-internal/src/util/addGlobalListeners.ts b/packages/replay-internal/src/util/addGlobalListeners.ts index 7ca4fd91f4f1..ac4d228f8976 100644 --- a/packages/replay-internal/src/util/addGlobalListeners.ts +++ b/packages/replay-internal/src/util/addGlobalListeners.ts @@ -1,7 +1,7 @@ import type { DynamicSamplingContext } from '@sentry/core'; import { addEventProcessor, getClient } from '@sentry/core'; import { addClickKeypressInstrumentationHandler, addHistoryInstrumentationHandler } from '@sentry/browser-utils'; -import { handleAfterSegmentSpanEnd } from '../coreHandlers/handleAfterSegmentSpanEnd'; +import { handleProcessSegmentSpan } from '../coreHandlers/handleProcessSegmentSpan'; import { handleAfterSendEvent } from '../coreHandlers/handleAfterSendEvent'; import { handleBeforeSendEvent } from '../coreHandlers/handleBeforeSendEvent'; import { handleBreadcrumbs } from '../coreHandlers/handleBreadcrumbs'; @@ -44,7 +44,7 @@ export function addGlobalListeners(replay: ReplayContainer): void { } }); - client.on('afterSegmentSpanEnd', handleAfterSegmentSpanEnd(replay)); + client.on('processSegmentSpan', handleProcessSegmentSpan(replay)); client.on('spanStart', span => { replay.lastActiveSpan = span; diff --git a/packages/replay-internal/src/util/sendReplayRequest.ts b/packages/replay-internal/src/util/sendReplayRequest.ts index 777b3f970712..6dea2a9821eb 100644 --- a/packages/replay-internal/src/util/sendReplayRequest.ts +++ b/packages/replay-internal/src/util/sendReplayRequest.ts @@ -26,7 +26,7 @@ export async function sendReplayRequest({ }, }); - const { urls, errorIds, traceIds, initialTimestamp } = eventContext; + const { urls, errorIds, traceIds, segmentNames, initialTimestamp } = eventContext; const client = getClient(); const scope = getCurrentScope(); @@ -43,6 +43,7 @@ export async function sendReplayRequest({ timestamp: timestamp / 1000, error_ids: errorIds, trace_ids: traceIds, + segment_names: segmentNames, urls, replay_id: replayId, segment_id, diff --git a/packages/replay-internal/test/integration/coreHandlers/handleAfterSegmentSpanEnd.test.ts b/packages/replay-internal/test/integration/coreHandlers/handleProcessSegmentSpan.test.ts similarity index 51% rename from packages/replay-internal/test/integration/coreHandlers/handleAfterSegmentSpanEnd.test.ts rename to packages/replay-internal/test/integration/coreHandlers/handleProcessSegmentSpan.test.ts index 45c754ce153a..88108784c961 100644 --- a/packages/replay-internal/test/integration/coreHandlers/handleAfterSegmentSpanEnd.test.ts +++ b/packages/replay-internal/test/integration/coreHandlers/handleProcessSegmentSpan.test.ts @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ -import type { Span } from '@sentry/core'; +import type { StreamedSpanJSON } from '@sentry/core'; import { getClient } from '@sentry/core'; import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import type { ReplayContainer } from '../../../src/replay'; @@ -10,7 +10,7 @@ import { resetSdkMock } from '../../mocks/resetSdkMock'; let replay: ReplayContainer; -describe('Integration | coreHandlers | handleAfterSegmentSpanEnd', () => { +describe('Integration | coreHandlers | handleProcessSegmentSpan', () => { beforeAll(() => { vi.useFakeTimers(); }); @@ -19,7 +19,7 @@ describe('Integration | coreHandlers | handleAfterSegmentSpanEnd', () => { replay.stop(); }); - it('records traceIds from afterSegmentSpanEnd', async () => { + it('records traceIds and segment names from processSegmentSpan', async () => { ({ replay } = await resetSdkMock({ replayOptions: { stickySession: false, @@ -32,18 +32,31 @@ describe('Integration | coreHandlers | handleAfterSegmentSpanEnd', () => { const client = getClient()!; - client.emit('afterSegmentSpanEnd', { - spanContext: () => ({ traceId: 'trace-stream-1', spanId: 'span1', traceFlags: 1 }), - } as unknown as Span); - - client.emit('afterSegmentSpanEnd', { - spanContext: () => ({ traceId: 'trace-stream-2', spanId: 'span2', traceFlags: 1 }), - } as unknown as Span); + client.emit('processSegmentSpan', { + trace_id: 'trace-stream-1', + span_id: 'span1', + name: 'GET /api/users', + is_segment: true, + start_timestamp: 0, + end_timestamp: 1, + status: 'ok', + } as StreamedSpanJSON); + + client.emit('processSegmentSpan', { + trace_id: 'trace-stream-2', + span_id: 'span2', + name: 'POST /api/items', + is_segment: true, + start_timestamp: 0, + end_timestamp: 1, + status: 'ok', + } as StreamedSpanJSON); expect(Array.from(replay.getContext().traceIds)).toEqual(['trace-stream-1', 'trace-stream-2']); + expect(Array.from(replay.getContext().segmentNames)).toEqual(['GET /api/users', 'POST /api/items']); }); - it('limits traceIds from afterSegmentSpanEnd to max. 100', async () => { + it('limits traceIds from processSegmentSpan to max. 100', async () => { ({ replay } = await resetSdkMock({ replayOptions: { stickySession: false, @@ -57,9 +70,15 @@ describe('Integration | coreHandlers | handleAfterSegmentSpanEnd', () => { const client = getClient()!; for (let i = 0; i < 150; i++) { - client.emit('afterSegmentSpanEnd', { - spanContext: () => ({ traceId: `tr-${i}`, spanId: `sp-${i}`, traceFlags: 1 }), - } as unknown as Span); + client.emit('processSegmentSpan', { + trace_id: `tr-${i}`, + span_id: `sp-${i}`, + name: `segment-${i}`, + is_segment: true, + start_timestamp: 0, + end_timestamp: 1, + status: 'ok', + } as StreamedSpanJSON); } expect(replay.getContext().traceIds.size).toBe(100); @@ -70,7 +89,7 @@ describe('Integration | coreHandlers | handleAfterSegmentSpanEnd', () => { ); }); - it('does not record traceIds from afterSegmentSpanEnd when replay is disabled', async () => { + it('does not record traceIds from processSegmentSpan when replay is disabled', async () => { ({ replay } = await resetSdkMock({ replayOptions: { stickySession: false, @@ -85,14 +104,20 @@ describe('Integration | coreHandlers | handleAfterSegmentSpanEnd', () => { replay['_isEnabled'] = false; - client.emit('afterSegmentSpanEnd', { - spanContext: () => ({ traceId: 'trace-stream-1', spanId: 'span1', traceFlags: 1 }), - } as unknown as Span); + client.emit('processSegmentSpan', { + trace_id: 'trace-stream-1', + span_id: 'span1', + name: 'GET /api/users', + is_segment: true, + start_timestamp: 0, + end_timestamp: 1, + status: 'ok', + } as StreamedSpanJSON); expect(Array.from(replay.getContext().traceIds)).toEqual([]); }); - it('does not record traceIds for unsampled spans', async () => { + it('does not record segment spans with empty names', async () => { ({ replay } = await resetSdkMock({ replayOptions: { stickySession: false, @@ -105,10 +130,17 @@ describe('Integration | coreHandlers | handleAfterSegmentSpanEnd', () => { const client = getClient()!; - client.emit('afterSegmentSpanEnd', { - spanContext: () => ({ traceId: 'trace-unsampled', spanId: 'span1', traceFlags: 0 }), - } as unknown as Span); + client.emit('processSegmentSpan', { + trace_id: 'trace-stream-1', + span_id: 'span1', + name: '', + is_segment: true, + start_timestamp: 0, + end_timestamp: 1, + status: 'ok', + } as StreamedSpanJSON); expect(Array.from(replay.getContext().traceIds)).toEqual([]); + expect(Array.from(replay.getContext().segmentNames)).toEqual([]); }); }); diff --git a/packages/replay-internal/test/integration/sampling.test.ts b/packages/replay-internal/test/integration/sampling.test.ts index 9ffa00c349c6..a86bf54b924a 100644 --- a/packages/replay-internal/test/integration/sampling.test.ts +++ b/packages/replay-internal/test/integration/sampling.test.ts @@ -38,6 +38,7 @@ describe('Integration | sampling', () => { expect(replay.getContext()).toEqual({ errorIds: new Set(), traceIds: new Set(), + segmentNames: new Set(), urls: [], initialTimestamp: expect.any(Number), initialUrl: '', @@ -79,6 +80,7 @@ describe('Integration | sampling', () => { initialTimestamp: expect.any(Number), initialUrl: 'http://localhost:3000/', traceIds: new Set(), + segmentNames: new Set(), urls: ['http://localhost:3000/'], }); expect(replay.recordingMode).toBe('buffer'); diff --git a/packages/replay-internal/test/integration/session.test.ts b/packages/replay-internal/test/integration/session.test.ts index 1c4b49bb1fad..31322dc4a3f8 100644 --- a/packages/replay-internal/test/integration/session.test.ts +++ b/packages/replay-internal/test/integration/session.test.ts @@ -253,6 +253,7 @@ describe('Integration | session', () => { urls: [], errorIds: new Set(), traceIds: new Set(), + segmentNames: new Set(), }); }); From 8f45b6360a8bc27ec4bcb76fc9a2a1291ad96dc9 Mon Sep 17 00:00:00 2001 From: Matt Quinn Date: Mon, 29 Jun 2026 14:44:03 -0400 Subject: [PATCH 2/3] fix integration tests --- .../suites/replay/captureReplay/test.ts | 2 ++ .../suites/replay/captureReplayFromReplayPackage/test.ts | 2 ++ .../browser-integration-tests/utils/replayEventTemplates.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts index ff200ae09869..92634cf92b5a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts @@ -29,6 +29,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 0, + segment_names: [], replay_type: 'session', event_id: expect.stringMatching(/\w{32}/), environment: 'production', @@ -77,6 +78,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 1, + segment_names: [], replay_type: 'session', event_id: expect.stringMatching(/\w{32}/), environment: 'production', diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts index 3a312bdb0e2a..05b419bf9636 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts @@ -29,6 +29,7 @@ sentryTest('should capture replays (@sentry/replay export)', async ({ getLocalTe replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 0, + segment_names: [], replay_type: 'session', event_id: expect.stringMatching(/\w{32}/), environment: 'production', @@ -77,6 +78,7 @@ sentryTest('should capture replays (@sentry/replay export)', async ({ getLocalTe replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 1, + segment_names: [], replay_type: 'session', event_id: expect.stringMatching(/\w{32}/), environment: 'production', diff --git a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts index b84818738d74..6f6ba2136ea8 100644 --- a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts +++ b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts @@ -12,6 +12,7 @@ const DEFAULT_REPLAY_EVENT = { replay_id: expect.stringMatching(/\w{32}/), replay_start_timestamp: expect.any(Number), segment_id: 0, + segment_names: [], replay_type: 'session', event_id: expect.stringMatching(/\w{32}/), environment: 'production', From 79864665e3626f1bf01c6b83b02e9d4f49518a90 Mon Sep 17 00:00:00 2001 From: Matt Quinn Date: Mon, 6 Jul 2026 15:25:47 -0400 Subject: [PATCH 3/3] review comments: decouple; missing tests --- .../src/coreHandlers/handleAfterSendEvent.ts | 5 +- .../coreHandlers/handleProcessSegmentSpan.ts | 6 +-- .../util/addSegmentDetailsToContext.ts | 6 +-- .../coreHandlers/handleAfterSendEvent.test.ts | 47 ++++++++++++++++++- .../handleProcessSegmentSpan.test.ts | 30 +++++++++++- 5 files changed, 79 insertions(+), 15 deletions(-) diff --git a/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts b/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts index d7247dd47d92..aeb4a1ef75b1 100644 --- a/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts +++ b/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts @@ -33,10 +33,7 @@ export function handleAfterSendEvent(replay: ReplayContainer): AfterSendEventCal } function handleTransactionEvent(replay: ReplayContainer, event: TransactionEvent): void { - const traceId = event.contexts?.trace?.trace_id; - if (traceId && event.transaction) { - addSegmentDetailsToContext(replay, traceId, event.transaction); - } + addSegmentDetailsToContext(replay, event.contexts?.trace?.trace_id, event.transaction); } function handleErrorEvent(replay: ReplayContainer, event: ErrorEvent): void { diff --git a/packages/replay-internal/src/coreHandlers/handleProcessSegmentSpan.ts b/packages/replay-internal/src/coreHandlers/handleProcessSegmentSpan.ts index f6ad31b8c5f2..cdc8184a7dd0 100644 --- a/packages/replay-internal/src/coreHandlers/handleProcessSegmentSpan.ts +++ b/packages/replay-internal/src/coreHandlers/handleProcessSegmentSpan.ts @@ -10,10 +10,6 @@ export function handleProcessSegmentSpan(replay: ReplayContainer): ProcessSegmen return; } - const traceId = spanJSON.trace_id; - const segmentName = spanJSON.name; - if (traceId && segmentName) { - addSegmentDetailsToContext(replay, traceId, segmentName); - } + addSegmentDetailsToContext(replay, spanJSON.trace_id, spanJSON.name || undefined); }; } diff --git a/packages/replay-internal/src/coreHandlers/util/addSegmentDetailsToContext.ts b/packages/replay-internal/src/coreHandlers/util/addSegmentDetailsToContext.ts index cb5b1a3f2d0e..88c873983fd8 100644 --- a/packages/replay-internal/src/coreHandlers/util/addSegmentDetailsToContext.ts +++ b/packages/replay-internal/src/coreHandlers/util/addSegmentDetailsToContext.ts @@ -2,12 +2,12 @@ import type { ReplayContainer } from '../../types'; const MAX_CONTEXT_VALUES = 100; -export function addSegmentDetailsToContext(replay: ReplayContainer, traceId: string, segmentName: string): void { +export function addSegmentDetailsToContext(replay: ReplayContainer, traceId?: string, segmentName?: string): void { const replayContext = replay.getContext(); - if (replayContext.traceIds.size < MAX_CONTEXT_VALUES) { + if (traceId && replayContext.traceIds.size < MAX_CONTEXT_VALUES) { replayContext.traceIds.add(traceId); } - if (replayContext.segmentNames.size < MAX_CONTEXT_VALUES) { + if (segmentName && replayContext.segmentNames.size < MAX_CONTEXT_VALUES) { replayContext.segmentNames.add(segmentName); } } diff --git a/packages/replay-internal/test/integration/coreHandlers/handleAfterSendEvent.test.ts b/packages/replay-internal/test/integration/coreHandlers/handleAfterSendEvent.test.ts index f45441a34caf..5fa9c0abec1f 100644 --- a/packages/replay-internal/test/integration/coreHandlers/handleAfterSendEvent.test.ts +++ b/packages/replay-internal/test/integration/coreHandlers/handleAfterSendEvent.test.ts @@ -54,9 +54,10 @@ describe('Integration | coreHandlers | handleAfterSendEvent', () => { expect(Array.from(replay.getContext().errorIds)).toEqual(['err2']); expect(Array.from(replay.getContext().traceIds)).toEqual([]); + expect(Array.from(replay.getContext().segmentNames)).toEqual([]); }); - it('records traceIds from sent transaction events', async () => { + it('records traceIds and segmentNames from sent transaction events', async () => { ({ replay } = await resetSdkMock({ replayOptions: { stickySession: false, @@ -85,17 +86,59 @@ describe('Integration | coreHandlers | handleAfterSendEvent', () => { expect(Array.from(replay.getContext().errorIds)).toEqual([]); expect(Array.from(replay.getContext().traceIds)).toEqual(['tr2']); + expect(Array.from(replay.getContext().segmentNames)).toEqual(['/organizations/:orgId/replays/:replaySlug/']); // Does not affect error session await vi.advanceTimersToNextTimerAsync(); expect(Array.from(replay.getContext().errorIds)).toEqual([]); expect(Array.from(replay.getContext().traceIds)).toEqual(['tr2']); + expect(Array.from(replay.getContext().segmentNames)).toEqual(['/organizations/:orgId/replays/:replaySlug/']); expect(replay.isEnabled()).toBe(true); expect(replay.isPaused()).toBe(false); expect(replay.recordingMode).toBe('buffer'); }); + it('records traceId but not segmentName for nameless transaction events', async () => { + ({ replay } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + sentryOptions: { + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + }, + })); + + const handler = handleAfterSendEvent(replay); + + const namelessTransaction = Transaction('tr-nameless', { transaction: undefined }); + handler(namelessTransaction, { statusCode: 200 }); + + expect(Array.from(replay.getContext().traceIds)).toEqual(['tr-nameless']); + expect(Array.from(replay.getContext().segmentNames)).toEqual([]); + }); + + it('records segmentName but not traceId for transaction events with missing trace context', async () => { + ({ replay } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + sentryOptions: { + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + }, + })); + + const handler = handleAfterSendEvent(replay); + + const noTraceTransaction = Transaction(undefined, { contexts: {} }); + handler(noTraceTransaction, { statusCode: 200 }); + + expect(Array.from(replay.getContext().traceIds)).toEqual([]); + expect(Array.from(replay.getContext().segmentNames)).toEqual(['/organizations/:orgId/replays/:replaySlug/']); + }); + it('limits errorIds to max. 100', async () => { ({ replay } = await resetSdkMock({ replayOptions: { @@ -146,6 +189,8 @@ describe('Integration | coreHandlers | handleAfterSendEvent', () => { .fill(undefined) .map((_, i) => `tr-${i}`), ); + expect(replay.getContext().segmentNames.size).toBe(1); + expect(Array.from(replay.getContext().segmentNames)).toEqual(['/organizations/:orgId/replays/:replaySlug/']); }); it('flushes when in buffer mode', async () => { diff --git a/packages/replay-internal/test/integration/coreHandlers/handleProcessSegmentSpan.test.ts b/packages/replay-internal/test/integration/coreHandlers/handleProcessSegmentSpan.test.ts index 88108784c961..cc670fbf5d45 100644 --- a/packages/replay-internal/test/integration/coreHandlers/handleProcessSegmentSpan.test.ts +++ b/packages/replay-internal/test/integration/coreHandlers/handleProcessSegmentSpan.test.ts @@ -117,7 +117,7 @@ describe('Integration | coreHandlers | handleProcessSegmentSpan', () => { expect(Array.from(replay.getContext().traceIds)).toEqual([]); }); - it('does not record segment spans with empty names', async () => { + it('records traceId but not segmentName for segment spans with empty names', async () => { ({ replay } = await resetSdkMock({ replayOptions: { stickySession: false, @@ -140,7 +140,33 @@ describe('Integration | coreHandlers | handleProcessSegmentSpan', () => { status: 'ok', } as StreamedSpanJSON); - expect(Array.from(replay.getContext().traceIds)).toEqual([]); + expect(Array.from(replay.getContext().traceIds)).toEqual(['trace-stream-1']); expect(Array.from(replay.getContext().segmentNames)).toEqual([]); }); + + it('records segmentName but not traceId for segment spans with missing trace_id', async () => { + ({ replay } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + sentryOptions: { + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + }, + })); + + const client = getClient()!; + + client.emit('processSegmentSpan', { + span_id: 'span1', + name: 'GET /api/users', + is_segment: true, + start_timestamp: 0, + end_timestamp: 1, + status: 'ok', + } as StreamedSpanJSON); + + expect(Array.from(replay.getContext().traceIds)).toEqual([]); + expect(Array.from(replay.getContext().segmentNames)).toEqual(['GET /api/users']); + }); });