From 383c8161067452b52b8c1a6d4b15273c9f694ea6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 14 Dec 2023 12:55:11 -0500 Subject: [PATCH 01/34] build: Enable codecov AI PR review (#9850) See: https://github.com/codecov/vscode/pull/35/files#diff-084bdbbae104c91c8af1a0b632b46b9e797366d5427a2f11aa0332007866af39R2334-R2359 This is undocumented and in alpha, but I figured we should try testing it out! --- codecov.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/codecov.yml b/codecov.yml index fcc0885b060b..1013e1b11e24 100644 --- a/codecov.yml +++ b/codecov.yml @@ -3,6 +3,11 @@ codecov: notify: require_ci_to_pass: no +ai_pr_review: + enabled: true + method: "label" + label_name: "ci-codecov-ai-review" + coverage: precision: 2 round: down From 1e8d2b3afdf413006459daf771f1e3c59bebc89a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 15 Dec 2023 10:16:29 +0100 Subject: [PATCH 02/34] ref: Use `getCurrentScope()`/`hub.getScope()` instead of `configureScope()` (#9846) In preparation for https://github.com/getsentry/sentry-javascript/issues/9841, as we want to deprecate `configureScope`, refactor our own usage of this away. I used a new transformer of sentry-migr8 for this, which did most of the heavy lifting. I only needed to fix the block usage (which is more a stylistic issue than an actual problem). In some follow up, I'll also create a migr8 transform to refactor hub usage away, to e.g. refactor `getCurrentHub().getScope()` to `getCurrentScope()` etc. But for now this is OK I think. --- packages/astro/src/client/sdk.ts | 6 +- packages/astro/src/server/middleware.ts | 6 +- packages/astro/src/server/sdk.ts | 6 +- packages/astro/test/server/middleware.test.ts | 4 +- .../suites/replay/dsc/test.ts | 31 ++++---- .../envelope-header-transaction-name/init.js | 9 +-- .../suites/tracing/envelope-header/init.js | 7 +- packages/browser/test/unit/index.test.ts | 22 ++---- packages/core/src/tracing/idletransaction.ts | 2 +- packages/core/test/lib/hint.test.ts | 4 +- packages/core/test/lib/tracing/errors.test.ts | 6 +- packages/core/test/mocks/integration.ts | 6 +- .../create-next-app/pages/api/success.ts | 2 +- .../node-express-app/src/app.ts | 2 +- packages/nextjs/src/client/index.ts | 21 +++-- .../src/common/wrapPageComponentWithSentry.ts | 57 +++++++------- packages/nextjs/src/server/index.ts | 21 +++-- .../configureScope/clear_scope/scenario.ts | 11 ++- .../configureScope/set_properties/scenario.ts | 9 +-- .../tracing-new/apollo-graphql/scenario.ts | 4 +- .../auto-instrument/mongodb/scenario.ts | 4 +- .../mysql/withConnect/scenario.ts | 4 +- .../mysql/withoutCallback/scenario.ts | 4 +- .../mysql/withoutConnect/scenario.ts | 4 +- .../auto-instrument/pg/scenario.ts | 4 +- .../suites/tracing-new/prisma-orm/scenario.ts | 4 +- .../tracePropagationTargets/scenario.ts | 4 +- .../suites/tracing/apollo-graphql/scenario.ts | 4 +- .../auto-instrument/mongodb/scenario.ts | 4 +- .../tracing/auto-instrument/mysql/scenario.ts | 4 +- .../tracing/auto-instrument/pg/scenario.ts | 4 +- .../suites/tracing/prisma-orm/scenario.ts | 4 +- .../tracePropagationTargets/scenario.ts | 4 +- packages/node/src/handlers.ts | 28 +++---- packages/node/test/eventbuilders.test.ts | 6 +- packages/node/test/index.test.ts | 28 ++----- packages/node/test/integrations/http.test.ts | 10 +-- .../test/spanprocessor.test.ts | 4 +- packages/react/src/redux.ts | 77 +++++++++---------- packages/react/test/redux.test.ts | 10 +-- packages/remix/src/index.client.tsx | 6 +- packages/remix/src/index.server.ts | 6 +- .../src/gcpfunction/cloud_events.ts | 11 ++- packages/serverless/src/gcpfunction/events.ts | 11 ++- packages/serverless/src/gcpfunction/http.ts | 16 ++-- .../serverless/test/__mocks__/@sentry/node.ts | 1 + packages/sveltekit/src/client/sdk.ts | 6 +- packages/sveltekit/src/server/sdk.ts | 6 +- .../test/browser/backgroundtab.test.ts | 4 +- .../test/browser/request.test.ts | 2 +- packages/tracing/test/hub.test.ts | 16 +--- packages/tracing/test/idletransaction.test.ts | 25 +++--- packages/tracing/test/span.test.ts | 8 +- packages/utils/src/eventbuilder.ts | 4 +- packages/utils/test/eventbuilder.test.ts | 11 ++- 55 files changed, 242 insertions(+), 342 deletions(-) diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts index aa32e9dcc095..2fd98b8a96cd 100644 --- a/packages/astro/src/client/sdk.ts +++ b/packages/astro/src/client/sdk.ts @@ -1,6 +1,6 @@ import type { BrowserOptions } from '@sentry/browser'; import { BrowserTracing, init as initBrowserSdk } from '@sentry/browser'; -import { configureScope, hasTracingEnabled } from '@sentry/core'; +import { getCurrentScope, hasTracingEnabled } from '@sentry/core'; import { addOrUpdateIntegration } from '@sentry/utils'; import { applySdkMetadata } from '../common/metadata'; @@ -20,9 +20,7 @@ export function init(options: BrowserOptions): void { initBrowserSdk(options); - configureScope(scope => { - scope.setTag('runtime', 'browser'); - }); + getCurrentScope().setTag('runtime', 'browser'); } function addClientIntegrations(options: BrowserOptions): void { diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 37603a2cdb62..5e3b2de18622 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,8 +1,8 @@ import { captureException, - configureScope, continueTrace, getCurrentHub, + getCurrentScope, runWithAsyncContext, startSpan, } from '@sentry/node'; @@ -106,9 +106,7 @@ async function instrumentRequest( } if (options.trackClientIp) { - configureScope(scope => { - scope.setUser({ ip_address: ctx.clientAddress }); - }); + getCurrentScope().setUser({ ip_address: ctx.clientAddress }); } try { diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts index 8c867ca46fc2..e69d27781ed5 100644 --- a/packages/astro/src/server/sdk.ts +++ b/packages/astro/src/server/sdk.ts @@ -1,4 +1,4 @@ -import { configureScope } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import type { NodeOptions } from '@sentry/node'; import { init as initNodeSdk } from '@sentry/node'; @@ -13,7 +13,5 @@ export function init(options: NodeOptions): void { initNodeSdk(options); - configureScope(scope => { - scope.setTag('runtime', 'node'); - }); + getCurrentScope().setTag('runtime', 'node'); } diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index dc3b0139b965..ef81d69214c5 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -172,7 +172,7 @@ describe('sentryMiddleware', () => { it('attaches client IP and request headers if options are set', async () => { const scope = { setUser: vi.fn(), setPropagationContext: vi.fn() }; // @ts-expect-error, only passing a partial Scope object - const configureScopeSpy = vi.spyOn(SentryNode, 'configureScope').mockImplementation(cb => cb(scope)); + const getCurrentScopeSpy = vi.spyOn(SentryNode, 'getCurrentScope').mockImplementation(() => scope); const middleware = handleRequest({ trackClientIp: true, trackHeaders: true }); const ctx = { @@ -192,7 +192,7 @@ describe('sentryMiddleware', () => { // @ts-expect-error, a partial ctx object is fine here await middleware(ctx, next); - expect(configureScopeSpy).toHaveBeenCalledTimes(1); + expect(getCurrentScopeSpy).toHaveBeenCalledTimes(1); expect(scope.setUser).toHaveBeenCalledWith({ ip_address: '192.168.0.1' }); expect(startSpanSpy).toHaveBeenCalledWith( diff --git a/packages/browser-integration-tests/suites/replay/dsc/test.ts b/packages/browser-integration-tests/suites/replay/dsc/test.ts index ffd2cf1877da..4468a254bde4 100644 --- a/packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -21,6 +21,9 @@ sentryTest( const transactionReq = waitForTransactionRequest(page); + // Wait for this to be available + await page.waitForFunction('!!window.Replay'); + await page.evaluate(() => { (window as unknown as TestWindow).Replay.start(); }); @@ -28,10 +31,9 @@ sentryTest( await waitForReplayRunning(page); await page.evaluate(() => { - (window as unknown as TestWindow).Sentry.configureScope(scope => { - scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); - }); + const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); + scope.setUser({ id: 'user123', segment: 'segmentB' }); + scope.setTransactionName('testTransactionDSC'); }); const req0 = await transactionReq; @@ -74,10 +76,9 @@ sentryTest( await waitForReplayRunning(page); await page.evaluate(() => { - (window as unknown as TestWindow).Sentry.configureScope(scope => { - scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); - }); + const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); + scope.setUser({ id: 'user123', segment: 'segmentB' }); + scope.setTransactionName('testTransactionDSC'); }); const req0 = await transactionReq; @@ -132,10 +133,9 @@ sentryTest( }); await page.evaluate(() => { - (window as unknown as TestWindow).Sentry.configureScope(scope => { - scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); - }); + const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); + scope.setUser({ id: 'user123', segment: 'segmentB' }); + scope.setTransactionName('testTransactionDSC'); }); const req0 = await transactionReq; @@ -181,10 +181,9 @@ sentryTest( const transactionReq = waitForTransactionRequest(page); await page.evaluate(async () => { - (window as unknown as TestWindow).Sentry.configureScope(scope => { - scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); - }); + const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); + scope.setUser({ id: 'user123', segment: 'segmentB' }); + scope.setTransactionName('testTransactionDSC'); }); const req0 = await transactionReq; diff --git a/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js b/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js index efb7b577f75b..7d000c0ac2cd 100644 --- a/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js +++ b/packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js @@ -11,8 +11,7 @@ Sentry.init({ debug: true, }); -Sentry.configureScope(scope => { - scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); - scope.getTransaction().setMetadata({ source: 'custom' }); -}); +const scope = Sentry.getCurrentScope(); +scope.setUser({ id: 'user123', segment: 'segmentB' }); +scope.setTransactionName('testTransactionDSC'); +scope.getTransaction().setMetadata({ source: 'custom' }); diff --git a/packages/browser-integration-tests/suites/tracing/envelope-header/init.js b/packages/browser-integration-tests/suites/tracing/envelope-header/init.js index fbce5a16116a..f382a49c153d 100644 --- a/packages/browser-integration-tests/suites/tracing/envelope-header/init.js +++ b/packages/browser-integration-tests/suites/tracing/envelope-header/init.js @@ -11,7 +11,6 @@ Sentry.init({ debug: true, }); -Sentry.configureScope(scope => { - scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); -}); +const scope = Sentry.getCurrentScope(); +scope.setUser({ id: 'user123', segment: 'segmentB' }); +scope.setTransactionName('testTransactionDSC'); diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts index bc0058ba7d16..62bf08d0ee25 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -12,10 +12,10 @@ import { captureEvent, captureException, captureMessage, - configureScope, flush, getClient, getCurrentHub, + getCurrentScope, init, showReportDialog, wrap, @@ -58,27 +58,21 @@ describe('SentryBrowser', () => { describe('getContext() / setContext()', () => { it('should store/load extra', () => { - configureScope((scope: Scope) => { - scope.setExtra('abc', { def: [1] }); - }); + getCurrentScope().setExtra('abc', { def: [1] }); expect(global.__SENTRY__.hub._stack[1].scope._extra).toEqual({ abc: { def: [1] }, }); }); it('should store/load tags', () => { - configureScope((scope: Scope) => { - scope.setTag('abc', 'def'); - }); + getCurrentScope().setTag('abc', 'def'); expect(global.__SENTRY__.hub._stack[1].scope._tags).toEqual({ abc: 'def', }); }); it('should store/load user', () => { - configureScope((scope: Scope) => { - scope.setUser({ id: 'def' }); - }); + getCurrentScope().setUser({ id: 'def' }); expect(global.__SENTRY__.hub._stack[1].scope._user).toEqual({ id: 'def', }); @@ -95,9 +89,7 @@ describe('SentryBrowser', () => { const options = getDefaultBrowserClientOptions({ dsn }); const client = new BrowserClient(options); it('uses the user on the scope', () => { - configureScope(scope => { - scope.setUser(EX_USER); - }); + getCurrentScope().setUser(EX_USER); getCurrentHub().bindClient(client); showReportDialog(); @@ -110,9 +102,7 @@ describe('SentryBrowser', () => { }); it('prioritizes options user over scope user', () => { - configureScope(scope => { - scope.setUser(EX_USER); - }); + getCurrentScope().setUser(EX_USER); getCurrentHub().bindClient(client); const DIALOG_OPTION_USER = { email: 'option@example.com' }; diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index b49b1d15e9b1..75630de373f1 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -121,7 +121,7 @@ export class IdleTransaction extends Transaction { // We set the transaction here on the scope so error events pick up the trace // context and attach it to the error. DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanId}`); - _idleHub.configureScope(scope => scope.setSpan(this)); + _idleHub.getScope().setSpan(this); } this._restartIdleTimeout(); diff --git a/packages/core/test/lib/hint.test.ts b/packages/core/test/lib/hint.test.ts index cdcfa9368cbe..5fb69ce39fff 100644 --- a/packages/core/test/lib/hint.test.ts +++ b/packages/core/test/lib/hint.test.ts @@ -1,4 +1,4 @@ -import { captureEvent, configureScope } from '@sentry/core'; +import { captureEvent, getCurrentScope } from '@sentry/core'; import { GLOBAL_OBJ } from '@sentry/utils'; import { initAndBind } from '../../src/sdk'; @@ -109,7 +109,7 @@ describe('Hint', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); initAndBind(TestClient, options); - configureScope(scope => scope.addAttachment({ filename: 'scope.file', data: 'great content!' })); + getCurrentScope().addAttachment({ filename: 'scope.file', data: 'great content!' }); captureEvent({}, { attachments: [{ filename: 'some-file.txt', data: 'Hello' }] }); diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts index f4de76234ca2..20db043865a9 100644 --- a/packages/core/test/lib/tracing/errors.test.ts +++ b/packages/core/test/lib/tracing/errors.test.ts @@ -40,7 +40,7 @@ describe('registerErrorHandlers()', () => { }); afterEach(() => { - hub.configureScope(scope => scope.setSpan(undefined)); + hub.getScope().setSpan(undefined); }); it('registers error instrumentation', () => { @@ -67,7 +67,7 @@ describe('registerErrorHandlers()', () => { it('sets status for transaction on scope on error', () => { registerErrorInstrumentation(); const transaction = hub.startTransaction({ name: 'test' }); - hub.configureScope(scope => scope.setSpan(transaction)); + hub.getScope().setSpan(transaction); mockErrorCallback({} as HandlerDataError); expect(transaction.status).toBe('internal_error'); @@ -78,7 +78,7 @@ describe('registerErrorHandlers()', () => { it('sets status for transaction on scope on unhandledrejection', () => { registerErrorInstrumentation(); const transaction = hub.startTransaction({ name: 'test' }); - hub.configureScope(scope => scope.setSpan(transaction)); + hub.getScope().setSpan(transaction); mockUnhandledRejectionCallback({}); expect(transaction.status).toBe('internal_error'); diff --git a/packages/core/test/mocks/integration.ts b/packages/core/test/mocks/integration.ts index ce95d04520a7..4c229ce27294 100644 --- a/packages/core/test/mocks/integration.ts +++ b/packages/core/test/mocks/integration.ts @@ -1,6 +1,6 @@ import type { Event, EventProcessor, Integration } from '@sentry/types'; -import { configureScope, getCurrentHub } from '../../src'; +import { getCurrentHub, getCurrentScope } from '../../src'; export class TestIntegration implements Integration { public static id: string = 'TestIntegration'; @@ -18,9 +18,7 @@ export class TestIntegration implements Integration { eventProcessor.id = this.name; - configureScope(scope => { - scope.addEventProcessor(eventProcessor); - }); + getCurrentScope().addEventProcessor(eventProcessor); } } diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts b/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts index ed4372065792..7585c88f0ab1 100644 --- a/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts +++ b/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts @@ -4,7 +4,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export default function handler(req: NextApiRequest, res: NextApiResponse) { const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); - Sentry.getCurrentHub().configureScope(scope => scope.setSpan(transaction)); + Sentry.getCurrentHub().getScope().setSpan(transaction); const span = transaction.startChild(); diff --git a/packages/e2e-tests/test-applications/node-express-app/src/app.ts b/packages/e2e-tests/test-applications/node-express-app/src/app.ts index e9de96631259..fd83fcdfa23a 100644 --- a/packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -35,7 +35,7 @@ app.get('/test-param/:param', function (req, res) { app.get('/test-transaction', async function (req, res) { const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); - Sentry.getCurrentHub().configureScope(scope => scope.setSpan(transaction)); + Sentry.getCurrentHub().getScope().setSpan(transaction); const span = transaction.startChild(); diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 8fd55568e70e..0c10a8344bd7 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -4,8 +4,8 @@ import type { BrowserOptions } from '@sentry/react'; import { BrowserTracing, Integrations, - configureScope, defaultRequestInstrumentationOptions, + getCurrentScope, init as reactInit, } from '@sentry/react'; import type { EventProcessor } from '@sentry/types'; @@ -56,17 +56,16 @@ export function init(options: BrowserOptions): void { reactInit(opts); - configureScope(scope => { - scope.setTag('runtime', 'browser'); - const filterTransactions: EventProcessor = event => - event.type === 'transaction' && event.transaction === '/404' ? null : event; - filterTransactions.id = 'NextClient404Filter'; - scope.addEventProcessor(filterTransactions); + const scope = getCurrentScope(); + scope.setTag('runtime', 'browser'); + const filterTransactions: EventProcessor = event => + event.type === 'transaction' && event.transaction === '/404' ? null : event; + filterTransactions.id = 'NextClient404Filter'; + scope.addEventProcessor(filterTransactions); - if (process.env.NODE_ENV === 'development') { - scope.addEventProcessor(devErrorSymbolicationEventProcessor); - } - }); + if (process.env.NODE_ENV === 'development') { + scope.addEventProcessor(devErrorSymbolicationEventProcessor); + } } function addClientIntegrations(options: BrowserOptions): void { diff --git a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts index ece566bc2e5a..2051d015b0c4 100644 --- a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts @@ -1,4 +1,4 @@ -import { addTracingExtensions, captureException, configureScope, runWithAsyncContext } from '@sentry/core'; +import { addTracingExtensions, captureException, getCurrentScope, runWithAsyncContext } from '@sentry/core'; import { extractTraceparentData } from '@sentry/utils'; interface FunctionComponent { @@ -26,24 +26,23 @@ export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | C return class SentryWrappedPageComponent extends pageComponent { public render(...args: unknown[]): unknown { return runWithAsyncContext(() => { - configureScope(scope => { - // We extract the sentry trace data that is put in the component props by datafetcher wrappers - const sentryTraceData = - typeof this.props === 'object' && - this.props !== null && - '_sentryTraceData' in this.props && - typeof this.props._sentryTraceData === 'string' - ? this.props._sentryTraceData - : undefined; + const scope = getCurrentScope(); + // We extract the sentry trace data that is put in the component props by datafetcher wrappers + const sentryTraceData = + typeof this.props === 'object' && + this.props !== null && + '_sentryTraceData' in this.props && + typeof this.props._sentryTraceData === 'string' + ? this.props._sentryTraceData + : undefined; - if (sentryTraceData) { - const traceparentData = extractTraceparentData(sentryTraceData); - scope.setContext('trace', { - span_id: traceparentData?.parentSpanId, - trace_id: traceparentData?.traceId, - }); - } - }); + if (sentryTraceData) { + const traceparentData = extractTraceparentData(sentryTraceData); + scope.setContext('trace', { + span_id: traceparentData?.parentSpanId, + trace_id: traceparentData?.traceId, + }); + } try { return super.render(...args); @@ -62,18 +61,18 @@ export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | C return new Proxy(pageComponent, { apply(target, thisArg, argArray: [{ _sentryTraceData?: string } | undefined]) { return runWithAsyncContext(() => { - configureScope(scope => { - // We extract the sentry trace data that is put in the component props by datafetcher wrappers - const sentryTraceData = argArray?.[0]?._sentryTraceData; + const scope = getCurrentScope(); + // We extract the sentry trace data that is put in the component props by datafetcher wrappers + const sentryTraceData = argArray?.[0]?._sentryTraceData; + + if (sentryTraceData) { + const traceparentData = extractTraceparentData(sentryTraceData); + scope.setContext('trace', { + span_id: traceparentData?.parentSpanId, + trace_id: traceparentData?.traceId, + }); + } - if (sentryTraceData) { - const traceparentData = extractTraceparentData(sentryTraceData); - scope.setContext('trace', { - span_id: traceparentData?.parentSpanId, - trace_id: traceparentData?.traceId, - }); - } - }); try { return target.apply(thisArg, argArray); } catch (e) { diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index b049be2c31b8..a7549506ae14 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { addTracingExtensions } from '@sentry/core'; import { RewriteFrames } from '@sentry/integrations'; import type { NodeOptions } from '@sentry/node'; -import { Integrations, configureScope, getCurrentHub, init as nodeInit } from '@sentry/node'; +import { Integrations, getCurrentHub, getCurrentScope, init as nodeInit } from '@sentry/node'; import type { EventProcessor } from '@sentry/types'; import type { IntegrationWithExclusionOption } from '@sentry/utils'; import { addOrUpdateIntegration, escapeStringForRegex, logger } from '@sentry/utils'; @@ -101,18 +101,17 @@ export function init(options: NodeOptions): void { filterTransactions.id = 'NextServer404TransactionFilter'; - configureScope(scope => { - scope.setTag('runtime', 'node'); - if (IS_VERCEL) { - scope.setTag('vercel', true); - } + const scope = getCurrentScope(); + scope.setTag('runtime', 'node'); + if (IS_VERCEL) { + scope.setTag('vercel', true); + } - scope.addEventProcessor(filterTransactions); + scope.addEventProcessor(filterTransactions); - if (process.env.NODE_ENV === 'development') { - scope.addEventProcessor(devErrorSymbolicationEventProcessor); - } - }); + if (process.env.NODE_ENV === 'development') { + scope.addEventProcessor(devErrorSymbolicationEventProcessor); + } DEBUG_BUILD && logger.log('SDK successfully initialized'); } diff --git a/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts b/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts index 93a950c257f2..588c56c273e9 100644 --- a/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts +++ b/packages/node-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts @@ -5,11 +5,10 @@ Sentry.init({ release: '1.0', }); -Sentry.configureScope(scope => { - scope.setTag('foo', 'bar'); - scope.setUser({ id: 'baz' }); - scope.setExtra('qux', 'quux'); - scope.clear(); -}); +const scope = Sentry.getCurrentScope(); +scope.setTag('foo', 'bar'); +scope.setUser({ id: 'baz' }); +scope.setExtra('qux', 'quux'); +scope.clear(); Sentry.captureMessage('cleared_scope'); diff --git a/packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts b/packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts index 941265006ee8..b3f3f4d4ae15 100644 --- a/packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts +++ b/packages/node-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts @@ -5,10 +5,9 @@ Sentry.init({ release: '1.0', }); -Sentry.configureScope(scope => { - scope.setTag('foo', 'bar'); - scope.setUser({ id: 'baz' }); - scope.setExtra('qux', 'quux'); -}); +const scope = Sentry.getCurrentScope(); +scope.setTag('foo', 'bar'); +scope.setUser({ id: 'baz' }); +scope.setExtra('qux', 'quux'); Sentry.captureMessage('configured_scope'); diff --git a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts index 5bd8aa815cbe..7fdbfce0351c 100644 --- a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts @@ -29,9 +29,7 @@ const server = new ApolloServer({ const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); void (async () => { // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts index 31d7356765e9..cae4627e7096 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts @@ -21,9 +21,7 @@ async function run(): Promise { op: 'transaction', }); - Sentry.configureScope(scope => { - scope.setSpan(transaction); - }); + Sentry.getCurrentScope().setSpan(transaction); try { await client.connect(); diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts index 0f576cb793aa..7d94099ea30c 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts @@ -24,9 +24,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { connection.query('SELECT NOW()', ['1', '2'], () => { diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts index ac1d6421dec8..4b3346caed20 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts @@ -24,9 +24,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); const query = connection.query('SELECT 1 + 1 AS solution'); const query2 = connection.query('SELECT NOW()', ['1', '2']); diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts index c7cc0e660fc4..2e13bf49b9ac 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts @@ -18,9 +18,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { connection.query('SELECT NOW()', ['1', '2'], () => { diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts index a7859fd562a3..c10661094981 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts @@ -13,9 +13,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); client.query('SELECT * FROM foo where bar ilike "baz%"', ['a', 'b'], () => diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts index 27d82a4c4dd1..c7a5ef761a82 100644 --- a/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts @@ -18,9 +18,7 @@ async function run(): Promise { op: 'transaction', }); - Sentry.configureScope(scope => { - scope.setSpan(transaction); - }); + Sentry.getCurrentScope().setSpan(transaction); try { await client.user.create({ diff --git a/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts index 9084c06441fb..d1eb5fe017ed 100644 --- a/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts @@ -12,9 +12,7 @@ Sentry.init({ const transaction = Sentry.startTransaction({ name: 'test_transaction' }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); http.get('http://match-this-url.com/api/v0'); http.get('http://match-this-url.com/api/v1'); diff --git a/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts b/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts index 732d32814f95..4a4d5a989227 100644 --- a/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts @@ -31,9 +31,7 @@ const server = new ApolloServer({ const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); void (async () => { // Ref: https://www.apollographql.com/docs/apollo-server/testing/testing/#testing-using-executeoperation diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts index 896a91181846..5bd16772d50f 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts @@ -22,9 +22,7 @@ async function run(): Promise { op: 'transaction', }); - Sentry.configureScope(scope => { - scope.setSpan(transaction); - }); + Sentry.getCurrentScope().setSpan(transaction); try { await client.connect(); diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts index f852eec7b2df..2cf161c0ab78 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts @@ -25,9 +25,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { connection.query('SELECT NOW()', ['1', '2'], () => { diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts index 97a08d088cce..c39069909082 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts @@ -14,9 +14,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); client.query('SELECT * FROM foo where bar ilike "baz%"', ['a', 'b'], () => diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts index 7d953353dfe3..0014717b5fc4 100644 --- a/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts @@ -20,9 +20,7 @@ async function run(): Promise { op: 'transaction', }); - Sentry.configureScope(scope => { - scope.setSpan(transaction); - }); + Sentry.getCurrentScope().setSpan(transaction); try { await client.user.create({ diff --git a/packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts index 23ca6e7122cc..c07faeeb9a3f 100644 --- a/packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -14,9 +14,7 @@ Sentry.init({ const transaction = Sentry.startTransaction({ name: 'test_transaction' }); -Sentry.configureScope(scope => { - scope.setSpan(transaction); -}); +Sentry.getCurrentScope().setSpan(transaction); http.get('http://match-this-url.com/api/v0'); http.get('http://match-this-url.com/api/v1'); diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 3160c77b416a..35f1a90190c3 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -86,9 +86,7 @@ export function tracingHandler(): ( ); // We put the transaction on the scope so users can attach children to it - hub.configureScope(scope => { - scope.setSpan(transaction); - }); + hub.getScope().setSpan(transaction); // We also set __sentry_transaction on the response so people can grab the transaction there to add // spans to it later. @@ -186,21 +184,19 @@ export function requestHandler( } runWithAsyncContext(() => { const currentHub = getCurrentHub(); - currentHub.configureScope(scope => { - scope.setSDKProcessingMetadata({ - request: req, - // TODO (v8): Stop passing this - requestDataOptionsFromExpressHandler: requestDataOptions, - }); - - const client = currentHub.getClient(); - if (isAutoSessionTrackingEnabled(client)) { - const scope = currentHub.getScope(); - // Set `status` of `RequestSession` to Ok, at the beginning of the request - scope.setRequestSession({ status: 'ok' }); - } + const scope = currentHub.getScope(); + scope.setSDKProcessingMetadata({ + request: req, + // TODO (v8): Stop passing this + requestDataOptionsFromExpressHandler: requestDataOptions, }); + const client = currentHub.getClient(); + if (isAutoSessionTrackingEnabled(client)) { + // Set `status` of `RequestSession` to Ok, at the beginning of the request + scope.setRequestSession({ status: 'ok' }); + } + res.once('finish', () => { const client = currentHub.getClient(); if (isAutoSessionTrackingEnabled(client)) { diff --git a/packages/node/test/eventbuilders.test.ts b/packages/node/test/eventbuilders.test.ts index 53598505d474..3c2ae88f03e3 100644 --- a/packages/node/test/eventbuilders.test.ts +++ b/packages/node/test/eventbuilders.test.ts @@ -12,7 +12,6 @@ jest.mock('@sentry/core', () => { getCurrentHub(): { getClient(): Client; getScope(): Scope; - configureScope(scopeFunction: (scope: Scope) => void): void; } { return { getClient(): any { @@ -23,10 +22,7 @@ jest.mock('@sentry/core', () => { }; }, getScope(): Scope { - return new Scope(); - }, - configureScope(scopeFunction: (scope: Scope) => void): void { - scopeFunction(testScope); + return testScope; }, }; }, diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 2b91dc414ee1..3bb90a710708 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -1,16 +1,16 @@ import { LinkedErrors, SDK_VERSION, getMainCarrier, initAndBind, runWithAsyncContext } from '@sentry/core'; import type { EventHint, Integration } from '@sentry/types'; -import type { Event, Scope } from '../src'; +import type { Event } from '../src'; import { NodeClient, addBreadcrumb, captureEvent, captureException, captureMessage, - configureScope, getClient, getCurrentHub, + getCurrentScope, init, } from '../src'; import { setNodeAsyncContextStrategy } from '../src/async'; @@ -48,27 +48,21 @@ describe('SentryNode', () => { describe('getContext() / setContext()', () => { test('store/load extra', async () => { - configureScope((scope: Scope) => { - scope.setExtra('abc', { def: [1] }); - }); + getCurrentScope().setExtra('abc', { def: [1] }); expect(global.__SENTRY__.hub._stack[1].scope._extra).toEqual({ abc: { def: [1] }, }); }); test('store/load tags', async () => { - configureScope((scope: Scope) => { - scope.setTag('abc', 'def'); - }); + getCurrentScope().setTag('abc', 'def'); expect(global.__SENTRY__.hub._stack[1].scope._tags).toEqual({ abc: 'def', }); }); test('store/load user', async () => { - configureScope((scope: Scope) => { - scope.setUser({ id: 'def' }); - }); + getCurrentScope().setUser({ id: 'def' }); expect(global.__SENTRY__.hub._stack[1].scope._user).toEqual({ id: 'def', }); @@ -138,9 +132,7 @@ describe('SentryNode', () => { dsn, }); getCurrentHub().bindClient(new NodeClient(options)); - configureScope((scope: Scope) => { - scope.setTag('test', '1'); - }); + getCurrentScope().setTag('test', '1'); try { throw new Error('test'); } catch (e) { @@ -165,9 +157,7 @@ describe('SentryNode', () => { dsn, }); getCurrentHub().bindClient(new NodeClient(options)); - configureScope((scope: Scope) => { - scope.setTag('test', '1'); - }); + getCurrentScope().setTag('test', '1'); try { throw 'test string exception'; } catch (e) { @@ -197,9 +187,7 @@ describe('SentryNode', () => { integrations: [new ContextLines()], }); getCurrentHub().bindClient(new NodeClient(options)); - configureScope((scope: Scope) => { - scope.setTag('test', '1'); - }); + getCurrentScope().setTag('test', '1'); try { throw new Error('test'); } catch (e) { diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index ba5bbe91151c..3a147af422dd 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -38,12 +38,10 @@ describe('tracing', () => { const hub = new Hub(new NodeClient(options)); addTracingExtensions(); - hub.configureScope(scope => - scope.setUser({ - id: 'uid123', - segment: 'segmentA', - }), - ); + hub.getScope().setUser({ + id: 'uid123', + segment: 'segmentA', + }); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 9de394d2232d..69ef554c132c 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -966,9 +966,7 @@ describe('SentrySpanProcessor', () => { makeMain(hub); const newHub = new Hub(client, hub.getScope().clone()); - newHub.configureScope(scope => { - scope.setTag('foo', 'bar'); - }); + newHub.getScope().setTag('foo', 'bar'); const tracer = provider.getTracer('default'); diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index b81622f8797c..38f99d7af825 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { addEventProcessor, configureScope, getClient } from '@sentry/browser'; +import { addEventProcessor, getClient, getCurrentScope } from '@sentry/browser'; import type { Scope } from '@sentry/types'; import { addNonEnumerableProperty } from '@sentry/utils'; @@ -116,44 +116,43 @@ function createReduxEnhancer(enhancerOptions?: Partial): const sentryReducer: Reducer = (state, action): S => { const newState = reducer(state, action); - configureScope(scope => { - /* Action breadcrumbs */ - const transformedAction = options.actionTransformer(action); - if (typeof transformedAction !== 'undefined' && transformedAction !== null) { - scope.addBreadcrumb({ - category: ACTION_BREADCRUMB_CATEGORY, - data: transformedAction, - type: ACTION_BREADCRUMB_TYPE, - }); - } - - /* Set latest state to scope */ - const transformedState = options.stateTransformer(newState); - if (typeof transformedState !== 'undefined' && transformedState !== null) { - const client = getClient(); - const options = client && client.getOptions(); - const normalizationDepth = (options && options.normalizeDepth) || 3; // default state normalization depth to 3 - - // Set the normalization depth of the redux state to the configured `normalizeDepth` option or a sane number as a fallback - const newStateContext = { state: { type: 'redux', value: transformedState } }; - addNonEnumerableProperty( - newStateContext, - '__sentry_override_normalization_depth__', - 3 + // 3 layers for `state.value.transformedState` - normalizationDepth, // rest for the actual state - ); - - scope.setContext('state', newStateContext); - } else { - scope.setContext('state', null); - } - - /* Allow user to configure scope with latest state */ - const { configureScopeWithState } = options; - if (typeof configureScopeWithState === 'function') { - configureScopeWithState(scope, newState); - } - }); + const scope = getCurrentScope(); + /* Action breadcrumbs */ + const transformedAction = options.actionTransformer(action); + if (typeof transformedAction !== 'undefined' && transformedAction !== null) { + scope.addBreadcrumb({ + category: ACTION_BREADCRUMB_CATEGORY, + data: transformedAction, + type: ACTION_BREADCRUMB_TYPE, + }); + } + + /* Set latest state to scope */ + const transformedState = options.stateTransformer(newState); + if (typeof transformedState !== 'undefined' && transformedState !== null) { + const client = getClient(); + const options = client && client.getOptions(); + const normalizationDepth = (options && options.normalizeDepth) || 3; // default state normalization depth to 3 + + // Set the normalization depth of the redux state to the configured `normalizeDepth` option or a sane number as a fallback + const newStateContext = { state: { type: 'redux', value: transformedState } }; + addNonEnumerableProperty( + newStateContext, + '__sentry_override_normalization_depth__', + 3 + // 3 layers for `state.value.transformedState` + normalizationDepth, // rest for the actual state + ); + + scope.setContext('state', newStateContext); + } else { + scope.setContext('state', null); + } + + /* Allow user to configure scope with latest state */ + const { configureScopeWithState } = options; + if (typeof configureScopeWithState === 'function') { + configureScopeWithState(scope, newState); + } return newState; }; diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index 61b908a1d5fe..60cf59abd74e 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -9,11 +9,12 @@ const mockSetContext = jest.fn(); jest.mock('@sentry/browser', () => ({ ...jest.requireActual('@sentry/browser'), - configureScope: (callback: (scope: any) => Partial) => - callback({ + getCurrentScope() { + return { addBreadcrumb: mockAddBreadcrumb, setContext: mockSetContext, - }), + }; + }, addEventProcessor: jest.fn(), })); @@ -240,8 +241,7 @@ describe('createReduxEnhancer', () => { value: 'latest', }); - let scopeRef; - Sentry.configureScope(scope => (scopeRef = scope)); + const scopeRef = Sentry.getCurrentScope(); expect(configureScopeWithState).toBeCalledWith(scopeRef, { value: 'latest', diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx index fd37f3001d83..63b39253416d 100644 --- a/packages/remix/src/index.client.tsx +++ b/packages/remix/src/index.client.tsx @@ -1,4 +1,4 @@ -import { configureScope, init as reactInit } from '@sentry/react'; +import { getCurrentScope, init as reactInit } from '@sentry/react'; import { buildMetadata } from './utils/metadata'; import type { RemixOptions } from './utils/remixOptions'; @@ -12,7 +12,5 @@ export function init(options: RemixOptions): void { reactInit(options); - configureScope(scope => { - scope.setTag('runtime', 'browser'); - }); + getCurrentScope().setTag('runtime', 'browser'); } diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 12fc10b522cf..1c1c0ffee072 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -1,5 +1,5 @@ import type { NodeOptions } from '@sentry/node'; -import { configureScope, getCurrentHub, init as nodeInit } from '@sentry/node'; +import { getCurrentHub, getCurrentScope, init as nodeInit } from '@sentry/node'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from './utils/debug-build'; @@ -86,7 +86,5 @@ export function init(options: RemixOptions): void { nodeInit(options as NodeOptions); - configureScope(scope => { - scope.setTag('runtime', 'node'); - }); + getCurrentScope().setTag('runtime', 'node'); } diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 05595867c191..63303470d9e9 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -1,4 +1,4 @@ -import { captureException, flush, getCurrentHub } from '@sentry/node'; +import { captureException, flush, getCurrentHub, getCurrentScope } from '@sentry/node'; import { isThenable, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -43,11 +43,10 @@ function _wrapCloudEventFunction( // getCurrentHub() is expected to use current active domain as a carrier // since functions-framework creates a domain for each incoming request. // So adding of event processors every time should not lead to memory bloat. - hub.configureScope(scope => { - scope.setContext('gcp.function.context', { ...context }); - // We put the transaction on the scope so users can attach children to it - scope.setSpan(transaction); - }); + const scope = getCurrentScope(); + scope.setContext('gcp.function.context', { ...context }); + // We put the transaction on the scope so users can attach children to it + scope.setSpan(transaction); const newCallback = domainify((...args: unknown[]) => { if (args[0] !== null && args[0] !== undefined) { diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index c3be42c6a6c2..29d151593990 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,4 +1,4 @@ -import { captureException, flush, getCurrentHub } from '@sentry/node'; +import { captureException, flush, getCurrentHub, getCurrentScope } from '@sentry/node'; import { isThenable, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -45,11 +45,10 @@ function _wrapEventFunction // getCurrentHub() is expected to use current active domain as a carrier // since functions-framework creates a domain for each incoming request. // So adding of event processors every time should not lead to memory bloat. - hub.configureScope(scope => { - scope.setContext('gcp.function.context', { ...context }); - // We put the transaction on the scope so users can attach children to it - scope.setSpan(transaction); - }); + const scope = getCurrentScope(); + scope.setContext('gcp.function.context', { ...context }); + // We put the transaction on the scope so users can attach children to it + scope.setSpan(transaction); const newCallback = domainify((...args: unknown[]) => { if (args[0] !== null && args[0] !== undefined) { diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 8f4a77099696..95c84cafeb80 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -1,4 +1,5 @@ import type { AddRequestDataToEventOptions } from '@sentry/node'; +import { getCurrentScope } from '@sentry/node'; import { captureException, flush, getCurrentHub } from '@sentry/node'; import { isString, isThenable, logger, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils'; @@ -63,6 +64,7 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { const hub = getCurrentHub(); + const scope = getCurrentScope(); const reqMethod = (req.method || '').toUpperCase(); const reqUrl = stripUrlQueryAndFragment(req.originalUrl || req.url || ''); @@ -73,7 +75,7 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { - scope.setSDKProcessingMetadata({ - request: req, - requestDataOptionsFromGCPWrapper: options.addRequestDataToEventOptions, - }); - // We put the transaction on the scope so users can attach children to it - scope.setSpan(transaction); + scope.setSDKProcessingMetadata({ + request: req, + requestDataOptionsFromGCPWrapper: options.addRequestDataToEventOptions, }); + // We put the transaction on the scope so users can attach children to it + scope.setSpan(transaction); // We also set __sentry_transaction on the response so people can grab the transaction there to add // spans to it later. diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index c29f8f78dd0a..b9eba4b132a9 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -35,6 +35,7 @@ export const fakeTransaction = { export const init = jest.fn(); export const addGlobalEventProcessor = jest.fn(); export const getCurrentHub = jest.fn(() => fakeHub); +export const getCurrentScope = jest.fn(() => fakeScope); export const startTransaction = jest.fn(_ => fakeTransaction); export const captureException = jest.fn(); export const captureMessage = jest.fn(); diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts index 900813ce7f9b..ebfd1f281404 100644 --- a/packages/sveltekit/src/client/sdk.ts +++ b/packages/sveltekit/src/client/sdk.ts @@ -1,6 +1,6 @@ import { hasTracingEnabled } from '@sentry/core'; import type { BrowserOptions } from '@sentry/svelte'; -import { BrowserTracing, WINDOW, configureScope, init as initSvelteSdk } from '@sentry/svelte'; +import { BrowserTracing, WINDOW, getCurrentScope, init as initSvelteSdk } from '@sentry/svelte'; import { addOrUpdateIntegration } from '@sentry/utils'; import { applySdkMetadata } from '../common/metadata'; @@ -34,9 +34,7 @@ export function init(options: BrowserOptions): void { restoreFetch(actualFetch); } - configureScope(scope => { - scope.setTag('runtime', 'browser'); - }); + getCurrentScope().setTag('runtime', 'browser'); } function addClientIntegrations(options: BrowserOptions): void { diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 613fe8d834f0..03847d6cc4e4 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -1,4 +1,4 @@ -import { configureScope } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { RewriteFrames } from '@sentry/integrations'; import type { NodeOptions } from '@sentry/node'; import { init as initNodeSdk } from '@sentry/node'; @@ -18,9 +18,7 @@ export function init(options: NodeOptions): void { initNodeSdk(options); - configureScope(scope => { - scope.setTag('runtime', 'node'); - }); + getCurrentScope().setTag('runtime', 'node'); } function addServerIntegrations(options: NodeOptions): void { diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index 031d68d01d78..2687d59069c5 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -30,7 +30,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { afterEach(() => { events = {}; - hub.configureScope(scope => scope.setSpan(undefined)); + hub.getScope().setSpan(undefined); }); it('does not create an event listener if global document is undefined', () => { @@ -48,7 +48,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { it('finishes a transaction on visibility change', () => { registerBackgroundTabDetection(); const transaction = hub.startTransaction({ name: 'test' }); - hub.configureScope(scope => scope.setSpan(transaction)); + hub.getScope().setSpan(transaction); // Simulate document visibility hidden event // @ts-expect-error need to override global document diff --git a/packages/tracing-internal/test/browser/request.test.ts b/packages/tracing-internal/test/browser/request.test.ts index 29cf7e287f0c..0f3ce191278a 100644 --- a/packages/tracing-internal/test/browser/request.test.ts +++ b/packages/tracing-internal/test/browser/request.test.ts @@ -74,7 +74,7 @@ describe('callbacks', () => { beforeEach(() => { transaction = hub.startTransaction({ name: 'organizations/users/:userid', op: 'pageload' }) as Transaction; - hub.configureScope(scope => scope.setSpan(transaction)); + hub.getScope().setSpan(transaction); }); afterEach(() => { diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts index 86fcf5d6807e..817e88c9c55e 100644 --- a/packages/tracing/test/hub.test.ts +++ b/packages/tracing/test/hub.test.ts @@ -40,9 +40,7 @@ describe('Hub', () => { const transaction = hub.startTransaction({ name: 'dogpark' }); transaction.sampled = true; - hub.configureScope(scope => { - scope.setSpan(transaction); - }); + hub.getScope().setSpan(transaction); expect(hub.getScope().getTransaction()).toBe(transaction); }); @@ -53,9 +51,7 @@ describe('Hub', () => { makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: false }); - hub.configureScope(scope => { - scope.setSpan(transaction); - }); + hub.getScope().setSpan(transaction); expect(hub.getScope().getTransaction()).toBe(transaction); }); @@ -472,9 +468,7 @@ The transaction will not be sampled. Please use the otel instrumentation to star makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); - hub.configureScope(scope => { - scope.setSpan(transaction); - }); + hub.getScope().setSpan(transaction); const request = new XMLHttpRequest(); await new Promise(resolve => { @@ -513,9 +507,7 @@ The transaction will not be sampled. Please use the otel instrumentation to star makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark', sampled: false }); - hub.configureScope(scope => { - scope.setSpan(transaction); - }); + hub.getScope().setSpan(transaction); const request = new XMLHttpRequest(); await new Promise(resolve => { diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index 32be1c2e421a..30cd97f775b1 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -25,18 +25,16 @@ describe('IdleTransaction', () => { ); transaction.initSpanRecorder(10); - hub.configureScope(s => { - expect(s.getTransaction()).toBe(transaction); - }); + const scope = hub.getScope(); + expect(scope.getTransaction()).toBe(transaction); }); it('does not set the transaction on the scope on creation if onScope is falsey', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); - hub.configureScope(s => { - expect(s.getTransaction()).toBe(undefined); - }); + const scope = hub.getScope(); + expect(scope.getTransaction()).toBe(undefined); }); it('removes sampled transaction from scope on finish if onScope is true', () => { @@ -53,9 +51,8 @@ describe('IdleTransaction', () => { transaction.finish(); jest.runAllTimers(); - hub.configureScope(s => { - expect(s.getTransaction()).toBe(undefined); - }); + const scope = hub.getScope(); + expect(scope.getTransaction()).toBe(undefined); }); it('removes unsampled transaction from scope on finish if onScope is true', () => { @@ -71,9 +68,8 @@ describe('IdleTransaction', () => { transaction.finish(); jest.runAllTimers(); - hub.configureScope(s => { - expect(s.getTransaction()).toBe(undefined); - }); + const scope = hub.getScope(); + expect(scope.getTransaction()).toBe(undefined); }); it('does not remove transaction from scope on finish if another transaction was set there', () => { @@ -94,9 +90,8 @@ describe('IdleTransaction', () => { transaction.finish(); jest.runAllTimers(); - hub.configureScope(s => { - expect(s.getTransaction()).toBe(otherTransaction); - }); + const scope = hub.getScope(); + expect(scope.getTransaction()).toBe(otherTransaction); }); }); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 1a7981ad95f6..3e6c267b1233 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -226,9 +226,7 @@ describe('Span', () => { const childSpanOne = transaction.startChild(); childSpanOne.finish(); - hub.configureScope(scope => { - scope.setSpan(childSpanOne); - }); + hub.getScope().setSpan(childSpanOne); const spanTwo = transaction.startChild(); spanTwo.finish(); @@ -282,9 +280,7 @@ describe('Span', () => { childSpanOne.finish(); - hub.configureScope(scope => { - scope.setSpan(transaction); - }); + hub.getScope().setSpan(transaction); const spanTwo = transaction.startChild({}); spanTwo.finish(); diff --git a/packages/utils/src/eventbuilder.ts b/packages/utils/src/eventbuilder.ts index 03af0b3d1905..5dac2f583bb6 100644 --- a/packages/utils/src/eventbuilder.ts +++ b/packages/utils/src/eventbuilder.ts @@ -82,9 +82,7 @@ export function eventFromUnknownInput( const hub = getCurrentHub(); const client = hub.getClient(); const normalizeDepth = client && client.getOptions().normalizeDepth; - hub.configureScope(scope => { - scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); - }); + hub.getScope().setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); const message = getMessageForObject(exception); ex = (hint && hint.syntheticException) || new Error(message); diff --git a/packages/utils/test/eventbuilder.test.ts b/packages/utils/test/eventbuilder.test.ts index 137860b16ce4..b1c46630de08 100644 --- a/packages/utils/test/eventbuilder.test.ts +++ b/packages/utils/test/eventbuilder.test.ts @@ -1,10 +1,17 @@ -import type { Hub } from '@sentry/types'; +import type { Hub, Scope } from '@sentry/types'; import { createStackParser, eventFromUnknownInput, nodeStackLineParser } from '../src'; function getCurrentHub(): Hub { // Some fake hub to get us through - return { getClient: () => undefined, configureScope: () => {} } as unknown as Hub; + return { + getClient: () => undefined, + getScope: () => { + return { + setExtra: () => {}, + } as unknown as Scope; + }, + } as unknown as Hub; } const stackParser = createStackParser(nodeStackLineParser()); From da8c9c3bd8ef5a915eabc02c2b7342542e6c13bd Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 15 Dec 2023 11:49:41 +0100 Subject: [PATCH 03/34] feat(nextjs): Auto instrument generation functions (#9781) --- .github/workflows/build.yml | 1 + .github/workflows/canary.yml | 6 + .../test-applications/nextjs-14/.gitignore | 45 ++++ .../test-applications/nextjs-14/.npmrc | 2 + .../app/generation-functions/page.tsx | 33 +++ .../nextjs-14/app/layout.tsx | 7 + .../nextjs-14/event-proxy-server.ts | 253 ++++++++++++++++++ .../test-applications/nextjs-14/globals.d.ts | 4 + .../test-applications/nextjs-14/next-env.d.ts | 5 + .../nextjs-14/next.config.js | 30 +++ .../test-applications/nextjs-14/package.json | 35 +++ .../nextjs-14/playwright.config.ts | 77 ++++++ .../nextjs-14/sentry.client.config.ts | 9 + .../nextjs-14/sentry.edge.config.ts | 9 + .../nextjs-14/sentry.server.config.ts | 9 + .../nextjs-14/start-event-proxy.ts | 6 + .../tests/generation-functions.test.ts | 79 ++++++ .../test-applications/nextjs-14/tsconfig.json | 30 +++ packages/nextjs/src/common/index.ts | 2 + packages/nextjs/src/common/types.ts | 8 + .../wrapGenerationFunctionWithSentry.ts | 80 ++++++ .../serverComponentWrapperTemplate.ts | 37 ++- 22 files changed, 764 insertions(+), 3 deletions(-) create mode 100644 packages/e2e-tests/test-applications/nextjs-14/.gitignore create mode 100644 packages/e2e-tests/test-applications/nextjs-14/.npmrc create mode 100644 packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx create mode 100644 packages/e2e-tests/test-applications/nextjs-14/app/layout.tsx create mode 100644 packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/globals.d.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/next-env.d.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/next.config.js create mode 100644 packages/e2e-tests/test-applications/nextjs-14/package.json create mode 100644 packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/sentry.edge.config.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/sentry.server.config.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts create mode 100644 packages/e2e-tests/test-applications/nextjs-14/tsconfig.json create mode 100644 packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31431df1c0cc..d76a1a270d74 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -864,6 +864,7 @@ jobs: 'create-remix-app-v2', 'debug-id-sourcemaps', 'nextjs-app-dir', + 'nextjs-14', 'react-create-hash-router', 'react-router-6-use-routes', 'standard-frontend-react', diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index ed05e9bfd1af..9801407515cd 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -73,6 +73,12 @@ jobs: - test-application: 'nextjs-app-dir' build-command: 'test:build-latest' label: 'nextjs-app-dir (latest)' + - test-application: 'nextjs-14' + build-command: 'test:build-canary' + label: 'nextjs-14 (canary)' + - test-application: 'nextjs-14' + build-command: 'test:build-latest' + label: 'nextjs-14 (latest)' - test-application: 'react-create-hash-router' build-command: 'test:build-canary' label: 'react-create-hash-router (canary)' diff --git a/packages/e2e-tests/test-applications/nextjs-14/.gitignore b/packages/e2e-tests/test-applications/nextjs-14/.gitignore new file mode 100644 index 000000000000..e799cc33c4e7 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +!*.d.ts + +# Sentry +.sentryclirc + +.vscode + +test-results diff --git a/packages/e2e-tests/test-applications/nextjs-14/.npmrc b/packages/e2e-tests/test-applications/nextjs-14/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx b/packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx new file mode 100644 index 000000000000..5ae73102057d --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/page.tsx @@ -0,0 +1,33 @@ +export const dynamic = 'force-dynamic'; + +export default function Page() { + return

Hello World!

; +} + +export async function generateMetadata({ + searchParams, +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) { + if (searchParams['shouldThrowInGenerateMetadata']) { + throw new Error('generateMetadata Error'); + } + + return { + title: searchParams['metadataTitle'] ?? 'not set', + }; +} + +export function generateViewport({ + searchParams, +}: { + searchParams: { [key: string]: string | undefined }; +}) { + if (searchParams['shouldThrowInGenerateViewport']) { + throw new Error('generateViewport Error'); + } + + return { + themeColor: searchParams['viewportThemeColor'] ?? 'black', + }; +} diff --git a/packages/e2e-tests/test-applications/nextjs-14/app/layout.tsx b/packages/e2e-tests/test-applications/nextjs-14/app/layout.tsx new file mode 100644 index 000000000000..c8f9cee0b787 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts b/packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts new file mode 100644 index 000000000000..9dee679c71e4 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/event-proxy-server.ts @@ -0,0 +1,253 @@ +import * as fs from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import type { AddressInfo } from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as util from 'util'; +import * as zlib from 'zlib'; +import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import { parseEnvelope } from '@sentry/utils'; + +const readFile = util.promisify(fs.readFile); +const writeFile = util.promisify(fs.writeFile); + +interface EventProxyServerOptions { + /** Port to start the event proxy server at. */ + port: number; + /** The name for the proxy server used for referencing it with listener functions */ + proxyServerName: string; +} + +interface SentryRequestCallbackData { + envelope: Envelope; + rawProxyRequestBody: string; + rawSentryResponseBody: string; + sentryResponseStatusCode?: number; +} + +/** + * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` + * option to this server (like this `tunnel: http://localhost:${port option}/`). + */ +export async function startEventProxyServer(options: EventProxyServerOptions): Promise { + const eventCallbackListeners: Set<(data: string) => void> = new Set(); + + const proxyServer = http.createServer((proxyRequest, proxyResponse) => { + const proxyRequestChunks: Uint8Array[] = []; + + proxyRequest.addListener('data', (chunk: Buffer) => { + proxyRequestChunks.push(chunk); + }); + + proxyRequest.addListener('error', err => { + throw err; + }); + + proxyRequest.addListener('end', () => { + const proxyRequestBody = + proxyRequest.headers['content-encoding'] === 'gzip' + ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() + : Buffer.concat(proxyRequestChunks).toString(); + + let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + + if (!envelopeHeader.dsn) { + throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const sentryResponseChunks: Uint8Array[] = []; + + const sentryRequest = https.request( + sentryIngestUrl, + { headers: proxyRequest.headers, method: proxyRequest.method }, + sentryResponse => { + sentryResponse.addListener('data', (chunk: Buffer) => { + proxyResponse.write(chunk, 'binary'); + sentryResponseChunks.push(chunk); + }); + + sentryResponse.addListener('end', () => { + eventCallbackListeners.forEach(listener => { + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody, new TextEncoder(), new TextDecoder()), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: sentryResponse.statusCode, + }; + + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + proxyResponse.end(); + }); + + sentryResponse.addListener('error', err => { + throw err; + }); + + proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); + }, + ); + + sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); + sentryRequest.end(); + }); + }); + + const proxyServerStartupPromise = new Promise(resolve => { + proxyServer.listen(options.port, () => { + resolve(); + }); + }); + + const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { + eventCallbackResponse.statusCode = 200; + eventCallbackResponse.setHeader('connection', 'keep-alive'); + + const callbackListener = (data: string): void => { + eventCallbackResponse.write(data.concat('\n'), 'utf8'); + }; + + eventCallbackListeners.add(callbackListener); + + eventCallbackRequest.on('close', () => { + eventCallbackListeners.delete(callbackListener); + }); + + eventCallbackRequest.on('error', () => { + eventCallbackListeners.delete(callbackListener); + }); + }); + + const eventCallbackServerStartupPromise = new Promise(resolve => { + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export async function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => Promise | boolean, +): Promise { + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); + + return new Promise((resolve, reject) => { + const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); + + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); + chunkString.split('').forEach(char => { + if (char === '\n') { + const eventCallbackData: SentryRequestCallbackData = JSON.parse( + Buffer.from(eventContents, 'base64').toString('utf8'), + ); + const callbackResult = callback(eventCallbackData); + if (typeof callbackResult !== 'boolean') { + callbackResult.then( + match => { + if (match) { + response.destroy(); + resolve(eventCallbackData); + } + }, + err => { + throw err; + }, + ); + } else if (callbackResult) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, async eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (await callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +const TEMP_FILE_PREFIX = 'event-proxy-server-'; + +async function registerCallbackServerPort(serverName: string, port: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + await writeFile(tmpFilePath, port, { encoding: 'utf8' }); +} + +function retrieveCallbackServerPort(serverName: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return readFile(tmpFilePath, 'utf8'); +} diff --git a/packages/e2e-tests/test-applications/nextjs-14/globals.d.ts b/packages/e2e-tests/test-applications/nextjs-14/globals.d.ts new file mode 100644 index 000000000000..109dbcd55648 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/globals.d.ts @@ -0,0 +1,4 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; +} diff --git a/packages/e2e-tests/test-applications/nextjs-14/next-env.d.ts b/packages/e2e-tests/test-applications/nextjs-14/next-env.d.ts new file mode 100644 index 000000000000..4f11a03dc6cc --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/e2e-tests/test-applications/nextjs-14/next.config.js b/packages/e2e-tests/test-applications/nextjs-14/next.config.js new file mode 100644 index 000000000000..4beb4fc356f4 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/next.config.js @@ -0,0 +1,30 @@ +// This file sets a custom webpack configuration to use your Next.js app +// with Sentry. +// https://nextjs.org/docs/api-reference/next.config.js/introduction +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const moduleExports = {}; + +const sentryWebpackPluginOptions = { + // Additional config options for the Sentry Webpack plugin. Keep in mind that + // the following options are set automatically, and overriding them is not + // recommended: + // release, url, org, project, authToken, configFile, stripPrefix, + // urlPrefix, include, ignore + + silent: true, // Suppresses all logs + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options. + + // We're not testing source map uploads at the moment. + dryRun: true, +}; + +// Make sure adding Sentry options is the last code to run before exporting, to +// ensure that your source maps include changes from all other Webpack plugins +module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions, { + hideSourceMaps: true, +}); diff --git a/packages/e2e-tests/test-applications/nextjs-14/package.json b/packages/e2e-tests/test-applications/nextjs-14/package.json new file mode 100644 index 000000000000..b822f4316566 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/package.json @@ -0,0 +1,35 @@ +{ + "name": "create-next-app", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr", + "clean": "npx rimraf node_modules,pnpm-lock.yaml", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod && pnpm test:dev" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@types/node": "18.11.17", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9", + "next": "14.0.4", + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "4.9.5", + "wait-port": "1.0.4", + "ts-node": "10.9.1", + "@playwright/test": "^1.27.1" + }, + "devDependencies": { + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts b/packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts new file mode 100644 index 000000000000..ab3c40a21471 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/playwright.config.ts @@ -0,0 +1,77 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const nextPort = 3030; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 10000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* `next dev` is incredibly buggy with the app dir */ + retries: testEnv === 'development' ? 3 : 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${nextPort}`, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: + testEnv === 'development' + ? `pnpm wait-port ${eventProxyPort} && pnpm next dev -p ${nextPort}` + : `pnpm wait-port ${eventProxyPort} && pnpm next start -p ${nextPort}`, + port: nextPort, + }, + ], +}; + +export default config; diff --git a/packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts b/packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); diff --git a/packages/e2e-tests/test-applications/nextjs-14/sentry.edge.config.ts b/packages/e2e-tests/test-applications/nextjs-14/sentry.edge.config.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/sentry.edge.config.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); diff --git a/packages/e2e-tests/test-applications/nextjs-14/sentry.server.config.ts b/packages/e2e-tests/test-applications/nextjs-14/sentry.server.config.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/sentry.server.config.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); diff --git a/packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts b/packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts new file mode 100644 index 000000000000..eb83fd6fb82d --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from './event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-14', +}); diff --git a/packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts new file mode 100644 index 000000000000..3828312607ea --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts @@ -0,0 +1,79 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '../event-proxy-server'; + +test('Should send a transaction event for a generateMetadata() function invokation', async ({ page }) => { + const testTitle = 'foobarasdf'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + ); + }); + + await page.goto(`/generation-functions?metadataTitle=${testTitle}`); + + expect(await transactionPromise).toBeDefined(); + + const pageTitle = await page.title(); + expect(pageTitle).toBe(testTitle); +}); + +test('Should send a transaction and an error event for a faulty generateMetadata() function invokation', async ({ + page, +}) => { + const testTitle = 'foobarbaz'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + ); + }); + + const errorEventPromise = waitForError('nextjs-14', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'generateMetadata Error'; + }); + + await page.goto(`/generation-functions?metadataTitle=${testTitle}&shouldThrowInGenerateMetadata=1`); + + expect(await transactionPromise).toBeDefined(); + expect(await errorEventPromise).toBeDefined(); +}); + +test('Should send a transaction event for a generateViewport() function invokation', async ({ page }) => { + const testTitle = 'floob'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateViewport (/generation-functions)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['viewportThemeColor'] === testTitle + ); + }); + + await page.goto(`/generation-functions?viewportThemeColor=${testTitle}`); + + expect(await transactionPromise).toBeDefined(); +}); + +test('Should send a transaction and an error event for a faulty generateViewport() function invokation', async ({ + page, +}) => { + const testTitle = 'blargh'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateViewport (/generation-functions)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['viewportThemeColor'] === testTitle + ); + }); + + const errorEventPromise = waitForError('nextjs-14', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'generateViewport Error'; + }); + + await page.goto(`/generation-functions?viewportThemeColor=${testTitle}&shouldThrowInGenerateViewport=1`); + + expect(await transactionPromise).toBeDefined(); + expect(await errorEventPromise).toBeDefined(); +}); diff --git a/packages/e2e-tests/test-applications/nextjs-14/tsconfig.json b/packages/e2e-tests/test-applications/nextjs-14/tsconfig.json new file mode 100644 index 000000000000..60825545944d --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-14/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ], + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"], + "exclude": ["node_modules"], + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + } +} diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts index 063c11bff62a..3b0ce67fb16c 100644 --- a/packages/nextjs/src/common/index.ts +++ b/packages/nextjs/src/common/index.ts @@ -44,4 +44,6 @@ export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry'; export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry'; +export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry'; + export { withServerActionInstrumentation } from './withServerActionInstrumentation'; diff --git a/packages/nextjs/src/common/types.ts b/packages/nextjs/src/common/types.ts index ffca3dc8ff61..cf7d881e9ea0 100644 --- a/packages/nextjs/src/common/types.ts +++ b/packages/nextjs/src/common/types.ts @@ -1,5 +1,6 @@ import type { Transaction, WebFetchHeaders, WrappedFunction } from '@sentry/types'; import type { NextApiRequest, NextApiResponse } from 'next'; +import type { RequestAsyncStorage } from '../config/templates/requestAsyncStorageShim'; export type ServerComponentContext = { componentRoute: string; @@ -17,6 +18,13 @@ export type ServerComponentContext = { headers?: WebFetchHeaders; }; +export type GenerationFunctionContext = { + requestAsyncStorage?: RequestAsyncStorage; + componentRoute: string; + componentType: string; + generationFunctionIdentifier: string; +}; + export interface RouteHandlerContext { method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'; parameterizedRoute: string; diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts new file mode 100644 index 000000000000..5aa9c436beef --- /dev/null +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -0,0 +1,80 @@ +import { + addTracingExtensions, + captureException, + continueTrace, + getCurrentHub, + runWithAsyncContext, + trace, +} from '@sentry/core'; +import type { WebFetchHeaders } from '@sentry/types'; +import { winterCGHeadersToDict } from '@sentry/utils'; + +import type { GenerationFunctionContext } from '../common/types'; + +/** + * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function wrapGenerationFunctionWithSentry any>( + generationFunction: F, + context: GenerationFunctionContext, +): F { + addTracingExtensions(); + const { requestAsyncStorage, componentRoute, componentType, generationFunctionIdentifier } = context; + return new Proxy(generationFunction, { + apply: (originalFunction, thisArg, args) => { + let headers: WebFetchHeaders | undefined = undefined; + // We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API + try { + headers = requestAsyncStorage?.getStore()?.headers; + } catch (e) { + /** empty */ + } + + let data: Record | undefined = undefined; + if (getCurrentHub().getClient()?.getOptions().sendDefaultPii) { + const props: unknown = args[0]; + const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined; + const searchParams = + props && typeof props === 'object' && 'searchParams' in props ? props.searchParams : undefined; + data = { params, searchParams }; + } + + return runWithAsyncContext(() => { + const transactionContext = continueTrace({ + baggage: headers?.get('baggage'), + sentryTrace: headers?.get('sentry-trace') ?? undefined, + }); + return trace( + { + op: 'function.nextjs', + name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, + origin: 'auto.function.nextjs', + ...transactionContext, + data, + metadata: { + ...transactionContext.metadata, + source: 'url', + request: { + headers: headers ? winterCGHeadersToDict(headers) : undefined, + }, + }, + }, + () => { + return originalFunction.apply(thisArg, args); + }, + err => { + captureException(err, { + mechanism: { + handled: false, + data: { + function: 'wrapGenerationFunctionWithSentry', + }, + }, + }); + }, + ); + }); + }, + }); +} diff --git a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts index d0cc4adc4466..56b9853fa1af 100644 --- a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts @@ -12,6 +12,9 @@ declare const requestAsyncStorage: RequestAsyncStorage; declare const serverComponentModule: { default: unknown; + generateMetadata?: () => unknown; + generateImageMetadata?: () => unknown; + generateViewport?: () => unknown; }; const serverComponent = serverComponentModule.default; @@ -30,14 +33,15 @@ if (typeof serverComponent === 'function') { // We try-catch here just in `requestAsyncStorage` is undefined since it may not be defined try { const requestAsyncStore = requestAsyncStorage.getStore(); - sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace'); - baggageHeader = requestAsyncStore?.headers.get('baggage'); + sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace') ?? undefined; + baggageHeader = requestAsyncStore?.headers.get('baggage') ?? undefined; headers = requestAsyncStore?.headers; } catch (e) { /** empty */ } - return Sentry.wrapServerComponentWithSentry(originalFunction, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + return Sentry.wrapServerComponentWithSentry(originalFunction as any, { componentRoute: '__ROUTE__', componentType: '__COMPONENT_TYPE__', sentryTraceHeader, @@ -50,6 +54,33 @@ if (typeof serverComponent === 'function') { wrappedServerComponent = serverComponent; } +export const generateMetadata = serverComponentModule.generateMetadata + ? Sentry.wrapGenerationFunctionWithSentry(serverComponentModule.generateMetadata, { + componentRoute: '__ROUTE__', + componentType: '__COMPONENT_TYPE__', + generationFunctionIdentifier: 'generateMetadata', + requestAsyncStorage, + }) + : undefined; + +export const generateImageMetadata = serverComponentModule.generateImageMetadata + ? Sentry.wrapGenerationFunctionWithSentry(serverComponentModule.generateImageMetadata, { + componentRoute: '__ROUTE__', + componentType: '__COMPONENT_TYPE__', + generationFunctionIdentifier: 'generateImageMetadata', + requestAsyncStorage, + }) + : undefined; + +export const generateViewport = serverComponentModule.generateViewport + ? Sentry.wrapGenerationFunctionWithSentry(serverComponentModule.generateViewport, { + componentRoute: '__ROUTE__', + componentType: '__COMPONENT_TYPE__', + generationFunctionIdentifier: 'generateViewport', + requestAsyncStorage, + }) + : undefined; + // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to // not include anything whose name matchs something we've explicitly exported above. // @ts-expect-error See above From 30bb8b56422d04c19d95767a7ba94fa9ec9c9c23 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 15 Dec 2023 12:48:11 +0100 Subject: [PATCH 04/34] fix(utils): Do not use `Event` type in worldwide (#9864) As that is browser only. Closes https://github.com/getsentry/sentry-javascript/issues/9860 --- packages/utils/src/instrument/globalError.ts | 2 +- packages/utils/src/worldwide.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/instrument/globalError.ts b/packages/utils/src/instrument/globalError.ts index df7ff21438cc..0bf50c52f2c4 100644 --- a/packages/utils/src/instrument/globalError.ts +++ b/packages/utils/src/instrument/globalError.ts @@ -21,7 +21,7 @@ function instrumentError(): void { _oldOnErrorHandler = GLOBAL_OBJ.onerror; GLOBAL_OBJ.onerror = function ( - msg: string | Event, + msg: string | object, url?: string, line?: number, column?: number, diff --git a/packages/utils/src/worldwide.ts b/packages/utils/src/worldwide.ts index 5caf1b137d5a..a2fdfb67e14b 100644 --- a/packages/utils/src/worldwide.ts +++ b/packages/utils/src/worldwide.ts @@ -24,7 +24,7 @@ export interface InternalGlobal { Integrations?: Integration[]; }; onerror?: { - (event: Event | string, source?: string, lineno?: number, colno?: number, error?: Error): any; + (event: object | string, source?: string, lineno?: number, colno?: number, error?: Error): any; __SENTRY_INSTRUMENTED__?: true; __SENTRY_LOADER__?: true; }; From ffc4181c2fb91f9ecb2cd4151343a124fa27e2a0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 15 Dec 2023 13:39:16 +0100 Subject: [PATCH 05/34] ref: Use `getCurrentScope()` / `getClient` instead of `hub.xxx` (#9862) Except for places where we are really passing a `hub` in, not using `getCurrentHub()`. --- packages/angular/src/tracing.ts | 11 +----- packages/angular/test/tracing.test.ts | 12 ++---- packages/astro/src/server/meta.ts | 10 +++-- packages/astro/src/server/middleware.ts | 16 ++++---- packages/astro/test/server/meta.test.ts | 39 +++++++++---------- packages/astro/test/server/middleware.test.ts | 27 +++++++------ packages/browser/src/eventbuilder.ts | 5 +-- packages/browser/src/profiling/utils.ts | 19 +++------ packages/core/src/baseclient.ts | 4 +- packages/core/src/exports.ts | 5 +-- packages/core/src/metrics/exports.ts | 7 ++-- packages/core/src/sessionflusher.ts | 5 +-- packages/core/src/tracing/trace.ts | 18 ++++----- .../node-express-app/src/app.ts | 2 +- .../feedback/src/util/sendFeedbackRequest.ts | 11 +++--- packages/feedback/src/widget/createWidget.ts | 4 +- .../pagesRouterRoutingInstrumentation.ts | 4 +- .../src/common/utils/edgeWrapperUtils.ts | 4 +- .../nextjs/src/common/utils/wrapperUtils.ts | 5 +-- .../common/withServerActionInstrumentation.ts | 14 +++++-- .../src/common/wrapApiHandlerWithSentry.ts | 8 ++-- .../wrapAppGetInitialPropsWithSentry.ts | 7 ++-- .../wrapErrorGetInitialPropsWithSentry.ts | 7 ++-- .../common/wrapGetInitialPropsWithSentry.ts | 7 ++-- .../wrapGetServerSidePropsWithSentry.ts | 7 ++-- .../src/common/wrapRouteHandlerWithSentry.ts | 7 +--- .../common/wrapServerComponentWithSentry.ts | 6 +-- .../src/edge/wrapApiHandlerWithSentry.ts | 4 +- packages/nextjs/src/server/index.ts | 7 ++-- packages/nextjs/test/config/wrappers.test.ts | 17 +++----- packages/node/src/anr/index.ts | 10 ++--- packages/node/src/handlers.ts | 24 +++++------- packages/node/src/integrations/http.ts | 6 +-- .../node/src/integrations/undici/index.ts | 4 +- packages/node/src/sdk.ts | 6 ++- packages/node/test/handlers.test.ts | 3 +- packages/node/test/integrations/http.test.ts | 23 +++++++---- packages/remix/src/index.server.ts | 6 +-- packages/remix/src/utils/instrumentServer.ts | 10 ++--- .../remix/src/utils/serverAdapters/express.ts | 6 +-- packages/replay/src/replay.ts | 6 +-- .../replay/src/util/addGlobalListeners.ts | 5 ++- packages/replay/src/util/sendReplayRequest.ts | 7 ++-- packages/serverless/src/awslambda.ts | 2 +- packages/serverless/src/awsservices.ts | 4 +- packages/serverless/src/google-cloud-grpc.ts | 4 +- packages/serverless/src/google-cloud-http.ts | 4 +- packages/svelte/src/performance.ts | 4 +- packages/svelte/test/performance.test.ts | 14 ++----- packages/sveltekit/src/server/handle.ts | 7 ++-- packages/sveltekit/src/server/load.ts | 4 +- .../tracing-internal/src/browser/request.ts | 7 ++-- packages/tracing-internal/src/common/fetch.ts | 7 ++-- .../src/integrations/wintercg-fetch.ts | 5 +-- .../vercel-edge/test/wintercg-fetch.test.ts | 2 + packages/vue/src/tracing.ts | 4 +- 56 files changed, 223 insertions(+), 260 deletions(-) diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 88a2490c1d58..25ddc2fd8dcf 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -7,7 +7,7 @@ import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router // eslint-disable-next-line @typescript-eslint/consistent-type-imports import { NavigationCancel, NavigationError, Router } from '@angular/router'; import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; -import { WINDOW, getCurrentHub } from '@sentry/browser'; +import { WINDOW, getCurrentScope } from '@sentry/browser'; import type { Span, Transaction, TransactionContext } from '@sentry/types'; import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils'; import type { Observable } from 'rxjs'; @@ -50,14 +50,7 @@ export const instrumentAngularRouting = routingInstrumentation; * Grabs active transaction off scope */ export function getActiveTransaction(): Transaction | undefined { - const currentHub = getCurrentHub(); - - if (currentHub) { - const scope = currentHub.getScope(); - return scope.getTransaction(); - } - - return undefined; + return getCurrentScope().getTransaction(); } /** diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index e290850241c8..635c8847b9bf 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -21,16 +21,12 @@ jest.mock('@sentry/browser', () => { const original = jest.requireActual('@sentry/browser'); return { ...original, - getCurrentHub: () => { + getCurrentScope() { return { - getScope: () => { - return { - getTransaction: () => { - return transaction; - }, - }; + getTransaction: () => { + return transaction; }, - } as unknown as Hub; + }; }, }; }); diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts index 4264be2733f5..7f1f544a19e6 100644 --- a/packages/astro/src/server/meta.ts +++ b/packages/astro/src/server/meta.ts @@ -1,5 +1,5 @@ import { getDynamicSamplingContextFromClient } from '@sentry/core'; -import type { Hub, Span } from '@sentry/types'; +import type { Client, Scope, Span } from '@sentry/types'; import { TRACEPARENT_REGEXP, dynamicSamplingContextToSentryBaggageHeader, @@ -22,9 +22,11 @@ import { * * @returns an object with the two serialized tags */ -export function getTracingMetaTags(span: Span | undefined, hub: Hub): { sentryTrace: string; baggage?: string } { - const scope = hub.getScope(); - const client = hub.getClient(); +export function getTracingMetaTags( + span: Span | undefined, + scope: Scope, + client: Client | undefined, +): { sentryTrace: string; baggage?: string } { const { dsc, sampled, traceId } = scope.getPropagationContext(); const transaction = span?.transaction; diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 5e3b2de18622..7b4a02cceddf 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,12 +1,12 @@ import { captureException, continueTrace, - getCurrentHub, + getClient, getCurrentScope, runWithAsyncContext, startSpan, } from '@sentry/node'; -import type { Hub, Span } from '@sentry/types'; +import type { Client, Scope, Span } from '@sentry/types'; import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; @@ -69,7 +69,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH // if there is an active span, we know that this handle call is nested and hence // we don't create a new domain for it. If we created one, nested server calls would // create new transactions instead of adding a child span to the currently active span. - if (getCurrentHub().getScope().getSpan()) { + if (getCurrentScope().getSpan()) { return instrumentRequest(ctx, next, handlerOptions); } return runWithAsyncContext(() => { @@ -139,8 +139,8 @@ async function instrumentRequest( span.setHttpStatus(originalResponse.status); } - const hub = getCurrentHub(); - const client = hub.getClient(); + const scope = getCurrentScope(); + const client = getClient(); const contentType = originalResponse.headers.get('content-type'); const isPageloadRequest = contentType && contentType.startsWith('text/html'); @@ -163,7 +163,7 @@ async function instrumentRequest( start: async controller => { for await (const chunk of originalBody) { const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk); - const modifiedHtml = addMetaTagToHead(html, hub, span); + const modifiedHtml = addMetaTagToHead(html, scope, client, span); controller.enqueue(new TextEncoder().encode(modifiedHtml)); } controller.close(); @@ -185,12 +185,12 @@ async function instrumentRequest( * This function optimistically assumes that the HTML coming in chunks will not be split * within the tag. If this still happens, we simply won't replace anything. */ -function addMetaTagToHead(htmlChunk: string, hub: Hub, span?: Span): string { +function addMetaTagToHead(htmlChunk: string, scope: Scope, client: Client, span?: Span): string { if (typeof htmlChunk !== 'string') { return htmlChunk; } - const { sentryTrace, baggage } = getTracingMetaTags(span, hub); + const { sentryTrace, baggage } = getTracingMetaTags(span, scope, client); const content = `\n${sentryTrace}\n${baggage}\n`; return htmlChunk.replace('', content); } diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts index 6298f5f2a20b..279f36395107 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/astro/test/server/meta.test.ts @@ -10,22 +10,20 @@ const mockedSpan = { environment: 'production', }), }, -}; +} as any; -const mockedHub = { - getScope: () => ({ - getPropagationContext: () => ({ - traceId: '123', - }), +const mockedClient = {} as any; + +const mockedScope = { + getPropagationContext: () => ({ + traceId: '123', }), - getClient: () => ({}), -}; +} as any; describe('getTracingMetaTags', () => { it('returns the tracing tags from the span, if it is provided', () => { { - // @ts-expect-error - only passing a partial span object - const tags = getTracingMetaTags(mockedSpan, mockedHub); + const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient); expect(tags).toEqual({ sentryTrace: '', @@ -35,10 +33,9 @@ describe('getTracingMetaTags', () => { }); it('returns propagationContext DSC data if no span is available', () => { - const tags = getTracingMetaTags(undefined, { - ...mockedHub, - // @ts-expect-error - only passing a partial scope object - getScope: () => ({ + const tags = getTracingMetaTags( + undefined, + { getPropagationContext: () => ({ traceId: '12345678901234567890123456789012', sampled: true, @@ -49,8 +46,9 @@ describe('getTracingMetaTags', () => { trace_id: '12345678901234567890123456789012', }, }), - }), - }); + } as any, + mockedClient, + ); expect(tags).toEqual({ sentryTrace: expect.stringMatching( @@ -73,7 +71,8 @@ describe('getTracingMetaTags', () => { toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1', transaction: undefined, }, - mockedHub, + mockedScope, + mockedClient, ); expect(tags).toEqual({ @@ -93,10 +92,8 @@ describe('getTracingMetaTags', () => { toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1', transaction: undefined, }, - { - ...mockedHub, - getClient: () => undefined, - }, + mockedScope, + undefined, ); expect(tags).toEqual({ diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index ef81d69214c5..5e56c6bd70ed 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,5 +1,6 @@ import * as SentryNode from '@sentry/node'; -import { vi } from 'vitest'; +import type { Client } from '@sentry/types'; +import { SpyInstance, vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; @@ -14,14 +15,17 @@ describe('sentryMiddleware', () => { const startSpanSpy = vi.spyOn(SentryNode, 'startSpan'); const getSpanMock = vi.fn(() => {}); - // @ts-expect-error only returning a partial hub here - vi.spyOn(SentryNode, 'getCurrentHub').mockImplementation(() => { - return { - getScope: () => ({ + const setUserMock = vi.fn(); + + beforeEach(() => { + vi.spyOn(SentryNode, 'getCurrentScope').mockImplementation(() => { + return { + setUser: setUserMock, + setPropagationContext: vi.fn(), getSpan: getSpanMock, - }), - getClient: () => ({}), - }; + } as any; + }); + vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client); }); const nextResult = Promise.resolve(new Response(null, { status: 200, headers: new Headers() })); @@ -170,10 +174,6 @@ describe('sentryMiddleware', () => { }); it('attaches client IP and request headers if options are set', async () => { - const scope = { setUser: vi.fn(), setPropagationContext: vi.fn() }; - // @ts-expect-error, only passing a partial Scope object - const getCurrentScopeSpy = vi.spyOn(SentryNode, 'getCurrentScope').mockImplementation(() => scope); - const middleware = handleRequest({ trackClientIp: true, trackHeaders: true }); const ctx = { request: { @@ -192,8 +192,7 @@ describe('sentryMiddleware', () => { // @ts-expect-error, a partial ctx object is fine here await middleware(ctx, next); - expect(getCurrentScopeSpy).toHaveBeenCalledTimes(1); - expect(scope.setUser).toHaveBeenCalledWith({ ip_address: '192.168.0.1' }); + expect(setUserMock).toHaveBeenCalledWith({ ip_address: '192.168.0.1' }); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index e361f1366cf3..6955fbfa26fe 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getClient } from '@sentry/core'; import type { Event, EventHint, Exception, Severity, SeverityLevel, StackFrame, StackParser } from '@sentry/types'; import { addExceptionMechanism, @@ -48,8 +48,7 @@ export function eventFromPlainObject( syntheticException?: Error, isUnhandledRejection?: boolean, ): Event { - const hub = getCurrentHub(); - const client = hub.getClient(); + const client = getClient(); const normalizeDepth = client && client.getOptions().normalizeDepth; const event: Event = { diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index 3edb82e0b539..f2fdc5e4c10d 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ -import { DEFAULT_ENVIRONMENT, getClient, getCurrentHub } from '@sentry/core'; +import { DEFAULT_ENVIRONMENT, getClient } from '@sentry/core'; 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'; @@ -347,19 +347,10 @@ export function applyDebugMetadata(resource_paths: ReadonlyArray): Debug return []; } - const hub = getCurrentHub(); - if (!hub) { - return []; - } - const client = hub.getClient(); - if (!client) { - return []; - } - const options = client.getOptions(); - if (!options) { - return []; - } - const stackParser = options.stackParser; + const client = getClient(); + const options = client && client.getOptions(); + const stackParser = options && options.stackParser; + if (!stackParser) { return []; } diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index a6e0575a2e67..c5d9fff7df6f 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -48,7 +48,7 @@ import { import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; -import { getCurrentHub } from './hub'; +import { getClient } from './exports'; import type { IntegrationIndex } from './integration'; import { setupIntegration, setupIntegrations } from './integration'; import { createMetricEnvelope } from './metrics/envelope'; @@ -870,7 +870,7 @@ function isTransactionEvent(event: Event): event is TransactionEvent { * This event processor will run for all events processed by this client. */ export function addEventProcessor(callback: EventProcessor): void { - const client = getCurrentHub().getClient(); + const client = getClient(); if (!client || !client.addEventProcessor) { return; diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 6f71e7dfbccb..0e574c4853cc 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -202,9 +202,8 @@ export function startTransaction( * to create a monitor automatically when sending a check in. */ export function captureCheckIn(checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig): string { - const hub = getCurrentHub(); - const scope = hub.getScope(); - const client = hub.getClient(); + const scope = getCurrentScope(); + const client = getClient(); if (!client) { DEBUG_BUILD && logger.warn('Cannot capture check-in. No client defined.'); } else if (!client.captureCheckIn) { diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index c27e76cf79b1..22a5e83ffb3d 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -2,7 +2,7 @@ import type { ClientOptions, MeasurementUnit, Primitive } from '@sentry/types'; import { logger } from '@sentry/utils'; import type { BaseClient } from '../baseclient'; import { DEBUG_BUILD } from '../debug-build'; -import { getCurrentHub } from '../hub'; +import { getClient, getCurrentScope } from '../exports'; import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; import { MetricsAggregator } from './integration'; import type { MetricType } from './types'; @@ -19,9 +19,8 @@ function addToMetricsAggregator( value: number | string, data: MetricData = {}, ): void { - const hub = getCurrentHub(); - const client = hub.getClient() as BaseClient; - const scope = hub.getScope(); + const client = getClient>(); + const scope = getCurrentScope(); if (client) { if (!client.metricsAggregator) { DEBUG_BUILD && diff --git a/packages/core/src/sessionflusher.ts b/packages/core/src/sessionflusher.ts index 0b0bc8455480..dac81b82336d 100644 --- a/packages/core/src/sessionflusher.ts +++ b/packages/core/src/sessionflusher.ts @@ -6,8 +6,7 @@ import type { SessionFlusherLike, } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; - -import { getCurrentHub } from './hub'; +import { getCurrentScope } from './exports'; type ReleaseHealthAttributes = { environment?: string; @@ -75,7 +74,7 @@ export class SessionFlusher implements SessionFlusherLike { if (!this._isEnabled) { return; } - const scope = getCurrentHub().getScope(); + const scope = getCurrentScope(); const requestSession = scope.getRequestSession(); if (requestSession && requestSession.status) { diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 667bedaaef6c..f8c8f5d6cbe3 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -2,6 +2,7 @@ import type { TransactionContext } from '@sentry/types'; import { dropUndefinedKeys, isThenable, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { getCurrentScope } from '../exports'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; @@ -28,7 +29,7 @@ export function trace( const ctx = normalizeContext(context); const hub = getCurrentHub(); - const scope = hub.getScope(); + const scope = getCurrentScope(); const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); @@ -37,7 +38,7 @@ export function trace( function finishAndSetSpan(): void { activeSpan && activeSpan.finish(); - hub.getScope().setSpan(parentSpan); + scope.setSpan(parentSpan); } let maybePromiseResult: T; @@ -83,7 +84,7 @@ export function startSpan(context: TransactionContext, callback: (span: Span const ctx = normalizeContext(context); const hub = getCurrentHub(); - const scope = hub.getScope(); + const scope = getCurrentScope(); const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); @@ -91,7 +92,7 @@ export function startSpan(context: TransactionContext, callback: (span: Span function finishAndSetSpan(): void { activeSpan && activeSpan.finish(); - hub.getScope().setSpan(parentSpan); + scope.setSpan(parentSpan); } let maybePromiseResult: T; @@ -143,7 +144,7 @@ export function startSpanManual( const ctx = normalizeContext(context); const hub = getCurrentHub(); - const scope = hub.getScope(); + const scope = getCurrentScope(); const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); @@ -151,7 +152,7 @@ export function startSpanManual( function finishAndSetSpan(): void { activeSpan && activeSpan.finish(); - hub.getScope().setSpan(parentSpan); + scope.setSpan(parentSpan); } let maybePromiseResult: T; @@ -201,7 +202,7 @@ export function startInactiveSpan(context: TransactionContext): Span | undefined * Returns the currently active span. */ export function getActiveSpan(): Span | undefined { - return getCurrentHub().getScope().getSpan(); + return getCurrentScope().getSpan(); } export function continueTrace({ @@ -238,8 +239,7 @@ export function continueTrace( }, callback?: (transactionContext: Partial) => V, ): V | Partial { - const hub = getCurrentHub(); - const currentScope = hub.getScope(); + const currentScope = getCurrentScope(); const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( sentryTrace, diff --git a/packages/e2e-tests/test-applications/node-express-app/src/app.ts b/packages/e2e-tests/test-applications/node-express-app/src/app.ts index fd83fcdfa23a..330a425cb494 100644 --- a/packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -35,7 +35,7 @@ app.get('/test-param/:param', function (req, res) { app.get('/test-transaction', async function (req, res) { const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); - Sentry.getCurrentHub().getScope().setSpan(transaction); + Sentry.getCurrentScope().setSpan(transaction); const span = transaction.startChild(); diff --git a/packages/feedback/src/util/sendFeedbackRequest.ts b/packages/feedback/src/util/sendFeedbackRequest.ts index b8ec16a15401..5e8e532ca58d 100644 --- a/packages/feedback/src/util/sendFeedbackRequest.ts +++ b/packages/feedback/src/util/sendFeedbackRequest.ts @@ -1,4 +1,4 @@ -import { createEventEnvelope, getCurrentHub } from '@sentry/core'; +import { createEventEnvelope, getClient, withScope } from '@sentry/core'; import type { FeedbackEvent, TransportMakeRequestResponse } from '@sentry/types'; import { FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE } from '../constants'; @@ -12,8 +12,7 @@ export async function sendFeedbackRequest( { feedback: { message, email, name, source, url } }: SendFeedbackData, { includeReplay = true }: SendFeedbackOptions = {}, ): Promise { - const hub = getCurrentHub(); - const client = hub.getClient(); + const client = getClient(); const transport = client && client.getTransport(); const dsn = client && client.getDsn(); @@ -35,7 +34,7 @@ export async function sendFeedbackRequest( }; return new Promise((resolve, reject) => { - hub.withScope(async scope => { + withScope(async scope => { // No use for breadcrumbs in feedback scope.clearBreadcrumbs(); @@ -49,12 +48,12 @@ export async function sendFeedbackRequest( event: baseEvent, }); - if (feedbackEvent === null) { + if (!feedbackEvent) { resolve(); return; } - if (client && client.emit) { + if (client.emit) { client.emit('beforeSendFeedback', feedbackEvent, { includeReplay: Boolean(includeReplay) }); } diff --git a/packages/feedback/src/widget/createWidget.ts b/packages/feedback/src/widget/createWidget.ts index 35f9fcf51f71..b5e414803121 100644 --- a/packages/feedback/src/widget/createWidget.ts +++ b/packages/feedback/src/widget/createWidget.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { logger } from '@sentry/utils'; import type { FeedbackFormData, FeedbackInternalOptions, FeedbackWidget } from '../types'; @@ -160,7 +160,7 @@ export function createWidget({ } const userKey = options.useSentryUser; - const scope = getCurrentHub().getScope(); + const scope = getCurrentScope(); const user = scope && scope.getUser(); dialog = Dialog({ diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 91929f885ae0..a7c3d5bd2344 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -1,5 +1,5 @@ import type { ParsedUrlQuery } from 'querystring'; -import { getClient, getCurrentHub } from '@sentry/core'; +import { getClient, getCurrentScope } from '@sentry/core'; import { WINDOW } from '@sentry/react'; import type { Primitive, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { @@ -124,7 +124,7 @@ export function pagesRouterInstrumentation( baggage, ); - getCurrentHub().getScope().setPropagationContext(propagationContext); + getCurrentScope().setPropagationContext(propagationContext); prevLocationName = route || globalObject.location.pathname; if (startTransactionOnPageLoad) { diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 008f8629f3ab..afdf686499c5 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -1,4 +1,4 @@ -import { addTracingExtensions, captureException, flush, getCurrentHub, startTransaction } from '@sentry/core'; +import { addTracingExtensions, captureException, getCurrentScope, startTransaction } from '@sentry/core'; import type { Span } from '@sentry/types'; import { addExceptionMechanism, @@ -23,7 +23,7 @@ export function withEdgeWrapping( addTracingExtensions(); const req = args[0]; - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); const prevSpan = currentScope.getSpan(); let span: Span | undefined; diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index fedb5ba6f3ff..5451b1264723 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { captureException, getActiveTransaction, - getCurrentHub, + getCurrentScope, runWithAsyncContext, startTransaction, } from '@sentry/core'; @@ -84,8 +84,7 @@ export function withTracedServerSideDataFetcher Pr ): (...params: Parameters) => Promise> { return async function (this: unknown, ...args: Parameters): Promise> { return runWithAsyncContext(async () => { - const hub = getCurrentHub(); - const scope = hub.getScope(); + const scope = getCurrentScope(); const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? scope.getSpan(); let dataFetcherSpan; diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index eafcff7b9075..d87429ad528c 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -1,4 +1,11 @@ -import { addTracingExtensions, captureException, flush, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core'; +import { + addTracingExtensions, + captureException, + getClient, + getCurrentScope, + runWithAsyncContext, + trace, +} from '@sentry/core'; import { logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; @@ -49,8 +56,7 @@ async function withServerActionInstrumentationImplementation> { addTracingExtensions(); return runWithAsyncContext(async () => { - const hub = getCurrentHub(); - const sendDefaultPii = hub.getClient()?.getOptions().sendDefaultPii; + const sendDefaultPii = getClient()?.getOptions().sendDefaultPii; let sentryTraceHeader; let baggageHeader; @@ -68,7 +74,7 @@ async function withServerActionInstrumentationImplementation { - const hub = getCurrentHub(); let transaction: Transaction | undefined; - const currentScope = hub.getScope(); - const options = hub.getClient()?.getOptions(); + const currentScope = getCurrentScope(); + const options = getClient()?.getOptions(); currentScope.setSDKProcessingMetadata({ request: req }); diff --git a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts index 1974cf6c5a13..eddf7f4e25e4 100644 --- a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts @@ -1,4 +1,4 @@ -import { addTracingExtensions, getCurrentHub } from '@sentry/core'; +import { addTracingExtensions, getClient, getCurrentScope } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type App from 'next/app'; @@ -32,8 +32,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI const { req, res } = context.ctx; const errorWrappedAppGetInitialProps = withErrorInstrumentation(wrappingTarget); - const hub = getCurrentHub(); - const options = hub.getClient()?.getOptions(); + const options = getClient()?.getOptions(); // Generally we can assume that `req` and `res` are always defined on the server: // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object @@ -53,7 +52,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI }; } = await tracedGetInitialProps.apply(thisArg, args); - const requestTransaction = getTransactionFromRequest(req) ?? hub.getScope().getTransaction(); + const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call // `App.getInitialProps(appContext)` in their custom `_app` pages which is required as per diff --git a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts index a6444a5e3d60..0e4601886cee 100644 --- a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts @@ -1,4 +1,4 @@ -import { addTracingExtensions, getCurrentHub } from '@sentry/core'; +import { addTracingExtensions, getClient, getCurrentScope } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; @@ -35,8 +35,7 @@ export function wrapErrorGetInitialPropsWithSentry( const { req, res } = context; const errorWrappedGetInitialProps = withErrorInstrumentation(wrappingTarget); - const hub = getCurrentHub(); - const options = hub.getClient()?.getOptions(); + const options = getClient()?.getOptions(); // Generally we can assume that `req` and `res` are always defined on the server: // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object @@ -54,7 +53,7 @@ export function wrapErrorGetInitialPropsWithSentry( _sentryBaggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); - const requestTransaction = getTransactionFromRequest(req) ?? hub.getScope().getTransaction(); + const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { errorGetInitialProps._sentryTraceData = requestTransaction.toTraceparent(); diff --git a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts index 594ef451c385..510cbae5684c 100644 --- a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts @@ -1,4 +1,4 @@ -import { addTracingExtensions, getCurrentHub } from '@sentry/core'; +import { addTracingExtensions, getClient, getCurrentScope } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPage } from 'next'; @@ -31,8 +31,7 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro const { req, res } = context; const errorWrappedGetInitialProps = withErrorInstrumentation(wrappingTarget); - const hub = getCurrentHub(); - const options = hub.getClient()?.getOptions(); + const options = getClient()?.getOptions(); // Generally we can assume that `req` and `res` are always defined on the server: // https://nextjs.org/docs/api-reference/data-fetching/get-initial-props#context-object @@ -50,7 +49,7 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro _sentryBaggage?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. - const requestTransaction = getTransactionFromRequest(req) ?? hub.getScope().getTransaction(); + const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { initialProps._sentryTraceData = requestTransaction.toTraceparent(); diff --git a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts index 1e10518245b9..f93c7193418e 100644 --- a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts @@ -1,4 +1,4 @@ -import { addTracingExtensions, getCurrentHub } from '@sentry/core'; +import { addTracingExtensions, getClient, getCurrentScope } from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { GetServerSideProps } from 'next'; @@ -32,8 +32,7 @@ export function wrapGetServerSidePropsWithSentry( const { req, res } = context; const errorWrappedGetServerSideProps = withErrorInstrumentation(wrappingTarget); - const hub = getCurrentHub(); - const options = hub.getClient()?.getOptions(); + const options = getClient()?.getOptions(); if (options?.instrumenter === 'sentry') { const tracedGetServerSideProps = withTracedServerSideDataFetcher(errorWrappedGetServerSideProps, req, res, { @@ -47,7 +46,7 @@ export function wrapGetServerSidePropsWithSentry( >); if (serverSideProps && 'props' in serverSideProps) { - const requestTransaction = getTransactionFromRequest(req) ?? hub.getScope().getTransaction(); + const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { serverSideProps.props._sentryTraceData = requestTransaction.toTraceparent(); diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index 79eaa78e3dff..1f294283c7d8 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -1,4 +1,4 @@ -import { addTracingExtensions, captureException, flush, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core'; +import { addTracingExtensions, captureException, getCurrentScope, runWithAsyncContext, trace } from '@sentry/core'; import { tracingContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; import { isRedirectNavigationError } from './nextNavigationErrorUtils'; @@ -20,14 +20,11 @@ export function wrapRouteHandlerWithSentry any>( return new Proxy(routeHandler, { apply: (originalFunction, thisArg, args) => { return runWithAsyncContext(async () => { - const hub = getCurrentHub(); - const currentScope = hub.getScope(); - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( sentryTraceHeader ?? headers?.get('sentry-trace') ?? undefined, baggageHeader ?? headers?.get('baggage'), ); - currentScope.setPropagationContext(propagationContext); + getCurrentScope().setPropagationContext(propagationContext); let res; try { diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index dade931bf074..9addbce2d589 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -1,7 +1,7 @@ import { addTracingExtensions, captureException, - getCurrentHub, + getCurrentScope, runWithAsyncContext, startTransaction, } from '@sentry/core'; @@ -28,9 +28,7 @@ export function wrapServerComponentWithSentry any> return new Proxy(appDirComponent, { apply: (originalFunction, thisArg, args) => { return runWithAsyncContext(() => { - const hub = getCurrentHub(); - const currentScope = hub.getScope(); - + const currentScope = getCurrentScope(); let maybePromiseResult; const completeHeadersDict: Record = context.headers diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index 46691b3cdce5..5a9398319ae2 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getCurrentHub, getCurrentScope } from '@sentry/core'; import { withEdgeWrapping } from '../common/utils/edgeWrapperUtils'; import type { EdgeRouteHandler } from './types'; @@ -14,7 +14,7 @@ export function wrapApiHandlerWithSentry( apply: (wrappingTarget, thisArg, args: Parameters) => { const req = args[0]; - const activeSpan = getCurrentHub().getScope().getSpan(); + const activeSpan = getCurrentScope().getSpan(); const wrappedHandler = withEdgeWrapping(wrappingTarget, { spanDescription: diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index a7549506ae14..f2146a2cba38 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,8 +1,8 @@ import * as path from 'path'; -import { addTracingExtensions } from '@sentry/core'; +import { addTracingExtensions, getClient } from '@sentry/core'; import { RewriteFrames } from '@sentry/integrations'; import type { NodeOptions } from '@sentry/node'; -import { Integrations, getCurrentHub, getCurrentScope, init as nodeInit } from '@sentry/node'; +import { Integrations, getCurrentScope, init as nodeInit } from '@sentry/node'; import type { EventProcessor } from '@sentry/types'; import type { IntegrationWithExclusionOption } from '@sentry/utils'; import { addOrUpdateIntegration, escapeStringForRegex, logger } from '@sentry/utils'; @@ -117,8 +117,7 @@ export function init(options: NodeOptions): void { } function sdkAlreadyInitialized(): boolean { - const hub = getCurrentHub(); - return !!hub.getClient(); + return !!getClient(); } function addServerIntegrations(options: NodeOptions): void { diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index b6d29d5ecff2..95b003e4e14d 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -2,10 +2,10 @@ import type { IncomingMessage, ServerResponse } from 'http'; import * as SentryCore from '@sentry/core'; import { addTracingExtensions } from '@sentry/core'; +import type { Client } from '@sentry/types'; import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/common'; const startTransactionSpy = jest.spyOn(SentryCore, 'startTransaction'); -const originalGetCurrentHub = jest.requireActual('@sentry/node').getCurrentHub; // The wrap* functions require the hub to have tracing extensions. This is normally called by the NodeClient // constructor but the client isn't used in these tests. @@ -22,16 +22,11 @@ describe('data-fetching function wrappers', () => { res = { end: jest.fn() } as unknown as ServerResponse; jest.spyOn(SentryCore, 'hasTracingEnabled').mockReturnValue(true); - jest.spyOn(SentryCore, 'getCurrentHub').mockImplementation(() => { - const hub = originalGetCurrentHub(); - - hub.getClient = () => - ({ - getOptions: () => ({ instrumenter: 'sentry' }), - getDsn: () => {}, - }) as any; - - return hub; + jest.spyOn(SentryCore, 'getClient').mockImplementation(() => { + return { + getOptions: () => ({ instrumenter: 'sentry' }), + getDsn: () => {}, + } as Client; }); }); diff --git a/packages/node/src/anr/index.ts b/packages/node/src/anr/index.ts index 84a3ebe52920..32117f21372b 100644 --- a/packages/node/src/anr/index.ts +++ b/packages/node/src/anr/index.ts @@ -1,9 +1,9 @@ import { spawn } from 'child_process'; -import { getClient, makeSession, updateSession } from '@sentry/core'; +import { getClient, getCurrentScope, makeSession, updateSession } from '@sentry/core'; import type { Event, Session, StackFrame } from '@sentry/types'; import { logger, watchdogTimer } from '@sentry/utils'; -import { addEventProcessor, captureEvent, flush, getCurrentHub } from '..'; +import { addEventProcessor, captureEvent, flush } from '..'; import { captureStackTrace } from './debugger'; const DEFAULT_INTERVAL = 50; @@ -91,8 +91,6 @@ function startChildProcess(options: Options): void { logger.log(`[ANR] ${message}`, ...args); } - const hub = getCurrentHub(); - try { const env = { ...process.env }; env.SENTRY_ANR_CHILD_PROCESS = 'true'; @@ -112,7 +110,7 @@ function startChildProcess(options: Options): void { const timer = setInterval(() => { try { - const currentSession = hub.getScope()?.getSession(); + const currentSession = getCurrentScope()?.getSession(); // We need to copy the session object and remove the toJSON method so it can be sent to the child process // serialized without making it a SerializedSession const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; @@ -126,7 +124,7 @@ function startChildProcess(options: Options): void { child.on('message', (msg: string) => { if (msg === 'session-ended') { log('ANR event sent from child process. Clearing session in this process.'); - hub.getScope()?.setSession(undefined); + getCurrentScope()?.setSession(undefined); } }); diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 35f1a90190c3..832d87139f83 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -5,7 +5,7 @@ import { continueTrace, flush, getClient, - getCurrentHub, + getCurrentScope, hasTracingEnabled, runWithAsyncContext, startTransaction, @@ -44,8 +44,7 @@ export function tracingHandler(): ( res: http.ServerResponse, next: (error?: any) => void, ): void { - const hub = getCurrentHub(); - const options = hub.getClient()?.getOptions(); + const options = getClient()?.getOptions(); if ( !options || @@ -86,7 +85,7 @@ export function tracingHandler(): ( ); // We put the transaction on the scope so users can attach children to it - hub.getScope().setSpan(transaction); + getCurrentScope().setSpan(transaction); // We also set __sentry_transaction on the response so people can grab the transaction there to add // spans to it later. @@ -149,15 +148,14 @@ export function requestHandler( // TODO (v8): Get rid of this const requestDataOptions = convertReqHandlerOptsToAddReqDataOpts(options); - const currentHub = getCurrentHub(); - const client = currentHub.getClient(); + const client = getClient(); // Initialise an instance of SessionFlusher on the client when `autoSessionTracking` is enabled and the // `requestHandler` middleware is used indicating that we are running in SessionAggregates mode if (client && isAutoSessionTrackingEnabled(client)) { client.initSessionFlusher(); // If Scope contains a Single mode Session, it is removed in favor of using Session Aggregates mode - const scope = currentHub.getScope(); + const scope = getCurrentScope(); if (scope.getSession()) { scope.setSession(); } @@ -183,22 +181,21 @@ export function requestHandler( }; } runWithAsyncContext(() => { - const currentHub = getCurrentHub(); - const scope = currentHub.getScope(); + const scope = getCurrentScope(); scope.setSDKProcessingMetadata({ request: req, // TODO (v8): Stop passing this requestDataOptionsFromExpressHandler: requestDataOptions, }); - const client = currentHub.getClient(); + const client = getClient(); if (isAutoSessionTrackingEnabled(client)) { // Set `status` of `RequestSession` to Ok, at the beginning of the request scope.setRequestSession({ status: 'ok' }); } res.once('finish', () => { - const client = currentHub.getClient(); + const client = getClient(); if (isAutoSessionTrackingEnabled(client)) { setImmediate(() => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -328,9 +325,8 @@ interface TrpcMiddlewareArguments { */ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { return function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): T { - const hub = getCurrentHub(); - const clientOptions = hub.getClient()?.getOptions(); - const sentryTransaction = hub.getScope().getTransaction(); + const clientOptions = getClient()?.getOptions(); + const sentryTransaction = getCurrentScope().getTransaction(); if (sentryTransaction) { sentryTransaction.setName(`trpc/${path}`, 'route'); diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 407343a96770..b61d34574457 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,6 +1,7 @@ import type * as http from 'http'; import type * as https from 'https'; import type { Hub } from '@sentry/core'; +import { getClient, getCurrentScope } from '@sentry/core'; import { getCurrentHub, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; import type { DynamicSamplingContext, @@ -243,8 +244,7 @@ function _createWrappedRequestMethodFactory( return originalRequestMethod.apply(httpModule, requestArgs); } - const hub = getCurrentHub(); - const scope = hub.getScope(); + const scope = getCurrentScope(); const parentSpan = scope.getSpan(); const data = getRequestSpanData(requestUrl, requestOptions); @@ -264,7 +264,7 @@ function _createWrappedRequestMethodFactory( const dynamicSamplingContext = requestSpan?.transaction?.getDynamicSamplingContext(); addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); } else { - const client = hub.getClient(); + const client = getClient(); const { traceId, sampled, dsc } = scope.getPropagationContext(); const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled); const dynamicSamplingContext = diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 50e8dd0b30fb..7681a26ba7ca 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; +import { getCurrentHub, getCurrentScope, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; import type { EventProcessor, Integration, Span } from '@sentry/types'; import { LRUMap, @@ -147,7 +147,7 @@ export class Undici implements Integration { } const clientOptions = client.getOptions(); - const scope = hub.getScope(); + const scope = getCurrentScope(); const parentSpan = scope.getSpan(); diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 5ef42128d9ab..07fd3f8b024a 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,7 +1,9 @@ /* eslint-disable max-lines */ import { Integrations as CoreIntegrations, + getClient, getCurrentHub, + getCurrentScope, getIntegrationsToSetup, getMainCarrier, initAndBind, @@ -182,7 +184,7 @@ export function init(options: NodeOptions = {}): void { updateScopeFromEnvVariables(); if (options.spotlight) { - const client = getCurrentHub().getClient(); + const client = getClient(); if (client && client.addIntegration) { // force integrations to be setup even if no DSN was set client.setupIntegrations(true); @@ -277,6 +279,6 @@ function updateScopeFromEnvVariables(): void { const sentryTraceEnv = process.env.SENTRY_TRACE; const baggageEnv = process.env.SENTRY_BAGGAGE; const { propagationContext } = tracingContextFromHeaders(sentryTraceEnv, baggageEnv); - getCurrentHub().getScope().setPropagationContext(propagationContext); + getCurrentScope().setPropagationContext(propagationContext); } } diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 37faef621907..cf6dab4d9338 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -458,10 +458,11 @@ describe('tracingHandler', () => { const hub = new sentryCore.Hub(new NodeClient(options)); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); sentryTracingMiddleware(req, res, next); - const transaction = sentryCore.getCurrentHub().getScope().getTransaction(); + const transaction = sentryCore.getCurrentScope().getTransaction(); expect(transaction?.metadata.request).toEqual(req); }); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 3a147af422dd..1bfafd5c256d 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -44,6 +44,8 @@ describe('tracing', () => { }); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); const transaction = hub.startTransaction({ name: 'dogpark', @@ -67,7 +69,8 @@ describe('tracing', () => { }); const hub = new Hub(new NodeClient(options)); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); - + jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); return hub; } @@ -236,11 +239,7 @@ describe('tracing', () => { const baggageHeader = request.getHeader('baggage') as string; const parts = sentryTraceHeader.split('-'); - expect(parts.length).toEqual(3); - expect(parts[0]).toEqual('86f39e84263a4de99c326acab3bfe3bd'); - expect(parts[1]).toEqual(expect.any(String)); - expect(parts[2]).toEqual('1'); - + expect(parts).toEqual(['86f39e84263a4de99c326acab3bfe3bd', expect.any(String), '1']); expect(baggageHeader).toEqual('sentry-trace_id=86f39e84263a4de99c326acab3bfe3bd,sentry-public_key=test-public-key'); }); @@ -355,7 +354,9 @@ describe('tracing', () => { const hub = new Hub(); - jest.spyOn(sentryCore, 'getCurrentHub').mockImplementation(() => hub); + jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); const client = new NodeClient(options); jest.spyOn(hub, 'getClient').mockImplementation(() => client); @@ -380,6 +381,10 @@ describe('tracing', () => { const hub = createHub({ shouldCreateSpanForRequest: () => false }); + jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); + httpIntegration.setupOnce( () => undefined, () => hub, @@ -485,6 +490,10 @@ describe('tracing', () => { const hub = createHub(); + jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); + httpIntegration.setupOnce( () => undefined, () => hub, diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 1c1c0ffee072..0600eb625b7f 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -1,5 +1,6 @@ import type { NodeOptions } from '@sentry/node'; -import { getCurrentHub, getCurrentScope, init as nodeInit } from '@sentry/node'; +import { getClient } from '@sentry/node'; +import { getCurrentScope, init as nodeInit } from '@sentry/node'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from './utils/debug-build'; @@ -68,8 +69,7 @@ export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express' export type { SentryMetaArgs } from './utils/types'; function sdkAlreadyInitialized(): boolean { - const hub = getCurrentHub(); - return !!hub.getClient(); + return !!getClient(); } /** Initializes Sentry Remix SDK on Node. */ diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 7e98dc123858..816410fd75f9 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { getActiveTransaction, hasTracingEnabled, runWithAsyncContext } from '@sentry/core'; +import { getActiveTransaction, getClient, getCurrentScope, hasTracingEnabled, runWithAsyncContext } from '@sentry/core'; import type { Hub } from '@sentry/node'; import { captureException, getCurrentHub } from '@sentry/node'; import type { Transaction, TransactionSource, WrappedFunction } from '@sentry/types'; @@ -225,7 +225,7 @@ function makeWrappedDataFunction( return async function (this: unknown, args: DataFunctionArgs): Promise { let res: Response | AppData; const activeTransaction = getActiveTransaction(); - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); try { const span = activeTransaction?.startChild({ @@ -280,7 +280,7 @@ function getTraceAndBaggage(): { sentryBaggage?: string; } { const transaction = getActiveTransaction(); - const currentScope = getCurrentHub().getScope(); + const currentScope = getCurrentScope(); if (isNodeEnv() && hasTracingEnabled()) { const span = currentScope.getSpan(); @@ -421,8 +421,8 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui return async function (this: unknown, request: RemixRequest, loadContext?: unknown): Promise { return runWithAsyncContext(async () => { const hub = getCurrentHub(); - const options = hub.getClient()?.getOptions(); - const scope = hub.getScope(); + const options = getClient()?.getOptions(); + const scope = getCurrentScope(); let normalizedRequest: Record = request; diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts index ab638866ffd4..b60b74a8e0ff 100644 --- a/packages/remix/src/utils/serverAdapters/express.ts +++ b/packages/remix/src/utils/serverAdapters/express.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, hasTracingEnabled } from '@sentry/core'; +import { getClient, getCurrentHub, getCurrentScope, hasTracingEnabled } from '@sentry/core'; import { flush } from '@sentry/node'; import type { Transaction } from '@sentry/types'; import { extractRequestData, isString, logger } from '@sentry/utils'; @@ -59,8 +59,8 @@ function wrapExpressRequestHandler( const request = extractRequestData(req); const hub = getCurrentHub(); - const options = hub.getClient()?.getOptions(); - const scope = hub.getScope(); + const options = getClient()?.getOptions(); + const scope = getCurrentScope(); scope.setSDKProcessingMetadata({ request }); diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 8e1740845c7b..0085c44c6eb8 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 { Event as SentryEvent, ReplayRecordingMode, Transaction } from '@sentry/types'; +import { captureException, getClient, getCurrentScope } from '@sentry/core'; +import type { ReplayRecordingMode, Transaction } from '@sentry/types'; import { logger } from '@sentry/utils'; import { @@ -698,7 +698,7 @@ export class ReplayContainer implements ReplayContainerInterface { * This is only available if performance is enabled, and if an instrumented router is used. */ public getCurrentRoute(): string | undefined { - const lastTransaction = this.lastTransaction || getCurrentHub().getScope().getTransaction(); + const lastTransaction = this.lastTransaction || getCurrentScope().getTransaction(); if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) { return undefined; } diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts index fac2b278e666..1824e1fa606c 100644 --- a/packages/replay/src/util/addGlobalListeners.ts +++ b/packages/replay/src/util/addGlobalListeners.ts @@ -1,5 +1,6 @@ import type { BaseClient } from '@sentry/core'; -import { addEventProcessor, getClient, getCurrentHub } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; +import { addEventProcessor, getClient } from '@sentry/core'; import type { Client, DynamicSamplingContext } from '@sentry/types'; import { addClickKeypressInstrumentationHandler, addHistoryInstrumentationHandler } from '@sentry/utils'; @@ -17,7 +18,7 @@ import type { ReplayContainer } from '../types'; */ export function addGlobalListeners(replay: ReplayContainer): void { // Listeners from core SDK // - const scope = getCurrentHub().getScope(); + const scope = getCurrentScope(); const client = getClient(); scope.addScopeListener(handleScopeListener(replay)); diff --git a/packages/replay/src/util/sendReplayRequest.ts b/packages/replay/src/util/sendReplayRequest.ts index 49710916fcb1..c030ea1f8c2f 100644 --- a/packages/replay/src/util/sendReplayRequest.ts +++ b/packages/replay/src/util/sendReplayRequest.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { getClient, getCurrentScope } from '@sentry/core'; import type { ReplayEvent, TransportMakeRequestResponse } from '@sentry/types'; import type { RateLimits } from '@sentry/utils'; import { isRateLimited, updateRateLimits } from '@sentry/utils'; @@ -30,9 +30,8 @@ export async function sendReplayRequest({ const { urls, errorIds, traceIds, initialTimestamp } = eventContext; - const hub = getCurrentHub(); - const client = hub.getClient(); - const scope = hub.getScope(); + const client = getClient(); + const scope = getCurrentScope(); const transport = client && client.getTransport(); const dsn = client && client.getDsn(); diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 1da44ed1b47c..aff1e58675f5 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -305,7 +305,7 @@ export function wrapHandler( sentryTrace, baggage, ); - hub.getScope().setPropagationContext(propagationContext); + Sentry.getCurrentScope().setPropagationContext(propagationContext); transaction = hub.startTransaction({ name: context.functionName, diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 699ae9c40ab5..33a3a25b8689 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/node'; +import { getCurrentScope } from '@sentry/node'; import type { Integration, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; // 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file. @@ -57,7 +57,7 @@ function wrapMakeRequest( ): MakeRequestFunction { return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) { let span: Span | undefined; - const scope = getCurrentHub().getScope(); + const scope = getCurrentScope(); const transaction = scope.getTransaction(); const req = orig.call(this, operation, params); req.on('afterBuild', () => { diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 8dfbcc092cb4..d475d9b3b421 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -1,5 +1,5 @@ import type { EventEmitter } from 'events'; -import { getCurrentHub } from '@sentry/node'; +import { getCurrentScope } from '@sentry/node'; import type { Integration, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; @@ -108,7 +108,7 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str return ret; } let span: Span | undefined; - const scope = getCurrentHub().getScope(); + const scope = getCurrentScope(); const transaction = scope.getTransaction(); if (transaction) { span = transaction.startChild({ diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index d3ef8646eab7..f9eb9a6cc3cd 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -1,7 +1,7 @@ // '@google-cloud/common' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. import type * as common from '@google-cloud/common'; -import { getCurrentHub } from '@sentry/node'; +import { getCurrentScope } from '@sentry/node'; import type { Integration, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; @@ -52,7 +52,7 @@ export class GoogleCloudHttp implements Integration { function wrapRequestFunction(orig: RequestFunction): RequestFunction { return function (this: common.Service, reqOpts: RequestOptions, callback: ResponseCallback): void { let span: Span | undefined; - const scope = getCurrentHub().getScope(); + const scope = getCurrentScope(); const transaction = scope.getTransaction(); if (transaction) { const httpMethod = reqOpts.method || 'GET'; diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index 2230db18f9a4..0afd5250a06f 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/browser'; +import { getCurrentScope } from '@sentry/browser'; import type { Span, Transaction } from '@sentry/types'; import { afterUpdate, beforeUpdate, onMount } from 'svelte'; import { current_component } from 'svelte/internal'; @@ -92,5 +92,5 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void { } function getActiveTransaction(): Transaction | undefined { - return getCurrentHub().getScope().getTransaction(); + return getCurrentScope().getTransaction(); } diff --git a/packages/svelte/test/performance.test.ts b/packages/svelte/test/performance.test.ts index aabf4462e8cb..e872ee7a283d 100644 --- a/packages/svelte/test/performance.test.ts +++ b/packages/svelte/test/performance.test.ts @@ -22,18 +22,12 @@ jest.mock('@sentry/core', () => { const original = jest.requireActual('@sentry/core'); return { ...original, - getCurrentHub(): { - getScope(): Scope; - } { + getCurrentScope(): Scope { return { - getScope(): any { - return { - getTransaction: () => { - return returnUndefinedTransaction ? undefined : testTransaction; - }, - }; + getTransaction: () => { + return returnUndefinedTransaction ? undefined : testTransaction; }, - }; + } as Scope; }, }; }); diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 3b16f659f6e0..7f9f581ca3c3 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,6 +1,7 @@ /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ import type { Span } from '@sentry/core'; -import { getActiveTransaction, getCurrentHub, runWithAsyncContext, startSpan } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; +import { getActiveTransaction, runWithAsyncContext, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; import { dynamicSamplingContextToSentryBaggageHeader, objectify } from '@sentry/utils'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; @@ -102,7 +103,7 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { // if there is an active transaction, we know that this handle call is nested and hence // we don't create a new domain for it. If we created one, nested server calls would // create new transactions instead of adding a child span to the currently active span. - if (getCurrentHub().getScope().getSpan()) { + if (getCurrentScope().getSpan()) { return instrumentHandle(input, options); } return runWithAsyncContext(() => { @@ -122,7 +123,7 @@ async function instrumentHandle( } const { dynamicSamplingContext, traceparentData, propagationContext } = getTracePropagationData(event); - getCurrentHub().getScope().setPropagationContext(propagationContext); + getCurrentScope().setPropagationContext(propagationContext); try { const resolveResult = await startSpan( diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts index c902fe4376d6..5d0cd3c1cb90 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server/load.ts @@ -1,5 +1,5 @@ /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ -import { getCurrentHub, startSpan } from '@sentry/core'; +import { getCurrentScope, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; import type { TransactionContext } from '@sentry/types'; import { addNonEnumerableProperty, objectify } from '@sentry/utils'; @@ -130,7 +130,7 @@ export function wrapServerLoadWithSentry any>(origSe const routeId = event.route && (Object.getOwnPropertyDescriptor(event.route, 'id')?.value as string | undefined); const { dynamicSamplingContext, traceparentData, propagationContext } = getTracePropagationData(event); - getCurrentHub().getScope().setPropagationContext(propagationContext); + getCurrentScope().setPropagationContext(propagationContext); const traceLoadContext: TransactionContext = { op: 'function.sveltekit.server.load', diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index b45e15679805..ab2f73b127f0 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { getCurrentHub, getDynamicSamplingContextFromClient, hasTracingEnabled } from '@sentry/core'; +import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, hasTracingEnabled } from '@sentry/core'; import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/types'; import { BAGGAGE_HEADER_NAME, @@ -264,8 +264,7 @@ export function xhrCallback( return undefined; } - const hub = getCurrentHub(); - const scope = hub.getScope(); + const scope = getCurrentScope(); const parentSpan = scope.getSpan(); const span = @@ -294,7 +293,7 @@ export function xhrCallback( const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); setHeaderOnXhr(xhr, span.toTraceparent(), sentryBaggageHeader); } else { - const client = hub.getClient(); + const client = getClient(); const { traceId, sampled, dsc } = scope.getPropagationContext(); const sentryTraceHeader = generateSentryTraceHeader(traceId, undefined, sampled); const dynamicSamplingContext = diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index 9c45da8adfa3..63f7f8ede721 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, getDynamicSamplingContextFromClient, hasTracingEnabled } from '@sentry/core'; +import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, hasTracingEnabled } from '@sentry/core'; import type { Client, HandlerDataFetch, Scope, Span, SpanOrigin } from '@sentry/types'; import { BAGGAGE_HEADER_NAME, @@ -65,9 +65,8 @@ export function instrumentFetchRequest( return undefined; } - const hub = getCurrentHub(); - const scope = hub.getScope(); - const client = hub.getClient(); + const scope = getCurrentScope(); + const client = getClient(); const parentSpan = scope.getSpan(); const { method, url } = handlerData.fetchData; diff --git a/packages/vercel-edge/src/integrations/wintercg-fetch.ts b/packages/vercel-edge/src/integrations/wintercg-fetch.ts index ded26ed21a9d..7c75308c72fe 100644 --- a/packages/vercel-edge/src/integrations/wintercg-fetch.ts +++ b/packages/vercel-edge/src/integrations/wintercg-fetch.ts @@ -1,5 +1,5 @@ import { instrumentFetchRequest } from '@sentry-internal/tracing'; -import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; +import { getClient, getCurrentHub, isSentryRequestUrl } from '@sentry/core'; import type { FetchBreadcrumbData, FetchBreadcrumbHint, HandlerDataFetch, Integration, Span } from '@sentry/types'; import { LRUMap, addFetchInstrumentationHandler, stringMatchesSomePattern } from '@sentry/utils'; @@ -74,8 +74,7 @@ export class WinterCGFetch implements Integration { /** Decides whether to attach trace data to the outgoing fetch request */ private _shouldAttachTraceData(url: string): boolean { - const hub = getCurrentHub(); - const client = hub.getClient(); + const client = getClient(); if (!client) { return false; diff --git a/packages/vercel-edge/test/wintercg-fetch.test.ts b/packages/vercel-edge/test/wintercg-fetch.test.ts index 22bd960defdf..d35aaa64f35c 100644 --- a/packages/vercel-edge/test/wintercg-fetch.test.ts +++ b/packages/vercel-edge/test/wintercg-fetch.test.ts @@ -29,6 +29,8 @@ const fakeHubInstance = new FakeHub( ); jest.spyOn(sentryCore, 'getCurrentHub').mockImplementation(() => fakeHubInstance); +jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => fakeHubInstance.getScope()); +jest.spyOn(sentryCore, 'getClient').mockImplementation(() => fakeHubInstance.getClient()); const addFetchInstrumentationHandlerSpy = jest.spyOn(sentryUtils, 'addFetchInstrumentationHandler'); const instrumentFetchRequestSpy = jest.spyOn(internalTracing, 'instrumentFetchRequest'); diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 82000b9799a4..ef509dcdb406 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/browser'; +import { getCurrentHub, getCurrentScope } from '@sentry/browser'; import type { Span, Transaction } from '@sentry/types'; import { logger, timestampInSeconds } from '@sentry/utils'; @@ -34,7 +34,7 @@ const HOOKS: { [key in Operation]: Hook[] } = { /** Grabs active transaction off scope, if any */ export function getActiveTransaction(): Transaction | undefined { - return getCurrentHub().getScope().getTransaction(); + return getCurrentScope().getTransaction(); } /** Finish top-level span and activity with a debounce configured using `timeout` option */ From 5bc9a38726ac567072cca8aa8537e9292234947d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 15 Dec 2023 14:54:22 +0100 Subject: [PATCH 06/34] fix(utils): Update `eventFromUnknownInput` to avoid scope pollution & `getCurrentHub` (#9868) Instead we can pass a client directly, and I refactored the method to avoid setting extra on the scope, and just set it on the event directly - as this is also kind of leaking right now, because the extra may also be applied to other events using the same scope. --- packages/core/src/server-runtime-client.ts | 4 +- .../deno/src/integrations/globalhandlers.ts | 7 +- packages/node/test/eventbuilders.test.ts | 85 ++++++++++++------- packages/utils/src/eventbuilder.ts | 20 +++-- packages/utils/test/eventbuilder.test.ts | 5 ++ 5 files changed, 79 insertions(+), 42 deletions(-) diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 4165aec8fc46..719e2b81f086 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -16,7 +16,7 @@ import { eventFromMessage, eventFromUnknownInput, logger, resolvedSyncPromise, u import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; import { DEBUG_BUILD } from './debug-build'; -import { getCurrentHub } from './hub'; +import { getClient } from './exports'; import type { Scope } from './scope'; import { SessionFlusher } from './sessionflusher'; import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; @@ -50,7 +50,7 @@ export class ServerRuntimeClient< * @inheritDoc */ public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { - return resolvedSyncPromise(eventFromUnknownInput(getCurrentHub, this._options.stackParser, exception, hint)); + return resolvedSyncPromise(eventFromUnknownInput(getClient(), this._options.stackParser, exception, hint)); } /** diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index d173780cfa50..9914764d4c45 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -1,5 +1,6 @@ import type { ServerRuntimeClient } from '@sentry/core'; -import { flush, getCurrentHub } from '@sentry/core'; +import { getClient, getCurrentHub, getCurrentScope } from '@sentry/core'; +import { flush } from '@sentry/core'; import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types'; import { eventFromUnknownInput, isPrimitive } from '@sentry/utils'; @@ -70,7 +71,7 @@ function installGlobalErrorHandler(): void { const [hub, stackParser] = getHubAndOptions(); const { message, error } = data; - const event = eventFromUnknownInput(getCurrentHub, stackParser, error || message); + const event = eventFromUnknownInput(getClient(), stackParser, error || message); event.level = 'fatal'; @@ -113,7 +114,7 @@ function installGlobalUnhandledRejectionHandler(): void { const event = isPrimitive(error) ? eventFromRejectionWithPrimitive(error) - : eventFromUnknownInput(getCurrentHub, stackParser, error, undefined); + : eventFromUnknownInput(getClient(), stackParser, error, undefined); event.level = 'fatal'; diff --git a/packages/node/test/eventbuilders.test.ts b/packages/node/test/eventbuilders.test.ts index 3c2ae88f03e3..ead2d01e9b44 100644 --- a/packages/node/test/eventbuilders.test.ts +++ b/packages/node/test/eventbuilders.test.ts @@ -1,40 +1,51 @@ -import type { Client } from '@sentry/types'; +import type { Hub } from '@sentry/types'; import { eventFromUnknownInput } from '@sentry/utils'; -import { Scope, defaultStackParser, getCurrentHub } from '../src'; +import { defaultStackParser } from '../src'; -const testScope = new Scope(); - -jest.mock('@sentry/core', () => { - const original = jest.requireActual('@sentry/core'); - return { - ...original, - getCurrentHub(): { - getClient(): Client; - getScope(): Scope; - } { - return { - getClient(): any { - return { - getOptions(): any { - return { normalizeDepth: 6 }; +describe('eventFromUnknownInput', () => { + test('uses normalizeDepth from init options', () => { + const deepObject = { + a: { + b: { + c: { + d: { + e: { + f: { + g: 'foo', + }, + }, }, - }; - }, - getScope(): Scope { - return testScope; + }, }, - }; - }, - }; -}); + }, + }; -afterEach(() => { - jest.resetAllMocks(); -}); + const client = { + getOptions(): any { + return { normalizeDepth: 6 }; + }, + } as any; + const event = eventFromUnknownInput(client, defaultStackParser, deepObject); -describe('eventFromUnknownInput', () => { - test('uses normalizeDepth from init options', () => { + const serializedObject = event.extra?.__serialized__; + expect(serializedObject).toBeDefined(); + expect(serializedObject).toEqual({ + a: { + b: { + c: { + d: { + e: { + f: '[Object]', + }, + }, + }, + }, + }, + }); + }); + + test('uses normalizeDepth from init options (passing getCurrentHub)', () => { const deepObject = { a: { b: { @@ -51,9 +62,19 @@ describe('eventFromUnknownInput', () => { }, }; - eventFromUnknownInput(getCurrentHub, defaultStackParser, deepObject); + const getCurrentHub = jest.fn(() => { + return { + getClient: () => ({ + getOptions(): any { + return { normalizeDepth: 6 }; + }, + }), + } as unknown as Hub; + }); + + const event = eventFromUnknownInput(getCurrentHub, defaultStackParser, deepObject); - const serializedObject = (testScope as any)._extra.__serialized__; + const serializedObject = event.extra?.__serialized__; expect(serializedObject).toBeDefined(); expect(serializedObject).toEqual({ a: { diff --git a/packages/utils/src/eventbuilder.ts b/packages/utils/src/eventbuilder.ts index 5dac2f583bb6..28b2d94b0c4f 100644 --- a/packages/utils/src/eventbuilder.ts +++ b/packages/utils/src/eventbuilder.ts @@ -1,7 +1,9 @@ import type { + Client, Event, EventHint, Exception, + Extras, Hub, Mechanism, Severity, @@ -61,14 +63,18 @@ function getMessageForObject(exception: object): string { /** * Builds and Event from a Exception + * + * TODO(v8): Remove getHub fallback * @hidden */ export function eventFromUnknownInput( - getCurrentHub: () => Hub, + getHubOrClient: (() => Hub) | Client | undefined, stackParser: StackParser, exception: unknown, hint?: EventHint, ): Event { + const client = typeof getHubOrClient === 'function' ? getHubOrClient().getClient() : getHubOrClient; + let ex: unknown = exception; const providedMechanism: Mechanism | undefined = hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; @@ -77,12 +83,12 @@ export function eventFromUnknownInput( type: 'generic', }; + let extras: Extras | undefined; + if (!isError(exception)) { if (isPlainObject(exception)) { - const hub = getCurrentHub(); - const client = hub.getClient(); const normalizeDepth = client && client.getOptions().normalizeDepth; - hub.getScope().setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); + extras = { ['__serialized__']: normalizeToSize(exception as Record, normalizeDepth) }; const message = getMessageForObject(exception); ex = (hint && hint.syntheticException) || new Error(message); @@ -96,12 +102,16 @@ export function eventFromUnknownInput( mechanism.synthetic = true; } - const event = { + const event: Event = { exception: { values: [exceptionFromError(stackParser, ex as Error)], }, }; + if (extras) { + event.extra = extras; + } + addExceptionTypeValue(event, undefined, undefined); addExceptionMechanism(event, mechanism); diff --git a/packages/utils/test/eventbuilder.test.ts b/packages/utils/test/eventbuilder.test.ts index b1c46630de08..ec3fdf4bf6ee 100644 --- a/packages/utils/test/eventbuilder.test.ts +++ b/packages/utils/test/eventbuilder.test.ts @@ -36,4 +36,9 @@ describe('eventFromUnknownInput', () => { const event = eventFromUnknownInput(getCurrentHub, stackParser, { foo: { bar: 'baz' }, message: 'Some message' }); expect(event.exception?.values?.[0].value).toBe('Some message'); }); + + test('passing client directly', () => { + const event = eventFromUnknownInput(undefined, stackParser, { foo: { bar: 'baz' }, prop: 1 }); + expect(event.exception?.values?.[0].value).toBe('Object captured as exception with keys: foo, prop'); + }); }); From 72c348827fe38c8eed1322a259a5dfa2d04a34fd Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 15 Dec 2023 14:54:51 +0100 Subject: [PATCH 07/34] ref(node): Refactor node integrations to use `processEvent` (#9018) This refactors Node integrations to use `processEvent`. Missing is the LocalVariables integration, as that is more complicated and may need to be refactored in a different way to properly work. --- packages/node/src/integrations/context.ts | 52 ++++++++++--------- .../node/src/integrations/contextlines.ts | 15 +++--- packages/node/src/integrations/modules.ts | 40 +++++++------- packages/node/test/index.test.ts | 39 ++++++++------ 4 files changed, 77 insertions(+), 69 deletions(-) diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index f7044c509265..ee565c8676e7 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -11,7 +11,6 @@ import type { CultureContext, DeviceContext, Event, - EventProcessor, Integration, OsContext, } from '@sentry/types'; @@ -60,20 +59,25 @@ export class Context implements Integration { }, ) {} - /** - * @inheritDoc - */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { - addGlobalEventProcessor(event => this.addContext(event)); + /** @inheritDoc */ + public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { + // noop } - /** Processes an event and adds context */ + /** @inheritDoc */ + public processEvent(event: Event): Promise { + return this.addContext(event); + } + + /** + * Processes an event and adds context. + */ public async addContext(event: Event): Promise { if (this._cachedContext === undefined) { this._cachedContext = this._getContexts(); } - const updatedContext = this._updateContext(await this._cachedContext); + const updatedContext = _updateContext(await this._cachedContext); event.contexts = { ...event.contexts, @@ -87,22 +91,6 @@ export class Context implements Integration { return event; } - /** - * Updates the context with dynamic values that can change - */ - private _updateContext(contexts: Contexts): Contexts { - // Only update properties if they exist - if (contexts?.app?.app_memory) { - contexts.app.app_memory = process.memoryUsage().rss; - } - - if (contexts?.device?.free_memory) { - contexts.device.free_memory = os.freemem(); - } - - return contexts; - } - /** * Gets the contexts for the current environment */ @@ -137,6 +125,22 @@ export class Context implements Integration { } } +/** + * Updates the context with dynamic values that can change + */ +function _updateContext(contexts: Contexts): Contexts { + // Only update properties if they exist + if (contexts?.app?.app_memory) { + contexts.app.app_memory = process.memoryUsage().rss; + } + + if (contexts?.device?.free_memory) { + contexts.device.free_memory = os.freemem(); + } + + return contexts; +} + /** * Returns the operating system context. * diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts index c48f35adfd8b..2cc9375a879a 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -55,14 +55,13 @@ export class ContextLines implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(event => { - const self = getCurrentHub().getIntegration(ContextLines); - if (!self) { - return event; - } - return this.addSourceContext(event); - }); + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + // noop + } + + /** @inheritDoc */ + public processEvent(event: Event): Promise { + return this.addSourceContext(event); } /** Processes an event and adds context lines */ diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index 9a81d5808425..cc8ecf621bb9 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; -import type { EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Event, EventProcessor, Hub, Integration } from '@sentry/types'; let moduleCache: { [key: string]: string }; @@ -65,6 +65,14 @@ function collectModules(): { return infos; } +/** Fetches the list of modules and the versions loaded by the entry file for your node.js app. */ +function _getModules(): { [key: string]: string } { + if (!moduleCache) { + moduleCache = collectModules(); + } + return moduleCache; +} + /** Add node modules / packages to the event */ export class Modules implements Integration { /** @@ -80,26 +88,18 @@ export class Modules implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(event => { - if (!getCurrentHub().getIntegration(Modules)) { - return event; - } - return { - ...event, - modules: { - ...event.modules, - ...this._getModules(), - }, - }; - }); + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + // noop } - /** Fetches the list of modules and the versions loaded by the entry file for your node.js app. */ - private _getModules(): { [key: string]: string } { - if (!moduleCache) { - moduleCache = collectModules(); - } - return moduleCache; + /** @inheritdoc */ + public processEvent(event: Event): Event { + return { + ...event, + modules: { + ...event.modules, + ..._getModules(), + }, + }; } } diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 3bb90a710708..7f9abbdc1072 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -165,34 +165,39 @@ describe('SentryNode', () => { } }); - test('capture an exception with pre/post context', done => { - expect.assertions(10); + test('capture an exception with pre/post context', async () => { + const beforeSend = jest.fn((event: Event) => { + expect(event.tags).toEqual({ test: '1' }); + expect(event.exception).not.toBeUndefined(); + expect(event.exception!.values![0]).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined(); + expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined(); + expect(event.exception!.values![0].type).toBe('Error'); + expect(event.exception!.values![0].value).toBe('test'); + expect(event.exception!.values![0].stacktrace).toBeTruthy(); + return null; + }); + const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, - beforeSend: (event: Event) => { - expect(event.tags).toEqual({ test: '1' }); - expect(event.exception).not.toBeUndefined(); - expect(event.exception!.values![0]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1]).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].pre_context).not.toBeUndefined(); - expect(event.exception!.values![0].stacktrace!.frames![1].post_context).not.toBeUndefined(); - expect(event.exception!.values![0].type).toBe('Error'); - expect(event.exception!.values![0].value).toBe('test'); - expect(event.exception!.values![0].stacktrace).toBeTruthy(); - done(); - return null; - }, + beforeSend, dsn, integrations: [new ContextLines()], }); - getCurrentHub().bindClient(new NodeClient(options)); + const client = new NodeClient(options); + getCurrentHub().bindClient(client); getCurrentScope().setTag('test', '1'); try { throw new Error('test'); } catch (e) { captureException(e); } + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); }); test('capture a linked exception with pre/post context', done => { From e128936a9b8ad3bff18b3f77ed88f58c9587f627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Garc=C3=ADa=20Hinestrosa?= Date: Fri, 15 Dec 2023 16:13:55 +0100 Subject: [PATCH 08/34] fix(utils): Support crypto.getRandomValues in old Chromium versions (#9251) Here is my proposal to fix `getRandomByte` function throwing an error due to `crypto.getRandomValues` returning `undefined` in old browser engines like Chromium 23. This error could be fixed as well by using a polyfill in every project that imports and uses Sentry but, since the change of this PR only involved keeping the `Uint8Array` reference in a variable, I thought it would be worth it to give it a try. --- packages/utils/src/misc.ts | 10 +++++++++- packages/utils/test/misc.test.ts | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index 9799df421c31..c8afc0818909 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -31,7 +31,15 @@ export function uuid4(): string { return crypto.randomUUID().replace(/-/g, ''); } if (crypto && crypto.getRandomValues) { - getRandomByte = () => crypto.getRandomValues(new Uint8Array(1))[0]; + getRandomByte = () => { + // crypto.getRandomValues might return undefined instead of the typed array + // in old Chromium versions (e.g. 23.0.1235.0 (151422)) + // However, `typedArray` is still filled in-place. + // @see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#typedarray + const typedArray = new Uint8Array(1); + crypto.getRandomValues(typedArray); + return typedArray[0]; + }; } } catch (_) { // some runtimes can crash invoking crypto diff --git a/packages/utils/test/misc.test.ts b/packages/utils/test/misc.test.ts index dc75b70d4286..c1eb978dcdbe 100644 --- a/packages/utils/test/misc.test.ts +++ b/packages/utils/test/misc.test.ts @@ -343,6 +343,25 @@ describe('uuid4 generation', () => { expect(uuid4()).toMatch(uuid4Regex); } }); + + // Corner case related to crypto.getRandomValues being only + // semi-implemented (e.g. Chromium 23.0.1235.0 (151422)) + it('returns valid uuid v4 even if crypto.getRandomValues does not return a typed array', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const cryptoMod = require('crypto'); + + const getRandomValues = (typedArray: Uint8Array) => { + if (cryptoMod.getRandomValues) { + cryptoMod.getRandomValues(typedArray); + } + }; + + (global as any).crypto = { getRandomValues }; + + for (let index = 0; index < 1_000; index++) { + expect(uuid4()).toMatch(uuid4Regex); + } + }); }); describe('arrayify()', () => { From 53724c519b1f0b1d614b5a8ea79b97ab696bf262 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 15 Dec 2023 15:23:17 -0330 Subject: [PATCH 09/34] meta(feedback): Fix syntax error in README (#9875) --- packages/feedback/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/feedback/README.md b/packages/feedback/README.md index 7aa8df72cd80..673bf344ea75 100644 --- a/packages/feedback/README.md +++ b/packages/feedback/README.md @@ -73,16 +73,15 @@ By default the Feedback integration will attempt to fill in the name/email field ```javascript Sentry.setUser({ - email: 'foo@example.com', + userEmail: 'foo@example.com', fullName: 'Jane Doe', }); - new Feedback({ - useSentryUser({ - email: 'email', - name: 'fullName', - }), + useSentryUser: { + email: 'userEmail', + name: 'fullName', + }, }) ``` From 12c146b0b76a651fad2ce0439d8a4d079b5038b1 Mon Sep 17 00:00:00 2001 From: Adam Misiorny Date: Mon, 18 Dec 2023 09:44:24 +0100 Subject: [PATCH 10/34] fix(nextjs): Export `createReduxEnhancer` (#9854) --- packages/nextjs/src/server/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index f2146a2cba38..f380f949ef6b 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -13,6 +13,7 @@ import { getVercelEnv } from '../common/getVercelEnv'; import { buildMetadata } from '../common/metadata'; import { isBuild } from '../common/utils/isBuild'; +export { createReduxEnhancer } from '@sentry/react'; export * from '@sentry/node'; export { captureUnderscoreErrorException } from '../common/_error'; From db4bef1e6d88b273a9dddd0d8a55a3822c9d50d8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Dec 2023 09:54:27 +0100 Subject: [PATCH 11/34] ref: Pass client instead of hub to `isSentryRequestUrl` (#9869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step by step, eradicating the hub... Note: I haven't found a way to mark an attribute type of a function as deprecated 😬 I think it's OK here, but generally a bit annoying/tricky... --- packages/core/src/utils/isSentryRequestUrl.ts | 12 +++++++++--- .../test/lib/utils/isSentryRequestUrl.test.ts | 16 +++++++++++----- packages/integrations/src/httpclient.ts | 6 ++---- .../node-experimental/src/integrations/http.ts | 2 +- packages/node/src/integrations/http.ts | 2 +- packages/node/src/integrations/undici/index.ts | 18 ++++++++++++------ .../src/utils/isSentryRequest.ts | 4 ++-- .../opentelemetry/src/utils/isSentryRequest.ts | 4 ++-- .../replay/src/util/shouldFilterRequest.ts | 4 ++-- .../src/integrations/wintercg-fetch.ts | 2 +- 10 files changed, 43 insertions(+), 27 deletions(-) diff --git a/packages/core/src/utils/isSentryRequestUrl.ts b/packages/core/src/utils/isSentryRequestUrl.ts index 0256e3cf7835..3a31f63cf46c 100644 --- a/packages/core/src/utils/isSentryRequestUrl.ts +++ b/packages/core/src/utils/isSentryRequestUrl.ts @@ -1,11 +1,13 @@ -import type { DsnComponents, Hub } from '@sentry/types'; +import type { Client, DsnComponents, Hub } from '@sentry/types'; /** * Checks whether given url points to Sentry server * @param url url to verify + * + * TODO(v8): Remove Hub fallback type */ -export function isSentryRequestUrl(url: string, hub: Hub): boolean { - const client = hub.getClient(); +export function isSentryRequestUrl(url: string, hubOrClient: Hub | Client | undefined): boolean { + const client = hubOrClient && isHub(hubOrClient) ? hubOrClient.getClient() : hubOrClient; const dsn = client && client.getDsn(); const tunnel = client && client.getOptions().tunnel; @@ -27,3 +29,7 @@ function checkDsn(url: string, dsn: DsnComponents | undefined): boolean { function removeTrailingSlash(str: string): string { return str[str.length - 1] === '/' ? str.slice(0, -1) : str; } + +function isHub(hubOrClient: Hub | Client | undefined): hubOrClient is Hub { + return (hubOrClient as Hub).getClient !== undefined; +} diff --git a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts index b1671b9410e8..98fd7e54207b 100644 --- a/packages/core/test/lib/utils/isSentryRequestUrl.test.ts +++ b/packages/core/test/lib/utils/isSentryRequestUrl.test.ts @@ -1,4 +1,4 @@ -import type { Hub } from '@sentry/types'; +import type { Client, Hub } from '@sentry/types'; import { isSentryRequestUrl } from '../../../src'; @@ -12,15 +12,21 @@ describe('isSentryRequestUrl', () => { ['http://tunnel:4200/', 'sentry-dsn.com', 'http://tunnel:4200', true], ['http://tunnel:4200/a', 'sentry-dsn.com', 'http://tunnel:4200', false], ])('works with url=%s, dsn=%s, tunnel=%s', (url: string, dsn: string, tunnel: string, expected: boolean) => { + const client = { + getOptions: () => ({ tunnel }), + getDsn: () => ({ host: dsn }), + } as unknown as Client; + const hub = { getClient: () => { - return { - getOptions: () => ({ tunnel }), - getDsn: () => ({ host: dsn }), - }; + return client; }, } as unknown as Hub; + // Works with hub passed expect(isSentryRequestUrl(url, hub)).toBe(expected); + + // Works with client passed + expect(isSentryRequestUrl(url, client)).toBe(expected); }); }); diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index 1e1ee0318861..c03cd63e6840 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; +import { getClient, isSentryRequestUrl } from '@sentry/core'; import type { Event as SentryEvent, EventProcessor, @@ -348,9 +348,7 @@ export class HttpClient implements Integration { */ private _shouldCaptureResponse(status: number, url: string): boolean { return ( - this._isInGivenStatusRanges(status) && - this._isInGivenRequestTargets(url) && - !isSentryRequestUrl(url, getCurrentHub()) + this._isInGivenStatusRanges(status) && this._isInGivenRequestTargets(url) && !isSentryRequestUrl(url, getClient()) ); } diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 974828fd46fe..6fe99e90101c 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -102,7 +102,7 @@ export class Http implements Integration { return false; } - if (isSentryRequestUrl(url, getCurrentHub())) { + if (isSentryRequestUrl(url, getClient())) { return true; } diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index b61d34574457..94aa36b80901 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -240,7 +240,7 @@ function _createWrappedRequestMethodFactory( const requestUrl = extractUrl(requestOptions); // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method - if (isSentryRequestUrl(requestUrl, getCurrentHub())) { + if (isSentryRequestUrl(requestUrl, getClient())) { return originalRequestMethod.apply(httpModule, requestArgs); } diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 7681a26ba7ca..b260f26e192d 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,4 +1,10 @@ -import { getCurrentHub, getCurrentScope, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; +import { + getClient, + getCurrentHub, + getCurrentScope, + getDynamicSamplingContextFromClient, + isSentryRequestUrl, +} from '@sentry/core'; import type { EventProcessor, Integration, Span } from '@sentry/types'; import { LRUMap, @@ -137,12 +143,12 @@ export class Undici implements Integration { const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - if (isSentryRequestUrl(stringUrl, hub) || request.__sentry_span__ !== undefined) { + const client = getClient(); + if (!client) { return; } - const client = hub.getClient(); - if (!client) { + if (isSentryRequestUrl(stringUrl, client) || request.__sentry_span__ !== undefined) { return; } @@ -197,7 +203,7 @@ export class Undici implements Integration { const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - if (isSentryRequestUrl(stringUrl, hub)) { + if (isSentryRequestUrl(stringUrl, getClient())) { return; } @@ -237,7 +243,7 @@ export class Undici implements Integration { const stringUrl = request.origin ? request.origin.toString() + request.path : request.path; - if (isSentryRequestUrl(stringUrl, hub)) { + if (isSentryRequestUrl(stringUrl, getClient())) { return; } diff --git a/packages/opentelemetry-node/src/utils/isSentryRequest.ts b/packages/opentelemetry-node/src/utils/isSentryRequest.ts index 5b285bb0ec68..85cb6c9c77b9 100644 --- a/packages/opentelemetry-node/src/utils/isSentryRequest.ts +++ b/packages/opentelemetry-node/src/utils/isSentryRequest.ts @@ -1,6 +1,6 @@ import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; +import { getClient, isSentryRequestUrl } from '@sentry/core'; /** * @@ -16,5 +16,5 @@ export function isSentryRequestSpan(otelSpan: OtelSpan): boolean { return false; } - return isSentryRequestUrl(httpUrl.toString(), getCurrentHub()); + return isSentryRequestUrl(httpUrl.toString(), getClient()); } diff --git a/packages/opentelemetry/src/utils/isSentryRequest.ts b/packages/opentelemetry/src/utils/isSentryRequest.ts index 361cc89d0ad7..7d146d551e12 100644 --- a/packages/opentelemetry/src/utils/isSentryRequest.ts +++ b/packages/opentelemetry/src/utils/isSentryRequest.ts @@ -1,5 +1,5 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; +import { getClient, isSentryRequestUrl } from '@sentry/core'; import type { AbstractSpan } from '../types'; import { spanHasAttributes } from './spanTypes'; @@ -22,5 +22,5 @@ export function isSentryRequestSpan(span: AbstractSpan): boolean { return false; } - return isSentryRequestUrl(httpUrl.toString(), getCurrentHub()); + return isSentryRequestUrl(httpUrl.toString(), getClient()); } diff --git a/packages/replay/src/util/shouldFilterRequest.ts b/packages/replay/src/util/shouldFilterRequest.ts index 274ad4ee488a..71a268be8d64 100644 --- a/packages/replay/src/util/shouldFilterRequest.ts +++ b/packages/replay/src/util/shouldFilterRequest.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, isSentryRequestUrl } from '@sentry/core'; +import { getClient, isSentryRequestUrl } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import type { ReplayContainer } from '../types'; @@ -13,5 +13,5 @@ export function shouldFilterRequest(replay: ReplayContainer, url: string): boole return false; } - return isSentryRequestUrl(url, getCurrentHub()); + return isSentryRequestUrl(url, getClient()); } diff --git a/packages/vercel-edge/src/integrations/wintercg-fetch.ts b/packages/vercel-edge/src/integrations/wintercg-fetch.ts index 7c75308c72fe..b03cae819073 100644 --- a/packages/vercel-edge/src/integrations/wintercg-fetch.ts +++ b/packages/vercel-edge/src/integrations/wintercg-fetch.ts @@ -54,7 +54,7 @@ export class WinterCGFetch implements Integration { return; } - if (isSentryRequestUrl(handlerData.fetchData.url, hub)) { + if (isSentryRequestUrl(handlerData.fetchData.url, getClient())) { return; } From f2a4caab9a9e56b856a659742908c185c206ce31 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Dec 2023 09:55:47 +0100 Subject: [PATCH 12/34] feat(core): Update `withScope` to return callback return value (#9866) To align this with OpenTelemetry and make some things possible that are currently not easily doable without `pushScope` / `popScope`. Noticed this because currently it's not easily possible to e.g. use `withScope` in places like [this](https://github.com/getsentry/sentry-javascript/pull/9862#discussion_r1427826506). This should be backwards compatible because any code that previously relied on this returning `void` should still work. --- packages/core/src/exports.ts | 4 +- packages/core/src/hub.ts | 4 +- packages/core/test/lib/exports.test.ts | 53 +++++++++++ .../feedback/src/util/sendFeedbackRequest.ts | 91 +++++++++---------- packages/types/src/hub.ts | 2 +- 5 files changed, 99 insertions(+), 55 deletions(-) create mode 100644 packages/core/test/lib/exports.test.ts diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 0e574c4853cc..c5ab1055e7f4 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -163,8 +163,8 @@ export function setUser(user: User | null): ReturnType { * * @param callback that will be enclosed into push/popScope. */ -export function withScope(callback: (scope: Scope) => void): ReturnType { - getCurrentHub().withScope(callback); +export function withScope(callback: (scope: Scope) => T): T { + return getCurrentHub().withScope(callback); } /** diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 13d5fd059e93..b7cffed42902 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -161,10 +161,10 @@ export class Hub implements HubInterface { /** * @inheritDoc */ - public withScope(callback: (scope: Scope) => void): void { + public withScope(callback: (scope: Scope) => T): T { const scope = this.pushScope(); try { - callback(scope); + return callback(scope); } finally { this.popScope(); } diff --git a/packages/core/test/lib/exports.test.ts b/packages/core/test/lib/exports.test.ts new file mode 100644 index 000000000000..89b4fd9105d5 --- /dev/null +++ b/packages/core/test/lib/exports.test.ts @@ -0,0 +1,53 @@ +import { Hub, Scope, getCurrentScope, makeMain, withScope } from '../../src'; +import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; + +function getTestClient(): TestClient { + return new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + }), + ); +} + +describe('withScope', () => { + beforeEach(() => { + const client = getTestClient(); + const hub = new Hub(client); + makeMain(hub); + }); + + it('works without a return value', () => { + const scope1 = getCurrentScope(); + expect(scope1).toBeInstanceOf(Scope); + + scope1.setTag('foo', 'bar'); + + const res = withScope(scope => { + expect(scope).toBeInstanceOf(Scope); + expect(scope).not.toBe(scope1); + expect(scope['_tags']).toEqual({ foo: 'bar' }); + + expect(getCurrentScope()).toBe(scope); + }); + + expect(getCurrentScope()).toBe(scope1); + expect(res).toBe(undefined); + }); + + it('works with a return value', () => { + const res = withScope(scope => { + return 'foo'; + }); + + expect(res).toBe('foo'); + }); + + it('works with an async function', async () => { + const res = withScope(async scope => { + return 'foo'; + }); + + expect(res).toBeInstanceOf(Promise); + expect(await res).toBe('foo'); + }); +}); diff --git a/packages/feedback/src/util/sendFeedbackRequest.ts b/packages/feedback/src/util/sendFeedbackRequest.ts index 5e8e532ca58d..f1629a00670a 100644 --- a/packages/feedback/src/util/sendFeedbackRequest.ts +++ b/packages/feedback/src/util/sendFeedbackRequest.ts @@ -33,67 +33,58 @@ export async function sendFeedbackRequest( type: 'feedback', }; - return new Promise((resolve, reject) => { - withScope(async scope => { - // No use for breadcrumbs in feedback - scope.clearBreadcrumbs(); - - if ([FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE].includes(String(source))) { - scope.setLevel('info'); - } + return withScope(async scope => { + // No use for breadcrumbs in feedback + scope.clearBreadcrumbs(); + + if ([FEEDBACK_API_SOURCE, FEEDBACK_WIDGET_SOURCE].includes(String(source))) { + scope.setLevel('info'); + } + + const feedbackEvent = await prepareFeedbackEvent({ + scope, + client, + event: baseEvent, + }); - const feedbackEvent = await prepareFeedbackEvent({ - scope, - client, - event: baseEvent, - }); + if (!feedbackEvent) { + return; + } - if (!feedbackEvent) { - resolve(); - return; - } + if (client.emit) { + client.emit('beforeSendFeedback', feedbackEvent, { includeReplay: Boolean(includeReplay) }); + } - if (client.emit) { - client.emit('beforeSendFeedback', feedbackEvent, { includeReplay: Boolean(includeReplay) }); - } + const envelope = createEventEnvelope(feedbackEvent, dsn, client.getOptions()._metadata, client.getOptions().tunnel); - const envelope = createEventEnvelope( - feedbackEvent, - dsn, - client.getOptions()._metadata, - client.getOptions().tunnel, - ); + let response: void | TransportMakeRequestResponse; - let response: void | TransportMakeRequestResponse; + try { + response = await transport.send(envelope); + } catch (err) { + const error = new Error('Unable to send Feedback'); try { - response = await transport.send(envelope); - } catch (err) { - const error = new Error('Unable to send Feedback'); - - try { - // In case browsers don't allow this property to be writable - // @ts-expect-error This needs lib es2022 and newer - error.cause = err; - } catch { - // nothing to do - } - reject(error); + // In case browsers don't allow this property to be writable + // @ts-expect-error This needs lib es2022 and newer + error.cause = err; + } catch { + // nothing to do } + throw error; + } - // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore - if (!response) { - resolve(response); - return; - } + // TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore + if (!response) { + return; + } - // Require valid status codes, otherwise can assume feedback was not sent successfully - if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) { - reject(new Error('Unable to send Feedback')); - } + // Require valid status codes, otherwise can assume feedback was not sent successfully + if (typeof response.statusCode === 'number' && (response.statusCode < 200 || response.statusCode >= 300)) { + throw new Error('Unable to send Feedback'); + } - resolve(response); - }); + return response; }); } diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index b7649cede039..a9c87cc157b7 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -65,7 +65,7 @@ export interface Hub { * * @param callback that will be enclosed into push/popScope. */ - withScope(callback: (scope: Scope) => void): void; + withScope(callback: (scope: Scope) => T): T; /** Returns the client of the top stack. */ getClient(): Client | undefined; From 01a4cc925f2326b5ae7821ec7c959dce6c1f235a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Dec 2023 09:56:31 +0100 Subject: [PATCH 13/34] feat(core): Add type & utility for function-based integrations (#9818) This PR adds new types for function-based integrations, that eventually (in v8) should fully replace the class-based functions. This also introduces a small helper function to make writing such integrations easier (as we need to set an id/name etc. on the integration). With this, you can write an integration like this: ```ts const inboundFiltersIntegration = makeIntegrationFn( 'InboundFilters', (options: Partial) => { return { processEvent(event, _hint, client) { const clientOptions = client.getOptions(); const mergedOptions = _mergeOptions(options, clientOptions); return _shouldDropEvent(event, mergedOptions) ? null : event; } } }); ``` And you get a fully typed integration ready to go! For backwards compatibility, and so that we can actually start converting integrations in v7 already, this PR also adds a small utility `convertIntegrationFnToClass()` to convert such an integration to the "current" integration class syntax. So we can actually already start porting integrations over like this: ```js /** Inbound filters configurable by the user */ // eslint-disable-next-line deprecation/deprecation export const InboundFilters = convertIntegrationFnToClass(inboundFiltersIntegration); ``` Then, in v8 we only have to remove all the `convertIntegrationFnToClass` calls, export the integration functions directly, and update the overall integration types which can be passed to `init()` etc. --- packages/core/src/index.ts | 7 ++- packages/core/src/integration.ts | 25 ++++++++- .../core/src/integrations/inboundfilters.ts | 55 ++++++------------ packages/core/test/lib/integration.test.ts | 56 ++++++++++++++++++- packages/types/src/index.ts | 2 +- packages/types/src/integration.ts | 41 ++++++++++++++ 6 files changed, 145 insertions(+), 41 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ccf219031b8f..3c39921f495a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -54,7 +54,12 @@ export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; export { makeMultiplexedTransport } from './transports/multiplexed'; export { SDK_VERSION } from './version'; -export { getIntegrationsToSetup, addIntegration } from './integration'; +export { + getIntegrationsToSetup, + addIntegration, + // eslint-disable-next-line deprecation/deprecation + convertIntegrationFnToClass, +} from './integration'; export { FunctionToString, InboundFilters, LinkedErrors } from './integrations'; export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 7be22a316e53..57adc3d33c36 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,4 +1,4 @@ -import type { Client, Event, EventHint, Integration, Options } from '@sentry/types'; +import type { Client, Event, EventHint, Integration, IntegrationClass, IntegrationFn, Options } from '@sentry/types'; import { arrayify, logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; @@ -155,3 +155,26 @@ function findIndex(arr: T[], callback: (item: T) => boolean): number { return -1; } + +/** + * Convert a new integration function to the legacy class syntax. + * In v8, we can remove this and instead export the integration functions directly. + * + * @deprecated This will be removed in v8! + */ +export function convertIntegrationFnToClass( + name: string, + fn: Fn, +): IntegrationClass { + return Object.assign( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function ConvertedIntegration(...rest: any[]) { + return { + // eslint-disable-next-line @typescript-eslint/no-empty-function + setupOnce: () => {}, + ...fn(...rest), + }; + }, + { id: name }, + ) as unknown as IntegrationClass; +} diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index 9d348a4b4d23..57c0387b25e4 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -1,7 +1,8 @@ -import type { Client, Event, EventHint, Integration, StackFrame } from '@sentry/types'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { getEventDescription, logger, stringMatchesSomePattern } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { convertIntegrationFnToClass } from '../integration'; // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. @@ -28,42 +29,23 @@ export interface InboundFiltersOptions { disableTransactionDefaults: boolean; } -/** Inbound filters configurable by the user */ -export class InboundFilters implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'InboundFilters'; - - /** - * @inheritDoc - */ - public name: string; - - private readonly _options: Partial; - - public constructor(options: Partial = {}) { - this.name = InboundFilters.id; - this._options = options; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } +const INTEGRATION_NAME = 'InboundFilters'; +const inboundFiltersIntegration: IntegrationFn = (options: Partial) => { + return { + name: INTEGRATION_NAME, + processEvent(event, _hint, client) { + const clientOptions = client.getOptions(); + const mergedOptions = _mergeOptions(options, clientOptions); + return _shouldDropEvent(event, mergedOptions) ? null : event; + }, + }; +}; - /** @inheritDoc */ - public processEvent(event: Event, _eventHint: EventHint, client: Client): Event | null { - const clientOptions = client.getOptions(); - const options = _mergeOptions(this._options, clientOptions); - return _shouldDropEvent(event, options) ? null : event; - } -} +/** Inbound filters configurable by the user */ +// eslint-disable-next-line deprecation/deprecation +export const InboundFilters = convertIntegrationFnToClass(INTEGRATION_NAME, inboundFiltersIntegration); -/** JSDoc */ -export function _mergeOptions( +function _mergeOptions( internalOptions: Partial = {}, clientOptions: Partial = {}, ): Partial { @@ -84,8 +66,7 @@ export function _mergeOptions( }; } -/** JSDoc */ -export function _shouldDropEvent(event: Event, options: Partial): boolean { +function _shouldDropEvent(event: Event, options: Partial): boolean { if (options.ignoreInternal && _isSentryError(event)) { DEBUG_BUILD && logger.warn(`Event dropped due to being internal Sentry Error.\nEvent: ${getEventDescription(event)}`); diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 54bd426abb5c..65bf30483d86 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -2,7 +2,13 @@ import type { Integration, Options } from '@sentry/types'; import { logger } from '@sentry/utils'; import { Hub, makeMain } from '../../src/hub'; -import { addIntegration, getIntegrationsToSetup, installedIntegrations, setupIntegration } from '../../src/integration'; +import { + addIntegration, + convertIntegrationFnToClass, + getIntegrationsToSetup, + installedIntegrations, + setupIntegration, +} from '../../src/integration'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; function getTestClient(): TestClient { @@ -647,3 +653,51 @@ describe('addIntegration', () => { expect(warnings).toHaveBeenCalledWith('Cannot add integration "test" because no SDK Client is available.'); }); }); + +describe('convertIntegrationFnToClass', () => { + /* eslint-disable deprecation/deprecation */ + it('works with a minimal integration', () => { + const integrationFn = () => ({ name: 'testName' }); + + const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); + + expect(IntegrationClass.id).toBe('testName'); + + const integration = new IntegrationClass(); + expect(integration).toEqual({ + name: 'testName', + setupOnce: expect.any(Function), + }); + }); + + it('works with integration hooks', () => { + const setup = jest.fn(); + const setupOnce = jest.fn(); + const processEvent = jest.fn(); + const preprocessEvent = jest.fn(); + + const integrationFn = () => { + return { + name: 'testName', + setup, + setupOnce, + processEvent, + preprocessEvent, + }; + }; + + const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); + + expect(IntegrationClass.id).toBe('testName'); + + const integration = new IntegrationClass(); + expect(integration).toEqual({ + name: 'testName', + setupOnce, + setup, + processEvent, + preprocessEvent, + }); + }); + /* eslint-enable deprecation/deprecation */ +}); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 7d3531599aa3..a2d75a193414 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -52,7 +52,7 @@ export type { EventProcessor } from './eventprocessor'; export type { Exception } from './exception'; export type { Extra, Extras } from './extra'; export type { Hub } from './hub'; -export type { Integration, IntegrationClass } from './integration'; +export type { Integration, IntegrationClass, IntegrationFn, IntegrationFnResult } from './integration'; export type { Mechanism } from './mechanism'; export type { ExtractedNodeRequestData, HttpHeaderValue, Primitive, WorkerLocation } from './misc'; export type { ClientOptions, Options } from './options'; diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts index 0c18845414e3..a4108a60c749 100644 --- a/packages/types/src/integration.ts +++ b/packages/types/src/integration.ts @@ -13,6 +13,47 @@ export interface IntegrationClass { new (...args: any[]): T; } +/** + * An integration in function form. + * This is expected to return an integration result, + */ +export type IntegrationFn = (...rest: any[]) => IntegrationFnResult; + +export interface IntegrationFnResult { + /** + * The name of the integration. + */ + name: string; + + /** + * This hook is only called once, even if multiple clients are created. + * It does not receives any arguments, and should only use for e.g. global monkey patching and similar things. + */ + setupOnce?(): void; + + /** + * Set up an integration for the given client. + * Receives the client as argument. + * + * Whenever possible, prefer this over `setupOnce`, as that is only run for the first client, + * whereas `setup` runs for each client. Only truly global things (e.g. registering global handlers) + * should be done in `setupOnce`. + */ + setup?(client: Client): void; + + /** + * An optional hook that allows to preprocess an event _before_ it is passed to all other event processors. + */ + preprocessEvent?(event: Event, hint: EventHint | undefined, client: Client): void; + + /** + * An optional hook that allows to process an event. + * Return `null` to drop the event, or mutate the event & return it. + * This receives the client that the integration was installed for as third argument. + */ + processEvent?(event: Event, hint: EventHint, client: Client): Event | null | PromiseLike; +} + /** Integration interface */ export interface Integration { /** From c0c5ecad52938d03cc280dc40ae321e096b2aaf7 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 18 Dec 2023 11:22:14 +0100 Subject: [PATCH 14/34] feat(node): Add Hapi Integration (#9539) Resolves: #9344 Adds a new node integration for Hapi framework. Also exports a Hapi plugin to capture errors when the tracing instrumentation from `node-experimental` is used. Can be used with `node-experimental` ([Sample Error Event](https://sentry-sdks.sentry.io/issues/4624554372/?project=4506162118983680&query=is%3Aunresolved&referrer=issue-stream&statsPeriod=1h&stream_index=0)) like: ```typescript const Sentry = require('@sentry/node-experimental'); Sentry.init({ dsn: '__DSN__', tracesSampleRate: 1.0, }); const Hapi = require('@hapi/hapi'); const init = async () => { const server = Hapi.server({ port: 3000, host: 'localhost' }); await server.register(Sentry.hapiErrorPlugin) server.route({ method: 'GET', path: '/', handler: (request, h) => { throw new Error('My Hapi Sentry error!'); } }); await server.start(); }; ``` Also can be used from `@sentry/node` with tracing ([Errored Transaction](https://sentry-sdks.sentry.io/performance/node-hapi:8a633340fc724472bb44aae4c7572827/?project=4506162118983680&query=&referrer=performance-transaction-summary&statsPeriod=1h&transaction=%2F&unselectedSeries=p100%28%29&unselectedSeries=avg%28%29), [Successful Transaction](https://sentry-sdks.sentry.io/performance/node-hapi:deeb79f0c6bf41c68c776833c4629e6e/?project=4506162118983680&query=&referrer=performance-transaction-summary&statsPeriod=1h&transaction=%2F&unselectedSeries=p100%28%29&unselectedSeries=avg%28%29)) and error tracking ([Event](https://sentry-sdks.sentry.io/issues/4626919129/?project=4506162118983680&query=is%3Aunresolved&referrer=issue-stream&statsPeriod=1h&stream_index=0)) like: ```typescript 'use strict'; const Sentry = require('@sentry/node'); const Hapi = require('@hapi/hapi'); const init = async () => { const server = Hapi.server({ port: 3000, host: 'localhost' }); Sentry.init({ dsn: '__DSN__', tracesSampleRate: 1.0, integrations: [ new Sentry.Integrations.Hapi({server}), ], debug: true, }); server.route({ method: 'GET', path: '/', handler: (request, h) => { return 'Hello World!'; } }); await server.start(); }; ``` --- .github/workflows/build.yml | 1 + .../node-hapi-app/.gitignore | 1 + .../test-applications/node-hapi-app/.npmrc | 2 + .../node-hapi-app/event-proxy-server.ts | 253 ++++++++++++++++ .../node-hapi-app/package.json | 29 ++ .../node-hapi-app/playwright.config.ts | 77 +++++ .../node-hapi-app/src/app.js | 61 ++++ .../node-hapi-app/start-event-proxy.ts | 6 + .../node-hapi-app/tests/server.test.ts | 194 ++++++++++++ .../node-hapi-app/tsconfig.json | 10 + packages/node-experimental/src/index.ts | 1 + packages/node/src/index.ts | 2 + packages/node/src/integrations/hapi/index.ts | 171 +++++++++++ packages/node/src/integrations/hapi/types.ts | 278 ++++++++++++++++++ packages/node/src/integrations/index.ts | 1 + 15 files changed, 1087 insertions(+) create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/.gitignore create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/.npmrc create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/package.json create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/playwright.config.ts create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/src/app.js create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts create mode 100644 packages/e2e-tests/test-applications/node-hapi-app/tsconfig.json create mode 100644 packages/node/src/integrations/hapi/index.ts create mode 100644 packages/node/src/integrations/hapi/types.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d76a1a270d74..fd47b36598f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -872,6 +872,7 @@ jobs: 'sveltekit', 'generic-ts3.8', 'node-experimental-fastify-app', + 'node-hapi-app', ] build-command: - false diff --git a/packages/e2e-tests/test-applications/node-hapi-app/.gitignore b/packages/e2e-tests/test-applications/node-hapi-app/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/e2e-tests/test-applications/node-hapi-app/.npmrc b/packages/e2e-tests/test-applications/node-hapi-app/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts b/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts new file mode 100644 index 000000000000..67cf80b4dabf --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts @@ -0,0 +1,253 @@ +import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import { parseEnvelope } from '@sentry/utils'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import type { AddressInfo } from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as util from 'util'; +import * as zlib from 'zlib'; + +const readFile = util.promisify(fs.readFile); +const writeFile = util.promisify(fs.writeFile); + +interface EventProxyServerOptions { + /** Port to start the event proxy server at. */ + port: number; + /** The name for the proxy server used for referencing it with listener functions */ + proxyServerName: string; +} + +interface SentryRequestCallbackData { + envelope: Envelope; + rawProxyRequestBody: string; + rawSentryResponseBody: string; + sentryResponseStatusCode?: number; +} + +/** + * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` + * option to this server (like this `tunnel: http://localhost:${port option}/`). + */ +export async function startEventProxyServer(options: EventProxyServerOptions): Promise { + const eventCallbackListeners: Set<(data: string) => void> = new Set(); + + const proxyServer = http.createServer((proxyRequest, proxyResponse) => { + const proxyRequestChunks: Uint8Array[] = []; + + proxyRequest.addListener('data', (chunk: Buffer) => { + proxyRequestChunks.push(chunk); + }); + + proxyRequest.addListener('error', err => { + throw err; + }); + + proxyRequest.addListener('end', () => { + const proxyRequestBody = + proxyRequest.headers['content-encoding'] === 'gzip' + ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() + : Buffer.concat(proxyRequestChunks).toString(); + + let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + + if (!envelopeHeader.dsn) { + throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const sentryResponseChunks: Uint8Array[] = []; + + const sentryRequest = https.request( + sentryIngestUrl, + { headers: proxyRequest.headers, method: proxyRequest.method }, + sentryResponse => { + sentryResponse.addListener('data', (chunk: Buffer) => { + proxyResponse.write(chunk, 'binary'); + sentryResponseChunks.push(chunk); + }); + + sentryResponse.addListener('end', () => { + eventCallbackListeners.forEach(listener => { + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody, new TextEncoder(), new TextDecoder()), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: sentryResponse.statusCode, + }; + + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + proxyResponse.end(); + }); + + sentryResponse.addListener('error', err => { + throw err; + }); + + proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); + }, + ); + + sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); + sentryRequest.end(); + }); + }); + + const proxyServerStartupPromise = new Promise(resolve => { + proxyServer.listen(options.port, () => { + resolve(); + }); + }); + + const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { + eventCallbackResponse.statusCode = 200; + eventCallbackResponse.setHeader('connection', 'keep-alive'); + + const callbackListener = (data: string): void => { + eventCallbackResponse.write(data.concat('\n'), 'utf8'); + }; + + eventCallbackListeners.add(callbackListener); + + eventCallbackRequest.on('close', () => { + eventCallbackListeners.delete(callbackListener); + }); + + eventCallbackRequest.on('error', () => { + eventCallbackListeners.delete(callbackListener); + }); + }); + + const eventCallbackServerStartupPromise = new Promise(resolve => { + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export async function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => Promise | boolean, +): Promise { + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); + + return new Promise((resolve, reject) => { + const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); + + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); + chunkString.split('').forEach(char => { + if (char === '\n') { + const eventCallbackData: SentryRequestCallbackData = JSON.parse( + Buffer.from(eventContents, 'base64').toString('utf8'), + ); + const callbackResult = callback(eventCallbackData); + if (typeof callbackResult !== 'boolean') { + callbackResult.then( + match => { + if (match) { + response.destroy(); + resolve(eventCallbackData); + } + }, + err => { + throw err; + }, + ); + } else if (callbackResult) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, async eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (await callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +const TEMP_FILE_PREFIX = 'event-proxy-server-'; + +async function registerCallbackServerPort(serverName: string, port: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + await writeFile(tmpFilePath, port, { encoding: 'utf8' }); +} + +function retrieveCallbackServerPort(serverName: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return readFile(tmpFilePath, 'utf8'); +} diff --git a/packages/e2e-tests/test-applications/node-hapi-app/package.json b/packages/e2e-tests/test-applications/node-hapi-app/package.json new file mode 100644 index 000000000000..1f667abc8987 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/package.json @@ -0,0 +1,29 @@ +{ + "name": "node-hapi-app", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "node src/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules,pnpm-lock.yaml", + "test:build": "pnpm install", + "test:assert": "pnpm test" + }, + "dependencies": { + "@hapi/hapi": "21.3.2", + "@sentry/integrations": "latest || *", + "@sentry/node": "latest || *", + "@sentry/tracing": "latest || *", + "@sentry/types": "latest || *", + "@types/node": "18.15.1", + "typescript": "4.9.5" + }, + "devDependencies": { + "@playwright/test": "^1.27.1", + "ts-node": "10.9.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/e2e-tests/test-applications/node-hapi-app/playwright.config.ts b/packages/e2e-tests/test-applications/node-hapi-app/playwright.config.ts new file mode 100644 index 000000000000..1b478c6ba6da --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/playwright.config.ts @@ -0,0 +1,77 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +const hapiPort = 3030; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${hapiPort}`, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + }, + { + command: 'pnpm start', + port: hapiPort, + }, + ], +}; + +export default config; diff --git a/packages/e2e-tests/test-applications/node-hapi-app/src/app.js b/packages/e2e-tests/test-applications/node-hapi-app/src/app.js new file mode 100644 index 000000000000..4c71802c9be2 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/src/app.js @@ -0,0 +1,61 @@ +const Sentry = require('@sentry/node'); +const Hapi = require('@hapi/hapi'); + +const server = Hapi.server({ + port: 3030, + host: 'localhost', +}); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + includeLocalVariables: true, + integrations: [new Sentry.Integrations.Hapi({ server })], + debug: true, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, +}); + +const init = async () => { + server.route({ + method: 'GET', + path: '/test-success', + handler: function (request, h) { + return { version: 'v1' }; + }, + }); + + server.route({ + method: 'GET', + path: '/test-param/{param}', + handler: function (request, h) { + return { paramWas: request.params.param }; + }, + }); + + server.route({ + method: 'GET', + path: '/test-error', + handler: async function (request, h) { + const exceptionId = Sentry.captureException(new Error('This is an error')); + + await Sentry.flush(2000); + + return { exceptionId }; + }, + }); + + server.route({ + method: 'GET', + path: '/test-failure', + handler: async function (request, h) { + throw new Error('This is an error'); + }, + }); +}; + +(async () => { + init(); + await server.start(); + console.log('Server running on %s', server.info.uri); +})(); diff --git a/packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts b/packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts new file mode 100644 index 000000000000..7a3ed463e2ae --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from './event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-hapi-app', +}); diff --git a/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts b/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts new file mode 100644 index 000000000000..0539ed6a3548 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts @@ -0,0 +1,194 @@ +import { test, expect } from '@playwright/test'; +import axios, { AxiosError, AxiosResponse } from 'axios'; +import { waitForError, waitForTransaction } from '../event-proxy-server'; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; +const EVENT_POLLING_TIMEOUT = 90_000; + +test('Sends captured exception to Sentry', async ({ baseURL }) => { + const { data } = await axios.get(`${baseURL}/test-error`); + const { exceptionId } = data; + + const url = `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionId}/`; + + console.log(`Polling for error eventId: ${exceptionId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get(url, { headers: { Authorization: `Bearer ${authToken}` } }); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { timeout: EVENT_POLLING_TIMEOUT }, + ) + .toBe(200); +}); + +test('Sends thrown error to Sentry', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-hapi-app', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'This is an error'; + }); + + try { + await axios.get(`${baseURL}/test-failure`); + } catch (e) {} + + const errorEvent = await errorEventPromise; + const errorEventId = errorEvent.event_id; + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${errorEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); + +test('Sends successful transactions to Sentry', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-hapi-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'hapi.request' && transactionEvent?.transaction === '/test-success' + ); + }); + + await axios.get(`${baseURL}/test-success`); + + const transactionEvent = await pageloadTransactionEventPromise; + const transactionEventId = transactionEvent.event_id; + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); + +test('Sends parameterized transactions to Sentry', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-hapi-app', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'hapi.request' && + transactionEvent?.transaction === '/test-param/{param}' + ); + }); + + await axios.get(`${baseURL}/test-param/123`); + + const transactionEvent = await pageloadTransactionEventPromise; + const transactionEventId = transactionEvent.event_id; + + expect(transactionEvent?.contexts?.trace?.op).toBe('hapi.request'); + expect(transactionEvent?.transaction).toBe('/test-param/{param}'); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); + +test('Sends sentry-trace and baggage as response headers', async ({ baseURL }) => { + const data = await axios.get(`${baseURL}/test-success`); + + expect(data.headers).toHaveProperty('sentry-trace'); + expect(data.headers).toHaveProperty('baggage'); +}); + +test('Continues trace and baggage from incoming headers', async ({ baseURL }) => { + const traceContent = '12312012123120121231201212312012-1121201211212012-0'; + const baggageContent = 'sentry-release=2.0.0,sentry-environment=myEnv'; + + await axios.get(`${baseURL}/test-success`); + + const data = await axios.get(`${baseURL}/test-success`, { + headers: { + 'sentry-trace': traceContent, + baggage: baggageContent, + }, + }); + + expect(data.headers).toHaveProperty('sentry-trace'); + expect(data.headers).toHaveProperty('baggage'); + + expect(data.headers['sentry-trace']).toContain('12312012123120121231201212312012-'); + expect(data.headers['baggage']).toContain(baggageContent); +}); diff --git a/packages/e2e-tests/test-applications/node-hapi-app/tsconfig.json b/packages/e2e-tests/test-applications/node-hapi-app/tsconfig.json new file mode 100644 index 000000000000..17bd2c1f4c00 --- /dev/null +++ b/packages/e2e-tests/test-applications/node-hapi-app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["dom", "dom.iterable", "esnext"], + "strict": true, + "outDir": "dist" + }, + "include": ["*.ts"] +} diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index e80e9ff4dc0b..d7df34bc6b87 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -55,6 +55,7 @@ export { withScope, captureCheckIn, withMonitor, + hapiErrorPlugin, } from '@sentry/node'; export type { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 9963258e48bb..fffa11939e8b 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -93,3 +93,5 @@ const INTEGRATIONS = { }; export { INTEGRATIONS as Integrations, Handlers }; + +export { hapiErrorPlugin } from './integrations/hapi'; diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts new file mode 100644 index 000000000000..e8c582f52e0c --- /dev/null +++ b/packages/node/src/integrations/hapi/index.ts @@ -0,0 +1,171 @@ +import { + captureException, + configureScope, + continueTrace, + getActiveTransaction, + SDK_VERSION, + startTransaction, +} from '@sentry/core'; +import type { Integration } from '@sentry/types'; +import { dynamicSamplingContextToSentryBaggageHeader, fill } from '@sentry/utils'; + +import type { Boom, RequestEvent, ResponseObject, Server } from './types'; + +function isResponseObject(response: ResponseObject | Boom): response is ResponseObject { + return response && (response as ResponseObject).statusCode !== undefined; +} + +function isBoomObject(response: ResponseObject | Boom): response is Boom { + return response && (response as Boom).isBoom !== undefined; +} + +function isErrorEvent(event: RequestEvent): event is RequestEvent { + return event && (event as RequestEvent).error !== undefined; +} + +function sendErrorToSentry(errorData: object): void { + captureException(errorData, { + mechanism: { + type: 'hapi', + handled: false, + data: { + function: 'hapiErrorPlugin', + }, + }, + }); +} + +export const hapiErrorPlugin = { + name: 'SentryHapiErrorPlugin', + version: SDK_VERSION, + register: async function (serverArg: Record) { + const server = serverArg as unknown as Server; + + server.events.on('request', (request, event) => { + const transaction = getActiveTransaction(); + + if (request.response && isBoomObject(request.response)) { + sendErrorToSentry(request.response); + } else if (isErrorEvent(event)) { + sendErrorToSentry(event.error); + } + + if (transaction) { + transaction.setStatus('internal_error'); + transaction.finish(); + } + }); + }, +}; + +export const hapiTracingPlugin = { + name: 'SentryHapiTracingPlugin', + version: SDK_VERSION, + register: async function (serverArg: Record) { + const server = serverArg as unknown as Server; + + server.ext('onPreHandler', (request, h) => { + const transaction = continueTrace( + { + sentryTrace: request.headers['sentry-trace'] || undefined, + baggage: request.headers['baggage'] || undefined, + }, + transactionContext => { + return startTransaction({ + ...transactionContext, + op: 'hapi.request', + name: request.route.path, + description: `${request.route.method} ${request.path}`, + }); + }, + ); + + configureScope(scope => { + scope.setSpan(transaction); + }); + + return h.continue; + }); + + server.ext('onPreResponse', (request, h) => { + const transaction = getActiveTransaction(); + + if (request.response && isResponseObject(request.response) && transaction) { + const response = request.response as ResponseObject; + response.header('sentry-trace', transaction.toTraceparent()); + + const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( + transaction.getDynamicSamplingContext(), + ); + + if (dynamicSamplingContext) { + response.header('baggage', dynamicSamplingContext); + } + } + + return h.continue; + }); + + server.ext('onPostHandler', (request, h) => { + const transaction = getActiveTransaction(); + + if (request.response && isResponseObject(request.response) && transaction) { + transaction.setHttpStatus(request.response.statusCode); + } + + if (transaction) { + transaction.finish(); + } + + return h.continue; + }); + }, +}; + +export type HapiOptions = { + /** Hapi server instance */ + server?: Record; +}; + +/** + * Hapi Framework Integration + */ +export class Hapi implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'Hapi'; + + /** + * @inheritDoc + */ + public name: string; + + public _hapiServer: Server | undefined; + + public constructor(options?: HapiOptions) { + if (options?.server) { + const server = options.server as unknown as Server; + + this._hapiServer = server; + } + + this.name = Hapi.id; + } + + /** @inheritDoc */ + public setupOnce(): void { + if (!this._hapiServer) { + return; + } + + fill(this._hapiServer, 'start', (originalStart: () => void) => { + return async function (this: Server) { + await this.register(hapiTracingPlugin); + await this.register(hapiErrorPlugin); + const result = originalStart.apply(this); + return result; + }; + }); + } +} diff --git a/packages/node/src/integrations/hapi/types.ts b/packages/node/src/integrations/hapi/types.ts new file mode 100644 index 000000000000..d74c171ef441 --- /dev/null +++ b/packages/node/src/integrations/hapi/types.ts @@ -0,0 +1,278 @@ +/* eslint-disable @typescript-eslint/no-misused-new */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/unified-signatures */ +/* eslint-disable @typescript-eslint/no-empty-interface */ +/* eslint-disable @typescript-eslint/no-namespace */ + +// Vendored and simplified from: +// - @types/hapi__hapi +// v17.8.9999 +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c73060bd14bb74a2f1906ccfc714d385863bc07d/types/hapi/v17/index.d.ts +// +// - @types/podium +// v1.0.9999 +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c73060bd14bb74a2f1906ccfc714d385863bc07d/types/podium/index.d.ts +// +// - @types/boom +// v7.3.9999 +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c73060bd14bb74a2f1906ccfc714d385863bc07d/types/boom/v4/index.d.ts + +import type * as stream from 'stream'; +import type * as url from 'url'; + +interface Podium { + new (events?: Events[]): Podium; + new (events?: Events): Podium; + + registerEvent(events: Events[]): void; + registerEvent(events: Events): void; + + registerPodium?(podiums: Podium[]): void; + registerPodium?(podiums: Podium): void; + + emit( + criteria: string | { name: string; channel?: string | undefined; tags?: string | string[] | undefined }, + data: any, + callback?: () => void, + ): void; + + on(criteria: string | Criteria, listener: Listener): void; + addListener(criteria: string | Criteria, listener: Listener): void; + once(criteria: string | Criteria, listener: Listener): void; + removeListener(name: string, listener: Listener): Podium; + removeAllListeners(name: string): Podium; + hasListeners(name: string): boolean; +} + +export interface Boom extends Error { + isBoom: boolean; + isServer: boolean; + message: string; + output: Output; + reformat: () => string; + isMissing?: boolean | undefined; + data: Data; +} + +export interface Output { + statusCode: number; + headers: { [index: string]: string }; + payload: Payload; +} + +export interface Payload { + statusCode: number; + error: string; + message: string; + attributes?: any; +} + +export type Events = string | EventOptionsObject | Podium; + +export interface EventOptionsObject { + name: string; + channels?: string | string[] | undefined; + clone?: boolean | undefined; + spread?: boolean | undefined; + tags?: boolean | undefined; + shared?: boolean | undefined; +} + +export interface CriteriaObject { + name: string; + block?: boolean | number | undefined; + channels?: string | string[] | undefined; + clone?: boolean | undefined; + count?: number | undefined; + filter?: string | string[] | CriteriaFilterOptionsObject | undefined; + spread?: boolean | undefined; + tags?: boolean | undefined; + listener?: Listener | undefined; +} + +export interface CriteriaFilterOptionsObject { + tags?: string | string[] | undefined; + all?: boolean | undefined; +} + +export type Criteria = string | CriteriaObject; + +export interface Listener { + (data: any, tags?: Tags, callback?: () => void): void; +} + +export type Tags = { [tag: string]: boolean }; + +type Dependencies = + | string + | string[] + | { + [key: string]: string; + }; + +interface PluginNameVersion { + name: string; + version?: string | undefined; +} + +interface PluginPackage { + pkg: any; +} + +interface PluginBase { + register: (server: Server, options: T) => void | Promise; + multiple?: boolean | undefined; + dependencies?: Dependencies | undefined; + requirements?: + | { + node?: string | undefined; + hapi?: string | undefined; + } + | undefined; + + once?: boolean | undefined; +} + +type Plugin = PluginBase & (PluginNameVersion | PluginPackage); + +interface UserCredentials {} + +interface AppCredentials {} + +interface AuthCredentials { + scope?: string[] | undefined; + user?: UserCredentials | undefined; + app?: AppCredentials | undefined; +} + +interface RequestAuth { + artifacts: object; + credentials: AuthCredentials; + error: Error; + isAuthenticated: boolean; + isAuthorized: boolean; + mode: string; + strategy: string; +} + +interface RequestEvents extends Podium { + on(criteria: 'peek', listener: PeekListener): void; + on(criteria: 'finish' | 'disconnect', listener: (data: undefined) => void): void; + once(criteria: 'peek', listener: PeekListener): void; + once(criteria: 'finish' | 'disconnect', listener: (data: undefined) => void): void; +} + +namespace Lifecycle { + export type Method = (request: Request, h: ResponseToolkit, err?: Error) => ReturnValue; + export type ReturnValue = ReturnValueTypes | Promise; + export type ReturnValueTypes = + | (null | string | number | boolean) + | Buffer + | (Error | Boom) + | stream.Stream + | (object | object[]) + | symbol + | ResponseToolkit; + export type FailAction = 'error' | 'log' | 'ignore' | Method; +} + +namespace Util { + export interface Dictionary { + [key: string]: T; + } + + export type HTTP_METHODS_PARTIAL_LOWERCASE = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'options'; + export type HTTP_METHODS_PARTIAL = + | 'GET' + | 'POST' + | 'PUT' + | 'PATCH' + | 'DELETE' + | 'OPTIONS' + | HTTP_METHODS_PARTIAL_LOWERCASE; + export type HTTP_METHODS = 'HEAD' | 'head' | HTTP_METHODS_PARTIAL; +} + +interface RequestRoute { + method: Util.HTTP_METHODS_PARTIAL; + path: string; + vhost?: string | string[] | undefined; + realm: any; + fingerprint: string; + + auth: { + access(request: Request): boolean; + }; +} + +interface Request extends Podium { + app: ApplicationState; + readonly auth: RequestAuth; + events: RequestEvents; + readonly headers: Util.Dictionary; + readonly path: string; + response: ResponseObject | Boom | null; + readonly route: RequestRoute; + readonly url: url.Url; +} + +interface ResponseObjectHeaderOptions { + append?: boolean | undefined; + separator?: string | undefined; + override?: boolean | undefined; + duplicate?: boolean | undefined; +} + +export interface ResponseObject extends Podium { + readonly statusCode: number; + header(name: string, value: string, options?: ResponseObjectHeaderOptions): ResponseObject; +} + +interface ResponseToolkit { + readonly continue: symbol; +} + +interface ServerEventCriteria { + name: T; + channels?: string | string[] | undefined; + clone?: boolean | undefined; + count?: number | undefined; + filter?: string | string[] | { tags: string | string[]; all?: boolean | undefined } | undefined; + spread?: boolean | undefined; + tags?: boolean | undefined; +} + +export interface RequestEvent { + timestamp: string; + tags: string[]; + channel: 'internal' | 'app' | 'error'; + data: object; + error: object; +} + +type RequestEventHandler = (request: Request, event: RequestEvent, tags: { [key: string]: true }) => void; +interface ServerEvents { + on(criteria: 'request' | ServerEventCriteria<'request'>, listener: RequestEventHandler): void; +} + +type RouteRequestExtType = + | 'onPreAuth' + | 'onCredentials' + | 'onPostAuth' + | 'onPreHandler' + | 'onPostHandler' + | 'onPreResponse'; + +type ServerRequestExtType = RouteRequestExtType | 'onRequest'; + +export type Server = Record & { + events: ServerEvents; + ext(event: ServerRequestExtType, method: Lifecycle.Method, options?: Record): void; + initialize(): Promise; + register(plugins: Plugin | Array>, options?: Record): Promise; + start(): Promise; +}; + +interface ApplicationState {} + +type PeekListener = (chunk: string, encoding: string) => void; diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts index 49820882fdc6..f2ac9c25b807 100644 --- a/packages/node/src/integrations/index.ts +++ b/packages/node/src/integrations/index.ts @@ -9,3 +9,4 @@ export { RequestData } from '@sentry/core'; export { LocalVariables } from './localvariables'; export { Undici } from './undici'; export { Spotlight } from './spotlight'; +export { Hapi } from './hapi'; From 35906d05811388e12db369ef2416ad05de2f0879 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Dec 2023 11:24:43 +0100 Subject: [PATCH 15/34] ref: Use `addBreadcrumb` directly & allow to pass hint (#9867) Instead of using `hub.addBreadcrumb()`. --- packages/browser/src/integrations/breadcrumbs.ts | 16 ++++++++-------- .../test/unit/integrations/breadcrumbs.test.ts | 13 ++++++------- packages/core/src/exports.ts | 5 +++-- .../node-experimental/src/integrations/http.ts | 4 ++-- .../src/integrations/node-fetch.ts | 6 +++--- packages/node/src/integrations/console.ts | 4 ++-- packages/node/src/integrations/http.ts | 4 ++-- packages/node/src/integrations/undici/index.ts | 5 +++-- packages/node/test/integrations/http.test.ts | 1 + packages/replay/src/util/log.ts | 11 +++++------ .../src/integrations/wintercg-fetch.ts | 6 +++--- packages/vercel-edge/test/wintercg-fetch.test.ts | 2 +- 12 files changed, 39 insertions(+), 38 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index cfcb255f5999..9c6b4cfb9764 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { getClient, getCurrentHub } from '@sentry/core'; +import { addBreadcrumb, getClient } from '@sentry/core'; import type { Event as SentryEvent, HandlerDataConsole, @@ -123,7 +123,7 @@ export class Breadcrumbs implements Integration { * Adds a breadcrumb for Sentry events or transactions if this option is enabled. */ function addSentryBreadcrumb(event: SentryEvent): void { - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: `sentry.${event.type === 'transaction' ? 'transaction' : 'event'}`, event_id: event.event_id, @@ -173,7 +173,7 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa return; } - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: `ui.${handlerData.name}`, message: target, @@ -213,7 +213,7 @@ function _consoleBreadcrumb(handlerData: HandlerDataConsole): void { } } - getCurrentHub().addBreadcrumb(breadcrumb, { + addBreadcrumb(breadcrumb, { input: handlerData.args, level: handlerData.level, }); @@ -247,7 +247,7 @@ function _xhrBreadcrumb(handlerData: HandlerDataXhr): void { endTimestamp, }; - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: 'xhr', data, @@ -282,7 +282,7 @@ function _fetchBreadcrumb(handlerData: HandlerDataFetch): void { endTimestamp, }; - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: 'fetch', data, @@ -303,7 +303,7 @@ function _fetchBreadcrumb(handlerData: HandlerDataFetch): void { startTimestamp, endTimestamp, }; - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: 'fetch', data, @@ -338,7 +338,7 @@ function _historyBreadcrumb(handlerData: HandlerDataHistory): void { from = parsedFrom.relative; } - getCurrentHub().addBreadcrumb({ + addBreadcrumb({ category: 'navigation', data: { from, diff --git a/packages/browser/test/unit/integrations/breadcrumbs.test.ts b/packages/browser/test/unit/integrations/breadcrumbs.test.ts index d81107a69c38..e87454737482 100644 --- a/packages/browser/test/unit/integrations/breadcrumbs.test.ts +++ b/packages/browser/test/unit/integrations/breadcrumbs.test.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import type { Client } from '@sentry/types'; import { Breadcrumbs, BrowserClient, Hub, flush } from '../../../src'; @@ -18,21 +18,20 @@ jest.mock('@sentry/core', () => { describe('Breadcrumbs', () => { it('Should add sentry breadcrumb', async () => { - const addBreadcrumb = jest.fn(); - hub.addBreadcrumb = addBreadcrumb; - client = new BrowserClient({ ...getDefaultBrowserClientOptions(), dsn: 'https://username@domain/123', integrations: [new Breadcrumbs()], }); - getCurrentHub().bindClient(client); + SentryCore.getCurrentHub().bindClient(client); + + const addBreadcrumbSpy = jest.spyOn(SentryCore, 'addBreadcrumb').mockImplementation(() => {}); client.captureMessage('test'); await flush(2000); - expect(addBreadcrumb.mock.calls[0][0].category).toEqual('sentry.event'); - expect(addBreadcrumb.mock.calls[0][0].message).toEqual('test'); + expect(addBreadcrumbSpy.mock.calls[0][0].category).toEqual('sentry.event'); + expect(addBreadcrumbSpy.mock.calls[0][0].message).toEqual('test'); }); }); diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index c5ab1055e7f4..c0ec78c7952f 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -1,5 +1,6 @@ import type { Breadcrumb, + BreadcrumbHint, CaptureContext, CheckIn, Client, @@ -90,8 +91,8 @@ export function configureScope(callback: (scope: Scope) => void): ReturnType { - getCurrentHub().addBreadcrumb(breadcrumb); +export function addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): ReturnType { + getCurrentHub().addBreadcrumb(breadcrumb, hint); } /** diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 6fe99e90101c..860169c6a43e 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -3,7 +3,7 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; -import { hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; +import { addBreadcrumb, hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; import { _INTERNAL, getClient, getCurrentHub, getSpanKind, setSpanMetadata } from '@sentry/opentelemetry'; import type { EventProcessor, Hub, Integration } from '@sentry/types'; import { stringMatchesSomePattern } from '@sentry/utils'; @@ -159,7 +159,7 @@ export class Http implements Integration { } const data = _INTERNAL.getRequestSpanData(span); - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: 'http', data: { diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node-experimental/src/integrations/node-fetch.ts index 54d67f33f4c2..2f9db367d511 100644 --- a/packages/node-experimental/src/integrations/node-fetch.ts +++ b/packages/node-experimental/src/integrations/node-fetch.ts @@ -1,8 +1,8 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { Instrumentation } from '@opentelemetry/instrumentation'; -import { hasTracingEnabled } from '@sentry/core'; -import { _INTERNAL, getClient, getCurrentHub, getSpanKind } from '@sentry/opentelemetry'; +import { addBreadcrumb, hasTracingEnabled } from '@sentry/core'; +import { _INTERNAL, getClient, getSpanKind } from '@sentry/opentelemetry'; import type { Integration } from '@sentry/types'; import type { NodeExperimentalClient } from '../types'; @@ -114,7 +114,7 @@ export class NodeFetch extends NodePerformanceIntegration impl } const data = _INTERNAL.getRequestSpanData(span); - getCurrentHub().addBreadcrumb({ + addBreadcrumb({ category: 'http', data: { ...data, diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index f99ab88b43b5..6c5142ae2a40 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,5 +1,5 @@ import * as util from 'util'; -import { getCurrentHub } from '@sentry/core'; +import { addBreadcrumb, getCurrentHub } from '@sentry/core'; import type { Integration } from '@sentry/types'; import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; @@ -26,7 +26,7 @@ export class Console implements Integration { return; } - hub.addBreadcrumb( + addBreadcrumb( { category: 'console', level: severityLevelFromString(level), diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 94aa36b80901..02e79d06b942 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,7 +1,7 @@ import type * as http from 'http'; import type * as https from 'https'; import type { Hub } from '@sentry/core'; -import { getClient, getCurrentScope } from '@sentry/core'; +import { addBreadcrumb, getClient, getCurrentScope } from '@sentry/core'; import { getCurrentHub, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; import type { DynamicSamplingContext, @@ -214,7 +214,7 @@ function _createWrappedRequestMethodFactory( return; } - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: 'http', data: { diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index b260f26e192d..b67562843d84 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,4 +1,5 @@ import { + addBreadcrumb, getClient, getCurrentHub, getCurrentScope, @@ -214,7 +215,7 @@ export class Undici implements Integration { } if (this._options.breadcrumbs) { - hub.addBreadcrumb( + addBreadcrumb( { category: 'http', data: { @@ -254,7 +255,7 @@ export class Undici implements Integration { } if (this._options.breadcrumbs) { - hub.addBreadcrumb( + addBreadcrumb( { category: 'http', data: { diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 1bfafd5c256d..2055aefeca39 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -592,6 +592,7 @@ describe('default protocols', () => { function captureBreadcrumb(key: string): Promise { const hub = new Hub(); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + jest.spyOn(sentryCore, 'addBreadcrumb').mockImplementation((...rest) => hub.addBreadcrumb(...rest)); let resolve: (value: Breadcrumb | PromiseLike) => void; const p = new Promise(r => { diff --git a/packages/replay/src/util/log.ts b/packages/replay/src/util/log.ts index 96b6e76dc498..3d16137bbbc0 100644 --- a/packages/replay/src/util/log.ts +++ b/packages/replay/src/util/log.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/core'; +import { addBreadcrumb } from '@sentry/core'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -14,7 +14,7 @@ export function logInfo(message: string, shouldAddBreadcrumb?: boolean): void { logger.info(message); if (shouldAddBreadcrumb) { - addBreadcrumb(message); + addLogBreadcrumb(message); } } @@ -33,14 +33,13 @@ export function logInfoNextTick(message: string, shouldAddBreadcrumb?: boolean): // Wait a tick here to avoid race conditions for some initial logs // which may be added before replay is initialized setTimeout(() => { - addBreadcrumb(message); + addLogBreadcrumb(message); }, 0); } } -function addBreadcrumb(message: string): void { - const hub = getCurrentHub(); - hub.addBreadcrumb( +function addLogBreadcrumb(message: string): void { + addBreadcrumb( { category: 'console', data: { diff --git a/packages/vercel-edge/src/integrations/wintercg-fetch.ts b/packages/vercel-edge/src/integrations/wintercg-fetch.ts index b03cae819073..18c4cb25df56 100644 --- a/packages/vercel-edge/src/integrations/wintercg-fetch.ts +++ b/packages/vercel-edge/src/integrations/wintercg-fetch.ts @@ -1,5 +1,5 @@ import { instrumentFetchRequest } from '@sentry-internal/tracing'; -import { getClient, getCurrentHub, isSentryRequestUrl } from '@sentry/core'; +import { addBreadcrumb, getClient, getCurrentHub, isSentryRequestUrl } from '@sentry/core'; import type { FetchBreadcrumbData, FetchBreadcrumbHint, HandlerDataFetch, Integration, Span } from '@sentry/types'; import { LRUMap, addFetchInstrumentationHandler, stringMatchesSomePattern } from '@sentry/utils'; @@ -130,7 +130,7 @@ function createBreadcrumb(handlerData: HandlerDataFetch): void { endTimestamp, }; - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: 'fetch', data, @@ -150,7 +150,7 @@ function createBreadcrumb(handlerData: HandlerDataFetch): void { startTimestamp, endTimestamp, }; - getCurrentHub().addBreadcrumb( + addBreadcrumb( { category: 'fetch', data, diff --git a/packages/vercel-edge/test/wintercg-fetch.test.ts b/packages/vercel-edge/test/wintercg-fetch.test.ts index d35aaa64f35c..3e6ac09330e0 100644 --- a/packages/vercel-edge/test/wintercg-fetch.test.ts +++ b/packages/vercel-edge/test/wintercg-fetch.test.ts @@ -34,7 +34,7 @@ jest.spyOn(sentryCore, 'getClient').mockImplementation(() => fakeHubInstance.get const addFetchInstrumentationHandlerSpy = jest.spyOn(sentryUtils, 'addFetchInstrumentationHandler'); const instrumentFetchRequestSpy = jest.spyOn(internalTracing, 'instrumentFetchRequest'); -const addBreadcrumbSpy = jest.spyOn(fakeHubInstance, 'addBreadcrumb'); +const addBreadcrumbSpy = jest.spyOn(sentryCore, 'addBreadcrumb'); beforeEach(() => { jest.clearAllMocks(); From f92241405000e2deffb2d240d71412adb40913a6 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Dec 2023 12:14:52 +0100 Subject: [PATCH 16/34] build: Fix linting (#9888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Damn, merged a PR without rebasing it first, it did not have the new biome rules yet 😅 so develop is failing linting right now... --- .../test-applications/node-hapi-app/event-proxy-server.ts | 4 ++-- .../test-applications/node-hapi-app/tests/server.test.ts | 2 +- packages/node/src/integrations/hapi/index.ts | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts b/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts index 67cf80b4dabf..9dee679c71e4 100644 --- a/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts +++ b/packages/e2e-tests/test-applications/node-hapi-app/event-proxy-server.ts @@ -1,5 +1,3 @@ -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; -import { parseEnvelope } from '@sentry/utils'; import * as fs from 'fs'; import * as http from 'http'; import * as https from 'https'; @@ -8,6 +6,8 @@ import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; import * as zlib from 'zlib'; +import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import { parseEnvelope } from '@sentry/utils'; const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); diff --git a/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts b/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts index 0539ed6a3548..cbcd99e756d7 100644 --- a/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts +++ b/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import axios, { AxiosError, AxiosResponse } from 'axios'; import { waitForError, waitForTransaction } from '../event-proxy-server'; diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index e8c582f52e0c..42e7d27bca9e 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -1,9 +1,9 @@ import { + SDK_VERSION, captureException, - configureScope, continueTrace, getActiveTransaction, - SDK_VERSION, + getCurrentScope, startTransaction, } from '@sentry/core'; import type { Integration } from '@sentry/types'; @@ -80,9 +80,7 @@ export const hapiTracingPlugin = { }, ); - configureScope(scope => { - scope.setSpan(transaction); - }); + getCurrentScope().setSpan(transaction); return h.continue; }); From e1d363322f6d803af3c1894bd5350fcdbbb8cac7 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Dec 2023 12:35:41 +0100 Subject: [PATCH 17/34] ref(serverless): Avoid using `pushScope` (#9883) This is the only place I've found where we use `pushScope`, and since we updated the `withScope` signature we can rewrite this. --- packages/serverless/src/awslambda.ts | 56 +++++++++++----------- packages/serverless/test/awslambda.test.ts | 9 ++-- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index aff1e58675f5..3f6ed2ad0ef8 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -319,35 +319,35 @@ export function wrapHandler( }); } - const scope = hub.pushScope(); - let rv: TResult; - try { - enhanceScopeWithEnvironmentData(scope, context, START_TIME); - if (options.startTrace) { - enhanceScopeWithTransactionData(scope, context); - // We put the transaction on the scope so users can attach children to it - scope.setSpan(transaction); - } - rv = await asyncHandler(event, context); - - // We manage lambdas that use Promise.allSettled by capturing the errors of failed promises - if (options.captureAllSettledReasons && Array.isArray(rv) && isPromiseAllSettledResult(rv)) { - const reasons = getRejectedReasons(rv); - reasons.forEach(exception => { - captureException(exception, scope => markEventUnhandled(scope)); + return withScope(async scope => { + let rv: TResult; + try { + enhanceScopeWithEnvironmentData(scope, context, START_TIME); + if (options.startTrace) { + enhanceScopeWithTransactionData(scope, context); + // We put the transaction on the scope so users can attach children to it + scope.setSpan(transaction); + } + rv = await asyncHandler(event, context); + + // We manage lambdas that use Promise.allSettled by capturing the errors of failed promises + if (options.captureAllSettledReasons && Array.isArray(rv) && isPromiseAllSettledResult(rv)) { + const reasons = getRejectedReasons(rv); + reasons.forEach(exception => { + captureException(exception, scope => markEventUnhandled(scope)); + }); + } + } catch (e) { + captureException(e, scope => markEventUnhandled(scope)); + throw e; + } finally { + clearTimeout(timeoutWarningTimer); + transaction?.finish(); + await flush(options.flushTimeout).catch(e => { + DEBUG_BUILD && logger.error(e); }); } - } catch (e) { - captureException(e, scope => markEventUnhandled(scope)); - throw e; - } finally { - clearTimeout(timeoutWarningTimer); - transaction?.finish(); - hub.popScope(); - await flush(options.flushTimeout).catch(e => { - DEBUG_BUILD && logger.error(e); - }); - } - return rv; + return rv; + }); }; } diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index a3085c8b0f65..5c67c8481d4a 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -95,8 +95,6 @@ describe('AWSLambda', () => { }); test('captureTimeoutWarning enabled (default)', async () => { - expect.assertions(2); - const handler: Handler = (_event, _context, callback) => { setTimeout(() => { callback(null, 42); @@ -105,14 +103,13 @@ describe('AWSLambda', () => { const wrappedHandler = wrapHandler(handler); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); + expect(Sentry.withScope).toBeCalledTimes(2); expect(Sentry.captureMessage).toBeCalled(); // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('timeout', '1s'); }); test('captureTimeoutWarning disabled', async () => { - expect.assertions(2); - const handler: Handler = (_event, _context, callback) => { setTimeout(() => { callback(null, 42); @@ -123,8 +120,10 @@ describe('AWSLambda', () => { }); await wrappedHandler(fakeEvent, fakeContext, fakeCallback); - expect(Sentry.withScope).not.toBeCalled(); + expect(Sentry.withScope).toBeCalledTimes(1); expect(Sentry.captureMessage).not.toBeCalled(); + // @ts-expect-error see "Why @ts-expect-error" note + expect(SentryNode.fakeScope.setTag).not.toBeCalledWith('timeout', '1s'); }); test('captureTimeoutWarning with configured timeoutWarningLimit', async () => { From ada32b2033b4db0a97ddb86831f8030c7f7ee4f3 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 18 Dec 2023 12:48:13 +0100 Subject: [PATCH 18/34] ref(nextjs): Simplify `wrapServerComponentWithSentry` (#9844) --- packages/core/src/tracing/trace.ts | 12 +- .../common/wrapServerComponentWithSentry.ts | 121 ++++++------------ 2 files changed, 51 insertions(+), 82 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index f8c8f5d6cbe3..cc73fe009e3d 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -24,7 +24,9 @@ export function trace( context: TransactionContext, callback: (span?: Span) => T, // eslint-disable-next-line @typescript-eslint/no-empty-function - onError: (error: unknown) => void = () => {}, + onError: (error: unknown, span?: Span) => void = () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + afterFinish: () => void = () => {}, ): T { const ctx = normalizeContext(context); @@ -46,8 +48,9 @@ export function trace( maybePromiseResult = callback(activeSpan); } catch (e) { activeSpan && activeSpan.setStatus('internal_error'); - onError(e); + onError(e, activeSpan); finishAndSetSpan(); + afterFinish(); throw e; } @@ -55,15 +58,18 @@ export function trace( Promise.resolve(maybePromiseResult).then( () => { finishAndSetSpan(); + afterFinish(); }, e => { activeSpan && activeSpan.setStatus('internal_error'); - onError(e); + onError(e, activeSpan); finishAndSetSpan(); + afterFinish(); }, ); } else { finishAndSetSpan(); + afterFinish(); } return maybePromiseResult; diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 9addbce2d589..d7ff31f3afd9 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -1,11 +1,5 @@ -import { - addTracingExtensions, - captureException, - getCurrentScope, - runWithAsyncContext, - startTransaction, -} from '@sentry/core'; -import { tracingContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; +import { addTracingExtensions, captureException, continueTrace, runWithAsyncContext, trace } from '@sentry/core'; +import { winterCGHeadersToDict } from '@sentry/utils'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; @@ -28,88 +22,57 @@ export function wrapServerComponentWithSentry any> return new Proxy(appDirComponent, { apply: (originalFunction, thisArg, args) => { return runWithAsyncContext(() => { - const currentScope = getCurrentScope(); - let maybePromiseResult; - const completeHeadersDict: Record = context.headers ? winterCGHeadersToDict(context.headers) : {}; - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( + const transactionContext = continueTrace({ // eslint-disable-next-line deprecation/deprecation - context.sentryTraceHeader ?? completeHeadersDict['sentry-trace'], + sentryTrace: context.sentryTraceHeader ?? completeHeadersDict['sentry-trace'], // eslint-disable-next-line deprecation/deprecation - context.baggageHeader ?? completeHeadersDict['baggage'], - ); - currentScope.setPropagationContext(propagationContext); - - const transaction = startTransaction({ - op: 'function.nextjs', - name: `${componentType} Server Component (${componentRoute})`, - status: 'ok', - origin: 'auto.function.nextjs', - ...traceparentData, - metadata: { - request: { - headers: completeHeadersDict, - }, - source: 'component', - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - }, + baggage: context.baggageHeader ?? completeHeadersDict['baggage'], }); - currentScope.setSpan(transaction); - - const handleErrorCase = (e: unknown): void => { - if (isNotFoundNavigationError(e)) { - // We don't want to report "not-found"s - transaction.setStatus('not_found'); - } else if (isRedirectNavigationError(e)) { - // We don't want to report redirects - } else { - transaction.setStatus('internal_error'); - - captureException(e, { - mechanism: { - handled: false, + const res = trace( + { + ...transactionContext, + op: 'function.nextjs', + name: `${componentType} Server Component (${componentRoute})`, + status: 'ok', + origin: 'auto.function.nextjs', + metadata: { + ...transactionContext.metadata, + request: { + headers: completeHeadersDict, }, - }); - } - - transaction.finish(); - }; - - try { - maybePromiseResult = originalFunction.apply(thisArg, args); - } catch (e) { - handleErrorCase(e); - void flushQueue(); - throw e; - } + source: 'component', + }, + }, + () => originalFunction.apply(thisArg, args), + (e, span) => { + if (isNotFoundNavigationError(e)) { + // We don't want to report "not-found"s + span?.setStatus('not_found'); + } else if (isRedirectNavigationError(e)) { + // We don't want to report redirects + // Since `trace` will automatically set the span status to "internal_error" we need to set it back to "ok" + span?.setStatus('ok'); + } else { + span?.setStatus('internal_error'); - 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); - }, - ) - .finally(() => { - void flushQueue(); - }); + captureException(e, { + mechanism: { + handled: false, + }, + }); + } + }, + () => { + 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 flushQueue(); - return maybePromiseResult; - } + return res; }); }, }); From 0efdb219e52f73844b244360d8a90bdf55a18dcc Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Dec 2023 14:46:02 +0100 Subject: [PATCH 19/34] feat(core): Deprecate `configureScope` (#9887) Instead, users should directly access the current scope via `getCurrentScope()`. --- MIGRATION.md | 8 ++++++++ packages/astro/src/index.server.ts | 1 + packages/browser/src/exports.ts | 1 + packages/bun/src/index.ts | 1 + packages/core/src/exports.ts | 3 +++ packages/core/src/hub.ts | 2 ++ packages/core/src/index.ts | 1 + packages/deno/src/index.ts | 1 + packages/hub/src/index.ts | 1 + packages/node-experimental/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/remix/src/index.server.ts | 1 + packages/serverless/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/types/src/hub.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 16 files changed, 26 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index 78f5f16d3002..40771832279a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,14 @@ 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 `configureScope` in favor of using `getCurrentScope()` + +Instead of updating the scope in a callback via `configureScope()`, you should access it via `getCurrentScope()` and configure it directly: + +```js +Sentry.getCurrentScope().setTag('xx', 'yy'); +``` + ## Deprecate `addGlobalEventProcessor` in favor of `addEventProcessor` Instead of using `addGlobalEventProcessor`, you should use `addEventProcessor` which does not add the event processor globally, but to the current client. diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index c62590180266..adcf95527364 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -17,6 +17,7 @@ export { captureMessage, captureCheckIn, withMonitor, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 82eedf4e846f..cc9712517b2b 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -30,6 +30,7 @@ export { captureEvent, captureMessage, close, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, flush, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 5a4260aaec38..499e969f3843 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -33,6 +33,7 @@ export { captureEvent, captureMessage, close, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index c0ec78c7952f..95c1e4b63de3 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -78,8 +78,11 @@ export function captureEvent(event: Event, hint?: EventHint): ReturnType void): ReturnType { + // eslint-disable-next-line deprecation/deprecation getCurrentHub().configureScope(callback); } diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index b7cffed42902..1d681b555816 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -335,6 +335,8 @@ export class Hub implements HubInterface { /** * @inheritDoc + * + * @deprecated Use `getScope()` directly. */ public configureScope(callback: (scope: Scope) => void): void { const { scope, client } = this.getStackTop(); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3c39921f495a..2fb2f17b6089 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,6 +14,7 @@ export { captureEvent, captureMessage, close, + // eslint-disable-next-line deprecation/deprecation configureScope, flush, lastEventId, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 52a878bdde17..bd2a7061019a 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -31,6 +31,7 @@ export { captureEvent, captureMessage, close, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts index 7797b1d0e7d5..057d0e6a9975 100644 --- a/packages/hub/src/index.ts +++ b/packages/hub/src/index.ts @@ -112,6 +112,7 @@ export const captureMessage = captureMessageCore; /** * @deprecated This export has moved to @sentry/core. The @sentry/hub package will be removed in v8. */ +// eslint-disable-next-line deprecation/deprecation export const configureScope = configureScopeCore; /** diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index d7df34bc6b87..5d976dcd4576 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -32,6 +32,7 @@ export { captureEvent, captureMessage, close, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index fffa11939e8b..950fe7bac197 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -32,6 +32,7 @@ export { captureEvent, captureMessage, close, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 0600eb625b7f..c62b3c9c729c 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -20,6 +20,7 @@ export { captureException, captureEvent, captureMessage, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 6df0dbedb2c3..c8086fc5d69e 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -24,6 +24,7 @@ export { captureMessage, captureCheckIn, withMonitor, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, getActiveTransaction, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index b75fa24ebe5b..560f839c9fe3 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -14,6 +14,7 @@ export { captureMessage, captureCheckIn, withMonitor, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index a9c87cc157b7..6edcb799b0e8 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -171,6 +171,7 @@ export interface Hub { * Callback to set context information onto the scope. * * @param callback Callback function that receives Scope. + * @deprecated Use `getScope()` directly. */ configureScope(callback: (scope: Scope) => void): void; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index ffce59b5dceb..76219f4faafa 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -32,6 +32,7 @@ export { captureEvent, captureMessage, close, + // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation From b27c2367acb312c4e9c2fd1aa2cdaf5b8cff1dad Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 18 Dec 2023 15:24:07 +0100 Subject: [PATCH 20/34] feat(core): Deprecate `pushScope` & `popScope` (#9890) This deprecates using `pushScope` / `popScope` on the hub. --- MIGRATION.md | 4 ++++ packages/browser/test/unit/index.test.ts | 18 +++++++----------- packages/core/src/hub.ts | 6 ++++++ packages/node/test/index.test.ts | 15 ++++++--------- packages/opentelemetry/test/custom/hub.test.ts | 1 + .../test/integration/eventProcessors.test.ts | 17 ++++++----------- packages/types/src/hub.ts | 4 ++++ 7 files changed, 34 insertions(+), 31 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 40771832279a..101f9de4469d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,10 @@ 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 `pushScope` & `popScope` in favor of `withScope` + +Instead of manually pushing/popping a scope, you should use `Sentry.withScope(callback: (scope: Scope))` instead. + ## Deprecate `configureScope` in favor of using `getCurrentScope()` Instead of updating the scope in a callback via `configureScope()`, you should access it via `getCurrentScope()` and configure it directly: diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/unit/index.test.ts index 62bf08d0ee25..5968349adc1a 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -37,9 +37,10 @@ jest.mock('@sentry/core', () => { }); describe('SentryBrowser', () => { - const beforeSend = jest.fn(); + const beforeSend = jest.fn(event => event); - beforeAll(() => { + beforeEach(() => { + WINDOW.__SENTRY__ = { hub: undefined, logger: undefined, globalEventProcessors: [] }; init({ beforeSend, dsn, @@ -47,33 +48,28 @@ describe('SentryBrowser', () => { }); }); - beforeEach(() => { - getCurrentHub().pushScope(); - }); - afterEach(() => { - getCurrentHub().popScope(); - beforeSend.mockReset(); + beforeSend.mockClear(); }); describe('getContext() / setContext()', () => { it('should store/load extra', () => { getCurrentScope().setExtra('abc', { def: [1] }); - expect(global.__SENTRY__.hub._stack[1].scope._extra).toEqual({ + expect(global.__SENTRY__.hub._stack[0].scope._extra).toEqual({ abc: { def: [1] }, }); }); it('should store/load tags', () => { getCurrentScope().setTag('abc', 'def'); - expect(global.__SENTRY__.hub._stack[1].scope._tags).toEqual({ + expect(global.__SENTRY__.hub._stack[0].scope._tags).toEqual({ abc: 'def', }); }); it('should store/load user', () => { getCurrentScope().setUser({ id: 'def' }); - expect(global.__SENTRY__.hub._stack[1].scope._user).toEqual({ + expect(global.__SENTRY__.hub._stack[0].scope._user).toEqual({ id: 'def', }); }); diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 1d681b555816..75960550081a 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -139,6 +139,8 @@ export class Hub implements HubInterface { /** * @inheritDoc + * + * @deprecated Use `withScope` instead. */ public pushScope(): Scope { // We want to clone the content of prev scope @@ -152,6 +154,8 @@ export class Hub implements HubInterface { /** * @inheritDoc + * + * @deprecated Use `withScope` instead. */ public popScope(): boolean { if (this.getStack().length <= 1) return false; @@ -162,10 +166,12 @@ export class Hub implements HubInterface { * @inheritDoc */ public withScope(callback: (scope: Scope) => T): T { + // eslint-disable-next-line deprecation/deprecation const scope = this.pushScope(); try { return callback(scope); } finally { + // eslint-disable-next-line deprecation/deprecation this.popScope(); } } diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 7f9abbdc1072..30658128d2b4 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -1,5 +1,6 @@ import { LinkedErrors, SDK_VERSION, getMainCarrier, initAndBind, runWithAsyncContext } from '@sentry/core'; import type { EventHint, Integration } from '@sentry/types'; +import { GLOBAL_OBJ } from '@sentry/utils'; import type { Event } from '../src'; import { @@ -33,37 +34,33 @@ const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; declare var global: any; describe('SentryNode', () => { - beforeAll(() => { + beforeEach(() => { + GLOBAL_OBJ.__SENTRY__ = { hub: undefined, logger: undefined, globalEventProcessors: [] }; init({ dsn }); }); beforeEach(() => { jest.clearAllMocks(); - getCurrentHub().pushScope(); - }); - - afterEach(() => { - getCurrentHub().popScope(); }); describe('getContext() / setContext()', () => { test('store/load extra', async () => { getCurrentScope().setExtra('abc', { def: [1] }); - expect(global.__SENTRY__.hub._stack[1].scope._extra).toEqual({ + expect(global.__SENTRY__.hub._stack[0].scope._extra).toEqual({ abc: { def: [1] }, }); }); test('store/load tags', async () => { getCurrentScope().setTag('abc', 'def'); - expect(global.__SENTRY__.hub._stack[1].scope._tags).toEqual({ + expect(global.__SENTRY__.hub._stack[0].scope._tags).toEqual({ abc: 'def', }); }); test('store/load user', async () => { getCurrentScope().setUser({ id: 'def' }); - expect(global.__SENTRY__.hub._stack[1].scope._user).toEqual({ + expect(global.__SENTRY__.hub._stack[0].scope._user).toEqual({ id: 'def', }); }); diff --git a/packages/opentelemetry/test/custom/hub.test.ts b/packages/opentelemetry/test/custom/hub.test.ts index 3fb707dca18a..e3f2eea70e6b 100644 --- a/packages/opentelemetry/test/custom/hub.test.ts +++ b/packages/opentelemetry/test/custom/hub.test.ts @@ -26,6 +26,7 @@ describe('OpenTelemetryHub', () => { it('pushScope() creates correct scope', () => { const hub = new OpenTelemetryHub(); + // eslint-disable-next-line deprecation/deprecation const scope = hub.pushScope(); expect(scope).toBeInstanceOf(OpenTelemetryScope); diff --git a/packages/replay/test/integration/eventProcessors.test.ts b/packages/replay/test/integration/eventProcessors.test.ts index b9c86a5c5966..41219c75ae6c 100644 --- a/packages/replay/test/integration/eventProcessors.test.ts +++ b/packages/replay/test/integration/eventProcessors.test.ts @@ -1,5 +1,5 @@ -import { getCurrentHub } from '@sentry/core'; -import type { Event, Hub, Scope } from '@sentry/types'; +import { getClient, getCurrentScope } from '@sentry/core'; +import type { Event } from '@sentry/types'; import { BASE_TIMESTAMP } from '..'; import { resetSdkMock } from '../mocks/resetSdkMock'; @@ -9,16 +9,11 @@ import { useFakeTimers } from '../utils/use-fake-timers'; useFakeTimers(); describe('Integration | eventProcessors', () => { - let hub: Hub; - let scope: Scope; - beforeEach(() => { - hub = getCurrentHub(); - scope = hub.pushScope(); + getCurrentScope().clear(); }); afterEach(() => { - hub.popScope(); jest.resetAllMocks(); }); @@ -31,7 +26,7 @@ describe('Integration | eventProcessors', () => { }, }); - const client = hub.getClient()!; + const client = getClient()!; jest.runAllTimers(); const mockTransportSend = jest.spyOn(client.getTransport()!, 'send'); @@ -47,7 +42,7 @@ describe('Integration | eventProcessors', () => { return null; }); - scope.addEventProcessor(handler1); + getCurrentScope().addEventProcessor(handler1); const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); @@ -58,7 +53,7 @@ describe('Integration | eventProcessors', () => { expect(mockTransportSend).toHaveBeenCalledTimes(1); - scope.addEventProcessor(handler2); + getCurrentScope().addEventProcessor(handler2); const TEST_EVENT2 = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 6edcb799b0e8..8d4d47885d40 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -40,6 +40,8 @@ export interface Hub { * when the operation finishes or throws. * * @returns Scope, the new cloned scope + * + * @deprecated Use `withScope` instead. */ pushScope(): Scope; @@ -49,6 +51,8 @@ export interface Hub { * This restores the state before the scope was pushed. All breadcrumbs and * context information added since the last call to {@link this.pushScope} are * discarded. + * + * @deprecated Use `withScope` instead. */ popScope(): boolean; From f12158560b78ba693a21467e02d67b069465c101 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 18 Dec 2023 15:41:41 +0100 Subject: [PATCH 21/34] feat(nextjs): Connect server component transactions if there is no incoming trace (#9845) --- .../app/(nested-layout)/layout.tsx | 12 +++++ .../(nested-layout)/nested-layout/layout.tsx | 12 +++++ .../(nested-layout)/nested-layout/page.tsx | 11 ++++ .../connected-servercomponent-trace.test.ts | 50 +++++++++++++++++++ .../src/common/utils/commonObjectTracing.ts | 23 +++++++++ .../wrapGenerationFunctionWithSentry.ts | 15 ++++++ .../common/wrapServerComponentWithSentry.ts | 22 +++++++- .../src/config/loaders/wrappingLoader.ts | 2 - 8 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/layout.tsx create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/layout.tsx create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/page.tsx create mode 100644 packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts create mode 100644 packages/nextjs/src/common/utils/commonObjectTracing.ts diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/layout.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/layout.tsx new file mode 100644 index 000000000000..ace0c2f086b7 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/layout.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; + +export const dynamic = 'force-dynamic'; + +export default function Layout({ children }: PropsWithChildren<{}>) { + return ( +
+

Layout

+ {children} +
+ ); +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/layout.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/layout.tsx new file mode 100644 index 000000000000..ace0c2f086b7 --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/layout.tsx @@ -0,0 +1,12 @@ +import { PropsWithChildren } from 'react'; + +export const dynamic = 'force-dynamic'; + +export default function Layout({ children }: PropsWithChildren<{}>) { + return ( +
+

Layout

+ {children} +
+ ); +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/page.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/page.tsx new file mode 100644 index 000000000000..8077c14d23ca --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/(nested-layout)/nested-layout/page.tsx @@ -0,0 +1,11 @@ +export const dynamic = 'force-dynamic'; + +export default function Page() { + return

Hello World!

; +} + +export async function generateMetadata() { + return { + title: 'I am generated metadata', + }; +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts new file mode 100644 index 000000000000..4acc41814d3c --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; + +test('Will capture a connected trace for all server components and generation functions when visiting a page', async ({ + page, +}) => { + const someConnectedEvent = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Layout Server Component (/(nested-layout)/nested-layout)' || + transactionEvent?.transaction === 'Layout Server Component (/(nested-layout))' || + transactionEvent?.transaction === 'Page Server Component (/(nested-layout)/nested-layout)' || + transactionEvent?.transaction === 'Page.generateMetadata (/(nested-layout)/nested-layout)' + ); + }); + + const layout1Transaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Layout Server Component (/(nested-layout)/nested-layout)' && + (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id + ); + }); + + const layout2Transaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Layout Server Component (/(nested-layout))' && + (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id + ); + }); + + const pageTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page Server Component (/(nested-layout)/nested-layout)' && + (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id + ); + }); + + const generateMetadataTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/(nested-layout)/nested-layout)' && + (await someConnectedEvent).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id + ); + }); + + await page.goto('/nested-layout'); + + expect(await layout1Transaction).toBeDefined(); + expect(await layout2Transaction).toBeDefined(); + expect(await pageTransaction).toBeDefined(); + expect(await generateMetadataTransaction).toBeDefined(); +}); diff --git a/packages/nextjs/src/common/utils/commonObjectTracing.ts b/packages/nextjs/src/common/utils/commonObjectTracing.ts new file mode 100644 index 000000000000..bb5cf130bab1 --- /dev/null +++ b/packages/nextjs/src/common/utils/commonObjectTracing.ts @@ -0,0 +1,23 @@ +import type { PropagationContext } from '@sentry/types'; + +const commonMap = new WeakMap(); + +/** + * Takes a shared (garbage collectable) object between resources, e.g. a headers object shared between Next.js server components and returns a common propagation context. + */ +export function commonObjectToPropagationContext( + commonObject: unknown, + propagationContext: PropagationContext, +): PropagationContext { + if (typeof commonObject === 'object' && commonObject) { + const memoPropagationContext = commonMap.get(commonObject); + if (memoPropagationContext) { + return memoPropagationContext; + } else { + commonMap.set(commonObject, propagationContext); + return propagationContext; + } + } else { + return propagationContext; + } +} diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 5aa9c436beef..80f7d62cc447 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -3,6 +3,7 @@ import { captureException, continueTrace, getCurrentHub, + getCurrentScope, runWithAsyncContext, trace, } from '@sentry/core'; @@ -10,6 +11,7 @@ import type { WebFetchHeaders } from '@sentry/types'; import { winterCGHeadersToDict } from '@sentry/utils'; import type { GenerationFunctionContext } from '../common/types'; +import { commonObjectToPropagationContext } from './utils/commonObjectTracing'; /** * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. @@ -45,6 +47,19 @@ export function wrapGenerationFunctionWithSentry a baggage: headers?.get('baggage'), sentryTrace: headers?.get('sentry-trace') ?? undefined, }); + + // If there is no incoming trace, we are setting the transaction context to one that is shared between all other + // transactions for this request. We do this based on the `headers` object, which is the same for all components. + const propagationContext = getCurrentScope().getPropagationContext(); + if (!transactionContext.traceId && !transactionContext.parentSpanId) { + const { traceId: commonTraceId, spanId: commonSpanId } = commonObjectToPropagationContext( + headers, + propagationContext, + ); + transactionContext.traceId = commonTraceId; + transactionContext.parentSpanId = commonSpanId; + } + return trace( { op: 'function.nextjs', diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index d7ff31f3afd9..8312121ae12c 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -1,8 +1,16 @@ -import { addTracingExtensions, captureException, continueTrace, runWithAsyncContext, trace } from '@sentry/core'; +import { + addTracingExtensions, + captureException, + continueTrace, + getCurrentScope, + runWithAsyncContext, + trace, +} from '@sentry/core'; import { winterCGHeadersToDict } from '@sentry/utils'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; +import { commonObjectToPropagationContext } from './utils/commonObjectTracing'; import { flushQueue } from './utils/responseEnd'; /** @@ -33,6 +41,18 @@ export function wrapServerComponentWithSentry any> baggage: context.baggageHeader ?? completeHeadersDict['baggage'], }); + // If there is no incoming trace, we are setting the transaction context to one that is shared between all other + // transactions for this request. We do this based on the `headers` object, which is the same for all components. + const propagationContext = getCurrentScope().getPropagationContext(); + if (!transactionContext.traceId && !transactionContext.parentSpanId) { + const { traceId: commonTraceId, spanId: commonSpanId } = commonObjectToPropagationContext( + context.headers, + propagationContext, + ); + transactionContext.traceId = commonTraceId; + transactionContext.parentSpanId = commonSpanId; + } + const res = trace( { ...transactionContext, diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index a6b852af8b28..3d2fd7c80bb4 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -157,8 +157,6 @@ export default function wrappingLoader( .replace(/(.*)/, '/$1') // Pull off the file name .replace(/\/[^/]+\.(js|ts|jsx|tsx)$/, '') - // Remove routing groups: https://beta.nextjs.org/docs/routing/defining-routes#example-creating-multiple-root-layouts - .replace(/\/(\(.*?\)\/)+/g, '/') // In case all of the above have left us with an empty string (which will happen if we're dealing with the // homepage), sub back in the root route .replace(/^$/, '/'); From 8fb1a2fba94e0d8e5685f3b6388bcf41c59e8505 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 18 Dec 2023 15:44:28 +0100 Subject: [PATCH 22/34] feat(deno): Support `Deno.CronSchedule` for cron jobs (#9880) This PR adds support for `Deno.CronSchedule`. Since the `CronSchedule` type cannot be fully expressed by the Sentry `IntervalSchedule`, I instead copied the Deno code to convert to `CrontabSchedule`. I also added `checkinMargin: 1` since the Deno docs state that on Deno Deploy, crons can be up to 1 minute late. --- packages/deno/lib.deno.d.ts | 1818 +++++++++++++++-- .../deno/src/integrations/deno-cron-format.ts | 84 + packages/deno/src/integrations/deno-cron.ts | 9 +- 3 files changed, 1714 insertions(+), 197 deletions(-) create mode 100644 packages/deno/src/integrations/deno-cron-format.ts diff --git a/packages/deno/lib.deno.d.ts b/packages/deno/lib.deno.d.ts index b576de192b36..62eec898407c 100644 --- a/packages/deno/lib.deno.d.ts +++ b/packages/deno/lib.deno.d.ts @@ -544,7 +544,7 @@ declare namespace Deno { * Examples: * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test({ * name: "inherit", @@ -559,7 +559,7 @@ declare namespace Deno { * ``` * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test({ * name: "true", @@ -574,7 +574,7 @@ declare namespace Deno { * ``` * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test({ * name: "false", @@ -589,7 +589,7 @@ declare namespace Deno { * ``` * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test({ * name: "localhost:8080", @@ -818,7 +818,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test({ * name: "example test", @@ -859,7 +859,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test({ * name: "example test", @@ -896,7 +896,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test("My test description", () => { * assertEquals("hello", "hello"); @@ -922,7 +922,7 @@ declare namespace Deno { * `fn` can be async if required. Declared function must have a name. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test(function myTestName() { * assertEquals("hello", "hello"); @@ -945,7 +945,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import {assert, fail, assertEquals} from "https://deno.land/std/testing/asserts.ts"; + * import {assert, fail, assertEquals} from "https://deno.land/std/assert/mod.ts"; * * Deno.test("My test description", { permissions: { read: true } }, (): void => { * assertEquals("hello", "hello"); @@ -972,7 +972,7 @@ declare namespace Deno { * `fn` can be async if required. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test( * { @@ -1010,7 +1010,7 @@ declare namespace Deno { * `fn` can be async if required. Declared function must have a name. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.test( * { permissions: { read: true } }, @@ -1234,7 +1234,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.bench({ * name: "example test", @@ -1273,7 +1273,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.bench("My test description", () => { * assertEquals("hello", "hello"); @@ -1301,7 +1301,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.bench(function myTestName() { * assertEquals("hello", "hello"); @@ -1326,7 +1326,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.bench( * "My test description", @@ -1363,7 +1363,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.bench( * { name: "My test description", permissions: { read: true } }, @@ -1397,7 +1397,7 @@ declare namespace Deno { * will await resolution to consider the test complete. * * ```ts - * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * import { assertEquals } from "https://deno.land/std/assert/mod.ts"; * * Deno.bench( * { permissions: { read: true } }, @@ -1624,6 +1624,9 @@ declare namespace Deno { * An abstract interface which when implemented provides an interface to read * bytes into an array buffer asynchronously. * + * @deprecated Use {@linkcode ReadableStream} instead. {@linkcode Reader} + * will be removed in v2.0.0. + * * @category I/O */ export interface Reader { /** Reads up to `p.byteLength` bytes into `p`. It resolves to the number of @@ -1658,6 +1661,9 @@ declare namespace Deno { * An abstract interface which when implemented provides an interface to read * bytes into an array buffer synchronously. * + * @deprecated Use {@linkcode ReadableStream} instead. {@linkcode ReaderSync} + * will be removed in v2.0.0. + * * @category I/O */ export interface ReaderSync { /** Reads up to `p.byteLength` bytes into `p`. It resolves to the number @@ -1692,6 +1698,9 @@ declare namespace Deno { * An abstract interface which when implemented provides an interface to write * bytes from an array buffer to a file/resource asynchronously. * + * @deprecated Use {@linkcode WritableStream} instead. {@linkcode Writer} + * will be removed in v2.0.0. + * * @category I/O */ export interface Writer { /** Writes `p.byteLength` bytes from `p` to the underlying data stream. It @@ -1716,6 +1725,9 @@ declare namespace Deno { * An abstract interface which when implemented provides an interface to write * bytes from an array buffer to a file/resource synchronously. * + * @deprecated Use {@linkcode WritableStream} instead. {@linkcode WriterSync} + * will be removed in v2.0.0. + * * @category I/O */ export interface WriterSync { /** Writes `p.byteLength` bytes from `p` to the underlying data @@ -1734,6 +1746,9 @@ declare namespace Deno { * An abstract interface which when implemented provides an interface to close * files/resources that were previously opened. * + * @deprecated Use {@linkcode ReadableStream} and {@linkcode WritableStream} + * instead. {@linkcode Closer} will be removed in v2.0.0. + * * @category I/O */ export interface Closer { /** Closes the resource, "freeing" the backing file/resource. */ @@ -2451,7 +2466,7 @@ declare namespace Deno { /** Resolves to a {@linkcode Deno.FileInfo} for the file. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * * const file = await Deno.open("hello.txt"); * const fileInfo = await file.stat(); @@ -2463,7 +2478,7 @@ declare namespace Deno { /** Synchronously returns a {@linkcode Deno.FileInfo} for the file. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * * const file = Deno.openSync("hello.txt") * const fileInfo = file.statSync(); @@ -3289,10 +3304,10 @@ declare namespace Deno { * * _Linux/Mac OS only._ */ ino: number | null; - /** **UNSTABLE**: Match behavior with Go on Windows for `mode`. + /** The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. * - * The underlying raw `st_mode` bits that contain the standard Unix - * permissions for this file/directory. */ + * _Linux/Mac OS only._ */ mode: number | null; /** Number of hard links pointing to this file. * @@ -3513,7 +3528,7 @@ declare namespace Deno { * of what it points to. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * const fileInfo = await Deno.lstat("hello.txt"); * assert(fileInfo.isFile); * ``` @@ -3530,7 +3545,7 @@ declare namespace Deno { * returned instead of what it points to. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * const fileInfo = Deno.lstatSync("hello.txt"); * assert(fileInfo.isFile); * ``` @@ -3546,7 +3561,7 @@ declare namespace Deno { * always follow symlinks. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * const fileInfo = await Deno.stat("hello.txt"); * assert(fileInfo.isFile); * ``` @@ -3562,7 +3577,7 @@ declare namespace Deno { * `path`. Will always follow symlinks. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * const fileInfo = Deno.statSync("hello.txt"); * assert(fileInfo.isFile); * ``` @@ -4312,7 +4327,7 @@ declare namespace Deno { * * @category Sub Process */ - export class ChildProcess implements Disposable { + export class ChildProcess implements AsyncDisposable { get stdin(): WritableStream; get stdout(): ReadableStream; get stderr(): ReadableStream; @@ -4338,7 +4353,7 @@ declare namespace Deno { * process from exiting. */ unref(): void; - [Symbol.dispose](): void; + [Symbol.asyncDispose](): Promise; } /** @@ -4794,7 +4809,7 @@ declare namespace Deno { /** Revokes a permission, and resolves to the state of the permission. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * * const status = await Deno.permissions.revoke({ name: "run" }); * assert(status.state !== "granted") @@ -4805,7 +4820,7 @@ declare namespace Deno { /** Revokes a permission, and returns the state of the permission. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * * const status = Deno.permissions.revokeSync({ name: "run" }); * assert(status.state !== "granted") @@ -4883,14 +4898,14 @@ declare namespace Deno { * ### Revoking * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * * const status = await Deno.permissions.revoke({ name: "run" }); * assert(status.state !== "granted") * ``` * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * * const status = Deno.permissions.revokeSync({ name: "run" }); * assert(status.state !== "granted") @@ -4986,13 +5001,13 @@ declare namespace Deno { * Give the following command line invocation of Deno: * * ```sh - * deno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd + * deno run --allow-read https://examples.deno.land/command-line-arguments.ts Sushi * ``` * * Then `Deno.args` will contain: * * ```ts - * [ "/etc/passwd" ] + * [ "Sushi" ] * ``` * * If you are looking for a structured way to parse arguments, there is the @@ -5200,7 +5215,7 @@ declare namespace Deno { * Returns a `Deno.FileInfo` for the given file stream. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * * const file = await Deno.open("file.txt", { read: true }); * const fileInfo = await Deno.fstat(file.rid); @@ -5216,7 +5231,7 @@ declare namespace Deno { * stream. * * ```ts - * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * import { assert } from "https://deno.land/std/assert/mod.ts"; * * const file = Deno.openSync("file.txt", { read: true }); * const fileInfo = Deno.fstatSync(file.rid); @@ -5939,6 +5954,50 @@ declare namespace Deno { handler: ServeHandler; } + export interface ServeUnixOptions { + /** The unix domain socket path to listen on. */ + path: string; + + /** An {@linkcode AbortSignal} to close the server and all connections. */ + signal?: AbortSignal; + + /** The handler to invoke when route handlers throw an error. */ + onError?: (error: unknown) => Response | Promise; + + /** The callback which is called when the server starts listening. */ + onListen?: (params: { path: string }) => void; + } + + /** Information for a unix domain socket HTTP request. + * + * @category HTTP Server + */ + export interface ServeUnixHandlerInfo { + /** The remote address of the connection. */ + remoteAddr: Deno.UnixAddr; + } + + /** A handler for unix domain socket HTTP requests. Consumes a request and returns a response. + * + * If a handler throws, the server calling the handler will assume the impact + * of the error is isolated to the individual request. It will catch the error + * and if necessary will close the underlying connection. + * + * @category HTTP Server + */ + export type ServeUnixHandler = ( + request: Request, + info: ServeUnixHandlerInfo, + ) => Response | Promise; + + /** + * @category HTTP Server + */ + export interface ServeUnixInit { + /** The handler to invoke to process each incoming request. */ + handler: ServeUnixHandler; + } + /** An instance of the server created using `Deno.serve()` API. * * @category HTTP Server @@ -5959,6 +6018,11 @@ declare namespace Deno { /** Make the server not block the event loop from finishing. */ unref(): void; + + /** Gracefully close the server. No more new connections will be accepted, + * while pending requests will be allowed to finish. + */ + shutdown(): Promise; } /** @@ -5978,6 +6042,55 @@ declare namespace Deno { * @category HTTP Server */ export function serve(handler: ServeHandler): HttpServer; + /** Serves HTTP requests with the given option bag and handler. + * + * You can specify the socket path with `path` option. + * + * ```ts + * Deno.serve( + * { path: "path/to/socket" }, + * (_req) => new Response("Hello, world") + * ); + * ``` + * + * You can stop the server with an {@linkcode AbortSignal}. The abort signal + * needs to be passed as the `signal` option in the options bag. The server + * aborts when the abort signal is aborted. To wait for the server to close, + * await the promise returned from the `Deno.serve` API. + * + * ```ts + * const ac = new AbortController(); + * + * const server = Deno.serve( + * { signal: ac.signal, path: "path/to/socket" }, + * (_req) => new Response("Hello, world") + * ); + * server.finished.then(() => console.log("Server closed")); + * + * console.log("Closing server..."); + * ac.abort(); + * ``` + * + * By default `Deno.serve` prints the message + * `Listening on path/to/socket` on listening. If you like to + * change this behavior, you can specify a custom `onListen` callback. + * + * ```ts + * Deno.serve({ + * onListen({ path }) { + * console.log(`Server started at ${path}`); + * // ... more info specific to your server .. + * }, + * path: "path/to/socket", + * }, (_req) => new Response("Hello, world")); + * ``` + * + * @category HTTP Server + */ + export function serve( + options: ServeUnixOptions, + handler: ServeUnixHandler, + ): HttpServer; /** Serves HTTP requests with the given option bag and handler. * * You can specify an object with a port and hostname option, which is the @@ -6038,6 +6151,33 @@ declare namespace Deno { options: ServeOptions | ServeTlsOptions, handler: ServeHandler, ): HttpServer; + /** Serves HTTP requests with the given option bag. + * + * You can specify an object with the path option, which is the + * unix domain socket to listen on. + * + * ```ts + * const ac = new AbortController(); + * + * const server = Deno.serve({ + * path: "path/to/socket", + * handler: (_req) => new Response("Hello, world"), + * signal: ac.signal, + * onListen({ path }) { + * console.log(`Server started at ${path}`); + * }, + * }); + * server.finished.then(() => console.log("Server closed")); + * + * console.log("Closing server..."); + * ac.abort(); + * ``` + * + * @category HTTP Server + */ + export function serve( + options: ServeUnixInit & ServeUnixOptions, + ): HttpServer; /** Serves HTTP requests with the given option bag. * * You can specify an object with a port and hostname option, which is the @@ -7113,12 +7253,18 @@ declare type ReadableStreamBYOBReadResult = | ReadableStreamBYOBReadDoneResult | ReadableStreamBYOBReadValueResult; +/** @category Streams API */ +declare interface ReadableStreamBYOBReaderReadOptions { + min?: number; +} + /** @category Streams API */ declare interface ReadableStreamBYOBReader { readonly closed: Promise; cancel(reason?: any): Promise; read( view: V, + options?: ReadableStreamBYOBReaderReadOptions, ): Promise>; releaseLock(): void; } @@ -7722,6 +7868,34 @@ declare function reportError( error: any, ): void; +/** @category Web APIs */ +type PredefinedColorSpace = "srgb" | "display-p3"; + +/** @category Web APIs */ +interface ImageDataSettings { + readonly colorSpace?: PredefinedColorSpace; +} + +/** @category Web APIs */ +interface ImageData { + readonly colorSpace: PredefinedColorSpace; + readonly data: Uint8ClampedArray; + readonly height: number; + readonly width: number; +} + +/** @category Web APIs */ +declare var ImageData: { + prototype: ImageData; + new (sw: number, sh: number, settings?: ImageDataSettings): ImageData; + new ( + data: Uint8ClampedArray, + sw: number, + sh?: number, + settings?: ImageDataSettings, + ): ImageData; +}; + // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. // deno-lint-ignore-file no-explicit-any no-var @@ -8132,6 +8306,1322 @@ declare function fetch( // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-explicit-any no-empty-interface + +/// +/// + +/** @category WebGPU */ +interface GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPUObjectDescriptorBase { + label?: string; +} + +/** @category WebGPU */ +declare class GPUSupportedLimits { + maxTextureDimension1D?: number; + maxTextureDimension2D?: number; + maxTextureDimension3D?: number; + maxTextureArrayLayers?: number; + maxBindGroups?: number; + maxBindingsPerBindGroup?: number; + maxDynamicUniformBuffersPerPipelineLayout?: number; + maxDynamicStorageBuffersPerPipelineLayout?: number; + maxSampledTexturesPerShaderStage?: number; + maxSamplersPerShaderStage?: number; + maxStorageBuffersPerShaderStage?: number; + maxStorageTexturesPerShaderStage?: number; + maxUniformBuffersPerShaderStage?: number; + maxUniformBufferBindingSize?: number; + maxStorageBufferBindingSize?: number; + minUniformBufferOffsetAlignment?: number; + minStorageBufferOffsetAlignment?: number; + maxVertexBuffers?: number; + maxBufferSize?: number; + maxVertexAttributes?: number; + maxVertexBufferArrayStride?: number; + maxInterStageShaderComponents?: number; + maxComputeWorkgroupStorageSize?: number; + maxComputeInvocationsPerWorkgroup?: number; + maxComputeWorkgroupSizeX?: number; + maxComputeWorkgroupSizeY?: number; + maxComputeWorkgroupSizeZ?: number; + maxComputeWorkgroupsPerDimension?: number; +} + +/** @category WebGPU */ +declare class GPUSupportedFeatures { + forEach( + callbackfn: ( + value: GPUFeatureName, + value2: GPUFeatureName, + set: Set, + ) => void, + thisArg?: any, + ): void; + has(value: GPUFeatureName): boolean; + size: number; + [Symbol.iterator](): IterableIterator; + entries(): IterableIterator<[GPUFeatureName, GPUFeatureName]>; + keys(): IterableIterator; + values(): IterableIterator; +} + +/** @category WebGPU */ +declare class GPUAdapterInfo { + readonly vendor: string; + readonly architecture: string; + readonly device: string; + readonly description: string; +} + +/** @category WebGPU */ +declare class GPU { + requestAdapter( + options?: GPURequestAdapterOptions, + ): Promise; +} + +/** @category WebGPU */ +declare interface GPURequestAdapterOptions { + powerPreference?: GPUPowerPreference; + forceFallbackAdapter?: boolean; +} + +/** @category WebGPU */ +declare type GPUPowerPreference = "low-power" | "high-performance"; + +/** @category WebGPU */ +declare class GPUAdapter { + readonly features: GPUSupportedFeatures; + readonly limits: GPUSupportedLimits; + readonly isFallbackAdapter: boolean; + + requestDevice(descriptor?: GPUDeviceDescriptor): Promise; + requestAdapterInfo(unmaskHints?: string[]): Promise; +} + +/** @category WebGPU */ +declare interface GPUDeviceDescriptor extends GPUObjectDescriptorBase { + requiredFeatures?: GPUFeatureName[]; + requiredLimits?: Record; +} + +/** @category WebGPU */ +declare type GPUFeatureName = + | "depth-clip-control" + | "depth32float-stencil8" + | "pipeline-statistics-query" + | "texture-compression-bc" + | "texture-compression-etc2" + | "texture-compression-astc" + | "timestamp-query" + | "indirect-first-instance" + | "shader-f16" + // extended from spec + | "mappable-primary-buffers" + | "sampled-texture-binding-array" + | "sampled-texture-array-dynamic-indexing" + | "sampled-texture-array-non-uniform-indexing" + | "unsized-binding-array" + | "multi-draw-indirect" + | "multi-draw-indirect-count" + | "push-constants" + | "address-mode-clamp-to-border" + | "texture-adapter-specific-format-features" + | "shader-float64" + | "vertex-attribute-64bit"; + +/** @category WebGPU */ +declare class GPUDevice extends EventTarget implements GPUObjectBase { + label: string; + + readonly lost: Promise; + pushErrorScope(filter: GPUErrorFilter): undefined; + popErrorScope(): Promise; + + readonly features: GPUSupportedFeatures; + readonly limits: GPUSupportedLimits; + readonly queue: GPUQueue; + + destroy(): undefined; + + createBuffer(descriptor: GPUBufferDescriptor): GPUBuffer; + createTexture(descriptor: GPUTextureDescriptor): GPUTexture; + createSampler(descriptor?: GPUSamplerDescriptor): GPUSampler; + + createBindGroupLayout( + descriptor: GPUBindGroupLayoutDescriptor, + ): GPUBindGroupLayout; + createPipelineLayout( + descriptor: GPUPipelineLayoutDescriptor, + ): GPUPipelineLayout; + createBindGroup(descriptor: GPUBindGroupDescriptor): GPUBindGroup; + + createShaderModule(descriptor: GPUShaderModuleDescriptor): GPUShaderModule; + createComputePipeline( + descriptor: GPUComputePipelineDescriptor, + ): GPUComputePipeline; + createRenderPipeline( + descriptor: GPURenderPipelineDescriptor, + ): GPURenderPipeline; + createComputePipelineAsync( + descriptor: GPUComputePipelineDescriptor, + ): Promise; + createRenderPipelineAsync( + descriptor: GPURenderPipelineDescriptor, + ): Promise; + + createCommandEncoder( + descriptor?: GPUCommandEncoderDescriptor, + ): GPUCommandEncoder; + createRenderBundleEncoder( + descriptor: GPURenderBundleEncoderDescriptor, + ): GPURenderBundleEncoder; + + createQuerySet(descriptor: GPUQuerySetDescriptor): GPUQuerySet; +} + +/** @category WebGPU */ +declare class GPUBuffer implements GPUObjectBase { + label: string; + + readonly size: number; + readonly usage: GPUFlagsConstant; + readonly mapState: GPUBufferMapState; + + mapAsync( + mode: GPUMapModeFlags, + offset?: number, + size?: number, + ): Promise; + getMappedRange(offset?: number, size?: number): ArrayBuffer; + unmap(): undefined; + + destroy(): undefined; +} + +/** @category WebGPU */ +declare type GPUBufferMapState = "unmapped" | "pending" | "mapped"; + +/** @category WebGPU */ +declare interface GPUBufferDescriptor extends GPUObjectDescriptorBase { + size: number; + usage: GPUBufferUsageFlags; + mappedAtCreation?: boolean; +} + +/** @category WebGPU */ +declare type GPUBufferUsageFlags = number; + +/** @category WebGPU */ +declare type GPUFlagsConstant = number; + +/** @category WebGPU */ +declare class GPUBufferUsage { + static MAP_READ: 0x0001; + static MAP_WRITE: 0x0002; + static COPY_SRC: 0x0004; + static COPY_DST: 0x0008; + static INDEX: 0x0010; + static VERTEX: 0x0020; + static UNIFORM: 0x0040; + static STORAGE: 0x0080; + static INDIRECT: 0x0100; + static QUERY_RESOLVE: 0x0200; +} + +/** @category WebGPU */ +declare type GPUMapModeFlags = number; + +/** @category WebGPU */ +declare class GPUMapMode { + static READ: 0x0001; + static WRITE: 0x0002; +} + +/** @category WebGPU */ +declare class GPUTexture implements GPUObjectBase { + label: string; + + createView(descriptor?: GPUTextureViewDescriptor): GPUTextureView; + destroy(): undefined; + + readonly width: number; + readonly height: number; + readonly depthOrArrayLayers: number; + readonly mipLevelCount: number; + readonly sampleCount: number; + readonly dimension: GPUTextureDimension; + readonly format: GPUTextureFormat; + readonly usage: GPUFlagsConstant; +} + +/** @category WebGPU */ +declare interface GPUTextureDescriptor extends GPUObjectDescriptorBase { + size: GPUExtent3D; + mipLevelCount?: number; + sampleCount?: number; + dimension?: GPUTextureDimension; + format: GPUTextureFormat; + usage: GPUTextureUsageFlags; + viewFormats?: GPUTextureFormat[]; +} + +/** @category WebGPU */ +declare type GPUTextureDimension = "1d" | "2d" | "3d"; + +/** @category WebGPU */ +declare type GPUTextureUsageFlags = number; + +/** @category WebGPU */ +declare class GPUTextureUsage { + static COPY_SRC: 0x01; + static COPY_DST: 0x02; + static TEXTURE_BINDING: 0x04; + static STORAGE_BINDING: 0x08; + static RENDER_ATTACHMENT: 0x10; +} + +/** @category WebGPU */ +declare class GPUTextureView implements GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPUTextureViewDescriptor extends GPUObjectDescriptorBase { + format?: GPUTextureFormat; + dimension?: GPUTextureViewDimension; + aspect?: GPUTextureAspect; + baseMipLevel?: number; + mipLevelCount?: number; + baseArrayLayer?: number; + arrayLayerCount?: number; +} + +/** @category WebGPU */ +declare type GPUTextureViewDimension = + | "1d" + | "2d" + | "2d-array" + | "cube" + | "cube-array" + | "3d"; + +/** @category WebGPU */ +declare type GPUTextureAspect = "all" | "stencil-only" | "depth-only"; + +/** @category WebGPU */ +declare type GPUTextureFormat = + | "r8unorm" + | "r8snorm" + | "r8uint" + | "r8sint" + | "r16uint" + | "r16sint" + | "r16float" + | "rg8unorm" + | "rg8snorm" + | "rg8uint" + | "rg8sint" + | "r32uint" + | "r32sint" + | "r32float" + | "rg16uint" + | "rg16sint" + | "rg16float" + | "rgba8unorm" + | "rgba8unorm-srgb" + | "rgba8snorm" + | "rgba8uint" + | "rgba8sint" + | "bgra8unorm" + | "bgra8unorm-srgb" + | "rgb9e5ufloat" + | "rgb10a2unorm" + | "rg11b10ufloat" + | "rg32uint" + | "rg32sint" + | "rg32float" + | "rgba16uint" + | "rgba16sint" + | "rgba16float" + | "rgba32uint" + | "rgba32sint" + | "rgba32float" + | "stencil8" + | "depth16unorm" + | "depth24plus" + | "depth24plus-stencil8" + | "depth32float" + | "depth32float-stencil8" + | "bc1-rgba-unorm" + | "bc1-rgba-unorm-srgb" + | "bc2-rgba-unorm" + | "bc2-rgba-unorm-srgb" + | "bc3-rgba-unorm" + | "bc3-rgba-unorm-srgb" + | "bc4-r-unorm" + | "bc4-r-snorm" + | "bc5-rg-unorm" + | "bc5-rg-snorm" + | "bc6h-rgb-ufloat" + | "bc6h-rgb-float" + | "bc7-rgba-unorm" + | "bc7-rgba-unorm-srgb" + | "etc2-rgb8unorm" + | "etc2-rgb8unorm-srgb" + | "etc2-rgb8a1unorm" + | "etc2-rgb8a1unorm-srgb" + | "etc2-rgba8unorm" + | "etc2-rgba8unorm-srgb" + | "eac-r11unorm" + | "eac-r11snorm" + | "eac-rg11unorm" + | "eac-rg11snorm" + | "astc-4x4-unorm" + | "astc-4x4-unorm-srgb" + | "astc-5x4-unorm" + | "astc-5x4-unorm-srgb" + | "astc-5x5-unorm" + | "astc-5x5-unorm-srgb" + | "astc-6x5-unorm" + | "astc-6x5-unorm-srgb" + | "astc-6x6-unorm" + | "astc-6x6-unorm-srgb" + | "astc-8x5-unorm" + | "astc-8x5-unorm-srgb" + | "astc-8x6-unorm" + | "astc-8x6-unorm-srgb" + | "astc-8x8-unorm" + | "astc-8x8-unorm-srgb" + | "astc-10x5-unorm" + | "astc-10x5-unorm-srgb" + | "astc-10x6-unorm" + | "astc-10x6-unorm-srgb" + | "astc-10x8-unorm" + | "astc-10x8-unorm-srgb" + | "astc-10x10-unorm" + | "astc-10x10-unorm-srgb" + | "astc-12x10-unorm" + | "astc-12x10-unorm-srgb" + | "astc-12x12-unorm" + | "astc-12x12-unorm-srgb"; + +/** @category WebGPU */ +declare class GPUSampler implements GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPUSamplerDescriptor extends GPUObjectDescriptorBase { + addressModeU?: GPUAddressMode; + addressModeV?: GPUAddressMode; + addressModeW?: GPUAddressMode; + magFilter?: GPUFilterMode; + minFilter?: GPUFilterMode; + mipmapFilter?: GPUMipmapFilterMode; + lodMinClamp?: number; + lodMaxClamp?: number; + compare?: GPUCompareFunction; + maxAnisotropy?: number; +} + +/** @category WebGPU */ +declare type GPUAddressMode = "clamp-to-edge" | "repeat" | "mirror-repeat"; + +/** @category WebGPU */ +declare type GPUFilterMode = "nearest" | "linear"; + +/** @category WebGPU */ +declare type GPUMipmapFilterMode = "nearest" | "linear"; + +/** @category WebGPU */ +declare type GPUCompareFunction = + | "never" + | "less" + | "equal" + | "less-equal" + | "greater" + | "not-equal" + | "greater-equal" + | "always"; + +/** @category WebGPU */ +declare class GPUBindGroupLayout implements GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPUBindGroupLayoutDescriptor extends GPUObjectDescriptorBase { + entries: GPUBindGroupLayoutEntry[]; +} + +/** @category WebGPU */ +declare interface GPUBindGroupLayoutEntry { + binding: number; + visibility: GPUShaderStageFlags; + + buffer?: GPUBufferBindingLayout; + sampler?: GPUSamplerBindingLayout; + texture?: GPUTextureBindingLayout; + storageTexture?: GPUStorageTextureBindingLayout; +} + +/** @category WebGPU */ +declare type GPUShaderStageFlags = number; + +/** @category WebGPU */ +declare class GPUShaderStage { + static VERTEX: 0x1; + static FRAGMENT: 0x2; + static COMPUTE: 0x4; +} + +/** @category WebGPU */ +declare interface GPUBufferBindingLayout { + type?: GPUBufferBindingType; + hasDynamicOffset?: boolean; + minBindingSize?: number; +} + +/** @category WebGPU */ +declare type GPUBufferBindingType = "uniform" | "storage" | "read-only-storage"; + +/** @category WebGPU */ +declare interface GPUSamplerBindingLayout { + type?: GPUSamplerBindingType; +} + +/** @category WebGPU */ +declare type GPUSamplerBindingType = + | "filtering" + | "non-filtering" + | "comparison"; + +/** @category WebGPU */ +declare interface GPUTextureBindingLayout { + sampleType?: GPUTextureSampleType; + viewDimension?: GPUTextureViewDimension; + multisampled?: boolean; +} + +/** @category WebGPU */ +declare type GPUTextureSampleType = + | "float" + | "unfilterable-float" + | "depth" + | "sint" + | "uint"; + +/** @category WebGPU */ +declare type GPUStorageTextureAccess = "write-only"; + +/** @category WebGPU */ +declare interface GPUStorageTextureBindingLayout { + access: GPUStorageTextureAccess; + format: GPUTextureFormat; + viewDimension?: GPUTextureViewDimension; +} + +/** @category WebGPU */ +declare class GPUBindGroup implements GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPUBindGroupDescriptor extends GPUObjectDescriptorBase { + layout: GPUBindGroupLayout; + entries: GPUBindGroupEntry[]; +} + +/** @category WebGPU */ +declare type GPUBindingResource = + | GPUSampler + | GPUTextureView + | GPUBufferBinding; + +/** @category WebGPU */ +declare interface GPUBindGroupEntry { + binding: number; + resource: GPUBindingResource; +} + +/** @category WebGPU */ +declare interface GPUBufferBinding { + buffer: GPUBuffer; + offset?: number; + size?: number; +} + +/** @category WebGPU */ +declare class GPUPipelineLayout implements GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPUPipelineLayoutDescriptor extends GPUObjectDescriptorBase { + bindGroupLayouts: GPUBindGroupLayout[]; +} + +/** @category WebGPU */ +declare type GPUCompilationMessageType = "error" | "warning" | "info"; + +/** @category WebGPU */ +declare interface GPUCompilationMessage { + readonly message: string; + readonly type: GPUCompilationMessageType; + readonly lineNum: number; + readonly linePos: number; +} + +/** @category WebGPU */ +declare interface GPUCompilationInfo { + readonly messages: ReadonlyArray; +} + +/** @category WebGPU */ +declare class GPUShaderModule implements GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPUShaderModuleDescriptor extends GPUObjectDescriptorBase { + code: string; + sourceMap?: any; +} + +/** @category WebGPU */ +declare type GPUAutoLayoutMode = "auto"; + +/** @category WebGPU */ +declare interface GPUPipelineDescriptorBase extends GPUObjectDescriptorBase { + layout: GPUPipelineLayout | GPUAutoLayoutMode; +} + +/** @category WebGPU */ +declare interface GPUPipelineBase { + getBindGroupLayout(index: number): GPUBindGroupLayout; +} + +/** @category WebGPU */ +declare interface GPUProgrammableStage { + module: GPUShaderModule; + entryPoint: string; +} + +/** @category WebGPU */ +declare class GPUComputePipeline implements GPUObjectBase, GPUPipelineBase { + label: string; + + getBindGroupLayout(index: number): GPUBindGroupLayout; +} + +/** @category WebGPU */ +declare interface GPUComputePipelineDescriptor + extends GPUPipelineDescriptorBase { + compute: GPUProgrammableStage; +} + +/** @category WebGPU */ +declare class GPURenderPipeline implements GPUObjectBase, GPUPipelineBase { + label: string; + + getBindGroupLayout(index: number): GPUBindGroupLayout; +} + +/** @category WebGPU */ +declare interface GPURenderPipelineDescriptor + extends GPUPipelineDescriptorBase { + vertex: GPUVertexState; + primitive?: GPUPrimitiveState; + depthStencil?: GPUDepthStencilState; + multisample?: GPUMultisampleState; + fragment?: GPUFragmentState; +} + +/** @category WebGPU */ +declare interface GPUPrimitiveState { + topology?: GPUPrimitiveTopology; + stripIndexFormat?: GPUIndexFormat; + frontFace?: GPUFrontFace; + cullMode?: GPUCullMode; + unclippedDepth?: boolean; +} + +/** @category WebGPU */ +declare type GPUPrimitiveTopology = + | "point-list" + | "line-list" + | "line-strip" + | "triangle-list" + | "triangle-strip"; + +/** @category WebGPU */ +declare type GPUFrontFace = "ccw" | "cw"; + +/** @category WebGPU */ +declare type GPUCullMode = "none" | "front" | "back"; + +/** @category WebGPU */ +declare interface GPUMultisampleState { + count?: number; + mask?: number; + alphaToCoverageEnabled?: boolean; +} + +/** @category WebGPU */ +declare interface GPUFragmentState extends GPUProgrammableStage { + targets: (GPUColorTargetState | null)[]; +} + +/** @category WebGPU */ +declare interface GPUColorTargetState { + format: GPUTextureFormat; + + blend?: GPUBlendState; + writeMask?: GPUColorWriteFlags; +} + +/** @category WebGPU */ +declare interface GPUBlendState { + color: GPUBlendComponent; + alpha: GPUBlendComponent; +} + +/** @category WebGPU */ +declare type GPUColorWriteFlags = number; + +/** @category WebGPU */ +declare class GPUColorWrite { + static RED: 0x1; + static GREEN: 0x2; + static BLUE: 0x4; + static ALPHA: 0x8; + static ALL: 0xF; +} + +/** @category WebGPU */ +declare interface GPUBlendComponent { + operation?: GPUBlendOperation; + srcFactor?: GPUBlendFactor; + dstFactor?: GPUBlendFactor; +} + +/** @category WebGPU */ +declare type GPUBlendFactor = + | "zero" + | "one" + | "src" + | "one-minus-src" + | "src-alpha" + | "one-minus-src-alpha" + | "dst" + | "one-minus-dst" + | "dst-alpha" + | "one-minus-dst-alpha" + | "src-alpha-saturated" + | "constant" + | "one-minus-constant"; + +/** @category WebGPU */ +declare type GPUBlendOperation = + | "add" + | "subtract" + | "reverse-subtract" + | "min" + | "max"; + +/** @category WebGPU */ +declare interface GPUDepthStencilState { + format: GPUTextureFormat; + + depthWriteEnabled: boolean; + depthCompare: GPUCompareFunction; + + stencilFront?: GPUStencilFaceState; + stencilBack?: GPUStencilFaceState; + + stencilReadMask?: number; + stencilWriteMask?: number; + + depthBias?: number; + depthBiasSlopeScale?: number; + depthBiasClamp?: number; +} + +/** @category WebGPU */ +declare interface GPUStencilFaceState { + compare?: GPUCompareFunction; + failOp?: GPUStencilOperation; + depthFailOp?: GPUStencilOperation; + passOp?: GPUStencilOperation; +} + +/** @category WebGPU */ +declare type GPUStencilOperation = + | "keep" + | "zero" + | "replace" + | "invert" + | "increment-clamp" + | "decrement-clamp" + | "increment-wrap" + | "decrement-wrap"; + +/** @category WebGPU */ +declare type GPUIndexFormat = "uint16" | "uint32"; + +/** @category WebGPU */ +declare type GPUVertexFormat = + | "uint8x2" + | "uint8x4" + | "sint8x2" + | "sint8x4" + | "unorm8x2" + | "unorm8x4" + | "snorm8x2" + | "snorm8x4" + | "uint16x2" + | "uint16x4" + | "sint16x2" + | "sint16x4" + | "unorm16x2" + | "unorm16x4" + | "snorm16x2" + | "snorm16x4" + | "float16x2" + | "float16x4" + | "float32" + | "float32x2" + | "float32x3" + | "float32x4" + | "uint32" + | "uint32x2" + | "uint32x3" + | "uint32x4" + | "sint32" + | "sint32x2" + | "sint32x3" + | "sint32x4"; + +/** @category WebGPU */ +declare type GPUVertexStepMode = "vertex" | "instance"; + +/** @category WebGPU */ +declare interface GPUVertexState extends GPUProgrammableStage { + buffers?: (GPUVertexBufferLayout | null)[]; +} + +/** @category WebGPU */ +declare interface GPUVertexBufferLayout { + arrayStride: number; + stepMode?: GPUVertexStepMode; + attributes: GPUVertexAttribute[]; +} + +/** @category WebGPU */ +declare interface GPUVertexAttribute { + format: GPUVertexFormat; + offset: number; + + shaderLocation: number; +} + +/** @category WebGPU */ +declare interface GPUImageDataLayout { + offset?: number; + bytesPerRow?: number; + rowsPerImage?: number; +} + +/** @category WebGPU */ +declare class GPUCommandBuffer implements GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPUCommandBufferDescriptor extends GPUObjectDescriptorBase {} + +/** @category WebGPU */ +declare class GPUCommandEncoder implements GPUObjectBase { + label: string; + + beginRenderPass(descriptor: GPURenderPassDescriptor): GPURenderPassEncoder; + beginComputePass( + descriptor?: GPUComputePassDescriptor, + ): GPUComputePassEncoder; + + copyBufferToBuffer( + source: GPUBuffer, + sourceOffset: number, + destination: GPUBuffer, + destinationOffset: number, + size: number, + ): undefined; + + copyBufferToTexture( + source: GPUImageCopyBuffer, + destination: GPUImageCopyTexture, + copySize: GPUExtent3D, + ): undefined; + + copyTextureToBuffer( + source: GPUImageCopyTexture, + destination: GPUImageCopyBuffer, + copySize: GPUExtent3D, + ): undefined; + + copyTextureToTexture( + source: GPUImageCopyTexture, + destination: GPUImageCopyTexture, + copySize: GPUExtent3D, + ): undefined; + + clearBuffer( + destination: GPUBuffer, + destinationOffset?: number, + size?: number, + ): undefined; + + pushDebugGroup(groupLabel: string): undefined; + popDebugGroup(): undefined; + insertDebugMarker(markerLabel: string): undefined; + + writeTimestamp(querySet: GPUQuerySet, queryIndex: number): undefined; + + resolveQuerySet( + querySet: GPUQuerySet, + firstQuery: number, + queryCount: number, + destination: GPUBuffer, + destinationOffset: number, + ): undefined; + + finish(descriptor?: GPUCommandBufferDescriptor): GPUCommandBuffer; +} + +/** @category WebGPU */ +declare interface GPUCommandEncoderDescriptor extends GPUObjectDescriptorBase {} + +/** @category WebGPU */ +declare interface GPUImageCopyBuffer extends GPUImageDataLayout { + buffer: GPUBuffer; +} + +/** @category WebGPU */ +declare interface GPUImageCopyTexture { + texture: GPUTexture; + mipLevel?: number; + origin?: GPUOrigin3D; + aspect?: GPUTextureAspect; +} + +/** @category WebGPU */ +interface GPUProgrammablePassEncoder { + setBindGroup( + index: number, + bindGroup: GPUBindGroup, + dynamicOffsets?: number[], + ): undefined; + + setBindGroup( + index: number, + bindGroup: GPUBindGroup, + dynamicOffsetsData: Uint32Array, + dynamicOffsetsDataStart: number, + dynamicOffsetsDataLength: number, + ): undefined; + + pushDebugGroup(groupLabel: string): undefined; + popDebugGroup(): undefined; + insertDebugMarker(markerLabel: string): undefined; +} + +/** @category WebGPU */ +declare class GPUComputePassEncoder + implements GPUObjectBase, GPUProgrammablePassEncoder { + label: string; + setBindGroup( + index: number, + bindGroup: GPUBindGroup, + dynamicOffsets?: number[], + ): undefined; + setBindGroup( + index: number, + bindGroup: GPUBindGroup, + dynamicOffsetsData: Uint32Array, + dynamicOffsetsDataStart: number, + dynamicOffsetsDataLength: number, + ): undefined; + pushDebugGroup(groupLabel: string): undefined; + popDebugGroup(): undefined; + insertDebugMarker(markerLabel: string): undefined; + setPipeline(pipeline: GPUComputePipeline): undefined; + dispatchWorkgroups(x: number, y?: number, z?: number): undefined; + dispatchWorkgroupsIndirect( + indirectBuffer: GPUBuffer, + indirectOffset: number, + ): undefined; + + end(): undefined; +} + +/** @category WebGPU */ +declare interface GPUComputePassTimestampWrites { + querySet: GPUQuerySet; + beginningOfPassWriteIndex?: number; + endOfPassWriteIndex?: number; +} + +/** @category WebGPU */ +declare interface GPUComputePassDescriptor extends GPUObjectDescriptorBase { + timestampWrites?: GPUComputePassTimestampWrites; +} + +/** @category WebGPU */ +interface GPURenderEncoderBase { + setPipeline(pipeline: GPURenderPipeline): undefined; + + setIndexBuffer( + buffer: GPUBuffer, + indexFormat: GPUIndexFormat, + offset?: number, + size?: number, + ): undefined; + setVertexBuffer( + slot: number, + buffer: GPUBuffer, + offset?: number, + size?: number, + ): undefined; + + draw( + vertexCount: number, + instanceCount?: number, + firstVertex?: number, + firstInstance?: number, + ): undefined; + drawIndexed( + indexCount: number, + instanceCount?: number, + firstIndex?: number, + baseVertex?: number, + firstInstance?: number, + ): undefined; + + drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined; + drawIndexedIndirect( + indirectBuffer: GPUBuffer, + indirectOffset: number, + ): undefined; +} + +/** @category WebGPU */ +declare class GPURenderPassEncoder + implements GPUObjectBase, GPUProgrammablePassEncoder, GPURenderEncoderBase { + label: string; + setBindGroup( + index: number, + bindGroup: GPUBindGroup, + dynamicOffsets?: number[], + ): undefined; + setBindGroup( + index: number, + bindGroup: GPUBindGroup, + dynamicOffsetsData: Uint32Array, + dynamicOffsetsDataStart: number, + dynamicOffsetsDataLength: number, + ): undefined; + pushDebugGroup(groupLabel: string): undefined; + popDebugGroup(): undefined; + insertDebugMarker(markerLabel: string): undefined; + setPipeline(pipeline: GPURenderPipeline): undefined; + setIndexBuffer( + buffer: GPUBuffer, + indexFormat: GPUIndexFormat, + offset?: number, + size?: number, + ): undefined; + setVertexBuffer( + slot: number, + buffer: GPUBuffer, + offset?: number, + size?: number, + ): undefined; + draw( + vertexCount: number, + instanceCount?: number, + firstVertex?: number, + firstInstance?: number, + ): undefined; + drawIndexed( + indexCount: number, + instanceCount?: number, + firstIndex?: number, + baseVertex?: number, + firstInstance?: number, + ): undefined; + drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined; + drawIndexedIndirect( + indirectBuffer: GPUBuffer, + indirectOffset: number, + ): undefined; + + setViewport( + x: number, + y: number, + width: number, + height: number, + minDepth: number, + maxDepth: number, + ): undefined; + + setScissorRect( + x: number, + y: number, + width: number, + height: number, + ): undefined; + + setBlendConstant(color: GPUColor): undefined; + setStencilReference(reference: number): undefined; + + beginOcclusionQuery(queryIndex: number): undefined; + endOcclusionQuery(): undefined; + + executeBundles(bundles: GPURenderBundle[]): undefined; + end(): undefined; +} + +/** @category WebGPU */ +declare interface GPURenderPassTimestampWrites { + querySet: GPUQuerySet; + beginningOfPassWriteIndex?: number; + endOfPassWriteIndex?: number; +} + +/** @category WebGPU */ +declare interface GPURenderPassDescriptor extends GPUObjectDescriptorBase { + colorAttachments: (GPURenderPassColorAttachment | null)[]; + depthStencilAttachment?: GPURenderPassDepthStencilAttachment; + occlusionQuerySet?: GPUQuerySet; + timestampWrites?: GPURenderPassTimestampWrites; +} + +/** @category WebGPU */ +declare interface GPURenderPassColorAttachment { + view: GPUTextureView; + resolveTarget?: GPUTextureView; + + clearValue?: GPUColor; + loadOp: GPULoadOp; + storeOp: GPUStoreOp; +} + +/** @category WebGPU */ +declare interface GPURenderPassDepthStencilAttachment { + view: GPUTextureView; + + depthClearValue?: number; + depthLoadOp?: GPULoadOp; + depthStoreOp?: GPUStoreOp; + depthReadOnly?: boolean; + + stencilClearValue?: number; + stencilLoadOp?: GPULoadOp; + stencilStoreOp?: GPUStoreOp; + stencilReadOnly?: boolean; +} + +/** @category WebGPU */ +declare type GPULoadOp = "load" | "clear"; + +/** @category WebGPU */ +declare type GPUStoreOp = "store" | "discard"; + +/** @category WebGPU */ +declare class GPURenderBundle implements GPUObjectBase { + label: string; +} + +/** @category WebGPU */ +declare interface GPURenderBundleDescriptor extends GPUObjectDescriptorBase {} + +/** @category WebGPU */ +declare class GPURenderBundleEncoder + implements GPUObjectBase, GPUProgrammablePassEncoder, GPURenderEncoderBase { + label: string; + draw( + vertexCount: number, + instanceCount?: number, + firstVertex?: number, + firstInstance?: number, + ): undefined; + drawIndexed( + indexCount: number, + instanceCount?: number, + firstIndex?: number, + baseVertex?: number, + firstInstance?: number, + ): undefined; + drawIndexedIndirect( + indirectBuffer: GPUBuffer, + indirectOffset: number, + ): undefined; + drawIndirect(indirectBuffer: GPUBuffer, indirectOffset: number): undefined; + insertDebugMarker(markerLabel: string): undefined; + popDebugGroup(): undefined; + pushDebugGroup(groupLabel: string): undefined; + setBindGroup( + index: number, + bindGroup: GPUBindGroup, + dynamicOffsets?: number[], + ): undefined; + setBindGroup( + index: number, + bindGroup: GPUBindGroup, + dynamicOffsetsData: Uint32Array, + dynamicOffsetsDataStart: number, + dynamicOffsetsDataLength: number, + ): undefined; + setIndexBuffer( + buffer: GPUBuffer, + indexFormat: GPUIndexFormat, + offset?: number, + size?: number, + ): undefined; + setPipeline(pipeline: GPURenderPipeline): undefined; + setVertexBuffer( + slot: number, + buffer: GPUBuffer, + offset?: number, + size?: number, + ): undefined; + + finish(descriptor?: GPURenderBundleDescriptor): GPURenderBundle; +} + +/** @category WebGPU */ +declare interface GPURenderPassLayout extends GPUObjectDescriptorBase { + colorFormats: (GPUTextureFormat | null)[]; + depthStencilFormat?: GPUTextureFormat; + sampleCount?: number; +} + +/** @category WebGPU */ +declare interface GPURenderBundleEncoderDescriptor extends GPURenderPassLayout { + depthReadOnly?: boolean; + stencilReadOnly?: boolean; +} + +/** @category WebGPU */ +declare class GPUQueue implements GPUObjectBase { + label: string; + + submit(commandBuffers: GPUCommandBuffer[]): undefined; + + onSubmittedWorkDone(): Promise; + + writeBuffer( + buffer: GPUBuffer, + bufferOffset: number, + data: BufferSource, + dataOffset?: number, + size?: number, + ): undefined; + + writeTexture( + destination: GPUImageCopyTexture, + data: BufferSource, + dataLayout: GPUImageDataLayout, + size: GPUExtent3D, + ): undefined; +} + +/** @category WebGPU */ +declare class GPUQuerySet implements GPUObjectBase { + label: string; + + destroy(): undefined; + + readonly type: GPUQueryType; + readonly count: number; +} + +/** @category WebGPU */ +declare interface GPUQuerySetDescriptor extends GPUObjectDescriptorBase { + type: GPUQueryType; + count: number; +} + +/** @category WebGPU */ +declare type GPUQueryType = "occlusion" | "timestamp"; + +/** @category WebGPU */ +declare type GPUDeviceLostReason = "destroyed"; + +/** @category WebGPU */ +declare interface GPUDeviceLostInfo { + readonly reason: GPUDeviceLostReason; + readonly message: string; +} + +/** @category WebGPU */ +declare class GPUError { + readonly message: string; +} + +/** @category WebGPU */ +declare class GPUOutOfMemoryError extends GPUError { + constructor(message: string); +} + +/** @category WebGPU */ +declare class GPUValidationError extends GPUError { + constructor(message: string); +} + +/** @category WebGPU */ +declare type GPUErrorFilter = "out-of-memory" | "validation"; + +/** @category WebGPU */ +declare interface GPUColorDict { + r: number; + g: number; + b: number; + a: number; +} + +/** @category WebGPU */ +declare type GPUColor = number[] | GPUColorDict; + +/** @category WebGPU */ +declare interface GPUOrigin3DDict { + x?: number; + y?: number; + z?: number; +} + +/** @category WebGPU */ +declare type GPUOrigin3D = number[] | GPUOrigin3DDict; + +/** @category WebGPU */ +declare interface GPUExtent3DDict { + width: number; + height?: number; + depthOrArrayLayers?: number; +} + +/** @category WebGPU */ +declare type GPUExtent3D = number[] | GPUExtent3DDict; + +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + // deno-lint-ignore-file no-explicit-any no-var /// @@ -9934,6 +11424,7 @@ declare interface CacheQueryOptions { /// /// /// +/// /// /// /// @@ -10033,6 +11524,7 @@ declare var caches: CacheStorage; /** @category Web APIs */ declare interface Navigator { + readonly gpu: GPU; readonly hardwareConcurrency: number; readonly userAgent: string; readonly language: string; @@ -11077,7 +12569,7 @@ declare namespace Deno { * * @category Fetch API */ - export interface HttpClient { + export interface HttpClient extends Disposable { /** The resource ID associated with the client. */ rid: number; /** Close the HTTP client. */ @@ -11547,6 +13039,32 @@ declare namespace Deno { */ export function openKv(path?: string): Promise; + /** **UNSTABLE**: New API, yet to be vetted. + * + * CronScheduleExpression is used as the type of `minute`, `hour`, + * `dayOfMonth`, `month`, and `dayOfWeek` in {@linkcode CronSchedule}. + * @category Cron + */ + type CronScheduleExpression = number | { exact: number | number[] } | { + start?: number; + end?: number; + every?: number; + }; + + /** **UNSTABLE**: New API, yet to be vetted. + * + * CronSchedule is the interface used for JSON format + * cron `schedule`. + * @category Cron + */ + export interface CronSchedule { + minute?: CronScheduleExpression; + hour?: CronScheduleExpression; + dayOfMonth?: CronScheduleExpression; + month?: CronScheduleExpression; + dayOfWeek?: CronScheduleExpression; + } + /** **UNSTABLE**: New API, yet to be vetted. * * Create a cron job that will periodically execute the provided handler @@ -11557,21 +13075,23 @@ declare namespace Deno { * console.log("cron job executed"); * }); * ``` - * `backoffSchedule` option can be used to specify the retry policy for failed - * executions. Each element in the array represents the number of milliseconds - * to wait before retrying the execution. For example, `[1000, 5000, 10000]` - * means that a failed execution will be retried at most 3 times, with 1 - * second, 5 seconds, and 10 seconds delay between each retry. + * + * ```ts + * Deno.cron("sample cron", { hour: { every: 6 } }, () => { + * console.log("cron job executed"); + * }); + * ``` + * + * `schedule` can be a string in the Unix cron format or in JSON format + * as specified by interface {@linkcode CronSchedule}, where time is specified + * using UTC time zone. * * @category Cron - * @deprecated Use other {@linkcode cron} overloads instead. This overload - * will be removed in the future. */ export function cron( name: string, - schedule: string, + schedule: string | CronSchedule, handler: () => Promise | void, - options: { backoffSchedule?: number[]; signal?: AbortSignal }, ): Promise; /** **UNSTABLE**: New API, yet to be vetted. @@ -11580,19 +13100,29 @@ declare namespace Deno { * callback based on the specified schedule. * * ```ts - * Deno.cron("sample cron", "20 * * * *", () => { + * Deno.cron("sample cron", "20 * * * *", { + * backoffSchedule: [10, 20] + * }, () => { * console.log("cron job executed"); * }); * ``` * - * `schedule` is a Unix cron format expression, where time is specified + * `schedule` can be a string in the Unix cron format or in JSON format + * as specified by interface {@linkcode CronSchedule}, where time is specified * using UTC time zone. * + * `backoffSchedule` option can be used to specify the retry policy for failed + * executions. Each element in the array represents the number of milliseconds + * to wait before retrying the execution. For example, `[1000, 5000, 10000]` + * means that a failed execution will be retried at most 3 times, with 1 + * second, 5 seconds, and 10 seconds delay between each retry. + * * @category Cron */ export function cron( name: string, - schedule: string, + schedule: string | CronSchedule, + options: { backoffSchedule?: number[]; signal?: AbortSignal }, handler: () => Promise | void, ): Promise; @@ -11601,17 +13131,15 @@ declare namespace Deno { * Create a cron job that will periodically execute the provided handler * callback based on the specified schedule. * + * `schedule` can be a string in the Unix cron format or in JSON format + * as specified by interface {@linkcode CronSchedule}, where time is specified + * using UTC time zone. + * * ```ts - * Deno.cron("sample cron", "20 * * * *", { - * backoffSchedule: [10, 20] - * }, () => { + * Deno.cron("sample cron", "20 * * * *", () => { * console.log("cron job executed"); * }); * ``` - * - * `schedule` is a Unix cron format expression, where time is specified - * using UTC time zone. - * * `backoffSchedule` option can be used to specify the retry policy for failed * executions. Each element in the array represents the number of milliseconds * to wait before retrying the execution. For example, `[1000, 5000, 10000]` @@ -11619,12 +13147,14 @@ declare namespace Deno { * second, 5 seconds, and 10 seconds delay between each retry. * * @category Cron + * @deprecated Use other {@linkcode cron} overloads instead. This overload + * will be removed in the future. */ export function cron( name: string, - schedule: string, - options: { backoffSchedule?: number[]; signal?: AbortSignal }, + schedule: string | CronSchedule, handler: () => Promise | void, + options: { backoffSchedule?: number[]; signal?: AbortSignal }, ): Promise; /** **UNSTABLE**: New API, yet to be vetted. @@ -11680,7 +13210,13 @@ declare namespace Deno { * * @category KV */ - export type KvKeyPart = Uint8Array | string | number | bigint | boolean; + export type KvKeyPart = + | Uint8Array + | string + | number + | bigint + | boolean + | symbol; /** **UNSTABLE**: New API, yet to be vetted. * @@ -11978,7 +13514,11 @@ declare namespace Deno { */ enqueue( value: unknown, - options?: { delay?: number; keysIfUndelivered?: Deno.KvKey[] }, + options?: { + delay?: number; + keysIfUndelivered?: Deno.KvKey[]; + backoffSchedule?: number[]; + }, ): this; /** * Commit the operation to the KV store. Returns a value indicating whether @@ -12187,14 +13727,28 @@ declare namespace Deno { * listener after several attempts. The values are set to the value of * the queued message. * + * The `backoffSchedule` option can be used to specify the retry policy for + * failed message delivery. Each element in the array represents the number of + * milliseconds to wait before retrying the delivery. For example, + * `[1000, 5000, 10000]` means that a failed delivery will be retried + * at most 3 times, with 1 second, 5 seconds, and 10 seconds delay + * between each retry. + * * ```ts * const db = await Deno.openKv(); - * await db.enqueue("bar", { keysIfUndelivered: [["foo", "bar"]] }); + * await db.enqueue("bar", { + * keysIfUndelivered: [["foo", "bar"]], + * backoffSchedule: [1000, 5000, 10000], + * }); * ``` */ enqueue( value: unknown, - options?: { delay?: number; keysIfUndelivered?: Deno.KvKey[] }, + options?: { + delay?: number; + keysIfUndelivered?: Deno.KvKey[]; + backoffSchedule?: number[]; + }, ): Promise; /** @@ -12273,6 +13827,14 @@ declare namespace Deno { */ close(): void; + /** + * Get a symbol that represents the versionstamp of the current atomic + * operation. This symbol can be used as the last part of a key in + * `.set()`, both directly on the `Kv` object and on an `AtomicOperation` + * object created from this `Kv` instance. + */ + commitVersionstamp(): symbol; + [Symbol.dispose](): void; } @@ -12291,138 +13853,6 @@ declare namespace Deno { readonly value: bigint; } - /** An instance of the server created using `Deno.serve()` API. - * - * @category HTTP Server - */ - export interface HttpServer { - /** Gracefully close the server. No more new connections will be accepted, - * while pending requests will be allowed to finish. - */ - shutdown(): Promise; - } - - export interface ServeUnixOptions { - /** The unix domain socket path to listen on. */ - path: string; - - /** An {@linkcode AbortSignal} to close the server and all connections. */ - signal?: AbortSignal; - - /** The handler to invoke when route handlers throw an error. */ - onError?: (error: unknown) => Response | Promise; - - /** The callback which is called when the server starts listening. */ - onListen?: (params: { path: string }) => void; - } - - /** Information for a unix domain socket HTTP request. - * - * @category HTTP Server - */ - export interface ServeUnixHandlerInfo { - /** The remote address of the connection. */ - remoteAddr: Deno.UnixAddr; - } - - /** A handler for unix domain socket HTTP requests. Consumes a request and returns a response. - * - * If a handler throws, the server calling the handler will assume the impact - * of the error is isolated to the individual request. It will catch the error - * and if necessary will close the underlying connection. - * - * @category HTTP Server - */ - export type ServeUnixHandler = ( - request: Request, - info: ServeUnixHandlerInfo, - ) => Response | Promise; - - /** - * @category HTTP Server - */ - export interface ServeUnixInit { - /** The handler to invoke to process each incoming request. */ - handler: ServeUnixHandler; - } - - /** Serves HTTP requests with the given option bag and handler. - * - * You can specify the socket path with `path` option. - * - * ```ts - * Deno.serve( - * { path: "path/to/socket" }, - * (_req) => new Response("Hello, world") - * ); - * ``` - * - * You can stop the server with an {@linkcode AbortSignal}. The abort signal - * needs to be passed as the `signal` option in the options bag. The server - * aborts when the abort signal is aborted. To wait for the server to close, - * await the promise returned from the `Deno.serve` API. - * - * ```ts - * const ac = new AbortController(); - * - * const server = Deno.serve( - * { signal: ac.signal, path: "path/to/socket" }, - * (_req) => new Response("Hello, world") - * ); - * server.finished.then(() => console.log("Server closed")); - * - * console.log("Closing server..."); - * ac.abort(); - * ``` - * - * By default `Deno.serve` prints the message - * `Listening on path/to/socket` on listening. If you like to - * change this behavior, you can specify a custom `onListen` callback. - * - * ```ts - * Deno.serve({ - * onListen({ path }) { - * console.log(`Server started at ${path}`); - * // ... more info specific to your server .. - * }, - * path: "path/to/socket", - * }, (_req) => new Response("Hello, world")); - * ``` - * - * @category HTTP Server - */ - export function serve( - options: ServeUnixOptions, - handler: ServeUnixHandler, - ): Server; - /** Serves HTTP requests with the given option bag. - * - * You can specify an object with the path option, which is the - * unix domain socket to listen on. - * - * ```ts - * const ac = new AbortController(); - * - * const server = Deno.serve({ - * path: "path/to/socket", - * handler: (_req) => new Response("Hello, world"), - * signal: ac.signal, - * onListen({ path }) { - * console.log(`Server started at ${path}`); - * }, - * }); - * server.finished.then(() => console.log("Server closed")); - * - * console.log("Closing server..."); - * ac.abort(); - * ``` - * - * @category HTTP Server - */ - export function serve( - options: ServeUnixInit & ServeUnixOptions, - ): Server; - /** * A namespace containing runtime APIs available in Jupyter notebooks. * diff --git a/packages/deno/src/integrations/deno-cron-format.ts b/packages/deno/src/integrations/deno-cron-format.ts new file mode 100644 index 000000000000..ac7bfc813fde --- /dev/null +++ b/packages/deno/src/integrations/deno-cron-format.ts @@ -0,0 +1,84 @@ +/** + * These functions were copied from the Deno source code here: + * https://github.com/denoland/deno/blob/cd480b481ee1b4209910aa7a8f81ffa996e7b0f9/ext/cron/01_cron.ts + * Below is the original license: + * + * MIT License + * + * Copyright 2018-2023 the Deno authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +function formatToCronSchedule( + value?: + | number + | { exact: number | number[] } + | { + start?: number; + end?: number; + every?: number; + }, +): string { + if (value === undefined) { + return '*'; + } else if (typeof value === 'number') { + return value.toString(); + } else { + const { exact } = value as { exact: number | number[] }; + if (exact === undefined) { + const { start, end, every } = value as { + start?: number; + end?: number; + every?: number; + }; + if (start !== undefined && end !== undefined && every !== undefined) { + return `${start}-${end}/${every}`; + } else if (start !== undefined && end !== undefined) { + return `${start}-${end}`; + } else if (start !== undefined && every !== undefined) { + return `${start}/${every}`; + } else if (start !== undefined) { + return `${start}/1`; + } else if (end === undefined && every !== undefined) { + return `*/${every}`; + } else { + throw new TypeError('Invalid cron schedule'); + } + } else { + if (typeof exact === 'number') { + return exact.toString(); + } else { + return exact.join(','); + } + } + } +} + +/** */ +export function parseScheduleToString(schedule: string | Deno.CronSchedule): string { + if (typeof schedule === 'string') { + return schedule; + } else { + const { minute, hour, dayOfMonth, month, dayOfWeek } = schedule; + + return `${formatToCronSchedule(minute)} ${formatToCronSchedule(hour)} ${formatToCronSchedule( + dayOfMonth, + )} ${formatToCronSchedule(month)} ${formatToCronSchedule(dayOfWeek)}`; + } +} diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts index f40d696f5e3c..475d3e9131b7 100644 --- a/packages/deno/src/integrations/deno-cron.ts +++ b/packages/deno/src/integrations/deno-cron.ts @@ -1,11 +1,12 @@ import { withMonitor } from '@sentry/core'; import type { Integration } from '@sentry/types'; import type { DenoClient } from '../client'; +import { parseScheduleToString } from './deno-cron-format'; type CronOptions = { backoffSchedule?: number[]; signal?: AbortSignal }; type CronFn = () => void | Promise; // Parameters doesn't work well with the overloads 🤔 -type CronParams = [string, string, CronFn | CronOptions, CronFn | CronOptions | undefined]; +type CronParams = [string, string | Deno.CronSchedule, CronFn | CronOptions, CronFn | CronOptions | undefined]; /** Instruments Deno.cron to automatically capture cron check-ins */ export class DenoCron implements Integration { @@ -21,7 +22,7 @@ export class DenoCron implements Integration { } /** @inheritDoc */ - public setup(client: DenoClient): void { + public setup(): void { // eslint-disable-next-line deprecation/deprecation if (!Deno.cron) { // The cron API is not available in this Deno version use --unstable flag! @@ -45,9 +46,11 @@ export class DenoCron implements Integration { async function cronCalled(): Promise { await withMonitor(monitorSlug, async () => fn(), { - schedule: { type: 'crontab', value: schedule }, + schedule: { type: 'crontab', value: parseScheduleToString(schedule) }, // (minutes) so 12 hours - just a very high arbitrary number since we don't know the actual duration of the users cron job maxRuntime: 60 * 12, + // Deno Deploy docs say that the cron job will be called within 1 minute of the scheduled time + checkinMargin: 1, }); } From 84299d0fdac69426d5fabd50038840e0b089b12d Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Mon, 18 Dec 2023 15:46:26 -0330 Subject: [PATCH 23/34] feat(replay): Add `canvas.type` setting (#9877) Defaults to `webp` instead of `png`. This will also allow `quality` to work. Closes https://github.com/getsentry/team-replay/issues/326 --- packages/replay/src/replay.ts | 5 ++++- packages/replay/src/types/replay.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 0085c44c6eb8..d0dc3097bcf6 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -343,7 +343,10 @@ export class ReplayContainer implements ReplayContainerInterface { ...(canvas && { recordCanvas: true, sampling: { canvas: canvas.fps || 4 }, - dataURLOptions: { quality: canvas.quality || 0.6 }, + dataURLOptions: { + type: canvas.type || 'image/webp', + quality: canvas.quality || 0.6, + }, getCanvasManager: canvas.manager, }), }); diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index d854f258c073..fb1f91c0e1a9 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -235,6 +235,7 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { canvas: { fps?: number; quality?: number; + type?: string; manager: (options: GetCanvasManagerOptions) => CanvasManagerInterface; }; }>; From 605fd503f43f04a8c305cccda91cf435e0caacac Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 19 Dec 2023 09:21:34 +0100 Subject: [PATCH 24/34] feat(node-experimental): Update to new Scope APIs (#9799) This PR introduces the new scope APIs to node-experimental. * `getCurrentHub()` is still around, but just a mock hub that uses other methods under the hood. * Instead, there are the following new APIs: * `getCurrentScope()` * `getIsolationScope()` * `getGlobalScope()` * `withIsolationScope()` Mostly existing tests should cover this OK. The main change here is that for spans, since we use the isolation scope any tags etc. added while the span is running are _also_ added to the resulting event. For POTEL, we automatically set an isolation scope whenever a http.server span is generated. Replaces https://github.com/getsentry/sentry-javascript/pull/9419 --------- Co-authored-by: Luca Forstner --- packages/core/src/index.ts | 1 + packages/core/src/integration.ts | 2 +- packages/node-experimental/src/index.ts | 49 +- .../src/integrations/http.ts | 7 + .../src/otel/asyncContextStrategy.ts | 29 ++ .../src/otel/contextManager.ts | 45 ++ packages/node-experimental/src/sdk/api.ts | 225 +++++++++ packages/node-experimental/src/sdk/client.ts | 63 ++- packages/node-experimental/src/sdk/globals.ts | 38 ++ packages/node-experimental/src/sdk/hub.ts | 170 +++++++ packages/node-experimental/src/sdk/init.ts | 193 +++++++- .../node-experimental/src/sdk/initOtel.ts | 13 +- packages/node-experimental/src/sdk/scope.ts | 406 ++++++++++++++++ .../src/sdk/spanProcessor.ts | 21 +- packages/node-experimental/src/sdk/types.ts | 91 ++++ .../src/utils/contextData.ts | 22 + .../src/utils/prepareEvent.ts | 58 +++ .../test/helpers/mockSdkInit.ts | 5 +- .../test/integration/breadcrumbs.test.ts | 86 ++-- .../test/integration/scope.test.ts | 458 +++++++++++++++++- .../test/integration/transactions.test.ts | 64 +-- .../node-experimental/test/sdk/scope.test.ts | 416 ++++++++++++++++ packages/node/src/index.ts | 2 +- packages/opentelemetry/src/custom/scope.ts | 5 + packages/opentelemetry/src/index.ts | 12 +- packages/opentelemetry/src/spanExporter.ts | 11 +- packages/opentelemetry/src/spanProcessor.ts | 19 +- packages/opentelemetry/src/utils/spanData.ts | 11 + 28 files changed, 2387 insertions(+), 135 deletions(-) create mode 100644 packages/node-experimental/src/otel/asyncContextStrategy.ts create mode 100644 packages/node-experimental/src/otel/contextManager.ts create mode 100644 packages/node-experimental/src/sdk/api.ts create mode 100644 packages/node-experimental/src/sdk/globals.ts create mode 100644 packages/node-experimental/src/sdk/hub.ts create mode 100644 packages/node-experimental/src/sdk/scope.ts create mode 100644 packages/node-experimental/src/sdk/types.ts create mode 100644 packages/node-experimental/src/utils/contextData.ts create mode 100644 packages/node-experimental/src/utils/prepareEvent.ts create mode 100644 packages/node-experimental/test/sdk/scope.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2fb2f17b6089..6468c312bbe1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -44,6 +44,7 @@ export { makeSession, closeSession, updateSession } from './session'; export { SessionFlusher } from './sessionflusher'; export { Scope } from './scope'; export { + notifyEventProcessors, // eslint-disable-next-line deprecation/deprecation addGlobalEventProcessor, } from './eventProcessors'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 57adc3d33c36..b7782fcfa65c 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -46,7 +46,7 @@ function filterDuplicates(integrations: Integration[]): Integration[] { } /** Gets integrations to install */ -export function getIntegrationsToSetup(options: Options): Integration[] { +export function getIntegrationsToSetup(options: Pick): Integration[] { const defaultIntegrations = options.defaultIntegrations || []; const userIntegrations = options.integrations; diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 5d976dcd4576..e45616b2e65a 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -13,7 +13,34 @@ export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanc export * as Handlers from './sdk/handlers'; export type { Span } from './types'; -export { startSpan, startInactiveSpan, getCurrentHub, getClient, getActiveSpan } from '@sentry/opentelemetry'; +export { startSpan, startInactiveSpan, getActiveSpan } from '@sentry/opentelemetry'; +export { + getClient, + addBreadcrumb, + captureException, + captureEvent, + captureMessage, + addGlobalEventProcessor, + addEventProcessor, + lastEventId, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + withScope, + withIsolationScope, + // eslint-disable-next-line deprecation/deprecation + configureScope, + getCurrentScope, + getGlobalScope, + getIsolationScope, + setIsolationScope, + setCurrentScope, +} from './sdk/api'; +export { getCurrentHub, makeMain } from './sdk/hub'; +export { Scope } from './sdk/scope'; export { makeNodeTransport, @@ -24,36 +51,16 @@ export { extractRequestData, deepReadDirSync, getModuleFromFilename, - // eslint-disable-next-line deprecation/deprecation - addGlobalEventProcessor, - addEventProcessor, - addBreadcrumb, - captureException, - captureEvent, - captureMessage, close, - // eslint-disable-next-line deprecation/deprecation - configureScope, createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, - getActiveTransaction, Hub, - lastEventId, - makeMain, runWithAsyncContext, - Scope, SDK_VERSION, - setContext, - setExtra, - setExtras, - setTag, - setTags, - setUser, spanStatusfromHttpCode, trace, - withScope, captureCheckIn, withMonitor, hapiErrorPlugin, diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 860169c6a43e..4588d1b36b15 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -8,6 +8,8 @@ import { _INTERNAL, getClient, getCurrentHub, getSpanKind, setSpanMetadata } fro import type { EventProcessor, Hub, Integration } from '@sentry/types'; import { stringMatchesSomePattern } from '@sentry/utils'; +import { getIsolationScope, setIsolationScope } from '../sdk/api'; +import { Scope } from '../sdk/scope'; import type { NodeExperimentalClient } from '../types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { getRequestUrl } from '../utils/getRequestUrl'; @@ -127,6 +129,11 @@ export class Http implements Integration { requireParentforIncomingSpans: false, requestHook: (span, req) => { this._updateSpan(span, req); + + // Update the isolation scope, isolate this request + if (getSpanKind(span) === SpanKind.SERVER) { + setIsolationScope(getIsolationScope().clone()); + } }, responseHook: (span, res) => { this._addRequestBreadcrumb(span, res); diff --git a/packages/node-experimental/src/otel/asyncContextStrategy.ts b/packages/node-experimental/src/otel/asyncContextStrategy.ts new file mode 100644 index 000000000000..e0d976c71ff1 --- /dev/null +++ b/packages/node-experimental/src/otel/asyncContextStrategy.ts @@ -0,0 +1,29 @@ +import * as api from '@opentelemetry/api'; + +import { setAsyncContextStrategy } from './../sdk/globals'; +import { getCurrentHub } from './../sdk/hub'; +import type { CurrentScopes } from './../sdk/types'; +import { getScopesFromContext } from './../utils/contextData'; + +/** + * Sets the async context strategy to use follow the OTEL context under the hood. + * We handle forking a hub inside of our custom OTEL Context Manager (./otelContextManager.ts) + */ +export function setOpenTelemetryContextAsyncContextStrategy(): void { + function getScopes(): CurrentScopes | undefined { + const ctx = api.context.active(); + return getScopesFromContext(ctx); + } + + /* This is more or less a NOOP - we rely on the OTEL context manager for this */ + function runWithAsyncContext(callback: () => T): T { + const ctx = api.context.active(); + + // We depend on the otelContextManager to handle the context/hub + return api.context.with(ctx, () => { + return callback(); + }); + } + + setAsyncContextStrategy({ getScopes, getCurrentHub, runWithAsyncContext }); +} diff --git a/packages/node-experimental/src/otel/contextManager.ts b/packages/node-experimental/src/otel/contextManager.ts new file mode 100644 index 000000000000..4ba4f0642b16 --- /dev/null +++ b/packages/node-experimental/src/otel/contextManager.ts @@ -0,0 +1,45 @@ +import type { Context } from '@opentelemetry/api'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { setHubOnContext } from '@sentry/opentelemetry'; +import { getCurrentHub } from '../sdk/hub'; + +import { getCurrentScope, getIsolationScope } from './../sdk/api'; +import { Scope } from './../sdk/scope'; +import type { CurrentScopes } from './../sdk/types'; +import { getScopesFromContext, setScopesOnContext } from './../utils/contextData'; + +/** + * This is a custom ContextManager for OpenTelemetry, which extends the default AsyncLocalStorageContextManager. + * It ensures that we create a new hub per context, so that the OTEL Context & the Sentry Hub are always in sync. + * + * Note that we currently only support AsyncHooks with this, + * but since this should work for Node 14+ anyhow that should be good enough. + */ +export class SentryContextManager extends AsyncLocalStorageContextManager { + /** + * Overwrite with() of the original AsyncLocalStorageContextManager + * to ensure we also create a new hub per context. + */ + public with
ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const previousScopes = getScopesFromContext(context); + + const currentScope = previousScopes ? previousScopes.scope : getCurrentScope(); + const isolationScope = previousScopes ? previousScopes.isolationScope : getIsolationScope(); + + const newCurrentScope = currentScope.clone(); + const scopes: CurrentScopes = { scope: newCurrentScope, isolationScope }; + + // We also need to "mock" the hub on the context, as the original @sentry/opentelemetry uses that... + const mockHub = { ...getCurrentHub(), getScope: () => scopes.scope }; + + const ctx1 = setHubOnContext(context, mockHub); + const ctx2 = setScopesOnContext(ctx1, scopes); + + return super.with(ctx2, fn, thisArg, ...args); + } +} diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node-experimental/src/sdk/api.ts new file mode 100644 index 000000000000..1a7ddfd52ad5 --- /dev/null +++ b/packages/node-experimental/src/sdk/api.ts @@ -0,0 +1,225 @@ +// PUBLIC APIS + +import { context } from '@opentelemetry/api'; +import { DEFAULT_ENVIRONMENT, closeSession, makeSession, updateSession } from '@sentry/core'; +import type { + Breadcrumb, + BreadcrumbHint, + CaptureContext, + Client, + Event, + EventHint, + EventProcessor, + Extra, + Extras, + Primitive, + Session, + Severity, + SeverityLevel, + User, +} from '@sentry/types'; +import { GLOBAL_OBJ, consoleSandbox, dateTimestampInSeconds } from '@sentry/utils'; +import { getScopesFromContext, setScopesOnContext } from '../utils/contextData'; + +import type { ExclusiveEventHintOrCaptureContext } from '../utils/prepareEvent'; +import { parseEventHintOrCaptureContext } from '../utils/prepareEvent'; +import type { Scope } from './scope'; +import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from './scope'; + +export { getCurrentScope, getGlobalScope, getIsolationScope, getClient }; +export { setCurrentScope, setIsolationScope } from './scope'; + +/** + * Fork a scope from the current scope, and make it the current scope in the given callback + */ +export function withScope(callback: (scope: Scope) => T): T { + return context.with(context.active(), () => callback(getCurrentScope())); +} + +/** + * For a new isolation scope from the current isolation scope, + * and make it the current isolation scope in the given callback. + */ +export function withIsolationScope(callback: (isolationScope: Scope) => T): T { + const ctx = context.active(); + const currentScopes = getScopesFromContext(ctx); + const scopes = currentScopes + ? { ...currentScopes } + : { + scope: getCurrentScope(), + isolationScope: getIsolationScope(), + }; + + scopes.isolationScope = scopes.isolationScope.clone(); + + return context.with(setScopesOnContext(ctx, scopes), () => { + return callback(getIsolationScope()); + }); +} + +/** Get the ID of the last sent error event. */ +export function lastEventId(): string | undefined { + return getCurrentScope().lastEventId(); +} + +/** + * Configure the current scope. + * @deprecated Use `getCurrentScope()` instead. + */ +export function configureScope(callback: (scope: Scope) => void): void { + callback(getCurrentScope()); +} + +/** Record an exception and send it to Sentry. */ +export function captureException(exception: unknown, hint?: ExclusiveEventHintOrCaptureContext): string { + return getCurrentScope().captureException(exception, parseEventHintOrCaptureContext(hint)); +} + +/** Record a message and send it to Sentry. */ +export function captureMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + captureContext?: CaptureContext | Severity | SeverityLevel, +): string { + // This is necessary to provide explicit scopes upgrade, without changing the original + // arity of the `captureMessage(message, level)` method. + const level = typeof captureContext === 'string' ? captureContext : undefined; + const context = typeof captureContext !== 'string' ? { captureContext } : undefined; + + return getCurrentScope().captureMessage(message, level, context); +} + +/** Capture a generic event and send it to Sentry. */ +export function captureEvent(event: Event, hint?: EventHint): string { + return getCurrentScope().captureEvent(event, hint); +} + +/** + * Add a breadcrumb to the current isolation scope. + */ +export function addBreadcrumb(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): void { + const client = getClient(); + + const { beforeBreadcrumb, maxBreadcrumbs } = client.getOptions(); + + if (maxBreadcrumbs && maxBreadcrumbs <= 0) return; + + const timestamp = dateTimestampInSeconds(); + const mergedBreadcrumb = { timestamp, ...breadcrumb }; + const finalBreadcrumb = beforeBreadcrumb + ? (consoleSandbox(() => beforeBreadcrumb(mergedBreadcrumb, hint)) as Breadcrumb | null) + : mergedBreadcrumb; + + if (finalBreadcrumb === null) return; + + if (client.emit) { + client.emit('beforeAddBreadcrumb', finalBreadcrumb, hint); + } + + getIsolationScope().addBreadcrumb(finalBreadcrumb, maxBreadcrumbs); +} + +/** + * Add a global event processor. + */ +export function addGlobalEventProcessor(eventProcessor: EventProcessor): void { + getGlobalScope().addEventProcessor(eventProcessor); +} + +/** + * Add an event processor to the current isolation scope. + */ +export function addEventProcessor(eventProcessor: EventProcessor): void { + getIsolationScope().addEventProcessor(eventProcessor); +} + +/** Set the user for the current isolation scope. */ +export function setUser(user: User | null): void { + getIsolationScope().setUser(user); +} + +/** Set tags for the current isolation scope. */ +export function setTags(tags: { [key: string]: Primitive }): void { + getIsolationScope().setTags(tags); +} + +/** Set a single tag user for the current isolation scope. */ +export function setTag(key: string, value: Primitive): void { + getIsolationScope().setTag(key, value); +} + +/** Set extra data for the current isolation scope. */ +export function setExtra(key: string, extra: Extra): void { + getIsolationScope().setExtra(key, extra); +} + +/** Set multiple extra data for the current isolation scope. */ +export function setExtras(extras: Extras): void { + getIsolationScope().setExtras(extras); +} + +/** Set context data for the current isolation scope. */ +export function setContext( + name: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + context: { [key: string]: any } | null, +): void { + getIsolationScope().setContext(name, context); +} + +/** Start a session on the current isolation scope. */ +export function startSession(context?: Session): Session { + const client = getClient(); + const isolationScope = getIsolationScope(); + + const { release, environment = DEFAULT_ENVIRONMENT } = client.getOptions(); + + // Will fetch userAgent if called from browser sdk + const { userAgent } = GLOBAL_OBJ.navigator || {}; + + const session = makeSession({ + release, + environment, + user: isolationScope.getUser(), + ...(userAgent && { userAgent }), + ...context, + }); + + // End existing session if there's one + const currentSession = isolationScope.getSession && isolationScope.getSession(); + if (currentSession && currentSession.status === 'ok') { + updateSession(currentSession, { status: 'exited' }); + } + endSession(); + + // Afterwards we set the new session on the scope + isolationScope.setSession(session); + + return session; +} + +/** End the session on the current isolation scope. */ +export function endSession(): void { + const isolationScope = getIsolationScope(); + const session = isolationScope.getSession(); + if (session) { + closeSession(session); + } + _sendSessionUpdate(); + + // the session is over; take it off of the scope + isolationScope.setSession(); +} + +/** + * Sends the current Session on the scope + */ +function _sendSessionUpdate(): void { + const scope = getCurrentScope(); + const client = getClient(); + + const session = scope.getSession(); + if (session && client.captureSession) { + client.captureSession(session); + } +} diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node-experimental/src/sdk/client.ts index 809d1fa49035..8a7626b4ff9c 100644 --- a/packages/node-experimental/src/sdk/client.ts +++ b/packages/node-experimental/src/sdk/client.ts @@ -1,7 +1,16 @@ import { NodeClient, SDK_VERSION } from '@sentry/node'; -import { wrapClientClass } from '@sentry/opentelemetry'; -class NodeExperimentalBaseClient extends NodeClient { +import type { Tracer } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import type { CaptureContext, Event, EventHint } from '@sentry/types'; +import { Scope } from './scope'; + +/** A client for using Sentry with Node & OpenTelemetry. */ +export class NodeExperimentalClient extends NodeClient { + public traceProvider: BasicTracerProvider | undefined; + private _tracer: Tracer | undefined; + public constructor(options: ConstructorParameters[0]) { options._metadata = options._metadata || {}; options._metadata.sdk = options._metadata.sdk || { @@ -17,6 +26,54 @@ class NodeExperimentalBaseClient extends NodeClient { super(options); } + + /** Get the OTEL tracer. */ + public get tracer(): Tracer { + if (this._tracer) { + return this._tracer; + } + + const name = '@sentry/node-experimental'; + const version = SDK_VERSION; + const tracer = trace.getTracer(name, version); + this._tracer = tracer; + + return tracer; + } + + /** + * @inheritDoc + */ + public async flush(timeout?: number): Promise { + const provider = this.traceProvider; + const spanProcessor = provider?.activeSpanProcessor; + + if (spanProcessor) { + await spanProcessor.forceFlush(); + } + + return super.flush(timeout); + } + + /** + * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. + * This uses `new Scope()`, which we need to replace with our own Scope 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 = getScopeForEvent(scope, hint.captureContext); + delete hint.captureContext; + } + + return super._prepareEvent(event, hint, actualScope); + } } -export const NodeExperimentalClient = wrapClientClass(NodeExperimentalBaseClient); +function getScopeForEvent(scope: Scope | undefined, captureContext: CaptureContext): Scope | undefined { + const finalScope = scope ? scope.clone() : new Scope(); + finalScope.update(captureContext); + return finalScope; +} diff --git a/packages/node-experimental/src/sdk/globals.ts b/packages/node-experimental/src/sdk/globals.ts new file mode 100644 index 000000000000..a91f07cd206d --- /dev/null +++ b/packages/node-experimental/src/sdk/globals.ts @@ -0,0 +1,38 @@ +import type { Hub } from '@sentry/types'; +import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import { DEBUG_BUILD } from '../debug-build'; + +import type { AsyncContextStrategy, SentryCarrier } from './types'; + +/** Update the async context strategy */ +export function setAsyncContextStrategy(strategy: AsyncContextStrategy | undefined): void { + const carrier = getGlobalCarrier(); + carrier.acs = strategy; +} + +/** + * Returns the global shim registry. + **/ +export function getGlobalCarrier(): SentryCarrier { + GLOBAL_OBJ.__SENTRY__ = GLOBAL_OBJ.__SENTRY__ || { + extensions: {}, + // For legacy reasons... + globalEventProcessors: [], + }; + + return GLOBAL_OBJ.__SENTRY__; +} + +/** + * Calls global extension method and binding current instance to the function call + */ +// @ts-expect-error Function lacks ending return statement and return type does not include 'undefined'. ts(2366) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function callExtensionMethod(hub: Hub, method: string, ...args: any[]): T { + const carrier = getGlobalCarrier(); + + if (carrier.extensions && typeof carrier.extensions[method] === 'function') { + return carrier.extensions[method].apply(hub, args); + } + DEBUG_BUILD && logger.warn(`Extension method ${method} couldn't be found, doing nothing.`); +} diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/node-experimental/src/sdk/hub.ts new file mode 100644 index 000000000000..21e1c83a34bb --- /dev/null +++ b/packages/node-experimental/src/sdk/hub.ts @@ -0,0 +1,170 @@ +import type { + Client, + CustomSamplingContext, + EventHint, + Hub, + Integration, + IntegrationClass, + Session, + Severity, + SeverityLevel, + TransactionContext, +} from '@sentry/types'; + +import { + addBreadcrumb, + captureEvent, + captureException, + captureMessage, + configureScope, + endSession, + getClient, + getCurrentScope, + lastEventId, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + startSession, + withScope, +} from './api'; +import { callExtensionMethod, getGlobalCarrier } from './globals'; +import type { Scope } from './scope'; +import type { SentryCarrier } from './types'; + +/** Ensure the global hub is our proxied hub. */ +export function setupGlobalHub(): void { + const carrier = getGlobalCarrier(); + carrier.hub = getCurrentHub(); +} + +/** + * This is for legacy reasons, and returns a proxy object instead of a hub to be used. + */ +export function getCurrentHub(): Hub { + return { + isOlderThan(_version: number): boolean { + return false; + }, + + bindClient(client: Client): void { + const scope = getCurrentScope(); + scope.setClient(client); + }, + + pushScope(): Scope { + // TODO: This does not work and is actually deprecated + return getCurrentScope(); + }, + + popScope(): boolean { + // TODO: This does not work and is actually deprecated + return false; + }, + + withScope, + getClient, + getScope: getCurrentScope, + captureException: (exception: unknown, hint?: EventHint) => { + return getCurrentScope().captureException(exception, hint); + }, + captureMessage: ( + message: string, + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel, + hint?: EventHint, + ) => { + return getCurrentScope().captureMessage(message, level, hint); + }, + captureEvent, + lastEventId, + addBreadcrumb, + setUser, + setTags, + setTag, + setExtra, + setExtras, + setContext, + // eslint-disable-next-line deprecation/deprecation + configureScope: configureScope, + + run(callback: (hub: Hub) => void): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return withScope(() => callback(this as any)); + }, + + getIntegration(integration: IntegrationClass): T | null { + return getClient().getIntegration(integration); + }, + + traceHeaders(): { [key: string]: string } { + return callExtensionMethod<{ [key: string]: string }>(this, 'traceHeaders'); + }, + + startTransaction( + _context: TransactionContext, + _customSamplingContext?: CustomSamplingContext, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): any { + // eslint-disable-next-line no-console + console.warn('startTransaction is a noop in @sentry/node-experimental. Use `startSpan` instead.'); + // We return an object here as hub.ts checks for the result of this + // and renders a different warning if this is empty + return {}; + }, + + startSession, + + endSession, + + captureSession(endSession?: boolean): void { + // both send the update and pull the session from the scope + if (endSession) { + return this.endSession(); + } + + // only send the update + _sendSessionUpdate(); + }, + + shouldSendDefaultPii(): boolean { + const client = getClient(); + const options = client.getOptions(); + return Boolean(options.sendDefaultPii); + }, + }; +} + +/** + * Replaces the current main hub with the passed one on the global object + * + * @returns The old replaced hub + */ +export function makeMain(hub: Hub): Hub { + // eslint-disable-next-line no-console + console.warn('makeMain is a noop in @sentry/node-experimental. Use `setCurrentScope` instead.'); + return hub; +} + +/** + * Sends the current Session on the scope + */ +function _sendSessionUpdate(): void { + const scope = getCurrentScope(); + const client = getClient(); + + const session = scope.getSession(); + if (session && client.captureSession) { + client.captureSession(session); + } +} + +/** + * Set a mocked hub on the current carrier. + */ +export function setLegacyHubOnCarrier(carrier: SentryCarrier): boolean { + carrier.hub = getCurrentHub(); + return true; +} diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index be4843a5d2f7..e7c6ebf72381 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,15 +1,32 @@ -import { hasTracingEnabled } from '@sentry/core'; -import type { NodeClient } from '@sentry/node'; -import { defaultIntegrations as defaultNodeIntegrations, init as initNode } from '@sentry/node'; -import { setOpenTelemetryContextAsyncContextStrategy, setupGlobalHub } from '@sentry/opentelemetry'; +import { getIntegrationsToSetup, hasTracingEnabled } from '@sentry/core'; +import { + Integrations, + defaultIntegrations as defaultNodeIntegrations, + defaultStackParser, + getSentryRelease, + isAnrChildProcess, + makeNodeTransport, +} from '@sentry/node'; import type { Integration } from '@sentry/types'; -import { parseSemver } from '@sentry/utils'; +import { + consoleSandbox, + dropUndefinedKeys, + logger, + parseSemver, + stackParserFromStackParserOptions, + tracingContextFromHeaders, +} from '@sentry/utils'; +import { DEBUG_BUILD } from '../debug-build'; import { getAutoPerformanceIntegrations } from '../integrations/getAutoPerformanceIntegrations'; import { Http } from '../integrations/http'; import { NodeFetch } from '../integrations/node-fetch'; -import type { NodeExperimentalOptions } from '../types'; +import { setOpenTelemetryContextAsyncContextStrategy } from '../otel/asyncContextStrategy'; +import type { NodeExperimentalClientOptions, NodeExperimentalOptions } from '../types'; +import { endSession, getClient, getCurrentScope, getGlobalScope, getIsolationScope, startSession } from './api'; import { NodeExperimentalClient } from './client'; +import { getGlobalCarrier } from './globals'; +import { setLegacyHubOnCarrier } from './hub'; import { initOtel } from './initOtel'; const NODE_VERSION: ReturnType = parseSemver(process.versions.node); @@ -29,24 +46,172 @@ if (NODE_VERSION.major && NODE_VERSION.major >= 16) { * Initialize Sentry for Node. */ export function init(options: NodeExperimentalOptions | undefined = {}): void { - setupGlobalHub(); + const clientOptions = getClientOptions(options); + + if (clientOptions.debug === true) { + if (DEBUG_BUILD) { + logger.enable(); + } else { + // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.'); + }); + } + } + + const scope = getCurrentScope(); + scope.update(options.initialScope); + + const client = new NodeExperimentalClient(clientOptions); + // The client is on the global scope, from where it generally is inherited + // unless somebody specifically sets a different one on a scope/isolations cope + getGlobalScope().setClient(client); + + client.setupIntegrations(); + + if (options.autoSessionTracking) { + startSessionTracking(); + } + + updateScopeFromEnvVariables(); + + if (options.spotlight) { + const client = getClient(); + if (client.addIntegration) { + // force integrations to be setup even if no DSN was set + client.setupIntegrations(true); + client.addIntegration( + new Integrations.Spotlight({ + sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, + }), + ); + } + } + + // Always init Otel, even if tracing is disabled, because we need it for trace propagation & the HTTP integration + initOtel(); + setOpenTelemetryContextAsyncContextStrategy(); +} + +function getClientOptions(options: NodeExperimentalOptions): NodeExperimentalClientOptions { + const carrier = getGlobalCarrier(); + setLegacyHubOnCarrier(carrier); const isTracingEnabled = hasTracingEnabled(options); - options.defaultIntegrations = + const autoloadedIntegrations = carrier.integrations || []; + + const fullDefaultIntegrations = options.defaultIntegrations === false ? [] : [ ...(Array.isArray(options.defaultIntegrations) ? options.defaultIntegrations : defaultIntegrations), ...(isTracingEnabled ? getAutoPerformanceIntegrations() : []), + ...autoloadedIntegrations, ]; - options.instrumenter = 'otel'; - options.clientClass = NodeExperimentalClient as unknown as typeof NodeClient; + const release = getRelease(options.release); - initNode(options); + // If there is no release, or we are in an ANR child process, we disable autoSessionTracking by default + const autoSessionTracking = + typeof release !== 'string' || isAnrChildProcess() + ? false + : options.autoSessionTracking === undefined + ? true + : options.autoSessionTracking; + // We enforce tracesSampleRate = 0 in ANR child processes + const tracesSampleRate = isAnrChildProcess() ? 0 : getTracesSampleRate(options.tracesSampleRate); - // Always init Otel, even if tracing is disabled, because we need it for trace propagation & the HTTP integration - initOtel(); - setOpenTelemetryContextAsyncContextStrategy(); + const baseOptions = dropUndefinedKeys({ + transport: makeNodeTransport, + dsn: process.env.SENTRY_DSN, + environment: process.env.SENTRY_ENVIRONMENT, + }); + + const overwriteOptions = dropUndefinedKeys({ + release, + autoSessionTracking, + tracesSampleRate, + }); + + const clientOptions: NodeExperimentalClientOptions = { + ...baseOptions, + ...options, + ...overwriteOptions, + instrumenter: 'otel', + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + integrations: getIntegrationsToSetup({ + defaultIntegrations: fullDefaultIntegrations, + integrations: options.integrations, + }), + }; + + return clientOptions; +} + +function getRelease(release: NodeExperimentalOptions['release']): string | undefined { + if (release !== undefined) { + return release; + } + + const detectedRelease = getSentryRelease(); + if (detectedRelease !== undefined) { + return detectedRelease; + } + + return undefined; +} + +function getTracesSampleRate(tracesSampleRate: NodeExperimentalOptions['tracesSampleRate']): number | undefined { + if (tracesSampleRate !== undefined) { + return tracesSampleRate; + } + + const sampleRateFromEnv = process.env.SENTRY_TRACES_SAMPLE_RATE; + if (!sampleRateFromEnv) { + return undefined; + } + + const parsed = parseFloat(sampleRateFromEnv); + return isFinite(parsed) ? parsed : undefined; +} + +/** + * Update scope and propagation context based on environmental variables. + * + * See https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md + * for more details. + */ +function updateScopeFromEnvVariables(): void { + const sentryUseEnvironment = (process.env.SENTRY_USE_ENVIRONMENT || '').toLowerCase(); + if (!['false', 'n', 'no', 'off', '0'].includes(sentryUseEnvironment)) { + const sentryTraceEnv = process.env.SENTRY_TRACE; + const baggageEnv = process.env.SENTRY_BAGGAGE; + const { propagationContext } = tracingContextFromHeaders(sentryTraceEnv, baggageEnv); + getCurrentScope().setPropagationContext(propagationContext); + } +} + +/** + * Enable automatic Session Tracking for the node process. + */ +function startSessionTracking(): void { + startSession(); + + // Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because + // The 'beforeExit' event is not emitted for conditions causing explicit termination, + // such as calling process.exit() or uncaught exceptions. + // Ref: https://nodejs.org/api/process.html#process_event_beforeexit + process.on('beforeExit', () => { + const session = getIsolationScope().getSession(); + + // Only call endSession, if the Session exists on Scope and SessionStatus is not a + // Terminal Status i.e. Exited or Crashed because + // "When a session is moved away from ok it must not be updated anymore." + // Ref: https://develop.sentry.dev/sdk/sessions/ + if (session && session.status !== 'ok') { + endSession(); + } + }); } diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index 53f319d313b6..1a078a1c013a 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -1,20 +1,15 @@ import { DiagLogLevel, diag } from '@opentelemetry/api'; -import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import { Resource } from '@opentelemetry/resources'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; -import { - SentryPropagator, - SentrySampler, - getClient, - setupEventContextTrace, - wrapContextManagerClass, -} from '@sentry/opentelemetry'; +import { SentryPropagator, SentrySampler, setupEventContextTrace } from '@sentry/opentelemetry'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { SentryContextManager } from '../otel/contextManager'; import type { NodeExperimentalClient } from '../types'; +import { getClient } from './api'; import { NodeExperimentalSentrySpanProcessor } from './spanProcessor'; /** @@ -62,8 +57,6 @@ export function setupOtel(client: NodeExperimentalClient): BasicTracerProvider { }); provider.addSpanProcessor(new NodeExperimentalSentrySpanProcessor()); - const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); - // Initialize the provider provider.register({ propagator: new SentryPropagator(), diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts new file mode 100644 index 000000000000..f3195c7141b6 --- /dev/null +++ b/packages/node-experimental/src/sdk/scope.ts @@ -0,0 +1,406 @@ +import { notifyEventProcessors } from '@sentry/core'; +import { OpenTelemetryScope } from '@sentry/opentelemetry'; +import type { + Attachment, + Breadcrumb, + Client, + Event, + EventHint, + EventProcessor, + Severity, + SeverityLevel, +} from '@sentry/types'; +import { uuid4 } from '@sentry/utils'; + +import { getGlobalCarrier } from './globals'; +import type { CurrentScopes, Scope as ScopeInterface, ScopeData, SentryCarrier } from './types'; + +/** Get the current scope. */ +export function getCurrentScope(): Scope { + return getScopes().scope as Scope; +} + +/** + * Set the current scope on the execution context. + * This should mostly only be called in Sentry.init() + */ +export function setCurrentScope(scope: Scope): void { + getScopes().scope = scope; +} + +/** Get the global scope. */ +export function getGlobalScope(): Scope { + const carrier = getGlobalCarrier(); + + if (!carrier.globalScope) { + carrier.globalScope = new Scope(); + } + + return carrier.globalScope as Scope; +} + +/** Get the currently active isolation scope. */ +export function getIsolationScope(): Scope { + return getScopes().isolationScope as Scope; +} + +/** + * Set the currently active isolation scope. + * Use this with caution! As it updates the isolation scope for the current execution context. + */ +export function setIsolationScope(isolationScope: Scope): void { + getScopes().isolationScope = isolationScope; +} + +/** Get the currently active client. */ +export function getClient(): C { + const currentScope = getCurrentScope(); + const isolationScope = getIsolationScope(); + const globalScope = getGlobalScope(); + + const client = currentScope.getClient() || isolationScope.getClient() || globalScope.getClient(); + if (client) { + return client as C; + } + + // TODO otherwise ensure we use a noop client + return {} as C; +} + +/** A fork of the classic scope with some otel specific stuff. */ +export class Scope extends OpenTelemetryScope implements ScopeInterface { + // Overwrite this if you want to use a specific isolation scope here + public isolationScope: Scope | undefined; + + protected _client: Client | undefined; + + protected _lastEventId: string | undefined; + + /** + * @inheritDoc + */ + public clone(): Scope { + const newScope = new Scope(); + 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; + } + + /** Update the client on the scope. */ + public setClient(client: Client): void { + this._client = client; + } + + /** + * Get the client assigned to this scope. + * Should generally not be used by users - use top-level `Sentry.getClient()` instead! + * @internal + */ + public getClient(): Client | undefined { + return this._client; + } + + /** @inheritdoc */ + public getAttachments(): Attachment[] { + const data = getGlobalScope().getScopeData(); + const isolationScopeData = this._getIsolationScope().getScopeData(); + const scopeData = this.getScopeData(); + + // Merge data together, in order + mergeData(data, isolationScopeData); + mergeData(data, scopeData); + + return data.attachments; + } + + /** Capture an exception for this scope. */ + public captureException(exception: unknown, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + const syntheticException = new Error('Sentry syntheticException'); + + getClient().captureException( + exception, + { + originalException: exception, + syntheticException, + ...hint, + event_id: eventId, + }, + this, + ); + + this._lastEventId = eventId; + + return eventId; + } + + /** Capture a message for this scope. */ + public captureMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel, + hint?: EventHint, + ): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + const syntheticException = new Error(message); + + getClient().captureMessage( + message, + level, + { + originalException: message, + syntheticException, + ...hint, + event_id: eventId, + }, + this, + ); + + this._lastEventId = eventId; + + return eventId; + } + + /** Capture a message for this scope. */ + public captureEvent(event: Event, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + if (!event.type) { + this._lastEventId = eventId; + } + + getClient().captureEvent(event, { ...hint, event_id: eventId }, this); + + return eventId; + } + + /** Get the ID of the last sent error event. */ + public lastEventId(): string | undefined { + return this._lastEventId; + } + + /** + * @inheritDoc + */ + public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { + return this._addBreadcrumb(breadcrumb, maxBreadcrumbs); + } + + /** Get all relevant data for this scope. */ + public getScopeData(): ScopeData { + const { + _breadcrumbs, + _attachments, + _contexts, + _tags, + _extra, + _user, + _level, + _fingerprint, + _eventProcessors, + _propagationContext, + _sdkProcessingMetadata, + } = this; + + return { + breadcrumbs: _breadcrumbs, + attachments: _attachments, + contexts: _contexts, + tags: _tags, + extra: _extra, + user: _user, + level: _level, + fingerprint: _fingerprint || [], + eventProcessors: _eventProcessors, + propagationContext: _propagationContext, + sdkProcessingMetadata: _sdkProcessingMetadata, + }; + } + + /** + * Applies data from the scope to the event and runs all event processors on it. + * + * @param event Event + * @param hint Object containing additional information about the original exception, for use by the event processors. + * @hidden + */ + public applyToEvent( + event: Event, + hint: EventHint = {}, + additionalEventProcessors: EventProcessor[] = [], + ): PromiseLike { + const data = getGlobalScope().getScopeData(); + const isolationScopeData = this._getIsolationScope().getScopeData(); + const scopeData = this.getScopeData(); + + // Merge data together, in order + mergeData(data, isolationScopeData); + mergeData(data, scopeData); + + // Apply the data + const { extra, tags, user, contexts, level, sdkProcessingMetadata, breadcrumbs, fingerprint, eventProcessors } = + data; + + mergePropKeep(event, 'extra', extra); + mergePropKeep(event, 'tags', tags); + mergePropKeep(event, 'user', user); + mergePropKeep(event, 'contexts', contexts); + mergePropKeep(event, 'sdkProcessingMetadata', sdkProcessingMetadata); + event.sdkProcessingMetadata = { + ...event.sdkProcessingMetadata, + propagationContext: this._propagationContext, + }; + + mergeArray(event, 'breadcrumbs', breadcrumbs); + mergeArray(event, 'fingerprint', fingerprint); + + if (level) { + event.level = level; + } + + const allEventProcessors = [...additionalEventProcessors, ...eventProcessors]; + + // Apply additional things to the event + if (this._transactionName) { + event.transaction = this._transactionName; + } + + return notifyEventProcessors(allEventProcessors, event, hint); + } + + /** + * Get all breadcrumbs attached to this scope. + * @internal + */ + public getBreadcrumbs(): Breadcrumb[] { + return this._breadcrumbs; + } + + /** Get the isolation scope for this scope. */ + protected _getIsolationScope(): Scope { + return this.isolationScope || getIsolationScope(); + } +} + +/** Exported only for tests */ +export function mergeData(data: ScopeData, mergeData: ScopeData): void { + const { + extra, + tags, + user, + contexts, + level, + sdkProcessingMetadata, + breadcrumbs, + fingerprint, + eventProcessors, + attachments, + } = mergeData; + + mergePropOverwrite(data, 'extra', extra); + mergePropOverwrite(data, 'tags', tags); + mergePropOverwrite(data, 'user', user); + mergePropOverwrite(data, 'contexts', contexts); + mergePropOverwrite(data, 'sdkProcessingMetadata', sdkProcessingMetadata); + + if (level) { + data.level = level; + } + + if (breadcrumbs.length) { + data.breadcrumbs = [...data.breadcrumbs, ...breadcrumbs]; + } + + if (fingerprint.length) { + data.fingerprint = [...data.fingerprint, ...fingerprint]; + } + + if (eventProcessors.length) { + data.eventProcessors = [...data.eventProcessors, ...eventProcessors]; + } + + if (attachments.length) { + data.attachments = [...data.attachments, ...attachments]; + } +} + +/** + * Merge properties, overwriting existing keys. + * Exported only for tests. + */ +export function mergePropOverwrite< + Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', + Data extends ScopeData | Event, +>(data: Data, prop: Prop, mergeVal: Data[Prop]): void { + if (mergeVal && Object.keys(mergeVal).length) { + data[prop] = { ...data[prop], ...mergeVal }; + } +} + +/** + * Merge properties, keeping existing keys. + * Exported only for tests. + */ +export function mergePropKeep< + Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', + Data extends ScopeData | Event, +>(data: Data, prop: Prop, mergeVal: Data[Prop]): void { + if (mergeVal && Object.keys(mergeVal).length) { + data[prop] = { ...mergeVal, ...data[prop] }; + } +} + +/** Exported only for tests */ +export function mergeArray( + event: Event, + prop: Prop, + mergeVal: ScopeData[Prop], +): void { + const prevVal = event[prop]; + // If we are not merging any new values, + // we only need to proceed if there was an empty array before (as we want to replace it with undefined) + if (!mergeVal.length && (!prevVal || prevVal.length)) { + return; + } + + const merged = [...(prevVal || []), ...mergeVal] as ScopeData[Prop]; + event[prop] = merged.length ? merged : undefined; +} + +function getScopes(): CurrentScopes { + const carrier = getGlobalCarrier(); + + if (carrier.acs && carrier.acs.getScopes) { + const scopes = carrier.acs.getScopes(); + + if (scopes) { + return scopes; + } + } + + return getGlobalCurrentScopes(carrier); +} + +function getGlobalCurrentScopes(carrier: SentryCarrier): CurrentScopes { + if (!carrier.scopes) { + carrier.scopes = { + scope: new Scope(), + isolationScope: new Scope(), + }; + } + + return carrier.scopes; +} diff --git a/packages/node-experimental/src/sdk/spanProcessor.ts b/packages/node-experimental/src/sdk/spanProcessor.ts index 067e1568e90f..175f3681479b 100644 --- a/packages/node-experimental/src/sdk/spanProcessor.ts +++ b/packages/node-experimental/src/sdk/spanProcessor.ts @@ -1,16 +1,35 @@ +import type { Context } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { Span } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { SentrySpanProcessor, getClient } from '@sentry/opentelemetry'; +import { SentrySpanProcessor, getClient, getSpanFinishScope } from '@sentry/opentelemetry'; import { Http } from '../integrations/http'; import { NodeFetch } from '../integrations/node-fetch'; import type { NodeExperimentalClient } from '../types'; +import { getIsolationScope } from './api'; +import { Scope } from './scope'; /** * Implement custom code to avoid sending spans in certain cases. */ export class NodeExperimentalSentrySpanProcessor extends SentrySpanProcessor { + public constructor() { + super({ scopeClass: Scope }); + } + + /** @inheritDoc */ + public onStart(span: Span, parentContext: Context): void { + super.onStart(span, parentContext); + + // We need to make sure that we use the correct isolation scope when finishing the span + // so we store it on the span finish scope for later use + const scope = getSpanFinishScope(span) as Scope | undefined; + if (scope) { + scope.isolationScope = getIsolationScope(); + } + } + /** @inheritDoc */ protected _shouldSendSpanToSentry(span: Span): boolean { const client = getClient(); diff --git a/packages/node-experimental/src/sdk/types.ts b/packages/node-experimental/src/sdk/types.ts new file mode 100644 index 000000000000..773c404d65ce --- /dev/null +++ b/packages/node-experimental/src/sdk/types.ts @@ -0,0 +1,91 @@ +import type { + Attachment, + Breadcrumb, + Client, + Contexts, + Event, + EventHint, + EventProcessor, + Extras, + Hub, + Integration, + Primitive, + PropagationContext, + Scope as BaseScope, + Severity, + SeverityLevel, + User, +} from '@sentry/types'; + +export interface ScopeData { + eventProcessors: EventProcessor[]; + breadcrumbs: Breadcrumb[]; + user: User; + tags: { [key: string]: Primitive }; + extra: Extras; + contexts: Contexts; + attachments: Attachment[]; + propagationContext: PropagationContext; + sdkProcessingMetadata: { [key: string]: unknown }; + fingerprint: string[]; + level?: SeverityLevel; +} + +export interface Scope extends BaseScope { + // @ts-expect-error typeof this is what we want here + isolationScope: typeof this | undefined; + // @ts-expect-error typeof this is what we want here + clone(scope?: Scope): typeof this; + setClient(client: Client): void; + getClient(): Client | undefined; + captureException(exception: unknown, hint?: EventHint): string; + captureMessage( + message: string, + // eslint-disable-next-line deprecation/deprecation + level?: Severity | SeverityLevel, + hint?: EventHint, + ): string; + captureEvent(event: Event, hint?: EventHint): string; + lastEventId(): string | undefined; + getScopeData(): ScopeData; +} + +export interface CurrentScopes { + scope: Scope; + isolationScope: Scope; +} + +/** + * Strategy used to track async context. + */ +export interface AsyncContextStrategy { + /** + * Gets the current async context. Returns undefined if there is no current async context. + */ + getScopes: () => CurrentScopes | undefined; + + /** This is here for legacy reasons. */ + getCurrentHub: () => Hub; + + /** + * Runs the supplied callback in its own async context. + */ + runWithAsyncContext(callback: () => T): T; +} + +export interface SentryCarrier { + globalScope?: Scope; + scopes?: CurrentScopes; + acs?: AsyncContextStrategy; + + // hub is here for legacy reasons + hub?: Hub; + + extensions?: { + /** Extension methods for the hub, which are bound to the current Hub instance */ + // eslint-disable-next-line @typescript-eslint/ban-types + [key: string]: Function; + }; + + integrations?: Integration[]; +} diff --git a/packages/node-experimental/src/utils/contextData.ts b/packages/node-experimental/src/utils/contextData.ts new file mode 100644 index 000000000000..5c69f186eb6d --- /dev/null +++ b/packages/node-experimental/src/utils/contextData.ts @@ -0,0 +1,22 @@ +import type { Context } from '@opentelemetry/api'; +import { createContextKey } from '@opentelemetry/api'; + +import type { CurrentScopes } from '../sdk/types'; + +export const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes'); + +/** + * Try to get the current scopes from the given OTEL context. + * This requires a Context Manager that was wrapped with getWrappedContextManager. + */ +export function getScopesFromContext(context: Context): CurrentScopes | undefined { + return context.getValue(SENTRY_SCOPES_CONTEXT_KEY) as CurrentScopes | undefined; +} + +/** + * Set the current scopes on an OTEL context. + * This will return a forked context with the Propagation Context set. + */ +export function setScopesOnContext(context: Context, scopes: CurrentScopes): Context { + return context.setValue(SENTRY_SCOPES_CONTEXT_KEY, scopes); +} diff --git a/packages/node-experimental/src/utils/prepareEvent.ts b/packages/node-experimental/src/utils/prepareEvent.ts new file mode 100644 index 000000000000..db89c2b198c0 --- /dev/null +++ b/packages/node-experimental/src/utils/prepareEvent.ts @@ -0,0 +1,58 @@ +import { Scope } from '@sentry/core'; +import type { CaptureContext, EventHint, Scope as ScopeInterface, ScopeContext } from '@sentry/types'; + +/** + * This type makes sure that we get either a CaptureContext, OR an EventHint. + * It does not allow mixing them, which could lead to unexpected outcomes, e.g. this is disallowed: + * { user: { id: '123' }, mechanism: { handled: false } } + */ +export type ExclusiveEventHintOrCaptureContext = + | (CaptureContext & Partial<{ [key in keyof EventHint]: never }>) + | (EventHint & Partial<{ [key in keyof ScopeContext]: never }>); + +/** + * 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`. + */ +export function parseEventHintOrCaptureContext( + hint: ExclusiveEventHintOrCaptureContext | undefined, +): EventHint | undefined { + if (!hint) { + return undefined; + } + + // If you pass a Scope or `() => Scope` as CaptureContext, we just return this as captureContext + if (hintIsScopeOrFunction(hint)) { + return { captureContext: hint }; + } + + if (hintIsScopeContext(hint)) { + return { + captureContext: hint, + }; + } + + return hint; +} + +function hintIsScopeOrFunction( + hint: CaptureContext | EventHint, +): hint is ScopeInterface | ((scope: ScopeInterface) => ScopeInterface) { + return hint instanceof Scope || typeof hint === 'function'; +} + +type ScopeContextProperty = keyof ScopeContext; +const captureContextKeys: readonly ScopeContextProperty[] = [ + 'user', + 'level', + 'extra', + 'contexts', + 'tags', + 'fingerprint', + 'requestSession', + 'propagationContext', +] as const; + +function hintIsScopeContext(hint: Partial | EventHint): hint is Partial { + return Object.keys(hint).some(key => captureContextKeys.includes(key as ScopeContextProperty)); +} diff --git a/packages/node-experimental/test/helpers/mockSdkInit.ts b/packages/node-experimental/test/helpers/mockSdkInit.ts index 82752ab203d0..9cc7692463d5 100644 --- a/packages/node-experimental/test/helpers/mockSdkInit.ts +++ b/packages/node-experimental/test/helpers/mockSdkInit.ts @@ -7,14 +7,17 @@ import type { NodeExperimentalClientOptions } from '../../src/types'; const PUBLIC_DSN = 'https://username@domain/123'; -export function mockSdkInit(options?: Partial) { +export function resetGlobals(): void { GLOBAL_OBJ.__SENTRY__ = { extensions: {}, hub: undefined, globalEventProcessors: [], logger: undefined, }; +} +export function mockSdkInit(options?: Partial) { + resetGlobals(); init({ dsn: PUBLIC_DSN, defaultIntegrations: false, ...options }); } diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node-experimental/test/integration/breadcrumbs.test.ts index 80842451c3bf..fea78a353011 100644 --- a/packages/node-experimental/test/integration/breadcrumbs.test.ts +++ b/packages/node-experimental/test/integration/breadcrumbs.test.ts @@ -1,5 +1,6 @@ -import { withScope } from '@sentry/core'; +import { captureException, withScope } from '@sentry/core'; import { getCurrentHub, startSpan } from '@sentry/opentelemetry'; +import { addBreadcrumb, getClient, withIsolationScope } from '../../src/sdk/api'; import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; @@ -55,24 +56,23 @@ describe('Integration | breadcrumbs', () => { mockSdkInit({ beforeSend, beforeBreadcrumb }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = getClient(); const error = new Error('test'); - hub.addBreadcrumb({ timestamp: 123456, message: 'test0' }); + addBreadcrumb({ timestamp: 123456, message: 'test0' }); - withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + withIsolationScope(() => { + addBreadcrumb({ timestamp: 123456, message: 'test1' }); }); - withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test2' }); - hub.captureException(error); + withIsolationScope(() => { + addBreadcrumb({ timestamp: 123456, message: 'test2' }); + captureException(error); }); - withScope(() => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test3' }); + withIsolationScope(() => { + addBreadcrumb({ timestamp: 123456, message: 'test3' }); }); await client.flush(); @@ -142,7 +142,7 @@ describe('Integration | breadcrumbs', () => { ); }); - it('correctly adds & retrieves breadcrumbs for the current root span only', async () => { + it('correctly adds & retrieves breadcrumbs for the current isolation span only', async () => { const beforeSend = jest.fn(() => null); const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); @@ -153,22 +153,26 @@ describe('Integration | breadcrumbs', () => { const error = new Error('test'); - startSpan({ name: 'test1' }, () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); + withIsolationScope(() => { + startSpan({ name: 'test1' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); - startSpan({ name: 'inner1' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); + startSpan({ name: 'inner1' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); + }); }); }); - startSpan({ name: 'test2' }, () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); + withIsolationScope(() => { + startSpan({ name: 'test2' }, () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); - startSpan({ name: 'inner2' }, () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); - }); + startSpan({ name: 'inner2' }, () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); - hub.captureException(error); + hub.captureException(error); + }); }); await client.flush(); @@ -303,31 +307,35 @@ describe('Integration | breadcrumbs', () => { const error = new Error('test'); - const promise1 = startSpan({ name: 'test' }, async () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); + const promise1 = withIsolationScope(async () => { + await startSpan({ name: 'test' }, async () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1' }); - await startSpan({ name: 'inner1' }, async () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); - }); + await startSpan({ name: 'inner1' }, async () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); - await startSpan({ name: 'inner2' }, async () => { - hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); - }); + await startSpan({ name: 'inner2' }, async () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise(resolve => setTimeout(resolve, 10)); - hub.captureException(error); + hub.captureException(error); + }); }); - const promise2 = startSpan({ name: 'test-b' }, async () => { - hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); + const promise2 = withIsolationScope(async () => { + await startSpan({ name: 'test-b' }, async () => { + hub.addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); - await startSpan({ name: 'inner1b' }, async () => { - hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); - }); + await startSpan({ name: 'inner1b' }, async () => { + hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); - await startSpan({ name: 'inner2b' }, async () => { - hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); + await startSpan({ name: 'inner2b' }, async () => { + hub.addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); + }); }); }); diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node-experimental/test/integration/scope.test.ts index 78579701e47e..57be6126bcae 100644 --- a/packages/node-experimental/test/integration/scope.test.ts +++ b/packages/node-experimental/test/integration/scope.test.ts @@ -2,7 +2,7 @@ import { getCurrentHub, getSpanScope } from '@sentry/opentelemetry'; import * as Sentry from '../../src/'; import type { NodeExperimentalClient } from '../../src/types'; -import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; +import { cleanupOtel, mockSdkInit, resetGlobals } from '../helpers/mockSdkInit'; describe('Integration | Scope', () => { afterEach(() => { @@ -101,6 +101,7 @@ describe('Integration | Scope', () => { tag1: 'val1', tag2: 'val2', tag3: 'val3', + tag4: 'val4', }, timestamp: expect.any(Number), transaction: 'outer', @@ -226,4 +227,459 @@ describe('Integration | Scope', () => { } }); }); + + describe('global scope', () => { + beforeEach(() => { + resetGlobals(); + }); + + it('works before calling init', () => { + const globalScope = Sentry.getGlobalScope(); + expect(globalScope).toBeDefined(); + expect(globalScope).toBeInstanceOf(Sentry.Scope); + // No client attached + expect(globalScope.getClient()).toBeUndefined(); + // Repeatedly returns the same instance + expect(Sentry.getGlobalScope()).toBe(globalScope); + + globalScope.setTag('tag1', 'val1'); + globalScope.setTag('tag2', 'val2'); + + expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + // Now when we call init, the global scope remains intact + Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false }); + + expect(globalScope.getClient()).toBeDefined(); + expect(Sentry.getGlobalScope()).toBe(globalScope); + expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + + it('is applied to events', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const globalScope = Sentry.getGlobalScope(); + globalScope.setTag('tag1', 'val1'); + globalScope.setTag('tag2', 'val2'); + + const error = new Error('test error'); + Sentry.captureException(error); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + describe('isolation scope', () => { + beforeEach(() => { + resetGlobals(); + }); + + it('works before calling init', () => { + const isolationScope = Sentry.getIsolationScope(); + expect(isolationScope).toBeDefined(); + expect(isolationScope).toBeInstanceOf(Sentry.Scope); + // No client attached + expect(isolationScope.getClient()).toBeUndefined(); + // Repeatedly returns the same instance + expect(Sentry.getIsolationScope()).toBe(isolationScope); + + isolationScope.setTag('tag1', 'val1'); + isolationScope.setTag('tag2', 'val2'); + + expect(isolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + // Now when we call init, the isolation scope remains intact + Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false }); + + // client is only attached to global scope by default + expect(isolationScope.getClient()).toBeUndefined(); + expect(Sentry.getIsolationScope()).toBe(isolationScope); + expect(isolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + + it('is applied to events', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const isolationScope = Sentry.getIsolationScope(); + isolationScope.setTag('tag1', 'val1'); + isolationScope.setTag('tag2', 'val2'); + + const error = new Error('test error'); + Sentry.captureException(error); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('withIsolationScope works', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const initialIsolationScope = Sentry.getIsolationScope(); + initialIsolationScope.setTag('tag1', 'val1'); + initialIsolationScope.setTag('tag2', 'val2'); + + const initialCurrentScope = Sentry.getCurrentScope(); + + const error = new Error('test error'); + + Sentry.withIsolationScope(newIsolationScope => { + newIsolationScope.setTag('tag4', 'val4'); + }); + + Sentry.withIsolationScope(newIsolationScope => { + expect(Sentry.getCurrentScope()).not.toBe(initialCurrentScope); + expect(Sentry.getIsolationScope()).toBe(newIsolationScope); + expect(newIsolationScope).not.toBe(initialIsolationScope); + + // Data is forked off original isolation scope + expect(newIsolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + newIsolationScope.setTag('tag3', 'val3'); + + Sentry.captureException(error); + }); + + expect(initialIsolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('can be deeply nested', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const initialIsolationScope = Sentry.getIsolationScope(); + initialIsolationScope.setTag('tag1', 'val1'); + + const error = new Error('test error'); + + Sentry.withIsolationScope(newIsolationScope => { + newIsolationScope.setTag('tag2', 'val2'); + + Sentry.withIsolationScope(newIsolationScope => { + newIsolationScope.setTag('tag3', 'val3'); + + Sentry.withIsolationScope(newIsolationScope => { + newIsolationScope.setTag('tag4', 'val4'); + }); + + Sentry.captureException(error); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + describe('current scope', () => { + beforeEach(() => { + resetGlobals(); + }); + + it('works before calling init', () => { + const currentScope = Sentry.getCurrentScope(); + expect(currentScope).toBeDefined(); + expect(currentScope).toBeInstanceOf(Sentry.Scope); + // No client attached + expect(currentScope.getClient()).toBeUndefined(); + // Repeatedly returns the same instance + expect(Sentry.getCurrentScope()).toBe(currentScope); + + currentScope.setTag('tag1', 'val1'); + currentScope.setTag('tag2', 'val2'); + + expect(currentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + // Now when we call init, the current scope remains intact + Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false }); + + // client is only attached to global scope by default + expect(currentScope.getClient()).toBeUndefined(); + // current scope remains intact + expect(Sentry.getCurrentScope()).toBe(currentScope); + expect(currentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + + it('is applied to events', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const currentScope = Sentry.getCurrentScope(); + currentScope.setTag('tag1', 'val1'); + currentScope.setTag('tag2', 'val2'); + + const error = new Error('test error'); + Sentry.captureException(error); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('withScope works', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const isolationScope = Sentry.getIsolationScope(); + const initialCurrentScope = Sentry.getCurrentScope(); + initialCurrentScope.setTag('tag1', 'val1'); + initialCurrentScope.setTag('tag2', 'val2'); + + const error = new Error('test error'); + + Sentry.withScope(newCurrentScope => { + newCurrentScope.setTag('tag4', 'val4'); + }); + + Sentry.withScope(newCurrentScope => { + expect(Sentry.getCurrentScope()).toBe(newCurrentScope); + expect(Sentry.getIsolationScope()).toBe(isolationScope); + expect(newCurrentScope).not.toBe(initialCurrentScope); + + // Data is forked off original isolation scope + expect(newCurrentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + newCurrentScope.setTag('tag3', 'val3'); + + Sentry.captureException(error); + }); + + expect(initialCurrentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('can be deeply nested', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const initialCurrentScope = Sentry.getCurrentScope(); + initialCurrentScope.setTag('tag1', 'val1'); + + const error = new Error('test error'); + + Sentry.withScope(currentScope => { + currentScope.setTag('tag2', 'val2'); + expect(Sentry.getCurrentScope()).toBe(currentScope); + + Sentry.withScope(currentScope => { + currentScope.setTag('tag3', 'val3'); + expect(Sentry.getCurrentScope()).toBe(currentScope); + + Sentry.withScope(currentScope => { + currentScope.setTag('tag4', 'val4'); + expect(Sentry.getCurrentScope()).toBe(currentScope); + }); + + Sentry.captureException(error); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('automatically forks with OTEL context', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const initialCurrentScope = Sentry.getCurrentScope(); + initialCurrentScope.setTag('tag1', 'val1'); + + const error = new Error('test error'); + + Sentry.startSpan({ name: 'outer' }, () => { + Sentry.getCurrentScope().setTag('tag2', 'val2'); + + Sentry.startSpan({ name: 'inner 1' }, () => { + Sentry.getCurrentScope().setTag('tag3', 'val3'); + + Sentry.startSpan({ name: 'inner 2' }, () => { + Sentry.getCurrentScope().setTag('tag4', 'val4'); + }); + + Sentry.captureException(error); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + describe('scope merging', () => { + beforeEach(() => { + resetGlobals(); + }); + + it('merges data from global, isolation and current scope', async () => { + const beforeSend = jest.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + Sentry.getGlobalScope().setTag('tag1', 'val1'); + + const error = new Error('test error'); + + Sentry.withIsolationScope(isolationScope => { + Sentry.getCurrentScope().setTag('tag2', 'val2a'); + isolationScope.setTag('tag2', 'val2b'); + isolationScope.setTag('tag3', 'val3'); + + Sentry.withScope(currentScope => { + currentScope.setTag('tag4', 'val4'); + + Sentry.captureException(error); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2a', + tag3: 'val3', + tag4: 'val4', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); }); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 107377c9a633..1a09b3234d92 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -8,6 +8,7 @@ import { logger } from '@sentry/utils'; import * as Sentry from '../../src'; import { startSpan } from '../../src'; import type { Http, NodeFetch } from '../../src/integrations'; +import { getIsolationScope } from '../../src/sdk/api'; import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; @@ -22,8 +23,7 @@ describe('Integration | Transactions', () => { mockSdkInit({ enableTracing: true, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = Sentry.getClient(); Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); Sentry.setTag('outer.tag', 'test value'); @@ -128,6 +128,7 @@ describe('Integration | Transactions', () => { start_timestamp: expect.any(Number), tags: { 'outer.tag': 'test value', + 'test.tag': 'test value', }, timestamp: expect.any(Number), transaction: 'test name', @@ -176,49 +177,52 @@ describe('Integration | Transactions', () => { mockSdkInit({ enableTracing: true, beforeSendTransaction }); - const hub = getCurrentHub(); - const client = hub.getClient() as NodeExperimentalClient; + const client = Sentry.getClient(); Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); - Sentry.startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { - Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + Sentry.withIsolationScope(() => { + Sentry.startSpan({ op: 'test op', name: 'test name', source: 'task', origin: 'auto.test' }, span => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); - span.setAttributes({ - 'test.outer': 'test value', - }); + span.setAttributes({ + 'test.outer': 'test value', + }); - const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); - Sentry.setTag('test.tag', 'test value'); + Sentry.setTag('test.tag', 'test value'); - Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { - Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); - innerSpan.setAttributes({ - 'test.inner': 'test value', + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); }); }); }); - Sentry.startSpan({ op: 'test op b', name: 'test name b' }, span => { - Sentry.addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 }); + Sentry.withIsolationScope(() => { + Sentry.startSpan({ op: 'test op b', name: 'test name b' }, span => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 }); - span.setAttributes({ - 'test.outer': 'test value b', - }); + span.setAttributes({ + 'test.outer': 'test value b', + }); - const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1b' }); - subSpan.end(); + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1b' }); + subSpan.end(); - Sentry.setTag('test.tag', 'test value b'); + Sentry.setTag('test.tag', 'test value b'); - Sentry.startSpan({ name: 'inner span 2b' }, innerSpan => { - Sentry.addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 }); + Sentry.startSpan({ name: 'inner span 2b' }, innerSpan => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 }); - innerSpan.setAttributes({ - 'test.inner': 'test value b', + innerSpan.setAttributes({ + 'test.inner': 'test value b', + }); }); }); }); @@ -257,7 +261,7 @@ describe('Integration | Transactions', () => { }), ], start_timestamp: expect.any(Number), - tags: {}, + tags: { 'test.tag': 'test value' }, timestamp: expect.any(Number), transaction: 'test name', transaction_info: { source: 'task' }, @@ -299,7 +303,7 @@ describe('Integration | Transactions', () => { }), ], start_timestamp: expect.any(Number), - tags: {}, + tags: { 'test.tag': 'test value b' }, timestamp: expect.any(Number), transaction: 'test name b', transaction_info: { source: 'custom' }, diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts new file mode 100644 index 000000000000..a0e179373626 --- /dev/null +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -0,0 +1,416 @@ +import type { Attachment, Breadcrumb, Client, EventProcessor } from '@sentry/types'; +import { Scope, getIsolationScope } from '../../src'; +import { getGlobalScope, mergeArray, mergeData, mergePropKeep, mergePropOverwrite } from '../../src/sdk/scope'; +import type { ScopeData } from '../../src/sdk/types'; +import { mockSdkInit, resetGlobals } from '../helpers/mockSdkInit'; + +describe('Unit | Scope', () => { + it('allows to create & update a scope', () => { + const scope = new Scope(); + + expect(scope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: {}, + extra: {}, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + + scope.update({ + tags: { foo: 'bar' }, + extra: { foo2: 'bar2' }, + }); + + expect(scope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: { + foo: 'bar', + }, + extra: { + foo2: 'bar2', + }, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + }); + + it('allows to clone a scope', () => { + const scope = new Scope(); + + scope.update({ + tags: { foo: 'bar' }, + extra: { foo2: 'bar2' }, + }); + + const newScope = scope.clone(); + expect(newScope).toBeInstanceOf(Scope); + expect(newScope).not.toBe(scope); + + expect(newScope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: { + foo: 'bar', + }, + extra: { + foo2: 'bar2', + }, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + }); + + it('allows to set & get a client', () => { + const scope = new Scope(); + expect(scope.getClient()).toBeUndefined(); + const client = {} as Client; + scope.setClient(client); + expect(scope.getClient()).toBe(client); + }); + + it('gets the correct isolationScope in _getIsolationScope', () => { + resetGlobals(); + + const scope = new Scope(); + const globalIsolationScope = getIsolationScope(); + + expect(scope['_getIsolationScope']()).toBe(globalIsolationScope); + + const customIsolationScope = new Scope(); + scope.isolationScope = customIsolationScope; + + expect(scope['_getIsolationScope']()).toBe(customIsolationScope); + }); + + describe('mergeArray', () => { + it.each([ + [[], [], undefined], + [undefined, [], undefined], + [['a'], [], ['a']], + [['a'], ['b', 'c'], ['a', 'b', 'c']], + [[], ['b', 'c'], ['b', 'c']], + [undefined, ['b', 'c'], ['b', 'c']], + ])('works with %s and %s', (a, b, expected) => { + const data = { fingerprint: a }; + mergeArray(data, 'fingerprint', b); + expect(data.fingerprint).toEqual(expected); + }); + + it('does not mutate the original array if no changes are made', () => { + const fingerprint = ['a']; + const data = { fingerprint }; + mergeArray(data, 'fingerprint', []); + expect(data.fingerprint).toBe(fingerprint); + }); + }); + + describe('mergePropKeep', () => { + it.each([ + [{}, {}, {}], + [{ a: 'aa' }, {}, { a: 'aa' }], + [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], + // Does not overwrite existing keys + [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'aa', b: 'bb' }], + ])('works with %s and %s', (a, b, expected) => { + const data = { tags: a } as unknown as ScopeData; + mergePropKeep(data, 'tags', b); + expect(data.tags).toEqual(expected); + }); + + it('does not deep merge', () => { + const data = { + contexts: { + app: { app_version: 'v1' }, + culture: { display_name: 'name1' }, + }, + } as unknown as ScopeData; + mergePropKeep(data, 'contexts', { + os: { name: 'os1' }, + app: { app_name: 'name1' }, + }); + expect(data.contexts).toEqual({ + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + app: { app_version: 'v1' }, + }); + }); + + it('does not mutate the original object if no changes are made', () => { + const tags = { a: 'aa' }; + const data = { tags } as unknown as ScopeData; + mergePropKeep(data, 'tags', {}); + expect(data.tags).toBe(tags); + }); + }); + + describe('mergePropOverwrite', () => { + it.each([ + [{}, {}, {}], + [{ a: 'aa' }, {}, { a: 'aa' }], + [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], + // overwrites existing keys + [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'cc', b: 'bb' }], + ])('works with %s and %s', (a, b, expected) => { + const data = { tags: a } as unknown as ScopeData; + mergePropOverwrite(data, 'tags', b); + expect(data.tags).toEqual(expected); + }); + + it('does not deep merge', () => { + const data = { + contexts: { + app: { app_version: 'v1' }, + culture: { display_name: 'name1' }, + }, + } as unknown as ScopeData; + mergePropOverwrite(data, 'contexts', { + os: { name: 'os1' }, + app: { app_name: 'name1' }, + }); + expect(data.contexts).toEqual({ + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + app: { app_name: 'name1' }, + }); + }); + + it('does not mutate the original object if no changes are made', () => { + const tags = { a: 'aa' }; + const data = { tags } as unknown as ScopeData; + mergePropOverwrite(data, 'tags', {}); + expect(data.tags).toBe(tags); + }); + }); + + describe('mergeData', () => { + it('works with empty data', () => { + const data1: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }; + const data2: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }; + mergeData(data1, data2); + expect(data1).toEqual({ + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }); + }); + + it('merges data correctly', () => { + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + const attachment3 = { filename: '3' } as Attachment; + + const breadcrumb1 = { message: '1' } as Breadcrumb; + const breadcrumb2 = { message: '2' } as Breadcrumb; + const breadcrumb3 = { message: '3' } as Breadcrumb; + + const eventProcessor1 = ((a: unknown) => null) as EventProcessor; + const eventProcessor2 = ((b: unknown) => null) as EventProcessor; + const eventProcessor3 = ((c: unknown) => null) as EventProcessor; + + const data1: ScopeData = { + eventProcessors: [eventProcessor1], + breadcrumbs: [breadcrumb1], + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + attachments: [attachment1], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: { aa: 'aa', bb: 'aa' }, + fingerprint: ['aa', 'bb'], + }; + const data2: ScopeData = { + eventProcessors: [eventProcessor2, eventProcessor3], + breadcrumbs: [breadcrumb2, breadcrumb3], + user: { id: '2', name: 'foo' }, + tags: { tag2: 'bb', tag3: 'bb' }, + extra: { extra2: 'bb', extra3: 'bb' }, + contexts: { os: { name: 'os2' } }, + attachments: [attachment2, attachment3], + propagationContext: { spanId: '2', traceId: '2' }, + sdkProcessingMetadata: { bb: 'bb', cc: 'bb' }, + fingerprint: ['cc'], + }; + mergeData(data1, data2); + expect(data1).toEqual({ + eventProcessors: [eventProcessor1, eventProcessor2, eventProcessor3], + breadcrumbs: [breadcrumb1, breadcrumb2, breadcrumb3], + user: { id: '2', name: 'foo', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'bb', tag3: 'bb' }, + extra: { extra1: 'aa', extra2: 'bb', extra3: 'bb' }, + contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, + attachments: [attachment1, attachment2, attachment3], + // This is not merged, we always use the one from the scope here anyhow + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: { aa: 'aa', bb: 'bb', cc: 'bb' }, + fingerprint: ['aa', 'bb', 'cc'], + }); + }); + }); + + describe('applyToEvent', () => { + it('works without any data', async () => { + mockSdkInit(); + + const scope = new Scope(); + + const event = await scope.applyToEvent({ message: 'foo' }); + + expect(event).toEqual({ + message: 'foo', + sdkProcessingMetadata: { + propagationContext: { + spanId: expect.any(String), + traceId: expect.any(String), + }, + }, + }); + }); + + it('merges scope data', async () => { + mockSdkInit(); + + const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; + const breadcrumb2 = { message: '2', timestamp: 222 } as Breadcrumb; + const breadcrumb3 = { message: '3', timestamp: 123 } as Breadcrumb; + const breadcrumb4 = { message: '4', timestamp: 333 } as Breadcrumb; + + const eventProcessor1 = jest.fn((a: unknown) => a) as EventProcessor; + const eventProcessor2 = jest.fn((b: unknown) => b) as EventProcessor; + const eventProcessor3 = jest.fn((c: unknown) => c) as EventProcessor; + + const scope = new Scope(); + scope.update({ + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + propagationContext: { spanId: '1', traceId: '1' }, + fingerprint: ['aa'], + }); + scope.addBreadcrumb(breadcrumb1); + scope.addEventProcessor(eventProcessor1); + + const globalScope = getGlobalScope(); + const isolationScope = getIsolationScope(); + + globalScope.addBreadcrumb(breadcrumb2); + globalScope.addEventProcessor(eventProcessor2); + globalScope.setSDKProcessingMetadata({ aa: 'aa' }); + + isolationScope.addBreadcrumb(breadcrumb3); + isolationScope.addEventProcessor(eventProcessor3); + globalScope.setSDKProcessingMetadata({ bb: 'bb' }); + + const event = await scope.applyToEvent({ + message: 'foo', + breadcrumbs: [breadcrumb4], + fingerprint: ['dd'], + }); + + expect(event).toEqual({ + message: 'foo', + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + fingerprint: ['dd', 'aa'], + breadcrumbs: [breadcrumb4, breadcrumb2, breadcrumb3, breadcrumb1], + sdkProcessingMetadata: { + aa: 'aa', + bb: 'bb', + propagationContext: { + spanId: '1', + traceId: '1', + }, + }, + }); + }); + }); + + describe('getAttachments', () => { + it('works without any data', async () => { + mockSdkInit(); + + const scope = new Scope(); + + const actual = scope.getAttachments(); + expect(actual).toEqual([]); + }); + + it('merges attachments data', async () => { + mockSdkInit(); + + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + const attachment3 = { filename: '3' } as Attachment; + + const scope = new Scope(); + scope.addAttachment(attachment1); + + const globalScope = getGlobalScope(); + const isolationScope = getIsolationScope(); + + globalScope.addAttachment(attachment2); + isolationScope.addAttachment(attachment3); + + const actual = scope.getAttachments(); + expect(actual).toEqual([attachment2, attachment3, attachment1]); + }); + }); +}); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 950fe7bac197..06524bcd0c0a 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -79,7 +79,7 @@ export { defaultIntegrations, init, defaultStackParser, getSentryRelease } from export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; export { deepReadDirSync } from './utils'; export { getModuleFromFilename } from './module'; -export { enableAnrDetection } from './anr'; +export { enableAnrDetection, isAnrChildProcess } from './anr'; import { Integrations as CoreIntegrations } from '@sentry/core'; diff --git a/packages/opentelemetry/src/custom/scope.ts b/packages/opentelemetry/src/custom/scope.ts index e206ba8d8096..e08f8484d87d 100644 --- a/packages/opentelemetry/src/custom/scope.ts +++ b/packages/opentelemetry/src/custom/scope.ts @@ -87,6 +87,11 @@ export class OpenTelemetryScope extends Scope { return this; } + return this._addBreadcrumb(breadcrumb, maxBreadcrumbs); + } + + /** Add a breadcrumb to this scope. */ + protected _addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { return super.addBreadcrumb(breadcrumb, maxBreadcrumbs); } diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts index 3ac617aada9d..f379b4216da5 100644 --- a/packages/opentelemetry/src/index.ts +++ b/packages/opentelemetry/src/index.ts @@ -6,9 +6,16 @@ export type { OpenTelemetryClient } from './types'; export { wrapClientClass } from './custom/client'; export { getSpanKind } from './utils/getSpanKind'; -export { getSpanHub, getSpanMetadata, getSpanParent, getSpanScope, setSpanMetadata } from './utils/spanData'; +export { + getSpanHub, + getSpanMetadata, + getSpanParent, + getSpanScope, + setSpanMetadata, + getSpanFinishScope, +} from './utils/spanData'; -export { getPropagationContextFromContext, setPropagationContextOnContext } from './utils/contextData'; +export { getPropagationContextFromContext, setPropagationContextOnContext, setHubOnContext } from './utils/contextData'; export { spanHasAttributes, @@ -25,6 +32,7 @@ export { getActiveSpan, getRootSpan } from './utils/getActiveSpan'; export { startSpan, startInactiveSpan } from './trace'; export { getCurrentHub, setupGlobalHub, getClient } from './custom/hub'; +export { OpenTelemetryScope } from './custom/scope'; export { addTracingExtensions } from './custom/hubextensions'; export { setupEventContextTrace } from './setupEventContextTrace'; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 95ad13997fb9..c15bd4483a9b 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -20,7 +20,7 @@ import type { SpanNode } from './utils/groupSpansWithParents'; import { groupSpansWithParents } from './utils/groupSpansWithParents'; import { mapStatus } from './utils/mapStatus'; import { parseSpanDescription } from './utils/parseSpanDescription'; -import { getSpanHub, getSpanMetadata, getSpanScope } from './utils/spanData'; +import { getSpanFinishScope, getSpanHub, getSpanMetadata, getSpanScope } from './utils/spanData'; type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; @@ -111,12 +111,9 @@ 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) as OpenTelemetryScope | undefined; - const forkedScope = scope ? scope.clone() : new OpenTelemetryScope(); - forkedScope.activeSpan = span as unknown as Span; - - transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), forkedScope); + // We make sure to use the finish scope + const scope = getSpanFinishScope(span); + transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), scope); }); return Array.from(remaining) diff --git a/packages/opentelemetry/src/spanProcessor.ts b/packages/opentelemetry/src/spanProcessor.ts index a2d7de69fb00..dd5e6de53cbd 100644 --- a/packages/opentelemetry/src/spanProcessor.ts +++ b/packages/opentelemetry/src/spanProcessor.ts @@ -5,13 +5,14 @@ import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { logger } from '@sentry/utils'; import { getCurrentHub } from './custom/hub'; +import { OpenTelemetryScope } from './custom/scope'; import { DEBUG_BUILD } from './debug-build'; import { SentrySpanExporter } from './spanExporter'; import { maybeCaptureExceptionForTimedEvent } from './utils/captureExceptionForTimedEvent'; import { getHubFromContext } from './utils/contextData'; -import { getSpanHub, setSpanHub, setSpanParent, setSpanScope } from './utils/spanData'; +import { getSpanHub, setSpanFinishScope, setSpanHub, setSpanParent, setSpanScope } from './utils/spanData'; -function onSpanStart(span: Span, parentContext: Context): void { +function onSpanStart(span: Span, parentContext: Context, ScopeClass: typeof OpenTelemetryScope): void { // This is a reliable way to get the parent span - because this is exactly how the parent is identified in the OTEL SDK const parentSpan = trace.getSpan(parentContext); const hub = getHubFromContext(parentContext); @@ -30,8 +31,14 @@ function onSpanStart(span: Span, parentContext: Context): void { // We need the scope at time of span creation in order to apply it to the event when the span is finished if (actualHub) { + const scope = actualHub.getScope(); setSpanScope(span, actualHub.getScope()); setSpanHub(span, actualHub); + + // Use this scope for finishing the span + const finishScope = (scope as OpenTelemetryScope).clone(); + finishScope.activeSpan = span; + setSpanFinishScope(span, finishScope); } } @@ -48,15 +55,19 @@ function onSpanEnd(span: Span): void { * the Sentry SDK. */ export class SentrySpanProcessor extends BatchSpanProcessor implements SpanProcessorInterface { - public constructor() { + private _scopeClass: typeof OpenTelemetryScope; + + public constructor(options: { scopeClass?: typeof OpenTelemetryScope } = {}) { super(new SentrySpanExporter()); + + this._scopeClass = options.scopeClass || OpenTelemetryScope; } /** * @inheritDoc */ public onStart(span: Span, parentContext: Context): void { - onSpanStart(span, parentContext); + onSpanStart(span, parentContext, this._scopeClass); DEBUG_BUILD && logger.log(`[Tracing] Starting span "${span.name}" (${span.spanContext().spanId})`); diff --git a/packages/opentelemetry/src/utils/spanData.ts b/packages/opentelemetry/src/utils/spanData.ts index e8fe58506866..18d9661a6488 100644 --- a/packages/opentelemetry/src/utils/spanData.ts +++ b/packages/opentelemetry/src/utils/spanData.ts @@ -7,6 +7,7 @@ import type { AbstractSpan } from '../types'; // This way we can enhance the data that an OTEL Span natively gives us // and since we are using weakmaps, we do not need to clean up after ourselves const SpanScope = new WeakMap(); +const SpanFinishScope = new WeakMap(); const SpanHub = new WeakMap(); const SpanParent = new WeakMap(); const SpanMetadata = new WeakMap>(); @@ -50,3 +51,13 @@ export function setSpanMetadata(span: AbstractSpan, metadata: Partial | undefined { return SpanMetadata.get(span); } + +/** Set the Sentry scope to be used for finishing a given OTEL span. */ +export function setSpanFinishScope(span: AbstractSpan, scope: Scope): void { + SpanFinishScope.set(span, scope); +} + +/** Get the Sentry scope to use for finishing an OTEL span. */ +export function getSpanFinishScope(span: AbstractSpan): Scope | undefined { + return SpanFinishScope.get(span); +} From 32f4bf0786e6e131e76c0999a4f47f94565b0ddf Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 19 Dec 2023 09:36:04 +0100 Subject: [PATCH 25/34] ref(deno): Refactor deno integration to avoid `setupOnce` (#9900) Slowly getting rid of `getCurrentHub()`... --- .../deno/src/integrations/globalhandlers.ts | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index 9914764d4c45..27745d6d6765 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -1,7 +1,8 @@ import type { ServerRuntimeClient } from '@sentry/core'; -import { getClient, getCurrentHub, getCurrentScope } from '@sentry/core'; +import { captureEvent } from '@sentry/core'; +import { getClient } from '@sentry/core'; import { flush } from '@sentry/core'; -import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types'; +import type { Client, Event, Integration, Primitive, StackParser } from '@sentry/types'; import { eventFromUnknownInput, isPrimitive } from '@sentry/utils'; type GlobalHandlersIntegrationsOptionKeys = 'error' | 'unhandledrejection'; @@ -26,15 +27,6 @@ export class GlobalHandlers implements Integration { /** JSDoc */ private readonly _options: GlobalHandlersIntegrations; - /** - * Stores references functions to installing handlers. Will set to undefined - * after they have been run so that they are not used twice. - */ - private _installFunc: Record void) | undefined> = { - error: installGlobalErrorHandler, - unhandledrejection: installGlobalUnhandledRejectionHandler, - }; - /** JSDoc */ public constructor(options?: GlobalHandlersIntegrations) { this._options = { @@ -47,35 +39,35 @@ export class GlobalHandlers implements Integration { * @inheritDoc */ public setupOnce(): void { - const options = this._options; - - // We can disable guard-for-in as we construct the options object above + do checks against - // `this._installFunc` for the property. - // eslint-disable-next-line guard-for-in - for (const key in options) { - const installFunc = this._installFunc[key as GlobalHandlersIntegrationsOptionKeys]; - if (installFunc && options[key as GlobalHandlersIntegrationsOptionKeys]) { - installFunc(); - this._installFunc[key as GlobalHandlersIntegrationsOptionKeys] = undefined; - } + // noop + } + + /** @inheritdoc */ + public setup(client: Client): void { + if (this._options.error) { + installGlobalErrorHandler(client); + } + if (this._options.unhandledrejection) { + installGlobalUnhandledRejectionHandler(client); } } } -function installGlobalErrorHandler(): void { +function installGlobalErrorHandler(client: Client): void { globalThis.addEventListener('error', data => { - if (isExiting) { + if (getClient() !== client || isExiting) { return; } - const [hub, stackParser] = getHubAndOptions(); + const stackParser = getStackParser(); + const { message, error } = data; const event = eventFromUnknownInput(getClient(), stackParser, error || message); event.level = 'fatal'; - hub.captureEvent(event, { + captureEvent(event, { originalException: error, mechanism: { handled: false, @@ -94,13 +86,13 @@ function installGlobalErrorHandler(): void { }); } -function installGlobalUnhandledRejectionHandler(): void { +function installGlobalUnhandledRejectionHandler(client: Client): void { globalThis.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => { - if (isExiting) { + if (getClient() !== client || isExiting) { return; } - const [hub, stackParser] = getHubAndOptions(); + const stackParser = getStackParser(); let error = e; // dig the object of the rejection out of known event types @@ -118,7 +110,7 @@ function installGlobalUnhandledRejectionHandler(): void { event.level = 'fatal'; - hub.captureEvent(event, { + captureEvent(event, { originalException: error, mechanism: { handled: false, @@ -157,12 +149,12 @@ function eventFromRejectionWithPrimitive(reason: Primitive): Event { }; } -function getHubAndOptions(): [Hub, StackParser] { - const hub = getCurrentHub(); - const client = hub.getClient(); - const options = (client && client.getOptions()) || { - stackParser: () => [], - attachStacktrace: false, - }; - return [hub, options.stackParser]; +function getStackParser(): StackParser { + const client = getClient(); + + if (!client) { + return () => []; + } + + return client.getOptions().stackParser; } From 91a6b4e5ae4d5cfe745b548a20ecefad8f4d3f0a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 19 Dec 2023 09:39:26 +0100 Subject: [PATCH 26/34] ref(browser): Refactor browser integrations to avoid `setupOnce` (#9898) Also a small core refactor... --- .../src/integrations/globalhandlers.ts | 73 ++++------- packages/browser/src/profiling/integration.ts | 124 +++++++++--------- .../wrapGenerationFunctionWithSentry.ts | 4 +- 3 files changed, 93 insertions(+), 108 deletions(-) diff --git a/packages/browser/src/integrations/globalhandlers.ts b/packages/browser/src/integrations/globalhandlers.ts index 0c3f3be60e2d..079ef6083212 100644 --- a/packages/browser/src/integrations/globalhandlers.ts +++ b/packages/browser/src/integrations/globalhandlers.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { getCurrentHub } from '@sentry/core'; -import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types'; +import { captureEvent, getClient } from '@sentry/core'; +import type { Client, Event, Integration, Primitive, StackParser } from '@sentry/types'; import { addGlobalErrorInstrumentationHandler, addGlobalUnhandledRejectionInstrumentationHandler, @@ -36,12 +36,6 @@ export class GlobalHandlers implements Integration { /** JSDoc */ private readonly _options: GlobalHandlersIntegrations; - /** - * Stores references functions to installing handlers. Will set to undefined - * after they have been run so that they are not used twice. - */ - private _installFunc: Record void) | undefined>; - /** JSDoc */ public constructor(options?: GlobalHandlersIntegrations) { this.name = GlobalHandlers.id; @@ -50,43 +44,36 @@ export class GlobalHandlers implements Integration { onunhandledrejection: true, ...options, }; - - this._installFunc = { - onerror: _installGlobalOnErrorHandler, - onunhandledrejection: _installGlobalOnUnhandledRejectionHandler, - }; } /** * @inheritDoc */ public setupOnce(): void { Error.stackTraceLimit = 50; - const options = this._options; - - // We can disable guard-for-in as we construct the options object above + do checks against - // `this._installFunc` for the property. - // eslint-disable-next-line guard-for-in - for (const key in options) { - const installFunc = this._installFunc[key as GlobalHandlersIntegrationsOptionKeys]; - if (installFunc && options[key as GlobalHandlersIntegrationsOptionKeys]) { - globalHandlerLog(key); - installFunc(); - this._installFunc[key as GlobalHandlersIntegrationsOptionKeys] = undefined; - } + } + + /** @inheritdoc */ + public setup(client: Client): void { + if (this._options.onerror) { + _installGlobalOnErrorHandler(client); + globalHandlerLog('onerror'); + } + if (this._options.onunhandledrejection) { + _installGlobalOnUnhandledRejectionHandler(client); + globalHandlerLog('onunhandledrejection'); } } } -function _installGlobalOnErrorHandler(): void { +function _installGlobalOnErrorHandler(client: Client): void { addGlobalErrorInstrumentationHandler(data => { - const [hub, stackParser, attachStacktrace] = getHubAndOptions(); - if (!hub.getIntegration(GlobalHandlers)) { + const { stackParser, attachStacktrace } = getOptions(); + + if (getClient() !== client || shouldIgnoreOnError()) { return; } + const { msg, url, line, column, error } = data; - if (shouldIgnoreOnError()) { - return; - } const event = error === undefined && isString(msg) @@ -100,7 +87,7 @@ function _installGlobalOnErrorHandler(): void { event.level = 'error'; - hub.captureEvent(event, { + captureEvent(event, { originalException: error, mechanism: { handled: false, @@ -110,15 +97,12 @@ function _installGlobalOnErrorHandler(): void { }); } -function _installGlobalOnUnhandledRejectionHandler(): void { +function _installGlobalOnUnhandledRejectionHandler(client: Client): void { addGlobalUnhandledRejectionInstrumentationHandler(e => { - const [hub, stackParser, attachStacktrace] = getHubAndOptions(); - if (!hub.getIntegration(GlobalHandlers)) { - return; - } + const { stackParser, attachStacktrace } = getOptions(); - if (shouldIgnoreOnError()) { - return true; + if (getClient() !== client || shouldIgnoreOnError()) { + return; } const error = _getUnhandledRejectionError(e as unknown); @@ -129,15 +113,13 @@ function _installGlobalOnUnhandledRejectionHandler(): void { event.level = 'error'; - hub.captureEvent(event, { + captureEvent(event, { originalException: error, mechanism: { handled: false, type: 'onunhandledrejection', }, }); - - return; }); } @@ -258,12 +240,11 @@ function globalHandlerLog(type: string): void { DEBUG_BUILD && logger.log(`Global Handler attached: ${type}`); } -function getHubAndOptions(): [Hub, StackParser, boolean | undefined] { - const hub = getCurrentHub(); - const client = hub.getClient(); +function getOptions(): { stackParser: StackParser; attachStacktrace?: boolean } { + const client = getClient(); const options = (client && client.getOptions()) || { stackParser: () => [], attachStacktrace: false, }; - return [hub, options.stackParser, options.attachStacktrace]; + return options; } diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 326af29492cf..5173705feaa6 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -1,4 +1,5 @@ -import type { EventEnvelope, EventProcessor, Hub, Integration, Transaction } from '@sentry/types'; +import { getCurrentScope } from '@sentry/core'; +import type { Client, EventEnvelope, EventProcessor, Hub, Integration, Transaction } from '@sentry/types'; import type { Profile } from '@sentry/types/src/profiling'; import { logger } from '@sentry/utils'; @@ -29,6 +30,7 @@ export class BrowserProfilingIntegration implements Integration { public readonly name: string; + /** @deprecated This is never set. */ public getCurrentHub?: () => Hub; public constructor() { @@ -38,12 +40,13 @@ export class BrowserProfilingIntegration implements Integration { /** * @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - this.getCurrentHub = getCurrentHub; + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { + // noop + } - const hub = this.getCurrentHub(); - const client = hub.getClient(); - const scope = hub.getScope(); + /** @inheritdoc */ + public setup(client: Client): void { + const scope = getCurrentScope(); const transaction = scope.getTransaction(); @@ -53,67 +56,68 @@ export class BrowserProfilingIntegration implements Integration { } } - if (client && typeof client.on === 'function') { - client.on('startTransaction', (transaction: Transaction) => { - if (shouldProfileTransaction(transaction)) { - startProfileForTransaction(transaction); + if (typeof client.on !== 'function') { + logger.warn('[Profiling] Client does not support hooks, profiling will be disabled'); + return; + } + + client.on('startTransaction', (transaction: Transaction) => { + if (shouldProfileTransaction(transaction)) { + startProfileForTransaction(transaction); + } + }); + + client.on('beforeEnvelope', (envelope): void => { + // if not profiles are in queue, there is nothing to add to the envelope. + if (!getActiveProfilesCount()) { + return; + } + + const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); + if (!profiledTransactionEvents.length) { + return; + } + + const profilesToAddToEnvelope: Profile[] = []; + + for (const profiledTransaction of profiledTransactionEvents) { + const context = profiledTransaction && profiledTransaction.contexts; + const profile_id = context && context['profile'] && context['profile']['profile_id']; + const start_timestamp = context && context['profile'] && context['profile']['start_timestamp']; + + if (typeof profile_id !== 'string') { + DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); + continue; } - }); - client.on('beforeEnvelope', (envelope): void => { - // if not profiles are in queue, there is nothing to add to the envelope. - if (!getActiveProfilesCount()) { - return; + if (!profile_id) { + DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); + continue; } - const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); - if (!profiledTransactionEvents.length) { - return; + // Remove the profile from the transaction context before sending, relay will take care of the rest. + if (context && context['profile']) { + delete context.profile; } - const profilesToAddToEnvelope: Profile[] = []; - - for (const profiledTransaction of profiledTransactionEvents) { - const context = profiledTransaction && profiledTransaction.contexts; - const profile_id = context && context['profile'] && context['profile']['profile_id']; - const start_timestamp = context && context['profile'] && context['profile']['start_timestamp']; - - if (typeof profile_id !== 'string') { - DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); - continue; - } - - if (!profile_id) { - DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); - continue; - } - - // Remove the profile from the transaction context before sending, relay will take care of the rest. - if (context && context['profile']) { - delete context.profile; - } - - const profile = takeProfileFromGlobalCache(profile_id); - if (!profile) { - DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); - continue; - } - - const profileEvent = createProfilingEvent( - profile_id, - start_timestamp as number | undefined, - profile, - profiledTransaction as ProfiledEvent, - ); - if (profileEvent) { - profilesToAddToEnvelope.push(profileEvent); - } + const profile = takeProfileFromGlobalCache(profile_id); + if (!profile) { + DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); + continue; } - addProfilesToEnvelope(envelope as EventEnvelope, profilesToAddToEnvelope); - }); - } else { - logger.warn('[Profiling] Client does not support hooks, profiling will be disabled'); - } + const profileEvent = createProfilingEvent( + profile_id, + start_timestamp as number | undefined, + profile, + profiledTransaction as ProfiledEvent, + ); + if (profileEvent) { + profilesToAddToEnvelope.push(profileEvent); + } + } + + addProfilesToEnvelope(envelope as EventEnvelope, profilesToAddToEnvelope); + }); } } diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 80f7d62cc447..3acaa849ff79 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -2,7 +2,7 @@ import { addTracingExtensions, captureException, continueTrace, - getCurrentHub, + getClient, getCurrentScope, runWithAsyncContext, trace, @@ -34,7 +34,7 @@ export function wrapGenerationFunctionWithSentry a } let data: Record | undefined = undefined; - if (getCurrentHub().getClient()?.getOptions().sendDefaultPii) { + if (getClient()?.getOptions().sendDefaultPii) { const props: unknown = args[0]; const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined; const searchParams = From c9552a4601df2fe4ac660cabd2e1ee3e64f798db Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 19 Dec 2023 09:59:19 +0100 Subject: [PATCH 27/34] ref(node): Refactor LocalVariables integration to avoid `setupOnce` (#9897) Slowly getting rid of `getCurrentHub()`... --- packages/core/src/baseclient.ts | 4 +- .../node/src/integrations/localvariables.ts | 29 +++++--- .../test/integrations/localvariables.test.ts | 68 ++++++++----------- 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index c5d9fff7df6f..3fbdd13250f8 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -115,14 +115,14 @@ export abstract class BaseClient implements Client { /** Number of calls being processed */ protected _numProcessing: number; + protected _eventProcessors: EventProcessor[]; + /** Holds flushable */ private _outcomes: { [key: string]: number }; // eslint-disable-next-line @typescript-eslint/ban-types private _hooks: Record; - private _eventProcessors: EventProcessor[]; - /** * Initializes this client instance. * diff --git a/packages/node/src/integrations/localvariables.ts b/packages/node/src/integrations/localvariables.ts index a41822331ea5..1ba9907c4806 100644 --- a/packages/node/src/integrations/localvariables.ts +++ b/packages/node/src/integrations/localvariables.ts @@ -2,9 +2,9 @@ import type { Event, EventProcessor, Exception, Hub, Integration, StackFrame, StackParser } from '@sentry/types'; import { LRUMap, logger } from '@sentry/utils'; import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; +import type { NodeClient } from '../client'; import { NODE_VERSION } from '../nodeVersion'; -import type { NodeClientOptions } from '../types'; type Variables = Record; type OnPauseEvent = InspectorNotification; @@ -332,6 +332,7 @@ export class LocalVariables implements Integration { private readonly _cachedFrames: LRUMap = new LRUMap(20); private _rateLimiter: RateLimitIncrement | undefined; + private _shouldProcessEvent = false; public constructor( private readonly _options: Options = {}, @@ -341,16 +342,15 @@ export class LocalVariables implements Integration { /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - this._setup(addGlobalEventProcessor, getCurrentHub().getClient()?.getOptions()); + public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { + // noop } - /** Setup in a way that's easier to call from tests */ - private _setup( - addGlobalEventProcessor: (callback: EventProcessor) => void, - clientOptions: NodeClientOptions | undefined, - ): void { - if (this._session && clientOptions?.includeLocalVariables) { + /** @inheritdoc */ + public setup(client: NodeClient): void { + const clientOptions = client.getOptions(); + + if (this._session && clientOptions.includeLocalVariables) { // Only setup this integration if the Node version is >= v18 // https://github.com/getsentry/sentry-javascript/issues/7697 const unsupportedNodeVersion = (NODE_VERSION.major || 0) < 18; @@ -386,10 +386,19 @@ export class LocalVariables implements Integration { ); } - addGlobalEventProcessor(async event => this._addLocalVariables(event)); + this._shouldProcessEvent = true; } } + /** @inheritdoc */ + public processEvent(event: Event): Event { + if (this._shouldProcessEvent) { + return this._addLocalVariables(event); + } + + return event; + } + /** * Handle the pause event */ diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 640a59e03ae4..6a09111370a8 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -2,7 +2,7 @@ import type { ClientOptions, EventProcessor } from '@sentry/types'; import type { LRUMap } from '@sentry/utils'; import type { Debugger, InspectorNotification } from 'inspector'; -import { defaultStackParser } from '../../src'; +import { NodeClient, defaultStackParser } from '../../src'; import type { DebugSession, FrameVariables } from '../../src/integrations/localvariables'; import { LocalVariables, createCallbackList, createRateLimiter } from '../../src/integrations/localvariables'; import { NODE_VERSION } from '../../src/nodeVersion'; @@ -52,7 +52,6 @@ class MockDebugSession implements DebugSession { interface LocalVariablesPrivate { _cachedFrames: LRUMap; - _setup(addGlobalEventProcessor: (callback: EventProcessor) => void, clientOptions: ClientOptions): void; } const exceptionEvent = { @@ -154,8 +153,6 @@ const exceptionEvent100Frames = { describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { it('Adds local variables to stack frames', async () => { - expect.assertions(7); - const session = new MockDebugSession({ '-6224981551105448869.1.2': { name: 'tim' }, '-6224981551105448869.1.6': { arr: [1, 2, 3] }, @@ -164,13 +161,14 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, includeLocalVariables: true, + integrations: [localVariables], }); - let eventProcessor: EventProcessor | undefined; + const client = new NodeClient(options); + client.setupIntegrations(true); - (localVariables as unknown as LocalVariablesPrivate)._setup(callback => { - eventProcessor = callback; - }, options); + const eventProcessors = client['_eventProcessors']; + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); @@ -189,7 +187,7 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { { function: 'one', vars: { arr: [1, 2, 3] } }, ]); - const event = await eventProcessor?.( + const event = await eventProcessor!( { event_id: '9cbf882ade9a415986632ac4e16918eb', platform: 'node', @@ -249,22 +247,16 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { }); it('Only considers the first 5 frames', async () => { - expect.assertions(4); - const session = new MockDebugSession({}); const localVariables = new LocalVariables({}, session); const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, includeLocalVariables: true, + integrations: [localVariables], }); - let eventProcessor: EventProcessor | undefined; - - (localVariables as unknown as LocalVariablesPrivate)._setup(callback => { - eventProcessor = callback; - }, options); - - expect(eventProcessor).toBeDefined(); + const client = new NodeClient(options); + client.setupIntegrations(true); await session.runPause(exceptionEvent100Frames); @@ -280,16 +272,16 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { }); it('Should not lookup variables for non-exception reasons', async () => { - expect.assertions(1); - const session = new MockDebugSession({}, { getLocalVariables: true }); const localVariables = new LocalVariables({}, session); const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, includeLocalVariables: true, + integrations: [localVariables], }); - (localVariables as unknown as LocalVariablesPrivate)._setup(_ => {}, options); + const client = new NodeClient(options); + client.setupIntegrations(true); const nonExceptionEvent = { method: exceptionEvent.method, @@ -302,43 +294,41 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { }); it('Should not initialize when disabled', async () => { - expect.assertions(1); - const session = new MockDebugSession({}, { configureAndConnect: true }); const localVariables = new LocalVariables({}, session); const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, + integrations: [localVariables], }); - let eventProcessor: EventProcessor | undefined; + const client = new NodeClient(options); + client.setupIntegrations(true); - (localVariables as unknown as LocalVariablesPrivate)._setup(callback => { - eventProcessor = callback; - }, options); + const eventProcessors = client['_eventProcessors']; + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); - expect(eventProcessor).toBeUndefined(); + expect(eventProcessor).toBeDefined(); + expect(localVariables['_shouldProcessEvent']).toBe(false); }); it('Should not initialize when inspector not loaded', async () => { - expect.assertions(1); - const localVariables = new LocalVariables({}, undefined); const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, + integrations: [localVariables], }); - let eventProcessor: EventProcessor | undefined; + const client = new NodeClient(options); + client.setupIntegrations(true); - (localVariables as unknown as LocalVariablesPrivate)._setup(callback => { - eventProcessor = callback; - }, options); + const eventProcessors = client['_eventProcessors']; + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); - expect(eventProcessor).toBeUndefined(); + expect(eventProcessor).toBeDefined(); + expect(localVariables['_shouldProcessEvent']).toBe(false); }); it('Should cache identical uncaught exception events', async () => { - expect.assertions(1); - const session = new MockDebugSession({ '-6224981551105448869.1.2': { name: 'tim' }, '-6224981551105448869.1.6': { arr: [1, 2, 3] }, @@ -347,9 +337,11 @@ describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, includeLocalVariables: true, + integrations: [localVariables], }); - (localVariables as unknown as LocalVariablesPrivate)._setup(_ => {}, options); + const client = new NodeClient(options); + client.setupIntegrations(true); await session.runPause(exceptionEvent); await session.runPause(exceptionEvent); From 4e0c460dc3c35855a79a308bf815eca74ed0be13 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 19 Dec 2023 11:51:26 +0100 Subject: [PATCH 28/34] chore(sveltekit): Add SvelteKit 2.0 to peer dependencies (#9861) Setting the range to `1.x || 2.x` to include kit 2.0 --- packages/sveltekit/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 93ccd2f2564d..dd08e6b92727 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -17,7 +17,7 @@ "access": "public" }, "peerDependencies": { - "@sveltejs/kit": "1.x" + "@sveltejs/kit": "1.x || 2.x" }, "dependencies": { "@sentry-internal/tracing": "7.88.0", From 6d3222820670b93d927c4d467e6d27845442659f Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 19 Dec 2023 12:20:15 +0100 Subject: [PATCH 29/34] fix(sveltekit): Add conditional exports (#9872) Looks like Vite 5 module resolution for `@sentry/sveltekit` only works with defining conditional exports. Tested this locally with a Sverdle kit@1, kit@2 and the syntax website. --- packages/sveltekit/package.json | 10 +++++++ scripts/prepack.ts | 48 ++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index dd08e6b92727..9458ce12e770 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -13,6 +13,16 @@ "module": "build/esm/index.server.js", "browser": "build/esm/index.client.js", "types": "build/types/index.types.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "browser": { + "import": "./build/esm/index.client.js", + "require": "./build/cjs/index.client.js" + }, + "node": "./build/cjs/index.server.js" + } + }, "publishConfig": { "access": "public" }, diff --git a/scripts/prepack.ts b/scripts/prepack.ts index 941c6e6f6218..9118226f08e7 100644 --- a/scripts/prepack.ts +++ b/scripts/prepack.ts @@ -15,6 +15,7 @@ const NPM_IGNORE = fs.existsSync('.npmignore') ? '.npmignore' : '../../.npmignor const ASSETS = ['README.md', 'LICENSE', 'package.json', NPM_IGNORE] as const; const ENTRY_POINTS = ['main', 'module', 'types', 'browser'] as const; +const CONDITIONAL_EXPORT_ENTRY_POINTS = ['import', 'require', ...ENTRY_POINTS] as const; const EXPORT_MAP_ENTRY_POINT = 'exports'; const TYPES_VERSIONS_ENTRY_POINT = 'typesVersions'; @@ -22,6 +23,7 @@ const packageWithBundles = process.argv.includes('--bundles'); const buildDir = packageWithBundles ? NPM_BUILD_DIR : BUILD_DIR; type PackageJsonEntryPoints = Record<(typeof ENTRY_POINTS)[number], string>; +type ConditionalExportEntryPoints = Record<(typeof CONDITIONAL_EXPORT_ENTRY_POINTS)[number], string>; interface TypeVersions { [key: string]: { @@ -29,17 +31,12 @@ interface TypeVersions { }; } +type PackageJsonExports = Partial & { + [key: string]: Partial; +}; + interface PackageJson extends Record, PackageJsonEntryPoints { - [EXPORT_MAP_ENTRY_POINT]: { - [key: string]: { - import: string; - require: string; - types: string; - node: string; - browser: string; - default: string; - }; - }; + [EXPORT_MAP_ENTRY_POINT]: PackageJsonExports; [TYPES_VERSIONS_ENTRY_POINT]: TypeVersions; } @@ -75,14 +72,26 @@ ENTRY_POINTS.filter(entryPoint => newPkgJson[entryPoint]).forEach(entryPoint => newPkgJson[entryPoint] = newPkgJson[entryPoint].replace(`${buildDir}/`, ''); }); +/** + * Recursively traverses the exports object and rewrites all string values to remove the build directory. + */ +function rewriteConditionalExportEntryPoint( + exportsObject: Record>, + key: string, +): void { + const exportsField = exportsObject[key]; + if (typeof exportsField === 'string') { + exportsObject[key] = exportsField.replace(`${buildDir}/`, ''); + return; + } + Object.keys(exportsField).forEach(subfieldKey => { + rewriteConditionalExportEntryPoint(exportsField, subfieldKey); + }); +} + if (newPkgJson[EXPORT_MAP_ENTRY_POINT]) { - Object.entries(newPkgJson[EXPORT_MAP_ENTRY_POINT]).forEach(([key, val]) => { - newPkgJson[EXPORT_MAP_ENTRY_POINT][key] = Object.entries(val).reduce( - (acc, [key, val]) => { - return { ...acc, [key]: val.replace(`${buildDir}/`, '') }; - }, - {} as typeof val, - ); + Object.keys(newPkgJson[EXPORT_MAP_ENTRY_POINT]).forEach(key => { + rewriteConditionalExportEntryPoint(newPkgJson[EXPORT_MAP_ENTRY_POINT], key); }); } @@ -90,7 +99,10 @@ 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]) => { const newKey = key.replace(`${buildDir}/`, ''); - return { ...acc, [newKey]: val.map(v => v.replace(`${buildDir}/`, '')) }; + return { + ...acc, + [newKey]: val.map(v => v.replace(`${buildDir}/`, '')), + }; }, {}); }); } From a8569130cad640a9e8ba634c952fb8f4e4b5911a Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 19 Dec 2023 12:22:39 +0000 Subject: [PATCH 30/34] fix(remix): Do not capture thrown redirect responses. (#9909) Skips capturing all responses < 500 that end up in `captureRemixServerException`. --- packages/remix/src/utils/instrumentServer.ts | 9 ++++----- .../integration/app_v1/routes/throw-redirect.tsx | 2 ++ .../integration/app_v2/routes/throw-redirect.tsx | 2 ++ .../integration/common/routes/throw-redirect.tsx | 11 +++++++++++ .../integration/test/client/throw-redirect.test.ts | 8 ++++++++ .../test/integration/test/server/loader.test.ts | 12 ++++++++++++ 6 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 packages/remix/test/integration/app_v1/routes/throw-redirect.tsx create mode 100644 packages/remix/test/integration/app_v2/routes/throw-redirect.tsx create mode 100644 packages/remix/test/integration/common/routes/throw-redirect.tsx create mode 100644 packages/remix/test/integration/test/client/throw-redirect.test.ts diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 816410fd75f9..ea87b961493c 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -106,14 +106,13 @@ export function wrapRemixHandleError(err: unknown, { request }: DataFunctionArgs export async function captureRemixServerException(err: unknown, name: string, request: Request): Promise { // Skip capturing if the thrown error is not a 5xx response // https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders - if (IS_REMIX_V2) { - if (isRouteErrorResponse(err) && err.status < 500) { - return; - } - } else if (isResponse(err) && err.status < 500) { + if (IS_REMIX_V2 && isRouteErrorResponse(err) && err.status < 500) { return; } + if (isResponse(err) && err.status < 500) { + return; + } // Skip capturing if the request is aborted as Remix docs suggest // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror if (request.signal.aborted) { diff --git a/packages/remix/test/integration/app_v1/routes/throw-redirect.tsx b/packages/remix/test/integration/app_v1/routes/throw-redirect.tsx new file mode 100644 index 000000000000..4425f3432b58 --- /dev/null +++ b/packages/remix/test/integration/app_v1/routes/throw-redirect.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/throw-redirect'; +export { default } from '../../common/routes/throw-redirect'; diff --git a/packages/remix/test/integration/app_v2/routes/throw-redirect.tsx b/packages/remix/test/integration/app_v2/routes/throw-redirect.tsx new file mode 100644 index 000000000000..4425f3432b58 --- /dev/null +++ b/packages/remix/test/integration/app_v2/routes/throw-redirect.tsx @@ -0,0 +1,2 @@ +export * from '../../common/routes/throw-redirect'; +export { default } from '../../common/routes/throw-redirect'; diff --git a/packages/remix/test/integration/common/routes/throw-redirect.tsx b/packages/remix/test/integration/common/routes/throw-redirect.tsx new file mode 100644 index 000000000000..2d530e41d0c0 --- /dev/null +++ b/packages/remix/test/integration/common/routes/throw-redirect.tsx @@ -0,0 +1,11 @@ +import { LoaderFunction, redirect } from '@remix-run/node'; +import { useLoaderData } from '@remix-run/react'; + +export const loader: LoaderFunction = async () => { + throw redirect('/'); +}; + +export default function ThrowRedirect() { + const data = useLoaderData(); + return
{data}
; +} diff --git a/packages/remix/test/integration/test/client/throw-redirect.test.ts b/packages/remix/test/integration/test/client/throw-redirect.test.ts new file mode 100644 index 000000000000..60ed3588c79b --- /dev/null +++ b/packages/remix/test/integration/test/client/throw-redirect.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/test'; +import { countEnvelopes } from './utils/helpers'; + +test('should not report thrown redirect response on client side.', async ({ page }) => { + const count = await countEnvelopes(page, { url: '/throw-redirect', envelopeType: 'event' }); + + expect(count).toBe(0); +}); diff --git a/packages/remix/test/integration/test/server/loader.test.ts b/packages/remix/test/integration/test/server/loader.test.ts index b6d4fc402ea7..24d67422c3ca 100644 --- a/packages/remix/test/integration/test/server/loader.test.ts +++ b/packages/remix/test/integration/test/server/loader.test.ts @@ -241,4 +241,16 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada ], }); }); + + it('does not capture thrown redirect responses', async () => { + const env = await RemixTestEnv.init(adapter); + const url = `${env.url}/throw-redirect`; + + const envelopesCount = await env.countEnvelopes({ + url, + envelopeType: ['event'], + }); + + expect(envelopesCount).toBe(0); + }); }); From 31c769cd548197da07211fe82697c3c4460cea23 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 19 Dec 2023 13:27:29 +0100 Subject: [PATCH 31/34] test(sveltekit): Add SvelteKit 2.0 E2E test app (#9873) adds a Sveltekit 2.0 E2E test application. Currently, we only test building. This shows that with #9872, our SDK works in SvelteKit 2.0. We should however add actual tests to both Kit 1.x and 2.x test apps. --- .github/workflows/build.yml | 1 + .../test-applications/sveltekit-2/.gitignore | 10 + .../test-applications/sveltekit-2/.npmrc | 2 + .../test-applications/sveltekit-2/README.md | 41 +++ .../sveltekit-2/event-proxy-server.ts | 253 ++++++++++++++++++ .../sveltekit-2/package.json | 41 +++ .../sveltekit-2/playwright.config.ts | 71 +++++ .../sveltekit-2/src/app.html | 12 + .../sveltekit-2/src/hooks.client.ts | 16 ++ .../sveltekit-2/src/hooks.server.ts | 18 ++ .../sveltekit-2/src/routes/+page.svelte | 2 + .../src/routes/building/+page.server.ts | 5 + .../src/routes/building/+page.svelte | 6 + .../sveltekit-2/src/routes/building/+page.ts | 5 + .../sveltekit-2/start-event-proxy.ts | 6 + .../sveltekit-2/static/favicon.png | Bin 0 -> 1571 bytes .../sveltekit-2/svelte.config.js | 18 ++ .../sveltekit-2/test/transaction.test.ts | 48 ++++ .../sveltekit-2/tsconfig.json | 21 ++ .../sveltekit-2/vite.config.js | 12 + 20 files changed, 588 insertions(+) create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/.gitignore create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/.npmrc create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/README.md create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/package.json create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/src/app.html create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/src/hooks.client.ts create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.server.ts create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.ts create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/static/favicon.png create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/svelte.config.js create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/tsconfig.json create mode 100644 packages/e2e-tests/test-applications/sveltekit-2/vite.config.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd47b36598f2..1c7d639db7ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -870,6 +870,7 @@ jobs: 'standard-frontend-react', 'standard-frontend-react-tracing-import', 'sveltekit', + 'sveltekit-2', 'generic-ts3.8', 'node-experimental-fastify-app', 'node-hapi-app', diff --git a/packages/e2e-tests/test-applications/sveltekit-2/.gitignore b/packages/e2e-tests/test-applications/sveltekit-2/.gitignore new file mode 100644 index 000000000000..6635cf554275 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/packages/e2e-tests/test-applications/sveltekit-2/.npmrc b/packages/e2e-tests/test-applications/sveltekit-2/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/packages/e2e-tests/test-applications/sveltekit-2/README.md b/packages/e2e-tests/test-applications/sveltekit-2/README.md new file mode 100644 index 000000000000..7c0d9fbb26ab --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/README.md @@ -0,0 +1,41 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by +[`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a +development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target +> environment. diff --git a/packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts b/packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts new file mode 100644 index 000000000000..66a9e744846e --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts @@ -0,0 +1,253 @@ +import * as fs from 'fs'; +import * as http from 'http'; +import * as https from 'https'; +import type { AddressInfo } from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as util from 'util'; +import * as zlib from 'zlib'; +import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import { parseEnvelope } from '@sentry/utils'; + +const readFile = util.promisify(fs.readFile); +const writeFile = util.promisify(fs.writeFile); + +interface EventProxyServerOptions { + /** Port to start the event proxy server at. */ + port: number; + /** The name for the proxy server used for referencing it with listener functions */ + proxyServerName: string; +} + +interface SentryRequestCallbackData { + envelope: Envelope; + rawProxyRequestBody: string; + rawSentryResponseBody: string; + sentryResponseStatusCode?: number; +} + +/** + * Starts an event proxy server that will proxy events to sentry when the `tunnel` option is used. Point the `tunnel` + * option to this server (like this `tunnel: http://localhost:${port option}/`). + */ +export async function startEventProxyServer(options: EventProxyServerOptions): Promise { + const eventCallbackListeners: Set<(data: string) => void> = new Set(); + + const proxyServer = http.createServer((proxyRequest, proxyResponse) => { + const proxyRequestChunks: Uint8Array[] = []; + + proxyRequest.addListener('data', (chunk: Buffer) => { + proxyRequestChunks.push(chunk); + }); + + proxyRequest.addListener('error', err => { + throw err; + }); + + proxyRequest.addListener('end', () => { + const proxyRequestBody = + proxyRequest.headers['content-encoding'] === 'gzip' + ? zlib.gunzipSync(Buffer.concat(proxyRequestChunks)).toString() + : Buffer.concat(proxyRequestChunks).toString(); + + let envelopeHeader = JSON.parse(proxyRequestBody.split('\n')[0]); + + if (!envelopeHeader.dsn) { + throw new Error('[event-proxy-server] No dsn on envelope header. Please set tunnel option.'); + } + + const { origin, pathname, host } = new URL(envelopeHeader.dsn); + + const projectId = pathname.substring(1); + const sentryIngestUrl = `${origin}/api/${projectId}/envelope/`; + + proxyRequest.headers.host = host; + + const sentryResponseChunks: Uint8Array[] = []; + + const sentryRequest = https.request( + sentryIngestUrl, + { headers: proxyRequest.headers, method: proxyRequest.method }, + sentryResponse => { + sentryResponse.addListener('data', (chunk: Buffer) => { + proxyResponse.write(chunk, 'binary'); + sentryResponseChunks.push(chunk); + }); + + sentryResponse.addListener('end', () => { + eventCallbackListeners.forEach(listener => { + const rawSentryResponseBody = Buffer.concat(sentryResponseChunks).toString(); + + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody, new TextEncoder(), new TextDecoder()), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody, + sentryResponseStatusCode: sentryResponse.statusCode, + }; + + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + proxyResponse.end(); + }); + + sentryResponse.addListener('error', err => { + throw err; + }); + + proxyResponse.writeHead(sentryResponse.statusCode || 500, sentryResponse.headers); + }, + ); + + sentryRequest.write(Buffer.concat(proxyRequestChunks), 'binary'); + sentryRequest.end(); + }); + }); + + const proxyServerStartupPromise = new Promise(resolve => { + proxyServer.listen(options.port, () => { + resolve(); + }); + }); + + const eventCallbackServer = http.createServer((eventCallbackRequest, eventCallbackResponse) => { + eventCallbackResponse.statusCode = 200; + eventCallbackResponse.setHeader('connection', 'keep-alive'); + + const callbackListener = (data: string): void => { + eventCallbackResponse.write(data.concat('\n'), 'utf8'); + }; + + eventCallbackListeners.add(callbackListener); + + eventCallbackRequest.on('close', () => { + eventCallbackListeners.delete(callbackListener); + }); + + eventCallbackRequest.on('error', () => { + eventCallbackListeners.delete(callbackListener); + }); + }); + + const eventCallbackServerStartupPromise = new Promise(resolve => { + eventCallbackServer.listen(0, () => { + const port = String((eventCallbackServer.address() as AddressInfo).port); + void registerCallbackServerPort(options.proxyServerName, port).then(resolve); + }); + }); + + await eventCallbackServerStartupPromise; + await proxyServerStartupPromise; + return; +} + +export async function waitForRequest( + proxyServerName: string, + callback: (eventData: SentryRequestCallbackData) => Promise | boolean, +): Promise { + const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); + + return new Promise((resolve, reject) => { + const request = http.request(`http://localhost:${eventCallbackServerPort}/`, {}, response => { + let eventContents = ''; + + response.on('error', err => { + reject(err); + }); + + response.on('data', (chunk: Buffer) => { + const chunkString = chunk.toString('utf8'); + chunkString.split('').forEach(char => { + if (char === '\n') { + const eventCallbackData: SentryRequestCallbackData = JSON.parse( + Buffer.from(eventContents, 'base64').toString('utf8'), + ); + const callbackResult = callback(eventCallbackData); + if (typeof callbackResult !== 'boolean') { + callbackResult.then( + match => { + if (match) { + response.destroy(); + resolve(eventCallbackData); + } + }, + err => { + throw err; + }, + ); + } else if (callbackResult) { + response.destroy(); + resolve(eventCallbackData); + } + eventContents = ''; + } else { + eventContents = eventContents.concat(char); + } + }); + }); + }); + + request.end(); + }); +} + +export function waitForEnvelopeItem( + proxyServerName: string, + callback: (envelopeItem: EnvelopeItem) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForRequest(proxyServerName, async eventData => { + const envelopeItems = eventData.envelope[1]; + for (const envelopeItem of envelopeItems) { + if (await callback(envelopeItem)) { + resolve(envelopeItem); + return true; + } + } + return false; + }).catch(reject); + }); +} + +export function waitForError( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +export function waitForTransaction( + proxyServerName: string, + callback: (transactionEvent: Event) => Promise | boolean, +): Promise { + return new Promise((resolve, reject) => { + waitForEnvelopeItem(proxyServerName, async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { + resolve(envelopeItemBody as Event); + return true; + } + return false; + }).catch(reject); + }); +} + +const TEMP_FILE_PREFIX = 'event-proxy-server-'; + +async function registerCallbackServerPort(serverName: string, port: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + await writeFile(tmpFilePath, port, { encoding: 'utf8' }); +} + +async function retrieveCallbackServerPort(serverName: string): Promise { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return await readFile(tmpFilePath, 'utf8'); +} diff --git a/packages/e2e-tests/test-applications/sveltekit-2/package.json b/packages/e2e-tests/test-applications/sveltekit-2/package.json new file mode 100644 index 000000000000..e10818e519e6 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -0,0 +1,41 @@ +{ + "name": "sveltekit-2.0", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "clean": "npx rimraf node_modules,pnpm-lock.yaml", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm -v" + }, + "dependencies": { + "@sentry/sveltekit": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.27.1", + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/adapter-node": "^2.0.0", + "@sveltejs/kit": "^2.0.0", + "svelte": "^4.2.8", + "svelte-check": "^3.6.0", + "ts-node": "10.9.1", + "typescript": "^5.0.0", + "vite": "^5.0.3", + "wait-port": "1.0.4" + }, + "pnpm": { + "overrides": { + "@sentry/node": "latest || *", + "@sentry/tracing": "latest || *" + } + }, + "type": "module" +} diff --git a/packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts b/packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts new file mode 100644 index 000000000000..bfa29df7d549 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts @@ -0,0 +1,71 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const port = 3030; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './test', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 10000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* `next dev` is incredibly buggy with the app dir */ + retries: testEnv === 'development' ? 3 : 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${port}`, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: 'pnpm ts-node --esm start-event-proxy.ts', + port: 3031, + }, + { + command: + testEnv === 'development' + ? `pnpm wait-port ${port} && pnpm dev --port ${port}` + : `pnpm wait-port ${port} && pnpm preview --port ${port}`, + port, + }, + ], +}; + +export default config; diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/app.html b/packages/e2e-tests/test-applications/sveltekit-2/src/app.html new file mode 100644 index 000000000000..117bd026151a --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.client.ts b/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.client.ts new file mode 100644 index 000000000000..bfe90b150886 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.client.ts @@ -0,0 +1,16 @@ +import { env } from '$env/dynamic/public'; +import * as Sentry from '@sentry/sveltekit'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: env.PUBLIC_E2E_TEST_DSN, + debug: true, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, +}); + +const myErrorHandler = ({ error, event }: any) => { + console.error('An error occurred on the client side:', error, event); +}; + +export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts b/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts new file mode 100644 index 000000000000..ae99e0e0e7b4 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts @@ -0,0 +1,18 @@ +import { E2E_TEST_DSN } from '$env/static/private'; +import * as Sentry from '@sentry/sveltekit'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: E2E_TEST_DSN, + debug: true, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, +}); + +const myErrorHandler = ({ error, event }: any) => { + console.error('An error occurred on the server side:', error, event); +}; + +export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); + +export const handle = Sentry.sentryHandle(); diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte new file mode 100644 index 000000000000..5982b0ae37dd --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit kit.svelte.dev to read the documentation

diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.server.ts b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.server.ts new file mode 100644 index 000000000000..b07376ba97c9 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.server.ts @@ -0,0 +1,5 @@ +import type { PageServerLoad } from './$types'; + +export const load = (async _event => { + return { name: 'building (server)' }; +}) satisfies PageServerLoad; diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte new file mode 100644 index 000000000000..fde274c60705 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte @@ -0,0 +1,6 @@ +

Check Build

+ +

+ This route only exists to check that Typescript definitions + and auto instrumentation are working when the project is built. +

diff --git a/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.ts b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.ts new file mode 100644 index 000000000000..049acdc1fafa --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.ts @@ -0,0 +1,5 @@ +import type { PageLoad } from './$types'; + +export const load = (async _event => { + return { name: 'building' }; +}) satisfies PageLoad; diff --git a/packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts b/packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts new file mode 100644 index 000000000000..3af64eb5960a --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/start-event-proxy.ts @@ -0,0 +1,6 @@ +import { startEventProxyServer } from './event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'sveltekit-2', +}); diff --git a/packages/e2e-tests/test-applications/sveltekit-2/static/favicon.png b/packages/e2e-tests/test-applications/sveltekit-2/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH { + const pageloadTransactionEventPromise = waitForTransaction('sveltekit', (transactionEvent: any) => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + + const transactionEvent = await pageloadTransactionEventPromise; + const transactionEventId = transactionEvent.event_id; + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); diff --git a/packages/e2e-tests/test-applications/sveltekit-2/tsconfig.json b/packages/e2e-tests/test-applications/sveltekit-2/tsconfig.json new file mode 100644 index 000000000000..12aa7328fc83 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "allowImportingTsExtensions": true + }, + "ts-node": { + "esm": true, + "experimentalSpecifierResolution": "node" + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/packages/e2e-tests/test-applications/sveltekit-2/vite.config.js b/packages/e2e-tests/test-applications/sveltekit-2/vite.config.js new file mode 100644 index 000000000000..1a410bee7e11 --- /dev/null +++ b/packages/e2e-tests/test-applications/sveltekit-2/vite.config.js @@ -0,0 +1,12 @@ +import { sentrySvelteKit } from '@sentry/sveltekit'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sentrySvelteKit({ + autoUploadSourceMaps: false, + }), + sveltekit(), + ], +}); From cef3621ce44067992a9ab530d81ef6966bf2b9d3 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 19 Dec 2023 13:35:20 +0100 Subject: [PATCH 32/34] ref(sveltekit): Improve SvelteKit 2.0 404 server error handling (#9901) Improve our logic to filter out "Not Found" errors in our server-side `handleError` hook wrapper: - We now use SvelteKit 2.0 - native [error properties](https://kit.svelte.dev/docs/migrating-to-sveltekit-2#improved-error-handling) (`status` ~and `message`~) to check for "Not Found" errors - Adjusted types for type safety and backwards compatibility with SvelteKit 1.x where the ~two properties~ property don't exist. - Updated Sveltekit to 2.0 in our dev dependency to work with latest types. --- packages/sveltekit/package.json | 6 +- packages/sveltekit/src/client/handleError.ts | 5 +- packages/sveltekit/src/server/handleError.ts | 35 +- packages/sveltekit/test/common/utils.test.ts | 2 +- .../sveltekit/test/server/handleError.test.ts | 32 +- yarn.lock | 475 +++++++++++++++--- 6 files changed, 458 insertions(+), 97 deletions(-) diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 9458ce12e770..bff5f1f06dec 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -42,10 +42,10 @@ "sorcery": "0.11.0" }, "devDependencies": { - "@sveltejs/kit": "^1.11.0", + "@sveltejs/kit": "^2.0.2", "rollup": "^3.20.2", - "svelte": "^3.44.0", - "vite": "4.0.5" + "svelte": "^4.2.8", + "vite": "^5.0.10" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/sveltekit/src/client/handleError.ts b/packages/sveltekit/src/client/handleError.ts index ae5cd84f17c8..34c235ee43df 100644 --- a/packages/sveltekit/src/client/handleError.ts +++ b/packages/sveltekit/src/client/handleError.ts @@ -11,13 +11,16 @@ function defaultErrorHandler({ error }: Parameters[0]): Retur }); } +// TODO: add backwards-compatible type for kit 1.x (soon) +type HandleClientErrorInput = Parameters[0]; + /** * Wrapper for the SvelteKit error handler that sends the error to Sentry. * * @param handleError The original SvelteKit error handler. */ export function handleErrorWithSentry(handleError: HandleClientError = defaultErrorHandler): HandleClientError { - return (input: { error: unknown; event: NavigationEvent }): ReturnType => { + return (input: HandleClientErrorInput): ReturnType => { captureException(input.error, { mechanism: { type: 'sveltekit', diff --git a/packages/sveltekit/src/server/handleError.ts b/packages/sveltekit/src/server/handleError.ts index c89cbaaecc0f..1289e76a5ee2 100644 --- a/packages/sveltekit/src/server/handleError.ts +++ b/packages/sveltekit/src/server/handleError.ts @@ -1,5 +1,5 @@ import { captureException } from '@sentry/node'; -import type { HandleServerError, RequestEvent } from '@sveltejs/kit'; +import type { HandleServerError } from '@sveltejs/kit'; import { flushIfServerless } from './utils'; @@ -11,14 +11,28 @@ function defaultErrorHandler({ error }: Parameters[0]): Retur console.error(error && error.stack); } +type HandleServerErrorInput = Parameters[0]; + +/** + * Backwards-compatible HandleServerError Input type for SvelteKit 1.x and 2.x + * `message` and `status` were added in 2.x. + * For backwards-compatibility, we make them optional + * + * @see https://kit.svelte.dev/docs/migrating-to-sveltekit-2#improved-error-handling + */ +type SafeHandleServerErrorInput = Omit & + Partial>; + /** * Wrapper for the SvelteKit error handler that sends the error to Sentry. * * @param handleError The original SvelteKit error handler. */ export function handleErrorWithSentry(handleError: HandleServerError = defaultErrorHandler): HandleServerError { - return async (input: { error: unknown; event: RequestEvent }): Promise => { + return async (input: SafeHandleServerErrorInput): Promise => { if (isNotFoundError(input)) { + // We're extra cautious with SafeHandleServerErrorInput - this type is not compatible with HandleServerErrorInput + // @ts-expect-error - we're still passing the same object, just with a different (backwards-compatible) type return handleError(input); } @@ -31,19 +45,26 @@ export function handleErrorWithSentry(handleError: HandleServerError = defaultEr await flushIfServerless(); + // We're extra cautious with SafeHandleServerErrorInput - this type is not compatible with HandleServerErrorInput + // @ts-expect-error - we're still passing the same object, just with a different (backwards-compatible) type return handleError(input); }; } /** * When a page request fails because the page is not found, SvelteKit throws a "Not found" error. - * In the error handler here, we can't access the response yet (which we do in the load instrumentation), - * so we have to check if the error is a "Not found" error by checking if the route id is missing and - * by checking the error message on top of the raw stack trace. */ -function isNotFoundError(input: { error: unknown; event: RequestEvent }): boolean { - const { error, event } = input; +function isNotFoundError(input: SafeHandleServerErrorInput): boolean { + const { error, event, status } = input; + + // SvelteKit 2.0 offers a reliable way to check for a Not Found error: + if (status === 404) { + return true; + } + // SvelteKit 1.x doesn't offer a reliable way to check for a Not Found error. + // So we check the route id (shouldn't exist) and the raw stack trace + // We can delete all of this below whenever we drop Kit 1.x support const hasNoRouteId = !event.route || !event.route.id; const rawStack: string = diff --git a/packages/sveltekit/test/common/utils.test.ts b/packages/sveltekit/test/common/utils.test.ts index 5581fe60c5e4..f048494881ec 100644 --- a/packages/sveltekit/test/common/utils.test.ts +++ b/packages/sveltekit/test/common/utils.test.ts @@ -34,7 +34,7 @@ describe('isHttpError', () => { expect(isHttpError(httpErrorObject)).toBe(true); }); - it.each([new Error(), redirect(301, '/users/id'), 'string error', { status: 404 }, { body: 'Not found' }])( + it.each([new Error(), { status: 301, message: '/users/id' }, 'string error', { status: 404 }, { body: 'Not found' }])( 'returns `false` for other thrown objects (%s)', httpErrorObject => { expect(isHttpError(httpErrorObject)).toBe(false); diff --git a/packages/sveltekit/test/server/handleError.test.ts b/packages/sveltekit/test/server/handleError.test.ts index 157108a8b68a..611fac1f9a4d 100644 --- a/packages/sveltekit/test/server/handleError.test.ts +++ b/packages/sveltekit/test/server/handleError.test.ts @@ -26,7 +26,7 @@ describe('handleError', () => { consoleErrorSpy.mockClear(); }); - it('doesn\'t capture "Not found" errors for incorrect navigations', async () => { + it('doesn\'t capture "Not found" errors for incorrect navigations [Kit 1.x]', async () => { const wrappedHandleError = handleErrorWithSentry(); const mockError = new Error('Not found: /asdf/123'); const mockEvent = { @@ -35,6 +35,7 @@ describe('handleError', () => { // ... } as RequestEvent; + // @ts-expect-error - purposefully omitting status and message to cover SvelteKit 1.x compatibility const returnVal = await wrappedHandleError({ error: mockError, event: mockEvent }); expect(returnVal).not.toBeDefined(); @@ -42,11 +43,31 @@ describe('handleError', () => { expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); + it('doesn\'t capture "Not found" errors for incorrect navigations [Kit 2.x]', async () => { + const wrappedHandleError = handleErrorWithSentry(); + + const returnVal = await wrappedHandleError({ + error: new Error('404 /asdf/123'), + event: requestEvent, + status: 404, + message: 'Not Found', + }); + + expect(returnVal).not.toBeDefined(); + expect(mockCaptureException).toHaveBeenCalledTimes(0); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + }); + describe('calls captureException', () => { it('invokes the default handler if no handleError func is provided', async () => { const wrappedHandleError = handleErrorWithSentry(); const mockError = new Error('test'); - const returnVal = await wrappedHandleError({ error: mockError, event: requestEvent }); + const returnVal = await wrappedHandleError({ + error: mockError, + event: requestEvent, + status: 500, + message: 'Internal Error', + }); expect(returnVal).not.toBeDefined(); expect(mockCaptureException).toHaveBeenCalledTimes(1); @@ -58,7 +79,12 @@ describe('handleError', () => { it('invokes the user-provided error handler', async () => { const wrappedHandleError = handleErrorWithSentry(handleError); const mockError = new Error('test'); - const returnVal = (await wrappedHandleError({ error: mockError, event: requestEvent })) as any; + const returnVal = (await wrappedHandleError({ + error: mockError, + event: requestEvent, + status: 500, + message: 'Internal Error', + })) as any; expect(returnVal.message).toEqual('Whoops!'); expect(mockCaptureException).toHaveBeenCalledTimes(1); diff --git a/yarn.lock b/yarn.lock index d130053d19d2..81114ac992a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,7 +17,7 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" -"@ampproject/remapping@^2.2.0": +"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== @@ -2744,6 +2744,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.4.tgz#74752a09301b8c6b9a415fbda9fb71406a62a7b7" integrity sha512-mRsi2vJsk4Bx/AFsNBqOH2fqedxn5L/moT58xgg51DjX1la64Z3Npicut2VbhvDFO26qjWtPMsVxCd80YTFVeg== +"@esbuild/android-arm64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.9.tgz#683794bdc3d27222d3eced7b74cad15979548031" + integrity sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ== + "@esbuild/android-arm@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" @@ -2759,6 +2764,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.4.tgz#c27363e1e280e577d9b5c8fa7c7a3be2a8d79bf5" integrity sha512-uBIbiYMeSsy2U0XQoOGVVcpIktjLMEKa7ryz2RLr7L/vTnANNEsPVAh4xOv7ondGz6ac1zVb0F8Jx20rQikffQ== +"@esbuild/android-arm@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.9.tgz#21a4de41f07b2af47401c601d64dfdefd056c595" + integrity sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA== + "@esbuild/android-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" @@ -2774,6 +2784,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.4.tgz#6c9ee03d1488973d928618100048b75b147e0426" integrity sha512-4iPufZ1TMOD3oBlGFqHXBpa3KFT46aLl6Vy7gwed0ZSYgHaZ/mihbYb4t7Z9etjkC9Al3ZYIoOaHrU60gcMy7g== +"@esbuild/android-x64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.9.tgz#e2d7674bc025ddc8699f0cc76cb97823bb63c252" + integrity sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA== + "@esbuild/darwin-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" @@ -2789,6 +2804,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.4.tgz#64e2ee945e5932cd49812caa80e8896e937e2f8b" integrity sha512-Lviw8EzxsVQKpbS+rSt6/6zjn9ashUZ7Tbuvc2YENgRl0yZTktGlachZ9KMJUsVjZEGFVu336kl5lBgDN6PmpA== +"@esbuild/darwin-arm64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.9.tgz#ae7a582289cc5c0bac15d4b9020a90cb7288f1e9" + integrity sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw== + "@esbuild/darwin-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" @@ -2804,6 +2824,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.4.tgz#d8e26e1b965df284692e4d1263ba69a49b39ac7a" integrity sha512-YHbSFlLgDwglFn0lAO3Zsdrife9jcQXQhgRp77YiTDja23FrC2uwnhXMNkAucthsf+Psr7sTwYEryxz6FPAVqw== +"@esbuild/darwin-x64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.9.tgz#8a216c66dcf51addeeb843d8cfaeff712821d12b" + integrity sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ== + "@esbuild/freebsd-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" @@ -2819,6 +2844,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.4.tgz#29751a41b242e0a456d89713b228f1da4f45582f" integrity sha512-vz59ijyrTG22Hshaj620e5yhs2dU1WJy723ofc+KUgxVCM6zxQESmWdMuVmUzxtGqtj5heHyB44PjV/HKsEmuQ== +"@esbuild/freebsd-arm64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.9.tgz#63d4f603e421252c3cd836b18d01545be7c6c440" + integrity sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g== + "@esbuild/freebsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" @@ -2834,6 +2864,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.4.tgz#873edc0f73e83a82432460ea59bf568c1e90b268" integrity sha512-3sRbQ6W5kAiVQRBWREGJNd1YE7OgzS0AmOGjDmX/qZZecq8NFlQsQH0IfXjjmD0XtUYqr64e0EKNFjMUlPL3Cw== +"@esbuild/freebsd-x64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.9.tgz#a3db52595be65360eae4de1d1fa3c1afd942e1e4" + integrity sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA== + "@esbuild/linux-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" @@ -2849,6 +2884,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.4.tgz#659f2fa988d448dbf5010b5cc583be757cc1b914" integrity sha512-ZWmWORaPbsPwmyu7eIEATFlaqm0QGt+joRE9sKcnVUG3oBbr/KYdNE2TnkzdQwX6EDRdg/x8Q4EZQTXoClUqqA== +"@esbuild/linux-arm64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.9.tgz#4ae5811ce9f8d7df5eb9edd9765ea9401a534f13" + integrity sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ== + "@esbuild/linux-arm@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" @@ -2864,6 +2904,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.4.tgz#d5b13a7ec1f1c655ce05c8d319b3950797baee55" integrity sha512-z/4ArqOo9EImzTi4b6Vq+pthLnepFzJ92BnofU1jgNlcVb+UqynVFdoXMCFreTK7FdhqAzH0vmdwW5373Hm9pg== +"@esbuild/linux-arm@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.9.tgz#9807e92cfd335f46326394805ad488e646e506f2" + integrity sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw== + "@esbuild/linux-ia32@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" @@ -2879,6 +2924,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.4.tgz#878cd8bf24c9847c77acdb5dd1b2ef6e4fa27a82" integrity sha512-EGc4vYM7i1GRUIMqRZNCTzJh25MHePYsnQfKDexD8uPTCm9mK56NIL04LUfX2aaJ+C9vyEp2fJ7jbqFEYgO9lQ== +"@esbuild/linux-ia32@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.9.tgz#18892c10f3106652b16f9da88a0362dc95ed46c7" + integrity sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q== + "@esbuild/linux-loong64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" @@ -2894,6 +2944,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.4.tgz#df890499f6e566b7de3aa2361be6df2b8d5fa015" integrity sha512-WVhIKO26kmm8lPmNrUikxSpXcgd6HDog0cx12BUfA2PkmURHSgx9G6vA19lrlQOMw+UjMZ+l3PpbtzffCxFDRg== +"@esbuild/linux-loong64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.9.tgz#dc2ebf9a125db0a1bba18c2bbfd4fbdcbcaf61c2" + integrity sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA== + "@esbuild/linux-mips64el@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" @@ -2909,6 +2964,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.4.tgz#76eae4e88d2ce9f4f1b457e93892e802851b6807" integrity sha512-keYY+Hlj5w86hNp5JJPuZNbvW4jql7c1eXdBUHIJGTeN/+0QFutU3GrS+c27L+NTmzi73yhtojHk+lr2+502Mw== +"@esbuild/linux-mips64el@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.9.tgz#4c2f7c5d901015e3faf1563c4a89a50776cb07fd" + integrity sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw== + "@esbuild/linux-ppc64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" @@ -2924,6 +2984,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.4.tgz#c49032f4abbcfa3f747b543a106931fe3dce41ff" integrity sha512-tQ92n0WMXyEsCH4m32S21fND8VxNiVazUbU4IUGVXQpWiaAxOBvtOtbEt3cXIV3GEBydYsY8pyeRMJx9kn3rvw== +"@esbuild/linux-ppc64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.9.tgz#8385332713b4e7812869622163784a5633f76fc4" + integrity sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ== + "@esbuild/linux-riscv64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" @@ -2939,6 +3004,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.4.tgz#0f815a090772138503ee0465a747e16865bf94b1" integrity sha512-tRRBey6fG9tqGH6V75xH3lFPpj9E8BH+N+zjSUCnFOX93kEzqS0WdyJHkta/mmJHn7MBaa++9P4ARiU4ykjhig== +"@esbuild/linux-riscv64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.9.tgz#23f1db24fa761be311874f32036c06249aa20cba" + integrity sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg== + "@esbuild/linux-s390x@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" @@ -2954,6 +3024,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.4.tgz#8d2cca20cd4e7c311fde8701d9f1042664f8b92b" integrity sha512-152aLpQqKZYhThiJ+uAM4PcuLCAOxDsCekIbnGzPKVBRUDlgaaAfaUl5NYkB1hgY6WN4sPkejxKlANgVcGl9Qg== +"@esbuild/linux-s390x@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.9.tgz#2dffe497726b897c9f0109e774006e25b33b4fd0" + integrity sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw== + "@esbuild/linux-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" @@ -2969,6 +3044,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.4.tgz#f618bec2655de49bff91c588777e37b5e3169d4a" integrity sha512-Mi4aNA3rz1BNFtB7aGadMD0MavmzuuXNTaYL6/uiYIs08U7YMPETpgNn5oue3ICr+inKwItOwSsJDYkrE9ekVg== +"@esbuild/linux-x64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.9.tgz#ceb1d62cd830724ff5b218e5d3172a8bad59420e" + integrity sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A== + "@esbuild/netbsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" @@ -2984,6 +3064,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.4.tgz#7889744ca4d60f1538d62382b95e90a49687cef2" integrity sha512-9+Wxx1i5N/CYo505CTT7T+ix4lVzEdz0uCoYGxM5JDVlP2YdDC1Bdz+Khv6IbqmisT0Si928eAxbmGkcbiuM/A== +"@esbuild/netbsd-x64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.9.tgz#0cbca65e9ef4d3fc41502d3e055e6f49479a8f18" + integrity sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug== + "@esbuild/openbsd-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" @@ -2999,6 +3084,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.4.tgz#c3e436eb9271a423d2e8436fcb120e3fd90e2b01" integrity sha512-MFsHleM5/rWRW9EivFssop+OulYVUoVcqkyOkjiynKBCGBj9Lihl7kh9IzrreDyXa4sNkquei5/DTP4uCk25xw== +"@esbuild/openbsd-x64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.9.tgz#1f57adfbee09c743292c6758a3642e875bcad1cf" + integrity sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw== + "@esbuild/sunos-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" @@ -3014,6 +3104,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.4.tgz#f63f5841ba8c8c1a1c840d073afc99b53e8ce740" integrity sha512-6Xq8SpK46yLvrGxjp6HftkDwPP49puU4OF0hEL4dTxqCbfx09LyrbUj/D7tmIRMj5D5FCUPksBbxyQhp8tmHzw== +"@esbuild/sunos-x64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.9.tgz#116be6adbd2c7479edeeb5f6ea0441002ab4cb9c" + integrity sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw== + "@esbuild/win32-arm64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" @@ -3029,6 +3124,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.4.tgz#80be69cec92da4da7781cf7a8351b95cc5a236b0" integrity sha512-PkIl7Jq4mP6ke7QKwyg4fD4Xvn8PXisagV/+HntWoDEdmerB2LTukRZg728Yd1Fj+LuEX75t/hKXE2Ppk8Hh1w== +"@esbuild/win32-arm64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.9.tgz#2be22131ab18af4693fd737b161d1ef34de8ca9d" + integrity sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg== + "@esbuild/win32-ia32@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" @@ -3044,6 +3144,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.4.tgz#15dc0ed83d2794872b05d8edc4a358fecf97eb54" integrity sha512-ga676Hnvw7/ycdKB53qPusvsKdwrWzEyJ+AtItHGoARszIqvjffTwaaW3b2L6l90i7MO9i+dlAW415INuRhSGg== +"@esbuild/win32-ia32@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.9.tgz#e10ead5a55789b167b4225d2469324538768af7c" + integrity sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg== + "@esbuild/win32-x64@0.16.17": version "0.16.17" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" @@ -3059,6 +3164,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.4.tgz#d46a6e220a717f31f39ae80f49477cc3220be0f0" integrity sha512-HP0GDNla1T3ZL8Ko/SHAS2GgtjOg+VmWnnYLhuTksr++EnduYB0f3Y2LzHsUwb2iQ13JGoY6G3R8h6Du/WG6uA== +"@esbuild/win32-x64@0.19.9": + version "0.19.9" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.9.tgz#b2da6219b603e3fa371a78f53f5361260d0c5585" + integrity sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ== + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -3874,6 +3984,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.18": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jsdevtools/coverage-istanbul-loader@3.0.5": version "3.0.5" resolved "https://registry.yarnpkg.com/@jsdevtools/coverage-istanbul-loader/-/coverage-istanbul-loader-3.0.5.tgz#2a4bc65d0271df8d4435982db4af35d81754ee26" @@ -5016,6 +5134,71 @@ estree-walker "^2.0.2" picomatch "^2.3.1" +"@rollup/rollup-android-arm-eabi@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.1.tgz#beaf518ee45a196448e294ad3f823d2d4576cf35" + integrity sha512-6vMdBZqtq1dVQ4CWdhFwhKZL6E4L1dV6jUjuBvsavvNJSppzi6dLBbuV+3+IyUREaj9ZFvQefnQm28v4OCXlig== + +"@rollup/rollup-android-arm64@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.1.tgz#6f76cfa759c2d0fdb92122ffe28217181a1664eb" + integrity sha512-Jto9Fl3YQ9OLsTDWtLFPtaIMSL2kwGyGoVCmPC8Gxvym9TCZm4Sie+cVeblPO66YZsYH8MhBKDMGZ2NDxuk/XQ== + +"@rollup/rollup-darwin-arm64@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.1.tgz#9aaefe33a5481d66322d1c62f368171c03eabe2b" + integrity sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA== + +"@rollup/rollup-darwin-x64@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.1.tgz#707dcaadcdc6bd3fd6c69f55d9456cd4446306a3" + integrity sha512-KyP/byeXu9V+etKO6Lw3E4tW4QdcnzDG/ake031mg42lob5tN+5qfr+lkcT/SGZaH2PdW4Z1NX9GHEkZ8xV7og== + +"@rollup/rollup-linux-arm-gnueabihf@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.1.tgz#7a4dbbd1dd98731d88a55aefcef0ec4c578fa9c7" + integrity sha512-Yqz/Doumf3QTKplwGNrCHe/B2p9xqDghBZSlAY0/hU6ikuDVQuOUIpDP/YcmoT+447tsZTmirmjgG3znvSCR0Q== + +"@rollup/rollup-linux-arm64-gnu@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.1.tgz#967ba8e6f68a5f21bd00cd97773dcdd6107e94ed" + integrity sha512-u3XkZVvxcvlAOlQJ3UsD1rFvLWqu4Ef/Ggl40WAVCuogf4S1nJPHh5RTgqYFpCOvuGJ7H5yGHabjFKEZGExk5Q== + +"@rollup/rollup-linux-arm64-musl@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.1.tgz#d3a4e1c9f21eef3b9f4e4989f334a519a1341462" + integrity sha512-0XSYN/rfWShW+i+qjZ0phc6vZ7UWI8XWNz4E/l+6edFt+FxoEghrJHjX1EY/kcUGCnZzYYRCl31SNdfOi450Aw== + +"@rollup/rollup-linux-riscv64-gnu@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.1.tgz#415c0533bb752164effd05f5613858e8f6779bc9" + integrity sha512-LmYIO65oZVfFt9t6cpYkbC4d5lKHLYv5B4CSHRpnANq0VZUQXGcCPXHzbCXCz4RQnx7jvlYB1ISVNCE/omz5cw== + +"@rollup/rollup-linux-x64-gnu@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.1.tgz#0983385dd753a2e0ecaddea7a81dd37fea5114f5" + integrity sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg== + +"@rollup/rollup-linux-x64-musl@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.1.tgz#eb7494ebc5199cbd2e5c38c2b8acbe2603f35e03" + integrity sha512-t4QSR7gN+OEZLG0MiCgPqMWZGwmeHhsM4AkegJ0Kiy6TnJ9vZ8dEIwHw1LcZKhbHxTY32hp9eVCMdR3/I8MGRw== + +"@rollup/rollup-win32-arm64-msvc@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.1.tgz#5bebc66e3a7f82d4b9aa9ff448e7fc13a69656e9" + integrity sha512-7XI4ZCBN34cb+BH557FJPmh0kmNz2c25SCQeT9OiFWEgf8+dL6ZwJ8f9RnUIit+j01u07Yvrsuu1rZGxJCc51g== + +"@rollup/rollup-win32-ia32-msvc@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.1.tgz#34156ebf8b4de3b20e6497260fe519a30263f8cf" + integrity sha512-yE5c2j1lSWOH5jp+Q0qNL3Mdhr8WuqCNVjc6BxbVfS5cAS6zRmdiw7ktb8GNpDCEUJphILY6KACoFoRtKoqNQg== + +"@rollup/rollup-win32-x64-msvc@4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz#d146db7a5949e10837b323ce933ed882ac878262" + integrity sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA== + "@schematics/angular@10.2.4": version "10.2.4" resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-10.2.4.tgz#3b99b9da572b57381d221e2008804e6bb9c98b82" @@ -5411,36 +5594,22 @@ dependencies: highlight.js "^9.15.6" -"@sveltejs/kit@^1.11.0": - version "1.15.2" - resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.15.2.tgz#2d351b15aa39ab792c36c2c236c7e31a2010a6b0" - integrity sha512-rLNxZrjbrlPf8AWW8GAU4L/Vvu17e9v8EYl7pUip7x72lTft7RcxeP3z7tsrHpMSBBxC9o4XdKzFvz1vMZyXZw== +"@sveltejs/kit@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-2.0.2.tgz#bd02523fe570ddaf89148bffb1eb2233c458054b" + integrity sha512-xFpnLxVQ4KgCbj4Cj2zCFUcyfAoO87nn4nf3XcGJ7ZtOwy20tZ91vXWrtyuum8hakJWVwdNYyGXG9aBoIEYpFQ== dependencies: - "@sveltejs/vite-plugin-svelte" "^2.0.0" - "@types/cookie" "^0.5.1" - cookie "^0.5.0" - devalue "^4.3.0" + "@types/cookie" "^0.6.0" + cookie "^0.6.0" + devalue "^4.3.2" esm-env "^1.0.0" kleur "^4.1.5" - magic-string "^0.30.0" - mime "^3.0.0" + magic-string "^0.30.5" + mrmime "^1.0.1" sade "^1.8.1" - set-cookie-parser "^2.5.1" - sirv "^2.0.2" + set-cookie-parser "^2.6.0" + sirv "^2.0.3" tiny-glob "^0.2.9" - undici "5.20.0" - -"@sveltejs/vite-plugin-svelte@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.0.3.tgz#3d276eab341638dd58691a3de610774e155a7578" - integrity sha512-o+cguBFdwIGtRbNkYOyqTM7KvRUffxh5bfK4oJsWKG2obu+v/cbpT03tJrGl58C7tRXo/aEC0/axN5FVHBj0nA== - dependencies: - debug "^4.3.4" - deepmerge "^4.3.0" - kleur "^4.1.5" - magic-string "^0.29.0" - svelte-hmr "^0.15.1" - vitefu "^0.2.4" "@szmarczak/http-timer@^1.1.2": version "1.1.2" @@ -5660,10 +5829,10 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== -"@types/cookie@^0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.5.1.tgz#b29aa1f91a59f35e29ff8f7cb24faf1a3a750554" - integrity sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g== +"@types/cookie@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" + integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== "@types/cors@2.8.12", "@types/cors@^2.8.12": version "2.8.12" @@ -5914,6 +6083,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@^1.0.1": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/express-serve-static-core@4.17.31": version "4.17.31" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" @@ -7470,6 +7644,11 @@ acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== +acorn@^8.9.0: + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -7939,6 +8118,13 @@ aria-query@^5.0.0, aria-query@^5.0.2: resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.2.tgz#0b8a744295271861e1d933f8feca13f9b70cfdc1" integrity sha512-eigU3vhqSO+Z8BKDnVLN/ompjhf3pYzecKXz8+whRy+9gZu8n1TCGfwzQUUPnqdHl9ax1Hr9031orZ+UOEYr7Q== +aria-query@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + arity-n@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" @@ -8438,6 +8624,13 @@ axios@^1.2.2: form-data "^4.0.0" proxy-from-env "^1.1.0" +axobject-query@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== + dependencies: + dequal "^2.0.3" + b4a@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.4.tgz#ef1c1422cae5ce6535ec191baeed7567443f36c9" @@ -10276,13 +10469,6 @@ bun-types@latest: resolved "https://registry.yarnpkg.com/bun-types/-/bun-types-1.0.1.tgz#8bcb10ae3a1548a39f0932fdb365f4b3a649efba" integrity sha512-7NrXqhMIaNKmWn2dSWEQ50znMZqrN/5Z0NBMXvQTRu/+Y1CvoXRznFy0pnqLe024CeZgVdXoEpARNO1JZLAPGw== -busboy@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" - integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== - dependencies: - streamsearch "^1.1.0" - byte-size@8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-8.1.1.tgz#3424608c62d59de5bfda05d31e0313c6174842ae" @@ -11109,6 +11295,17 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +code-red@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/code-red/-/code-red-1.0.4.tgz#59ba5c9d1d320a4ef795bc10a28bd42bfebe3e35" + integrity sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + "@types/estree" "^1.0.1" + acorn "^8.10.0" + estree-walker "^3.0.3" + periscopic "^3.1.0" + codecov@^3.6.5: version "3.8.1" resolved "https://registry.yarnpkg.com/codecov/-/codecov-3.8.1.tgz#06fe026b75525ed1ce864d4a34f1010c52c51546" @@ -11584,6 +11781,11 @@ cookie@^0.4.1, cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookie@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + copy-anything@^2.0.1: version "2.0.6" resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" @@ -12000,6 +12202,14 @@ css-tree@^2.0.4: mdn-data "2.0.28" source-map-js "^1.0.1" +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + css-what@^3.2.1: version "3.4.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" @@ -12471,11 +12681,6 @@ deepmerge@^4.2.2: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== -deepmerge@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" - integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== - default-gateway@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" @@ -12600,7 +12805,7 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -dequal@^2.0.0: +dequal@^2.0.0, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -12747,11 +12952,6 @@ deterministic-object-hash@^1.3.1: resolved "https://registry.yarnpkg.com/deterministic-object-hash/-/deterministic-object-hash-1.3.1.tgz#8df6723f71d005600041aad39054b35ecdf536ac" integrity sha512-kQDIieBUreEgY+akq0N7o4FzZCr27dPG1xr3wq267vPwDlSXQ3UMcBXHqTGUBaM/5WDS1jwTYjxRhUzHeuiAvw== -devalue@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/devalue/-/devalue-4.3.0.tgz#d86db8fee63a70317c2355be0d3d1b4d8f89a44e" - integrity sha512-n94yQo4LI3w7erwf84mhRUkUJfhLoCZiLyoOZ/QFsDbcWNZePrLwbQpvZBUG2TNxwV3VjCKPxkiiQA6pe3TrTA== - devalue@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/devalue/-/devalue-4.3.2.tgz#cc44e4cf3872ac5a78229fbce3b77e57032727b5" @@ -14309,6 +14509,34 @@ esbuild@^0.19.2: "@esbuild/win32-ia32" "0.19.4" "@esbuild/win32-x64" "0.19.4" +esbuild@^0.19.3: + version "0.19.9" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.9.tgz#423a8f35153beb22c0b695da1cd1e6c0c8cdd490" + integrity sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg== + optionalDependencies: + "@esbuild/android-arm" "0.19.9" + "@esbuild/android-arm64" "0.19.9" + "@esbuild/android-x64" "0.19.9" + "@esbuild/darwin-arm64" "0.19.9" + "@esbuild/darwin-x64" "0.19.9" + "@esbuild/freebsd-arm64" "0.19.9" + "@esbuild/freebsd-x64" "0.19.9" + "@esbuild/linux-arm" "0.19.9" + "@esbuild/linux-arm64" "0.19.9" + "@esbuild/linux-ia32" "0.19.9" + "@esbuild/linux-loong64" "0.19.9" + "@esbuild/linux-mips64el" "0.19.9" + "@esbuild/linux-ppc64" "0.19.9" + "@esbuild/linux-riscv64" "0.19.9" + "@esbuild/linux-s390x" "0.19.9" + "@esbuild/linux-x64" "0.19.9" + "@esbuild/netbsd-x64" "0.19.9" + "@esbuild/openbsd-x64" "0.19.9" + "@esbuild/sunos-x64" "0.19.9" + "@esbuild/win32-arm64" "0.19.9" + "@esbuild/win32-ia32" "0.19.9" + "@esbuild/win32-x64" "0.19.9" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -14693,7 +14921,7 @@ estree-walker@^2.0.1, estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== -estree-walker@^3.0.3: +estree-walker@^3.0.0, estree-walker@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== @@ -15842,6 +16070,11 @@ fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -18199,6 +18432,13 @@ is-reference@1.2.1, is-reference@^1.2.1: dependencies: "@types/estree" "*" +is-reference@^3.0.0, is-reference@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c" + integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== + dependencies: + "@types/estree" "*" + is-regex@^1.0.4, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -20064,6 +20304,11 @@ localforage@^1.8.1: dependencies: lie "3.1.1" +locate-character@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974" + integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA== + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -20540,13 +20785,6 @@ magic-string@^0.25.0, magic-string@^0.25.1, magic-string@^0.25.7: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.29.0: - version "0.29.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.29.0.tgz#f034f79f8c43dba4ae1730ffb5e8c4e084b16cf3" - integrity sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - magic-string@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529" @@ -20561,6 +20799,13 @@ magic-string@^0.30.3, magic-string@^0.30.4: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" +magic-string@^0.30.5: + version "0.30.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" + integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + magicast@0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.2.8.tgz#02b298c65fbc5b7d1fce52ef779c59caf68cc9cf" @@ -20956,6 +21201,11 @@ mdn-data@2.0.28: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + mdn-data@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" @@ -21956,6 +22206,11 @@ mrmime@^1.0.0: resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.0.tgz#14d387f0585a5233d291baba339b063752a2398b" integrity sha512-a70zx7zFfVO7XpnQ2IX1Myh9yY4UYvfld/dikWRnsXxbyvMcfz+u6UfgNAtH+k2QqtJuzVpv6eLTx1G2+WKZbQ== +mrmime@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" + integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -22054,6 +22309,11 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -24061,6 +24321,15 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +periscopic@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" + integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^3.0.0" + is-reference "^3.0.0" + pg-connection-string@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" @@ -25297,6 +25566,15 @@ postcss@^8.4.27: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.32: + version "8.4.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9" + integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -27187,6 +27465,26 @@ rollup@^3.27.1: optionalDependencies: fsevents "~2.3.2" +rollup@^4.2.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.1.tgz#351d6c03e4e6bcd7a0339df3618d2aeeb108b507" + integrity sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw== + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.9.1" + "@rollup/rollup-android-arm64" "4.9.1" + "@rollup/rollup-darwin-arm64" "4.9.1" + "@rollup/rollup-darwin-x64" "4.9.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.9.1" + "@rollup/rollup-linux-arm64-gnu" "4.9.1" + "@rollup/rollup-linux-arm64-musl" "4.9.1" + "@rollup/rollup-linux-riscv64-gnu" "4.9.1" + "@rollup/rollup-linux-x64-gnu" "4.9.1" + "@rollup/rollup-linux-x64-musl" "4.9.1" + "@rollup/rollup-win32-arm64-msvc" "4.9.1" + "@rollup/rollup-win32-ia32-msvc" "4.9.1" + "@rollup/rollup-win32-x64-msvc" "4.9.1" + fsevents "~2.3.2" + rsvp@^3.0.14, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0: version "3.6.2" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" @@ -27632,11 +27930,16 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-cookie-parser@^2.4.8, set-cookie-parser@^2.5.1: +set-cookie-parser@^2.4.8: version "2.5.1" resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz#ddd3e9a566b0e8e0862aca974a6ac0e01349430b" integrity sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ== +set-cookie-parser@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" + integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -27862,10 +28165,10 @@ sinon@^7.3.2: nise "^1.5.2" supports-color "^5.5.0" -sirv@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.2.tgz#128b9a628d77568139cff85703ad5497c46a4760" - integrity sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w== +sirv@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.3.tgz#ca5868b87205a74bef62a469ed0296abceccd446" + integrity sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA== dependencies: "@polka/url" "^1.0.0-next.20" mrmime "^1.0.0" @@ -28628,11 +28931,6 @@ streamroller@^3.0.2: debug "^4.1.1" fs-extra "^10.0.0" -streamsearch@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" - integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== - streamx@^2.15.0: version "2.15.1" resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.15.1.tgz#396ad286d8bc3eeef8f5cea3f029e81237c024c6" @@ -29116,11 +29414,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -svelte-hmr@^0.15.1: - version "0.15.1" - resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.15.1.tgz#d11d878a0bbb12ec1cba030f580cd2049f4ec86b" - integrity sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA== - svelte-jester@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/svelte-jester/-/svelte-jester-2.3.2.tgz#9eb818da30807bbcc940b6130d15b2c34408d64f" @@ -29131,10 +29424,24 @@ svelte@3.49.0: resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029" integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA== -svelte@^3.44.0: - version "3.57.0" - resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.57.0.tgz#a3969cfe51f25f2a55e75f7b98dbd02c3af0980b" - integrity sha512-WMXEvF+RtAaclw0t3bPDTUe19pplMlfyKDsixbHQYgCWi9+O9VN0kXU1OppzrB9gPAvz4NALuoca2LfW2bOjTQ== +svelte@^4.2.8: + version "4.2.8" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.8.tgz#a279d8b6646131ffb11bc692840f8839b8ae4ed1" + integrity sha512-hU6dh1MPl8gh6klQZwK/n73GiAHiR95IkFsesLPbMeEZi36ydaXL/ZAb4g9sayT0MXzpxyZjR28yderJHxcmYA== + dependencies: + "@ampproject/remapping" "^2.2.1" + "@jridgewell/sourcemap-codec" "^1.4.15" + "@jridgewell/trace-mapping" "^0.3.18" + acorn "^8.9.0" + aria-query "^5.3.0" + axobject-query "^3.2.1" + code-red "^1.0.3" + css-tree "^2.3.1" + estree-walker "^3.0.3" + is-reference "^3.0.1" + locate-character "^3.0.0" + magic-string "^0.30.4" + periscopic "^3.1.0" svgo@^1.0.0: version "1.3.2" @@ -30242,13 +30549,6 @@ underscore@>=1.8.3: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== -undici@5.20.0: - version "5.20.0" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.20.0.tgz#6327462f5ce1d3646bcdac99da7317f455bcc263" - integrity sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g== - dependencies: - busboy "^1.6.0" - undici@^5.21.0: version "5.26.2" resolved "https://registry.yarnpkg.com/undici/-/undici-5.26.2.tgz#fa61bfe40f732540d15e58b0c1271872d8e3c995" @@ -30973,6 +31273,17 @@ vite@^4.4.9: optionalDependencies: fsevents "~2.3.2" +vite@^5.0.10: + version "5.0.10" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.0.10.tgz#1e13ef5c3cf5aa4eed81f5df6d107b3c3f1f6356" + integrity sha512-2P8J7WWgmc355HUMlFrwofacvr98DAjoE52BfdbwQtyLH06XKwaL/FMnmKM2crF0iX4MpmMKoDlNCB1ok7zHCw== + dependencies: + esbuild "^0.19.3" + postcss "^8.4.32" + rollup "^4.2.0" + optionalDependencies: + fsevents "~2.3.3" + vitefu@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.4.tgz#212dc1a9d0254afe65e579351bed4e25d81e0b35" From cf773fccb1735ff2a3152aafb5e14d75bbd7be38 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 19 Dec 2023 14:16:57 +0100 Subject: [PATCH 33/34] fix(sveltekit): Avoid capturing 404 errors on client side (#9902) Looks like 404 errors weren't passed to the client side `handleError` hook in Kit 1.x but in 2.x they're now passed into the hook. This means, we need to filter them out. --- .../sveltekit-2/package.json | 1 + packages/sveltekit/src/client/handleError.ts | 30 ++++++++++++++----- .../sveltekit/test/client/handleError.test.ts | 17 +++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/e2e-tests/test-applications/sveltekit-2/package.json b/packages/e2e-tests/test-applications/sveltekit-2/package.json index e10818e519e6..b55d9ff74df6 100644 --- a/packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -24,6 +24,7 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^2.0.0", "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.2.8", "svelte-check": "^3.6.0", "ts-node": "10.9.1", diff --git a/packages/sveltekit/src/client/handleError.ts b/packages/sveltekit/src/client/handleError.ts index 34c235ee43df..799e6e36db72 100644 --- a/packages/sveltekit/src/client/handleError.ts +++ b/packages/sveltekit/src/client/handleError.ts @@ -11,23 +11,37 @@ function defaultErrorHandler({ error }: Parameters[0]): Retur }); } -// TODO: add backwards-compatible type for kit 1.x (soon) type HandleClientErrorInput = Parameters[0]; +/** + * Backwards-compatible HandleServerError Input type for SvelteKit 1.x and 2.x + * `message` and `status` were added in 2.x. + * For backwards-compatibility, we make them optional + * + * @see https://kit.svelte.dev/docs/migrating-to-sveltekit-2#improved-error-handling + */ +type SafeHandleServerErrorInput = Omit & + Partial>; + /** * Wrapper for the SvelteKit error handler that sends the error to Sentry. * * @param handleError The original SvelteKit error handler. */ export function handleErrorWithSentry(handleError: HandleClientError = defaultErrorHandler): HandleClientError { - return (input: HandleClientErrorInput): ReturnType => { - captureException(input.error, { - mechanism: { - type: 'sveltekit', - handled: false, - }, - }); + return (input: SafeHandleServerErrorInput): ReturnType => { + // SvelteKit 2.0 offers a reliable way to check for a 404 error: + if (input.status !== 404) { + captureException(input.error, { + mechanism: { + type: 'sveltekit', + handled: false, + }, + }); + } + // We're extra cautious with SafeHandleServerErrorInput - this type is not compatible with HandleServerErrorInput + // @ts-expect-error - we're still passing the same object, just with a different (backwards-compatible) type return handleError(input); }; } diff --git a/packages/sveltekit/test/client/handleError.test.ts b/packages/sveltekit/test/client/handleError.test.ts index 0262f0b1b1cc..b4eba9ff56e9 100644 --- a/packages/sveltekit/test/client/handleError.test.ts +++ b/packages/sveltekit/test/client/handleError.test.ts @@ -38,6 +38,7 @@ describe('handleError', () => { it('invokes the default handler if no handleError func is provided', async () => { const wrappedHandleError = handleErrorWithSentry(); const mockError = new Error('test'); + // @ts-expect-error - purposefully omitting status and message to cover SvelteKit 1.x compatibility const returnVal = await wrappedHandleError({ error: mockError, event: navigationEvent }); expect(returnVal).not.toBeDefined(); @@ -50,6 +51,7 @@ describe('handleError', () => { it('invokes the user-provided error handler', async () => { const wrappedHandleError = handleErrorWithSentry(handleError); const mockError = new Error('test'); + // @ts-expect-error - purposefully omitting status and message to cover SvelteKit 1.x compatibility const returnVal = (await wrappedHandleError({ error: mockError, event: navigationEvent })) as any; expect(returnVal.message).toEqual('Whoops!'); @@ -59,4 +61,19 @@ describe('handleError', () => { expect(consoleErrorSpy).toHaveBeenCalledTimes(0); }); }); + + it("doesn't capture 404 errors", async () => { + const wrappedHandleError = handleErrorWithSentry(handleError); + const returnVal = (await wrappedHandleError({ + error: new Error('404 Not Found'), + event: navigationEvent, + status: 404, + message: 'Not Found', + })) as any; + + expect(returnVal.message).toEqual('Whoops!'); + expect(mockCaptureException).not.toHaveBeenCalled(); + // Check that the default handler wasn't invoked + expect(consoleErrorSpy).toHaveBeenCalledTimes(0); + }); }); From 9f173e165e772c2ad2f8c773a594de2df70c2a8a Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 19 Dec 2023 14:22:37 +0000 Subject: [PATCH 34/34] meta(changelog): Update changelog for 7.89.0 --- CHANGELOG.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7284b059cd4d..4345f368f6fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,74 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.89.0 + +### Important Changes + +#### Deprecations + +- **feat(core): Deprecate `configureScope` (#9887)** +- **feat(core): Deprecate `pushScope` & `popScope` (#9890)** + +This release deprecates `configureScope`, `pushScope`, and `popScope`, which will be removed in the upcoming v8 major release. + +#### Hapi Integration + +- **feat(node): Add Hapi Integration (#9539)** + +This release adds an integration for Hapi. It can be used as follows: + +```ts +const Sentry = require('@sentry/node'); +const Hapi = require('@hapi/hapi'); + +const init = async () => { + const server = Hapi.server({ + // your server configuration ... + }); + + Sentry.init({ + dsn: '__DSN__', + tracesSampleRate: 1.0, + integrations: [ + new Sentry.Integrations.Hapi({ server }), + ], + }); + + server.route({ + // your route configuration ... + }); + + await server.start(); +}; +``` + +#### SvelteKit 2.0 + +- **chore(sveltekit): Add SvelteKit 2.0 to peer dependencies (#9861)** + +This release adds support for SvelteKit 2.0 in the `@sentry/sveltekit` package. If you're upgrading from SvelteKit 1.x to 2.x and already use the Sentry SvelteKit SDK, no changes apart from upgrading to this (or a newer) version are necessary. + +### Other Changes + +- feat(core): Add type & utility for function-based integrations (#9818) +- feat(core): Update `withScope` to return callback return value (#9866) +- feat(deno): Support `Deno.CronSchedule` for cron jobs (#9880) +- feat(nextjs): Auto instrument generation functions (#9781) +- feat(nextjs): Connect server component transactions if there is no incoming trace (#9845) +- feat(node-experimental): Update to new Scope APIs (#9799) +- feat(replay): Add `canvas.type` setting (#9877) +- fix(nextjs): Export `createReduxEnhancer` (#9854) +- fix(remix): Do not capture thrown redirect responses. (#9909) +- fix(sveltekit): Add conditional exports (#9872) +- fix(sveltekit): Avoid capturing 404 errors on client side (#9902) +- fix(utils): Do not use `Event` type in worldwide (#9864) +- fix(utils): Support crypto.getRandomValues in old Chromium versions (#9251) +- fix(utils): Update `eventFromUnknownInput` to avoid scope pollution & `getCurrentHub` (#9868) +- ref: Use `addBreadcrumb` directly & allow to pass hint (#9867) + +Work in this release contributed by @adam187, and @jghinestrosa. Thank you for your contributions! + ## 7.88.0 ### Important Changes