Skip to content

Commit 05cb1a4

Browse files
committed
feat(core): Add support for beforeSendLog
1 parent 2ed6fc2 commit 05cb1a4

File tree

5 files changed

+132
-6
lines changed

5 files changed

+132
-6
lines changed

packages/core/src/client.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -623,12 +623,19 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
623623
public on(hook: 'close', callback: () => void): () => void;
624624

625625
/**
626-
* A hook that is called before a log is captured
626+
* A hook that is called before a log is captured. This hooks runs before `beforeSendLog` is fired.
627627
*
628628
* @returns {() => void} A function that, when executed, removes the registered callback.
629629
*/
630630
public on(hook: 'beforeCaptureLog', callback: (log: Log) => void): () => void;
631631

632+
/**
633+
* A hook that is called after a log is captured
634+
*
635+
* @returns {() => void} A function that, when executed, removes the registered callback.
636+
*/
637+
public on(hook: 'afterCaptureLog', callback: (log: Log) => void): () => void;
638+
632639
/**
633640
* Register a hook on this client.
634641
*/
@@ -777,10 +784,15 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
777784
public emit(hook: 'close'): void;
778785

779786
/**
780-
* Emit a hook event for client before capturing a log
787+
* Emit a hook event for client before capturing a log. This hooks runs before `beforeSendLog` is fired.
781788
*/
782789
public emit(hook: 'beforeCaptureLog', log: Log): void;
783790

791+
/**
792+
* Emit a hook event for client after capturing a log.
793+
*/
794+
public emit(hook: 'afterCaptureLog', log: Log): void;
795+
784796
/**
785797
* Emit a hook that was previously registered via `on()`.
786798
*/

packages/core/src/logs/index.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,28 @@ 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(log: Log, client = getClient(), scope = getCurrentScope()): void {
65+
export function _INTERNAL_captureLog(beforeLog: Log, client = getClient(), scope = getCurrentScope()): void {
6666
if (!client) {
6767
DEBUG_BUILD && logger.warn('No client available to capture log.');
6868
return;
6969
}
7070

7171
const { _experiments, release, environment } = client.getOptions();
72-
if (!_experiments?.enableLogs) {
72+
const { enableLogs = false, beforeSendLog } = _experiments ?? {};
73+
if (!enableLogs) {
7374
DEBUG_BUILD && logger.warn('logging option not enabled, log will not be captured.');
7475
return;
7576
}
7677

78+
client.emit('beforeCaptureLog', beforeLog);
79+
80+
const log = beforeSendLog ? beforeSendLog(beforeLog) : beforeLog;
81+
if (!log) {
82+
client.recordDroppedEvent('before_send', 'log_item', 1);
83+
DEBUG_BUILD && logger.warn('beforeSendLog returned null, log will not be captured.');
84+
return;
85+
}
86+
7787
const [, traceContext] = _getTraceInfoFromScope(client, scope);
7888

7989
const { level, message, attributes, severityNumber } = log;
@@ -117,7 +127,7 @@ export function _INTERNAL_captureLog(log: Log, client = getClient(), scope = get
117127
}
118128
}
119129

120-
client.emit('beforeCaptureLog', log);
130+
client.emit('afterCaptureLog', log);
121131
}
122132

123133
/**

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class ServerRuntimeClient<
5757
_INTERNAL_flushLogsBuffer(client);
5858
});
5959

60-
this.on('beforeCaptureLog', log => {
60+
this.on('afterCaptureLog', log => {
6161
client._logWeight += estimateLogSizeInBytes(log);
6262

6363
// We flush the logs buffer if it exceeds 0.8 MB

packages/core/src/types-hoist/options.ts

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { CaptureContext } from '../scope';
22
import type { Breadcrumb, BreadcrumbHint } from './breadcrumb';
33
import type { ErrorEvent, EventHint, TransactionEvent } from './event';
44
import type { Integration } from './integration';
5+
import type { Log } from './log';
56
import type { TracesSamplerSamplingContext } from './samplingcontext';
67
import type { SdkMetadata } from './sdkmetadata';
78
import type { SpanJSON } from './span';
@@ -188,6 +189,17 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
188189
* If logs support should be enabled. Defaults to false.
189190
*/
190191
enableLogs?: boolean;
192+
/**
193+
* An event-processing callback for logs, guaranteed to be invoked after all other log
194+
* processors. This allows a log to be modified or dropped before it's sent.
195+
*
196+
* Note that you must return a valid log from this callback. If you do not wish to modify the log, simply return
197+
* it at the end. Returning `null` will cause the log to be dropped.
198+
*
199+
* @param log The log generated by the SDK.
200+
* @returns A new log that will be sent | null.
201+
*/
202+
beforeSendLog?: (log: Log) => Log | null;
191203
};
192204

