diff --git a/.size-limit.js b/.size-limit.js index ffa69d850947..d312dbcdac3b 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -8,7 +8,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init'), gzip: true, - limit: '24 KB', + limit: '25 KB', }, { name: '@sentry/browser - with treeshaking flags', @@ -40,14 +40,14 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '37.5 KB', + limit: '38 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '75.2 KB', + limit: '77 KB', }, { name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags', @@ -79,7 +79,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '80 KB', + limit: '82 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', @@ -210,7 +210,7 @@ module.exports = [ import: createImport('init'), ignore: ['next/router', 'next/constants'], gzip: true, - limit: '41 KB', + limit: '43 KB', }, // SvelteKit SDK (ESM) { diff --git a/CHANGELOG.md b/CHANGELOG.md index 116621720966..3504c061d5b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,36 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.4.0-alpha.0 + +This is an alpha release that includes experimental functionality for the new logs API in Sentry. Support for these methods are only avaliable in the browser and core SDKs. + +- feat(logs): Add experimental user-callable logging methods (#15442) + +Logging is gated by an experimental option, `_experiments.enableLogs`. + +```js +Sentry.init({ + _experiments: { + enableLogs: true, + }, +}); +``` + +These API are exposed in the `Sentry._experiment_log` namespace. + +On the high level, there are functions for each of the logging severity levels `critical`, `fatal`, `error`, `warn`, `info`, `debug`, `trace`. These functions are tagged template functions, so they use a special string template syntax that we use to parameterize functions accordingly. + +```js +Sentry._experiment_log.info`user ${username} just bought ${item}!`; +``` + +If you want more custom usage, we also expose a `captureLog` method that allows you to pass custom attributes, but it's less easy to use than the tagged template functions. + +```js +Sentry._experiment_log.captureLog('error', 'Hello world!', { 'user.id': 123 }); +``` + ## 9.3.0 ### Important Changes diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index d034330b6283..07f772da5db9 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -10,6 +10,7 @@ export { extraErrorDataIntegration, rewriteFramesIntegration, captureFeedback, + _experiment_log, } from '@sentry/core'; export { replayIntegration, getReplay } from '@sentry-internal/replay'; diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 4854ee86efb8..c64bc1a86a48 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -1,5 +1,6 @@ import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; +import { captureLog, sendLog } from './log'; import type { CaptureContext } from './scope'; import { closeSession, makeSession, updateSession } from './session'; import type { @@ -10,6 +11,7 @@ import type { Extra, Extras, FinishedCheckIn, + LogSeverityLevel, MonitorConfig, Primitive, Session, @@ -334,3 +336,68 @@ export function captureSession(end: boolean = false): void { // only send the update _sendSessionUpdate(); } + +type OmitFirstArg = F extends (x: LogSeverityLevel, ...args: infer P) => infer R ? (...args: P) => R : never; + +/** + * A namespace for experimental logging functions. + * + * @experimental Will be removed in future versions. Do not use. + */ +export const _experiment_log = { + /** + * A utility to record a log with level 'TRACE' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: Sentry._experiment_log.trace`user ${username} just bought ${item}!` + */ + trace: sendLog.bind(null, 'trace') as OmitFirstArg, + /** + * A utility to record a log with level 'DEBUG' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: Sentry._experiment_log.debug`user ${username} just bought ${item}!` + */ + debug: sendLog.bind(null, 'debug') as OmitFirstArg, + /** + * A utility to record a log with level 'INFO' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: Sentry._experiment_log.info`user ${username} just bought ${item}!` + */ + info: sendLog.bind(null, 'info') as OmitFirstArg, + /** + * A utility to record a log with level 'INFO' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: Sentry._experiment_log.log`user ${username} just bought ${item}!` + */ + log: sendLog.bind(null, 'log') as OmitFirstArg, + /** + * A utility to record a log with level 'ERROR' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: Sentry._experiment_log.error`user ${username} just bought ${item}!` + */ + error: sendLog.bind(null, 'error') as OmitFirstArg, + /** + * A utility to record a log with level 'WARN' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: Sentry._experiment_log.warn`user ${username} just bought ${item}!` + */ + warn: sendLog.bind(null, 'warn') as OmitFirstArg, + /** + * A utility to record a log with level 'FATAL' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: Sentry._experiment_log.warn`user ${username} just bought ${item}!` + */ + fatal: sendLog.bind(null, 'fatal') as OmitFirstArg, + /** + * A flexible utility to record a log with a custom level and send it to sentry. + * + * You can optionally pass in custom attributes and a custom severity number to be attached to the log. + */ + captureLog, +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 35bfc35bc603..859f98837b8b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,6 +29,7 @@ export { endSession, captureSession, addEventProcessor, + _experiment_log, } from './exports'; export { getCurrentScope, diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts new file mode 100644 index 000000000000..8e8971e694cc --- /dev/null +++ b/packages/core/src/log.ts @@ -0,0 +1,188 @@ +import type { Client } from './client'; +import { getClient, getCurrentScope } from './currentScopes'; +import { DEBUG_BUILD } from './debug-build'; +import type { Scope } from './scope'; +import { getDynamicSamplingContextFromScope } from './tracing'; +import type { DynamicSamplingContext, LogEnvelope, LogItem } from './types-hoist/envelope'; +import type { Log, LogAttribute, LogSeverityLevel } from './types-hoist/log'; +import { createEnvelope, dropUndefinedKeys, dsnToString, logger } from './utils-hoist'; + +const LOG_BUFFER_MAX_LENGTH = 25; + +let GLOBAL_LOG_BUFFER: Log[] = []; + +let isFlushingLogs = false; + +const SEVERITY_TEXT_TO_SEVERITY_NUMBER: Partial> = { + trace: 1, + debug: 5, + info: 9, + log: 10, + warn: 13, + error: 17, + fatal: 21, +}; + +/** + * Creates envelope item for a single log + */ +export function createLogEnvelopeItem(log: Log): LogItem { + const headers: LogItem[0] = { + type: 'otel_log', + }; + + return [headers, log]; +} + +/** + * Records a log and sends it to sentry. + * + * Logs represent a message (and optionally some structured data) which provide context for a trace or error. + * Ex: sentry.addLog({level: 'warning', message: `user ${user} just bought ${item}`, attributes: {user, item}} + * + * @params log - the log object which will be sent + */ +function createLogEnvelope(logs: Log[], client: Client, scope: Scope): LogEnvelope { + const dsc = getDynamicSamplingContextFromScope(client, scope); + + const dsn = client.getDsn(); + + const headers: LogEnvelope[0] = { + trace: dropUndefinedKeys(dsc) as DynamicSamplingContext, + ...(dsn ? { dsn: dsnToString(dsn) } : {}), + }; + + return createEnvelope(headers, logs.map(createLogEnvelopeItem)); +} + +function valueToAttribute(key: string, value: unknown): LogAttribute { + switch (typeof value) { + case 'number': + return { + key, + value: { doubleValue: value }, + }; + case 'boolean': + return { + key, + value: { boolValue: value }, + }; + case 'string': + return { + key, + value: { stringValue: value }, + }; + default: + return { + key, + value: { stringValue: JSON.stringify(value) ?? '' }, + }; + } +} + +function addToLogBuffer(client: Client, log: Log, scope: Scope): void { + function sendLogs(flushedLogs: Log[]): void { + const envelope = createLogEnvelope(flushedLogs, client, scope); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + void client.sendEnvelope(envelope); + } + + if (GLOBAL_LOG_BUFFER.length >= LOG_BUFFER_MAX_LENGTH) { + sendLogs(GLOBAL_LOG_BUFFER); + GLOBAL_LOG_BUFFER = []; + } else { + GLOBAL_LOG_BUFFER.push(log); + } + + // this is the first time logs have been enabled, let's kick off an interval to flush them + // we should only do this once. + if (!isFlushingLogs) { + setInterval(() => { + if (GLOBAL_LOG_BUFFER.length > 0) { + sendLogs(GLOBAL_LOG_BUFFER); + GLOBAL_LOG_BUFFER = []; + } + }, 5000); + } + isFlushingLogs = true; +} + +/** + * A utility function to be able to create methods like Sentry.info`...` that use tagged template functions. + * + * The first parameter is bound with, e.g., const info = captureLog.bind(null, 'info') + * The other parameters are in the format to be passed a tagged template, Sentry.info`hello ${world}` + */ +export function sendLog(level: LogSeverityLevel, messageArr: TemplateStringsArray, ...values: unknown[]): void { + const message = messageArr.reduce((acc, str, i) => acc + str + (JSON.stringify(values[i]) ?? ''), ''); + + const attributes = values.reduce>( + (acc, value, index) => { + acc[`sentry.message.parameters.${index}`] = value; + return acc; + }, + { + 'sentry.message.template': messageArr.map((s, i) => s + (i < messageArr.length - 1 ? `$param.${i}` : '')).join(''), + }, + ); + + captureLog(level, message, attributes); +} + +/** + * Sends a log to Sentry. + */ +export function captureLog( + level: LogSeverityLevel, + message: string, + customAttributes: Record = {}, + severityNumber?: number, +): void { + const client = getClient(); + + if (!client) { + DEBUG_BUILD && logger.warn('No client available, log will not be captured.'); + return; + } + + if (!client.getOptions()._experiments?.enableLogs) { + DEBUG_BUILD && logger.warn('logging option not enabled, log will not be captured.'); + return; + } + + const { release, environment } = client.getOptions(); + + const logAttributes = { + ...customAttributes, + }; + + if (release) { + logAttributes['sentry.release'] = release; + } + + if (environment) { + logAttributes['sentry.environment'] = environment; + } + + const scope = getCurrentScope(); + + const attributes = Object.entries(logAttributes).map(([key, value]) => valueToAttribute(key, value)); + + const log: Log = { + severityText: level, + body: { + stringValue: message, + }, + attributes, + timeUnixNano: `${new Date().getTime().toString()}000000`, + traceId: scope.getPropagationContext().traceId, + severityNumber, + }; + + const maybeSeverityNumber = SEVERITY_TEXT_TO_SEVERITY_NUMBER[level]; + if (maybeSeverityNumber !== undefined && log.severityNumber === undefined) { + log.severityNumber = maybeSeverityNumber; + } + + addToLogBuffer(client, log, scope); +} diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 8e52b32eacf7..d0474b959fa9 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -182,7 +182,12 @@ export interface ClientOptions