Skip to content

Commit e55b8ee

Browse files
authored
feat(browser): Add logger.X methods to browser SDK (#15763)
ref #15526 Continuing off the work from #15717, this PR adds the logging public API to the Browser SDK. It also adds a basic flushing strategy to the SDK that is timeout based. This is done to help save bundle size. The main file added was `log.ts`. This has three areas to look at: 1. The logger methods for `trace`, `debug`, `info`, `warn`, `error`, `fatal` (the log severity levels) as well as an internal capture log helper all these methods call. 2. `addFlushingListeners` which adds listeners to flush the logs buffer on client flush and document visibility hidden. 3. a flush timeout that flushes logs after X seconds, which gets restarted when new logs are captured. I also removed any logs logic from the `BrowserClient`, which should ensure this stays as bundle size efficient as possible. Usage: ```js import * as Sentry from "@sentry/browser"; Sentry.init({ dsn: "your-dsn-here", _experiments: { enableLogs: true // This is required to use the logging features } }); // Trace level (lowest severity) Sentry.logger.trace("This is a trace message", { userId: 123 }); // Debug level Sentry.logger.debug("This is a debug message", { component: "UserProfile" }); // Info level Sentry.logger.info("User logged in successfully", { userId: 123 }); // Warning level Sentry.logger.warn("API response was slow", { responseTime: 2500 }); // Error level Sentry.logger.error("Failed to load user data", { userId: 123, errorCode: 404 }); // Critical level Sentry.logger.critical("Database connection failed", { dbHost: "primary-db" }); // Fatal level (highest severity) Sentry.logger.fatal("Application is shutting down unexpectedly", { memory: "exhausted" }); ```
1 parent 75f7b93 commit e55b8ee

File tree

8 files changed

+431
-30
lines changed

8 files changed

+431
-30
lines changed

packages/browser/src/client.ts

-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
addAutoIpAddressToUser,
1616
applySdkMetadata,
1717
getSDKSource,
18-
_INTERNAL_flushLogsBuffer,
1918
} from '@sentry/core';
2019
import { eventFromException, eventFromMessage } from './eventbuilder';
2120
import { WINDOW } from './helpers';
@@ -86,9 +85,6 @@ export class BrowserClient extends Client<BrowserClientOptions> {
8685
WINDOW.document.addEventListener('visibilitychange', () => {
8786
if (WINDOW.document.visibilityState === 'hidden') {
8887
this._flushOutcomes();
89-
if (this._options._experiments?.enableLogs) {
90-
_INTERNAL_flushLogsBuffer(this);
91-
}
9288
}
9389
});
9490
}

packages/browser/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export * from './exports';
22

3+
import * as logger from './log';
4+
5+
export { logger };
6+
37
export { reportingObserverIntegration } from './integrations/reportingobserver';
48
export { httpClientIntegration } from './integrations/httpclient';
59
export { contextLinesIntegration } from './integrations/contextlines';

