Skip to content

Commit 80d5ff2

Browse files
authored
feat(logs): Add internal replay_is_buffering flag (#17752)
1 parent 8424fdc commit 80d5ff2

File tree

2 files changed

+211
-4
lines changed

2 files changed

+211
-4
lines changed

packages/core/src/logs/internal.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,20 @@ export function _INTERNAL_captureLog(
151151
setLogAttribute(processedLogAttributes, 'sentry.sdk.name', name);
152152
setLogAttribute(processedLogAttributes, 'sentry.sdk.version', version);
153153

154-
const replay = client.getIntegrationByName<Integration & { getReplayId: (onlyIfSampled?: boolean) => string }>(
155-
'Replay',
156-
);
157-
setLogAttribute(processedLogAttributes, 'sentry.replay_id', replay?.getReplayId(true));
154+
const replay = client.getIntegrationByName<
155+
Integration & {
156+
getReplayId: (onlyIfSampled?: boolean) => string;
157+
getRecordingMode: () => 'session' | 'buffer' | undefined;
158+
}
159+
>('Replay');
160+
161+
const replayId = replay?.getReplayId(true);
162+
setLogAttribute(processedLogAttributes, 'sentry.replay_id', replayId);
163+
164+
if (replayId && replay?.getRecordingMode() === 'buffer') {
165+
// We send this so we can identify cases where the replayId is attached but the replay itself might not have been sent to Sentry
166+
setLogAttribute(processedLogAttributes, 'sentry._internal.replay_is_buffering', true);
167+
}
158168

159169
const beforeLogMessage = beforeLog.message;
160170
if (isParameterizedString(beforeLogMessage)) {

packages/core/test/lib/logs/internal.test.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ describe('_INTERNAL_captureLog', () => {
424424
// Simulate behavior: return ID for sampled sessions
425425
return onlyIfSampled ? 'sampled-replay-id' : 'any-replay-id';
426426
}),
427+
getRecordingMode: vi.fn(() => 'session'),
427428
};
428429

429430
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any);
@@ -480,6 +481,7 @@ describe('_INTERNAL_captureLog', () => {
480481
// Buffer mode should still return ID even with onlyIfSampled=true
481482
return 'buffer-replay-id';
482483
}),
484+
getRecordingMode: vi.fn(() => 'buffer'),
483485
};
484486

485487
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any);
@@ -494,6 +496,10 @@ describe('_INTERNAL_captureLog', () => {
494496
value: 'buffer-replay-id',
495497
type: 'string',
496498
},
499+
'sentry._internal.replay_is_buffering': {
500+
value: true,
501+
type: 'boolean',
502+
},
497503
});
498504
});
499505