193205
/**

packages/core/test/lib/log/index.test.ts

+92
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,96 @@ describe('_INTERNAL_captureLog', () => {
187187
_INTERNAL_flushLogsBuffer(client);
188188
expect(mockSendEnvelope).not.toHaveBeenCalled();
189189
});
190+
191+
it('processes logs through beforeSendLog when provided', () => {
192+
const beforeSendLog = vi.fn().mockImplementation(log => ({
193+
...log,
194+
message: `Modified: ${log.message}`,
195+
attributes: { ...log.attributes, processed: true },
196+
}));
197+
198+
const options = getDefaultTestClientOptions({
199+
dsn: PUBLIC_DSN,
200+
_experiments: { enableLogs: true, beforeSendLog },
201+
});
202+
const client = new TestClient(options);
203+
204+
_INTERNAL_captureLog(
205+
{
206+
level: 'info',
207+
message: 'original message',
208+
attributes: { original: true },
209+
},
210+
client,
211+
undefined,
212+
);
213+
214+
expect(beforeSendLog).toHaveBeenCalledWith({
215+
level: 'info',
216+
message: 'original message',
217+
attributes: { original: true },
218+
});
219+
220+
const logBuffer = _INTERNAL_getLogBuffer(client);
221+
expect(logBuffer).toBeDefined();
222+
expect(logBuffer?.[0]).toEqual(
223+
expect.objectContaining({
224+
body: {
225+
stringValue: 'Modified: original message',
226+
},
227+
attributes: expect.arrayContaining([
228+
expect.objectContaining({ key: 'processed', value: { boolValue: true } }),
229+
expect.objectContaining({ key: 'original', value: { boolValue: true } }),
230+
]),
231+
}),
232+
);
233+
});
234+
235+
it('drops logs when beforeSendLog returns null', () => {
236+
const beforeSendLog = vi.fn().mockReturnValue(null);
237+
const recordDroppedEventSpy = vi.spyOn(TestClient.prototype, 'recordDroppedEvent');
238+
const loggerWarnSpy = vi.spyOn(loggerModule.logger, 'warn').mockImplementation(() => undefined);
239+
240+
const options = getDefaultTestClientOptions({
241+
dsn: PUBLIC_DSN,
242+
_experiments: { enableLogs: true, beforeSendLog },
243+
});
244+
const client = new TestClient(options);
245+
246+
_INTERNAL_captureLog(
247+
{
248+
level: 'info',
249+
message: 'test message',
250+
},
251+
client,
252+
undefined,
253+
);
254+
255+
expect(beforeSendLog).toHaveBeenCalled();
256+
expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'log_item', 1);
257+
expect(loggerWarnSpy).toHaveBeenCalledWith('beforeSendLog returned null, log will not be captured.');
258+
expect(_INTERNAL_getLogBuffer(client)).toBeUndefined();
259+
260+
recordDroppedEventSpy.mockRestore();
261+
loggerWarnSpy.mockRestore();
262+
});
263+
264+
it('emits beforeCaptureLog and afterCaptureLog events', () => {
265+
const beforeCaptureLogSpy = vi.spyOn(TestClient.prototype, 'emit');
266+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, _experiments: { enableLogs: true } });
267+
const client = new TestClient(options);
268+
269+
_INTERNAL_captureLog(
270+
{
271+
level: 'info',
272+
message: 'test message',
273+
},
274+
client,
275+
undefined,
276+
);
277+
278+
expect(beforeCaptureLogSpy).toHaveBeenCalledWith('beforeCaptureLog', log);
279+
expect(beforeCaptureLogSpy).toHaveBeenCalledWith('afterCaptureLog', log);
280+
beforeCaptureLogSpy.mockRestore();
281+
});
190282
});

0 commit comments

Comments
 (0)