From d65681c141546b37a81daeb8d477c0ad9ebaa43a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 24 Sep 2025 12:42:13 +0200 Subject: [PATCH 1/4] add replay_is_buffering flag --- packages/core/src/logs/internal.ts | 18 +- packages/core/test/lib/logs/internal.test.ts | 197 +++++++++++++++++++ 2 files changed, 211 insertions(+), 4 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index b439d04ec075..a70aa5cd59d0 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -151,10 +151,20 @@ export function _INTERNAL_captureLog( setLogAttribute(processedLogAttributes, 'sentry.sdk.name', name); setLogAttribute(processedLogAttributes, 'sentry.sdk.version', version); - const replay = client.getIntegrationByName string }>( - 'Replay', - ); - setLogAttribute(processedLogAttributes, 'sentry.replay_id', replay?.getReplayId(true)); + const replay = client.getIntegrationByName< + Integration & { + getReplayId: (onlyIfSampled?: boolean) => string; + getRecordingMode: () => 'session' | 'buffer' | undefined; + } + >('Replay'); + + const replayId = replay?.getReplayId(true); + setLogAttribute(processedLogAttributes, 'sentry.replay_id', replayId); + + if (replayId && replay?.getRecordingMode() === 'buffer') { + // We send this so we can identify cases where the replayId is attached but the replay itself might not have been sent to Sentry + setLogAttribute(processedLogAttributes, 'sentry.internal.replay_is_buffering', true); + } const beforeLogMessage = beforeLog.message; if (isParameterizedString(beforeLogMessage)) { diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index b2d5569ecfa7..e0d06f6e6a3f 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -424,6 +424,7 @@ describe('_INTERNAL_captureLog', () => { // Simulate behavior: return ID for sampled sessions return onlyIfSampled ? 'sampled-replay-id' : 'any-replay-id'; }), + getRecordingMode: vi.fn(() => 'session'), }; vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any); @@ -480,6 +481,7 @@ describe('_INTERNAL_captureLog', () => { // Buffer mode should still return ID even with onlyIfSampled=true return 'buffer-replay-id'; }), + getRecordingMode: vi.fn(() => 'buffer'), }; vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any); @@ -494,6 +496,10 @@ describe('_INTERNAL_captureLog', () => { value: 'buffer-replay-id', type: 'string', }, + 'sentry.internal.replay_is_buffering': { + value: true, + type: 'boolean', + }, }); }); @@ -527,6 +533,7 @@ describe('_INTERNAL_captureLog', () => { // Mock replay integration const mockReplayIntegration = { getReplayId: vi.fn(() => 'test-replay-id'), + getRecordingMode: vi.fn(() => 'session'), }; vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any); @@ -590,6 +597,196 @@ describe('_INTERNAL_captureLog', () => { expect(logAttributes).not.toHaveProperty('sentry.replay_id'); }); }); + + it('sets replay_is_buffering attribute when replay is in buffer mode', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + // Mock replay integration with buffer mode + const mockReplayIntegration = { + getReplayId: vi.fn(() => 'buffer-replay-id'), + getRecordingMode: vi.fn(() => 'buffer'), + }; + + vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any); + + _INTERNAL_captureLog({ level: 'info', message: 'test log with buffered replay' }, scope); + + expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true); + expect(mockReplayIntegration.getRecordingMode).toHaveBeenCalled(); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual({ + 'sentry.replay_id': { + value: 'buffer-replay-id', + type: 'string', + }, + 'sentry.internal.replay_is_buffering': { + value: true, + type: 'boolean', + }, + }); + }); + + it('does not set replay_is_buffering attribute when replay is in session mode', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + // Mock replay integration with session mode + const mockReplayIntegration = { + getReplayId: vi.fn(() => 'session-replay-id'), + getRecordingMode: vi.fn(() => 'session'), + }; + + vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any); + + _INTERNAL_captureLog({ level: 'info', message: 'test log with session replay' }, scope); + + expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true); + expect(mockReplayIntegration.getRecordingMode).toHaveBeenCalled(); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual({ + 'sentry.replay_id': { + value: 'session-replay-id', + type: 'string', + }, + }); + expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering'); + }); + + it('does not set replay_is_buffering attribute when replay is undefined mode', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + // Mock replay integration with undefined mode (replay stopped/disabled) + const mockReplayIntegration = { + getReplayId: vi.fn(() => 'stopped-replay-id'), + getRecordingMode: vi.fn(() => undefined), + }; + + vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any); + + _INTERNAL_captureLog({ level: 'info', message: 'test log with stopped replay' }, scope); + + expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true); + expect(mockReplayIntegration.getRecordingMode).toHaveBeenCalled(); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual({ + 'sentry.replay_id': { + value: 'stopped-replay-id', + type: 'string', + }, + }); + expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering'); + }); + + it('does not set replay_is_buffering attribute when no replay ID is available', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + // Mock replay integration that returns no replay ID but has buffer mode + const mockReplayIntegration = { + getReplayId: vi.fn(() => undefined), + getRecordingMode: vi.fn(() => 'buffer'), + }; + + vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any); + + _INTERNAL_captureLog({ level: 'info', message: 'test log with buffer mode but no replay ID' }, scope); + + expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true); + // getRecordingMode should not be called if there's no replay ID + expect(mockReplayIntegration.getRecordingMode).not.toHaveBeenCalled(); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual({}); + expect(logAttributes).not.toHaveProperty('sentry.replay_id'); + expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering'); + }); + + it('does not set replay_is_buffering attribute when replay integration is missing', () => { + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + // Mock no replay integration found + vi.spyOn(client, 'getIntegrationByName').mockReturnValue(undefined); + + _INTERNAL_captureLog({ level: 'info', message: 'test log without replay integration' }, scope); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual({}); + expect(logAttributes).not.toHaveProperty('sentry.replay_id'); + expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering'); + }); + + it('combines replay_is_buffering with other replay attributes', () => { + const options = getDefaultTestClientOptions({ + dsn: PUBLIC_DSN, + enableLogs: true, + release: '1.0.0', + environment: 'test', + }); + const client = new TestClient(options); + const scope = new Scope(); + scope.setClient(client); + + // Mock replay integration with buffer mode + const mockReplayIntegration = { + getReplayId: vi.fn(() => 'buffer-replay-id'), + getRecordingMode: vi.fn(() => 'buffer'), + }; + + vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any); + + _INTERNAL_captureLog( + { + level: 'info', + message: 'test log with buffer replay and other attributes', + attributes: { component: 'auth', action: 'login' }, + }, + scope, + ); + + const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; + expect(logAttributes).toEqual({ + component: { + value: 'auth', + type: 'string', + }, + action: { + value: 'login', + type: 'string', + }, + 'sentry.release': { + value: '1.0.0', + type: 'string', + }, + 'sentry.environment': { + value: 'test', + type: 'string', + }, + 'sentry.replay_id': { + value: 'buffer-replay-id', + type: 'string', + }, + 'sentry.internal.replay_is_buffering': { + value: true, + type: 'boolean', + }, + }); + }); }); describe('user functionality', () => { From 7112b90a3ec16b6a8b640e59a6249ddfdc08bf39 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 24 Sep 2025 15:29:37 +0200 Subject: [PATCH 2/4] update internal attribute --- packages/core/src/logs/internal.ts | 2 +- packages/core/test/lib/logs/internal.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/src/logs/internal.ts b/packages/core/src/logs/internal.ts index a70aa5cd59d0..b3bda05d97f7 100644 --- a/packages/core/src/logs/internal.ts +++ b/packages/core/src/logs/internal.ts @@ -163,7 +163,7 @@ export function _INTERNAL_captureLog( if (replayId && replay?.getRecordingMode() === 'buffer') { // We send this so we can identify cases where the replayId is attached but the replay itself might not have been sent to Sentry - setLogAttribute(processedLogAttributes, 'sentry.internal.replay_is_buffering', true); + setLogAttribute(processedLogAttributes, 'sentry._internal.replay_is_buffering', true); } const beforeLogMessage = beforeLog.message; diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index e0d06f6e6a3f..6629954d0688 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -496,7 +496,7 @@ describe('_INTERNAL_captureLog', () => { value: 'buffer-replay-id', type: 'string', }, - 'sentry.internal.replay_is_buffering': { + 'sentry._internal.replay_is_buffering': { value: true, type: 'boolean', }, @@ -623,7 +623,7 @@ describe('_INTERNAL_captureLog', () => { value: 'buffer-replay-id', type: 'string', }, - 'sentry.internal.replay_is_buffering': { + 'sentry._internal.replay_is_buffering': { value: true, type: 'boolean', }, @@ -656,7 +656,7 @@ describe('_INTERNAL_captureLog', () => { type: 'string', }, }); - expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering'); + expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering'); }); it('does not set replay_is_buffering attribute when replay is undefined mode', () => { @@ -685,7 +685,7 @@ describe('_INTERNAL_captureLog', () => { type: 'string', }, }); - expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering'); + expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering'); }); it('does not set replay_is_buffering attribute when no replay ID is available', () => { From ac2751a667b9d04888dc2b9384fb022a62df4492 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 25 Sep 2025 15:15:02 +0200 Subject: [PATCH 3/4] fix test --- packages/core/test/lib/logs/internal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index 6629954d0688..e7a49b6e8106 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -781,7 +781,7 @@ describe('_INTERNAL_captureLog', () => { value: 'buffer-replay-id', type: 'string', }, - 'sentry.internal.replay_is_buffering': { + 'sentry._internal.replay_is_buffering': { value: true, type: 'boolean', }, From 2238e523a589a58f1c2e218a39a6d799f00709c8 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 25 Sep 2025 17:55:25 +0200 Subject: [PATCH 4/4] Update packages/core/test/lib/logs/internal.test.ts Co-authored-by: Billy Vong --- packages/core/test/lib/logs/internal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/lib/logs/internal.test.ts b/packages/core/test/lib/logs/internal.test.ts index e7a49b6e8106..dbb2966dc076 100644 --- a/packages/core/test/lib/logs/internal.test.ts +++ b/packages/core/test/lib/logs/internal.test.ts @@ -728,7 +728,7 @@ describe('_INTERNAL_captureLog', () => { const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes; expect(logAttributes).toEqual({}); expect(logAttributes).not.toHaveProperty('sentry.replay_id'); - expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering'); + expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering'); }); it('combines replay_is_buffering with other replay attributes', () => {