Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c36e432

Browse files
AbhiPrasadonurtemizkan
authored andcommittedApr 3, 2025
ref(browser/core): Move all log flushing logic into clients (#15831)
I experimented with a bunch of different approaches to register log flushing implementations, but I found the cleanest and most performant solution was to have log flushing logic live in clients. This refactors the browser client to do that, and adds tests for all of our flushing logic everywhere. The bundle size increase is not ideal, but I recognize we have no choice. After this gets merged in, I'll add the console logging integration to core. I'll also add some quick integrations for pino and winston.
1 parent 5f70e70 commit c36e432

File tree

9 files changed

+314
-201
lines changed

9 files changed

+314
-201
lines changed
 

‎packages/browser/src/client.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ import {
1515
addAutoIpAddressToUser,
1616
applySdkMetadata,
1717
getSDKSource,
18+
_INTERNAL_flushLogsBuffer,
1819
} from '@sentry/core';
1920
import { eventFromException, eventFromMessage } from './eventbuilder';
2021
import { WINDOW } from './helpers';
2122
import type { BrowserTransportOptions } from './transports/types';
2223

24+
const DEFAULT_FLUSH_INTERVAL = 5000;
25+
2326
/**
2427
* Configuration options for the Sentry Browser SDK.
2528
* @see @sentry/core Options for more information.
@@ -65,6 +68,7 @@ export type BrowserClientOptions = ClientOptions<BrowserTransportOptions> &
6568
* @see SentryClient for usage documentation.
6669
*/
6770
export class BrowserClient extends Client<BrowserClientOptions> {
71+
private _logFlushIdleTimeout: ReturnType<typeof setTimeout> | undefined;
6872
/**
6973
* Creates a new Browser SDK instance.
7074
*
@@ -81,17 +85,41 @@ export class BrowserClient extends Client<BrowserClientOptions> {
8185

8286
super(opts);
8387

88+
// eslint-disable-next-line @typescript-eslint/no-this-alias
89+
const client = this;
90+
const { sendDefaultPii, _experiments } = client._options;
91+
const enableLogs = _experiments?.enableLogs;
92+
8493
if (opts.sendClientReports && WINDOW.document) {
8594
WINDOW.document.addEventListener('visibilitychange', () => {
8695
if (WINDOW.document.visibilityState === 'hidden') {
8796
this._flushOutcomes();
97+
if (enableLogs) {
98+
_INTERNAL_flushLogsBuffer(client);
99+
}
88100
}
89101
});
90102
}
91103

92-
if (this._options.sendDefaultPii) {
93-
this.on('postprocessEvent', addAutoIpAddressToUser);
94-
this.on('beforeSendSession', addAutoIpAddressToSession);
104+
if (enableLogs) {
105+
client.on('flush', () => {
106+
_INTERNAL_flushLogsBuffer(client);
107+
});
108+
109+
client.on('afterCaptureLog', () => {
110+
if (client._logFlushIdleTimeout) {
111+
clearTimeout(client._logFlushIdleTimeout);
112+
}
113+
114+
client._logFlushIdleTimeout = setTimeout(() => {
115+
_INTERNAL_flushLogsBuffer(client);
116+
}, DEFAULT_FLUSH_INTERVAL);
117+
});
118+
}
119+
120+
if (sendDefaultPii) {
121+
client.on('postprocessEvent', addAutoIpAddressToUser);
122+
client.on('beforeSendSession', addAutoIpAddressToSession);
95123
}
96124
}
97125

‎packages/browser/src/log.ts

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,5 @@
1-
import type { LogSeverityLevel, Log, Client, ParameterizedString } from '@sentry/core';
2-
import { getClient, _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from '@sentry/core';
3-
4-
import { WINDOW } from './helpers';
5-
6-
/**
7-
* TODO: Make this configurable
8-
*/
9-
const DEFAULT_FLUSH_INTERVAL = 5000;
10-
11-
let timeout: ReturnType<typeof setTimeout> | undefined;
12-
13-
/**
14-
* This is a global timeout that is used to flush the logs buffer.
15-
* It is used to ensure that logs are flushed even if the client is not flushed.
16-
*/
17-
function startFlushTimeout(client: Client): void {
18-
if (timeout) {
19-
clearTimeout(timeout);
20-
}
21-
22-
timeout = setTimeout(() => {
23-
_INTERNAL_flushLogsBuffer(client);
24-
}, DEFAULT_FLUSH_INTERVAL);
25-
}
26-
27-
let isClientListenerAdded = false;
28-
/**
29-
* This is a function that is used to add a flush listener to the client.
30-
* It is used to ensure that the logger buffer is flushed when the client is flushed.
31-
*/
32-
function addFlushingListeners(client: Client): void {
33-
if (isClientListenerAdded || !client.getOptions()._experiments?.enableLogs) {
34-
return;
35-
}
36-
37-
isClientListenerAdded = true;
38-
39-
if (WINDOW.document) {
40-
WINDOW.document.addEventListener('visibilitychange', () => {
41-
if (WINDOW.document.visibilityState === 'hidden') {
42-
_INTERNAL_flushLogsBuffer(client);
43-
}
44-
});
45-
}
46-
47-
client.on('flush', () => {
48-
_INTERNAL_flushLogsBuffer(client);
49-
});
50-
}
1+
import type { LogSeverityLevel, Log, ParameterizedString } from '@sentry/core';
2+
import { _INTERNAL_captureLog } from '@sentry/core';
513

524
/**
535
* Capture a log with the given level.
@@ -63,14 +15,7 @@ function captureLog(
6315
attributes?: Log['attributes'],
6416
severityNumber?: Log['severityNumber'],
6517
): void {
66-
const client = getClient();
67-
if (client) {
68-
addFlushingListeners(client);
69-
70-
startFlushTimeout(client);
71-
}
72-
73-
_INTERNAL_captureLog({ level, message, attributes, severityNumber }, client, undefined);
18+
_INTERNAL_captureLog({ level, message, attributes, severityNumber });
7419
}
7520

7621
/**

‎packages/browser/test/client.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
5+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6+
import * as sentryCore from '@sentry/core';
7+
import { BrowserClient } from '../src/client';
8+
import { WINDOW } from '../src/helpers';
9+
import { getDefaultBrowserClientOptions } from './helper/browser-client-options';
10+
11+
vi.mock('@sentry/core', async requireActual => {
12+
return {
13+
...((await requireActual()) as any),
14+
_INTERNAL_flushLogsBuffer: vi.fn(),
15+
};
16+
});
17+
18+
describe('BrowserClient', () => {
19+
let client: BrowserClient;
20+
const DEFAULT_FLUSH_INTERVAL = 5000;
21+
22+
afterEach(() => {
23+
vi.useRealTimers();
24+
vi.clearAllMocks();
25+
});
26+
27+
it('does not flush logs when logs are disabled', () => {
28+
client = new BrowserClient(
29+
getDefaultBrowserClientOptions({
30+
_experiments: { enableLogs: false },
31+
sendClientReports: true,
32+
}),
33+
);
34+
35+
// Add some logs
36+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, client);
37+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, client);
38+
39+
// Simulate visibility change to hidden
40+
if (WINDOW.document) {
41+
Object.defineProperty(WINDOW.document, 'visibilityState', { value: 'hidden' });
42+
WINDOW.document.dispatchEvent(new Event('visibilitychange'));
43+
}
44+
45+
expect(sentryCore._INTERNAL_flushLogsBuffer).not.toHaveBeenCalled();
46+
});
47+
48+
describe('log flushing', () => {
49+
beforeEach(() => {
50+
vi.useFakeTimers();
51+
client = new BrowserClient(
52+
getDefaultBrowserClientOptions({
53+
_experiments: { enableLogs: true },
54+
sendClientReports: true,
55+
}),
56+
);
57+
});
58+
59+
it('flushes logs when page visibility changes to hidden', () => {
60+
const flushOutcomesSpy = vi.spyOn(client as any, '_flushOutcomes');
61+
62+
// Add some logs
63+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, client);
64+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, client);
65+
66+
// Simulate visibility change to hidden
67+
if (WINDOW.document) {
68+
Object.defineProperty(WINDOW.document, 'visibilityState', { value: 'hidden' });
69+
WINDOW.document.dispatchEvent(new Event('visibilitychange'));
70+
}
71+
72+
expect(flushOutcomesSpy).toHaveBeenCalled();
73+
expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client);
74+
});
75+
76+
it('flushes logs on flush event', () => {
77+
// Add some logs
78+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, client);
79+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, client);
80+
81+
// Trigger flush event
82+
client.emit('flush');
83+
84+
expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client);
85+
});
86+
87+
it('flushes logs after idle timeout', () => {
88+
// Add a log which will trigger afterCaptureLog event
89+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log' }, client);
90+
91+
// Fast forward the idle timeout
92+
vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL);
93+
94+
expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client);
95+
});
96+
97+
it('resets idle timeout when new logs are captured', () => {
98+
// Add initial log
99+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 1' }, client);
100+
101+
// Fast forward part of the idle timeout
102+
vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL / 2);
103+
104+
// Add another log which should reset the timeout
105+
sentryCore._INTERNAL_captureLog({ level: 'info', message: 'test log 2' }, client);
106+
107+
// Fast forward the remaining time
108+
vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL / 2);
109+
110+
// Should not have flushed yet since timeout was reset
111+
expect(sentryCore._INTERNAL_flushLogsBuffer).not.toHaveBeenCalled();
112+
113+
// Fast forward the full timeout
114+
vi.advanceTimersByTime(DEFAULT_FLUSH_INTERVAL);
115+
116+
// Now should have flushed both logs
117+
expect(sentryCore._INTERNAL_flushLogsBuffer).toHaveBeenCalledWith(client);
118+
});
119+
});
120+
});

‎packages/browser/test/log.test.ts

Lines changed: 56 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -68,151 +68,87 @@ describe('Logger', () => {
6868

6969
it('should call _INTERNAL_captureLog with trace level', () => {
7070
logger.trace('Test trace message', { key: 'value' });
71-
expect(mockCaptureLog).toHaveBeenCalledWith(
72-
{
73-
level: 'trace',
74-
message: 'Test trace message',
75-
attributes: { key: 'value' },
76-
severityNumber: undefined,
77-
},
78-
expect.any(Object),
79-
undefined,
80-
);
71+
expect(mockCaptureLog).toHaveBeenCalledWith({
72+
level: 'trace',
73+
message: 'Test trace message',
74+
attributes: { key: 'value' },
75+
severityNumber: undefined,
76+
});
8177
});
8278

8379
it('should call _INTERNAL_captureLog with debug level', () => {
8480
logger.debug('Test debug message', { key: 'value' });
85-
expect(mockCaptureLog).toHaveBeenCalledWith(
86-
{
87-
level: 'debug',
88-
message: 'Test debug message',
89-
attributes: { key: 'value' },
90-
severityNumber: undefined,
91-
},
92-
expect.any(Object),
93-
undefined,
94-
);
81+
expect(mockCaptureLog).toHaveBeenCalledWith({
82+
level: 'debug',
83+
message: 'Test debug message',
84+
attributes: { key: 'value' },
85+
severityNumber: undefined,
86+
});
9587
});
9688

9789
it('should call _INTERNAL_captureLog with info level', () => {
9890
logger.info('Test info message', { key: 'value' });
99-
expect(mockCaptureLog).toHaveBeenCalledWith(
100-
{
101-
level: 'info',
102-
message: 'Test info message',
103-
attributes: { key: 'value' },
104-
severityNumber: undefined,
105-
},
106-
expect.any(Object),
107-
undefined,
108-
);
91+
expect(mockCaptureLog).toHaveBeenCalledWith({
92+
level: 'info',
93+
message: 'Test info message',
94+
attributes: { key: 'value' },
95+
severityNumber: undefined,
96+
});
10997
});
11098

11199
it('should call _INTERNAL_captureLog with warn level', () => {
112100
logger.warn('Test warn message', { key: 'value' });
113-
expect(mockCaptureLog).toHaveBeenCalledWith(
114-
{
115-
level: 'warn',
116-
message: 'Test warn message',
117-
attributes: { key: 'value' },
118-
severityNumber: undefined,
119-
},
120-
expect.any(Object),
121-
undefined,
122-
);
101+
expect(mockCaptureLog).toHaveBeenCalledWith({
102+
level: 'warn',
103+
message: 'Test warn message',
104+
attributes: { key: 'value' },
105+
severityNumber: undefined,
106+
});
123107
});
124108

125109
it('should call _INTERNAL_captureLog with error level', () => {
126110
logger.error('Test error message', { key: 'value' });
127-
expect(mockCaptureLog).toHaveBeenCalledWith(
128-
{
129-
level: 'error',
130-
message: 'Test error message',
131-
attributes: { key: 'value' },
132-
severityNumber: undefined,
133-
},
134-
expect.any(Object),
135-
undefined,
136-
);
111+
expect(mockCaptureLog).toHaveBeenCalledWith({
112+
level: 'error',
113+
message: 'Test error message',
114+
attributes: { key: 'value' },
115+
severityNumber: undefined,
116+
});
137117
});
138118

139119
it('should call _INTERNAL_captureLog with fatal level', () => {
140120
logger.fatal('Test fatal message', { key: 'value' });
141-
expect(mockCaptureLog).toHaveBeenCalledWith(
142-
{
143-
level: 'fatal',
144-
message: 'Test fatal message',
145-
attributes: { key: 'value' },
146-
severityNumber: undefined,
147-
},
148-
expect.any(Object),
149-
undefined,
150-
);
121+
expect(mockCaptureLog).toHaveBeenCalledWith({
122+
level: 'fatal',
123+
message: 'Test fatal message',
124+
attributes: { key: 'value' },
125+
severityNumber: undefined,
126+
});
151127
});
152128
});
153129

154-
describe('Automatic flushing', () => {
155-
it('should flush logs after timeout', () => {
156-
logger.info('Test message');
157-
expect(mockFlushLogsBuffer).not.toHaveBeenCalled();
158-
159-
// Fast-forward time by 5000ms (the default flush interval)
160-
vi.advanceTimersByTime(5000);
161-
162-
expect(mockFlushLogsBuffer).toHaveBeenCalledTimes(1);
163-
expect(mockFlushLogsBuffer).toHaveBeenCalledWith(expect.any(Object));
164-
});
165-
166-
it('should restart the flush timeout when a new log is captured', () => {
167-
logger.info('First message');
168-
169-
// Advance time by 3000ms (not enough to trigger flush)
170-
vi.advanceTimersByTime(3000);
171-
expect(mockFlushLogsBuffer).not.toHaveBeenCalled();
172-
173-
// Log another message, which should reset the timer
174-
logger.info('Second message');
175-
176-
// Advance time by 3000ms again (should be 6000ms total, but timer was reset)
177-
vi.advanceTimersByTime(3000);
178-
expect(mockFlushLogsBuffer).not.toHaveBeenCalled();
179-
180-
// Advance time to complete the 5000ms after the second message
181-
vi.advanceTimersByTime(2000);
182-
expect(mockFlushLogsBuffer).toHaveBeenCalledTimes(1);
183-
});
184-
185-
it('should handle parameterized strings with parameters', () => {
186-
logger.info(logger.fmt`Hello ${'John'}, your balance is ${100}`, { userId: 123 });
187-
expect(mockCaptureLog).toHaveBeenCalledWith(
188-
{
189-
level: 'info',
190-
message: expect.objectContaining({
191-
__sentry_template_string__: 'Hello %s, your balance is %s',
192-
__sentry_template_values__: ['John', 100],
193-
}),
194-
attributes: {
195-
userId: 123,
196-
},
197-
},
198-
expect.any(Object),
199-
undefined,
200-
);
130+
it('should handle parameterized strings with parameters', () => {
131+
logger.info(logger.fmt`Hello ${'John'}, your balance is ${100}`, { userId: 123 });
132+
expect(mockCaptureLog).toHaveBeenCalledWith({
133+
level: 'info',
134+
message: expect.objectContaining({
135+
__sentry_template_string__: 'Hello %s, your balance is %s',
136+
__sentry_template_values__: ['John', 100],
137+
}),
138+
attributes: {
139+
userId: 123,
140+
},
201141
});
142+
});
202143

203-
it('should handle parameterized strings without additional attributes', () => {
204-
logger.debug(logger.fmt`User ${'Alice'} logged in from ${'mobile'}`);
205-
expect(mockCaptureLog).toHaveBeenCalledWith(
206-
{
207-
level: 'debug',
208-
message: expect.objectContaining({
209-
__sentry_template_string__: 'User %s logged in from %s',
210-
__sentry_template_values__: ['Alice', 'mobile'],
211-
}),
212-
},
213-
expect.any(Object),
214-
undefined,
215-
);
144+
it('should handle parameterized strings without additional attributes', () => {
145+
logger.debug(logger.fmt`User ${'Alice'} logged in from ${'mobile'}`);
146+
expect(mockCaptureLog).toHaveBeenCalledWith({
147+
level: 'debug',
148+
message: expect.objectContaining({
149+
__sentry_template_string__: 'User %s logged in from %s',
150+
__sentry_template_values__: ['Alice', 'mobile'],
151+
}),
216152
});
217153
});
218154
});

‎packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export { instrumentFetchRequest } from './fetch';
113113
export { trpcMiddleware } from './trpc';
114114
export { captureFeedback } from './feedback';
115115
export type { ReportDialogOptions } from './report-dialog';
116-
export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from './logs';
116+
export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from './logs/exports';
117117

118118
// TODO: Make this structure pretty again and don't do "export *"
119119
export * from './utils-hoist/index';

‎packages/core/src/logs/index.ts renamed to ‎packages/core/src/logs/exports.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function logAttributeToSerializedLogAttribute(key: string, value: unknown
4141
let stringValue = '';
4242
try {
4343
stringValue = JSON.stringify(value) ?? '';
44-
} catch (_) {
44+
} catch {
4545
// Do nothing
4646
}
4747
return {
@@ -62,7 +62,11 @@ export function logAttributeToSerializedLogAttribute(key: string, value: unknown
6262
* @experimental This method will experience breaking changes. This is not yet part of
6363
* the stable Sentry SDK API and can be changed or removed without warning.
6464
*/
65-
export function _INTERNAL_captureLog(beforeLog: Log, client = getClient(), scope = getCurrentScope()): void {
65+
export function _INTERNAL_captureLog(
66+
beforeLog: Log,
67+
client: Client | undefined = getClient(),
68+
scope = getCurrentScope(),
69+
): void {
6670
if (!client) {
6771
DEBUG_BUILD && logger.warn('No client available to capture log.');
6872
return;
@@ -143,6 +147,9 @@ export function _INTERNAL_captureLog(beforeLog: Log, client = getClient(), scope
143147
*
144148
* @param client - A client.
145149
* @param maybeLogBuffer - A log buffer. Uses the log buffer for the given client if not provided.
150+
*
151+
* @experimental This method will experience breaking changes. This is not yet part of
152+
* the stable Sentry SDK API and can be changed or removed without warning.
146153
*/
147154
export function _INTERNAL_flushLogsBuffer(client: Client, maybeLogBuffer?: Array<SerializedOtelLog>): void {
148155
const logBuffer = maybeLogBuffer ?? CLIENT_TO_LOG_BUFFER_MAP.get(client) ?? [];

‎packages/core/src/server-runtime-client.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { eventFromMessage, eventFromUnknownInput } from './utils-hoist/eventbuil
2222
import { logger } from './utils-hoist/logger';
2323
import { uuid4 } from './utils-hoist/misc';
2424
import { resolvedSyncPromise } from './utils-hoist/syncpromise';
25-
import { _INTERNAL_flushLogsBuffer } from './logs';
25+
import { _INTERNAL_flushLogsBuffer } from './logs/exports';
2626
import { isPrimitive } from './utils-hoist';
2727

2828
export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportOptions> {
@@ -51,23 +51,26 @@ export class ServerRuntimeClient<
5151

5252
this._logWeight = 0;
5353

54-
// eslint-disable-next-line @typescript-eslint/no-this-alias
55-
const client = this;
56-
this.on('flush', () => {
57-
_INTERNAL_flushLogsBuffer(client);
58-
});
59-
60-
this.on('afterCaptureLog', log => {
61-
client._logWeight += estimateLogSizeInBytes(log);
62-
63-
// We flush the logs buffer if it exceeds 0.8 MB
64-
// The log weight is a rough estimate, so we flush way before
65-
// the payload gets too big.
66-
if (client._logWeight > 800_000) {
54+
if (this._options._experiments?.enableLogs) {
55+
// eslint-disable-next-line @typescript-eslint/no-this-alias
56+
const client = this;
57+
client.on('flush', () => {
6758
_INTERNAL_flushLogsBuffer(client);
6859
client._logWeight = 0;
69-
}
70-
});
60+
});
61+
62+
client.on('afterCaptureLog', log => {
63+
client._logWeight += estimateLogSizeInBytes(log);
64+
65+
// We flush the logs buffer if it exceeds 0.8 MB
66+
// The log weight is a rough estimate, so we flush way before
67+
// the payload gets too big.
68+
if (client._logWeight >= 800_000) {
69+
_INTERNAL_flushLogsBuffer(client);
70+
client._logWeight = 0;
71+
}
72+
});
73+
}
7174
}
7275

7376
/**

‎packages/core/test/lib/logs/index.test.ts renamed to ‎packages/core/test/lib/logs/exports.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
_INTERNAL_getLogBuffer,
55
_INTERNAL_captureLog,
66
logAttributeToSerializedLogAttribute,
7-
} from '../../../src/logs';
7+
} from '../../../src/logs/exports';
88
import { TestClient, getDefaultTestClientOptions } from '../../mocks/client';
99
import * as loggerModule from '../../../src/utils-hoist/logger';
1010
import { Scope, fmt } from '../../../src';

‎packages/core/test/lib/server-runtime-client.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, expect, it, test, vi } from 'vitest';
44
import { Scope, createTransport } from '../../src';
55
import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client';
66
import { ServerRuntimeClient } from '../../src/server-runtime-client';
7+
import { _INTERNAL_captureLog } from '../../src/logs/exports';
78

89
const PUBLIC_DSN = 'https://username@domain/123';
910

@@ -206,4 +207,77 @@ describe('ServerRuntimeClient', () => {
206207
]);
207208
});
208209
});
210+
211+
describe('log weight-based flushing', () => {
212+
it('flushes logs when weight exceeds 800KB', () => {
213+
const options = getDefaultClientOptions({
214+
dsn: PUBLIC_DSN,
215+
_experiments: { enableLogs: true },
216+
});
217+
client = new ServerRuntimeClient(options);
218+
219+
const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
220+
221+
// Create a large log message that will exceed the 800KB threshold
222+
const largeMessage = 'x'.repeat(400_000); // 400KB string
223+
_INTERNAL_captureLog({ message: largeMessage, level: 'info' }, client);
224+
225+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
226+
expect(client['_logWeight']).toBe(0); // Weight should be reset after flush
227+
});
228+
229+
it('accumulates log weight without flushing when under threshold', () => {
230+
const options = getDefaultClientOptions({
231+
dsn: PUBLIC_DSN,
232+
_experiments: { enableLogs: true },
233+
});
234+
client = new ServerRuntimeClient(options);
235+
236+
const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
237+
238+
// Create a log message that won't exceed the threshold
239+
const message = 'x'.repeat(100_000); // 100KB string
240+
_INTERNAL_captureLog({ message, level: 'info' }, client);
241+
242+
expect(sendEnvelopeSpy).not.toHaveBeenCalled();
243+
expect(client['_logWeight']).toBeGreaterThan(0);
244+
});
245+
246+
it('flushes logs on flush event', () => {
247+
const options = getDefaultClientOptions({
248+
dsn: PUBLIC_DSN,
249+
_experiments: { enableLogs: true },
250+
});
251+
client = new ServerRuntimeClient(options);
252+
253+
const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
254+
255+
// Add some logs
256+
_INTERNAL_captureLog({ message: 'test1', level: 'info' }, client);
257+
_INTERNAL_captureLog({ message: 'test2', level: 'info' }, client);
258+
259+
// Trigger flush event
260+
client.emit('flush');
261+
262+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
263+
expect(client['_logWeight']).toBe(0); // Weight should be reset after flush
264+
});
265+
266+
it('does not flush logs when logs are disabled', () => {
267+
const options = getDefaultClientOptions({
268+
dsn: PUBLIC_DSN,
269+
_experiments: { enableLogs: false },
270+
});
271+
client = new ServerRuntimeClient(options);
272+
273+
const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
274+
275+
// Create a large log message
276+
const largeMessage = 'x'.repeat(400_000);
277+
_INTERNAL_captureLog({ message: largeMessage, level: 'info' }, client);
278+
279+
expect(sendEnvelopeSpy).not.toHaveBeenCalled();
280+
expect(client['_logWeight']).toBe(0);
281+
});
282+
});
209283
});

0 commit comments

Comments
 (0)
Please sign in to comment.