From bdcb9e116c0226a6db2f6da6e79965da01a0db45 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Fri, 14 Feb 2025 10:33:04 -0500 Subject: [PATCH 01/21] initial work on .log --- packages/browser/src/index.ts | 3 + packages/core/src/exports.ts | 26 +++++ packages/core/src/index.ts | 3 + packages/core/src/ourlogs.ts | 111 ++++++++++++++++++++++ packages/core/src/types-hoist/envelope.ts | 9 +- packages/core/src/types-hoist/index.ts | 6 ++ packages/core/src/types-hoist/ourlogs.ts | 67 +++++++++++++ packages/core/src/utils-hoist/envelope.ts | 1 + 8 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/ourlogs.ts create mode 100644 packages/core/src/types-hoist/ourlogs.ts diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 63da52dfd30e..4449b637a34b 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -10,6 +10,9 @@ export { extraErrorDataIntegration, rewriteFramesIntegration, captureFeedback, + _experimentalLogError, + _experimentalLogInfo, + _experimentalLogWarning, } 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..d1a15479a760 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -24,6 +24,7 @@ import { timestampInSeconds } from './utils-hoist/time'; import { GLOBAL_OBJ } from './utils-hoist/worldwide'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; +import { captureLog } from './ourlogs'; /** * Captures an exception event and sends it to Sentry. @@ -334,3 +335,28 @@ export function captureSession(end: boolean = false): void { // only send the update _sendSessionUpdate(); } + + +/** + * 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.logInfo`user ${username} just bought ${item}!` + */ +export const _experimentalLogInfo = captureLog.bind(null, 'info'); + +/** + * 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.logError`user ${username} just bought ${item}!` + */ +export const _experimentalLogError = captureLog.bind(null, 'error'); + +/** + * A utility to record a log with level 'WARNING' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: sentry.logWarning`user ${username} just bought ${item}!` + */ +export const _experimentalLogWarning = captureLog.bind(null, 'warning'); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e0e9097bbc53..8e40617e2743 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,6 +29,9 @@ export { endSession, captureSession, addEventProcessor, + _experimentalLogError, + _experimentalLogInfo, + _experimentalLogWarning } from './exports'; export { getCurrentScope, diff --git a/packages/core/src/ourlogs.ts b/packages/core/src/ourlogs.ts new file mode 100644 index 000000000000..42066ea61ec4 --- /dev/null +++ b/packages/core/src/ourlogs.ts @@ -0,0 +1,111 @@ +import { getClient, getGlobalScope } from './currentScopes'; +import type { LogEnvelope, LogItem } from './types-hoist/envelope'; +import type { Log, LogAttribute, LogSeverityLevel } from './types-hoist/ourlogs'; +import { createEnvelope, dsnToString } from './utils-hoist'; + +/** + * 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 addLog(log: Log): void { + const client = getClient(); + + if (!client) { + return; + } + + // if (!client.getOptions()._experiments?.logSupport) { + // return; + // } + + const globalScope = getGlobalScope(); + const dsn = client.getDsn(); + + const headers: LogEnvelope[0] = { + trace: { + trace_id: globalScope.getPropagationContext().traceId, + public_key: dsn?.publicKey, + }, + ...(dsn ? {dsn: dsnToString(dsn)} : {}), + } + if(!log.traceId) { + log.traceId = globalScope.getPropagationContext().traceId || '00000000-0000-0000-0000-000000000000'; + } + if(!log.timeUnixNano) { + log.timeUnixNano = `${(new Date()).getTime().toString()}000000`; + } + + const envelope = createEnvelope(headers, [createLogEnvelopeItem(log)]); + + client.sendEnvelope(envelope).then(null, ex => console.error(ex)); +} + +/** + * A utility function to be able to create methods like Sentry.info(...) + * + * 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 template, Sentry.info`hello ${world}` + */ +export function captureLog(level: LogSeverityLevel, strings: string[], ...values: unknown[]): void { + addLog({ + severityText: level, + body: { + stringValue: strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '' ), + }, + attributes: values.map((value, index) => { + const key = `param${index}`; + if (typeof value === 'number') { + if(Number.isInteger(value)) { + return { + key, + value: { + intValue: value + } + } + } + return { + key, + value: { + doubleValue: value + } + } + } else if (typeof value === 'boolean') { + return { + key, + value: { + boolValue: value + } + } + } else if (typeof value === 'string') { + return { + key, + value: { + stringValue: value + } + } + } else { + return { + key, + value: { + stringValue: JSON.stringify(value) + } + } + } + }, {}) + }) +} diff --git a/packages/core/src/types-hoist/envelope.ts b/packages/core/src/types-hoist/envelope.ts index 5a54ffc7b8c2..d2486f94d5e0 100644 --- a/packages/core/src/types-hoist/envelope.ts +++ b/packages/core/src/types-hoist/envelope.ts @@ -10,6 +10,7 @@ import type { ReplayEvent, ReplayRecordingData } from './replay'; import type { SdkInfo } from './sdkinfo'; import type { SerializedSession, SessionAggregates } from './session'; import type { SpanJSON } from './span'; +import { Log } from './ourlogs'; // Based on: https://develop.sentry.dev/sdk/envelopes/ @@ -43,6 +44,7 @@ export type EnvelopeItemType = | 'replay_recording' | 'check_in' | 'span' + | 'otel_log' | 'raw_security'; export type BaseEnvelopeHeaders = { @@ -85,6 +87,7 @@ type CheckInItemHeaders = { type: 'check_in' }; type ProfileItemHeaders = { type: 'profile' }; type ProfileChunkItemHeaders = { type: 'profile_chunk' }; type SpanItemHeaders = { type: 'span' }; +type LogItemHeaders = { type: 'otel_log' }; type RawSecurityHeaders = { type: 'raw_security'; sentry_release?: string; sentry_environment?: string }; export type EventItem = BaseEnvelopeItem; @@ -101,6 +104,7 @@ export type FeedbackItem = BaseEnvelopeItem; export type ProfileItem = BaseEnvelopeItem; export type ProfileChunkItem = BaseEnvelopeItem; export type SpanItem = BaseEnvelopeItem>; +export type LogItem = BaseEnvelopeItem; export type RawSecurityItem = BaseEnvelopeItem; export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: Partial }; @@ -109,6 +113,7 @@ type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext }; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; type ReplayEnvelopeHeaders = BaseEnvelopeHeaders; type SpanEnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext }; +type LogEnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext }; export type EventEnvelope = BaseEnvelope< EventEnvelopeHeaders, @@ -121,6 +126,7 @@ export type CheckInEnvelope = BaseEnvelope; export type SpanEnvelope = BaseEnvelope; export type ProfileChunkEnvelope = BaseEnvelope; export type RawSecurityEnvelope = BaseEnvelope; +export type LogEnvelope = BaseEnvelope; export type Envelope = | EventEnvelope @@ -130,6 +136,7 @@ export type Envelope = | ReplayEnvelope | CheckInEnvelope | SpanEnvelope - | RawSecurityEnvelope; + | RawSecurityEnvelope + | LogEnvelope; export type EnvelopeItem = Envelope[1][number]; diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index c1cbe5284808..469f896ae7fe 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -113,6 +113,12 @@ export type { SpanContextData, TraceFlag, } from './span'; +export type { + Log, + LogAttribute, + LogSeverityLevel, + LogAttributeValueType +} from './ourlogs'; export type { SpanStatus } from './spanStatus'; export type { TimedEvent } from './timedEvent'; export type { StackFrame } from './stackframe'; diff --git a/packages/core/src/types-hoist/ourlogs.ts b/packages/core/src/types-hoist/ourlogs.ts new file mode 100644 index 000000000000..bda15b12692f --- /dev/null +++ b/packages/core/src/types-hoist/ourlogs.ts @@ -0,0 +1,67 @@ +import type { SeverityLevel } from './severity'; + +export type LogSeverityLevel = SeverityLevel | 'critical' | 'trace'; + +export type LogAttributeValueType = { + stringValue: string +} | { + intValue: number +} | { + boolValue: boolean +} | { + doubleValue: number +} + +export type LogAttribute = { + key: string, + value: LogAttributeValueType +}; + +export interface Log { + /** + * Allowed values are, from highest to lowest: + * `critical`, `fatal`, `error`, `warning`, `info`, `debug`, `trace`. + * + * The log level changes how logs are filtered and displayed. + * Critical level logs are emphasized more than trace level logs. + * + * @summary The severity level of the log. + */ + severityText?: LogSeverityLevel; + + /** + * The severity number - generally higher severity are levels like 'error' and lower are levels like 'debug' + */ + severityNumber?: number; + + /** + * OTEL trace flags (bitmap) - currently 1 means sampled, 0 means unsampled - for sentry always set to 0 + */ + traceFlags?: number; + + /** + * The trace ID for this log + */ + traceId?: string; + + /** + * The message to be logged - for example, 'hello world' would become a log like '[INFO] hello world' + */ + body: { + stringValue: string, + }; + + /** + * Arbitrary structured data that stores information about the log - e.g., userId: 100. + */ + attributes?: LogAttribute[]; + + /** + * This doesn't have to be explicitly specified most of the time. If you need to set it, the value + * is the number of seconds since midnight on January 1, 1970 ("unix epoch time") + * + * @summary A timestamp representing when the log occurred. + * @link https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#:~:text=is%20info.-,timestamp,-(recommended) + */ + timeUnixNano?: string; +} diff --git a/packages/core/src/utils-hoist/envelope.ts b/packages/core/src/utils-hoist/envelope.ts index 46512850cefc..9655f9312579 100644 --- a/packages/core/src/utils-hoist/envelope.ts +++ b/packages/core/src/utils-hoist/envelope.ts @@ -223,6 +223,7 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = { feedback: 'feedback', span: 'span', raw_security: 'security', + otel_log: 'log_item', }; /** From 4281cbeeb1f1e837f4fe6d4bfe19f1db36120bf8 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Fri, 14 Feb 2025 11:48:32 -0500 Subject: [PATCH 02/21] improve a bit --- packages/core/src/ourlogs.ts | 89 +++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/packages/core/src/ourlogs.ts b/packages/core/src/ourlogs.ts index 42066ea61ec4..82fd6697a7bb 100644 --- a/packages/core/src/ourlogs.ts +++ b/packages/core/src/ourlogs.ts @@ -55,57 +55,60 @@ function addLog(log: Log): void { client.sendEnvelope(envelope).then(null, ex => console.error(ex)); } +function valueToAttribute(key: string, value: unknown): LogAttribute { + if (typeof value === 'number') { + if(Number.isInteger(value)) { + return { + key, + value: { + intValue: value + } + } + } + return { + key, + value: { + doubleValue: value + } + } + } else if (typeof value === 'boolean') { + return { + key, + value: { + boolValue: value + } + } + } else if (typeof value === 'string') { + return { + key, + value: { + stringValue: value + } + } + } else { + return { + key, + value: { + stringValue: JSON.stringify(value) + } + } + } +} + /** - * A utility function to be able to create methods like Sentry.info(...) + * A utility function to be able to create methods like Sentry.info`...` * * 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 template, Sentry.info`hello ${world}` */ -export function captureLog(level: LogSeverityLevel, strings: string[], ...values: unknown[]): void { +export function captureLog(level: LogSeverityLevel, messages: string[] | string, ...values: unknown[]): void { + const message = Array.isArray(messages) ? messages.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '') : messages; + addLog({ severityText: level, body: { - stringValue: strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '' ), + stringValue: message, }, - attributes: values.map((value, index) => { - const key = `param${index}`; - if (typeof value === 'number') { - if(Number.isInteger(value)) { - return { - key, - value: { - intValue: value - } - } - } - return { - key, - value: { - doubleValue: value - } - } - } else if (typeof value === 'boolean') { - return { - key, - value: { - boolValue: value - } - } - } else if (typeof value === 'string') { - return { - key, - value: { - stringValue: value - } - } - } else { - return { - key, - value: { - stringValue: JSON.stringify(value) - } - } - } - }, {}) + attributes: values.map((value, index) => valueToAttribute(`param${index}`, value)), }) } From 1869d6fcdcd402b1890a81175bfdaed643381ef3 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Tue, 18 Feb 2025 15:12:13 -0500 Subject: [PATCH 03/21] linting, etc --- packages/core/src/exports.ts | 3 +- packages/core/src/index.ts | 2 +- packages/core/src/ourlogs.ts | 72 +++++++++++++---------- packages/core/src/types-hoist/envelope.ts | 2 +- packages/core/src/types-hoist/index.ts | 2 +- packages/core/src/types-hoist/ourlogs.ts | 28 +++++---- 6 files changed, 62 insertions(+), 47 deletions(-) diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index d1a15479a760..8470d9445b25 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 } from './ourlogs'; import type { CaptureContext } from './scope'; import { closeSession, makeSession, updateSession } from './session'; import type { @@ -24,7 +25,6 @@ import { timestampInSeconds } from './utils-hoist/time'; import { GLOBAL_OBJ } from './utils-hoist/worldwide'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; -import { captureLog } from './ourlogs'; /** * Captures an exception event and sends it to Sentry. @@ -336,7 +336,6 @@ export function captureSession(end: boolean = false): void { _sendSessionUpdate(); } - /** * A utility to record a log with level 'INFO' and send it to sentry. * diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8e40617e2743..3fb120a41c5a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -31,7 +31,7 @@ export { addEventProcessor, _experimentalLogError, _experimentalLogInfo, - _experimentalLogWarning + _experimentalLogWarning, } from './exports'; export { getCurrentScope, diff --git a/packages/core/src/ourlogs.ts b/packages/core/src/ourlogs.ts index 82fd6697a7bb..d3c6fa8c8bd5 100644 --- a/packages/core/src/ourlogs.ts +++ b/packages/core/src/ourlogs.ts @@ -29,9 +29,9 @@ function addLog(log: Log): void { return; } - // if (!client.getOptions()._experiments?.logSupport) { - // return; - // } + if (!client.getOptions()._experiments?.logSupport) { + return; + } const globalScope = getGlobalScope(); const dsn = client.getDsn(); @@ -41,57 +41,59 @@ function addLog(log: Log): void { trace_id: globalScope.getPropagationContext().traceId, public_key: dsn?.publicKey, }, - ...(dsn ? {dsn: dsnToString(dsn)} : {}), - } - if(!log.traceId) { + ...(dsn ? { dsn: dsnToString(dsn) } : {}), + }; + if (!log.traceId) { log.traceId = globalScope.getPropagationContext().traceId || '00000000-0000-0000-0000-000000000000'; } - if(!log.timeUnixNano) { - log.timeUnixNano = `${(new Date()).getTime().toString()}000000`; + if (!log.timeUnixNano) { + log.timeUnixNano = `${new Date().getTime().toString()}000000`; } const envelope = createEnvelope(headers, [createLogEnvelopeItem(log)]); - client.sendEnvelope(envelope).then(null, ex => console.error(ex)); + // sendEnvelope should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + client.sendEnvelope(envelope); } function valueToAttribute(key: string, value: unknown): LogAttribute { if (typeof value === 'number') { - if(Number.isInteger(value)) { + if (Number.isInteger(value)) { return { key, value: { - intValue: value - } - } + intValue: value, + }, + }; } return { key, value: { - doubleValue: value - } - } + doubleValue: value, + }, + }; } else if (typeof value === 'boolean') { return { key, value: { - boolValue: value - } - } + boolValue: value, + }, + }; } else if (typeof value === 'string') { return { key, value: { - stringValue: value - } - } + stringValue: value, + }, + }; } else { return { key, value: { - stringValue: JSON.stringify(value) - } - } + stringValue: JSON.stringify(value), + }, + }; } } @@ -99,16 +101,26 @@ function valueToAttribute(key: string, value: unknown): LogAttribute { * A utility function to be able to create methods like Sentry.info`...` * * 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 template, Sentry.info`hello ${world}` + * The other parameters are in the format to be passed a tagged template, Sentry.info`hello ${world}` */ export function captureLog(level: LogSeverityLevel, messages: string[] | string, ...values: unknown[]): void { - const message = Array.isArray(messages) ? messages.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '') : messages; - + const message = Array.isArray(messages) + ? messages.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '') + : messages; + const attributes = values.map((value, index) => valueToAttribute(`param${index}`, value)); + if (Array.isArray(messages)) { + attributes.push({ + key: 'sentry.template', + value: { + stringValue: messages.map((s, i) => s + (i < messages.length - 1 ? `$param${i}` : '')).join(''), + }, + }); + } addLog({ severityText: level, body: { stringValue: message, }, - attributes: values.map((value, index) => valueToAttribute(`param${index}`, value)), - }) + attributes: attributes, + }); } diff --git a/packages/core/src/types-hoist/envelope.ts b/packages/core/src/types-hoist/envelope.ts index d2486f94d5e0..b219e78f90c2 100644 --- a/packages/core/src/types-hoist/envelope.ts +++ b/packages/core/src/types-hoist/envelope.ts @@ -5,12 +5,12 @@ import type { LegacyCSPReport } from './csp'; import type { DsnComponents } from './dsn'; import type { Event } from './event'; import type { FeedbackEvent, UserFeedback } from './feedback'; +import type { Log } from './ourlogs'; import type { Profile, ProfileChunk } from './profiling'; import type { ReplayEvent, ReplayRecordingData } from './replay'; import type { SdkInfo } from './sdkinfo'; import type { SerializedSession, SessionAggregates } from './session'; import type { SpanJSON } from './span'; -import { Log } from './ourlogs'; // Based on: https://develop.sentry.dev/sdk/envelopes/ diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index 469f896ae7fe..66ec0647d182 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -117,7 +117,7 @@ export type { Log, LogAttribute, LogSeverityLevel, - LogAttributeValueType + LogAttributeValueType, } from './ourlogs'; export type { SpanStatus } from './spanStatus'; export type { TimedEvent } from './timedEvent'; diff --git a/packages/core/src/types-hoist/ourlogs.ts b/packages/core/src/types-hoist/ourlogs.ts index bda15b12692f..5324aa683d9e 100644 --- a/packages/core/src/types-hoist/ourlogs.ts +++ b/packages/core/src/types-hoist/ourlogs.ts @@ -2,19 +2,23 @@ import type { SeverityLevel } from './severity'; export type LogSeverityLevel = SeverityLevel | 'critical' | 'trace'; -export type LogAttributeValueType = { - stringValue: string -} | { - intValue: number -} | { - boolValue: boolean -} | { - doubleValue: number -} +export type LogAttributeValueType = + | { + stringValue: string; + } + | { + intValue: number; + } + | { + boolValue: boolean; + } + | { + doubleValue: number; + }; export type LogAttribute = { - key: string, - value: LogAttributeValueType + key: string; + value: LogAttributeValueType; }; export interface Log { @@ -48,7 +52,7 @@ export interface Log { * The message to be logged - for example, 'hello world' would become a log like '[INFO] hello world' */ body: { - stringValue: string, + stringValue: string; }; /** From 4b894862cebed897af3d5632e3baa8451c8860f3 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 19 Feb 2025 20:55:32 -0500 Subject: [PATCH 04/21] fix: Rename ourlog -> log --- packages/core/src/exports.ts | 2 +- packages/core/src/{ourlogs.ts => log.ts} | 2 +- packages/core/src/types-hoist/envelope.ts | 2 +- packages/core/src/types-hoist/index.ts | 2 +- packages/core/src/types-hoist/{ourlogs.ts => log.ts} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename packages/core/src/{ourlogs.ts => log.ts} (99%) rename packages/core/src/types-hoist/{ourlogs.ts => log.ts} (100%) diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 8470d9445b25..5c51a90a6d40 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -1,6 +1,6 @@ import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; -import { captureLog } from './ourlogs'; +import { captureLog } from './log'; import type { CaptureContext } from './scope'; import { closeSession, makeSession, updateSession } from './session'; import type { diff --git a/packages/core/src/ourlogs.ts b/packages/core/src/log.ts similarity index 99% rename from packages/core/src/ourlogs.ts rename to packages/core/src/log.ts index d3c6fa8c8bd5..db27eb1458b8 100644 --- a/packages/core/src/ourlogs.ts +++ b/packages/core/src/log.ts @@ -1,6 +1,6 @@ import { getClient, getGlobalScope } from './currentScopes'; import type { LogEnvelope, LogItem } from './types-hoist/envelope'; -import type { Log, LogAttribute, LogSeverityLevel } from './types-hoist/ourlogs'; +import type { Log, LogAttribute, LogSeverityLevel } from './types-hoist/log'; import { createEnvelope, dsnToString } from './utils-hoist'; /** diff --git a/packages/core/src/types-hoist/envelope.ts b/packages/core/src/types-hoist/envelope.ts index b219e78f90c2..d78cccc8384a 100644 --- a/packages/core/src/types-hoist/envelope.ts +++ b/packages/core/src/types-hoist/envelope.ts @@ -5,7 +5,7 @@ import type { LegacyCSPReport } from './csp'; import type { DsnComponents } from './dsn'; import type { Event } from './event'; import type { FeedbackEvent, UserFeedback } from './feedback'; -import type { Log } from './ourlogs'; +import type { Log } from './log'; import type { Profile, ProfileChunk } from './profiling'; import type { ReplayEvent, ReplayRecordingData } from './replay'; import type { SdkInfo } from './sdkinfo'; diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index 66ec0647d182..57bacb75c7d2 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -118,7 +118,7 @@ export type { LogAttribute, LogSeverityLevel, LogAttributeValueType, -} from './ourlogs'; +} from './log'; export type { SpanStatus } from './spanStatus'; export type { TimedEvent } from './timedEvent'; export type { StackFrame } from './stackframe'; diff --git a/packages/core/src/types-hoist/ourlogs.ts b/packages/core/src/types-hoist/log.ts similarity index 100% rename from packages/core/src/types-hoist/ourlogs.ts rename to packages/core/src/types-hoist/log.ts From 2fe0e1b19b4063b13fc6de020372fc97f3105f16 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 19 Feb 2025 20:56:21 -0500 Subject: [PATCH 05/21] ref: Namespace logging methods in experiment obj --- packages/browser/src/index.ts | 4 +--- packages/core/src/exports.ts | 45 +++++++++++++++++++---------------- packages/core/src/index.ts | 4 +--- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 4449b637a34b..78ddf94246ef 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -10,9 +10,7 @@ export { extraErrorDataIntegration, rewriteFramesIntegration, captureFeedback, - _experimentalLogError, - _experimentalLogInfo, - _experimentalLogWarning, + _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 5c51a90a6d40..f04d4289b53f 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -337,25 +337,28 @@ export function captureSession(end: boolean = false): void { } /** - * 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.logInfo`user ${username} just bought ${item}!` - */ -export const _experimentalLogInfo = captureLog.bind(null, 'info'); - -/** - * 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.logError`user ${username} just bought ${item}!` - */ -export const _experimentalLogError = captureLog.bind(null, 'error'); - -/** - * A utility to record a log with level 'WARNING' and send it to sentry. - * - * Logs represent a message and some parameters which provide context for a trace or error. - * Ex: sentry.logWarning`user ${username} just bought ${item}!` + * A namespace for experimental logging functions. */ -export const _experimentalLogWarning = captureLog.bind(null, 'warning'); +export const _experiment_log = { + /** + * 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.logInfo`user ${username} just bought ${item}!` + */ + info: captureLog.bind(null, 'info'), + /** + * 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.logError`user ${username} just bought ${item}!` + */ + error: captureLog.bind(null, 'error'), + /** + * A utility to record a log with level 'WARNING' and send it to sentry. + * + * Logs represent a message and some parameters which provide context for a trace or error. + * Ex: sentry.logWarning`user ${username} just bought ${item}!` + */ + warning: captureLog.bind(null, 'warning'), +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3fb120a41c5a..f8ac69ed0b97 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,9 +29,7 @@ export { endSession, captureSession, addEventProcessor, - _experimentalLogError, - _experimentalLogInfo, - _experimentalLogWarning, + _experiment_log, } from './exports'; export { getCurrentScope, From 21b16e8d45a25c622bde171df1b335220f933280 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 19 Feb 2025 21:03:54 -0500 Subject: [PATCH 06/21] ref: Improve log creation logic and update option name --- packages/core/src/log.ts | 77 ++++++++++-------------- packages/core/src/types-hoist/options.ts | 5 ++ 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index db27eb1458b8..c8a26cae8542 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -1,7 +1,9 @@ -import { getClient, getGlobalScope } from './currentScopes'; -import type { LogEnvelope, LogItem } from './types-hoist/envelope'; +import { getClient, getCurrentScope } from './currentScopes'; +import { DEBUG_BUILD } from './debug-build'; +import { getDynamicSamplingContextFromScope } from './tracing'; +import type { DynamicSamplingContext, LogEnvelope, LogItem } from './types-hoist/envelope'; import type { Log, LogAttribute, LogSeverityLevel } from './types-hoist/log'; -import { createEnvelope, dsnToString } from './utils-hoist'; +import { createEnvelope, dropUndefinedKeys, dsnToString, logger } from './utils-hoist'; /** * Creates envelope item for a single log @@ -26,25 +28,26 @@ function addLog(log: Log): void { const client = getClient(); if (!client) { + DEBUG_BUILD && logger.warn('No client available, log will not be captured.'); return; } - if (!client.getOptions()._experiments?.logSupport) { + if (!client.getOptions()._experiments?.enableLogs) { + DEBUG_BUILD && logger.warn('logging option not enabled, log will not be captured.'); return; } - const globalScope = getGlobalScope(); + const scope = getCurrentScope(); + const dsc = getDynamicSamplingContextFromScope(client, scope); + const dsn = client.getDsn(); const headers: LogEnvelope[0] = { - trace: { - trace_id: globalScope.getPropagationContext().traceId, - public_key: dsn?.publicKey, - }, + trace: dropUndefinedKeys(dsc) as DynamicSamplingContext, ...(dsn ? { dsn: dsnToString(dsn) } : {}), }; if (!log.traceId) { - log.traceId = globalScope.getPropagationContext().traceId || '00000000-0000-0000-0000-000000000000'; + log.traceId = dsc.trace_id; } if (!log.timeUnixNano) { log.timeUnixNano = `${new Date().getTime().toString()}000000`; @@ -52,48 +55,32 @@ function addLog(log: Log): void { const envelope = createEnvelope(headers, [createLogEnvelopeItem(log)]); - // sendEnvelope should not throw // eslint-disable-next-line @typescript-eslint/no-floating-promises - client.sendEnvelope(envelope); + void client.sendEnvelope(envelope); } function valueToAttribute(key: string, value: unknown): LogAttribute { - if (typeof value === 'number') { - if (Number.isInteger(value)) { + switch (typeof value) { + case 'number': return { key, - value: { - intValue: value, - }, + 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) }, }; - } - return { - key, - value: { - doubleValue: value, - }, - }; - } else if (typeof value === 'boolean') { - return { - key, - value: { - boolValue: value, - }, - }; - } else if (typeof value === 'string') { - return { - key, - value: { - stringValue: value, - }, - }; - } else { - return { - key, - value: { - stringValue: JSON.stringify(value), - }, - }; } } 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 Date: Wed, 19 Feb 2025 21:26:18 -0500 Subject: [PATCH 07/21] allow for multiple logs to be flushed in the same envelope --- packages/core/src/exports.ts | 10 ++--- packages/core/src/log.ts | 82 ++++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index f04d4289b53f..3e4136788689 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -344,21 +344,21 @@ export const _experiment_log = { * 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.logInfo`user ${username} just bought ${item}!` + * Ex: Sentry._experiment_log.info`user ${username} just bought ${item}!` */ info: captureLog.bind(null, 'info'), /** * 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.logError`user ${username} just bought ${item}!` + * Ex: Sentry._experiment_log.error`user ${username} just bought ${item}!` */ error: captureLog.bind(null, 'error'), /** - * A utility to record a log with level 'WARNING' and send it to sentry. + * 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.logWarning`user ${username} just bought ${item}!` + * Ex: Sentry._experiment_log.warn`user ${username} just bought ${item}!` */ - warning: captureLog.bind(null, 'warning'), + warn: captureLog.bind(null, 'warn'), }; diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index c8a26cae8542..c1a956a384d9 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -1,5 +1,7 @@ +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'; @@ -24,20 +26,7 @@ export function createLogEnvelopeItem(log: Log): LogItem { * * @params log - the log object which will be sent */ -function addLog(log: Log): 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 scope = getCurrentScope(); +function createLogEnvelope(logs: Log[], client: Client, scope: Scope): LogEnvelope { const dsc = getDynamicSamplingContextFromScope(client, scope); const dsn = client.getDsn(); @@ -46,17 +35,8 @@ function addLog(log: Log): void { trace: dropUndefinedKeys(dsc) as DynamicSamplingContext, ...(dsn ? { dsn: dsnToString(dsn) } : {}), }; - if (!log.traceId) { - log.traceId = dsc.trace_id; - } - if (!log.timeUnixNano) { - log.timeUnixNano = `${new Date().getTime().toString()}000000`; - } - - const envelope = createEnvelope(headers, [createLogEnvelopeItem(log)]); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - void client.sendEnvelope(envelope); + return createEnvelope(headers, logs.map(createLogEnvelopeItem)); } function valueToAttribute(key: string, value: unknown): LogAttribute { @@ -84,6 +64,37 @@ function valueToAttribute(key: string, value: unknown): LogAttribute { } } +let GLOBAL_LOG_BUFFER: Log[] = []; + +let isFlushingLogs = false; + +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 >= 100) { + 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`...` * @@ -91,6 +102,18 @@ function valueToAttribute(key: string, value: unknown): LogAttribute { * The other parameters are in the format to be passed a tagged template, Sentry.info`hello ${world}` */ export function captureLog(level: LogSeverityLevel, messages: string[] | string, ...values: unknown[]): 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 message = Array.isArray(messages) ? messages.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '') : messages; @@ -103,11 +126,18 @@ export function captureLog(level: LogSeverityLevel, messages: string[] | string, }, }); } - addLog({ + + const scope = getCurrentScope(); + + const log: Log = { severityText: level, body: { stringValue: message, }, attributes: attributes, - }); + timeUnixNano: `${new Date().getTime().toString()}000000`, + traceId: scope.getPropagationContext().traceId, + }; + + addToLogBuffer(client, log, scope); } From 8fc4ae2a1f4a91d4ba8f3aefa5879055e2663399 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Feb 2025 09:26:48 -0500 Subject: [PATCH 08/21] feat: Add release and environment to logs --- packages/core/src/log.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index c1a956a384d9..91a9d551537c 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -127,6 +127,26 @@ export function captureLog(level: LogSeverityLevel, messages: string[] | string, }); } + const { release, environment } = client.getOptions(); + + if (release) { + attributes.push({ + key: 'sentry.release', + value: { + stringValue: release, + }, + }); + } + + if (environment) { + attributes.push({ + key: 'sentry.environment', + value: { + stringValue: environment, + }, + }); + } + const scope = getCurrentScope(); const log: Log = { From f3adcde33a25af5b5607a9f65817ba44aaa9cc6d Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Feb 2025 09:29:03 -0500 Subject: [PATCH 09/21] ref: Change log buffer max length from 100 -> 25 --- packages/core/src/log.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index 91a9d551537c..cc98dab8879b 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -64,6 +64,8 @@ function valueToAttribute(key: string, value: unknown): LogAttribute { } } +const LOG_BUFFER_MAX_LENGTH = 25; + let GLOBAL_LOG_BUFFER: Log[] = []; let isFlushingLogs = false; @@ -75,7 +77,7 @@ function addToLogBuffer(client: Client, log: Log, scope: Scope): void { void client.sendEnvelope(envelope); } - if (GLOBAL_LOG_BUFFER.length >= 100) { + if (GLOBAL_LOG_BUFFER.length >= LOG_BUFFER_MAX_LENGTH) { sendLogs(GLOBAL_LOG_BUFFER); GLOBAL_LOG_BUFFER = []; } else { From bbc170da095484e224403c6a8953a454008b30dc Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Feb 2025 12:01:54 -0500 Subject: [PATCH 10/21] feat: Send severityNumber --- packages/core/src/log.ts | 25 +++++++++++++++++++------ packages/core/src/types-hoist/log.ts | 4 +--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index cc98dab8879b..81d3f4bf7ca2 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -7,6 +7,20 @@ import type { DynamicSamplingContext, LogEnvelope, LogItem } from './types-hoist 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> = { + debug: 10, + info: 20, + warning: 30, + error: 40, + critical: 50, +}; + /** * Creates envelope item for a single log */ @@ -64,12 +78,6 @@ function valueToAttribute(key: string, value: unknown): LogAttribute { } } -const LOG_BUFFER_MAX_LENGTH = 25; - -let GLOBAL_LOG_BUFFER: Log[] = []; - -let isFlushingLogs = false; - function addToLogBuffer(client: Client, log: Log, scope: Scope): void { function sendLogs(flushedLogs: Log[]): void { const envelope = createLogEnvelope(flushedLogs, client, scope); @@ -161,5 +169,10 @@ export function captureLog(level: LogSeverityLevel, messages: string[] | string, traceId: scope.getPropagationContext().traceId, }; + const maybeSeverityNumber = SEVERITY_TEXT_TO_SEVERITY_NUMBER[level]; + if (maybeSeverityNumber !== undefined) { + log.severityNumber = maybeSeverityNumber; + } + addToLogBuffer(client, log, scope); } diff --git a/packages/core/src/types-hoist/log.ts b/packages/core/src/types-hoist/log.ts index 5324aa683d9e..2d890ca11982 100644 --- a/packages/core/src/types-hoist/log.ts +++ b/packages/core/src/types-hoist/log.ts @@ -1,6 +1,4 @@ -import type { SeverityLevel } from './severity'; - -export type LogSeverityLevel = SeverityLevel | 'critical' | 'trace'; +export type LogSeverityLevel = 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal' | 'critical'; export type LogAttributeValueType = | { From bab4c6ec751ac15b877c5a88903bce9968989d74 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Feb 2025 12:47:54 -0500 Subject: [PATCH 11/21] ref: re-org functions and exports --- packages/core/src/exports.ts | 40 +++++++++++++-- packages/core/src/log.ts | 77 ++++++++++++++++------------ packages/core/src/types-hoist/log.ts | 4 +- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 3e4136788689..a71de1dbb2cc 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -1,6 +1,6 @@ import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; -import { captureLog } from './log'; +import { captureLog, sendLog } from './log'; import type { CaptureContext } from './scope'; import { closeSession, makeSession, updateSession } from './session'; import type { @@ -11,6 +11,7 @@ import type { Extra, Extras, FinishedCheckIn, + LogSeverityLevel, MonitorConfig, Primitive, Session, @@ -336,29 +337,60 @@ export function captureSession(end: boolean = false): void { _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: captureLog.bind(null, 'info'), + info: sendLog.bind(null, 'info') 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: captureLog.bind(null, 'error'), + 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: captureLog.bind(null, 'warn'), + 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/log.ts b/packages/core/src/log.ts index 81d3f4bf7ca2..2057e5c91d74 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -14,11 +14,12 @@ let GLOBAL_LOG_BUFFER: Log[] = []; let isFlushingLogs = false; const SEVERITY_TEXT_TO_SEVERITY_NUMBER: Partial> = { - debug: 10, - info: 20, - warning: 30, - error: 40, - critical: 50, + trace: 1, + debug: 5, + info: 9, + warn: 13, + error: 17, + fatal: 21, }; /** @@ -106,12 +107,36 @@ function addToLogBuffer(client: Client, log: Log, scope: Scope): void { } /** - * A utility function to be able to create methods like Sentry.info`...` + * 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 captureLog(level: LogSeverityLevel, messages: string[] | string, ...values: unknown[]): void { +export function sendLog(level: LogSeverityLevel, messageArr: TemplateStringsArray, ...values: unknown[]): void { + const message = messageArr.reduce((acc, str, i) => acc + str + (values[i] ?? ''), ''); + + const attributes = values.reduce>( + (acc, value, index) => { + acc[`param${index}`] = value; + return acc; + }, + { + 'sentry.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) { @@ -124,53 +149,37 @@ export function captureLog(level: LogSeverityLevel, messages: string[] | string, return; } - const message = Array.isArray(messages) - ? messages.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '') - : messages; - const attributes = values.map((value, index) => valueToAttribute(`param${index}`, value)); - if (Array.isArray(messages)) { - attributes.push({ - key: 'sentry.template', - value: { - stringValue: messages.map((s, i) => s + (i < messages.length - 1 ? `$param${i}` : '')).join(''), - }, - }); - } - const { release, environment } = client.getOptions(); + const logAttributes = { + ...customAttributes, + }; + if (release) { - attributes.push({ - key: 'sentry.release', - value: { - stringValue: release, - }, - }); + logAttributes['sentry.release'] = release; } if (environment) { - attributes.push({ - key: 'sentry.environment', - value: { - stringValue: 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: attributes, + attributes, timeUnixNano: `${new Date().getTime().toString()}000000`, traceId: scope.getPropagationContext().traceId, + severityNumber, }; const maybeSeverityNumber = SEVERITY_TEXT_TO_SEVERITY_NUMBER[level]; - if (maybeSeverityNumber !== undefined) { + if (maybeSeverityNumber !== undefined && log.severityNumber === undefined) { log.severityNumber = maybeSeverityNumber; } diff --git a/packages/core/src/types-hoist/log.ts b/packages/core/src/types-hoist/log.ts index 2d890ca11982..a4ca06133a2c 100644 --- a/packages/core/src/types-hoist/log.ts +++ b/packages/core/src/types-hoist/log.ts @@ -1,4 +1,4 @@ -export type LogSeverityLevel = 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'fatal' | 'critical'; +export type LogSeverityLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'critical'; export type LogAttributeValueType = | { @@ -22,7 +22,7 @@ export type LogAttribute = { export interface Log { /** * Allowed values are, from highest to lowest: - * `critical`, `fatal`, `error`, `warning`, `info`, `debug`, `trace`. + * `critical`, `fatal`, `error`, `warn`, `info`, `debug`, `trace`. * * The log level changes how logs are filtered and displayed. * Critical level logs are emphasized more than trace level logs. From f750d08c7b300e595e093360d3c2db5d11fbbc4b Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Feb 2025 13:59:16 -0500 Subject: [PATCH 12/21] feat: Add log function --- packages/core/src/exports.ts | 7 +++++++ packages/core/src/log.ts | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index a71de1dbb2cc..c64bc1a86a48 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -366,6 +366,13 @@ export const _experiment_log = { * 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. * diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index 2057e5c91d74..56edf0702658 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -13,8 +13,9 @@ let GLOBAL_LOG_BUFFER: Log[] = []; let isFlushingLogs = false; -const SEVERITY_TEXT_TO_SEVERITY_NUMBER: Partial> = { +const SEVERITY_TEXT_TO_SEVERITY_NUMBER: Partial> = { trace: 1, + log: 2, debug: 5, info: 9, warn: 13, From 7218377cb7464c644cb2332f0b18aca721d2bd3f Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Feb 2025 14:09:11 -0500 Subject: [PATCH 13/21] fix: use correct severity number for log --- packages/core/src/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index 56edf0702658..a0202f102622 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -15,9 +15,9 @@ let isFlushingLogs = false; const SEVERITY_TEXT_TO_SEVERITY_NUMBER: Partial> = { trace: 1, - log: 2, debug: 5, info: 9, + log: 10, warn: 13, error: 17, fatal: 21, From 3e936c97d122b7b4320b6dc28ff38ba2ddbd973d Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 24 Feb 2025 13:52:50 -0500 Subject: [PATCH 14/21] chore: Bump size-limit --- .size-limit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 08adf5a80c29..c9a1163acb81 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', @@ -47,7 +47,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '75 KB', + limit: '77 KB', }, { name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags', @@ -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) { From b702cf0df1c719210bd0bdcef96fa1de99475eef Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 24 Feb 2025 14:36:54 -0500 Subject: [PATCH 15/21] chore: size limit entries that were missed --- .size-limit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index c9a1163acb81..d312dbcdac3b 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -40,7 +40,7 @@ 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)', @@ -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)', From 301a1ca64dfe70ff903a0c13b2387354d307dc07 Mon Sep 17 00:00:00 2001 From: Kev <6111995+k-fish@users.noreply.github.com> Date: Tue, 4 Mar 2025 14:12:04 -0500 Subject: [PATCH 16/21] Update packages/core/src/log.ts --- packages/core/src/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index a0202f102622..af34a86a3cc3 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -75,7 +75,7 @@ function valueToAttribute(key: string, value: unknown): LogAttribute { default: return { key, - value: { stringValue: JSON.stringify(value) }, + value: { stringValue: JSON.stringify(value) ?? "" }, }; } } From 6f3560cb5768a2ca757bf296e33e4ddce67c3e09 Mon Sep 17 00:00:00 2001 From: Kev Date: Tue, 4 Mar 2025 14:36:18 -0500 Subject: [PATCH 17/21] Use '.' for param so it renders more nicely in the log attribute tree structure --- packages/core/src/log.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index af34a86a3cc3..480ad85e9e39 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -75,7 +75,7 @@ function valueToAttribute(key: string, value: unknown): LogAttribute { default: return { key, - value: { stringValue: JSON.stringify(value) ?? "" }, + value: { stringValue: JSON.stringify(value) ?? '' }, }; } } @@ -118,11 +118,11 @@ export function sendLog(level: LogSeverityLevel, messageArr: TemplateStringsArra const attributes = values.reduce>( (acc, value, index) => { - acc[`param${index}`] = value; + acc[`param.${index}`] = value; return acc; }, { - 'sentry.template': messageArr.map((s, i) => s + (i < messageArr.length - 1 ? `$param${i}` : '')).join(''), + 'sentry.template': messageArr.map((s, i) => s + (i < messageArr.length - 1 ? `$param.${i}` : '')).join(''), }, ); From 6215ad99866b110e7f8bd2744a10d86be1c674a0 Mon Sep 17 00:00:00 2001 From: Colin Chartier Date: Tue, 4 Mar 2025 14:45:59 -0500 Subject: [PATCH 18/21] json the parameters --- packages/core/src/log.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index 480ad85e9e39..08f03425a5e0 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -114,7 +114,7 @@ function addToLogBuffer(client: Client, log: Log, scope: Scope): void { * 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 + (values[i] ?? ''), ''); + const message = messageArr.reduce((acc, str, i) => acc + str + (JSON.stringify(values[i]) ?? ''), ''); const attributes = values.reduce>( (acc, value, index) => { From 9048164c6a56714b8b5f6c90fa6c4e80c6183ecb Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 4 Mar 2025 15:05:23 -0500 Subject: [PATCH 19/21] align attributes with what was agreed --- packages/core/src/log.ts | 4 ++-- packages/eslint-config-sdk/src/base.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/log.ts b/packages/core/src/log.ts index 08f03425a5e0..8e8971e694cc 100644 --- a/packages/core/src/log.ts +++ b/packages/core/src/log.ts @@ -118,11 +118,11 @@ export function sendLog(level: LogSeverityLevel, messageArr: TemplateStringsArra const attributes = values.reduce>( (acc, value, index) => { - acc[`param.${index}`] = value; + acc[`sentry.message.parameters.${index}`] = value; return acc; }, { - 'sentry.template': messageArr.map((s, i) => s + (i < messageArr.length - 1 ? `$param.${i}` : '')).join(''), + 'sentry.message.template': messageArr.map((s, i) => s + (i < messageArr.length - 1 ? `$param.${i}` : '')).join(''), }, ); diff --git a/packages/eslint-config-sdk/src/base.js b/packages/eslint-config-sdk/src/base.js index 8c11f26dd925..0b513b7316ba 100644 --- a/packages/eslint-config-sdk/src/base.js +++ b/packages/eslint-config-sdk/src/base.js @@ -84,7 +84,7 @@ module.exports = { // Make sure all expressions are used. Turned off in tests // Must disable base rule to prevent false positives 'no-unused-expressions': 'off', - '@typescript-eslint/no-unused-expressions': ['error', { allowShortCircuit: true }], + '@typescript-eslint/no-unused-expressions': ['error', { allowShortCircuit: true, allowTaggedTemplates: true }], // Make sure Promises are handled appropriately '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: false }], From a8673b7a01bbebf9ae555ef39dcc8331ea536c32 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 4 Mar 2025 15:14:56 -0500 Subject: [PATCH 20/21] fix: Duplicate export identifier --- packages/core/src/types-hoist/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/src/types-hoist/index.ts b/packages/core/src/types-hoist/index.ts index 899a2aa62a01..95bc5fc55bd4 100644 --- a/packages/core/src/types-hoist/index.ts +++ b/packages/core/src/types-hoist/index.ts @@ -109,12 +109,6 @@ export type { SpanContextData, TraceFlag, } from './span'; -export type { - Log, - LogAttribute, - LogSeverityLevel, - LogAttributeValueType, -} from './log'; export type { SpanStatus } from './spanStatus'; export type { Log, LogAttribute, LogSeverityLevel, LogAttributeValueType } from './log'; export type { TimedEvent } from './timedEvent'; From 94b2cca346f0ca0c76f1ffb5a3355f0967bb78fc Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 4 Mar 2025 15:12:48 -0500 Subject: [PATCH 21/21] meta: CHANGELOG for 9.4.0-alpha.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) 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