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

Lines changed: 129 additions & 23 deletions
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

Lines changed: 33 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 9 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 3 additions & 1 deletion
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.
Lines changed: 1 addition & 1 deletion
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

Lines changed: 13 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy