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 793d8636a066e99aa8c0ed657a5921e747ebfd60 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Feb 2025 14:17:48 -0500 Subject: [PATCH 16/21] meta: CHANGELOG for 9.2.0-alpha.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bddbe3762189..0b42bdd421b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,36 @@ Work in this release was contributed by @6farer. Thank you for your contribution! +## 9.2.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.1.0 - feat(browser): Add `graphqlClientIntegration` ([#13783](https://github.com/getsentry/sentry-javascript/pull/13783)) From 43456366277b248a128d679742eb0d0d619f78a6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 20 Feb 2025 14:24:51 -0500 Subject: [PATCH 17/21] fix: format changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b42bdd421b6..703b75f58064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ Sentry.init({ _experiments: { enableLogs: true, }, -}) +}); ``` These API are exposed in the `Sentry._experiment_log` namespace. From a73d05a0bcd152feff0ff2acdf5c47ee4c7d41b6 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 24 Feb 2025 20:42:42 +0000 Subject: [PATCH 18/21] release: 9.2.0-alpha.0 --- .../browser-integration-tests/package.json | 4 ++-- .../bundle-analyzer-scenarios/package.json | 2 +- dev-packages/clear-cache-gh-action/package.json | 2 +- dev-packages/e2e-tests/package.json | 2 +- .../external-contributor-gh-action/package.json | 2 +- dev-packages/node-integration-tests/package.json | 8 ++++---- dev-packages/rollup-utils/package.json | 2 +- dev-packages/size-limit-gh-action/package.json | 2 +- dev-packages/test-utils/package.json | 4 ++-- lerna.json | 2 +- packages/angular/package.json | 6 +++--- packages/astro/package.json | 8 ++++---- packages/aws-serverless/package.json | 6 +++--- packages/browser-utils/package.json | 4 ++-- packages/browser/package.json | 14 +++++++------- packages/bun/package.json | 8 ++++---- packages/cloudflare/package.json | 4 ++-- packages/core/package.json | 2 +- packages/deno/package.json | 4 ++-- packages/ember/package.json | 6 +++--- packages/eslint-config-sdk/package.json | 6 +++--- packages/eslint-plugin-sdk/package.json | 2 +- packages/feedback/package.json | 4 ++-- packages/gatsby/package.json | 6 +++--- packages/google-cloud-serverless/package.json | 6 +++--- packages/integration-shims/package.json | 4 ++-- packages/nestjs/package.json | 6 +++--- packages/nextjs/package.json | 14 +++++++------- packages/node/package.json | 6 +++--- packages/nuxt/package.json | 12 ++++++------ packages/opentelemetry/package.json | 4 ++-- packages/profiling-node/package.json | 6 +++--- packages/react-router/package.json | 8 ++++---- packages/react/package.json | 6 +++--- packages/remix/package.json | 10 +++++----- packages/replay-canvas/package.json | 6 +++--- packages/replay-internal/package.json | 8 ++++---- packages/replay-worker/package.json | 2 +- packages/solid/package.json | 6 +++--- packages/solidstart/package.json | 10 +++++----- packages/svelte/package.json | 6 +++--- packages/sveltekit/package.json | 16 +++++++++------- packages/types/package.json | 4 ++-- packages/typescript/package.json | 2 +- packages/vercel-edge/package.json | 6 +++--- packages/vue/package.json | 6 +++--- packages/wasm/package.json | 6 +++--- 47 files changed, 136 insertions(+), 134 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 3380f6a07352..3749e5b3cb2f 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "main": "index.js", "license": "MIT", "engines": { @@ -42,7 +42,7 @@ "@babel/preset-typescript": "^7.16.7", "@playwright/test": "~1.50.0", "@sentry-internal/rrweb": "2.32.0", - "@sentry/browser": "9.1.0", + "@sentry/browser": "9.2.0-alpha.0", "axios": "1.7.7", "babel-loader": "^8.2.2", "fflate": "0.8.2", diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index 5b5f04fece4f..ee323db8239f 100644 --- a/dev-packages/bundle-analyzer-scenarios/package.json +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/bundle-analyzer-scenarios", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Scenarios to test bundle analysis with", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios", diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json index 2d485501d38e..e57e17249449 100644 --- a/dev-packages/clear-cache-gh-action/package.json +++ b/dev-packages/clear-cache-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/clear-cache-gh-action", "description": "An internal Github Action to clear GitHub caches.", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index eedbab432243..064d004b1401 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "license": "MIT", "private": true, "scripts": { diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json index 078572e6fcfe..32257aaa3b96 100644 --- a/dev-packages/external-contributor-gh-action/package.json +++ b/dev-packages/external-contributor-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/external-contributor-gh-action", "description": "An internal Github Action to add external contributors to the CHANGELOG.md file.", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index bbb7e300ecee..29612f13da7e 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "license": "MIT", "engines": { "node": ">=18" @@ -31,9 +31,9 @@ "@nestjs/common": "10.4.6", "@nestjs/core": "10.4.6", "@nestjs/platform-express": "10.4.6", - "@sentry/aws-serverless": "9.1.0", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", + "@sentry/aws-serverless": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json index c35e55fa8cb0..ac371b9d13b2 100644 --- a/dev-packages/rollup-utils/package.json +++ b/dev-packages/rollup-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/rollup-utils", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index 0a4fafb0923e..d9f8903c8b60 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/size-limit-gh-action", "description": "An internal Github Action to compare the current size of a PR against the one on develop.", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 6e84bb4946d8..cb8fa7dc3ba0 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "9.1.0", + "version": "9.2.0-alpha.0", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -45,7 +45,7 @@ }, "devDependencies": { "@playwright/test": "~1.50.0", - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "volta": { "extends": "../../package.json" diff --git a/lerna.json b/lerna.json index 20be3bfcd101..63e0350e70d6 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "npmClient": "yarn" } diff --git a/packages/angular/package.json b/packages/angular/package.json index f315baf655e4..fb13fc0b5be0 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,8 +21,8 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0", + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index 563dc6fac385..5ec0d64a8880 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", @@ -56,9 +56,9 @@ "astro": ">=3.x || >=4.0.0-beta || >=5.x" }, "dependencies": { - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", "@sentry/vite-plugin": "^2.22.6" }, "devDependencies": { diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 9b1789ef7182..f74a5d120a07 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/aws-serverless", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -68,8 +68,8 @@ "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-aws-lambda": "0.50.3", "@opentelemetry/instrumentation-aws-sdk": "0.49.1", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 8e24d7524fc5..1e3ce25f7c2e 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Browser Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser/package.json b/packages/browser/package.json index 1fc61a897f5a..f855020f65e3 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -39,14 +39,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "9.1.0", - "@sentry-internal/feedback": "9.1.0", - "@sentry-internal/replay": "9.1.0", - "@sentry-internal/replay-canvas": "9.1.0", - "@sentry/core": "9.1.0" + "@sentry-internal/browser-utils": "9.2.0-alpha.0", + "@sentry-internal/feedback": "9.2.0-alpha.0", + "@sentry-internal/replay": "9.2.0-alpha.0", + "@sentry-internal/replay-canvas": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "9.1.0", + "@sentry-internal/integration-shims": "9.2.0-alpha.0", "fake-indexeddb": "^4.0.1" }, "scripts": { diff --git a/packages/bun/package.json b/packages/bun/package.json index cab5a593caca..7562a92b5d95 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for bun", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", - "@sentry/opentelemetry": "9.1.0" + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", + "@sentry/opentelemetry": "9.2.0-alpha.0" }, "devDependencies": { "bun-types": "latest" diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 9d40d3853937..d25ce6c5b769 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Cloudflare Workers and Pages", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "optionalDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/core/package.json b/packages/core/package.json index 12f0a26d24ae..240f675214a1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", diff --git a/packages/deno/package.json b/packages/deno/package.json index a614a67b24ac..f8460e7957e8 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Deno", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", @@ -24,7 +24,7 @@ "/build" ], "dependencies": { - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "scripts": { "deno-types": "node ./scripts/download-deno-types.mjs", diff --git a/packages/ember/package.json b/packages/ember/package.json index ff7cacc66601..db58181620c1 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -32,8 +32,8 @@ "dependencies": { "@babel/core": "^7.24.4", "@embroider/macros": "^1.16.0", - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0", + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 19525dff960e..9d568512bc83 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "9.1.0", - "@sentry-internal/typescript": "9.1.0", + "@sentry-internal/eslint-plugin-sdk": "9.2.0-alpha.0", + "@sentry-internal/typescript": "9.2.0-alpha.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index b451073de5f1..6f81032edde7 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index ec6782f6f072..6a9e7edc847a 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index d0bf2943065f..d99d5dfa0134 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -45,8 +45,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.1.0", - "@sentry/react": "9.1.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/react": "9.2.0-alpha.0", "@sentry/webpack-plugin": "3.1.2" }, "peerDependencies": { diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 461031588c13..6e7af67542f1 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/google-cloud-serverless", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Google Cloud Functions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud-serverless", @@ -48,8 +48,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index 22187ca8fa96..e97f299f4b99 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -55,7 +55,7 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "engines": { "node": ">=18" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 8747d32d8ead..5391f510499e 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for NestJS", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs", @@ -49,8 +49,8 @@ "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/instrumentation-nestjs-core": "0.44.1", "@opentelemetry/semantic-conventions": "^1.30.0", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0" + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0" }, "devDependencies": { "@nestjs/common": "^10.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 58a2ebd1ed3b..2e15fa1ed9a3 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -79,12 +79,12 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.30.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry-internal/browser-utils": "9.1.0", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", - "@sentry/opentelemetry": "9.1.0", - "@sentry/react": "9.1.0", - "@sentry/vercel-edge": "9.1.0", + "@sentry-internal/browser-utils": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", + "@sentry/opentelemetry": "9.2.0-alpha.0", + "@sentry/react": "9.2.0-alpha.0", + "@sentry/vercel-edge": "9.2.0-alpha.0", "@sentry/webpack-plugin": "3.1.2", "chalk": "3.0.0", "resolve": "1.22.8", diff --git a/packages/node/package.json b/packages/node/package.json index b0ebe6278cdb..1d922475e8fb 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -96,8 +96,8 @@ "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.30.0", "@prisma/instrumentation": "6.2.1", - "@sentry/core": "9.1.0", - "@sentry/opentelemetry": "9.1.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/opentelemetry": "9.2.0-alpha.0", "import-in-the-middle": "^1.13.0" }, "devDependencies": { diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 22671e5e99f5..a68f29e07125 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Nuxt (EXPERIMENTAL)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt", @@ -43,13 +43,13 @@ }, "dependencies": { "@nuxt/kit": "^3.13.2", - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", - "@sentry/opentelemetry": "9.1.0", + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", + "@sentry/opentelemetry": "9.2.0-alpha.0", "@sentry/rollup-plugin": "3.1.2", "@sentry/vite-plugin": "2.22.6", - "@sentry/vue": "9.1.0" + "@sentry/vue": "9.2.0-alpha.0" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 397ceec96960..edccf0e0f5bf 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry utilities for OpenTelemetry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 4bc692a18d45..112495ead1a5 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/profiling-node", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Node.js Profiling", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", @@ -63,8 +63,8 @@ }, "dependencies": { "@sentry-internal/node-cpu-profiler": "^2.0.0", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0" + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/react-router/package.json b/packages/react-router/package.json index f13709ecccf8..7f80dd15b8e0 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react-router", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for React Router (Framework)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react-router", @@ -34,9 +34,9 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0" + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0" }, "devDependencies": { "@react-router/node": "^7.1.5", diff --git a/packages/react/package.json b/packages/react/package.json index 1e5a93996303..c67cad7fd9d2 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0", + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index 7c38b98ea854..494dc5a36b6c 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -55,10 +55,10 @@ "@opentelemetry/api": "^1.9.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.41.1", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", - "@sentry/opentelemetry": "9.1.0", - "@sentry/react": "9.1.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", + "@sentry/opentelemetry": "9.2.0-alpha.0", + "@sentry/react": "9.2.0-alpha.0", "glob": "^10.3.4", "opentelemetry-instrumentation-remix": "0.8.0", "yargs": "^17.6.0" diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index a59d60cc86c8..66450b25171c 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-canvas", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Replay canvas integration", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -68,8 +68,8 @@ "@sentry-internal/rrweb": "2.32.0" }, "dependencies": { - "@sentry-internal/replay": "9.1.0", - "@sentry/core": "9.1.0" + "@sentry-internal/replay": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0" }, "engines": { "node": ">=18" diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index bb93eafc8060..51cb3bec53a7 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -68,7 +68,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/replay-worker": "9.1.0", + "@sentry-internal/replay-worker": "9.2.0-alpha.0", "@sentry-internal/rrweb": "2.32.0", "@sentry-internal/rrweb-snapshot": "2.32.0", "fflate": "0.8.2", @@ -76,8 +76,8 @@ "jsdom-worker": "^0.2.1" }, "dependencies": { - "@sentry-internal/browser-utils": "9.1.0", - "@sentry/core": "9.1.0" + "@sentry-internal/browser-utils": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0" }, "engines": { "node": ">=18" diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index d45b7ecc2c1d..4ee06ebc069c 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Worker for @sentry-internal/replay", "main": "build/esm/index.js", "module": "build/esm/index.js", diff --git a/packages/solid/package.json b/packages/solid/package.json index f24a26a0b47a..cb7e267e7d04 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solid", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Solid", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid", @@ -44,8 +44,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0" + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0" }, "peerDependencies": { "@solidjs/router": "^0.13.4", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index a1e6d666866c..f430b87ccb2a 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solidstart", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Solid Start", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidstart", @@ -66,10 +66,10 @@ } }, "dependencies": { - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", - "@sentry/opentelemetry": "9.1.0", - "@sentry/solid": "9.1.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", + "@sentry/opentelemetry": "9.2.0-alpha.0", + "@sentry/solid": "9.2.0-alpha.0", "@sentry/vite-plugin": "2.22.6" }, "devDependencies": { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b5d0c528d0c8..bf2a70d33dd1 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0", + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", "magic-string": "^0.30.0" }, "peerDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index dbcc01675dc3..53e8f3c61fab 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -9,7 +9,9 @@ "engines": { "node": ">=18" }, - "files": ["/build"], + "files": [ + "/build" + ], "main": "build/cjs/index.server.js", "module": "build/esm/index.server.js", "browser": "build/esm/index.client.js", @@ -42,11 +44,11 @@ } }, "dependencies": { - "@sentry/cloudflare": "9.1.0", - "@sentry/core": "9.1.0", - "@sentry/node": "9.1.0", - "@sentry/opentelemetry": "9.1.0", - "@sentry/svelte": "9.1.0", + "@sentry/cloudflare": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0", + "@sentry/node": "9.2.0-alpha.0", + "@sentry/opentelemetry": "9.2.0-alpha.0", + "@sentry/svelte": "9.2.0-alpha.0", "@sentry/vite-plugin": "3.2.0", "magic-string": "0.30.7", "magicast": "0.2.8", diff --git a/packages/types/package.json b/packages/types/package.json index d20b843418b8..2d17a084e3d8 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", @@ -56,7 +56,7 @@ "yalc:publish": "yalc publish --push --sig" }, "dependencies": { - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/typescript/package.json b/packages/typescript/package.json index 57dca752a88a..8270fecb46e5 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index ba834fa206fd..e08bc776dad6 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vercel-edge", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for the Vercel Edge Runtime", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge", @@ -40,7 +40,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@sentry/core": "9.1.0" + "@sentry/core": "9.2.0-alpha.0" }, "devDependencies": { "@edge-runtime/types": "3.0.1", @@ -48,7 +48,7 @@ "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.28.0", - "@sentry/opentelemetry": "9.1.0" + "@sentry/opentelemetry": "9.2.0-alpha.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/vue/package.json b/packages/vue/package.json index fdcc3924a8f7..fd7fcf0ccc78 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0" + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0" }, "peerDependencies": { "pinia": "2.x || 3.x", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 96e88f52eaa2..f9241b2e3c1f 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "9.1.0", + "version": "9.2.0-alpha.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.1.0", - "@sentry/core": "9.1.0" + "@sentry/browser": "9.2.0-alpha.0", + "@sentry/core": "9.2.0-alpha.0" }, "scripts": { "build": "run-p build:transpile build:bundle build:types", From 850af3ae3e41c5d37e7153ebc4a8d832f56d0965 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:23:11 -0500 Subject: [PATCH 19/21] feat(feedback): Revamp of user feedback screenshot editing (#15424) Allows you to edit screenshots with highlight and hide boxes, with the ability to delete individual boxes and resize the page. This will replace the previous cropping and pen annotation. ![image](https://github.com/user-attachments/assets/9cf9dbfa-b65b-47fc-8933-bafe33687eb8) Related to https://github.com/getsentry/sentry/issues/69786 --- .../core/src/types-hoist/feedback/config.ts | 9 - packages/feedback/src/core/integration.ts | 3 - .../src/screenshot/components/Annotations.tsx | 91 ----- .../src/screenshot/components/Crop.tsx | 334 ---------------- .../src/screenshot/components/CropCorner.tsx | 38 -- .../src/screenshot/components/CropIcon.tsx | 23 -- .../src/screenshot/components/IconClose.tsx | 29 ++ .../src/screenshot/components/PenIcon.tsx | 31 -- .../components/ScreenshotEditor.tsx | 366 ++++++++++++++---- .../components/ScreenshotInput.css.ts | 103 ++--- .../src/screenshot/components/Toolbar.tsx | 35 +- yarn.lock | 7 - 12 files changed, 365 insertions(+), 704 deletions(-) delete mode 100644 packages/feedback/src/screenshot/components/Annotations.tsx delete mode 100644 packages/feedback/src/screenshot/components/Crop.tsx delete mode 100644 packages/feedback/src/screenshot/components/CropCorner.tsx delete mode 100644 packages/feedback/src/screenshot/components/CropIcon.tsx create mode 100644 packages/feedback/src/screenshot/components/IconClose.tsx delete mode 100644 packages/feedback/src/screenshot/components/PenIcon.tsx diff --git a/packages/core/src/types-hoist/feedback/config.ts b/packages/core/src/types-hoist/feedback/config.ts index d7b3d78995bb..4ec846c7d98d 100644 --- a/packages/core/src/types-hoist/feedback/config.ts +++ b/packages/core/src/types-hoist/feedback/config.ts @@ -57,15 +57,6 @@ export interface FeedbackGeneralConfiguration { name: string; }; - /** - * _experiments allows users to enable experimental or internal features. - * We don't consider such features as part of the public API and hence we don't guarantee semver for them. - * Experimental features can be added, changed or removed at any time. - * - * Default: undefined - */ - _experiments: Partial<{ annotations: boolean }>; - /** * Set an object that will be merged sent as tags data with the event. */ diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index 8b312b902258..e5f1092856f1 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -84,7 +84,6 @@ export const buildFeedbackIntegration = ({ email: 'email', name: 'username', }, - _experiments = {}, tags, styleNonce, scriptNonce, @@ -159,8 +158,6 @@ export const buildFeedbackIntegration = ({ onSubmitError, onSubmitSuccess, onFormSubmitted, - - _experiments, }; let _shadow: ShadowRoot | null = null; diff --git a/packages/feedback/src/screenshot/components/Annotations.tsx b/packages/feedback/src/screenshot/components/Annotations.tsx deleted file mode 100644 index eb897b40f166..000000000000 --- a/packages/feedback/src/screenshot/components/Annotations.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import type { VNode, h as hType } from 'preact'; -import type * as Hooks from 'preact/hooks'; -import { DOCUMENT } from '../../constants'; - -interface FactoryParams { - h: typeof hType; -} - -export default function AnnotationsFactory({ - h, // eslint-disable-line @typescript-eslint/no-unused-vars -}: FactoryParams) { - return function Annotations({ - action, - imageBuffer, - annotatingRef, - }: { - action: 'crop' | 'annotate' | ''; - imageBuffer: HTMLCanvasElement; - annotatingRef: Hooks.Ref; - }): VNode { - const onAnnotateStart = (): void => { - if (action !== 'annotate') { - return; - } - - const handleMouseMove = (moveEvent: MouseEvent): void => { - const annotateCanvas = annotatingRef.current; - if (annotateCanvas) { - const rect = annotateCanvas.getBoundingClientRect(); - const x = moveEvent.clientX - rect.x; - const y = moveEvent.clientY - rect.y; - - const ctx = annotateCanvas.getContext('2d'); - if (ctx) { - ctx.lineTo(x, y); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(x, y); - } - } - }; - - const handleMouseUp = (): void => { - const ctx = annotatingRef.current?.getContext('2d'); - if (ctx) { - ctx.beginPath(); - } - - // Add your apply annotation logic here - applyAnnotation(); - - DOCUMENT.removeEventListener('mousemove', handleMouseMove); - DOCUMENT.removeEventListener('mouseup', handleMouseUp); - }; - - DOCUMENT.addEventListener('mousemove', handleMouseMove); - DOCUMENT.addEventListener('mouseup', handleMouseUp); - }; - - const applyAnnotation = (): void => { - // Logic to apply the annotation - const imageCtx = imageBuffer.getContext('2d'); - const annotateCanvas = annotatingRef.current; - if (imageCtx && annotateCanvas) { - imageCtx.drawImage( - annotateCanvas, - 0, - 0, - annotateCanvas.width, - annotateCanvas.height, - 0, - 0, - imageBuffer.width, - imageBuffer.height, - ); - - const annotateCtx = annotateCanvas.getContext('2d'); - if (annotateCtx) { - annotateCtx.clearRect(0, 0, annotateCanvas.width, annotateCanvas.height); - } - } - }; - return ( - - ); - }; -} diff --git a/packages/feedback/src/screenshot/components/Crop.tsx b/packages/feedback/src/screenshot/components/Crop.tsx deleted file mode 100644 index e019d8c510e0..000000000000 --- a/packages/feedback/src/screenshot/components/Crop.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import type { FeedbackInternalOptions } from '@sentry/core'; -import type { VNode, h as hType } from 'preact'; -import type * as Hooks from 'preact/hooks'; -import { DOCUMENT, WINDOW } from '../../constants'; -import CropCornerFactory from './CropCorner'; - -const CROP_BUTTON_SIZE = 30; -const CROP_BUTTON_BORDER = 3; -const CROP_BUTTON_OFFSET = CROP_BUTTON_SIZE + CROP_BUTTON_BORDER; -const DPI = WINDOW.devicePixelRatio; - -interface Box { - startX: number; - startY: number; - endX: number; - endY: number; -} - -interface Rect { - x: number; - y: number; - height: number; - width: number; -} - -const constructRect = (box: Box): Rect => ({ - x: Math.min(box.startX, box.endX), - y: Math.min(box.startY, box.endY), - width: Math.abs(box.startX - box.endX), - height: Math.abs(box.startY - box.endY), -}); - -const getContainedSize = (img: HTMLCanvasElement): Rect => { - const imgClientHeight = img.clientHeight; - const imgClientWidth = img.clientWidth; - const ratio = img.width / img.height; - let width = imgClientHeight * ratio; - let height = imgClientHeight; - if (width > imgClientWidth) { - width = imgClientWidth; - height = imgClientWidth / ratio; - } - const x = (imgClientWidth - width) / 2; - const y = (imgClientHeight - height) / 2; - return { x: x, y: y, width: width, height: height }; -}; - -interface FactoryParams { - h: typeof hType; - hooks: typeof Hooks; - options: FeedbackInternalOptions; -} - -export default function CropFactory({ h, hooks, options }: FactoryParams): (props: { - action: 'crop' | 'annotate' | ''; - imageBuffer: HTMLCanvasElement; - croppingRef: Hooks.Ref; - cropContainerRef: Hooks.Ref; - croppingRect: Box; - setCroppingRect: Hooks.StateUpdater; - resize: () => void; -}) => VNode { - const CropCorner = CropCornerFactory({ h }); - return function Crop({ - action, - imageBuffer, - croppingRef, - cropContainerRef, - croppingRect, - setCroppingRect, - resize, - }: { - action: 'crop' | 'annotate' | ''; - imageBuffer: HTMLCanvasElement; - croppingRef: Hooks.Ref; - cropContainerRef: Hooks.Ref; - croppingRect: Box; - setCroppingRect: Hooks.StateUpdater; - resize: () => void; - }): VNode { - const initialPositionRef = hooks.useRef({ initialX: 0, initialY: 0 }); - - const [isResizing, setIsResizing] = hooks.useState(false); - const [confirmCrop, setConfirmCrop] = hooks.useState(false); - - hooks.useEffect(() => { - const cropper = croppingRef.current; - if (!cropper) { - return; - } - - const ctx = cropper.getContext('2d'); - if (!ctx) { - return; - } - - const imageDimensions = getContainedSize(imageBuffer); - const croppingBox = constructRect(croppingRect); - ctx.clearRect(0, 0, imageDimensions.width, imageDimensions.height); - - if (action !== 'crop') { - return; - } - - // draw gray overlay around the selection - ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; - ctx.fillRect(0, 0, imageDimensions.width, imageDimensions.height); - ctx.clearRect(croppingBox.x, croppingBox.y, croppingBox.width, croppingBox.height); - - // draw selection border - ctx.strokeStyle = '#ffffff'; - ctx.lineWidth = 3; - ctx.strokeRect(croppingBox.x + 1, croppingBox.y + 1, croppingBox.width - 2, croppingBox.height - 2); - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 1; - ctx.strokeRect(croppingBox.x + 3, croppingBox.y + 3, croppingBox.width - 6, croppingBox.height - 6); - }, [croppingRect, action]); - - // Resizing logic - const makeHandleMouseMove = hooks.useCallback((corner: string) => { - return (e: MouseEvent) => { - if (!croppingRef.current) { - return; - } - - const cropCanvas = croppingRef.current; - const cropBoundingRect = cropCanvas.getBoundingClientRect(); - const mouseX = e.clientX - cropBoundingRect.x; - const mouseY = e.clientY - cropBoundingRect.y; - - switch (corner) { - case 'top-left': - setCroppingRect(prev => ({ - ...prev, - startX: Math.min(Math.max(0, mouseX), prev.endX - CROP_BUTTON_OFFSET), - startY: Math.min(Math.max(0, mouseY), prev.endY - CROP_BUTTON_OFFSET), - })); - break; - case 'top-right': - setCroppingRect(prev => ({ - ...prev, - endX: Math.max(Math.min(mouseX, cropCanvas.width / DPI), prev.startX + CROP_BUTTON_OFFSET), - startY: Math.min(Math.max(0, mouseY), prev.endY - CROP_BUTTON_OFFSET), - })); - break; - case 'bottom-left': - setCroppingRect(prev => ({ - ...prev, - startX: Math.min(Math.max(0, mouseX), prev.endX - CROP_BUTTON_OFFSET), - endY: Math.max(Math.min(mouseY, cropCanvas.height / DPI), prev.startY + CROP_BUTTON_OFFSET), - })); - break; - case 'bottom-right': - setCroppingRect(prev => ({ - ...prev, - endX: Math.max(Math.min(mouseX, cropCanvas.width / DPI), prev.startX + CROP_BUTTON_OFFSET), - endY: Math.max(Math.min(mouseY, cropCanvas.height / DPI), prev.startY + CROP_BUTTON_OFFSET), - })); - break; - } - }; - }, []); - - // Dragging logic - const onDragStart = (e: MouseEvent): void => { - if (isResizing) { - return; - } - - initialPositionRef.current = { initialX: e.clientX, initialY: e.clientY }; - - const handleMouseMove = (moveEvent: MouseEvent): void => { - const cropCanvas = croppingRef.current; - if (!cropCanvas) { - return; - } - - const deltaX = moveEvent.clientX - initialPositionRef.current.initialX; - const deltaY = moveEvent.clientY - initialPositionRef.current.initialY; - - setCroppingRect(prev => { - const newStartX = Math.max( - 0, - Math.min(prev.startX + deltaX, cropCanvas.width / DPI - (prev.endX - prev.startX)), - ); - const newStartY = Math.max( - 0, - Math.min(prev.startY + deltaY, cropCanvas.height / DPI - (prev.endY - prev.startY)), - ); - - const newEndX = newStartX + (prev.endX - prev.startX); - const newEndY = newStartY + (prev.endY - prev.startY); - - initialPositionRef.current.initialX = moveEvent.clientX; - initialPositionRef.current.initialY = moveEvent.clientY; - - return { startX: newStartX, startY: newStartY, endX: newEndX, endY: newEndY }; - }); - }; - - const handleMouseUp = (): void => { - DOCUMENT.removeEventListener('mousemove', handleMouseMove); - DOCUMENT.removeEventListener('mouseup', handleMouseUp); - }; - - DOCUMENT.addEventListener('mousemove', handleMouseMove); - DOCUMENT.addEventListener('mouseup', handleMouseUp); - }; - - const onGrabButton = (e: Event, corner: string): void => { - setIsResizing(true); - const handleMouseMove = makeHandleMouseMove(corner); - const handleMouseUp = (): void => { - DOCUMENT.removeEventListener('mousemove', handleMouseMove); - DOCUMENT.removeEventListener('mouseup', handleMouseUp); - setConfirmCrop(true); - setIsResizing(false); - }; - - DOCUMENT.addEventListener('mouseup', handleMouseUp); - DOCUMENT.addEventListener('mousemove', handleMouseMove); - }; - - function applyCrop(): void { - const cutoutCanvas = DOCUMENT.createElement('canvas'); - const imageBox = getContainedSize(imageBuffer); - const croppingBox = constructRect(croppingRect); - cutoutCanvas.width = croppingBox.width * DPI; - cutoutCanvas.height = croppingBox.height * DPI; - - const cutoutCtx = cutoutCanvas.getContext('2d'); - if (cutoutCtx && imageBuffer) { - cutoutCtx.drawImage( - imageBuffer, - (croppingBox.x / imageBox.width) * imageBuffer.width, - (croppingBox.y / imageBox.height) * imageBuffer.height, - (croppingBox.width / imageBox.width) * imageBuffer.width, - (croppingBox.height / imageBox.height) * imageBuffer.height, - 0, - 0, - cutoutCanvas.width, - cutoutCanvas.height, - ); - } - - const ctx = imageBuffer.getContext('2d'); - if (ctx) { - ctx.clearRect(0, 0, imageBuffer.width, imageBuffer.height); - imageBuffer.width = cutoutCanvas.width; - imageBuffer.height = cutoutCanvas.height; - imageBuffer.style.width = `${croppingBox.width}px`; - imageBuffer.style.height = `${croppingBox.height}px`; - ctx.drawImage(cutoutCanvas, 0, 0); - - resize(); - } - } - - return ( -
- - {action === 'crop' && ( -
- - - - -
- )} - {action === 'crop' && ( -
- - -
- )} -
- ); - }; -} diff --git a/packages/feedback/src/screenshot/components/CropCorner.tsx b/packages/feedback/src/screenshot/components/CropCorner.tsx deleted file mode 100644 index de3b6e506e71..000000000000 --- a/packages/feedback/src/screenshot/components/CropCorner.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { VNode, h as hType } from 'preact'; - -interface FactoryParams { - h: typeof hType; -} - -export default function CropCornerFactory({ - h, // eslint-disable-line @typescript-eslint/no-unused-vars -}: FactoryParams) { - return function CropCorner({ - top, - left, - corner, - onGrabButton, - }: { - top: number; - left: number; - corner: string; - onGrabButton: (e: Event, corner: string) => void; - }): VNode { - return ( - - ); - }; -} diff --git a/packages/feedback/src/screenshot/components/CropIcon.tsx b/packages/feedback/src/screenshot/components/CropIcon.tsx deleted file mode 100644 index 091179d86004..000000000000 --- a/packages/feedback/src/screenshot/components/CropIcon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { VNode, h as hType } from 'preact'; - -interface FactoryParams { - h: typeof hType; -} - -export default function CropIconFactory({ - h, // eslint-disable-line @typescript-eslint/no-unused-vars -}: FactoryParams) { - return function CropIcon(): VNode { - return ( - - - - ); - }; -} diff --git a/packages/feedback/src/screenshot/components/IconClose.tsx b/packages/feedback/src/screenshot/components/IconClose.tsx new file mode 100644 index 000000000000..dea383a61839 --- /dev/null +++ b/packages/feedback/src/screenshot/components/IconClose.tsx @@ -0,0 +1,29 @@ +import type { VNode, h as hType } from 'preact'; + +interface FactoryParams { + h: typeof hType; +} + +export default function IconCloseFactory({ + h, // eslint-disable-line @typescript-eslint/no-unused-vars +}: FactoryParams) { + return function IconClose(): VNode { + return ( + + + + + + + ); + }; +} diff --git a/packages/feedback/src/screenshot/components/PenIcon.tsx b/packages/feedback/src/screenshot/components/PenIcon.tsx deleted file mode 100644 index 75a0faedf480..000000000000 --- a/packages/feedback/src/screenshot/components/PenIcon.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { VNode, h as hType } from 'preact'; - -interface FactoryParams { - h: typeof hType; -} - -export default function PenIconFactory({ - h, // eslint-disable-line @typescript-eslint/no-unused-vars -}: FactoryParams) { - return function PenIcon(): VNode { - return ( - - - - - - ); - }; -} diff --git a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx index 9f49abf60e6f..d6a257c056dd 100644 --- a/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx +++ b/packages/feedback/src/screenshot/components/ScreenshotEditor.tsx @@ -1,17 +1,15 @@ +/* eslint-disable max-lines */ import type { FeedbackInternalOptions, FeedbackModalIntegration } from '@sentry/core'; import type { ComponentType, VNode, h as hType } from 'preact'; -// biome-ignore lint/nursery/noUnusedImports: reason +// biome-ignore lint/nursery/noUnusedImports: need Preact import for JSX import { h } from 'preact'; // eslint-disable-line @typescript-eslint/no-unused-vars import type * as Hooks from 'preact/hooks'; -import { WINDOW } from '../../constants'; -import AnnotationsFactory from './Annotations'; -import CropFactory from './Crop'; +import { DOCUMENT, WINDOW } from '../../constants'; +import IconCloseFactory from './IconClose'; import { createScreenshotInputStyles } from './ScreenshotInput.css'; import ToolbarFactory from './Toolbar'; import { useTakeScreenshotFactory } from './useTakeScreenshot'; -const DPI = WINDOW.devicePixelRatio; - interface FactoryParams { h: typeof hType; hooks: typeof Hooks; @@ -24,6 +22,8 @@ interface Props { onError: (error: Error) => void; } +type Action = 'highlight' | 'hide'; + interface Box { startX: number; startY: number; @@ -31,17 +31,31 @@ interface Box { endY: number; } -interface Rect { +interface Dimensions { x: number; y: number; height: number; width: number; } -const getContainedSize = (img: HTMLCanvasElement): Rect => { - const imgClientHeight = img.clientHeight; - const imgClientWidth = img.clientWidth; - const ratio = img.width / img.height; +interface Rect extends Dimensions { + action: Action; +} + +const DPI = WINDOW.devicePixelRatio; + +const constructRect = (action: Action, box: Box): Rect => ({ + action, + x: Math.min(box.startX, box.endX), + y: Math.min(box.startY, box.endY), + width: Math.abs(box.startX - box.endX), + height: Math.abs(box.startY - box.endY), +}); + +const getContainedSize = (measurementDiv: HTMLDivElement, imageSource: HTMLCanvasElement): Dimensions => { + const imgClientHeight = measurementDiv.clientHeight; + const imgClientWidth = measurementDiv.clientWidth; + const ratio = imageSource.width / imageSource.height; let width = imgClientHeight * ratio; let height = imgClientHeight; if (width > imgClientWidth) { @@ -53,6 +67,53 @@ const getContainedSize = (img: HTMLCanvasElement): Rect => { return { x: x, y: y, width: width, height: height }; }; +function drawRect(rect: Rect, ctx: CanvasRenderingContext2D, color: string, scale: number = 1): void { + const scaledX = rect.x * scale; + const scaledY = rect.y * scale; + const scaledWidth = rect.width * scale; + const scaledHeight = rect.height * scale; + + switch (rect.action) { + case 'highlight': { + // creates a shadow around + ctx.shadowColor = 'rgba(0, 0, 0, 0.7)'; + ctx.shadowBlur = 50; + + // draws a rectangle first so that the shadow is visible before clearing + ctx.fillStyle = 'rgb(0, 0, 0)'; + ctx.fillRect(scaledX, scaledY, scaledWidth, scaledHeight); + ctx.clearRect(scaledX, scaledY, scaledWidth, scaledHeight); + + // Disable shadow after the action is drawn + ctx.shadowColor = 'transparent'; + ctx.shadowBlur = 0; + + ctx.strokeStyle = color; + ctx.strokeRect(scaledX + 1, scaledY + 1, scaledWidth - 2, scaledHeight - 2); + + break; + } + case 'hide': + ctx.fillStyle = 'rgb(0, 0, 0)'; + ctx.fillRect(scaledX, scaledY, scaledWidth, scaledHeight); + + break; + default: + break; + } +} + +function resizeCanvas(canvas: HTMLCanvasElement, imageDimensions: Dimensions): void { + canvas.width = imageDimensions.width * DPI; + canvas.height = imageDimensions.height * DPI; + canvas.style.width = `${imageDimensions.width}px`; + canvas.style.height = `${imageDimensions.height}px`; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.scale(DPI, DPI); + } +} + export function ScreenshotEditorFactory({ h, hooks, @@ -62,23 +123,73 @@ export function ScreenshotEditorFactory({ }: FactoryParams): ComponentType { const useTakeScreenshot = useTakeScreenshotFactory({ hooks }); const Toolbar = ToolbarFactory({ h }); - const Annotations = AnnotationsFactory({ h }); - const Crop = CropFactory({ h, hooks, options }); + const IconClose = IconCloseFactory({ h }); + const styles = { __html: createScreenshotInputStyles(options.styleNonce).innerText }; return function ScreenshotEditor({ onError }: Props): VNode { - const styles = hooks.useMemo(() => ({ __html: createScreenshotInputStyles(options.styleNonce).innerText }), []); + // Data for rendering: + const [action, setAction] = hooks.useState('highlight'); + const [drawRects, setDrawRects] = hooks.useState([]); + const [currentRect, setCurrentRect] = hooks.useState(undefined); - const canvasContainerRef = hooks.useRef(null); - const cropContainerRef = hooks.useRef(null); + // Refs to our html components: + const measurementRef = hooks.useRef(null); + const screenshotRef = hooks.useRef(null); const annotatingRef = hooks.useRef(null); - const croppingRef = hooks.useRef(null); - const [action, setAction] = hooks.useState<'annotate' | 'crop' | ''>('crop'); - const [croppingRect, setCroppingRect] = hooks.useState({ - startX: 0, - startY: 0, - endX: 0, - endY: 0, - }); + const rectContainerRef = hooks.useRef(null); + + // The canvas that contains the original screenshot + const [imageSource, setImageSource] = hooks.useState(null); + + // Hide the whole feedback widget when we take the screenshot + const [displayEditor, setDisplayEditor] = hooks.useState(true); + + // The size of our window, relative to the imageSource + const [scaleFactor, setScaleFactor] = hooks.useState(1); + + const strokeColor = hooks.useMemo((): string => { + const sentryFeedback = DOCUMENT.getElementById(options.id); + if (!sentryFeedback) { + return 'white'; + } + const computedStyle = getComputedStyle(sentryFeedback); + return ( + computedStyle.getPropertyValue('--button-primary-background') || + computedStyle.getPropertyValue('--accent-background') + ); + }, [options.id]); + + const resize = hooks.useCallback((): void => { + if (!displayEditor) { + return; + } + + const screenshotCanvas = screenshotRef.current; + const annotatingCanvas = annotatingRef.current; + const measurementDiv = measurementRef.current; + const rectContainer = rectContainerRef.current; + if (!screenshotCanvas || !annotatingCanvas || !imageSource || !measurementDiv || !rectContainer) { + return; + } + + const imageDimensions = getContainedSize(measurementDiv, imageSource); + + resizeCanvas(screenshotCanvas, imageDimensions); + resizeCanvas(annotatingCanvas, imageDimensions); + + rectContainer.style.width = `${imageDimensions.width}px`; + rectContainer.style.height = `${imageDimensions.height}px`; + + const scale = annotatingCanvas.clientWidth / imageBuffer.width; + setScaleFactor(scale); + + const screenshotContext = screenshotCanvas.getContext('2d', { alpha: false }); + if (!screenshotContext) { + return; + } + screenshotContext.drawImage(imageSource, 0, 0, imageDimensions.width, imageDimensions.height); + drawScene(); + }, [imageSource, drawRects, displayEditor]); hooks.useEffect(() => { WINDOW.addEventListener('resize', resize); @@ -86,87 +197,192 @@ export function ScreenshotEditorFactory({ return () => { WINDOW.removeEventListener('resize', resize); }; - }, []); + }, [resize]); + + hooks.useLayoutEffect(() => { + resize(); + }, [resize]); + + hooks.useEffect(() => { + drawScene(); + drawBuffer(); + }, [drawRects]); - function resizeCanvas(canvasRef: Hooks.Ref, imageDimensions: Rect): void { - const canvas = canvasRef.current; - if (!canvas) { + hooks.useEffect(() => { + if (currentRect) { + drawScene(); + } + }, [currentRect]); + + // draws the commands onto the imageBuffer, which is what's sent to Sentry + const drawBuffer = hooks.useCallback((): void => { + const ctx = imageBuffer.getContext('2d', { alpha: false }); + const measurementDiv = measurementRef.current; + if (!imageBuffer || !ctx || !imageSource || !measurementDiv) { return; } - canvas.width = imageDimensions.width * DPI; - canvas.height = imageDimensions.height * DPI; - canvas.style.width = `${imageDimensions.width}px`; - canvas.style.height = `${imageDimensions.height}px`; - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.scale(DPI, DPI); + ctx.drawImage(imageSource, 0, 0); + + const annotatingBufferBig = DOCUMENT.createElement('canvas'); + annotatingBufferBig.width = imageBuffer.width; + annotatingBufferBig.height = imageBuffer.height; + + const grayCtx = annotatingBufferBig.getContext('2d'); + if (!grayCtx) { + return; } - } - function resize(): void { - const imageDimensions = getContainedSize(imageBuffer); + // applies the graywash if there's any boxes drawn + if (drawRects.length || currentRect) { + grayCtx.fillStyle = 'rgba(0, 0, 0, 0.25)'; + grayCtx.fillRect(0, 0, imageBuffer.width, imageBuffer.height); + } - resizeCanvas(croppingRef, imageDimensions); - resizeCanvas(annotatingRef, imageDimensions); + grayCtx.lineWidth = 4; + drawRects.forEach(rect => { + drawRect(rect, grayCtx, strokeColor); + }); + ctx.drawImage(annotatingBufferBig, 0, 0); + }, [drawRects, strokeColor]); - const cropContainer = cropContainerRef.current; - if (cropContainer) { - cropContainer.style.width = `${imageDimensions.width}px`; - cropContainer.style.height = `${imageDimensions.height}px`; + const drawScene = hooks.useCallback((): void => { + const annotatingCanvas = annotatingRef.current; + if (!annotatingCanvas) { + return; } - setCroppingRect({ startX: 0, startY: 0, endX: imageDimensions.width, endY: imageDimensions.height }); - } + const ctx = annotatingCanvas.getContext('2d'); + if (!ctx) { + return; + } + + ctx.clearRect(0, 0, annotatingCanvas.width, annotatingCanvas.height); + + // applies the graywash if there's any boxes drawn + if (drawRects.length || currentRect) { + ctx.fillStyle = 'rgba(0, 0, 0, 0.25)'; + ctx.fillRect(0, 0, annotatingCanvas.width, annotatingCanvas.height); + } + + ctx.lineWidth = 2; + const scale = annotatingCanvas.clientWidth / imageBuffer.width; + drawRects.forEach(rect => { + drawRect(rect, ctx, strokeColor, scale); + }); + + if (currentRect) { + drawRect(currentRect, ctx, strokeColor); + } + }, [drawRects, currentRect, strokeColor]); useTakeScreenshot({ onBeforeScreenshot: hooks.useCallback(() => { (dialog.el as HTMLElement).style.display = 'none'; + setDisplayEditor(false); + }, []), + onScreenshot: hooks.useCallback((imageSource: HTMLVideoElement) => { + const bufferCanvas = DOCUMENT.createElement('canvas'); + bufferCanvas.width = imageSource.videoWidth; + bufferCanvas.height = imageSource.videoHeight; + bufferCanvas.getContext('2d', { alpha: false })?.drawImage(imageSource, 0, 0); + setImageSource(bufferCanvas); + + imageBuffer.width = imageSource.videoWidth; + imageBuffer.height = imageSource.videoHeight; }, []), - onScreenshot: hooks.useCallback( - (imageSource: HTMLVideoElement) => { - const context = imageBuffer.getContext('2d'); - if (!context) { - throw new Error('Could not get canvas context'); - } - imageBuffer.width = imageSource.videoWidth; - imageBuffer.height = imageSource.videoHeight; - imageBuffer.style.width = '100%'; - imageBuffer.style.height = '100%'; - context.drawImage(imageSource, 0, 0); - }, - [imageBuffer], - ), onAfterScreenshot: hooks.useCallback(() => { (dialog.el as HTMLElement).style.display = 'block'; - const container = canvasContainerRef.current; - container?.appendChild(imageBuffer); - resize(); + setDisplayEditor(true); }, []), onError: hooks.useCallback(error => { (dialog.el as HTMLElement).style.display = 'block'; + setDisplayEditor(true); onError(error); }, []), }); + const handleMouseDown = (e: MouseEvent): void => { + const annotatingCanvas = annotatingRef.current; + if (!action || !annotatingCanvas) { + return; + } + + const boundingRect = annotatingCanvas.getBoundingClientRect(); + + const startX = e.clientX - boundingRect.left; + const startY = e.clientY - boundingRect.top; + + const handleMouseMove = (e: MouseEvent): void => { + const endX = e.clientX - boundingRect.left; + const endY = e.clientY - boundingRect.top; + const rect = constructRect(action, { startX, startY, endX, endY }); + // prevent drawing when just clicking (not dragging) on the canvas + if (startX != endX && startY != endY) { + setCurrentRect(rect); + } + }; + + const handleMouseUp = (e: MouseEvent): void => { + // no rect is being drawn anymore, so setting active rect to undefined + setCurrentRect(undefined); + const endX = Math.max(0, Math.min(e.clientX - boundingRect.left, annotatingCanvas.width / DPI)); + const endY = Math.max(0, Math.min(e.clientY - boundingRect.top, annotatingCanvas.height / DPI)); + // prevent drawing a rect when just clicking (not dragging) on the canvas (ie. clicking delete) + if (startX != endX && startY != endY) { + // scale to image buffer + const scale = imageBuffer.width / annotatingCanvas.clientWidth; + const rect = constructRect(action, { + startX: startX * scale, + startY: startY * scale, + endX: endX * scale, + endY: endY * scale, + }); + setDrawRects(prev => [...prev, rect]); + } + + DOCUMENT.removeEventListener('mousemove', handleMouseMove); + DOCUMENT.removeEventListener('mouseup', handleMouseUp); + }; + + DOCUMENT.addEventListener('mousemove', handleMouseMove); + DOCUMENT.addEventListener('mouseup', handleMouseUp); + }; + + const handleDeleteRect = (index: number): void => { + const updatedRects = [...drawRects]; + updatedRects.splice(index, 1); + setDrawRects(updatedRects); + }; + return (