Skip to content

Commit cc3ecb2

Browse files
billyvgclaude
andauthored
fix(replay): Set replay_id on DSC after buffer-to-session conversion (#20686)
Follow-up to a code review comment on #20129 -- this sets a replayId on the DSC when a buffer mode replay is saved and converted to a session-based replay. Normally, we attach the replay_id to a DSC using the `createDsc` hook, but only when replay is enabled and recordingMode is `session` (and not for `buffer`). https://github.com/getsentry/sentry-javascript/blob/billy/fix-dsc-after-buffer-to-session/packages/replay-internal/src/util/addGlobalListeners.ts#L40-L46 What I'm unsure of is if this does anything since we're mutating the DSC after an unknown period of time (e.g. we're in buffer mode, and an error happens after 30 minutes). ## Slop - When `sendBufferedReplayOrFlush` converts from buffer to session mode, it calls `startRecording()` directly but never updates the cached DSC with `replay_id` - The `createDsc` hook only fires for new spans, not for an already-cached DSC on the scope (set by `browserTracingIntegration`), so `replay_id` was missing from outgoing requests until a new span happened to be created - Adds `setReplayIdOnDynamicSamplingContext()` (symmetric to the existing `resetReplayIdOnDynamicSamplingContext()`) and calls it after the buffer-to-session conversion completes 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f2aa3d commit cc3ecb2

2 files changed

Lines changed: 24 additions & 1 deletion

File tree

packages/replay-internal/src/replay.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ export class ReplayContainer implements ReplayContainerInterface {
618618
this._updateUserActivity(activityTime);
619619
this._updateSessionActivity(activityTime);
620620
this._maybeSaveSession();
621+
setReplayIdOnDynamicSamplingContext(this.session.id);
621622
}
622623

623624
this.startRecording();

packages/replay-internal/test/integration/errorSampleRate.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import '../utils/mock-internal-setTimeout';
6-
import { captureException, getClient } from '@sentry/core';
6+
import { captureException, getClient, getCurrentScope } from '@sentry/core';
77
import type { MockInstance } from 'vitest';
88
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
99
import {
@@ -383,6 +383,28 @@ describe('Integration | errorSampleRate', () => {
383383
});
384384
});
385385

386+
it('sets replay_id on DSC after converting from buffer to session mode', async () => {
387+
const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP });
388+
mockRecord._emitter(TEST_EVENT);
389+
390+
// Simulate a cached DSC on the scope (as browserTracingIntegration would set)
391+
getCurrentScope().setPropagationContext({
392+
traceId: '00000000000000000000000000000000',
393+
sampleRand: 0,
394+
dsc: { trace_id: '00000000000000000000000000000000', sampled: 'true' },
395+
});
396+
397+
expect(replay.recordingMode).toBe('buffer');
398+
const dsc = getCurrentScope().getPropagationContext().dsc!;
399+
expect(dsc.replay_id).toBeUndefined();
400+
401+
await replay.sendBufferedReplayOrFlush({ continueRecording: true });
402+
await vi.advanceTimersToNextTimerAsync();
403+
404+
expect(replay.recordingMode).toBe('session');
405+
expect(dsc.replay_id).toBe(replay.getSessionId());
406+
});
407+
386408
// This tests a regression where we were calling flush indiscriminantly in `stop()`
387409
it('does not upload a replay event if error is not sampled', async () => {
388410
// We are trying to replicate the case where error rate is 0 and session

0 commit comments

Comments
 (0)