From e313acd4f12f2fbbe1bbc0946dba6e6a97e13191 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 12:45:42 +0200 Subject: [PATCH 01/15] feat(core): Add `ServerRuntimeClient` --- packages/core/src/eventbuilder.ts | 131 ++++++++++++++++ packages/core/src/index.ts | 1 + packages/core/src/server-runtime-client.ts | 170 +++++++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100644 packages/core/src/eventbuilder.ts create mode 100644 packages/core/src/server-runtime-client.ts diff --git a/packages/core/src/eventbuilder.ts b/packages/core/src/eventbuilder.ts new file mode 100644 index 000000000000..63d12db56a0e --- /dev/null +++ b/packages/core/src/eventbuilder.ts @@ -0,0 +1,131 @@ +import type { + Event, + EventHint, + Exception, + Mechanism, + Severity, + SeverityLevel, + StackFrame, + StackParser, +} from '@sentry/types'; +import { + addExceptionMechanism, + addExceptionTypeValue, + extractExceptionKeysForMessage, + isError, + isPlainObject, + normalizeToSize, +} from '@sentry/utils'; + +import { getCurrentHub } from './hub'; + +/** + * Extracts stack frames from the error.stack string + */ +export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { + return stackParser(error.stack || '', 1); +} + +/** + * Extracts stack frames from the error and builds a Sentry Exception + */ +export function exceptionFromError(stackParser: StackParser, error: Error): Exception { + const exception: Exception = { + type: error.name || error.constructor.name, + value: error.message, + }; + + const frames = parseStackFrames(stackParser, error); + if (frames.length) { + exception.stacktrace = { frames }; + } + + return exception; +} + +/** + * Builds and Event from a Exception + * @hidden + */ +export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { + let ex: unknown = exception; + const providedMechanism: Mechanism | undefined = + hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; + const mechanism: Mechanism = providedMechanism || { + handled: true, + type: 'generic', + }; + + if (!isError(exception)) { + if (isPlainObject(exception)) { + // This will allow us to group events based on top-level keys + // which is much better than creating new group when any key/value change + const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`; + + const hub = getCurrentHub(); + const client = hub.getClient(); + const normalizeDepth = client && client.getOptions().normalizeDepth; + hub.configureScope(scope => { + scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); + }); + + ex = (hint && hint.syntheticException) || new Error(message); + (ex as Error).message = message; + } else { + // This handles when someone does: `throw "something awesome";` + // We use synthesized Error here so we can extract a (rough) stack trace. + ex = (hint && hint.syntheticException) || new Error(exception as string); + (ex as Error).message = exception as string; + } + mechanism.synthetic = true; + } + + const event = { + exception: { + values: [exceptionFromError(stackParser, ex as Error)], + }, + }; + + addExceptionTypeValue(event, undefined, undefined); + addExceptionMechanism(event, mechanism); + + return { + ...event, + event_id: hint && hint.event_id, + }; +} + +/** + * Builds and Event from a Message + * @hidden + */ +export function eventFromMessage( + stackParser: StackParser, + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + hint?: EventHint, + attachStacktrace?: boolean, +): Event { + const event: Event = { + event_id: hint && hint.event_id, + level, + message, + }; + + if (attachStacktrace && hint && hint.syntheticException) { + const frames = parseStackFrames(stackParser, hint.syntheticException); + if (frames.length) { + event.exception = { + values: [ + { + value: message, + stacktrace: { frames }, + }, + ], + }; + } + } + + return event; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a0cc7e627bf7..e1a3484cce07 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,6 +38,7 @@ export { SessionFlusher } from './sessionflusher'; export { addGlobalEventProcessor, Scope } from './scope'; export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from './api'; export { BaseClient } from './baseclient'; +export { ServerRuntimeClient } from './server-runtime-client'; export { initAndBind } from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts new file mode 100644 index 000000000000..c5cc60223c7d --- /dev/null +++ b/packages/core/src/server-runtime-client.ts @@ -0,0 +1,170 @@ +import type { + BaseTransportOptions, + CheckIn, + ClientOptions, + DynamicSamplingContext, + Event, + EventHint, + MonitorConfig, + SerializedCheckIn, + Severity, + SeverityLevel, + TraceContext, +} from '@sentry/types'; +import { logger, uuid4 } from '@sentry/utils'; + +import { BaseClient } from './baseclient'; +import { createCheckInEnvelope } from './checkin'; +import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; +import type { Scope } from './scope'; +import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; + +export interface ServerRuntimeClientOptions extends ClientOptions { + platform?: string; + runtime?: { name: string; version?: string }; + serverName?: string; +} + +/** + * The Sentry Server Runtime Client SDK. + */ +export class ServerRuntimeClient extends BaseClient { + /** + * Creates a new Edge SDK instance. + * @param options Configuration options for this SDK. + */ + public constructor(options: ServerRuntimeClientOptions) { + // Server clients always support tracing + addTracingExtensions(); + + super(options); + } + + /** + * @inheritDoc + */ + public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { + return Promise.resolve(eventFromUnknownInput(this._options.stackParser, exception, hint)); + } + + /** + * @inheritDoc + */ + public eventFromMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level: Severity | SeverityLevel = 'info', + hint?: EventHint, + ): PromiseLike { + return Promise.resolve( + eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), + ); + } + + /** + * Create a cron monitor check in and send it to Sentry. + * + * @param checkIn An object that describes a check in. + * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want + * to create a monitor automatically when sending a check in. + */ + public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { + const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4(); + if (!this._isEnabled()) { + __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); + return id; + } + + const options = this.getOptions(); + const { release, environment, tunnel } = options; + + const serializedCheckIn: SerializedCheckIn = { + check_in_id: id, + monitor_slug: checkIn.monitorSlug, + status: checkIn.status, + release, + environment, + }; + + if (checkIn.status !== 'in_progress') { + serializedCheckIn.duration = checkIn.duration; + } + + if (monitorConfig) { + serializedCheckIn.monitor_config = { + schedule: monitorConfig.schedule, + checkin_margin: monitorConfig.checkinMargin, + max_runtime: monitorConfig.maxRuntime, + timezone: monitorConfig.timezone, + }; + } + + const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope); + if (traceContext) { + serializedCheckIn.contexts = { + trace: traceContext, + }; + } + + const envelope = createCheckInEnvelope( + serializedCheckIn, + dynamicSamplingContext, + this.getSdkMetadata(), + tunnel, + this.getDsn(), + ); + + __DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); + void this._sendEnvelope(envelope); + return id; + } + + /** + * @inheritDoc + */ + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + if (this._options.platform) { + event.platform = event.platform || this._options.platform; + } + + if (this._options.runtime) { + event.contexts = { + ...event.contexts, + runtime: (event.contexts || {}).runtime || this._options.runtime, + }; + } + + if (this._options.serverName) { + event.server_name = event.server_name || this._options.serverName; + } + + return super._prepareEvent(event, hint, scope); + } + + /** Extract trace information from scope */ + private _getTraceInfoFromScope( + scope: Scope | undefined, + ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { + if (!scope) { + return [undefined, undefined]; + } + + const span = scope.getSpan(); + if (span) { + const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined; + return [samplingContext, span.getTraceContext()]; + } + + const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); + const traceContext: TraceContext = { + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }; + if (dsc) { + return [dsc, traceContext]; + } + + return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext]; + } +} From f490dd55a2ff579ecb96f872418d5879e1e2246c Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 12:59:07 +0200 Subject: [PATCH 02/15] Add tests --- .../core/test/lib/serverruntimeclient.test.ts | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 packages/core/test/lib/serverruntimeclient.test.ts diff --git a/packages/core/test/lib/serverruntimeclient.test.ts b/packages/core/test/lib/serverruntimeclient.test.ts new file mode 100644 index 000000000000..8f4c898fe580 --- /dev/null +++ b/packages/core/test/lib/serverruntimeclient.test.ts @@ -0,0 +1,156 @@ +import type { Event, EventHint } from '@sentry/types'; + +import { createTransport } from '../../src'; +import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client'; +import { ServerRuntimeClient } from '../../src/server-runtime-client'; + +const PUBLIC_DSN = 'https://username@domain/123'; + +function getDefaultClientOptions(options: Partial = {}): ServerRuntimeClientOptions { + return { + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), + stackParser: () => [], + instrumenter: 'sentry', + ...options, + }; +} + +describe('ServerRuntimeClient', () => { + let client: ServerRuntimeClient; + + describe('_prepareEvent', () => { + test('adds platform to event', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); + const client = new ServerRuntimeClient({ ...options, platform: 'edge' }); + + const event: Event = {}; + const hint: EventHint = {}; + (client as any)._prepareEvent(event, hint); + + expect(event.platform).toEqual('edge'); + }); + + test('adds server_name to event', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); + const client = new ServerRuntimeClient({ ...options, serverName: 'server' }); + + const event: Event = {}; + const hint: EventHint = {}; + (client as any)._prepareEvent(event, hint); + + expect(event.server_name).toEqual('server'); + }); + + test('adds runtime context to event', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); + const client = new ServerRuntimeClient({ ...options, runtime: { name: 'edge' } }); + + const event: Event = {}; + const hint: EventHint = {}; + (client as any)._prepareEvent(event, hint); + + expect(event.contexts?.runtime).toEqual({ + name: 'edge', + }); + }); + + test("doesn't clobber existing runtime data", () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN }); + const client = new ServerRuntimeClient({ ...options, runtime: { name: 'edge' } }); + + const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } }; + const hint: EventHint = {}; + (client as any)._prepareEvent(event, hint); + + expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' }); + expect(event.contexts?.runtime).not.toEqual({ name: 'edge' }); + }); + }); + + describe('captureCheckIn', () => { + it('sends a checkIn envelope', () => { + const options = getDefaultClientOptions({ + dsn: PUBLIC_DSN, + serverName: 'bar', + release: '1.0.0', + environment: 'dev', + }); + client = new ServerRuntimeClient(options); + + // @ts-ignore accessing private method + const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); + + const id = client.captureCheckIn( + { monitorSlug: 'foo', status: 'in_progress' }, + { + schedule: { + type: 'crontab', + value: '0 * * * *', + }, + checkinMargin: 2, + maxRuntime: 12333, + timezone: 'Canada/Eastern', + }, + ); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + { + check_in_id: id, + monitor_slug: 'foo', + status: 'in_progress', + release: '1.0.0', + environment: 'dev', + monitor_config: { + schedule: { + type: 'crontab', + value: '0 * * * *', + }, + checkin_margin: 2, + max_runtime: 12333, + timezone: 'Canada/Eastern', + }, + }, + ], + ], + ]); + + client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222, checkInId: id }); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(2); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + { + check_in_id: id, + monitor_slug: 'foo', + duration: 1222, + status: 'ok', + release: '1.0.0', + environment: 'dev', + }, + ], + ], + ]); + }); + + it('does not send a checkIn envelope if disabled', () => { + const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar', enabled: false }); + client = new ServerRuntimeClient(options); + + // @ts-ignore accessing private method + const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); + + client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' }); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); + }); + }); +}); From a82b1f72dfd9692dba55212b4b24f0dbc990ab2d Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 14:56:46 +0200 Subject: [PATCH 03/15] export `ServerRuntimeClientOptions` --- packages/core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e1a3484cce07..67c28a3e3c57 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,7 @@ export type { ClientClass } from './sdk'; export type { AsyncContextStrategy, Carrier, Layer, RunWithAsyncContextOptions } from './hub'; export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; +export type { ServerRuntimeClientOptions } from './server-runtime-client'; export * from './tracing'; export { From e2edcb619eb601c8042e33b6709ab22762271d2c Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 15:26:24 +0200 Subject: [PATCH 04/15] Make it generic --- packages/core/src/server-runtime-client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index c5cc60223c7d..d4e96e7ed571 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -28,12 +28,12 @@ export interface ServerRuntimeClientOptions extends ClientOptions { +export class ServerRuntimeClient extends BaseClient { /** * Creates a new Edge SDK instance. * @param options Configuration options for this SDK. */ - public constructor(options: ServerRuntimeClientOptions) { + public constructor(options: O) { // Server clients always support tracing addTracingExtensions(); From 85093dac429788ea3afc770bd6c6610e62c37c72 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 15:38:53 +0200 Subject: [PATCH 05/15] Move eventbuilder to unils --- packages/core/src/server-runtime-client.ts | 6 +++--- packages/{core => utils}/src/eventbuilder.ts | 21 ++++++++++---------- packages/utils/src/index.ts | 1 + 3 files changed, 15 insertions(+), 13 deletions(-) rename packages/{core => utils}/src/eventbuilder.ts (89%) diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index d4e96e7ed571..64bcb1e3066f 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -11,11 +11,11 @@ import type { SeverityLevel, TraceContext, } from '@sentry/types'; -import { logger, uuid4 } from '@sentry/utils'; +import { eventFromMessage, eventFromUnknownInput, logger, uuid4 } from '@sentry/utils'; import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; -import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; +import { getCurrentHub } from './hub'; import type { Scope } from './scope'; import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; @@ -44,7 +44,7 @@ export class ServerRuntimeClient { - return Promise.resolve(eventFromUnknownInput(this._options.stackParser, exception, hint)); + return Promise.resolve(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint)); } /** diff --git a/packages/core/src/eventbuilder.ts b/packages/utils/src/eventbuilder.ts similarity index 89% rename from packages/core/src/eventbuilder.ts rename to packages/utils/src/eventbuilder.ts index 63d12db56a0e..01e217921d87 100644 --- a/packages/core/src/eventbuilder.ts +++ b/packages/utils/src/eventbuilder.ts @@ -2,22 +2,18 @@ import type { Event, EventHint, Exception, + Hub, Mechanism, Severity, SeverityLevel, StackFrame, StackParser, } from '@sentry/types'; -import { - addExceptionMechanism, - addExceptionTypeValue, - extractExceptionKeysForMessage, - isError, - isPlainObject, - normalizeToSize, -} from '@sentry/utils'; -import { getCurrentHub } from './hub'; +import { isError, isPlainObject } from './is'; +import { addExceptionMechanism, addExceptionTypeValue } from './misc'; +import { normalizeToSize } from './normalize'; +import { extractExceptionKeysForMessage } from './object'; /** * Extracts stack frames from the error.stack string @@ -47,7 +43,12 @@ export function exceptionFromError(stackParser: StackParser, error: Error): Exce * Builds and Event from a Exception * @hidden */ -export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { +export function eventFromUnknownInput( + getCurrentHub: () => Hub, + stackParser: StackParser, + exception: unknown, + hint?: EventHint, +): Event { let ex: unknown = exception; const providedMechanism: Mechanism | undefined = hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 0464dbec25da..8de4941f6b96 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -30,3 +30,4 @@ export * from './baggage'; export * from './url'; export * from './userIntegrations'; export * from './cache'; +export * from './eventbuilder'; From 4e107aa53dff45c57a34e73bff6839b33de37951 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 16:25:01 +0200 Subject: [PATCH 06/15] add a default for the generic --- packages/core/src/server-runtime-client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 64bcb1e3066f..7f3d5a6cf315 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -28,7 +28,9 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends BaseClient { +export class ServerRuntimeClient< + O extends ClientOptions & ServerRuntimeClientOptions = ServerRuntimeClientOptions, +> extends BaseClient { /** * Creates a new Edge SDK instance. * @param options Configuration options for this SDK. From 088ea8835b3a5d2e646dc1d3c5b550ba5b5b24ff Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 22:31:51 +0200 Subject: [PATCH 07/15] feat(node): Use `ServerRuntimeClient` --- packages/node/src/client.ts | 162 ++---------------- packages/node/src/eventbuilder.ts | 131 -------------- .../node/src/integrations/linkederrors.ts | 3 +- packages/node/test/client.test.ts | 2 +- packages/node/test/context-lines.test.ts | 2 +- packages/node/test/eventbuilders.test.ts | 6 +- packages/node/test/stacktrace.test.ts | 5 +- 7 files changed, 21 insertions(+), 290 deletions(-) delete mode 100644 packages/node/src/eventbuilder.ts diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index 50af36448046..88e99e7eb07f 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,28 +1,10 @@ -import type { Scope } from '@sentry/core'; -import { - addTracingExtensions, - BaseClient, - createCheckInEnvelope, - getDynamicSamplingContextFromClient, - SDK_VERSION, - SessionFlusher, -} from '@sentry/core'; -import type { - CheckIn, - DynamicSamplingContext, - Event, - EventHint, - MonitorConfig, - SerializedCheckIn, - Severity, - SeverityLevel, - TraceContext, -} from '@sentry/types'; -import { logger, resolvedSyncPromise, uuid4 } from '@sentry/utils'; +import type { Scope, ServerRuntimeClientOptions } from '@sentry/core'; +import { SDK_VERSION, ServerRuntimeClient, SessionFlusher } from '@sentry/core'; +import type { Event, EventHint } from '@sentry/types'; +import { logger } from '@sentry/utils'; import * as os from 'os'; import { TextEncoder } from 'util'; -import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; import type { NodeClientOptions } from './types'; /** @@ -31,7 +13,7 @@ import type { NodeClientOptions } from './types'; * @see NodeClientOptions for documentation on configuration options. * @see SentryClient for usage documentation. */ -export class NodeClient extends BaseClient { +export class NodeClient extends ServerRuntimeClient { protected _sessionFlusher: SessionFlusher | undefined; /** @@ -57,10 +39,14 @@ export class NodeClient extends BaseClient { ...options.transportOptions, }; - // The Node client always supports tracing - addTracingExtensions(); + const clientOptions: ServerRuntimeClientOptions = { + ...options, + platform: 'node', + runtime: { name: 'node', version: global.process.version }, + serverName: options.serverName || global.process.env.SENTRY_NAME || os.hostname(), + }; - super(options); + super(clientOptions); } /** @@ -133,104 +119,6 @@ export class NodeClient extends BaseClient { } } - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - public eventFromException(exception: any, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromUnknownInput(this._options.stackParser, exception, hint)); - } - - /** - * @inheritDoc - */ - public eventFromMessage( - message: string, - // eslint-disable-next-line deprecation/deprecation - level: Severity | SeverityLevel = 'info', - hint?: EventHint, - ): PromiseLike { - return resolvedSyncPromise( - eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), - ); - } - - /** - * Create a cron monitor check in and send it to Sentry. - * - * @param checkIn An object that describes a check in. - * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want - * to create a monitor automatically when sending a check in. - * @returns A string representing the id of the check in. - */ - public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { - const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4(); - if (!this._isEnabled()) { - __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); - return id; - } - - const options = this.getOptions(); - const { release, environment, tunnel } = options; - - const serializedCheckIn: SerializedCheckIn = { - check_in_id: id, - monitor_slug: checkIn.monitorSlug, - status: checkIn.status, - release, - environment, - }; - - if (checkIn.status !== 'in_progress') { - serializedCheckIn.duration = checkIn.duration; - } - - if (monitorConfig) { - serializedCheckIn.monitor_config = { - schedule: monitorConfig.schedule, - checkin_margin: monitorConfig.checkinMargin, - max_runtime: monitorConfig.maxRuntime, - timezone: monitorConfig.timezone, - }; - } - - const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope); - if (traceContext) { - serializedCheckIn.contexts = { - trace: traceContext, - }; - } - - const envelope = createCheckInEnvelope( - serializedCheckIn, - dynamicSamplingContext, - this.getSdkMetadata(), - tunnel, - this.getDsn(), - ); - - __DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); - void this._sendEnvelope(envelope); - return id; - } - - /** - * @inheritDoc - */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { - event.platform = event.platform || 'node'; - event.contexts = { - ...event.contexts, - runtime: event.contexts?.runtime || { - name: 'node', - version: global.process.version, - }, - }; - event.server_name = - event.server_name || this.getOptions().serverName || global.process.env.SENTRY_NAME || os.hostname(); - return super._prepareEvent(event, hint, scope); - } - /** * Method responsible for capturing/ending a request session by calling `incrementSessionStatusCount` to increment * appropriate session aggregates bucket @@ -242,30 +130,4 @@ export class NodeClient extends BaseClient { this._sessionFlusher.incrementSessionStatusCount(); } } - - /** Extract trace information from scope */ - private _getTraceInfoFromScope( - scope: Scope | undefined, - ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { - if (!scope) { - return [undefined, undefined]; - } - - const span = scope.getSpan(); - if (span) { - return [span?.transaction?.getDynamicSamplingContext(), span?.getTraceContext()]; - } - - const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); - const traceContext: TraceContext = { - trace_id: traceId, - span_id: spanId, - parent_span_id: parentSpanId, - }; - if (dsc) { - return [dsc, traceContext]; - } - - return [getDynamicSamplingContextFromClient(traceId, this, scope), traceContext]; - } } diff --git a/packages/node/src/eventbuilder.ts b/packages/node/src/eventbuilder.ts deleted file mode 100644 index f2bb1443a40f..000000000000 --- a/packages/node/src/eventbuilder.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { getCurrentHub } from '@sentry/core'; -import type { - Event, - EventHint, - Exception, - Mechanism, - Severity, - SeverityLevel, - StackFrame, - StackParser, -} from '@sentry/types'; -import { - addExceptionMechanism, - addExceptionTypeValue, - extractExceptionKeysForMessage, - isError, - isPlainObject, - normalizeToSize, -} from '@sentry/utils'; - -/** - * Extracts stack frames from the error.stack string - */ -export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { - return stackParser(error.stack || '', 1); -} - -/** - * Extracts stack frames from the error and builds a Sentry Exception - */ -export function exceptionFromError(stackParser: StackParser, error: Error): Exception { - const exception: Exception = { - type: error.name || error.constructor.name, - value: error.message, - }; - - const frames = parseStackFrames(stackParser, error); - if (frames.length) { - exception.stacktrace = { frames }; - } - - return exception; -} - -/** - * Builds and Event from a Exception - * @hidden - */ -export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let ex: unknown = exception; - const providedMechanism: Mechanism | undefined = - hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; - const mechanism: Mechanism = providedMechanism || { - handled: true, - type: 'generic', - }; - - if (!isError(exception)) { - if (isPlainObject(exception)) { - // This will allow us to group events based on top-level keys - // which is much better than creating new group when any key/value change - const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`; - - const hub = getCurrentHub(); - const client = hub.getClient(); - const normalizeDepth = client && client.getOptions().normalizeDepth; - hub.configureScope(scope => { - scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); - }); - - ex = (hint && hint.syntheticException) || new Error(message); - (ex as Error).message = message; - } else { - // This handles when someone does: `throw "something awesome";` - // We use synthesized Error here so we can extract a (rough) stack trace. - ex = (hint && hint.syntheticException) || new Error(exception as string); - (ex as Error).message = exception as string; - } - mechanism.synthetic = true; - } - - const event = { - exception: { - values: [exceptionFromError(stackParser, ex as Error)], - }, - }; - - addExceptionTypeValue(event, undefined, undefined); - addExceptionMechanism(event, mechanism); - - return { - ...event, - event_id: hint && hint.event_id, - }; -} - -/** - * Builds and Event from a Message - * @hidden - */ -export function eventFromMessage( - stackParser: StackParser, - message: string, - // eslint-disable-next-line deprecation/deprecation - level: Severity | SeverityLevel = 'info', - hint?: EventHint, - attachStacktrace?: boolean, -): Event { - const event: Event = { - event_id: hint && hint.event_id, - level, - message, - }; - - if (attachStacktrace && hint && hint.syntheticException) { - const frames = parseStackFrames(stackParser, hint.syntheticException); - if (frames.length) { - event.exception = { - values: [ - { - value: message, - stacktrace: { frames }, - }, - ], - }; - } - } - - return event; -} diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index 610a376f640a..451e8dcb299b 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,7 +1,6 @@ import type { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; -import { applyAggregateErrorsToEvent } from '@sentry/utils'; +import { applyAggregateErrorsToEvent, exceptionFromError } from '@sentry/utils'; -import { exceptionFromError } from '../eventbuilder'; import { ContextLines } from './contextlines'; const DEFAULT_KEY = 'cause'; diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index ee5fd5bdd957..2a3f09acce17 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -233,8 +233,8 @@ describe('NodeClient', () => { test('adds server name to event when value given in env', () => { const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN }); - client = new NodeClient(options); process.env.SENTRY_NAME = 'foo'; + client = new NodeClient(options); const event: Event = {}; const hint: EventHint = {}; diff --git a/packages/node/test/context-lines.test.ts b/packages/node/test/context-lines.test.ts index cfdd44e8b840..25b17e29ba77 100644 --- a/packages/node/test/context-lines.test.ts +++ b/packages/node/test/context-lines.test.ts @@ -1,7 +1,7 @@ import type { StackFrame } from '@sentry/types'; +import { parseStackFrames } from '@sentry/utils'; import * as fs from 'fs'; -import { parseStackFrames } from '../src/eventbuilder'; import { ContextLines, resetFileContentCache } from '../src/integrations/contextlines'; import { defaultStackParser } from '../src/sdk'; import { getError } from './helper/error'; diff --git a/packages/node/test/eventbuilders.test.ts b/packages/node/test/eventbuilders.test.ts index 46dfc02a3c33..cf612afef508 100644 --- a/packages/node/test/eventbuilders.test.ts +++ b/packages/node/test/eventbuilders.test.ts @@ -1,7 +1,7 @@ import type { Client } from '@sentry/types'; +import { eventFromUnknownInput } from '@sentry/utils'; -import { defaultStackParser, Scope } from '../src'; -import { eventFromUnknownInput } from '../src/eventbuilder'; +import { defaultStackParser, getCurrentHub, Scope } from '../src'; const testScope = new Scope(); @@ -55,7 +55,7 @@ describe('eventFromUnknownInput', () => { }, }; - eventFromUnknownInput(defaultStackParser, deepObject); + eventFromUnknownInput(getCurrentHub, defaultStackParser, deepObject); const serializedObject = (testScope as any)._extra.__serialized__; expect(serializedObject).toBeDefined(); diff --git a/packages/node/test/stacktrace.test.ts b/packages/node/test/stacktrace.test.ts index f5a1b453609f..5b0f6fc52e25 100644 --- a/packages/node/test/stacktrace.test.ts +++ b/packages/node/test/stacktrace.test.ts @@ -10,7 +10,8 @@ * @license MIT */ -import { parseStackFrames } from '../src/eventbuilder'; +import { parseStackFrames } from '@sentry/utils'; + import { defaultStackParser as stackParser } from '../src/sdk'; function testBasic() { @@ -32,7 +33,7 @@ describe('Stack parsing', () => { const last = frames.length - 1; expect(frames[last].filename).toEqual(__filename); expect(frames[last].function).toEqual('testBasic'); - expect(frames[last].lineno).toEqual(17); + expect(frames[last].lineno).toEqual(18); expect(frames[last].colno).toEqual(10); }); From 5ff28a691b61c0d63c56684ad7f4a69bbb821b64 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 4 Sep 2023 17:56:17 +0200 Subject: [PATCH 08/15] Move request sessions and flusher to `ServerRuntimeClient` --- packages/core/src/server-runtime-client.ts | 87 +++++++++++++++++++++ packages/node/src/client.ts | 90 +--------------------- 2 files changed, 89 insertions(+), 88 deletions(-) diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 7f3d5a6cf315..96ebaa2d9fcf 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -17,6 +17,7 @@ import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; import { getCurrentHub } from './hub'; import type { Scope } from './scope'; +import { SessionFlusher } from './sessionflusher'; import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; export interface ServerRuntimeClientOptions extends ClientOptions { @@ -31,6 +32,8 @@ export interface ServerRuntimeClientOptions extends ClientOptions extends BaseClient { + protected _sessionFlusher: SessionFlusher | undefined; + /** * Creates a new Edge SDK instance. * @param options Configuration options for this SDK. @@ -63,6 +66,78 @@ export class ServerRuntimeClient< ); } + /** + * @inheritDoc + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types + public captureException(exception: any, hint?: EventHint, scope?: Scope): string | undefined { + // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only + // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload + // sent to the Server only when the `requestHandler` middleware is used + if (this._options.autoSessionTracking && this._sessionFlusher && scope) { + const requestSession = scope.getRequestSession(); + + // Necessary checks to ensure this is code block is executed only within a request + // Should override the status only if `requestSession.status` is `Ok`, which is its initial stage + if (requestSession && requestSession.status === 'ok') { + requestSession.status = 'errored'; + } + } + + return super.captureException(exception, hint, scope); + } + + /** + * @inheritDoc + */ + public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined { + // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only + // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload + // sent to the Server only when the `requestHandler` middleware is used + if (this._options.autoSessionTracking && this._sessionFlusher && scope) { + const eventType = event.type || 'exception'; + const isException = + eventType === 'exception' && event.exception && event.exception.values && event.exception.values.length > 0; + + // If the event is of type Exception, then a request session should be captured + if (isException) { + const requestSession = scope.getRequestSession(); + + // Ensure that this is happening within the bounds of a request, and make sure not to override + // Session Status if Errored / Crashed + if (requestSession && requestSession.status === 'ok') { + requestSession.status = 'errored'; + } + } + } + + return super.captureEvent(event, hint, scope); + } + + /** + * + * @inheritdoc + */ + public close(timeout?: number): PromiseLike { + if (this._sessionFlusher) { + this._sessionFlusher.close(); + } + return super.close(timeout); + } + + /** Method that initialises an instance of SessionFlusher on Client */ + public initSessionFlusher(): void { + const { release, environment } = this._options; + if (!release) { + __DEBUG_BUILD__ && logger.warn('Cannot initialise an instance of SessionFlusher if no release is provided!'); + } else { + this._sessionFlusher = new SessionFlusher(this, { + release, + environment, + }); + } + } + /** * Create a cron monitor check in and send it to Sentry. * @@ -121,6 +196,18 @@ export class ServerRuntimeClient< return id; } + /** + * Method responsible for capturing/ending a request session by calling `incrementSessionStatusCount` to increment + * appropriate session aggregates bucket + */ + protected _captureRequestSession(): void { + if (!this._sessionFlusher) { + __DEBUG_BUILD__ && logger.warn('Discarded request mode session because autoSessionTracking option was disabled'); + } else { + this._sessionFlusher.incrementSessionStatusCount(); + } + } + /** * @inheritDoc */ diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index 88e99e7eb07f..8a174754d1f1 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,7 +1,5 @@ -import type { Scope, ServerRuntimeClientOptions } from '@sentry/core'; -import { SDK_VERSION, ServerRuntimeClient, SessionFlusher } from '@sentry/core'; -import type { Event, EventHint } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import type { ServerRuntimeClientOptions } from '@sentry/core'; +import { SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; import * as os from 'os'; import { TextEncoder } from 'util'; @@ -14,8 +12,6 @@ import type { NodeClientOptions } from './types'; * @see SentryClient for usage documentation. */ export class NodeClient extends ServerRuntimeClient { - protected _sessionFlusher: SessionFlusher | undefined; - /** * Creates a new Node SDK instance. * @param options Configuration options for this SDK. @@ -48,86 +44,4 @@ export class NodeClient extends ServerRuntimeClient { super(clientOptions); } - - /** - * @inheritDoc - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types - public captureException(exception: any, hint?: EventHint, scope?: Scope): string | undefined { - // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only - // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload - // sent to the Server only when the `requestHandler` middleware is used - if (this._options.autoSessionTracking && this._sessionFlusher && scope) { - const requestSession = scope.getRequestSession(); - - // Necessary checks to ensure this is code block is executed only within a request - // Should override the status only if `requestSession.status` is `Ok`, which is its initial stage - if (requestSession && requestSession.status === 'ok') { - requestSession.status = 'errored'; - } - } - - return super.captureException(exception, hint, scope); - } - - /** - * @inheritDoc - */ - public captureEvent(event: Event, hint?: EventHint, scope?: Scope): string | undefined { - // Check if the flag `autoSessionTracking` is enabled, and if `_sessionFlusher` exists because it is initialised only - // when the `requestHandler` middleware is used, and hence the expectation is to have SessionAggregates payload - // sent to the Server only when the `requestHandler` middleware is used - if (this._options.autoSessionTracking && this._sessionFlusher && scope) { - const eventType = event.type || 'exception'; - const isException = - eventType === 'exception' && event.exception && event.exception.values && event.exception.values.length > 0; - - // If the event is of type Exception, then a request session should be captured - if (isException) { - const requestSession = scope.getRequestSession(); - - // Ensure that this is happening within the bounds of a request, and make sure not to override - // Session Status if Errored / Crashed - if (requestSession && requestSession.status === 'ok') { - requestSession.status = 'errored'; - } - } - } - - return super.captureEvent(event, hint, scope); - } - - /** - * - * @inheritdoc - */ - public close(timeout?: number): PromiseLike { - this._sessionFlusher?.close(); - return super.close(timeout); - } - - /** Method that initialises an instance of SessionFlusher on Client */ - public initSessionFlusher(): void { - const { release, environment } = this._options; - if (!release) { - __DEBUG_BUILD__ && logger.warn('Cannot initialise an instance of SessionFlusher if no release is provided!'); - } else { - this._sessionFlusher = new SessionFlusher(this, { - release, - environment, - }); - } - } - - /** - * Method responsible for capturing/ending a request session by calling `incrementSessionStatusCount` to increment - * appropriate session aggregates bucket - */ - protected _captureRequestSession(): void { - if (!this._sessionFlusher) { - __DEBUG_BUILD__ && logger.warn('Discarded request mode session because autoSessionTracking option was disabled'); - } else { - this._sessionFlusher.incrementSessionStatusCount(); - } - } } From cfd7dda201944b27b10b197c6a465fc89ff1f0e9 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 12 Sep 2023 22:37:03 +0200 Subject: [PATCH 09/15] Fix otel test by @AbhiPrasad --- .../test/spanprocessor.test.ts | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 695086d9cce2..1fa6db96c537 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -766,13 +766,20 @@ describe('SentrySpanProcessor', () => { }); it('associates an error to a transaction', () => { - let sentryEvent: any; let otelSpan: any; + expect.assertions(3); + client = new NodeClient({ ...DEFAULT_NODE_CLIENT_OPTIONS, - beforeSend: event => { - sentryEvent = event; + beforeSend: sentryEvent => { + expect(sentryEvent).toBeDefined(); + expect(sentryEvent.exception).toBeDefined(); + expect(sentryEvent.contexts?.trace).toEqual({ + parent_span_id: otelSpan.parentSpanId, + span_id: otelSpan.spanContext().spanId, + trace_id: otelSpan.spanContext().traceId, + }); return null; }, }); @@ -790,24 +797,28 @@ describe('SentrySpanProcessor', () => { parentOtelSpan.end(); }); - - expect(sentryEvent).toBeDefined(); - expect(sentryEvent.exception).toBeDefined(); - expect(sentryEvent.contexts.trace).toEqual({ - parent_span_id: otelSpan.parentSpanId, - span_id: otelSpan.spanContext().spanId, - trace_id: otelSpan.spanContext().traceId, - }); }); it('generates Sentry errors from opentelemetry span exception events', () => { - let sentryEvent: any; let otelSpan: any; + expect.assertions(4); + client = new NodeClient({ ...DEFAULT_NODE_CLIENT_OPTIONS, - beforeSend: event => { - sentryEvent = event; + beforeSend: sentryEvent => { + expect(sentryEvent).toBeDefined(); + expect(sentryEvent.exception).toBeDefined(); + expect(sentryEvent.exception?.values?.[0]).toEqual({ + mechanism: expect.any(Object), + type: 'Error', + value: 'this is an otel error!', + }); + expect(sentryEvent.contexts?.trace).toEqual({ + parent_span_id: otelSpan.parentSpanId, + span_id: otelSpan.spanContext().spanId, + trace_id: otelSpan.spanContext().traceId, + }); return null; }, }); @@ -825,19 +836,6 @@ describe('SentrySpanProcessor', () => { parentOtelSpan.end(); }); - - expect(sentryEvent).toBeDefined(); - expect(sentryEvent.exception).toBeDefined(); - expect(sentryEvent.exception.values[0]).toEqual({ - mechanism: expect.any(Object), - type: 'Error', - value: 'this is an otel error!', - }); - expect(sentryEvent.contexts.trace).toEqual({ - parent_span_id: otelSpan.parentSpanId, - span_id: otelSpan.spanContext().spanId, - trace_id: otelSpan.spanContext().traceId, - }); }); // Regression test for https://github.com/getsentry/sentry-javascript/issues/7538 From fd5cecaf0d5492f804ed55e19ca32928a4fa66b1 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 13 Sep 2023 17:07:22 +0200 Subject: [PATCH 10/15] Fix unit tests --- packages/opentelemetry-node/test/spanprocessor.test.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 2fbd22c2d49a..12af1ec0067e 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -863,11 +863,9 @@ describe('SentrySpanProcessor', () => { }); }); - it('associates an error to a transaction', () => { + it('associates an error to a transaction', done => { let otelSpan: any; - expect.assertions(3); - client = new NodeClient({ ...DEFAULT_NODE_CLIENT_OPTIONS, beforeSend: sentryEvent => { @@ -878,6 +876,7 @@ describe('SentrySpanProcessor', () => { span_id: otelSpan.spanContext().spanId, trace_id: otelSpan.spanContext().traceId, }); + done(); return null; }, }); @@ -897,11 +896,9 @@ describe('SentrySpanProcessor', () => { }); }); - it('generates Sentry errors from opentelemetry span exception events', () => { + it('generates Sentry errors from opentelemetry span exception events', done => { let otelSpan: any; - expect.assertions(4); - client = new NodeClient({ ...DEFAULT_NODE_CLIENT_OPTIONS, beforeSend: sentryEvent => { @@ -917,6 +914,7 @@ describe('SentrySpanProcessor', () => { span_id: otelSpan.spanContext().spanId, trace_id: otelSpan.spanContext().traceId, }); + done(); return null; }, }); From 9bab099d70987b2c6f4d55aac133cd5752e6b8a6 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 13 Sep 2023 18:56:36 +0200 Subject: [PATCH 11/15] Fix broken session aggregates test due to different envelope order --- .../aggregates-disable-single-session.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js index 300870d21fac..bba4bbc88bb1 100644 --- a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js +++ b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js @@ -34,6 +34,13 @@ function makeDummyTransport() { .split('\n') .filter(l => !!l) .map(e => JSON.parse(e)); + + if (sessionEnv[1].type !== 'sessions') { + return Promise.resolve({ + statusCode: 200, + }); + } + assertSessionAggregates(sessionEnv[2], { attrs: { release: '1.1' }, aggregates: [{ crashed: 2, errored: 1, exited: 1 }], From d23dff2321dd1c7277e7fa8a9656371ba1ed34e8 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 13 Sep 2023 19:22:43 +0200 Subject: [PATCH 12/15] skipTypeImports for madge config --- packages/node/package.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/node/package.json b/packages/node/package.json index 3665f74b55b3..b2368d02ac39 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -72,5 +72,12 @@ "volta": { "extends": "../../package.json" }, + "madge":{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } + }, "sideEffects": false } From 050633dbf30543986c2d27cb9074fc61eb85411a Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 13 Sep 2023 19:23:41 +0200 Subject: [PATCH 13/15] add comment --- .../session-aggregates/aggregates-disable-single-session.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js index bba4bbc88bb1..5f78cbdcf421 100644 --- a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js +++ b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js @@ -35,6 +35,7 @@ function makeDummyTransport() { .filter(l => !!l) .map(e => JSON.parse(e)); + // if this is not a session payload, just return because we don't care about it if (sessionEnv[1].type !== 'sessions') { return Promise.resolve({ statusCode: 200, From 76245c449578d05cbe5d937000d83cd435631191 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 14 Sep 2023 00:00:24 +0200 Subject: [PATCH 14/15] Use resolvedSyncPromise and revert test "fixes" --- packages/core/src/server-runtime-client.ts | 6 +++--- .../aggregates-disable-single-session.js | 7 ------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 96ebaa2d9fcf..67d7055a1623 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -11,7 +11,7 @@ import type { SeverityLevel, TraceContext, } from '@sentry/types'; -import { eventFromMessage, eventFromUnknownInput, logger, uuid4 } from '@sentry/utils'; +import { eventFromMessage, eventFromUnknownInput, logger, resolvedSyncPromise, uuid4 } from '@sentry/utils'; import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; @@ -49,7 +49,7 @@ export class ServerRuntimeClient< * @inheritDoc */ public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { - return Promise.resolve(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint)); + return resolvedSyncPromise(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint)); } /** @@ -61,7 +61,7 @@ export class ServerRuntimeClient< level: Severity | SeverityLevel = 'info', hint?: EventHint, ): PromiseLike { - return Promise.resolve( + return resolvedSyncPromise( eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), ); } diff --git a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js index 5f78cbdcf421..dc6aa4485dcb 100644 --- a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js +++ b/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js @@ -35,13 +35,6 @@ function makeDummyTransport() { .filter(l => !!l) .map(e => JSON.parse(e)); - // if this is not a session payload, just return because we don't care about it - if (sessionEnv[1].type !== 'sessions') { - return Promise.resolve({ - statusCode: 200, - }); - } - assertSessionAggregates(sessionEnv[2], { attrs: { release: '1.1' }, aggregates: [{ crashed: 2, errored: 1, exited: 1 }], From 7018d419efdc786594d2460b7239e8f3f59a57d5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 14 Sep 2023 00:02:33 +0200 Subject: [PATCH 15/15] Revert test "fixes" --- .../test/spanprocessor.test.ts | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 12af1ec0067e..3b87068c76f7 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -863,20 +863,14 @@ describe('SentrySpanProcessor', () => { }); }); - it('associates an error to a transaction', done => { + it('associates an error to a transaction', () => { + let sentryEvent: any; let otelSpan: any; client = new NodeClient({ ...DEFAULT_NODE_CLIENT_OPTIONS, - beforeSend: sentryEvent => { - expect(sentryEvent).toBeDefined(); - expect(sentryEvent.exception).toBeDefined(); - expect(sentryEvent.contexts?.trace).toEqual({ - parent_span_id: otelSpan.parentSpanId, - span_id: otelSpan.spanContext().spanId, - trace_id: otelSpan.spanContext().traceId, - }); - done(); + beforeSend: event => { + sentryEvent = event; return null; }, }); @@ -894,27 +888,24 @@ describe('SentrySpanProcessor', () => { parentOtelSpan.end(); }); + + expect(sentryEvent).toBeDefined(); + expect(sentryEvent.exception).toBeDefined(); + expect(sentryEvent.contexts.trace).toEqual({ + parent_span_id: otelSpan.parentSpanId, + span_id: otelSpan.spanContext().spanId, + trace_id: otelSpan.spanContext().traceId, + }); }); - it('generates Sentry errors from opentelemetry span exception events', done => { + it('generates Sentry errors from opentelemetry span exception events', () => { + let sentryEvent: any; let otelSpan: any; client = new NodeClient({ ...DEFAULT_NODE_CLIENT_OPTIONS, - beforeSend: sentryEvent => { - expect(sentryEvent).toBeDefined(); - expect(sentryEvent.exception).toBeDefined(); - expect(sentryEvent.exception?.values?.[0]).toEqual({ - mechanism: expect.any(Object), - type: 'Error', - value: 'this is an otel error!', - }); - expect(sentryEvent.contexts?.trace).toEqual({ - parent_span_id: otelSpan.parentSpanId, - span_id: otelSpan.spanContext().spanId, - trace_id: otelSpan.spanContext().traceId, - }); - done(); + beforeSend: event => { + sentryEvent = event; return null; }, }); @@ -932,6 +923,19 @@ describe('SentrySpanProcessor', () => { parentOtelSpan.end(); }); + + expect(sentryEvent).toBeDefined(); + expect(sentryEvent.exception).toBeDefined(); + expect(sentryEvent.exception.values[0]).toEqual({ + mechanism: expect.any(Object), + type: 'Error', + value: 'this is an otel error!', + }); + expect(sentryEvent.contexts.trace).toEqual({ + parent_span_id: otelSpan.parentSpanId, + span_id: otelSpan.spanContext().spanId, + trace_id: otelSpan.spanContext().traceId, + }); }); // Regression test for https://github.com/getsentry/sentry-javascript/issues/7538