diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts index 6db78dced270..c91fad87ff20 100644 --- a/packages/replay-internal/src/integration.ts +++ b/packages/replay-internal/src/integration.ts @@ -320,7 +320,20 @@ export class Replay implements Integration { /** Setup the integration. */ private _setup(client: Client): void { // Client is not available in constructor, so we need to wait until setupOnce - const finalOptions = loadReplayOptionsFromClient(this._initialOptions, client); + const clientOptions = client.getOptions(); + const finalOptions = loadReplayOptionsFromClient(this._initialOptions, clientOptions as BrowserClientReplayOptions); + + if (clientOptions._experiments?.enableLogs) { + client.on('beforeCaptureLog', log => { + const replayId = this.getReplayId(); + if (replayId) { + log.attributes = { + ...log.attributes, + 'replay.id': replayId, + }; + } + }); + } this._replay = new ReplayContainer({ options: finalOptions, @@ -350,9 +363,10 @@ export class Replay implements Integration { } /** Parse Replay-related options from SDK options */ -function loadReplayOptionsFromClient(initialOptions: InitialReplayPluginOptions, client: Client): ReplayPluginOptions { - const opt = client.getOptions() as BrowserClientReplayOptions; - +function loadReplayOptionsFromClient( + initialOptions: InitialReplayPluginOptions, + opt: BrowserClientReplayOptions, +): ReplayPluginOptions { const finalOptions: ReplayPluginOptions = { sessionSampleRate: 0, errorSampleRate: 0, diff --git a/packages/replay-internal/test/integration/logAttributes.test.ts b/packages/replay-internal/test/integration/logAttributes.test.ts new file mode 100644 index 000000000000..338ea9838e46 --- /dev/null +++ b/packages/replay-internal/test/integration/logAttributes.test.ts @@ -0,0 +1,164 @@ +/** + * @vitest-environment jsdom + */ + +import type { Log } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mockSdk } from '../index'; + +describe('Integration | logAttributes', () => { + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('log attributes with replay ID', () => { + it('adds replay ID to log attributes when replay is enabled and has a session', async () => { + const { replay, integration, client } = await mockSdk({ + replayOptions: {}, + sentryOptions: { + _experiments: { enableLogs: true }, + replaysSessionSampleRate: 1.0, + }, + }); + + // Start replay to create a session + replay.start(); + expect(replay.isEnabled()).toBe(true); + expect(integration.getReplayId()).toBeDefined(); + + const replayId = integration.getReplayId(); + + // Mock the client and emit a log event + const log: Log = { + level: 'info', + message: 'test log message', + attributes: { 'existing.attr': 'value' }, + }; + + client.emit('beforeCaptureLog', log); + + expect(log.attributes).toEqual({ + 'existing.attr': 'value', + 'replay.id': replayId, + }); + }); + + it('preserves existing log attributes when adding replay ID', async () => { + const { replay, integration, client } = await mockSdk({ + replayOptions: {}, + sentryOptions: { + _experiments: { enableLogs: true }, + replaysSessionSampleRate: 1.0, + }, + }); + + // Start replay to create a session + replay.start(); + const replayId = integration.getReplayId(); + + const log: Log = { + level: 'error', + message: 'error log message', + attributes: { + 'user.id': 'test-user', + 'request.id': 'req-123', + module: 'auth', + }, + }; + + client.emit('beforeCaptureLog', log); + + expect(log.attributes).toEqual({ + 'user.id': 'test-user', + 'request.id': 'req-123', + module: 'auth', + 'replay.id': replayId, + }); + }); + + it('does not add replay ID when replay is not enabled', async () => { + const { replay, client } = await mockSdk({ + replayOptions: {}, + sentryOptions: { + _experiments: { enableLogs: true }, + replaysSessionSampleRate: 0.0, // Disabled + }, + }); + + // Replay should not be enabled + expect(replay.isEnabled()).toBe(false); + + const log: Log = { + level: 'info', + message: 'test log message', + attributes: { 'existing.attr': 'value' }, + }; + + client.emit('beforeCaptureLog', log); + + // Replay ID should not be added + expect(log.attributes).toEqual({ + 'existing.attr': 'value', + }); + }); + + it('does not register log handler when enableLogs experiment is disabled', async () => { + const { replay, client } = await mockSdk({ + replayOptions: {}, + sentryOptions: { + // enableLogs experiment is not set (defaults to false) + replaysSessionSampleRate: 1.0, + }, + }); + + replay.start(); + + const log: Log = { + level: 'info', + message: 'test log message', + attributes: { 'existing.attr': 'value' }, + }; + + client.emit('beforeCaptureLog', log); + + // Replay ID should not be added since the handler wasn't registered + expect(log.attributes).toEqual({ + 'existing.attr': 'value', + }); + }); + + it('works with buffer mode replay', async () => { + const { replay, integration, client } = await mockSdk({ + replayOptions: {}, + sentryOptions: { + _experiments: { enableLogs: true }, + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, // Buffer mode + }, + }); + + // Start buffering mode + replay.startBuffering(); + expect(integration.getRecordingMode()).toBe('buffer'); + + const replayId = integration.getReplayId(); + expect(replayId).toBeDefined(); + + const log: Log = { + level: 'warn', + message: 'warning message', + attributes: {}, + }; + + client.emit('beforeCaptureLog', log); + + expect(log.attributes).toEqual({ + 'replay.id': replayId, + }); + }); + }); +}); diff --git a/packages/replay-internal/test/mocks/mockSdk.ts b/packages/replay-internal/test/mocks/mockSdk.ts index 9ec20553529b..8a1751827d14 100644 --- a/packages/replay-internal/test/mocks/mockSdk.ts +++ b/packages/replay-internal/test/mocks/mockSdk.ts @@ -47,6 +47,7 @@ class MockTransport implements Transport { export async function mockSdk({ replayOptions, sentryOptions, autoStart = true }: MockSdkParams = {}): Promise<{ replay: ReplayContainer; integration: ReplayIntegration; + client: ReturnType; }> { const { Replay } = await import('../../src/integration'); @@ -92,5 +93,5 @@ export async function mockSdk({ replayOptions, sentryOptions, autoStart = true } const replay = replayIntegration['_replay']!; - return { replay, integration: replayIntegration }; + return { replay, integration: replayIntegration, client }; }