From 9fd30287ff9d97a2c3783cf7c6e6a7ba84b95661 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 5 Sep 2023 11:42:20 +0200 Subject: [PATCH 1/9] docs: Reference `@sentry/migr8` in migration docs (#8942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a reference to migr8 in the migration docs 🚀 --------- Co-authored-by: Luca Forstner --- MIGRATION.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index 77cb959525a8..5aec615751c7 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,13 @@ # Deprecations in 7.x +You can use [@sentry/migr8](https://www.npmjs.com/package/@sentry/migr8) to automatically update your SDK usage and fix most deprecations: + +```bash +npx @sentry/migr8@latest +``` + +This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! + ## Deprecate `timestampWithMs` export - #7878 The `timestampWithMs` util is deprecated in favor of using `timestampInSeconds`. From 1e8921bd9610f865d09cf737966ca1082ce8d8ee Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 5 Sep 2023 14:03:37 +0200 Subject: [PATCH 2/9] feat(browser): Add `BroadcastChannel` and `SharedWorker` to TryCatch EventTargets (#8943) --- packages/browser/src/integrations/trycatch.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index 0ec05cc552df..56e414f46e64 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -9,6 +9,7 @@ const DEFAULT_EVENT_TARGET = [ 'Node', 'ApplicationCache', 'AudioTrackList', + 'BroadcastChannel', 'ChannelMergerNode', 'CryptoOperation', 'EventSource', @@ -24,6 +25,7 @@ const DEFAULT_EVENT_TARGET = [ 'Notification', 'SVGElementInstance', 'Screen', + 'SharedWorker', 'TextTrack', 'TextTrackCue', 'TextTrackList', From e2f0f4b81f730f443e450486c8bce118848708c5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 5 Sep 2023 14:04:10 +0200 Subject: [PATCH 3/9] docs(node-experimental): Update readme for current status (#8945) Update the docs for the current status of the POTEL experiment. --- packages/node-experimental/README.md | 48 ++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/node-experimental/README.md b/packages/node-experimental/README.md index 2b091a3379b0..461a04f28bfb 100644 --- a/packages/node-experimental/README.md +++ b/packages/node-experimental/README.md @@ -49,12 +49,54 @@ Currently, this SDK: * Will capture errors (same as @sentry/node) * Auto-instrument for performance - see below for which performance integrations are available. +* Provide _some_ manual instrumentation APIs +* Sync OpenTelemetry Context with our Sentry Hub/Scope ### Manual Instrumentation -**Manual instrumentation is not supported!** -This is because the current Sentry-Performance-APIs like `Sentry.startTransaction()` are not compatible with the OpenTelemetry tracing model. -We may add manual tracing capabilities in a later version. +You can manual instrument using the following APIs: + +```js +const Sentry = require('@sentry/node-experimental'); + +Sentry.startActiveSpan({ description: 'outer' }, function (span) { + span.setData(customData); + doSomethingSlow(); + Sentry.startActiveSpan({ description: 'inner' }, function() { + // inner span is a child of outer span + doSomethingVerySlow(); + // inner span is auto-ended when this callback ends + }); + // outer span is auto-ended when this callback ends +}); +``` + +You can also create spans without marking them as the active span. +Note that for most scenarios, we recommend the `startActiveSpan` syntax. + +```js +const Sentry = require('@sentry/node-experimental'); + +// This will _not_ be put on the scope/set as active, so no other spans will be attached to it +const span = Sentry.startSpan({ description: 'non-active span' }); + +doSomethingSlow(); + +span?.finish(); +``` + +Finally you can also get the currently active span, if you need to do more with it: + +```js +const Sentry = require('@sentry/node-experimental'); +const span = Sentry.getActiveSpan(); +``` + +### Async Context + +We leverage the OpenTelemetry context forking in order to ensure isolation of parallel requests. +This means that as long as you are using an OpenTelemetry instrumentation for your framework of choice +(currently: Express or Fastify), you do not need to setup any `requestHandler` or similar. ### ESM Support From f54e12137ee86e43a4b2fdeb73f812912f67cfdd Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 5 Sep 2023 19:16:29 +0200 Subject: [PATCH 4/9] feat(core): Add `ServerRuntimeClient` (#8930) The `ServerRuntimeClient` is a near identical copy of the nextjs `EdgeClient`. To make it a direct replacement it has constructor options to override the event `platform`, `runtime`, and `server_name`. This PR makes yet another copy of the Node `eventbuilder.ts` but after future PRs to remove the `EdgeClient` and make `NodeClient` extend `ServerRuntimeClient`, this will be the only copy. I've put the `eventbuilder` code in utils since some of these functions are used elsewhere outside of the clients and I don't want to export these from core and them become part of our public API. This is especially important since the browser SDK already exports it's own slightly different `exceptionFromError`. --- packages/core/src/index.ts | 2 + packages/core/src/server-runtime-client.ts | 172 ++++++++++++++++++ .../core/test/lib/serverruntimeclient.test.ts | 156 ++++++++++++++++ packages/utils/src/eventbuilder.ts | 132 ++++++++++++++ packages/utils/src/index.ts | 1 + 5 files changed, 463 insertions(+) create mode 100644 packages/core/src/server-runtime-client.ts create mode 100644 packages/core/test/lib/serverruntimeclient.test.ts create mode 100644 packages/utils/src/eventbuilder.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a0cc7e627bf7..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 { @@ -38,6 +39,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..7f3d5a6cf315 --- /dev/null +++ b/packages/core/src/server-runtime-client.ts @@ -0,0 +1,172 @@ +import type { + BaseTransportOptions, + CheckIn, + ClientOptions, + DynamicSamplingContext, + Event, + EventHint, + MonitorConfig, + SerializedCheckIn, + Severity, + SeverityLevel, + TraceContext, +} from '@sentry/types'; +import { eventFromMessage, eventFromUnknownInput, logger, uuid4 } from '@sentry/utils'; + +import { BaseClient } from './baseclient'; +import { createCheckInEnvelope } from './checkin'; +import { getCurrentHub } from './hub'; +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< + O extends ClientOptions & ServerRuntimeClientOptions = ServerRuntimeClientOptions, +> extends BaseClient { + /** + * Creates a new Edge SDK instance. + * @param options Configuration options for this SDK. + */ + public constructor(options: O) { + // Server clients always support tracing + addTracingExtensions(); + + super(options); + } + + /** + * @inheritDoc + */ + public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { + return Promise.resolve(eventFromUnknownInput(getCurrentHub, 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]; + } +} 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); + }); + }); +}); diff --git a/packages/utils/src/eventbuilder.ts b/packages/utils/src/eventbuilder.ts new file mode 100644 index 000000000000..01e217921d87 --- /dev/null +++ b/packages/utils/src/eventbuilder.ts @@ -0,0 +1,132 @@ +import type { + Event, + EventHint, + Exception, + Hub, + Mechanism, + Severity, + SeverityLevel, + StackFrame, + StackParser, +} from '@sentry/types'; + +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 + */ +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( + 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; + 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/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 3487fa3af7aa72ac7fdb0439047cb7367c591e77 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 5 Sep 2023 18:36:54 +0100 Subject: [PATCH 5/9] fix(remix): Add new sourcemap-upload script files to prepack assets. (#8948) --- packages/remix/scripts/prepack.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/remix/scripts/prepack.ts b/packages/remix/scripts/prepack.ts index 890b3bc2b9f1..bd32f6fe094e 100644 --- a/packages/remix/scripts/prepack.ts +++ b/packages/remix/scripts/prepack.ts @@ -3,7 +3,12 @@ import * as fs from 'fs'; import * as path from 'path'; -const PACKAGE_ASSETS = ['scripts/sentry-upload-sourcemaps.js', 'scripts/createRelease.js']; +const PACKAGE_ASSETS = [ + 'scripts/sentry-upload-sourcemaps.js', + 'scripts/createRelease.js', + 'scripts/deleteSourcemaps.js', + 'scripts/injectDebugId.js', +]; export function prepack(buildDir: string): boolean { // copy package-specific assets to build dir From 0d49557258ca3596e4e7e2b4ccbf44361d4ab686 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 6 Sep 2023 10:28:22 +0200 Subject: [PATCH 6/9] fix(node-experimental): Ensure `span.finish()` works as expected (#8947) For `Sentry.startSpan()` API, we need to ensure that when the Sentry Span is finished, we also finish the OTEL Span. Also adding some tests for these APIs. --- packages/node-experimental/src/sdk/trace.ts | 25 ++- .../test/helpers/mockSdkInit.ts | 13 ++ .../test/{sdk.test.ts => sdk/init.test.ts} | 6 +- .../node-experimental/test/sdk/trace.test.ts | 173 ++++++++++++++++++ 4 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 packages/node-experimental/test/helpers/mockSdkInit.ts rename packages/node-experimental/test/{sdk.test.ts => sdk/init.test.ts} (96%) create mode 100644 packages/node-experimental/test/sdk/trace.test.ts diff --git a/packages/node-experimental/src/sdk/trace.ts b/packages/node-experimental/src/sdk/trace.ts index 8585c0fa8597..a086b8edd2c2 100644 --- a/packages/node-experimental/src/sdk/trace.ts +++ b/packages/node-experimental/src/sdk/trace.ts @@ -24,7 +24,7 @@ export function startActiveSpan(context: TransactionContext, callback: (span: return callback(undefined); } - const name = context.description || context.op || ''; + const name = context.name || context.description || context.op || ''; return tracer.startActiveSpan(name, (span: OtelSpan): T => { const otelSpanId = span.spanContext().spanId; @@ -82,18 +82,35 @@ export function startSpan(context: TransactionContext): Span | undefined { return undefined; } - const name = context.description || context.op || ''; + const name = context.name || context.description || context.op || ''; const otelSpan = tracer.startSpan(name); const otelSpanId = otelSpan.spanContext().spanId; const sentrySpan = _INTERNAL_getSentrySpan(otelSpanId); - if (sentrySpan && isTransaction(sentrySpan) && context.metadata) { + if (!sentrySpan) { + return undefined; + } + + if (isTransaction(sentrySpan) && context.metadata) { sentrySpan.setMetadata(context.metadata); } - return sentrySpan; + // Monkey-patch `finish()` to finish the OTEL span instead + // This will also in turn finish the Sentry Span, so no need to call this ourselves + const wrappedSentrySpan = new Proxy(sentrySpan, { + get(target, prop, receiver) { + if (prop === 'finish') { + return () => { + otelSpan.end(); + }; + } + return Reflect.get(target, prop, receiver); + }, + }); + + return wrappedSentrySpan; } /** diff --git a/packages/node-experimental/test/helpers/mockSdkInit.ts b/packages/node-experimental/test/helpers/mockSdkInit.ts new file mode 100644 index 000000000000..f7bfb68f6bf6 --- /dev/null +++ b/packages/node-experimental/test/helpers/mockSdkInit.ts @@ -0,0 +1,13 @@ +import { init } from '../../src/sdk/init'; +import type { NodeExperimentalClientOptions } from '../../src/types'; + +// eslint-disable-next-line no-var +declare var global: any; + +const PUBLIC_DSN = 'https://username@domain/123'; + +export function mockSdkInit(options?: Partial) { + global.__SENTRY__ = {}; + + init({ dsn: PUBLIC_DSN, defaultIntegrations: false, ...options }); +} diff --git a/packages/node-experimental/test/sdk.test.ts b/packages/node-experimental/test/sdk/init.test.ts similarity index 96% rename from packages/node-experimental/test/sdk.test.ts rename to packages/node-experimental/test/sdk/init.test.ts index ea230368b1d8..a9c2a11885a8 100644 --- a/packages/node-experimental/test/sdk.test.ts +++ b/packages/node-experimental/test/sdk/init.test.ts @@ -1,8 +1,8 @@ import type { Integration } from '@sentry/types'; -import * as auto from '../src/integrations/getAutoPerformanceIntegrations'; -import { init } from '../src/sdk/init'; -import * as sdk from '../src/sdk/init'; +import * as auto from '../../src/integrations/getAutoPerformanceIntegrations'; +import * as sdk from '../../src/sdk/init'; +import { init } from '../../src/sdk/init'; // eslint-disable-next-line no-var declare var global: any; diff --git a/packages/node-experimental/test/sdk/trace.test.ts b/packages/node-experimental/test/sdk/trace.test.ts new file mode 100644 index 000000000000..87aae6c66689 --- /dev/null +++ b/packages/node-experimental/test/sdk/trace.test.ts @@ -0,0 +1,173 @@ +import { Span, Transaction } from '@sentry/core'; + +import * as Sentry from '../../src'; +import { mockSdkInit } from '../helpers/mockSdkInit'; + +describe('trace', () => { + beforeEach(() => { + mockSdkInit({ enableTracing: true }); + }); + + describe('startActiveSpan', () => { + it('works with a sync callback', () => { + const spans: Span[] = []; + + expect(Sentry.getActiveSpan()).toEqual(undefined); + + Sentry.startActiveSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + spans.push(outerSpan!); + + expect(outerSpan?.name).toEqual('outer'); + expect(outerSpan).toBeInstanceOf(Transaction); + expect(Sentry.getActiveSpan()).toEqual(outerSpan); + + Sentry.startActiveSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeDefined(); + spans.push(innerSpan!); + + expect(innerSpan?.description).toEqual('inner'); + expect(innerSpan).toBeInstanceOf(Span); + expect(innerSpan).not.toBeInstanceOf(Transaction); + expect(Sentry.getActiveSpan()).toEqual(innerSpan); + }); + }); + + expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(spans).toHaveLength(2); + const [outerSpan, innerSpan] = spans; + + expect((outerSpan as Transaction).name).toEqual('outer'); + expect(innerSpan.description).toEqual('inner'); + + expect(outerSpan.endTimestamp).toEqual(expect.any(Number)); + expect(innerSpan.endTimestamp).toEqual(expect.any(Number)); + }); + + it('works with an async callback', async () => { + const spans: Span[] = []; + + expect(Sentry.getActiveSpan()).toEqual(undefined); + + await Sentry.startActiveSpan({ name: 'outer' }, async outerSpan => { + expect(outerSpan).toBeDefined(); + spans.push(outerSpan!); + + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(outerSpan?.name).toEqual('outer'); + expect(outerSpan).toBeInstanceOf(Transaction); + expect(Sentry.getActiveSpan()).toEqual(outerSpan); + + await Sentry.startActiveSpan({ name: 'inner' }, async innerSpan => { + expect(innerSpan).toBeDefined(); + spans.push(innerSpan!); + + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(innerSpan?.description).toEqual('inner'); + expect(innerSpan).toBeInstanceOf(Span); + expect(innerSpan).not.toBeInstanceOf(Transaction); + expect(Sentry.getActiveSpan()).toEqual(innerSpan); + }); + }); + + expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(spans).toHaveLength(2); + const [outerSpan, innerSpan] = spans; + + expect((outerSpan as Transaction).name).toEqual('outer'); + expect(innerSpan.description).toEqual('inner'); + + expect(outerSpan.endTimestamp).toEqual(expect.any(Number)); + expect(innerSpan.endTimestamp).toEqual(expect.any(Number)); + }); + + it('works with multiple parallel calls', () => { + const spans1: Span[] = []; + const spans2: Span[] = []; + + expect(Sentry.getActiveSpan()).toEqual(undefined); + + Sentry.startActiveSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + spans1.push(outerSpan!); + + expect(outerSpan?.name).toEqual('outer'); + expect(outerSpan).toBeInstanceOf(Transaction); + expect(Sentry.getActiveSpan()).toEqual(outerSpan); + + Sentry.startActiveSpan({ name: 'inner' }, innerSpan => { + expect(innerSpan).toBeDefined(); + spans1.push(innerSpan!); + + expect(innerSpan?.description).toEqual('inner'); + expect(innerSpan).toBeInstanceOf(Span); + expect(innerSpan).not.toBeInstanceOf(Transaction); + expect(Sentry.getActiveSpan()).toEqual(innerSpan); + }); + }); + + Sentry.startActiveSpan({ name: 'outer2' }, outerSpan => { + expect(outerSpan).toBeDefined(); + spans2.push(outerSpan!); + + expect(outerSpan?.name).toEqual('outer2'); + expect(outerSpan).toBeInstanceOf(Transaction); + expect(Sentry.getActiveSpan()).toEqual(outerSpan); + + Sentry.startActiveSpan({ name: 'inner2' }, innerSpan => { + expect(innerSpan).toBeDefined(); + spans2.push(innerSpan!); + + expect(innerSpan?.description).toEqual('inner2'); + expect(innerSpan).toBeInstanceOf(Span); + expect(innerSpan).not.toBeInstanceOf(Transaction); + expect(Sentry.getActiveSpan()).toEqual(innerSpan); + }); + }); + + expect(Sentry.getActiveSpan()).toEqual(undefined); + expect(spans1).toHaveLength(2); + expect(spans2).toHaveLength(2); + }); + }); + + describe('startSpan', () => { + it('works at the root', () => { + const span = Sentry.startSpan({ name: 'test' }); + + expect(span).toBeDefined(); + expect(span).toBeInstanceOf(Transaction); + expect(span?.name).toEqual('test'); + expect(span?.endTimestamp).toBeUndefined(); + expect(Sentry.getActiveSpan()).toBeUndefined(); + + span?.finish(); + + expect(span?.endTimestamp).toEqual(expect.any(Number)); + expect(Sentry.getActiveSpan()).toBeUndefined(); + }); + + it('works as a child span', () => { + Sentry.startActiveSpan({ name: 'outer' }, outerSpan => { + expect(outerSpan).toBeDefined(); + expect(Sentry.getActiveSpan()).toEqual(outerSpan); + + const innerSpan = Sentry.startSpan({ name: 'test' }); + + expect(innerSpan).toBeDefined(); + expect(innerSpan).toBeInstanceOf(Span); + expect(innerSpan).not.toBeInstanceOf(Transaction); + expect(innerSpan?.description).toEqual('test'); + expect(innerSpan?.endTimestamp).toBeUndefined(); + expect(Sentry.getActiveSpan()).toEqual(outerSpan); + + innerSpan?.finish(); + + expect(innerSpan?.endTimestamp).toEqual(expect.any(Number)); + expect(Sentry.getActiveSpan()).toEqual(outerSpan); + }); + }); + }); +}); From 66436715aeb2dc47988645d324b37303a3d3f35d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 6 Sep 2023 10:32:04 +0200 Subject: [PATCH 7/9] feat(core): Add `name` to `Span` (#8949) In order to align `Span` & `Transaction` for usage in new performance APIs, this add `name` to the `Span` that also always returns a string. --- packages/core/src/tracing/span.ts | 9 ++++++ packages/core/test/lib/tracing/span.test.ts | 32 +++++++++++++++++++ .../core/test/lib/tracing/transaction.test.ts | 17 ++++++++++ packages/types/src/span.ts | 5 +++ 4 files changed, 63 insertions(+) create mode 100644 packages/core/test/lib/tracing/span.test.ts create mode 100644 packages/core/test/lib/tracing/transaction.test.ts diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 3ce158c8addf..27cd287a017e 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -161,6 +161,15 @@ export class Span implements SpanInterface { } } + /** An alias for `description` of the Span. */ + public get name(): string { + return this.description || ''; + } + /** Update the name of the span. */ + public set name(name: string) { + this.setName(name); + } + /** * @inheritDoc */ diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/span.test.ts new file mode 100644 index 000000000000..c54085704022 --- /dev/null +++ b/packages/core/test/lib/tracing/span.test.ts @@ -0,0 +1,32 @@ +import { Span } from '../../../src'; + +describe('span', () => { + it('works with name', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + }); + + it('works with description', () => { + const span = new Span({ description: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + }); + + it('works without name', () => { + const span = new Span({}); + expect(span.name).toEqual(''); + expect(span.description).toEqual(undefined); + }); + + it('allows to update the name', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + + span.name = 'new name'; + + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); +}); diff --git a/packages/core/test/lib/tracing/transaction.test.ts b/packages/core/test/lib/tracing/transaction.test.ts new file mode 100644 index 000000000000..b9218eae77cb --- /dev/null +++ b/packages/core/test/lib/tracing/transaction.test.ts @@ -0,0 +1,17 @@ +import { Transaction } from '../../../src'; + +describe('transaction', () => { + it('works with name', () => { + const transaction = new Transaction({ name: 'span name' }); + expect(transaction.name).toEqual('span name'); + }); + + it('allows to update the name', () => { + const transaction = new Transaction({ name: 'span name' }); + expect(transaction.name).toEqual('span name'); + + transaction.name = 'new name'; + + expect(transaction.name).toEqual('new name'); + }); +}); diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 7485bbf88d72..c931b7e457c2 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -88,6 +88,11 @@ export interface SpanContext { /** Span holding trace_id, span_id */ export interface Span extends SpanContext { + /** + * Human-readable identifier for the span. Identical to span.description. + */ + name: string; + /** * @inheritDoc */ From 8766bdd6e4677b21de44eca1af16a81be1234f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Wed, 6 Sep 2023 12:09:51 +0200 Subject: [PATCH 8/9] fix(ts): Publish downleveled TS3.8 types, fix types path (#8954) --- .npmignore | 1 + packages/gatsby/.npmignore | 1 + scripts/prepack.ts | 15 +++++++++------ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.npmignore b/.npmignore index cb864514088e..6cf3cd53d7e6 100644 --- a/.npmignore +++ b/.npmignore @@ -6,3 +6,4 @@ !/cjs/**/* !/esm/**/* !/types/**/* +!/types-ts3.8/**/* diff --git a/packages/gatsby/.npmignore b/packages/gatsby/.npmignore index 35348e6a718d..05a81b2542dd 100644 --- a/packages/gatsby/.npmignore +++ b/packages/gatsby/.npmignore @@ -6,6 +6,7 @@ !/cjs/**/* !/esm/**/* !/types/**/* +!/types-ts3.8/**/* # Gatsby specific !gatsby-browser.js diff --git a/scripts/prepack.ts b/scripts/prepack.ts index bcad9dee0ef8..0c810f3e9030 100644 --- a/scripts/prepack.ts +++ b/scripts/prepack.ts @@ -23,6 +23,12 @@ const buildDir = packageWithBundles ? NPM_BUILD_DIR : BUILD_DIR; type PackageJsonEntryPoints = Record; +interface TypeVersions { + [key: string]: { + [key: string]: string[]; + }; +}; + interface PackageJson extends Record, PackageJsonEntryPoints { [EXPORT_MAP_ENTRY_POINT]: { [key: string]: { @@ -31,11 +37,7 @@ interface PackageJson extends Record, PackageJsonEntryPoints { types: string; }; }; - [TYPES_VERSIONS_ENTRY_POINT]: { - [key: string]: { - [key: string]: string[]; - }; - }; + [TYPES_VERSIONS_ENTRY_POINT]: TypeVersions; } // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -81,7 +83,8 @@ if (newPkgJson[EXPORT_MAP_ENTRY_POINT]) { if (newPkgJson[TYPES_VERSIONS_ENTRY_POINT]) { Object.entries(newPkgJson[TYPES_VERSIONS_ENTRY_POINT]).forEach(([key, val]) => { newPkgJson[TYPES_VERSIONS_ENTRY_POINT][key] = Object.entries(val).reduce((acc, [key, val]) => { - return { ...acc, [key]: val.map(v => v.replace(`${buildDir}/`, '')) }; + const newKey = key.replace(`${buildDir}/`, ''); + return { ...acc, [newKey]: val.map(v => v.replace(`${buildDir}/`, '')) }; }, {}); }); } From 5dd16de9bf6c17b7c6f72cd1ea937b62c9e2f33d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 6 Sep 2023 12:15:16 +0200 Subject: [PATCH 9/9] meta: Update Changelog for 7.68.0 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29701eff6213..99a1680a6365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.68.0 + +- feat(browser): Add `BroadcastChannel` and `SharedWorker` to TryCatch EventTargets (#8943) +- feat(core): Add `name` to `Span` (#8949) +- feat(core): Add `ServerRuntimeClient` (#8930) +- fix(node-experimental): Ensure `span.finish()` works as expected (#8947) +- fix(remix): Add new sourcemap-upload script files to prepack assets. (#8948) +- fix(publish): Publish downleveled TS3.8 types and fix types path (#8954) + ## 7.67.0 ### Important Changes