From e313acd4f12f2fbbe1bbc0946dba6e6a97e13191 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 12:45:42 +0200 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 827936063b41fc756ff61031e914030600865664 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 15:04:17 +0200 Subject: [PATCH 4/7] feat(nextjs): Replace `EdgeClient` with `ServerRuntimeClient` --- packages/nextjs/src/edge/edgeclient.ts | 174 ------------------- packages/nextjs/src/edge/eventbuilder.ts | 130 -------------- packages/nextjs/src/edge/index.ts | 30 +++- packages/nextjs/test/edge/edgeclient.test.ts | 57 ------ 4 files changed, 26 insertions(+), 365 deletions(-) delete mode 100644 packages/nextjs/src/edge/edgeclient.ts delete mode 100644 packages/nextjs/src/edge/eventbuilder.ts delete mode 100644 packages/nextjs/test/edge/edgeclient.test.ts diff --git a/packages/nextjs/src/edge/edgeclient.ts b/packages/nextjs/src/edge/edgeclient.ts deleted file mode 100644 index 39b795c81a53..000000000000 --- a/packages/nextjs/src/edge/edgeclient.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { Scope } from '@sentry/core'; -import { - addTracingExtensions, - BaseClient, - createCheckInEnvelope, - getDynamicSamplingContextFromClient, - SDK_VERSION, -} from '@sentry/core'; -import type { - CheckIn, - ClientOptions, - DynamicSamplingContext, - Event, - EventHint, - MonitorConfig, - SerializedCheckIn, - Severity, - SeverityLevel, - TraceContext, -} from '@sentry/types'; -import { logger, uuid4 } from '@sentry/utils'; - -import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; -import type { EdgeTransportOptions } from './transport'; - -export type EdgeClientOptions = ClientOptions; - -/** - * The Sentry Edge SDK Client. - */ -export class EdgeClient extends BaseClient { - /** - * Creates a new Edge SDK instance. - * @param options Configuration options for this SDK. - */ - public constructor(options: EdgeClientOptions) { - options._metadata = options._metadata || {}; - options._metadata.sdk = options._metadata.sdk || { - name: 'sentry.javascript.nextjs', - packages: [ - { - name: 'npm:@sentry/nextjs', - version: SDK_VERSION, - }, - ], - version: SDK_VERSION, - }; - - // The Edge client always supports 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 { - event.platform = event.platform || 'edge'; - event.contexts = { - ...event.contexts, - runtime: event.contexts?.runtime || { - name: 'edge', - }, - }; - event.server_name = event.server_name || process.env.SENTRY_NAME; - 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) { - 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/nextjs/src/edge/eventbuilder.ts b/packages/nextjs/src/edge/eventbuilder.ts deleted file mode 100644 index 4e483fce3ff7..000000000000 --- a/packages/nextjs/src/edge/eventbuilder.ts +++ /dev/null @@ -1,130 +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 { - 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/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 6c2967d30f9b..281dd215001f 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,10 +1,16 @@ -import { getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import type { ServerRuntimeClientOptions } from '@sentry/core'; +import { + getIntegrationsToSetup, + initAndBind, + Integrations as CoreIntegrations, + SDK_VERSION, + ServerRuntimeClient, +} from '@sentry/core'; import type { Options } from '@sentry/types'; import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; import { getVercelEnv } from '../common/getVercelEnv'; import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageAsyncContextStrategy'; -import { EdgeClient } from './edgeclient'; import { makeEdgeTransport } from './transport'; const nodeStackParser = createStackParser(nodeStackLineParser()); @@ -53,14 +59,30 @@ export function init(options: EdgeOptions = {}): void { options.instrumenter = 'sentry'; } - const clientOptions = { + const clientOptions: ServerRuntimeClientOptions = { ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), integrations: getIntegrationsToSetup(options), transport: options.transport || makeEdgeTransport, }; - initAndBind(EdgeClient, clientOptions); + clientOptions._metadata = clientOptions._metadata || {}; + clientOptions._metadata.sdk = clientOptions._metadata.sdk || { + name: 'sentry.javascript.nextjs', + packages: [ + { + name: 'npm:@sentry/nextjs', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; + + clientOptions.platform = 'edge'; + clientOptions.runtime = { name: 'edge' }; + clientOptions.serverName = process.env.SENTRY_NAME; + + initAndBind(ServerRuntimeClient, clientOptions); // TODO?: Sessiontracking } diff --git a/packages/nextjs/test/edge/edgeclient.test.ts b/packages/nextjs/test/edge/edgeclient.test.ts deleted file mode 100644 index cba4a751c71e..000000000000 --- a/packages/nextjs/test/edge/edgeclient.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { createTransport } from '@sentry/core'; -import type { Event, EventHint } from '@sentry/types'; - -import type { EdgeClientOptions } from '../../src/edge/edgeclient'; -import { EdgeClient } from '../../src/edge/edgeclient'; - -const PUBLIC_DSN = 'https://username@domain/123'; - -function getDefaultEdgeClientOptions(options: Partial = {}): EdgeClientOptions { - return { - integrations: [], - transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), - stackParser: () => [], - instrumenter: 'sentry', - ...options, - }; -} - -describe('NodeClient', () => { - describe('_prepareEvent', () => { - test('adds platform to event', () => { - const options = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); - const client = new EdgeClient(options); - - const event: Event = {}; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.platform).toEqual('edge'); - }); - - test('adds runtime context to event', () => { - const options = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); - const client = new EdgeClient(options); - - 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 = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); - const client = new EdgeClient(options); - - 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' }); - }); - }); -}); From e2edcb619eb601c8042e33b6709ab22762271d2c Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 1 Sep 2023 15:26:24 +0200 Subject: [PATCH 5/7] 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 6/7] 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 7/7] 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.