From d69fb25575739be640e6274184539a26a0c0bea0 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 7 Dec 2023 07:58:57 -0500 Subject: [PATCH 01/15] ci: Add sentry cli and vite plugin to dependabot (#9752) To make sure we keep up to date with sentry-cli and vite plugin deps, add dependabot reminders for them. --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 65b47c6c2672..43d75c60ba14 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,14 @@ updates: prefix: ci prefix-development: ci include: scope + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'weekly' + allow: + - dependency-name: "@sentry/cli" + - dependency-name: "@sentry/vite-plugin" + commit-message: + prefix: feat + prefix-development: feat + include: scope From 1eb2dedcc1a033e8ef6fa4e0ada5c7d4ee497ef2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 7 Dec 2023 15:46:37 +0100 Subject: [PATCH 02/15] fix(browser): Avoid importing from `./exports` (#9775) This has a side effect in `@sentry/browser`, so importing from there is not safe. Instead just directly import this from core. --- packages/browser/src/integrations/breadcrumbs.ts | 3 +-- packages/browser/src/profiling/utils.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 1a817cceb5b5..cfcb255f5999 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { getCurrentHub } from '@sentry/core'; +import { getClient, getCurrentHub } from '@sentry/core'; import type { Event as SentryEvent, HandlerDataConsole, @@ -31,7 +31,6 @@ import { } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { getClient } from '../exports'; import { WINDOW } from '../helpers'; /** JSDoc */ diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index c7f056868732..1aac3d397ca7 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -1,12 +1,11 @@ /* eslint-disable max-lines */ -import { DEFAULT_ENVIRONMENT, getCurrentHub } from '@sentry/core'; +import { DEFAULT_ENVIRONMENT, getClient, getCurrentHub } from '@sentry/core'; import type { DebugImage, Envelope, Event, StackFrame, StackParser, Transaction } from '@sentry/types'; import type { Profile, ThreadCpuProfile } from '@sentry/types/src/profiling'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, forEachEnvelopeItem, logger, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { getClient } from '../exports'; import { WINDOW } from '../helpers'; import type { JSSelfProfile, JSSelfProfileStack, JSSelfProfiler, JSSelfProfilerConstructor } from './jsSelfProfiling'; From 59d5554b6a4b6c4ca94d61a19586b9fec4705e1e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 7 Dec 2023 15:53:29 +0100 Subject: [PATCH 03/15] fix(nextjs): Fix devserver CORS blockage when `assetPrefix` is defined (#9766) --- packages/nextjs/src/client/index.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 9b4e9b724f09..8fd55568e70e 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -113,7 +113,17 @@ function addClientIntegrations(options: BrowserOptions): void { if (hasTracingEnabled(options)) { const defaultBrowserTracingIntegration = new BrowserTracing({ // eslint-disable-next-line deprecation/deprecation - tracingOrigins: [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/], + tracingOrigins: + process.env.NODE_ENV === 'development' + ? [ + // Will match any URL that contains "localhost" but not "webpack.hot-update.json" - The webpack dev-server + // has cors and it doesn't like extra headers when it's accessed from a different URL. + // TODO(v8): Ideally we rework our tracePropagationTargets logic so this hack won't be necessary anymore (see issue #9764) + /^(?=.*localhost)(?!.*webpack\.hot-update\.json).*/, + /^\/(?!\/)/, + ] + : // eslint-disable-next-line deprecation/deprecation + [...defaultRequestInstrumentationOptions.tracingOrigins, /^(api\/)/], routingInstrumentation: nextRouterInstrumentation, }); From b3f8d9bcb067b391c1d0eba4f537ff09be56c1e9 Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:50:21 -0500 Subject: [PATCH 04/15] feat(replays): Add an SDK _experiments configuration flag to enable canvas recording (#9723) SDK _experiments configuration flag to enable canvas recording. It allows snapshot canvas recording at 4fps. Closes https://github.com/getsentry/team-replay/issues/306 --------- Co-authored-by: Billy Vong --- packages/replay/src/replay.ts | 7 +++++++ packages/replay/src/types/replay.ts | 7 ++++++- packages/replay/src/types/rrweb.ts | 20 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 5b46ac64e110..635db47245d6 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -331,6 +331,7 @@ export class ReplayContainer implements ReplayContainerInterface { */ public startRecording(): void { try { + const canvas = this._options._experiments.canvas; this._stopRecording = record({ ...this._recordingOptions, // When running in error sampling mode, we need to overwrite `checkoutEveryNms` @@ -339,6 +340,12 @@ export class ReplayContainer implements ReplayContainerInterface { ...(this.recordingMode === 'buffer' && { checkoutEveryNms: BUFFER_CHECKOUT_TIME }), emit: getHandleRecordingEmit(this), onMutation: this._onMutationHandler, + ...(canvas && { + recordCanvas: true, + sampling: { canvas: canvas.fps || 4 }, + dataURLOptions: { quality: canvas.quality || 0.6 }, + getCanvasManager: canvas.manager, + }), }); } catch (err) { this._handleException(err); diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index 38ca35ac8f0e..d854f258c073 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -14,7 +14,7 @@ import type { SKIPPED, THROTTLED } from '../util/throttle'; import type { AllPerformanceEntry, AllPerformanceEntryData, ReplayPerformanceEntry } from './performance'; import type { ReplayFrameEvent } from './replayFrame'; import type { ReplayNetworkRequestOrResponse } from './request'; -import type { ReplayEventWithTime, RrwebRecordOptions } from './rrweb'; +import type { CanvasManagerInterface, GetCanvasManagerOptions, ReplayEventWithTime, RrwebRecordOptions } from './rrweb'; export type RecordingEvent = ReplayFrameEvent | ReplayEventWithTime; export type RecordingOptions = RrwebRecordOptions; @@ -232,6 +232,11 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { _experiments: Partial<{ captureExceptions: boolean; traceInternals: boolean; + canvas: { + fps?: number; + quality?: number; + manager: (options: GetCanvasManagerOptions) => CanvasManagerInterface; + }; }>; } diff --git a/packages/replay/src/types/rrweb.ts b/packages/replay/src/types/rrweb.ts index bc78c5811b12..bbef4f94903f 100644 --- a/packages/replay/src/types/rrweb.ts +++ b/packages/replay/src/types/rrweb.ts @@ -44,3 +44,23 @@ export type RrwebRecordOptions = { blockSelector?: string; maskInputOptions?: Record; } & Record; + +export interface CanvasManagerInterface { + reset(): void; + freeze(): void; + unfreeze(): void; + lock(): void; + unlock(): void; +} + +export interface GetCanvasManagerOptions { + recordCanvas: boolean; + blockClass: string | RegExp; + blockSelector: string | null; + unblockSelector: string | null; + sampling?: 'all' | number; + dataURLOptions: Partial<{ + type: string; + quality: number; + }>; +} From 93bbeb782ae588aee2d021a7eba3cd6d946e69be Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 11 Dec 2023 12:51:41 +0100 Subject: [PATCH 05/15] fix(node): Capture errors in tRPC middleware (#9782) --- packages/node/src/handlers.ts | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 83f23ccaec7c..3160c77b416a 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -351,36 +351,32 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { sentryTransaction.setContext('trpc', trpcContext); } - function shouldCaptureError(e: unknown): boolean { - if (typeof e === 'object' && e && 'code' in e) { - // Is likely TRPCError - we only want to capture internal server errors - return e.code === 'INTERNAL_SERVER_ERROR'; - } else { - // Is likely random error that bubbles up - return true; - } - } - - function handleErrorCase(e: unknown): void { - if (shouldCaptureError(e)) { - captureException(e, { mechanism: { handled: false } }); + function captureIfError(nextResult: { ok: false; error?: Error } | { ok: true }): void { + if (!nextResult.ok) { + captureException(nextResult.error, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } }); } } let maybePromiseResult; - try { maybePromiseResult = next(); } catch (e) { - handleErrorCase(e); + captureException(e, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } }); throw e; } if (isThenable(maybePromiseResult)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - Promise.resolve(maybePromiseResult).then(null, e => { - handleErrorCase(e); - }); + Promise.resolve(maybePromiseResult).then( + nextResult => { + captureIfError(nextResult as any); + }, + e => { + captureException(e, { mechanism: { handled: false, data: { function: 'trpcMiddleware' } } }); + }, + ); + } else { + captureIfError(maybePromiseResult as any); } // We return the original promise just to be safe. From 1512d2355ebe8c6b4006db0adf51ed2c9cceaf11 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Tue, 12 Dec 2023 12:06:05 -0330 Subject: [PATCH 06/15] feat(replay): Bump `rrweb` to 2.5.0 (#9803) > * Revert "fix: isCheckout is not included in fullsnapshot event (rrweb-io#1141)" Fixes an issue where Meta event is being lost in buffered replays. > revert: feat: Remove plugins related code, which is not used #123 > feat: Export additional canvas-related types and functions (#134) Changes needed for canvas playback > feat: Skip addHoverClass when stylesheet is >= 1MB #130 Affects playback only --- packages/replay/package.json | 4 ++-- yarn.lock | 42 ++++++++++++++++++------------------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/replay/package.json b/packages/replay/package.json index 035390206954..17ace93fc627 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -48,8 +48,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.86.0", - "@sentry-internal/rrweb": "2.2.0", - "@sentry-internal/rrweb-snapshot": "2.2.0", + "@sentry-internal/rrweb": "2.5.0", + "@sentry-internal/rrweb-snapshot": "2.5.0", "fflate": "^0.8.1", "jsdom-worker": "^0.2.1" }, diff --git a/yarn.lock b/yarn.lock index ef0bcb3c6b02..8d0f29d0c6c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5048,33 +5048,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.2.0.tgz#edc292e55263e1e2541a9ed282ff998aae272a0a" - integrity sha512-pX2YxpZfKxfbeEKG+sc0WzlSMUsG9mgwK3IioeVulNovUmus6UhLLQHYeZEsQg0WehClCgBRZakk8qEFXbmwdg== +"@sentry-internal/rrdom@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.5.0.tgz#5571b9791f1efc79141237bf8d4faaf7b872b7bb" + integrity sha512-1wGVQXoC7wnk6/T8BdHd6fK9F4fGn6huQGtss77uCvX9gJaXpfwUDV7Rbj8bOhTMn8bXsIIhpuBg7OMMgXYTzA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.2.0" + "@sentry-internal/rrweb-snapshot" "2.5.0" -"@sentry-internal/rrweb-snapshot@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.2.0.tgz#23a231ffd8ef734c2665690e8040a4ffc2faaf9f" - integrity sha512-txHynfjaJHLJvS+Kf9DOJyL7ur/nIBSJsF7OVKoJYJr43uRJzOyJ/SdliIrGz71IpVOCkuBa/ufRRVEVneRDDg== +"@sentry-internal/rrweb-snapshot@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.5.0.tgz#c13faa437cf703568a5dae4ba67e4e0adc356a23" + integrity sha512-7UMeAPLzXCDmB6CnuGUSHQ4TZ1YJc7a4u8HHoQhbL4gYhi2kgsmE5KMkulV5IKOUqZLlwFkzcXTqf37BXcEMUQ== -"@sentry-internal/rrweb-types@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.2.0.tgz#79a76a8d5c436cf18cdee604ab95a9e3049d1949" - integrity sha512-0x7gL9cEF/rrdx1feIORD8+pHihJVA2t1wMRnT+PyiEJZToNJO08Mpbe2rlWgovfK3kGDj5GVFVXsPKGTj8c3A== +"@sentry-internal/rrweb-types@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.5.0.tgz#5ecd52f83b561ae011f2db1f9e3d0ea3273f1b32" + integrity sha512-m9X9F3rOqEDUPaNxlhs6NNDB6GPJf4jbdNqFrKa9Qj567S5iInQTj5yplEUuD84ydpXFgoOTm2Kp2sLhP8SEwg== dependencies: - "@sentry-internal/rrweb-snapshot" "2.2.0" + "@sentry-internal/rrweb-snapshot" "2.5.0" -"@sentry-internal/rrweb@2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.2.0.tgz#f9b538f16718b88f6ca99b3393c5fbebc274bc06" - integrity sha512-gcdC9uuw6b5WZp1wsqC8p8yBN8bmiZN7X7UaxMrhBwH9TifbYxK1gNbgeKOI/GzZF9OVbduXYm3UDt9ZS1njGg== +"@sentry-internal/rrweb@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.5.0.tgz#32c8496bd113bc93bef6d8f11100e8b5193e4225" + integrity sha512-kl6Tyk77j+zREgZVg0YH8YOn7xQxYB+1RRzqRL8rMrKou64uRSGVLMjhb4MR3i4hizkp4ILV3TinaondSx6v8w== dependencies: - "@sentry-internal/rrdom" "2.2.0" - "@sentry-internal/rrweb-snapshot" "2.2.0" - "@sentry-internal/rrweb-types" "2.2.0" + "@sentry-internal/rrdom" "2.5.0" + "@sentry-internal/rrweb-snapshot" "2.5.0" + "@sentry-internal/rrweb-types" "2.5.0" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" From 2b4d56b788dabdccf337b786ec6035658b782a38 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 12 Dec 2023 17:14:31 +0100 Subject: [PATCH 07/15] ref(core): Replace `Scope.clone()` with non-static `scope.clone()` (#9801) To avoid this static method there. It is deprecated to use `Scope.clone()` (but still works), but better to just use `scope.clone()` or `new Scope()` directly. --- packages/core/src/hub.ts | 4 +- packages/core/src/scope.ts | 42 +++++++++++-------- packages/core/src/utils/prepareEvent.ts | 15 +++++-- .../test/spanprocessor.test.ts | 12 +----- packages/opentelemetry/src/contextManager.ts | 3 +- packages/opentelemetry/src/custom/client.ts | 12 ++++-- packages/opentelemetry/src/custom/hub.ts | 18 +------- packages/opentelemetry/src/custom/scope.ts | 40 ++++++++++-------- packages/opentelemetry/src/spanExporter.ts | 4 +- .../opentelemetry/test/custom/scope.test.ts | 2 + 10 files changed, 78 insertions(+), 74 deletions(-) diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 8306d033cd75..13d5fd059e93 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -142,7 +142,7 @@ export class Hub implements HubInterface { */ public pushScope(): Scope { // We want to clone the content of prev scope - const scope = Scope.clone(this.getScope()); + const scope = this.getScope().clone(); this.getStack().push({ client: this.getClient(), scope, @@ -578,7 +578,7 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub( // If there's no hub on current domain, or it's an old API, assign a new one if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { const globalHubTopStack = parent.getStackTop(); - setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, Scope.clone(globalHubTopStack.scope))); + setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, globalHubTopStack.scope.clone())); } } diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 266087dd6090..ae6fe70e3185 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -110,27 +110,33 @@ export class Scope implements ScopeInterface { /** * Inherit values from the parent scope. - * @param scope to clone. + * @deprecated Use `scope.clone()` and `new Scope()` instead. */ public static clone(scope?: Scope): Scope { + return scope ? scope.clone() : new Scope(); + } + + /** + * Clone this scope instance. + */ + public clone(): Scope { const newScope = new Scope(); - if (scope) { - newScope._breadcrumbs = [...scope._breadcrumbs]; - newScope._tags = { ...scope._tags }; - newScope._extra = { ...scope._extra }; - newScope._contexts = { ...scope._contexts }; - newScope._user = scope._user; - newScope._level = scope._level; - newScope._span = scope._span; - newScope._session = scope._session; - newScope._transactionName = scope._transactionName; - newScope._fingerprint = scope._fingerprint; - newScope._eventProcessors = [...scope._eventProcessors]; - newScope._requestSession = scope._requestSession; - newScope._attachments = [...scope._attachments]; - newScope._sdkProcessingMetadata = { ...scope._sdkProcessingMetadata }; - newScope._propagationContext = { ...scope._propagationContext }; - } + newScope._breadcrumbs = [...this._breadcrumbs]; + newScope._tags = { ...this._tags }; + newScope._extra = { ...this._extra }; + newScope._contexts = { ...this._contexts }; + newScope._user = this._user; + newScope._level = this._level; + newScope._span = this._span; + newScope._session = this._session; + newScope._transactionName = this._transactionName; + newScope._fingerprint = this._fingerprint; + newScope._eventProcessors = [...this._eventProcessors]; + newScope._requestSession = this._requestSession; + newScope._attachments = [...this._attachments]; + newScope._sdkProcessingMetadata = { ...this._sdkProcessingMetadata }; + newScope._propagationContext = { ...this._propagationContext }; + return newScope; } diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 6e25350202b4..46b26070653f 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -74,10 +74,7 @@ export function prepareEvent( // If we have scope given to us, use it as the base for further modifications. // This allows us to prevent unnecessary copying of data if `captureContext` is not provided. - let finalScope = scope; - if (hint.captureContext) { - finalScope = Scope.clone(finalScope).update(hint.captureContext); - } + const finalScope = getFinalScope(scope, hint.captureContext); if (hint.mechanism) { addExceptionMechanism(prepared, hint.mechanism); @@ -349,6 +346,16 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number): return normalized; } +function getFinalScope(scope: Scope | undefined, captureContext: CaptureContext | undefined): Scope | undefined { + if (!captureContext) { + return scope; + } + + const finalScope = scope ? scope.clone() : new Scope(); + finalScope.update(captureContext); + return finalScope; +} + /** * Parse either an `EventHint` directly, or convert a `CaptureContext` to an `EventHint`. * This is used to allow to update method signatures that used to accept a `CaptureContext` but should now accept an `EventHint`. diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 5361055755ff..9de394d2232d 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -5,15 +5,7 @@ import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import type { SpanStatusType } from '@sentry/core'; -import { - Hub, - Scope, - Span as SentrySpan, - Transaction, - addTracingExtensions, - createTransport, - makeMain, -} from '@sentry/core'; +import { Hub, Span as SentrySpan, Transaction, addTracingExtensions, createTransport, makeMain } from '@sentry/core'; import { NodeClient } from '@sentry/node'; import { resolvedSyncPromise } from '@sentry/utils'; @@ -973,7 +965,7 @@ describe('SentrySpanProcessor', () => { hub = new Hub(client); makeMain(hub); - const newHub = new Hub(client, Scope.clone(hub.getScope())); + const newHub = new Hub(client, hub.getScope().clone()); newHub.configureScope(scope => { scope.setTag('foo', 'bar'); }); diff --git a/packages/opentelemetry/src/contextManager.ts b/packages/opentelemetry/src/contextManager.ts index ca9305dfea9b..3b3a6a280928 100644 --- a/packages/opentelemetry/src/contextManager.ts +++ b/packages/opentelemetry/src/contextManager.ts @@ -1,7 +1,8 @@ import type { Context, ContextManager } from '@opentelemetry/api'; import type { Carrier, Hub } from '@sentry/core'; +import { ensureHubOnCarrier } from '@sentry/core'; -import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './custom/hub'; +import { getCurrentHub, getHubFromCarrier } from './custom/hub'; import { setHubOnContext } from './utils/contextData'; function createNewHub(parent: Hub | undefined): Hub { diff --git a/packages/opentelemetry/src/custom/client.ts b/packages/opentelemetry/src/custom/client.ts index 36648b63c22f..44dd0cf80f4f 100644 --- a/packages/opentelemetry/src/custom/client.ts +++ b/packages/opentelemetry/src/custom/client.ts @@ -3,7 +3,7 @@ import { trace } from '@opentelemetry/api'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { BaseClient, Scope } from '@sentry/core'; import { SDK_VERSION } from '@sentry/core'; -import type { Client, Event, EventHint } from '@sentry/types'; +import type { CaptureContext, Client, Event, EventHint } from '@sentry/types'; import type { OpenTelemetryClient as OpenTelemetryClientInterface } from '../types'; import { OpenTelemetryScope } from './scope'; @@ -65,14 +65,14 @@ export function wrapClientClass< /** * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. - * This uses `Scope.clone()`, which we need to replace with `NodeExperimentalScope.clone()` for this client. + * This uses `Scope.clone()`, which we need to replace with `OpenTelemetryScope.clone()` for this client. */ protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { let actualScope = scope; // Remove `captureContext` hint and instead clone already here if (hint && hint.captureContext) { - actualScope = OpenTelemetryScope.clone(scope); + actualScope = getFinalScope(scope, hint.captureContext); delete hint.captureContext; } @@ -83,3 +83,9 @@ export function wrapClientClass< return OpenTelemetryClient as unknown as WrappedClassConstructor; } /* eslint-enable @typescript-eslint/no-explicit-any */ + +function getFinalScope(scope: Scope | undefined, captureContext: CaptureContext): Scope | undefined { + const finalScope = scope ? scope.clone() : new OpenTelemetryScope(); + finalScope.update(captureContext); + return finalScope; +} diff --git a/packages/opentelemetry/src/custom/hub.ts b/packages/opentelemetry/src/custom/hub.ts index 3ecea85b0a6f..ed37ae75b09f 100644 --- a/packages/opentelemetry/src/custom/hub.ts +++ b/packages/opentelemetry/src/custom/hub.ts @@ -13,19 +13,6 @@ export class OpenTelemetryHub extends Hub { public constructor(client?: Client, scope: Scope = new OpenTelemetryScope()) { super(client, scope); } - - /** - * @inheritDoc - */ - public pushScope(): Scope { - // We want to clone the content of prev scope - const scope = OpenTelemetryScope.clone(this.getScope()); - this.getStack().push({ - client: this.getClient(), - scope, - }); - return scope; - } } /** Custom getClient method that uses the custom hub. */ @@ -110,10 +97,7 @@ export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub( // If there's no hub on current domain, or it's an old API, assign a new one if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { const globalHubTopStack = parent.getStackTop(); - setHubOnCarrier( - carrier, - new OpenTelemetryHub(globalHubTopStack.client, OpenTelemetryScope.clone(globalHubTopStack.scope)), - ); + setHubOnCarrier(carrier, new OpenTelemetryHub(globalHubTopStack.client, globalHubTopStack.scope.clone())); } } diff --git a/packages/opentelemetry/src/custom/scope.ts b/packages/opentelemetry/src/custom/scope.ts index b65aa437b14f..e206ba8d8096 100644 --- a/packages/opentelemetry/src/custom/scope.ts +++ b/packages/opentelemetry/src/custom/scope.ts @@ -23,24 +23,30 @@ export class OpenTelemetryScope extends Scope { * @inheritDoc */ public static clone(scope?: Scope): Scope { + return scope ? scope.clone() : new OpenTelemetryScope(); + } + + /** + * Clone this scope instance. + */ + public clone(): OpenTelemetryScope { const newScope = new OpenTelemetryScope(); - if (scope) { - newScope._breadcrumbs = [...scope['_breadcrumbs']]; - newScope._tags = { ...scope['_tags'] }; - newScope._extra = { ...scope['_extra'] }; - newScope._contexts = { ...scope['_contexts'] }; - newScope._user = scope['_user']; - newScope._level = scope['_level']; - newScope._span = scope['_span']; - newScope._session = scope['_session']; - newScope._transactionName = scope['_transactionName']; - newScope._fingerprint = scope['_fingerprint']; - newScope._eventProcessors = [...scope['_eventProcessors']]; - newScope._requestSession = scope['_requestSession']; - newScope._attachments = [...scope['_attachments']]; - newScope._sdkProcessingMetadata = { ...scope['_sdkProcessingMetadata'] }; - newScope._propagationContext = { ...scope['_propagationContext'] }; - } + newScope._breadcrumbs = [...this['_breadcrumbs']]; + newScope._tags = { ...this['_tags'] }; + newScope._extra = { ...this['_extra'] }; + newScope._contexts = { ...this['_contexts'] }; + newScope._user = this['_user']; + newScope._level = this['_level']; + newScope._span = this['_span']; + newScope._session = this['_session']; + newScope._transactionName = this['_transactionName']; + newScope._fingerprint = this['_fingerprint']; + newScope._eventProcessors = [...this['_eventProcessors']]; + newScope._requestSession = this['_requestSession']; + newScope._attachments = [...this['_attachments']]; + newScope._sdkProcessingMetadata = { ...this['_sdkProcessingMetadata'] }; + newScope._propagationContext = { ...this['_propagationContext'] }; + return newScope; } diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index d3c3b5b3da90..95ad13997fb9 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -112,8 +112,8 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { // Now finish the transaction, which will send it together with all the spans // We make sure to use the current span as the activeSpan for this transaction - const scope = getSpanScope(span); - const forkedScope = OpenTelemetryScope.clone(scope as OpenTelemetryScope | undefined) as OpenTelemetryScope; + const scope = getSpanScope(span) as OpenTelemetryScope | undefined; + const forkedScope = scope ? scope.clone() : new OpenTelemetryScope(); forkedScope.activeSpan = span as unknown as Span; transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), forkedScope); diff --git a/packages/opentelemetry/test/custom/scope.test.ts b/packages/opentelemetry/test/custom/scope.test.ts index 6c96fab2f3c5..41827fcd772d 100644 --- a/packages/opentelemetry/test/custom/scope.test.ts +++ b/packages/opentelemetry/test/custom/scope.test.ts @@ -30,6 +30,7 @@ describe('NodeExperimentalScope', () => { scope['_attachments'] = [{ data: '123', filename: 'test.txt' }]; scope['_sdkProcessingMetadata'] = { sdk: 'bar' }; + // eslint-disable-next-line deprecation/deprecation const scope2 = OpenTelemetryScope.clone(scope); expect(scope2).toBeInstanceOf(OpenTelemetryScope); @@ -68,6 +69,7 @@ describe('NodeExperimentalScope', () => { }); it('clone() works without existing scope', () => { + // eslint-disable-next-line deprecation/deprecation const scope = OpenTelemetryScope.clone(undefined); expect(scope).toBeInstanceOf(OpenTelemetryScope); From 13e3425d8a1e4e862d9d010b7293ba869b041f30 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 12 Dec 2023 17:43:39 +0100 Subject: [PATCH 08/15] feat(types): Add profile envelope types (#9798) --- packages/browser/src/profiling/integration.ts | 4 ++-- packages/browser/src/profiling/utils.ts | 5 ++--- packages/types/src/envelope.ts | 5 ++++- packages/types/src/index.ts | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 3f5251c4583f..326af29492cf 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -1,4 +1,4 @@ -import type { EventProcessor, Hub, Integration, Transaction } from '@sentry/types'; +import type { EventEnvelope, EventProcessor, Hub, Integration, Transaction } from '@sentry/types'; import type { Profile } from '@sentry/types/src/profiling'; import { logger } from '@sentry/utils'; @@ -110,7 +110,7 @@ export class BrowserProfilingIntegration implements Integration { } } - addProfilesToEnvelope(envelope, profilesToAddToEnvelope); + addProfilesToEnvelope(envelope as EventEnvelope, profilesToAddToEnvelope); }); } else { logger.warn('[Profiling] Client does not support hooks, profiling will be disabled'); diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 1aac3d397ca7..3edb82e0b539 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import { DEFAULT_ENVIRONMENT, getClient, getCurrentHub } from '@sentry/core'; -import type { DebugImage, Envelope, Event, StackFrame, StackParser, Transaction } from '@sentry/types'; +import type { DebugImage, Envelope, Event, EventEnvelope, StackFrame, StackParser, Transaction } from '@sentry/types'; import type { Profile, ThreadCpuProfile } from '@sentry/types/src/profiling'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, forEachEnvelopeItem, logger, uuid4 } from '@sentry/utils'; @@ -300,13 +300,12 @@ export function convertJSSelfProfileToSampledFormat(input: JSSelfProfile): Profi * Adds items to envelope if they are not already present - mutates the envelope. * @param envelope */ -export function addProfilesToEnvelope(envelope: Envelope, profiles: Profile[]): Envelope { +export function addProfilesToEnvelope(envelope: EventEnvelope, profiles: Profile[]): Envelope { if (!profiles.length) { return envelope; } for (const profile of profiles) { - // @ts-expect-error untyped envelope envelope[1].push([{ type: 'profile' }, profile]); } return envelope; diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index fbe593dd21f9..8d47206f4217 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -3,6 +3,7 @@ import type { ClientReport } from './clientreport'; import type { DsnComponents } from './dsn'; import type { Event } from './event'; import type { FeedbackEvent } from './feedback'; +import type { Profile } from './profiling'; import type { ReplayEvent, ReplayRecordingData } from './replay'; import type { SdkInfo } from './sdkinfo'; import type { SerializedSession, Session, SessionAggregates } from './session'; @@ -77,6 +78,7 @@ type ReplayEventItemHeaders = { type: 'replay_event' }; type ReplayRecordingItemHeaders = { type: 'replay_recording'; length: number }; type CheckInItemHeaders = { type: 'check_in' }; type StatsdItemHeaders = { type: 'statsd' }; +type ProfileItemHeaders = { type: 'profile' }; export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; @@ -91,6 +93,7 @@ type ReplayEventItem = BaseEnvelopeItem; type ReplayRecordingItem = BaseEnvelopeItem; export type StatsdItem = BaseEnvelopeItem; export type FeedbackItem = BaseEnvelopeItem; +export type ProfileItem = BaseEnvelopeItem; export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext }; type SessionEnvelopeHeaders = { sent_at: string }; @@ -101,7 +104,7 @@ type StatsdEnvelopeHeaders = BaseEnvelopeHeaders; export type EventEnvelope = BaseEnvelope< EventEnvelopeHeaders, - EventItem | AttachmentItem | UserFeedbackItem | FeedbackItem + EventItem | AttachmentItem | UserFeedbackItem | FeedbackItem | ProfileItem >; export type SessionEnvelope = BaseEnvelope; export type ClientReportEnvelope = BaseEnvelope; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 615bf71ff785..5603854170f5 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -44,6 +44,7 @@ export type { CheckInEnvelope, StatsdItem, StatsdEnvelope, + ProfileItem, } from './envelope'; export type { ExtendedError } from './error'; export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './event'; From a05de17fb8be3cf0eb6e715de2dbfe94356d50af Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 12 Dec 2023 10:25:40 -0800 Subject: [PATCH 09/15] feat(replay): Capture hydration error breadcrumb (#9759) Adds a hydration error breadcrumb for nextjs / other react ssr frameworks. fixes #9649 --- .../src/coreHandlers/handleBeforeSendEvent.ts | 42 ++++++++++ packages/replay/src/replay.ts | 2 +- .../replay/src/util/addGlobalListeners.ts | 2 + .../handleBeforeSendEvent.test.ts | 80 +++++++++++++++++++ 4 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 packages/replay/src/coreHandlers/handleBeforeSendEvent.ts create mode 100644 packages/replay/test/integration/coreHandlers/handleBeforeSendEvent.test.ts diff --git a/packages/replay/src/coreHandlers/handleBeforeSendEvent.ts b/packages/replay/src/coreHandlers/handleBeforeSendEvent.ts new file mode 100644 index 000000000000..d7276897497e --- /dev/null +++ b/packages/replay/src/coreHandlers/handleBeforeSendEvent.ts @@ -0,0 +1,42 @@ +import type { ErrorEvent, Event } from '@sentry/types'; + +import type { ReplayContainer } from '../types'; +import { createBreadcrumb } from '../util/createBreadcrumb'; +import { isErrorEvent } from '../util/eventUtils'; +import { addBreadcrumbEvent } from './util/addBreadcrumbEvent'; + +type BeforeSendEventCallback = (event: Event) => void; + +/** + * Returns a listener to be added to `client.on('afterSendErrorEvent, listener)`. + */ +export function handleBeforeSendEvent(replay: ReplayContainer): BeforeSendEventCallback { + return (event: Event) => { + if (!replay.isEnabled() || !isErrorEvent(event)) { + return; + } + + handleHydrationError(replay, event); + }; +} + +function handleHydrationError(replay: ReplayContainer, event: ErrorEvent): void { + const exceptionValue = event.exception && event.exception.values && event.exception.values[0].value; + if (typeof exceptionValue !== 'string') { + return; + } + + if ( + // Only matches errors in production builds of react-dom + // Example https://reactjs.org/docs/error-decoder.html?invariant=423 + exceptionValue.match(/reactjs\.org\/docs\/error-decoder\.html\?invariant=(418|419|422|423|425)/) || + // Development builds of react-dom + // Example Text: content did not match. Server: "A" Client: "B" + exceptionValue.match(/(hydration|content does not match|did not match)/i) + ) { + const breadcrumb = createBreadcrumb({ + category: 'replay.hydrate-error', + }); + addBreadcrumbEvent(replay, breadcrumb); + } +} diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 635db47245d6..8e1740845c7b 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { EventType, record } from '@sentry-internal/rrweb'; import { captureException, getClient, getCurrentHub } from '@sentry/core'; -import type { ReplayRecordingMode, Transaction } from '@sentry/types'; +import type { Event as SentryEvent, ReplayRecordingMode, Transaction } from '@sentry/types'; import { logger } from '@sentry/utils'; import { diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts index 828731bf814f..fac2b278e666 100644 --- a/packages/replay/src/util/addGlobalListeners.ts +++ b/packages/replay/src/util/addGlobalListeners.ts @@ -4,6 +4,7 @@ import type { Client, DynamicSamplingContext } from '@sentry/types'; import { addClickKeypressInstrumentationHandler, addHistoryInstrumentationHandler } from '@sentry/utils'; import { handleAfterSendEvent } from '../coreHandlers/handleAfterSendEvent'; +import { handleBeforeSendEvent } from '../coreHandlers/handleBeforeSendEvent'; import { handleDomListener } from '../coreHandlers/handleDom'; import { handleGlobalEventListener } from '../coreHandlers/handleGlobalEvent'; import { handleHistorySpanListener } from '../coreHandlers/handleHistory'; @@ -35,6 +36,7 @@ export function addGlobalListeners(replay: ReplayContainer): void { // If a custom client has no hooks yet, we continue to use the "old" implementation if (hasHooks(client)) { + client.on('beforeSendEvent', handleBeforeSendEvent(replay)); client.on('afterSendEvent', handleAfterSendEvent(replay)); client.on('createDsc', (dsc: DynamicSamplingContext) => { const replayId = replay.getSessionId(); diff --git a/packages/replay/test/integration/coreHandlers/handleBeforeSendEvent.test.ts b/packages/replay/test/integration/coreHandlers/handleBeforeSendEvent.test.ts new file mode 100644 index 000000000000..92cdc5a88698 --- /dev/null +++ b/packages/replay/test/integration/coreHandlers/handleBeforeSendEvent.test.ts @@ -0,0 +1,80 @@ +import { handleBeforeSendEvent } from '../../../src/coreHandlers/handleBeforeSendEvent'; +import type { ReplayContainer } from '../../../src/replay'; +import { Error } from '../../fixtures/error'; +import { resetSdkMock } from '../../mocks/resetSdkMock'; +import { useFakeTimers } from '../../utils/use-fake-timers'; + +useFakeTimers(); +let replay: ReplayContainer; + +describe('Integration | coreHandlers | handleBeforeSendEvent', () => { + afterEach(() => { + replay.stop(); + }); + + it('adds a hydration breadcrumb on development hydration error', async () => { + ({ replay } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + sentryOptions: { + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + }, + })); + + const handler = handleBeforeSendEvent(replay); + const addBreadcrumbSpy = jest.spyOn(replay, 'throttledAddEvent'); + + const error = Error(); + error.exception.values[0].value = 'Text content did not match. Server: "A" Client: "B"'; + handler(error); + + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenCalledWith({ + data: { + payload: { + category: 'replay.hydrate-error', + timestamp: expect.any(Number), + type: 'default', + }, + tag: 'breadcrumb', + }, + timestamp: expect.any(Number), + type: 5, + }); + }); + + it('adds a hydration breadcrumb on production hydration error', async () => { + ({ replay } = await resetSdkMock({ + replayOptions: { + stickySession: false, + }, + sentryOptions: { + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + }, + })); + + const handler = handleBeforeSendEvent(replay); + const addBreadcrumbSpy = jest.spyOn(replay, 'throttledAddEvent'); + + const error = Error(); + error.exception.values[0].value = 'https://reactjs.org/docs/error-decoder.html?invariant=423'; + handler(error); + + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenCalledWith({ + data: { + payload: { + category: 'replay.hydrate-error', + timestamp: expect.any(Number), + type: 'default', + }, + tag: 'breadcrumb', + }, + timestamp: expect.any(Number), + type: 5, + }); + }); +}); From 380a552d3646e94b2f2f41cc995bb6c365137fc1 Mon Sep 17 00:00:00 2001 From: Scott Cooper Date: Tue, 12 Dec 2023 13:35:11 -0800 Subject: [PATCH 10/15] chore(vscode): Use new code action values `"explicit"` (#9805) latest vscode will attempt to correct these values every time you open the project. **The new options are:** explicit - Triggers Code Actions when explicitly saved. Same as true. always - Triggers Code Actions when explicitly saved and on Auto Saves from window or focus changes. never - Never triggers Code Actions on save. Same as false. https://code.visualstudio.com/updates/v1_85#_code-actions-on-save-and-auto --- .vscode/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d60ded5fea96..7835767bad18 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,8 +30,8 @@ ], "deno.enablePaths": ["packages/deno/test"], "editor.codeActionsOnSave": { - "source.organizeImports.biome": true, - "quickfix.biome": true + "source.organizeImports.biome": "explicit", + "quickfix.biome": "explicit" }, "editor.defaultFormatter": "biomejs.biome" } From 7303cdacb3ba2da1e71844e9daa5ffdb91d9a8a7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 13 Dec 2023 08:42:46 +0100 Subject: [PATCH 11/15] feat: Add top level `getCurrentScope()` method (#9800) This can be used instead of `getCurrentHub().getScope()`, and is a step towards getting rid of `getCurrentHub()` as a top level API. --- packages/astro/src/index.server.ts | 1 + packages/browser/src/exports.ts | 1 + packages/bun/src/index.ts | 1 + packages/core/src/exports.ts | 7 +++++++ packages/core/src/index.ts | 1 + packages/deno/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/serverless/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 10 files changed, 16 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index a0c18c0cb6e7..c62590180266 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -25,6 +25,7 @@ export { getHubFromCarrier, getCurrentHub, getClient, + getCurrentScope, Hub, makeMain, Scope, diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 0804dbe49ac0..367905a2d770 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -36,6 +36,7 @@ export { getHubFromCarrier, getCurrentHub, getClient, + getCurrentScope, Hub, lastEventId, makeMain, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 302d36fb0b62..5a4260aaec38 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -42,6 +42,7 @@ export { getHubFromCarrier, getCurrentHub, getClient, + getCurrentScope, Hub, lastEventId, makeMain, diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 6d1e80600a75..6f71e7dfbccb 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -308,3 +308,10 @@ export function lastEventId(): string | undefined { export function getClient(): C | undefined { return getCurrentHub().getClient(); } + +/** + * Get the currently active scope. + */ +export function getCurrentScope(): Scope { + return getCurrentHub().getScope(); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 07871633f16c..f261a2963364 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -26,6 +26,7 @@ export { setUser, withScope, getClient, + getCurrentScope, } from './exports'; export { getCurrentHub, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 846ad000bcaf..52a878bdde17 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -41,6 +41,7 @@ export { getHubFromCarrier, getCurrentHub, getClient, + getCurrentScope, Hub, lastEventId, makeMain, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 28fa7e6e603d..9963258e48bb 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -41,6 +41,7 @@ export { getHubFromCarrier, getCurrentHub, getClient, + getCurrentScope, Hub, lastEventId, makeMain, diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 2ac8f77274a5..6df0dbedb2c3 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -29,6 +29,7 @@ export { getActiveTransaction, getCurrentHub, getClient, + getCurrentScope, getHubFromCarrier, makeMain, setContext, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index f584759da054..b75fa24ebe5b 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -22,6 +22,7 @@ export { getHubFromCarrier, getCurrentHub, getClient, + getCurrentScope, Hub, makeMain, Scope, diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 3a13f97beb03..ffce59b5dceb 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -41,6 +41,7 @@ export { getHubFromCarrier, getCurrentHub, getClient, + getCurrentScope, Hub, lastEventId, makeMain, From be32c1edd5eabecde6cbe6bcf8047f7b47d976bf Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 13 Dec 2023 11:31:29 +0100 Subject: [PATCH 12/15] fix(nextjs): Catch rejecting flushes (#9811) --- packages/nextjs/src/common/_error.ts | 9 ++----- .../src/common/utils/edgeWrapperUtils.ts | 3 ++- .../common/withServerActionInstrumentation.ts | 5 ++-- .../src/common/wrapRouteHandlerWithSentry.ts | 3 ++- .../common/wrapServerComponentWithSentry.ts | 27 ++++++++++--------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/nextjs/src/common/_error.ts b/packages/nextjs/src/common/_error.ts index 1ddd5ea63e01..1f114494567b 100644 --- a/packages/nextjs/src/common/_error.ts +++ b/packages/nextjs/src/common/_error.ts @@ -1,5 +1,6 @@ import { captureException, getClient, withScope } from '@sentry/core'; import type { NextPageContext } from 'next'; +import { flushQueue } from './utils/responseEnd'; type ContextOrProps = { req?: NextPageContext['req']; @@ -9,12 +10,6 @@ type ContextOrProps = { statusCode?: number; }; -/** Platform-agnostic version of `flush` */ -function flush(timeout?: number): PromiseLike { - const client = getClient(); - return client ? client.flush(timeout) : Promise.resolve(false); -} - /** * Capture the exception passed by nextjs to the `_error` page, adding context data as appropriate. * @@ -60,5 +55,5 @@ export async function captureUnderscoreErrorException(contextOrProps: ContextOrP // In case this is being run as part of a serverless function (as is the case with the server half of nextjs apps // deployed to vercel), make sure the error gets sent to Sentry before the lambda exits. - await flush(2000); + await flushQueue(); } diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 72dc2f142365..008f8629f3ab 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -10,6 +10,7 @@ import { import type { EdgeRouteHandler } from '../../edge/types'; import { DEBUG_BUILD } from '../debug-build'; +import { flushQueue } from './responseEnd'; /** * Wraps a function on the edge runtime with error and performance monitoring. @@ -97,7 +98,7 @@ export function withEdgeWrapping( } finally { span?.finish(); currentScope?.setSpan(prevSpan); - await flush(2000); + await flushQueue(); } }; } diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index dcc1c7d29382..eafcff7b9075 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -3,6 +3,7 @@ import { logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; +import { flushQueue } from './utils/responseEnd'; interface Options { formData?: FormData; @@ -118,11 +119,11 @@ async function withServerActionInstrumentationImplementation any>( if (!platformSupportsStreaming() || process.env.NEXT_RUNTIME === 'edge') { // 1. Edge tranpsort requires manual flushing // 2. Lambdas require manual flushing to prevent execution freeze before the event is sent - await flush(1000); + await flushQueue(); } } diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index beb17980de5e..dade931bf074 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -1,7 +1,6 @@ import { addTracingExtensions, captureException, - flush, getCurrentHub, runWithAsyncContext, startTransaction, @@ -10,6 +9,7 @@ import { tracingContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils' import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; +import { flushQueue } from './utils/responseEnd'; /** * Wraps an `app` directory server component with Sentry error instrumentation. @@ -85,28 +85,31 @@ export function wrapServerComponentWithSentry any> maybePromiseResult = originalFunction.apply(thisArg, args); } catch (e) { handleErrorCase(e); - void flush(); + void flushQueue(); throw e; } if (typeof maybePromiseResult === 'object' && maybePromiseResult !== null && 'then' in maybePromiseResult) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - Promise.resolve(maybePromiseResult).then( - () => { - transaction.finish(); - }, - e => { - handleErrorCase(e); - }, - ); - void flush(); + Promise.resolve(maybePromiseResult) + .then( + () => { + transaction.finish(); + }, + e => { + handleErrorCase(e); + }, + ) + .finally(() => { + void flushQueue(); + }); // It is very important that we return the original promise here, because Next.js attaches various properties // to that promise and will throw if they are not on the returned value. return maybePromiseResult; } else { transaction.finish(); - void flush(); + void flushQueue(); return maybePromiseResult; } }); From 35f177ac13519ccf7992e21fe89958ee66ff01d3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 13 Dec 2023 11:59:27 +0100 Subject: [PATCH 13/15] ref(deno): Update deno integrations to avoid `setupOnce` (#9812) Where possible, we should use the new `processEvent` and/or `setup` hooks of the integrations, instead of `setupOnce`. This updates this for the deno integrations. --- packages/deno/src/integrations/context.ts | 9 ++++-- .../deno/src/integrations/contextlines.ts | 9 ++++-- .../deno/src/integrations/normalizepaths.ts | 31 ++++++++++--------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts index 49269c81be4e..f1c29dddda2b 100644 --- a/packages/deno/src/integrations/context.ts +++ b/packages/deno/src/integrations/context.ts @@ -58,7 +58,12 @@ export class DenoContext implements Integration { public name: string = DenoContext.id; /** @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { - addGlobalEventProcessor(async (event: Event) => denoRuntime(event)); + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void { + // noop + } + + /** @inheritDoc */ + public processEvent(event: Event): Promise { + return denoRuntime(event); } } diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts index 10131b77ca27..8c6ef510fd2e 100644 --- a/packages/deno/src/integrations/contextlines.ts +++ b/packages/deno/src/integrations/contextlines.ts @@ -67,8 +67,13 @@ export class ContextLines implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { - addGlobalEventProcessor(event => this.addSourceContext(event)); + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void { + // noop + } + + /** @inheritDoc */ + public processEvent(event: Event): Promise { + return this.addSourceContext(event); } /** Processes an event and adds context lines */ diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts index bf8a3986c93d..ab705a3a20a0 100644 --- a/packages/deno/src/integrations/normalizepaths.ts +++ b/packages/deno/src/integrations/normalizepaths.ts @@ -72,29 +72,32 @@ export class NormalizePaths implements Integration { public name: string = NormalizePaths.id; /** @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void { + // noop + } + + /** @inheritDoc */ + public processEvent(event: Event): Event | null { // This error.stack hopefully contains paths that traverse the app cwd const error = new Error(); - addGlobalEventProcessor((event: Event): Event | null => { - const appRoot = getAppRoot(error); + const appRoot = getAppRoot(error); - if (appRoot) { - for (const exception of event.exception?.values || []) { - for (const frame of exception.stacktrace?.frames || []) { - if (frame.filename && frame.in_app) { - const startIndex = frame.filename.indexOf(appRoot); + if (appRoot) { + for (const exception of event.exception?.values || []) { + for (const frame of exception.stacktrace?.frames || []) { + if (frame.filename && frame.in_app) { + const startIndex = frame.filename.indexOf(appRoot); - if (startIndex > -1) { - const endIndex = startIndex + appRoot.length; - frame.filename = `app://${frame.filename.substring(endIndex)}`; - } + if (startIndex > -1) { + const endIndex = startIndex + appRoot.length; + frame.filename = `app://${frame.filename.substring(endIndex)}`; } } } } + } - return event; - }); + return event; } } From fe24eb5eefa9d27b14b2b6f9ebd1debca1c208fb Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 13 Dec 2023 13:18:14 +0100 Subject: [PATCH 14/15] fix(astro): Avoid RegExp creation during route interpolation (#9815) - iterate over route segments to replace parameter values - decode raw url to match previously unmatched param values - prioritize multi-segment [rest parameters](https://docs.astro.build/en/core-concepts/routing/#rest-parameters) before iterating over individual segments --- packages/astro/src/server/middleware.ts | 94 ++++++++++++++++--- packages/astro/test/server/middleware.test.ts | 60 ++++++++++++ 2 files changed, 140 insertions(+), 14 deletions(-) diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 8ff33382d916..37603a2cdb62 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -7,12 +7,7 @@ import { startSpan, } from '@sentry/node'; import type { Hub, Span } from '@sentry/types'; -import { - addNonEnumerableProperty, - objectify, - stripUrlQueryAndFragment, - tracingContextFromHeaders, -} from '@sentry/utils'; +import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; import { getTracingMetaTags } from './meta'; @@ -64,7 +59,11 @@ type AstroLocalsWithSentry = Record & { }; export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseHandler = options => { - const handlerOptions = { trackClientIp: false, trackHeaders: false, ...options }; + const handlerOptions = { + trackClientIp: false, + trackHeaders: false, + ...options, + }; return async (ctx, next) => { // if there is an active span, we know that this handle call is nested and hence @@ -113,18 +112,19 @@ async function instrumentRequest( } try { + const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); // storing res in a variable instead of directly returning is necessary to // invoke the catch block if next() throws const res = await startSpan( { ...traceCtx, - name: `${method} ${interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params)}`, + name: `${method} ${interpolatedRoute || ctx.url.pathname}`, op: 'http.server', origin: 'auto.http.astro', status: 'ok', metadata: { ...traceCtx?.metadata, - source: 'route', + source: interpolatedRoute ? 'route' : 'url', }, data: { method, @@ -202,10 +202,76 @@ function addMetaTagToHead(htmlChunk: string, hub: Hub, span?: Span): string { * Best we can do to get a route name instead of a raw URL. * * exported for testing + * + * @param rawUrlPathname - The raw URL pathname, e.g. '/users/123/details' + * @param params - The params object, e.g. `{ userId: '123' }` + * + * @returns The interpolated route, e.g. '/users/[userId]/details' */ -export function interpolateRouteFromUrlAndParams(rawUrl: string, params: APIContext['params']): string { - return Object.entries(params).reduce((interpolateRoute, value) => { - const [paramId, paramValue] = value; - return interpolateRoute.replace(new RegExp(`(/|-)${paramValue}(/|-|$)`), `$1[${paramId}]$2`); - }, rawUrl); +export function interpolateRouteFromUrlAndParams( + rawUrlPathname: string, + params: APIContext['params'], +): string | undefined { + const decodedUrlPathname = tryDecodeUrl(rawUrlPathname); + if (!decodedUrlPathname) { + return undefined; + } + + // Invert params map so that the param values are the keys + // differentiate between rest params spanning multiple url segments + // and normal, single-segment params. + const valuesToMultiSegmentParams: Record = {}; + const valuesToParams: Record = {}; + Object.entries(params).forEach(([key, value]) => { + if (!value) { + return; + } + if (value.includes('/')) { + valuesToMultiSegmentParams[value] = key; + return; + } + valuesToParams[value] = key; + }); + + function replaceWithParamName(segment: string): string { + const param = valuesToParams[segment]; + if (param) { + return `[${param}]`; + } + return segment; + } + + // before we match single-segment params, we first replace multi-segment params + const urlWithReplacedMultiSegmentParams = Object.keys(valuesToMultiSegmentParams).reduce((acc, key) => { + return acc.replace(key, `[${valuesToMultiSegmentParams[key]}]`); + }, decodedUrlPathname); + + return urlWithReplacedMultiSegmentParams + .split('/') + .map(segment => { + if (!segment) { + return ''; + } + + if (valuesToParams[segment]) { + return replaceWithParamName(segment); + } + + // astro permits multiple params in a single path segment, e.g. /[foo]-[bar]/ + const segmentParts = segment.split('-'); + if (segmentParts.length > 1) { + return segmentParts.map(part => replaceWithParamName(part)).join('-'); + } + + return segment; + }) + .join('/'); +} + +function tryDecodeUrl(url: string): string | undefined { + try { + return decodeURI(url); + } catch { + return undefined; + } } diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 19ee0ef1b5c6..dc3b0139b965 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -69,6 +69,43 @@ describe('sentryMiddleware', () => { expect(resultFromNext).toStrictEqual(nextResult); }); + it("sets source route if the url couldn't be decoded correctly", async () => { + const middleware = handleRequest(); + const ctx = { + request: { + method: 'GET', + url: '/a%xx', + headers: new Headers(), + }, + url: { pathname: 'a%xx', href: 'http://localhost:1234/a%xx' }, + params: {}, + }; + const next = vi.fn(() => nextResult); + + // @ts-expect-error, a partial ctx object is fine here + const resultFromNext = middleware(ctx, next); + + expect(startSpanSpy).toHaveBeenCalledWith( + { + data: { + method: 'GET', + url: 'http://localhost:1234/a%xx', + }, + metadata: { + source: 'url', + }, + name: 'GET a%xx', + op: 'http.server', + origin: 'auto.http.astro', + status: 'ok', + }, + expect.any(Function), // the `next` function + ); + + expect(next).toHaveBeenCalled(); + expect(resultFromNext).toStrictEqual(nextResult); + }); + it('throws and sends an error to sentry if `next()` throws', async () => { const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException'); @@ -299,15 +336,31 @@ describe('sentryMiddleware', () => { describe('interpolateRouteFromUrlAndParams', () => { it.each([ + ['/', {}, '/'], ['/foo/bar', {}, '/foo/bar'], ['/users/123', { id: '123' }, '/users/[id]'], ['/users/123', { id: '123', foo: 'bar' }, '/users/[id]'], ['/lang/en-US', { lang: 'en', region: 'US' }, '/lang/[lang]-[region]'], ['/lang/en-US/posts', { lang: 'en', region: 'US' }, '/lang/[lang]-[region]/posts'], + // edge cases that astro doesn't support + ['/lang/-US', { region: 'US' }, '/lang/-[region]'], + ['/lang/en-', { lang: 'en' }, '/lang/[lang]-'], ])('interpolates route from URL and params %s', (rawUrl, params, expectedRoute) => { expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute); }); + it.each([ + ['/(a+)+/aaaaaaaaa!', { id: '(a+)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'], + ['/([a-zA-Z]+)*/aaaaaaaaa!', { id: '([a-zA-Z]+)*', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'], + ['/(a|aa)+/aaaaaaaaa!', { id: '(a|aa)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'], + ['/(a|a?)+/aaaaaaaaa!', { id: '(a|a?)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'], + // with URL encoding + ['/(a%7Caa)+/aaaaaaaaa!', { id: '(a|aa)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'], + ['/(a%7Ca?)+/aaaaaaaaa!', { id: '(a|a?)+', slug: 'aaaaaaaaa!' }, '/[id]/[slug]'], + ])('handles regex characters in param values correctly %s', (rawUrl, params, expectedRoute) => { + expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute); + }); + it('handles params across multiple URL segments in catchall routes', () => { // Ideally, Astro would let us know that this is a catchall route so we can make the param [...catchall] but it doesn't expect( @@ -324,4 +377,11 @@ describe('interpolateRouteFromUrlAndParams', () => { const expectedRoute = '/usernames/[name]'; expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute); }); + + it('handles set but undefined params', () => { + const rawUrl = '/usernames/user'; + const params = { name: undefined, name2: '' }; + const expectedRoute = '/usernames/user'; + expect(interpolateRouteFromUrlAndParams(rawUrl, params)).toEqual(expectedRoute); + }); }); From 28ba0196f5a8f78a184cda9bb7420d5f95af2775 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 13 Dec 2023 13:23:06 +0100 Subject: [PATCH 15/15] meta: Update CHANGELOG for 7.87.0 remove refs Update CHANGELOG.md Co-authored-by: Francesco Novy --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41aebacf874f..dc6d013d9b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.87.0 + +- feat: Add top level `getCurrentScope()` method (#9800) +- feat(replay): Bump `rrweb` to 2.5.0 (#9803) +- feat(replay): Capture hydration error breadcrumb (#9759) +- feat(types): Add profile envelope types (#9798) +- fix(astro): Avoid RegExp creation during route interpolation (#9815) +- fix(browser): Avoid importing from `./exports` (#9775) +- fix(nextjs): Catch rejecting flushes (#9811) +- fix(nextjs): Fix devserver CORS blockage when `assetPrefix` is defined (#9766) +- fix(node): Capture errors in tRPC middleware (#9782) + ## 7.86.0 - feat(core): Use SDK_VERSION for hub API version (#9732)