Skip to content

Commit d650218

Browse files
authored
feat(core): Add support for parameterizing logs (#15812)
ref #15526 This adds support for parameterizing logs via the existing `ParameterizedString` type and `parameterize` function exported from the SDK. This works for all usages of the logger, so browser and Node.js. Usage: ```js Sentry.logger.info(Sentry.logger.fmt`User ${user} updated profile picture`, { userId: 'user-123', imageSize: '2.5MB', timestamp: Date.now() }); ``` `fmt` is an alias to `Sentry.parameterize` that is exported from the `logger` namespace. To support this change, I changed the typing of `ParameterizedString` to accept `unknown[]` for `__sentry_template_values__`. This is broadening the type, so should not be a breaking change. [`logentry.params`](https://github.com/getsentry/relay/blob/a91f0c92860f88789ad6092ef5b1062aa3e34b80/relay-event-schema/src/protocol/logentry.rs#L51C27-L51C32) should accept all kinds of values, relay handles formatting them correctly.
1 parent 9ca030d commit d650218

File tree

12 files changed

+248
-33
lines changed

12 files changed

+248
-33
lines changed

packages/browser/src/log.ts

+129-23
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { LogSeverityLevel, Log, Client } from '@sentry/core';
1+
import type { LogSeverityLevel, Log, Client, ParameterizedString } from '@sentry/core';
22
import { getClient, _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from '@sentry/core';
33

44
import { WINDOW } from './helpers';
@@ -59,7 +59,7 @@ function addFlushingListeners(client: Client): void {
5959
*/
6060
function captureLog(
6161
level: LogSeverityLevel,
62-
message: string,
62+
message: ParameterizedString,
6363
attributes?: Log['attributes'],
6464
severityNumber?: Log['severityNumber'],
6565
): void {
@@ -77,110 +77,216 @@ function captureLog(
7777
* @summary Capture a log with the `trace` level. Requires `_experiments.enableLogs` to be enabled.
7878
*
7979
* @param message - The message to log.
80-
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
80+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., { userId: 100, route: '/dashboard' }.
8181
*
8282
* @example
8383
*
8484
* ```
85-
* Sentry.logger.trace('Hello world', { userId: 100 });
85+
* Sentry.logger.trace('User clicked submit button', {
86+
* buttonId: 'submit-form',
87+
* formId: 'user-profile',
88+
* timestamp: Date.now()
89+
* });
90+
* ```
91+
*
92+
* @example With template strings
93+
*
94+
* ```
95+
* Sentry.logger.trace(Sentry.logger.fmt`User ${user} navigated to ${page}`, {
96+
* userId: '123',
97+
* sessionId: 'abc-xyz'
98+
* });
8699
* ```
87100
*/
88-
export function trace(message: string, attributes?: Log['attributes']): void {
101+
export function trace(message: ParameterizedString, attributes?: Log['attributes']): void {
89102
captureLog('trace', message, attributes);
90103
}
91104

92105
/**
93106
* @summary Capture a log with the `debug` level. Requires `_experiments.enableLogs` to be enabled.
94107
*
95108
* @param message - The message to log.
96-
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
109+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., { component: 'Header', state: 'loading' }.
97110
*
98111
* @example
99112
*
100113
* ```
101-
* Sentry.logger.debug('Hello world', { userId: 100 });
114+
* Sentry.logger.debug('Component mounted', {
115+
* component: 'UserProfile',
116+
* props: { userId: 123 },
117+
* renderTime: 150
118+
* });
119+
* ```
120+
*
121+
* @example With template strings
122+
*
123+
* ```
124+
* Sentry.logger.debug(Sentry.logger.fmt`API request to ${endpoint} failed`, {
125+
* statusCode: 404,
126+
* requestId: 'req-123',
127+
* duration: 250
128+
* });
102129
* ```
103130
*/
104-
export function debug(message: string, attributes?: Log['attributes']): void {
131+
export function debug(message: ParameterizedString, attributes?: Log['attributes']): void {
105132
captureLog('debug', message, attributes);
106133
}
107134

108135
/**
109136
* @summary Capture a log with the `info` level. Requires `_experiments.enableLogs` to be enabled.
110137
*
111138
* @param message - The message to log.
112-
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
139+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., { feature: 'checkout', status: 'completed' }.
113140
*
114141
* @example
115142
*
116143
* ```
117-
* Sentry.logger.info('Hello world', { userId: 100 });
144+
* Sentry.logger.info('User completed checkout', {
145+
* orderId: 'order-123',
146+
* amount: 99.99,
147+
* paymentMethod: 'credit_card'
148+
* });
149+
* ```
150+
*
151+
* @example With template strings
152+
*
153+
* ```
154+
* Sentry.logger.info(Sentry.logger.fmt`User ${user} updated profile picture`, {
155+
* userId: 'user-123',
156+
* imageSize: '2.5MB',
157+
* timestamp: Date.now()
158+
* });
118159
* ```
119160
*/
120-
export function info(message: string, attributes?: Log['attributes']): void {
161+
export function info(message: ParameterizedString, attributes?: Log['attributes']): void {
121162
captureLog('info', message, attributes);
122163
}
123164

124165
/**
125166
* @summary Capture a log with the `warn` level. Requires `_experiments.enableLogs` to be enabled.
126167
*
127168
* @param message - The message to log.
128-
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
169+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., { browser: 'Chrome', version: '91.0' }.
129170
*
130171
* @example
131172
*
132173
* ```
133-
* Sentry.logger.warn('Hello world', { userId: 100 });
174+
* Sentry.logger.warn('Browser compatibility issue detected', {
175+
* browser: 'Safari',
176+
* version: '14.0',
177+
* feature: 'WebRTC',
178+
* fallback: 'enabled'
179+
* });
180+
* ```
181+
*
182+
* @example With template strings
183+
*
184+
* ```
185+
* Sentry.logger.warn(Sentry.logger.fmt`API endpoint ${endpoint} is deprecated`, {
186+
* recommendedEndpoint: '/api/v2/users',
187+
* sunsetDate: '2024-12-31',
188+
* clientVersion: '1.2.3'
189+
* });
134190
* ```
135191
*/
136-
export function warn(message: string, attributes?: Log['attributes']): void {
192+
export function warn(message: ParameterizedString, attributes?: Log['attributes']): void {
137193
captureLog('warn', message, attributes);
138194
}
139195

140196
/**
141197
* @summary Capture a log with the `error` level. Requires `_experiments.enableLogs` to be enabled.
142198
*
143199
* @param message - The message to log.
144-
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
200+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., { error: 'NetworkError', url: '/api/data' }.
145201
*
146202
* @example
147203
*
148204
* ```
149-
* Sentry.logger.error('Hello world', { userId: 100 });
205+
* Sentry.logger.error('Failed to load user data', {
206+
* error: 'NetworkError',
207+
* url: '/api/users/123',
208+
* statusCode: 500,
209+
* retryCount: 3
210+
* });
211+
* ```
212+
*
213+
* @example With template strings
214+
*
215+
* ```
216+
* Sentry.logger.error(Sentry.logger.fmt`Payment processing failed for order ${orderId}`, {
217+
* error: 'InsufficientFunds',
218+
* amount: 100.00,
219+
* currency: 'USD',
220+
* userId: 'user-456'
221+
* });
150222
* ```
151223
*/
152-
export function error(message: string, attributes?: Log['attributes']): void {
224+
export function error(message: ParameterizedString, attributes?: Log['attributes']): void {
153225
captureLog('error', message, attributes);
154226
}
155227

156228
/**
157229
* @summary Capture a log with the `fatal` level. Requires `_experiments.enableLogs` to be enabled.
158230
*
159231
* @param message - The message to log.
160-
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
232+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., { appState: 'corrupted', sessionId: 'abc-123' }.
161233
*
162234
* @example
163235
*
164236
* ```
165-
* Sentry.logger.fatal('Hello world', { userId: 100 });
237+
* Sentry.logger.fatal('Application state corrupted', {
238+
* lastKnownState: 'authenticated',
239+
* sessionId: 'session-123',
240+
* timestamp: Date.now(),
241+
* recoveryAttempted: true
242+
* });
243+
* ```
244+
*
245+
* @example With template strings
246+
*
247+
* ```
248+
* Sentry.logger.fatal(Sentry.logger.fmt`Critical system failure in ${service}`, {
249+
* service: 'payment-processor',
250+
* errorCode: 'CRITICAL_FAILURE',
251+
* affectedUsers: 150,
252+
* timestamp: Date.now()
253+
* });
166254
* ```
167255
*/
168-
export function fatal(message: string, attributes?: Log['attributes']): void {
256+
export function fatal(message: ParameterizedString, attributes?: Log['attributes']): void {
169257
captureLog('fatal', message, attributes);
170258
}
171259

172260
/**
173261
* @summary Capture a log with the `critical` level. Requires `_experiments.enableLogs` to be enabled.
174262
*
175263
* @param message - The message to log.
176-
* @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100.
264+
* @param attributes - Arbitrary structured data that stores information about the log - e.g., { security: 'breach', severity: 'high' }.
177265
*
178266
* @example
179267
*
180268
* ```
181-
* Sentry.logger.critical('Hello world', { userId: 100 });
269+
* Sentry.logger.critical('Security breach detected', {
270+
* type: 'unauthorized_access',
271+
* user: '132123',
272+
* endpoint: '/api/admin',
273+
* timestamp: Date.now()
274+
* });
275+
* ```
276+
*
277+
* @example With template strings
278+
*
279+
* ```
280+
* Sentry.logger.critical(Sentry.logger.fmt`Multiple failed login attempts from user ${user}`, {
281+
* attempts: 10,
282+
* timeWindow: '5m',
283+
* blocked: true,
284+
* timestamp: Date.now()
285+
* });
182286
* ```
183287
*/
184-
export function critical(message: string, attributes?: Log['attributes']): void {
288+
export function critical(message: ParameterizedString, attributes?: Log['attributes']): void {
185289
captureLog('critical', message, attributes);
186290
}
291+
292+
export { fmt } from '@sentry/core';

packages/browser/test/log.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -196,5 +196,38 @@ describe('Logger', () => {
196196
vi.advanceTimersByTime(2000);
197197
expect(mockFlushLogsBuffer).toHaveBeenCalledTimes(1);
198198
});
199+
200+
it('should handle parameterized strings with parameters', () => {
201+
logger.info(logger.fmt`Hello ${'John'}, your balance is ${100}`, { userId: 123 });
202+
expect(mockCaptureLog).toHaveBeenCalledWith(
203+
{
204+
level: 'info',
205+
message: expect.objectContaining({
206+
__sentry_template_string__: 'Hello %s, your balance is %s',
207+
__sentry_template_values__: ['John', 100],
208+
}),
209+
attributes: {
210+
userId: 123,
211+
},
212+
},
213+
expect.any(Object),
214+
undefined,
215+
);
216+
});
217+
218+
it('should handle parameterized strings without additional attributes', () => {
219+
logger.debug(logger.fmt`User ${'Alice'} logged in from ${'mobile'}`);
220+
expect(mockCaptureLog).toHaveBeenCalledWith(
221+
{
222+
level: 'debug',
223+
message: expect.objectContaining({
224+
__sentry_template_string__: 'User %s logged in from %s',
225+
__sentry_template_values__: ['Alice', 'mobile'],
226+
}),
227+
},
228+
expect.any(Object),
229+
undefined,
230+
);
231+
});
199232
});
200233
});

packages/core/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export { hasTracingEnabled } from './utils/hasSpansEnabled';
6666
export { hasSpansEnabled } from './utils/hasSpansEnabled';
6767
export { isSentryRequestUrl } from './utils/isSentryRequestUrl';
6868
export { handleCallbackErrors } from './utils/handleCallbackErrors';
69-
export { parameterize } from './utils/parameterize';
69+
export { parameterize, fmt } from './utils/parameterize';
7070
export { addAutoIpAddressToSession, addAutoIpAddressToUser } from './utils/ipAddress';
7171
export {
7272
convertSpanLinksForEnvelope,

packages/core/src/logs/index.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { DEBUG_BUILD } from '../debug-build';
55
import { SEVERITY_TEXT_TO_SEVERITY_NUMBER } from './constants';
66
import type { SerializedLogAttribute, SerializedOtelLog } from '../types-hoist';
77
import type { Log } from '../types-hoist/log';
8-
import { logger } from '../utils-hoist';
8+
import { isParameterizedString, logger } from '../utils-hoist';
99
import { _getSpanForScope } from '../utils/spanOnScope';
1010
import { createOtelLogEnvelope } from './envelope';
1111

@@ -100,6 +100,14 @@ export function _INTERNAL_captureLog(beforeLog: Log, client = getClient(), scope
100100
logAttributes.environment = environment;
101101
}
102102

103+
if (isParameterizedString(message)) {
104+
const { __sentry_template_string__, __sentry_template_values__ = [] } = message;
105+
logAttributes['sentry.message.template'] = __sentry_template_string__;
106+
__sentry_template_values__.forEach((param, index) => {
107+
logAttributes[`sentry.message.param.${index}`] = param;
108+
});
109+
}
110+
103111
const span = _getSpanForScope(scope);
104112
if (span) {
105113
// Add the parent span ID to the log attributes for trace context

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface Event {
2222
message?: string;
2323
logentry?: {
2424
message?: string;
25-
params?: string[];
25+
params?: unknown[];
2626
};
2727
timestamp?: number;
2828
start_timestamp?: number;

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ParameterizedString } from './parameterize';
2+
13
export type LogSeverityLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'critical';
24

35
export type SerializedLogAttributeValueType =
@@ -36,7 +38,7 @@ export interface Log {
3638
/**
3739
* The message to be logged - for example, 'hello world' would become a log like '[INFO] hello world'
3840
*/
39-
message: string;
41+
message: ParameterizedString;
4042

4143
/**
4244
* Arbitrary structured data that stores information about the log - e.g., userId: 100.
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export type ParameterizedString = string & {
22
__sentry_template_string__?: string;
3-
__sentry_template_values__?: string[];
3+
__sentry_template_values__?: unknown[];
44
};

packages/core/src/utils/parameterize.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,24 @@ import type { ParameterizedString } from '../types-hoist';
55
* For example: parameterize`This is a log statement with ${x} and ${y} params`, would return:
66
* "__sentry_template_string__": 'This is a log statement with %s and %s params',
77
* "__sentry_template_values__": ['first', 'second']
8+
*
89
* @param strings An array of string values splitted between expressions
910
* @param values Expressions extracted from template string
10-
* @returns String with template information in __sentry_template_string__ and __sentry_template_values__ properties
11+
*
12+
* @returns A `ParameterizedString` object that can be passed into `captureMessage` or Sentry.logger.X methods.
1113
*/
12-
export function parameterize(strings: TemplateStringsArray, ...values: string[]): ParameterizedString {
14+
export function parameterize(strings: TemplateStringsArray, ...values: unknown[]): ParameterizedString {
1315
const formatted = new String(String.raw(strings, ...values)) as ParameterizedString;
1416
formatted.__sentry_template_string__ = strings.join('\x00').replace(/%/g, '%%').replace(/\0/g, '%s');
1517
formatted.__sentry_template_values__ = values;
1618
return formatted;
1719
}
20+
21+
/**
22+
* Tagged template function which returns parameterized representation of the message.
23+
*
24+
* @param strings An array of string values splitted between expressions
25+
* @param values Expressions extracted from template string
26+
* @returns A `ParameterizedString` object that can be passed into `captureMessage` or Sentry.logger.X methods.
27+
*/
28+
export const fmt = parameterize;

0 commit comments

Comments
 (0)