packages/browser/src/log.ts

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import type { LogSeverityLevel, Log, Client } 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+
}
51+
52+
/**
53+
* Capture a log with the given level.
54+
*
55+
* @param level - The level of the log.
56+
* @param message - The message to log.
57+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
58+
* @param severityNumber - The severity number of the log.
59+
*/
60+
function captureLog(
61+
level: LogSeverityLevel,
62+
message: string,
63+
attributes?: Log['attributes'],
64+
severityNumber?: Log['severityNumber'],
65+
): 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);
74+
}
75+
76+
/**
77+
* @summary Capture a log with the `trace` level. Requires `_experiments.enableLogs` to be enabled.
78+
*
79+
* @param message - The message to log.
80+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
81+
*
82+
* @example
83+
*
84+
* ```
85+
* Sentry.logger.trace('Hello world', { userId: 100 });
86+
* ```
87+
*/
88+
export function trace(message: string, attributes?: Log['attributes']): void {
89+
captureLog('trace', message, attributes);
90+
}
91+
92+
/**
93+
* @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled.
94+
*
95+
* @param message - The message to log.
96+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
97+
*
98+
* @example
99+
*
100+
* ```
101+
* Sentry.logger.debug('Hello world', { userId: 100 });
102+
* ```
103+
*/
104+
export function debug(message: string, attributes?: Log['attributes']): void {
105+
captureLog('debug', message, attributes);
106+
}
107+
108+
/**
109+
* @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled.
110+
*
111+
* @param message - The message to log.
112+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
113+
*
114+
* @example
115+
*
116+
* ```
117+
* Sentry.logger.info('Hello world', { userId: 100 });
118+
* ```
119+
*/
120+
export function info(message: string, attributes?: Log['attributes']): void {
121+
captureLog('info', message, attributes);
122+
}
123+
124+
/**
125+
* @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled.
126+
*
127+
* @param message - The message to log.
128+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
129+
*
130+
* @example
131+
*
132+
* ```
133+
* Sentry.logger.warn('Hello world', { userId: 100 });
134+
* ```
135+
*/
136+
export function warn(message: string, attributes?: Log['attributes']): void {
137+
captureLog('warn', message, attributes);
138+
}
139+
140+
/**
141+
* @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled.
142+
*
143+
* @param message - The message to log.
144+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
145+
*
146+
* @example
147+
*
148+
* ```
149+
* Sentry.logger.error('Hello world', { userId: 100 });
150+
* ```
151+
*/
152+
export function error(message: string, attributes?: Log['attributes']): void {
153+
captureLog('error', message, attributes);
154+
}
155+
156+
/**
157+
* @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled.
158+
*
159+
* @param message - The message to log.
160+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
161+
*
162+
* @example
163+
*
164+
* ```
165+
* Sentry.logger.fatal('Hello world', { userId: 100 });
166+
* ```
167+
*/
168+
export function fatal(message: string, attributes?: Log['attributes']): void {
169+
captureLog('fatal', message, attributes);
170+
}
171+
172+
/**
173+
* @summary Capture a log with the `critical` level. Requires `_experiments.enableLogs` to be enabled.
174+
*
175+
* @param message - The message to log.
176+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
177+
*
178+
* @example
179+
*
180+
* ```
181+
* Sentry.logger.critical('Hello world', { userId: 100 });
182+
* ```
183+
*/
184+
export function critical(message: string, attributes?: Log['attributes']): void {
185+
captureLog('critical', message, attributes);
186+
}

packages/browser/test/index.test.ts

+29-14
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
getCurrentScope,
3030
init,
3131
showReportDialog,
32+
logger,
3233
} from '../src';
3334
import { getDefaultBrowserClientOptions } from './helper/browser-client-options';
3435
import { makeSimpleTransport } from './mocks/simpletransport';
@@ -243,20 +244,21 @@ describe('SentryBrowser', () => {
243244
expect(event.exception.values[0]?.stacktrace.frames).not.toHaveLength(0);
244245
});
245246

246-
it('should capture a message', done => {
247-
const options = getDefaultBrowserClientOptions({
248-
beforeSend: (event: Event): Event | null => {
249-
expect(event.level).toBe('info');
250-
expect(event.message).toBe('test');
251-
expect(event.exception).toBeUndefined();
252-
done();
253-
return event;
254-
},
255-
dsn,
256-
});
257-
setCurrentClient(new BrowserClient(options));
258-
captureMessage('test');
259-
});
247+
it('should capture an message', () =>
248+
new Promise<void>(resolve => {
249+
const options = getDefaultBrowserClientOptions({
250+
beforeSend: event => {
251+
expect(event.level).toBe('info');
252+
expect(event.message).toBe('test');
253+
expect(event.exception).toBeUndefined();
254+
resolve();
255+
return event;
256+
},
257+
dsn,
258+
});
259+
setCurrentClient(new BrowserClient(options));
260+
captureMessage('test');
261+
}));
260262

261263
it('should capture an event', () =>
262264
new Promise<void>(resolve => {
@@ -322,6 +324,19 @@ describe('SentryBrowser', () => {
322324
expect(localBeforeSend).not.toHaveBeenCalled();
323325
});
324326
});
327+
328+
describe('logger', () => {
329+
it('exports all log methods', () => {
330+
expect(logger).toBeDefined();
331+
expect(logger.trace).toBeDefined();
332+
expect(logger.debug).toBeDefined();
333+
expect(logger.info).toBeDefined();
334+
expect(logger.warn).toBeDefined();
335+
expect(logger.error).toBeDefined();
336+
expect(logger.fatal).toBeDefined();
337+
expect(logger.critical).toBeDefined();
338+
});
339+
});
325340
});
326341

327342
describe('SentryBrowser initialization', () => {

0 commit comments

Comments
 (0)