@@ -527,6 +533,7 @@ describe('_INTERNAL_captureLog', () => {
527533
// Mock replay integration
528534
const mockReplayIntegration = {
529535
getReplayId: vi.fn(() => 'test-replay-id'),
536+
getRecordingMode: vi.fn(() => 'session'),
530537
};
531538

532539
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any);
@@ -590,6 +597,196 @@ describe('_INTERNAL_captureLog', () => {
590597
expect(logAttributes).not.toHaveProperty('sentry.replay_id');
591598
});
592599
});
600+
601+
it('sets replay_is_buffering attribute when replay is in buffer mode', () => {
602+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true });
603+
const client = new TestClient(options);
604+
const scope = new Scope();
605+
scope.setClient(client);
606+
607+
// Mock replay integration with buffer mode
608+
const mockReplayIntegration = {
609+
getReplayId: vi.fn(() => 'buffer-replay-id'),
610+
getRecordingMode: vi.fn(() => 'buffer'),
611+
};
612+
613+
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any);
614+
615+
_INTERNAL_captureLog({ level: 'info', message: 'test log with buffered replay' }, scope);
616+
617+
expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true);
618+
expect(mockReplayIntegration.getRecordingMode).toHaveBeenCalled();
619+
620+
const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes;
621+
expect(logAttributes).toEqual({
622+
'sentry.replay_id': {
623+
value: 'buffer-replay-id',
624+
type: 'string',
625+
},
626+
'sentry._internal.replay_is_buffering': {
627+
value: true,
628+
type: 'boolean',
629+
},
630+
});
631+
});
632+
633+
it('does not set replay_is_buffering attribute when replay is in session mode', () => {
634+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true });
635+
const client = new TestClient(options);
636+
const scope = new Scope();
637+
scope.setClient(client);
638+
639+
// Mock replay integration with session mode
640+
const mockReplayIntegration = {
641+
getReplayId: vi.fn(() => 'session-replay-id'),
642+
getRecordingMode: vi.fn(() => 'session'),
643+
};
644+
645+
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any);
646+
647+
_INTERNAL_captureLog({ level: 'info', message: 'test log with session replay' }, scope);
648+
649+
expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true);
650+
expect(mockReplayIntegration.getRecordingMode).toHaveBeenCalled();
651+
652+
const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes;
653+
expect(logAttributes).toEqual({
654+
'sentry.replay_id': {
655+
value: 'session-replay-id',
656+
type: 'string',
657+
},
658+
});
659+
expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering');
660+
});
661+
662+
it('does not set replay_is_buffering attribute when replay is undefined mode', () => {
663+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true });
664+
const client = new TestClient(options);
665+
const scope = new Scope();
666+
scope.setClient(client);
667+
668+
// Mock replay integration with undefined mode (replay stopped/disabled)
669+
const mockReplayIntegration = {
670+
getReplayId: vi.fn(() => 'stopped-replay-id'),
671+
getRecordingMode: vi.fn(() => undefined),
672+
};
673+
674+
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any);
675+
676+
_INTERNAL_captureLog({ level: 'info', message: 'test log with stopped replay' }, scope);
677+
678+
expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true);
679+
expect(mockReplayIntegration.getRecordingMode).toHaveBeenCalled();
680+
681+
const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes;
682+
expect(logAttributes).toEqual({
683+
'sentry.replay_id': {
684+
value: 'stopped-replay-id',
685+
type: 'string',
686+
},
687+
});
688+
expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering');
689+
});
690+
691+
it('does not set replay_is_buffering attribute when no replay ID is available', () => {
692+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true });
693+
const client = new TestClient(options);
694+
const scope = new Scope();
695+
scope.setClient(client);
696+
697+
// Mock replay integration that returns no replay ID but has buffer mode
698+
const mockReplayIntegration = {
699+
getReplayId: vi.fn(() => undefined),
700+
getRecordingMode: vi.fn(() => 'buffer'),
701+
};
702+
703+
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any);
704+
705+
_INTERNAL_captureLog({ level: 'info', message: 'test log with buffer mode but no replay ID' }, scope);
706+
707+
expect(mockReplayIntegration.getReplayId).toHaveBeenCalledWith(true);
708+
// getRecordingMode should not be called if there's no replay ID
709+
expect(mockReplayIntegration.getRecordingMode).not.toHaveBeenCalled();
710+
711+
const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes;
712+
expect(logAttributes).toEqual({});
713+
expect(logAttributes).not.toHaveProperty('sentry.replay_id');
714+
expect(logAttributes).not.toHaveProperty('sentry.internal.replay_is_buffering');
715+
});
716+
717+
it('does not set replay_is_buffering attribute when replay integration is missing', () => {
718+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableLogs: true });
719+
const client = new TestClient(options);
720+
const scope = new Scope();
721+
scope.setClient(client);
722+
723+
// Mock no replay integration found
724+
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(undefined);
725+
726+
_INTERNAL_captureLog({ level: 'info', message: 'test log without replay integration' }, scope);
727+
728+
const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes;
729+
expect(logAttributes).toEqual({});
730+
expect(logAttributes).not.toHaveProperty('sentry.replay_id');
731+
expect(logAttributes).not.toHaveProperty('sentry._internal.replay_is_buffering');
732+
});
733+
734+
it('combines replay_is_buffering with other replay attributes', () => {
735+
const options = getDefaultTestClientOptions({
736+
dsn: PUBLIC_DSN,
737+
enableLogs: true,
738+
release: '1.0.0',
739+
environment: 'test',
740+
});
741+
const client = new TestClient(options);
742+
const scope = new Scope();
743+
scope.setClient(client);
744+
745+
// Mock replay integration with buffer mode
746+
const mockReplayIntegration = {
747+
getReplayId: vi.fn(() => 'buffer-replay-id'),
748+
getRecordingMode: vi.fn(() => 'buffer'),
749+
};
750+
751+
vi.spyOn(client, 'getIntegrationByName').mockReturnValue(mockReplayIntegration as any);
752+
753+
_INTERNAL_captureLog(
754+
{
755+
level: 'info',
756+
message: 'test log with buffer replay and other attributes',
757+
attributes: { component: 'auth', action: 'login' },
758+
},
759+
scope,
760+
);
761+
762+
const logAttributes = _INTERNAL_getLogBuffer(client)?.[0]?.attributes;
763+
expect(logAttributes).toEqual({
764+
component: {
765+
value: 'auth',
766+
type: 'string',
767+
},
768+
action: {
769+
value: 'login',
770+
type: 'string',
771+
},
772+
'sentry.release': {
773+
value: '1.0.0',
774+
type: 'string',
775+
},
776+
'sentry.environment': {
777+
value: 'test',
778+
type: 'string',
779+
},
780+
'sentry.replay_id': {
781+
value: 'buffer-replay-id',
782+
type: 'string',
783+
},
784+
'sentry._internal.replay_is_buffering': {
785+
value: true,
786+
type: 'boolean',
787+
},
788+
});
789+
});
593790
});
594791

595792
describe('user functionality', () => {

0 commit comments

Comments
 (0)