From f5cbb04f45a8d03cd84e0c4a7f9fcf5715d04aa8 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 5 Apr 2023 15:59:20 +0200 Subject: [PATCH 01/48] test(node): Fix conditional test in `LocaVariables` tests (#7759) --- packages/node/test/integrations/localvariables.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 6cd3527b9dae..15f62f305231 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -148,7 +148,7 @@ const exceptionEvent100Frames = { }, }; -describeIf(NODE_VERSION >= 18)('LocalVariables', () => { +describeIf((NODE_VERSION.major || 0) >= 18)('LocalVariables', () => { it('Adds local variables to stack frames', async () => { expect.assertions(7); From 170ffc8c18deb212b782099d64c41079d493feb4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 5 Apr 2023 19:33:41 +0200 Subject: [PATCH 02/48] feat(core): Add async context abstraction (#7753) --- packages/core/src/hub.ts | 45 ++++++++++++++++++++++++++++++++++++++ packages/core/src/index.ts | 13 +++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 6f109dfe1ce4..1a2358b81555 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -50,6 +50,14 @@ export const API_VERSION = 4; */ const DEFAULT_BREADCRUMBS = 100; +/** + * Strategy used to track async context. + */ +export interface AsyncContextStrategy { + getCurrentHub: () => Hub | undefined; + runWithAsyncContext(callback: (hub: Hub, ...args: A[]) => T, ...args: A[]): T; +} + /** * A layer in the process stack. * @hidden @@ -66,6 +74,7 @@ export interface Layer { export interface Carrier { __SENTRY__?: { hub?: Hub; + acs?: AsyncContextStrategy; /** * Extra Hub properties injected by various SDKs */ @@ -519,6 +528,14 @@ export function getCurrentHub(): Hub { // Get main carrier (global for every environment) const registry = getMainCarrier(); + if (registry.__SENTRY__ && registry.__SENTRY__.acs) { + const hub = registry.__SENTRY__.acs.getCurrentHub(); + + if (hub) { + return hub; + } + } + // If there's no hub, or its an old API, assign a new one if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { setHubOnCarrier(registry, new Hub()); @@ -532,6 +549,34 @@ export function getCurrentHub(): Hub { return getHubFromCarrier(registry); } +/** + * @private Private API with no semver guarantees! + * + * Sets the global async context strategy + */ +export function setAsyncContextStrategy(strategy: AsyncContextStrategy): void { + // Get main carrier (global for every environment) + const registry = getMainCarrier(); + registry.__SENTRY__ = registry.__SENTRY__ || {}; + registry.__SENTRY__.acs = strategy; +} + +/** + * @private Private API with no semver guarantees! + * + * Runs the given callback function with the global async context strategy + */ +export function runWithAsyncContext(callback: (hub: Hub, ...args: A[]) => T, ...args: A[]): T { + const registry = getMainCarrier(); + + if (registry.__SENTRY__ && registry.__SENTRY__.acs) { + return registry.__SENTRY__.acs.runWithAsyncContext(callback, ...args); + } + + // if there was no strategy, fallback to just calling the callback + return callback(getCurrentHub(), ...args); +} + /** * Try to read the hub from an active domain, and fallback to the registry if one doesn't exist * @returns discovered hub diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e5ee3c623d76..ee8e624709a0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,5 @@ export type { ClientClass } from './sdk'; -export type { Carrier, Layer } from './hub'; +export type { AsyncContextStrategy, Carrier, Layer } from './hub'; export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; export * from './tracing'; @@ -18,7 +18,16 @@ export { setUser, withScope, } from './exports'; -export { getCurrentHub, getHubFromCarrier, Hub, makeMain, getMainCarrier, setHubOnCarrier } from './hub'; +export { + getCurrentHub, + getHubFromCarrier, + Hub, + makeMain, + getMainCarrier, + runWithAsyncContext, + setHubOnCarrier, + setAsyncContextStrategy, +} from './hub'; export { makeSession, closeSession, updateSession } from './session'; export { SessionFlusher } from './sessionflusher'; export { addGlobalEventProcessor, Scope } from './scope'; From 56790be44649dac85c69f31532fb48db960e3793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Thu, 6 Apr 2023 09:32:14 +0200 Subject: [PATCH 03/48] chore(browser): Update browser example (#7764) --- packages/browser/examples/README.md | 2 +- packages/browser/examples/app.js | 36 +++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/browser/examples/README.md b/packages/browser/examples/README.md index 65faec148b70..c582e5f5a371 100644 --- a/packages/browser/examples/README.md +++ b/packages/browser/examples/README.md @@ -1,5 +1,5 @@ Assuming `npm@>=5.2.0` is installed and `@sentry/browser` package is built locally: ```sh -$ npx serve -S +$ npx serve -S examples ``` diff --git a/packages/browser/examples/app.js b/packages/browser/examples/app.js index 0ab6d7960915..f9e9e54a338e 100644 --- a/packages/browser/examples/app.js +++ b/packages/browser/examples/app.js @@ -18,17 +18,35 @@ class HappyIntegration { } } -class HappyTransport extends Sentry.Transports.BaseTransport { - sendEvent(event) { +function makeHappyTransport(options) { + function makeRequest(request) { console.log( - `This is the place where you'd implement your own sending logic. It'd get url: ${this.url} and an event itself:`, - event, + `This is the place to implement your own sending logic. It'd get url: ${options.url} and a raw envelope:`, + request.body, ); - return Promise.resolve({ - status: 'success', - }); + // this is where your sending logic goes + const myCustomRequest = { + body: request.body, + method: 'POST', + referrerPolicy: 'origin', + headers: options.headers, + ...options.fetchOptions + }; + + // you define how `sendMyCustomRequest` works + const sendMyCustomRequest = (r) => fetch(options.url, r); + return sendMyCustomRequest(myCustomRequest).then(response => ({ + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + })); } + + // `createTransport` takes care of rate limiting and flushing + return Sentry.createTransport(options, makeRequest); } Sentry.init({ @@ -39,7 +57,7 @@ Sentry.init({ // An array of strings or regexps that'll be used to ignore specific errors based on their origin url denyUrls: ['external-lib.js'], // An array of strings or regexps that'll be used to allow specific errors based on their origin url - allowUrls: ['http://localhost:5000', 'https://browser.sentry-cdn'], + allowUrls: ['http://localhost:3000', 'https://browser.sentry-cdn'], // Debug mode with valuable initialization/lifecycle informations. debug: true, // Whether SDK should be enabled or not. @@ -53,7 +71,7 @@ Sentry.init({ // An environment identifier. environment: 'staging', // Custom event transport that will be used to send things to Sentry - transport: HappyTransport, + transport: makeHappyTransport, // Method called for every captured event async beforeSend(event, hint) { // Because beforeSend and beforeBreadcrumb are async, user can fetch some data From ff71b78bec5dcb5a0ce72daa84f82163e53f1b29 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 6 Apr 2023 10:06:22 +0200 Subject: [PATCH 04/48] build: Use setup node instead of volta action (#7763) --- .github/workflows/build.yml | 49 +++++++++++++++++------ .github/workflows/canary.yml | 9 +++-- .github/workflows/flaky-test-detector.yml | 5 ++- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed8c098467c3..43edc616f74b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -161,7 +161,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' # we use a hash of yarn.lock as our cache key, because if it hasn't changed, our dependencies haven't changed, # so no need to reinstall them - name: Compute dependency cache key @@ -207,8 +209,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 - + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Check dependency cache uses: actions/cache/restore@v3 with: @@ -262,7 +265,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -331,7 +336,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -352,7 +359,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -372,7 +381,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -526,7 +537,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -580,7 +593,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -631,7 +646,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -655,7 +672,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -745,7 +764,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -803,7 +824,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 2ef55cfa9b05..a0bff1d37b61 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -27,7 +27,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Install dependencies run: yarn install --ignore-engines --frozen-lockfile - name: Build packages @@ -70,8 +72,9 @@ jobs: with: ref: ${{ env.HEAD_COMMIT }} - name: Set up Node - uses: volta-cli/action@v4 - + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Install dependencies run: yarn install --ignore-engines --frozen-lockfile diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml index 51002e6a6d4b..6057361b0174 100644 --- a/.github/workflows/flaky-test-detector.yml +++ b/.github/workflows/flaky-test-detector.yml @@ -32,8 +32,9 @@ jobs: - name: Check out current branch uses: actions/checkout@v3 - name: Set up Node - uses: volta-cli/action@v4 - + uses: actions/setup-node@v3 + with: + node-version-file: 'package.json' - name: Install dependencies run: yarn install --ignore-engines --frozen-lockfile From 1ab3477bebef94c1b49813d88c9043f7ee9f67c8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 6 Apr 2023 10:56:02 +0200 Subject: [PATCH 05/48] test(replay): Streamline replay test imports (#7774) We want to generally import from `@sentry/browser`, and just have one specific test to import from `@sentry/replay`. --- .../suites/replay/captureReplay/test.ts | 2 +- .../init.js | 3 ++- .../template.html | 0 .../test.ts | 8 +++----- .../suites/replay/fileInput/init.js | 3 +-- .../suites/replay/privacyBlock/init.js | 3 +-- .../suites/replay/privacyDefault/init.js | 3 +-- .../suites/replay/privacyInput/init.js | 3 +-- .../suites/replay/privacyInputMaskAll/init.js | 3 +-- 9 files changed, 11 insertions(+), 17 deletions(-) rename packages/browser-integration-tests/suites/replay/{captureReplayViaBrowser => captureReplayFromReplayPackage}/init.js (81%) rename packages/browser-integration-tests/suites/replay/{captureReplayViaBrowser => captureReplayFromReplayPackage}/template.html (100%) rename packages/browser-integration-tests/suites/replay/{captureReplayViaBrowser => captureReplayFromReplayPackage}/test.ts (86%) diff --git a/packages/browser-integration-tests/suites/replay/captureReplay/test.ts b/packages/browser-integration-tests/suites/replay/captureReplay/test.ts index d06be65f1cb6..96d2ff60fb11 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplay/test.ts +++ b/packages/browser-integration-tests/suites/replay/captureReplay/test.ts @@ -4,7 +4,7 @@ import { SDK_VERSION } from '@sentry/browser'; import { sentryTest } from '../../../utils/fixtures'; import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; -sentryTest('should capture replays', async ({ getLocalTestPath, page }) => { +sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalTestPath, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } diff --git a/packages/browser-integration-tests/suites/replay/captureReplayViaBrowser/init.js b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js similarity index 81% rename from packages/browser-integration-tests/suites/replay/captureReplayViaBrowser/init.js rename to packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js index 7a0337445768..16b46e3adc54 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplayViaBrowser/init.js +++ b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/init.js @@ -1,7 +1,8 @@ import * as Sentry from '@sentry/browser'; +import { Replay } from '@sentry/replay'; window.Sentry = Sentry; -window.Replay = new Sentry.Replay({ +window.Replay = new Replay({ flushMinDelay: 200, flushMaxDelay: 200, }); diff --git a/packages/browser-integration-tests/suites/replay/captureReplayViaBrowser/template.html b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/template.html similarity index 100% rename from packages/browser-integration-tests/suites/replay/captureReplayViaBrowser/template.html rename to packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/template.html diff --git a/packages/browser-integration-tests/suites/replay/captureReplayViaBrowser/test.ts b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts similarity index 86% rename from packages/browser-integration-tests/suites/replay/captureReplayViaBrowser/test.ts rename to packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts index 0f50a194e01c..82f791ec7be8 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplayViaBrowser/test.ts +++ b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts @@ -2,12 +2,10 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/browser'; import { sentryTest } from '../../../utils/fixtures'; -import { getReplayEvent, waitForReplayRequest } from '../../../utils/replayHelpers'; +import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; -sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalTestPath, page }) => { - // For this test, we skip all bundle tests, as we're only interested in Replay being correctly - // exported from the `@sentry/browser` npm package. - if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) { +sentryTest('should capture replays (@sentry/replay export)', async ({ getLocalTestPath, page }) => { + if (shouldSkipReplayTest()) { sentryTest.skip(); } diff --git a/packages/browser-integration-tests/suites/replay/fileInput/init.js b/packages/browser-integration-tests/suites/replay/fileInput/init.js index a09c517b6a92..4081a8b9182d 100644 --- a/packages/browser-integration-tests/suites/replay/fileInput/init.js +++ b/packages/browser-integration-tests/suites/replay/fileInput/init.js @@ -1,8 +1,7 @@ import * as Sentry from '@sentry/browser'; -import { Replay } from '@sentry/replay'; window.Sentry = Sentry; -window.Replay = new Replay({ +window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, useCompression: false, diff --git a/packages/browser-integration-tests/suites/replay/privacyBlock/init.js b/packages/browser-integration-tests/suites/replay/privacyBlock/init.js index 31d8a00ed0fd..6c37e3d85e44 100644 --- a/packages/browser-integration-tests/suites/replay/privacyBlock/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyBlock/init.js @@ -1,8 +1,7 @@ import * as Sentry from '@sentry/browser'; -import { Replay } from '@sentry/replay'; window.Sentry = Sentry; -window.Replay = new Replay({ +window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, useCompression: false, diff --git a/packages/browser-integration-tests/suites/replay/privacyDefault/init.js b/packages/browser-integration-tests/suites/replay/privacyDefault/init.js index ccb9689a14d6..10f4385a1369 100644 --- a/packages/browser-integration-tests/suites/replay/privacyDefault/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyDefault/init.js @@ -1,8 +1,7 @@ import * as Sentry from '@sentry/browser'; -import { Replay } from '@sentry/replay'; window.Sentry = Sentry; -window.Replay = new Replay({ +window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, useCompression: false, diff --git a/packages/browser-integration-tests/suites/replay/privacyInput/init.js b/packages/browser-integration-tests/suites/replay/privacyInput/init.js index a09c517b6a92..4081a8b9182d 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInput/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyInput/init.js @@ -1,8 +1,7 @@ import * as Sentry from '@sentry/browser'; -import { Replay } from '@sentry/replay'; window.Sentry = Sentry; -window.Replay = new Replay({ +window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, useCompression: false, diff --git a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js index 6345c0f75f4e..cff4c6dab7bd 100644 --- a/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js +++ b/packages/browser-integration-tests/suites/replay/privacyInputMaskAll/init.js @@ -1,8 +1,7 @@ import * as Sentry from '@sentry/browser'; -import { Replay } from '@sentry/replay'; window.Sentry = Sentry; -window.Replay = new Replay({ +window.Replay = new Sentry.Replay({ flushMinDelay: 200, flushMaxDelay: 200, useCompression: false, From 4395d74c47b4925a3ac932e5bbc2fa5da7b04000 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 6 Apr 2023 11:18:45 +0200 Subject: [PATCH 06/48] fix(browser): Adjust `BrowserTransportOptions` to support offline transport options (#7775) Our `transportOptions` init field currently isn't typed correctly to accept `BrowserOfflineTransportOptions`, causing type errors when trying to configure the offline transport. This patch fixes this bug by making `BrowserTransportOptions` extend `BrowserOfflineTransportOptions`. --- packages/browser/src/transports/offline.ts | 2 +- packages/browser/src/transports/types.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/browser/src/transports/offline.ts b/packages/browser/src/transports/offline.ts index 9bb3e5dbfe2e..8f7a399e3fd2 100644 --- a/packages/browser/src/transports/offline.ts +++ b/packages/browser/src/transports/offline.ts @@ -79,7 +79,7 @@ export function pop(store: Store): Promise { }); } -interface BrowserOfflineTransportOptions extends OfflineTransportOptions { +export interface BrowserOfflineTransportOptions extends OfflineTransportOptions { /** * Name of indexedDb database to store envelopes in * Default: 'sentry-offline' diff --git a/packages/browser/src/transports/types.ts b/packages/browser/src/transports/types.ts index e0ed666cc787..127a7697ddb5 100644 --- a/packages/browser/src/transports/types.ts +++ b/packages/browser/src/transports/types.ts @@ -1,6 +1,10 @@ import type { BaseTransportOptions } from '@sentry/types'; -export interface BrowserTransportOptions extends BaseTransportOptions { +import type { BrowserOfflineTransportOptions } from './offline'; + +type BaseTransportAndOfflineTransportOptions = BaseTransportOptions & BrowserOfflineTransportOptions; + +export interface BrowserTransportOptions extends BaseTransportAndOfflineTransportOptions { /** Fetch API init parameters. Used by the FetchTransport */ fetchOptions?: RequestInit; /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */ From 3f084ac668d9530306fac7f680f5f246606b79f3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 6 Apr 2023 11:31:34 +0200 Subject: [PATCH 07/48] test(loader): Use new min. loader (#7772) --- .prettierignore | 1 + .../browser-integration-tests/.eslintrc.js | 1 + .../fixtures/loader.js | 247 +----------------- .../onLoad/captureExceptionInOnLoad/init.js | 7 + .../captureExceptionInOnLoad/subject.js | 1 + .../onLoad/captureExceptionInOnLoad/test.ts | 15 ++ 6 files changed, 26 insertions(+), 246 deletions(-) create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/init.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/subject.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts diff --git a/.prettierignore b/.prettierignore index f2ddf012c98e..113a311c2c62 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ *.md .nxcache +packages/browser-integration-tests/fixtures diff --git a/packages/browser-integration-tests/.eslintrc.js b/packages/browser-integration-tests/.eslintrc.js index 977d75b6a27f..16de146cce24 100644 --- a/packages/browser-integration-tests/.eslintrc.js +++ b/packages/browser-integration-tests/.eslintrc.js @@ -10,6 +10,7 @@ module.exports = { 'loader-suites/**/dist/*', 'loader-suites/**/subject.js', 'scripts/**', + 'fixtures/**', ], parserOptions: { sourceType: 'module', diff --git a/packages/browser-integration-tests/fixtures/loader.js b/packages/browser-integration-tests/fixtures/loader.js index 731ee8af2a6c..c95b5eabdcac 100644 --- a/packages/browser-integration-tests/fixtures/loader.js +++ b/packages/browser-integration-tests/fixtures/loader.js @@ -1,249 +1,4 @@ -/* eslint-disable */ -// prettier-ignore -// Prettier disabled due to trailing comma not working in IE10/11 -(function( - _window, - _document, - _script, - _onerror, - _onunhandledrejection, - _namespace, - _publicKey, - _sdkBundleUrl, - _config, - _lazy -) { - var lazy = _lazy; - var forceLoad = false; - - for (var i = 0; i < document.scripts.length; i++) { - if (document.scripts[i].src.indexOf(_publicKey) > -1) { - // If lazy was set to true above, we need to check if the user has set data-lazy="no" - // to confirm that we should lazy load the CDN bundle - if (lazy && document.scripts[i].getAttribute('data-lazy') === 'no') { - lazy = false; - } - break; - } - } - - var injected = false; - var onLoadCallbacks = []; - - // Create a namespace and attach function that will store captured exception - // Because functions are also objects, we can attach the queue itself straight to it and save some bytes - var queue = function(content) { - // content.e = error - // content.p = promise rejection - // content.f = function call the Sentry - if ( - ('e' in content || - 'p' in content || - (content.f && content.f.indexOf('capture') > -1) || - (content.f && content.f.indexOf('showReportDialog') > -1)) && - lazy - ) { - // We only want to lazy inject/load the sdk bundle if - // an error or promise rejection occured - // OR someone called `capture...` on the SDK - injectSdk(onLoadCallbacks); - } - queue.data.push(content); - }; - queue.data = []; - - function injectSdk(callbacks) { - if (injected) { - return; - } - injected = true; - - // Create a `script` tag with provided SDK `url` and attach it just before the first, already existing `script` tag - // Scripts that are dynamically created and added to the document are async by default, - // they don't block rendering and execute as soon as they download, meaning they could - // come out in the wrong order. Because of that we don't need async=1 as GA does. - // it was probably(?) a legacy behavior that they left to not modify few years old snippet - // https://www.html5rocks.com/en/tutorials/speed/script-loading/ - var _currentScriptTag = _document.scripts[0]; - var _newScriptTag = _document.createElement(_script); - _newScriptTag.src = _sdkBundleUrl; - _newScriptTag.crossOrigin = 'anonymous'; - - // Once our SDK is loaded - _newScriptTag.addEventListener('load', function () { - try { - // Restore onerror/onunhandledrejection handlers - only if not mutated in the meanwhile - if (_window[_onerror] && _window[_onerror].__SENTRY_LOADER__) { - _window[_onerror] = _oldOnerror; - } - if (_window[_onunhandledrejection] && _window[_onunhandledrejection].__SENTRY_LOADER__) { - _window[_onunhandledrejection] = _oldOnunhandledrejection; - } - - // Add loader as SDK source - _window.SENTRY_SDK_SOURCE = 'loader'; - - var SDK = _window[_namespace]; - - var oldInit = SDK.init; - - var integrations = []; - if (_config.tracesSampleRate) { - integrations.push(new Sentry.BrowserTracing()); - } - - if (_config.replaysSessionSampleRate || _config.replaysOnErrorSampleRate) { - integrations.push(new Sentry.Replay()); - } - - if (integrations.length) { - _config.integrations = integrations; - } - - // Configure it using provided DSN and config object - SDK.init = function(options) { - var target = _config; - for (var key in options) { - if (Object.prototype.hasOwnProperty.call(options, key)) { - target[key] = options[key]; - } - } - oldInit(target); - }; - - sdkLoaded(callbacks, SDK); - } catch (o_O) { - console.error(o_O); - } - }); - - _currentScriptTag.parentNode.insertBefore(_newScriptTag, _currentScriptTag); - } - - function sdkIsLoaded() { - var __sentry = _window['__SENTRY__']; - // If there is a global __SENTRY__ that means that in any of the callbacks init() was already invoked - return !!(!(typeof __sentry === 'undefined') && __sentry.hub && __sentry.hub.getClient()); - } - - function sdkLoaded(callbacks, SDK) { - try { - // We have to make sure to call all callbacks first - for (var i = 0; i < callbacks.length; i++) { - if (typeof callbacks[i] === 'function') { - callbacks[i](); - } - } - - var data = queue.data; - - var initAlreadyCalled = sdkIsLoaded(); - - // Call init first, if provided - data.sort((a, b) => a.f === 'init' ? -1 : 0); - - // We want to replay all calls to Sentry and also make sure that `init` is called if it wasn't already - // We replay all calls to `Sentry.*` now - var calledSentry = false; - for (var i = 0; i < data.length; i++) { - if (data[i].f) { - calledSentry = true; - var call = data[i]; - if (initAlreadyCalled === false && call.f !== 'init') { - // First call always has to be init, this is a conveniece for the user so call to init is optional - SDK.init(); - } - initAlreadyCalled = true; - SDK[call.f].apply(SDK, call.a); - } - } - if (initAlreadyCalled === false && calledSentry === false) { - // Sentry has never been called but we need Sentry.init() so call it - SDK.init(); - } - - // Because we installed the SDK, at this point we have an access to TraceKit's handler, - // which can take care of browser differences (eg. missing exception argument in onerror) - var tracekitErrorHandler = _window[_onerror]; - var tracekitUnhandledRejectionHandler = _window[_onunhandledrejection]; - - // And now capture all previously caught exceptions - for (var i = 0; i < data.length; i++) { - if ('e' in data[i] && tracekitErrorHandler) { - tracekitErrorHandler.apply(_window, data[i].e); - } else if ('p' in data[i] && tracekitUnhandledRejectionHandler) { - tracekitUnhandledRejectionHandler.apply(_window, [data[i].p]); - } - } - } catch (o_O) { - console.error(o_O); - } - } - - // We make sure we do not overwrite window.Sentry since there could be already integrations in there - _window[_namespace] = _window[_namespace] || {}; - - _window[_namespace].onLoad = function (callback) { - onLoadCallbacks.push(callback); - if (lazy && !forceLoad) { - return; - } - injectSdk(onLoadCallbacks); - }; - - _window[_namespace].forceLoad = function() { - forceLoad = true; - if (lazy) { - setTimeout(function() { - injectSdk(onLoadCallbacks); - }); - } - }; - - [ - 'init', - 'addBreadcrumb', - 'captureMessage', - 'captureException', - 'captureEvent', - 'configureScope', - 'withScope', - 'showReportDialog' - ].forEach(function(f) { - _window[_namespace][f] = function() { - queue({ f: f, a: arguments }); - }; - }); - - // Store reference to the old `onerror` handler and override it with our own function - // that will just push exceptions to the queue and call through old handler if we found one - var _oldOnerror = _window[_onerror]; - _window[_onerror] = function() { - // Use keys as "data type" to save some characters" - queue({ - e: [].slice.call(arguments) - }); - - if (_oldOnerror) _oldOnerror.apply(_window, arguments); - }; - _window[_onerror].__SENTRY_LOADER__ = true; - - // Do the same store/queue/call operations for `onunhandledrejection` event - var _oldOnunhandledrejection = _window[_onunhandledrejection]; - _window[_onunhandledrejection] = function(e) { - queue({ - p: 'reason' in e ? e.reason : 'detail' in e && 'reason' in e.detail ? e.detail.reason : e - }); - if (_oldOnunhandledrejection) _oldOnunhandledrejection.apply(_window, arguments); - }; - _window[_onunhandledrejection].__SENTRY_LOADER__ = true; - - if (!lazy) { - setTimeout(function () { - injectSdk(onLoadCallbacks); - }); - } -})( +!function(n,e,t,r,o,a,i,c,_,p){for(var s=p,forceLoad=!1,f=0;f-1){s&&"no"===document.scripts[f].getAttribute("data-lazy")&&(s=!1);break}var u=!1,l=[],d=function(n){("e"in n||"p"in n||n.f&&n.f.indexOf("capture")>-1||n.f&&n.f.indexOf("showReportDialog")>-1)&&s&&E(l),d.data.push(n)};function E(i){if(!u){u=!0;var p=e.scripts[0],s=e.createElement(t);s.src=c,s.crossOrigin="anonymous",s.addEventListener("load",(function(){try{n[r]&&n[r].__SENTRY_LOADER__&&(n[r]=R),n[o]&&n[o].__SENTRY_LOADER__&&(n[o]=h),n.SENTRY_SDK_SOURCE="loader";var e=n[a],t=e.init,c=[];_.tracesSampleRate&&c.push(new e.BrowserTracing),(_.replaysSessionSampleRate||_.replaysOnErrorSampleRate)&&c.push(new e.Replay),c.length&&(_.integrations=c),e.init=function(n){var e=_;for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r]);t(e)},function(e,t){try{for(var a=0;a { + const req = waitForErrorRequest(page); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const eventData = envelopeRequestParser(await req); + + expect(eventData.message).toBe('Test exception'); +}); From e741dd1fc904897f914967de830b1d90ddca2ea9 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 6 Apr 2023 14:46:19 +0200 Subject: [PATCH 08/48] feat(node): Adds `domain` implementation of `AsyncContextStrategy` (#7767) --- packages/core/src/hub.ts | 35 ++++++++++--- packages/core/src/index.ts | 1 + packages/node/src/async/domain.ts | 45 ++++++++++++++++ packages/node/test/async/domain.test.ts | 69 +++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 packages/node/src/async/domain.ts create mode 100644 packages/node/test/async/domain.test.ts diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 1a2358b81555..2168ebb3a490 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -55,7 +55,7 @@ const DEFAULT_BREADCRUMBS = 100; */ export interface AsyncContextStrategy { getCurrentHub: () => Hub | undefined; - runWithAsyncContext(callback: (hub: Hub, ...args: A[]) => T, ...args: A[]): T; + runWithAsyncContext(callback: (hub: Hub) => T, ...args: unknown[]): T; } /** @@ -536,25 +536,44 @@ export function getCurrentHub(): Hub { } } + // Prefer domains over global if they are there (applicable only to Node environment) + if (isNodeEnv()) { + return getHubFromActiveDomain(registry); + } + + // Return hub that lives on a global object + return getGlobalHub(registry); +} + +function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { // If there's no hub, or its an old API, assign a new one if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { setHubOnCarrier(registry, new Hub()); } - // Prefer domains over global if they are there (applicable only to Node environment) - if (isNodeEnv()) { - return getHubFromActiveDomain(registry); - } // Return hub that lives on a global object return getHubFromCarrier(registry); } +/** + * @private Private API with no semver guarantees! + * + * If the carrier does not contain a hub, a new hub is created with the global hub client and scope. + */ +export function ensureHubOnCarrier(carrier: Carrier): void { + // If there's no hub on current domain, or it's an old API, assign a new one + if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { + const globalHubTopStack = getGlobalHub().getStackTop(); + setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, Scope.clone(globalHubTopStack.scope))); + } +} + /** * @private Private API with no semver guarantees! * * Sets the global async context strategy */ -export function setAsyncContextStrategy(strategy: AsyncContextStrategy): void { +export function setAsyncContextStrategy(strategy: AsyncContextStrategy | undefined): void { // Get main carrier (global for every environment) const registry = getMainCarrier(); registry.__SENTRY__ = registry.__SENTRY__ || {}; @@ -566,7 +585,7 @@ export function setAsyncContextStrategy(strategy: AsyncContextStrategy): void { * * Runs the given callback function with the global async context strategy */ -export function runWithAsyncContext(callback: (hub: Hub, ...args: A[]) => T, ...args: A[]): T { +export function runWithAsyncContext(callback: (hub: Hub) => T, ...args: unknown[]): T { const registry = getMainCarrier(); if (registry.__SENTRY__ && registry.__SENTRY__.acs) { @@ -574,7 +593,7 @@ export function runWithAsyncContext(callback: (hub: Hub, ...args: A[]) => } // if there was no strategy, fallback to just calling the callback - return callback(getCurrentHub(), ...args); + return callback(getCurrentHub()); } /** diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ee8e624709a0..fd43f4565f88 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -26,6 +26,7 @@ export { getMainCarrier, runWithAsyncContext, setHubOnCarrier, + ensureHubOnCarrier, setAsyncContextStrategy, } from './hub'; export { makeSession, closeSession, updateSession } from './session'; diff --git a/packages/node/src/async/domain.ts b/packages/node/src/async/domain.ts new file mode 100644 index 000000000000..a63dac92849f --- /dev/null +++ b/packages/node/src/async/domain.ts @@ -0,0 +1,45 @@ +import type { Carrier, Hub } from '@sentry/core'; +import { + ensureHubOnCarrier, + getCurrentHub as getCurrentHubCore, + getHubFromCarrier, + setAsyncContextStrategy, +} from '@sentry/core'; +import * as domain from 'domain'; +import { EventEmitter } from 'events'; + +function getCurrentHub(): Hub | undefined { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const activeDomain = (domain as any).active as Carrier; + + // If there's no active domain, just return undefined and the global hub will be used + if (!activeDomain) { + return undefined; + } + + ensureHubOnCarrier(activeDomain); + + return getHubFromCarrier(activeDomain); +} + +function runWithAsyncContext(callback: (hub: Hub) => T, ...args: A[]): T { + const local = domain.create(); + + for (const emitter of args) { + if (emitter instanceof EventEmitter) { + local.add(emitter); + } + } + + return local.bind(() => { + const hub = getCurrentHubCore(); + return callback(hub); + })(); +} + +/** + * Sets the async context strategy to use Node.js domains. + */ +export function setDomainAsyncContextStrategy(): void { + setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext }); +} diff --git a/packages/node/test/async/domain.test.ts b/packages/node/test/async/domain.test.ts new file mode 100644 index 000000000000..e12fb590d014 --- /dev/null +++ b/packages/node/test/async/domain.test.ts @@ -0,0 +1,69 @@ +import { getCurrentHub, Hub, runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core'; +import * as domain from 'domain'; + +import { setDomainAsyncContextStrategy } from '../../src/async/domain'; + +describe('domains', () => { + afterAll(() => { + // clear the strategy + setAsyncContextStrategy(undefined); + }); + + test('without domain', () => { + // @ts-ignore property active does not exist on domain + expect(domain.active).toBeFalsy(); + const hub = getCurrentHub(); + expect(hub).toEqual(new Hub()); + }); + + test('domain hub scope inheritance', () => { + const globalHub = getCurrentHub(); + globalHub.configureScope(scope => { + scope.setExtra('a', 'b'); + scope.setTag('a', 'b'); + scope.addBreadcrumb({ message: 'a' }); + }); + runWithAsyncContext(hub => { + expect(globalHub).toEqual(hub); + }); + }); + + test('domain hub single instance', () => { + setDomainAsyncContextStrategy(); + + runWithAsyncContext(hub => { + expect(hub).toBe(getCurrentHub()); + }); + }); + + test('concurrent domain hubs', done => { + setDomainAsyncContextStrategy(); + + let d1done = false; + let d2done = false; + + runWithAsyncContext(hub => { + hub.getStack().push({ client: 'process' } as any); + expect(hub.getStack()[1]).toEqual({ client: 'process' }); + // Just in case so we don't have to worry which one finishes first + // (although it always should be d2) + setTimeout(() => { + d1done = true; + if (d2done) { + done(); + } + }); + }); + + runWithAsyncContext(hub => { + hub.getStack().push({ client: 'local' } as any); + expect(hub.getStack()[1]).toEqual({ client: 'local' }); + setTimeout(() => { + d2done = true; + if (d1done) { + done(); + } + }); + }); + }); +}); From f0f9b8aeb46f1f840faee459eee3f60a09edfadf Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 6 Apr 2023 15:52:33 +0200 Subject: [PATCH 09/48] ref(tracing): Convert prisma integration to use trace func (#7776) --- packages/node-integration-tests/README.md | 2 +- .../src/node/integrations/prisma.ts | 24 ++---------- .../test/integrations/node/prisma.test.ts | 39 +++++++++---------- 3 files changed, 23 insertions(+), 42 deletions(-) diff --git a/packages/node-integration-tests/README.md b/packages/node-integration-tests/README.md index e4354ad4fdee..ec202b5a8252 100644 --- a/packages/node-integration-tests/README.md +++ b/packages/node-integration-tests/README.md @@ -47,7 +47,7 @@ Tests can be run locally with: To run tests with Jest's watch mode: -`yarn test:jest` +`yarn test:watch` To filter tests by their title: diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts index 2215cf2a817a..7d23cf6adbb5 100644 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ b/packages/tracing-internal/src/node/integrations/prisma.ts @@ -1,6 +1,7 @@ import type { Hub } from '@sentry/core'; +import { trace } from '@sentry/core'; import type { EventProcessor, Integration } from '@sentry/types'; -import { isThenable, logger } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import { shouldDisableAutoInstrumentation } from './utils/node-utils'; @@ -88,28 +89,9 @@ export class Prisma implements Integration { } this._client.$use((params, next: (params: PrismaMiddlewareParams) => Promise) => { - const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); - const action = params.action; const model = params.model; - - const span = parentSpan?.startChild({ - description: model ? `${model} ${action}` : action, - op: 'db.sql.prisma', - }); - - const rv = next(params); - - if (isThenable(rv)) { - return rv.then((res: unknown) => { - span?.finish(); - return res; - }); - } - - span?.finish(); - return rv; + return trace({ name: model ? `${model} ${action}` : action, op: 'db.sql.prisma' }, () => next(params)); }); } } diff --git a/packages/tracing/test/integrations/node/prisma.test.ts b/packages/tracing/test/integrations/node/prisma.test.ts index d1acdc65190e..1eb85a251704 100644 --- a/packages/tracing/test/integrations/node/prisma.test.ts +++ b/packages/tracing/test/integrations/node/prisma.test.ts @@ -3,9 +3,22 @@ import { Hub, Scope } from '@sentry/core'; import { logger } from '@sentry/utils'; -import { Integrations, Span } from '../../../src'; +import { Integrations } from '../../../src'; import { getTestClient } from '../../testutils'; +const mockTrace = jest.fn(); + +jest.mock('@sentry/core', () => { + const original = jest.requireActual('@sentry/core'); + return { + ...original, + trace: (...args: unknown[]) => { + mockTrace(...args); + return original.trace(...args); + }, + }; +}); + type PrismaMiddleware = (params: unknown, next: (params?: unknown) => Promise) => Promise; class PrismaClient { @@ -27,35 +40,21 @@ class PrismaClient { describe('setupOnce', function () { const Client: PrismaClient = new PrismaClient(); - let scope = new Scope(); - let parentSpan: Span; - let childSpan: Span; - beforeAll(() => { new Integrations.Prisma({ client: Client }).setupOnce( () => undefined, - () => new Hub(undefined, scope), + () => new Hub(undefined, new Scope()), ); }); beforeEach(() => { - scope = new Scope(); - parentSpan = new Span(); - childSpan = parentSpan.startChild(); - jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan); - jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan); - jest.spyOn(childSpan, 'finish'); + mockTrace.mockClear(); }); it('should add middleware with $use method correctly', done => { - void Client.user.create()?.then(res => { - expect(res).toBe('result'); - expect(scope.getSpan).toBeCalled(); - expect(parentSpan.startChild).toBeCalledWith({ - description: 'user create', - op: 'db.sql.prisma', - }); - expect(childSpan.finish).toBeCalled(); + void Client.user.create()?.then(() => { + expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenLastCalledWith({ name: 'user create', op: 'db.sql.prisma' }, expect.any(Function)); done(); }); }); From 715876b3710ef10c3914b249232e8ea7b4248c05 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 6 Apr 2023 17:47:32 +0200 Subject: [PATCH 10/48] feat(core): Extend `AsyncContextStrategy` to allow reuse of existing context (#7778) --- packages/core/src/hub.ts | 23 +++++++++++++++++++---- packages/core/src/index.ts | 2 +- packages/node/src/async/domain.ts | 16 ++++++++++------ packages/node/test/async/domain.test.ts | 23 +++++++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 2168ebb3a490..905fb4f3bcde 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -50,12 +50,27 @@ export const API_VERSION = 4; */ const DEFAULT_BREADCRUMBS = 100; +export interface RunWithAsyncContextOptions { + /** Whether to reuse an existing async context if one exists. Defaults to false. */ + reuseExisting?: boolean; + /** Instances that should be referenced and retained in the new context */ + args?: unknown[]; +} + /** + * @private Private API with no semver guarantees! + * * Strategy used to track async context. */ export interface AsyncContextStrategy { + /** + * Gets the current async context. Returns undefined if there is no current async context. + */ getCurrentHub: () => Hub | undefined; - runWithAsyncContext(callback: (hub: Hub) => T, ...args: unknown[]): T; + /** + * Runs the supplied callback in its own async context. + */ + runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T; } /** @@ -583,13 +598,13 @@ export function setAsyncContextStrategy(strategy: AsyncContextStrategy | undefin /** * @private Private API with no semver guarantees! * - * Runs the given callback function with the global async context strategy + * Runs the supplied callback in its own async context. */ -export function runWithAsyncContext(callback: (hub: Hub) => T, ...args: unknown[]): T { +export function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions = {}): T { const registry = getMainCarrier(); if (registry.__SENTRY__ && registry.__SENTRY__.acs) { - return registry.__SENTRY__.acs.runWithAsyncContext(callback, ...args); + return registry.__SENTRY__.acs.runWithAsyncContext(callback, options); } // if there was no strategy, fallback to just calling the callback diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fd43f4565f88..04fc78e12f12 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,5 @@ export type { ClientClass } from './sdk'; -export type { AsyncContextStrategy, Carrier, Layer } from './hub'; +export type { AsyncContextStrategy, Carrier, Layer, RunWithAsyncContextOptions } from './hub'; export type { OfflineStore, OfflineTransportOptions } from './transports/offline'; export * from './tracing'; diff --git a/packages/node/src/async/domain.ts b/packages/node/src/async/domain.ts index a63dac92849f..a13e6d1ee88e 100644 --- a/packages/node/src/async/domain.ts +++ b/packages/node/src/async/domain.ts @@ -1,4 +1,4 @@ -import type { Carrier, Hub } from '@sentry/core'; +import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core'; import { ensureHubOnCarrier, getCurrentHub as getCurrentHubCore, @@ -8,9 +8,13 @@ import { import * as domain from 'domain'; import { EventEmitter } from 'events'; -function getCurrentHub(): Hub | undefined { +function getActiveDomain(): T | undefined { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - const activeDomain = (domain as any).active as Carrier; + return (domain as any).active as T | undefined; +} + +function getCurrentHub(): Hub | undefined { + const activeDomain = getActiveDomain(); // If there's no active domain, just return undefined and the global hub will be used if (!activeDomain) { @@ -22,10 +26,10 @@ function getCurrentHub(): Hub | undefined { return getHubFromCarrier(activeDomain); } -function runWithAsyncContext(callback: (hub: Hub) => T, ...args: A[]): T { - const local = domain.create(); +function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T { + const local = options?.reuseExisting ? getActiveDomain() || domain.create() : domain.create(); - for (const emitter of args) { + for (const emitter of options.args || []) { if (emitter instanceof EventEmitter) { local.add(emitter); } diff --git a/packages/node/test/async/domain.test.ts b/packages/node/test/async/domain.test.ts index e12fb590d014..9a1f39ee5b23 100644 --- a/packages/node/test/async/domain.test.ts +++ b/packages/node/test/async/domain.test.ts @@ -36,6 +36,29 @@ describe('domains', () => { }); }); + test('domain within a domain not reused', () => { + setDomainAsyncContextStrategy(); + + runWithAsyncContext(hub1 => { + runWithAsyncContext(hub2 => { + expect(hub1).not.toBe(hub2); + }); + }); + }); + + test('domain within a domain reused when requested', () => { + setDomainAsyncContextStrategy(); + + runWithAsyncContext(hub1 => { + runWithAsyncContext( + hub2 => { + expect(hub1).toBe(hub2); + }, + { reuseExisting: true }, + ); + }); + }); + test('concurrent domain hubs', done => { setDomainAsyncContextStrategy(); From f4e92ca3f21aa2ff95f174a3703362bcb74e525e Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 7 Apr 2023 08:23:46 +0200 Subject: [PATCH 11/48] fix(node): `reuseExisting` does not need to call bind on domain (#7780) --- packages/node/src/async/domain.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/node/src/async/domain.ts b/packages/node/src/async/domain.ts index a13e6d1ee88e..c34bf2e2124f 100644 --- a/packages/node/src/async/domain.ts +++ b/packages/node/src/async/domain.ts @@ -27,7 +27,22 @@ function getCurrentHub(): Hub | undefined { } function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T { - const local = options?.reuseExisting ? getActiveDomain() || domain.create() : domain.create(); + if (options?.reuseExisting) { + const activeDomain = getActiveDomain(); + + if (activeDomain) { + for (const emitter of options.args || []) { + if (emitter instanceof EventEmitter) { + activeDomain.add(emitter); + } + } + + // We're already in a domain, so we don't need to create a new one, just call the callback with the current hub + return callback(getHubFromCarrier(activeDomain)); + } + } + + const local = domain.create(); for (const emitter of options.args || []) { if (emitter instanceof EventEmitter) { From ad8ce235d95476a5b6c1324abb6dd3bcf45b1e14 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 7 Apr 2023 12:17:40 +0200 Subject: [PATCH 12/48] ref(replay): Always send `replay_start_timestamp` (#7773) This should be sent with each segment now. --- .../suites/replay/captureReplay/test.ts | 1 + .../captureReplayFromReplayPackage/test.ts | 1 + .../suites/replay/customEvents/test.ts | 4 +--- .../suites/replay/errors/errorMode/test.ts | 2 -- .../replay/errors/errorsInSession/test.ts | 2 -- .../suites/replay/flushing/test.ts | 4 ++-- .../suites/replay/multiple-pages/test.ts | 18 +++++------------- .../suites/replay/sessionExpiry/test.ts | 2 +- .../suites/replay/sessionMaxAge/test.ts | 2 +- packages/replay/src/replay.ts | 1 - packages/replay/src/types.ts | 1 - packages/replay/src/util/sendReplayRequest.ts | 3 +-- packages/replay/test/integration/flush.test.ts | 2 -- 13 files changed, 13 insertions(+), 30 deletions(-) diff --git a/packages/browser-integration-tests/suites/replay/captureReplay/test.ts b/packages/browser-integration-tests/suites/replay/captureReplay/test.ts index 96d2ff60fb11..473d88ea53db 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplay/test.ts +++ b/packages/browser-integration-tests/suites/replay/captureReplay/test.ts @@ -75,6 +75,7 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT trace_ids: [], urls: [], replay_id: expect.stringMatching(/\w{32}/), + replay_start_timestamp: expect.any(Number), segment_id: 1, replay_type: 'session', event_id: expect.stringMatching(/\w{32}/), diff --git a/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts index 82f791ec7be8..03ee0f78e540 100644 --- a/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts +++ b/packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts @@ -75,6 +75,7 @@ sentryTest('should capture replays (@sentry/replay export)', async ({ getLocalTe trace_ids: [], urls: [], replay_id: expect.stringMatching(/\w{32}/), + replay_start_timestamp: expect.any(Number), segment_id: 1, replay_type: 'session', event_id: expect.stringMatching(/\w{32}/), diff --git a/packages/browser-integration-tests/suites/replay/customEvents/test.ts b/packages/browser-integration-tests/suites/replay/customEvents/test.ts index 7d782b24e85c..ce8f27bf4995 100644 --- a/packages/browser-integration-tests/suites/replay/customEvents/test.ts +++ b/packages/browser-integration-tests/suites/replay/customEvents/test.ts @@ -52,9 +52,7 @@ sentryTest( const replayEvent1 = getReplayEvent(await reqPromise1); const { performanceSpans: performanceSpans1 } = getCustomRecordingEvents(await reqPromise1); - expect(replayEvent1).toEqual( - getExpectedReplayEvent({ segment_id: 1, urls: [], replay_start_timestamp: undefined }), - ); + expect(replayEvent1).toEqual(getExpectedReplayEvent({ segment_id: 1, urls: [] })); // We can't guarantee the order of the performance spans, or in which of the two segments they are sent // So to avoid flakes, we collect them all and check that they are all there diff --git a/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts b/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts index cc17fa80cb9c..18dd4b40e2a1 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts +++ b/packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts @@ -119,7 +119,6 @@ sentryTest( getExpectedReplayEvent({ contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, replay_type: 'error', // although we're in session mode, we still send 'error' as replay_type - replay_start_timestamp: undefined, segment_id: 1, urls: [], }), @@ -135,7 +134,6 @@ sentryTest( getExpectedReplayEvent({ contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } }, replay_type: 'error', - replay_start_timestamp: undefined, segment_id: 2, urls: [], }), diff --git a/packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts b/packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts index 01962b6667e5..9385e044a75c 100644 --- a/packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts +++ b/packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts @@ -55,7 +55,6 @@ sentryTest( expect(event1).toEqual( getExpectedReplayEvent({ - replay_start_timestamp: undefined, segment_id: 1, error_ids: [errorEventId], urls: [], @@ -117,7 +116,6 @@ sentryTest( expect(event1).toEqual( getExpectedReplayEvent({ - replay_start_timestamp: undefined, segment_id: 1, error_ids: [], // <-- no error id urls: [], diff --git a/packages/browser-integration-tests/suites/replay/flushing/test.ts b/packages/browser-integration-tests/suites/replay/flushing/test.ts index dfc361398def..2d57da3b4d30 100644 --- a/packages/browser-integration-tests/suites/replay/flushing/test.ts +++ b/packages/browser-integration-tests/suites/replay/flushing/test.ts @@ -40,7 +40,7 @@ sentryTest('replay events are flushed after max flush delay was reached', async // this must eventually lead to a flush after the max delay was reached const replayEvent1 = getReplayEvent(await reqPromise1); - expect(replayEvent1).toEqual(getExpectedReplayEvent({ replay_start_timestamp: undefined, segment_id: 1, urls: [] })); + expect(replayEvent1).toEqual(getExpectedReplayEvent({ segment_id: 1, urls: [] })); // trigger mouse click every 100ms, it should still flush after the max delay even if clicks are ongoing for (let i = 0; i < 700; i++) { @@ -54,5 +54,5 @@ sentryTest('replay events are flushed after max flush delay was reached', async } const replayEvent2 = getReplayEvent(await reqPromise2); - expect(replayEvent2).toEqual(getExpectedReplayEvent({ replay_start_timestamp: undefined, segment_id: 2, urls: [] })); + expect(replayEvent2).toEqual(getExpectedReplayEvent({ segment_id: 2, urls: [] })); }); diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts index 9fc3c3f0481a..c54baf8be6f4 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts @@ -71,9 +71,7 @@ sentryTest( const replayEvent1 = getReplayEvent(req1); const recording1 = getReplayRecordingContent(req1); - expect(replayEvent1).toEqual( - getExpectedReplayEvent({ segment_id: 1, urls: [], replay_start_timestamp: undefined }), - ); + expect(replayEvent1).toEqual(getExpectedReplayEvent({ segment_id: 1, urls: [] })); expect(recording1.fullSnapshots.length).toEqual(0); expect(normalize(recording1.incrementalSnapshots)).toMatchSnapshot('seg-1-snap-incremental'); @@ -105,7 +103,7 @@ sentryTest( const replayEvent2 = getReplayEvent(req2); const recording2 = getReplayRecordingContent(req2); - expect(replayEvent2).toEqual(getExpectedReplayEvent({ segment_id: 2, replay_start_timestamp: undefined })); + expect(replayEvent2).toEqual(getExpectedReplayEvent({ segment_id: 2 })); expect(normalize(recording2.fullSnapshots)).toMatchSnapshot('seg-2-snap-full'); expect(recording2.incrementalSnapshots.length).toEqual(0); @@ -115,9 +113,7 @@ sentryTest( const replayEvent3 = getReplayEvent(req3); const recording3 = getReplayRecordingContent(req3); - expect(replayEvent3).toEqual( - getExpectedReplayEvent({ segment_id: 3, urls: [], replay_start_timestamp: undefined }), - ); + expect(replayEvent3).toEqual(getExpectedReplayEvent({ segment_id: 3, urls: [] })); expect(recording3.fullSnapshots.length).toEqual(0); expect(normalize(recording3.incrementalSnapshots)).toMatchSnapshot('seg-3-snap-incremental'); @@ -150,7 +146,6 @@ sentryTest( expect(replayEvent4).toEqual( getExpectedReplayEvent({ segment_id: 4, - replay_start_timestamp: undefined, // @ts-ignore this is fine urls: [expect.stringContaining('page-0.html')], request: { @@ -176,7 +171,6 @@ sentryTest( getExpectedReplayEvent({ segment_id: 5, urls: [], - replay_start_timestamp: undefined, request: { // @ts-ignore this is fine url: expect.stringContaining('page-0.html'), @@ -223,7 +217,7 @@ sentryTest( getExpectedReplayEvent({ segment_id: 6, urls: ['/spa'], - replay_start_timestamp: undefined, + request: { // @ts-ignore this is fine url: expect.stringContaining('page-0.html'), @@ -247,7 +241,7 @@ sentryTest( getExpectedReplayEvent({ segment_id: 7, urls: [], - replay_start_timestamp: undefined, + request: { // @ts-ignore this is fine url: expect.stringContaining('page-0.html'), @@ -294,7 +288,6 @@ sentryTest( expect(replayEvent8).toEqual( getExpectedReplayEvent({ segment_id: 8, - replay_start_timestamp: undefined, }), ); expect(normalize(recording8.fullSnapshots)).toMatchSnapshot('seg-8-snap-full'); @@ -310,7 +303,6 @@ sentryTest( getExpectedReplayEvent({ segment_id: 9, urls: [], - replay_start_timestamp: undefined, }), ); expect(recording9.fullSnapshots.length).toEqual(0); diff --git a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts index d817e7175840..f1765b2a3c22 100644 --- a/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts +++ b/packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts @@ -50,7 +50,7 @@ sentryTest('handles an expired session', async ({ getLocalTestPath, page }) => { const req1 = await reqPromise1; const replayEvent1 = getReplayEvent(req1); - expect(replayEvent1).toEqual(getExpectedReplayEvent({ replay_start_timestamp: undefined, segment_id: 1, urls: [] })); + expect(replayEvent1).toEqual(getExpectedReplayEvent({ segment_id: 1, urls: [] })); const replay = await getReplaySnapshot(page); const oldSessionId = replay.session?.id; diff --git a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts index 89ad76ea4d4a..ca50c5a62203 100644 --- a/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts +++ b/packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts @@ -64,7 +64,7 @@ sentryTest('handles session that exceeds max age', async ({ getLocalTestPath, pa const req1 = await reqPromise1; const replayEvent1 = getReplayEvent(req1); - expect(replayEvent1).toEqual(getExpectedReplayEvent({ replay_start_timestamp: undefined, segment_id: 1, urls: [] })); + expect(replayEvent1).toEqual(getExpectedReplayEvent({ segment_id: 1, urls: [] })); const replay1 = await getReplaySnapshot(page); const oldSessionId = replay1.session?.id; diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 8bb426874442..4c6ff1d07bd8 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -748,7 +748,6 @@ export class ReplayContainer implements ReplayContainerInterface { replayId, recordingData, segmentId, - includeReplayStartTimestamp: segmentId === 0, eventContext, session: this.session, options: this.getOptions(), diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index f6d4566d2d7c..6f2db385ed1c 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -18,7 +18,6 @@ export interface SendReplayData { recordingData: ReplayRecordingData; replayId: string; segmentId: number; - includeReplayStartTimestamp: boolean; eventContext: PopEventContext; timestamp: number; session: Session; diff --git a/packages/replay/src/util/sendReplayRequest.ts b/packages/replay/src/util/sendReplayRequest.ts index f08063f38e45..009fdc2067bf 100644 --- a/packages/replay/src/util/sendReplayRequest.ts +++ b/packages/replay/src/util/sendReplayRequest.ts @@ -15,7 +15,6 @@ export async function sendReplayRequest({ recordingData, replayId, segmentId: segment_id, - includeReplayStartTimestamp, eventContext, timestamp, session, @@ -42,7 +41,7 @@ export async function sendReplayRequest({ const baseEvent: ReplayEvent = { type: REPLAY_EVENT_NAME, - ...(includeReplayStartTimestamp ? { replay_start_timestamp: initialTimestamp / 1000 } : {}), + replay_start_timestamp: initialTimestamp / 1000, timestamp: timestamp / 1000, error_ids: errorIds, trace_ids: traceIds, diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index 5d91edf483a3..a97d2b3c878c 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -179,7 +179,6 @@ describe('Integration | flush', () => { expect(mockSendReplay).toHaveBeenLastCalledWith({ recordingData: expect.any(String), replayId: expect.any(String), - includeReplayStartTimestamp: true, segmentId: 0, eventContext: expect.anything(), session: expect.any(Object), @@ -229,7 +228,6 @@ describe('Integration | flush', () => { expect(mockSendReplay).toHaveBeenLastCalledWith({ recordingData: expect.any(String), replayId: expect.any(String), - includeReplayStartTimestamp: false, segmentId: 1, eventContext: expect.anything(), session: expect.any(Object), From a2cda4df089ad372f721c94b07dd752211acf820 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 7 Apr 2023 12:49:21 +0200 Subject: [PATCH 13/48] feat(node): Add checkin envelope types (#7777) Co-authored-by: Lukas Stracke --- packages/node/src/checkin.ts | 33 ++++++++++++++++ packages/node/test/checkin.test.ts | 61 ++++++++++++++++++++++++++++++ packages/types/src/checkin.ts | 13 +++++++ packages/types/src/datacategory.ts | 6 ++- packages/types/src/envelope.ts | 10 ++++- packages/types/src/index.ts | 3 ++ packages/utils/src/envelope.ts | 1 + 7 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 packages/node/src/checkin.ts create mode 100644 packages/node/test/checkin.test.ts create mode 100644 packages/types/src/checkin.ts diff --git a/packages/node/src/checkin.ts b/packages/node/src/checkin.ts new file mode 100644 index 000000000000..c2b56509e12a --- /dev/null +++ b/packages/node/src/checkin.ts @@ -0,0 +1,33 @@ +import type { CheckIn, CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata } from '@sentry/types'; +import { createEnvelope, dsnToString } from '@sentry/utils'; + +/** + * Create envelope from check in item. + */ +export function createCheckInEnvelope( + checkIn: CheckIn, + metadata?: SdkMetadata, + tunnel?: string, + dsn?: DsnComponents, +): CheckInEvelope { + const headers: CheckInEvelope[0] = { + sent_at: new Date().toISOString(), + ...(metadata && + metadata.sdk && { + sdk: { + name: metadata.sdk.name, + version: metadata.sdk.version, + }, + }), + ...(!!tunnel && !!dsn && { dsn: dsnToString(dsn) }), + }; + const item = createCheckInEnvelopeItem(checkIn); + return createEnvelope(headers, [item]); +} + +function createCheckInEnvelopeItem(checkIn: CheckIn): CheckInItem { + const checkInHeaders: CheckInItem[0] = { + type: 'check_in', + }; + return [checkInHeaders, checkIn]; +} diff --git a/packages/node/test/checkin.test.ts b/packages/node/test/checkin.test.ts new file mode 100644 index 000000000000..da082207b00d --- /dev/null +++ b/packages/node/test/checkin.test.ts @@ -0,0 +1,61 @@ +import { createCheckInEnvelope } from '../src/checkin'; + +describe('userFeedback', () => { + test('creates user feedback envelope header', () => { + const envelope = createCheckInEnvelope( + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'in_progress', + }, + { + sdk: { + name: 'testSdkName', + version: 'testSdkVersion', + }, + }, + 'testTunnel', + { + host: 'testHost', + projectId: 'testProjectId', + protocol: 'http', + }, + ); + + expect(envelope[0]).toEqual({ + dsn: 'http://undefined@testHost/undefinedtestProjectId', + sdk: { + name: 'testSdkName', + version: 'testSdkVersion', + }, + sent_at: expect.any(String), + }); + }); + + test('creates user feedback envelope item', () => { + const envelope = createCheckInEnvelope({ + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'ok', + duration: 10.0, + release: '1.0.0', + environment: 'production', + }); + + expect(envelope[1]).toEqual([ + [ + { + type: 'check_in', + }, + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'ok', + duration: 10.0, + release: '1.0.0', + environment: 'production', + }, + ], + ]); + }); +}); diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts new file mode 100644 index 000000000000..071afce9640d --- /dev/null +++ b/packages/types/src/checkin.ts @@ -0,0 +1,13 @@ +// https://develop.sentry.dev/sdk/check-ins/ +export interface CheckIn { + // Check-In ID (unique and client generated). + check_in_id: string; + // The distinct slug of the monitor. + monitor_slug: string; + // The status of the check-in. + status: 'in_progress' | 'ok' | 'error'; + // The duration of the check-in in seconds. Will only take effect if the status is ok or error. + duration?: number; + release?: string; + environment?: string; +} diff --git a/packages/types/src/datacategory.ts b/packages/types/src/datacategory.ts index 3456ce2c757d..06f64c8525bb 100644 --- a/packages/types/src/datacategory.ts +++ b/packages/types/src/datacategory.ts @@ -1,7 +1,7 @@ // This type is used in various places like Client Reports and Rate Limit Categories // See: // - https://develop.sentry.dev/sdk/rate-limiting/#definitions -// - https://github.com/getsentry/relay/blob/10874b587bb676bd6d50ad42d507216513660082/relay-common/src/constants.rs#L97-L113 +// - https://github.com/getsentry/relay/blob/c3b339e151c1e548ede489a01c65db82472c8751/relay-common/src/constants.rs#L139-L152 // - https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload under `discarded_events` export type DataCategory = // Reserved and only used in edgecases, unlikely to be ever actually used @@ -21,4 +21,6 @@ export type DataCategory = // SDK internal event, like client_reports | 'internal' // Profile event type - | 'profile'; + | 'profile' + // Check-in event (monitor) + | 'monitor'; diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 2234317ef8ce..be146bdd6fc5 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -1,3 +1,4 @@ +import type { CheckIn } from './checkin'; import type { ClientReport } from './clientreport'; import type { DsnComponents } from './dsn'; import type { Event } from './event'; @@ -31,7 +32,8 @@ export type EnvelopeItemType = | 'event' | 'profile' | 'replay_event' - | 'replay_recording'; + | 'replay_recording' + | 'check_in'; export type BaseEnvelopeHeaders = { [key: string]: unknown; @@ -68,6 +70,7 @@ type SessionAggregatesItemHeaders = { type: 'sessions' }; type ClientReportItemHeaders = { type: 'client_report' }; type ReplayEventItemHeaders = { type: 'replay_event' }; type ReplayRecordingItemHeaders = { type: 'replay_recording'; length: number }; +type CheckInItemHeaders = { type: 'check_in' }; export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; @@ -76,11 +79,13 @@ export type SessionItem = | BaseEnvelopeItem | BaseEnvelopeItem; export type ClientReportItem = BaseEnvelopeItem; +export type CheckInItem = BaseEnvelopeItem; type ReplayEventItem = BaseEnvelopeItem; type ReplayRecordingItem = BaseEnvelopeItem; export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext }; type SessionEnvelopeHeaders = { sent_at: string }; +type CheckInEnvelopeHeaders = BaseEnvelopeHeaders; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; type ReplayEnvelopeHeaders = BaseEnvelopeHeaders; @@ -88,6 +93,7 @@ export type EventEnvelope = BaseEnvelope; export type ClientReportEnvelope = BaseEnvelope; export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRecordingItem]]; +export type CheckInEvelope = BaseEnvelope; -export type Envelope = EventEnvelope | SessionEnvelope | ClientReportEnvelope | ReplayEnvelope; +export type Envelope = EventEnvelope | SessionEnvelope | ClientReportEnvelope | ReplayEnvelope | CheckInEvelope; export type EnvelopeItem = Envelope[1][number]; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 7768a73fb6da..f0a30806ed6e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -30,6 +30,8 @@ export type { SessionEnvelope, SessionItem, UserFeedbackItem, + CheckInItem, + CheckInEvelope, } from './envelope'; export type { ExtendedError } from './error'; export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './event'; @@ -104,3 +106,4 @@ export type { Instrumenter } from './instrumenter'; export type { HandlerDataFetch, HandlerDataXhr, SentryXhrData, SentryWrappedXMLHttpRequest } from './instrument'; export type { BrowserClientReplayOptions } from './browseroptions'; +export type { CheckIn } from './checkin'; diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index 705326b6c9ba..580a50d019e0 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -207,6 +207,7 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = { profile: 'profile', replay_event: 'replay', replay_recording: 'replay', + check_in: 'monitor', }; /** From c3a42f3ed6c02e069277ea4c869d16b9875ac718 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 7 Apr 2023 12:52:22 +0200 Subject: [PATCH 14/48] fix(integrations): Ensure httpclient integration works with Request (#7786) --- .../integrations/httpclient/fetch/init.js | 11 +++ .../httpclient/fetch/{ => simple}/subject.js | 0 .../httpclient/fetch/{ => simple}/test.ts | 4 +- .../httpclient/fetch/withRequest/subject.js | 12 ++++ .../httpclient/fetch/withRequest/test.ts | 64 ++++++++++++++++++ .../withRequestAndBodyAndOptions/subject.js | 18 +++++ .../withRequestAndBodyAndOptions/test.ts | 67 +++++++++++++++++++ .../fetch/withRequestAndOptions/subject.js | 17 +++++ .../fetch/withRequestAndOptions/test.ts | 64 ++++++++++++++++++ packages/integration-shims/package.json | 2 +- packages/integrations/src/httpclient.ts | 17 ++++- packages/replay-worker/package.json | 2 +- 12 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 packages/browser-integration-tests/suites/integrations/httpclient/fetch/init.js rename packages/browser-integration-tests/suites/integrations/httpclient/fetch/{ => simple}/subject.js (100%) rename packages/browser-integration-tests/suites/integrations/httpclient/fetch/{ => simple}/test.ts (92%) create mode 100644 packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/subject.js create mode 100644 packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/test.ts create mode 100644 packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/subject.js create mode 100644 packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/test.ts create mode 100644 packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/subject.js create mode 100644 packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/test.ts diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/init.js b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/init.js new file mode 100644 index 000000000000..5d43b49e75fb --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; +import { HttpClient } from '@sentry/integrations'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new HttpClient()], + tracesSampleRate: 1, + sendDefaultPii: true, +}); diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/subject.js b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/subject.js similarity index 100% rename from packages/browser-integration-tests/suites/integrations/httpclient/fetch/subject.js rename to packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/subject.js diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/test.ts b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/test.ts similarity index 92% rename from packages/browser-integration-tests/suites/integrations/httpclient/fetch/test.ts rename to packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/test.ts index 3b845c8a8029..07eafb3185ae 100644 --- a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/test.ts +++ b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/simple/test.ts @@ -1,8 +1,8 @@ import { expect } from '@playwright/test'; import type { Event } from '@sentry/types'; -import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; sentryTest( 'should assign request and response context from a failed 500 fetch request', diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/subject.js b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/subject.js new file mode 100644 index 000000000000..07b538291b73 --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/subject.js @@ -0,0 +1,12 @@ +const request = new Request('http://localhost:7654/foo', { + method: 'POST', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Cache: 'no-cache', + }, + body: JSON.stringify({ test: true }), +}); + +fetch(request); diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/test.ts b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/test.ts new file mode 100644 index 000000000000..dd829a2bcc22 --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequest/test.ts @@ -0,0 +1,64 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +sentryTest('works with a Request passed in', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 500, + body: JSON.stringify({ + error: { + message: 'Internal Server Error', + }, + }), + headers: { + 'Content-Type': 'text/html', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + + // Not able to get the cookies from the request/response because of Playwright bug + // https://github.com/microsoft/playwright/issues/11035 + expect(eventData).toMatchObject({ + message: 'HTTP Client Error with status code: 500', + exception: { + values: [ + { + type: 'Error', + value: 'HTTP Client Error with status code: 500', + mechanism: { + type: 'http.client', + handled: true, + }, + }, + ], + }, + request: { + url: 'http://localhost:7654/foo', + method: 'POST', + headers: { + accept: 'application/json', + cache: 'no-cache', + 'content-type': 'application/json', + }, + }, + contexts: { + response: { + status_code: 500, + body_size: 45, + headers: { + 'content-type': 'text/html', + 'content-length': '45', + }, + }, + }, + }); +}); diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/subject.js b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/subject.js new file mode 100644 index 000000000000..4659addc56d2 --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/subject.js @@ -0,0 +1,18 @@ +const request = new Request('http://localhost:7654/foo', { + method: 'POST', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Cache: 'no-cache', + }, + body: JSON.stringify({ test: true }), +}); + +fetch(request, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Cache: 'cache', + }, +}); diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/test.ts b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/test.ts new file mode 100644 index 000000000000..208db16c84c9 --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndBodyAndOptions/test.ts @@ -0,0 +1,67 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +sentryTest( + 'works with a Request (with body) & options passed in - handling used body', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 500, + body: JSON.stringify({ + error: { + message: 'Internal Server Error', + }, + }), + headers: { + 'Content-Type': 'text/html', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + + // Not able to get the cookies from the request/response because of Playwright bug + // https://github.com/microsoft/playwright/issues/11035 + expect(eventData).toMatchObject({ + message: 'HTTP Client Error with status code: 500', + exception: { + values: [ + { + type: 'Error', + value: 'HTTP Client Error with status code: 500', + mechanism: { + type: 'http.client', + handled: true, + }, + }, + ], + }, + request: { + url: 'http://localhost:7654/foo', + method: 'POST', + headers: { + accept: 'application/json', + cache: 'no-cache', + 'content-type': 'application/json', + }, + }, + contexts: { + response: { + status_code: 500, + body_size: 45, + headers: { + 'content-type': 'text/html', + 'content-length': '45', + }, + }, + }, + }); + }, +); diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/subject.js b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/subject.js new file mode 100644 index 000000000000..96e3194dcfe2 --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/subject.js @@ -0,0 +1,17 @@ +const request = new Request('http://localhost:7654/foo', { + method: 'POST', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Cache: 'no-cache', + }, +}); + +fetch(request, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Cache: 'cache', + }, +}); diff --git a/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/test.ts b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/test.ts new file mode 100644 index 000000000000..a288f6fae1fb --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/httpclient/fetch/withRequestAndOptions/test.ts @@ -0,0 +1,64 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; + +sentryTest('works with a Request (without body) & options passed in', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 500, + body: JSON.stringify({ + error: { + message: 'Internal Server Error', + }, + }), + headers: { + 'Content-Type': 'text/html', + }, + }); + }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.exception?.values).toHaveLength(1); + + // Not able to get the cookies from the request/response because of Playwright bug + // https://github.com/microsoft/playwright/issues/11035 + expect(eventData).toMatchObject({ + message: 'HTTP Client Error with status code: 500', + exception: { + values: [ + { + type: 'Error', + value: 'HTTP Client Error with status code: 500', + mechanism: { + type: 'http.client', + handled: true, + }, + }, + ], + }, + request: { + url: 'http://localhost:7654/foo', + method: 'POST', + headers: { + accept: 'application/json', + cache: 'cache', + 'content-type': 'application/json', + }, + }, + contexts: { + response: { + status_code: 500, + body_size: 45, + headers: { + 'content-type': 'text/html', + 'content-length': '45', + }, + }, + }, + }); +}); diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index f2cab5072d78..015c6cc6f4cd 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -14,7 +14,7 @@ "build:dev": "yarn build", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-p build:watch", - "build:transpile:watch": "yarn build:rollup --watch", + "build:transpile:watch": "yarn build:transpile --watch", "build:types:watch": "yarn build:types --watch", "clean": "rimraf build", "fix": "run-s fix:eslint fix:prettier", diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index 9608068edf94..6c71763b2ab8 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -90,7 +90,7 @@ export class HttpClient implements Integration { */ private _fetchResponseHandler(requestInfo: RequestInfo, response: Response, requestInit?: RequestInit): void { if (this._getCurrentHub && this._shouldCaptureResponse(response.status, response.url)) { - const request = new Request(requestInfo, requestInit); + const request = _getRequest(requestInfo, requestInit); const hub = this._getCurrentHub(); let requestHeaders, responseHeaders, requestCookies, responseCookies; @@ -417,3 +417,18 @@ export class HttpClient implements Integration { return event; } } + +function _getRequest(requestInfo: RequestInfo, requestInit?: RequestInit): Request { + if (!requestInit && requestInfo instanceof Request) { + return requestInfo; + } + + // If both are set, we try to construct a new Request with the given arguments + // However, if e.g. the original request has a `body`, this will throw an error because it was already accessed + // In this case, as a fallback, we just use the original request - using both is rather an edge case + if (requestInfo instanceof Request && requestInfo.bodyUsed) { + return requestInfo; + } + + return new Request(requestInfo, requestInit); +} diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index 8e01441f16e3..ac5e4f040c0f 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -14,7 +14,7 @@ "build:dev": "yarn build", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-p build:watch", - "build:transpile:watch": "yarn build:rollup --watch", + "build:transpile:watch": "yarn build:transpile --watch", "build:types:watch": "yarn build:types --watch", "clean": "rimraf build", "fix": "run-s fix:eslint fix:prettier", From d23214d72046c8eeb828c6593a043601a28557ce Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 7 Apr 2023 15:30:29 +0200 Subject: [PATCH 15/48] feat(sveltekit): Add `sentrySvelteKitPlugin` (#7788) Add the `sentrySvelteKitPlugin` vite plugin to the SvelteKit SDK which will replace the `withSentryViteConfig` wrapper. Currently, this plugin does exactly what the wrapper is doing, namely, adding the injectInitPlugin. In the future, this plugin will add the source maps plugin. --- packages/sveltekit/src/config/index.ts | 1 - packages/sveltekit/src/index.server.ts | 2 +- packages/sveltekit/src/index.types.ts | 2 +- packages/sveltekit/src/vite/index.ts | 2 + .../injectInitPlugin.ts} | 19 +-- .../src/vite/sentrySvelteKitPlugin.ts | 54 +++++++++ packages/sveltekit/src/vite/utils.ts | 28 +++++ .../{config => vite}/withSentryViteConfig.ts | 9 +- .../injectInitPlugin.test.ts} | 2 +- .../test/vite/sentrySvelteKitPlugin.test.ts | 108 ++++++++++++++++++ .../withSentryViteConfig.test.ts | 2 +- 11 files changed, 200 insertions(+), 29 deletions(-) delete mode 100644 packages/sveltekit/src/config/index.ts create mode 100644 packages/sveltekit/src/vite/index.ts rename packages/sveltekit/src/{config/vitePlugins.ts => vite/injectInitPlugin.ts} (81%) create mode 100644 packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts create mode 100644 packages/sveltekit/src/vite/utils.ts rename packages/sveltekit/src/{config => vite}/withSentryViteConfig.ts (86%) rename packages/sveltekit/test/{config/vitePlugins.test.ts => vite/injectInitPlugin.test.ts} (96%) create mode 100644 packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts rename packages/sveltekit/test/{config => vite}/withSentryViteConfig.test.ts (98%) diff --git a/packages/sveltekit/src/config/index.ts b/packages/sveltekit/src/config/index.ts deleted file mode 100644 index 5648cfb6bffc..000000000000 --- a/packages/sveltekit/src/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { withSentryViteConfig } from './withSentryViteConfig'; diff --git a/packages/sveltekit/src/index.server.ts b/packages/sveltekit/src/index.server.ts index 82b6fe6cbff4..d675a1c72820 100644 --- a/packages/sveltekit/src/index.server.ts +++ b/packages/sveltekit/src/index.server.ts @@ -1,2 +1,2 @@ export * from './server'; -export * from './config'; +export * from './vite'; diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index c29315bc0181..63345e81ccab 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -4,7 +4,7 @@ // Some of the exports collide, which is not allowed, unless we redifine the colliding // exports in this file - which we do below. export * from './client'; -export * from './config'; +export * from './vite'; export * from './server'; import type { Integration, Options, StackParser } from '@sentry/types'; diff --git a/packages/sveltekit/src/vite/index.ts b/packages/sveltekit/src/vite/index.ts new file mode 100644 index 000000000000..a29a564fd275 --- /dev/null +++ b/packages/sveltekit/src/vite/index.ts @@ -0,0 +1,2 @@ +export { withSentryViteConfig } from './withSentryViteConfig'; +export { sentrySvelteKitPlugin } from './sentrySvelteKitPlugin'; diff --git a/packages/sveltekit/src/config/vitePlugins.ts b/packages/sveltekit/src/vite/injectInitPlugin.ts similarity index 81% rename from packages/sveltekit/src/config/vitePlugins.ts rename to packages/sveltekit/src/vite/injectInitPlugin.ts index 1bb2a477b3e5..324f83404eff 100644 --- a/packages/sveltekit/src/config/vitePlugins.ts +++ b/packages/sveltekit/src/vite/injectInitPlugin.ts @@ -1,9 +1,10 @@ import { logger } from '@sentry/utils'; -import * as fs from 'fs'; import MagicString from 'magic-string'; import * as path from 'path'; import type { Plugin, TransformResult } from 'vite'; +import { getUserConfigFile } from './utils'; + const serverIndexFilePath = path.join('@sveltejs', 'kit', 'src', 'runtime', 'server', 'index.js'); const devClientAppFilePath = path.join('generated', 'client', 'app.js'); const prodClientAppFilePath = path.join('generated', 'client-optimized', 'app.js'); @@ -59,19 +60,3 @@ function addSentryConfigFileImport( return { code: ms.toString(), map: ms.generateMap() }; } - -/** - * Looks up the sentry.{@param platform}.config.(ts|js) file - * @returns the file path to the file or undefined if it doesn't exist - */ -export function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string | undefined { - const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; - - for (const filename of possibilities) { - if (fs.existsSync(path.resolve(projectDir, filename))) { - return filename; - } - } - - return undefined; -} diff --git a/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts b/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts new file mode 100644 index 000000000000..1cb5b9a5b4f4 --- /dev/null +++ b/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts @@ -0,0 +1,54 @@ +import type { Plugin, UserConfig } from 'vite'; + +import { injectSentryInitPlugin } from './injectInitPlugin'; +import { hasSentryInitFiles } from './utils'; + +/** + * Vite Plugin for the Sentry SvelteKit SDK, taking care of: + * + * - Creating Sentry releases and uploading source maps to Sentry + * - Injecting Sentry.init calls if you use dedicated `sentry.(client|server).config.ts` files + * + * This plugin adds a few additional properties to your Vite config. + * Make sure, it is registered before the SvelteKit plugin. + */ +export function sentrySvelteKitPlugin(): Plugin { + return { + name: 'sentry-sveltekit', + enforce: 'pre', // we want this plugin to run early enough + config: originalConfig => { + return addSentryConfig(originalConfig); + }, + }; +} + +function addSentryConfig(originalConfig: UserConfig): UserConfig { + const sentryPlugins = []; + + const shouldAddInjectInitPlugin = hasSentryInitFiles(); + + if (shouldAddInjectInitPlugin) { + sentryPlugins.push(injectSentryInitPlugin); + } + + const config = { + ...originalConfig, + plugins: originalConfig.plugins ? [...sentryPlugins, ...originalConfig.plugins] : [...sentryPlugins], + }; + + const mergedDevServerFileSystemConfig: UserConfig['server'] = shouldAddInjectInitPlugin + ? { + fs: { + ...(config.server && config.server.fs), + allow: [...((config.server && config.server.fs && config.server.fs.allow) || []), '.'], + }, + } + : {}; + + config.server = { + ...config.server, + ...mergedDevServerFileSystemConfig, + }; + + return config; +} diff --git a/packages/sveltekit/src/vite/utils.ts b/packages/sveltekit/src/vite/utils.ts new file mode 100644 index 000000000000..5cd67784a493 --- /dev/null +++ b/packages/sveltekit/src/vite/utils.ts @@ -0,0 +1,28 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Checks if the user has a Sentry init file in the root of their project. + * @returns true if the user has a Sentry init file, false otherwise. + */ +export function hasSentryInitFiles(): boolean { + const hasSentryServerInit = !!getUserConfigFile(process.cwd(), 'server'); + const hasSentryClientInit = !!getUserConfigFile(process.cwd(), 'client'); + return hasSentryServerInit || hasSentryClientInit; +} + +/** + * Looks up the sentry.{@param platform}.config.(ts|js) file + * @returns the file path to the file or undefined if it doesn't exist + */ +export function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string | undefined { + const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; + + for (const filename of possibilities) { + if (fs.existsSync(path.resolve(projectDir, filename))) { + return filename; + } + } + + return undefined; +} diff --git a/packages/sveltekit/src/config/withSentryViteConfig.ts b/packages/sveltekit/src/vite/withSentryViteConfig.ts similarity index 86% rename from packages/sveltekit/src/config/withSentryViteConfig.ts rename to packages/sveltekit/src/vite/withSentryViteConfig.ts index 94a493b14974..1dad490a4a58 100644 --- a/packages/sveltekit/src/config/withSentryViteConfig.ts +++ b/packages/sveltekit/src/vite/withSentryViteConfig.ts @@ -1,6 +1,7 @@ import type { UserConfig, UserConfigExport } from 'vite'; -import { getUserConfigFile, injectSentryInitPlugin } from './vitePlugins'; +import { injectSentryInitPlugin } from './injectInitPlugin'; +import { hasSentryInitFiles } from './utils'; /** * This function adds Sentry-specific configuration to your Vite config. @@ -60,9 +61,3 @@ function addSentryConfig(originalConfig: UserConfig): UserConfig { return config; } - -function hasSentryInitFiles(): boolean { - const hasSentryServerInit = !!getUserConfigFile(process.cwd(), 'server'); - const hasSentryClientInit = !!getUserConfigFile(process.cwd(), 'client'); - return hasSentryServerInit || hasSentryClientInit; -} diff --git a/packages/sveltekit/test/config/vitePlugins.test.ts b/packages/sveltekit/test/vite/injectInitPlugin.test.ts similarity index 96% rename from packages/sveltekit/test/config/vitePlugins.test.ts rename to packages/sveltekit/test/vite/injectInitPlugin.test.ts index f04077383d49..f9fcc91b3d3f 100644 --- a/packages/sveltekit/test/config/vitePlugins.test.ts +++ b/packages/sveltekit/test/vite/injectInitPlugin.test.ts @@ -1,7 +1,7 @@ import type * as fs from 'fs'; import { vi } from 'vitest'; -import { injectSentryInitPlugin } from '../../src/config/vitePlugins'; +import { injectSentryInitPlugin } from '../../src/vite/injectInitPlugin'; vi.mock('fs', async () => { const original = await vi.importActual('fs'); diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts new file mode 100644 index 000000000000..97948003ee44 --- /dev/null +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts @@ -0,0 +1,108 @@ +import { vi } from 'vitest'; + +import { sentrySvelteKitPlugin } from './../../src/vite/sentrySvelteKitPlugin'; +import * as utils from './../../src/vite/utils'; + +describe('sentrySvelteKitPlugin', () => { + it('returns a Vite plugin with name, enforce, and config hook', () => { + const plugin = sentrySvelteKitPlugin(); + expect(plugin).toHaveProperty('name'); + expect(plugin).toHaveProperty('enforce'); + expect(plugin).toHaveProperty('config'); + expect(plugin.name).toEqual('sentry-sveltekit'); + expect(plugin.enforce).toEqual('pre'); + }); + + describe('config hook', () => { + const hasSentryInitFilesSpy = vi.spyOn(utils, 'hasSentryInitFiles').mockReturnValue(true); + + beforeEach(() => { + hasSentryInitFilesSpy.mockClear(); + }); + + it('adds the injectInitPlugin and adjusts the dev server config if init config files exist', () => { + const plugin = sentrySvelteKitPlugin(); + const originalConfig = {}; + + // @ts-ignore - plugin.config exists and is callable + const modifiedConfig = plugin.config(originalConfig); + + expect(modifiedConfig).toEqual({ + plugins: [ + { + enforce: 'pre', + name: 'sentry-init-injection-plugin', + transform: expect.any(Function), + }, + ], + server: { + fs: { + allow: ['.'], + }, + }, + }); + expect(hasSentryInitFilesSpy).toHaveBeenCalledTimes(1); + }); + + it('merges user-defined options with Sentry-specifc ones', () => { + const plugin = sentrySvelteKitPlugin(); + const originalConfig = { + test: { + include: ['src/**/*.{test,spec}.{js,ts}'], + }, + build: { + sourcemap: 'css', + }, + plugins: [{ name: 'some plugin' }], + server: { + fs: { + allow: ['./build/**/*.{js}'], + }, + }, + }; + + // @ts-ignore - plugin.config exists and is callable + const modifiedConfig = plugin.config(originalConfig); + + expect(modifiedConfig).toEqual({ + test: { + include: ['src/**/*.{test,spec}.{js,ts}'], + }, + build: { + sourcemap: 'css', + }, + plugins: [ + { + enforce: 'pre', + name: 'sentry-init-injection-plugin', + transform: expect.any(Function), + }, + { name: 'some plugin' }, + ], + server: { + fs: { + allow: ['./build/**/*.{js}', '.'], + }, + }, + }); + expect(hasSentryInitFilesSpy).toHaveBeenCalledTimes(1); + }); + + it("doesn't add the injectInitPlugin if init config files don't exist", () => { + hasSentryInitFilesSpy.mockReturnValue(false); + const plugin = sentrySvelteKitPlugin(); + const originalConfig = { + plugins: [{ name: 'some plugin' }], + }; + + // @ts-ignore - plugin.config exists and is callable + const modifiedConfig = plugin.config(originalConfig); + + expect(modifiedConfig).toEqual({ + plugins: [{ name: 'some plugin' }], + server: {}, + }); + expect(hasSentryInitFilesSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/sveltekit/test/config/withSentryViteConfig.test.ts b/packages/sveltekit/test/vite/withSentryViteConfig.test.ts similarity index 98% rename from packages/sveltekit/test/config/withSentryViteConfig.test.ts rename to packages/sveltekit/test/vite/withSentryViteConfig.test.ts index 9dc08eae487a..8add96c00b05 100644 --- a/packages/sveltekit/test/config/withSentryViteConfig.test.ts +++ b/packages/sveltekit/test/vite/withSentryViteConfig.test.ts @@ -2,7 +2,7 @@ import type fs from 'fs'; import type { Plugin, UserConfig } from 'vite'; import { vi } from 'vitest'; -import { withSentryViteConfig } from '../../src/config/withSentryViteConfig'; +import { withSentryViteConfig } from '../../src/vite/withSentryViteConfig'; let existsFile = true; vi.mock('fs', async () => { From d099af85cf81fab8520e4cc6641901c77786f3fc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 7 Apr 2023 15:54:27 +0200 Subject: [PATCH 16/48] feat(sveltekit): Remove `withSentryViteConfig` (#7789) Remove the `withSentryViteConfig` wrapper in favour of `sentrySvelteKitPlugin` introduced in #7788. --- packages/sveltekit/src/vite/index.ts | 1 - .../src/vite/withSentryViteConfig.ts | 63 -------- .../test/vite/withSentryViteConfig.test.ts | 152 ------------------ 3 files changed, 216 deletions(-) delete mode 100644 packages/sveltekit/src/vite/withSentryViteConfig.ts delete mode 100644 packages/sveltekit/test/vite/withSentryViteConfig.test.ts diff --git a/packages/sveltekit/src/vite/index.ts b/packages/sveltekit/src/vite/index.ts index a29a564fd275..2e736f9290e8 100644 --- a/packages/sveltekit/src/vite/index.ts +++ b/packages/sveltekit/src/vite/index.ts @@ -1,2 +1 @@ -export { withSentryViteConfig } from './withSentryViteConfig'; export { sentrySvelteKitPlugin } from './sentrySvelteKitPlugin'; diff --git a/packages/sveltekit/src/vite/withSentryViteConfig.ts b/packages/sveltekit/src/vite/withSentryViteConfig.ts deleted file mode 100644 index 1dad490a4a58..000000000000 --- a/packages/sveltekit/src/vite/withSentryViteConfig.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { UserConfig, UserConfigExport } from 'vite'; - -import { injectSentryInitPlugin } from './injectInitPlugin'; -import { hasSentryInitFiles } from './utils'; - -/** - * This function adds Sentry-specific configuration to your Vite config. - * Pass your config to this function and make sure the return value is exported - * from your `vite.config.js` file. - * - * Note: If you're already wrapping your config with another wrapper, - * for instance with `defineConfig` from vitest, make sure - * that the Sentry wrapper is the outermost one. - * - * @param originalConfig your original vite config - * - * @returns a vite config with Sentry-specific configuration added to it. - */ -export function withSentryViteConfig(originalConfig: UserConfigExport): UserConfigExport { - if (typeof originalConfig === 'function') { - return function (this: unknown, ...viteConfigFunctionArgs: unknown[]): UserConfig | Promise { - const userViteConfigObject = originalConfig.apply(this, viteConfigFunctionArgs); - if (userViteConfigObject instanceof Promise) { - return userViteConfigObject.then(userConfig => addSentryConfig(userConfig)); - } - return addSentryConfig(userViteConfigObject); - }; - } else if (originalConfig instanceof Promise) { - return originalConfig.then(userConfig => addSentryConfig(userConfig)); - } - return addSentryConfig(originalConfig); -} - -function addSentryConfig(originalConfig: UserConfig): UserConfig { - const sentryPlugins = []; - - const shouldAddInjectInitPlugin = hasSentryInitFiles(); - - if (shouldAddInjectInitPlugin) { - sentryPlugins.push(injectSentryInitPlugin); - } - - const config = { - ...originalConfig, - plugins: originalConfig.plugins ? [...sentryPlugins, ...originalConfig.plugins] : [...sentryPlugins], - }; - - const mergedDevServerFileSystemConfig: UserConfig['server'] = shouldAddInjectInitPlugin - ? { - fs: { - ...(config.server && config.server.fs), - allow: [...((config.server && config.server.fs && config.server.fs.allow) || []), '.'], - }, - } - : {}; - - config.server = { - ...config.server, - ...mergedDevServerFileSystemConfig, - }; - - return config; -} diff --git a/packages/sveltekit/test/vite/withSentryViteConfig.test.ts b/packages/sveltekit/test/vite/withSentryViteConfig.test.ts deleted file mode 100644 index 8add96c00b05..000000000000 --- a/packages/sveltekit/test/vite/withSentryViteConfig.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import type fs from 'fs'; -import type { Plugin, UserConfig } from 'vite'; -import { vi } from 'vitest'; - -import { withSentryViteConfig } from '../../src/vite/withSentryViteConfig'; - -let existsFile = true; -vi.mock('fs', async () => { - const original = await vi.importActual('fs'); - return { - ...original, - existsSync: vi.fn().mockImplementation(() => existsFile), - }; -}); -describe('withSentryViteConfig', () => { - const originalConfig = { - plugins: [{ name: 'foo' }], - server: { - fs: { - allow: ['./bar'], - }, - }, - test: { - include: ['src/**/*.{test,spec}.{js,ts}'], - }, - }; - - it('takes a POJO Vite config and returns the sentrified version', () => { - const sentrifiedConfig = withSentryViteConfig(originalConfig); - - expect(typeof sentrifiedConfig).toBe('object'); - - const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; - - expect(plugins).toHaveLength(2); - expect(plugins[0].name).toBe('sentry-init-injection-plugin'); - expect(plugins[1].name).toBe('foo'); - - expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); - - expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); - }); - - it('takes a Vite config Promise and returns the sentrified version', async () => { - const sentrifiedConfig = await withSentryViteConfig(Promise.resolve(originalConfig)); - - expect(typeof sentrifiedConfig).toBe('object'); - - const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; - - expect(plugins).toHaveLength(2); - expect(plugins[0].name).toBe('sentry-init-injection-plugin'); - expect(plugins[1].name).toBe('foo'); - - expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); - - expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); - }); - - it('takes a function returning a Vite config and returns the sentrified version', () => { - const sentrifiedConfigFunction = withSentryViteConfig(_env => { - return originalConfig; - }); - const sentrifiedConfig = - typeof sentrifiedConfigFunction === 'function' && sentrifiedConfigFunction({ command: 'build', mode: 'test' }); - - expect(typeof sentrifiedConfig).toBe('object'); - - const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; - - expect(plugins).toHaveLength(2); - expect(plugins[0].name).toBe('sentry-init-injection-plugin'); - expect(plugins[1].name).toBe('foo'); - - expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); - - expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); - }); - - it('takes a function returning a Vite config promise and returns the sentrified version', async () => { - const sentrifiedConfigFunction = withSentryViteConfig(_env => { - return Promise.resolve(originalConfig); - }); - const sentrifiedConfig = - typeof sentrifiedConfigFunction === 'function' && - (await sentrifiedConfigFunction({ command: 'build', mode: 'test' })); - - expect(typeof sentrifiedConfig).toBe('object'); - - const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; - - expect(plugins).toHaveLength(2); - expect(plugins[0].name).toBe('sentry-init-injection-plugin'); - expect(plugins[1].name).toBe('foo'); - - expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar', '.']); - - expect((sentrifiedConfig as any).test).toEqual(originalConfig.test); - }); - - it('adds the vite plugin if no plugins are present', () => { - const sentrifiedConfig = withSentryViteConfig({ - test: { - include: ['src/**/*.{test,spec}.{js,ts}'], - }, - } as UserConfig); - - expect(typeof sentrifiedConfig).toBe('object'); - - const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; - - expect(plugins).toHaveLength(1); - expect(plugins[0].name).toBe('sentry-init-injection-plugin'); - }); - - it('adds the vite plugin and server config to an empty vite config', () => { - const sentrifiedConfig = withSentryViteConfig({}); - - expect(typeof sentrifiedConfig).toBe('object'); - - const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; - - expect(plugins).toHaveLength(1); - expect(plugins[0].name).toBe('sentry-init-injection-plugin'); - - expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['.']); - }); - - it("doesn't add the inject init plugin or the server config if sentry config files don't exist", () => { - existsFile = false; - - const sentrifiedConfig = withSentryViteConfig({ - plugins: [{ name: 'some plugin' }], - test: { - include: ['src/**/*.{test,spec}.{js,ts}'], - }, - server: { - fs: { - allow: ['./bar'], - }, - }, - } as UserConfig); - - expect(typeof sentrifiedConfig).toBe('object'); - const plugins = (sentrifiedConfig as UserConfig).plugins as Plugin[]; - expect(plugins).toHaveLength(1); - expect(plugins[0].name).toBe('some plugin'); - expect((sentrifiedConfig as UserConfig).server?.fs?.allow).toStrictEqual(['./bar']); - - existsFile = true; - }); -}); From 87b33e2923e252157d8bc1737ac79bf417789fab Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 7 Apr 2023 16:50:23 +0200 Subject: [PATCH 17/48] feat(sveltekit): Remove SDK initialization via dedicated files (#7791) This PR removes the `injectInitPlugin` which means that initialization of the SDK via dedicated files is no longer supported. To initialized the SDK, the `hooks.(client|server).js` should be used, as already documented in the `README`. --- .../sveltekit/src/vite/injectInitPlugin.ts | 62 ------------ .../src/vite/sentrySvelteKitPlugin.ts | 29 +----- packages/sveltekit/src/vite/utils.ts | 28 ------ .../test/vite/injectInitPlugin.test.ts | 65 ------------- .../test/vite/sentrySvelteKitPlugin.test.ts | 96 ------------------- 5 files changed, 4 insertions(+), 276 deletions(-) delete mode 100644 packages/sveltekit/src/vite/injectInitPlugin.ts delete mode 100644 packages/sveltekit/src/vite/utils.ts delete mode 100644 packages/sveltekit/test/vite/injectInitPlugin.test.ts diff --git a/packages/sveltekit/src/vite/injectInitPlugin.ts b/packages/sveltekit/src/vite/injectInitPlugin.ts deleted file mode 100644 index 324f83404eff..000000000000 --- a/packages/sveltekit/src/vite/injectInitPlugin.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { logger } from '@sentry/utils'; -import MagicString from 'magic-string'; -import * as path from 'path'; -import type { Plugin, TransformResult } from 'vite'; - -import { getUserConfigFile } from './utils'; - -const serverIndexFilePath = path.join('@sveltejs', 'kit', 'src', 'runtime', 'server', 'index.js'); -const devClientAppFilePath = path.join('generated', 'client', 'app.js'); -const prodClientAppFilePath = path.join('generated', 'client-optimized', 'app.js'); - -/** - * This plugin injects the `Sentry.init` calls from `sentry.(client|server).config.(ts|js)` - * into SvelteKit runtime files. - */ -export const injectSentryInitPlugin: Plugin = { - name: 'sentry-init-injection-plugin', - - // In this hook, we inject the `Sentry.init` calls from `sentry.(client|server).config.(ts|js)` - // into SvelteKit runtime files: For the server, we inject it into the server's `index.js` - // file. For the client, we use the `_app.js` file. - transform(code, id) { - if (id.endsWith(serverIndexFilePath)) { - logger.debug('Injecting Server Sentry.init into', id); - return addSentryConfigFileImport('server', code, id) || code; - } - - if (id.endsWith(devClientAppFilePath) || id.endsWith(prodClientAppFilePath)) { - logger.debug('Injecting Client Sentry.init into', id); - return addSentryConfigFileImport('client', code, id) || code; - } - - return code; - }, - - // This plugin should run as early as possible, - // setting `enforce: 'pre'` ensures that it runs before the built-in vite plugins. - // see: https://vitejs.dev/guide/api-plugin.html#plugin-ordering - enforce: 'pre', -}; - -function addSentryConfigFileImport( - platform: 'server' | 'client', - originalCode: string, - entryFileId: string, -): TransformResult | undefined { - const projectRoot = process.cwd(); - const sentryConfigFilename = getUserConfigFile(projectRoot, platform); - - if (!sentryConfigFilename) { - logger.error(`Could not find sentry.${platform}.config.(ts|js) file.`); - return undefined; - } - - const filePath = path.join(path.relative(path.dirname(entryFileId), projectRoot), sentryConfigFilename); - const importStmt = `\nimport "${filePath}";`; - - const ms = new MagicString(originalCode); - ms.append(importStmt); - - return { code: ms.toString(), map: ms.generateMap() }; -} diff --git a/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts b/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts index 1cb5b9a5b4f4..959e6e94a417 100644 --- a/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts +++ b/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts @@ -1,8 +1,5 @@ import type { Plugin, UserConfig } from 'vite'; -import { injectSentryInitPlugin } from './injectInitPlugin'; -import { hasSentryInitFiles } from './utils'; - /** * Vite Plugin for the Sentry SvelteKit SDK, taking care of: * @@ -23,31 +20,13 @@ export function sentrySvelteKitPlugin(): Plugin { } function addSentryConfig(originalConfig: UserConfig): UserConfig { - const sentryPlugins = []; - - const shouldAddInjectInitPlugin = hasSentryInitFiles(); + const sentryPlugins: Plugin[] = []; - if (shouldAddInjectInitPlugin) { - sentryPlugins.push(injectSentryInitPlugin); - } + // TODO: Add sentry vite plugin here - const config = { + const config: UserConfig = { ...originalConfig, - plugins: originalConfig.plugins ? [...sentryPlugins, ...originalConfig.plugins] : [...sentryPlugins], - }; - - const mergedDevServerFileSystemConfig: UserConfig['server'] = shouldAddInjectInitPlugin - ? { - fs: { - ...(config.server && config.server.fs), - allow: [...((config.server && config.server.fs && config.server.fs.allow) || []), '.'], - }, - } - : {}; - - config.server = { - ...config.server, - ...mergedDevServerFileSystemConfig, + plugins: [...sentryPlugins, ...(originalConfig.plugins || [])], }; return config; diff --git a/packages/sveltekit/src/vite/utils.ts b/packages/sveltekit/src/vite/utils.ts deleted file mode 100644 index 5cd67784a493..000000000000 --- a/packages/sveltekit/src/vite/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -/** - * Checks if the user has a Sentry init file in the root of their project. - * @returns true if the user has a Sentry init file, false otherwise. - */ -export function hasSentryInitFiles(): boolean { - const hasSentryServerInit = !!getUserConfigFile(process.cwd(), 'server'); - const hasSentryClientInit = !!getUserConfigFile(process.cwd(), 'client'); - return hasSentryServerInit || hasSentryClientInit; -} - -/** - * Looks up the sentry.{@param platform}.config.(ts|js) file - * @returns the file path to the file or undefined if it doesn't exist - */ -export function getUserConfigFile(projectDir: string, platform: 'server' | 'client'): string | undefined { - const possibilities = [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]; - - for (const filename of possibilities) { - if (fs.existsSync(path.resolve(projectDir, filename))) { - return filename; - } - } - - return undefined; -} diff --git a/packages/sveltekit/test/vite/injectInitPlugin.test.ts b/packages/sveltekit/test/vite/injectInitPlugin.test.ts deleted file mode 100644 index f9fcc91b3d3f..000000000000 --- a/packages/sveltekit/test/vite/injectInitPlugin.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type * as fs from 'fs'; -import { vi } from 'vitest'; - -import { injectSentryInitPlugin } from '../../src/vite/injectInitPlugin'; - -vi.mock('fs', async () => { - const original = await vi.importActual('fs'); - return { - ...original, - existsSync: vi.fn().mockReturnValue(true), - }; -}); - -describe('injectSentryInitPlugin', () => { - it('has its basic properties set', () => { - expect(injectSentryInitPlugin.name).toBe('sentry-init-injection-plugin'); - expect(injectSentryInitPlugin.enforce).toBe('pre'); - expect(typeof injectSentryInitPlugin.transform).toBe('function'); - }); - - describe('transform', () => { - it('transforms the server index file', () => { - const code = 'foo();'; - const id = '/node_modules/@sveltejs/kit/src/runtime/server/index.js'; - - // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. - const result = injectSentryInitPlugin.transform(code, id); - - expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.server\.config\.ts";/gm); - expect(result.map).toBeDefined(); - }); - - it('transforms the client index file (dev server)', () => { - const code = 'foo();'; - const id = '.svelte-kit/generated/client/app.js'; - - // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. - const result = injectSentryInitPlugin.transform(code, id); - - expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.client\.config\.ts";/gm); - expect(result.map).toBeDefined(); - }); - - it('transforms the client index file (prod build)', () => { - const code = 'foo();'; - const id = '.svelte-kit/generated/client-optimized/app.js'; - - // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. - const result = injectSentryInitPlugin.transform(code, id); - - expect(result.code).toMatch(/foo\(\);\n.*import ".*sentry\.client\.config\.ts";/gm); - expect(result.map).toBeDefined(); - }); - - it("doesn't transform other files", () => { - const code = 'foo();'; - const id = './src/routes/+page.ts'; - - // @ts-ignore -- transform is definitely defined and callable. Seems like TS doesn't know that. - const result = injectSentryInitPlugin.transform(code, id); - - expect(result).toBe(code); - }); - }); -}); diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts index 97948003ee44..ebfa67320840 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts @@ -1,7 +1,4 @@ -import { vi } from 'vitest'; - import { sentrySvelteKitPlugin } from './../../src/vite/sentrySvelteKitPlugin'; -import * as utils from './../../src/vite/utils'; describe('sentrySvelteKitPlugin', () => { it('returns a Vite plugin with name, enforce, and config hook', () => { @@ -12,97 +9,4 @@ describe('sentrySvelteKitPlugin', () => { expect(plugin.name).toEqual('sentry-sveltekit'); expect(plugin.enforce).toEqual('pre'); }); - - describe('config hook', () => { - const hasSentryInitFilesSpy = vi.spyOn(utils, 'hasSentryInitFiles').mockReturnValue(true); - - beforeEach(() => { - hasSentryInitFilesSpy.mockClear(); - }); - - it('adds the injectInitPlugin and adjusts the dev server config if init config files exist', () => { - const plugin = sentrySvelteKitPlugin(); - const originalConfig = {}; - - // @ts-ignore - plugin.config exists and is callable - const modifiedConfig = plugin.config(originalConfig); - - expect(modifiedConfig).toEqual({ - plugins: [ - { - enforce: 'pre', - name: 'sentry-init-injection-plugin', - transform: expect.any(Function), - }, - ], - server: { - fs: { - allow: ['.'], - }, - }, - }); - expect(hasSentryInitFilesSpy).toHaveBeenCalledTimes(1); - }); - - it('merges user-defined options with Sentry-specifc ones', () => { - const plugin = sentrySvelteKitPlugin(); - const originalConfig = { - test: { - include: ['src/**/*.{test,spec}.{js,ts}'], - }, - build: { - sourcemap: 'css', - }, - plugins: [{ name: 'some plugin' }], - server: { - fs: { - allow: ['./build/**/*.{js}'], - }, - }, - }; - - // @ts-ignore - plugin.config exists and is callable - const modifiedConfig = plugin.config(originalConfig); - - expect(modifiedConfig).toEqual({ - test: { - include: ['src/**/*.{test,spec}.{js,ts}'], - }, - build: { - sourcemap: 'css', - }, - plugins: [ - { - enforce: 'pre', - name: 'sentry-init-injection-plugin', - transform: expect.any(Function), - }, - { name: 'some plugin' }, - ], - server: { - fs: { - allow: ['./build/**/*.{js}', '.'], - }, - }, - }); - expect(hasSentryInitFilesSpy).toHaveBeenCalledTimes(1); - }); - - it("doesn't add the injectInitPlugin if init config files don't exist", () => { - hasSentryInitFilesSpy.mockReturnValue(false); - const plugin = sentrySvelteKitPlugin(); - const originalConfig = { - plugins: [{ name: 'some plugin' }], - }; - - // @ts-ignore - plugin.config exists and is callable - const modifiedConfig = plugin.config(originalConfig); - - expect(modifiedConfig).toEqual({ - plugins: [{ name: 'some plugin' }], - server: {}, - }); - expect(hasSentryInitFilesSpy).toHaveBeenCalledTimes(1); - }); - }); }); From 781c3a960ac74d661d46a97d6259df2b5c8093d0 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 7 Apr 2023 16:51:56 +0200 Subject: [PATCH 18/48] doc(sveltekit): Adjust README Vite setup section (#7792) --- packages/sveltekit/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md index 2d2b9fdd3dba..c14d7c124b83 100644 --- a/packages/sveltekit/README.md +++ b/packages/sveltekit/README.md @@ -150,19 +150,20 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d ### 5. Vite Setup -3. Add our `withSentryViteConfig` wrapper around your Vite config so that the Sentry SDK can add its plugins to your Vite config `vite.config.(js|ts)`: +1. Add our `sentrySvelteKitPlugin` to your `vite.config.(js|ts)` file so that the Sentry SDK can apply build-time features. + Make sure that it is added before the `sveltekit` plugin: + ```javascript import { sveltekit } from '@sveltejs/kit/vite'; - import { withSentryViteConfig } from '@sentry/sveltekit'; + import { sentrySvelteKitPlugin } from '@sentry/sveltekit'; - export default withSentryViteConfig({ - plugins: [sveltekit()], - // ... - }); + export default { + plugins: [sentrySvelteKitPlugin(), sveltekit()], + // ... rest of your Vite config + }; ``` - In the near future this wrapper will add and configure our [Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin) to automatically upload source maps to Sentry. - Furthermore, if you prefer to intialize the Sentry SDK in dedicated files, instead of the hook files, you can move the `Sentry.init` code to `sentry.(client|server).config.(js|ts)` files and `withSentryViteConfig` will take care of adding them to your server and client bundles. + In the near future this plugin will add and configure our [Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin) to automatically upload source maps to Sentry. ## Known Limitations From a9621fc9bd2bf25b5140f576dbba5517df3a33d5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 11 Apr 2023 07:38:25 +0200 Subject: [PATCH 19/48] fix(core): Log warning when tracing extensions are missing (#7601) --- packages/core/src/hub.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 905fb4f3bcde..1258548dbd20 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -385,7 +385,17 @@ export class Hub implements HubInterface { * @inheritDoc */ public startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction { - return this._callExtensionMethod('startTransaction', context, customSamplingContext); + const result = this._callExtensionMethod('startTransaction', context, customSamplingContext); + + if (__DEBUG_BUILD__ && !result) { + // eslint-disable-next-line no-console + console.warn(`Tracing extension 'startTransaction' has not been added. Call 'addTracingExtensions' before calling 'init': +Sentry.addTracingExtensions(); +Sentry.init({...}); +`); + } + + return result; } /** From 7987221a2f72267b96be815e158916d9392a5cdb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Apr 2023 19:41:56 +0000 Subject: [PATCH 20/48] build(deps-dev): Bump @sveltejs/kit from 1.15.1 to 1.15.2 Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 1.15.1 to 1.15.2. - [Release notes](https://github.com/sveltejs/kit/releases) - [Changelog](https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md) - [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@1.15.2/packages/kit) --- updated-dependencies: - dependency-name: "@sveltejs/kit" dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fd08d8a2daa3..8eb45ddf8fc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4215,9 +4215,9 @@ highlight.js "^9.15.6" "@sveltejs/kit@^1.11.0": - version "1.15.1" - resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.15.1.tgz#241f1d7e6cf457112b8c098ca26fd2eb85f8db5f" - integrity sha512-Wexy3N+COoClTuRawVJRbLoH5HFxNrXG3uoHt/Yd5IGx8WAcJM9Nj/CcBLw/tjCR9uDDYMnx27HxuPy3YIYQUA== + version "1.15.2" + resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.15.2.tgz#2d351b15aa39ab792c36c2c236c7e31a2010a6b0" + integrity sha512-rLNxZrjbrlPf8AWW8GAU4L/Vvu17e9v8EYl7pUip7x72lTft7RcxeP3z7tsrHpMSBBxC9o4XdKzFvz1vMZyXZw== dependencies: "@sveltejs/vite-plugin-svelte" "^2.0.0" "@types/cookie" "^0.5.1" From 5062ce149380063a8a0359dc91e92c7ab13a4ea6 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 11 Apr 2023 07:39:06 +0200 Subject: [PATCH 21/48] feat(node): Migrate to domains used through `AsyncContextStrategy` (#7779) --- packages/core/src/hub.ts | 49 +--- packages/nextjs/src/server/index.ts | 30 +- .../nextjs/src/server/utils/wrapperUtils.ts | 9 +- .../src/server/wrapApiHandlerWithSentry.ts | 272 +++++++++--------- .../server/wrapServerComponentWithSentry.ts | 9 +- packages/nextjs/test/serverSdk.test.ts | 35 +-- packages/node/package.json | 3 +- packages/node/src/async/domain.ts | 4 +- packages/node/src/handlers.ts | 79 ++--- packages/node/src/index.ts | 12 +- packages/node/src/sdk.ts | 11 +- packages/node/test/domain.test.ts | 72 ----- packages/node/test/handlers.test.ts | 3 + packages/node/test/index.test.ts | 13 +- .../node/test/manual/webpack-domain/index.js | 3 +- packages/remix/src/utils/instrumentServer.ts | 13 +- packages/serverless/package.json | 1 + .../src/gcpfunction/cloud_events.ts | 6 +- packages/serverless/src/gcpfunction/events.ts | 6 +- packages/serverless/src/utils.ts | 18 +- packages/sveltekit/src/server/handle.ts | 7 +- 21 files changed, 236 insertions(+), 419 deletions(-) delete mode 100644 packages/node/test/domain.test.ts diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 1258548dbd20..46fd2d0f7139 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -20,15 +20,7 @@ import type { TransactionContext, User, } from '@sentry/types'; -import { - consoleSandbox, - dateTimestampInSeconds, - getGlobalSingleton, - GLOBAL_OBJ, - isNodeEnv, - logger, - uuid4, -} from '@sentry/utils'; +import { consoleSandbox, dateTimestampInSeconds, getGlobalSingleton, GLOBAL_OBJ, logger, uuid4 } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from './constants'; import { Scope } from './scope'; @@ -54,7 +46,7 @@ export interface RunWithAsyncContextOptions { /** Whether to reuse an existing async context if one exists. Defaults to false. */ reuseExisting?: boolean; /** Instances that should be referenced and retained in the new context */ - args?: unknown[]; + emitters?: unknown[]; } /** @@ -95,10 +87,6 @@ export interface Carrier { */ integrations?: Integration[]; extensions?: { - /** Hack to prevent bundlers from breaking our usage of the domain package in the cross-platform Hub package */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - domain?: { [key: string]: any }; - } & { /** Extension methods for the hub, which are bound to the current Hub instance */ // eslint-disable-next-line @typescript-eslint/ban-types [key: string]: Function; @@ -561,11 +549,6 @@ export function getCurrentHub(): Hub { } } - // Prefer domains over global if they are there (applicable only to Node environment) - if (isNodeEnv()) { - return getHubFromActiveDomain(registry); - } - // Return hub that lives on a global object return getGlobalHub(registry); } @@ -621,34 +604,6 @@ export function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWi return callback(getCurrentHub()); } -/** - * Try to read the hub from an active domain, and fallback to the registry if one doesn't exist - * @returns discovered hub - */ -function getHubFromActiveDomain(registry: Carrier): Hub { - try { - const sentry = getMainCarrier().__SENTRY__; - const activeDomain = sentry && sentry.extensions && sentry.extensions.domain && sentry.extensions.domain.active; - - // If there's no active domain, just return global hub - if (!activeDomain) { - return getHubFromCarrier(registry); - } - - // If there's no hub on current domain, or it's an old API, assign a new one - if (!hasHubOnCarrier(activeDomain) || getHubFromCarrier(activeDomain).isOlderThan(API_VERSION)) { - const registryHubTopStack = getHubFromCarrier(registry).getStackTop(); - setHubOnCarrier(activeDomain, new Hub(registryHubTopStack.client, Scope.clone(registryHubTopStack.scope))); - } - - // Return hub that lives on a domain - return getHubFromCarrier(activeDomain); - } catch (_Oo) { - // Return hub that lives on a global object - return getHubFromCarrier(registry); - } -} - /** * This will tell whether a carrier has a hub on it or not * @param carrier object diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 1eeafddb4251..b22a6c977a78 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,12 +1,10 @@ -import type { Carrier } from '@sentry/core'; -import { getHubFromCarrier, getMainCarrier, hasTracingEnabled } from '@sentry/core'; +import { hasTracingEnabled } from '@sentry/core'; import { RewriteFrames } from '@sentry/integrations'; import type { NodeOptions } from '@sentry/node'; import { configureScope, getCurrentHub, init as nodeInit, Integrations } from '@sentry/node'; import type { EventProcessor } from '@sentry/types'; import type { IntegrationWithExclusionOption } from '@sentry/utils'; import { addOrUpdateIntegration, escapeStringForRegex, logger } from '@sentry/utils'; -import * as domainModule from 'domain'; import * as path from 'path'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; @@ -55,8 +53,6 @@ const globalWithInjectedValues = global as typeof global & { __rewriteFramesDistDir__: string; }; -const domain = domainModule as typeof domainModule & { active: (domainModule.Domain & Carrier) | null }; - // TODO (v8): Remove this /** * @deprecated This constant will be removed in the next major update. @@ -87,16 +83,6 @@ export function init(options: NodeOptions): void { // Right now we only capture frontend sessions for Next.js options.autoSessionTracking = false; - // In an ideal world, this init function would be called before any requests are handled. That way, every domain we - // use to wrap a request would inherit its scope and client from the global hub. In practice, however, handling the - // first request is what causes us to initialize the SDK, as the init code is injected into `_app` and all API route - // handlers, and those are only accessed in the course of handling a request. As a result, we're already in a domain - // when `init` is called. In order to compensate for this and mimic the ideal world scenario, we stash the active - // domain, run `init` as normal, and then restore the domain afterwards, copying over data from the main hub as if we - // really were inheriting. - const activeDomain = domain.active; - domain.active = null; - nodeInit(options); const filterTransactions: EventProcessor = event => { @@ -118,20 +104,6 @@ export function init(options: NodeOptions): void { } }); - if (activeDomain) { - const globalHub = getHubFromCarrier(getMainCarrier()); - const domainHub = getHubFromCarrier(activeDomain); - - // apply the changes made by `nodeInit` to the domain's hub also - domainHub.bindClient(globalHub.getClient()); - domainHub.getScope()?.update(globalHub.getScope()); - // `scope.update()` doesn’t copy over event processors, so we have to add it manually - domainHub.getScope()?.addEventProcessor(filterTransactions); - - // restore the domain hub as the current one - domain.active = activeDomain; - } - __DEBUG_BUILD__ && logger.log('SDK successfully initialized'); } diff --git a/packages/nextjs/src/server/utils/wrapperUtils.ts b/packages/nextjs/src/server/utils/wrapperUtils.ts index 9fa91fbbee5a..2448cfb7b8b6 100644 --- a/packages/nextjs/src/server/utils/wrapperUtils.ts +++ b/packages/nextjs/src/server/utils/wrapperUtils.ts @@ -1,7 +1,6 @@ -import { captureException, getActiveTransaction, getCurrentHub, startTransaction } from '@sentry/core'; +import { captureException, getActiveTransaction, runWithAsyncContext, startTransaction } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils'; -import * as domain from 'domain'; import type { IncomingMessage, ServerResponse } from 'http'; import { platformSupportsStreaming } from './platformSupportsStreaming'; @@ -75,7 +74,7 @@ export function withTracedServerSideDataFetcher Pr }, ): (...params: Parameters) => Promise> { return async function (this: unknown, ...args: Parameters): Promise> { - return domain.create().bind(async () => { + return runWithAsyncContext(async hub => { let requestTransaction: Transaction | undefined = getTransactionFromRequest(req); let dataFetcherSpan; @@ -134,7 +133,7 @@ export function withTracedServerSideDataFetcher Pr }); } - const currentScope = getCurrentHub().getScope(); + const currentScope = hub.getScope(); if (currentScope) { currentScope.setSpan(dataFetcherSpan); currentScope.setSDKProcessingMetadata({ request: req }); @@ -154,7 +153,7 @@ export function withTracedServerSideDataFetcher Pr await flushQueue(); } } - })(); + }); }; } diff --git a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts index 37ef93bf30cc..b1388352c039 100644 --- a/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/server/wrapApiHandlerWithSentry.ts @@ -1,5 +1,5 @@ -import { hasTracingEnabled } from '@sentry/core'; -import { captureException, getCurrentHub, startTransaction } from '@sentry/node'; +import { hasTracingEnabled, runWithAsyncContext } from '@sentry/core'; +import { captureException, startTransaction } from '@sentry/node'; import type { Transaction } from '@sentry/types'; import { addExceptionMechanism, @@ -10,7 +10,6 @@ import { objectify, stripUrlQueryAndFragment, } from '@sentry/utils'; -import * as domain from 'domain'; import type { AugmentedNextApiRequest, AugmentedNextApiResponse, NextApiHandler } from './types'; import { platformSupportsStreaming } from './utils/platformSupportsStreaming'; @@ -61,159 +60,154 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri } req.__withSentry_applied__ = true; - // use a domain in order to prevent scope bleed between requests - const local = domain.create(); - local.add(req); - local.add(res); - - // `local.bind` causes everything to run inside a domain, just like `local.run` does, but it also lets the callback - // return a value. In our case, all any of the codepaths return is a promise of `void`, but nextjs still counts on - // getting that before it will finish the response. - // eslint-disable-next-line complexity - const boundHandler = local.bind(async () => { - let transaction: Transaction | undefined; - const hub = getCurrentHub(); - const currentScope = hub.getScope(); - const options = hub.getClient()?.getOptions(); - - if (currentScope) { - currentScope.setSDKProcessingMetadata({ request: req }); - - if (hasTracingEnabled(options) && options?.instrumenter === 'sentry') { - // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision) - let traceparentData; - if (req.headers && isString(req.headers['sentry-trace'])) { - traceparentData = extractTraceparentData(req.headers['sentry-trace']); - __DEBUG_BUILD__ && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`); - } + // eslint-disable-next-line complexity, @typescript-eslint/no-explicit-any + const boundHandler = runWithAsyncContext( + // eslint-disable-next-line complexity + async hub => { + let transaction: Transaction | undefined; + const currentScope = hub.getScope(); + const options = hub.getClient()?.getOptions(); - const baggageHeader = req.headers && req.headers.baggage; - const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader); - - // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler) - let reqPath = parameterizedRoute; - - // If not, fake it by just replacing parameter values with their names, hoping that none of them match either - // each other or any hard-coded parts of the path - if (!reqPath) { - const url = `${req.url}`; - // pull off query string, if any - reqPath = stripUrlQueryAndFragment(url); - // Replace with placeholder - if (req.query) { - for (const [key, value] of Object.entries(req.query)) { - reqPath = reqPath.replace(`${value}`, `[${key}]`); - } + if (currentScope) { + currentScope.setSDKProcessingMetadata({ request: req }); + + if (hasTracingEnabled(options) && options?.instrumenter === 'sentry') { + // If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision) + let traceparentData; + if (req.headers && isString(req.headers['sentry-trace'])) { + traceparentData = extractTraceparentData(req.headers['sentry-trace']); + __DEBUG_BUILD__ && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`); } - } - const reqMethod = `${(req.method || 'GET').toUpperCase()} `; - - transaction = startTransaction( - { - name: `${reqMethod}${reqPath}`, - op: 'http.server', - ...traceparentData, - metadata: { - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - source: 'route', - request: req, - }, - }, - // extra context passed to the `tracesSampler` - { request: req }, - ); - currentScope.setSpan(transaction); - if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { - autoEndTransactionOnResponseEnd(transaction, res); - } else { - // If we're not on a platform that supports streaming, we're blocking res.end() until the queue is flushed. - // res.json() and res.send() will implicitly call res.end(), so it is enough to wrap res.end(). - - // eslint-disable-next-line @typescript-eslint/unbound-method - const origResEnd = res.end; - res.end = async function (this: unknown, ...args: unknown[]) { - if (transaction) { - await finishTransaction(transaction, res); - await flushQueue(); + const baggageHeader = req.headers && req.headers.baggage; + const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader); + + // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler) + let reqPath = parameterizedRoute; + + // If not, fake it by just replacing parameter values with their names, hoping that none of them match either + // each other or any hard-coded parts of the path + if (!reqPath) { + const url = `${req.url}`; + // pull off query string, if any + reqPath = stripUrlQueryAndFragment(url); + // Replace with placeholder + if (req.query) { + for (const [key, value] of Object.entries(req.query)) { + reqPath = reqPath.replace(`${value}`, `[${key}]`); + } } + } - origResEnd.apply(this, args); - }; + const reqMethod = `${(req.method || 'GET').toUpperCase()} `; + + transaction = startTransaction( + { + name: `${reqMethod}${reqPath}`, + op: 'http.server', + ...traceparentData, + metadata: { + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + source: 'route', + request: req, + }, + }, + // extra context passed to the `tracesSampler` + { request: req }, + ); + currentScope.setSpan(transaction); + if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { + autoEndTransactionOnResponseEnd(transaction, res); + } else { + // If we're not on a platform that supports streaming, we're blocking res.end() until the queue is flushed. + // res.json() and res.send() will implicitly call res.end(), so it is enough to wrap res.end(). + + // eslint-disable-next-line @typescript-eslint/unbound-method + const origResEnd = res.end; + res.end = async function (this: unknown, ...args: unknown[]) { + if (transaction) { + await finishTransaction(transaction, res); + await flushQueue(); + } + + origResEnd.apply(this, args); + }; + } } } - } - - try { - const handlerResult = await wrappingTarget.apply(thisArg, args); - - if ( - process.env.NODE_ENV === 'development' && - !process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR && - !res.finished - // This can only happen (not always) when the user is using `withSentry` manually, which we're deprecating. - // Warning suppression on Next.JS is only necessary in that case. - ) { - // eslint-disable-next-line no-console - console.warn( - `[sentry] If Next.js logs a warning "API resolved without sending a response", it's a false positive, which may happen when you use \`withSentry\` manually to wrap your routes. + + try { + const handlerResult = await wrappingTarget.apply(thisArg, args); + + if ( + process.env.NODE_ENV === 'development' && + !process.env.SENTRY_IGNORE_API_RESOLUTION_ERROR && + !res.finished + // This can only happen (not always) when the user is using `withSentry` manually, which we're deprecating. + // Warning suppression on Next.JS is only necessary in that case. + ) { + // eslint-disable-next-line no-console + console.warn( + `[sentry] If Next.js logs a warning "API resolved without sending a response", it's a false positive, which may happen when you use \`withSentry\` manually to wrap your routes. To suppress this warning, set \`SENTRY_IGNORE_API_RESOLUTION_ERROR\` to 1 in your env. To suppress the nextjs warning, use the \`externalResolver\` API route option (see https://nextjs.org/docs/api-routes/api-middlewares#custom-config for details).`, - ); - } - - return handlerResult; - } catch (e) { - // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can - // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced - // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a - // way to prevent it from actually being reported twice.) - const objectifiedErr = objectify(e); + ); + } - if (currentScope) { - currentScope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'instrument', - handled: true, - data: { - wrapped_handler: wrappingTarget.name, - function: 'withSentry', - }, + return handlerResult; + } catch (e) { + // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can + // store a seen flag on it. (Because of the one-way-on-Vercel-one-way-off-of-Vercel approach we've been forced + // to take, it can happen that the same thrown object gets caught in two different ways, and flagging it is a + // way to prevent it from actually being reported twice.) + const objectifiedErr = objectify(e); + + if (currentScope) { + currentScope.addEventProcessor(event => { + addExceptionMechanism(event, { + type: 'instrument', + handled: true, + data: { + wrapped_handler: wrappingTarget.name, + function: 'withSentry', + }, + }); + return event; }); - return event; - }); - captureException(objectifiedErr); - } + captureException(objectifiedErr); + } - // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet - // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that - // the transaction was error-free - res.statusCode = 500; - res.statusMessage = 'Internal Server Error'; - - // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors - // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the - // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not - // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already - // be finished and the queue will already be empty, so effectively it'll just no-op.) - if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { - void finishTransaction(transaction, res); - } else { - await finishTransaction(transaction, res); - await flushQueue(); - } + // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet + // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that + // the transaction was error-free + res.statusCode = 500; + res.statusMessage = 'Internal Server Error'; + + // Make sure we have a chance to finish the transaction and flush events to Sentry before the handler errors + // out. (Apps which are deployed on Vercel run their API routes in lambdas, and those lambdas will shut down the + // moment they detect an error, so it's important to get this done before rethrowing the error. Apps not + // deployed serverlessly will run into this cleanup code again in `res.end(), but the transaction will already + // be finished and the queue will already be empty, so effectively it'll just no-op.) + if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { + void finishTransaction(transaction, res); + } else { + await finishTransaction(transaction, res); + await flushQueue(); + } - // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it - // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark - // the error as already having been captured.) - throw objectifiedErr; - } - }); + // We rethrow here so that nextjs can do with the error whatever it would normally do. (Sometimes "whatever it + // would normally do" is to allow the error to bubble up to the global handlers - another reason we need to mark + // the error as already having been captured.) + throw objectifiedErr; + } + }, + { emitters: [req, res] }, + ); // Since API route handlers are all async, nextjs always awaits the return value (meaning it's fine for us to return // a promise here rather than a real result, and it saves us the overhead of an `await` call.) - return boundHandler(); + return boundHandler; }, }); } diff --git a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts b/packages/nextjs/src/server/wrapServerComponentWithSentry.ts index 2c25e8811409..73e331424c5c 100644 --- a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/server/wrapServerComponentWithSentry.ts @@ -1,6 +1,5 @@ -import { addTracingExtensions, captureException, getCurrentHub, startTransaction } from '@sentry/core'; +import { addTracingExtensions, captureException, runWithAsyncContext, startTransaction } from '@sentry/core'; import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils'; -import * as domain from 'domain'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; @@ -21,7 +20,7 @@ export function wrapServerComponentWithSentry any> // hook. 🤯 return new Proxy(appDirComponent, { apply: (originalFunction, thisArg, args) => { - return domain.create().bind(() => { + return runWithAsyncContext(hub => { let maybePromiseResult; const traceparentData = context.sentryTraceHeader @@ -41,7 +40,7 @@ export function wrapServerComponentWithSentry any> }, }); - const currentScope = getCurrentHub().getScope(); + const currentScope = hub.getScope(); if (currentScope) { currentScope.setSpan(transaction); } @@ -85,7 +84,7 @@ export function wrapServerComponentWithSentry any> transaction.finish(); return maybePromiseResult; } - })(); + }); }, }); } diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index bb55d9f6e184..4367c9ec1abc 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -1,8 +1,8 @@ +import { runWithAsyncContext } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import { getCurrentHub, NodeClient } from '@sentry/node'; import type { Integration } from '@sentry/types'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; -import * as domain from 'domain'; import { init } from '../src/server'; @@ -21,7 +21,7 @@ function findIntegrationByName(integrations: Integration[] = [], name: string): describe('Server init()', () => { afterEach(() => { jest.clearAllMocks(); - GLOBAL_OBJ.__SENTRY__.hub = undefined; + delete GLOBAL_OBJ.__SENTRY__; delete process.env.VERCEL; }); @@ -116,26 +116,27 @@ describe('Server init()', () => { it("initializes both global hub and domain hub when there's an active domain", () => { const globalHub = getCurrentHub(); - const local = domain.create(); - local.run(() => { - const domainHub = getCurrentHub(); - // they are in fact two different hubs, and neither one yet has a client - expect(domainHub).not.toBe(globalHub); + runWithAsyncContext(globalHub2 => { + // If we call runWithAsyncContext before init, it executes the callback in the same context as there is no + // strategy yet + expect(globalHub2).toBe(globalHub); expect(globalHub.getClient()).toBeUndefined(); - expect(domainHub.getClient()).toBeUndefined(); - - // this tag should end up only in the domain hub - domainHub.setTag('dogs', 'areGreat'); + expect(globalHub2.getClient()).toBeUndefined(); init({}); - expect(globalHub.getClient()).toEqual(expect.any(NodeClient)); - expect(domainHub.getClient()).toBe(globalHub.getClient()); - // @ts-ignore need access to protected _tags attribute - expect(globalHub.getScope()._tags).toEqual({ runtime: 'node' }); - // @ts-ignore need access to protected _tags attribute - expect(domainHub.getScope()._tags).toEqual({ runtime: 'node', dogs: 'areGreat' }); + runWithAsyncContext(domainHub => { + // this tag should end up only in the domain hub + domainHub.setTag('dogs', 'areGreat'); + + expect(globalHub.getClient()).toEqual(expect.any(NodeClient)); + expect(domainHub.getClient()).toBe(globalHub.getClient()); + // @ts-ignore need access to protected _tags attribute + expect(globalHub.getScope()._tags).toEqual({ runtime: 'node' }); + // @ts-ignore need access to protected _tags attribute + expect(domainHub.getScope()._tags).toEqual({ runtime: 'node', dogs: 'areGreat' }); + }); }); }); diff --git a/packages/node/package.json b/packages/node/package.json index 49e7a09eb998..483bf278610b 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -62,5 +62,6 @@ }, "volta": { "extends": "../../package.json" - } + }, + "sideEffects": false } diff --git a/packages/node/src/async/domain.ts b/packages/node/src/async/domain.ts index c34bf2e2124f..b41ae623d594 100644 --- a/packages/node/src/async/domain.ts +++ b/packages/node/src/async/domain.ts @@ -31,7 +31,7 @@ function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsync const activeDomain = getActiveDomain(); if (activeDomain) { - for (const emitter of options.args || []) { + for (const emitter of options.emitters || []) { if (emitter instanceof EventEmitter) { activeDomain.add(emitter); } @@ -44,7 +44,7 @@ function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsync const local = domain.create(); - for (const emitter of options.args || []) { + for (const emitter of options.emitters || []) { if (emitter instanceof EventEmitter) { local.add(emitter); } diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 5dfcb7bf0a13..17800cadb03b 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,5 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { captureException, getCurrentHub, hasTracingEnabled, startTransaction, withScope } from '@sentry/core'; +import { + captureException, + getCurrentHub, + hasTracingEnabled, + runWithAsyncContext, + startTransaction, + withScope, +} from '@sentry/core'; import type { Span } from '@sentry/types'; import type { AddRequestDataToEventOptions } from '@sentry/utils'; import { @@ -12,7 +19,6 @@ import { logger, normalize, } from '@sentry/utils'; -import * as domain from 'domain'; import type * as http from 'http'; import type { NodeClient } from './client'; @@ -181,46 +187,43 @@ export function requestHandler( }); }; } - const local = domain.create(); - local.add(req); - local.add(res); - - local.run(() => { - const currentHub = getCurrentHub(); - - currentHub.configureScope(scope => { - scope.setSDKProcessingMetadata({ - request: req, - // TODO (v8): Stop passing this - requestDataOptionsFromExpressHandler: requestDataOptions, - }); + runWithAsyncContext( + currentHub => { + 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(); - if (scope) { - // Set `status` of `RequestSession` to Ok, at the beginning of the request - scope.setRequestSession({ status: 'ok' }); + const client = currentHub.getClient(); + if (isAutoSessionTrackingEnabled(client)) { + const scope = currentHub.getScope(); + if (scope) { + // 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)) { - setImmediate(() => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (client && (client as any)._captureRequestSession) { - // Calling _captureRequestSession to capture request session at the end of the request by incrementing - // the correct SessionAggregates bucket i.e. crashed, errored or exited + res.once('finish', () => { + const client = currentHub.getClient(); + if (isAutoSessionTrackingEnabled(client)) { + setImmediate(() => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (client as any)._captureRequestSession(); - } - }); - } - }); - next(); - }); + if (client && (client as any)._captureRequestSession) { + // Calling _captureRequestSession to capture request session at the end of the request by incrementing + // the correct SessionAggregates bucket i.e. crashed, errored or exited + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (client as any)._captureRequestSession(); + } + }); + } + }); + next(); + }, + { emitters: [req, res] }, + ); }; } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 7b4e3fe5d56b..6537b16aca70 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -37,6 +37,7 @@ export { getCurrentHub, Hub, makeMain, + runWithAsyncContext, Scope, startTransaction, SDK_VERSION, @@ -59,8 +60,7 @@ export { defaultIntegrations, init, defaultStackParser, lastEventId, flush, clos export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from './requestdata'; export { deepReadDirSync } from './utils'; -import { getMainCarrier, Integrations as CoreIntegrations } from '@sentry/core'; -import * as domain from 'domain'; +import { Integrations as CoreIntegrations } from '@sentry/core'; import * as Handlers from './handlers'; import * as NodeIntegrations from './integrations'; @@ -73,11 +73,3 @@ const INTEGRATIONS = { }; export { INTEGRATIONS as Integrations, Handlers }; - -// We need to patch domain on the global __SENTRY__ object to make it work for node in cross-platform packages like -// @sentry/core. If we don't do this, browser bundlers will have troubles resolving `require('domain')`. -const carrier = getMainCarrier(); -if (carrier.__SENTRY__) { - carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {}; - carrier.__SENTRY__.extensions.domain = carrier.__SENTRY__.extensions.domain || domain; -} diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 71ae258826c8..6e8ff2b16d1f 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -5,7 +5,6 @@ import { getMainCarrier, initAndBind, Integrations as CoreIntegrations, - setHubOnCarrier, } from '@sentry/core'; import type { SessionStatus, StackParser } from '@sentry/types'; import { @@ -15,8 +14,8 @@ import { nodeStackLineParser, stackParserFromStackParserOptions, } from '@sentry/utils'; -import * as domain from 'domain'; +import { setDomainAsyncContextStrategy } from './async/domain'; import { NodeClient } from './client'; import { Console, @@ -111,6 +110,9 @@ export const defaultIntegrations = [ */ export function init(options: NodeOptions = {}): void { const carrier = getMainCarrier(); + + setDomainAsyncContextStrategy(); + const autoloadedIntegrations = carrier.__SENTRY__?.integrations || []; options.defaultIntegrations = @@ -154,11 +156,6 @@ export function init(options: NodeOptions = {}): void { options.instrumenter = 'sentry'; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - if ((domain as any).active) { - setHubOnCarrier(carrier, getCurrentHub()); - } - // TODO(v7): Refactor this to reduce the logic above const clientOptions: NodeClientOptions = { ...options, diff --git a/packages/node/test/domain.test.ts b/packages/node/test/domain.test.ts deleted file mode 100644 index fa6bb94db292..000000000000 --- a/packages/node/test/domain.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { getCurrentHub, Hub } from '@sentry/core'; -import * as domain from 'domain'; - -// We need this import here to patch domain on the global object -import * as Sentry from '../src'; - -// TODO This is here because if we don't use the `Sentry` object, the 'concurrent domain hubs' test will fail. Is this a -// product of treeshaking? -Sentry.getCurrentHub(); - -describe('domains', () => { - test('without domain', () => { - // @ts-ignore property active does not exist on domain - expect(domain.active).toBeFalsy(); - const hub = getCurrentHub(); - expect(hub).toEqual(new Hub()); - }); - - test('domain hub scope inheritance', () => { - const globalHub = getCurrentHub(); - globalHub.configureScope(scope => { - scope.setExtra('a', 'b'); - scope.setTag('a', 'b'); - scope.addBreadcrumb({ message: 'a' }); - }); - const d = domain.create(); - d.run(() => { - const hub = getCurrentHub(); - expect(globalHub).toEqual(hub); - }); - }); - - test('domain hub single instance', () => { - const d = domain.create(); - d.run(() => { - expect(getCurrentHub()).toBe(getCurrentHub()); - }); - }); - - test('concurrent domain hubs', done => { - const d1 = domain.create(); - const d2 = domain.create(); - let d1done = false; - let d2done = false; - - d1.run(() => { - const hub = getCurrentHub(); - hub.getStack().push({ client: 'process' } as any); - expect(hub.getStack()[1]).toEqual({ client: 'process' }); - // Just in case so we don't have to worry which one finishes first - // (although it always should be d2) - setTimeout(() => { - d1done = true; - if (d2done) { - done(); - } - }); - }); - - d2.run(() => { - const hub = getCurrentHub(); - hub.getStack().push({ client: 'local' } as any); - expect(hub.getStack()[1]).toEqual({ client: 'local' }); - setTimeout(() => { - d2done = true; - if (d1done) { - done(); - } - }); - }); - }); -}); diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 028cdff98af1..4f7060b655ff 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -4,11 +4,14 @@ import type { Event } from '@sentry/types'; import { SentryError } from '@sentry/utils'; import * as http from 'http'; +import { setDomainAsyncContextStrategy } from '../src/async/domain'; import { NodeClient } from '../src/client'; import { errorHandler, requestHandler, tracingHandler } from '../src/handlers'; import * as SDK from '../src/sdk'; import { getDefaultNodeClientOptions } from './helper/node-client-options'; +setDomainAsyncContextStrategy(); + describe('requestHandler', () => { const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; const method = 'wagging'; diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 6ca834874d2e..8a4c971d4f3f 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -1,6 +1,5 @@ -import { getMainCarrier, initAndBind, SDK_VERSION } from '@sentry/core'; +import { getMainCarrier, initAndBind, runWithAsyncContext, SDK_VERSION } from '@sentry/core'; import type { EventHint, Integration } from '@sentry/types'; -import * as domain from 'domain'; import type { Event, Scope } from '../src'; import { @@ -13,6 +12,7 @@ import { init, NodeClient, } from '../src'; +import { setDomainAsyncContextStrategy } from '../src/async/domain'; import { ContextLines, LinkedErrors } from '../src/integrations'; import { defaultStackParser } from '../src/sdk'; import type { NodeClientOptions } from '../src/types'; @@ -278,8 +278,6 @@ describe('SentryNode', () => { }); test('capture an event in a domain', done => { - const d = domain.create(); - const options = getDefaultNodeClientOptions({ stackParser: defaultStackParser, beforeSend: (event: Event) => { @@ -290,12 +288,13 @@ describe('SentryNode', () => { }, dsn, }); + setDomainAsyncContextStrategy(); const client = new NodeClient(options); - d.run(() => { - getCurrentHub().bindClient(client); + runWithAsyncContext(hub => { + hub.bindClient(client); expect(getCurrentHub().getClient()).toBe(client); - getCurrentHub().captureEvent({ message: 'test domain' }); + hub.captureEvent({ message: 'test domain' }); }); }); diff --git a/packages/node/test/manual/webpack-domain/index.js b/packages/node/test/manual/webpack-domain/index.js index 4a9a5a0f902a..27f3fe0376ef 100644 --- a/packages/node/test/manual/webpack-domain/index.js +++ b/packages/node/test/manual/webpack-domain/index.js @@ -47,8 +47,7 @@ Sentry.configureScope(scope => { scope.setTag('a', 'b'); }); -const d = require('domain').create(); -d.run(() => { +Sentry.runWithAsyncContext(() => { Sentry.configureScope(scope => { scope.setTag('a', 'x'); scope.setTag('b', 'c'); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 8e916d75ffc4..96acde1db2aa 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 } from '@sentry/core'; +import { getActiveTransaction, 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'; @@ -13,7 +13,6 @@ import { loadModule, logger, } from '@sentry/utils'; -import * as domain from 'domain'; import type { AppData, @@ -314,13 +313,7 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui const pkg = loadModule('react-router-dom'); return async function (this: unknown, request: RemixRequest, loadContext?: unknown): Promise { - const local = domain.create(); - - // Waiting for the next tick to make sure that the domain is active - // https://github.com/nodejs/node/issues/40999#issuecomment-1002719169 - await new Promise(resolve => setImmediate(resolve)); - - return local.bind(async () => { + return runWithAsyncContext(async () => { const hub = getCurrentHub(); const options = hub.getClient()?.getOptions(); const scope = hub.getScope(); @@ -365,7 +358,7 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui transaction.finish(); return res; - })(); + }); }; } diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 00c1e8bf1964..5d3cb0ab65a6 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -16,6 +16,7 @@ "access": "public" }, "dependencies": { + "@sentry/core": "7.47.0", "@sentry/node": "7.47.0", "@sentry/types": "7.47.0", "@sentry/utils": "7.47.0", diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 9783d5a530b3..6a8bc47f226f 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -1,7 +1,7 @@ import { captureException, flush, getCurrentHub } from '@sentry/node'; import { isThenable, logger } from '@sentry/utils'; -import { domainify, getActiveDomain, proxyFunction } from '../utils'; +import { domainify, proxyFunction } from '../utils'; import type { CloudEventFunction, CloudEventFunctionWithCallback, WrapperOptions } from './general'; export type CloudEventFunctionWrapperOptions = WrapperOptions; @@ -47,9 +47,7 @@ function _wrapCloudEventFunction( scope.setSpan(transaction); }); - const activeDomain = getActiveDomain()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - - const newCallback = activeDomain.bind((...args: unknown[]) => { + const newCallback = domainify((...args: unknown[]) => { if (args[0] !== null && args[0] !== undefined) { captureException(args[0]); } diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 8f87bd478b0d..c00838ccaae6 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,7 +1,7 @@ import { captureException, flush, getCurrentHub } from '@sentry/node'; import { isThenable, logger } from '@sentry/utils'; -import { domainify, getActiveDomain, proxyFunction } from '../utils'; +import { domainify, proxyFunction } from '../utils'; import type { EventFunction, EventFunctionWithCallback, WrapperOptions } from './general'; export type EventFunctionWrapperOptions = WrapperOptions; @@ -49,9 +49,7 @@ function _wrapEventFunction scope.setSpan(transaction); }); - const activeDomain = getActiveDomain()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion - - const newCallback = activeDomain.bind((...args: unknown[]) => { + const newCallback = domainify((...args: unknown[]) => { if (args[0] !== null && args[0] !== undefined) { captureException(args[0]); } diff --git a/packages/serverless/src/utils.ts b/packages/serverless/src/utils.ts index 38e85c6a1c4e..ae1f4b987ffb 100644 --- a/packages/serverless/src/utils.ts +++ b/packages/serverless/src/utils.ts @@ -1,6 +1,6 @@ +import { runWithAsyncContext } from '@sentry/core'; import type { Event } from '@sentry/node'; import { addExceptionMechanism } from '@sentry/utils'; -import * as domain from 'domain'; /** * Event processor that will override SDK details to point to the serverless SDK instead of Node, @@ -17,26 +17,12 @@ export function serverlessEventProcessor(event: Event): Event { return event; } -/** - * @returns Current active domain with a correct type. - */ -export function getActiveDomain(): domain.Domain | null { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - return (domain as any).active as domain.Domain | null; -} - /** * @param fn function to run * @returns function which runs in the newly created domain or in the existing one */ export function domainify(fn: (...args: A) => R): (...args: A) => R | void { - return (...args) => { - if (getActiveDomain()) { - return fn(...args); - } - const dom = domain.create(); - return dom.run(() => fn(...args)); - }; + return (...args) => runWithAsyncContext(() => fn(...args), { reuseExisting: true }); } /** diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index cc291e63f579..419e749d1732 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,10 +1,9 @@ /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ import type { Span } from '@sentry/core'; -import { getActiveTransaction, getCurrentHub, trace } from '@sentry/core'; +import { getActiveTransaction, getCurrentHub, runWithAsyncContext, trace } from '@sentry/core'; import { captureException } from '@sentry/node'; import { addExceptionMechanism, dynamicSamplingContextToSentryBaggageHeader, objectify } from '@sentry/utils'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; -import * as domain from 'domain'; import { getTracePropagationData } from './utils'; @@ -67,9 +66,9 @@ export const sentryHandle: Handle = input => { if (getCurrentHub().getScope().getSpan()) { return instrumentHandle(input); } - return domain.create().bind(() => { + return runWithAsyncContext(() => { return instrumentHandle(input); - })(); + }); }; function instrumentHandle({ event, resolve }: Parameters[0]): ReturnType { From 73c2be440c48c8ec89b0381e80946aa9dce9f255 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 11 Apr 2023 08:35:06 +0200 Subject: [PATCH 22/48] fix(node): Make `trpcMiddleware` factory synchronous (#7802) --- packages/node/src/handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 17800cadb03b..6fc43d574df6 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -337,7 +337,7 @@ interface TrpcMiddlewareArguments { * Use the Sentry tRPC middleware in combination with the Sentry server integration, * e.g. Express Request Handlers or Next.js SDK. */ -export async function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { +export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { return function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): T { const hub = getCurrentHub(); const clientOptions = hub.getClient()?.getOptions(); From 1ad518f9ead99f8eff912ec065332dd96903fd73 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 11 Apr 2023 09:24:49 +0200 Subject: [PATCH 23/48] test(loader): Add tests for new loader integration handling (#7790) --- .../fixtures/loader.js | 3 +- .../onLoad/customBrowserTracing/init.js | 15 +++++++ .../onLoad/customBrowserTracing/test.ts | 33 +++++++++++++++ .../loader/onLoad/customIntegrations/init.js | 19 +++++++++ .../onLoad/customIntegrations/subject.js | 1 + .../loader/onLoad/customIntegrations/test.ts | 40 +++++++++++++++++++ .../onLoad/customIntegrationsFunction/init.js | 19 +++++++++ .../customIntegrationsFunction/subject.js | 1 + .../onLoad/customIntegrationsFunction/test.ts | 38 ++++++++++++++++++ .../loader/onLoad/customReplay/init.js | 14 +++++++ .../loader/onLoad/customReplay/test.ts | 35 ++++++++++++++++ .../browser-integration-tests/package.json | 3 +- packages/integrations/package.json | 1 + 13 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/init.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customBrowserTracing/test.ts create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/init.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/subject.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/test.ts create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/init.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/subject.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/test.ts create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/test.ts diff --git a/packages/browser-integration-tests/fixtures/loader.js b/packages/browser-integration-tests/fixtures/loader.js index c95b5eabdcac..36d7d2401856 100644 --- a/packages/browser-integration-tests/fixtures/loader.js +++ b/packages/browser-integration-tests/fixtures/loader.js @@ -1,4 +1,5 @@ -!function(n,e,t,r,o,a,i,c,_,p){for(var s=p,forceLoad=!1,f=0;f-1){s&&"no"===document.scripts[f].getAttribute("data-lazy")&&(s=!1);break}var u=!1,l=[],d=function(n){("e"in n||"p"in n||n.f&&n.f.indexOf("capture")>-1||n.f&&n.f.indexOf("showReportDialog")>-1)&&s&&E(l),d.data.push(n)};function E(i){if(!u){u=!0;var p=e.scripts[0],s=e.createElement(t);s.src=c,s.crossOrigin="anonymous",s.addEventListener("load",(function(){try{n[r]&&n[r].__SENTRY_LOADER__&&(n[r]=R),n[o]&&n[o].__SENTRY_LOADER__&&(n[o]=h),n.SENTRY_SDK_SOURCE="loader";var e=n[a],t=e.init,c=[];_.tracesSampleRate&&c.push(new e.BrowserTracing),(_.replaysSessionSampleRate||_.replaysOnErrorSampleRate)&&c.push(new e.Replay),c.length&&(_.integrations=c),e.init=function(n){var e=_;for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r]);t(e)},function(e,t){try{for(var a=0;a-1){p&&"no"===document.scripts[s].getAttribute("data-lazy")&&(p=!1);break}var u=!1,l=[],d=function(n){("e"in n||"p"in n||n.f&&n.f.indexOf("capture")>-1||n.f&&n.f.indexOf("showReportDialog")>-1)&&p&&R(l),d.data.push(n)};function R(o){if(!u){u=!0;var f=e.scripts[0],p=e.createElement(r);p.src=c,p.crossOrigin="anonymous",p.addEventListener("load",(function(){try{n[t]&&n[t].__SENTRY_LOADER__&&(n[t]=E),n[a]&&n[a].__SENTRY_LOADER__&&(n[a]=v),n.SENTRY_SDK_SOURCE="loader";var e=n[i],r=e.init;e.init=function(n){var t=_;for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(t[a]=n[a]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new e.BrowserTracing);(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new e.Replay);n.integrations=r}(t,e),r(t)},function(e,r){try{for(var i=0;i { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const req = waitForTransactionRequest(page); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const eventData = envelopeRequestParser(await req); + const timeOrigin = await page.evaluate('window._testBaseTimestamp'); + + const { start_timestamp: startTimestamp } = eventData; + + expect(startTimestamp).toBeCloseTo(timeOrigin, 1); + + expect(eventData.contexts?.trace?.op).toBe('pageload'); + expect(eventData.spans?.length).toBeGreaterThan(0); + expect(eventData.transaction_info?.source).toEqual('url'); + + const tracePropagationTargets = await page.evaluate(() => { + const browserTracing = (window as any).Sentry.getCurrentHub().getClient().getIntegrationById('BrowserTracing'); + return browserTracing.options.tracePropagationTargets; + }); + + expect(tracePropagationTargets).toEqual(['http://localhost:1234']); +}); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/init.js new file mode 100644 index 000000000000..a5440c1979c5 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +class CustomIntegration { + constructor() { + this.name = 'CustomIntegration'; + } + + setupOnce() {} +} + +Sentry.onLoad(function () { + Sentry.init({ + integrations: [new CustomIntegration()], + }); + + window.__sentryLoaded = true; +}); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/subject.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/subject.js new file mode 100644 index 000000000000..707e54eefe7a --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/subject.js @@ -0,0 +1 @@ +Sentry.forceLoad(); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/test.ts new file mode 100644 index 000000000000..836d10a8d486 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/test.ts @@ -0,0 +1,40 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipReplayTest } from '../../../../utils/replayHelpers'; + +sentryTest('should handle custom added integrations & default integrations', async ({ getLocalTestUrl, page }) => { + const shouldHaveReplay = !shouldSkipReplayTest(); + const shouldHaveBrowserTracing = !shouldSkipTracingTest(); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + await page.waitForFunction(() => { + return (window as any).__sentryLoaded; + }); + + const hasCustomIntegration = await page.evaluate(() => { + return !!(window as any).Sentry.getCurrentHub().getClient().getIntegrationById('CustomIntegration'); + }); + + const hasReplay = await page.evaluate(() => { + return !!(window as any).Sentry.getCurrentHub().getClient().getIntegrationById('Replay'); + }); + const hasBrowserTracing = await page.evaluate(() => { + return !!(window as any).Sentry.getCurrentHub().getClient().getIntegrationById('BrowserTracing'); + }); + + expect(hasCustomIntegration).toEqual(true); + expect(hasReplay).toEqual(shouldHaveReplay); + expect(hasBrowserTracing).toEqual(shouldHaveBrowserTracing); +}); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/init.js new file mode 100644 index 000000000000..4c1e600794d5 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +class CustomIntegration { + constructor() { + this.name = 'CustomIntegration'; + } + + setupOnce() {} +} + +Sentry.onLoad(function () { + Sentry.init({ + integrations: integrations => [new CustomIntegration()].concat(integrations), + }); + + window.__sentryLoaded = true; +}); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/subject.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/subject.js new file mode 100644 index 000000000000..707e54eefe7a --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/subject.js @@ -0,0 +1 @@ +Sentry.forceLoad(); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/test.ts new file mode 100644 index 000000000000..7ff989922f33 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; + +sentryTest( + 'should not add default integrations if integrations function is provided', + async ({ getLocalTestUrl, page }) => { + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + await page.waitForFunction(() => { + return (window as any).__sentryLoaded; + }); + + const hasCustomIntegration = await page.evaluate(() => { + return !!(window as any).Sentry.getCurrentHub().getClient().getIntegrationById('CustomIntegration'); + }); + + const hasReplay = await page.evaluate(() => { + return !!(window as any).Sentry.getCurrentHub().getClient().getIntegrationById('Replay'); + }); + const hasBrowserTracing = await page.evaluate(() => { + return !!(window as any).Sentry.getCurrentHub().getClient().getIntegrationById('BrowserTracing'); + }); + + expect(hasCustomIntegration).toEqual(true); + expect(hasReplay).toEqual(false); + expect(hasBrowserTracing).toEqual(false); + }, +); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js b/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js new file mode 100644 index 000000000000..64d2463ed668 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.onLoad(function () { + Sentry.init({ + integrations: [ + // Without this syntax, this will be re-written by the test framework + new window['Sentry'].Replay({ + useCompression: false, + }), + ], + }); +}); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/test.ts new file mode 100644 index 000000000000..f42868cf9ead --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getReplayEvent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers'; + +sentryTest('should handle custom added Replay integration', async ({ getLocalTestUrl, page }) => { + if (shouldSkipReplayTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const req = waitForReplayRequest(page); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const eventData = getReplayEvent(await req); + + expect(eventData).toBeDefined(); + expect(eventData.segment_id).toBe(0); + + const useCompression = await page.evaluate(() => { + const replay = (window as any).Sentry.getCurrentHub().getClient().getIntegrationById('Replay'); + return replay._replay.getOptions().useCompression; + }); + + expect(useCompression).toEqual(false); +}); diff --git a/packages/browser-integration-tests/package.json b/packages/browser-integration-tests/package.json index a61de94fb0bc..c8b9c92d19e8 100644 --- a/packages/browser-integration-tests/package.json +++ b/packages/browser-integration-tests/package.json @@ -41,7 +41,8 @@ "test:loader:full": "PW_BUNDLE=loader_tracing_replay yarn test:loader", "test:ci": "playwright test ./suites --browser='all' --reporter='line'", "test:update-snapshots": "yarn test --update-snapshots --browser='all' && yarn test --update-snapshots", - "test:detect-flaky": "ts-node scripts/detectFlakyTests.ts" + "test:detect-flaky": "ts-node scripts/detectFlakyTests.ts", + "validate:es5": "es-check es5 'fixtures/loader.js'" }, "dependencies": { "@babel/preset-typescript": "^7.16.7", diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 6b79350fa5a9..6e799214516a 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -44,6 +44,7 @@ "lint": "run-s lint:prettier lint:eslint", "lint:eslint": "eslint . --format stylish", "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", + "validate:es5": "es-check es5 'build/bundles/*.es5*.js'", "test": "jest", "test:watch": "jest --watch", "yalc:publish": "ts-node ../../scripts/prepack.ts --bundles && yalc publish ./build/npm --push" From 27db6608f2c28a16977765884aa5449e3ec204b3 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 11 Apr 2023 09:49:25 +0200 Subject: [PATCH 24/48] fix(node): Fix domain scope inheritance (#7799) --- packages/core/src/hub.ts | 4 +-- packages/node/src/async/domain.ts | 40 +++++++++++++------------ packages/node/test/async/domain.test.ts | 34 +++++++++++++-------- packages/node/test/handlers.test.ts | 36 ++++++++++++++++++---- 4 files changed, 75 insertions(+), 39 deletions(-) diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 46fd2d0f7139..bc3e1ef4fd38 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -568,10 +568,10 @@ function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { * * If the carrier does not contain a hub, a new hub is created with the global hub client and scope. */ -export function ensureHubOnCarrier(carrier: Carrier): void { +export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void { // If there's no hub on current domain, or it's an old API, assign a new one if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { - const globalHubTopStack = getGlobalHub().getStackTop(); + const globalHubTopStack = parent.getStackTop(); setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, Scope.clone(globalHubTopStack.scope))); } } diff --git a/packages/node/src/async/domain.ts b/packages/node/src/async/domain.ts index b41ae623d594..371b2451e1b8 100644 --- a/packages/node/src/async/domain.ts +++ b/packages/node/src/async/domain.ts @@ -1,10 +1,5 @@ import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core'; -import { - ensureHubOnCarrier, - getCurrentHub as getCurrentHubCore, - getHubFromCarrier, - setAsyncContextStrategy, -} from '@sentry/core'; +import { ensureHubOnCarrier, getHubFromCarrier, setAsyncContextStrategy, setHubOnCarrier } from '@sentry/core'; import * as domain from 'domain'; import { EventEmitter } from 'events'; @@ -26,23 +21,27 @@ function getCurrentHub(): Hub | undefined { return getHubFromCarrier(activeDomain); } +function createNewHub(parent: Hub | undefined): Hub { + const carrier: Carrier = {}; + ensureHubOnCarrier(carrier, parent); + return getHubFromCarrier(carrier); +} + function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T { - if (options?.reuseExisting) { - const activeDomain = getActiveDomain(); + const activeDomain = getActiveDomain(); - if (activeDomain) { - for (const emitter of options.emitters || []) { - if (emitter instanceof EventEmitter) { - activeDomain.add(emitter); - } + if (activeDomain && options?.reuseExisting) { + for (const emitter of options.emitters || []) { + if (emitter instanceof EventEmitter) { + activeDomain.add(emitter); } - - // We're already in a domain, so we don't need to create a new one, just call the callback with the current hub - return callback(getHubFromCarrier(activeDomain)); } + + // We're already in a domain, so we don't need to create a new one, just call the callback with the current hub + return callback(getHubFromCarrier(activeDomain)); } - const local = domain.create(); + const local = domain.create() as domain.Domain & Carrier; for (const emitter of options.emitters || []) { if (emitter instanceof EventEmitter) { @@ -50,9 +49,12 @@ function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsync } } + const parentHub = activeDomain ? getHubFromCarrier(activeDomain) : undefined; + const newHub = createNewHub(parentHub); + setHubOnCarrier(local, newHub); + return local.bind(() => { - const hub = getCurrentHubCore(); - return callback(hub); + return callback(newHub); })(); } diff --git a/packages/node/test/async/domain.test.ts b/packages/node/test/async/domain.test.ts index 9a1f39ee5b23..445718de227d 100644 --- a/packages/node/test/async/domain.test.ts +++ b/packages/node/test/async/domain.test.ts @@ -16,19 +16,29 @@ describe('domains', () => { expect(hub).toEqual(new Hub()); }); - test('domain hub scope inheritance', () => { + test('hub scope inheritance', () => { + setDomainAsyncContextStrategy(); + const globalHub = getCurrentHub(); - globalHub.configureScope(scope => { - scope.setExtra('a', 'b'); - scope.setTag('a', 'b'); - scope.addBreadcrumb({ message: 'a' }); - }); - runWithAsyncContext(hub => { - expect(globalHub).toEqual(hub); + globalHub.setExtra('a', 'b'); + + runWithAsyncContext(hub1 => { + expect(hub1).toEqual(globalHub); + + hub1.setExtra('c', 'd'); + expect(hub1).not.toEqual(globalHub); + + runWithAsyncContext(hub2 => { + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + hub2.setExtra('e', 'f'); + expect(hub2).not.toEqual(hub1); + }); }); }); - test('domain hub single instance', () => { + test('hub single instance', () => { setDomainAsyncContextStrategy(); runWithAsyncContext(hub => { @@ -36,7 +46,7 @@ describe('domains', () => { }); }); - test('domain within a domain not reused', () => { + test('within a domain not reused', () => { setDomainAsyncContextStrategy(); runWithAsyncContext(hub1 => { @@ -46,7 +56,7 @@ describe('domains', () => { }); }); - test('domain within a domain reused when requested', () => { + test('within a domain reused when requested', () => { setDomainAsyncContextStrategy(); runWithAsyncContext(hub1 => { @@ -59,7 +69,7 @@ describe('domains', () => { }); }); - test('concurrent domain hubs', done => { + test('concurrent hub contexts', done => { setDomainAsyncContextStrategy(); let d1done = false; diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 4f7060b655ff..3cf5127ce848 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -1,16 +1,26 @@ +import type { Hub } from '@sentry/core'; import * as sentryCore from '@sentry/core'; -import { Transaction } from '@sentry/core'; +import { setAsyncContextStrategy, Transaction } from '@sentry/core'; import type { Event } from '@sentry/types'; import { SentryError } from '@sentry/utils'; import * as http from 'http'; -import { setDomainAsyncContextStrategy } from '../src/async/domain'; import { NodeClient } from '../src/client'; import { errorHandler, requestHandler, tracingHandler } from '../src/handlers'; import * as SDK from '../src/sdk'; import { getDefaultNodeClientOptions } from './helper/node-client-options'; -setDomainAsyncContextStrategy(); +function mockAsyncContextStrategy(getHub: () => Hub): void { + function getCurrentHub(): Hub | undefined { + return getHub(); + } + + function runWithAsyncContext(fn: (hub: Hub) => T): T { + return fn(getHub()); + } + + setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext }); +} describe('requestHandler', () => { const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; @@ -52,6 +62,7 @@ describe('requestHandler', () => { const hub = new sentryCore.Hub(client); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + mockAsyncContextStrategy(() => hub); sentryRequestMiddleware(req, res, next); @@ -65,6 +76,7 @@ describe('requestHandler', () => { const hub = new sentryCore.Hub(client); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + mockAsyncContextStrategy(() => hub); sentryRequestMiddleware(req, res, next); @@ -78,6 +90,7 @@ describe('requestHandler', () => { const hub = new sentryCore.Hub(client); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + mockAsyncContextStrategy(() => hub); const captureRequestSession = jest.spyOn(client, '_captureRequestSession'); @@ -97,7 +110,9 @@ describe('requestHandler', () => { const options = getDefaultNodeClientOptions({ autoSessionTracking: false, release: '1.2' }); client = new NodeClient(options); const hub = new sentryCore.Hub(client); + jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + mockAsyncContextStrategy(() => hub); const captureRequestSession = jest.spyOn(client, '_captureRequestSession'); @@ -142,6 +157,7 @@ describe('requestHandler', () => { it('stores request and request data options in `sdkProcessingMetadata`', () => { const hub = new sentryCore.Hub(new NodeClient(getDefaultNodeClientOptions())); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + mockAsyncContextStrategy(() => hub); const requestHandlerOptions = { include: { ip: false } }; const sentryRequestMiddleware = requestHandler(requestHandlerOptions); @@ -177,6 +193,7 @@ describe('tracingHandler', () => { beforeEach(() => { hub = new sentryCore.Hub(new NodeClient(getDefaultNodeClientOptions({ tracesSampleRate: 1.0 }))); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + mockAsyncContextStrategy(() => hub); req = { headers, method, @@ -274,6 +291,8 @@ describe('tracingHandler', () => { const tracesSampler = jest.fn(); const options = getDefaultNodeClientOptions({ tracesSampler }); const hub = new sentryCore.Hub(new NodeClient(options)); + mockAsyncContextStrategy(() => hub); + hub.run(() => { sentryTracingMiddleware(req, res, next); @@ -296,6 +315,7 @@ describe('tracingHandler', () => { const hub = new sentryCore.Hub(new NodeClient(options)); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); + mockAsyncContextStrategy(() => hub); sentryTracingMiddleware(req, res, next); @@ -502,14 +522,17 @@ describe('errorHandler()', () => { client.initSessionFlusher(); const scope = new sentryCore.Scope(); const hub = new sentryCore.Hub(client, scope); + mockAsyncContextStrategy(() => hub); jest.spyOn(client, '_captureRequestSession'); hub.run(() => { scope?.setRequestSession({ status: 'ok' }); - sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, next); - const requestSession = scope?.getRequestSession(); - expect(requestSession).toEqual({ status: 'crashed' }); + sentryErrorMiddleware({ name: 'error', message: 'this is an error' }, req, res, () => { + const scope = sentryCore.getCurrentHub().getScope(); + const requestSession = scope?.getRequestSession(); + expect(requestSession).toEqual({ status: 'crashed' }); + }); }); }); @@ -535,6 +558,7 @@ describe('errorHandler()', () => { client = new NodeClient(options); const hub = new sentryCore.Hub(client); + mockAsyncContextStrategy(() => hub); sentryCore.makeMain(hub); // `sentryErrorMiddleware` uses `withScope`, and we need access to the temporary scope it creates, so monkeypatch From 53ae9aefbc6c70120da5b2e5157d357d3e411555 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 11 Apr 2023 10:23:26 +0200 Subject: [PATCH 25/48] feat(node): Add `AsyncLocalStorage` implementation of `AsyncContextStrategy` (#7800) --- packages/node/src/async/hooks.ts | 48 +++++++++++ packages/node/test/async/hooks.test.ts | 108 +++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 packages/node/src/async/hooks.ts create mode 100644 packages/node/test/async/hooks.test.ts diff --git a/packages/node/src/async/hooks.ts b/packages/node/src/async/hooks.ts new file mode 100644 index 000000000000..840236d5b2c1 --- /dev/null +++ b/packages/node/src/async/hooks.ts @@ -0,0 +1,48 @@ +import type { Carrier, Hub, RunWithAsyncContextOptions } from '@sentry/core'; +import { ensureHubOnCarrier, getHubFromCarrier, setAsyncContextStrategy } from '@sentry/core'; +import * as async_hooks from 'async_hooks'; + +interface AsyncLocalStorage { + getStore(): T | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; +} + +type AsyncLocalStorageConstructor = { new (): AsyncLocalStorage }; +// AsyncLocalStorage only exists in async_hook after Node v12.17.0 or v13.10.0 +type NewerAsyncHooks = typeof async_hooks & { AsyncLocalStorage: AsyncLocalStorageConstructor }; + +/** + * Sets the async context strategy to use AsyncLocalStorage which requires Node v12.17.0 or v13.10.0. + */ +export function setHooksAsyncContextStrategy(): void { + const asyncStorage = new (async_hooks as NewerAsyncHooks).AsyncLocalStorage(); + + function getCurrentHub(): Hub | undefined { + return asyncStorage.getStore(); + } + + function createNewHub(parent: Hub | undefined): Hub { + const carrier: Carrier = {}; + ensureHubOnCarrier(carrier, parent); + return getHubFromCarrier(carrier); + } + + function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions): T { + const existingHub = getCurrentHub(); + + if (existingHub && options?.reuseExisting) { + // We're already in an async context, so we don't need to create a new one + // just call the callback with the current hub + return callback(existingHub); + } + + const newHub = createNewHub(existingHub); + + return asyncStorage.run(newHub, () => { + return callback(newHub); + }); + } + + setAsyncContextStrategy({ getCurrentHub, runWithAsyncContext }); +} diff --git a/packages/node/test/async/hooks.test.ts b/packages/node/test/async/hooks.test.ts new file mode 100644 index 000000000000..dbd1904c34dc --- /dev/null +++ b/packages/node/test/async/hooks.test.ts @@ -0,0 +1,108 @@ +import { getCurrentHub, Hub, runWithAsyncContext, setAsyncContextStrategy } from '@sentry/core'; + +import { setHooksAsyncContextStrategy } from '../../src/async/hooks'; +import { conditionalTest } from '../utils'; + +conditionalTest({ min: 12 })('async_hooks', () => { + afterAll(() => { + // clear the strategy + setAsyncContextStrategy(undefined); + }); + + test('without context', () => { + const hub = getCurrentHub(); + expect(hub).toEqual(new Hub()); + }); + + test('without strategy hubs should be equal', () => { + runWithAsyncContext(hub1 => { + runWithAsyncContext(hub2 => { + expect(hub1).toBe(hub2); + }); + }); + }); + + test('hub scope inheritance', () => { + setHooksAsyncContextStrategy(); + + const globalHub = getCurrentHub(); + globalHub.setExtra('a', 'b'); + + runWithAsyncContext(hub1 => { + expect(hub1).toEqual(globalHub); + + hub1.setExtra('c', 'd'); + expect(hub1).not.toEqual(globalHub); + + runWithAsyncContext(hub2 => { + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + hub2.setExtra('e', 'f'); + expect(hub2).not.toEqual(hub1); + }); + }); + }); + + test('context single instance', () => { + setHooksAsyncContextStrategy(); + + runWithAsyncContext(hub => { + expect(hub).toBe(getCurrentHub()); + }); + }); + + test('context within a context not reused', () => { + setHooksAsyncContextStrategy(); + + runWithAsyncContext(hub1 => { + runWithAsyncContext(hub2 => { + expect(hub1).not.toBe(hub2); + }); + }); + }); + + test('context within a context reused when requested', () => { + setHooksAsyncContextStrategy(); + + runWithAsyncContext(hub1 => { + runWithAsyncContext( + hub2 => { + expect(hub1).toBe(hub2); + }, + { reuseExisting: true }, + ); + }); + }); + + test('concurrent hub contexts', done => { + setHooksAsyncContextStrategy(); + + let d1done = false; + let d2done = false; + + runWithAsyncContext(hub => { + hub.getStack().push({ client: 'process' } as any); + expect(hub.getStack()[1]).toEqual({ client: 'process' }); + // Just in case so we don't have to worry which one finishes first + // (although it always should be d2) + setTimeout(() => { + d1done = true; + if (d2done) { + done(); + } + }); + }); + + runWithAsyncContext(hub => { + hub.getStack().push({ client: 'local' } as any); + expect(hub.getStack()[1]).toEqual({ client: 'local' }); + setTimeout(() => { + d2done = true; + if (d1done) { + done(); + } + }); + }); + }); +}); From 48566858987c23d12b6c229b7b75826d2e9b6769 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 11 Apr 2023 10:29:26 +0100 Subject: [PATCH 26/48] feat(node): Auto-select best `AsyncContextStrategy` for Node.js version (#7804) Co-authored-by: Abhijeet Prasad --- packages/node/src/async/index.ts | 17 +++++++++++++++++ packages/node/src/sdk.ts | 4 ++-- packages/node/test/index.test.ts | 4 ++-- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 packages/node/src/async/index.ts diff --git a/packages/node/src/async/index.ts b/packages/node/src/async/index.ts new file mode 100644 index 000000000000..c8bb8e2ebb9f --- /dev/null +++ b/packages/node/src/async/index.ts @@ -0,0 +1,17 @@ +import { NODE_VERSION } from '../nodeVersion'; +import { setDomainAsyncContextStrategy } from './domain'; +import { setHooksAsyncContextStrategy } from './hooks'; + +/** + * Sets the correct async context strategy for Node.js + * + * Node.js >= 14 uses AsyncLocalStorage + * Node.js < 14 uses domains + */ +export function setNodeAsyncContextStrategy(): void { + if (NODE_VERSION.major && NODE_VERSION.major >= 14) { + setHooksAsyncContextStrategy(); + } else { + setDomainAsyncContextStrategy(); + } +} diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 6e8ff2b16d1f..2f6ec6787655 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -15,7 +15,7 @@ import { stackParserFromStackParserOptions, } from '@sentry/utils'; -import { setDomainAsyncContextStrategy } from './async/domain'; +import { setNodeAsyncContextStrategy } from './async'; import { NodeClient } from './client'; import { Console, @@ -111,7 +111,7 @@ export const defaultIntegrations = [ export function init(options: NodeOptions = {}): void { const carrier = getMainCarrier(); - setDomainAsyncContextStrategy(); + setNodeAsyncContextStrategy(); const autoloadedIntegrations = carrier.__SENTRY__?.integrations || []; diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 8a4c971d4f3f..e72ab8ecf3ae 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -12,7 +12,7 @@ import { init, NodeClient, } from '../src'; -import { setDomainAsyncContextStrategy } from '../src/async/domain'; +import { setNodeAsyncContextStrategy } from '../src/async'; import { ContextLines, LinkedErrors } from '../src/integrations'; import { defaultStackParser } from '../src/sdk'; import type { NodeClientOptions } from '../src/types'; @@ -288,7 +288,7 @@ describe('SentryNode', () => { }, dsn, }); - setDomainAsyncContextStrategy(); + setNodeAsyncContextStrategy(); const client = new NodeClient(options); runWithAsyncContext(hub => { From 5d58d5299a3d76b50519ac21c9d6d7cda0116dfd Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 11 Apr 2023 11:59:16 +0100 Subject: [PATCH 27/48] test(node): Add some async tests to async strategies (#7808) --- packages/node/test/async/domain.test.ts | 31 +++++++++++++++++++++++++ packages/node/test/async/hooks.test.ts | 31 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/node/test/async/domain.test.ts b/packages/node/test/async/domain.test.ts index 445718de227d..8b608c778fa0 100644 --- a/packages/node/test/async/domain.test.ts +++ b/packages/node/test/async/domain.test.ts @@ -38,6 +38,37 @@ describe('domains', () => { }); }); + test('async hub scope inheritance', async () => { + setDomainAsyncContextStrategy(); + + async function addRandomExtra(hub: Hub, key: string): Promise { + return new Promise(resolve => { + setTimeout(() => { + hub.setExtra(key, Math.random()); + resolve(); + }, 100); + }); + } + + const globalHub = getCurrentHub(); + await addRandomExtra(globalHub, 'a'); + + await runWithAsyncContext(async hub1 => { + expect(hub1).toEqual(globalHub); + + await addRandomExtra(hub1, 'b'); + expect(hub1).not.toEqual(globalHub); + + await runWithAsyncContext(async hub2 => { + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + await addRandomExtra(hub1, 'c'); + expect(hub2).not.toEqual(hub1); + }); + }); + }); + test('hub single instance', () => { setDomainAsyncContextStrategy(); diff --git a/packages/node/test/async/hooks.test.ts b/packages/node/test/async/hooks.test.ts index dbd1904c34dc..da741d7684f5 100644 --- a/packages/node/test/async/hooks.test.ts +++ b/packages/node/test/async/hooks.test.ts @@ -44,6 +44,37 @@ conditionalTest({ min: 12 })('async_hooks', () => { }); }); + test('async hub scope inheritance', async () => { + setHooksAsyncContextStrategy(); + + async function addRandomExtra(hub: Hub, key: string): Promise { + return new Promise(resolve => { + setTimeout(() => { + hub.setExtra(key, Math.random()); + resolve(); + }, 100); + }); + } + + const globalHub = getCurrentHub(); + await addRandomExtra(globalHub, 'a'); + + await runWithAsyncContext(async hub1 => { + expect(hub1).toEqual(globalHub); + + await addRandomExtra(hub1, 'b'); + expect(hub1).not.toEqual(globalHub); + + await runWithAsyncContext(async hub2 => { + expect(hub2).toEqual(hub1); + expect(hub2).not.toEqual(globalHub); + + await addRandomExtra(hub1, 'c'); + expect(hub2).not.toEqual(hub1); + }); + }); + }); + test('context single instance', () => { setHooksAsyncContextStrategy(); From 84d007ea01d2aa2a620456e1c3636328107fc035 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 11 Apr 2023 17:56:37 +0200 Subject: [PATCH 28/48] fix(e2e): Fix various issues with concurrent E2E and Canary tests (#7805) --- .github/workflows/build.yml | 8 ++++++- .github/workflows/canary.yml | 2 +- packages/e2e-tests/.env.example | 3 +++ packages/e2e-tests/README.md | 2 +- packages/e2e-tests/lib/buildApp.ts | 24 ++++++++++++++----- ...stances.ts => constructRecipeInstances.ts} | 19 +++++++++------ packages/e2e-tests/lib/runAllTestApps.ts | 12 +++++++--- packages/e2e-tests/lib/runTestApp.ts | 5 ++-- packages/e2e-tests/lib/testApp.ts | 19 +++++++++++---- packages/e2e-tests/lib/types.ts | 3 ++- packages/e2e-tests/lib/utils.ts | 10 ++++++++ packages/e2e-tests/run.ts | 1 + .../create-next-app/package.json | 8 ++----- .../create-next-app/playwright.config.ts | 12 ++++++++-- .../create-next-app/test-recipe.json | 2 +- .../create-react-app/test-recipe.json | 2 +- .../nextjs-app-dir/package.json | 3 --- .../nextjs-app-dir/playwright.config.ts | 6 ++--- .../nextjs-app-dir/sentry.client.config.ts | 6 ++++- .../nextjs-app-dir/sentry.edge.config.ts | 6 ++++- .../nextjs-app-dir/sentry.server.config.ts | 6 ++++- .../nextjs-app-dir/start-event-proxy.ts | 2 +- .../nextjs-app-dir/test-recipe.json | 2 +- .../node-express-app/playwright.config.ts | 2 +- .../node-express-app/src/app.ts | 2 +- .../node-express-app/test-recipe.json | 2 +- .../playwright.config.ts | 5 +++- .../test-recipe.json | 2 +- .../playwright.config.ts | 5 +++- .../standard-frontend-react/test-recipe.json | 2 +- 30 files changed, 129 insertions(+), 54 deletions(-) rename packages/e2e-tests/lib/{buildRecipeInstances.ts => constructRecipeInstances.ts} (66%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 43edc616f74b..4b9d5ec1f8be 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -749,7 +749,7 @@ jobs: yarn test:integration:ci job_e2e_tests: - name: E2E Tests + name: E2E Tests (Shard ${{ matrix.shard }}) # We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks # Dependabot PRs sadly also don't have access to secrets, so we skip them as well if: @@ -758,6 +758,10 @@ jobs: needs: [job_get_metadata, job_build] runs-on: ubuntu-20.04 timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3] steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v3 @@ -782,6 +786,8 @@ jobs: E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks' E2E_TEST_SENTRY_TEST_PROJECT: 'sentry-javascript-e2e-tests' + E2E_TEST_SHARD: ${{ matrix.shard }} + E2E_TEST_SHARD_AMOUNT: 3 run: | cd packages/e2e-tests yarn test:e2e diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index a0bff1d37b61..d3968a0dfb75 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -20,7 +20,7 @@ jobs: job_canary_test: name: Canary Tests runs-on: ubuntu-20.04 - timeout-minutes: 30 + timeout-minutes: 60 steps: - name: 'Check out current commit' uses: actions/checkout@v3 diff --git a/packages/e2e-tests/.env.example b/packages/e2e-tests/.env.example index 559550968130..2262e0fa6ccc 100644 --- a/packages/e2e-tests/.env.example +++ b/packages/e2e-tests/.env.example @@ -2,3 +2,6 @@ E2E_TEST_AUTH_TOKEN= E2E_TEST_DSN= E2E_TEST_SENTRY_ORG_SLUG= E2E_TEST_SENTRY_TEST_PROJECT= +E2E_TEST_SHARD= # optional +E2E_TEST_SHARD_AMOUNT= # optional +CANARY_E2E_TEST= # optional diff --git a/packages/e2e-tests/README.md b/packages/e2e-tests/README.md index d8227481a25a..2d8db0f5b41f 100644 --- a/packages/e2e-tests/README.md +++ b/packages/e2e-tests/README.md @@ -54,7 +54,7 @@ To get you started with the recipe, you can copy the following into `test-recipe { "$schema": "../../test-recipe-schema.json", "testApplicationName": "My New Test Application", - "buildCommand": "yarn install --network-concurrency 1", + "buildCommand": "yarn install", "tests": [ { "testName": "My new test", diff --git a/packages/e2e-tests/lib/buildApp.ts b/packages/e2e-tests/lib/buildApp.ts index e802d691e96a..15b9e0497cdc 100644 --- a/packages/e2e-tests/lib/buildApp.ts +++ b/packages/e2e-tests/lib/buildApp.ts @@ -1,13 +1,14 @@ /* eslint-disable no-console */ import * as fs from 'fs-extra'; +import * as os from 'os'; import * as path from 'path'; import { DEFAULT_BUILD_TIMEOUT_SECONDS } from './constants'; import type { Env, RecipeInstance } from './types'; -import { spawnAsync } from './utils'; +import { prefixObjectKeys, spawnAsync } from './utils'; -export async function buildApp(appDir: string, recipeInstance: RecipeInstance, env: Env): Promise { +export async function buildApp(appDir: string, recipeInstance: RecipeInstance, envVars: Env): Promise { const { recipe, label, dependencyOverrides } = recipeInstance; const packageJsonPath = path.resolve(appDir, 'package.json'); @@ -28,13 +29,23 @@ export async function buildApp(appDir: string, recipeInstance: RecipeInstance, e if (recipe.buildCommand) { console.log(`Running build command for test application "${label}"`); + fs.mkdirSync(path.join(os.tmpdir(), 'e2e-test-yarn-caches'), { recursive: true }); + const tempYarnCache = fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-test-yarn-caches', 'cache-')); + + const env = { + ...process.env, + ...envVars, + YARN_CACHE_FOLDER: tempYarnCache, // Use a separate yarn cache for each build commmand because multiple yarn commands running at the same time may corrupt the cache + }; + const buildResult = await spawnAsync(recipe.buildCommand, { cwd: appDir, timeout: (recipe.buildTimeoutSeconds ?? DEFAULT_BUILD_TIMEOUT_SECONDS) * 1000, env: { - ...process.env, ...env, - } as unknown as NodeJS.ProcessEnv, + ...prefixObjectKeys(env, 'NEXT_PUBLIC_'), + ...prefixObjectKeys(env, 'REACT_APP_'), + }, }); if (buildResult.error) { @@ -57,9 +68,10 @@ export async function buildApp(appDir: string, recipeInstance: RecipeInstance, e cwd: appDir, timeout: (recipe.buildTimeoutSeconds ?? DEFAULT_BUILD_TIMEOUT_SECONDS) * 1000, env: { - ...process.env, ...env, - } as unknown as NodeJS.ProcessEnv, + ...prefixObjectKeys(env, 'NEXT_PUBLIC_'), + ...prefixObjectKeys(env, 'REACT_APP_'), + }, }, buildResult.stdout, ); diff --git a/packages/e2e-tests/lib/buildRecipeInstances.ts b/packages/e2e-tests/lib/constructRecipeInstances.ts similarity index 66% rename from packages/e2e-tests/lib/buildRecipeInstances.ts rename to packages/e2e-tests/lib/constructRecipeInstances.ts index 2820c2e055ce..86dfd1ef89f2 100644 --- a/packages/e2e-tests/lib/buildRecipeInstances.ts +++ b/packages/e2e-tests/lib/constructRecipeInstances.ts @@ -2,13 +2,11 @@ import * as fs from 'fs'; import type { Recipe, RecipeInput, RecipeInstance } from './types'; -export function buildRecipeInstances(recipePaths: string[]): RecipeInstance[] { +export function constructRecipeInstances(recipePaths: string[]): RecipeInstance[] { const recipes = buildRecipes(recipePaths); - const recipeInstances: RecipeInstance[] = []; + const recipeInstances: Omit[] = []; - const basePort = 3001; - - recipes.forEach((recipe, i) => { + recipes.forEach(recipe => { recipe.versions.forEach(version => { const dependencyOverrides = Object.keys(version.dependencyOverrides).length > 0 ? version.dependencyOverrides : undefined; @@ -20,12 +18,19 @@ export function buildRecipeInstances(recipePaths: string[]): RecipeInstance[] { label: `${recipe.testApplicationName}${dependencyOverridesInformationString}`, recipe, dependencyOverrides, - port: basePort + i, }); }); }); - return recipeInstances; + return recipeInstances + .map((instance, i) => ({ ...instance, portModulo: i, portGap: recipeInstances.length })) + .filter((_, i) => { + if (process.env.E2E_TEST_SHARD && process.env.E2E_TEST_SHARD_AMOUNT) { + return (i + Number(process.env.E2E_TEST_SHARD)) % Number(process.env.E2E_TEST_SHARD_AMOUNT) === 0; + } else { + return true; + } + }); } function buildRecipes(recipePaths: string[]): Recipe[] { diff --git a/packages/e2e-tests/lib/runAllTestApps.ts b/packages/e2e-tests/lib/runAllTestApps.ts index b12a0ccb2bdb..4f2e405d58aa 100644 --- a/packages/e2e-tests/lib/runAllTestApps.ts +++ b/packages/e2e-tests/lib/runAllTestApps.ts @@ -1,5 +1,9 @@ /* eslint-disable no-console */ -import { buildRecipeInstances } from './buildRecipeInstances'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +import { constructRecipeInstances } from './constructRecipeInstances'; import { buildAndTestApp } from './runTestApp'; import type { RecipeInstance, RecipeTestResult } from './types'; @@ -7,9 +11,9 @@ export async function runAllTestApps( recipePaths: string[], envVarsToInject: Record, ): Promise { - const maxParallel = process.env.CI ? 2 : 5; + const maxParallel = process.env.CI ? 1 : 1; // For now we are disabling parallel execution because it was causing problems (runners were too slow and timeouts happened) - const recipeInstances = buildRecipeInstances(recipePaths); + const recipeInstances = constructRecipeInstances(recipePaths); const results = await shardPromises( recipeInstances, @@ -33,6 +37,8 @@ export async function runAllTestApps( const failed = results.filter(result => result.buildFailed || result.testFailed); + fs.rmSync(path.join(os.tmpdir(), 'e2e-test-yarn-caches'), { force: true, recursive: true }); + if (failed.length) { console.log(`${failed.length} test(s) failed.`); process.exit(1); diff --git a/packages/e2e-tests/lib/runTestApp.ts b/packages/e2e-tests/lib/runTestApp.ts index f38740fe5dd0..22dbdb9c5693 100644 --- a/packages/e2e-tests/lib/runTestApp.ts +++ b/packages/e2e-tests/lib/runTestApp.ts @@ -15,7 +15,7 @@ export async function buildAndTestApp( recipeInstance: RecipeInstance, envVarsToInject: Record, ): Promise { - const { recipe, port } = recipeInstance; + const { recipe, portModulo, portGap } = recipeInstance; const recipeDirname = path.dirname(recipe.path); const targetDir = path.join(TMP_DIR, `${recipe.testApplicationName}-${tmpDirCount++}`); @@ -24,7 +24,8 @@ export async function buildAndTestApp( const env: Env = { ...envVarsToInject, - PORT: port.toString(), + PORT_MODULO: portModulo.toString(), + PORT_GAP: portGap.toString(), }; try { diff --git a/packages/e2e-tests/lib/testApp.ts b/packages/e2e-tests/lib/testApp.ts index e25418662c38..02743c26f633 100644 --- a/packages/e2e-tests/lib/testApp.ts +++ b/packages/e2e-tests/lib/testApp.ts @@ -2,7 +2,7 @@ import { DEFAULT_TEST_TIMEOUT_SECONDS } from './constants'; import type { Env, RecipeInstance, TestDef, TestResult } from './types'; -import { spawnAsync } from './utils'; +import { prefixObjectKeys, spawnAsync } from './utils'; export async function testApp(appDir: string, recipeInstance: RecipeInstance, env: Env): Promise { const { recipe } = recipeInstance; @@ -15,17 +15,28 @@ export async function testApp(appDir: string, recipeInstance: RecipeInstance, en return results; } -async function runTest(appDir: string, recipeInstance: RecipeInstance, test: TestDef, env: Env): Promise { +async function runTest( + appDir: string, + recipeInstance: RecipeInstance, + test: TestDef, + envVars: Env, +): Promise { const { recipe, label } = recipeInstance; console.log(`Running test command for test application "${label}", test "${test.testName}"`); + const env = { + ...process.env, + ...envVars, + }; + const testResult = await spawnAsync(test.testCommand, { cwd: appDir, timeout: (recipe.testTimeoutSeconds ?? DEFAULT_TEST_TIMEOUT_SECONDS) * 1000, env: { - ...process.env, ...env, - } as unknown as NodeJS.ProcessEnv, + ...prefixObjectKeys(env, 'NEXT_PUBLIC_'), + ...prefixObjectKeys(env, 'REACT_APP_'), + }, }); if (testResult.error) { diff --git a/packages/e2e-tests/lib/types.ts b/packages/e2e-tests/lib/types.ts index c78bc45b6bf5..a86c748ef6d0 100644 --- a/packages/e2e-tests/lib/types.ts +++ b/packages/e2e-tests/lib/types.ts @@ -37,7 +37,8 @@ export interface RecipeInstance { label: string; recipe: Recipe; dependencyOverrides?: DependencyOverrides; - port: number; + portModulo: number; + portGap: number; } export interface RecipeTestResult extends RecipeInstance { diff --git a/packages/e2e-tests/lib/utils.ts b/packages/e2e-tests/lib/utils.ts index caf2cf424171..0c786a34f520 100644 --- a/packages/e2e-tests/lib/utils.ts +++ b/packages/e2e-tests/lib/utils.ts @@ -67,3 +67,13 @@ export function spawnAsync( } }); } + +export function prefixObjectKeys( + obj: Record, + prefix: string, +): Record { + return Object.keys(obj).reduce>((result, key) => { + result[prefix + key] = obj[key]; + return result; + }, {}); +} diff --git a/packages/e2e-tests/run.ts b/packages/e2e-tests/run.ts index a4fec2c480dc..29ab6797edcc 100644 --- a/packages/e2e-tests/run.ts +++ b/packages/e2e-tests/run.ts @@ -18,6 +18,7 @@ async function run(): Promise { const envVarsToInject = { REACT_APP_E2E_TEST_DSN: process.env.E2E_TEST_DSN, NEXT_PUBLIC_E2E_TEST_DSN: process.env.E2E_TEST_DSN, + BASE_PORT: '27496', // just some random port }; try { diff --git a/packages/e2e-tests/test-applications/create-next-app/package.json b/packages/e2e-tests/test-applications/create-next-app/package.json index dee9275c2bdc..af2a7830f3d8 100644 --- a/packages/e2e-tests/test-applications/create-next-app/package.json +++ b/packages/e2e-tests/test-applications/create-next-app/package.json @@ -3,13 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", "build": "next build", - "start": "next start", - "lint": "next lint", - "test": "test:prod && test:dev", - "test:prod": "TEST_MODE=prod playwright test", - "test:dev": "TEST_MODE=dev playwright test" + "test:prod": "TEST_ENV=prod playwright test", + "test:dev": "TEST_ENV=dev playwright test" }, "dependencies": { "@next/font": "13.0.7", diff --git a/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts b/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts index b2c8ace9d92d..3d47581acf13 100644 --- a/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts +++ b/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts @@ -1,6 +1,14 @@ 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 = Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO); + /** * See https://playwright.dev/docs/test-configuration. */ @@ -59,8 +67,8 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: { - command: process.env.TEST_MODE === 'prod' ? 'yarn start' : 'yarn dev', - port: process.env.PORT ? parseInt(process.env.PORT) : 3000, + command: testEnv === 'development' ? `yarn next dev -p ${port}` : `yarn next start -p ${port}`, + port, }, }; diff --git a/packages/e2e-tests/test-applications/create-next-app/test-recipe.json b/packages/e2e-tests/test-applications/create-next-app/test-recipe.json index 141f9e1489c1..38b4bbc06af0 100644 --- a/packages/e2e-tests/test-applications/create-next-app/test-recipe.json +++ b/packages/e2e-tests/test-applications/create-next-app/test-recipe.json @@ -1,7 +1,7 @@ { "$schema": "../../test-recipe-schema.json", "testApplicationName": "create-next-app", - "buildCommand": "yarn install --network-concurrency 1 && npx playwright install && yarn build", + "buildCommand": "yarn install && npx playwright install && yarn build", "tests": [ { "testName": "Playwright tests - Prod Mode", diff --git a/packages/e2e-tests/test-applications/create-react-app/test-recipe.json b/packages/e2e-tests/test-applications/create-react-app/test-recipe.json index 3f3c496c4857..ee9c8e1dc40c 100644 --- a/packages/e2e-tests/test-applications/create-react-app/test-recipe.json +++ b/packages/e2e-tests/test-applications/create-react-app/test-recipe.json @@ -1,6 +1,6 @@ { "$schema": "../../test-recipe-schema.json", "testApplicationName": "create-react-app", - "buildCommand": "yarn install --network-concurrency 1 && yarn build", + "buildCommand": "yarn install && yarn build", "tests": [] } diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 064e5d396d1f..4e0c1831440f 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -3,10 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", "build": "next build", - "start": "next start", - "lint": "next lint", "test:prod": "TEST_ENV=production playwright test", "test:dev": "TEST_ENV=development playwright test" }, diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts index 2bacdad88e2e..a6886d2d8308 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts @@ -7,7 +7,7 @@ if (!testEnv) { throw new Error('No test env defined'); } -const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; +const port = Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO); /** * See https://playwright.dev/docs/test-configuration. @@ -55,12 +55,12 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: [ { - command: testEnv === 'development' ? 'yarn dev' : 'yarn start', + command: testEnv === 'development' ? `yarn next dev -p ${port}` : `yarn next start -p ${port}`, port, }, { command: 'yarn ts-node-script start-event-proxy.ts', - port: 27496, + port: Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO) + Number(process.env.PORT_GAP), }, ], }; diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts index af39dd76f384..b216b06f752c 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts @@ -2,6 +2,10 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, - tunnel: 'http://localhost:27496/', // proxy server + tunnel: `http://localhost:${ + Number(process.env.NEXT_PUBLIC_BASE_PORT) + + Number(process.env.NEXT_PUBLIC_PORT_MODULO) + + Number(process.env.NEXT_PUBLIC_PORT_GAP) + }/`, // proxy server tracesSampleRate: 1.0, }); diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.edge.config.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.edge.config.ts index af39dd76f384..b216b06f752c 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.edge.config.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.edge.config.ts @@ -2,6 +2,10 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, - tunnel: 'http://localhost:27496/', // proxy server + tunnel: `http://localhost:${ + Number(process.env.NEXT_PUBLIC_BASE_PORT) + + Number(process.env.NEXT_PUBLIC_PORT_MODULO) + + Number(process.env.NEXT_PUBLIC_PORT_GAP) + }/`, // proxy server tracesSampleRate: 1.0, }); diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.server.config.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.server.config.ts index af39dd76f384..b216b06f752c 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.server.config.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/sentry.server.config.ts @@ -2,6 +2,10 @@ import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, - tunnel: 'http://localhost:27496/', // proxy server + tunnel: `http://localhost:${ + Number(process.env.NEXT_PUBLIC_BASE_PORT) + + Number(process.env.NEXT_PUBLIC_PORT_MODULO) + + Number(process.env.NEXT_PUBLIC_PORT_GAP) + }/`, // proxy server tracesSampleRate: 1.0, }); diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts index c4b99606e9bb..705af6e98a7a 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.ts @@ -1,6 +1,6 @@ import { startEventProxyServer } from '../../test-utils/event-proxy-server'; startEventProxyServer({ - port: 27496, + port: Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO) + Number(process.env.PORT_GAP), proxyServerName: 'nextjs-13-app-dir', }); diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/test-recipe.json b/packages/e2e-tests/test-applications/nextjs-app-dir/test-recipe.json index 4f6290d444d0..b711dd6e922c 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/test-recipe.json +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/test-recipe.json @@ -1,7 +1,7 @@ { "$schema": "../../test-recipe-schema.json", "testApplicationName": "nextjs-13-app-dir", - "buildCommand": "yarn install --network-concurrency 1 && npx playwright install && yarn build", + "buildCommand": "yarn install && npx playwright install && yarn build", "buildAssertionCommand": "yarn ts-node --script-mode assert-build.ts", "tests": [ { diff --git a/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts b/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts index ef7bf3704715..d3690ad2c8fd 100644 --- a/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts +++ b/packages/e2e-tests/test-applications/node-express-app/playwright.config.ts @@ -58,7 +58,7 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: { command: 'yarn start', - port: process.env.PORT ? parseInt(process.env.PORT) : 3000, + port: Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO), }, }; 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 ec4a5c73fcb6..005c83390ec8 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 @@ -17,7 +17,7 @@ Sentry.init({ }); const app = express(); -const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; +const port = Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO); app.use(Sentry.Handlers.requestHandler()); app.use(Sentry.Handlers.tracingHandler()); diff --git a/packages/e2e-tests/test-applications/node-express-app/test-recipe.json b/packages/e2e-tests/test-applications/node-express-app/test-recipe.json index 4859973cd360..039049258171 100644 --- a/packages/e2e-tests/test-applications/node-express-app/test-recipe.json +++ b/packages/e2e-tests/test-applications/node-express-app/test-recipe.json @@ -1,7 +1,7 @@ { "$schema": "../../test-recipe-schema.json", "testApplicationName": "node-express-app", - "buildCommand": "yarn install --network-concurrency 1 && yarn build", + "buildCommand": "yarn install && yarn build", "tests": [ { "testName": "Test express server", diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts index 1a6a49424672..0554e04f5daa 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts +++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/playwright.config.ts @@ -60,7 +60,10 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: { command: 'yarn start', - port: process.env.PORT ? parseInt(process.env.PORT) : 3000, + port: Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO), + env: { + PORT: String(Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO)), + }, }, }; diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/test-recipe.json b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/test-recipe.json index fce2379a280f..864736daaad8 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/test-recipe.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/test-recipe.json @@ -1,7 +1,7 @@ { "$schema": "../../test-recipe-schema.json", "testApplicationName": "standard-frontend-react-tracing-import", - "buildCommand": "yarn install --network-concurrency 1 && npx playwright install && yarn build", + "buildCommand": "yarn install && npx playwright install && yarn build", "tests": [ { "testName": "Playwright tests", diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/playwright.config.ts b/packages/e2e-tests/test-applications/standard-frontend-react/playwright.config.ts index 1a6a49424672..0554e04f5daa 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/playwright.config.ts +++ b/packages/e2e-tests/test-applications/standard-frontend-react/playwright.config.ts @@ -60,7 +60,10 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: { command: 'yarn start', - port: process.env.PORT ? parseInt(process.env.PORT) : 3000, + port: Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO), + env: { + PORT: String(Number(process.env.BASE_PORT) + Number(process.env.PORT_MODULO)), + }, }, }; diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/test-recipe.json b/packages/e2e-tests/test-applications/standard-frontend-react/test-recipe.json index 207dd5409b50..76916d74d280 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/test-recipe.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react/test-recipe.json @@ -1,7 +1,7 @@ { "$schema": "../../test-recipe-schema.json", "testApplicationName": "standard-frontend-react", - "buildCommand": "yarn install --network-concurrency 1 && npx playwright install && yarn build", + "buildCommand": "yarn install && npx playwright install && yarn build", "tests": [ { "testName": "Playwright tests", From 0e20bb55131ffd38e351fc1d29a7c8fededeb7f9 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 12 Apr 2023 10:57:52 +0200 Subject: [PATCH 29/48] feat(sveltekit): Add Sentry Vite Plugin to upload source maps (#7811) Add a customized version of the Sentry Vite plugin to the SvelteKit SDK. Rework the SDK's build API: * Instead of having a super plugin that adds our other plugins (this doesn't work), we now export a factory function that returns an array of plugins. [SvelteKit also creates its plugins](https://github.com/Lms24/kit/blob/f7de9556319f652cabb89dd6f17b21e25326759c/packages/kit/src/exports/vite/index.js#L114-L143) this way. * The currently only plugin in this array is the customized Vite plugin. The customized Vite plugin differs from the Vite plugin as follows: * It only runs on builds (not on the dev server) * It tries to run as late as possible by setting `enforce: 'post'` * It uses the `closeBundle` hook instead of the `writeBundle` hook to upload source maps. * This is because the SvelteKit adapters also run only at closeBundle but luckily before our plugin * It uses the `configure` hook to enable source map generation * It flattens source maps before uploading them * We to flatten them (actually [`sorcery`](https://github.com/Rich-Harris/sorcery) does) to work around [weird source maps generation behaviour](https://github.com/sveltejs/kit/discussions/9608). --- packages/sveltekit/README.md | 10 +- packages/sveltekit/package.json | 4 +- packages/sveltekit/src/vite/index.ts | 2 +- .../src/vite/sentrySvelteKitPlugin.ts | 33 ---- .../sveltekit/src/vite/sentryVitePlugins.ts | 57 +++++++ packages/sveltekit/src/vite/sourceMaps.ts | 151 +++++++++++++++++ .../test/vite/sentrySvelteKitPlugin.test.ts | 12 -- .../test/vite/sentrySvelteKitPlugins.test.ts | 41 +++++ .../sveltekit/test/vite/sourceMaps.test.ts | 76 +++++++++ yarn.lock | 155 +++++++++++++++--- 10 files changed, 470 insertions(+), 71 deletions(-) delete mode 100644 packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts create mode 100644 packages/sveltekit/src/vite/sentryVitePlugins.ts create mode 100644 packages/sveltekit/src/vite/sourceMaps.ts delete mode 100644 packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts create mode 100644 packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts create mode 100644 packages/sveltekit/test/vite/sourceMaps.test.ts diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md index c14d7c124b83..c8abe8be49fd 100644 --- a/packages/sveltekit/README.md +++ b/packages/sveltekit/README.md @@ -150,20 +150,20 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d ### 5. Vite Setup -1. Add our `sentrySvelteKitPlugin` to your `vite.config.(js|ts)` file so that the Sentry SDK can apply build-time features. - Make sure that it is added before the `sveltekit` plugin: +1. Add our `sentrySvelteKit` plugins to your `vite.config.(js|ts)` file so that the Sentry SDK can apply build-time features. + Make sure that it is added before the `sveltekit` plugin: ```javascript import { sveltekit } from '@sveltejs/kit/vite'; - import { sentrySvelteKitPlugin } from '@sentry/sveltekit'; + import { sentrySvelteKit } from '@sentry/sveltekit'; export default { - plugins: [sentrySvelteKitPlugin(), sveltekit()], + plugins: [sentrySvelteKit(), sveltekit()], // ... rest of your Vite config }; ``` - In the near future this plugin will add and configure our [Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin) to automatically upload source maps to Sentry. + This adds the [Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin) to your Vite config to automatically upload source maps to Sentry. ## Known Limitations diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 433fcbe502c8..3edbba488334 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -26,7 +26,9 @@ "@sentry/svelte": "7.47.0", "@sentry/types": "7.47.0", "@sentry/utils": "7.47.0", - "magic-string": "^0.30.0" + "@sentry/vite-plugin": "^0.6.0", + "magic-string": "^0.30.0", + "sorcery": "0.11.0" }, "devDependencies": { "@sveltejs/kit": "^1.11.0", diff --git a/packages/sveltekit/src/vite/index.ts b/packages/sveltekit/src/vite/index.ts index 2e736f9290e8..c9c5beecef28 100644 --- a/packages/sveltekit/src/vite/index.ts +++ b/packages/sveltekit/src/vite/index.ts @@ -1 +1 @@ -export { sentrySvelteKitPlugin } from './sentrySvelteKitPlugin'; +export { sentrySvelteKit } from './sentryVitePlugins'; diff --git a/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts b/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts deleted file mode 100644 index 959e6e94a417..000000000000 --- a/packages/sveltekit/src/vite/sentrySvelteKitPlugin.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Plugin, UserConfig } from 'vite'; - -/** - * Vite Plugin for the Sentry SvelteKit SDK, taking care of: - * - * - Creating Sentry releases and uploading source maps to Sentry - * - Injecting Sentry.init calls if you use dedicated `sentry.(client|server).config.ts` files - * - * This plugin adds a few additional properties to your Vite config. - * Make sure, it is registered before the SvelteKit plugin. - */ -export function sentrySvelteKitPlugin(): Plugin { - return { - name: 'sentry-sveltekit', - enforce: 'pre', // we want this plugin to run early enough - config: originalConfig => { - return addSentryConfig(originalConfig); - }, - }; -} - -function addSentryConfig(originalConfig: UserConfig): UserConfig { - const sentryPlugins: Plugin[] = []; - - // TODO: Add sentry vite plugin here - - const config: UserConfig = { - ...originalConfig, - plugins: [...sentryPlugins, ...(originalConfig.plugins || [])], - }; - - return config; -} diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts new file mode 100644 index 000000000000..ce6c4703ea1b --- /dev/null +++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts @@ -0,0 +1,57 @@ +import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; +import type { Plugin } from 'vite'; + +import { makeCustomSentryVitePlugin } from './sourceMaps'; + +type SourceMapsUploadOptions = { + /** + * If this flag is `true`, the Sentry plugins will automatically upload source maps to Sentry. + * Defaults to `true`. + */ + autoUploadSourceMaps?: boolean; + + /** + * Options for the Sentry Vite plugin to customize and override the release creation and source maps upload process. + * See [Sentry Vite Plugin Options](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin#configuration) for a detailed description. + */ + sourceMapsUploadOptions?: Partial; +}; + +export type SentrySvelteKitPluginOptions = { + /** + * If this flag is `true`, the Sentry plugins will log some useful debug information. + * Defaults to `false`. + */ + debug?: boolean; +} & SourceMapsUploadOptions; + +const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = { + autoUploadSourceMaps: true, + debug: false, +}; + +/** + * Vite Plugins for the Sentry SvelteKit SDK, taking care of creating + * Sentry releases and uploading source maps to Sentry. + * + * Sentry adds a few additional properties to your Vite config. + * Make sure, it is registered before the SvelteKit plugin. + */ +export function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}): Plugin[] { + const mergedOptions = { + ...DEFAULT_PLUGIN_OPTIONS, + ...options, + }; + + const sentryPlugins = []; + + if (mergedOptions.autoUploadSourceMaps) { + const pluginOptions = { + ...mergedOptions.sourceMapsUploadOptions, + debug: mergedOptions.debug, // override the plugin's debug flag with the one from the top-level options + }; + sentryPlugins.push(makeCustomSentryVitePlugin(pluginOptions)); + } + + return sentryPlugins; +} diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts new file mode 100644 index 000000000000..b388e4353681 --- /dev/null +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -0,0 +1,151 @@ +import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; +import { sentryVitePlugin } from '@sentry/vite-plugin'; +import * as fs from 'fs'; +import * as path from 'path'; +// @ts-ignore -sorcery has no types :( +// eslint-disable-next-line import/default +import * as sorcery from 'sorcery'; +import type { Plugin } from 'vite'; + +const DEFAULT_PLUGIN_OPTIONS: SentryVitePluginOptions = { + // TODO: Read these values from the node adapter somehow as the out dir can be changed in the adapter options + include: ['build/server', 'build/client'], +}; + +// sorcery has no types, so these are some basic type definitions: +type Chain = { + write(): Promise; + apply(): Promise; +}; +type Sorcery = { + load(filepath: string): Promise; +}; + +type SentryVitePluginOptionsOptionalInclude = Omit & { + include?: SentryVitePluginOptions['include']; +}; + +/** + * Creates a new Vite plugin that uses the unplugin-based Sentry Vite plugin to create + * releases and upload source maps to Sentry. + * + * Because the unplugin-based Sentry Vite plugin doesn't work ootb with SvelteKit, + * we need to add some additional stuff to make source maps work: + * + * - the `config` hook needs to be added to generate source maps + * - the `configResolved` hook tells us when to upload source maps. + * We only want to upload once at the end, given that SvelteKit makes multiple builds + * - the `closeBundle` hook is used to flatten server source maps, which at the moment is necessary for SvelteKit. + * After the maps are flattened, they're uploaded to Sentry as in the original plugin. + * see: https://github.com/sveltejs/kit/discussions/9608 + * + * @returns the custom Sentry Vite plugin + */ +export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOptionalInclude): Plugin { + const mergedOptions = { + ...DEFAULT_PLUGIN_OPTIONS, + ...options, + }; + const sentryPlugin: Plugin = sentryVitePlugin(mergedOptions); + + const { debug } = mergedOptions; + const { buildStart, resolveId, transform, renderChunk } = sentryPlugin; + + let upload = true; + + const customPlugin: Plugin = { + name: 'sentry-vite-plugin-custom', + apply: 'build', // only apply this plugin at build time + enforce: 'post', + + // These hooks are copied from the original Sentry Vite plugin. + // They're mostly responsible for options parsing and release injection. + buildStart, + resolveId, + renderChunk, + transform, + + // Modify the config to generate source maps + config: config => { + // eslint-disable-next-line no-console + debug && console.log('[Source Maps Plugin] Enabeling source map generation'); + return { + ...config, + build: { + ...config.build, + sourcemap: true, + }, + }; + }, + + configResolved: config => { + // The SvelteKit plugins trigger additional builds within the main (SSR) build. + // We just need a mechanism to upload source maps only once. + // `config.build.ssr` is `true` for that first build and `false` in the other ones. + // Hence we can use it as a switch to upload source maps only once in main build. + if (!config.build.ssr) { + upload = false; + } + }, + + // We need to start uploading source maps later than in the original plugin + // because SvelteKit is still doing some stuff at closeBundle. + closeBundle: () => { + if (!upload) { + return; + } + + // TODO: Read the out dir from the node adapter somehow as it can be changed in the adapter options + const outDir = path.resolve(process.cwd(), 'build'); + + const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js')); + // eslint-disable-next-line no-console + debug && console.log('[Source Maps Plugin] Flattening source maps'); + + jsFiles.forEach(async file => { + try { + await (sorcery as Sorcery).load(file).then(async chain => { + if (!chain) { + // We end up here, if we don't have a source map for the file. + // This is fine, as we're not interested in files w/o source maps. + return; + } + // This flattens the source map + await chain.apply(); + // Write it back to the original file + await chain.write(); + }); + } catch (e) { + // Sometimes sorcery fails to flatten the source map. While this isn't ideal, it seems to be mostly + // happening in Kit-internal files which is fine as they're not in-app. + // This mostly happens when sorcery tries to resolve a source map while flattening that doesn't exist. + const isKnownError = e instanceof Error && e.message.includes('ENOENT: no such file or directory, open'); + if (debug && !isKnownError) { + // eslint-disable-next-line no-console + console.error('[Source Maps Plugin] error while flattening', file, e); + } + } + }); + + // @ts-ignore - this hook exists on the plugin! + sentryPlugin.writeBundle(); + }, + }; + + return customPlugin; +} + +function getFiles(dir: string): string[] { + if (!fs.existsSync(dir)) { + return []; + } + const dirents = fs.readdirSync(dir, { withFileTypes: true }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const files: string[] = dirents.map(dirent => { + const resFileOrDir = path.resolve(dir, dirent.name); + return dirent.isDirectory() ? getFiles(resFileOrDir) : resFileOrDir; + }); + + return Array.prototype.concat(...files); +} diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts deleted file mode 100644 index ebfa67320840..000000000000 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugin.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { sentrySvelteKitPlugin } from './../../src/vite/sentrySvelteKitPlugin'; - -describe('sentrySvelteKitPlugin', () => { - it('returns a Vite plugin with name, enforce, and config hook', () => { - const plugin = sentrySvelteKitPlugin(); - expect(plugin).toHaveProperty('name'); - expect(plugin).toHaveProperty('enforce'); - expect(plugin).toHaveProperty('config'); - expect(plugin.name).toEqual('sentry-sveltekit'); - expect(plugin.enforce).toEqual('pre'); - }); -}); diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts new file mode 100644 index 000000000000..2ddb1de3b5a0 --- /dev/null +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -0,0 +1,41 @@ +import { vi } from 'vitest'; + +import { sentrySvelteKit } from '../../src/vite/sentryVitePlugins'; +import * as sourceMaps from '../../src/vite/sourceMaps'; + +describe('sentryVite()', () => { + it('returns an array of Vite plugins', () => { + const plugins = sentrySvelteKit(); + expect(plugins).toBeInstanceOf(Array); + expect(plugins).toHaveLength(1); + }); + + it('returns the custom sentry source maps plugin by default', () => { + const plugins = sentrySvelteKit(); + const plugin = plugins[0]; + expect(plugin.name).toEqual('sentry-vite-plugin-custom'); + }); + + it("doesn't return the custom sentry source maps plugin if autoUploadSourcemaps is `false`", () => { + const plugins = sentrySvelteKit({ autoUploadSourceMaps: false }); + expect(plugins).toHaveLength(0); + }); + + it('passes user-specified vite pugin options to the custom sentry source maps plugin', () => { + const makePluginSpy = vi.spyOn(sourceMaps, 'makeCustomSentryVitePlugin'); + const plugins = sentrySvelteKit({ + debug: true, + sourceMapsUploadOptions: { + include: ['foo.js'], + ignore: ['bar.js'], + }, + }); + const plugin = plugins[0]; + expect(plugin.name).toEqual('sentry-vite-plugin-custom'); + expect(makePluginSpy).toHaveBeenCalledWith({ + debug: true, + ignore: ['bar.js'], + include: ['foo.js'], + }); + }); +}); diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts new file mode 100644 index 000000000000..9db7bd5fb39d --- /dev/null +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -0,0 +1,76 @@ +import { vi } from 'vitest'; + +import { makeCustomSentryVitePlugin } from '../../src/vite/sourceMaps'; + +const mockedSentryVitePlugin = { + buildStart: vi.fn(), + resolveId: vi.fn(), + renderChunk: vi.fn(), + transform: vi.fn(), + writeBundle: vi.fn(), +}; + +vi.mock('@sentry/vite-plugin', async () => { + const original = (await vi.importActual('@sentry/vite-plugin')) as any; + + return { + ...original, + sentryVitePlugin: () => mockedSentryVitePlugin, + }; +}); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('makeCustomSentryVitePlugin()', () => { + it('returns the custom sentry source maps plugin', () => { + const plugin = makeCustomSentryVitePlugin(); + expect(plugin.name).toEqual('sentry-vite-plugin-custom'); + expect(plugin.apply).toEqual('build'); + expect(plugin.enforce).toEqual('post'); + + expect(plugin.buildStart).toBeInstanceOf(Function); + expect(plugin.resolveId).toBeInstanceOf(Function); + expect(plugin.renderChunk).toBeInstanceOf(Function); + expect(plugin.transform).toBeInstanceOf(Function); + + expect(plugin.config).toBeInstanceOf(Function); + expect(plugin.configResolved).toBeInstanceOf(Function); + expect(plugin.closeBundle).toBeInstanceOf(Function); + }); + + describe('Custom sentry vite plugin', () => { + it('enables source map generation', () => { + const plugin = makeCustomSentryVitePlugin(); + // @ts-ignore this function exists! + const sentrifiedConfig = plugin.config({ build: { foo: {} }, test: {} }); + expect(sentrifiedConfig).toEqual({ + build: { + foo: {}, + sourcemap: true, + }, + test: {}, + }); + }); + + it('uploads source maps during the SSR build', () => { + const plugin = makeCustomSentryVitePlugin(); + // @ts-ignore this function exists! + plugin.configResolved({ build: { ssr: true } }); + // @ts-ignore this function exists! + plugin.closeBundle(); + expect(mockedSentryVitePlugin.writeBundle).toHaveBeenCalledTimes(1); + }); + + it("doesn't upload source maps during the non-SSR builds", () => { + const plugin = makeCustomSentryVitePlugin(); + + // @ts-ignore this function exists! + plugin.configResolved({ build: { ssr: false } }); + // @ts-ignore this function exists! + plugin.closeBundle(); + expect(mockedSentryVitePlugin.writeBundle).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 8eb45ddf8fc4..afcf0c584dbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3030,6 +3030,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -4058,6 +4063,20 @@ fflate "^0.4.4" mitt "^1.1.3" +"@sentry/bundler-plugin-core@0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-0.6.0.tgz#70ad3740b2f90cdca1aff5fdbcd7306566a2f51e" + integrity sha512-gDPBkFxiOkc525U9pxnGMI5B2DAG0+UCsNuiNgl9+AieDcPSYTwdzfGHytxDZrQgPMvIHEnTAp1VlNB+6UxUGQ== + dependencies: + "@sentry/cli" "^2.17.0" + "@sentry/node" "^7.19.0" + "@sentry/tracing" "^7.19.0" + find-up "5.0.0" + glob "9.3.2" + magic-string "0.27.0" + unplugin "1.0.1" + webpack-sources "3.2.3" + "@sentry/cli@2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.2.0.tgz#0cf4d529d87e290dea54d7e58fa5ff87ea200e4e" @@ -4083,6 +4102,24 @@ proxy-from-env "^1.1.0" which "^2.0.2" +"@sentry/cli@^2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.17.0.tgz#fc809ecd721eb5323502625fa904b786af28ad89" + integrity sha512-CHIMEg8+YNCpEBDgUctu+DvG3S8+g8Zn9jTE5MMGINNmGkQTMG179LuDE04B/inaCYixLVNpFPTe6Iow3tXjnQ== + dependencies: + https-proxy-agent "^5.0.0" + node-fetch "^2.6.7" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" + +"@sentry/vite-plugin@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-0.6.0.tgz#3902a5224d52b06d753a1deeb6b722bf6523840c" + integrity sha512-3J1ESvbI5okGJaSWm+gTAOOIa96u4ZwfI/C3n+0HSStz3e4vGiGUh59iNyb1/2m5HFgR5OLaHNfAvlyP8GM/ew== + dependencies: + "@sentry/bundler-plugin-core" "0.6.0" + "@sentry/webpack-plugin@1.19.0": version "1.19.0" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.19.0.tgz#2b134318f1552ba7f3e3f9c83c71a202095f7a44" @@ -8402,7 +8439,7 @@ btoa@^1.2.1: resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g== -buffer-crc32@~0.2.3: +buffer-crc32@^0.2.5, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= @@ -8940,7 +8977,7 @@ chokidar@3.5.1: optionalDependencies: fsevents "~2.3.1" -"chokidar@>=2.0.0 <4.0.0", "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.0.2, chokidar@^3.2.1, chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3.5.1, chokidar@^3.5.2: +"chokidar@>=2.0.0 <4.0.0", "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.0.2, chokidar@^3.2.1, chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3.5.1, chokidar@^3.5.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -12110,6 +12147,11 @@ es6-object-assign@^1.1.0: resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" integrity sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw= +es6-promise@^3.1.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg== + es6-symbol@^3.1.1, es6-symbol@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" @@ -13229,6 +13271,14 @@ find-up@3.0.0, find-up@^3.0.0: dependencies: locate-path "^3.0.0" +find-up@5.0.0, find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -13252,14 +13302,6 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - find-up@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" @@ -14036,6 +14078,16 @@ glob@8.0.3, glob@^8.0.1, glob@^8.0.3: minimatch "^5.0.1" once "^1.3.0" +glob@9.3.2: + version "9.3.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.2.tgz#8528522e003819e63d11c979b30896e0eaf52eda" + integrity sha512-BTv/JhKXFEHsErMte/AnfiSv8yYOLLiyH2lTg8vn02O21zWFgHPTfxtgn1QRe7NRgggUhC8hacR2Re94svHqeA== + dependencies: + fs.realpath "^1.0.0" + minimatch "^7.4.1" + minipass "^4.2.4" + path-scurry "^1.6.1" + glob@^5.0.10, glob@^5.0.15: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -17837,6 +17889,11 @@ lru-cache@^7.10.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== +lru-cache@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.0.1.tgz#ac061ed291f8b9adaca2b085534bb1d3b61bef83" + integrity sha512-C8QsKIN1UIXeOs3iWmiZ1lQY+EnKDojWd37fXy1aSbJvH4iSma1uy2OWuoB3m4SYRli5+CUjDv3Dij5DVoetmg== + lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" @@ -17887,6 +17944,13 @@ magic-string@0.25.7: dependencies: sourcemap-codec "^1.4.4" +magic-string@0.27.0, magic-string@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" + integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.13" + magic-string@^0.25.0, magic-string@^0.25.1, magic-string@^0.25.7: version "0.25.9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" @@ -17901,13 +17965,6 @@ magic-string@^0.26.2: dependencies: sourcemap-codec "^1.4.8" -magic-string@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" - integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - magic-string@^0.29.0: version "0.29.0" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.29.0.tgz#f034f79f8c43dba4ae1730ffb5e8c4e084b16cf3" @@ -18406,6 +18463,13 @@ minimatch@^5.0.1, minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" +minimatch@^7.4.1: + version "7.4.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb" + integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw== + dependencies: + brace-expansion "^2.0.1" + minimatch@~3.0.4: version "3.0.8" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" @@ -18515,6 +18579,16 @@ minipass@^4.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.3.tgz#00bfbaf1e16e35e804f4aa31a7c1f6b8d9f0ee72" integrity sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw== +minipass@^4.2.4: + version "4.2.7" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.7.tgz#14c6fc0dcab54d9c4dd64b2b7032fef04efec218" + integrity sha512-ScVIgqHcXRMyfflqHmEW0bm8z8rb5McHyOY3ewX9JBgZaR77G7nxq9L/mtV96/QbAAwtbCAHVVLzD1kkyfFQEw== + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + minizlib@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" @@ -20649,6 +20723,14 @@ path-root@^0.1.1: dependencies: path-root-regex "^0.1.0" +path-scurry@^1.6.1: + version "1.6.4" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.4.tgz#020a9449e5382a4acb684f9c7e1283bc5695de66" + integrity sha512-Qp/9IHkdNiXJ3/Kon++At2nVpnhRiPq/aSvQN+H3U1WZbvNRK0RIQK/o4HMqPoXjpuGJUEWpHSs6Mnjxqh3TQg== + dependencies: + lru-cache "^9.0.0" + minipass "^5.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -23428,7 +23510,7 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.1, rimraf@^3.0.2: dependencies: glob "^7.1.3" -rimraf@^2.2.8, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: +rimraf@^2.2.8, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.5.2, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -23657,6 +23739,16 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sander@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad" + integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA== + dependencies: + es6-promise "^3.1.2" + graceful-fs "^4.1.3" + mkdirp "^0.5.1" + rimraf "^2.5.2" + sane@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" @@ -24331,6 +24423,16 @@ socks@~2.3.2: ip "1.1.5" smart-buffer "^4.1.0" +sorcery@0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.11.0.tgz#310c80ee993433854bb55bb9aa4003acd147fca8" + integrity sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.14" + buffer-crc32 "^0.2.5" + minimist "^1.2.0" + sander "^0.5.0" + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" @@ -26498,6 +26600,16 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= +unplugin@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.0.1.tgz#83b528b981cdcea1cad422a12cd02e695195ef3f" + integrity sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA== + dependencies: + acorn "^8.8.1" + chokidar "^3.5.3" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.5.0" + unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" @@ -27283,7 +27395,7 @@ webpack-sources@1.4.3, webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-s source-list-map "^2.0.0" source-map "~0.6.1" -"webpack-sources@^2.0.0 || ^3.0.0", webpack-sources@^3.2.0, webpack-sources@^3.2.3: +webpack-sources@3.2.3, "webpack-sources@^2.0.0 || ^3.0.0", webpack-sources@^3.2.0, webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== @@ -27302,6 +27414,11 @@ webpack-subresource-integrity@1.5.2: dependencies: webpack-sources "^1.3.0" +webpack-virtual-modules@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" + integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== + webpack@4.44.1: version "4.44.1" resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.1.tgz#17e69fff9f321b8f117d1fda714edfc0b939cc21" From a5c0430c8b939c18fb030413755fb202b88456ee Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 12 Apr 2023 11:10:20 +0200 Subject: [PATCH 30/48] feat(core): Make `runWithAsyncContext` public API (#7817) --- packages/core/src/hub.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index bc3e1ef4fd38..9d731b90296d 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -589,9 +589,11 @@ export function setAsyncContextStrategy(strategy: AsyncContextStrategy | undefin } /** - * @private Private API with no semver guarantees! + * Runs the supplied callback in its own async context. Async Context strategies are defined per SDK. * - * Runs the supplied callback in its own async context. + * @param callback The callback to run in its own async context + * @param options Options to pass to the async context strategy + * @returns The result of the callback */ export function runWithAsyncContext(callback: (hub: Hub) => T, options: RunWithAsyncContextOptions = {}): T { const registry = getMainCarrier(); From 3efb55656dab655aaf32e3ecc9c399e7db1e3652 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 12 Apr 2023 12:26:04 +0200 Subject: [PATCH 31/48] feat(browser): Export request instrumentation options (#7818) --- packages/browser/src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 75137f210816..59c037ef1b7c 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -21,7 +21,12 @@ const INTEGRATIONS = { export { INTEGRATIONS as Integrations }; export { Replay } from '@sentry/replay'; -export { BrowserTracing, defaultRequestInstrumentationOptions } from '@sentry-internal/tracing'; +export { + BrowserTracing, + defaultRequestInstrumentationOptions, + instrumentOutgoingRequests, +} from '@sentry-internal/tracing'; +export type { RequestInstrumentationOptions } from '@sentry-internal/tracing'; export { addTracingExtensions, extractTraceparentData, From 943df9240048625b098e15d748de2bf51d6afaeb Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 12 Apr 2023 14:59:00 +0200 Subject: [PATCH 32/48] feat(core): Add DSC to all outgoing envelopes (#7820) --- packages/core/src/scope.ts | 13 ++++++++++--- packages/core/test/lib/envelope.test.ts | 17 +++++++++-------- packages/hub/test/scope.test.ts | 3 ++- packages/utils/src/envelope.ts | 8 +++----- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 7143275baa8b..40a1fa135417 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -482,9 +482,16 @@ export class Scope implements ScopeInterface { // errors with transaction and it relies on that. if (this._span) { event.contexts = { trace: this._span.getTraceContext(), ...event.contexts }; - const transactionName = this._span.transaction && this._span.transaction.name; - if (transactionName) { - event.tags = { transaction: transactionName, ...event.tags }; + const transaction = this._span.transaction; + if (transaction) { + event.sdkProcessingMetadata = { + dynamicSamplingContext: transaction.getDynamicSamplingContext(), + ...event.sdkProcessingMetadata, + }; + const transactionName = transaction.name; + if (transactionName) { + event.tags = { transaction: transactionName, ...event.tags }; + } } } diff --git a/packages/core/test/lib/envelope.test.ts b/packages/core/test/lib/envelope.test.ts index 9d24e1eef19e..4c0c45585d60 100644 --- a/packages/core/test/lib/envelope.test.ts +++ b/packages/core/test/lib/envelope.test.ts @@ -6,14 +6,6 @@ const testDsn: DsnComponents = { protocol: 'https', projectId: 'abc', host: 'tes describe('createEventEnvelope', () => { describe('trace header', () => { - it("doesn't add trace header if event is not a transaction", () => { - const event: Event = {}; - const envelopeHeaders = createEventEnvelope(event, testDsn)[0]; - - expect(envelopeHeaders).toBeDefined(); - expect(envelopeHeaders.trace).toBeUndefined(); - }); - const testTable: Array<[string, Event, DynamicSamplingContext]> = [ [ 'adds minimal baggage items', @@ -66,6 +58,15 @@ describe('createEventEnvelope', () => { trace_id: '1234', }, ], + [ + 'with error event', + { + sdkProcessingMetadata: { + dynamicSamplingContext: { trace_id: '1234', public_key: 'pubKey123' }, + }, + }, + { trace_id: '1234', public_key: 'pubKey123' }, + ], ]; it.each(testTable)('%s', (_: string, event, trace) => { const envelopeHeaders = createEventEnvelope(event, testDsn)[0]; diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index d2686afb6477..6571cd3122b4 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -354,6 +354,7 @@ describe('Scope', () => { fake: 'span', getTraceContext: () => ({ a: 'b' }), name: 'fake transaction', + getDynamicSamplingContext: () => ({}), } as any; transaction.transaction = transaction; // because this is a transaction, its `transaction` pointer points to itself scope.setSpan(transaction); @@ -366,7 +367,7 @@ describe('Scope', () => { test('adds `transaction` tag when span on scope', async () => { expect.assertions(1); const scope = new Scope(); - const transaction = { name: 'fake transaction' }; + const transaction = { name: 'fake transaction', getDynamicSamplingContext: () => ({}) }; const span = { fake: 'span', getTraceContext: () => ({ a: 'b' }), diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index 580a50d019e0..66adb7e78ebd 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -237,15 +237,13 @@ export function createEventEnvelopeHeaders( dsn: DsnComponents, ): EventEnvelopeHeaders { const dynamicSamplingContext = event.sdkProcessingMetadata && event.sdkProcessingMetadata.dynamicSamplingContext; - return { event_id: event.event_id as string, sent_at: new Date().toISOString(), ...(sdkInfo && { sdk: sdkInfo }), ...(!!tunnel && { dsn: dsnToString(dsn) }), - ...(event.type === 'transaction' && - dynamicSamplingContext && { - trace: dropUndefinedKeys({ ...dynamicSamplingContext }), - }), + ...(dynamicSamplingContext && { + trace: dropUndefinedKeys({ ...dynamicSamplingContext }), + }), }; } From 4ff406618ad37f6caf71a3181d5a695a2c070352 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 12 Apr 2023 15:05:58 +0200 Subject: [PATCH 33/48] fix(core): Only call `applyDebugMetadata` for error events (#7824) --- packages/core/src/utils/prepareEvent.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index f51b4afe4212..92a446ea1d85 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -37,7 +37,11 @@ export function prepareEvent( applyClientOptions(prepared, options); applyIntegrationsMetadata(prepared, integrations); - applyDebugMetadata(prepared, options.stackParser); + + // Only apply debug metadata to error events. + if (event.type === undefined) { + applyDebugMetadata(prepared, options.stackParser); + } // If we have scope given to us, use it as the base for further modifications. // This allows us to prevent unnecessary copying of data if `captureContext` is not provided. From c1055f5eb622c4800bba0bb5d5291a442be8f64e Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 12 Apr 2023 15:24:19 +0200 Subject: [PATCH 34/48] chore(node): Rename webpack-domain test (#7823) --- packages/node/package.json | 2 +- .../manual/{webpack-domain => webpack-async-context}/index.js | 0 .../{webpack-domain => webpack-async-context}/npm-build.js | 0 .../{webpack-domain => webpack-async-context}/package.json | 2 +- .../manual/{webpack-domain => webpack-async-context}/yarn.lock | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename packages/node/test/manual/{webpack-domain => webpack-async-context}/index.js (100%) rename packages/node/test/manual/{webpack-domain => webpack-async-context}/npm-build.js (100%) rename packages/node/test/manual/{webpack-domain => webpack-async-context}/package.json (83%) rename packages/node/test/manual/{webpack-domain => webpack-async-context}/yarn.lock (100%) diff --git a/packages/node/package.json b/packages/node/package.json index 483bf278610b..f83be371a2bb 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -56,7 +56,7 @@ "test:express": "node test/manual/express-scope-separation/start.js", "test:jest": "jest", "test:release-health": "node test/manual/release-health/runner.js", - "test:webpack": "cd test/manual/webpack-domain/ && yarn --silent && node npm-build.js", + "test:webpack": "cd test/manual/webpack-async-context/ && yarn --silent && node npm-build.js", "test:watch": "jest --watch", "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push" }, diff --git a/packages/node/test/manual/webpack-domain/index.js b/packages/node/test/manual/webpack-async-context/index.js similarity index 100% rename from packages/node/test/manual/webpack-domain/index.js rename to packages/node/test/manual/webpack-async-context/index.js diff --git a/packages/node/test/manual/webpack-domain/npm-build.js b/packages/node/test/manual/webpack-async-context/npm-build.js similarity index 100% rename from packages/node/test/manual/webpack-domain/npm-build.js rename to packages/node/test/manual/webpack-async-context/npm-build.js diff --git a/packages/node/test/manual/webpack-domain/package.json b/packages/node/test/manual/webpack-async-context/package.json similarity index 83% rename from packages/node/test/manual/webpack-domain/package.json rename to packages/node/test/manual/webpack-async-context/package.json index 462e8d8920da..ff8f85afdafa 100644 --- a/packages/node/test/manual/webpack-domain/package.json +++ b/packages/node/test/manual/webpack-async-context/package.json @@ -1,5 +1,5 @@ { - "name": "webpack-domain", + "name": "webpack-async-context", "version": "1.0.0", "main": "index.js", "license": "MIT", diff --git a/packages/node/test/manual/webpack-domain/yarn.lock b/packages/node/test/manual/webpack-async-context/yarn.lock similarity index 100% rename from packages/node/test/manual/webpack-domain/yarn.lock rename to packages/node/test/manual/webpack-async-context/yarn.lock From 14849db0dc7e85381de9b1d07f9dc3fe36740ed4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 12 Apr 2023 14:45:37 +0100 Subject: [PATCH 35/48] fix(browser): DOMException SecurityError stacktrace parsing bug (#7821) Co-authored-by: Francesco Novy --- .../browser/test/unit/tracekit/misc.test.ts | 103 ++++++++++++++++++ packages/utils/src/stacktrace.ts | 6 + 2 files changed, 109 insertions(+) diff --git a/packages/browser/test/unit/tracekit/misc.test.ts b/packages/browser/test/unit/tracekit/misc.test.ts index e9db457ea196..b092c8d10723 100644 --- a/packages/browser/test/unit/tracekit/misc.test.ts +++ b/packages/browser/test/unit/tracekit/misc.test.ts @@ -26,4 +26,107 @@ describe('Tracekit - Misc Tests', () => { }, }); }); + + it('should parse SecurityError', () => { + const SECURITY_ERROR = { + name: 'SecurityError', + message: 'Blocked a frame with origin "https://SENTRY_URL.sentry.io" from accessing a cross-origin frame.', + stack: + 'SecurityError: Blocked a frame with origin "https://SENTRY_URL.sentry.io" from accessing a cross-origin frame.\n' + + ' at Error: Blocked a frame with origin "(https://SENTRY_URL.sentry.io" from accessing a cross-origin frame.)\n' + + ' at castFn(../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js:368:76)\n' + + ' at castFn(../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js:409:17)\n' + + ' at Replayer.applyEventsSynchronously(../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js:325:13)\n' + + ' at .actions.play(../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/machine.js:132:17)\n' + + ' at (../node_modules/@sentry-internal/rrweb/es/rrweb/ext/@xstate/fsm/es/index.js:15:2595)\n' + + ' at Array.forEach()\n' + + ' at l(../node_modules/@sentry-internal/rrweb/es/rrweb/ext/@xstate/fsm/es/index.js:15:2551)\n' + + ' at c.send(../node_modules/@sentry-internal/rrweb/es/rrweb/ext/@xstate/fsm/es/index.js:15:2741)\n' + + ' at Replayer.play(../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js:220:26)\n' + + ' at Replayer.pause(../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js:235:18)\n' + + ' at playTimer.current(./app/components/replays/replayContext.tsx:397:62)\n' + + ' at sentryWrapped(../node_modules/@sentry/browser/esm/helpers.js:90:17)', + }; + const ex = exceptionFromError(parser, SECURITY_ERROR); + + expect(ex).toEqual({ + type: 'SecurityError', + value: 'Blocked a frame with origin "https://SENTRY_URL.sentry.io" from accessing a cross-origin frame.', + stacktrace: { + frames: [ + { + filename: './app/components/replays/replayContext.tsx', + function: 'playTimer.current', + in_app: true, + lineno: 397, + colno: 62, + }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js', + function: 'Replayer.pause', + in_app: true, + lineno: 235, + colno: 18, + }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js', + function: 'Replayer.play', + in_app: true, + lineno: 220, + colno: 26, + }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/ext/@xstate/fsm/es/index.js', + function: 'c.send', + in_app: true, + lineno: 15, + colno: 2741, + }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/ext/@xstate/fsm/es/index.js', + function: 'l', + in_app: true, + lineno: 15, + colno: 2551, + }, + { filename: '', function: 'Array.forEach', in_app: true }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/ext/@xstate/fsm/es/index.js', + function: '', + in_app: true, + lineno: 15, + colno: 2595, + }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/machine.js', + function: '.actions.play', + in_app: true, + lineno: 132, + colno: 17, + }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js', + function: 'Replayer.applyEventsSynchronously', + in_app: true, + lineno: 325, + colno: 13, + }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js', + function: 'castFn', + in_app: true, + lineno: 409, + colno: 17, + }, + { + filename: '../node_modules/@sentry-internal/rrweb/es/rrweb/packages/rrweb/src/replay/index.js', + function: 'castFn', + in_app: true, + lineno: 368, + colno: 76, + }, + ], + }, + }); + }); }); diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index 2cce7a7879ae..f137b73264ba 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -35,6 +35,12 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser { // Remove webpack (error: *) wrappers const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line; + // https://github.com/getsentry/sentry-javascript/issues/7813 + // Skip Error: lines + if (cleanedLine.match(/\S*Error: /)) { + continue; + } + for (const parser of sortedParsers) { const frame = parser(cleanedLine); From dced9b26b2f88dac4fef24ce4aaa844d7d924f85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:25:13 +0000 Subject: [PATCH 36/48] build(deps): Bump minimatch Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2. - [Release notes](https://github.com/isaacs/minimatch/releases) - [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md) - [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2) --- updated-dependencies: - dependency-name: minimatch dependency-type: indirect ... Signed-off-by: dependabot[bot] --- packages/node/test/manual/webpack-async-context/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node/test/manual/webpack-async-context/yarn.lock b/packages/node/test/manual/webpack-async-context/yarn.lock index 4c93e5750bcf..79d15b238de4 100644 --- a/packages/node/test/manual/webpack-async-context/yarn.lock +++ b/packages/node/test/manual/webpack-async-context/yarn.lock @@ -1570,9 +1570,9 @@ minimalistic-crypto-utils@^1.0.1: integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" From 371f80dc6436bddab8834e9d994b5c3acbf0ab23 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 12 Apr 2023 17:37:30 +0200 Subject: [PATCH 37/48] feat(replay): Add `getReplayId()` method (#7822) --- packages/replay/README.md | 5 ++++ packages/replay/src/integration.ts | 11 ++++++++ .../test/integration/getReplayId.test.ts | 26 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 packages/replay/test/integration/getReplayId.test.ts diff --git a/packages/replay/README.md b/packages/replay/README.md index 80d568cff52e..8c0ea81e5cba 100644 --- a/packages/replay/README.md +++ b/packages/replay/README.md @@ -138,6 +138,11 @@ A session starts when the Session Replay SDK is first loaded and initialized. Th [^1]: An 'interaction' refers to either a mouse click or a browser navigation event. +### Accessing the Replay Session ID + +You can get the ID of the currently running session via `replay.getReplayId()`. +This will return `undefined` if no session is ongoing. + ### Replay Captures Only on Errors Alternatively, rather than recording an entire session, you can capture a replay only when an error occurs. In this case, the integration will buffer up to one minute worth of events prior to the error being thrown. It will continue to record the session following the rules above regarding session life and activity. Read the [sampling](#Sampling) section for configuration options. diff --git a/packages/replay/src/integration.ts b/packages/replay/src/integration.ts index c08eb0431cac..f426a9051f97 100644 --- a/packages/replay/src/integration.ts +++ b/packages/replay/src/integration.ts @@ -226,6 +226,17 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`, return this._replay.flushImmediate(); } + /** + * Get the current session ID. + */ + public getReplayId(): string | undefined { + if (!this._replay || !this._replay.isEnabled()) { + return; + } + + return this._replay.getSessionId(); + } + /** Setup the integration. */ private _setup(): void { // Client is not available in constructor, so we need to wait until setupOnce diff --git a/packages/replay/test/integration/getReplayId.test.ts b/packages/replay/test/integration/getReplayId.test.ts new file mode 100644 index 000000000000..1080186974fc --- /dev/null +++ b/packages/replay/test/integration/getReplayId.test.ts @@ -0,0 +1,26 @@ +import { mockSdk } from '../mocks/mockSdk'; +import { useFakeTimers } from '../utils/use-fake-timers'; + +useFakeTimers(); + +describe('Integration | getReplayId', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('works', async () => { + const { integration, replay } = await mockSdk({ + replayOptions: { + stickySession: true, + }, + }); + + expect(integration.getReplayId()).toBeDefined(); + expect(integration.getReplayId()).toEqual(replay.session?.id); + + // When stopped, it is undefined + integration.stop(); + + expect(integration.getReplayId()).toBeUndefined(); + }); +}); From 7f54143cc5bb36be415fa400963c7ac69fc9b700 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 13 Apr 2023 11:04:14 +0200 Subject: [PATCH 38/48] chore: Upgrade prettier to 2.8.7 (#7832) --- package.json | 2 +- packages/core/src/tracing/idletransaction.ts | 2 +- .../nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts | 2 +- .../nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts | 2 +- packages/node/src/requestdata.ts | 4 ++-- packages/tracing-internal/src/node/integrations/mongo.ts | 2 +- packages/utils/src/instrument.ts | 4 ++-- packages/utils/src/logger.ts | 6 +++--- yarn.lock | 7 ++++++- 9 files changed, 18 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 33010e45cf14..9e02a3f547df 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "mocha": "^6.1.4", "nodemon": "^2.0.16", "npm-run-all": "^4.1.5", - "prettier": "2.7.1", + "prettier": "2.8.7", "recast": "^0.20.5", "replace-in-file": "^4.0.0", "rimraf": "^3.0.2", diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index afb81fd2dbf8..2a25fa9b45bf 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -90,7 +90,7 @@ export class IdleTransaction extends Transaction { */ private _idleTimeoutID: ReturnType | undefined; - private _finishReason: typeof IDLE_TRANSACTION_FINISH_REASONS[number] = IDLE_TRANSACTION_FINISH_REASONS[4]; + private _finishReason: (typeof IDLE_TRANSACTION_FINISH_REASONS)[number] = IDLE_TRANSACTION_FINISH_REASONS[4]; public constructor( transactionContext: TransactionContext, diff --git a/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts index 41a24d57c5c2..8c757be6a3c8 100644 --- a/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/client/wrapAppGetInitialPropsWithSentry.ts @@ -1,6 +1,6 @@ import type App from 'next/app'; -type AppGetInitialProps = typeof App['getInitialProps']; +type AppGetInitialProps = (typeof App)['getInitialProps']; /** * A passthrough function in case this function is used on the clientside. We need to make the returned function async diff --git a/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts index 4499b6bacee2..178e7617866c 100644 --- a/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/server/wrapAppGetInitialPropsWithSentry.ts @@ -10,7 +10,7 @@ import { withTracedServerSideDataFetcher, } from './utils/wrapperUtils'; -type AppGetInitialProps = typeof App['getInitialProps']; +type AppGetInitialProps = (typeof App)['getInitialProps']; /** * Create a wrapped version of the user's exported `getInitialProps` function in diff --git a/packages/node/src/requestdata.ts b/packages/node/src/requestdata.ts index d13aa78700f2..a0d5aed926a9 100644 --- a/packages/node/src/requestdata.ts +++ b/packages/node/src/requestdata.ts @@ -25,9 +25,9 @@ export type AddRequestDataToEventOptions = { /** Flags controlling whether each type of data should be added to the event */ include?: { ip?: boolean; - request?: boolean | Array; + request?: boolean | Array<(typeof DEFAULT_REQUEST_INCLUDES)[number]>; transaction?: boolean | TransactionNamingScheme; - user?: boolean | Array; + user?: boolean | Array<(typeof DEFAULT_USER_INCLUDES)[number]>; }; }; diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts index 5364f234df8d..dd5474dd4d51 100644 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ b/packages/tracing-internal/src/node/integrations/mongo.ts @@ -8,7 +8,7 @@ import { shouldDisableAutoInstrumentation } from './utils/node-utils'; // This allows us to use the same array for both defaults options and the type itself. // (note `as const` at the end to make it a union of string literal types (i.e. "a" | "b" | ... ) // and not just a string[]) -type Operation = typeof OPERATIONS[number]; +type Operation = (typeof OPERATIONS)[number]; const OPERATIONS = [ 'aggregate', // aggregate(pipeline, options, callback) 'bulkWrite', // bulkWrite(operations, options, callback) diff --git a/packages/utils/src/instrument.ts b/packages/utils/src/instrument.ts index d8779afee1b5..f6927b34be46 100644 --- a/packages/utils/src/instrument.ts +++ b/packages/utils/src/instrument.ts @@ -624,7 +624,7 @@ function instrumentDOM(): void { }); } -let _oldOnErrorHandler: typeof WINDOW['onerror'] | null = null; +let _oldOnErrorHandler: (typeof WINDOW)['onerror'] | null = null; /** JSDoc */ function instrumentError(): void { _oldOnErrorHandler = WINDOW.onerror; @@ -649,7 +649,7 @@ function instrumentError(): void { WINDOW.onerror.__SENTRY_INSTRUMENTED__ = true; } -let _oldOnUnhandledRejectionHandler: typeof WINDOW['onunhandledrejection'] | null = null; +let _oldOnUnhandledRejectionHandler: (typeof WINDOW)['onunhandledrejection'] | null = null; /** JSDoc */ function instrumentUnhandledRejection(): void { _oldOnUnhandledRejectionHandler = WINDOW.onunhandledrejection; diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index b3cc6ddacdea..e773ae28f0b5 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -6,10 +6,10 @@ import { getGlobalSingleton, GLOBAL_OBJ } from './worldwide'; const PREFIX = 'Sentry Logger '; export const CONSOLE_LEVELS = ['debug', 'info', 'warn', 'error', 'log', 'assert', 'trace'] as const; -export type ConsoleLevel = typeof CONSOLE_LEVELS[number]; +export type ConsoleLevel = (typeof CONSOLE_LEVELS)[number]; type LoggerMethod = (...args: unknown[]) => void; -type LoggerConsoleMethods = Record; +type LoggerConsoleMethods = Record<(typeof CONSOLE_LEVELS)[number], LoggerMethod>; /** JSDoc */ interface Logger extends LoggerConsoleMethods { @@ -47,7 +47,7 @@ export function consoleSandbox(callback: () => T): T { } finally { // Revert restoration to wrapped state Object.keys(wrappedLevels).forEach(level => { - originalConsole[level] = wrappedLevels[level as typeof CONSOLE_LEVELS[number]]; + originalConsole[level] = wrappedLevels[level as (typeof CONSOLE_LEVELS)[number]]; }); } } diff --git a/yarn.lock b/yarn.lock index afcf0c584dbb..b0f473085a6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22093,7 +22093,12 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@2.7.1, prettier@^2.5.1: +prettier@2.8.7: + version "2.8.7" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450" + integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw== + +prettier@^2.5.1: version "2.7.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== From 39bcfffa4d9dfd1c590dd90ca607e7f4aae2b473 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 13 Apr 2023 13:21:56 +0200 Subject: [PATCH 39/48] test(loader): Update loader & test for `window.onerror` (#7838) --- .github/workflows/build.yml | 1 + .../browser-integration-tests/fixtures/loader.js | 2 +- .../loader/noOnLoad/captureException/test.ts | 2 +- .../noOnLoad/customOnErrorHandler/subject.js | 8 ++++++++ .../loader/noOnLoad/customOnErrorHandler/test.ts | 15 +++++++++++++++ .../loader/onLoad/captureException/test.ts | 2 +- packages/browser-integration-tests/package.json | 1 + .../utils/generatePage.ts | 11 ++++++++--- 8 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/subject.js create mode 100644 packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/test.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b9d5ec1f8be..ae90621dc220 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -583,6 +583,7 @@ jobs: bundle: - loader_base - loader_eager + - loader_debug - loader_tracing - loader_replay - loader_tracing_replay diff --git a/packages/browser-integration-tests/fixtures/loader.js b/packages/browser-integration-tests/fixtures/loader.js index 36d7d2401856..e8e84a3a7a9a 100644 --- a/packages/browser-integration-tests/fixtures/loader.js +++ b/packages/browser-integration-tests/fixtures/loader.js @@ -1,4 +1,4 @@ -!function(n,e,r,t,a,i,o,c,_,f){for(var p=f,forceLoad=!1,s=0;s-1){p&&"no"===document.scripts[s].getAttribute("data-lazy")&&(p=!1);break}var u=!1,l=[],d=function(n){("e"in n||"p"in n||n.f&&n.f.indexOf("capture")>-1||n.f&&n.f.indexOf("showReportDialog")>-1)&&p&&R(l),d.data.push(n)};function R(o){if(!u){u=!0;var f=e.scripts[0],p=e.createElement(r);p.src=c,p.crossOrigin="anonymous",p.addEventListener("load",(function(){try{n[t]&&n[t].__SENTRY_LOADER__&&(n[t]=E),n[a]&&n[a].__SENTRY_LOADER__&&(n[a]=v),n.SENTRY_SDK_SOURCE="loader";var e=n[i],r=e.init;e.init=function(n){var t=_;for(var a in n)Object.prototype.hasOwnProperty.call(n,a)&&(t[a]=n[a]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new e.BrowserTracing);(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new e.Replay);n.integrations=r}(t,e),r(t)},function(e,r){try{for(var i=0;i-1){u&&"no"===document.scripts[p].getAttribute("data-lazy")&&(u=!1);break}var d=!1,l=[],_=function(n){("e"in n||"p"in n||n.f&&n.f.indexOf("capture")>-1||n.f&&n.f.indexOf("showReportDialog")>-1)&&u&&E(l),_.data.push(n)};function v(){_({e:[].slice.call(arguments)})}function h(n){_({p:"reason"in n?n.reason:"detail"in n&&"reason"in n.detail?n.detail.reason:n})}function E(a){if(!d){d=!0;var f=e.scripts[0],u=e.createElement(r);u.src=c,u.crossOrigin="anonymous",u.addEventListener("load",(function(){try{n.removeEventListener("error",v),n.removeEventListener("unhandledrejection",h),n.SENTRY_SDK_SOURCE="loader";var e=n[o],r=e.init;e.init=function(n){var t=s;for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new e.BrowserTracing);(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new e.Replay);n.integrations=r}(t,e),r(t)},function(e,r){try{for(var o=0;o { const req = waitForErrorRequest(page); diff --git a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/subject.js b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/subject.js new file mode 100644 index 000000000000..405fb09bbac5 --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/subject.js @@ -0,0 +1,8 @@ +const oldOnError = window.onerror; + +window.onerror = function () { + console.log('custom error'); + oldOnError && oldOnError.apply(this, arguments); +}; + +window.doSomethingWrong(); diff --git a/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/test.ts b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/test.ts new file mode 100644 index 000000000000..96c49a49ab4c --- /dev/null +++ b/packages/browser-integration-tests/loader-suites/loader/noOnLoad/customOnErrorHandler/test.ts @@ -0,0 +1,15 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; + +sentryTest('error handler works with a recursive custom error handler', async ({ getLocalTestUrl, page }) => { + const req = waitForErrorRequest(page); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const eventData = envelopeRequestParser(await req); + expect(eventData.exception?.values?.length).toBe(1); + expect(eventData.exception?.values?.[0]?.value).toBe('window.doSomethingWrong is not a function'); +}); diff --git a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureException/test.ts b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureException/test.ts index e7441a654724..896ba2d53c0a 100644 --- a/packages/browser-integration-tests/loader-suites/loader/onLoad/captureException/test.ts +++ b/packages/browser-integration-tests/loader-suites/loader/onLoad/captureException/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser,waitForErrorRequest } from '../../../../utils/helpers'; +import { envelopeRequestParser, waitForErrorRequest } from '../../../../utils/helpers'; sentryTest('captureException works', async ({ getLocalTestUrl, page }) => { const req = waitForErrorRequest(page); diff --git a/packages/browser-integration-tests/package.json b/packages/browser-integration-tests/package.json index c8b9c92d19e8..61645e7960e5 100644 --- a/packages/browser-integration-tests/package.json +++ b/packages/browser-integration-tests/package.json @@ -39,6 +39,7 @@ "test:loader:tracing": "PW_BUNDLE=loader_tracing yarn test:loader", "test:loader:replay": "PW_BUNDLE=loader_replay yarn test:loader", "test:loader:full": "PW_BUNDLE=loader_tracing_replay yarn test:loader", + "test:loader:debug": "PW_BUNDLE=loader_debug yarn test:loader", "test:ci": "playwright test ./suites --browser='all' --reporter='line'", "test:update-snapshots": "yarn test --update-snapshots --browser='all' && yarn test --update-snapshots", "test:detect-flaky": "ts-node scripts/detectFlakyTests.ts", diff --git a/packages/browser-integration-tests/utils/generatePage.ts b/packages/browser-integration-tests/utils/generatePage.ts index a3bd1d7c8991..4a6afb3eab2e 100644 --- a/packages/browser-integration-tests/utils/generatePage.ts +++ b/packages/browser-integration-tests/utils/generatePage.ts @@ -11,17 +11,22 @@ const LOADER_TEMPLATE = readFileSync(path.join(__dirname, '../fixtures/loader.js const LOADER_CONFIGS: Record; lazy: boolean }> = { loader_base: { - bundle: 'browser/build/bundles/bundle.es5.js', + bundle: 'browser/build/bundles/bundle.es5.min.js', options: {}, lazy: true, }, loader_eager: { - bundle: 'browser/build/bundles/bundle.es5.js', + bundle: 'browser/build/bundles/bundle.es5.min.js', options: {}, lazy: false, }, + loader_debug: { + bundle: 'browser/build/bundles/bundle.es5.debug.min.js', + options: { debug: true }, + lazy: true, + }, loader_tracing: { - bundle: 'browser/build/bundles/bundle.tracing.es5.js', + bundle: 'browser/build/bundles/bundle.tracing.es5.min.js', options: { tracesSampleRate: 1 }, lazy: false, }, From 3503b91c8f253c28efe1f38e39da71d94f3cec86 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 13 Apr 2023 13:43:32 +0200 Subject: [PATCH 40/48] test(loader): Update loader script (#7840) --- packages/browser-integration-tests/fixtures/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-integration-tests/fixtures/loader.js b/packages/browser-integration-tests/fixtures/loader.js index e8e84a3a7a9a..15a74d26f13d 100644 --- a/packages/browser-integration-tests/fixtures/loader.js +++ b/packages/browser-integration-tests/fixtures/loader.js @@ -1,4 +1,4 @@ -!function(n,e,r,t,i,o,a,c,s,f){for(var u=f,forceLoad=!1,p=0;p-1){u&&"no"===document.scripts[p].getAttribute("data-lazy")&&(u=!1);break}var d=!1,l=[],_=function(n){("e"in n||"p"in n||n.f&&n.f.indexOf("capture")>-1||n.f&&n.f.indexOf("showReportDialog")>-1)&&u&&E(l),_.data.push(n)};function v(){_({e:[].slice.call(arguments)})}function h(n){_({p:"reason"in n?n.reason:"detail"in n&&"reason"in n.detail?n.detail.reason:n})}function E(a){if(!d){d=!0;var f=e.scripts[0],u=e.createElement(r);u.src=c,u.crossOrigin="anonymous",u.addEventListener("load",(function(){try{n.removeEventListener("error",v),n.removeEventListener("unhandledrejection",h),n.SENTRY_SDK_SOURCE="loader";var e=n[o],r=e.init;e.init=function(n){var t=s;for(var i in n)Object.prototype.hasOwnProperty.call(n,i)&&(t[i]=n[i]);!function(n,e){var r=n.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(n){return n.name}));n.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new e.BrowserTracing);(n.replaysSessionSampleRate||n.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new e.Replay);n.integrations=r}(t,e),r(t)},function(e,r){try{for(var o=0;o-1){u&&"no"===document.scripts[p].getAttribute("data-lazy")&&(u=!1);break}var d=!1,l=[],_=function(e){("e"in e||"p"in e||e.f&&e.f.indexOf("capture")>-1||e.f&&e.f.indexOf("showReportDialog")>-1)&&u&&E(l),_.data.push(e)};function v(){_({e:[].slice.call(arguments)})}function h(e){_({p:"reason"in e?e.reason:"detail"in e&&"reason"in e.detail?e.detail.reason:e})}function E(a){if(!d){d=!0;var f=n.scripts[0],u=n.createElement(r);u.src=c,u.crossOrigin="anonymous",u.addEventListener("load",(function(){try{e.removeEventListener("error",v),e.removeEventListener("unhandledrejection",h),e.SENTRY_SDK_SOURCE="loader";var n=e[o],r=n.init;n.init=function(e){var t=s;for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);!function(e,n){var r=e.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(e){return e.name}));e.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new n.BrowserTracing);(e.replaysSessionSampleRate||e.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new n.Replay);e.integrations=r}(t,n),r(t)},function(n,r){try{for(var o=0;o Date: Thu, 13 Apr 2023 13:59:43 +0200 Subject: [PATCH 41/48] fix(sveltekit): Rewrite server-side stack frames to match source maps (#7831) Add the `RewriteFrames` integration to match the server-side stack frames with the uploaded source maps. --- packages/sveltekit/package.json | 1 + packages/sveltekit/src/server/sdk.ts | 2 ++ packages/sveltekit/src/vite/sourceMaps.ts | 6 +++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 3edbba488334..183a2d254501 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -22,6 +22,7 @@ "dependencies": { "@sentry-internal/tracing": "7.47.0", "@sentry/core": "7.47.0", + "@sentry/integrations": "7.47.0", "@sentry/node": "7.47.0", "@sentry/svelte": "7.47.0", "@sentry/types": "7.47.0", diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 79f78b2b1908..902a6a7d1a06 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -1,4 +1,5 @@ import { configureScope } from '@sentry/core'; +import { RewriteFrames } from '@sentry/integrations'; import type { NodeOptions } from '@sentry/node'; import { init as initNodeSdk, Integrations } from '@sentry/node'; import { addOrUpdateIntegration } from '@sentry/utils'; @@ -23,4 +24,5 @@ export function init(options: NodeOptions): void { function addServerIntegrations(options: NodeOptions): void { options.integrations = addOrUpdateIntegration(new Integrations.Undici(), options.integrations || []); + options.integrations = addOrUpdateIntegration(new RewriteFrames(), options.integrations || []); } diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index b388e4353681..4ecdf8d40bdd 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -9,7 +9,11 @@ import type { Plugin } from 'vite'; const DEFAULT_PLUGIN_OPTIONS: SentryVitePluginOptions = { // TODO: Read these values from the node adapter somehow as the out dir can be changed in the adapter options - include: ['build/server', 'build/client'], + include: [ + { paths: ['build/client'] }, + { paths: ['build/server/chunks'] }, + { paths: ['build/server'], ignore: ['chunks/**'] }, + ], }; // sorcery has no types, so these are some basic type definitions: From 95f721f47dd4f9fb205e245e7a31bc983bbc46ff Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 13 Apr 2023 14:06:25 +0200 Subject: [PATCH 42/48] fix(utils): Make xhr instrumentation independent of parallel running SDK versions (#7836) --- .../browser/src/integrations/breadcrumbs.ts | 7 ++-- packages/integrations/src/httpclient.ts | 7 ++-- packages/replay/src/coreHandlers/handleXhr.ts | 7 ++-- .../test/unit/coreHandlers/handleXhr.test.ts | 7 ++-- .../tracing-internal/src/browser/request.ts | 34 ++++++++++--------- .../test/browser/request.test.ts | 7 ++-- packages/types/src/instrument.ts | 3 +- packages/utils/src/instrument.ts | 13 ++++--- 8 files changed, 51 insertions(+), 34 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index cfd833ef759a..3d019945b53b 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -15,6 +15,7 @@ import { logger, parseUrl, safeJoin, + SENTRY_XHR_DATA_KEY, severityLevelFromString, } from '@sentry/utils'; @@ -225,12 +226,14 @@ function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level: function _xhrBreadcrumb(handlerData: HandlerData & HandlerDataXhr): void { const { startTimestamp, endTimestamp } = handlerData; + const sentryXhrData = handlerData.xhr[SENTRY_XHR_DATA_KEY]; + // We only capture complete, non-sentry requests - if (!startTimestamp || !endTimestamp || !handlerData.xhr.__sentry_xhr__) { + if (!startTimestamp || !endTimestamp || !sentryXhrData) { return; } - const { method, url, status_code, body } = handlerData.xhr.__sentry_xhr__; + const { method, url, status_code, body } = sentryXhrData; const data: XhrBreadcrumbData = { method, diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index 6c71763b2ab8..92ef392b872b 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -12,6 +12,7 @@ import { addInstrumentationHandler, GLOBAL_OBJ, logger, + SENTRY_XHR_DATA_KEY, supportsNativeFetch, } from '@sentry/utils'; @@ -322,11 +323,13 @@ export class HttpClient implements Integration { (handlerData: HandlerDataXhr & { xhr: SentryWrappedXMLHttpRequest & XMLHttpRequest }) => { const { xhr } = handlerData; - if (!xhr.__sentry_xhr__) { + const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; + + if (!sentryXhrData) { return; } - const { method, request_headers: headers } = xhr.__sentry_xhr__; + const { method, request_headers: headers } = sentryXhrData; if (!method) { return; diff --git a/packages/replay/src/coreHandlers/handleXhr.ts b/packages/replay/src/coreHandlers/handleXhr.ts index eca1994c80b5..d3ec1736427a 100644 --- a/packages/replay/src/coreHandlers/handleXhr.ts +++ b/packages/replay/src/coreHandlers/handleXhr.ts @@ -1,4 +1,5 @@ import type { HandlerDataXhr } from '@sentry/types'; +import { SENTRY_XHR_DATA_KEY } from '@sentry/utils'; import type { NetworkRequestData, ReplayContainer, ReplayPerformanceEntry } from '../types'; import { addNetworkBreadcrumb } from './util/addNetworkBreadcrumb'; @@ -7,12 +8,14 @@ import { addNetworkBreadcrumb } from './util/addNetworkBreadcrumb'; export function handleXhr(handlerData: HandlerDataXhr): ReplayPerformanceEntry | null { const { startTimestamp, endTimestamp, xhr } = handlerData; - if (!startTimestamp || !endTimestamp || !xhr.__sentry_xhr__) { + const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; + + if (!startTimestamp || !endTimestamp || !sentryXhrData) { return null; } // This is only used as a fallback, so we know the body sizes are never set here - const { method, url, status_code: statusCode } = xhr.__sentry_xhr__; + const { method, url, status_code: statusCode } = sentryXhrData; if (url === undefined) { return null; diff --git a/packages/replay/test/unit/coreHandlers/handleXhr.test.ts b/packages/replay/test/unit/coreHandlers/handleXhr.test.ts index 48e4d0ebece6..84fdf2facfdf 100644 --- a/packages/replay/test/unit/coreHandlers/handleXhr.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleXhr.test.ts @@ -1,11 +1,12 @@ import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, SentryXhrData } from '@sentry/types'; +import { SENTRY_XHR_DATA_KEY } from '@sentry/utils'; import { handleXhr } from '../../../src/coreHandlers/handleXhr'; const DEFAULT_DATA: HandlerDataXhr = { args: ['GET', '/api/0/organizations/sentry/'], xhr: { - __sentry_xhr__: { + [SENTRY_XHR_DATA_KEY]: { method: 'GET', url: '/api/0/organizations/sentry/', status_code: 200, @@ -45,8 +46,8 @@ describe('Unit | coreHandlers | handleXhr', () => { ...DEFAULT_DATA, xhr: { ...DEFAULT_DATA.xhr, - __sentry_xhr__: { - ...(DEFAULT_DATA.xhr.__sentry_xhr__ as SentryXhrData), + [SENTRY_XHR_DATA_KEY]: { + ...(DEFAULT_DATA.xhr[SENTRY_XHR_DATA_KEY] as SentryXhrData), request_body_size: 123, response_body_size: 456, }, diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index bce017af48f4..22477e258ccd 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -6,6 +6,7 @@ import { BAGGAGE_HEADER_NAME, dynamicSamplingContextToSentryBaggageHeader, isInstanceOf, + SENTRY_XHR_DATA_KEY, stringMatchesSomePattern, } from '@sentry/utils'; @@ -74,7 +75,7 @@ export interface FetchData { /** Data returned from XHR request */ export interface XHRData { xhr?: { - __sentry_xhr__?: { + [SENTRY_XHR_DATA_KEY]?: { method: string; url: string; status_code: number; @@ -297,24 +298,25 @@ export function xhrCallback( shouldAttachHeaders: (url: string) => boolean, spans: Record, ): void { + const xhr = handlerData.xhr; + const sentryXhrData = xhr && xhr[SENTRY_XHR_DATA_KEY]; + if ( !hasTracingEnabled() || - (handlerData.xhr && handlerData.xhr.__sentry_own_request__) || - !(handlerData.xhr && handlerData.xhr.__sentry_xhr__ && shouldCreateSpan(handlerData.xhr.__sentry_xhr__.url)) + (xhr && xhr.__sentry_own_request__) || + !(xhr && sentryXhrData && shouldCreateSpan(sentryXhrData.url)) ) { return; } - const xhr = handlerData.xhr.__sentry_xhr__; - // check first if the request has finished and is tracked by an existing span which should now end if (handlerData.endTimestamp) { - const spanId = handlerData.xhr.__sentry_xhr_span_id__; + const spanId = xhr.__sentry_xhr_span_id__; if (!spanId) return; const span = spans[spanId]; if (span) { - span.setHttpStatus(xhr.status_code); + span.setHttpStatus(sentryXhrData.status_code); span.finish(); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete @@ -330,21 +332,21 @@ export function xhrCallback( if (currentSpan && activeTransaction) { const span = currentSpan.startChild({ data: { - ...xhr.data, + ...sentryXhrData.data, type: 'xhr', - method: xhr.method, - url: xhr.url, + method: sentryXhrData.method, + url: sentryXhrData.url, }, - description: `${xhr.method} ${xhr.url}`, + description: `${sentryXhrData.method} ${sentryXhrData.url}`, op: 'http.client', }); - handlerData.xhr.__sentry_xhr_span_id__ = span.spanId; - spans[handlerData.xhr.__sentry_xhr_span_id__] = span; + xhr.__sentry_xhr_span_id__ = span.spanId; + spans[xhr.__sentry_xhr_span_id__] = span; - if (handlerData.xhr.setRequestHeader && shouldAttachHeaders(handlerData.xhr.__sentry_xhr__.url)) { + if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) { try { - handlerData.xhr.setRequestHeader('sentry-trace', span.toTraceparent()); + xhr.setRequestHeader('sentry-trace', span.toTraceparent()); const dynamicSamplingContext = activeTransaction.getDynamicSamplingContext(); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); @@ -353,7 +355,7 @@ export function xhrCallback( // From MDN: "If this method is called several times with the same header, the values are merged into one single request header." // We can therefore simply set a baggage header without checking what was there before // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader - handlerData.xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader); + xhr.setRequestHeader(BAGGAGE_HEADER_NAME, sentryBaggageHeader); } } catch (_) { // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED. diff --git a/packages/tracing-internal/test/browser/request.test.ts b/packages/tracing-internal/test/browser/request.test.ts index a423986884b7..7fbe23a41295 100644 --- a/packages/tracing-internal/test/browser/request.test.ts +++ b/packages/tracing-internal/test/browser/request.test.ts @@ -1,6 +1,7 @@ /* eslint-disable deprecation/deprecation */ import * as sentryCore from '@sentry/core'; import * as utils from '@sentry/utils'; +import { SENTRY_XHR_DATA_KEY } from '@sentry/utils'; import type { Transaction } from '../../../tracing/src'; import { addExtensionMethods, Span, spanStatusfromHttpCode } from '../../../tracing/src'; @@ -234,7 +235,7 @@ describe('callbacks', () => { beforeEach(() => { xhrHandlerData = { xhr: { - __sentry_xhr__: { + [SENTRY_XHR_DATA_KEY]: { method: 'GET', url: 'http://dogs.are.great/', status_code: 200, @@ -328,7 +329,7 @@ describe('callbacks', () => { ...xhrHandlerData, endTimestamp, }; - postRequestXHRHandlerData.xhr!.__sentry_xhr__!.status_code = 404; + postRequestXHRHandlerData.xhr![SENTRY_XHR_DATA_KEY]!.status_code = 404; // triggered by response coming back xhrCallback(postRequestXHRHandlerData, alwaysCreateSpan, alwaysAttachHeaders, spans); @@ -342,7 +343,7 @@ describe('callbacks', () => { const postRequestXHRHandlerData = { ...{ xhr: { - __sentry_xhr__: xhrHandlerData.xhr?.__sentry_xhr__, + [SENTRY_XHR_DATA_KEY]: xhrHandlerData.xhr?.[SENTRY_XHR_DATA_KEY], }, }, startTimestamp, diff --git a/packages/types/src/instrument.ts b/packages/types/src/instrument.ts index 5f00c0df926e..b47426e69049 100644 --- a/packages/types/src/instrument.ts +++ b/packages/types/src/instrument.ts @@ -4,10 +4,11 @@ type XHRSendInput = unknown; export interface SentryWrappedXMLHttpRequest { - __sentry_xhr__?: SentryXhrData; + __sentry_xhr_v2__?: SentryXhrData; __sentry_own_request__?: boolean; } +// WARNING: When the shape of this type is changed bump the version in `SentryWrappedXMLHttpRequest` export interface SentryXhrData { method?: string; url?: string; diff --git a/packages/utils/src/instrument.ts b/packages/utils/src/instrument.ts index f6927b34be46..81de75b02f8d 100644 --- a/packages/utils/src/instrument.ts +++ b/packages/utils/src/instrument.ts @@ -19,6 +19,8 @@ import { getGlobalObject } from './worldwide'; // eslint-disable-next-line deprecation/deprecation const WINDOW = getGlobalObject(); +export const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v2__'; + export type InstrumentHandlerType = | 'console' | 'dom' @@ -244,7 +246,7 @@ function instrumentXHR(): void { fill(xhrproto, 'open', function (originalOpen: () => void): () => void { return function (this: XMLHttpRequest & SentryWrappedXMLHttpRequest, ...args: any[]): void { const url = args[1]; - const xhrInfo: SentryXhrData = (this.__sentry_xhr__ = { + const xhrInfo: SentryXhrData = (this[SENTRY_XHR_DATA_KEY] = { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access method: isString(args[0]) ? args[0].toUpperCase() : args[0], url: args[1], @@ -259,7 +261,7 @@ function instrumentXHR(): void { const onreadystatechangeHandler: () => void = () => { // For whatever reason, this is not the same instance here as from the outer method - const xhrInfo = this.__sentry_xhr__; + const xhrInfo = this[SENTRY_XHR_DATA_KEY]; if (!xhrInfo) { return; @@ -301,7 +303,7 @@ function instrumentXHR(): void { return function (this: SentryWrappedXMLHttpRequest, ...setRequestHeaderArgs: unknown[]): void { const [header, value] = setRequestHeaderArgs as [string, string]; - const xhrInfo = this.__sentry_xhr__; + const xhrInfo = this[SENTRY_XHR_DATA_KEY]; if (xhrInfo) { xhrInfo.request_headers[header] = value; @@ -317,8 +319,9 @@ function instrumentXHR(): void { fill(xhrproto, 'send', function (originalSend: () => void): () => void { return function (this: XMLHttpRequest & SentryWrappedXMLHttpRequest, ...args: any[]): void { - if (this.__sentry_xhr__ && args[0] !== undefined) { - this.__sentry_xhr__.body = args[0]; + const sentryXhrData = this[SENTRY_XHR_DATA_KEY]; + if (sentryXhrData && args[0] !== undefined) { + sentryXhrData.body = args[0]; } triggerHandlers('xhr', { From d32451664ce5baef31c7ee9270ff98473a495c14 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 13 Apr 2023 14:13:15 +0200 Subject: [PATCH 43/48] fix(serverless): Account when transaction undefined (#7829) --- packages/serverless/src/awslambda.ts | 4 ++-- packages/serverless/src/gcpfunction/cloud_events.ts | 4 ++-- packages/serverless/src/gcpfunction/events.ts | 4 ++-- packages/serverless/src/gcpfunction/http.ts | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index ccfc1e191024..3f2883385422 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -328,7 +328,7 @@ export function wrapHandler( dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, source: 'component', }, - }); + }) as Sentry.Transaction | undefined; const scope = hub.pushScope(); let rv: TResult; @@ -350,7 +350,7 @@ export function wrapHandler( throw e; } finally { clearTimeout(timeoutWarningTimer); - transaction.finish(); + transaction?.finish(); hub.popScope(); await flush(options.flushTimeout).catch(e => { __DEBUG_BUILD__ && logger.error(e); diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 6a8bc47f226f..f20f7f941ee2 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -36,7 +36,7 @@ function _wrapCloudEventFunction( name: context.type || '', op: 'function.gcp.cloud_event', metadata: { source: 'component' }, - }); + }) as ReturnType | undefined; // getCurrentHub() is expected to use current active domain as a carrier // since functions-framework creates a domain for each incoming request. @@ -51,7 +51,7 @@ function _wrapCloudEventFunction( if (args[0] !== null && args[0] !== undefined) { captureException(args[0]); } - transaction.finish(); + transaction?.finish(); void flush(options.flushTimeout) .then(null, e => { diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index c00838ccaae6..f6485de054d7 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -38,7 +38,7 @@ function _wrapEventFunction name: context.eventType, op: 'function.gcp.event', metadata: { source: 'component' }, - }); + }) as ReturnType | undefined; // getCurrentHub() is expected to use current active domain as a carrier // since functions-framework creates a domain for each incoming request. @@ -53,7 +53,7 @@ function _wrapEventFunction if (args[0] !== null && args[0] !== undefined) { captureException(args[0]); } - transaction.finish(); + transaction?.finish(); void flush(options.flushTimeout) .then(null, e => { diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 8892353fd4bf..9bc9052d179d 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -92,7 +92,7 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial | undefined; // getCurrentHub() is expected to use current active domain as a carrier // since functions-framework creates a domain for each incoming request. @@ -115,8 +115,8 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial void), encoding?: string | (() => void), cb?: () => void): any { - transaction.setHttpStatus(res.statusCode); - transaction.finish(); + transaction?.setHttpStatus(res.statusCode); + transaction?.finish(); void flush(options.flushTimeout) .then(null, e => { From b61ac96dd90db23b84b16feedd5d0be552488608 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 13 Apr 2023 14:20:22 +0200 Subject: [PATCH 44/48] fix(sveltekit): Improve server-side grouping by removing the stack frame module (#7835) Add a custom `RewriteFrames` iteratee to the server SDK which * Does exactly the same thing as the default iteratee if simply initializiung `RewriteFrames()` without custom options * Removes the `module` field from each stack frame --- packages/sveltekit/src/server/sdk.ts | 6 ++- packages/sveltekit/src/server/utils.ts | 41 +++++++++++++++++- packages/sveltekit/test/server/utils.test.ts | 44 +++++++++++++++++++- 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 902a6a7d1a06..670f7879e7ba 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -5,6 +5,7 @@ import { init as initNodeSdk, Integrations } from '@sentry/node'; import { addOrUpdateIntegration } from '@sentry/utils'; import { applySdkMetadata } from '../common/metadata'; +import { rewriteFramesIteratee } from './utils'; /** * @@ -24,5 +25,8 @@ export function init(options: NodeOptions): void { function addServerIntegrations(options: NodeOptions): void { options.integrations = addOrUpdateIntegration(new Integrations.Undici(), options.integrations || []); - options.integrations = addOrUpdateIntegration(new RewriteFrames(), options.integrations || []); + options.integrations = addOrUpdateIntegration( + new RewriteFrames({ iteratee: rewriteFramesIteratee }), + options.integrations || [], + ); } diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts index 67c3bfe9e050..644cd8477fff 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server/utils.ts @@ -1,5 +1,5 @@ -import type { DynamicSamplingContext, TraceparentData } from '@sentry/types'; -import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils'; +import type { DynamicSamplingContext, StackFrame, TraceparentData } from '@sentry/types'; +import { baggageHeaderToDynamicSamplingContext, basename, extractTraceparentData } from '@sentry/utils'; import type { RequestEvent } from '@sveltejs/kit'; /** @@ -17,3 +17,40 @@ export function getTracePropagationData(event: RequestEvent): { return { traceparentData, dynamicSamplingContext }; } + +/** + * A custom iteratee function for the `RewriteFrames` integration. + * + * Does the same as the default iteratee, but also removes the `module` property from the + * frame to improve issue grouping. + * + * For some reason, our stack trace processing pipeline isn't able to resolve the bundled + * module name to the original file name correctly, leading to individual error groups for + * each module. Removing the `module` field makes the grouping algorithm fall back to the + * `filename` field, which is correctly resolved and hence grouping works as expected. + */ +export function rewriteFramesIteratee(frame: StackFrame): StackFrame { + if (!frame.filename) { + return frame; + } + + const prefix = 'app:///'; + + // Check if the frame filename begins with `/` or a Windows-style prefix such as `C:\` + const isWindowsFrame = /^[a-zA-Z]:\\/.test(frame.filename); + const startsWithSlash = /^\//.test(frame.filename); + if (isWindowsFrame || startsWithSlash) { + const filename = isWindowsFrame + ? frame.filename + .replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix + .replace(/\\/g, '/') // replace all `\\` instances with `/` + : frame.filename; + + const base = basename(filename); + frame.filename = `${prefix}${base}`; + } + + delete frame.module; + + return frame; +} diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 8e5c064c338c..179cc6682d85 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -1,4 +1,7 @@ -import { getTracePropagationData } from '../../src/server/utils'; +import { RewriteFrames } from '@sentry/integrations'; +import type { StackFrame } from '@sentry/types'; + +import { getTracePropagationData, rewriteFramesIteratee } from '../../src/server/utils'; const MOCK_REQUEST_EVENT: any = { request: { @@ -53,3 +56,42 @@ describe('getTracePropagationData', () => { expect(dynamicSamplingContext).toBeUndefined(); }); }); + +describe('rewriteFramesIteratee', () => { + it('removes the module property from the frame', () => { + const frame: StackFrame = { + filename: '/some/path/to/server/chunks/3-ab34d22f.js', + module: '3-ab34d22f.js', + }; + + const result = rewriteFramesIteratee(frame); + + expect(result).not.toHaveProperty('module'); + }); + + it('does the same filename modification as the default RewriteFrames iteratee', () => { + const frame: StackFrame = { + filename: '/some/path/to/server/chunks/3-ab34d22f.js', + lineno: 1, + colno: 1, + module: '3-ab34d22f.js', + }; + + const originalRewriteFrames = new RewriteFrames(); + // @ts-ignore this property exists + const defaultIteratee = originalRewriteFrames._iteratee; + + const defaultResult = defaultIteratee({ ...frame }); + delete defaultResult.module; + + const result = rewriteFramesIteratee({ ...frame }); + + expect(result).toEqual({ + filename: 'app:///3-ab34d22f.js', + lineno: 1, + colno: 1, + }); + + expect(result).toStrictEqual(defaultResult); + }); +}); From 21a975ec4027b42722f83a44f3a029fb5d7e674e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 13 Apr 2023 14:46:36 +0200 Subject: [PATCH 45/48] test(loader): Update loader script (#7841) --- packages/browser-integration-tests/fixtures/loader.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/browser-integration-tests/fixtures/loader.js b/packages/browser-integration-tests/fixtures/loader.js index 15a74d26f13d..32ed5b94860c 100644 --- a/packages/browser-integration-tests/fixtures/loader.js +++ b/packages/browser-integration-tests/fixtures/loader.js @@ -1,10 +1,10 @@ -!function(e,n,r,t,i,o,a,c,s,f){for(var u=f,forceLoad=!1,p=0;p-1){u&&"no"===document.scripts[p].getAttribute("data-lazy")&&(u=!1);break}var d=!1,l=[],_=function(e){("e"in e||"p"in e||e.f&&e.f.indexOf("capture")>-1||e.f&&e.f.indexOf("showReportDialog")>-1)&&u&&E(l),_.data.push(e)};function v(){_({e:[].slice.call(arguments)})}function h(e){_({p:"reason"in e?e.reason:"detail"in e&&"reason"in e.detail?e.detail.reason:e})}function E(a){if(!d){d=!0;var f=n.scripts[0],u=n.createElement(r);u.src=c,u.crossOrigin="anonymous",u.addEventListener("load",(function(){try{e.removeEventListener("error",v),e.removeEventListener("unhandledrejection",h),e.SENTRY_SDK_SOURCE="loader";var n=e[o],r=n.init;n.init=function(e){var t=s;for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);!function(e,n){var r=e.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(e){return e.name}));e.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new n.BrowserTracing);(e.replaysSessionSampleRate||e.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new n.Replay);e.integrations=r}(t,n),r(t)},function(n,r){try{for(var o=0;o-1){u&&"no"===document.scripts[p].getAttribute("data-lazy")&&(u=!1);break}var d=!1,l=[],_=function(e){("e"in e||"p"in e||e.f&&e.f.indexOf("capture")>-1||e.f&&e.f.indexOf("showReportDialog")>-1)&&u&&E(l),_.data.push(e)};function v(){_({e:[].slice.call(arguments)})}function h(e){_({p:"reason"in e?e.reason:"detail"in e&&"reason"in e.detail?e.detail.reason:e})}function E(a){if(!d){d=!0;var f=n.scripts[0],u=n.createElement(r);u.src=c,u.crossOrigin="anonymous",u.addEventListener("load",(function(){try{e.removeEventListener(t,v),e.removeEventListener(i,h),e.SENTRY_SDK_SOURCE="loader";var n=e[o],r=n.init;n.init=function(e){var t=s;for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);!function(e,n){var r=e.integrations||[];if(!Array.isArray(r))return;var t=r.map((function(e){return e.name}));e.tracesSampleRate&&-1===t.indexOf("BrowserTracing")&&r.push(new n.BrowserTracing);(e.replaysSessionSampleRate||e.replaysOnErrorSampleRate)&&-1===t.indexOf("Replay")&&r.push(new n.Replay);e.integrations=r}(t,n),r(t)},function(n,r){try{for(var t=0;t Date: Thu, 13 Apr 2023 15:17:59 +0200 Subject: [PATCH 46/48] docs(sveltekit): Add more source maps upload documentation (#7844) --- packages/sveltekit/README.md | 87 ++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/packages/sveltekit/README.md b/packages/sveltekit/README.md index c8abe8be49fd..cdc3e0e9582a 100644 --- a/packages/sveltekit/README.md +++ b/packages/sveltekit/README.md @@ -165,15 +165,94 @@ The Sentry SvelteKit SDK mostly relies on [SvelteKit Hooks](https://kit.svelte.d This adds the [Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/tree/main/packages/vite-plugin) to your Vite config to automatically upload source maps to Sentry. +## Uploading Source Maps + +After completing the [Vite Setup](#5-vite-setup), the SDK will automatically upload source maps to Sentry, when you +build your project. However, you still need to specify your Sentry auth token as well as your org and project slugs. You +can either set them as env variables (for example in a `.env` file): + +- `SENTRY_ORG` your Sentry org slug +- `SENTRY_PROJECT` your Sentry project slug +- `SENTRY_AUTH_TOKEN` your Sentry auth token + +Or you can pass them in whatever form you prefer to `sentrySvelteKit`: + +```js +// vite.config.js +import { sveltekit } from '@sveltejs/kit/vite'; +import { sentrySvelteKit } from '@sentry/sveltekit'; + +export default { + plugins: [ + sentrySvelteKit({ + sourceMapsUploadOptions: { + org: 'my-org-slug', + project: 'my-project-slug', + authToken: process.env.SENTRY_AUTH_TOKEN, + }, + }), + sveltekit(), + ], + // ... rest of your Vite config +}; +``` + +### Configuring Source maps upload + +Under `sourceMapsUploadOptions`, you can also specify all additional options supported by the +[Sentry Vite Plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/packages/vite-plugin/README.md#configuration). +This might be useful if you're using adapters other than the Node adapter or have a more customized build setup. + +```js +// vite.config.js +import { sveltekit } from '@sveltejs/kit/vite'; +import { sentrySvelteKit } from '@sentry/sveltekit'; + +export default { + plugins: [ + sentrySvelteKit({ + sourceMapsUploadOptions: { + org: 'my-org-slug', + project: 'my-project-slug', + authToken: 'process.env.SENTRY_AUTH_TOKEN', + include: ['dist'], + cleanArtifacts: true, + setCommits: { + auto: true, + }, + }, + }), + sveltekit(), + ], + // ... rest of your Vite config +}; +``` + +### Disabeling automatic source map upload + +If you don't want to upload source maps automatically, you can disable it as follows: + +```js +// vite.config.js +import { sveltekit } from '@sveltejs/kit/vite'; +import { sentrySvelteKit } from '@sentry/sveltekit'; + +export default { + plugins: [ + sentrySvelteKit({ + autoUploadSourceMaps: false, + }), + sveltekit(), + ], + // ... rest of your Vite config +}; +``` + ## Known Limitations This SDK is still under active development and several features are missing. Take a look at our [SvelteKit SDK Development Roadmap](https://github.com/getsentry/sentry-javascript/issues/6692) to follow the progress: -- **Source Maps** upload is not yet working correctly. - We already investigated [some options](https://github.com/getsentry/sentry-javascript/discussions/5838#discussioncomment-4696985) but uploading source maps doesn't work automtatically out of the box yet. - This will be addressed next, as we release the next alpha builds. - - **Adapters** other than `@sveltejs/adapter-node` are currently not supported. We haven't yet tested other platforms like Vercel. This is on our roadmap but it will come at a later time. From ed371869c3d905e40b77b9280ec7f52e1bccd70a Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 13 Apr 2023 15:41:55 +0200 Subject: [PATCH 47/48] fix(sveltekit): Don't crash build when CLI credentials are missing (#7846) Previously, a project build would crash when users didn't specify org, project slugs or auth tokens. This patch fixes that by catching the error and providing additional information to the Sentry CLI error message. --- packages/sveltekit/src/vite/sourceMaps.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 4ecdf8d40bdd..d72995de6d9e 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -94,7 +94,7 @@ export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOpti // We need to start uploading source maps later than in the original plugin // because SvelteKit is still doing some stuff at closeBundle. - closeBundle: () => { + closeBundle: async () => { if (!upload) { return; } @@ -131,8 +131,21 @@ export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOpti } }); - // @ts-ignore - this hook exists on the plugin! - sentryPlugin.writeBundle(); + try { + // @ts-ignore - this hook exists on the plugin! + await sentryPlugin.writeBundle(); + } catch (_) { + // eslint-disable-next-line no-console + console.warn('[Source Maps Plugin] Failed to upload source maps!'); + // eslint-disable-next-line no-console + console.log( + '[Source Maps Plugin] Please make sure, you specified a valid Sentry auth token, as well as your org and project slugs.', + ); + // eslint-disable-next-line no-console + console.log( + '[Source Maps Plugin] Further information: https://github.com/getsentry/sentry-javascript/blob/develop/packages/sveltekit/README.md#uploading-source-maps', + ); + } }, }; From a4bb082bd4e0472ba0f478c12fdb47e0cec0af32 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 13 Apr 2023 12:56:03 +0200 Subject: [PATCH 48/48] meta: Update CHANGELOG for 7.48.0 Apply suggestions from code review Co-authored-by: Abhijeet Prasad links links links update SvelteKit section add remaining entries --- CHANGELOG.md | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fdb4871c5b3..85b4325081ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,97 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.48.0 + +### Important Changes + + +- **feat(node): Add `AsyncLocalStorage` implementation of `AsyncContextStrategy` (#7800)** + - feat(core): Extend `AsyncContextStrategy` to allow reuse of existing context (#7778) + - feat(core): Make `runWithAsyncContext` public API (#7817) + - feat(core): Add async context abstraction (#7753) + - feat(node): Adds `domain` implementation of `AsyncContextStrategy` (#7767) + - feat(node): Auto-select best `AsyncContextStrategy` for Node.js version (#7804) + - feat(node): Migrate to domains used through `AsyncContextStrategy` (#7779) + +This release switches the SDK to use [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) as the async context isolation mechanism in the SDK for Node 14+. For Node 10 - 13, we continue to use the Node [`domain`](https://nodejs.org/api/domain.html) standard library, since `AsyncLocalStorage` is not supported there. **Preliminary testing showed [a 30% improvement in latency and rps](https://github.com/getsentry/sentry-javascript/issues/7691#issuecomment-1504009089) when making the switch from domains to `AsyncLocalStorage` on Node 16.** + +If you want to manually add async context isolation to your application, you can use the new `runWithAsyncContext` API. + +```js +const requestHandler = (ctx, next) => { + return new Promise((resolve, reject) => { + Sentry.runWithAsyncContext( + async hub => { + hub.configureScope(scope => + scope.addEventProcessor(event => + Sentry.addRequestDataToEvent(event, ctx.request, { + include: { + user: false, + }, + }) + ) + ); + + await next(); + resolve(); + }, + { emitters: [ctx] } + ); + }); +}; +``` + +If you're manually using domains to isolate Sentry data, we strongly recommend switching to this API! + +In addition to exporting `runWithAsyncContext` publicly, the SDK also uses it internally where we previously used domains. + +- **feat(sveltekit): Remove `withSentryViteConfig` (#7789)** + - feat(sveltekit): Remove SDK initialization via dedicated files (#7791) + +This release removes our `withSentryViteConfig` wrapper we previously instructed you to add to your `vite.config.js` file. It is replaced Vite plugins which you simply add to your Vite config, just like the `sveltekit()` Vite plugins. We believe this is a more transparent and Vite/SvelteKit-native way of applying build time modifications. Here's how to use the plugins: + +```js +// vite.config.js +import { sveltekit } from '@sveltejs/kit/vite'; +import { sentrySvelteKit } from '@sentry/sveltekit'; + +export default { + plugins: [sentrySvelteKit(), sveltekit()], + // ... rest of your Vite config +}; +``` + +Take a look at the [`README`](https://github.com/getsentry/sentry-javascript/blob/develop/packages/sveltekit/README.md) for updated instructions! + +Furthermore, with this transition, we removed the possibility to intialize the SDK in dedicated `sentry.(client|server).config.js` files. Please use SvelteKit's [hooks files](https://github.com/getsentry/sentry-javascript/blob/develop/packages/sveltekit/README.md#2-client-side-setup) to initialize the SDK. + +Please note that these are **breaking changes**! We're sorry for the inconvenience but the SvelteKit SDK is still in alpha stage and we want to establish a clean and SvelteKit-friendly API before making the SDK stable. You have been [warned](https://github.com/getsentry/sentry-javascript/blob/eb921275f9c572e72c2348a91cb39fcbb8275b8d/packages/sveltekit/README.md#L20-L24) ;) + +- **feat(sveltekit): Add Sentry Vite Plugin to upload source maps (#7811)** + +This release adds automatic upload of source maps to the SvelteKit SDK. No need to configure anything other than adding our Vite plugins to your SDK. The example above shows you how to do this. + +Please make sure to follow the [`README`](https://github.com/getsentry/sentry-javascript/blob/develop/packages/sveltekit/README.md#uploading-source-maps) to specify your Sentry auth token, as well as org and project slugs. + +### Additional Features and Fixes + +- feat(browser): Export request instrumentation options (#7818) +- feat(core): Add async context abstraction (#7753) +- feat(core): Add DSC to all outgoing envelopes (#7820) +- feat(node): Add checkin envelope types (#7777) +- feat(replay): Add `getReplayId()` method (#7822) +- fix(browser): Adjust `BrowserTransportOptions` to support offline transport options (#7775) +- fix(browser): DOMException SecurityError stacktrace parsing bug (#7821) +- fix(core): Log warning when tracing extensions are missing (#7601) +- fix(core): Only call `applyDebugMetadata` for error events (#7824) +- fix(integrations): Ensure httpclient integration works with Request (#7786) +- fix(node): `reuseExisting` does not need to call bind on domain (#7780) +- fix(node): Fix domain scope inheritance (#7799) +- fix(node): Make `trpcMiddleware` factory synchronous (#7802) +- fix(serverless): Account when transaction undefined (#7829) +- fix(utils): Make xhr instrumentation independent of parallel running SDK versions (#7836) + ## 7.47.0 ### Important Changes