From 0815309d1eec13ea21e3d601fab2188c81694d40 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 21 Oct 2024 11:52:51 +0200 Subject: [PATCH 01/44] fix(nextjs): Await flush in api handlers (#14023) --- packages/nextjs/src/common/wrapApiHandlerWithSentry.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index cef85c320ae0..a6463b0a7791 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -86,7 +86,6 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz return target.apply(thisArg, argArray); }, }); - try { return await wrappingTarget.apply(thisArg, args); } catch (e) { @@ -110,7 +109,9 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz setHttpStatus(span, 500); span.end(); - vercelWaitUntil(flushSafelyWithTimeout()); + // we need to await the flush here to ensure that the error is captured + // as the runtime freezes as soon as the error is thrown below + await flushSafelyWithTimeout(); // 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 From 8b1f130e932212c842f8c3b7450b392735505a5d Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:03:39 +0200 Subject: [PATCH 02/44] fix(solidstart): Use production server for e2e tests (#14033) Adds a small post-build script to copy over deps that are not correctly resolved in our yarn workspace/pnpm e2e test applications (but do work correctly in standalone projects). This allows us to finally use the vinxi production server to run e2e tests and is similar to what the Nuxt SDK has to do. Also allows us to get rid of the double page loads for the first test to avoid hydration errors that were caused by the dev server previously. --- .../test-applications/solidstart/package.json | 18 +++++------------- .../test-applications/solidstart/post_build.sh | 8 ++++++++ .../solidstart/tests/errorboundary.test.ts | 2 -- 3 files changed, 13 insertions(+), 15 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/solidstart/post_build.sh diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index e831a14c1f47..97a7e8812d73 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -3,20 +3,9 @@ "version": "0.0.0", "scripts": { "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", - "clean:build": "pnpx rimraf .vinxi .output", "dev": "NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev", - "build": "vinxi build", - "//": [ - "We are using `vinxi dev` to start the server because `vinxi start` is experimental and ", - "doesn't correctly resolve modules for @sentry/solidstart/solidrouter.", - "This is currently not an issue outside of our repo. See: https://github.com/nksaraf/vinxi/issues/177", - "We run the build command to ensure building succeeds. However, keeping", - "build output around slows down the vite dev server when using `@sentry/vite-plugin` so we clear it out", - "before actually running the tests.", - "Cleaning the build output should be removed once we can use `vinxi start`." - ], - "preview": "pnpm clean:build && HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev", - "start": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start", + "build": "vinxi build && sh ./post_build.sh", + "preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start", "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && npx playwright install && pnpm build", "test:assert": "pnpm test:prod" @@ -41,5 +30,8 @@ "vite": "^5.2.8", "vite-plugin-solid": "^2.10.2", "vitest": "^1.5.0" + }, + "overrides": { + "@vercel/nft": "0.27.4" } } diff --git a/dev-packages/e2e-tests/test-applications/solidstart/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart/post_build.sh new file mode 100644 index 000000000000..6ed67c9afb8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/post_build.sh @@ -0,0 +1,8 @@ +# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved. + +# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules` +# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm +# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is +# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher). +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules +cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts index b6164d541b93..b709760aab94 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts @@ -10,7 +10,6 @@ test('captures an exception', async ({ page }) => { ); }); - await page.goto('/error-boundary'); await page.goto('/error-boundary'); await page.locator('#caughtErrorBtn').click(); const errorEvent = await errorEventPromise; @@ -41,7 +40,6 @@ test('captures a second exception after resetting the boundary', async ({ page } ); }); - await page.goto('/error-boundary'); await page.goto('/error-boundary'); await page.locator('#caughtErrorBtn').click(); const firstErrorEvent = await firstErrorEventPromise; From 022caf9a10fbdbc052035c424f64a91c19dbbde2 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 22 Oct 2024 11:16:56 +0200 Subject: [PATCH 03/44] test(e2e): Create event dumps for all Next.js test apps (#14034) - create dumps for all nextjs apps - wipe old dumps when testing - bump timeouts we experience in other branches too --- .github/workflows/build.yml | 14 +++++++------- .github/workflows/canary.yml | 2 +- .../test-applications/nextjs-13/.gitignore | 1 + .../nextjs-13/start-event-proxy.mjs | 8 ++++++++ .../test-applications/nextjs-14/.gitignore | 1 + .../nextjs-14/start-event-proxy.mjs | 8 ++++++++ .../nextjs-15/start-event-proxy.mjs | 2 +- .../test-applications/nextjs-app-dir/.gitignore | 1 + .../nextjs-app-dir/start-event-proxy.mjs | 8 ++++++++ dev-packages/test-utils/src/event-proxy-server.ts | 5 +++++ 10 files changed, 41 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 299ed59cd220..5fc8f9d55e5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1018,12 +1018,12 @@ jobs: - name: Build E2E app working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 5 + timeout-minutes: 7 run: pnpm ${{ matrix.build-command || 'test:build' }} - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 5 + timeout-minutes: 10 run: pnpm test:assert - name: Upload Playwright Traces @@ -1039,7 +1039,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: playwright-event-dumps-job_e2e_playwright_tests-${{ matrix.test-application }} + name: e2e-test-event-dumps path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/event-dumps overwrite: true retention-days: 7 @@ -1176,12 +1176,12 @@ jobs: - name: Build E2E app working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 5 + timeout-minutes: 7 run: pnpm ${{ matrix.build-command || 'test:build' }} - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 5 + timeout-minutes: 10 run: pnpm ${{ matrix.assert-command || 'test:assert' }} - name: Deploy Astro to Cloudflare @@ -1282,12 +1282,12 @@ jobs: - name: Build E2E app working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 5 + timeout-minutes: 7 run: yarn ${{ matrix.build-command || 'test:build' }} - name: Run E2E test working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 5 + timeout-minutes: 10 run: yarn test:assert job_required_jobs_passed: diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 1f584d2a921c..4b8fff855049 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -140,7 +140,7 @@ jobs: - name: Build E2E app working-directory: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }} - timeout-minutes: 5 + timeout-minutes: 7 run: yarn ${{ matrix.build-command }} - name: Run E2E test diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore index b7a8bf3b3701..68d4c4a9cbf2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore @@ -38,5 +38,6 @@ next-env.d.ts !*.d.ts test-results +event-dumps .vscode diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs index 9983d484bcbc..b45472a5484f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs @@ -1,6 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; import { startEventProxyServer } from '@sentry-internal/test-utils'; +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + startEventProxyServer({ port: 3031, proxyServerName: 'nextjs-13', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/next-13-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-14/.gitignore index e799cc33c4e7..ebdbfc025b6a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/.gitignore +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/.gitignore @@ -43,3 +43,4 @@ next-env.d.ts .vscode test-results +event-dumps diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.mjs index 39babfb19b2a..7e0ebce43981 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/start-event-proxy.mjs @@ -1,6 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; import { startEventProxyServer } from '@sentry-internal/test-utils'; +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + startEventProxyServer({ port: 3031, proxyServerName: 'nextjs-14', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/next-14-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs index 959b40d253e8..d78a76402bbb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs @@ -9,6 +9,6 @@ startEventProxyServer({ proxyServerName: 'nextjs-15', envelopeDumpPath: path.join( process.cwd(), - `event-dumps/next-${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + `event-dumps/next-15-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, ), }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/.gitignore index e799cc33c4e7..ebdbfc025b6a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/.gitignore +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/.gitignore @@ -43,3 +43,4 @@ next-env.d.ts .vscode test-results +event-dumps diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.mjs index 7e8016bf98a5..c243a72b8f07 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/start-event-proxy.mjs @@ -1,6 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; import { startEventProxyServer } from '@sentry-internal/test-utils'; +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + startEventProxyServer({ port: 3031, proxyServerName: 'nextjs-app-dir', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/next-app-dir-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), }); diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 448dd6e34ef0..6508dd9e5a0c 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -171,6 +171,11 @@ export async function startProxyServer( export async function startEventProxyServer(options: EventProxyServerOptions): Promise { if (options.envelopeDumpPath) { await fs.promises.mkdir(path.dirname(path.resolve(options.envelopeDumpPath)), { recursive: true }); + try { + await fs.promises.unlink(path.resolve(options.envelopeDumpPath)); + } catch { + // noop + } } await startProxyServer(options, async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => { From 6e2c0d13f5fd7f5ae03fcccc7f0b84a1225d08ff Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:16:20 +0200 Subject: [PATCH 04/44] fix(nuxt): Server-side setup in readme (#14049) Clarify override for pnpm and fix file name. --- packages/nuxt/README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/nuxt/README.md b/packages/nuxt/README.md index abf7f0d7c594..429fd7487ddc 100644 --- a/packages/nuxt/README.md +++ b/packages/nuxt/README.md @@ -75,7 +75,7 @@ Sentry.init({ ### 4. Server-side setup -Add an `sentry.client.config.ts` file to the root of your project: +Add a `sentry.server.config.ts` file to the root of your project: ```javascript import * as Sentry from '@sentry/nuxt'; @@ -137,16 +137,28 @@ When adding `sentry.server.config.ts`, you might get an error like this: for `@vercel/nft` to fix this. This will add the `hook.mjs` file to your build output ([Nitro issue here](https://github.com/unjs/nitro/issues/2703)). +For `npm`: + ```json "overrides": { "@vercel/nft": "^0.27.4" } ``` -or in `yarn`: +for `yarn`: ```json "resolutions": { "@vercel/nft": "^0.27.4" } ``` + +or for `pnpm`: + +```json +"pnpm": { + "overrides": { + "@vercel/nft": "^0.27.4" + } +} +``` From 7b815bf481051a546e6078157b87b4c10ccd70d4 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:16:53 +0200 Subject: [PATCH 05/44] feat(nuxt): Add Sentry Pinia plugin (#14047) closes https://github.com/getsentry/sentry-javascript/issues/14039 By adding `trackPinia`, the Pinia store is monitored with Sentry. ```js Sentry.init({ dsn: useRuntimeConfig().public.sentry.dsn, trackPinia: true }); ``` or with custom options: ```js Sentry.init({ dsn: useRuntimeConfig().public.sentry.dsn, trackPinia: { actionTransformer: action => `Transformed: ${action}`, }, }); ``` --- .../nuxt-4/app/pages/pinia-cart.vue | 73 +++++++++++++++++++ .../test-applications/nuxt-4/nuxt.config.ts | 2 +- .../test-applications/nuxt-4/package.json | 1 + .../nuxt-4/sentry.client.config.ts | 7 ++ .../test-applications/nuxt-4/stores/cart.ts | 43 +++++++++++ .../nuxt-4/tests/pinia.test.ts | 35 +++++++++ packages/nuxt/src/common/types.ts | 15 +++- .../nuxt/src/runtime/plugins/sentry.client.ts | 25 ++++++- 8 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/pinia-cart.vue create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-4/tests/pinia.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/pinia-cart.vue b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/pinia-cart.vue new file mode 100644 index 000000000000..3d210cf459de --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/app/pages/pinia-cart.vue @@ -0,0 +1,73 @@ + + + + + + + diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts index c00ba0d5d9ed..da988a9ee003 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts @@ -4,7 +4,7 @@ export default defineNuxtConfig({ compatibilityDate: '2024-04-03', imports: { autoImport: false }, - modules: ['@sentry/nuxt/module'], + modules: ['@pinia/nuxt', '@sentry/nuxt/module'], runtimeConfig: { public: { diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json index db56273a7493..178804768e87 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json @@ -14,6 +14,7 @@ "test:assert": "pnpm test" }, "dependencies": { + "@pinia/nuxt": "^0.5.5", "@sentry/nuxt": "latest || *", "nuxt": "^3.13.2" }, diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts index 7547bafa6618..b32effbff3b8 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts @@ -7,4 +7,11 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1.0, trackComponents: true, + trackPinia: { + actionTransformer: action => `Transformed: ${action}`, + stateTransformer: state => ({ + transformed: true, + ...state, + }), + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts new file mode 100644 index 000000000000..cad52916ac25 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/stores/cart.ts @@ -0,0 +1,43 @@ +import { acceptHMRUpdate, defineStore } from '#imports'; + +export const useCartStore = defineStore({ + id: 'cart', + state: () => ({ + rawItems: [] as string[], + }), + getters: { + items: (state): Array<{ name: string; amount: number }> => + state.rawItems.reduce( + (items: any, item: any) => { + const existingItem = items.find((it: any) => it.name === item); + + if (!existingItem) { + items.push({ name: item, amount: 1 }); + } else { + existingItem.amount++; + } + + return items; + }, + [] as Array<{ name: string; amount: number }>, + ), + }, + actions: { + addItem(name: string) { + this.rawItems.push(name); + }, + + removeItem(name: string) { + const i = this.rawItems.lastIndexOf(name); + if (i > -1) this.rawItems.splice(i, 1); + }, + + throwError() { + throw new Error('error'); + }, + }, +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useCartStore, import.meta.hot)); +} diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/pinia.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/pinia.test.ts new file mode 100644 index 000000000000..44b057a29f15 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/pinia.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('sends pinia action breadcrumbs and state context', async ({ page }) => { + await page.goto('/pinia-cart'); + + await page.locator('#item-input').fill('item'); + await page.locator('#item-add').click(); + + const errorPromise = waitForError('nuxt-4', async errorEvent => { + return errorEvent?.exception?.values?.[0].value === 'This is an error'; + }); + + await page.locator('#throw-error').click(); + + const error = await errorPromise; + + expect(error).toBeTruthy(); + expect(error.breadcrumbs?.length).toBeGreaterThan(0); + + const actionBreadcrumb = error.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'action'); + + expect(actionBreadcrumb).toBeDefined(); + expect(actionBreadcrumb?.message).toBe('Transformed: addItem'); + expect(actionBreadcrumb?.level).toBe('info'); + + const stateContext = error.contexts?.state?.state; + + expect(stateContext).toBeDefined(); + expect(stateContext?.type).toBe('pinia'); + expect(stateContext?.value).toEqual({ + transformed: true, + rawItems: ['item'], + }); +}); diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 6ba29752a308..5b714968d3ca 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -1,10 +1,21 @@ import type { init as initNode } from '@sentry/node'; import type { SentryRollupPluginOptions } from '@sentry/rollup-plugin'; import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; -import type { init as initVue } from '@sentry/vue'; +import type { createSentryPiniaPlugin, init as initVue } from '@sentry/vue'; // Omitting 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this) -export type SentryNuxtClientOptions = Omit[0] & object, 'app'>; +export type SentryNuxtClientOptions = Omit[0] & object, 'app'> & { + /** + * Control if an existing Pinia store should be monitored. + * Set this to `true` to track with default options or provide your custom Pinia plugin options. + * + * This only works if "@pinia/nuxt" is added to the `modules` array. + * + * @default false + */ + trackPinia?: true | Parameters[0]; +}; + export type SentryNuxtServerOptions = Omit[0] & object, 'app'>; type SourceMapsOptions = { diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts index b89a2fa87a8d..a8b15b937d53 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.client.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.client.ts @@ -1,5 +1,6 @@ import { getClient } from '@sentry/core'; -import { browserTracingIntegration, vueIntegration } from '@sentry/vue'; +import { consoleSandbox } from '@sentry/utils'; +import { browserTracingIntegration, createSentryPiniaPlugin, vueIntegration } from '@sentry/vue'; import { defineNuxtPlugin } from 'nuxt/app'; import { reportNuxtError } from '../utils'; @@ -34,11 +35,12 @@ export default defineNuxtPlugin({ name: 'sentry-client-integrations', dependsOn: ['sentry-client-config'], async setup(nuxtApp) { + const sentryClient = getClient(); + const clientOptions = sentryClient && sentryClient.getOptions(); + // This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", in which case everything inside // will get tree-shaken away if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { - const sentryClient = getClient(); - if (sentryClient && '$router' in nuxtApp) { sentryClient.addIntegration( browserTracingIntegration({ router: nuxtApp.$router as VueRouter, routeLabel: 'path' }), @@ -46,6 +48,23 @@ export default defineNuxtPlugin({ } } + if (clientOptions && 'trackPinia' in clientOptions && clientOptions.trackPinia) { + if ('$pinia' in nuxtApp) { + (nuxtApp.$pinia as { use: (plugin: unknown) => void }).use( + // `trackPinia` is an object with custom options or `true` (pass `undefined` to use default options) + createSentryPiniaPlugin(clientOptions.trackPinia === true ? undefined : clientOptions.trackPinia), + ); + } else { + clientOptions.debug && + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] You set `trackPinia`, but the Pinia module was not found. Make sure to add `"@pinia/nuxt"` to your modules array.', + ); + }); + } + } + nuxtApp.hook('app:created', vueApp => { const sentryClient = getClient(); From 5b22b267d6fbf767bc6a518dbaee65914e85f437 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 22 Oct 2024 13:49:23 +0200 Subject: [PATCH 06/44] fix(nextjs): Fix matching logic for file convention type for root level components (#14038) --- .github/workflows/build.yml | 25 ++- .../src/config/loaders/wrappingLoader.ts | 4 +- ...malize-e2e-test-dump-transaction-events.js | 150 ++++++++++-------- 3 files changed, 105 insertions(+), 74 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fc8f9d55e5c..8fd537898b28 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1035,14 +1035,19 @@ jobs: overwrite: true retention-days: 7 + - name: Pre-process E2E Test Dumps + run: | + node ./scripts/normalize-e2e-test-dump-transaction-events.js + - name: Upload E2E Test Event Dumps uses: actions/upload-artifact@v4 if: always() with: - name: e2e-test-event-dumps + name: E2E Test Dump (${{ matrix.label || matrix.test-application }}) path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/event-dumps overwrite: true retention-days: 7 + if-no-files-found: ignore - name: Upload test results to Codecov if: cancelled() == false @@ -1083,10 +1088,6 @@ jobs: 'react-send-to-sentry', 'node-express-send-to-sentry', 'debug-id-sourcemaps', - 'nextjs-app-dir', - 'nextjs-13', - 'nextjs-14', - 'nextjs-15', ] build-command: - false @@ -1184,6 +1185,20 @@ jobs: timeout-minutes: 10 run: pnpm ${{ matrix.assert-command || 'test:assert' }} + - name: Pre-process E2E Test Dumps + run: | + node ./scripts/normalize-e2e-test-dump-transaction-events.js + + - name: Upload E2E Test Event Dumps + uses: actions/upload-artifact@v4 + if: always() + with: + name: E2E Test Dump (${{ matrix.label || matrix.test-application }}) + path: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/event-dumps + overwrite: true + retention-days: 7 + if-no-files-found: ignore + - name: Deploy Astro to Cloudflare uses: cloudflare/pages-action@v1 if: matrix.test-application == 'cloudflare-astro' diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index d3c1d62c9330..e298a459d16e 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -182,8 +182,10 @@ export default function wrappingLoader( const componentTypeMatch = path.posix .normalize(path.relative(appDir, this.resourcePath)) + // Replace all backslashes with forward slashes (windows) + .replace(/\\/g, '/') // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor - .match(new RegExp(`/\\/?([^/]+)\\.(?:${pageExtensionRegex})$`)); + .match(new RegExp(`(?:^|/)?([^/]+)\\.(?:${pageExtensionRegex})$`)); if (componentTypeMatch && componentTypeMatch[1]) { let componentType: ServerComponentContext['componentType']; diff --git a/scripts/normalize-e2e-test-dump-transaction-events.js b/scripts/normalize-e2e-test-dump-transaction-events.js index ba06a63fa020..771dcccd8f87 100644 --- a/scripts/normalize-e2e-test-dump-transaction-events.js +++ b/scripts/normalize-e2e-test-dump-transaction-events.js @@ -1,81 +1,95 @@ +// @ts-check /* eslint-disable no-console */ const fs = require('fs'); const path = require('path'); +const glob = require('glob'); + +glob.glob( + '../dev-packages/e2e-tests/test-applications/*/event-dumps/*.dump', + { + cwd: __dirname, + absolute: true, + }, + (err, dumpPaths) => { + if (err) { + throw err; + } -if (process.argv.length < 4) { - throw new Error('Please provide an input and output file path as an argument.'); -} - -const resolvedInputPath = path.resolve(process.argv[2]); -const resolvedOutputPath = path.resolve(process.argv[3]); - -const fileContents = fs.readFileSync(resolvedInputPath, 'utf8'); - -const transactionNodes = []; - -fileContents.split('\n').forEach(serializedEnvelope => { - let envelope; - try { - envelope = JSON.parse(serializedEnvelope); - } catch (e) { - return; - // noop - } + dumpPaths.forEach(dumpPath => { + const fileContents = fs.readFileSync(dumpPath, 'utf8'); - const envelopeItems = envelope[1]; - - envelopeItems.forEach(([envelopeItemHeader, transaction]) => { - if (envelopeItemHeader.type === 'transaction') { - const rootNode = { - runtime: transaction.contexts.runtime?.name, - op: transaction.contexts.trace.op, - name: transaction.transaction, - children: [], - }; - - const spanMap = new Map(); - spanMap.set(transaction.contexts.trace.span_id, rootNode); - - transaction.spans.forEach(span => { - const node = { - op: span.data['sentry.op'], - name: span.description, - parent_span_id: span.parent_span_id, - children: [], - }; - spanMap.set(span.span_id, node); - }); + const transactionNodes = []; - transaction.spans.forEach(span => { - const node = spanMap.get(span.span_id); - if (node && node.parent_span_id) { - const parentNode = spanMap.get(node.parent_span_id); - parentNode.children.push(node); + fileContents.split('\n').forEach(serializedEnvelope => { + let envelope; + try { + envelope = JSON.parse(serializedEnvelope); + } catch (e) { + return; + // noop } - }); - transactionNodes.push(rootNode); - } - }); -}); - -const output = transactionNodes - .sort((a, b) => { - const aSerialized = serializeNode(a); - const bSerialized = serializeNode(b); - if (aSerialized < bSerialized) { - return -1; - } else if (aSerialized > bSerialized) { - return 1; - } else { - return 0; - } - }) - .map(node => buildDeterministicStringFromNode(node)) - .join('\n\n-----------------------\n\n'); + const envelopeItems = envelope[1]; + + envelopeItems.forEach(([envelopeItemHeader, transaction]) => { + if (envelopeItemHeader.type === 'transaction') { + const rootNode = { + runtime: transaction.contexts.runtime?.name, + op: transaction.contexts.trace.op, + name: transaction.transaction, + children: [], + }; + + const spanMap = new Map(); + spanMap.set(transaction.contexts.trace.span_id, rootNode); + + transaction.spans.forEach(span => { + const node = { + op: span.data['sentry.op'], + name: span.description, + parent_span_id: span.parent_span_id, + children: [], + }; + spanMap.set(span.span_id, node); + }); + + transaction.spans.forEach(span => { + const node = spanMap.get(span.span_id); + if (node && node.parent_span_id) { + const parentNode = spanMap.get(node.parent_span_id); + parentNode.children.push(node); + } + }); + + transactionNodes.push(rootNode); + } + }); + }); -fs.writeFileSync(resolvedOutputPath, output, 'utf-8'); + const output = transactionNodes + .sort((a, b) => { + const aSerialized = serializeNode(a); + const bSerialized = serializeNode(b); + if (aSerialized < bSerialized) { + return -1; + } else if (aSerialized > bSerialized) { + return 1; + } else { + return 0; + } + }) + .map(node => buildDeterministicStringFromNode(node)) + .join('\n\n-----------------------\n\n'); + + fs.writeFileSync( + path.join(path.dirname(dumpPath), `normalized-transactions-${path.basename(dumpPath, '.dump')}.txt`), + output, + 'utf-8', + ); + }); + }, +); // ------- utility fns ---------- From d5756ea7de872d5b44acb51d521f0ec4936a2a85 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 22 Oct 2024 17:37:10 +0200 Subject: [PATCH 07/44] deps: Bump bundler plugins and CLI to 2.22.6 and 2.37.0 respectively (#14050) Comes with a few fixes --- .../debug-id-sourcemaps/package.json | 2 +- packages/astro/package.json | 2 +- packages/gatsby/package.json | 2 +- packages/nextjs/package.json | 2 +- packages/nuxt/package.json | 4 +- packages/remix/package.json | 2 +- packages/solidstart/package.json | 2 +- packages/sveltekit/package.json | 2 +- yarn.lock | 146 +++++++++--------- 9 files changed, 82 insertions(+), 82 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index bc61532b71fa..a40e67f4d44c 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -15,7 +15,7 @@ "devDependencies": { "rollup": "^4.0.2", "vitest": "^0.34.6", - "@sentry/rollup-plugin": "2.22.3" + "@sentry/rollup-plugin": "2.22.6" }, "volta": { "extends": "../../package.json" diff --git a/packages/astro/package.json b/packages/astro/package.json index 8b53f185c7e7..480e883f801d 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -61,7 +61,7 @@ "@sentry/node": "8.35.0", "@sentry/types": "8.35.0", "@sentry/utils": "8.35.0", - "@sentry/vite-plugin": "^2.22.3" + "@sentry/vite-plugin": "^2.22.6" }, "devDependencies": { "astro": "^3.5.0", diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index ad231e84fb1a..95de9536cce3 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -49,7 +49,7 @@ "@sentry/react": "8.35.0", "@sentry/types": "8.35.0", "@sentry/utils": "8.35.0", - "@sentry/webpack-plugin": "2.22.3" + "@sentry/webpack-plugin": "2.22.6" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index dbdd89f2b710..8dd13919442a 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -87,7 +87,7 @@ "@sentry/types": "8.35.0", "@sentry/utils": "8.35.0", "@sentry/vercel-edge": "8.35.0", - "@sentry/webpack-plugin": "2.22.3", + "@sentry/webpack-plugin": "2.22.6", "chalk": "3.0.0", "resolve": "1.22.8", "rollup": "3.29.5", diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 9f2bff0bf98c..5936e2d25308 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -47,10 +47,10 @@ "@sentry/core": "8.35.0", "@sentry/node": "8.35.0", "@sentry/opentelemetry": "8.35.0", - "@sentry/rollup-plugin": "2.22.3", + "@sentry/rollup-plugin": "2.22.6", "@sentry/types": "8.35.0", "@sentry/utils": "8.35.0", - "@sentry/vite-plugin": "2.22.3", + "@sentry/vite-plugin": "2.22.6", "@sentry/vue": "8.35.0" }, "devDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index 088998855c6e..b389f2e663db 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@remix-run/router": "1.x", - "@sentry/cli": "^2.35.0", + "@sentry/cli": "^2.37.0", "@sentry/core": "8.35.0", "@sentry/node": "8.35.0", "@sentry/opentelemetry": "8.35.0", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index bdafc4e2738d..dddfd55a9942 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -73,7 +73,7 @@ "@sentry/solid": "8.35.0", "@sentry/types": "8.35.0", "@sentry/utils": "8.35.0", - "@sentry/vite-plugin": "2.22.3" + "@sentry/vite-plugin": "2.22.6" }, "devDependencies": { "@solidjs/router": "^0.13.4", diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 51c04c80ebd5..fd614c8d00ae 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -46,7 +46,7 @@ "@sentry/svelte": "8.35.0", "@sentry/types": "8.35.0", "@sentry/utils": "8.35.0", - "@sentry/vite-plugin": "2.22.3", + "@sentry/vite-plugin": "2.22.6", "magic-string": "0.30.7", "magicast": "0.2.8", "sorcery": "1.0.0" diff --git a/yarn.lock b/yarn.lock index 48991d296050..cdd55b36d9a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8425,64 +8425,64 @@ fflate "^0.4.4" mitt "^3.0.0" -"@sentry/babel-plugin-component-annotate@2.22.3": - version "2.22.3" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.3.tgz#de4970d51a54ef52b21f0d6ec49bd06bf37753c1" - integrity sha512-OlHA+i+vnQHRIdry4glpiS/xTOtgjmpXOt6IBOUqynx5Jd/iK1+fj+t8CckqOx9wRacO/hru2wfW/jFq0iViLg== +"@sentry/babel-plugin-component-annotate@2.22.6": + version "2.22.6" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.6.tgz#829d6caf2c95c1c46108336de4e1049e6521435e" + integrity sha512-V2g1Y1I5eSe7dtUVMBvAJr8BaLRr4CLrgNgtPaZyMT4Rnps82SrZ5zqmEkLXPumlXhLUWR6qzoMNN2u+RXVXfQ== -"@sentry/bundler-plugin-core@2.22.3": - version "2.22.3" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.3.tgz#f8c0a25321216ae9777749c1a4b9d982ae1ec2e1" - integrity sha512-DeoUl0WffcqZZRl5Wy9aHvX4WfZbbWt0QbJ7NJrcEViq+dRAI2FQTYECFLwdZi5Gtb3oyqZICO+P7k8wDnzsjQ== +"@sentry/bundler-plugin-core@2.22.6": + version "2.22.6" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.6.tgz#a1ea1fd43700a3ece9e7db016997e79a2782b87d" + integrity sha512-1esQdgSUCww9XAntO4pr7uAM5cfGhLsgTK9MEwAKNfvpMYJi9NUTYa3A7AZmdA8V6107Lo4OD7peIPrDRbaDCg== dependencies: "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.22.3" - "@sentry/cli" "^2.33.1" + "@sentry/babel-plugin-component-annotate" "2.22.6" + "@sentry/cli" "^2.36.1" dotenv "^16.3.1" find-up "^5.0.0" glob "^9.3.2" magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.35.0": - version "2.35.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.35.0.tgz#4bc9a07690f0de75d930ba47f4655f6465191768" - integrity sha512-dRtDaASkB1ncSbCLMIL8bxki4dPMimSdYz74XOUJ5IvDVVzEInEO7PqvyOj/cyafB+1FSNudaZ90ZRvsNN1Maw== - -"@sentry/cli-linux-arm64@2.35.0": - version "2.35.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.35.0.tgz#bad8a45b81d2b317f702991783a503f566b2294e" - integrity sha512-NpyVz2lQWWkMa9GZkt0m4cA/wsgYnWOE6Z+4ePUGjbOIG3Ws9DLaHjYxUUYI79kxfbVCp7wLo1S6kOkj+M1Dlw== - -"@sentry/cli-linux-arm@2.35.0": - version "2.35.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.35.0.tgz#dacfc219876f5dce3d8c65dab7128ea3e493f561" - integrity sha512-zNL+/HnepZ4/MkIS8wfoUQxSa+k6r0DSSdX1TpDH5436u+3LB5rfCTBfZ624DWHKMoXX+1dI+rWSi+zL8QFMsg== - -"@sentry/cli-linux-i686@2.35.0": - version "2.35.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.35.0.tgz#d0e6401b60b0a4b6c3578998995ba6cb31c1bf20" - integrity sha512-vIYwZVqx+kYZdPsenIm+UqjSCKe9Q2Aof6kzrzW0DPR1WyqIWbWG4NbiugiPTiuA1dLjUjYpGP8wyIqb8hxv4w== - -"@sentry/cli-linux-x64@2.35.0": - version "2.35.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.35.0.tgz#a1e8e7bff960ed8916b4cc9c0ef75a057e30f989" - integrity sha512-7Wy5QNt6wZ8EaxEbHqP0DEiyUcXRVItRt9jzhpa2nCaawL+fwDOQCjUkHGsdIC+y14UqA+er9CaPCSp8sA6Vaw== - -"@sentry/cli-win32-i686@2.35.0": - version "2.35.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.35.0.tgz#c1b090f7c740c5b22d1019ca48a84f58cd4b2670" - integrity sha512-XDcBUtO5A9elH+xgFNs6NBjkMBnz0sZLo5DU7LE77qKXULnlLeJ63eZD1ukQIRPvxEDsIEPOllRweLuAlUMDtw== - -"@sentry/cli-win32-x64@2.35.0": - version "2.35.0" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.35.0.tgz#f592af483da239be846e556f57f5c6fc7dc1dc54" - integrity sha512-86yHO+31qAXUeAdSCH7MNodn/cn/9xd2fTrxjtfNZWO0pX0jW91sCdomfBxhu5b977cyV9gNcqeBbc9XSIKIIA== - -"@sentry/cli@^2.33.1", "@sentry/cli@^2.35.0": - version "2.35.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.35.0.tgz#5514eb8f5808bc70707ffa186156f8ff7ca5971e" - integrity sha512-7sHRJViEgHTfEXf+HD1Fb2cwmnxlILmb2NNxghP2vvrgC2PhuwuJU7AX4zg7HjJgxH9HBmnn4AJskDujaJ/6cQ== +"@sentry/cli-darwin@2.37.0": + version "2.37.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.37.0.tgz#9c890c68abf30ceaad27826212a0963b125b8bbf" + integrity sha512-CsusyMvO0eCPSN7H+sKHXS1pf637PWbS4rZak/7giz/z31/6qiXmeMlcL3f9lLZKtFPJmXVFO9uprn1wbBVF8A== + +"@sentry/cli-linux-arm64@2.37.0": + version "2.37.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.37.0.tgz#2070155bade6d72d6b706807c6f365c65f9b82ea" + integrity sha512-2vzUWHLZ3Ct5gpcIlfd/2Qsha+y9M8LXvbZE26VxzYrIkRoLAWcnClBv8m4XsHLMURYvz3J9QSZHMZHSO7kAzw== + +"@sentry/cli-linux-arm@2.37.0": + version "2.37.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.37.0.tgz#a08c2133e8e2566074fd6fe4f68e9ffd0c85664a" + integrity sha512-Dz0qH4Yt+gGUgoVsqVt72oDj4VQynRF1QB1/Sr8g76Vbi+WxWZmUh0iFwivYVwWxdQGu/OQrE0tx946HToCRyA== + +"@sentry/cli-linux-i686@2.37.0": + version "2.37.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.37.0.tgz#53fff0e7f232b656b0ee3413b66006ee724a4abf" + integrity sha512-MHRLGs4t/CQE1pG+mZBQixyWL6xDZfNalCjO8GMcTTbZFm44S3XRHfYJZNVCgdtnUP7b6OHGcu1v3SWE10LcwQ== + +"@sentry/cli-linux-x64@2.37.0": + version "2.37.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.37.0.tgz#2fbaf51ef3884bd6561c987f01ac98f544457150" + integrity sha512-k76ClefKZaDNJZU/H3mGeR8uAzAGPzDRG/A7grzKfBeyhP3JW09L7Nz9IQcSjCK+xr399qLhM2HFCaPWQ6dlMw== + +"@sentry/cli-win32-i686@2.37.0": + version "2.37.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.37.0.tgz#fa195664da27ce8c40fdb6db1bf1d125cdf587d9" + integrity sha512-FFyi5RNYQQkEg4GkP2f3BJcgQn0F4fjFDMiWkjCkftNPXQG+HFUEtrGsWr6mnHPdFouwbYg3tEPUWNxAoypvTw== + +"@sentry/cli-win32-x64@2.37.0": + version "2.37.0" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.37.0.tgz#84fa4d070b8a4a115c46ab38f42d29580143fd26" + integrity sha512-nSMj4OcfQmyL+Tu/jWCJwhKCXFsCZW1MUk6wjjQlRt9SDLfgeapaMlK1ZvT1eZv5ZH6bj3qJfefwj4U8160uOA== + +"@sentry/cli@^2.36.1", "@sentry/cli@^2.37.0": + version "2.37.0" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.37.0.tgz#dd01e933cf1caed7d7b6abab5a96044fe1c9c7a1" + integrity sha512-fM3V4gZRJR/s8lafc3O07hhOYRnvkySdPkvL/0e0XW0r+xRwqIAgQ5ECbsZO16A5weUiXVSf03ztDL1FcmbJCQ== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -8490,36 +8490,36 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.35.0" - "@sentry/cli-linux-arm" "2.35.0" - "@sentry/cli-linux-arm64" "2.35.0" - "@sentry/cli-linux-i686" "2.35.0" - "@sentry/cli-linux-x64" "2.35.0" - "@sentry/cli-win32-i686" "2.35.0" - "@sentry/cli-win32-x64" "2.35.0" - -"@sentry/rollup-plugin@2.22.3": - version "2.22.3" - resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-2.22.3.tgz#18ab4b7903ee723bee4cf789b38bb3febb05faae" - integrity sha512-I1UsnYzZm5W7/Pyah2yxuMRxmzgf5iDKoptFfMaerpRO5oBhFO3tMnKSLAlYMvuXKRoYkInNv6ckkUcSOF6jig== - dependencies: - "@sentry/bundler-plugin-core" "2.22.3" + "@sentry/cli-darwin" "2.37.0" + "@sentry/cli-linux-arm" "2.37.0" + "@sentry/cli-linux-arm64" "2.37.0" + "@sentry/cli-linux-i686" "2.37.0" + "@sentry/cli-linux-x64" "2.37.0" + "@sentry/cli-win32-i686" "2.37.0" + "@sentry/cli-win32-x64" "2.37.0" + +"@sentry/rollup-plugin@2.22.6": + version "2.22.6" + resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-2.22.6.tgz#74e9ab69729ee024a497b21b66be3b1992e786d5" + integrity sha512-UmTT4kLytwDJkmfwFCOXIgS6pBi2+ZeM/zU/xJ2R4jE0+s1VvYP3DBGYsUhp4Uf/zDanCawpKJqYZMZtq9EyMA== + dependencies: + "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" -"@sentry/vite-plugin@2.22.3", "@sentry/vite-plugin@^2.22.3": - version "2.22.3" - resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.22.3.tgz#b52802412b6f3d8e3e56742afc9624d9babae5b6" - integrity sha512-+5bsLFRKOZzBp68XigoNE1pJ3tJ4gt2jXluApu54ui0N/yjfqGQ7LQTD7nL4tmJvB5Agwi0e7M7+fcxe9gSgBA== +"@sentry/vite-plugin@2.22.6", "@sentry/vite-plugin@^2.22.6": + version "2.22.6" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.22.6.tgz#d08a1ede05f137636d5b3c61845d24c0114f0d76" + integrity sha512-zIieP1VLWQb3wUjFJlwOAoaaJygJhXeUoGd0e/Ha2RLb2eW2S+4gjf6y6NqyY71tZ74LYVZKg/4prB6FAZSMXQ== dependencies: - "@sentry/bundler-plugin-core" "2.22.3" + "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" -"@sentry/webpack-plugin@2.22.3": - version "2.22.3" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.3.tgz#a9eeb4689c062eb6dc50671c09f06ec6875b9b02" - integrity sha512-Sq1S6bL3nuoTP5typkj+HPjQ13dqftIE8kACAq4tKkXOpWO9bf6HtqcruEQCxMekbWDTdljsrknQ17ZBx2q66Q== +"@sentry/webpack-plugin@2.22.6": + version "2.22.6" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.6.tgz#8c9d27d5cd89153a5b6e08cc9dcb3048b122ffbc" + integrity sha512-BiLhAzQYAz/9kCXKj2LeUKWf/9GBVn2dD0DeYK89s+sjDEaxjbcLBBiLlLrzT7eC9QVj2tUZRKOi6puCfc8ysw== dependencies: - "@sentry/bundler-plugin-core" "2.22.3" + "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" uuid "^9.0.0" From eb17d628a500fddba0f31a1a49eb66072c2aa61a Mon Sep 17 00:00:00 2001 From: Matthew T <20070360+mdtro@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:40:34 -0500 Subject: [PATCH 08/44] meta: Allow old Next.js version for dependency review (#14012) --- .github/dependency-review-config.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependency-review-config.yml diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml new file mode 100644 index 000000000000..99deb0e2677c --- /dev/null +++ b/.github/dependency-review-config.yml @@ -0,0 +1,7 @@ +fail-on-severity: 'high' +allow-ghsas: + # dependency review does not allow specific file exclusions + # we use an older version of NextJS in our tests and thus need to + # exclude this + # once our minimum supported version is over 14.1.1 this can be removed + - GHSA-fr5h-rqp8-mj6g From 6cee1bf33e4782d050778e45094d743ffb6fec38 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 23 Oct 2024 11:56:39 +0200 Subject: [PATCH 09/44] test(e2e): Add Next.js Turbopack E2E tests (#14031) --- .github/workflows/build.yml | 7 +++ .github/workflows/canary.yml | 6 +++ .../test-applications/nextjs-turbo/.gitignore | 46 +++++++++++++++++ .../test-applications/nextjs-turbo/.npmrc | 2 + .../app/[param]/rsc-page-error/page.tsx | 9 ++++ .../nextjs-turbo/app/global-error.tsx | 27 ++++++++++ .../nextjs-turbo/app/layout.tsx | 7 +++ .../nextjs-turbo/globals.d.ts | 4 ++ .../nextjs-turbo/instrumentation.ts | 13 +++++ .../nextjs-turbo/next-env.d.ts | 5 ++ .../nextjs-turbo/next.config.js | 12 +++++ .../nextjs-turbo/package.json | 49 +++++++++++++++++++ .../nextjs-turbo/playwright.config.mjs | 19 +++++++ .../nextjs-turbo/sentry.client.config.ts | 9 ++++ .../nextjs-turbo/sentry.edge.config.ts | 13 +++++ .../nextjs-turbo/sentry.server.config.ts | 13 +++++ .../nextjs-turbo/start-event-proxy.mjs | 14 ++++++ .../nextjs-turbo/tests/rsc-error.test.ts | 14 ++++++ .../nextjs-turbo/tsconfig.json | 25 ++++++++++ 19 files changed, 294 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8fd537898b28..73e5a1c6ee3c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -904,6 +904,7 @@ jobs: 'nextjs-13', 'nextjs-14', 'nextjs-15', + 'nextjs-turbo', 'nextjs-t3', 'react-17', 'react-19', @@ -1126,6 +1127,12 @@ jobs: - test-application: 'nextjs-15' build-command: 'test:build-latest' label: 'nextjs-15 (latest)' + - test-application: 'nextjs-turbo' + build-command: 'test:build-canary' + label: 'nextjs-turbo (canary)' + - test-application: 'nextjs-turbo' + build-command: 'test:build-latest' + label: 'nextjs-turbo (latest)' steps: - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 4b8fff855049..b964e6b3d1b0 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -96,6 +96,12 @@ jobs: - test-application: 'nextjs-15' build-command: 'test:build-latest' label: 'nextjs-15 (latest)' + - test-application: 'nextjs-turbo' + build-command: 'test:build-canary' + label: 'nextjs-turbo (canary)' + - test-application: 'nextjs-turbo' + build-command: 'test:build-latest' + label: 'nextjs-turbo (latest)' - test-application: 'react-create-hash-router' build-command: 'test:build-canary' label: 'react-create-hash-router (canary)' diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore new file mode 100644 index 000000000000..ebdbfc025b6a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +!*.d.ts + +# Sentry +.sentryclirc + +.vscode + +test-results +event-dumps diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx new file mode 100644 index 000000000000..a6ae11918445 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx @@ -0,0 +1,9 @@ +export const dynamic = 'force-dynamic'; + +export default function Page() { + if (Math.random() > -1) { + throw new Error('page rsc render error'); + } + + return null; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx new file mode 100644 index 000000000000..912ad3606a61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx @@ -0,0 +1,27 @@ +'use client'; + +import * as Sentry from '@sentry/nextjs'; +import NextError from 'next/error'; +import { useEffect } from 'react'; + +export default function GlobalError({ + error, +}: { + error: Error & { digest?: string }; +}) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + {/* `NextError` is the default Next.js error page component. Its type + definition requires a `statusCode` prop. However, since the App Router + does not expose status codes for errors, we simply pass 0 to render a + generic error message. */} + + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx new file mode 100644 index 000000000000..c8f9cee0b787 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts new file mode 100644 index 000000000000..109dbcd55648 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts @@ -0,0 +1,4 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts new file mode 100644 index 000000000000..964f937c439a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('./sentry.server.config'); + } + + if (process.env.NEXT_RUNTIME === 'edge') { + await import('./sentry.edge.config'); + } +} + +export const onRequestError = Sentry.captureRequestError; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts new file mode 100644 index 000000000000..40c3d68096c2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js new file mode 100644 index 000000000000..e09e64bac6a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js @@ -0,0 +1,12 @@ +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + turbo: {}, // Enables Turbopack for builds + }, +}; + +module.exports = withSentryConfig(nextConfig, { + silent: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json new file mode 100644 index 000000000000..900e0b5b2efc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -0,0 +1,49 @@ +{ + "name": "create-next-app", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@canary && pnpm add react-dom@canary && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && pnpm add react@rc && pnpm add react-dom@rc && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod && pnpm test:dev" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@types/node": "18.11.17", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9", + "next": "15.0.0", + "react": "rc", + "react-dom": "rc", + "typescript": "4.9.5" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry-internal/feedback": "latest || *", + "@sentry-internal/replay-canvas": "latest || *", + "@sentry-internal/browser-utils": "latest || *", + "@sentry/browser": "latest || *", + "@sentry/core": "latest || *", + "@sentry/nextjs": "latest || *", + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@sentry/react": "latest || *", + "@sentry-internal/replay": "latest || *", + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *", + "@sentry/vercel-edge": "latest || *", + "import-in-the-middle": "1.11.2" + }, + "overrides": { + "import-in-the-middle": "1.11.2" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs new file mode 100644 index 000000000000..a62bec62a5c8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs @@ -0,0 +1,19 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const config = getPlaywrightConfig( + { + startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030 --turbo' : 'pnpm next start -p 3030', + port: 3030, + }, + { + // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize + workers: '100%', + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts new file mode 100644 index 000000000000..067d2ead0b8b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, + transportOptions: { + // We are doing a lot of events at once in this test + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts new file mode 100644 index 000000000000..067d2ead0b8b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, + transportOptions: { + // We are doing a lot of events at once in this test + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs new file mode 100644 index 000000000000..2773cf8fa977 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs @@ -0,0 +1,14 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'))); + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-turbo', + envelopeDumpPath: path.join( + process.cwd(), + `event-dumps/nextjs-turbo-${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`, + ), +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts new file mode 100644 index 000000000000..604faae7ea59 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Should capture errors from server components', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-turbo', errorEvent => { + return !!errorEvent?.exception?.values?.some(value => value.value === 'page rsc render error'); + }); + + await page.goto(`/123/rsc-page-error`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json new file mode 100644 index 000000000000..ef9e351d7a7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ], + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"], + "exclude": ["node_modules", "playwright.config.ts"] +} From 8a68fa90f5182d67c4e33e62472f146ae37a96a4 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:31:18 +0200 Subject: [PATCH 10/44] fix(nuxt): Only wrap `.mjs` entry files in rollup (#14060) fixes https://github.com/getsentry/sentry-javascript/issues/14057 `@nuxt/content` adds entry files to rollup. This fix adds a check for `.mjs` to ignore other files in the rollup plugin. --- packages/nuxt/src/vite/addServerConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 4c12c6fd7dc2..80efc1da4b2e 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -120,7 +120,7 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug return { id: source, moduleSideEffects: true, external: true }; } - if (options.isEntry && !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { + if (options.isEntry && source.includes('.mjs') && !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { const resolution = await this.resolve(source, importer, options); // If it cannot be resolved or is external, just return it so that Rollup can display an error From c56d84de956981e959bcab672b8cce80217dc8f8 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 23 Oct 2024 15:02:39 +0200 Subject: [PATCH 11/44] test(node): Fix test runner (#14019) The changes in #13280 did not pass the error through the `done` callback which means test failures are not detected when using `createTestServer`. Some tests had to be fixed because this change showed that they were in fact failing! --- .../tracing/requests/fetch-breadcrumbs/scenario.ts | 1 + .../suites/tracing/requests/http-no-tracing/scenario.ts | 9 +++++++++ .../suites/tracing/requests/http-no-tracing/test.ts | 8 ++++---- dev-packages/node-integration-tests/utils/server.ts | 4 ++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.ts index eff91b2cd3e4..8d704042a8ce 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.ts @@ -7,6 +7,7 @@ Sentry.init({ tracePropagationTargets: [/\/v0/, 'v1'], integrations: [], transport: loggingTransport, + tracesSampleRate: 0.0, // Ensure this gets a correct hint beforeBreadcrumb(breadcrumb, hint) { breadcrumb.data = breadcrumb.data || {}; diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/scenario.ts index 1eb618d97dcc..3ae59e5ee6b7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/scenario.ts @@ -7,11 +7,20 @@ Sentry.init({ tracePropagationTargets: [/\/v0/, 'v1'], integrations: [], transport: loggingTransport, + // Ensure this gets a correct hint + beforeBreadcrumb(breadcrumb, hint) { + breadcrumb.data = breadcrumb.data || {}; + const req = hint?.request as { path?: string }; + breadcrumb.data.ADDED_PATH = req?.path; + return breadcrumb; + }, }); import * as http from 'http'; async function run(): Promise { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); await makeHttpGet(`${process.env.SERVER_URL}/api/v1`); await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); diff --git a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/test.ts b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/test.ts index e65278c3efd5..3ab1090806cb 100644 --- a/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/requests/http-no-tracing/test.ts @@ -48,7 +48,7 @@ test('outgoing http requests are correctly instrumented with tracing disabled', data: { 'http.method': 'GET', url: `${SERVER_URL}/api/v0`, - status_code: 404, + status_code: 200, ADDED_PATH: '/api/v0', }, timestamp: expect.any(Number), @@ -59,7 +59,7 @@ test('outgoing http requests are correctly instrumented with tracing disabled', data: { 'http.method': 'GET', url: `${SERVER_URL}/api/v1`, - status_code: 404, + status_code: 200, ADDED_PATH: '/api/v1', }, timestamp: expect.any(Number), @@ -70,7 +70,7 @@ test('outgoing http requests are correctly instrumented with tracing disabled', data: { 'http.method': 'GET', url: `${SERVER_URL}/api/v2`, - status_code: 404, + status_code: 200, ADDED_PATH: '/api/v2', }, timestamp: expect.any(Number), @@ -81,7 +81,7 @@ test('outgoing http requests are correctly instrumented with tracing disabled', data: { 'http.method': 'GET', url: `${SERVER_URL}/api/v3`, - status_code: 404, + status_code: 200, ADDED_PATH: '/api/v3', }, timestamp: expect.any(Number), diff --git a/dev-packages/node-integration-tests/utils/server.ts b/dev-packages/node-integration-tests/utils/server.ts index 71a7adf9798f..5f9afeeb556d 100644 --- a/dev-packages/node-integration-tests/utils/server.ts +++ b/dev-packages/node-integration-tests/utils/server.ts @@ -70,9 +70,9 @@ export function createTestServer(done: (error?: unknown) => void) { const address = server.address() as AddressInfo; resolve([ `http://localhost:${address.port}`, - () => { + (error?: unknown) => { server.close(); - done(); + done(error); }, ]); }); From fe639f4caaa39fdfee8edc1ec8be2336ce69ffaa Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 24 Oct 2024 15:19:20 +0200 Subject: [PATCH 12/44] feat(nextjs/vercel-edge/cloudflare): Switch to OTEL for performance monitoring (#13889) Co-authored-by: Charly Gomez --- .github/workflows/build.yml | 2 + .size-limit.js | 2 +- dev-packages/e2e-tests/publish-packages.ts | 10 +- .../test-applications/nextjs-13/package.json | 2 +- .../customPageExtension.page.tsx | 0 .../error-getServerSideProps.tsx | 0 .../tests/client/pages-dir-pageload.test.ts | 35 +++ .../tests/isomorphic/getInitialProps.test.ts | 4 +- .../isomorphic/getServerSideProps.test.ts | 4 +- .../server/excluded-api-endpoints.test.ts | 24 +- .../tests/server/getServerSideProps.test.ts | 25 +- .../nextjs-app-dir/app/layout.tsx | 36 ++- .../nextjs-app-dir/middleware.ts | 2 +- .../pages/api/async-context-edge-endpoint.ts | 19 +- .../api/endpoint-behind-faulty-middleware.ts | 9 + .../tests/async-context-edge.test.ts | 10 +- ...client-app-routing-instrumentation.test.ts | 8 +- .../nextjs-app-dir/tests/edge-route.test.ts | 48 ++-- .../nextjs-app-dir/tests/edge.test.ts | 23 +- .../nextjs-app-dir/tests/middleware.test.ts | 47 ++-- .../tests/pages-ssr-errors.test.ts | 4 +- .../tests/route-handlers.test.ts | 23 +- .../tests/server-components.test.ts | 4 +- .../nextjs-app-dir/tests/transactions.test.ts | 28 +++ dev-packages/rollup-utils/npmHelpers.mjs | 3 +- packages/nextjs/package.json | 1 + packages/nextjs/src/client/index.ts | 2 +- .../pagesRouterRoutingInstrumentation.ts | 12 +- packages/nextjs/src/common/index.ts | 16 +- .../_error.ts | 2 +- .../wrapApiHandlerWithSentry.ts | 13 +- .../wrapApiHandlerWithSentryVercelCrons.ts | 4 +- .../wrapAppGetInitialPropsWithSentry.ts | 4 +- .../wrapDocumentGetInitialPropsWithSentry.ts | 4 +- .../wrapErrorGetInitialPropsWithSentry.ts | 4 +- .../wrapGetInitialPropsWithSentry.ts | 4 +- .../wrapGetServerSidePropsWithSentry.ts | 4 +- .../wrapGetStaticPropsWithSentry.ts | 11 +- .../wrapPageComponentWithSentry.ts | 91 +++++++ .../span-attributes-with-logic-attached.ts | 8 + .../src/common/utils/edgeWrapperUtils.ts | 91 ------- .../nextjs/src/common/utils/tracingUtils.ts | 19 +- .../nextjs/src/common/utils/wrapperUtils.ts | 163 ++---------- .../common/withServerActionInstrumentation.ts | 157 ++++++------ .../wrapGenerationFunctionWithSentry.ts | 13 +- .../src/common/wrapMiddlewareWithSentry.ts | 85 ++++++- .../src/common/wrapPageComponentWithSentry.ts | 96 ------- .../src/common/wrapRouteHandlerWithSentry.ts | 147 +++++------ .../common/wrapServerComponentWithSentry.ts | 13 +- packages/nextjs/src/config/types.ts | 7 +- packages/nextjs/src/edge/index.ts | 55 +++- .../src/edge/wrapApiHandlerWithSentry.ts | 94 ++++++- packages/nextjs/src/server/index.ts | 194 +++++++++------ packages/nextjs/test/config/mocks.ts | 8 +- packages/nextjs/test/config/wrappers.test.ts | 65 +++-- .../nextjs/test/edge/edgeWrapperUtils.test.ts | 109 -------- .../nextjs/test/edge/withSentryAPI.test.ts | 42 +--- packages/opentelemetry/src/spanExporter.ts | 1 - .../src/utils/parseSpanDescription.ts | 13 - .../test/utils/parseSpanDescription.test.ts | 21 -- packages/vercel-edge/package.json | 6 + packages/vercel-edge/rollup.npm.config.mjs | 61 ++++- packages/vercel-edge/src/async.ts | 87 ------- packages/vercel-edge/src/client.ts | 20 ++ packages/vercel-edge/src/sdk.ts | 140 ++++++++++- packages/vercel-edge/src/types.ts | 21 ++ .../abstract-async-hooks-context-manager.ts | 234 ++++++++++++++++++ .../async-local-storage-context-manager.ts | 89 +++++++ packages/vercel-edge/test/async.test.ts | 43 ++-- packages/vercel-edge/test/sdk.test.ts | 13 - .../{vite.config.ts => vite.config.mts} | 0 71 files changed, 1565 insertions(+), 1094 deletions(-) rename dev-packages/e2e-tests/test-applications/nextjs-13/pages/{ => [param]}/customPageExtension.page.tsx (100%) rename dev-packages/e2e-tests/test-applications/nextjs-13/pages/{ => [param]}/error-getServerSideProps.tsx (100%) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts rename packages/nextjs/src/common/{ => pages-router-instrumentation}/_error.ts (97%) rename packages/nextjs/src/common/{ => pages-router-instrumentation}/wrapApiHandlerWithSentry.ts (93%) rename packages/nextjs/src/common/{ => pages-router-instrumentation}/wrapApiHandlerWithSentryVercelCrons.ts (96%) rename packages/nextjs/src/common/{ => pages-router-instrumentation}/wrapAppGetInitialPropsWithSentry.ts (97%) rename packages/nextjs/src/common/{ => pages-router-instrumentation}/wrapDocumentGetInitialPropsWithSentry.ts (96%) rename packages/nextjs/src/common/{ => pages-router-instrumentation}/wrapErrorGetInitialPropsWithSentry.ts (97%) rename packages/nextjs/src/common/{ => pages-router-instrumentation}/wrapGetInitialPropsWithSentry.ts (96%) rename packages/nextjs/src/common/{ => pages-router-instrumentation}/wrapGetServerSidePropsWithSentry.ts (96%) rename packages/nextjs/src/common/{ => pages-router-instrumentation}/wrapGetStaticPropsWithSentry.ts (82%) create mode 100644 packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts create mode 100644 packages/nextjs/src/common/span-attributes-with-logic-attached.ts delete mode 100644 packages/nextjs/src/common/utils/edgeWrapperUtils.ts delete mode 100644 packages/nextjs/src/common/wrapPageComponentWithSentry.ts delete mode 100644 packages/nextjs/test/edge/edgeWrapperUtils.test.ts delete mode 100644 packages/vercel-edge/src/async.ts create mode 100644 packages/vercel-edge/src/vendored/abstract-async-hooks-context-manager.ts create mode 100644 packages/vercel-edge/src/vendored/async-local-storage-context-manager.ts delete mode 100644 packages/vercel-edge/test/sdk.test.ts rename packages/vercel-edge/{vite.config.ts => vite.config.mts} (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73e5a1c6ee3c..511299fd3e3e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1037,6 +1037,7 @@ jobs: retention-days: 7 - name: Pre-process E2E Test Dumps + if: always() run: | node ./scripts/normalize-e2e-test-dump-transaction-events.js @@ -1193,6 +1194,7 @@ jobs: run: pnpm ${{ matrix.assert-command || 'test:assert' }} - name: Pre-process E2E Test Dumps + if: always() run: | node ./scripts/normalize-e2e-test-dump-transaction-events.js diff --git a/.size-limit.js b/.size-limit.js index bdfe8a4397e2..75545fd89194 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -224,7 +224,7 @@ module.exports = [ import: createImport('init'), ignore: ['next/router', 'next/constants'], gzip: true, - limit: '39 KB', + limit: '39.1 KB', }, // SvelteKit SDK (ESM) { diff --git a/dev-packages/e2e-tests/publish-packages.ts b/dev-packages/e2e-tests/publish-packages.ts index 408d046977a2..4f2cc4056826 100644 --- a/dev-packages/e2e-tests/publish-packages.ts +++ b/dev-packages/e2e-tests/publish-packages.ts @@ -12,6 +12,8 @@ const packageTarballPaths = glob.sync('packages/*/sentry-*.tgz', { // Publish built packages to the fake registry packageTarballPaths.forEach(tarballPath => { + // eslint-disable-next-line no-console + console.log(`Publishing tarball ${tarballPath} ...`); // `--userconfig` flag needs to be before `publish` childProcess.exec( `npm --userconfig ${__dirname}/test-registry.npmrc publish ${tarballPath}`, @@ -19,14 +21,10 @@ packageTarballPaths.forEach(tarballPath => { cwd: repositoryRoot, // Can't use __dirname here because npm would try to publish `@sentry-internal/e2e-tests` encoding: 'utf8', }, - (err, stdout, stderr) => { - // eslint-disable-next-line no-console - console.log(stdout); - // eslint-disable-next-line no-console - console.log(stderr); + err => { if (err) { // eslint-disable-next-line no-console - console.error(err); + console.error(`Error publishing tarball ${tarballPath}`, err); process.exit(1); } }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json index 5be9ecbfc32c..3e7a0ac88266 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json @@ -17,7 +17,7 @@ "@types/node": "18.11.17", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "13.2.0", + "next": "13.5.7", "react": "18.2.0", "react-dom": "18.2.0", "typescript": "4.9.5" diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/customPageExtension.page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/customPageExtension.page.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-13/pages/customPageExtension.page.tsx rename to dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/customPageExtension.page.tsx diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/error-getServerSideProps.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/error-getServerSideProps.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-13/pages/error-getServerSideProps.tsx rename to dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/error-getServerSideProps.tsx diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts index 8c74b2c99427..af59b41c2908 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts @@ -46,3 +46,38 @@ test('should create a pageload transaction when the `pages` directory is used', type: 'transaction', }); }); + +test('should create a pageload transaction with correct name when an error occurs in getServerSideProps', async ({ + page, +}) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/error-getServerSideProps' && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/something/error-getServerSideProps`, { waitUntil: 'networkidle' }); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation', + 'sentry.source': 'route', + }, + op: 'pageload', + origin: 'auto.pageload.nextjs.pages_router_instrumentation', + }, + }, + transaction: '/[param]/error-getServerSideProps', + transaction_info: { source: 'route' }, + type: 'transaction', + }); + + // Ensure the transaction name is not '/_error' + expect(transaction.transaction).not.toBe('/_error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts index 22da2071d533..570b19b3271d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts @@ -11,7 +11,7 @@ test('should propagate serverside `getInitialProps` trace to client', async ({ p const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { return ( - transactionEvent.transaction === '/[param]/withInitialProps' && + transactionEvent.transaction === 'GET /[param]/withInitialProps' && transactionEvent.contexts?.trace?.op === 'http.server' ); }); @@ -47,7 +47,7 @@ test('should propagate serverside `getInitialProps` trace to client', async ({ p status: 'ok', }, }, - transaction: '/[param]/withInitialProps', + transaction: 'GET /[param]/withInitialProps', transaction_info: { source: 'route', }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts index 20bbbc9437f6..765864dbf4a1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts @@ -11,7 +11,7 @@ test('Should record performance for getServerSideProps', async ({ page }) => { const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { return ( - transactionEvent.transaction === '/[param]/withServerSideProps' && + transactionEvent.transaction === 'GET /[param]/withServerSideProps' && transactionEvent.contexts?.trace?.op === 'http.server' ); }); @@ -47,7 +47,7 @@ test('Should record performance for getServerSideProps', async ({ page }) => { status: 'ok', }, }, - transaction: '/[param]/withServerSideProps', + transaction: 'GET /[param]/withServerSideProps', transaction_info: { source: 'route', }, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts index 63082fee6e07..2d3854e2a2a4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -test('should not automatically create transactions for routes that were excluded from auto wrapping (string)', async ({ +test('should not apply build-time instrumentation for routes that were excluded from auto wrapping (string)', async ({ request, }) => { const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { @@ -13,17 +13,13 @@ test('should not automatically create transactions for routes that were excluded expect(await (await request.get(`/api/endpoint-excluded-with-string`)).text()).toBe('{"success":true}'); - let transactionPromiseReceived = false; - transactionPromise.then(() => { - transactionPromiseReceived = true; - }); - - await new Promise(resolve => setTimeout(resolve, 5_000)); + const transaction = await transactionPromise; - expect(transactionPromiseReceived).toBe(false); + expect(transaction.contexts?.trace?.data?.['sentry.origin']).toBeDefined(); + expect(transaction.contexts?.trace?.data?.['sentry.origin']).not.toBe('auto.http.nextjs'); // This is the origin set by the build time instrumentation }); -test('should not automatically create transactions for routes that were excluded from auto wrapping (regex)', async ({ +test('should not apply build-time instrumentation for routes that were excluded from auto wrapping (regex)', async ({ request, }) => { const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { @@ -35,12 +31,8 @@ test('should not automatically create transactions for routes that were excluded expect(await (await request.get(`/api/endpoint-excluded-with-regex`)).text()).toBe('{"success":true}'); - let transactionPromiseReceived = false; - transactionPromise.then(() => { - transactionPromiseReceived = true; - }); - - await new Promise(resolve => setTimeout(resolve, 5_000)); + const transaction = await transactionPromise; - expect(transactionPromiseReceived).toBe(false); + expect(transaction.contexts?.trace?.data?.['sentry.origin']).toBeDefined(); + expect(transaction.contexts?.trace?.data?.['sentry.origin']).not.toBe('auto.http.nextjs'); // This is the origin set by the build time instrumentation }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts index 0c99ba302dfa..9ae79d7bd4b0 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts @@ -8,12 +8,12 @@ test('Should report an error event for errors thrown in getServerSideProps', asy const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { return ( - transactionEvent.transaction === '/error-getServerSideProps' && + transactionEvent.transaction === 'GET /[param]/error-getServerSideProps' && transactionEvent.contexts?.trace?.op === 'http.server' ); }); - await page.goto('/error-getServerSideProps'); + await page.goto('/dogsaregreat/error-getServerSideProps'); expect(await errorEventPromise).toMatchObject({ contexts: { @@ -40,7 +40,7 @@ test('Should report an error event for errors thrown in getServerSideProps', asy url: expect.stringMatching(/^http.*\/error-getServerSideProps/), }, timestamp: expect.any(Number), - transaction: 'getServerSideProps (/error-getServerSideProps)', + transaction: 'getServerSideProps (/[param]/error-getServerSideProps)', }); expect(await transactionEventPromise).toMatchObject({ @@ -60,11 +60,11 @@ test('Should report an error event for errors thrown in getServerSideProps', asy data: { 'http.response.status_code': 500, 'sentry.op': 'http.server', - 'sentry.origin': 'auto.function.nextjs', + 'sentry.origin': 'auto', 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto.function.nextjs', + origin: 'auto', span_id: expect.any(String), status: 'internal_error', trace_id: expect.any(String), @@ -80,7 +80,7 @@ test('Should report an error event for errors thrown in getServerSideProps', asy }, start_timestamp: expect.any(Number), timestamp: expect.any(Number), - transaction: '/error-getServerSideProps', + transaction: 'GET /[param]/error-getServerSideProps', transaction_info: { source: 'route' }, type: 'transaction', }); @@ -95,11 +95,12 @@ test('Should report an error event for errors thrown in getServerSideProps in pa const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { return ( - transactionEvent.transaction === '/customPageExtension' && transactionEvent.contexts?.trace?.op === 'http.server' + transactionEvent.transaction === 'GET /[param]/customPageExtension' && + transactionEvent.contexts?.trace?.op === 'http.server' ); }); - await page.goto('/customPageExtension'); + await page.goto('/123/customPageExtension'); expect(await errorEventPromise).toMatchObject({ contexts: { @@ -126,7 +127,7 @@ test('Should report an error event for errors thrown in getServerSideProps in pa url: expect.stringMatching(/^http.*\/customPageExtension/), }, timestamp: expect.any(Number), - transaction: 'getServerSideProps (/customPageExtension)', + transaction: 'getServerSideProps (/[param]/customPageExtension)', }); expect(await transactionEventPromise).toMatchObject({ @@ -146,11 +147,11 @@ test('Should report an error event for errors thrown in getServerSideProps in pa data: { 'http.response.status_code': 500, 'sentry.op': 'http.server', - 'sentry.origin': 'auto.function.nextjs', + 'sentry.origin': 'auto', 'sentry.source': 'route', }, op: 'http.server', - origin: 'auto.function.nextjs', + origin: 'auto', span_id: expect.any(String), status: 'internal_error', trace_id: expect.any(String), @@ -166,7 +167,7 @@ test('Should report an error event for errors thrown in getServerSideProps in pa }, start_timestamp: expect.any(Number), timestamp: expect.any(Number), - transaction: '/customPageExtension', + transaction: 'GET /[param]/customPageExtension', transaction_info: { source: 'route' }, type: 'transaction', }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx index d2aae8c9cd8d..006a01fcfa76 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx @@ -9,31 +9,49 @@ export default function Layout({ children }: { children: React.ReactNode }) {

Layout (/)

  • - / + + / +
  • - /client-component + + /client-component +
  • - /client-component/parameter/42 + + /client-component/parameter/42 +
  • - /client-component/parameter/foo/bar/baz + + /client-component/parameter/foo/bar/baz +
  • - /server-component + + /server-component +
  • - /server-component/parameter/42 + + /server-component/parameter/42 +
  • - /server-component/parameter/foo/bar/baz + + /server-component/parameter/foo/bar/baz +
  • - /not-found + + /not-found +
  • - /redirect + + /redirect +
{children} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts index 6096fcfb1493..abc565f438b4 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts @@ -20,5 +20,5 @@ export async function middleware(request: NextRequest) { // See "Matching Paths" below to learn more export const config = { - matcher: ['/api/endpoint-behind-middleware'], + matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'], }; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts index 6dc023fdf1ed..d6a129f9e056 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/async-context-edge-endpoint.ts @@ -7,21 +7,22 @@ export const config = { export default async function handler() { // Without a working async context strategy the two spans created by `Sentry.startSpan()` would be nested. - const outerSpanPromise = Sentry.withIsolationScope(() => { - return Sentry.startSpan({ name: 'outer-span' }, () => { - return new Promise(resolve => setTimeout(resolve, 300)); - }); + const outerSpanPromise = Sentry.startSpan({ name: 'outer-span' }, () => { + return new Promise(resolve => setTimeout(resolve, 300)); }); - setTimeout(() => { - Sentry.withIsolationScope(() => { - return Sentry.startSpan({ name: 'inner-span' }, () => { + const innerSpanPromise = new Promise(resolve => { + setTimeout(() => { + Sentry.startSpan({ name: 'inner-span' }, () => { return new Promise(resolve => setTimeout(resolve, 100)); + }).then(() => { + resolve(); }); - }); - }, 100); + }, 100); + }); await outerSpanPromise; + await innerSpanPromise; return new Response('ok', { status: 200 }); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts new file mode 100644 index 000000000000..2ca75a33ba7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/pages/api/endpoint-behind-faulty-middleware.ts @@ -0,0 +1,9 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +type Data = { + name: string; +}; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json({ name: 'John Doe' }); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts index ecce719f0656..cb92cb2bab49 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts @@ -3,7 +3,10 @@ import { waitForTransaction } from '@sentry-internal/test-utils'; test('Should allow for async context isolation in the edge SDK', async ({ request }) => { const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint'; + return ( + transactionEvent?.transaction === 'GET /api/async-context-edge-endpoint' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); }); await request.get('/api/async-context-edge-endpoint'); @@ -13,8 +16,5 @@ test('Should allow for async context isolation in the edge SDK', async ({ reques const outerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'outer-span'); const innerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'inner-span'); - // @ts-expect-error parent_span_id exists - expect(outerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id); - // @ts-expect-error parent_span_id exists - expect(innerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id); + expect(outerSpan?.parent_span_id).toStrictEqual(innerSpan?.parent_span_id); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts index 35984640bcf6..abfe9b323d0f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/client-app-routing-instrumentation.test.ts @@ -42,9 +42,7 @@ test('Creates a navigation transaction for app router routes', async ({ page }) // It seems to differ between Next.js versions whether the route is parameterized or not (transactionEvent?.transaction === 'GET /server-component/parameter/foo/bar/baz' || transactionEvent?.transaction === 'GET /server-component/parameter/[...parameters]') && - transactionEvent.contexts?.trace?.data?.['http.target'].startsWith('/server-component/parameter/foo/bar/baz') && - (await clientNavigationTransactionPromise).contexts?.trace?.trace_id === - transactionEvent.contexts?.trace?.trace_id + transactionEvent.contexts?.trace?.data?.['http.target'].startsWith('/server-component/parameter/foo/bar/baz') ); }); @@ -52,6 +50,10 @@ test('Creates a navigation transaction for app router routes', async ({ page }) expect(await clientNavigationTransactionPromise).toBeDefined(); expect(await serverComponentTransactionPromise).toBeDefined(); + + expect((await serverComponentTransactionPromise).contexts?.trace?.trace_id).toBe( + (await clientNavigationTransactionPromise).contexts?.trace?.trace_id, + ); }); test('Creates a navigation transaction for `router.push()`', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts index df7ce7afd19a..88460e3ab533 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge-route.test.ts @@ -4,7 +4,8 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Should create a transaction for edge routes', async ({ request }) => { const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { return ( - transactionEvent?.transaction === 'GET /api/edge-endpoint' && transactionEvent?.contexts?.trace?.status === 'ok' + transactionEvent?.transaction === 'GET /api/edge-endpoint' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' ); }); @@ -19,47 +20,42 @@ test('Should create a transaction for edge routes', async ({ request }) => { expect(edgerouteTransaction.contexts?.trace?.status).toBe('ok'); expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); - expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge'); expect(edgerouteTransaction.request?.headers?.['x-yeet']).toBe('test-value'); }); -test('Should create a transaction with error status for faulty edge routes', async ({ request }) => { +test('Faulty edge routes', async ({ request }) => { const edgerouteTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { return ( transactionEvent?.transaction === 'GET /api/error-edge-endpoint' && - transactionEvent?.contexts?.trace?.status === 'internal_error' + transactionEvent.contexts?.runtime?.name === 'vercel-edge' ); }); - request.get('/api/error-edge-endpoint').catch(() => { - // Noop - }); - - const edgerouteTransaction = await edgerouteTransactionPromise; - - expect(edgerouteTransaction.contexts?.trace?.status).toBe('internal_error'); - expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); - expect(edgerouteTransaction.contexts?.runtime?.name).toBe('vercel-edge'); - - // Assert that isolation scope works properly - expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true); - expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); -}); - -test('Should record exceptions for faulty edge routes', async ({ request }) => { const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error'; + return ( + errorEvent?.exception?.values?.[0]?.value === 'Edge Route Error' && + errorEvent.contexts?.runtime?.name === 'vercel-edge' + ); }); request.get('/api/error-edge-endpoint').catch(() => { // Noop }); - const errorEvent = await errorEventPromise; + const [edgerouteTransaction, errorEvent] = await Promise.all([ + test.step('should create a transaction', () => edgerouteTransactionPromise), + test.step('should create an error event', () => errorEventPromise), + ]); - // Assert that isolation scope works properly - expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); - expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + test.step('should create transactions with the right fields', () => { + expect(edgerouteTransaction.contexts?.trace?.status).toBe('unknown_error'); + expect(edgerouteTransaction.contexts?.trace?.op).toBe('http.server'); + }); - expect(errorEvent.transaction).toBe('GET /api/error-edge-endpoint'); + test.step('should have scope isolation', () => { + expect(edgerouteTransaction.tags?.['my-isolated-tag']).toBe(true); + expect(edgerouteTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts index f5277dee6f66..934cfa2e472d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/edge.test.ts @@ -1,6 +1,8 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +const packageJson = require('../package.json'); + test('Should record exceptions for faulty edge server components', async ({ page }) => { const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Edge Server Component Error'; @@ -20,8 +22,14 @@ test('Should record exceptions for faulty edge server components', async ({ page }); test('Should record transaction for edge server components', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return transactionEvent?.transaction === 'Page Server Component (/edge-server-components)'; + return ( + transactionEvent?.transaction === 'GET /edge-server-components' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); }); await page.goto('/edge-server-components'); @@ -29,9 +37,14 @@ test('Should record transaction for edge server components', async ({ page }) => const serverComponentTransaction = await serverComponentTransactionPromise; expect(serverComponentTransaction).toBeDefined(); - expect(serverComponentTransaction.request?.headers).toBeDefined(); + expect(serverComponentTransaction.contexts?.trace?.op).toBe('http.server'); - // Assert that isolation scope works properly - expect(serverComponentTransaction.tags?.['my-isolated-tag']).toBe(true); - expect(serverComponentTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + // For some reason headers aren't picked up on Next.js 13 - also causing scope isolation to be broken + if (nextjsMajor >= 14) { + expect(serverComponentTransaction.request?.headers).toBeDefined(); + + // Assert that isolation scope works properly + expect(serverComponentTransaction.tags?.['my-isolated-tag']).toBe(true); + expect(serverComponentTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + } }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts index 11a5f48799bd..a00a29672ed6 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts @@ -3,7 +3,7 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Should create a transaction for middleware', async ({ request }) => { const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'ok'; + return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware'; }); const response = await request.get('/api/endpoint-behind-middleware'); @@ -12,53 +12,50 @@ test('Should create a transaction for middleware', async ({ request }) => { const middlewareTransaction = await middlewareTransactionPromise; expect(middlewareTransaction.contexts?.trace?.status).toBe('ok'); - expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs'); + expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware'); expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge'); + expect(middlewareTransaction.transaction_info?.source).toBe('url'); // Assert that isolation scope works properly expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true); expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); }); -test('Should create a transaction with error status for faulty middleware', async ({ request }) => { +test('Faulty middlewares', async ({ request }) => { const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return ( - transactionEvent?.transaction === 'middleware' && transactionEvent?.contexts?.trace?.status === 'internal_error' - ); + return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-faulty-middleware'; }); - request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => { - // Noop - }); - - const middlewareTransaction = await middlewareTransactionPromise; - - expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error'); - expect(middlewareTransaction.contexts?.trace?.op).toBe('middleware.nextjs'); - expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge'); -}); - -test('Records exceptions happening in middleware', async ({ request }) => { const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error'; }); - request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => { + request.get('/api/endpoint-behind-faulty-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => { // Noop }); - const errorEvent = await errorEventPromise; + await test.step('should record transactions', async () => { + const middlewareTransaction = await middlewareTransactionPromise; + expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error'); + expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware'); + expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge'); + expect(middlewareTransaction.transaction_info?.source).toBe('url'); + }); - // Assert that isolation scope works properly - expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); - expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); - expect(errorEvent.transaction).toBe('middleware'); + await test.step('should record exceptions', async () => { + const errorEvent = await errorEventPromise; + + // Assert that isolation scope works properly + expect(errorEvent.tags?.['my-isolated-tag']).toBe(true); + expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + expect(errorEvent.transaction).toBe('middleware GET /api/endpoint-behind-faulty-middleware'); + }); }); test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => { const middlewareTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { return ( - transactionEvent?.transaction === 'middleware' && + transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware' && !!transactionEvent.spans?.find(span => span.op === 'http.client') ); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts index a67e4328ba1c..10a4cd77f111 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts @@ -8,7 +8,7 @@ test('Will capture error for SSR rendering error with a connected trace (Class C const serverComponentTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => { return ( - transactionEvent?.transaction === '/pages-router/ssr-error-class' && + transactionEvent?.transaction === 'GET /pages-router/ssr-error-class' && (await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id ); }); @@ -26,7 +26,7 @@ test('Will capture error for SSR rendering error with a connected trace (Functio const ssrTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { return ( - transactionEvent?.transaction === '/pages-router/ssr-error-fc' && + transactionEvent?.transaction === 'GET /pages-router/ssr-error-fc' && (await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id ); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index afa02e60884a..7e6dc5fbe300 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -54,9 +54,9 @@ test('Should record exceptions and transactions for faulty route handlers', asyn expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true); expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); - expect(routehandlerTransaction.contexts?.trace?.status).toBe('unknown_error'); + expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error'); expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); - expect(routehandlerTransaction.contexts?.trace?.origin).toBe('auto.function.nextjs'); + expect(routehandlerTransaction.contexts?.trace?.origin).toContain('auto'); expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-error'); @@ -66,7 +66,10 @@ test('Should record exceptions and transactions for faulty route handlers', asyn test.describe('Edge runtime', () => { test('should create a transaction for route handlers', async ({ request }) => { const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge'; + return ( + transactionEvent?.transaction === 'PATCH /route-handlers/[param]/edge' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); }); const response = await request.patch('/route-handlers/bar/edge'); @@ -80,11 +83,17 @@ test.describe('Edge runtime', () => { test('should record exceptions and transactions for faulty route handlers', async ({ request }) => { const errorEventPromise = waitForError('nextjs-app-dir', errorEvent => { - return errorEvent?.exception?.values?.[0]?.value === 'route-handler-edge-error'; + return ( + errorEvent?.exception?.values?.[0]?.value === 'route-handler-edge-error' && + errorEvent.contexts?.runtime?.name === 'vercel-edge' + ); }); const routehandlerTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { - return transactionEvent?.transaction === 'DELETE /route-handlers/[param]/edge'; + return ( + transactionEvent?.transaction === 'DELETE /route-handlers/[param]/edge' && + transactionEvent.contexts?.runtime?.name === 'vercel-edge' + ); }); await request.delete('/route-handlers/baz/edge').catch(() => { @@ -100,12 +109,10 @@ test.describe('Edge runtime', () => { expect(routehandlerError.tags?.['my-isolated-tag']).toBe(true); expect(routehandlerError.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); - expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error'); + expect(routehandlerTransaction.contexts?.trace?.status).toBe('unknown_error'); expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server'); - expect(routehandlerTransaction.contexts?.runtime?.name).toBe('vercel-edge'); expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-edge-error'); - expect(routehandlerError.contexts?.runtime?.name).toBe('vercel-edge'); expect(routehandlerError.transaction).toBe('DELETE /route-handlers/[param]/edge'); }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts index 49afe791328f..75f30075a47f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts @@ -16,7 +16,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => { expect(transactionEvent.contexts?.trace).toEqual({ data: expect.objectContaining({ 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.otel.http', + 'sentry.origin': 'auto', 'sentry.sample_rate': 1, 'sentry.source': 'route', 'http.method': 'GET', @@ -27,7 +27,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => { 'otel.kind': 'SERVER', }), op: 'http.server', - origin: 'auto.http.otel.http', + origin: 'auto', span_id: expect.any(String), status: 'ok', trace_id: expect.any(String), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index 8d2489bab34d..278b6b1074eb 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -76,6 +76,34 @@ test('Should send a transaction for instrumented server actions', async ({ page expect(Object.keys(transactionEvent.request?.headers || {}).length).toBeGreaterThan(0); }); +test('Should send a wrapped server action as a child of a nextjs transaction', async ({ page }) => { + const nextjsVersion = packageJson.dependencies.next; + const nextjsMajor = Number(nextjsVersion.split('.')[0]); + test.skip(!isNaN(nextjsMajor) && nextjsMajor < 14, 'only applies to nextjs apps >= version 14'); + test.skip(process.env.TEST_ENV === 'development', 'this magically only works in production'); + + const nextjsPostTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return ( + transactionEvent?.transaction === 'POST /server-action' && transactionEvent.contexts?.trace?.origin === 'auto' + ); + }); + + const serverActionTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'serverAction/myServerAction'; + }); + + await page.goto('/server-action'); + await page.getByText('Run Action').click(); + + const nextjsTransaction = await nextjsPostTransactionPromise; + const serverActionTransaction = await serverActionTransactionPromise; + + expect(nextjsTransaction).toBeDefined(); + expect(serverActionTransaction).toBeDefined(); + + expect(nextjsTransaction.contexts?.trace?.span_id).toBe(serverActionTransaction.contexts?.trace?.parent_span_id); +}); + test('Should set not_found status for server actions calling notFound()', async ({ page }) => { const nextjsVersion = packageJson.dependencies.next; const nextjsMajor = Number(nextjsVersion.split('.')[0]); diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 1a855e5674b7..4e6483364ee4 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -36,6 +36,7 @@ export function makeBaseNPMConfig(options = {}) { packageSpecificConfig = {}, addPolyfills = true, sucrase = {}, + bundledBuiltins = [], } = options; const nodeResolvePlugin = makeNodeResolvePlugin(); @@ -113,7 +114,7 @@ export function makeBaseNPMConfig(options = {}) { // don't include imported modules from outside the package in the final output external: [ - ...builtinModules, + ...builtinModules.filter(m => !bundledBuiltins.includes(m)), ...Object.keys(packageDotJSON.dependencies || {}), ...Object.keys(packageDotJSON.peerDependencies || {}), ...Object.keys(packageDotJSON.optionalDependencies || {}), diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 8dd13919442a..23047338e280 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -76,6 +76,7 @@ "access": "public" }, "dependencies": { + "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation-http": "0.53.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@rollup/plugin-commonjs": "26.0.1", diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index c66f50a293f2..c50bbce37305 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -13,7 +13,7 @@ import { applyTunnelRouteOption } from './tunnelRoute'; export * from '@sentry/react'; -export { captureUnderscoreErrorException } from '../common/_error'; +export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error'; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { __rewriteFramesAssetPrefixPath__: string; diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index f6906a566050..2380b743cced 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -6,7 +6,7 @@ import { } from '@sentry/core'; import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react'; import type { Client, TransactionSource } from '@sentry/types'; -import { browserPerformanceTimeOrigin, logger, stripUrlQueryAndFragment } from '@sentry/utils'; +import { browserPerformanceTimeOrigin, logger, parseBaggageHeader, stripUrlQueryAndFragment } from '@sentry/utils'; import type { NEXT_DATA } from 'next/dist/shared/lib/utils'; import RouterImport from 'next/router'; @@ -106,7 +106,15 @@ function extractNextDataTagInformation(): NextDataTagInfo { */ export function pagesRouterInstrumentPageLoad(client: Client): void { const { route, params, sentryTrace, baggage } = extractNextDataTagInformation(); - const name = route || globalObject.location.pathname; + const parsedBaggage = parseBaggageHeader(baggage); + let name = route || globalObject.location.pathname; + + // /_error is the fallback page for all errors. If there is a transaction name for /_error, use that instead + if (parsedBaggage && parsedBaggage['sentry-transaction'] && name === '/_error') { + name = parsedBaggage['sentry-transaction']; + // Strip any HTTP method from the span name + name = name.replace(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|TRACE|CONNECT)\s+/i, ''); + } startBrowserTracingPageLoadSpan( client, diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts index 354113637a30..7740c35c016c 100644 --- a/packages/nextjs/src/common/index.ts +++ b/packages/nextjs/src/common/index.ts @@ -1,14 +1,14 @@ -export { wrapGetStaticPropsWithSentry } from './wrapGetStaticPropsWithSentry'; -export { wrapGetInitialPropsWithSentry } from './wrapGetInitialPropsWithSentry'; -export { wrapAppGetInitialPropsWithSentry } from './wrapAppGetInitialPropsWithSentry'; -export { wrapDocumentGetInitialPropsWithSentry } from './wrapDocumentGetInitialPropsWithSentry'; -export { wrapErrorGetInitialPropsWithSentry } from './wrapErrorGetInitialPropsWithSentry'; -export { wrapGetServerSidePropsWithSentry } from './wrapGetServerSidePropsWithSentry'; +export { wrapGetStaticPropsWithSentry } from './pages-router-instrumentation/wrapGetStaticPropsWithSentry'; +export { wrapGetInitialPropsWithSentry } from './pages-router-instrumentation/wrapGetInitialPropsWithSentry'; +export { wrapAppGetInitialPropsWithSentry } from './pages-router-instrumentation/wrapAppGetInitialPropsWithSentry'; +export { wrapDocumentGetInitialPropsWithSentry } from './pages-router-instrumentation/wrapDocumentGetInitialPropsWithSentry'; +export { wrapErrorGetInitialPropsWithSentry } from './pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry'; +export { wrapGetServerSidePropsWithSentry } from './pages-router-instrumentation/wrapGetServerSidePropsWithSentry'; export { wrapServerComponentWithSentry } from './wrapServerComponentWithSentry'; export { wrapRouteHandlerWithSentry } from './wrapRouteHandlerWithSentry'; -export { wrapApiHandlerWithSentryVercelCrons } from './wrapApiHandlerWithSentryVercelCrons'; +export { wrapApiHandlerWithSentryVercelCrons } from './pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons'; export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry'; -export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry'; +export { wrapPageComponentWithSentry } from './pages-router-instrumentation/wrapPageComponentWithSentry'; export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry'; export { withServerActionInstrumentation } from './withServerActionInstrumentation'; // eslint-disable-next-line deprecation/deprecation diff --git a/packages/nextjs/src/common/_error.ts b/packages/nextjs/src/common/pages-router-instrumentation/_error.ts similarity index 97% rename from packages/nextjs/src/common/_error.ts rename to packages/nextjs/src/common/pages-router-instrumentation/_error.ts index 385df8244a17..3450aad8ef5e 100644 --- a/packages/nextjs/src/common/_error.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/_error.ts @@ -1,7 +1,7 @@ import { captureException, withScope } from '@sentry/core'; import { vercelWaitUntil } from '@sentry/utils'; import type { NextPageContext } from 'next'; -import { flushSafelyWithTimeout } from './utils/responseEnd'; +import { flushSafelyWithTimeout } from '../utils/responseEnd'; type ContextOrProps = { req?: NextPageContext['req']; diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts similarity index 93% rename from packages/nextjs/src/common/wrapApiHandlerWithSentry.ts rename to packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts index a6463b0a7791..30fce67e482e 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, continueTrace, @@ -6,12 +7,13 @@ import { startSpanManual, withIsolationScope, } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { isString, logger, objectify, vercelWaitUntil } from '@sentry/utils'; +import { isString, logger, objectify } from '@sentry/utils'; + +import { vercelWaitUntil } from '@sentry/utils'; import type { NextApiRequest } from 'next'; -import type { AugmentedNextApiResponse, NextApiHandler } from './types'; -import { flushSafelyWithTimeout } from './utils/responseEnd'; -import { escapeNextjsTracing } from './utils/tracingUtils'; +import type { AugmentedNextApiResponse, NextApiHandler } from '../types'; +import { flushSafelyWithTimeout } from '../utils/responseEnd'; +import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils'; export type AugmentedNextApiRequest = NextApiRequest & { __withSentry_applied__?: boolean; @@ -32,6 +34,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz thisArg, args: [AugmentedNextApiRequest | undefined, AugmentedNextApiResponse | undefined], ) => { + dropNextjsRootContext(); return escapeNextjsTracing(() => { const [req, res] = args; diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons.ts similarity index 96% rename from packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts rename to packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons.ts index 4974cd827e9a..5ca29b338cda 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentryVercelCrons.ts @@ -1,7 +1,7 @@ import { captureCheckIn } from '@sentry/core'; import type { NextApiRequest } from 'next'; -import type { VercelCronsConfig } from './types'; +import type { VercelCronsConfig } from '../types'; type EdgeRequest = { nextUrl: URL; @@ -9,7 +9,7 @@ type EdgeRequest = { }; /** - * Wraps a function with Sentry crons instrumentation by automaticaly sending check-ins for the given Vercel crons config. + * Wraps a function with Sentry crons instrumentation by automatically sending check-ins for the given Vercel crons config. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function wrapApiHandlerWithSentryVercelCrons any>( diff --git a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts similarity index 97% rename from packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts rename to packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts index 2c7b0adc7d7b..10f783b9e9e6 100644 --- a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapAppGetInitialPropsWithSentry.ts @@ -1,7 +1,7 @@ import type App from 'next/app'; -import { isBuild } from './utils/isBuild'; -import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; +import { isBuild } from '../utils/isBuild'; +import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; type AppGetInitialProps = (typeof App)['getInitialProps']; diff --git a/packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapDocumentGetInitialPropsWithSentry.ts similarity index 96% rename from packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts rename to packages/nextjs/src/common/pages-router-instrumentation/wrapDocumentGetInitialPropsWithSentry.ts index 192e70f093b1..d7f69c621132 100644 --- a/packages/nextjs/src/common/wrapDocumentGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapDocumentGetInitialPropsWithSentry.ts @@ -1,7 +1,7 @@ import type Document from 'next/document'; -import { isBuild } from './utils/isBuild'; -import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; +import { isBuild } from '../utils/isBuild'; +import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; type DocumentGetInitialProps = typeof Document.getInitialProps; diff --git a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts similarity index 97% rename from packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts rename to packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts index a2bd559342a4..731d3fe1e24a 100644 --- a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapErrorGetInitialPropsWithSentry.ts @@ -1,8 +1,8 @@ import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; -import { isBuild } from './utils/isBuild'; -import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; +import { isBuild } from '../utils/isBuild'; +import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; type ErrorGetInitialProps = (context: NextPageContext) => Promise; diff --git a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts similarity index 96% rename from packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts rename to packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts index 2624aefb4d24..97246ec9d122 100644 --- a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetInitialPropsWithSentry.ts @@ -1,7 +1,7 @@ import type { NextPage } from 'next'; -import { isBuild } from './utils/isBuild'; -import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; +import { isBuild } from '../utils/isBuild'; +import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; type GetInitialProps = Required['getInitialProps']; diff --git a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts similarity index 96% rename from packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts rename to packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts index 0037bad36300..7c4b4101d80e 100644 --- a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetServerSidePropsWithSentry.ts @@ -1,7 +1,7 @@ import type { GetServerSideProps } from 'next'; -import { isBuild } from './utils/isBuild'; -import { withErrorInstrumentation, withTracedServerSideDataFetcher } from './utils/wrapperUtils'; +import { isBuild } from '../utils/isBuild'; +import { withErrorInstrumentation, withTracedServerSideDataFetcher } from '../utils/wrapperUtils'; /** * Create a wrapped version of the user's exported `getServerSideProps` function diff --git a/packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetStaticPropsWithSentry.ts similarity index 82% rename from packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts rename to packages/nextjs/src/common/pages-router-instrumentation/wrapGetStaticPropsWithSentry.ts index aebbf42ac684..5d083eb97ca8 100644 --- a/packages/nextjs/src/common/wrapGetStaticPropsWithSentry.ts +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapGetStaticPropsWithSentry.ts @@ -1,7 +1,7 @@ import type { GetStaticProps } from 'next'; -import { isBuild } from './utils/isBuild'; -import { callDataFetcherTraced, withErrorInstrumentation } from './utils/wrapperUtils'; +import { isBuild } from '../utils/isBuild'; +import { callDataFetcherTraced, withErrorInstrumentation } from '../utils/wrapperUtils'; type Props = { [key: string]: unknown }; @@ -14,7 +14,7 @@ type Props = { [key: string]: unknown }; */ export function wrapGetStaticPropsWithSentry( origGetStaticPropsa: GetStaticProps, - parameterizedRoute: string, + _parameterizedRoute: string, ): GetStaticProps { return new Proxy(origGetStaticPropsa, { apply: async (wrappingTarget, thisArg, args: Parameters>) => { @@ -23,10 +23,7 @@ export function wrapGetStaticPropsWithSentry( } const errorWrappedGetStaticProps = withErrorInstrumentation(wrappingTarget); - return callDataFetcherTraced(errorWrappedGetStaticProps, args, { - parameterizedRoute, - dataFetchingMethodName: 'getStaticProps', - }); + return callDataFetcherTraced(errorWrappedGetStaticProps, args); }, }); } diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts new file mode 100644 index 000000000000..8b6a45faa63b --- /dev/null +++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts @@ -0,0 +1,91 @@ +import { captureException, getCurrentScope, withIsolationScope } from '@sentry/core'; +import { extractTraceparentData } from '@sentry/utils'; + +interface FunctionComponent { + (...args: unknown[]): unknown; +} + +interface ClassComponent { + new (...args: unknown[]): { + props?: unknown; + render(...args: unknown[]): unknown; + }; +} + +function isReactClassComponent(target: unknown): target is ClassComponent { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return typeof target === 'function' && target?.prototype?.isReactComponent; +} + +/** + * Wraps a page component with Sentry error instrumentation. + */ +export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | ClassComponent): unknown { + if (isReactClassComponent(pageComponent)) { + return class SentryWrappedPageComponent extends pageComponent { + public render(...args: unknown[]): unknown { + return withIsolationScope(() => { + const scope = getCurrentScope(); + // We extract the sentry trace data that is put in the component props by datafetcher wrappers + const sentryTraceData = + typeof this.props === 'object' && + this.props !== null && + '_sentryTraceData' in this.props && + typeof this.props._sentryTraceData === 'string' + ? this.props._sentryTraceData + : undefined; + + if (sentryTraceData) { + const traceparentData = extractTraceparentData(sentryTraceData); + scope.setContext('trace', { + span_id: traceparentData?.parentSpanId, + trace_id: traceparentData?.traceId, + }); + } + + try { + return super.render(...args); + } catch (e) { + captureException(e, { + mechanism: { + handled: false, + }, + }); + throw e; + } + }); + } + }; + } else if (typeof pageComponent === 'function') { + return new Proxy(pageComponent, { + apply(target, thisArg, argArray: [{ _sentryTraceData?: string } | undefined]) { + return withIsolationScope(() => { + const scope = getCurrentScope(); + // We extract the sentry trace data that is put in the component props by datafetcher wrappers + const sentryTraceData = argArray?.[0]?._sentryTraceData; + + if (sentryTraceData) { + const traceparentData = extractTraceparentData(sentryTraceData); + scope.setContext('trace', { + span_id: traceparentData?.parentSpanId, + trace_id: traceparentData?.traceId, + }); + } + + try { + return target.apply(thisArg, argArray); + } catch (e) { + captureException(e, { + mechanism: { + handled: false, + }, + }); + throw e; + } + }); + }, + }); + } else { + return pageComponent; + } +} diff --git a/packages/nextjs/src/common/span-attributes-with-logic-attached.ts b/packages/nextjs/src/common/span-attributes-with-logic-attached.ts new file mode 100644 index 000000000000..a272ef525dff --- /dev/null +++ b/packages/nextjs/src/common/span-attributes-with-logic-attached.ts @@ -0,0 +1,8 @@ +/** + * If this attribute is attached to a transaction, the Next.js SDK will drop that transaction. + */ +export const TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION = 'sentry.drop_transaction'; + +export const TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL = 'sentry.sentry_trace_backfill'; + +export const TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL = 'sentry.route_backfill'; diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts deleted file mode 100644 index 5eed59aca0a3..000000000000 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - SPAN_STATUS_OK, - captureException, - continueTrace, - handleCallbackErrors, - setHttpStatus, - startSpan, - withIsolationScope, -} from '@sentry/core'; -import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/utils'; - -import type { EdgeRouteHandler } from '../../edge/types'; -import { flushSafelyWithTimeout } from './responseEnd'; -import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils'; - -/** - * Wraps a function on the edge runtime with error and performance monitoring. - */ -export function withEdgeWrapping( - handler: H, - options: { spanDescription: string; spanOp: string; mechanismFunctionName: string }, -): (...params: Parameters) => Promise> { - return async function (this: unknown, ...args) { - return escapeNextjsTracing(() => { - const req: unknown = args[0]; - return withIsolationScope(commonObjectToIsolationScope(req), isolationScope => { - let sentryTrace; - let baggage; - - if (req instanceof Request) { - sentryTrace = req.headers.get('sentry-trace') || ''; - baggage = req.headers.get('baggage'); - - isolationScope.setSDKProcessingMetadata({ - request: winterCGRequestToRequestData(req), - }); - } - - isolationScope.setTransactionName(options.spanDescription); - - return continueTrace( - { - sentryTrace, - baggage, - }, - () => { - return startSpan( - { - name: options.spanDescription, - op: options.spanOp, - forceTransaction: true, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', - }, - }, - async span => { - const handlerResult = await handleCallbackErrors( - () => handler.apply(this, args), - error => { - captureException(error, { - mechanism: { - type: 'instrument', - handled: false, - data: { - function: options.mechanismFunctionName, - }, - }, - }); - }, - ); - - if (handlerResult instanceof Response) { - setHttpStatus(span, handlerResult.status); - } else { - span.setStatus({ code: SPAN_STATUS_OK }); - } - - return handlerResult; - }, - ); - }, - ).finally(() => { - vercelWaitUntil(flushSafelyWithTimeout()); - }); - }); - }); - }; -} diff --git a/packages/nextjs/src/common/utils/tracingUtils.ts b/packages/nextjs/src/common/utils/tracingUtils.ts index b996b6af1877..ff57fcae3acc 100644 --- a/packages/nextjs/src/common/utils/tracingUtils.ts +++ b/packages/nextjs/src/common/utils/tracingUtils.ts @@ -1,7 +1,8 @@ -import { Scope, startNewTrace } from '@sentry/core'; +import { Scope, getActiveSpan, getRootSpan, spanToJSON, startNewTrace } from '@sentry/core'; import type { PropagationContext } from '@sentry/types'; import { GLOBAL_OBJ, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached'; const commonPropagationContextMap = new WeakMap(); @@ -92,3 +93,19 @@ export function escapeNextjsTracing(cb: () => T): T { }); } } + +/** + * Ideally this function never lands in the develop branch. + * + * Drops the entire span tree this function was called in, if it was a span tree created by Next.js. + */ +export function dropNextjsRootContext(): void { + const nextJsOwnedSpan = getActiveSpan(); + if (nextJsOwnedSpan) { + const rootSpan = getRootSpan(nextJsOwnedSpan); + const rootSpanAttributes = spanToJSON(rootSpan).data; + if (rootSpanAttributes?.['next.span_type']) { + getRootSpan(nextJsOwnedSpan)?.setAttribute(TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, true); + } + } +} diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index ff04aebbd3ed..159ee669ec09 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -1,44 +1,13 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - SPAN_STATUS_ERROR, - SPAN_STATUS_OK, captureException, - continueTrace, + getActiveSpan, + getCurrentScope, + getIsolationScope, + getRootSpan, getTraceData, - startInactiveSpan, - startSpan, - startSpanManual, - withActiveSpan, - withIsolationScope, } from '@sentry/core'; -import type { Span } from '@sentry/types'; -import { isString, vercelWaitUntil } from '@sentry/utils'; - -import { autoEndSpanOnResponseEnd, flushSafelyWithTimeout } from './responseEnd'; -import { commonObjectToIsolationScope, escapeNextjsTracing } from './tracingUtils'; - -declare module 'http' { - interface IncomingMessage { - _sentrySpan?: Span; - } -} - -/** - * Grabs a span off a Next.js datafetcher request object, if it was previously put there via - * `setSpanOnRequest`. - * - * @param req The Next.js datafetcher request object - * @returns the span on the request object if there is one, or `undefined` if the request object didn't have one. - */ -export function getSpanFromRequest(req: IncomingMessage): Span | undefined { - return req._sentrySpan; -} - -function setSpanOnRequest(span: Span, req: IncomingMessage): void { - req._sentrySpan = span; -} +import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached'; /** * Wraps a function that potentially throws. If it does, the error is passed to `captureException` and rethrown. @@ -55,7 +24,6 @@ export function withErrorInstrumentation any>( } catch (e) { // TODO: Extract error logic from `withSentry` in here or create a new wrapper with said logic or something like that. captureException(e, { mechanism: { handled: false } }); - throw e; } }; @@ -93,78 +61,27 @@ export function withTracedServerSideDataFetcher Pr this: unknown, ...args: Parameters ): Promise<{ data: ReturnType; sentryTrace?: string; baggage?: string }> { - return escapeNextjsTracing(() => { - const isolationScope = commonObjectToIsolationScope(req); - return withIsolationScope(isolationScope, () => { - isolationScope.setTransactionName(`${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`); - isolationScope.setSDKProcessingMetadata({ - request: req, - }); - - const sentryTrace = - req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; - const baggage = req.headers?.baggage; - - return continueTrace({ sentryTrace, baggage }, () => { - const requestSpan = getOrStartRequestSpan(req, res, options.requestedRouteName); - return withActiveSpan(requestSpan, () => { - return startSpanManual( - { - op: 'function.nextjs', - name: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - }, - async dataFetcherSpan => { - dataFetcherSpan.setStatus({ code: SPAN_STATUS_OK }); - const { 'sentry-trace': sentryTrace, baggage } = getTraceData(); - try { - return { - sentryTrace: sentryTrace, - baggage: baggage, - data: await origDataFetcher.apply(this, args), - }; - } catch (e) { - dataFetcherSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - requestSpan?.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - throw e; - } finally { - dataFetcherSpan.end(); - } - }, - ); - }); - }); - }); - }).finally(() => { - vercelWaitUntil(flushSafelyWithTimeout()); + getCurrentScope().setTransactionName(`${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`); + getIsolationScope().setSDKProcessingMetadata({ + request: req, }); - }; -} -function getOrStartRequestSpan(req: IncomingMessage, res: ServerResponse, name: string): Span { - const existingSpan = getSpanFromRequest(req); - if (existingSpan) { - return existingSpan; - } + const span = getActiveSpan(); - const requestSpan = startInactiveSpan({ - name, - forceTransaction: true, - op: 'http.server', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - }); + // Only set the route backfill if the span is not for /_error + if (span && options.requestedRouteName !== '/_error') { + const root = getRootSpan(span); + root.setAttribute(TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL, options.requestedRouteName); + } - requestSpan.setStatus({ code: SPAN_STATUS_OK }); - setSpanOnRequest(requestSpan, req); - autoEndSpanOnResponseEnd(requestSpan, res); + const { 'sentry-trace': sentryTrace, baggage } = getTraceData(); - return requestSpan; + return { + sentryTrace: sentryTrace, + baggage: baggage, + data: await origDataFetcher.apply(this, args), + }; + }; } /** @@ -177,37 +94,11 @@ function getOrStartRequestSpan(req: IncomingMessage, res: ServerResponse, name: export async function callDataFetcherTraced Promise | any>( origFunction: F, origFunctionArgs: Parameters, - options: { - parameterizedRoute: string; - dataFetchingMethodName: string; - }, ): Promise> { - const { parameterizedRoute, dataFetchingMethodName } = options; - - return startSpan( - { - op: 'function.nextjs', - name: `${dataFetchingMethodName} (${parameterizedRoute})`, - onlyIfParent: true, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - }, - async dataFetcherSpan => { - dataFetcherSpan.setStatus({ code: SPAN_STATUS_OK }); - - try { - return await origFunction(...origFunctionArgs); - } catch (e) { - dataFetcherSpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(e, { mechanism: { handled: false } }); - throw e; - } finally { - dataFetcherSpan.end(); - } - }, - ).finally(() => { - vercelWaitUntil(flushSafelyWithTimeout()); - }); + try { + return await origFunction(...origFunctionArgs); + } catch (e) { + captureException(e, { mechanism: { handled: false } }); + throw e; + } } diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 0b8d3b6d7c60..b13d3ebef3dd 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -1,16 +1,19 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SPAN_STATUS_ERROR, + captureException, + continueTrace, + getClient, getIsolationScope, + handleCallbackErrors, + startSpan, withIsolationScope, } from '@sentry/core'; -import { captureException, continueTrace, getClient, handleCallbackErrors, startSpan } from '@sentry/core'; import { logger, vercelWaitUntil } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { flushSafelyWithTimeout } from './utils/responseEnd'; -import { escapeNextjsTracing } from './utils/tracingUtils'; interface Options { formData?: FormData; @@ -64,88 +67,86 @@ async function withServerActionInstrumentationImplementation> { - return escapeNextjsTracing(() => { - return withIsolationScope(async isolationScope => { - const sendDefaultPii = getClient()?.getOptions().sendDefaultPii; + return withIsolationScope(async isolationScope => { + const sendDefaultPii = getClient()?.getOptions().sendDefaultPii; - let sentryTraceHeader; - let baggageHeader; - const fullHeadersObject: Record = {}; - try { - const awaitedHeaders: Headers = await options.headers; - sentryTraceHeader = awaitedHeaders?.get('sentry-trace') ?? undefined; - baggageHeader = awaitedHeaders?.get('baggage'); - awaitedHeaders?.forEach((value, key) => { - fullHeadersObject[key] = value; - }); - } catch (e) { - DEBUG_BUILD && - logger.warn( - "Sentry wasn't able to extract the tracing headers for a server action. Will not trace this request.", - ); - } - - isolationScope.setTransactionName(`serverAction/${serverActionName}`); - isolationScope.setSDKProcessingMetadata({ - request: { - headers: fullHeadersObject, - }, + let sentryTraceHeader; + let baggageHeader; + const fullHeadersObject: Record = {}; + try { + const awaitedHeaders: Headers = await options.headers; + sentryTraceHeader = awaitedHeaders?.get('sentry-trace') ?? undefined; + baggageHeader = awaitedHeaders?.get('baggage'); + awaitedHeaders?.forEach((value, key) => { + fullHeadersObject[key] = value; }); + } catch (e) { + DEBUG_BUILD && + logger.warn( + "Sentry wasn't able to extract the tracing headers for a server action. Will not trace this request.", + ); + } - return continueTrace( - { - sentryTrace: sentryTraceHeader, - baggage: baggageHeader, - }, - async () => { - try { - return await startSpan( - { - op: 'function.server_action', - name: `serverAction/${serverActionName}`, - forceTransaction: true, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - }, - async span => { - const result = await handleCallbackErrors(callback, error => { - if (isNotFoundNavigationError(error)) { - // We don't want to report "not-found"s - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); - } else if (isRedirectNavigationError(error)) { - // Don't do anything for redirects - } else { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); - captureException(error, { - mechanism: { - handled: false, - }, - }); - } - }); - - if (options.recordResponse !== undefined ? options.recordResponse : sendDefaultPii) { - getIsolationScope().setExtra('server_action_result', result); - } + isolationScope.setTransactionName(`serverAction/${serverActionName}`); + isolationScope.setSDKProcessingMetadata({ + request: { + headers: fullHeadersObject, + }, + }); - if (options.formData) { - options.formData.forEach((value, key) => { - getIsolationScope().setExtra( - `server_action_form_data.${key}`, - typeof value === 'string' ? value : '[non-string value]', - ); + return continueTrace( + { + sentryTrace: sentryTraceHeader, + baggage: baggageHeader, + }, + async () => { + try { + return await startSpan( + { + op: 'function.server_action', + name: `serverAction/${serverActionName}`, + forceTransaction: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + }, + async span => { + const result = await handleCallbackErrors(callback, error => { + if (isNotFoundNavigationError(error)) { + // We don't want to report "not-found"s + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); + } else if (isRedirectNavigationError(error)) { + // Don't do anything for redirects + } else { + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); + captureException(error, { + mechanism: { + handled: false, + }, }); } + }); - return result; - }, - ); - } finally { - vercelWaitUntil(flushSafelyWithTimeout()); - } - }, - ); - }); + if (options.recordResponse !== undefined ? options.recordResponse : sendDefaultPii) { + getIsolationScope().setExtra('server_action_result', result); + } + + if (options.formData) { + options.formData.forEach((value, key) => { + getIsolationScope().setExtra( + `server_action_form_data.${key}`, + typeof value === 'string' ? value : '[non-string value]', + ); + }); + } + + return result; + }, + ); + } finally { + vercelWaitUntil(flushSafelyWithTimeout()); + } + }, + ); }); } diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 5944b520f6ea..5c9b2506ecee 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -20,6 +20,7 @@ import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@se import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import type { GenerationFunctionContext } from '../common/types'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; +import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; /** @@ -49,9 +50,6 @@ export function wrapGenerationFunctionWithSentry a const rootSpan = getRootSpan(activeSpan); const { scope } = getCapturedScopesOnSpan(rootSpan); setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); - - // We mark the root span as an app router span so we can allow-list it in our span processor that would normally filter out all Next.js transactions/spans - rootSpan.setAttribute('sentry.rsc', true); } let data: Record | undefined = undefined; @@ -75,6 +73,15 @@ export function wrapGenerationFunctionWithSentry a }, }); + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + const sentryTrace = headersDict?.['sentry-trace']; + if (sentryTrace) { + rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace); + } + } + const propagationContext = commonObjectToPropagationContext( headers, headersDict?.['sentry-trace'] diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index 66cbbb046300..e8b57c7d2b8b 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -1,5 +1,19 @@ +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + captureException, + getActiveSpan, + getCurrentScope, + getRootSpan, + handleCallbackErrors, + setCapturedScopesOnSpan, + startSpan, + withIsolationScope, +} from '@sentry/core'; +import type { TransactionSource } from '@sentry/types'; +import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/utils'; import type { EdgeRouteHandler } from '../edge/types'; -import { withEdgeWrapping } from './utils/edgeWrapperUtils'; +import { flushSafelyWithTimeout } from './utils/responseEnd'; /** * Wraps Next.js middleware with Sentry error and performance instrumentation. @@ -11,12 +25,69 @@ export function wrapMiddlewareWithSentry( middleware: H, ): (...params: Parameters) => Promise> { return new Proxy(middleware, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - return withEdgeWrapping(wrappingTarget, { - spanDescription: 'middleware', - spanOp: 'middleware.nextjs', - mechanismFunctionName: 'withSentryMiddleware', - }).apply(thisArg, args); + apply: async (wrappingTarget, thisArg, args: Parameters) => { + // TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack. + return withIsolationScope(isolationScope => { + const req: unknown = args[0]; + const currentScope = getCurrentScope(); + + let spanName: string; + let spanSource: TransactionSource; + + if (req instanceof Request) { + isolationScope.setSDKProcessingMetadata({ + request: winterCGRequestToRequestData(req), + }); + spanName = `middleware ${req.method} ${new URL(req.url).pathname}`; + spanSource = 'url'; + } else { + spanName = 'middleware'; + spanSource = 'component'; + } + + currentScope.setTransactionName(spanName); + + const activeSpan = getActiveSpan(); + + if (activeSpan) { + // If there is an active span, it likely means that the automatic Next.js OTEL instrumentation worked and we can + // rely on that for parameterization. + spanName = 'middleware'; + spanSource = 'component'; + + const rootSpan = getRootSpan(activeSpan); + if (rootSpan) { + setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope); + } + } + + return startSpan( + { + name: spanName, + op: 'http.server.middleware', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrapMiddlewareWithSentry', + }, + }, + () => { + return handleCallbackErrors( + () => wrappingTarget.apply(thisArg, args), + error => { + captureException(error, { + mechanism: { + type: 'instrument', + handled: false, + }, + }); + }, + () => { + vercelWaitUntil(flushSafelyWithTimeout()); + }, + ); + }, + ); + }); }, }); } diff --git a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts b/packages/nextjs/src/common/wrapPageComponentWithSentry.ts deleted file mode 100644 index 8cd4a250ac14..000000000000 --- a/packages/nextjs/src/common/wrapPageComponentWithSentry.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { captureException, getCurrentScope, withIsolationScope } from '@sentry/core'; -import { extractTraceparentData } from '@sentry/utils'; -import { escapeNextjsTracing } from './utils/tracingUtils'; - -interface FunctionComponent { - (...args: unknown[]): unknown; -} - -interface ClassComponent { - new (...args: unknown[]): { - props?: unknown; - render(...args: unknown[]): unknown; - }; -} - -function isReactClassComponent(target: unknown): target is ClassComponent { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return typeof target === 'function' && target?.prototype?.isReactComponent; -} - -/** - * Wraps a page component with Sentry error instrumentation. - */ -export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | ClassComponent): unknown { - if (isReactClassComponent(pageComponent)) { - return class SentryWrappedPageComponent extends pageComponent { - public render(...args: unknown[]): unknown { - return escapeNextjsTracing(() => { - return withIsolationScope(() => { - const scope = getCurrentScope(); - // We extract the sentry trace data that is put in the component props by datafetcher wrappers - const sentryTraceData = - typeof this.props === 'object' && - this.props !== null && - '_sentryTraceData' in this.props && - typeof this.props._sentryTraceData === 'string' - ? this.props._sentryTraceData - : undefined; - - if (sentryTraceData) { - const traceparentData = extractTraceparentData(sentryTraceData); - scope.setContext('trace', { - span_id: traceparentData?.parentSpanId, - trace_id: traceparentData?.traceId, - }); - } - - try { - return super.render(...args); - } catch (e) { - captureException(e, { - mechanism: { - handled: false, - }, - }); - throw e; - } - }); - }); - } - }; - } else if (typeof pageComponent === 'function') { - return new Proxy(pageComponent, { - apply(target, thisArg, argArray: [{ _sentryTraceData?: string } | undefined]) { - return escapeNextjsTracing(() => { - return withIsolationScope(() => { - const scope = getCurrentScope(); - // We extract the sentry trace data that is put in the component props by datafetcher wrappers - const sentryTraceData = argArray?.[0]?._sentryTraceData; - - if (sentryTraceData) { - const traceparentData = extractTraceparentData(sentryTraceData); - scope.setContext('trace', { - span_id: traceparentData?.parentSpanId, - trace_id: traceparentData?.traceId, - }); - } - - try { - return target.apply(thisArg, argArray); - } catch (e) { - captureException(e, { - mechanism: { - handled: false, - }, - }); - throw e; - } - }); - }); - }, - }); - } else { - return pageComponent; - } -} diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index 71061e913dac..6ff4e314b17b 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -1,24 +1,24 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - SPAN_STATUS_ERROR, + Scope, captureException, + getActiveSpan, + getCapturedScopesOnSpan, + getRootSpan, handleCallbackErrors, + setCapturedScopesOnSpan, setHttpStatus, - startSpan, withIsolationScope, withScope, } from '@sentry/core'; -import { propagationContextFromHeaders, vercelWaitUntil, winterCGHeadersToDict } from '@sentry/utils'; -import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; + import type { RouteHandlerContext } from './types'; -import { flushSafelyWithTimeout } from './utils/responseEnd'; -import { - commonObjectToIsolationScope, - commonObjectToPropagationContext, - escapeNextjsTracing, -} from './utils/tracingUtils'; + +import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; + +import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; +import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; /** * Wraps a Next.js App Router Route handler with Sentry error and performance instrumentation. @@ -33,74 +33,79 @@ export function wrapRouteHandlerWithSentry any>( const { method, parameterizedRoute, headers } = context; return new Proxy(routeHandler, { - apply: (originalFunction, thisArg, args) => { - return escapeNextjsTracing(() => { - const isolationScope = commonObjectToIsolationScope(headers); + apply: async (originalFunction, thisArg, args) => { + const isolationScope = commonObjectToIsolationScope(headers); - const completeHeadersDict: Record = headers ? winterCGHeadersToDict(headers) : {}; + const completeHeadersDict: Record = headers ? winterCGHeadersToDict(headers) : {}; - isolationScope.setSDKProcessingMetadata({ - request: { - headers: completeHeadersDict, - }, - }); + isolationScope.setSDKProcessingMetadata({ + request: { + headers: completeHeadersDict, + }, + }); - const incomingPropagationContext = propagationContextFromHeaders( - completeHeadersDict['sentry-trace'], - completeHeadersDict['baggage'], - ); + const incomingPropagationContext = propagationContextFromHeaders( + completeHeadersDict['sentry-trace'], + completeHeadersDict['baggage'], + ); - const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext); + const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext); - return withIsolationScope(isolationScope, () => { - return withScope(async scope => { - scope.setTransactionName(`${method} ${parameterizedRoute}`); - scope.setPropagationContext(propagationContext); - try { - return startSpan( - { - name: `${method} ${parameterizedRoute}`, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - }, - forceTransaction: true, - }, - async span => { - const response: Response = await handleCallbackErrors( - () => originalFunction.apply(thisArg, args), - error => { - // Next.js throws errors when calling `redirect()`. We don't wanna report these. - if (isRedirectNavigationError(error)) { - // Don't do anything - } else if (isNotFoundNavigationError(error) && span) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'not_found' }); - } else { - captureException(error, { - mechanism: { - handled: false, - }, - }); - } - }, - ); + const activeSpan = getActiveSpan(); + const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; + if (rootSpan) { + const { scope } = getCapturedScopesOnSpan(rootSpan); + setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); + + if (process.env.NEXT_RUNTIME === 'edge') { + rootSpan.updateName(`${method} ${parameterizedRoute}`); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); + } + } - try { - if (span && response.status) { - setHttpStatus(span, response.status); - } - } catch { - // best effort - response may be undefined? - } + return withIsolationScope(isolationScope, () => { + return withScope(async scope => { + scope.setTransactionName(`${method} ${parameterizedRoute}`); + scope.setPropagationContext(propagationContext); - return response; - }, - ); - } finally { - vercelWaitUntil(flushSafelyWithTimeout()); + const response: Response = await handleCallbackErrors( + () => originalFunction.apply(thisArg, args), + error => { + // Next.js throws errors when calling `redirect()`. We don't wanna report these. + if (isRedirectNavigationError(error)) { + // Don't do anything + } else if (isNotFoundNavigationError(error)) { + if (activeSpan) { + setHttpStatus(activeSpan, 404); + } + if (rootSpan) { + setHttpStatus(rootSpan, 404); + } + } else { + captureException(error, { + mechanism: { + handled: false, + }, + }); + } + }, + ); + + try { + if (response.status) { + if (activeSpan) { + setHttpStatus(activeSpan, response.status); + } + if (rootSpan) { + setHttpStatus(rootSpan, response.status); + } } - }); + } catch { + // best effort - response may be undefined? + } + + return response; }); }); }, diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 079722dad76d..c4bbde29eb53 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -18,6 +18,7 @@ import { propagationContextFromHeaders, uuid4, vercelWaitUntil, winterCGHeadersT import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; +import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; @@ -43,9 +44,6 @@ export function wrapServerComponentWithSentry any> const rootSpan = getRootSpan(activeSpan); const { scope } = getCapturedScopesOnSpan(rootSpan); setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); - - // We mark the root span as an app router span so we can allow-list it in our span processor that would normally filter out all Next.js transactions/spans - rootSpan.setAttribute('sentry.rsc', true); } const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined; @@ -74,6 +72,15 @@ export function wrapServerComponentWithSentry any> scope.setPropagationContext(propagationContext); } + const activeSpan = getActiveSpan(); + if (activeSpan) { + const rootSpan = getRootSpan(activeSpan); + const sentryTrace = headersDict?.['sentry-trace']; + if (sentryTrace) { + rootSpan.setAttribute(TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, sentryTrace); + } + } + return startSpanManual( { op: 'function.nextjs', diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index d2e78d87f4ae..63f678cdbc66 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -409,12 +409,15 @@ export type SentryBuildOptions = { autoInstrumentAppDirectory?: boolean; /** - * Exclude certain serverside API routes or pages from being instrumented with Sentry. This option takes an array of - * strings or regular expressions. This options also affects pages in the `app` directory. + * Exclude certain serverside API routes or pages from being instrumented with Sentry during build-time. This option + * takes an array of strings or regular expressions. This options also affects pages in the `app` directory. * * NOTE: Pages should be specified as routes (`/animals` or `/api/animals/[animalType]/habitat`), not filepaths * (`pages/animals/index.js` or `.\src\pages\api\animals\[animalType]\habitat.tsx`), and strings must be be a full, * exact match. + * + * Notice: If you build Next.js with turbopack, the Sentry SDK will no longer apply build-time instrumentation and + * purely rely on Next.js telemetry features, meaning that this option will effectively no-op. */ excludeServerRoutes?: Array; diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 7034873f665e..fff4236bf3be 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,11 +1,23 @@ -import { applySdkMetadata, registerSpanErrorInstrumentation } from '@sentry/core'; -import { GLOBAL_OBJ } from '@sentry/utils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + applySdkMetadata, + getRootSpan, + registerSpanErrorInstrumentation, + spanToJSON, +} from '@sentry/core'; + +import { GLOBAL_OBJ, stripUrlQueryAndFragment, vercelWaitUntil } from '@sentry/utils'; import type { VercelEdgeOptions } from '@sentry/vercel-edge'; import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge'; import { isBuild } from '../common/utils/isBuild'; +import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; +export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error'; + export type EdgeOptions = VercelEdgeOptions; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { @@ -37,7 +49,44 @@ export function init(options: VercelEdgeOptions = {}): void { applySdkMetadata(opts, 'nextjs'); - vercelEdgeInit(opts); + const client = vercelEdgeInit(opts); + + client?.on('spanStart', span => { + const spanAttributes = spanToJSON(span).data; + + // Mark all spans generated by Next.js as 'auto' + if (spanAttributes?.['next.span_type'] !== undefined) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); + } + + // Make sure middleware spans get the right op + if (spanAttributes?.['next.span_type'] === 'Middleware.execute') { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware'); + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); + } + }); + + // Use the preprocessEvent hook instead of an event processor, so that the users event processors receive the most + // up-to-date value, but also so that the logic that detects changes to the transaction names to set the source to + // "custom", doesn't trigger. + client?.on('preprocessEvent', event => { + // The otel auto inference will clobber the transaction name because the span has an http.target + if ( + event.type === 'transaction' && + event.contexts?.trace?.data?.['next.span_type'] === 'Middleware.execute' && + event.contexts?.trace?.data?.['next.span_name'] + ) { + if (event.transaction) { + event.transaction = stripUrlQueryAndFragment(event.contexts.trace.data['next.span_name']); + } + } + }); + + client?.on('spanEnd', span => { + if (span === getRootSpan(span)) { + vercelWaitUntil(flushSafelyWithTimeout()); + } + }); } /** diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index e5191ea27dbe..5c8ce043ecb8 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -1,4 +1,18 @@ -import { withEdgeWrapping } from '../common/utils/edgeWrapperUtils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + captureException, + getActiveSpan, + getCurrentScope, + getRootSpan, + handleCallbackErrors, + setCapturedScopesOnSpan, + startSpan, + withIsolationScope, +} from '@sentry/core'; +import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/utils'; +import { flushSafelyWithTimeout } from '../common/utils/responseEnd'; import type { EdgeRouteHandler } from './types'; /** @@ -9,18 +23,76 @@ export function wrapApiHandlerWithSentry( parameterizedRoute: string, ): (...params: Parameters) => Promise> { return new Proxy(handler, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - const req = args[0]; + apply: async (wrappingTarget, thisArg, args: Parameters) => { + // TODO: We still should add central isolation scope creation for when our build-time instrumentation does not work anymore with turbopack. - const wrappedHandler = withEdgeWrapping(wrappingTarget, { - spanDescription: !(req instanceof Request) - ? `handler (${parameterizedRoute})` - : `${req.method} ${parameterizedRoute}`, - spanOp: 'http.server', - mechanismFunctionName: 'wrapApiHandlerWithSentry', - }); + return withIsolationScope(isolationScope => { + const req: unknown = args[0]; + const currentScope = getCurrentScope(); + + if (req instanceof Request) { + isolationScope.setSDKProcessingMetadata({ + request: winterCGRequestToRequestData(req), + }); + currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`); + } else { + currentScope.setTransactionName(`handler (${parameterizedRoute})`); + } + + let spanName: string; + let op: string | undefined = 'http.server'; - return wrappedHandler.apply(thisArg, args); + // If there is an active span, it likely means that the automatic Next.js OTEL instrumentation worked and we can + // rely on that for parameterization. + const activeSpan = getActiveSpan(); + if (activeSpan) { + spanName = `handler (${parameterizedRoute})`; + op = undefined; + + const rootSpan = getRootSpan(activeSpan); + if (rootSpan) { + rootSpan.updateName( + req instanceof Request ? `${req.method} ${parameterizedRoute}` : `handler ${parameterizedRoute}`, + ); + rootSpan.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }); + setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope); + } + } else if (req instanceof Request) { + spanName = `${req.method} ${parameterizedRoute}`; + } else { + spanName = `handler ${parameterizedRoute}`; + } + + return startSpan( + { + name: spanName, + op: op, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrapApiHandlerWithSentry', + }, + }, + () => { + return handleCallbackErrors( + () => wrappingTarget.apply(thisArg, args), + error => { + captureException(error, { + mechanism: { + type: 'instrument', + handled: false, + }, + }); + }, + () => { + vercelWaitUntil(flushSafelyWithTimeout()); + }, + ); + }, + ); + }); }, }); } diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 6249fcd8bad4..7aade8cbd5c3 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -1,16 +1,22 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, applySdkMetadata, + getCapturedScopesOnSpan, getClient, + getCurrentScope, getGlobalScope, + getIsolationScope, getRootSpan, + setCapturedScopesOnSpan, spanToJSON, } from '@sentry/core'; -import { getDefaultIntegrations, init as nodeInit } from '@sentry/node'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node'; +import { GLOBAL_OBJ, extractTraceparentData, logger, stripUrlQueryAndFragment } from '@sentry/utils'; +import { context } from '@opentelemetry/api'; import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, @@ -18,38 +24,28 @@ import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_TARGET, } from '@opentelemetry/semantic-conventions'; +import { getScopesFromContext } from '@sentry/opentelemetry'; import type { EventProcessor } from '@sentry/types'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; +import { + TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL, + TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL, + TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION, +} from '../common/span-attributes-with-logic-attached'; import { isBuild } from '../common/utils/isBuild'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; export * from '@sentry/node'; -export { captureUnderscoreErrorException } from '../common/_error'; +export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error'; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { __rewriteFramesDistDir__?: string; __sentryRewritesTunnelPath__?: string; }; -// https://github.com/lforst/nextjs-fork/blob/9051bc44d969a6e0ab65a955a2fc0af522a83911/packages/next/src/server/lib/trace/constants.ts#L11 -const NEXTJS_SPAN_NAME_PREFIXES = [ - 'BaseServer.', - 'LoadComponents.', - 'NextServer.', - 'createServer.', - 'startServer.', - 'NextNodeServer.', - 'Render.', - 'AppRender.', - 'Router.', - 'Node.', - 'AppRouteRouteHandlers.', - 'ResolveMetadata.', -]; - /** * A passthrough error boundary for the server that doesn't depend on any react. Error boundaries don't catch SSR errors * so they should simply be a passthrough. @@ -98,7 +94,14 @@ export function init(options: NodeOptions): NodeClient | undefined { return; } - const customDefaultIntegrations = getDefaultIntegrations(options); + const customDefaultIntegrations = getDefaultIntegrations(options) + .filter(integration => integration.name !== 'Http') + .concat( + // We are using the HTTP integration without instrumenting incoming HTTP requests because Next.js does that by itself. + httpIntegration({ + disableIncomingRequestSpans: true, + }), + ); // Turn off Next.js' own fetch instrumentation // https://github.com/lforst/nextjs-fork/blob/1994fd186defda77ad971c36dc3163db263c993f/packages/next/src/server/lib/patch-fetch.ts#L245 @@ -133,25 +136,7 @@ export function init(options: NodeOptions): NodeClient | undefined { applySdkMetadata(opts, 'nextjs', ['nextjs', 'node']); const client = nodeInit(opts); - client?.on('beforeSampling', ({ spanAttributes, spanName, parentSampled, parentContext }, samplingDecision) => { - // We allowlist the "BaseServer.handleRequest" span, since that one is responsible for App Router requests, which are actually useful for us. - // HOWEVER, that span is not only responsible for App Router requests, which is why we additionally filter for certain transactions in an - // event processor further below. - if (spanAttributes['next.span_type'] === 'BaseServer.handleRequest') { - return; - } - - // If we encounter a span emitted by Next.js, we do not want to sample it - // The reason for this is that the data quality of the spans varies, it is different per version of Next, - // and we need to keep our manual instrumentation around for the edge runtime anyhow. - // BUT we only do this if we don't have a parent span with a sampling decision yet (or if the parent is remote) - if ( - (spanAttributes['next.span_type'] || NEXTJS_SPAN_NAME_PREFIXES.some(prefix => spanName.startsWith(prefix))) && - (parentSampled === undefined || parentContext?.isRemote) - ) { - samplingDecision.decision = false; - } - + client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing // because we didn't get the chance to do `suppressTracing`, since this happens outside of userland. @@ -176,7 +161,7 @@ export function init(options: NodeOptions): NodeClient | undefined { // What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted // by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute. - if (spanAttributes?.['next.route']) { + if (typeof spanAttributes?.['next.route'] === 'string') { const rootSpan = getRootSpan(span); const rootSpanAttributes = spanToJSON(rootSpan).data; @@ -186,21 +171,31 @@ export function init(options: NodeOptions): NodeClient | undefined { (rootSpanAttributes?.[ATTR_HTTP_REQUEST_METHOD] || rootSpanAttributes?.[SEMATTRS_HTTP_METHOD]) && !rootSpanAttributes?.[ATTR_HTTP_ROUTE] ) { - rootSpan.setAttribute(ATTR_HTTP_ROUTE, spanAttributes['next.route']); + const route = spanAttributes['next.route'].replace(/\/route$/, ''); + rootSpan.updateName(route); + rootSpan.setAttribute(ATTR_HTTP_ROUTE, route); } } // We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans // with patterns (e.g. http.server spans) that will produce confusing data. if (spanAttributes?.['next.span_type'] !== undefined) { - span.setAttribute('sentry.skip_span_data_inference', true); span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto'); } - // We want to rename these spans because they look like "GET /path/to/route" and we already emit spans that look - // like this with our own http instrumentation. - if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest') { - span.updateName('next server handler'); // This is all lowercase because the spans that Next.js emits by itself generally look like this. + // We want to fork the isolation scope for incoming requests + if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest' && span === getRootSpan(span)) { + const scopes = getCapturedScopesOnSpan(span); + + const isolationScope = (scopes.isolationScope || getIsolationScope()).clone(); + const scope = scopes.scope || getCurrentScope(); + + const currentScopesPointer = getScopesFromContext(context.active()); + if (currentScopesPointer) { + currentScopesPointer.isolationScope = isolationScope; + } + + setCapturedScopesOnSpan(span, scope, isolationScope); } }); @@ -215,15 +210,6 @@ export function init(options: NodeOptions): NodeClient | undefined { return null; } - // We only want to use our HTTP integration/instrumentation for app router requests, which are marked with the `sentry.rsc` attribute. - if ( - (event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.http.otel.http' || - event.contexts?.trace?.data?.['next.span_type'] === 'BaseServer.handleRequest') && - event.contexts?.trace?.data?.['sentry.rsc'] !== true - ) { - return null; - } - // Filter out transactions for requests to the tunnel route if ( globalWithInjectedValues.__sentryRewritesTunnelPath__ && @@ -247,6 +233,27 @@ export function init(options: NodeOptions): NodeClient | undefined { return null; } + // Filter transactions that we explicitly want to drop. + if (event.contexts?.trace?.data?.[TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION]) { + return null; + } + + // Next.js 13 sometimes names the root transactions like this containing useless tracing. + if (event.transaction === 'NextServer.getRequestHandler') { + return null; + } + + // Next.js 13 is not correctly picking up tracing data for trace propagation so we use a back-fill strategy + if (typeof event.contexts?.trace?.data?.[TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL] === 'string') { + const traceparentData = extractTraceparentData( + event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL], + ); + + if (traceparentData?.parentSampled === false) { + return null; + } + } + return event; } else { return event; @@ -291,27 +298,62 @@ export function init(options: NodeOptions): NodeClient | undefined { ), ); - getGlobalScope().addEventProcessor( - Object.assign( - (event => { - // Sometimes, the HTTP integration will not work, causing us not to properly set an op for spans generated by - // Next.js that are actually more or less correct server HTTP spans, so we are backfilling the op here. - if ( - event.type === 'transaction' && - event.transaction?.match(/^(RSC )?GET /) && - event.contexts?.trace?.data?.['sentry.rsc'] === true && - !event.contexts.trace.op - ) { - event.contexts.trace.data = event.contexts.trace.data || {}; - event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; - event.contexts.trace.op = 'http.server'; - } + // Use the preprocessEvent hook instead of an event processor, so that the users event processors receive the most + // up-to-date value, but also so that the logic that detects changes to the transaction names to set the source to + // "custom", doesn't trigger. + client?.on('preprocessEvent', event => { + // Enhance route handler transactions + if ( + event.type === 'transaction' && + event.contexts?.trace?.data?.['next.span_type'] === 'BaseServer.handleRequest' + ) { + event.contexts.trace.data = event.contexts.trace.data || {}; + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server'; + event.contexts.trace.op = 'http.server'; - return event; - }) satisfies EventProcessor, - { id: 'NextjsTransactionEnhancer' }, - ), - ); + if (event.transaction) { + event.transaction = stripUrlQueryAndFragment(event.transaction); + } + + // eslint-disable-next-line deprecation/deprecation + const method = event.contexts.trace.data[SEMATTRS_HTTP_METHOD]; + // eslint-disable-next-line deprecation/deprecation + const target = event.contexts?.trace?.data?.[SEMATTRS_HTTP_TARGET]; + const route = event.contexts.trace.data[ATTR_HTTP_ROUTE]; + + if (typeof method === 'string' && typeof route === 'string') { + event.transaction = `${method} ${route.replace(/\/route$/, '')}`; + event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route'; + } + + // backfill transaction name for pages that would otherwise contain unparameterized routes + if (event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL] && event.transaction !== 'GET /_app') { + event.transaction = `${method} ${event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL]}`; + } + + // Next.js overrides transaction names for page loads that throw an error + // but we want to keep the original target name + if (event.transaction === 'GET /_error' && target) { + event.transaction = `${method ? `${method} ` : ''}${target}`; + } + } + + // Next.js 13 is not correctly picking up tracing data for trace propagation so we use a back-fill strategy + if ( + event.type === 'transaction' && + typeof event.contexts?.trace?.data?.[TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL] === 'string' + ) { + const traceparentData = extractTraceparentData(event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL]); + + if (traceparentData?.traceId) { + event.contexts.trace.trace_id = traceparentData.traceId; + } + + if (traceparentData?.parentSpanId) { + event.contexts.trace.parent_span_id = traceparentData.parentSpanId; + } + } + }); if (process.env.NODE_ENV === 'development') { getGlobalScope().addEventProcessor(devErrorSymbolicationEventProcessor); @@ -328,4 +370,4 @@ function sdkAlreadyInitialized(): boolean { export * from '../common'; -export { wrapApiHandlerWithSentry } from '../common/wrapApiHandlerWithSentry'; +export { wrapApiHandlerWithSentry } from '../common/pages-router-instrumentation/wrapApiHandlerWithSentry'; diff --git a/packages/nextjs/test/config/mocks.ts b/packages/nextjs/test/config/mocks.ts index 5c27c743c9f9..7d2cc1a0f4ac 100644 --- a/packages/nextjs/test/config/mocks.ts +++ b/packages/nextjs/test/config/mocks.ts @@ -58,15 +58,11 @@ afterEach(() => { mkdtempSyncSpy.mockClear(); }); -// TODO (v8): This shouldn't be necessary once `hideSourceMaps` gets a default value, even for the updated error message // eslint-disable-next-line @typescript-eslint/unbound-method const realConsoleWarn = global.console.warn; global.console.warn = (...args: unknown[]) => { - // Suppress the warning message about the `hideSourceMaps` option. This is better than forcing a value for - // `hideSourceMaps` because that would mean we couldn't test it easily and would muddy the waters of other tests. Note - // that doing this here, as a side effect, only works because the tests which trigger this warning are the same tests - // which need other mocks from this file. - if (typeof args[0] === 'string' && args[0]?.includes('your original code may be visible in browser devtools')) { + // Suppress the v7 -> v8 migration warning which would get spammed for the unit tests otherwise + if (typeof args[0] === 'string' && args[0]?.includes('Learn more about setting up an instrumentation hook')) { return; } diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts index 284737f4335d..e2928d59016e 100644 --- a/packages/nextjs/test/config/wrappers.test.ts +++ b/packages/nextjs/test/config/wrappers.test.ts @@ -1,13 +1,12 @@ import type { IncomingMessage, ServerResponse } from 'http'; import * as SentryCore from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Client } from '@sentry/types'; import { wrapGetInitialPropsWithSentry, wrapGetServerSidePropsWithSentry } from '../../src/common'; const startSpanManualSpy = jest.spyOn(SentryCore, 'startSpanManual'); -describe('data-fetching function wrappers should create spans', () => { +describe('data-fetching function wrappers should not create manual spans', () => { const route = '/tricks/[trickName]'; let req: IncomingMessage; let res: ServerResponse; @@ -35,17 +34,7 @@ describe('data-fetching function wrappers should create spans', () => { const wrappedOriginal = wrapGetServerSidePropsWithSentry(origFunction, route); await wrappedOriginal({ req, res } as any); - expect(startSpanManualSpy).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'getServerSideProps (/tricks/[trickName])', - op: 'function.nextjs', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - }), - expect.any(Function), - ); + expect(startSpanManualSpy).not.toHaveBeenCalled(); }); test('wrapGetInitialPropsWithSentry', async () => { @@ -54,16 +43,44 @@ describe('data-fetching function wrappers should create spans', () => { const wrappedOriginal = wrapGetInitialPropsWithSentry(origFunction); await wrappedOriginal({ req, res, pathname: route } as any); - expect(startSpanManualSpy).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'getInitialProps (/tricks/[trickName])', - op: 'function.nextjs', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - }, - }), - expect.any(Function), - ); + expect(startSpanManualSpy).not.toHaveBeenCalled(); + }); + + test('wrapped function sets route backfill attribute when called within an active span', async () => { + const mockSetAttribute = jest.fn(); + const mockGetActiveSpan = jest.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({ + setAttribute: mockSetAttribute, + } as any); + const mockGetRootSpan = jest.spyOn(SentryCore, 'getRootSpan').mockReturnValue({ + setAttribute: mockSetAttribute, + } as any); + + const origFunction = jest.fn(async () => ({ props: {} })); + const wrappedOriginal = wrapGetServerSidePropsWithSentry(origFunction, route); + + await wrappedOriginal({ req, res } as any); + + expect(mockGetActiveSpan).toHaveBeenCalled(); + expect(mockGetRootSpan).toHaveBeenCalled(); + expect(mockSetAttribute).toHaveBeenCalledWith('sentry.route_backfill', '/tricks/[trickName]'); + }); + + test('wrapped function does not set route backfill attribute for /_error route', async () => { + const mockSetAttribute = jest.fn(); + const mockGetActiveSpan = jest.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({ + setAttribute: mockSetAttribute, + } as any); + const mockGetRootSpan = jest.spyOn(SentryCore, 'getRootSpan').mockReturnValue({ + setAttribute: mockSetAttribute, + } as any); + + const origFunction = jest.fn(async () => ({ props: {} })); + const wrappedOriginal = wrapGetServerSidePropsWithSentry(origFunction, '/_error'); + + await wrappedOriginal({ req, res } as any); + + expect(mockGetActiveSpan).toHaveBeenCalled(); + expect(mockGetRootSpan).not.toHaveBeenCalled(); + expect(mockSetAttribute).not.toHaveBeenCalled(); }); }); diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts deleted file mode 100644 index 029ee9d97fce..000000000000 --- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as coreSdk from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; - -import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils'; - -const origRequest = global.Request; -const origResponse = global.Response; - -// @ts-expect-error Request does not exist on type Global -global.Request = class Request { - headers = { - get() { - return null; - }, - }; -}; - -// @ts-expect-error Response does not exist on type Global -global.Response = class Request {}; - -afterAll(() => { - global.Request = origRequest; - global.Response = origResponse; -}); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -describe('withEdgeWrapping', () => { - it('should return a function that calls the passed function', async () => { - const origFunctionReturnValue = new Response(); - const origFunction = jest.fn(_req => origFunctionReturnValue); - - const wrappedFunction = withEdgeWrapping(origFunction, { - spanDescription: 'some label', - mechanismFunctionName: 'some name', - spanOp: 'some op', - }); - - const returnValue = await wrappedFunction(new Request('https://sentry.io/')); - - expect(returnValue).toBe(origFunctionReturnValue); - expect(origFunction).toHaveBeenCalledTimes(1); - }); - - it('should return a function that calls captureException on error', async () => { - const captureExceptionSpy = jest.spyOn(coreSdk, 'captureException'); - const error = new Error(); - const origFunction = jest.fn(_req => { - throw error; - }); - - const wrappedFunction = withEdgeWrapping(origFunction, { - spanDescription: 'some label', - mechanismFunctionName: 'some name', - spanOp: 'some op', - }); - - await expect(wrappedFunction(new Request('https://sentry.io/'))).rejects.toBe(error); - expect(captureExceptionSpy).toHaveBeenCalledTimes(1); - }); - - it('should return a function that calls trace', async () => { - const startSpanSpy = jest.spyOn(coreSdk, 'startSpan'); - - const request = new Request('https://sentry.io/'); - const origFunction = jest.fn(_req => new Response()); - - const wrappedFunction = withEdgeWrapping(origFunction, { - spanDescription: 'some label', - mechanismFunctionName: 'some name', - spanOp: 'some op', - }); - - await wrappedFunction(request); - - expect(startSpanSpy).toHaveBeenCalledTimes(1); - expect(startSpanSpy).toHaveBeenCalledWith( - expect.objectContaining({ - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [coreSdk.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', - }, - name: 'some label', - op: 'some op', - }), - expect.any(Function), - ); - - expect(coreSdk.getIsolationScope().getScopeData().sdkProcessingMetadata).toEqual({ - request: { headers: {} }, - }); - }); - - it("should return a function that doesn't crash when req isn't passed", async () => { - const origFunctionReturnValue = new Response(); - const origFunction = jest.fn(() => origFunctionReturnValue); - - const wrappedFunction = withEdgeWrapping(origFunction, { - spanDescription: 'some label', - mechanismFunctionName: 'some name', - spanOp: 'some op', - }); - - await expect(wrappedFunction()).resolves.toBe(origFunctionReturnValue); - expect(origFunction).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts index 6e24eca21bfe..11449da0e1ef 100644 --- a/packages/nextjs/test/edge/withSentryAPI.test.ts +++ b/packages/nextjs/test/edge/withSentryAPI.test.ts @@ -1,6 +1,3 @@ -import * as coreSdk from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; - import { wrapApiHandlerWithSentry } from '../../src/edge'; const origRequest = global.Request; @@ -31,53 +28,16 @@ afterAll(() => { global.Response = origResponse; }); -const startSpanSpy = jest.spyOn(coreSdk, 'startSpan'); - afterEach(() => { jest.clearAllMocks(); }); describe('wrapApiHandlerWithSentry', () => { - it('should return a function that calls trace', async () => { - const request = new Request('https://sentry.io/'); - const origFunction = jest.fn(_req => new Response()); - - const wrappedFunction = wrapApiHandlerWithSentry(origFunction, '/user/[userId]/post/[postId]'); - - await wrappedFunction(request); - - expect(startSpanSpy).toHaveBeenCalledTimes(1); - expect(startSpanSpy).toHaveBeenCalledWith( - expect.objectContaining({ - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', - }, - name: 'POST /user/[userId]/post/[postId]', - op: 'http.server', - }), - expect.any(Function), - ); - }); - - it('should return a function that calls trace without throwing when no request is passed', async () => { + it('should return a function that does not throw when no request is passed', async () => { const origFunction = jest.fn(() => new Response()); const wrappedFunction = wrapApiHandlerWithSentry(origFunction, '/user/[userId]/post/[postId]'); await wrappedFunction(); - - expect(startSpanSpy).toHaveBeenCalledTimes(1); - expect(startSpanSpy).toHaveBeenCalledWith( - expect.objectContaining({ - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', - [coreSdk.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.withEdgeWrapping', - }, - name: 'handler (/user/[userId]/post/[postId])', - op: 'http.server', - }), - expect.any(Function), - ); }); }); diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 18c935863b75..d00319ec2c98 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -345,7 +345,6 @@ function removeSentryAttributes(data: Record): Record { source: 'route', }, ], - [ - "should not do any data parsing when the 'sentry.skip_span_data_inference' attribute is set", - { - 'sentry.skip_span_data_inference': true, - - // All of these should be ignored - [SEMATTRS_HTTP_METHOD]: 'GET', - [SEMATTRS_DB_SYSTEM]: 'mysql', - [SEMATTRS_DB_STATEMENT]: 'SELECT * from users', - }, - 'test name', - undefined, - { - op: undefined, - description: 'test name', - source: 'custom', - data: { - 'sentry.skip_span_data_inference': undefined, - }, - }, - ], ])('%s', (_, attributes, name, kind, expected) => { const actual = parseSpanDescription({ attributes, kind, name } as unknown as Span); expect(actual).toEqual(expected); diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 448ae6250f4a..c2e5bc6e1259 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -39,11 +39,17 @@ "access": "public" }, "dependencies": { + "@opentelemetry/api": "^1.9.0", "@sentry/core": "8.35.0", "@sentry/types": "8.35.0", "@sentry/utils": "8.35.0" }, "devDependencies": { + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/core": "^1.25.1", + "@opentelemetry/resources": "^1.26.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@sentry/opentelemetry": "8.35.0", "@edge-runtime/types": "3.0.1" }, "scripts": { diff --git a/packages/vercel-edge/rollup.npm.config.mjs b/packages/vercel-edge/rollup.npm.config.mjs index 84a06f2fb64a..3cfd779d57f6 100644 --- a/packages/vercel-edge/rollup.npm.config.mjs +++ b/packages/vercel-edge/rollup.npm.config.mjs @@ -1,3 +1,60 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import replace from '@rollup/plugin-replace'; +import { makeBaseNPMConfig, makeNPMConfigVariants, plugins } from '@sentry-internal/rollup-utils'; -export default makeNPMConfigVariants(makeBaseNPMConfig()); +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/index.ts'], + bundledBuiltins: ['perf_hooks'], + packageSpecificConfig: { + context: 'globalThis', + output: { + preserveModules: false, + }, + plugins: [ + plugins.makeCommonJSPlugin({ transformMixedEsModules: true }), // Needed because various modules in the OTEL toolchain use CJS (require-in-the-middle, shimmer, etc..) + plugins.makeJsonPlugin(), // Needed because `require-in-the-middle` imports json via require + replace({ + preventAssignment: true, + values: { + 'process.argv0': JSON.stringify(''), // needed because otel relies on process.argv0 for the default service name, but that api is not available in the edge runtime. + }, + }), + { + // This plugin is needed because otel imports `performance` from `perf_hooks` and also uses it via the `performance` global. + // Both of these APIs are not available in the edge runtime so we need to define a polyfill. + // Vercel does something similar in the `@vercel/otel` package: https://github.com/vercel/otel/blob/087601ae585cb116bb2b46c211d014520de76c71/packages/otel/build.ts#L62 + name: 'perf-hooks-performance-polyfill', + banner: ` + { + if (globalThis.performance === undefined) { + globalThis.performance = { + timeOrigin: 0, + now: () => Date.now() + }; + } + } + `, + resolveId: source => { + if (source === 'perf_hooks') { + return '\0perf_hooks_sentry_shim'; + } else { + return null; + } + }, + load: id => { + if (id === '\0perf_hooks_sentry_shim') { + return ` + export const performance = { + timeOrigin: 0, + now: () => Date.now() + } + `; + } else { + return null; + } + }, + }, + ], + }, + }), +); diff --git a/packages/vercel-edge/src/async.ts b/packages/vercel-edge/src/async.ts deleted file mode 100644 index dd7432c8e959..000000000000 --- a/packages/vercel-edge/src/async.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { getDefaultCurrentScope, getDefaultIsolationScope, setAsyncContextStrategy } from '@sentry/core'; -import type { Scope } from '@sentry/types'; -import { GLOBAL_OBJ, logger } from '@sentry/utils'; - -import { DEBUG_BUILD } from './debug-build'; - -interface AsyncLocalStorage { - getStore(): T | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R; -} - -let asyncStorage: AsyncLocalStorage<{ scope: Scope; isolationScope: Scope }>; - -/** - * Sets the async context strategy to use AsyncLocalStorage which should be available in the edge runtime. - */ -export function setAsyncLocalStorageAsyncContextStrategy(): void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - const MaybeGlobalAsyncLocalStorage = (GLOBAL_OBJ as any).AsyncLocalStorage; - - if (!MaybeGlobalAsyncLocalStorage) { - DEBUG_BUILD && - logger.warn( - "Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", - ); - return; - } - - if (!asyncStorage) { - asyncStorage = new MaybeGlobalAsyncLocalStorage(); - } - - function getScopes(): { scope: Scope; isolationScope: Scope } { - const scopes = asyncStorage.getStore(); - - if (scopes) { - return scopes; - } - - // fallback behavior: - // if, for whatever reason, we can't find scopes on the context here, we have to fix this somehow - return { - scope: getDefaultCurrentScope(), - isolationScope: getDefaultIsolationScope(), - }; - } - - function withScope(callback: (scope: Scope) => T): T { - const scope = getScopes().scope.clone(); - const isolationScope = getScopes().isolationScope; - return asyncStorage.run({ scope, isolationScope }, () => { - return callback(scope); - }); - } - - function withSetScope(scope: Scope, callback: (scope: Scope) => T): T { - const isolationScope = getScopes().isolationScope.clone(); - return asyncStorage.run({ scope, isolationScope }, () => { - return callback(scope); - }); - } - - function withIsolationScope(callback: (isolationScope: Scope) => T): T { - const scope = getScopes().scope; - const isolationScope = getScopes().isolationScope.clone(); - return asyncStorage.run({ scope, isolationScope }, () => { - return callback(isolationScope); - }); - } - - function withSetIsolationScope(isolationScope: Scope, callback: (isolationScope: Scope) => T): T { - const scope = getScopes().scope; - return asyncStorage.run({ scope, isolationScope }, () => { - return callback(isolationScope); - }); - } - - setAsyncContextStrategy({ - withScope, - withSetScope, - withIsolationScope, - withSetIsolationScope, - getCurrentScope: () => getScopes().scope, - getIsolationScope: () => getScopes().isolationScope, - }); -} diff --git a/packages/vercel-edge/src/client.ts b/packages/vercel-edge/src/client.ts index b2c7416130bc..09987eacd030 100644 --- a/packages/vercel-edge/src/client.ts +++ b/packages/vercel-edge/src/client.ts @@ -2,6 +2,7 @@ import type { ServerRuntimeClientOptions } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; import { ServerRuntimeClient } from '@sentry/core'; +import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { VercelEdgeClientOptions } from './types'; declare const process: { @@ -15,6 +16,8 @@ declare const process: { * @see ServerRuntimeClient for usage documentation. */ export class VercelEdgeClient extends ServerRuntimeClient { + public traceProvider: BasicTracerProvider | undefined; + /** * Creates a new Vercel Edge Runtime SDK instance. * @param options Configuration options for this SDK. @@ -33,4 +36,21 @@ export class VercelEdgeClient extends ServerRuntimeClient { + const provider = this.traceProvider; + const spanProcessor = provider?.activeSpanProcessor; + + if (spanProcessor) { + await spanProcessor.forceFlush(); + } + + if (this.getOptions().sendClientReports) { + this._flushOutcomes(); + } + + return super.flush(timeout); + } } diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index 4e1bed208c34..4fa8415b2184 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -1,21 +1,48 @@ import { dedupeIntegration, functionToStringIntegration, + getCurrentScope, getIntegrationsToSetup, + hasTracingEnabled, inboundFiltersIntegration, - initAndBind, linkedErrorsIntegration, requestDataIntegration, } from '@sentry/core'; import type { Client, Integration, Options } from '@sentry/types'; -import { GLOBAL_OBJ, createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; +import { + GLOBAL_OBJ, + SDK_VERSION, + createStackParser, + logger, + nodeStackLineParser, + stackParserFromStackParserOptions, +} from '@sentry/utils'; -import { setAsyncLocalStorageAsyncContextStrategy } from './async'; +import { DiagLogLevel, diag } from '@opentelemetry/api'; +import { Resource } from '@opentelemetry/resources'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, + SEMRESATTRS_SERVICE_NAMESPACE, +} from '@opentelemetry/semantic-conventions'; +import { + SentryPropagator, + SentrySampler, + SentrySpanProcessor, + enhanceDscWithOpenTelemetryRootSpanName, + openTelemetrySetupCheck, + setOpenTelemetryContextAsyncContextStrategy, + setupEventContextTrace, + wrapContextManagerClass, +} from '@sentry/opentelemetry'; import { VercelEdgeClient } from './client'; +import { DEBUG_BUILD } from './debug-build'; import { winterCGFetchIntegration } from './integrations/wintercg-fetch'; import { makeEdgeTransport } from './transports'; -import type { VercelEdgeClientOptions, VercelEdgeOptions } from './types'; +import type { VercelEdgeOptions } from './types'; import { getVercelEnv } from './utils/vercel'; +import { AsyncLocalStorageContextManager } from './vendored/async-local-storage-context-manager'; declare const process: { env: Record; @@ -37,7 +64,10 @@ export function getDefaultIntegrations(options: Options): Integration[] { /** Inits the Sentry NextJS SDK on the Edge Runtime. */ export function init(options: VercelEdgeOptions = {}): Client | undefined { - setAsyncLocalStorageAsyncContextStrategy(); + setOpenTelemetryContextAsyncContextStrategy(); + + const scope = getCurrentScope(); + scope.update(options.initialScope); if (options.defaultIntegrations === undefined) { options.defaultIntegrations = getDefaultIntegrations(options); @@ -71,14 +101,108 @@ export function init(options: VercelEdgeOptions = {}): Client | undefined { options.autoSessionTracking = true; } - const clientOptions: VercelEdgeClientOptions = { + const client = new VercelEdgeClient({ ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), integrations: getIntegrationsToSetup(options), transport: options.transport || makeEdgeTransport, - }; + }); + // The client is on the current scope, from where it generally is inherited + getCurrentScope().setClient(client); + + client.init(); + + // If users opt-out of this, they _have_ to set up OpenTelemetry themselves + // There is no way to use this SDK without OpenTelemetry! + if (!options.skipOpenTelemetrySetup) { + setupOtel(client); + validateOpenTelemetrySetup(); + } + + enhanceDscWithOpenTelemetryRootSpanName(client); + setupEventContextTrace(client); + + return client; +} + +function validateOpenTelemetrySetup(): void { + if (!DEBUG_BUILD) { + return; + } + + const setup = openTelemetrySetupCheck(); + + const required: ReturnType = ['SentryContextManager', 'SentryPropagator']; + + if (hasTracingEnabled()) { + required.push('SentrySpanProcessor'); + } + + for (const k of required) { + if (!setup.includes(k)) { + logger.error( + `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`, + ); + } + } + + if (!setup.includes('SentrySampler')) { + logger.warn( + 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.', + ); + } +} + +// exported for tests +// eslint-disable-next-line jsdoc/require-jsdoc +export function setupOtel(client: VercelEdgeClient): void { + if (client.getOptions().debug) { + setupOpenTelemetryLogger(); + } + + // Create and configure NodeTracerProvider + const provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + resource: new Resource({ + [ATTR_SERVICE_NAME]: 'edge', + // eslint-disable-next-line deprecation/deprecation + [SEMRESATTRS_SERVICE_NAMESPACE]: 'sentry', + [ATTR_SERVICE_VERSION]: SDK_VERSION, + }), + forceFlushTimeoutMillis: 500, + }); + + provider.addSpanProcessor( + new SentrySpanProcessor({ + timeout: client.getOptions().maxSpanWaitDuration, + }), + ); + + const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); + + // Initialize the provider + provider.register({ + propagator: new SentryPropagator(), + contextManager: new SentryContextManager(), + }); + + client.traceProvider = provider; +} + +/** + * Setup the OTEL logger to use our own logger. + */ +function setupOpenTelemetryLogger(): void { + const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { + get(target, prop, receiver) { + const actualProp = prop === 'verbose' ? 'debug' : prop; + return Reflect.get(target, actualProp, receiver); + }, + }); - return initAndBind(VercelEdgeClient, clientOptions); + // Disable diag, to ensure this works even if called multiple times + diag.disable(); + diag.setLogger(otelLogger, DiagLogLevel.DEBUG); } /** diff --git a/packages/vercel-edge/src/types.ts b/packages/vercel-edge/src/types.ts index 7544820c75a3..26bc1b911875 100644 --- a/packages/vercel-edge/src/types.ts +++ b/packages/vercel-edge/src/types.ts @@ -33,6 +33,27 @@ export interface BaseVercelEdgeOptions { * */ clientClass?: typeof VercelEdgeClient; + /** + * If this is set to true, the SDK will not set up OpenTelemetry automatically. + * In this case, you _have_ to ensure to set it up correctly yourself, including: + * * The `SentrySpanProcessor` + * * The `SentryPropagator` + * * The `SentryContextManager` + * * The `SentrySampler` + */ + skipOpenTelemetrySetup?: boolean; + + /** + * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span. + * The SDK will automatically clean up spans that have no finished parent after this duration. + * This is necessary to prevent memory leaks in case of parent spans that are never finished or otherwise dropped/missing. + * However, if you have very long-running spans in your application, a shorter duration might cause spans to be discarded too early. + * In this case, you can increase this duration to a value that fits your expected data. + * + * Defaults to 300 seconds (5 minutes). + */ + maxSpanWaitDuration?: number; + /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; } diff --git a/packages/vercel-edge/src/vendored/abstract-async-hooks-context-manager.ts b/packages/vercel-edge/src/vendored/abstract-async-hooks-context-manager.ts new file mode 100644 index 000000000000..883e9e43cf54 --- /dev/null +++ b/packages/vercel-edge/src/vendored/abstract-async-hooks-context-manager.ts @@ -0,0 +1,234 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Code vendored from: https://github.com/open-telemetry/opentelemetry-js/blob/6515ed8098333646a63a74a8c0150cc2daf520db/packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts + * - Modifications: + * - Added lint rules + * - Modified bind() method not to rely on Node.js specific APIs + */ + +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +/* eslint-disable @typescript-eslint/member-ordering */ +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable prefer-rest-params */ +/* eslint-disable @typescript-eslint/no-dynamic-delete */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-this-alias */ + +import type { EventEmitter } from 'events'; +import type { Context, ContextManager } from '@opentelemetry/api'; + +type Func = (...args: unknown[]) => T; + +/** + * Store a map for each event of all original listeners and their "patched" + * version. So when a listener is removed by the user, the corresponding + * patched function will be also removed. + */ +interface PatchMap { + [name: string]: WeakMap, Func>; +} + +const ADD_LISTENER_METHODS = [ + 'addListener' as const, + 'on' as const, + 'once' as const, + 'prependListener' as const, + 'prependOnceListener' as const, +]; + +export abstract class AbstractAsyncHooksContextManager implements ContextManager { + abstract active(): Context; + + abstract with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType; + + abstract enable(): this; + + abstract disable(): this; + + /** + * Binds a the certain context or the active one to the target function and then returns the target + * @param context A context (span) to be bind to target + * @param target a function or event emitter. When target or one of its callbacks is called, + * the provided context will be used as the active context for the duration of the call. + */ + bind(context: Context, target: T): T { + if (typeof target === 'object' && target !== null && 'on' in target) { + return this._bindEventEmitter(context, target as unknown as EventEmitter) as T; + } + + if (typeof target === 'function') { + return this._bindFunction(context, target); + } + return target; + } + + private _bindFunction(context: Context, target: T): T { + const manager = this; + const contextWrapper = function (this: never, ...args: unknown[]) { + return manager.with(context, () => target.apply(this, args)); + }; + Object.defineProperty(contextWrapper, 'length', { + enumerable: false, + configurable: true, + writable: false, + value: target.length, + }); + /** + * It isn't possible to tell Typescript that contextWrapper is the same as T + * so we forced to cast as any here. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return contextWrapper as any; + } + + /** + * By default, EventEmitter call their callback with their context, which we do + * not want, instead we will bind a specific context to all callbacks that + * go through it. + * @param context the context we want to bind + * @param ee EventEmitter an instance of EventEmitter to patch + */ + private _bindEventEmitter(context: Context, ee: T): T { + const map = this._getPatchMap(ee); + if (map !== undefined) return ee; + this._createPatchMap(ee); + + // patch methods that add a listener to propagate context + ADD_LISTENER_METHODS.forEach(methodName => { + if (ee[methodName] === undefined) return; + ee[methodName] = this._patchAddListener(ee, ee[methodName], context); + }); + // patch methods that remove a listener + if (typeof ee.removeListener === 'function') { + ee.removeListener = this._patchRemoveListener(ee, ee.removeListener); + } + if (typeof ee.off === 'function') { + ee.off = this._patchRemoveListener(ee, ee.off); + } + // patch method that remove all listeners + if (typeof ee.removeAllListeners === 'function') { + ee.removeAllListeners = this._patchRemoveAllListeners(ee, ee.removeAllListeners); + } + return ee; + } + + /** + * Patch methods that remove a given listener so that we match the "patched" + * version of that listener (the one that propagate context). + * @param ee EventEmitter instance + * @param original reference to the patched method + */ + private _patchRemoveListener(ee: EventEmitter, original: Function) { + const contextManager = this; + return function (this: never, event: string, listener: Func) { + const events = contextManager._getPatchMap(ee)?.[event]; + if (events === undefined) { + return original.call(this, event, listener); + } + const patchedListener = events.get(listener); + return original.call(this, event, patchedListener || listener); + }; + } + + /** + * Patch methods that remove all listeners so we remove our + * internal references for a given event. + * @param ee EventEmitter instance + * @param original reference to the patched method + */ + private _patchRemoveAllListeners(ee: EventEmitter, original: Function) { + const contextManager = this; + return function (this: never, event: string) { + const map = contextManager._getPatchMap(ee); + if (map !== undefined) { + if (arguments.length === 0) { + contextManager._createPatchMap(ee); + } else if (map[event] !== undefined) { + delete map[event]; + } + } + return original.apply(this, arguments); + }; + } + + /** + * Patch methods on an event emitter instance that can add listeners so we + * can force them to propagate a given context. + * @param ee EventEmitter instance + * @param original reference to the patched method + * @param [context] context to propagate when calling listeners + */ + private _patchAddListener(ee: EventEmitter, original: Function, context: Context) { + const contextManager = this; + return function (this: never, event: string, listener: Func) { + /** + * This check is required to prevent double-wrapping the listener. + * The implementation for ee.once wraps the listener and calls ee.on. + * Without this check, we would wrap that wrapped listener. + * This causes an issue because ee.removeListener depends on the onceWrapper + * to properly remove the listener. If we wrap their wrapper, we break + * that detection. + */ + if (contextManager._wrapped) { + return original.call(this, event, listener); + } + let map = contextManager._getPatchMap(ee); + if (map === undefined) { + map = contextManager._createPatchMap(ee); + } + let listeners = map[event]; + if (listeners === undefined) { + listeners = new WeakMap(); + map[event] = listeners; + } + const patchedListener = contextManager.bind(context, listener); + // store a weak reference of the user listener to ours + listeners.set(listener, patchedListener); + + /** + * See comment at the start of this function for the explanation of this property. + */ + contextManager._wrapped = true; + try { + return original.call(this, event, patchedListener); + } finally { + contextManager._wrapped = false; + } + }; + } + + private _createPatchMap(ee: EventEmitter): PatchMap { + const map = Object.create(null); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (ee as any)[this._kOtListeners] = map; + return map; + } + private _getPatchMap(ee: EventEmitter): PatchMap | undefined { + return (ee as never)[this._kOtListeners]; + } + + private readonly _kOtListeners = Symbol('OtListeners'); + private _wrapped = false; +} diff --git a/packages/vercel-edge/src/vendored/async-local-storage-context-manager.ts b/packages/vercel-edge/src/vendored/async-local-storage-context-manager.ts new file mode 100644 index 000000000000..99520a3c0362 --- /dev/null +++ b/packages/vercel-edge/src/vendored/async-local-storage-context-manager.ts @@ -0,0 +1,89 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Code vendored from: https://github.com/open-telemetry/opentelemetry-js/blob/6515ed8098333646a63a74a8c0150cc2daf520db/packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts + * - Modifications: + * - Added lint rules + * - Modified import path to AbstractAsyncHooksContextManager + * - Added Sentry logging + * - Modified constructor to access AsyncLocalStorage class from global object instead of the Node.js API + */ + +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +/* eslint-disable jsdoc/require-jsdoc */ + +import type { Context } from '@opentelemetry/api'; +import { ROOT_CONTEXT } from '@opentelemetry/api'; + +import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import type { AsyncLocalStorage } from 'async_hooks'; +import { DEBUG_BUILD } from '../debug-build'; +import { AbstractAsyncHooksContextManager } from './abstract-async-hooks-context-manager'; + +export class AsyncLocalStorageContextManager extends AbstractAsyncHooksContextManager { + private _asyncLocalStorage: AsyncLocalStorage; + + constructor() { + super(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const MaybeGlobalAsyncLocalStorageConstructor = (GLOBAL_OBJ as any).AsyncLocalStorage; + + if (!MaybeGlobalAsyncLocalStorageConstructor) { + DEBUG_BUILD && + logger.warn( + "Tried to register AsyncLocalStorage async context strategy in a runtime that doesn't support AsyncLocalStorage.", + ); + + // @ts-expect-error Vendored type shenanigans + this._asyncLocalStorage = { + getStore() { + return undefined; + }, + run(_store, callback, ...args) { + return callback.apply(this, args); + }, + disable() { + // noop + }, + }; + } else { + this._asyncLocalStorage = new MaybeGlobalAsyncLocalStorageConstructor(); + } + } + + active(): Context { + return this._asyncLocalStorage.getStore() ?? ROOT_CONTEXT; + } + + with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const cb = thisArg == null ? fn : fn.bind(thisArg); + return this._asyncLocalStorage.run(context, cb as never, ...args); + } + + enable(): this { + return this; + } + + disable(): this { + this._asyncLocalStorage.disable(); + return this; + } +} diff --git a/packages/vercel-edge/test/async.test.ts b/packages/vercel-edge/test/async.test.ts index a4423e0ca434..75c7d56803cd 100644 --- a/packages/vercel-edge/test/async.test.ts +++ b/packages/vercel-edge/test/async.test.ts @@ -1,19 +1,33 @@ import { Scope, getCurrentScope, getGlobalScope, getIsolationScope, withIsolationScope, withScope } from '@sentry/core'; +import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; import { GLOBAL_OBJ } from '@sentry/utils'; import { AsyncLocalStorage } from 'async_hooks'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { setAsyncLocalStorageAsyncContextStrategy } from '../src/async'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { VercelEdgeClient } from '../src'; +import { setupOtel } from '../src/sdk'; +import { makeEdgeTransport } from '../src/transports'; + +beforeAll(() => { + (GLOBAL_OBJ as any).AsyncLocalStorage = AsyncLocalStorage; + + const client = new VercelEdgeClient({ + stackParser: () => [], + integrations: [], + transport: makeEdgeTransport, + }); -describe('withScope()', () => { - beforeEach(() => { - getIsolationScope().clear(); - getCurrentScope().clear(); - getGlobalScope().clear(); + setupOtel(client); - (GLOBAL_OBJ as any).AsyncLocalStorage = AsyncLocalStorage; - setAsyncLocalStorageAsyncContextStrategy(); - }); + setOpenTelemetryContextAsyncContextStrategy(); +}); + +beforeEach(() => { + getIsolationScope().clear(); + getCurrentScope().clear(); + getGlobalScope().clear(); +}); +describe('withScope()', () => { it('will make the passed scope the active scope within the callback', () => new Promise(done => { withScope(scope => { @@ -84,15 +98,6 @@ describe('withScope()', () => { }); describe('withIsolationScope()', () => { - beforeEach(() => { - getIsolationScope().clear(); - getCurrentScope().clear(); - getGlobalScope().clear(); - (GLOBAL_OBJ as any).AsyncLocalStorage = AsyncLocalStorage; - - setAsyncLocalStorageAsyncContextStrategy(); - }); - it('will make the passed isolation scope the active isolation scope within the callback', () => new Promise(done => { withIsolationScope(scope => { diff --git a/packages/vercel-edge/test/sdk.test.ts b/packages/vercel-edge/test/sdk.test.ts deleted file mode 100644 index b1367716c73a..000000000000 --- a/packages/vercel-edge/test/sdk.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -import * as SentryCore from '@sentry/core'; -import { init } from '../src/sdk'; - -describe('init', () => { - it('initializes and returns client', () => { - const initSpy = vi.spyOn(SentryCore, 'initAndBind'); - - expect(init({})).not.toBeUndefined(); - expect(initSpy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/vercel-edge/vite.config.ts b/packages/vercel-edge/vite.config.mts similarity index 100% rename from packages/vercel-edge/vite.config.ts rename to packages/vercel-edge/vite.config.mts From e0820cf2896312d2c4d5d26fdc29e153650a7dbd Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Thu, 24 Oct 2024 20:26:23 +0200 Subject: [PATCH 13/44] fix(replay): Fix `onError` callback (#14002) This fixes the `onError` callback added in https://github.com/getsentry/sentry-javascript/pull/13721 -- the option itself was not being propagated to the replay options. --- packages/replay-internal/src/integration.ts | 2 ++ .../test/integration/sendReplayEvent.test.ts | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts index 8f70e3099a97..c3c448a3c6f2 100644 --- a/packages/replay-internal/src/integration.ts +++ b/packages/replay-internal/src/integration.ts @@ -114,6 +114,7 @@ export class Replay implements Integration { beforeAddRecordingEvent, beforeErrorSampling, + onError, }: ReplayConfiguration = {}) { this.name = Replay.id; @@ -183,6 +184,7 @@ export class Replay implements Integration { networkResponseHeaders: _getMergedNetworkHeaders(networkResponseHeaders), beforeAddRecordingEvent, beforeErrorSampling, + onError, _experiments, }; diff --git a/packages/replay-internal/test/integration/sendReplayEvent.test.ts b/packages/replay-internal/test/integration/sendReplayEvent.test.ts index a58da34b521c..29f063fa4b94 100644 --- a/packages/replay-internal/test/integration/sendReplayEvent.test.ts +++ b/packages/replay-internal/test/integration/sendReplayEvent.test.ts @@ -28,6 +28,7 @@ describe('Integration | sendReplayEvent', () => { let mockTransportSend: MockTransportSend; let mockSendReplayRequest: MockInstance; let domHandler: DomHandler; + const onError: () => void = vi.fn(); const { record: mockRecord } = mockRrweb(); beforeAll(async () => { @@ -44,6 +45,7 @@ describe('Integration | sendReplayEvent', () => { _experiments: { captureExceptions: true, }, + onError, }, })); @@ -54,6 +56,7 @@ describe('Integration | sendReplayEvent', () => { }); beforeEach(() => { + vi.clearAllMocks(); vi.setSystemTime(new Date(BASE_TIMESTAMP)); mockRecord.takeFullSnapshot.mockClear(); mockTransportSend.mockClear(); @@ -357,8 +360,9 @@ describe('Integration | sendReplayEvent', () => { expect(replay).not.toHaveLastSentReplay(); }); - it('fails to upload data and hits retry max and stops', async () => { + it('fails to upload data, hits retry max, stops, and calls `onError` with the error', async () => { const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + const ERROR = new Error('Something bad happened'); const spyHandleException = vi.spyOn(SentryCore, 'captureException'); @@ -369,7 +373,7 @@ describe('Integration | sendReplayEvent', () => { // fail all requests mockSendReplayRequest.mockImplementation(async () => { - throw new Error('Something bad happened'); + throw ERROR; }); mockRecord._emitter(TEST_EVENT); @@ -406,6 +410,8 @@ describe('Integration | sendReplayEvent', () => { // Replay has stopped, no session should exist expect(replay.session).toBe(undefined); expect(replay.isEnabled()).toBe(false); + expect(onError).toHaveBeenCalledTimes(5); + expect(onError).toHaveBeenCalledWith(ERROR); // Events are ignored now, because we stopped mockRecord._emitter(TEST_EVENT); From ff8e780d14fd1fded91c87749b3ec77d596cd28c Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 28 Oct 2024 09:17:30 +0100 Subject: [PATCH 14/44] test(nextjs): Test for client trace propagation with Turbopack (#14059) Resolves https://github.com/getsentry/sentry-javascript/issues/13656 --- .../nextjs-turbo/next-env.d.ts | 1 + .../[param]/client-trace-propagation.tsx | 10 ++++++++ .../tests/{ => app-router}/rsc-error.test.ts | 0 .../client-trace-propagation.test.ts | 25 +++++++++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/client-trace-propagation.tsx rename dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/{ => app-router}/rsc-error.test.ts (100%) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts index 40c3d68096c2..725dd6f24515 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/client-trace-propagation.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/client-trace-propagation.tsx new file mode 100644 index 000000000000..be0391c3618e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/client-trace-propagation.tsx @@ -0,0 +1,10 @@ +export default function Page() { +

Hello World!

; +} + +// getServerSideProps makes this page dynamic and allows tracing data to be inserted +export async function getServerSideProps() { + return { + props: {}, + }; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/app-router/rsc-error.test.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts rename to dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/app-router/rsc-error.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts new file mode 100644 index 000000000000..6991466acb71 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts @@ -0,0 +1,25 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { extractTraceparentData } from '@sentry/utils'; + +test('Should propagate traces from server to client in pages router', async ({ page }) => { + const serverTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { + return transactionEvent?.transaction === 'GET /[param]/client-trace-propagation'; + }); + + await page.goto(`/123/client-trace-propagation`); + + const sentryTraceLocator = await page.locator('meta[name="sentry-trace"]'); + const sentryTraceValue = await sentryTraceLocator.getAttribute('content'); + expect(sentryTraceValue).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-[0-1]$/); + + const baggageLocator = await page.locator('meta[name="baggage"]'); + const baggageValue = await baggageLocator.getAttribute('content'); + expect(baggageValue).toMatch(/sentry-public_key=/); + + const traceparentData = extractTraceparentData(sentryTraceValue!); + + const serverTransaction = await serverTransactionPromise; + + expect(serverTransaction.contexts?.trace?.trace_id).toBe(traceparentData?.traceId); +}); From e68865a50610b76f476a96a58115ddc996721580 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 28 Oct 2024 14:03:07 +0100 Subject: [PATCH 15/44] fix(nextjs): Respect directives in value injection loader (#14083) This PR is in preparation for turbopack (https://github.com/getsentry/sentry-javascript/issues/8105). In the future, `sentry.client.config.ts` will likely need to be configured with a `"use client"` directive so that turbopack knows it needs to be treated as a file on the client. Our value injection loader currently always prepends the `sentry.client.config.ts` file with statements, rendering any directives in the file useless and crashing turbopack when the file is attempted to be imported somewhere. This PR detects any comments and directives on top of a file to only inject values after. --- .../nextjs-13/sentry.client.config.ts | 2 + .../nextjs-14/sentry.client.config.ts | 2 + .../nextjs-15/sentry.client.config.ts | 2 + .../nextjs-app-dir/sentry.client.config.ts | 2 + .../nextjs-t3/sentry.client.config.ts | 2 + .../nextjs-turbo/app/layout.tsx | 7 +- ...pages-router-client-trace-propagation.tsx} | 0 .../nextjs-turbo/pages/_app.tsx | 6 + .../nextjs-turbo/sentry.client.config.ts | 22 ++- .../client-trace-propagation.test.ts | 21 +-- .../config/loaders/valueInjectionLoader.ts | 29 +++- .../valueInjectionLoader.test.ts.snap | 83 ++++++++++ .../test/config/valueInjectionLoader.test.ts | 146 ++++++++++++++++++ 13 files changed, 297 insertions(+), 27 deletions(-) rename dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/{client-trace-propagation.tsx => pages-router-client-trace-propagation.tsx} (100%) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx create mode 100644 packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap create mode 100644 packages/nextjs/test/config/valueInjectionLoader.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts index 85bd765c9c44..f2c7e4aef94d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts @@ -1,3 +1,5 @@ +'use client'; + import * as Sentry from '@sentry/nextjs'; Sentry.init({ diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts index 85bd765c9c44..f2c7e4aef94d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/sentry.client.config.ts @@ -1,3 +1,5 @@ +'use client'; + import * as Sentry from '@sentry/nextjs'; Sentry.init({ diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts index 85bd765c9c44..f2c7e4aef94d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts @@ -1,3 +1,5 @@ +'use client'; + import * as Sentry from '@sentry/nextjs'; Sentry.init({ diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts index 85bd765c9c44..f2c7e4aef94d 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/sentry.client.config.ts @@ -1,3 +1,5 @@ +'use client'; + import * as Sentry from '@sentry/nextjs'; Sentry.init({ diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts index 0e3121a8f01b..6d63ba9325fe 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/sentry.client.config.ts @@ -1,3 +1,5 @@ +'use client'; + import * as Sentry from '@sentry/nextjs'; Sentry.init({ diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx index c8f9cee0b787..999836e58b3b 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx @@ -1,7 +1,12 @@ +import { HackComponentToRunSideEffectsInSentryClientConfig } from '../sentry.client.config'; + export default function Layout({ children }: { children: React.ReactNode }) { return ( - {children} + + + {children} + ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/client-trace-propagation.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/pages-router-client-trace-propagation.tsx similarity index 100% rename from dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/client-trace-propagation.tsx rename to dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/[param]/pages-router-client-trace-propagation.tsx diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx new file mode 100644 index 000000000000..6b90ee6bc586 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx @@ -0,0 +1,6 @@ +import type { AppProps } from 'next/app'; +import '../sentry.client.config'; + +export default function CustomApp({ Component, pageProps }: AppProps) { + return ; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts index 85bd765c9c44..7a49f1b55e11 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts @@ -1,9 +1,17 @@ +'use client'; + import * as Sentry from '@sentry/nextjs'; -Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, - tunnel: `http://localhost:3031/`, // proxy server - tracesSampleRate: 1.0, - sendDefaultPii: true, -}); +if (typeof window !== 'undefined') { + Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, + }); +} + +export function HackComponentToRunSideEffectsInSentryClientConfig() { + return null; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts index 6991466acb71..20a9181d7f8e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts @@ -1,25 +1,20 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -import { extractTraceparentData } from '@sentry/utils'; test('Should propagate traces from server to client in pages router', async ({ page }) => { const serverTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { - return transactionEvent?.transaction === 'GET /[param]/client-trace-propagation'; + return transactionEvent?.transaction === 'GET /[param]/pages-router-client-trace-propagation'; }); - await page.goto(`/123/client-trace-propagation`); - - const sentryTraceLocator = await page.locator('meta[name="sentry-trace"]'); - const sentryTraceValue = await sentryTraceLocator.getAttribute('content'); - expect(sentryTraceValue).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-[0-1]$/); - - const baggageLocator = await page.locator('meta[name="baggage"]'); - const baggageValue = await baggageLocator.getAttribute('content'); - expect(baggageValue).toMatch(/sentry-public_key=/); + const pageloadTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { + return transactionEvent?.transaction === '/[param]/pages-router-client-trace-propagation'; + }); - const traceparentData = extractTraceparentData(sentryTraceValue!); + await page.goto(`/123/pages-router-client-trace-propagation`); const serverTransaction = await serverTransactionPromise; + const pageloadTransaction = await pageloadTransactionPromise; - expect(serverTransaction.contexts?.trace?.trace_id).toBe(traceparentData?.traceId); + expect(serverTransaction.contexts?.trace?.trace_id).toBeDefined(); + expect(pageloadTransaction.contexts?.trace?.trace_id).toBe(serverTransaction.contexts?.trace?.trace_id); }); diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index bf89ce90ac2c..c3d7b499fabb 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -1,9 +1,20 @@ +// Rollup doesn't like if we put the directive regex as a literal (?). No idea why. +/* eslint-disable @sentry-internal/sdk/no-regexp-constructor */ + import type { LoaderThis } from './types'; -type LoaderOptions = { +export type ValueInjectionLoaderOptions = { values: Record; }; +// We need to be careful not to inject anything before any `"use strict";`s or "use client"s or really any other directive. +// As an additional complication directives may come after any number of comments. +// This regex is shamelessly stolen from: https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/7f984482c73e4284e8b12a08dfedf23b5a82f0af/packages/bundler-plugin-core/src/index.ts#L535-L539 +const SKIP_COMMENT_AND_DIRECTIVE_REGEX = + // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files. + // biome-ignore lint/nursery/useRegexLiterals: No user input + new RegExp('^(?:\\s*|/\\*(?:.|\\r|\\n)*?\\*/|//.*[\\n\\r])*(?:"[^"]*";?|\'[^\']*\';?)?'); + /** * Set values on the global/window object at the start of a module. * @@ -11,16 +22,22 @@ type LoaderOptions = { * - `values`: An object where the keys correspond to the keys of the global values to set and the values * correspond to the values of the values on the global object. Values must be JSON serializable. */ -export default function valueInjectionLoader(this: LoaderThis, userCode: string): string { +export default function valueInjectionLoader(this: LoaderThis, userCode: string): string { // We know one or the other will be defined, depending on the version of webpack being used const { values } = 'getOptions' in this ? this.getOptions() : this.query; // We do not want to cache injected values across builds this.cacheable(false); - const injectedCode = Object.entries(values) - .map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`) - .join('\n'); + // Not putting any newlines in the generated code will decrease the likelihood of sourcemaps breaking + const injectedCode = + // eslint-disable-next-line prefer-template + ';' + + Object.entries(values) + .map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`) + .join(''); - return `${injectedCode}\n${userCode}`; + return userCode.replace(SKIP_COMMENT_AND_DIRECTIVE_REGEX, match => { + return match + injectedCode; + }); } diff --git a/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap b/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap new file mode 100644 index 000000000000..ca901580da63 --- /dev/null +++ b/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`valueInjectionLoader should correctly insert values for basic config 1`] = ` +" + ;globalThis[\\"foo\\"] = \\"bar\\";import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + " +`; + +exports[`valueInjectionLoader should correctly insert values with a misplaced directive 1`] = ` +" + ;globalThis[\\"foo\\"] = \\"bar\\";console.log('This will render the directive useless'); + \\"use client\\"; + + + + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + " +`; + +exports[`valueInjectionLoader should correctly insert values with directive 1`] = ` +" + \\"use client\\";globalThis[\\"foo\\"] = \\"bar\\"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + " +`; + +exports[`valueInjectionLoader should correctly insert values with directive and block comments 1`] = ` +" + /* test */ + \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + " +`; + +exports[`valueInjectionLoader should correctly insert values with directive and inline comments 1`] = ` +" + // test + \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + " +`; + +exports[`valueInjectionLoader should correctly insert values with directive and multiline block comments 1`] = ` +" + /* + test + */ + \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + " +`; + +exports[`valueInjectionLoader should correctly insert values with directive and multiline block comments and a bunch of whitespace 1`] = ` +" + /* + test + */ + + + + + \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\"; + + + + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + " +`; + +exports[`valueInjectionLoader should correctly insert values with directive and semicolon 1`] = ` +" + \\"use client\\";;globalThis[\\"foo\\"] = \\"bar\\"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + " +`; diff --git a/packages/nextjs/test/config/valueInjectionLoader.test.ts b/packages/nextjs/test/config/valueInjectionLoader.test.ts new file mode 100644 index 000000000000..2d810ad87c5a --- /dev/null +++ b/packages/nextjs/test/config/valueInjectionLoader.test.ts @@ -0,0 +1,146 @@ +import type { LoaderThis } from '../../src/config/loaders/types'; +import type { ValueInjectionLoaderOptions } from '../../src/config/loaders/valueInjectionLoader'; +import valueInjectionLoader from '../../src/config/loaders/valueInjectionLoader'; + +const defaultLoaderThis = { + addDependency: () => undefined, + async: () => undefined, + cacheable: () => undefined, + callback: () => undefined, +}; + +const loaderThis = { + ...defaultLoaderThis, + resourcePath: './client.config.ts', + getOptions() { + return { + values: { + foo: 'bar', + }, + }; + }, +} satisfies LoaderThis; + +describe('valueInjectionLoader', () => { + it('should correctly insert values for basic config', () => { + const userCode = ` + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + expect(result).toMatchSnapshot(); + expect(result).toMatch(';globalThis["foo"] = "bar";'); + }); + + it('should correctly insert values with directive', () => { + const userCode = ` + "use client" + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + expect(result).toMatchSnapshot(); + expect(result).toMatch(';globalThis["foo"] = "bar";'); + }); + + it('should correctly insert values with directive and semicolon', () => { + const userCode = ` + "use client"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + expect(result).toMatchSnapshot(); + expect(result).toMatch(';globalThis["foo"] = "bar";'); + }); + + it('should correctly insert values with directive and inline comments', () => { + const userCode = ` + // test + "use client"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + expect(result).toMatchSnapshot(); + expect(result).toMatch(';globalThis["foo"] = "bar";'); + }); + + it('should correctly insert values with directive and block comments', () => { + const userCode = ` + /* test */ + "use client"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + expect(result).toMatchSnapshot(); + expect(result).toMatch(';globalThis["foo"] = "bar";'); + }); + + it('should correctly insert values with directive and multiline block comments', () => { + const userCode = ` + /* + test + */ + "use client"; + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + expect(result).toMatchSnapshot(); + expect(result).toMatch(';globalThis["foo"] = "bar";'); + }); + + it('should correctly insert values with directive and multiline block comments and a bunch of whitespace', () => { + const userCode = ` + /* + test + */ + + + + + "use client"; + + + + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + expect(result).toMatchSnapshot(); + expect(result).toMatch(';globalThis["foo"] = "bar";'); + }); + + it('should correctly insert values with a misplaced directive', () => { + const userCode = ` + console.log('This will render the directive useless'); + "use client"; + + + + import * as Sentry from '@sentry/nextjs'; + Sentry.init(); + `; + + const result = valueInjectionLoader.call(loaderThis, userCode); + + expect(result).toMatchSnapshot(); + expect(result).toMatch(';globalThis["foo"] = "bar";'); + }); +}); From cc30c141c173911a14e72de9249d693e4399e507 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 28 Oct 2024 14:08:11 +0100 Subject: [PATCH 16/44] fix(core): Ensure standalone spans are not sent if SDK is disabled (#14088) Change the sending logic for standalone spans to to use the client's `sendEnvelope` method which we generally use to send envelopes (sessions, client reports, checkins, metrics (RIP), and also events). This has a minor implication: We will now also emit a `beforeEnvelope` client hook event for sending standalone spans. fixes #14082 --- .../startSpan/standalone-sdk-disabled/init.js | 18 +++++++++++++++ .../standalone-sdk-disabled/subject.js | 3 +++ .../startSpan/standalone-sdk-disabled/test.ts | 22 +++++++++++++++++++ packages/core/src/baseclient.ts | 2 +- packages/core/src/tracing/sentrySpan.ts | 9 +++----- 5 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/init.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/init.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/init.js new file mode 100644 index 000000000000..b1e11c77c2c0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/init.js @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +window.fetchCallCount = 0; +window.spanEnded = false; + +const originalWindowFetch = window.fetch; +window.fetch = (...args) => { + window.fetchCallCount++; + return originalWindowFetch(...args); +}; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + enabled: false, +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/subject.js new file mode 100644 index 000000000000..07d058f2db97 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/subject.js @@ -0,0 +1,3 @@ +Sentry.startSpan({ name: 'standalone_segment_span', experimental: { standalone: true } }, () => {}); + +window.spanEnded = true; diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/test.ts new file mode 100644 index 000000000000..c8d05c056b20 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/standalone-sdk-disabled/test.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest("doesn't send a standalone span envelope if SDK is disabled", async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + // @ts-expect-error this exists in the test init/subject + await page.waitForFunction(() => !!window.spanEnded); + await page.waitForTimeout(2000); + + // @ts-expect-error this exists in the test init + const fetchCallCount = await page.evaluate(() => window.fetchCallCount); + // We expect no fetch calls because the SDK is disabled + expect(fetchCallCount).toBe(0); +}); diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index c7a26f45ab70..6071f644c37c 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -565,7 +565,7 @@ export abstract class BaseClient implements Client { if (this._isEnabled() && this._transport) { return this._transport.send(envelope).then(null, reason => { - DEBUG_BUILD && logger.error('Error while sending event:', reason); + DEBUG_BUILD && logger.error('Error while sending envelope:', reason); return reason; }); } diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 7e1083142314..54f59386ff17 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -429,10 +429,7 @@ function sendSpanEnvelope(envelope: SpanEnvelope): void { return; } - const transport = client.getTransport(); - if (transport) { - transport.send(envelope).then(null, reason => { - DEBUG_BUILD && logger.error('Error while sending span:', reason); - }); - } + // sendEnvelope should not throw + // eslint-disable-next-line @typescript-eslint/no-floating-promises + client.sendEnvelope(envelope); } From 974818a7d88605be03b683b247827d91cb9117be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:27:49 +0000 Subject: [PATCH 17/44] chore(deps): bump rollup from 4.18.0 to 4.24.2 in /dev-packages/e2e-tests/test-applications/tanstack-router (#14089) --- .../tanstack-router/yarn.lock | 220 +++++++++--------- 1 file changed, 116 insertions(+), 104 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock b/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock index 75a09184cb81..de5c5679acb1 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock @@ -157,85 +157,95 @@ dependencies: playwright "1.46.1" -"@rollup/rollup-android-arm-eabi@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" - integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== - -"@rollup/rollup-android-arm64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" - integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== - -"@rollup/rollup-darwin-arm64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" - integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== - -"@rollup/rollup-darwin-x64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c" - integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== - -"@rollup/rollup-linux-arm-gnueabihf@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" - integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== - -"@rollup/rollup-linux-arm-musleabihf@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" - integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== - -"@rollup/rollup-linux-arm64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577" - integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== - -"@rollup/rollup-linux-arm64-musl@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c" - integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== - -"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" - integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== - -"@rollup/rollup-linux-riscv64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" - integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== - -"@rollup/rollup-linux-s390x-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" - integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== - -"@rollup/rollup-linux-x64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" - integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== - -"@rollup/rollup-linux-x64-musl@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" - integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== - -"@rollup/rollup-win32-arm64-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" - integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== - -"@rollup/rollup-win32-ia32-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" - integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== - -"@rollup/rollup-win32-x64-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" - integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== +"@rollup/rollup-android-arm-eabi@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz#07db37fcd9d401aae165f662c0069efd61d4ffcc" + integrity sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA== + +"@rollup/rollup-android-arm64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.2.tgz#160975402adf85ecd58a0721ad60ae1779a68147" + integrity sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA== + +"@rollup/rollup-darwin-arm64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.2.tgz#2b126f0aa4349694fe2941bcbcc4b0982b7f1a49" + integrity sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg== + +"@rollup/rollup-darwin-x64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.2.tgz#3f4987eff6195532037c50b8db92736e326b5bb2" + integrity sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA== + +"@rollup/rollup-freebsd-arm64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.2.tgz#15fe184ecfafc635879500f6985c954e57697c44" + integrity sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw== + +"@rollup/rollup-freebsd-x64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.2.tgz#c72d37315d36b6e0763b7aabb6ae53c361b45e05" + integrity sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg== + +"@rollup/rollup-linux-arm-gnueabihf@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.2.tgz#f274f81abf845dcca5f1f40d434a09a79a3a73a0" + integrity sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig== + +"@rollup/rollup-linux-arm-musleabihf@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.2.tgz#9edaeb1a9fa7d4469917cb0614f665f1cf050625" + integrity sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw== + +"@rollup/rollup-linux-arm64-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.2.tgz#6eb6851f594336bfa00f074f58a00a61e9751493" + integrity sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ== + +"@rollup/rollup-linux-arm64-musl@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.2.tgz#9d8dc8e80df8f156d2888ecb8d6c96d653580731" + integrity sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.2.tgz#358e3e7dda2d60c46ff7c74f7075045736df5b50" + integrity sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw== + +"@rollup/rollup-linux-riscv64-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.2.tgz#b08461ace599c3f0b5f27051f1756b6cf1c78259" + integrity sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg== + +"@rollup/rollup-linux-s390x-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.2.tgz#daab36c9b5c8ac4bfe5a9c4c39ad711464b7dfee" + integrity sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q== + +"@rollup/rollup-linux-x64-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz#4cc3a4f31920bdb028dbfd7ce0e972a17424a63c" + integrity sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ== + +"@rollup/rollup-linux-x64-musl@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.2.tgz#59800e26c538517ee05f4645315d9e1aded93200" + integrity sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q== + +"@rollup/rollup-win32-arm64-msvc@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.2.tgz#c80e2c33c952b6b171fa6ad9a97dfbb2e4ebee44" + integrity sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw== + +"@rollup/rollup-win32-ia32-msvc@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.2.tgz#a1e9d275cb16f6d5feb9c20aee7e897b1e193359" + integrity sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw== + +"@rollup/rollup-win32-x64-msvc@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.2.tgz#0610af0fb8fec52be779d5b163bbbd6930150467" + integrity sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA== "@sentry-internal/browser-utils@8.4.0": version "8.4.0" @@ -276,7 +286,7 @@ "@sentry/utils" "8.4.0" "@sentry-internal/test-utils@link:../../../test-utils": - version "8.26.0" + version "8.35.0" "@sentry/browser@8.4.0": version "8.4.0" @@ -431,10 +441,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.1.3.tgz#b8410435dac0a0f6d3fe77d49509f296905d4c73" integrity sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw== -"@types/estree@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" - integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/prop-types@*": version "15.7.12" @@ -825,28 +835,30 @@ reusify@^1.0.4: integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rollup@^4.13.0: - version "4.18.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda" - integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg== + version "4.24.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.2.tgz#04bbe819c1a0cd933533b79687f5dc43efb7a7f0" + integrity sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww== dependencies: - "@types/estree" "1.0.5" + "@types/estree" "1.0.6" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.18.0" - "@rollup/rollup-android-arm64" "4.18.0" - "@rollup/rollup-darwin-arm64" "4.18.0" - "@rollup/rollup-darwin-x64" "4.18.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.18.0" - "@rollup/rollup-linux-arm-musleabihf" "4.18.0" - "@rollup/rollup-linux-arm64-gnu" "4.18.0" - "@rollup/rollup-linux-arm64-musl" "4.18.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.18.0" - "@rollup/rollup-linux-riscv64-gnu" "4.18.0" - "@rollup/rollup-linux-s390x-gnu" "4.18.0" - "@rollup/rollup-linux-x64-gnu" "4.18.0" - "@rollup/rollup-linux-x64-musl" "4.18.0" - "@rollup/rollup-win32-arm64-msvc" "4.18.0" - "@rollup/rollup-win32-ia32-msvc" "4.18.0" - "@rollup/rollup-win32-x64-msvc" "4.18.0" + "@rollup/rollup-android-arm-eabi" "4.24.2" + "@rollup/rollup-android-arm64" "4.24.2" + "@rollup/rollup-darwin-arm64" "4.24.2" + "@rollup/rollup-darwin-x64" "4.24.2" + "@rollup/rollup-freebsd-arm64" "4.24.2" + "@rollup/rollup-freebsd-x64" "4.24.2" + "@rollup/rollup-linux-arm-gnueabihf" "4.24.2" + "@rollup/rollup-linux-arm-musleabihf" "4.24.2" + "@rollup/rollup-linux-arm64-gnu" "4.24.2" + "@rollup/rollup-linux-arm64-musl" "4.24.2" + "@rollup/rollup-linux-powerpc64le-gnu" "4.24.2" + "@rollup/rollup-linux-riscv64-gnu" "4.24.2" + "@rollup/rollup-linux-s390x-gnu" "4.24.2" + "@rollup/rollup-linux-x64-gnu" "4.24.2" + "@rollup/rollup-linux-x64-musl" "4.24.2" + "@rollup/rollup-win32-arm64-msvc" "4.24.2" + "@rollup/rollup-win32-ia32-msvc" "4.24.2" + "@rollup/rollup-win32-x64-msvc" "4.24.2" fsevents "~2.3.2" run-parallel@^1.1.9: From 765213be4c843b4e01c39518960ef71043e970f4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 28 Oct 2024 08:20:34 -0700 Subject: [PATCH 18/44] ref: Remove overhead-metrics dev package (#14087) We are not really using this ever, so we may as well remove it as it only adds complexity and dependencies to the monorepo. --- .github/workflows/build.yml | 42 ---- dev-packages/overhead-metrics/.eslintrc.cjs | 17 -- dev-packages/overhead-metrics/.gitignore | 1 - dev-packages/overhead-metrics/README.md | 13 - .../overhead-metrics/configs/README.md | 4 - .../overhead-metrics/configs/ci/collect.ts | 62 ----- .../overhead-metrics/configs/ci/env.ts | 4 - .../overhead-metrics/configs/ci/process.ts | 54 ---- .../overhead-metrics/configs/dev/collect.ts | 36 --- .../overhead-metrics/configs/dev/env.ts | 2 - .../overhead-metrics/configs/dev/process.ts | 13 - dev-packages/overhead-metrics/package.json | 36 --- .../overhead-metrics/src/collector.ts | 204 --------------- dev-packages/overhead-metrics/src/perf/cpu.ts | 52 ---- .../overhead-metrics/src/perf/memory.ts | 31 --- .../overhead-metrics/src/perf/network.ts | 88 ------- .../overhead-metrics/src/perf/sampler.ts | 118 --------- .../overhead-metrics/src/results/analyzer.ts | 173 ------------- .../src/results/metrics-stats.ts | 64 ----- .../src/results/pr-comment.ts | 170 ------------- .../overhead-metrics/src/results/result.ts | 41 --- .../src/results/results-set.ts | 116 --------- .../overhead-metrics/src/scenarios.ts | 80 ------ .../overhead-metrics/src/util/console.ts | 47 ---- dev-packages/overhead-metrics/src/util/git.ts | 75 ------ .../overhead-metrics/src/util/github.ts | 205 --------------- .../overhead-metrics/src/util/json.ts | 20 -- .../overhead-metrics/src/vitals/cls.ts | 38 --- .../overhead-metrics/src/vitals/fid.ts | 34 --- .../overhead-metrics/src/vitals/index.ts | 31 --- .../overhead-metrics/src/vitals/lcp.ts | 34 --- .../test-apps/booking-app/img/house-0.jpg | Bin 34805 -> 0 bytes .../test-apps/booking-app/img/house-1.jpg | Bin 42043 -> 0 bytes .../test-apps/booking-app/img/house-2.jpg | Bin 46950 -> 0 bytes .../test-apps/booking-app/index.html | 219 ---------------- .../test-apps/booking-app/main.js | 180 ------------- .../test-apps/booking-app/with-replay.html | 236 ------------------ .../test-apps/booking-app/with-sentry.html | 227 ----------------- .../overhead-metrics/test-apps/jank/README.md | 6 - .../overhead-metrics/test-apps/jank/app.js | 170 ------------- .../test-apps/jank/favicon-96x96.png | Bin 8194 -> 0 bytes .../test-apps/jank/index.html | 41 --- .../test-apps/jank/logo-1024px.png | Bin 305497 -> 0 bytes .../test-apps/jank/styles.css | 59 ----- .../test-apps/jank/with-replay.html | 51 ---- .../test-apps/jank/with-sentry.html | 48 ---- dev-packages/overhead-metrics/tsconfig.json | 10 - package.json | 7 +- yarn.lock | 91 +------ 49 files changed, 6 insertions(+), 3244 deletions(-) delete mode 100644 dev-packages/overhead-metrics/.eslintrc.cjs delete mode 100644 dev-packages/overhead-metrics/.gitignore delete mode 100644 dev-packages/overhead-metrics/README.md delete mode 100644 dev-packages/overhead-metrics/configs/README.md delete mode 100644 dev-packages/overhead-metrics/configs/ci/collect.ts delete mode 100644 dev-packages/overhead-metrics/configs/ci/env.ts delete mode 100644 dev-packages/overhead-metrics/configs/ci/process.ts delete mode 100644 dev-packages/overhead-metrics/configs/dev/collect.ts delete mode 100644 dev-packages/overhead-metrics/configs/dev/env.ts delete mode 100644 dev-packages/overhead-metrics/configs/dev/process.ts delete mode 100644 dev-packages/overhead-metrics/package.json delete mode 100644 dev-packages/overhead-metrics/src/collector.ts delete mode 100644 dev-packages/overhead-metrics/src/perf/cpu.ts delete mode 100644 dev-packages/overhead-metrics/src/perf/memory.ts delete mode 100644 dev-packages/overhead-metrics/src/perf/network.ts delete mode 100644 dev-packages/overhead-metrics/src/perf/sampler.ts delete mode 100644 dev-packages/overhead-metrics/src/results/analyzer.ts delete mode 100644 dev-packages/overhead-metrics/src/results/metrics-stats.ts delete mode 100644 dev-packages/overhead-metrics/src/results/pr-comment.ts delete mode 100644 dev-packages/overhead-metrics/src/results/result.ts delete mode 100644 dev-packages/overhead-metrics/src/results/results-set.ts delete mode 100644 dev-packages/overhead-metrics/src/scenarios.ts delete mode 100644 dev-packages/overhead-metrics/src/util/console.ts delete mode 100644 dev-packages/overhead-metrics/src/util/git.ts delete mode 100644 dev-packages/overhead-metrics/src/util/github.ts delete mode 100644 dev-packages/overhead-metrics/src/util/json.ts delete mode 100644 dev-packages/overhead-metrics/src/vitals/cls.ts delete mode 100644 dev-packages/overhead-metrics/src/vitals/fid.ts delete mode 100644 dev-packages/overhead-metrics/src/vitals/index.ts delete mode 100644 dev-packages/overhead-metrics/src/vitals/lcp.ts delete mode 100644 dev-packages/overhead-metrics/test-apps/booking-app/img/house-0.jpg delete mode 100644 dev-packages/overhead-metrics/test-apps/booking-app/img/house-1.jpg delete mode 100644 dev-packages/overhead-metrics/test-apps/booking-app/img/house-2.jpg delete mode 100644 dev-packages/overhead-metrics/test-apps/booking-app/index.html delete mode 100644 dev-packages/overhead-metrics/test-apps/booking-app/main.js delete mode 100644 dev-packages/overhead-metrics/test-apps/booking-app/with-replay.html delete mode 100644 dev-packages/overhead-metrics/test-apps/booking-app/with-sentry.html delete mode 100644 dev-packages/overhead-metrics/test-apps/jank/README.md delete mode 100644 dev-packages/overhead-metrics/test-apps/jank/app.js delete mode 100644 dev-packages/overhead-metrics/test-apps/jank/favicon-96x96.png delete mode 100644 dev-packages/overhead-metrics/test-apps/jank/index.html delete mode 100644 dev-packages/overhead-metrics/test-apps/jank/logo-1024px.png delete mode 100644 dev-packages/overhead-metrics/test-apps/jank/styles.css delete mode 100644 dev-packages/overhead-metrics/test-apps/jank/with-replay.html delete mode 100644 dev-packages/overhead-metrics/test-apps/jank/with-sentry.html delete mode 100644 dev-packages/overhead-metrics/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 511299fd3e3e..6f357d9a4a7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1345,48 +1345,6 @@ jobs: run: | echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1 - overhead_metrics: - name: Overhead metrics - needs: [job_get_metadata, job_build] - runs-on: ubuntu-20.04 - timeout-minutes: 30 - if: | - contains(github.event.pull_request.labels.*.name, 'ci-overhead-measurements') - steps: - - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_COMMIT }} - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version-file: 'package.json' - - name: Restore caches - uses: ./.github/actions/restore-cache - with: - dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - - name: Collect - run: yarn ci:collect - working-directory: dev-packages/overhead-metrics - - - name: Process - id: process - run: yarn ci:process - working-directory: dev-packages/overhead-metrics - # Don't run on forks - the PR comment cannot be added. - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Upload results - uses: actions/upload-artifact@v4 - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - with: - name: ${{ steps.process.outputs.artifactName }} - path: ${{ steps.process.outputs.artifactPath }} - retention-days: 7 - job_compile_bindings_profiling_node: name: Compile & Test Profiling Bindings (v${{ matrix.node }}) ${{ matrix.target_platform || matrix.os }}, ${{ matrix.node || matrix.container }}, ${{ matrix.arch || matrix.container }}, ${{ contains(matrix.container, 'alpine') && 'musl' || 'glibc' }} needs: [job_get_metadata, job_build] diff --git a/dev-packages/overhead-metrics/.eslintrc.cjs b/dev-packages/overhead-metrics/.eslintrc.cjs deleted file mode 100644 index 3eed32128e5c..000000000000 --- a/dev-packages/overhead-metrics/.eslintrc.cjs +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - extends: ['../../.eslintrc.js'], - ignorePatterns: ['test-apps'], - overrides: [ - { - files: ['*.ts'], - rules: { - 'no-console': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@sentry-internal/sdk/no-optional-chaining': 'off', - '@sentry-internal/sdk/no-nullish-coalescing': 'off', - '@sentry-internal/sdk/no-class-field-initializers': 'off', - 'jsdoc/require-jsdoc': 'off', - }, - }, - ], -}; diff --git a/dev-packages/overhead-metrics/.gitignore b/dev-packages/overhead-metrics/.gitignore deleted file mode 100644 index 505d701f0e12..000000000000 --- a/dev-packages/overhead-metrics/.gitignore +++ /dev/null @@ -1 +0,0 @@ -out diff --git a/dev-packages/overhead-metrics/README.md b/dev-packages/overhead-metrics/README.md deleted file mode 100644 index 51e7d2587ac0..000000000000 --- a/dev-packages/overhead-metrics/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Overhead performance metrics - -Evaluates Sentry & Replay impact on website performance by running a web app in Chromium via Playwright and collecting -various metrics. - -The general idea is to run a web app without Sentry, and then run the same app again with Sentry and another one with -Sentry+Replay included. For the three scenarios, we collect some metrics (CPU, memory, vitals) and later compare them -and post as a comment in a PR. Changes in the metrics, compared to previous runs from the main branch, should be -evaluated on case-by-case basis when preparing and reviewing the PR. - -## Resources - -- https://github.com/addyosmani/puppeteer-webperf diff --git a/dev-packages/overhead-metrics/configs/README.md b/dev-packages/overhead-metrics/configs/README.md deleted file mode 100644 index ceb96835f975..000000000000 --- a/dev-packages/overhead-metrics/configs/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Replay metrics configuration & entrypoints (scripts) - -- [dev](dev) contains scripts launched during local development -- [ci](ci) contains scripts launched in CI diff --git a/dev-packages/overhead-metrics/configs/ci/collect.ts b/dev-packages/overhead-metrics/configs/ci/collect.ts deleted file mode 100644 index 88a510fabdf0..000000000000 --- a/dev-packages/overhead-metrics/configs/ci/collect.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { Metrics } from '../../src/collector.js'; -import { MetricsCollector } from '../../src/collector.js'; -import type { NumberProvider } from '../../src/results/metrics-stats.js'; -import { MetricsStats } from '../../src/results/metrics-stats.js'; -import { BookingAppScenario } from '../../src/scenarios.js'; -import { printStats } from '../../src/util/console.js'; -import { latestResultFile } from './env.js'; - -function checkStdDev(results: Metrics[], name: string, provider: NumberProvider, max: number): boolean { - const value = MetricsStats.stddev(results, provider); - if (value == undefined) { - console.warn(`✗ | Discarding results because StandardDeviation(${name}) is undefined`); - return false; - } else if (value > max) { - console.warn( - `✗ | Discarding results because StandardDeviation(${name}) is larger than ${max}. Actual value: ${value}`, - ); - return false; - } else { - console.log(`✓ | StandardDeviation(${name}) is ${value} (<= ${max})`); - } - return true; -} - -const collector = new MetricsCollector({ headless: true, cpuThrottling: 2 }); -const result = await collector.execute({ - name: 'jank', - scenarios: [ - new BookingAppScenario('index.html', 100), - new BookingAppScenario('with-sentry.html', 100), - new BookingAppScenario('with-replay.html', 100), - ], - runs: 10, - tries: 10, - async shouldAccept(results: Metrics[]): Promise { - await printStats(results); - - if ( - !checkStdDev(results, 'lcp', MetricsStats.lcp, 50) || - !checkStdDev(results, 'cls', MetricsStats.cls, 0.1) || - !checkStdDev(results, 'cpu', MetricsStats.cpu, 1) || - !checkStdDev(results, 'memory-mean', MetricsStats.memoryMean, 1000 * 1024) || - !checkStdDev(results, 'memory-max', MetricsStats.memoryMax, 1000 * 1024) - ) { - return false; - } - - const cpuUsage = MetricsStats.mean(results, MetricsStats.cpu)!; - if (cpuUsage > 0.85) { - // Note: complexity on the "JankTest" is defined by the `minimum = ...,` setting in app.js - specifying the number of animated elements. - console.warn( - `✗ | Discarding results because CPU usage is too high and may be inaccurate: ${(cpuUsage * 100).toFixed(2)} %.`, - 'Consider simplifying the scenario or changing the CPU throttling factor.', - ); - return false; - } - - return true; - }, -}); - -result.writeToFile(latestResultFile); diff --git a/dev-packages/overhead-metrics/configs/ci/env.ts b/dev-packages/overhead-metrics/configs/ci/env.ts deleted file mode 100644 index 511941e433b7..000000000000 --- a/dev-packages/overhead-metrics/configs/ci/env.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const previousResultsDir = 'out/previous-results'; -export const baselineResultsDir = 'out/baseline-results'; -export const latestResultFile = 'out/latest-result.json'; -export const artifactName = 'replay-sdk-metrics'; diff --git a/dev-packages/overhead-metrics/configs/ci/process.ts b/dev-packages/overhead-metrics/configs/ci/process.ts deleted file mode 100644 index 31e7842844ef..000000000000 --- a/dev-packages/overhead-metrics/configs/ci/process.ts +++ /dev/null @@ -1,54 +0,0 @@ -import path from 'path'; -import fs from 'fs-extra'; - -import { ResultsAnalyzer } from '../../src/results/analyzer.js'; -import { PrCommentBuilder } from '../../src/results/pr-comment.js'; -import { Result } from '../../src/results/result.js'; -import { ResultsSet } from '../../src/results/results-set.js'; -import { Git } from '../../src/util/git.js'; -import { GitHub } from '../../src/util/github.js'; -import { artifactName, baselineResultsDir, latestResultFile, previousResultsDir } from './env.js'; - -const latestResult = Result.readFromFile(latestResultFile); -const branch = await Git.branch; -const baseBranch = await Git.baseBranch; -const branchIsBase = await Git.branchIsBase; - -await GitHub.downloadPreviousArtifact(baseBranch, baselineResultsDir, artifactName); - -if (branchIsBase) { - await GitHub.downloadPreviousArtifact(branch, previousResultsDir, artifactName); -} else { - // Copy over same results - await fs.copy(baselineResultsDir, previousResultsDir); -} - -GitHub.writeOutput('artifactName', artifactName); -GitHub.writeOutput('artifactPath', path.resolve(previousResultsDir)); - -const previousResults = new ResultsSet(previousResultsDir); - -const prComment = new PrCommentBuilder(); -if (baseBranch != branch) { - const baseResults = new ResultsSet(baselineResultsDir); - await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, baseResults), 'Baseline'); - await prComment.addAdditionalResultsSet( - `Baseline results on branch: ${baseBranch}`, - // We skip the first one here because it's already included as `Baseline` column above in addCurrentResult(). - baseResults - .items() - .slice(1, 10), - ); -} else { - await prComment.addCurrentResult(await ResultsAnalyzer.analyze(latestResult, previousResults), 'Previous'); -} - -await prComment.addAdditionalResultsSet( - `Previous results on branch: ${branch}`, - previousResults.items().slice(0, 10), -); - -await GitHub.addOrUpdateComment(prComment); - -// Copy the latest test run results to the archived result dir. -await previousResults.add(latestResultFile, true); diff --git a/dev-packages/overhead-metrics/configs/dev/collect.ts b/dev-packages/overhead-metrics/configs/dev/collect.ts deleted file mode 100644 index 4b2ffbc5480a..000000000000 --- a/dev-packages/overhead-metrics/configs/dev/collect.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Metrics } from '../../src/collector.js'; -import { MetricsCollector } from '../../src/collector.js'; -import { MetricsStats } from '../../src/results/metrics-stats.js'; -import { BookingAppScenario } from '../../src/scenarios.js'; -import { printStats } from '../../src/util/console.js'; -import { latestResultFile } from './env.js'; - -const collector = new MetricsCollector(); -const result = await collector.execute({ - name: 'dummy', - scenarios: [ - new BookingAppScenario('index.html', 50), - new BookingAppScenario('with-sentry.html', 50), - new BookingAppScenario('with-replay.html', 50), - new BookingAppScenario('index.html', 500), - new BookingAppScenario('with-sentry.html', 500), - new BookingAppScenario('with-replay.html', 500), - ], - runs: 1, - tries: 1, - async shouldAccept(results: Metrics[]): Promise { - printStats(results); - - const cpuUsage = MetricsStats.mean(results, MetricsStats.cpu)!; - if (cpuUsage > 0.9) { - console.error( - `CPU usage too high to be accurate: ${(cpuUsage * 100).toFixed(2)} %.`, - 'Consider simplifying the scenario or changing the CPU throttling factor.', - ); - return false; - } - return true; - }, -}); - -result.writeToFile(latestResultFile); diff --git a/dev-packages/overhead-metrics/configs/dev/env.ts b/dev-packages/overhead-metrics/configs/dev/env.ts deleted file mode 100644 index c2168763ea6e..000000000000 --- a/dev-packages/overhead-metrics/configs/dev/env.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const outDir = 'out/results-dev'; -export const latestResultFile = 'out/latest-result.json'; diff --git a/dev-packages/overhead-metrics/configs/dev/process.ts b/dev-packages/overhead-metrics/configs/dev/process.ts deleted file mode 100644 index 096244b5c750..000000000000 --- a/dev-packages/overhead-metrics/configs/dev/process.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ResultsAnalyzer } from '../../src/results/analyzer.js'; -import { Result } from '../../src/results/result.js'; -import { ResultsSet } from '../../src/results/results-set.js'; -import { printAnalysis } from '../../src/util/console.js'; -import { latestResultFile, outDir } from './env.js'; - -const resultsSet = new ResultsSet(outDir); -const latestResult = Result.readFromFile(latestResultFile); - -const analysis = await ResultsAnalyzer.analyze(latestResult, resultsSet); -printAnalysis(analysis); - -await resultsSet.add(latestResultFile, true); diff --git a/dev-packages/overhead-metrics/package.json b/dev-packages/overhead-metrics/package.json deleted file mode 100644 index 878b29487f54..000000000000 --- a/dev-packages/overhead-metrics/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "private": true, - "version": "8.35.0", - "name": "@sentry-internal/overhead-metrics", - "main": "index.js", - "author": "Sentry", - "license": "MIT", - "type": "module", - "scripts": { - "build": "tsc", - "dev:collect": "ts-node-esm ./configs/dev/collect.ts", - "dev:process": "ts-node-esm ./configs/dev/process.ts", - "dev:run:replay": "npx chrome ./test-apps/booking-app/with-replay.html", - "ci:collect": "ts-node-esm ./configs/ci/collect.ts", - "ci:process": "ts-node-esm ./configs/ci/process.ts", - "fix": "eslint . --format stylish --fix", - "lint": "eslint . --format stylish" - }, - "dependencies": { - "@octokit/rest": "^19.0.5", - "@types/node": "^18.11.17", - "axios": "^1.6.7", - "extract-zip": "^2.0.1", - "filesize": "^10.0.6", - "fs-extra": "^11.1.0", - "p-timeout": "^6.0.0", - "playwright": "^1.44.1", - "playwright-core": "^1.44.1", - "simple-git": "^3.16.0", - "simple-statistics": "^7.8.0", - "typescript": "4.9.5" - }, - "devDependencies": { - "ts-node": "^10.9.1" - } -} diff --git a/dev-packages/overhead-metrics/src/collector.ts b/dev-packages/overhead-metrics/src/collector.ts deleted file mode 100644 index 0cdb80130f80..000000000000 --- a/dev-packages/overhead-metrics/src/collector.ts +++ /dev/null @@ -1,204 +0,0 @@ -import pTimeout from 'p-timeout'; -import * as playwright from 'playwright'; - -import type { CpuUsageSerialized } from './perf/cpu.js'; -import { CpuUsage, CpuUsageSampler } from './perf/cpu.js'; -import type { JsHeapUsageSerialized } from './perf/memory.js'; -import { JsHeapUsage, JsHeapUsageSampler } from './perf/memory.js'; -import type { NetworkUsageSerialized } from './perf/network.js'; -import { NetworkUsage, NetworkUsageCollector } from './perf/network.js'; -import { PerfMetricsSampler } from './perf/sampler.js'; -import { Result } from './results/result.js'; -import type { Scenario, TestCase } from './scenarios.js'; -import { consoleGroup } from './util/console.js'; -import { WebVitals, WebVitalsCollector } from './vitals/index.js'; - -const networkConditions = 'Fast 3G'; - -// Same as puppeteer-core PredefinedNetworkConditions -const PredefinedNetworkConditions = Object.freeze({ - 'Slow 3G': { - download: ((500 * 1000) / 8) * 0.8, - upload: ((500 * 1000) / 8) * 0.8, - latency: 400 * 5, - connectionType: 'cellular3g', - }, - 'Fast 3G': { - download: ((1.6 * 1000 * 1000) / 8) * 0.9, - upload: ((750 * 1000) / 8) * 0.9, - latency: 150 * 3.75, - connectionType: 'cellular3g', - }, -}); - -export class Metrics { - public constructor( - public readonly vitals: WebVitals, - public readonly cpu: CpuUsage, - public readonly memory: JsHeapUsage, - public readonly network: NetworkUsage, - ) {} - - /** - * - */ - public static fromJSON( - data: Partial<{ - vitals: Partial; - cpu: CpuUsageSerialized; - memory: JsHeapUsageSerialized; - network: NetworkUsageSerialized; - }>, - ): Metrics { - return new Metrics( - WebVitals.fromJSON(data.vitals || {}), - CpuUsage.fromJSON(data.cpu || {}), - JsHeapUsage.fromJSON(data.memory || {}), - NetworkUsage.fromJSON(data.network || {}), - ); - } -} - -export interface MetricsCollectorOptions { - headless: boolean; - cpuThrottling: number; -} - -export class MetricsCollector { - private _options: MetricsCollectorOptions; - - public constructor(options?: Partial) { - this._options = { - headless: false, - cpuThrottling: 4, - ...options, - }; - } - - /** - * - */ - public async execute(testCase: TestCase): Promise { - console.log(`Executing test case ${testCase.name}`); - return consoleGroup(async () => { - const scenarioResults: Metrics[][] = []; - for (let s = 0; s < testCase.scenarios.length; s++) { - scenarioResults.push(await this._collect(testCase, s.toString(), testCase.scenarios[s])); - } - return new Result(testCase.name, this._options.cpuThrottling, networkConditions, scenarioResults); - }); - } - - /** - * - */ - private async _collect(testCase: TestCase, name: string, scenario: Scenario): Promise { - const label = `Scenario ${name} data collection (total ${testCase.runs} runs)`; - for (let try_ = 1; try_ <= testCase.tries; try_++) { - console.time(label); - const results: Metrics[] = []; - for (let run = 1; run <= testCase.runs; run++) { - const innerLabel = `Scenario ${name} data collection, run ${run}/${testCase.runs}`; - console.time(innerLabel); - try { - results.push(await this._run(scenario)); - } catch (e) { - console.warn(`${innerLabel} failed with ${e}`); - break; - } finally { - console.timeEnd(innerLabel); - } - } - console.timeEnd(label); - if (results.length == testCase.runs && (await testCase.shouldAccept(results))) { - console.log(`Test case ${testCase.name}, scenario ${name} passed on try ${try_}/${testCase.tries}`); - return results; - } else if (try_ != testCase.tries) { - console.log(`Test case ${testCase.name} failed on try ${try_}/${testCase.tries}, retrying`); - } else { - throw `Test case ${testCase.name}, scenario ${name} failed after ${testCase.tries} tries.`; - } - } - // Unreachable code, if configured properly: - console.assert(testCase.tries >= 1); - return []; - } - - /** - * - */ - private async _run(scenario: Scenario): Promise { - const disposeCallbacks: (() => Promise)[] = []; - try { - return await pTimeout( - (async () => { - const browser = await playwright.chromium.launch({ - headless: this._options.headless, - }); - disposeCallbacks.push(() => browser.close()); - const page = await browser.newPage(); - disposeCallbacks.push(() => page.close()); - - const errorLogs: Array = []; - await page.on('console', message => { - if (message.type() === 'error') errorLogs.push(message.text()); - }); - await page.on('crash', _ => { - errorLogs.push('Page crashed'); - }); - await page.on('pageerror', error => { - errorLogs.push(`${error.name}: ${error.message}`); - }); - - const cdp = await page.context().newCDPSession(page); - - // Simulate throttling. - await cdp.send('Network.emulateNetworkConditions', { - offline: false, - latency: PredefinedNetworkConditions[networkConditions].latency, - uploadThroughput: PredefinedNetworkConditions[networkConditions].upload, - downloadThroughput: PredefinedNetworkConditions[networkConditions].download, - }); - await cdp.send('Emulation.setCPUThrottlingRate', { rate: this._options.cpuThrottling }); - - // Collect CPU and memory info 10 times per second. - const perfSampler = await PerfMetricsSampler.create(cdp, 100); - disposeCallbacks.push(async () => perfSampler.stop()); - const cpuSampler = new CpuUsageSampler(perfSampler); - const memSampler = new JsHeapUsageSampler(perfSampler); - - const networkCollector = await NetworkUsageCollector.create(page); - const vitalsCollector = await WebVitalsCollector.create(page); - - await scenario.run(browser, page); - - // NOTE: FID needs some interaction to actually show a value - const vitals = await vitalsCollector.collect(); - - if (errorLogs.length > 0) { - throw `Error logs in browser console:\n\t\t${errorLogs.join('\n\t\t')}`; - } - - return new Metrics(vitals, cpuSampler.getData(), memSampler.getData(), networkCollector.getData()); - })(), - { milliseconds: 60 * 1000 }, - ); - } finally { - console.log('Disposing of browser and resources'); - disposeCallbacks.reverse(); - const errors = []; - for (const cb of disposeCallbacks) { - try { - await cb(); - } catch (e) { - errors.push(e instanceof Error ? `${e.name}: ${e.message}` : `${e}`); - } - } - if (errors.length > 0) { - console.warn(`All disposose callbacks have finished. Errors: ${errors}`); - } else { - console.warn('All disposose callbacks have finished.'); - } - } - } -} diff --git a/dev-packages/overhead-metrics/src/perf/cpu.ts b/dev-packages/overhead-metrics/src/perf/cpu.ts deleted file mode 100644 index 2ec17c1866fa..000000000000 --- a/dev-packages/overhead-metrics/src/perf/cpu.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { JsonObject } from '../util/json.js'; -import type { PerfMetrics, PerfMetricsSampler } from './sampler.js'; -import { TimeBasedMap } from './sampler.js'; - -export { CpuUsageSampler, CpuUsage }; - -export type CpuUsageSerialized = Partial<{ snapshots: JsonObject; average: number }>; - -class CpuUsage { - public constructor(public snapshots: TimeBasedMap, public average: number) {} - - public static fromJSON(data: CpuUsageSerialized): CpuUsage { - return new CpuUsage(TimeBasedMap.fromJSON(data.snapshots || {}), data.average as number); - } -} - -class MetricsDataPoint { - public constructor(public timestamp: number, public activeTime: number) {} -} - -class CpuUsageSampler { - private _snapshots: TimeBasedMap = new TimeBasedMap(); - private _average: number = 0; - private _initial?: MetricsDataPoint = undefined; - private _startTime!: number; - private _lastTimestamp!: number; - private _cumulativeActiveTime!: number; - - public constructor(sampler: PerfMetricsSampler) { - sampler.subscribe(this._collect.bind(this)); - } - - public getData(): CpuUsage { - return new CpuUsage(this._snapshots, this._average); - } - - private async _collect(metrics: PerfMetrics): Promise { - const data = new MetricsDataPoint(metrics.Timestamp, metrics.Duration); - if (this._initial == undefined) { - this._initial = data; - this._startTime = data.timestamp; - } else { - const frameDuration = data.timestamp - this._lastTimestamp; - const usage = frameDuration == 0 ? 0 : (data.activeTime - this._cumulativeActiveTime) / frameDuration; - - this._snapshots.set(data.timestamp, usage); - this._average = data.activeTime / (data.timestamp - this._startTime); - } - this._lastTimestamp = data.timestamp; - this._cumulativeActiveTime = data.activeTime; - } -} diff --git a/dev-packages/overhead-metrics/src/perf/memory.ts b/dev-packages/overhead-metrics/src/perf/memory.ts deleted file mode 100644 index 561d5d7ebd99..000000000000 --- a/dev-packages/overhead-metrics/src/perf/memory.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { JsonObject } from '../util/json.js'; -import type { PerfMetrics, PerfMetricsSampler } from './sampler.js'; -import { TimeBasedMap } from './sampler.js'; - -export { JsHeapUsageSampler, JsHeapUsage }; - -export type JsHeapUsageSerialized = Partial<{ snapshots: JsonObject }>; - -class JsHeapUsage { - public constructor(public snapshots: TimeBasedMap) {} - - public static fromJSON(data: JsHeapUsageSerialized): JsHeapUsage { - return new JsHeapUsage(TimeBasedMap.fromJSON(data.snapshots || {})); - } -} - -class JsHeapUsageSampler { - private _snapshots: TimeBasedMap = new TimeBasedMap(); - - public constructor(sampler: PerfMetricsSampler) { - sampler.subscribe(this._collect.bind(this)); - } - - public getData(): JsHeapUsage { - return new JsHeapUsage(this._snapshots); - } - - private async _collect(metrics: PerfMetrics): Promise { - this._snapshots.set(metrics.Timestamp, metrics.JSHeapUsedSize!); - } -} diff --git a/dev-packages/overhead-metrics/src/perf/network.ts b/dev-packages/overhead-metrics/src/perf/network.ts deleted file mode 100644 index 03b76d2fcc4d..000000000000 --- a/dev-packages/overhead-metrics/src/perf/network.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type * as playwright from 'playwright'; - -export class NetworkEvent { - public constructor( - public url: string | undefined, - public requestSize: number | undefined, - public responseSize: number | undefined, - public requestTimeNs: bigint | undefined, - public responseTimeNs: bigint | undefined, - ) {} - - /** - * - */ - public static fromJSON(data: Partial): NetworkEvent { - return new NetworkEvent( - data.url as string, - data.requestSize as number, - data.responseSize as number, - data.requestTimeNs == undefined ? undefined : BigInt(data.requestTimeNs), - data.responseTimeNs == undefined ? undefined : BigInt(data.responseTimeNs), - ); - } -} - -export type NetworkUsageSerialized = Partial<{ events: Array }>; - -export class NetworkUsage { - public constructor(public events: Array) {} - - /** - * - */ - public static fromJSON(data: NetworkUsageSerialized): NetworkUsage { - return new NetworkUsage(data.events?.map(e => NetworkEvent.fromJSON(e)) || []); - } -} - -export class NetworkUsageCollector { - private _events = new Array(); - - /** - * - */ - public static async create(page: playwright.Page): Promise { - const self = new NetworkUsageCollector(); - await page.route(_ => true, self._captureRequest.bind(self)); - return self; - } - - /** - * - */ - public getData(): NetworkUsage { - return new NetworkUsage(this._events); - } - - /** - * - */ - private async _captureRequest(route: playwright.Route, request: playwright.Request): Promise { - const url = request.url(); - try { - const event = new NetworkEvent( - url, - request.postDataBuffer()?.length, - undefined, - process.hrtime.bigint(), - undefined, - ); - this._events.push(event); - // Note: playwright would error out on file:/// requests. They are used to access local test app resources. - if (url.startsWith('file:///')) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - route.continue(); - } else { - const response = await route.fetch(); - const body = await response.body(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - route.fulfill({ response, body }); - event.responseTimeNs = process.hrtime.bigint(); - event.responseSize = body.length; - } - } catch (e) { - console.log(`Error when capturing request: ${request.method()} ${url} - ${e}`); - } - } -} diff --git a/dev-packages/overhead-metrics/src/perf/sampler.ts b/dev-packages/overhead-metrics/src/perf/sampler.ts deleted file mode 100644 index 1c5c631f231a..000000000000 --- a/dev-packages/overhead-metrics/src/perf/sampler.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type * as playwright from 'playwright'; -import type { Protocol } from 'playwright-core/types/protocol'; - -import type { JsonObject } from '../util/json'; - -export type PerfMetricsConsumer = (metrics: PerfMetrics) => Promise; -export type TimestampSeconds = number; - -export class TimeBasedMap extends Map { - /** - * - */ - public static fromJSON(entries: JsonObject): TimeBasedMap { - const result = new TimeBasedMap(); - // eslint-disable-next-line guard-for-in - for (const key in entries) { - result.set(parseFloat(key), entries[key]); - } - return result; - } - - /** - * - */ - public toJSON(): JsonObject { - return Object.fromEntries(this.entries()); - } -} - -export class PerfMetrics { - public constructor(private _metrics: Protocol.Performance.Metric[]) {} - - /** - * - */ - public get Timestamp(): number { - return this._find('Timestamp'); - } - - /** - * - */ - public get Duration(): number { - return this._find('TaskDuration'); - } - - /** - * - */ - public get JSHeapUsedSize(): number { - return this._find('JSHeapUsedSize'); - } - - /** - * - */ - private _find(name: string): number { - return this._metrics.find(metric => metric.name == name)!.value; - } -} - -export class PerfMetricsSampler { - private _consumers: PerfMetricsConsumer[] = []; - private _timer!: NodeJS.Timer; - private _errorPrinted: boolean = false; - - private constructor(private _cdp: playwright.CDPSession) {} - - /** - * - */ - public static async create(cdp: playwright.CDPSession, interval: number): Promise { - const self = new PerfMetricsSampler(cdp); - await cdp.send('Performance.enable', { timeDomain: 'timeTicks' }); - - // collect first sample immediately - self._collectSample(); - - // and set up automatic collection in the given interval - self._timer = setInterval(self._collectSample.bind(self), interval); - - return self; - } - - /** - * - */ - public subscribe(consumer: PerfMetricsConsumer): void { - this._consumers.push(consumer); - } - - /** - * - */ - public stop(): void { - clearInterval(this._timer); - } - - /** - * - */ - private _collectSample(): void { - this._cdp.send('Performance.getMetrics').then( - response => { - const metrics = new PerfMetrics(response.metrics); - this._consumers.forEach(cb => cb(metrics).catch(console.error)); - }, - e => { - // This happens if the browser closed unexpectedly. No reason to try again. - if (!this._errorPrinted) { - this._errorPrinted = true; - console.log(e); - this.stop(); - } - }, - ); - } -} diff --git a/dev-packages/overhead-metrics/src/results/analyzer.ts b/dev-packages/overhead-metrics/src/results/analyzer.ts deleted file mode 100644 index f27356f4d507..000000000000 --- a/dev-packages/overhead-metrics/src/results/analyzer.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { filesize } from 'filesize'; - -import type { GitHash } from '../util/git.js'; -import { JsonStringify } from '../util/json.js'; -import type { AnalyticsFunction, NumberProvider } from './metrics-stats.js'; -import { MetricsStats } from './metrics-stats.js'; -import type { Result } from './result.js'; -import type { ResultsSet } from './results-set.js'; - -// Compares latest result to previous/baseline results and produces the needed info. - -export class ResultsAnalyzer { - private constructor(private _result: Result) {} - - /** - * - */ - public static async analyze(currentResult: Result, baselineResults?: ResultsSet): Promise { - const items = new ResultsAnalyzer(currentResult)._collect(); - - const baseline = baselineResults?.find( - other => - other.cpuThrottling == currentResult.cpuThrottling && - other.name == currentResult.name && - other.networkConditions == currentResult.networkConditions && - JsonStringify(other) != JsonStringify(currentResult), - ); - - let otherHash: GitHash | undefined; - if (baseline != undefined) { - otherHash = baseline[0]; - const baseItems = new ResultsAnalyzer(baseline[1])._collect(); - // update items with baseline results - for (const base of baseItems) { - for (const item of items) { - if (item.metric == base.metric) { - item.others = base.values; - } - } - } - } - - return { - items: items, - otherHash: otherHash, - }; - } - - /** - * - */ - private _collect(): AnalyzerItem[] { - const items = new Array(); - - const scenarioResults = this._result.scenarioResults; - - const pushIfDefined = function ( - metric: AnalyzerItemMetric, - unit: AnalyzerItemUnit, - source: NumberProvider, - fn: AnalyticsFunction, - ): void { - const values = scenarioResults.map(items => fn(items, source)); - // only push if at least one value is defined - if (values.findIndex(v => v != undefined) >= 0) { - items.push({ - metric: metric, - values: new AnalyzerItemNumberValues(unit, values), - }); - } - }; - - pushIfDefined(AnalyzerItemMetric.lcp, AnalyzerItemUnit.ms, MetricsStats.lcp, MetricsStats.mean); - pushIfDefined(AnalyzerItemMetric.cls, AnalyzerItemUnit.ms, MetricsStats.cls, MetricsStats.mean); - pushIfDefined(AnalyzerItemMetric.cpu, AnalyzerItemUnit.ratio, MetricsStats.cpu, MetricsStats.mean); - pushIfDefined(AnalyzerItemMetric.memoryAvg, AnalyzerItemUnit.bytes, MetricsStats.memoryMean, MetricsStats.mean); - pushIfDefined(AnalyzerItemMetric.memoryMax, AnalyzerItemUnit.bytes, MetricsStats.memoryMax, MetricsStats.max); - pushIfDefined(AnalyzerItemMetric.netTx, AnalyzerItemUnit.bytes, MetricsStats.netTx, MetricsStats.mean); - pushIfDefined(AnalyzerItemMetric.netRx, AnalyzerItemUnit.bytes, MetricsStats.netRx, MetricsStats.mean); - pushIfDefined(AnalyzerItemMetric.netCount, AnalyzerItemUnit.integer, MetricsStats.netCount, MetricsStats.mean); - pushIfDefined(AnalyzerItemMetric.netTime, AnalyzerItemUnit.ms, MetricsStats.netTime, MetricsStats.mean); - - return items; - } -} - -export enum AnalyzerItemUnit { - ms, - ratio, // 1.0 == 100 % - bytes, - integer, -} - -export interface AnalyzerItemValues { - value(index: number): string; - diff(aIndex: number, bIndex: number): string; - percent(aIndex: number, bIndex: number): string; -} - -const AnalyzerItemValueNotAvailable = 'n/a'; - -class AnalyzerItemNumberValues implements AnalyzerItemValues { - public constructor(private _unit: AnalyzerItemUnit, private _values: (number | undefined)[]) {} - - public value(index: number): string { - if (!this._has(index)) return AnalyzerItemValueNotAvailable; - return this._withUnit(this._get(index)); - } - - public diff(aIndex: number, bIndex: number): string { - if (!this._has(aIndex) || !this._has(bIndex)) return AnalyzerItemValueNotAvailable; - const diff = this._get(bIndex) - this._get(aIndex); - const str = this._withUnit(diff, true); - return diff > 0 ? `+${str}` : str; - } - - public percent(aIndex: number, bIndex: number): string { - if (!this._has(aIndex) || !this._has(bIndex) || this._get(aIndex) == 0.0) return AnalyzerItemValueNotAvailable; - const percent = (this._get(bIndex) / this._get(aIndex)) * 100 - 100; - const str = `${percent.toFixed(2)} %`; - return percent > 0 ? `+${str}` : str; - } - - private _has(index: number): boolean { - return index >= 0 && index < this._values.length && this._values[index] != undefined; - } - - private _get(index: number): number { - return this._values[index]!; - } - - private _withUnit(value: number, isDiff: boolean = false): string { - switch (this._unit) { - case AnalyzerItemUnit.bytes: - return filesize(value) as string; - case AnalyzerItemUnit.ratio: - return `${(value * 100).toFixed(2)} ${isDiff ? 'pp' : '%'}`; - case AnalyzerItemUnit.integer: - return `${value}`; - default: - return `${value.toFixed(2)} ${AnalyzerItemUnit[this._unit]}`; - } - } -} - -export enum AnalyzerItemMetric { - lcp, - cls, - cpu, - memoryAvg, - memoryMax, - netTx, - netRx, - netCount, - netTime, -} - -export interface AnalyzerItem { - metric: AnalyzerItemMetric; - - // Current (latest) result. - values: AnalyzerItemValues; - - // Previous or baseline results, depending on the context. - others?: AnalyzerItemValues; -} - -export interface Analysis { - items: AnalyzerItem[]; - - // Commit hash that the the previous or baseline (depending on the context) result was collected for. - otherHash?: GitHash; -} diff --git a/dev-packages/overhead-metrics/src/results/metrics-stats.ts b/dev-packages/overhead-metrics/src/results/metrics-stats.ts deleted file mode 100644 index 2ccab6632905..000000000000 --- a/dev-packages/overhead-metrics/src/results/metrics-stats.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as ss from 'simple-statistics'; - -import type { Metrics } from '../collector'; - -export type NumberProvider = (metrics: Metrics) => number | undefined; -export type AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => number | undefined; - -export class MetricsStats { - public static lcp: NumberProvider = metrics => metrics.vitals.lcp; - public static cls: NumberProvider = metrics => metrics.vitals.cls; - public static cpu: NumberProvider = metrics => metrics.cpu.average; - public static memoryMean: NumberProvider = metrics => ss.mean(Array.from(metrics.memory.snapshots.values())); - public static memoryMax: NumberProvider = metrics => ss.max(Array.from(metrics.memory.snapshots.values())); - public static netTx: NumberProvider = metrics => ss.sum(metrics.network.events.map(e => e.requestSize || 0)); - public static netRx: NumberProvider = metrics => ss.sum(metrics.network.events.map(e => e.responseSize || 0)); - public static netCount: NumberProvider = metrics => - ss.sum(metrics.network.events.map(e => (e.requestTimeNs && e.responseTimeNs ? 1 : 0))); - public static netTime: NumberProvider = metrics => - ss.sum( - metrics.network.events.map(e => - e.requestTimeNs && e.responseTimeNs ? Number(e.responseTimeNs - e.requestTimeNs) / 1e6 : 0, - ), - ); - - public static mean: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { - const numbers = MetricsStats._filteredValues(MetricsStats._collect(items, dataProvider)); - return numbers.length > 0 ? ss.mean(numbers) : undefined; - }; - - public static max: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { - const numbers = MetricsStats._filteredValues(MetricsStats._collect(items, dataProvider)); - return numbers.length > 0 ? ss.max(numbers) : undefined; - }; - - public static stddev: AnalyticsFunction = (items: Metrics[], dataProvider: NumberProvider) => { - const numbers = MetricsStats._filteredValues(MetricsStats._collect(items, dataProvider)); - return numbers.length > 0 ? ss.standardDeviation(numbers) : undefined; - }; - - /** - * - */ - private static _collect(items: Metrics[], dataProvider: NumberProvider): number[] { - return items.map(dataProvider).filter(v => v != undefined && !Number.isNaN(v)) as number[]; - } - - // See https://en.wikipedia.org/wiki/Interquartile_range#Outliers for details on filtering. - /** - * - */ - private static _filteredValues(numbers: number[]): number[] { - numbers.sort((a, b) => a - b); - - if (numbers.length < 1) { - return []; - } - - const q1 = ss.quantileSorted(numbers, 0.25); - const q3 = ss.quantileSorted(numbers, 0.75); - const iqr = q3 - q1; - - return numbers.filter(num => num >= q1 - 1.5 * iqr && num <= q3 + 1.5 * iqr); - } -} diff --git a/dev-packages/overhead-metrics/src/results/pr-comment.ts b/dev-packages/overhead-metrics/src/results/pr-comment.ts deleted file mode 100644 index cd81d54dce20..000000000000 --- a/dev-packages/overhead-metrics/src/results/pr-comment.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Git } from '../util/git.js'; -import type { Analysis, AnalyzerItemValues } from './analyzer.js'; -import { AnalyzerItemMetric, ResultsAnalyzer } from './analyzer.js'; -import { Result } from './result.js'; -import type { ResultSetItem } from './results-set.js'; - -function trimIndent(str: string): string { - return str - .trim() - .split('\n') - .map(s => s.trim()) - .join('\n'); -} - -function printableMetricName(metric: AnalyzerItemMetric): string { - switch (metric) { - case AnalyzerItemMetric.lcp: - return '
LCP'; - case AnalyzerItemMetric.cls: - return 'CLS'; - case AnalyzerItemMetric.cpu: - return 'CPU'; - case AnalyzerItemMetric.memoryAvg: - return 'JS heap avg'; - case AnalyzerItemMetric.memoryMax: - return 'JS heap max'; - default: - return AnalyzerItemMetric[metric]; - } -} - -export class PrCommentBuilder { - private _buffer: string = ''; - - /** - * - */ - public get title(): string { - return 'Replay SDK metrics :rocket:'; - } - - /** - * - */ - public get body(): string { - const now = new Date(); - return trimIndent(` - ${this._buffer} -
-
- *) pp - percentage points - an absolute difference between two percentages.
- Last updated: -
- `); - } - - /** - * - */ - public async addCurrentResult(analysis: Analysis, otherName: string): Promise { - // Decides whether to print the "Other" for comparison depending on it being set in the input data. - const hasOther = analysis.otherHash != undefined; - const maybeOther = function (content: () => string): string { - return hasOther ? content() : ''; - }; - - const currentHash = await Git.hash; - - this._buffer += `

${this.title}

`; - if (!hasOther) { - this._buffer += `Latest data for: ${currentHash}`; - } - this._buffer += ` - - - - - ${maybeOther(() => '')} - - - - - - ${maybeOther(() => '')} - - - - - - - - `; - - const valueColumns = function (values: AnalyzerItemValues): string { - return ` - - - - - - - - `; - }; - - for (const item of analysis.items) { - if (hasOther) { - this._buffer += ` - - - - - `; - } else { - this._buffer += ` - - - ${valueColumns(item.values)} - `; - } - } - - this._buffer += ` -
  Plain+Sentry+Replay
RevisionValueValueDiffRatioValueDiffRatio
${values.value(0)}${values.value(1)}${values.diff(0, 1)}${values.percent(0, 1)}${values.value(2)}${values.diff(0, 2)}${values.percent(0, 2)}
${printableMetricName(item.metric)}This PR ${currentHash} - ${valueColumns(item.values)} -
${otherName} ${analysis.otherHash} - ${valueColumns(item.others!)} -
${printableMetricName(item.metric)}
`; - } - - /** - * - */ - public async addAdditionalResultsSet(name: string, resultFiles: ResultSetItem[]): Promise { - if (resultFiles.length == 0) return; - - this._buffer += ` -
-

${name}

- `; - - // Each `resultFile` will be printed as a single row - with metrics as table columns. - for (let i = 0; i < resultFiles.length; i++) { - const resultFile = resultFiles[i]; - // Load the file and "analyse" - collect stats we want to print. - const analysis = await ResultsAnalyzer.analyze(Result.readFromFile(resultFile.path)); - - if (i == 0) { - // Add table header - this._buffer += ''; - for (const item of analysis.items) { - this._buffer += ``; - } - this._buffer += ''; - } - - // Add table row - this._buffer += ``; - for (const item of analysis.items) { - // TODO maybe find a better way of showing this. After the change to multiple scenarios, this shows diff between "With Sentry" and "With Sentry + Replay" - this._buffer += ``; - } - this._buffer += ''; - } - - this._buffer += ` -
Revision${printableMetricName(item.metric)}
${resultFile.hash}${item.values.diff(0, 2)}
-
`; - } -} diff --git a/dev-packages/overhead-metrics/src/results/result.ts b/dev-packages/overhead-metrics/src/results/result.ts deleted file mode 100644 index 3794e0163c39..000000000000 --- a/dev-packages/overhead-metrics/src/results/result.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as fs from 'fs'; -import path from 'path'; - -import { Metrics } from '../collector.js'; -import type { JsonObject } from '../util/json.js'; -import { JsonStringify } from '../util/json.js'; - -export class Result { - public constructor( - public readonly name: string, - public readonly cpuThrottling: number, - public readonly networkConditions: string, - public readonly scenarioResults: Metrics[][], - ) {} - - /** - * - */ - public static readFromFile(filePath: string): Result { - const json = fs.readFileSync(filePath, { encoding: 'utf-8' }); - const data = JSON.parse(json) as JsonObject; - return new Result( - data.name as string, - data.cpuThrottling as number, - data.networkConditions as string, - ((data.scenarioResults as Partial[][]) || []).map(list => list.map(Metrics.fromJSON.bind(Metrics))), - ); - } - - /** - * - */ - public writeToFile(filePath: string): void { - const dir = path.dirname(filePath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - const json = JsonStringify(this); - fs.writeFileSync(filePath, json); - } -} diff --git a/dev-packages/overhead-metrics/src/results/results-set.ts b/dev-packages/overhead-metrics/src/results/results-set.ts deleted file mode 100644 index 9eb65ff3cb4f..000000000000 --- a/dev-packages/overhead-metrics/src/results/results-set.ts +++ /dev/null @@ -1,116 +0,0 @@ -import assert from 'assert'; -import * as fs from 'fs'; -import path from 'path'; - -import type { GitHash } from '../util/git.js'; -import { Git } from '../util/git.js'; -import { Result } from './result.js'; - -const delimiter = '-'; - -export class ResultSetItem { - public constructor(public path: string) {} - - /** - * - */ - public get name(): string { - return path.basename(this.path); - } - - /** - * - */ - public get number(): number { - return parseInt(this.parts[0]); - } - - /** - * - */ - public get hash(): GitHash { - return this.parts[1]; - } - - /** - * - */ - public get parts(): string[] { - return path.basename(this.path).split(delimiter); - } -} - -/// Wraps a directory containing multiple (N--result.json) files. -/// The files are numbered from the most recently added one, to the oldest one. - -export class ResultsSet { - public constructor(private _directory: string) { - if (!fs.existsSync(_directory)) { - fs.mkdirSync(_directory, { recursive: true }); - } - } - - /** - * - */ - public find(predicate: (value: Result) => boolean): [GitHash, Result] | undefined { - for (const item of this.items()) { - const result = Result.readFromFile(item.path); - if (predicate(result)) { - return [item.hash, result]; - } - } - return undefined; - } - - /** - * - */ - public items(): ResultSetItem[] { - return this._files() - .map(file => { - return new ResultSetItem(path.join(this._directory, file.name)); - }) - .filter(item => !isNaN(item.number)) - .sort((a, b) => a.number - b.number); - } - - /** - * - */ - public async add(newFile: string, onlyIfDifferent: boolean = false): Promise { - console.log(`Preparing to add ${newFile} to ${this._directory}`); - assert(fs.existsSync(newFile)); - - // Get the list of file sorted by the prefix number in the descending order (starting with the oldest files). - const files = this.items().sort((a, b) => b.number - a.number); - - if (onlyIfDifferent && files.length > 0) { - const latestFile = files[files.length - 1]; - if (fs.readFileSync(latestFile.path, { encoding: 'utf-8' }) == fs.readFileSync(newFile, { encoding: 'utf-8' })) { - console.log(`Skipping - it's already stored as ${latestFile.name}`); - return; - } - } - - // Rename all existing files, increasing the prefix - for (const file of files) { - const parts = file.name.split(delimiter); - parts[0] = (file.number + 1).toString(); - const newPath = path.join(this._directory, parts.join(delimiter)); - console.log(`Renaming ${file.path} to ${newPath}`); - fs.renameSync(file.path, newPath); - } - - const newName = `1${delimiter}${await Git.hash}${delimiter}result.json`; - console.log(`Adding ${newFile} to ${this._directory} as ${newName}`); - fs.copyFileSync(newFile, path.join(this._directory, newName)); - } - - /** - * - */ - private _files(): fs.Dirent[] { - return fs.readdirSync(this._directory, { withFileTypes: true }).filter(v => v.isFile()); - } -} diff --git a/dev-packages/overhead-metrics/src/scenarios.ts b/dev-packages/overhead-metrics/src/scenarios.ts deleted file mode 100644 index f4d59cf06d53..000000000000 --- a/dev-packages/overhead-metrics/src/scenarios.ts +++ /dev/null @@ -1,80 +0,0 @@ -import assert from 'assert'; -import * as fs from 'fs'; -import path from 'path'; -import type * as playwright from 'playwright'; - -import type { Metrics } from './collector'; - -// A testing scenario we want to collect metrics for. -export interface Scenario { - run(browser: playwright.Browser, page: playwright.Page): Promise; -} - -// Two scenarios that are compared to each other. -export interface TestCase { - name: string; - scenarios: Scenario[]; - runs: number; - tries: number; - - // Test function that will be executed and given a scenarios result set with exactly `runs` number of items. - // Should returns true if this "try" should be accepted and collected. - // If false is returned, `Collector` will retry up to `tries` number of times. - shouldAccept(results: Metrics[]): Promise; -} - -// A simple scenario that just loads the given URL. - -export class LoadPageScenario implements Scenario { - public constructor(public url: string) {} - - /** - * - */ - public async run(_: playwright.Browser, page: playwright.Page): Promise { - await page.goto(this.url, { waitUntil: 'load', timeout: 60000 }); - } -} - -// Loads test-apps/jank/ as a page source & waits for a short time before quitting. - -export class JankTestScenario implements Scenario { - public constructor(private _indexFile: string) {} - - /** - * - */ - public async run(_: playwright.Browser, page: playwright.Page): Promise { - let url = path.resolve(`./test-apps/jank/${this._indexFile}`); - assert(fs.existsSync(url)); - url = `file:///${url.replace(/\\/g, '/')}`; - console.log('Navigating to ', url); - await page.goto(url, { waitUntil: 'load', timeout: 60000 }); - await new Promise(resolve => setTimeout(resolve, 12000)); - } -} - -export class BookingAppScenario implements Scenario { - public constructor(private _indexFile: string, private _count: number) {} - - /** - * - */ - public async run(_: playwright.Browser, page: playwright.Page): Promise { - let url = path.resolve(`./test-apps/booking-app/${this._indexFile}`); - assert(fs.existsSync(url)); - url = `file:///${url.replace(/\\/g, '/')}?count=${this._count}`; - console.log('Navigating to ', url); - await page.goto(url, { waitUntil: 'load', timeout: 60000 }); - - // Click "Update" - await page.locator('#search button').click(); - - for (let i = 1; i < 10; i++) { - await page.locator(`.result:nth-child(${i}) [data-select]`).click(); - } - - // Wait for flushing, which we set to 2000ms - to be safe, we add 1s on top - await new Promise(resolve => setTimeout(resolve, 3000)); - } -} diff --git a/dev-packages/overhead-metrics/src/util/console.ts b/dev-packages/overhead-metrics/src/util/console.ts deleted file mode 100644 index b3343e05ffae..000000000000 --- a/dev-packages/overhead-metrics/src/util/console.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { filesize } from 'filesize'; - -import type { Metrics } from '../collector.js'; -import type { Analysis } from '../results/analyzer.js'; -import { AnalyzerItemMetric } from '../results/analyzer.js'; -import { MetricsStats } from '../results/metrics-stats.js'; - -export async function consoleGroup(code: () => Promise): Promise { - console.group(); - return code().finally(console.groupEnd); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type PrintableTable = { [k: string]: any }; - -export function printStats(items: Metrics[]): void { - console.table({ - lcp: `${MetricsStats.mean(items, MetricsStats.lcp)?.toFixed(2)} ms`, - cls: `${MetricsStats.mean(items, MetricsStats.cls)?.toFixed(2)} ms`, - cpu: `${((MetricsStats.mean(items, MetricsStats.cpu) || 0) * 100).toFixed(2)} %`, - memoryMean: filesize(MetricsStats.mean(items, MetricsStats.memoryMean)), - memoryMax: filesize(MetricsStats.max(items, MetricsStats.memoryMax)), - netTx: filesize(MetricsStats.mean(items, MetricsStats.netTx)), - netRx: filesize(MetricsStats.mean(items, MetricsStats.netRx)), - netCount: MetricsStats.mean(items, MetricsStats.netCount), - netTime: `${MetricsStats.mean(items, MetricsStats.netTime)?.toFixed(2)} ms`, - }); -} - -export function printAnalysis(analysis: Analysis): void { - const table: PrintableTable = {}; - for (const item of analysis.items) { - table[AnalyzerItemMetric[item.metric]] = { - value: item.values.value(0), - withSentry: item.values.diff(0, 1), - withReplay: item.values.diff(0, 2), - ...(item.others == undefined - ? {} - : { - previous: item.others.value(0), - previousWithSentry: item.others.diff(0, 1), - previousWithReplay: item.others.diff(0, 2), - }), - }; - } - console.table(table); -} diff --git a/dev-packages/overhead-metrics/src/util/git.ts b/dev-packages/overhead-metrics/src/util/git.ts deleted file mode 100644 index 6882c2f213b5..000000000000 --- a/dev-packages/overhead-metrics/src/util/git.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { simpleGit } from 'simple-git'; - -export type GitHash = string; -const git = simpleGit(); - -async function defaultBranch(): Promise { - const remoteInfo = (await git.remote(['show', 'origin'])) as string; - for (let line of remoteInfo.split('\n')) { - line = line.trim(); - if (line.startsWith('HEAD branch:')) { - return line.substring('HEAD branch:'.length).trim(); - } - } - throw "Couldn't find base branch name"; -} - -export const Git = { - get repository(): Promise { - return (async () => { - if (typeof process.env.GITHUB_REPOSITORY == 'string' && process.env.GITHUB_REPOSITORY.length > 0) { - return `github.com/${process.env.GITHUB_REPOSITORY}`; - } else { - let url = (await git.remote(['get-url', 'origin'])) as string; - url = url.trim(); - url = url.replace(/^git@/, ''); - url = url.replace(/\.git$/, ''); - return url.replace(':', '/'); - } - })(); - }, - - get branch(): Promise { - return (async () => { - if (typeof process.env.GITHUB_HEAD_REF == 'string' && process.env.GITHUB_HEAD_REF.length > 0) { - return process.env.GITHUB_HEAD_REF; - } else if (typeof process.env.GITHUB_REF == 'string' && process.env.GITHUB_REF.startsWith('refs/heads/')) { - return process.env.GITHUB_REF.substring('refs/heads/'.length); - } else { - const branches = (await git.branchLocal()).branches; - for (const name in branches) { - if (branches[name].current) return name; - } - throw "Couldn't find current branch name"; - } - })(); - }, - - get baseBranch(): Promise { - if (typeof process.env.GITHUB_BASE_REF == 'string' && process.env.GITHUB_BASE_REF.length > 0) { - return Promise.resolve(process.env.GITHUB_BASE_REF); - } else { - return defaultBranch(); - } - }, - - get branchIsBase(): Promise { - return (async () => { - const branch = await this.branch; - const baseBranch = await this.baseBranch; - - return branch === baseBranch; - })(); - }, - - get hash(): Promise { - return (async () => { - let gitHash = await git.revparse('HEAD'); - const diff = await git.diff(); - if (diff.trim().length > 0) { - gitHash += '+dirty'; - } - return gitHash; - })(); - }, -}; diff --git a/dev-packages/overhead-metrics/src/util/github.ts b/dev-packages/overhead-metrics/src/util/github.ts deleted file mode 100644 index 707d9529f8ae..000000000000 --- a/dev-packages/overhead-metrics/src/util/github.ts +++ /dev/null @@ -1,205 +0,0 @@ -import * as fs from 'fs'; -import path from 'path'; -import { Octokit } from '@octokit/rest'; -import axios from 'axios'; -import extract from 'extract-zip'; - -import type { PrCommentBuilder } from '../results/pr-comment.js'; -import { consoleGroup } from './console.js'; -import { Git } from './git.js'; - -const octokit = new Octokit({ - auth: process.env.GITHUB_TOKEN, - // log: console, -}); - -const [, owner, repo] = (await Git.repository).split('/') as [string, string, string]; -const defaultArgs = { owner, repo }; - -async function downloadArtifact(url: string, path: string): Promise { - const writer = fs.createWriteStream(path); - return axios({ - method: 'get', - url: url, - responseType: 'stream', - headers: { - Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, - }, - }).then(response => { - return new Promise((resolve, reject) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - response.data.pipe(writer); - let error: Error; - writer.on('error', err => { - error = err; - writer.close(); - reject(err); - }); - writer.on('close', () => { - if (!error) resolve(); - }); - }); - }); -} - -async function tryAddOrUpdateComment(commentBuilder: PrCommentBuilder): Promise { - /* Env var GITHUB_REF is only set if a branch or tag is available for the current CI event trigger type. - The ref given is fully-formed, meaning that - * for branches the format is refs/heads/, - * for pull requests it is refs/pull//merge, - * and for tags it is refs/tags/. - For example, refs/heads/feature-branch-1. - */ - let prNumber: number | undefined; - const githubRef = process.env.GITHUB_REF; - if (typeof githubRef == 'string' && githubRef.length > 0 && githubRef.startsWith('refs/pull/')) { - prNumber = parseInt(githubRef.split('/')[2] as string); - console.log(`Determined PR number ${prNumber} based on GITHUB_REF environment variable: '${githubRef}'`); - } else if (!(await Git.branchIsBase)) { - prNumber = ( - await octokit.rest.pulls.list({ - ...defaultArgs, - base: await Git.baseBranch, - head: await Git.branch, - }) - ).data[0]?.number; - if (prNumber != undefined) { - console.log(`Found PR number ${prNumber} based on base and head branches`); - } - } - - if (prNumber == undefined) return false; - - // Determine the PR comment author: - // Trying to fetch `octokit.users.getAuthenticated()` throws (in CI only): - // {"message":"Resource not accessible by integration","documentation_url":"https://docs.github.com/rest/reference/users#get-the-authenticated-user"} - // Let's make this conditional on some env variable that's unlikely to be set locally but will be set in GH Actions. - // Do not use "CI" because that's commonly set during local development and testing. - const author = - typeof process.env.GITHUB_ACTION == 'string' - ? 'github-actions[bot]' - : (await octokit.users.getAuthenticated()).data.login; - - // Try to find an existing comment by the author and title. - const comment = await (async () => { - for await (const comments of octokit.paginate.iterator(octokit.rest.issues.listComments, { - ...defaultArgs, - issue_number: prNumber, - })) { - const found = comments.data.find(comment => { - return ( - comment.user?.login == author && comment.body != undefined && comment.body.indexOf(commentBuilder.title) >= 0 - ); - }); - if (found) return found; - } - return undefined; - })(); - - if (comment != undefined) { - console.log(`Updating PR comment ${comment.html_url} body`); - await octokit.rest.issues.updateComment({ - ...defaultArgs, - comment_id: comment.id, - body: commentBuilder.body, - }); - } else { - console.log(`Adding a new comment to PR ${prNumber}`); - await octokit.rest.issues.createComment({ - ...defaultArgs, - issue_number: prNumber, - body: commentBuilder.body, - }); - } - - return true; -} - -export const GitHub = { - writeOutput(name: string, value: string): void { - if (typeof process.env.GITHUB_OUTPUT == 'string' && process.env.GITHUB_OUTPUT.length > 0) { - fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`); - } - console.log(`Output ${name} = ${value}`); - }, - - downloadPreviousArtifact(branch: string, targetDir: string, artifactName: string): Promise { - console.log(`Trying to download previous artifact '${artifactName}' for branch '${branch}'`); - return consoleGroup(async () => { - fs.mkdirSync(targetDir, { recursive: true }); - - const workflow = await (async () => { - for await (const workflows of octokit.paginate.iterator(octokit.rest.actions.listRepoWorkflows, defaultArgs)) { - const found = workflows.data.find(w => w.name == process.env.GITHUB_WORKFLOW); - if (found) return found; - } - return undefined; - })(); - if (workflow == undefined) { - console.log( - `Skipping previous artifact '${artifactName}' download for branch '${branch}' - not running in CI?`, - "Environment variable GITHUB_WORKFLOW isn't set.", - ); - return; - } - - const workflowRuns = await octokit.actions.listWorkflowRuns({ - ...defaultArgs, - workflow_id: workflow.id, - branch: branch, - status: 'success', - }); - - const firstRun = workflowRuns.data.workflow_runs[0]; - - if (workflowRuns.data.total_count == 0 || !firstRun) { - console.warn(`Couldn't find any successful run for workflow '${workflow.name}'`); - return; - } - - const artifact = ( - await octokit.actions.listWorkflowRunArtifacts({ - ...defaultArgs, - run_id: firstRun.id, - }) - ).data.artifacts.find(it => it.name == artifactName); - - if (artifact == undefined) { - console.warn(`Couldn't find any artifact matching ${artifactName}`); - return; - } - - console.log(`Downloading artifact ${artifact.archive_download_url} and extracting to ${targetDir}`); - - const tempFilePath = path.resolve(targetDir, '../tmp-artifacts.zip'); - if (fs.existsSync(tempFilePath)) { - fs.unlinkSync(tempFilePath); - } - - try { - await downloadArtifact(artifact.archive_download_url, tempFilePath); - await extract(tempFilePath, { dir: path.resolve(targetDir) }); - } finally { - if (fs.existsSync(tempFilePath)) { - fs.unlinkSync(tempFilePath); - } - } - }); - }, - - async addOrUpdateComment(commentBuilder: PrCommentBuilder): Promise { - console.log('Adding/updating PR comment'); - return consoleGroup(async () => { - let successful = false; - try { - successful = await tryAddOrUpdateComment(commentBuilder); - } finally { - if (!successful) { - const file = 'out/comment.html'; - console.log(`Writing built comment to ${path.resolve(file)}`); - fs.writeFileSync(file, commentBuilder.body); - } - } - }); - }, -}; diff --git a/dev-packages/overhead-metrics/src/util/json.ts b/dev-packages/overhead-metrics/src/util/json.ts deleted file mode 100644 index 87a5676869aa..000000000000 --- a/dev-packages/overhead-metrics/src/util/json.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ - -export type JsonObject = { [k: string]: T }; - -export function JsonStringify(object: T): string { - return JSON.stringify( - object, - (_: unknown, value: any): unknown => { - if (typeof value != 'undefined' && typeof value.toJSON == 'function') { - return value.toJSON(); - } else if (typeof value == 'bigint') { - return value.toString(); - } else { - return value; - } - }, - 2, - ); -} diff --git a/dev-packages/overhead-metrics/src/vitals/cls.ts b/dev-packages/overhead-metrics/src/vitals/cls.ts deleted file mode 100644 index 3e1ab977fb86..000000000000 --- a/dev-packages/overhead-metrics/src/vitals/cls.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type * as playwright from 'playwright'; - -export { CLS }; - -// https://web.dev/cls/ -class CLS { - public constructor(private _page: playwright.Page) {} - - public async setup(): Promise { - await this._page.context().addInitScript(`{ - window.cumulativeLayoutShiftScore = undefined; - - const observer = new PerformanceObserver((list) => { - for (const entry of list.getEntries()) { - if (window.cumulativeLayoutShiftScore === undefined) { - window.cumulativeLayoutShiftScore = entry.value; - } else if (!entry.hadRecentInput) { - window.cumulativeLayoutShiftScore += entry.value; - } - } - }); - - observer.observe({type: 'layout-shift', buffered: true}); - - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'hidden') { - observer.takeRecords(); - observer.disconnect(); - } - }); - }`); - } - - public async collect(): Promise { - const result = await this._page.evaluate('window.cumulativeLayoutShiftScore'); - return result as number; - } -} diff --git a/dev-packages/overhead-metrics/src/vitals/fid.ts b/dev-packages/overhead-metrics/src/vitals/fid.ts deleted file mode 100644 index feb2324aa034..000000000000 --- a/dev-packages/overhead-metrics/src/vitals/fid.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type * as playwright from 'playwright'; - -export { FID }; - -// https://web.dev/fid/ -class FID { - public constructor(private _page: playwright.Page) {} - - public async setup(): Promise { - await this._page.context().addInitScript(`{ - window.firstInputDelay = undefined; - - const observer = new PerformanceObserver((entryList) => { - for (const entry of entryList.getEntries()) { - window.firstInputDelay = entry.processingStart - entry.startTime; - } - }) - - observer.observe({type: 'first-input', buffered: true}); - - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'hidden') { - observer.takeRecords(); - observer.disconnect(); - } - }); - }`); - } - - public async collect(): Promise { - const result = await this._page.evaluate('window.firstInputDelay'); - return result as number; - } -} diff --git a/dev-packages/overhead-metrics/src/vitals/index.ts b/dev-packages/overhead-metrics/src/vitals/index.ts deleted file mode 100644 index b573edb26bb6..000000000000 --- a/dev-packages/overhead-metrics/src/vitals/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type * as playwright from 'playwright'; - -import { CLS } from './cls.js'; -import { FID } from './fid.js'; -import { LCP } from './lcp.js'; - -export { WebVitals, WebVitalsCollector }; - -class WebVitals { - public constructor(public lcp: number | undefined, public cls: number | undefined, public fid: number | undefined) {} - - public static fromJSON(data: Partial): WebVitals { - return new WebVitals(data.lcp as number, data.cls as number, data.fid as number); - } -} - -class WebVitalsCollector { - private constructor(private _lcp: LCP, private _cls: CLS, private _fid: FID) {} - - public static async create(page: playwright.Page): Promise { - const result = new WebVitalsCollector(new LCP(page), new CLS(page), new FID(page)); - await result._lcp.setup(); - await result._cls.setup(); - await result._fid.setup(); - return result; - } - - public async collect(): Promise { - return new WebVitals(await this._lcp.collect(), await this._cls.collect(), await this._fid.collect()); - } -} diff --git a/dev-packages/overhead-metrics/src/vitals/lcp.ts b/dev-packages/overhead-metrics/src/vitals/lcp.ts deleted file mode 100644 index a471bf60ba4b..000000000000 --- a/dev-packages/overhead-metrics/src/vitals/lcp.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type * as playwright from 'playwright'; - -export { LCP }; - -// https://web.dev/lcp/ -class LCP { - public constructor(private _page: playwright.Page) {} - - public async setup(): Promise { - await this._page.context().addInitScript(`{ - window.largestContentfulPaint = undefined; - - const observer = new PerformanceObserver((list) => { - const entries = list.getEntries(); - const lastEntry = entries[entries.length - 1]; - window.largestContentfulPaint = lastEntry.renderTime || lastEntry.loadTime; - }); - - observer.observe({ type: 'largest-contentful-paint', buffered: true }); - - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'hidden') { - observer.takeRecords(); - observer.disconnect(); - } - }); - }`); - } - - public async collect(): Promise { - const result = await this._page.evaluate('window.largestContentfulPaint'); - return result as number; - } -} diff --git a/dev-packages/overhead-metrics/test-apps/booking-app/img/house-0.jpg b/dev-packages/overhead-metrics/test-apps/booking-app/img/house-0.jpg deleted file mode 100644 index ff0962fc24f1fefe767773d4c39fe4a6d53af54f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34805 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 Wr?CG3MnBK4uTyC?6u48Eum9QJ)41CJ diff --git a/dev-packages/overhead-metrics/test-apps/booking-app/img/house-1.jpg b/dev-packages/overhead-metrics/test-apps/booking-app/img/house-1.jpg deleted file mode 100644 index 138f0b58ab2e16fd8057a7651e62ebce7e7e02f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42043 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 LvuK}|V!!{{-{KkG diff --git a/dev-packages/overhead-metrics/test-apps/booking-app/img/house-2.jpg b/dev-packages/overhead-metrics/test-apps/booking-app/img/house-2.jpg deleted file mode 100644 index 31f6bc1d5184ef0396d3a0580d16d106c5c921a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46950 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 IN4eAg+5O*}s{jB1 diff --git a/dev-packages/overhead-metrics/test-apps/booking-app/index.html b/dev-packages/overhead-metrics/test-apps/booking-app/index.html deleted file mode 100644 index e3972c61e4c0..000000000000 --- a/dev-packages/overhead-metrics/test-apps/booking-app/index.html +++ /dev/null @@ -1,219 +0,0 @@ - - - - Demo Booking Engine - - - - - - - - -
-
-

This is a test app.

- -
- -
- -
-
-
-
-
-
-
- - - - diff --git a/dev-packages/overhead-metrics/test-apps/booking-app/main.js b/dev-packages/overhead-metrics/test-apps/booking-app/main.js deleted file mode 100644 index 1ad19f429506..000000000000 --- a/dev-packages/overhead-metrics/test-apps/booking-app/main.js +++ /dev/null @@ -1,180 +0,0 @@ -(function () { - const searchForm = document.querySelector('#search'); - - searchForm.addEventListener('submit', event => { - event.preventDefault(); - - updateOffers(); - }); - - const obs = new MutationObserver(function (mutations) { - console.log(mutations); - }); - - obs.observe(document.documentElement, { - attributes: true, - attributeOldValue: true, - characterData: true, - characterDataOldValue: true, - childList: true, - subtree: true, - }); -})(); - -function updateOffers() { - const list = document.querySelector('.result-list'); - - // Clear out existing children - for (let el of list.children) { - list.removeChild(el); - } - - // Add new children - // Allow to define children count via URL ?count=100 - const url = new URL(window.location.href); - const count = parseInt(url.searchParams.get('count') || 50); - for (let i = 0; i < count; i++) { - const el = document.createElement('div'); - el.classList.add('result'); - el.innerHTML = generateResult(); - - const id = crypto.randomUUID(); - el.setAttribute('id', id); - - addListeners(id, el); - - list.appendChild(el); - } -} - -function addListeners(id, el) { - el.querySelector('[data-long-text-open]').addEventListener('click', event => { - const parent = event.target.closest('.long-text'); - parent.setAttribute('data-show-long', ''); - }); - el.querySelector('[data-long-text-close]').addEventListener('click', event => { - const parent = event.target.closest('.long-text'); - parent.removeAttribute('data-show-long'); - }); - - // These are purposefully inefficient - el.querySelector('[data-select]').addEventListener('click', () => { - document.querySelectorAll('.result').forEach(result => { - if (result.getAttribute('id') === id) { - result.setAttribute('data-show-options', 'yes'); - } else { - result.setAttribute('data-show-options', 'no'); - } - }); - - // Do some more, extra expensive work - document.querySelectorAll('.select__price').forEach(el => { - el.setAttribute('js-is-checked', new Date().toISOString()); - el.setAttribute('js-is-checked-2', new Date().toISOString()); - el.setAttribute('js-is-checked-3', 'yes'); - el.setAttribute('js-is-checked-4', 'yes'); - el.setAttribute('js-is-checked-5', 'yes'); - el.setAttribute('js-is-checked-6', 'yes'); - }); - document.querySelectorAll('.tag').forEach(el => el.setAttribute('js-is-checked', 'yes')); - document.querySelectorAll('h3').forEach(el => el.setAttribute('js-is-checked', 'yes')); - }); -} - -const baseTitles = ['Cottage house', 'Cabin', 'Villa', 'House', 'Appartment', 'Cosy appartment']; -const baseBeds = ['2', '2+2', '4+2', '6+2', '6+4']; -const baseDescription = - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; - -function generateResult() { - const title = `${getRandomItem(baseTitles)} ${Math.ceil(Math.random() * 20)}`; - const beds = getRandomItem(baseBeds); - const description = baseDescription - .split(' ') - .slice(Math.ceil(Math.random() * 10)) - .join(' '); - const price = 200 + Math.random() * 800; - - // Make short version of description - const descriptionShort = description.slice(0, 200); - const priceStr = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(price); - - const imgSrc = `./img/house-${Math.floor(Math.random() * 3)}.jpg`; - - const placeholders = { - title, - beds, - description, - descriptionShort, - priceStr, - imgSrc, - }; - - return replacePlaceholders(template, placeholders); -} - -function getRandomItem(list) { - return list[Math.floor(Math.random() * list.length)]; -} - -function replacePlaceholders(str, placeholders) { - let replacedStr = str; - Object.keys(placeholders).forEach(placeholder => { - replacedStr = replacedStr.replaceAll(`{{${placeholder}}}`, placeholders[placeholder]); - }); - - return replacedStr; -} - -const template = `
- {{title}} -
- -
-
-

{{title}}

- -
- {{beds}} -
-
- -
-
- {{descriptionShort}} -
- -
- {{description}} - -
-
- -
- -
- -
-
- -
- -
- -
-
-
`; diff --git a/dev-packages/overhead-metrics/test-apps/booking-app/with-replay.html b/dev-packages/overhead-metrics/test-apps/booking-app/with-replay.html deleted file mode 100644 index ae99a6171f0c..000000000000 --- a/dev-packages/overhead-metrics/test-apps/booking-app/with-replay.html +++ /dev/null @@ -1,236 +0,0 @@ - - - - Demo Booking Engine - - - - - - - - -
-
-

This is a test app.

- -
- -
- -
-
-
-
-
-
-
- - - - - - diff --git a/dev-packages/overhead-metrics/test-apps/booking-app/with-sentry.html b/dev-packages/overhead-metrics/test-apps/booking-app/with-sentry.html deleted file mode 100644 index 94c581f184ab..000000000000 --- a/dev-packages/overhead-metrics/test-apps/booking-app/with-sentry.html +++ /dev/null @@ -1,227 +0,0 @@ - - - - Demo Booking Engine - - - - - - - - -
-
-

This is a test app.

- -
- -
- -
-
-
-
-
-
-
- - - - - - diff --git a/dev-packages/overhead-metrics/test-apps/jank/README.md b/dev-packages/overhead-metrics/test-apps/jank/README.md deleted file mode 100644 index beb81a4eadd2..000000000000 --- a/dev-packages/overhead-metrics/test-apps/jank/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Chrome DevTools Jank article sample code - -- Originally coming from - [devtools-samples](https://github.com/GoogleChrome/devtools-samples/tree/4818abc9dbcdb954d0eb9b70879f4ea18756451f/jank), - licensed under Apache 2.0. -- Linking article: diff --git a/dev-packages/overhead-metrics/test-apps/jank/app.js b/dev-packages/overhead-metrics/test-apps/jank/app.js deleted file mode 100644 index 23fc9ced5111..000000000000 --- a/dev-packages/overhead-metrics/test-apps/jank/app.js +++ /dev/null @@ -1,170 +0,0 @@ -/* Copyright 2016 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ - -document.addEventListener('DOMContentLoaded', function () { - 'use strict'; - - var app = {}, - proto = document.querySelector('.proto'), - movers, - bodySize = document.body.getBoundingClientRect(), - ballSize = proto.getBoundingClientRect(), - maxHeight = Math.floor(bodySize.height - ballSize.height), - maxWidth = 97, // 100vw - width of square (3vw) - incrementor = 10, - distance = 3, - frame, - minimum = 20, - subtract = document.querySelector('.subtract'), - add = document.querySelector('.add'); - - app.optimize = true; - app.count = minimum; - app.enableApp = true; - - app.init = function () { - if (movers) { - bodySize = document.body.getBoundingClientRect(); - for (var i = 0; i < movers.length; i++) { - document.body.removeChild(movers[i]); - } - document.body.appendChild(proto); - ballSize = proto.getBoundingClientRect(); - document.body.removeChild(proto); - maxHeight = Math.floor(bodySize.height - ballSize.height); - } - for (var i = 0; i < app.count; i++) { - var m = proto.cloneNode(); - var top = Math.floor(Math.random() * maxHeight); - if (top === maxHeight) { - m.classList.add('up'); - } else { - m.classList.add('down'); - } - m.style.left = i / (app.count / maxWidth) + 'vw'; - m.style.top = top + 'px'; - document.body.appendChild(m); - } - movers = document.querySelectorAll('.mover'); - }; - - app.update = function (timestamp) { - for (var i = 0; i < app.count; i++) { - var m = movers[i]; - if (!app.optimize) { - var pos = m.classList.contains('down') ? m.offsetTop + distance : m.offsetTop - distance; - if (pos < 0) pos = 0; - if (pos > maxHeight) pos = maxHeight; - m.style.top = pos + 'px'; - if (m.offsetTop === 0) { - m.classList.remove('up'); - m.classList.add('down'); - } - if (m.offsetTop === maxHeight) { - m.classList.remove('down'); - m.classList.add('up'); - } - } else { - var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px'))); - m.classList.contains('down') ? (pos += distance) : (pos -= distance); - if (pos < 0) pos = 0; - if (pos > maxHeight) pos = maxHeight; - m.style.top = pos + 'px'; - if (pos === 0) { - m.classList.remove('up'); - m.classList.add('down'); - } - if (pos === maxHeight) { - m.classList.remove('down'); - m.classList.add('up'); - } - } - } - frame = window.requestAnimationFrame(app.update); - }; - - document.querySelector('.stop').addEventListener('click', function (e) { - if (app.enableApp) { - cancelAnimationFrame(frame); - e.target.textContent = 'Start'; - app.enableApp = false; - } else { - frame = window.requestAnimationFrame(app.update); - e.target.textContent = 'Stop'; - app.enableApp = true; - } - }); - - document.querySelector('.optimize').addEventListener('click', function (e) { - if (e.target.textContent === 'Optimize') { - app.optimize = true; - e.target.textContent = 'Un-Optimize'; - } else { - app.optimize = false; - e.target.textContent = 'Optimize'; - } - }); - - add.addEventListener('click', function (e) { - cancelAnimationFrame(frame); - app.count += incrementor; - subtract.disabled = false; - app.init(); - frame = requestAnimationFrame(app.update); - }); - - subtract.addEventListener('click', function () { - cancelAnimationFrame(frame); - app.count -= incrementor; - app.init(); - frame = requestAnimationFrame(app.update); - if (app.count === minimum) { - subtract.disabled = true; - } - }); - - function debounce(func, wait, immediate) { - var timeout; - return function () { - var context = this, - args = arguments; - var later = function () { - timeout = null; - if (!immediate) func.apply(context, args); - }; - var callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) func.apply(context, args); - }; - } - - var onResize = debounce(function () { - if (app.enableApp) { - cancelAnimationFrame(frame); - app.init(); - frame = requestAnimationFrame(app.update); - } - }, 500); - - window.addEventListener('resize', onResize); - - add.textContent = 'Add ' + incrementor; - subtract.textContent = 'Subtract ' + incrementor; - document.body.removeChild(proto); - proto.classList.remove('.proto'); - app.init(); - window.app = app; - frame = window.requestAnimationFrame(app.update); -}); diff --git a/dev-packages/overhead-metrics/test-apps/jank/favicon-96x96.png b/dev-packages/overhead-metrics/test-apps/jank/favicon-96x96.png deleted file mode 100644 index 7f01723cee0fa55387387b753b4a4990fb072daa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8194 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 diff --git a/dev-packages/overhead-metrics/test-apps/jank/index.html b/dev-packages/overhead-metrics/test-apps/jank/index.html deleted file mode 100644 index 5c06fc377622..000000000000 --- a/dev-packages/overhead-metrics/test-apps/jank/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - Janky Animation - - - - - - - -
- - diff --git a/dev-packages/overhead-metrics/test-apps/jank/logo-1024px.png b/dev-packages/overhead-metrics/test-apps/jank/logo-1024px.png deleted file mode 100644 index 84df3e22f6b09e248701d3fbfb11a1f94e11d05b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 305497 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 PZ9rawD@)OHv8s5YNAh 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

U%l}FlcCh5-x1s# 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

-oBOEqPck-}P8`y_daoUx7Pc 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

%{xLDf7Y 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

V!2CBh>Xbwx#vGzR&>pggy5>m; zGS8C&U>QZGuC2eJ>{<%-29!ZoqkI*IuJ=iTw8|8PLkqR(I{9|naSsZcBjNP*Q3I6A zuY5HXpa-Hgy)@PuNh>9C0`-fzcKT~t)l<)MQC=b&KVj`7Cx+cm!QvOQvuY>V=5mut zhxt}RO+d%g{yA*du5-706;36@6!%>2;;!fUG@@^TR-WD!mR}2DDbhn)X^*<3g2&F; z$VC$Bhn@}IWzPOMFXE8e8)BkgjHHkJrUc)Z$C4rUGwVRInraV!j9q}XH#7^BN*<+d zZO0IGr@Ik74;N}yooKit>7&z=pfGWdaqiC_FhNAr!@6+|;5o zOiG%%uGWwpRs3*;IZM-lj4GNCn<2w=w3fccmmll&V=2cWj!$#0Ulr#0{^$=OnBVCp z;>wWah<|OpE>LECuk$qgb#O^rt^W)^KX7t-UTmFWH{zfK8~|#bMok3)uWXF6vlxs7 znq=YX&A3Q@kyc?X&EWhgk)n@DfYJy!8`Dx8kKa?eDz@nwRUm)?X;8d>&d7)m6%Uc} z;);ZiQyUUjAK|wLX~{Sw_4De2;+J+}7y0%db06LUL~NHVbz#}-Vtp~mvUSlX!mPIo zJXU3G^Y4PCcen5{E&sIy;poRRB5}P6*G9`v%(n%|!8SHo)xfCR&9kfPTs3Vg=8T0; zV#1mZHDuWrY1asfmOfhljsNPkX|-9#t6AeAhuO)ZwnWreg-f2YTPaCK3$(G6Z5F&2 zHm-Rkb_FJz>yNqy`6mD8XYTc>>)@sY&8mSwWq0B%vA%B-y*>Vz7MSGgqq_21N<-dp zH@SlAupGRkXG?xZT*?b8!+jOV6U{b_0t0HybmpI61x$P9blM4#p{-GiE#8d6pYoNh zqL+P4q5W z&2O^r-()V&Ok#*iCn?*?TpjiZ%LL4s&Qyl1D{F1*_>A6iAZVEZ%W_7~CTswLzv@=C74I5;$j|(Z=FKwJ;v_s<54QKHf1W=1Mwx?Z&-MJ?2Bc zBM`#mmC61AJppSUgn_uSq^77xMdUYaAhl02)V9iB>y&uPbeWjMtHlYRCoeD~n$y-^ z&b@C0&{<)JTcEOL&{<B-kC;JV$CU`1jCZCc%!4>NS4C}y0iQ5pXDhC6X%P|T;gx69*MYo*L!T(jcn{$K zj{50gZNGiQlhy!6KTm(L@_5+j49adPrWv}uZnDECQr~7Ta!3RtQ@i{QB3!Qsg2oYy z4KVRGW zM!~-|I2l#T0bNzj|0o!eQH|qf!GnI{^%ugI1}-tY&?tTMM&oCFYs-5ERUUN=PX0XK z#3`R=S(uTNpUu^b{_W3*Fjujt)?9V(On*T-fhmghOu@gaa(=*}* zE>5S5pPz@1@Pe$CLU9wtFj|Iey$a{Po)A3BA(gWh;xOl=#pe@r;MlP~IM>!!EFwK8dI4l+cAa_p;fZ{bz)|suhyCLV|2mrp< z9NGfncqr*87R9HL+x0npB6XV2|LK(@PsCU^4bm#yP%lZzy3`!Atl-q0n*IVl9ofSV zQm`ti&l$(H`V=@H4Z06hEi5Gf#AX70=|^hZD)wVJ$V=7ko?fj;8z`=9t z%F(35V8OulOuy~3eTYo+FM|ZUUOR9cNoxiZnsr_lpK3L|+%hOu{Cpv8RwO7z>$7$O zQYHjHo=1HGF&4L0Y3F+L<$}GF}Vy?4mjjadpj%3BQuBFQJ+&fJu?!v>L78L{-l1-}9B* zTAw$iq?(3~k`!4D5M2zI@~*4M_^g8SY3R@QpMrOzqD*B~0Qx61_7e?1efC=EANA&u z@~^wp_xPPJniBfbCSxM4n9aE2nfTu8=fxhLvO6_geVZZ}*NkoD$v>*{wA6n}tUl%w z6Fxq~*E$shsM(s^%D|6d^a9)O$SH@uNosC8O#S%XTS-=LjT#kIW6Y4uNi(sFKtzMk z`-m)xDni7dW<&RY9^ui%KiFH@l zaG=!|@_*FOsM81$0Ua%B>q>`kpH^=w!dwsmb97xoYsfexH-(&cw-OUZ)5APiT*JPD zj4MxAeM$^^oLUc_0Zb(#t}ak=o@PF#ASMo!z=2Mz6ee&8HzUC$`;9bHg&X zbiNR@x`GfMB zi{@z{Q@W8uKuzu`qX)_l5<$4JHw706MSs8$A_N0o2QH$h{CCN^!yGSc&TdXFPr6F8zAkxDQ+*enmWv6GXPc43cN(__w6o^ z+2b-o2LzLHO8aWeo8mNoLY;N?mC^PR#8KO_EA0lq%cx^JJnARG>(UgHMB;k8u7grp zyf~)x0s#*Qy)X=vZuACs!NT2`Q(k-cRow3UB`;>MKIzSANU&j&or#>&fuY%@XfXUY zd4+Rc7&AB@Z1!rS>00{``fopGJ%tPT3|V zmKXrXrc9_jYD`ztk8w4=XMTVV(Q4E(m~3iS}{|ST{yC`*^@UF4M+J zyqYLV>6T_u+d1sxC7*Zv9b3rHhjrAAl)Dydr2L9r^D`wewRhDy>Z_Rw2vjmYxyaCn z`9^BVmogq|_~Ce>y{3Y<0zp_SGfg-Ethk08r z8*2)O+a`pb-3e3m9(Y;b2IdrOEqnKXi}N@-BXb8`SZ~x|}>k z&7D!%sZzJ7Fdnyk-8G++=HFC)%1c&aECzHb%s}a?)PuS-wKEEIqsCi}Bx?MO*DPZR zvtj=?LvrzR#Bf5yf-nXUWR`w4+|IRi5#xa}?sWfk0A%$c8>?)U?kqm2Vl=H2cI7c! zXPMgMmV0m4JUq-UInO*OcWB$sFb+oa8Cu{KFLR{$K5*s_rnvd8^y>uVxfNG2^=5jY zfPgbhd|YI%&ghjx=$LotOd1Hc7>r|MAm>nS|_WS zdT`TcodXpyY~~7HVBKVW9k3t$9c9N4H*I}%86-=CfKtu8J^2CDSJ7&wg2=1NU=0$! z&_7XH_s;n|P(-M2Hf^i^xq9U0gtlk{nV+&TALry8R=)kWY|*4$=gt&P4}a!TpB^Um z+SBhEjn8B)u`i&6zbRGwzrV_;1Q zhNOp^leKlDdaEK$q>ib`SkFhCQbluD^ILDz5C!&0jFDYLEC(rV?2oHBuGIC z5(KD1WqoXn3@T$4ynCZ_mFV;EGE3oRqdG=98fbgJ)qDk?F4I(a7Zkc)9$J+~o)p#! z^mtTa(pmLdhp@u5%fwSOFWTw6ZoF>Nx?YLx!K**gv&l+q(JgXhAA>0I>I(*%_^+25 z)zz(=v=*semHug=%_#$;j}LN!9yF1gd*+qvH5LAih+6t_5!#-)YO!&)bzDHifuwWZ z0uY^1Xcz0C{1EB&zU$jAfl~*(z?wd4`{a9VzUL8XnO?4lT(Tvy2^00??dUbW(vSPL zxQcDUXfp5VK9OBLeravJwo-ywrH7y0oxyi!S_XsE?B# z=74|wsWWlMp+=)S3X0?A1^BYz3NlbK(>3Cob>7*p`C_+w)6fL_%45MO8y1x5(G2eW zI-ElYoZ6gy;(gWjbX8ZJ@{oH+hb%L41b|ofi#-w{YbgDY#fC`hBl2jma@YoA`8{*( zz7PgVI9J?Xq^6~)44ux{7_Us|yRI#htr+qMFy+2aF!!BaItQrbkp?PU_TOG6LI-|a z&G2n#3Wupnyw6jBTkF7Adw&+T2%R!A<0_l!5T@H0*?tFe%5*P8hsj;8oHxL3RKr1kJgQ{_tH9gjPLe>3wAIWR88b3^?Bb7V*oPQMYX(* zyaU3?Ih(%7v9qbGEVnVJO!-Cbqgv{EXkI77&Jpxg*}n)gi8Ng!^d&%7Ma{7>B`{GXBVmCKMSF zk$T0#)|a)vvY)S;5Qdjd6%`_Fp4t@IZ#JV71~$R(#Xs^@n$%n~a!-UQu>}0XCR$O0 zw9WDp&=bh{C<5=74>lOSh2v*WZ6pFNh;q&DbLN3v*KKP)J>_OaCd8Q{=^hsSy(coKw_njBuILuA1A^)0yRJ20_(WXH|fX zL>XxK@8Nsgz_GmHC4lDI{a=%{IVCkd^}s96qmC(=YtAO_ouAX+I3d#WbsKGQ{4TbL43_3-(4O_>)jh45 zJ!|Z@rN{T&?6S`I_-W%u|6pxvVGFRSQ`qGa;+4`6>1ZN~-m>8LsZ@8*GB*FeIx*vv z_}i5`ntY8ns9*n_E3Z@AkBsx=n#r#BO6TIhCJ3 zKr58QT5c))m_B^=_qAYY{+0u=6vP2U~!^%{$=5!7PC+;ZL2hAlhg9Kar0f(;NED+m9*Zo>6%{a>F7DeT@hV)wkJi6 zZuHXZc#^wH=s5PUC|cjxPIoxB(XQc-?(uEaNxR87ddmNI(6+hupVRq&04cD zScPa({?VHEFFIZ?(jp^_CRDCabJKvPX|yR@Z&lMbuRIW#I07Vr8nVH`J8)E6E4Tg6 zJqy48U|8)=TZQUr#)gP#&Ce=wzpOka#;HCqmUBv$@8QwwNTZYMMF`-~-ie%|rUC0+ zx2Zf)VGa!nKY<4wVp#e!7GCF=;Qu=dK%}*(<(z2Y zOoKPgICZ`i`guM>9G)i5&+1kBR8$e>nLgkKSZRF}A1uB`h7J0P^@fNWkB?&dlGC~x z$^1)db0*u6P-}i%V+;79es|A&Q(~Y8r!hfoDI!?9C(`Y z4zh{<2L+Dukp>_BR(@cS=d;gG3a3a(kV9qz8Q$s)d1<4AQ?_r$H*QJysXRz2T(@<0 zi^M<&k(JbF0<9;c7FIhhil4J8CI7YK-{Q}ShJW#AiR9Ver2;@LK4Qk{^YF3Z(Ke4u zd>TRMS^BfHYsY260h5x4+S#FtvJ&*6N91?*`~ZmBkOXSTO3j?kG67sxprtImk-3E! zwirB@r#-A_*#Va_(+;>bu`t`qTbVj>{HXM{$;x!;ux8Je^>owCG!S6>Xa&eKNJrNJ z;jpjdS@`uNmqz@?kyO>S^Tk7o|#SN(9NAIhL9l{8j-N%{ToeuN%8UuC?7 z=-4DD=yNNgiMRpKc4Zd`B2^0C)coJNOy3tU6HZgn?B*(>%H`Ojv`i_W(oAS; zZAO%+bu?@gVq3n=Idy~cTJ%`!Io2JW*F7%We3LTofvKEBL<`hM#(d>k9ehVR=iT?R zt)14h)3lompLx{az&BJhg^c9i!^dWk(=n`FR+)-vMt68Tcia$ykq~%L_xqRvT9YZfS zj-w3iK;Muma4$?O3J%A>`Lhr~;>N6fZ7k>@;0K%dI$m8r7w~{RrDL`?*JT%-P!2O16ATRDAAUo19wUNq1UvdHgp^Y^jHX`rR?Oe6@c(VDXJG_?M1oz8zg zPv&{*_~rz^#|Cox<)9z;%XS-sN>t|o=*pnjJ=z*UtCl?7PX=-=S*``*v$Db$)sWup zGn2ANZxb)^T0(?PpnJ@VX6$A=({#+TE#iA@4g9yv=9xqzibM9xynTox|DRYQ5aRRp z?BbSvUb^a)>lOYYkE3L$nkCUJon3!j}QD^}iDsV}u%qJ!d@=5z$@UQrzx=7*f)J6kZx(nk8EQ$f2^zWiJZzdN|2GEL;|(9C|*m7q^*? z-oNiH^O^u_l1Avg@l80e(3{nN8ewAwKe7)pkNQ^X3HP`pq69)E^5su%@N9)v*E#|> z1I&!$3MddHIZmRKUJNg_84;1D1&|PJrLfSYfU(5+VL58kBLFnf?J|S~P zQsc)0!xS5yiGY@ZDJ6dT5M~=j3+a3sca=ePT$Vpnu4lZod{uv8#Zu(nVhSz0 zC`(k&(m%`OYe#7yr@Do=f_Yt^8B)Qd0=mo1-wrzP7lsaHt|7n0ykDEt4DUUba;Lka<4m`9& zG*5n!fDq(PWUXR3wnW~M?_RM=xLKIS-I>S6LBIZv$||7!Tr+Fuqunlf$zN##6p5Xv z+ArUj3%EQwQ^a6#!~p4TN;qWU{-8LNw6z!_?wQ)Pwf%4rD=j0EFLK4Ij#CI9A9$^% zg=w!W8@0*0#d8W}meF1sIL|p@QFy3DF7+_~PRjS;46=D~B>OR|t@6}NiO1kMfzcsT zChAP_=`JtvyA?XK;_06=-Wy(q(aOt96OQwtrZWeZJ{7$+_E6YWv8IEtUHxFu($-g> zfB;$MX(?K1YJ|_>hAV4nB_5I2GvYYPvkaR{5nYUJt1c^%$3Y)VDdt zrSWerhb2@Mu^Ul5!0M~lziW=^%r&&N8*PJzN)6K#O9)SdPiTz-d)#fFCxuW)ok+s7*_=&|WqLbVRv*XtC1QJSz< zh^=tG%H-o3;o{IsJ~+S;{GqwA)vuPYTuZ<_1B_0J@??t#=M}*q+2BOr$ZgB1C^#c} z9`tQDY z2*V#9Rf9$@A#4obIoEt@$7c|dFseA$!JSlnRqRNppRGIf^XGc#9K>ettH*LtFZWFh zPJn#6X9p`SP?Pp<TYsm^P~PgulGvvBljF;F&uKF7YPq__7}X2% zBb6}L;Dd1L(2tDH<;!+UTzSvCZA{{~m)s|Mk!mGH^W_F{VfU4foPoet1>al9JPx>W z!`d9pN(Jsmn_l*EIr>s(X*zQ!^qGi9gFO0qaaP?Ik8N|g#PnHTWd8U$!qnHwVq@0E zj2dfbt}Jr9Ul_1ozH81#0Eu(St?dXMVPhs>C-z=vwvtg89KSg`Ux7surM%s5stxwB z*T}$>wEg`=ThYt0glrzGuhm}t1raC;3b6U*xX;K60@qzoZ)}J})W6b+OWS1J5?(v0 zh3ae0F7A-w7n!0088DoTX7IYg{x=BAED>qM)}g+xS)XvWY;W{xX^5@JP2W@H1!c6r zPc`oExFZ_c%Aqb0j}UnnOEgb|0uUOxR&C=4I2(T%Fuy4;^}U=bi0kP^EEn}e8Qov} z2G5%O+58K2_~E!0z-m`-w$sLS>mr4w=B2$9q7rQbdS`B*os@?92pVe{8K8^Y$XY0x zE5GmW1kGI^Y2RY|*P&|+PpA@V6N@5MeNM9(s=?<> z=B9oW_=isLMwo2Dpin+NhGe`r=T~v#jL2Mh+d0yJ4(^T@f4s2*C$p>k0uL=epxk5e zwShQiPoAPS^0a6qFf%-IElYZ>X}BUMTl=aqsM>um;rgEwib1CZ#-{--Zy>S4rLto9 zE-($Ezv8ydv_U#m1@gQ+5GSoUL_VviKorW#^orj7mDI0V^(pzThT~I@{{Wd3)8}ng zd$^dm)ODUSgTQNJbI{D-^tct-w_|Ve1Sor1Z_HHP0^8lFK6%r?Geca7S46OFv70Sq z-?5mfpt+pMQ~mdc^!stftCB8S;hRP6X55J>V3RPqdRXA$!Cr0MU&<+S48XBBtyo|l3DC&n1FBoETa6Ouv7W%aG^t3QjpZlq zck&kXC+sSTd#%vjhuTmH5}}vi9u5sfrhh9W{*({spFC5yWQbK#e0ylxPUiR|%;BT= zwi}83O|j^*u*RrGtdsJd~j72P;Sb*a)?H}ZV)o(RZh zWQ6NGQM#7#L~V`no~VMHV|)b+?EvAIhS019+O{b<>X%6q?;Z$fil&fg>#-EgUsp+* z?=i#Yd(u} z2+^v>NK--`pl?(2r*YmzgegE69iflzv{r*xfXI=vAtp?G-)qW-z)SU3Q8$FxvkNw) z3x|1!I)0x=oiD$bA%?M9{8?zjN(`#k$fe-_K*@%@*I?XUlu}N}WW|sIbz4ig8zaMV z8OpAV4!v@i4|QXawd*ra5R&(?Nt6x)TXAmm^XeUjZ}J%hmmja4?NbSM?ly+22j$QI zr9MO(BJY{T3H^LOH9sBN&mF~irJerA0XC++mAbXKrtM?D*MDAW)-E?_0AKyBquTQV z%h)%)G9Yo!0W&^4>Azvs$f@%B)ja0gXQ8s@Fe)a_>{1tPu>U3_9&FH9L`x|oKf%V# zR@%5J4(WGe$n22D9RMuiB#T(9)gB!?vG_8%%E|hnt>4?*#xbOl4bp{d+il)ZsjY2$ zvM+^}b6=%5=C`d*jAKF^pl4bA+D}1>?JP-{?d@60rGFJ8e48t#nf_th{}3x|v} zb>>l7z9AHsLDP=Pt-X#s%B>A}>!nF(OcGD+hCV{b^9g?b_B+NC;Tm~cU{SR$E40Ar zJuI29-E2FP3-!g;1q<&Y0bd*%by6u>+sBFn>$BoD*kUicLS$A>5HDQb8cQGPS16hG zXSqp{+6`9XJ?f3;{<5YRq*nAN`u=XhffEgiotR)51XDSM+{6E@Bw(@C!s7vV{j1bq zV!agf*u&&j_6rpRGyC1Rp5ta3_wQ0*WK(nC{isz)sD2lhyW5R*@Vex&)NP$E-g}wR z3T*5rOG!K$AQ#-&H;-2H&Q6Jch&A$ig%<{0vr&~L@-+95`uNQ-U_5O=&{v{L0HUKG z0;(q57gRvh68NU(M~9s)Qa$;n$X`1qr-H_`91&*x5@ZfQP8>AcZrpIwy-agV&04MM^ieI3>aBvDdCPWQE6(l35Wx7BM)FZxjkX zn&Rp7^lsD0+tZ{Ki#V(z_e_qcUHIB>+Lg*Fv4&uw>BQuv+Z#Q4gq27_8*FNGV=Z#b zq&@@|Fkn=SH(V7z(UYjXU^4jbF2$F!-{p@2>9tuqeE5Hkcy*NPkdIZ@%)4ve1p?d< zl1fe??ejfrYt-r9T|B!$H3!kAH~1^%BO9pM4%Z!dT7<{HM6slj6{VGrWo)lW;m`MS zW07Zqp7xVHuzN<#l0QL>0YFIf+9UDNjb5+0ktd?z+n2G@*6l;vsT5Xd<|eNy0@Qe>KqC#3w%O-|l@XSVq_%4*c5 za)xMDwX7TPEQr*+L3xbnisZGzPBXxN1cXxird$YO;(UH8?kCF%$Tl}aJZkV2Dr%0& z_Ol0dYX1x1T&nY*{zn`tRIF?bLM5`?xnZ8-Ye6JlFKetVdN7yX_+(;D#%G4>=lJgp zYVYY`8T7~NiOGBoS9n@?J7rfs1(KA>ugVzZ*dAx!3vspY6e&>*_>mEk@Q+7Iya`ad zjlhpIgGvLzp{Yw^Y+&vWI%x2dfapl5@?Cj!C!69r{BX%2z0PXMVXcuKY4BQh;I9VPp8fSt-kBQR{(8C(cDitd zy&-W>!Zcl~l;;M|&6$IbA!j2Z6;4`|ZtgK>oiS!RQ7E_FR=inpZ_xVGl%#NIBI=@% zz&+)d18usm#6AtU&Cwmhx6INn=ft%Rcsv_}GP`z^YAHWj;GzfyXE%lx_vgcGA+Jvq z#igYEgTmp8!2UydBNF`-MG!cK-!@_3{f{jkIwSmGSWI!KE#`X$E?r zK3cCdsADEvlF*B(iVkCr(A($D8WsGkH`+^ZWL`NfMx311!g{Syv#%|^Kwh6ASL)CEUCSN?DDg=r{Bgp;?Dki6+tV0F74C4z38JSv=+>!oR&)q zpQiA1{g}b<2`2q@GlVh*b@BS*vpOoL7)ezn_RL%MV zV8IU2{O?b#9Un%H8PxvY&ly%sbh$UWv^MIF?4}B{C2i_*sp0rvIrz%fb8cIHFYbtJNdl((=?UaHwpiWofw#f)r@~3XOC2529(WIzKlV)cKh5BPSq0*s6!eO zh^75&>4FC}=WR={pB7hg1^PoKFQ7RK@8lLAzqeAi!hRrc#z^^g)n#jni%5y;>q+}` z3$slwp<~oN$2ZnIjRN!y zJB}Jp>8Bdo<4^wFlOLO9VE-BQ5mITWj3&*eks%j9i>(j0qSe)6VDeA^X8SQMP8rEf zOTWN>12we7Z_h>nh+YfOFdqdfr(HE?i)m5=r8z*zkZFqP3L1;6Z9BIUtc>JJAp#fu zIf<%mFZxKyai_}xOHRn)PhVro#s+UxpvxVn#}-1-SH#u_a9E~Tnll}>j%PW;V?H+e zG;NAzR~}C70vz78##CojsbWc^$y6*6;RoLYNX|*wL5Vk2O=Y(Uo~U zxuwdQD>|E|BZ|-u^d2Glw3LnEni@l4iCP%u)i6vS;uEfqz4XaSS~-Zs7+<>VP*yCa zXE^1cCxc#JXTknm)7iT8WJ~5h;@fFo+wTfNye;f#fEYhmOxlqeNn&*a9L07qw z`dy=cr+UWTh)&$5{)if3Wxy{#>iuXdDgK#%CFxv^?st_ivj@kAWJozz+;)5Lai`+C zta@tbqY-3AWTGu|u^fAUv73-bAK$ru-|gUvjQHm+m_Kg_F>;fFSjD;#Gu|COXoTra z63?Z7=vs+$&^xyn&zF5pn3Tg$2?PlHjmb~kfhh8q@oO8tO!np@RAF_N05_6s=PB0t zk?8RNtmXsTOyw%$quqOC%09$!razK3=b^8nd9%WMG|mE;@GH3D|0`KgNBpM@<#3 zbx3`3SWM_S`_f^s6xJ8wb5v%*({kEo1yI6ur$&xo^qzhCd7WcDaYb?U@>|oe$1Ee; z0NA$G*z@~%ydTr`Kor4hE5EG6z_L0`evh3q>5!V^0Fb(6v0y;#)}*G3Ig!z`JfVqr zQ)9uLU*n4s`yxq>B8fxR6GmTd7q@$6QOH#K|L=j}VY-HR9e?1x{(_vLXIc;qvfEd> zx=&FKGEqaU4&{}%K-k<<*E*VUE4;NH^uwbZUH9^7p;(-bMxe>;ON4ZAUYsJ8{$QP8 z+yySAWk&E6f}jGrZu%2vQBHKZE@hvGex8DY`AXj~agajYv)@h&&-?ZKEC`q|z>b*{ z(k7~k=CrWu=kJau&u3U(lN{;`sYv0C%ZNHy+$aY7-tDVCzp}LmgToW+In6?bo)y@G z$%@0|B<_XM-7I*Dq{LGiewV=eM{&=gh^69r!QwvulrN6H61AeKCKT*`Cz5z+MQK|n zws+wpx9fkl`4nn4YhEM0GKJM>~5 zJ$6VHF=*IA;rwkvtVC4c`VU2F8Fj<7xCXF>qs#KiAJFpW3q^krSIg`I^A%EqE=*bH zxO@Ymjs@RtFz-Yj$_mzf-mzTrF<^PqbtKqEw^WG!TfK)hcYj=!q^#)qweg62{)KNC zG2=Rivh3X!f46d%IE+Uf0w&JAy!*YLk}HB94oFFcu)*pMFpZs!P?y2Eb1LQWHVQ$V z_*IT-Der~9#ubDwb|2_F1s{21m+9f-XOygo-+P1m0z({7=0zNnsYLHA?y_5a1X}^J zrSxX?C}kFWNu26mA^2QxCH@#+3K2x`&>rx1W@C%2w>lf|9$NCS4_>W8^d1@593c3 z+##LM$!mGqz!sL8a%tiEn!>y{FE;v);F6aI%Ge*!wP(ZV30sR@BpBAoq@^T7AXQB| z8CtUpcH3*El>nesVK0SzccFtf0Ym7RuV24*vH2HI^@np>lHNlef%h;~n}tKDU%yyYLXbqvb~mP;QTJ#OIKv zcX?B;cy(b+RjzeqFbpK$=}>qoBBrR*5fH`du{m15(VPiOO?NGDeY$d5_UITi>hfF> zpPA{^7E0(F`AQcNsvhxt@yrh$(p`+2T8`dCxuFtl`%keE5i3qY`k<9ZUkP@*F&qFcmf-X@@#L66Z&>?{E zpI9)qPf?8WpTQD&oohSH)PB`K(Y_C|C7zUho=l1BrOqN>3j3GOdgnxwZopWVvjq=0 zU*Pdk4ibX(ShX1rO#Hgrzjj5*ke)WolMbp|GJBL49a<_Rzsz@TDY&Eah0OICm`YxX zn%Vc3zb60Yyb&dm1)^m_V}qsFO~xi#acCgzQcK_!IWLS{nd#T*H_lcJ8@l0vTP@o( z)u0imH4g!9SN=agVN6KC+pozHXbupLMLv5x`K>P`e+^aOIAu8z15ULj9lg9Em4W5`^iG zA9Sg%xrWw5WsgmPA)PS~v~FhsN~|)rF`{~Mz`4}ux>mGr3Nn$|LmU70^XzkR*WAZ8 zERpVrC0U0t^9WaK4aNzJ5B6rJ9z^}$wjKpH%hNDg9`n@NkK7k2U4tz^K)u34@rFWZ z`ELWg^oHU5N^ojdnxc=x_ok1URUx3{0iCsjL~Omb
lI28%|hF9ycar^H8e{ZTW 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�wjyWVZWzhueiS3}@y9CgxvJ#sdQ6b>TE; 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 K=3TLR^8WzivOVAc diff --git a/dev-packages/overhead-metrics/test-apps/jank/styles.css b/dev-packages/overhead-metrics/test-apps/jank/styles.css deleted file mode 100644 index 052c8c4e608c..000000000000 --- a/dev-packages/overhead-metrics/test-apps/jank/styles.css +++ /dev/null @@ -1,59 +0,0 @@ -/* Copyright 2016 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - * implied. See the License for the specific language governing permissions - * and limitations under the License. */ - -* { - margin: 0; - padding: 0; -} - -body { - height: 100vh; - width: 100vw; -} - -.controls { - position: fixed; - top: 2vw; - left: 2vw; - z-index: 1; -} - -.controls button { - display: block; - font-size: 1em; - padding: 1em; - margin: 1em; - background-color: beige; - color: black; -} - -.subtract:disabled { - opacity: 0.2; -} - -.mover { - height: 3vw; - position: absolute; - z-index: 0; -} - -.border { - border: 1px solid black; -} - -@media (max-width: 600px) { - .controls button { - min-width: 20vw; - } -} diff --git a/dev-packages/overhead-metrics/test-apps/jank/with-replay.html b/dev-packages/overhead-metrics/test-apps/jank/with-replay.html deleted file mode 100644 index 6c5f32cc7e8d..000000000000 --- a/dev-packages/overhead-metrics/test-apps/jank/with-replay.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - Janky Animation - - - - - - - - - - -

- - - - - - - -
- - diff --git a/dev-packages/overhead-metrics/test-apps/jank/with-sentry.html b/dev-packages/overhead-metrics/test-apps/jank/with-sentry.html deleted file mode 100644 index 8beacb69b440..000000000000 --- a/dev-packages/overhead-metrics/test-apps/jank/with-sentry.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - Janky Animation - - - - - - - - - - -
- - - - - - - -
- - diff --git a/dev-packages/overhead-metrics/tsconfig.json b/dev-packages/overhead-metrics/tsconfig.json deleted file mode 100644 index 193dd1f8acde..000000000000 --- a/dev-packages/overhead-metrics/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "target": "es2020", - "module": "esnext", - "outDir": "build", - "esModuleInterop": true - }, - "include": ["src/**/*.ts", "configs/**/*.ts"] -} diff --git a/package.json b/package.json index 365e1eb13922..12476eed2ccd 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,10 @@ "lint:biome": "biome check .", "lint:prettier": "prettier \"**/*.md\" \"**/*.css\" --check", "postpublish": "lerna run --stream --concurrency 1 postpublish", - "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test", - "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test:unit", + "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test", + "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test:unit", "test:update-snapshots": "lerna run test:update-snapshots", - "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", + "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\"", "test:pr:browser": "UNIT_TEST_ENV=browser ts-node ./scripts/ci-unit-tests.ts --affected", "test:pr:node": "UNIT_TEST_ENV=node ts-node ./scripts/ci-unit-tests.ts --affected", "test:ci:browser": "UNIT_TEST_ENV=browser ts-node ./scripts/ci-unit-tests.ts", @@ -88,7 +88,6 @@ "dev-packages/bundle-analyzer-scenarios", "dev-packages/e2e-tests", "dev-packages/node-integration-tests", - "dev-packages/overhead-metrics", "dev-packages/test-utils", "dev-packages/size-limit-gh-action", "dev-packages/clear-cache-gh-action", diff --git a/yarn.lock b/yarn.lock index cdd55b36d9a6..15543cfd506a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6974,19 +6974,6 @@ before-after-hook "^2.2.0" universal-user-agent "^6.0.0" -"@octokit/core@^4.1.0": - version "4.2.0" - resolved "https://registry.npmjs.org/@octokit/core/-/core-4.2.0.tgz#8c253ba9605aca605bc46187c34fcccae6a96648" - integrity sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg== - dependencies: - "@octokit/auth-token" "^3.0.0" - "@octokit/graphql" "^5.0.0" - "@octokit/request" "^6.0.0" - "@octokit/request-error" "^3.0.0" - "@octokit/types" "^9.0.0" - before-after-hook "^2.2.0" - universal-user-agent "^6.0.0" - "@octokit/core@^4.2.1": version "4.2.4" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-4.2.4.tgz#d8769ec2b43ff37cc3ea89ec4681a20ba58ef907" @@ -7068,13 +7055,6 @@ dependencies: "@octokit/types" "^6.40.0" -"@octokit/plugin-paginate-rest@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.0.0.tgz#f34b5a7d9416019126042cd7d7b811e006c0d561" - integrity sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw== - dependencies: - "@octokit/types" "^9.0.0" - "@octokit/plugin-paginate-rest@^6.1.2": version "6.1.2" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz#f86456a7a1fe9e58fec6385a85cf1b34072341f8" @@ -7096,14 +7076,6 @@ "@octokit/types" "^6.39.0" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@^7.0.0": - version "7.0.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.0.1.tgz#f7ebe18144fd89460f98f35a587b056646e84502" - integrity sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA== - dependencies: - "@octokit/types" "^9.0.0" - deprecation "^2.3.1" - "@octokit/plugin-rest-endpoint-methods@^7.1.2": version "7.2.3" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz#37a84b171a6cb6658816c82c4082ac3512021797" @@ -7163,16 +7135,6 @@ "@octokit/plugin-request-log" "^1.0.4" "@octokit/plugin-rest-endpoint-methods" "^7.1.2" -"@octokit/rest@^19.0.5": - version "19.0.7" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-19.0.7.tgz#d2e21b4995ab96ae5bfae50b4969da7e04e0bb70" - integrity sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA== - dependencies: - "@octokit/core" "^4.1.0" - "@octokit/plugin-paginate-rest" "^6.0.0" - "@octokit/plugin-request-log" "^1.0.4" - "@octokit/plugin-rest-endpoint-methods" "^7.0.0" - "@octokit/tsconfig@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@octokit/tsconfig/-/tsconfig-1.0.2.tgz#59b024d6f3c0ed82f00d08ead5b3750469125af7" @@ -10069,11 +10031,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== -"@types/node@^18.11.17": - version "18.14.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.2.tgz#c076ed1d7b6095078ad3cf21dfeea951842778b1" - integrity sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA== - "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -10361,13 +10318,6 @@ dependencies: "@types/yargs-parser" "*" -"@types/yauzl@^2.9.1": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" - integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== - dependencies: - "@types/node" "*" - "@typescript-eslint/eslint-plugin@^5.48.0": version "5.48.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz#54f8368d080eb384a455f60c2ee044e948a8ce67" @@ -18385,17 +18335,6 @@ extract-stack@^2.0.0: resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-2.0.0.tgz#11367bc865bfcd9bc0db3123e5edb57786f11f9b" integrity sha512-AEo4zm+TenK7zQorGK1f9mJ8L14hnTDi2ZQPR+Mub1NX8zimka1mXpV5LpH8x9HoUmFSHZCfLHqWvp0Y4FxxzQ== -extract-zip@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - fake-indexeddb@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-4.0.2.tgz#e7a884158fa576e00f03e973b9874619947013e4" @@ -18616,11 +18555,6 @@ filelist@^1.0.1: dependencies: minimatch "^5.0.1" -filesize@^10.0.6: - version "10.0.6" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.0.6.tgz#5f4cd2721664cd925db3a7a5a87bbfd6ab5ebb1a" - integrity sha512-rzpOZ4C9vMFDqOa6dNpog92CoLYjD79dnjLk2TYDDtImRIyLTOzqojCb05Opd1WuiWjs+fshhCgTd8cl7y5t+g== - filesize@^9.0.11: version "9.0.11" resolved "https://registry.yarnpkg.com/filesize/-/filesize-9.0.11.tgz#4ac3a42c084232dd9b2a1da0107f32d42fcfa5e4" @@ -26527,11 +26461,6 @@ p-timeout@^5.0.2: resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-5.1.0.tgz#b3c691cf4415138ce2d9cfe071dba11f0fee085b" integrity sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew== -p-timeout@^6.0.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-6.1.1.tgz#bcee5e37d730f5474d973b6ff226751a1a5e6ff1" - integrity sha512-yqz2Wi4fiFRpMmK0L2pGAU49naSUaP23fFIQL2Y6YT+qDGPoFwpvgQM/wzc6F8JoenUkIlAFa4Ql7NguXBxI7w== - p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -27185,12 +27114,12 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -playwright-core@1.44.1, playwright-core@^1.44.1: +playwright-core@1.44.1: version "1.44.1" resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.44.1.tgz#53ec975503b763af6fc1a7aa995f34bc09ff447c" integrity sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA== -playwright@1.44.1, playwright@^1.44.1: +playwright@1.44.1: version "1.44.1" resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.44.1.tgz#5634369d777111c1eea9180430b7a184028e7892" integrity sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg== @@ -30403,15 +30332,6 @@ simple-get@^4.0.0, simple-get@^4.0.1: once "^1.3.1" simple-concat "^1.0.0" -simple-git@^3.16.0: - version "3.16.1" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.16.1.tgz#b67f18cbd3c68bbc4b9177ed49256afe51f12d47" - integrity sha512-xzRxMKiy1zEYeHGXgAzvuXffDS0xgsq07Oi4LWEEcVH29vLpcZ2tyQRWyK0NLLlCVaKysZeem5tC1qHEOxsKwA== - dependencies: - "@kwsites/file-exists" "^1.1.1" - "@kwsites/promise-deferred" "^1.1.1" - debug "^4.3.4" - simple-git@^3.27.0: version "3.27.0" resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-3.27.0.tgz#f4b09e807bda56a4a3968f635c0e4888d3decbd5" @@ -30426,11 +30346,6 @@ simple-html-tokenizer@^0.5.11: resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.5.11.tgz#4c5186083c164ba22a7b477b7687ac056ad6b1d9" integrity sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og== -simple-statistics@^7.8.0: - version "7.8.3" - resolved "https://registry.yarnpkg.com/simple-statistics/-/simple-statistics-7.8.3.tgz#62998dd7786ba14fa27b07f4f3cd498466f7961a" - integrity sha512-JFvMY00t6SBGtwMuJ+nqgsx9ylkMiJ5JlK9bkj8AdvniIe5615wWQYkKHXe84XtSuc40G/tlrPu0A5/NlJvv8A== - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -32187,7 +32102,7 @@ ts-jest@^27.1.4: semver "7.x" yargs-parser "20.x" -ts-node@10.9.1, ts-node@^10.9.1: +ts-node@10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== From a70f7975c94329fb563ddf7451129f23472eae25 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 28 Oct 2024 08:20:42 -0700 Subject: [PATCH 19/44] ci: Update dependabot config (#14090) Just streamlining this a bit and adding the remix instrumentation. --- .github/dependabot.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d332007ee284..0c9602267379 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,13 +13,10 @@ updates: schedule: interval: 'weekly' allow: - - dependency-name: "@sentry/cli" - - dependency-name: "@sentry/vite-plugin" - - dependency-name: "@sentry/webpack-plugin" - - dependency-name: "@sentry/rollup-plugin" - - dependency-name: "@sentry/esbuild-plugin" + - dependency-name: "@sentry/*" - dependency-name: "@opentelemetry/*" - dependency-name: "@prisma/instrumentation" + - dependency-name: "opentelemetry-instrumentation-remix" versioning-strategy: increase commit-message: prefix: feat From babfaed28b68a19de6c1a12dc8d6fb9b3ed699a3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 28 Oct 2024 09:01:32 -0700 Subject: [PATCH 20/44] chore(dev-deps): Bump rollup from 4.13.0 to 4.24.2 (#14093) This is the rollup version used for building the SDK itself, it is not shipped to users. --- .../debug-id-sourcemaps/package.json | 2 +- package.json | 2 +- packages/nuxt/src/vite/addServerConfig.ts | 1 - yarn.lock | 412 ++++++------------ 4 files changed, 127 insertions(+), 290 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json index a40e67f4d44c..9295b7997ee6 100644 --- a/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json +++ b/dev-packages/e2e-tests/test-applications/debug-id-sourcemaps/package.json @@ -13,7 +13,7 @@ "@sentry/node": "latest || *" }, "devDependencies": { - "rollup": "^4.0.2", + "rollup": "^4.24.2", "vitest": "^0.34.6", "@sentry/rollup-plugin": "2.22.6" }, diff --git a/package.json b/package.json index 12476eed2ccd..419465ec95d4 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "npm-run-all2": "^6.2.0", "prettier": "^3.1.1", "rimraf": "^3.0.2", - "rollup": "^4.13.0", + "rollup": "^4.24.2", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-license": "^3.3.1", "size-limit": "~11.1.0", diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 80efc1da4b2e..85fdefda740d 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -94,7 +94,6 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile: } nitro.options.rollupConfig.plugins.push( - // @ts-expect-error - This is the correct type, but it shows an error because of two different definitions wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)), ); } diff --git a/yarn.lock b/yarn.lock index 15543cfd506a..2f6d8299eb64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8101,215 +8101,95 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rollup/rollup-android-arm-eabi@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz#b98786c1304b4ff8db3a873180b778649b5dff2b" - integrity sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg== - -"@rollup/rollup-android-arm-eabi@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz#8b613b9725e8f9479d142970b106b6ae878610d5" - integrity sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w== - -"@rollup/rollup-android-arm-eabi@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.1.tgz#beaf518ee45a196448e294ad3f823d2d4576cf35" - integrity sha512-6vMdBZqtq1dVQ4CWdhFwhKZL6E4L1dV6jUjuBvsavvNJSppzi6dLBbuV+3+IyUREaj9ZFvQefnQm28v4OCXlig== - -"@rollup/rollup-android-arm64@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz#8833679af11172b1bf1ab7cb3bad84df4caf0c9e" - integrity sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q== - -"@rollup/rollup-android-arm64@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz#654ca1049189132ff602bfcf8df14c18da1f15fb" - integrity sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA== - -"@rollup/rollup-android-arm64@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.1.tgz#6f76cfa759c2d0fdb92122ffe28217181a1664eb" - integrity sha512-Jto9Fl3YQ9OLsTDWtLFPtaIMSL2kwGyGoVCmPC8Gxvym9TCZm4Sie+cVeblPO66YZsYH8MhBKDMGZ2NDxuk/XQ== - -"@rollup/rollup-darwin-arm64@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz#ef02d73e0a95d406e0eb4fd61a53d5d17775659b" - integrity sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g== - -"@rollup/rollup-darwin-arm64@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz#6d241d099d1518ef0c2205d96b3fa52e0fe1954b" - integrity sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q== - -"@rollup/rollup-darwin-arm64@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.1.tgz#9aaefe33a5481d66322d1c62f368171c03eabe2b" - integrity sha512-LtYcLNM+bhsaKAIGwVkh5IOWhaZhjTfNOkGzGqdHvhiCUVuJDalvDxEdSnhFzAn+g23wgsycmZk1vbnaibZwwA== - -"@rollup/rollup-darwin-x64@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz#3ce5b9bcf92b3341a5c1c58a3e6bcce0ea9e7455" - integrity sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg== - -"@rollup/rollup-darwin-x64@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz#42bd19d292a57ee11734c980c4650de26b457791" - integrity sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw== - -"@rollup/rollup-darwin-x64@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.1.tgz#707dcaadcdc6bd3fd6c69f55d9456cd4446306a3" - integrity sha512-KyP/byeXu9V+etKO6Lw3E4tW4QdcnzDG/ake031mg42lob5tN+5qfr+lkcT/SGZaH2PdW4Z1NX9GHEkZ8xV7og== - -"@rollup/rollup-linux-arm-gnueabihf@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz#3d3d2c018bdd8e037c6bfedd52acfff1c97e4be4" - integrity sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ== - -"@rollup/rollup-linux-arm-gnueabihf@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz#f23555ee3d8fe941c5c5fd458cd22b65eb1c2232" - integrity sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ== - -"@rollup/rollup-linux-arm-gnueabihf@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.1.tgz#7a4dbbd1dd98731d88a55aefcef0ec4c578fa9c7" - integrity sha512-Yqz/Doumf3QTKplwGNrCHe/B2p9xqDghBZSlAY0/hU6ikuDVQuOUIpDP/YcmoT+447tsZTmirmjgG3znvSCR0Q== - -"@rollup/rollup-linux-arm-musleabihf@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz#f3bbd1ae2420f5539d40ac1fde2b38da67779baa" - integrity sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg== - -"@rollup/rollup-linux-arm64-gnu@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz#5fc8cc978ff396eaa136d7bfe05b5b9138064143" - integrity sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w== - -"@rollup/rollup-linux-arm64-gnu@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz#7abe900120113e08a1f90afb84c7c28774054d15" - integrity sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw== - -"@rollup/rollup-linux-arm64-gnu@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.1.tgz#967ba8e6f68a5f21bd00cd97773dcdd6107e94ed" - integrity sha512-u3XkZVvxcvlAOlQJ3UsD1rFvLWqu4Ef/Ggl40WAVCuogf4S1nJPHh5RTgqYFpCOvuGJ7H5yGHabjFKEZGExk5Q== - -"@rollup/rollup-linux-arm64-musl@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz#f2ae7d7bed416ffa26d6b948ac5772b520700eef" - integrity sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw== - -"@rollup/rollup-linux-arm64-musl@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz#9e655285c8175cd44f57d6a1e8e5dedfbba1d820" - integrity sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA== - -"@rollup/rollup-linux-arm64-musl@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.1.tgz#d3a4e1c9f21eef3b9f4e4989f334a519a1341462" - integrity sha512-0XSYN/rfWShW+i+qjZ0phc6vZ7UWI8XWNz4E/l+6edFt+FxoEghrJHjX1EY/kcUGCnZzYYRCl31SNdfOi450Aw== - -"@rollup/rollup-linux-powerpc64le-gnu@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz#9a79ae6c9e9d8fe83d49e2712ecf4302db5bef5e" - integrity sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg== - -"@rollup/rollup-linux-riscv64-gnu@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz#303d57a328ee9a50c85385936f31cf62306d30b6" - integrity sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA== - -"@rollup/rollup-linux-riscv64-gnu@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz#67ac70eca4ace8e2942fabca95164e8874ab8128" - integrity sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA== - -"@rollup/rollup-linux-riscv64-gnu@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.1.tgz#415c0533bb752164effd05f5613858e8f6779bc9" - integrity sha512-LmYIO65oZVfFt9t6cpYkbC4d5lKHLYv5B4CSHRpnANq0VZUQXGcCPXHzbCXCz4RQnx7jvlYB1ISVNCE/omz5cw== - -"@rollup/rollup-linux-s390x-gnu@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz#9f883a7440f51a22ed7f99e1d070bd84ea5005fc" - integrity sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q== - -"@rollup/rollup-linux-x64-gnu@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz#f672f6508f090fc73f08ba40ff76c20b57424778" - integrity sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA== - -"@rollup/rollup-linux-x64-gnu@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz#70116ae6c577fe367f58559e2cffb5641a1dd9d0" - integrity sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg== - -"@rollup/rollup-linux-x64-gnu@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.1.tgz#0983385dd753a2e0ecaddea7a81dd37fea5114f5" - integrity sha512-kr8rEPQ6ns/Lmr/hiw8sEVj9aa07gh1/tQF2Y5HrNCCEPiCBGnBUt9tVusrcBBiJfIt1yNaXN6r1CCmpbFEDpg== - -"@rollup/rollup-linux-x64-musl@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz#d2f34b1b157f3e7f13925bca3288192a66755a89" - integrity sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw== - -"@rollup/rollup-linux-x64-musl@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz#f473f88219feb07b0b98b53a7923be716d1d182f" - integrity sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g== - -"@rollup/rollup-linux-x64-musl@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.1.tgz#eb7494ebc5199cbd2e5c38c2b8acbe2603f35e03" - integrity sha512-t4QSR7gN+OEZLG0MiCgPqMWZGwmeHhsM4AkegJ0Kiy6TnJ9vZ8dEIwHw1LcZKhbHxTY32hp9eVCMdR3/I8MGRw== - -"@rollup/rollup-win32-arm64-msvc@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz#8ffecc980ae4d9899eb2f9c4ae471a8d58d2da6b" - integrity sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA== - -"@rollup/rollup-win32-arm64-msvc@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz#4349482d17f5d1c58604d1c8900540d676f420e0" - integrity sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw== - -"@rollup/rollup-win32-arm64-msvc@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.1.tgz#5bebc66e3a7f82d4b9aa9ff448e7fc13a69656e9" - integrity sha512-7XI4ZCBN34cb+BH557FJPmh0kmNz2c25SCQeT9OiFWEgf8+dL6ZwJ8f9RnUIit+j01u07Yvrsuu1rZGxJCc51g== - -"@rollup/rollup-win32-ia32-msvc@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz#a7505884f415662e088365b9218b2b03a88fc6f2" - integrity sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw== - -"@rollup/rollup-win32-ia32-msvc@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz#a6fc39a15db618040ec3c2a24c1e26cb5f4d7422" - integrity sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g== - -"@rollup/rollup-win32-ia32-msvc@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.1.tgz#34156ebf8b4de3b20e6497260fe519a30263f8cf" - integrity sha512-yE5c2j1lSWOH5jp+Q0qNL3Mdhr8WuqCNVjc6BxbVfS5cAS6zRmdiw7ktb8GNpDCEUJphILY6KACoFoRtKoqNQg== - -"@rollup/rollup-win32-x64-msvc@4.13.0": - version "4.13.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10" - integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw== - -"@rollup/rollup-win32-x64-msvc@4.22.4": - version "4.22.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz#3dd5d53e900df2a40841882c02e56f866c04d202" - integrity sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q== - -"@rollup/rollup-win32-x64-msvc@4.9.1": - version "4.9.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.1.tgz#d146db7a5949e10837b323ce933ed882ac878262" - integrity sha512-PyJsSsafjmIhVgaI1Zdj7m8BB8mMckFah/xbpplObyHfiXzKcI5UOUXRyOdHW7nz4DpMCuzLnF7v5IWHenCwYA== +"@rollup/rollup-android-arm-eabi@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.2.tgz#07db37fcd9d401aae165f662c0069efd61d4ffcc" + integrity sha512-ufoveNTKDg9t/b7nqI3lwbCG/9IJMhADBNjjz/Jn6LxIZxD7T5L8l2uO/wD99945F1Oo8FvgbbZJRguyk/BdzA== + +"@rollup/rollup-android-arm64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.2.tgz#160975402adf85ecd58a0721ad60ae1779a68147" + integrity sha512-iZoYCiJz3Uek4NI0J06/ZxUgwAfNzqltK0MptPDO4OR0a88R4h0DSELMsflS6ibMCJ4PnLvq8f7O1d7WexUvIA== + +"@rollup/rollup-darwin-arm64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.2.tgz#2b126f0aa4349694fe2941bcbcc4b0982b7f1a49" + integrity sha512-/UhrIxobHYCBfhi5paTkUDQ0w+jckjRZDZ1kcBL132WeHZQ6+S5v9jQPVGLVrLbNUebdIRpIt00lQ+4Z7ys4Rg== + +"@rollup/rollup-darwin-x64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.2.tgz#3f4987eff6195532037c50b8db92736e326b5bb2" + integrity sha512-1F/jrfhxJtWILusgx63WeTvGTwE4vmsT9+e/z7cZLKU8sBMddwqw3UV5ERfOV+H1FuRK3YREZ46J4Gy0aP3qDA== + +"@rollup/rollup-freebsd-arm64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.2.tgz#15fe184ecfafc635879500f6985c954e57697c44" + integrity sha512-1YWOpFcGuC6iGAS4EI+o3BV2/6S0H+m9kFOIlyFtp4xIX5rjSnL3AwbTBxROX0c8yWtiWM7ZI6mEPTI7VkSpZw== + +"@rollup/rollup-freebsd-x64@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.2.tgz#c72d37315d36b6e0763b7aabb6ae53c361b45e05" + integrity sha512-3qAqTewYrCdnOD9Gl9yvPoAoFAVmPJsBvleabvx4bnu1Kt6DrB2OALeRVag7BdWGWLhP1yooeMLEi6r2nYSOjg== + +"@rollup/rollup-linux-arm-gnueabihf@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.2.tgz#f274f81abf845dcca5f1f40d434a09a79a3a73a0" + integrity sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig== + +"@rollup/rollup-linux-arm-musleabihf@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.2.tgz#9edaeb1a9fa7d4469917cb0614f665f1cf050625" + integrity sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw== + +"@rollup/rollup-linux-arm64-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.2.tgz#6eb6851f594336bfa00f074f58a00a61e9751493" + integrity sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ== + +"@rollup/rollup-linux-arm64-musl@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.2.tgz#9d8dc8e80df8f156d2888ecb8d6c96d653580731" + integrity sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.2.tgz#358e3e7dda2d60c46ff7c74f7075045736df5b50" + integrity sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw== + +"@rollup/rollup-linux-riscv64-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.2.tgz#b08461ace599c3f0b5f27051f1756b6cf1c78259" + integrity sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg== + +"@rollup/rollup-linux-s390x-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.2.tgz#daab36c9b5c8ac4bfe5a9c4c39ad711464b7dfee" + integrity sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q== + +"@rollup/rollup-linux-x64-gnu@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz#4cc3a4f31920bdb028dbfd7ce0e972a17424a63c" + integrity sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ== + +"@rollup/rollup-linux-x64-musl@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.2.tgz#59800e26c538517ee05f4645315d9e1aded93200" + integrity sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q== + +"@rollup/rollup-win32-arm64-msvc@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.2.tgz#c80e2c33c952b6b171fa6ad9a97dfbb2e4ebee44" + integrity sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw== + +"@rollup/rollup-win32-ia32-msvc@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.2.tgz#a1e9d275cb16f6d5feb9c20aee7e897b1e193359" + integrity sha512-ZhcrakbqA1SCiJRMKSU64AZcYzlZ/9M5LaYil9QWxx9vLnkQ9Vnkve17Qn4SjlipqIIBFKjBES6Zxhnvh0EAEw== + +"@rollup/rollup-win32-x64-msvc@4.24.2": + version "4.24.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.2.tgz#0610af0fb8fec52be779d5b163bbbd6930150467" + integrity sha512-2mLH46K1u3r6uwc95hU+OR9q/ggYMpnS7pSp83Ece1HUQgF9Nh/QwTK5rcgbFnV9j+08yBrU5sA/P0RK2MSBNA== "@schematics/angular@14.2.13": version "14.2.13" @@ -9669,16 +9549,21 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/estree@1.0.5", "@types/estree@^1.0.1", "@types/estree@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" - integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/estree@^0.0.51": version "0.0.51" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@^1.0.1", "@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/express-serve-static-core@*": version "4.17.43" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" @@ -29622,7 +29507,7 @@ rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@3.29.5: +rollup@3.29.5, rollup@^3.27.1, rollup@^3.28.1: version "3.29.5" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w== @@ -29630,84 +29515,37 @@ rollup@3.29.5: fsevents "~2.3.2" rollup@^2.70.0: - version "2.79.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" - integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== - optionalDependencies: - fsevents "~2.3.2" - -rollup@^3.27.1, rollup@^3.28.1: - version "3.29.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" - integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + version "2.79.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" + integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ== optionalDependencies: fsevents "~2.3.2" -rollup@^4.13.0: - version "4.13.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.0.tgz#dd2ae144b4cdc2ea25420477f68d4937a721237a" - integrity sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg== +rollup@^4.13.0, rollup@^4.18.0, rollup@^4.2.0, rollup@^4.20.0, rollup@^4.24.2: + version "4.24.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.2.tgz#04bbe819c1a0cd933533b79687f5dc43efb7a7f0" + integrity sha512-do/DFGq5g6rdDhdpPq5qb2ecoczeK6y+2UAjdJ5trjQJj5f1AiVdLRWRc9A9/fFukfvJRgM0UXzxBIYMovm5ww== dependencies: - "@types/estree" "1.0.5" - optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.13.0" - "@rollup/rollup-android-arm64" "4.13.0" - "@rollup/rollup-darwin-arm64" "4.13.0" - "@rollup/rollup-darwin-x64" "4.13.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.13.0" - "@rollup/rollup-linux-arm64-gnu" "4.13.0" - "@rollup/rollup-linux-arm64-musl" "4.13.0" - "@rollup/rollup-linux-riscv64-gnu" "4.13.0" - "@rollup/rollup-linux-x64-gnu" "4.13.0" - "@rollup/rollup-linux-x64-musl" "4.13.0" - "@rollup/rollup-win32-arm64-msvc" "4.13.0" - "@rollup/rollup-win32-ia32-msvc" "4.13.0" - "@rollup/rollup-win32-x64-msvc" "4.13.0" - fsevents "~2.3.2" - -rollup@^4.18.0, rollup@^4.20.0: - version "4.22.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.22.4.tgz#4135a6446671cd2a2453e1ad42a45d5973ec3a0f" - integrity sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A== - dependencies: - "@types/estree" "1.0.5" - optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.22.4" - "@rollup/rollup-android-arm64" "4.22.4" - "@rollup/rollup-darwin-arm64" "4.22.4" - "@rollup/rollup-darwin-x64" "4.22.4" - "@rollup/rollup-linux-arm-gnueabihf" "4.22.4" - "@rollup/rollup-linux-arm-musleabihf" "4.22.4" - "@rollup/rollup-linux-arm64-gnu" "4.22.4" - "@rollup/rollup-linux-arm64-musl" "4.22.4" - "@rollup/rollup-linux-powerpc64le-gnu" "4.22.4" - "@rollup/rollup-linux-riscv64-gnu" "4.22.4" - "@rollup/rollup-linux-s390x-gnu" "4.22.4" - "@rollup/rollup-linux-x64-gnu" "4.22.4" - "@rollup/rollup-linux-x64-musl" "4.22.4" - "@rollup/rollup-win32-arm64-msvc" "4.22.4" - "@rollup/rollup-win32-ia32-msvc" "4.22.4" - "@rollup/rollup-win32-x64-msvc" "4.22.4" - fsevents "~2.3.2" - -rollup@^4.2.0: - version "4.9.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.9.1.tgz#351d6c03e4e6bcd7a0339df3618d2aeeb108b507" - integrity sha512-pgPO9DWzLoW/vIhlSoDByCzcpX92bKEorbgXuZrqxByte3JFk2xSW2JEeAcyLc9Ru9pqcNNW+Ob7ntsk2oT/Xw== + "@types/estree" "1.0.6" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.9.1" - "@rollup/rollup-android-arm64" "4.9.1" - "@rollup/rollup-darwin-arm64" "4.9.1" - "@rollup/rollup-darwin-x64" "4.9.1" - "@rollup/rollup-linux-arm-gnueabihf" "4.9.1" - "@rollup/rollup-linux-arm64-gnu" "4.9.1" - "@rollup/rollup-linux-arm64-musl" "4.9.1" - "@rollup/rollup-linux-riscv64-gnu" "4.9.1" - "@rollup/rollup-linux-x64-gnu" "4.9.1" - "@rollup/rollup-linux-x64-musl" "4.9.1" - "@rollup/rollup-win32-arm64-msvc" "4.9.1" - "@rollup/rollup-win32-ia32-msvc" "4.9.1" - "@rollup/rollup-win32-x64-msvc" "4.9.1" + "@rollup/rollup-android-arm-eabi" "4.24.2" + "@rollup/rollup-android-arm64" "4.24.2" + "@rollup/rollup-darwin-arm64" "4.24.2" + "@rollup/rollup-darwin-x64" "4.24.2" + "@rollup/rollup-freebsd-arm64" "4.24.2" + "@rollup/rollup-freebsd-x64" "4.24.2" + "@rollup/rollup-linux-arm-gnueabihf" "4.24.2" + "@rollup/rollup-linux-arm-musleabihf" "4.24.2" + "@rollup/rollup-linux-arm64-gnu" "4.24.2" + "@rollup/rollup-linux-arm64-musl" "4.24.2" + "@rollup/rollup-linux-powerpc64le-gnu" "4.24.2" + "@rollup/rollup-linux-riscv64-gnu" "4.24.2" + "@rollup/rollup-linux-s390x-gnu" "4.24.2" + "@rollup/rollup-linux-x64-gnu" "4.24.2" + "@rollup/rollup-linux-x64-musl" "4.24.2" + "@rollup/rollup-win32-arm64-msvc" "4.24.2" + "@rollup/rollup-win32-ia32-msvc" "4.24.2" + "@rollup/rollup-win32-x64-msvc" "4.24.2" fsevents "~2.3.2" rrweb-cssom@^0.6.0: From e72d31d4ce247a1fd6632c7c9ac622a91414da3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:04:41 +0000 Subject: [PATCH 21/44] feat(deps): bump @opentelemetry/instrumentation-fs from 0.15.0 to 0.16.0 (#14098) --- packages/node/package.json | 2 +- yarn.lock | 31 +++++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 7d0239317d90..55ff27fb8b48 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -74,7 +74,7 @@ "@opentelemetry/instrumentation-dataloader": "0.12.0", "@opentelemetry/instrumentation-express": "0.43.0", "@opentelemetry/instrumentation-fastify": "0.40.0", - "@opentelemetry/instrumentation-fs": "0.15.0", + "@opentelemetry/instrumentation-fs": "0.16.0", "@opentelemetry/instrumentation-generic-pool": "0.39.0", "@opentelemetry/instrumentation-graphql": "0.43.0", "@opentelemetry/instrumentation-hapi": "0.41.0", diff --git a/yarn.lock b/yarn.lock index 2f6d8299eb64..ee692b334aff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7189,6 +7189,13 @@ dependencies: "@opentelemetry/api" "^1.0.0" +"@opentelemetry/api-logs@0.54.0": + version "0.54.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.54.0.tgz#a8e09ae22f6d318b6202765dbc2cc0b05e3377be" + integrity sha512-9HhEh5GqFrassUndqJsyW7a0PzfyWr2eV2xwzHLIS+wX3125+9HE9FMRAKmJRwxZhgZGwH3HNQQjoMGZqmOeVA== + dependencies: + "@opentelemetry/api" "^1.3.0" + "@opentelemetry/api@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.12.0.tgz#0359c3926e8f16fdcd8c78f196bd1e9fc4e66777" @@ -7196,7 +7203,7 @@ dependencies: "@opentelemetry/context-base" "^0.12.0" -"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0": +"@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.8", "@opentelemetry/api@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== @@ -7306,13 +7313,13 @@ "@opentelemetry/instrumentation" "^0.53.0" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-fs@0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.15.0.tgz#41658507860f39fee5209bca961cea8d24ca2a83" - integrity sha512-JWVKdNLpu1skqZQA//jKOcKdJC66TWKqa2FUFq70rKohvaSq47pmXlnabNO+B/BvLfmidfiaN35XakT5RyMl2Q== +"@opentelemetry/instrumentation-fs@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.16.0.tgz#aa1cc3aa81011ad9843a0156b200f06f31ffa03e" + integrity sha512-hMDRUxV38ln1R3lNz6osj3YjlO32ykbHqVrzG7gEhGXFQfu7LJUx8t9tEwE4r2h3CD4D0Rw4YGDU4yF4mP3ilg== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.54.0" "@opentelemetry/instrumentation-generic-pool@0.39.0": version "0.39.0" @@ -7487,6 +7494,18 @@ semver "^7.5.2" shimmer "^1.2.1" +"@opentelemetry/instrumentation@^0.54.0": + version "0.54.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.54.0.tgz#3fa9df964d3b157ea7ef2270168d343331d6448e" + integrity sha512-B0Ydo9g9ehgNHwtpc97XivEzjz0XBKR6iQ83NTENIxEEf5NHE0otZQuZLgDdey1XNk+bP1cfRpIkSFWM5YlSyg== + dependencies: + "@opentelemetry/api-logs" "0.54.0" + "@types/shimmer" "^1.2.0" + import-in-the-middle "^1.8.1" + require-in-the-middle "^7.1.1" + semver "^7.5.2" + shimmer "^1.2.1" + "@opentelemetry/propagation-utils@^0.30.11": version "0.30.11" resolved "https://registry.yarnpkg.com/@opentelemetry/propagation-utils/-/propagation-utils-0.30.11.tgz#0a1c51cb4a2724fa41c41be07024bbb6f0aade46" From 8e2d7d2fa054fc9fc662ef5e6d9aa43286e46f4a Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 28 Oct 2024 12:10:06 -0400 Subject: [PATCH 22/44] perf(otel): Only calculate current timestamp once (#14094) resolves https://github.com/getsentry/sentry-javascript/issues/14067 Avoid calling `Date.now()` for each span in the span exporter. This should reduce blocking I/O. --- packages/opentelemetry/src/spanExporter.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index d00319ec2c98..5ba28f8b1607 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -105,8 +105,9 @@ export class SentrySpanExporter { * We do this to avoid leaking memory. */ private _cleanupOldSpans(spans = this._finishedSpans): void { + const currentTimeSeconds = Date.now() / 1000; this._finishedSpans = spans.filter(span => { - const shouldDrop = shouldCleanupSpan(span, this._timeout); + const shouldDrop = shouldCleanupSpan(span, currentTimeSeconds, this._timeout); DEBUG_BUILD && shouldDrop && logger.log( @@ -174,8 +175,8 @@ function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { return nodes.filter(nodeIsCompletedRootNode); } -function shouldCleanupSpan(span: ReadableSpan, maxStartTimeOffsetSeconds: number): boolean { - const cutoff = Date.now() / 1000 - maxStartTimeOffsetSeconds; +function shouldCleanupSpan(span: ReadableSpan, currentTimeSeconds: number, maxStartTimeOffsetSeconds: number): boolean { + const cutoff = currentTimeSeconds - maxStartTimeOffsetSeconds; return spanTimeInputToSeconds(span.startTime) < cutoff; } From 5a2719b4289abe98c4930ad2b287d179ac3fb87a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:34:53 +0000 Subject: [PATCH 23/44] feat(deps): bump @opentelemetry/instrumentation-express from 0.43.0 to 0.44.0 (#14102) --- packages/node/package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 55ff27fb8b48..5aeb4e2a829d 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -72,7 +72,7 @@ "@opentelemetry/instrumentation-amqplib": "^0.42.0", "@opentelemetry/instrumentation-connect": "0.39.0", "@opentelemetry/instrumentation-dataloader": "0.12.0", - "@opentelemetry/instrumentation-express": "0.43.0", + "@opentelemetry/instrumentation-express": "0.44.0", "@opentelemetry/instrumentation-fastify": "0.40.0", "@opentelemetry/instrumentation-fs": "0.16.0", "@opentelemetry/instrumentation-generic-pool": "0.39.0", diff --git a/yarn.lock b/yarn.lock index ee692b334aff..2b966cb4f3b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7295,13 +7295,13 @@ dependencies: "@opentelemetry/instrumentation" "^0.53.0" -"@opentelemetry/instrumentation-express@0.43.0": - version "0.43.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.43.0.tgz#35ff5bcf40b816d9a9159d5f7814ed7e5d83f69b" - integrity sha512-bxTIlzn9qPXJgrhz8/Do5Q3jIlqfpoJrSUtVGqH+90eM1v2PkPHc+SdE+zSqe4q9Y1UQJosmZ4N4bm7Zj/++MA== +"@opentelemetry/instrumentation-express@0.44.0": + version "0.44.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-express/-/instrumentation-express-0.44.0.tgz#51dc11e3152ffbee1c4e389298aac30231c8270a" + integrity sha512-GWgibp6Q0wxyFaaU8ERIgMMYgzcHmGrw3ILUtGchLtLncHNOKk0SNoWGqiylXWWT4HTn5XdV8MGawUgpZh80cA== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.54.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@opentelemetry/instrumentation-fastify@0.40.0": @@ -7587,12 +7587,12 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz#390eb4d42a29c66bdc30066af9035645e9bb7270" integrity sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ== -"@opentelemetry/semantic-conventions@1.25.1", "@opentelemetry/semantic-conventions@^1.17.0": +"@opentelemetry/semantic-conventions@1.25.1": version "1.25.1" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz#0deecb386197c5e9c2c28f2f89f51fb8ae9f145e" integrity sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ== -"@opentelemetry/semantic-conventions@1.27.0", "@opentelemetry/semantic-conventions@^1.27.0": +"@opentelemetry/semantic-conventions@1.27.0", "@opentelemetry/semantic-conventions@^1.17.0", "@opentelemetry/semantic-conventions@^1.27.0": version "1.27.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz#1a857dcc95a5ab30122e04417148211e6f945e6c" integrity sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg== From bf1187e3e444402adb0919131d91abef461e35b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:35:26 +0000 Subject: [PATCH 24/44] ci(deps): bump denoland/setup-deno from 1.5.1 to 2.0.1 (#14096) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f357d9a4a7b..92ec497cb027 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -437,7 +437,7 @@ jobs: with: node-version-file: 'package.json' - name: Set up Deno - uses: denoland/setup-deno@v1.5.1 + uses: denoland/setup-deno@v2.0.1 with: deno-version: v1.38.5 - name: Restore caches From 3dc4d088491c658a0611a3d301653273ebb53c07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:36:59 +0000 Subject: [PATCH 25/44] feat(deps): bump @opentelemetry/instrumentation-aws-sdk from 0.44.0 to 0.45.0 (#14099) --- packages/aws-serverless/package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 76973f30944e..7c9e716df177 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@opentelemetry/instrumentation-aws-lambda": "0.44.0", - "@opentelemetry/instrumentation-aws-sdk": "0.44.0", + "@opentelemetry/instrumentation-aws-sdk": "0.45.0", "@sentry/core": "8.35.0", "@sentry/node": "8.35.0", "@sentry/types": "8.35.0", diff --git a/yarn.lock b/yarn.lock index 2b966cb4f3b3..c3aea5ce6a2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7268,14 +7268,14 @@ "@opentelemetry/semantic-conventions" "^1.27.0" "@types/aws-lambda" "8.10.143" -"@opentelemetry/instrumentation-aws-sdk@0.44.0": - version "0.44.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.44.0.tgz#f1a2d8c186d37fae42954921bbdcc3555aac331c" - integrity sha512-HIWFg4TDQsayceiikOnruMmyQ0SZYW6WiR+wknWwWVLHC3lHTCpAnqzp5V42ckArOdlwHZu2Jvq2GMSM4Myx3w== +"@opentelemetry/instrumentation-aws-sdk@0.45.0": + version "0.45.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.45.0.tgz#269371282ba95877937a11170843cf9d9436706f" + integrity sha512-3EGgC0LFZuFfXcOeslhXHhsiInVhhN046YQsYIPflsicAk7v0wN946sZKWuerEfmqx/kFXOsbOeI1SkkTRmqWQ== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" - "@opentelemetry/propagation-utils" "^0.30.11" + "@opentelemetry/instrumentation" "^0.54.0" + "@opentelemetry/propagation-utils" "^0.30.12" "@opentelemetry/semantic-conventions" "^1.27.0" "@opentelemetry/instrumentation-connect@0.39.0": @@ -7506,10 +7506,10 @@ semver "^7.5.2" shimmer "^1.2.1" -"@opentelemetry/propagation-utils@^0.30.11": - version "0.30.11" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagation-utils/-/propagation-utils-0.30.11.tgz#0a1c51cb4a2724fa41c41be07024bbb6f0aade46" - integrity sha512-rY4L/2LWNk5p/22zdunpqVmgz6uN419DsRTw5KFMa6u21tWhXS8devlMy4h8m8nnS20wM7r6yYweCNNKjgLYJw== +"@opentelemetry/propagation-utils@^0.30.12": + version "0.30.12" + resolved "https://registry.yarnpkg.com/@opentelemetry/propagation-utils/-/propagation-utils-0.30.12.tgz#58200cfd085e791bab5e3c4d36d77b2c60fc2b6b" + integrity sha512-bgab3q/4dYUutUpQCEaSDa+mLoQJG3vJKeSiGuhM4iZaSpkz8ov0fs1MGil5PfxCo6Hhw3bB3bFYhUtnsfT/Pg== "@opentelemetry/propagator-aws-xray@^1.3.1": version "1.25.1" From cfd411e814d33e1940c1695ec77b12268fa96dbb Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 28 Oct 2024 15:55:01 -0400 Subject: [PATCH 26/44] fix(profiling-node): Always warn when running on incompatible major version of Node.js (#14043) This seems important enough to warn about each time regardless of debug build Fixes: https://github.com/getsentry/sentry-javascript/issues/14015 --------- Co-authored-by: Luca Forstner --- packages/profiling-node/src/integration.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 2eb0a59142ae..1af7b8d82dea 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -11,7 +11,7 @@ import { import type { NodeClient } from '@sentry/node'; import type { Event, IntegrationFn, Profile, ProfileChunk, ProfilingIntegration, Span } from '@sentry/types'; -import { LRUMap, logger, uuid4 } from '@sentry/utils'; +import { LRUMap, consoleSandbox, logger, uuid4 } from '@sentry/utils'; import { CpuProfilerBindings } from './cpu_profiler'; import { DEBUG_BUILD } from './debug-build'; @@ -426,13 +426,16 @@ class ContinuousProfiler { /** Exported only for tests. */ export const _nodeProfilingIntegration = ((): ProfilingIntegration => { - if (DEBUG_BUILD && ![16, 18, 20, 22].includes(NODE_MAJOR)) { - logger.warn( - `[Profiling] You are using a Node.js version that does not have prebuilt binaries (${NODE_VERSION}).`, - 'The @sentry/profiling-node package only has prebuilt support for the following LTS versions of Node.js: 16, 18, 20, 22.', - 'To use the @sentry/profiling-node package with this version of Node.js, you will need to compile the native addon from source.', - 'See: https://github.com/getsentry/sentry-javascript/tree/develop/packages/profiling-node#building-the-package-from-source', - ); + if (![16, 18, 20, 22].includes(NODE_MAJOR)) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry Profiling] You are using a Node.js version that does not have prebuilt binaries (${NODE_VERSION}).`, + 'The @sentry/profiling-node package only has prebuilt support for the following LTS versions of Node.js: 16, 18, 20, 22.', + 'To use the @sentry/profiling-node package with this version of Node.js, you will need to compile the native addon from source.', + 'See: https://github.com/getsentry/sentry-javascript/tree/develop/packages/profiling-node#building-the-package-from-source', + ); + }); } return { From 5d2fc2cf2db14d3fff8f9f8f6fd6fdb81ac3c61b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:59:42 +0000 Subject: [PATCH 27/44] feat(deps): bump @opentelemetry/instrumentation-kafkajs from 0.3.0 to 0.4.0 (#14100) --- packages/node/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 5aeb4e2a829d..3b183e093f92 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -80,7 +80,7 @@ "@opentelemetry/instrumentation-hapi": "0.41.0", "@opentelemetry/instrumentation-http": "0.53.0", "@opentelemetry/instrumentation-ioredis": "0.43.0", - "@opentelemetry/instrumentation-kafkajs": "0.3.0", + "@opentelemetry/instrumentation-kafkajs": "0.4.0", "@opentelemetry/instrumentation-koa": "0.43.0", "@opentelemetry/instrumentation-lru-memoizer": "0.40.0", "@opentelemetry/instrumentation-mongodb": "0.47.0", diff --git a/yarn.lock b/yarn.lock index c3aea5ce6a2f..8961bcc8b006 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7363,12 +7363,12 @@ "@opentelemetry/redis-common" "^0.36.2" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-kafkajs@0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.3.0.tgz#6687bce4dac8b90ef8ccbf1b662d5d1e95a34414" - integrity sha512-UnkZueYK1ise8FXQeKlpBd7YYUtC7mM8J0wzUSccEfc/G8UqHQqAzIyYCUOUPUKp8GsjLnWOOK/3hJc4owb7Jg== +"@opentelemetry/instrumentation-kafkajs@0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.4.0.tgz#c1fe0de45a65a66581be0d7422f6828cc806b3bb" + integrity sha512-I9VwDG314g7SDL4t8kD/7+1ytaDBRbZQjhVaQaVIDR8K+mlsoBhLsWH79yHxhHQKvwCSZwqXF+TiTOhoQVUt7A== dependencies: - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.54.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@opentelemetry/instrumentation-koa@0.43.0": From dd0d7e004ec4d2fdc59ca8dd3a518508cd02896a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:04:31 +0000 Subject: [PATCH 28/44] feat(deps): bump @opentelemetry/instrumentation-connect from 0.39.0 to 0.40.0 (#14101) --- packages/node/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node/package.json b/packages/node/package.json index 3b183e093f92..a4d3bdd3e457 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -70,7 +70,7 @@ "@opentelemetry/core": "^1.25.1", "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/instrumentation-amqplib": "^0.42.0", - "@opentelemetry/instrumentation-connect": "0.39.0", + "@opentelemetry/instrumentation-connect": "0.40.0", "@opentelemetry/instrumentation-dataloader": "0.12.0", "@opentelemetry/instrumentation-express": "0.44.0", "@opentelemetry/instrumentation-fastify": "0.40.0", diff --git a/yarn.lock b/yarn.lock index 8961bcc8b006..92ab170a2250 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7278,13 +7278,13 @@ "@opentelemetry/propagation-utils" "^0.30.12" "@opentelemetry/semantic-conventions" "^1.27.0" -"@opentelemetry/instrumentation-connect@0.39.0": - version "0.39.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.39.0.tgz#32bdbaac464cba061c95df6c850ee81efdd86f8b" - integrity sha512-pGBiKevLq7NNglMgqzmeKczF4XQMTOUOTkK8afRHMZMnrK3fcETyTH7lVaSozwiOM3Ws+SuEmXZT7DYrrhxGlg== +"@opentelemetry/instrumentation-connect@0.40.0": + version "0.40.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.40.0.tgz#cb151b860ad8a711ebce4d7e025dcde95e4ba2c5" + integrity sha512-3aR/3YBQ160siitwwRLjwqrv2KBT16897+bo6yz8wIfel6nWOxTZBJudcbsK3p42pTC7qrbotJ9t/1wRLpv79Q== dependencies: "@opentelemetry/core" "^1.8.0" - "@opentelemetry/instrumentation" "^0.53.0" + "@opentelemetry/instrumentation" "^0.54.0" "@opentelemetry/semantic-conventions" "^1.27.0" "@types/connect" "3.4.36" From f32db112485603c2c7c1e9ed7b6da7eade2e1440 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:04:47 +0000 Subject: [PATCH 29/44] chore(deps-dev): bump vite from 5.2.11 to 5.2.14 in /dev-packages/e2e-tests/test-applications/tanstack-router (#14097) --- .../test-applications/tanstack-router/package.json | 2 +- .../e2e-tests/test-applications/tanstack-router/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json index 7bee3d4ba828..f2b82afa15c8 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/package.json @@ -24,7 +24,7 @@ "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.5.0", "typescript": "^5.2.2", - "vite": "^5.2.0", + "vite": "^5.2.14", "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils" }, diff --git a/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock b/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock index de5c5679acb1..6ef9cc8aa826 100644 --- a/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock +++ b/dev-packages/e2e-tests/test-applications/tanstack-router/yarn.lock @@ -922,10 +922,10 @@ use-sync-external-store@^1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== -vite@^5.2.0: - version "5.2.11" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.11.tgz#726ec05555431735853417c3c0bfb36003ca0cbd" - integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ== +vite@^5.2.14: + version "5.2.14" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.14.tgz#fd5f60facf6b5f90ec7da6323c467a365d380c3d" + integrity sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA== dependencies: esbuild "^0.20.1" postcss "^8.4.38" From d62a65caae8e684a57431773cbe34478b3fa93ad Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 28 Oct 2024 21:17:24 +0100 Subject: [PATCH 30/44] meta: Bump E2E test timeouts (#14103) --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92ec497cb027..3f07002e0c5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -858,7 +858,7 @@ jobs: if: always() && needs.job_e2e_prepare.result == 'success' needs: [job_get_metadata, job_build, job_e2e_prepare] runs-on: ubuntu-20.04 - timeout-minutes: 10 + timeout-minutes: 15 env: # We just use a dummy DSN here, only send to the tunnel anyhow E2E_TEST_DSN: 'https://username@domain/123' @@ -1070,7 +1070,7 @@ jobs: github.actor != 'dependabot[bot]' needs: [job_get_metadata, job_build, job_e2e_prepare] runs-on: ubuntu-20.04 - timeout-minutes: 10 + timeout-minutes: 15 env: E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }} E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} @@ -1234,7 +1234,7 @@ jobs: ) needs: [job_get_metadata, job_build, job_e2e_prepare] runs-on: ubuntu-20.04 - timeout-minutes: 10 + timeout-minutes: 15 env: E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }} E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }} From 64108415c68ffc4c1fbfec2de0256a9981da7963 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 28 Oct 2024 16:20:21 -0400 Subject: [PATCH 31/44] chore(tests): Upgrade mysql2 package to 3.11.3 (#14105) Only used in tests, but I'm upgrading this because most users will be on these versions anyway considering the amount of security issues in earlier versions. resolves https://github.com/getsentry/sentry-javascript/security/dependabot/342 resolves https://github.com/getsentry/sentry-javascript/security/dependabot/336 resolves https://github.com/getsentry/sentry-javascript/security/dependabot/334 resolves https://github.com/getsentry/sentry-javascript/security/dependabot/335 resolves https://github.com/getsentry/sentry-javascript/security/dependabot/338 --- .../node-integration-tests/package.json | 2 +- yarn.lock | 26 ++++++++++++------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 0870127845c7..ca15cd078ecf 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -56,7 +56,7 @@ "mongodb-memory-server-global": "^7.6.3", "mongoose": "^5.13.22", "mysql": "^2.18.1", - "mysql2": "^3.7.1", + "mysql2": "^3.11.3", "nock": "^13.1.0", "node-cron": "^3.0.3", "node-schedule": "^2.1.1", diff --git a/yarn.lock b/yarn.lock index 92ab170a2250..729c2ad3f5b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12235,6 +12235,11 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +aws-ssl-profiles@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" + integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== + axios@1.6.7, axios@^1.0.0, axios@^1.6.7: version "1.6.7" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" @@ -23268,11 +23273,6 @@ lru-cache@^7.14.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -lru-cache@^8.0.0: - version "8.0.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e" - integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA== - lru-cache@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.0.1.tgz#ac061ed291f8b9adaca2b085534bb1d3b61bef83" @@ -23291,6 +23291,11 @@ lru-memoizer@2.3.0: lodash.clonedeep "^4.5.0" lru-cache "6.0.0" +lru.min@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/lru.min/-/lru.min-1.1.1.tgz#146e01e3a183fa7ba51049175de04667d5701f0e" + integrity sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q== + lunr@^2.3.8: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" @@ -24802,16 +24807,17 @@ mute-stream@~1.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e" integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== -mysql2@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.7.1.tgz#bb088fa3f01deefbfe04adaf0d3ec18571b33410" - integrity sha512-4EEqYu57mnkW5+Bvp5wBebY7PpfyrmvJ3knHcmLkp8FyBu4kqgrF2GxIjsC2tbLNZWqJaL21v/MYH7bU5f03oA== +mysql2@^3.11.3: + version "3.11.3" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.11.3.tgz#8291e6069a0784310846f6437b8527050dfc10c4" + integrity sha512-Qpu2ADfbKzyLdwC/5d4W7+5Yz7yBzCU05YWt5npWzACST37wJsB23wgOSo00qi043urkiRwXtEvJc9UnuLX/MQ== dependencies: + aws-ssl-profiles "^1.1.1" denque "^2.1.0" generate-function "^2.3.1" iconv-lite "^0.6.3" long "^5.2.1" - lru-cache "^8.0.0" + lru.min "^1.0.0" named-placeholders "^1.1.3" seq-queue "^0.0.5" sqlstring "^2.3.2" From 09826558f793ebc27879096a0c69792ef90e5b87 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 29 Oct 2024 03:12:05 -0400 Subject: [PATCH 32/44] chore: Don't run dependabot on CodeQL PRs (#14109) We get failures when running dependabot on CodeQL PRs: https://github.com/getsentry/sentry-javascript/actions/runs/11561736812/job/32181414647 ``` Warning: Resource not accessible by integration Error: Resource not accessible by integration Warning: Workflows triggered by Dependabot on the "push" event run with read-only access. Uploading Code Scanning results requires write access. To use Code Scanning with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches. See https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#scanning-on-push for more information on how to configure these events. ``` Given dependabot is not going to change any code (just deps), I think we are safe to remove CodeQL scanning. --- .github/workflows/codeql-analysis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 73ce7ec1f698..c3a36e5a34f7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,6 +14,9 @@ name: 'CI: CodeQL' on: push: branches: [develop] + branches-ignore: + # Ignore dependabot branches + - "dependabot/**" pull_request: # The branches below must be a subset of the branches above branches: [develop] From 22c3865ae849f42ad467a0be3e16eaee566345b1 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 29 Oct 2024 03:27:33 -0400 Subject: [PATCH 33/44] chore: Unify nock versions (#14111) resolves https://github.com/getsentry/sentry-javascript/security/dependabot/313 --- dev-packages/node-integration-tests/package.json | 2 +- packages/google-cloud-serverless/package.json | 2 +- packages/remix/test/integration/package.json | 2 +- yarn.lock | 14 ++++---------- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index ca15cd078ecf..b21948783ff4 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -57,7 +57,7 @@ "mongoose": "^5.13.22", "mysql": "^2.18.1", "mysql2": "^3.11.3", - "nock": "^13.1.0", + "nock": "^13.5.5", "node-cron": "^3.0.3", "node-schedule": "^2.1.1", "pg": "^8.7.3", diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 084aa77101fd..90ee6a85725b 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -61,7 +61,7 @@ "@google-cloud/pubsub": "^2.5.0", "@types/node": "^14.18.0", "google-gax": "^2.9.0", - "nock": "^13.0.4" + "nock": "^13.5.5" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index dba3c023903a..4604b8c8b067 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -19,7 +19,7 @@ "@remix-run/dev": "1.17.0", "@types/react": "^17.0.47", "@types/react-dom": "^17.0.17", - "nock": "^13.1.0", + "nock": "^13.5.5", "typescript": "4.9.5" }, "resolutions": { diff --git a/yarn.lock b/yarn.lock index 729c2ad3f5b9..d49a1943fd7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23098,11 +23098,6 @@ lodash.restparam@^3.0.0: resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - lodash.snakecase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" @@ -25121,14 +25116,13 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -nock@^13.0.4, nock@^13.1.0: - version "13.2.4" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.4.tgz#43a309d93143ee5cdcca91358614e7bde56d20e1" - integrity sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug== +nock@^13.5.5: + version "13.5.5" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.5.5.tgz#cd1caaca281d42be17d51946367a3d53a6af3e78" + integrity sha512-XKYnqUrCwXC8DGG1xX4YH5yNIrlh9c065uaMZZHUoeUUINTOyt+x/G+ezYk0Ft6ExSREVIs+qBJDK503viTfFA== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" - lodash.set "^4.3.2" propagate "^2.0.0" node-abi@^3.3.0: From c7378e608a7d02e2177a87b4fa7887c12ee11ce5 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:04:12 +0100 Subject: [PATCH 34/44] fix(nuxt): Re-export all exported bindings (#14086) This was brought up here: https://github.com/getsentry/sentry-javascript/discussions/14064 Only the "root bindings" were exported, which led to not re-exported functions that were imported from another file before they were exported (like firebase `server`). --- packages/nuxt/src/vite/addServerConfig.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 85fdefda740d..fe86895a76f2 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import { createResolver } from '@nuxt/kit'; import type { Nuxt } from '@nuxt/schema'; -import { consoleSandbox } from '@sentry/utils'; +import { consoleSandbox, flatten } from '@sentry/utils'; import type { Nitro } from 'nitropack'; import type { InputPluginOption } from 'rollup'; import type { SentryNuxtModuleOptions } from '../common/types'; @@ -129,8 +129,9 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug moduleInfo.moduleSideEffects = true; - // The key `.` in `exportedBindings` refer to the exports within the file - const exportedFunctions = moduleInfo.exportedBindings?.['.']; + // `exportedBindings` can look like this: `{ '.': [ 'handler' ], './firebase-gen-1.mjs': [ 'server' ] }` + // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. + const exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {})); // The enclosing `if` already checks for the suffix in `source`, but a check in `resolution.id` is needed as well to prevent multiple attachment of the suffix return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) From 9e676e049dff53f7ff6f018bc9f1210ed587422a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 29 Oct 2024 09:08:51 +0100 Subject: [PATCH 35/44] test(browser-integration): Add sentry DSN route handler by default (#14095) Instead of sprinkling this through tests, we can just generally handle this route, streamlining tests a bit and avoid unexpected errors/console warnings messing with things. So this PR basically inverses this - by default, we add a "success" route handler for the Sentry DSN, and if you want to have special handling you can opt-out of this. Supersedes https://github.com/getsentry/sentry-javascript/pull/14092/files --- .../loader/noOnLoad/replay/test.ts | 8 ---- .../loader/noOnLoad/replayError/test.ts | 8 ---- .../noOnLoad/sdkLoadedInMeanwhile/test.ts | 2 +- .../onLoad/captureExceptionInOnLoad/test.ts | 16 -------- .../loader/onLoad/customInit/test.ts | 8 ---- .../loader/onLoad/customIntegrations/test.ts | 8 ---- .../onLoad/customIntegrationsFunction/test.ts | 8 ---- .../loader/onLoad/customReplay/test.ts | 8 ---- .../loader/onLoad/keepSentryGlobal/test.ts | 8 ---- .../loader/onLoad/onLoadLate/test.ts | 8 ---- .../loader/onLoad/replay/test.ts | 8 ---- .../suites/feedback/attachTo/test.ts | 8 ---- .../suites/feedback/captureFeedback/test.ts | 8 ---- .../hasSampling/test.ts | 8 ---- .../feedback/captureFeedbackCsp/test.ts | 8 ---- .../suites/feedback/logger/test.ts | 8 ---- .../integrations/captureConsole/test.ts | 8 ---- .../skip-init-browser-extension/test.ts | 8 ---- .../skip-init-chrome-extension/test.ts | 8 ---- .../suites/metrics/metricsShim/test.ts | 2 +- .../suites/metrics/timing/test.ts | 16 -------- .../suites/replay/bufferModeManual/test.ts | 22 ++-------- .../suites/replay/bufferModeReload/test.ts | 8 ---- .../replay/canvas/manualSnapshot/test.ts | 8 ---- .../suites/replay/canvas/records/test.ts | 8 ---- .../canvas/withCanvasIntegrationFirst/test.ts | 8 ---- .../withCanvasIntegrationSecond/test.ts | 8 ---- .../canvas/withoutCanvasIntegration/test.ts | 8 ---- .../replay/captureComponentName/test.ts | 16 -------- .../suites/replay/captureConsoleLog/test.ts | 16 -------- .../suites/replay/captureReplay/test.ts | 8 ---- .../captureReplayFromReplayPackage/test.ts | 8 ---- .../replay/captureReplayOffline/test.ts | 8 ---- .../suites/replay/compressionDisabled/test.ts | 8 ---- .../suites/replay/compressionEnabled/test.ts | 8 ---- .../replay/compressionWorkerUrl/test.ts | 8 ---- .../suites/replay/customEvents/test.ts | 24 ----------- .../suites/replay/dsc/test.ts | 24 ----------- .../suites/replay/errorResponse/test.ts | 2 +- .../replay/errors/beforeErrorSampling/test.ts | 8 ---- .../suites/replay/errors/droppedError/test.ts | 2 +- .../suites/replay/errors/errorMode/test.ts | 2 +- .../suites/replay/errors/errorNotSent/test.ts | 2 +- .../replay/errors/errorsInSession/test.ts | 10 +---- .../replay/errors/immediateError/test.ts | 8 ---- .../suites/replay/eventBufferError/test.ts | 6 --- .../fetch/captureRequestBody/test.ts | 40 ------------------- .../fetch/captureRequestHeaders/test.ts | 34 ---------------- .../fetch/captureRequestSize/test.ts | 16 -------- .../fetch/captureResponseBody/test.ts | 32 --------------- .../fetch/captureResponseHeaders/test.ts | 24 ----------- .../fetch/captureResponseSize/test.ts | 24 ----------- .../fetch/captureTimestamps/test.ts | 2 +- .../xhr/captureRequestBody/test.ts | 40 ------------------- .../xhr/captureRequestHeaders/test.ts | 16 -------- .../xhr/captureRequestSize/test.ts | 16 -------- .../xhr/captureResponseBody/test.ts | 40 ------------------- .../xhr/captureResponseHeaders/test.ts | 16 -------- .../xhr/captureResponseSize/test.ts | 24 ----------- .../xhr/captureTimestamps/test.ts | 2 +- .../suites/replay/fileInput/test.ts | 8 ---- .../suites/replay/flushing/test.ts | 8 ---- .../suites/replay/keyboardEvents/test.ts | 8 ---- .../largeMutations/defaultOptions/test.ts | 8 ---- .../largeMutations/mutationLimit/test.ts | 8 ---- .../suites/replay/logger/test.ts | 8 ---- .../suites/replay/maxReplayDuration/test.ts | 8 ---- .../suites/replay/minReplayDuration/test.ts | 8 ---- .../suites/replay/multiple-pages/test.ts | 8 ---- .../suites/replay/privacyBlock/test.ts | 8 ---- .../suites/replay/privacyDefault/test.ts | 8 ---- .../suites/replay/privacyInput/test.ts | 16 -------- .../suites/replay/privacyInputMaskAll/test.ts | 16 -------- .../replay/replayIntegrationShim/test.ts | 2 +- .../suites/replay/replayShim/test.ts | 2 +- .../suites/replay/requests/test.ts | 16 -------- .../suites/replay/sampling/test.ts | 2 +- .../suites/replay/sessionExpiry/test.ts | 8 ---- .../suites/replay/sessionInactive/test.ts | 8 ---- .../suites/replay/sessionMaxAge/test.ts | 8 ---- .../replay/slowClick/clickTargets/test.ts | 16 -------- .../suites/replay/slowClick/disable/test.ts | 8 ---- .../suites/replay/slowClick/error/test.ts | 16 -------- .../suites/replay/slowClick/ignore/test.ts | 16 -------- .../replay/slowClick/multiClick/test.ts | 16 -------- .../suites/replay/slowClick/mutation/test.ts | 40 ------------------- .../suites/replay/slowClick/scroll/test.ts | 16 -------- .../suites/replay/slowClick/timeout/test.ts | 16 -------- .../replay/slowClick/windowOpen/test.ts | 8 ---- .../suites/replay/throttleBreadcrumbs/test.ts | 8 ---- .../suites/replay/unicode/compressed/test.ts | 8 ---- .../replay/unicode/uncompressed/test.ts | 8 ---- .../suites/sessions/initial-scope/test.ts | 8 ---- .../suites/sessions/start-session/test.ts | 8 ---- .../sessions/v7-hub-start-session/test.ts | 8 ---- .../browserTracingIntegrationShim/test.ts | 2 +- .../metrics/web-vitals-inp-late/test.ts | 8 ---- .../web-vitals-inp-parametrized-late/test.ts | 8 ---- .../web-vitals-inp-parametrized/test.ts | 8 ---- .../tracing/metrics/web-vitals-inp/test.ts | 16 -------- .../tracing/trace-lifetime/navigation/test.ts | 8 ---- .../utils/fixtures.ts | 34 +++++++++++++--- 102 files changed, 45 insertions(+), 1141 deletions(-) diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/replay/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/replay/test.ts index 25bcebcd074c..b690ebb36baa 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/replay/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/replay/test.ts @@ -11,14 +11,6 @@ sentryTest('should capture a replay', async ({ getLocalTestUrl, page }) => { 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 }); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/replayError/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/replayError/test.ts index 379697881165..87d0f7ff3db4 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/replayError/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/replayError/test.ts @@ -9,14 +9,6 @@ sentryTest('should capture a replay & attach an error', async ({ getLocalTestUrl 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 }); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts index 62456093a9a6..8a1f1a7c721f 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/noOnLoad/sdkLoadedInMeanwhile/test.ts @@ -28,7 +28,7 @@ sentryTest('it does not download the SDK if the SDK was loaded in the meanwhile' }); }); - const tmpDir = await getLocalTestUrl({ testDir: __dirname, skipRouteHandler: true }); + const tmpDir = await getLocalTestUrl({ testDir: __dirname, skipRouteHandler: true, skipDsnRouteHandler: true }); await page.route(`${TEST_HOST}/*.*`, route => { const file = route.request().url().split('/').pop(); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts index 90868c3e2e8f..f5355ff765e9 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/captureExceptionInOnLoad/test.ts @@ -4,14 +4,6 @@ import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('captureException works inside of onLoad', 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 }); const req = await waitForErrorRequestOnUrl(page, url); @@ -21,14 +13,6 @@ sentryTest('captureException works inside of onLoad', async ({ getLocalTestUrl, }); sentryTest('should set SENTRY_SDK_SOURCE value', 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 }); const req = await waitForErrorRequestOnUrl(page, url); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/test.ts index 7171d2e0121f..689f6b88dd57 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customInit/test.ts @@ -7,14 +7,6 @@ const bundle = process.env.PW_BUNDLE || ''; const isLazy = LOADER_CONFIGS[bundle]?.lazy; sentryTest('always calls onLoad init correctly', 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); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/test.ts index bf08c3df2a0e..b8fc45402b76 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrations/test.ts @@ -8,14 +8,6 @@ sentryTest('should handle custom added integrations & default integrations', asy 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); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/test.ts index 2b4685979e8c..038feb7c7652 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customIntegrationsFunction/test.ts @@ -5,14 +5,6 @@ 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); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/test.ts index d53987b69385..07d877c00211 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/customReplay/test.ts @@ -8,14 +8,6 @@ sentryTest('should handle custom added Replay integration', async ({ getLocalTes 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 }); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/keepSentryGlobal/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/keepSentryGlobal/test.ts index e4a2bf12e829..0f13098d7ae6 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/keepSentryGlobal/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/keepSentryGlobal/test.ts @@ -4,14 +4,6 @@ import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('keeps data on window.Sentry intact', 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 }); const req = await waitForErrorRequestOnUrl(page, url); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/test.ts index 46bbf81f3c58..2aa720fd274e 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/onLoadLate/test.ts @@ -4,14 +4,6 @@ import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers'; sentryTest('late onLoad call is handled', 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 }); const req = await waitForErrorRequestOnUrl(page, url); diff --git a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/replay/test.ts b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/replay/test.ts index c162db40a9e5..afbfbeaba0b8 100644 --- a/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/replay/test.ts +++ b/dev-packages/browser-integration-tests/loader-suites/loader/onLoad/replay/test.ts @@ -8,14 +8,6 @@ sentryTest('should capture a replay', async ({ getLocalTestUrl, page }) => { 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 }); diff --git a/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts b/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts index 507b08685092..8c605597020d 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/attachTo/test.ts @@ -23,14 +23,6 @@ sentryTest('should capture feedback with custom button', async ({ getLocalTestUr } }); - 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); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts index eb16cf1e1848..c553412d5933 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedback/test.ts @@ -23,14 +23,6 @@ sentryTest('should capture feedback', 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); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts index 5a88a429e53c..e42c42433de3 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackAndReplay/hasSampling/test.ts @@ -31,14 +31,6 @@ sentryTest('should capture feedback', async ({ forceFlushReplay, getLocalTestUrl } }); - 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 Promise.all([page.goto(url), page.getByText('Report a Bug').click(), reqPromise0]); diff --git a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/test.ts b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/test.ts index bca9b498fed0..a80fa4c2c6bf 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/captureFeedbackCsp/test.ts @@ -23,14 +23,6 @@ sentryTest('should capture feedback', 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); diff --git a/dev-packages/browser-integration-tests/suites/feedback/logger/test.ts b/dev-packages/browser-integration-tests/suites/feedback/logger/test.ts index 34fadfc2503b..fe9fdf2e8065 100644 --- a/dev-packages/browser-integration-tests/suites/feedback/logger/test.ts +++ b/dev-packages/browser-integration-tests/suites/feedback/logger/test.ts @@ -19,14 +19,6 @@ sentryTest('should log error correctly', async ({ getLocalTestUrl, page }) => { messages.push(message.text()); }); - 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); diff --git a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts index c704725822e7..606ef8925afd 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/captureConsole/test.ts @@ -7,14 +7,6 @@ import { getMultipleSentryEnvelopeRequests } from '../../../utils/helpers'; sentryTest('it captures console messages correctly', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const [, events] = await Promise.all([page.goto(url), getMultipleSentryEnvelopeRequests(page, 7)]); expect(events).toHaveLength(7); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts index aeac53b9957a..7da78e3c1434 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts @@ -4,14 +4,6 @@ import { sentryTest } from '../../../utils/fixtures'; sentryTest( 'should not initialize when inside a Firefox/Safari browser extension', 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 errorLogs: string[] = []; page.on('console', message => { diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts index 3c5cc9c7648b..df0d70d779d2 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts @@ -2,14 +2,6 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; sentryTest('should not initialize when inside a Chrome browser extension', 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 errorLogs: string[] = []; page.on('console', message => { diff --git a/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts b/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts index ba86f0a991f5..d95633393eda 100644 --- a/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/metrics/metricsShim/test.ts @@ -22,7 +22,7 @@ sentryTest('exports shim metrics integration for non-tracing bundles', async ({ }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts b/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts index c6b369025c7a..215f042dcdf7 100644 --- a/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts +++ b/dev-packages/browser-integration-tests/suites/metrics/timing/test.ts @@ -13,14 +13,6 @@ sentryTest('allows to wrap sync methods with a timing metric', async ({ getLocal 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 url = await getLocalTestUrl({ testDir: __dirname }); const beforeTime = Math.floor(Date.now() / 1000); @@ -96,14 +88,6 @@ sentryTest('allows to wrap async methods with a timing metric', async ({ getLoca 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 url = await getLocalTestUrl({ testDir: __dirname }); const beforeTime = Math.floor(Date.now() / 1000); diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/test.ts b/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/test.ts index 4ac7b9ea9cf1..8d6d87e40d36 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/bufferModeManual/test.ts @@ -45,7 +45,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); await page.locator('#go-background').click(); @@ -190,7 +190,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); await page.locator('#go-background').click(); @@ -297,14 +297,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); - 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); @@ -359,14 +351,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); - 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 }); page.goto(url); @@ -440,7 +424,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); // Start buffering and assert that it is enabled diff --git a/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/test.ts b/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/test.ts index 79fc60497ccc..89c4f4d4f369 100644 --- a/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/bufferModeReload/test.ts @@ -15,14 +15,6 @@ sentryTest('continues buffer session in session mode after error & reload', asyn const reqPromise1 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts index 60a0864b71ee..affa6d2f90c5 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/manualSnapshot/test.ts @@ -13,14 +13,6 @@ sentryTest('can manually snapshot canvas', async ({ getLocalTestUrl, page, brows const reqPromise2 = waitForReplayRequest(page, 2); const reqPromise3 = waitForReplayRequest(page, 3); - 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); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts index e2beb0afc2df..c6ba950978cd 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts @@ -12,14 +12,6 @@ sentryTest('can record canvas', async ({ getLocalTestUrl, page, browserName }) = const reqPromise1 = waitForReplayRequest(page, 1); const reqPromise2 = waitForReplayRequest(page, 2); - 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); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/test.ts index 104098eed2cf..514b22af6d5c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationFirst/test.ts @@ -10,14 +10,6 @@ sentryTest('sets up canvas when adding ReplayCanvas integration first', async ({ const reqPromise0 = waitForReplayRequest(page, 0); - 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); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/test.ts index d25066dc065b..a36fc86bcdbe 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withCanvasIntegrationSecond/test.ts @@ -10,14 +10,6 @@ sentryTest('sets up canvas when adding ReplayCanvas integration after Replay', a const reqPromise0 = waitForReplayRequest(page, 0); - 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); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/test.ts index af3ec4a4bc97..8eac849408a7 100644 --- a/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/withoutCanvasIntegration/test.ts @@ -8,14 +8,6 @@ sentryTest('does not setup up canvas without ReplayCanvas integration', async ({ 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 url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts index 9ad1a99c32aa..29c400f4288d 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts @@ -10,14 +10,6 @@ sentryTest('captures component name attribute when available', async ({ forceFlu const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -95,14 +87,6 @@ sentryTest('sets element name to component name attribute', async ({ forceFlushR const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts index 8ea7d10bd158..b55b23c10f04 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts @@ -8,14 +8,6 @@ sentryTest('should capture console messages in replay', async ({ getLocalTestPat 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 reqPromise0 = waitForReplayRequest(page, 0); const url = await getLocalTestPath({ testDir: __dirname }); @@ -59,14 +51,6 @@ sentryTest('should capture very large console logs', async ({ getLocalTestPath, 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 reqPromise0 = waitForReplayRequest(page, 0); const url = await getLocalTestPath({ testDir: __dirname }); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts index b2a7fa6dc3ac..b2cd4196643b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplay/test.ts @@ -12,14 +12,6 @@ sentryTest('should capture replays (@sentry/browser export)', async ({ getLocalT const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts index 6267413ff84e..e9db4c92343c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayFromReplayPackage/test.ts @@ -12,14 +12,6 @@ sentryTest('should capture replays (@sentry-internal/replay export)', async ({ g const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts index a74a2c891fad..f22abf0e3451 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureReplayOffline/test.ts @@ -12,14 +12,6 @@ sentryTest('should capture replays offline', async ({ getLocalTestPath, page }) const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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 getLocalTestPath({ testDir: __dirname }); // This would be the obvious way to test offline support but it doesn't appear to work! diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/test.ts b/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/test.ts index 31ad7fc22991..a719bcd1d844 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/compressionDisabled/test.ts @@ -19,14 +19,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/test.ts b/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/test.ts index a080b2de5bdf..753fe57a0c01 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/compressionEnabled/test.ts @@ -17,14 +17,6 @@ sentryTest('replay recording should be compressed by default', async ({ getLocal const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/test.ts b/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/test.ts index 0b34803e3b7a..0c5710c0dfb7 100644 --- a/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/compressionWorkerUrl/test.ts @@ -21,14 +21,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); - 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 }); let customCompressCalled = 0; diff --git a/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts b/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts index 053c31c3881e..5c93870d0937 100644 --- a/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/customEvents/test.ts @@ -33,14 +33,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -95,14 +87,6 @@ sentryTest( const reqPromise2 = waitForReplayRequest(page, 2); const reqPromise3 = waitForReplayRequest(page, 3); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -198,14 +182,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts index b6bb4da00abb..62961caef062 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -120,14 +120,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -181,14 +173,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -229,14 +213,6 @@ sentryTest('should add replay_id to error DSC while replay is active', async ({ const hasTracing = !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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/errorResponse/test.ts b/dev-packages/browser-integration-tests/suites/replay/errorResponse/test.ts index 67f1d1d12d6d..048ca898d892 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errorResponse/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errorResponse/test.ts @@ -22,7 +22,7 @@ sentryTest('should stop recording after receiving an error response', async ({ g }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await Promise.all([page.goto(url), waitForReplayRequest(page)]); await page.locator('button').click(); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/test.ts index cbb4826df195..71cea051ce94 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/beforeErrorSampling/test.ts @@ -11,14 +11,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts index 04503ca52cb7..2ae046bf6c58 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts @@ -28,7 +28,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); await forceFlushReplay(); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts index b2f7e067e969..8eeb544d06ef 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts @@ -48,7 +48,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await Promise.all([ page.goto(url), diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts index c47f4bf4e105..123ec1609a04 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts @@ -21,7 +21,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); await forceFlushReplay(); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts index 5fb1e7b4e340..bc9453f58135 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts @@ -37,7 +37,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); const req0 = await reqPromise0; @@ -94,14 +94,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/errors/immediateError/test.ts b/dev-packages/browser-integration-tests/suites/replay/errors/immediateError/test.ts index 7c82f29256d9..8ce1af848952 100644 --- a/dev-packages/browser-integration-tests/suites/replay/errors/immediateError/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/errors/immediateError/test.ts @@ -11,14 +11,6 @@ sentryTest( 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 }); diff --git a/dev-packages/browser-integration-tests/suites/replay/eventBufferError/test.ts b/dev-packages/browser-integration-tests/suites/replay/eventBufferError/test.ts index a6924bdda3c9..0502cf8fdcc7 100644 --- a/dev-packages/browser-integration-tests/suites/replay/eventBufferError/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/eventBufferError/test.ts @@ -19,12 +19,6 @@ sentryTest( sentryTest.skip(); } - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - }); - }); - const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts index f8b48ffec598..b7fd02bc463c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts @@ -21,14 +21,6 @@ sentryTest('captures text request body', async ({ getLocalTestUrl, page, browser }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -99,14 +91,6 @@ sentryTest('captures JSON request body', async ({ getLocalTestUrl, page, browser }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -177,14 +161,6 @@ sentryTest('captures non-text request body', async ({ getLocalTestUrl, page, bro }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -259,14 +235,6 @@ sentryTest('captures text request body when matching relative URL', async ({ get }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -335,14 +303,6 @@ sentryTest('does not capture request body when URL does not match', async ({ get }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts index f1952da08f0a..9bbd1f0c487e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts @@ -21,12 +21,6 @@ sentryTest('handles empty/missing request headers', async ({ getLocalTestUrl, pa }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -92,14 +86,6 @@ sentryTest('captures request headers as POJO', async ({ getLocalTestUrl, page, b }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -179,12 +165,6 @@ sentryTest('captures request headers on Request', async ({ getLocalTestUrl, page }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -264,12 +244,6 @@ sentryTest('captures request headers as Headers instance', async ({ getLocalTest }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -347,14 +321,6 @@ sentryTest('does not captures request headers if URL does not match', async ({ g }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts index 4da3c7142996..2e7c22d29fbe 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts @@ -19,14 +19,6 @@ sentryTest('captures request body size when body is sent', async ({ getLocalTest }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -104,14 +96,6 @@ sentryTest('captures request size from non-text request body', async ({ getLocal }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts index 7ce8ecb87748..1d727564a852 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts @@ -22,14 +22,6 @@ sentryTest('captures text response body', async ({ getLocalTestUrl, page, browse }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -102,14 +94,6 @@ sentryTest('captures JSON response body', async ({ getLocalTestUrl, page, browse }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -182,14 +166,6 @@ sentryTest('captures non-text response body', async ({ getLocalTestUrl, page, br }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -262,14 +238,6 @@ sentryTest.skip('does not capture response body when URL does not match', async }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts index ec4a26dd199f..7142a16dd174 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts @@ -21,14 +21,6 @@ sentryTest('handles empty headers', async ({ getLocalTestUrl, page, browserName }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -97,14 +89,6 @@ sentryTest('captures response headers', 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 requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); @@ -179,14 +163,6 @@ sentryTest('does not capture response headers if URL does not match', async ({ g }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts index 0191617d46da..805413f70884 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts @@ -26,14 +26,6 @@ sentryTest('captures response size from Content-Length header if available', asy }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { @@ -116,14 +108,6 @@ sentryTest('captures response size without Content-Length header', async ({ getL }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { @@ -203,14 +187,6 @@ sentryTest('captures response size from non-text response body', async ({ getLoc }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts index d77796f92fc2..e3acdb5dc6e0 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureTimestamps/test.ts @@ -34,7 +34,7 @@ sentryTest('captures correct timestamps', async ({ getLocalTestUrl, page, browse return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.fetch'); }); - const url = await getLocalTestUrl({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); await page.evaluate(() => { diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts index d12d74381fc2..7108bde5fc71 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts @@ -20,14 +20,6 @@ sentryTest('captures text request body', async ({ getLocalTestUrl, page, browser }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -100,14 +92,6 @@ sentryTest('captures JSON request body', async ({ getLocalTestUrl, page, browser }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -180,14 +164,6 @@ sentryTest('captures non-text request body', async ({ getLocalTestUrl, page, bro }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -264,14 +240,6 @@ sentryTest('captures text request body when matching relative URL', async ({ get }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -344,14 +312,6 @@ sentryTest('does not capture request body when URL does not match', async ({ get }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts index be6ac82d4b5c..8485a7078b38 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts @@ -20,14 +20,6 @@ sentryTest('captures request headers', async ({ getLocalTestUrl, page, browserNa }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -105,14 +97,6 @@ sentryTest('does not capture request headers if URL does not match', async ({ ge }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts index 73d063ea1b47..8f263b1d8349 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts @@ -20,14 +20,6 @@ sentryTest('captures request body size when body is sent', async ({ getLocalTest }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -110,14 +102,6 @@ sentryTest('captures request size from non-text request body', async ({ getLocal }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts index afa2c669f204..8975729b7c54 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts @@ -24,14 +24,6 @@ sentryTest('captures text response body', async ({ getLocalTestUrl, page, browse }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -108,14 +100,6 @@ sentryTest('captures JSON response body', async ({ getLocalTestUrl, page, browse }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -192,14 +176,6 @@ sentryTest('captures JSON response body when responseType=json', async ({ getLoc }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -278,14 +254,6 @@ sentryTest('captures non-text response body', async ({ getLocalTestUrl, page, br }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -362,14 +330,6 @@ sentryTest('does not capture response body when URL does not match', async ({ ge }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts index 4726f69c641d..68dcad0af664 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts @@ -27,14 +27,6 @@ sentryTest('captures response headers', async ({ getLocalTestUrl, page, browserN }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -117,14 +109,6 @@ sentryTest( }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts index dda9e4e642c1..6ffccfdbc47e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts @@ -25,14 +25,6 @@ sentryTest( }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -120,14 +112,6 @@ sentryTest('captures response size without Content-Length header', async ({ getL }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); @@ -212,14 +196,6 @@ sentryTest('captures response size for non-string bodies', async ({ getLocalTest }); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const requestPromise = waitForErrorRequest(page); const replayRequestPromise = collectReplayRequests(page, recordingEvents => { return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); diff --git a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts index e89fa6e4a24e..19781d7312bb 100644 --- a/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureTimestamps/test.ts @@ -34,7 +34,7 @@ sentryTest('captures correct timestamps', async ({ getLocalTestUrl, page, browse return getReplayPerformanceSpans(recordingEvents).some(span => span.op === 'resource.xhr'); }); - const url = await getLocalTestUrl({ testDir: __dirname }); + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); void page.evaluate(() => { diff --git a/dev-packages/browser-integration-tests/suites/replay/fileInput/test.ts b/dev-packages/browser-integration-tests/suites/replay/fileInput/test.ts index be8db2893a4f..153ec956fa4b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/fileInput/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/fileInput/test.ts @@ -26,14 +26,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/flushing/test.ts b/dev-packages/browser-integration-tests/suites/replay/flushing/test.ts index 91308d7736c8..52d72fb9a58a 100644 --- a/dev-packages/browser-integration-tests/suites/replay/flushing/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/flushing/test.ts @@ -21,14 +21,6 @@ sentryTest('replay events are flushed after max flush delay was reached', async const reqPromise1 = waitForReplayRequest(page, 1); const reqPromise2 = waitForReplayRequest(page, 2); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/test.ts b/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/test.ts index edeacb7f2db0..d23bfafea394 100644 --- a/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/keyboardEvents/test.ts @@ -10,14 +10,6 @@ sentryTest('captures keyboard events', async ({ forceFlushReplay, getLocalTestPa const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts b/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts index 948c3bb4ea41..07f5362f4f7b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/largeMutations/defaultOptions/test.ts @@ -10,14 +10,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); // We have to click in order to ensure the LCP is generated, leading to consistent results diff --git a/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts b/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts index 672190e2d0a0..d86617396cf6 100644 --- a/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/largeMutations/mutationLimit/test.ts @@ -15,14 +15,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); // We have to click in order to ensure the LCP is generated, leading to consistent results diff --git a/dev-packages/browser-integration-tests/suites/replay/logger/test.ts b/dev-packages/browser-integration-tests/suites/replay/logger/test.ts index fa034a12b003..e194c80f05c4 100644 --- a/dev-packages/browser-integration-tests/suites/replay/logger/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/logger/test.ts @@ -15,14 +15,6 @@ sentryTest('should output logger messages', async ({ getLocalTestPath, page }) = messages.push(message.text()); }); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const reqPromise0 = waitForReplayRequest(page, 0); const url = await getLocalTestPath({ testDir: __dirname }); diff --git a/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/test.ts b/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/test.ts index a133f7ae77d9..538cc5f3aa20 100644 --- a/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/maxReplayDuration/test.ts @@ -11,14 +11,6 @@ sentryTest('keeps track of max duration across reloads', async ({ getLocalTestPa 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 reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); diff --git a/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts b/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts index 1bdb0567de77..967de1ecfe0f 100644 --- a/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/minReplayDuration/test.ts @@ -17,14 +17,6 @@ sentryTest('doest not send replay before min. duration', async ({ getLocalTestPa return true; }); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts index 7bacf5a8ae17..3ee84086cc37 100644 --- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts @@ -37,14 +37,6 @@ sentryTest( 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 reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); const reqPromise2 = waitForReplayRequest(page, 2); diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyBlock/test.ts b/dev-packages/browser-integration-tests/suites/replay/privacyBlock/test.ts index 8b000b07e461..a6f40d884e8b 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyBlock/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/privacyBlock/test.ts @@ -15,14 +15,6 @@ sentryTest('should allow to manually block elements', async ({ getLocalTestPath, const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts index 713431ca3063..75ba5d2831c6 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/privacyDefault/test.ts @@ -15,14 +15,6 @@ sentryTest('should have the correct default privacy settings', async ({ getLocal const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInput/test.ts b/dev-packages/browser-integration-tests/suites/replay/privacyInput/test.ts index f2c506f90132..19dddd68ec0c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInput/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInput/test.ts @@ -54,14 +54,6 @@ sentryTest( return inputMutationSegmentIds.length === 2 && inputMutationSegmentIds[1] < event.segment_id; }); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -139,14 +131,6 @@ sentryTest( return check; }); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts b/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts index 88bc531017d9..7d64cd839d22 100644 --- a/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/privacyInputMaskAll/test.ts @@ -46,14 +46,6 @@ sentryTest( ); }); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -110,14 +102,6 @@ sentryTest( ); }); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts index 952c841b253e..7eb84f7da310 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/replayIntegrationShim/test.ts @@ -24,7 +24,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); await forceFlushReplay(); diff --git a/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts b/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts index b906deefb71b..8d3b7bab9aa0 100644 --- a/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/replayShim/test.ts @@ -24,7 +24,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); await forceFlushReplay(); diff --git a/dev-packages/browser-integration-tests/suites/replay/requests/test.ts b/dev-packages/browser-integration-tests/suites/replay/requests/test.ts index a15665b357dc..eedfb5ae5fb2 100644 --- a/dev-packages/browser-integration-tests/suites/replay/requests/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/requests/test.ts @@ -10,14 +10,6 @@ sentryTest('replay recording should contain fetch request span', async ({ getLoc 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' }), - }); - }); - await page.route('https://example.com', route => { return route.fulfill({ status: 200, @@ -48,14 +40,6 @@ sentryTest('replay recording should contain XHR request span', async ({ getLocal 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' }), - }); - }); - await page.route('https://example.com', route => { return route.fulfill({ status: 200, diff --git a/dev-packages/browser-integration-tests/suites/replay/sampling/test.ts b/dev-packages/browser-integration-tests/suites/replay/sampling/test.ts index 92aa50ff3744..cc68be486749 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sampling/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/sampling/test.ts @@ -19,7 +19,7 @@ sentryTest('should not send replays if both sample rates are 0', async ({ getLoc }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); await page.locator('button').click(); diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts b/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts index 05842705227e..49a95345fc43 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/sessionExpiry/test.ts @@ -22,14 +22,6 @@ sentryTest('handles an expired session', async ({ browserName, forceFlushReplay, const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionInactive/test.ts b/dev-packages/browser-integration-tests/suites/replay/sessionInactive/test.ts index 614f9a3d99d7..0ac765c3a969 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionInactive/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/sessionInactive/test.ts @@ -23,14 +23,6 @@ sentryTest('handles an inactive session', async ({ getLocalTestPath, page, brows const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts b/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts index 0a253597f367..d658f407009c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/sessionMaxAge/test.ts @@ -26,14 +26,6 @@ sentryTest('handles session that exceeds max age', async ({ forceFlushReplay, ge const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/clickTargets/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/clickTargets/test.ts index b5b8a5b24988..73910e1190e0 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/clickTargets/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/clickTargets/test.ts @@ -35,14 +35,6 @@ import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); @@ -91,14 +83,6 @@ import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/test.ts index c57b5d05b3f2..f5337f1feb63 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/disable/test.ts @@ -8,14 +8,6 @@ sentryTest('does not capture slow click when slowClickTimeout === 0', async ({ g 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/error/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/error/test.ts index 570a633443d4..5d106fb9c0e9 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/error/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/error/test.ts @@ -13,14 +13,6 @@ sentryTest('slow click that triggers error is captured', async ({ getLocalTestUr 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 url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); @@ -70,14 +62,6 @@ sentryTest( 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 url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/ignore/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/ignore/test.ts index 9d6c49abc29d..1928bfcf2c2e 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/ignore/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/ignore/test.ts @@ -8,14 +8,6 @@ sentryTest('click is ignored on ignoreSelectors', async ({ getLocalTestUrl, page 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); @@ -58,14 +50,6 @@ sentryTest('click is ignored on div', async ({ getLocalTestUrl, page }) => { 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts index 16f3036a3cca..04ab9c059f34 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/multiClick/test.ts @@ -13,14 +13,6 @@ sentryTest('captures multi click when not detecting slow click', async ({ getLoc 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); @@ -105,14 +97,6 @@ sentryTest('captures multiple multi clicks', async ({ getLocalTestUrl, page, for 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts index aafdced81505..1c5aab9f4183 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/mutation/test.ts @@ -8,14 +8,6 @@ sentryTest('mutation after threshold results in slow click', async ({ forceFlush 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 url = await getLocalTestUrl({ testDir: __dirname }); const replayRequestPromise = waitForReplayRequest(page, 0); @@ -70,14 +62,6 @@ sentryTest('multiple clicks are counted', async ({ getLocalTestUrl, page }) => { 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 url = await getLocalTestUrl({ testDir: __dirname }); const replayRequestPromise = waitForReplayRequest(page, 0); @@ -131,14 +115,6 @@ sentryTest('immediate mutation does not trigger slow click', async ({ forceFlush 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 url = await getLocalTestUrl({ testDir: __dirname }); const replayRequestPromise = waitForReplayRequest(page, 0); @@ -198,14 +174,6 @@ sentryTest('inline click handler does not trigger slow click', async ({ forceFlu 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 url = await getLocalTestUrl({ testDir: __dirname }); const replayRequestPromise = waitForReplayRequest(page, 0); @@ -250,14 +218,6 @@ sentryTest('mouseDown events are considered', async ({ getLocalTestUrl, page }) 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 url = await getLocalTestUrl({ testDir: __dirname }); const replayRequestPromise = waitForReplayRequest(page, 0); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/scroll/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/scroll/test.ts index a7101351061f..cb0e467de4f6 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/scroll/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/scroll/test.ts @@ -8,14 +8,6 @@ sentryTest('immediate scroll does not trigger slow click', async ({ getLocalTest 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); @@ -57,14 +49,6 @@ sentryTest('late scroll triggers slow click', async ({ getLocalTestUrl, page }) 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/timeout/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/timeout/test.ts index 8adf24302089..1bc8219d22ab 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/timeout/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/timeout/test.ts @@ -8,14 +8,6 @@ sentryTest('mutation after timeout results in slow click', async ({ getLocalTest 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); @@ -63,14 +55,6 @@ sentryTest('console.log results in slow click', async ({ getLocalTestUrl, page } 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 url = await getLocalTestUrl({ testDir: __dirname }); await Promise.all([waitForReplayRequest(page, 0), page.goto(url)]); diff --git a/dev-packages/browser-integration-tests/suites/replay/slowClick/windowOpen/test.ts b/dev-packages/browser-integration-tests/suites/replay/slowClick/windowOpen/test.ts index d68f27a5f45b..3a5187b0fc85 100644 --- a/dev-packages/browser-integration-tests/suites/replay/slowClick/windowOpen/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/slowClick/windowOpen/test.ts @@ -8,14 +8,6 @@ sentryTest('window.open() is considered for slow click', async ({ getLocalTestUr 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' }), - }); - }); - await page.route('http://example.com/', route => { return route.fulfill({ status: 200, diff --git a/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts b/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts index 0eb0495eb1ea..62bfda2b0a8c 100644 --- a/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/throttleBreadcrumbs/test.ts @@ -15,14 +15,6 @@ sentryTest( const reqPromise0 = waitForReplayRequest(page, 0); const reqPromise1 = waitForReplayRequest(page, 1); - 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); diff --git a/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/test.ts b/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/test.ts index 741a702fbef9..fd8250fe86ad 100644 --- a/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/unicode/compressed/test.ts @@ -15,14 +15,6 @@ sentryTest('replay should handle unicode characters', async ({ getLocalTestPath, const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/test.ts b/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/test.ts index 1ee9d73e5f57..782d44702c34 100644 --- a/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/unicode/uncompressed/test.ts @@ -15,14 +15,6 @@ sentryTest('replay should handle unicode characters', async ({ getLocalTestPath, const reqPromise0 = waitForReplayRequest(page, 0); - 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 getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts index 9d7b07f1cfd1..e140cf14ebea 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/initial-scope/test.ts @@ -21,14 +21,6 @@ sentryTest('should start a new session with navigation.', async ({ getLocalTestU await page.route('**/foo', (route: Route) => route.continue({ url })); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const initSession = await getFirstSentryEnvelopeRequest(page, url); await page.click('#navigate'); diff --git a/dev-packages/browser-integration-tests/suites/sessions/start-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/start-session/test.ts index 65f9eef8e9ae..6dffb1f85902 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/start-session/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/start-session/test.ts @@ -19,14 +19,6 @@ sentryTest('should start a new session with navigation.', async ({ getLocalTestU const url = await getLocalTestUrl({ testDir: __dirname }); await page.route('**/foo', (route: Route) => route.continue({ url })); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const initSession = await getFirstSentryEnvelopeRequest(page, url); await page.locator('#navigate').click(); diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts index 65f9eef8e9ae..6dffb1f85902 100644 --- a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts @@ -19,14 +19,6 @@ sentryTest('should start a new session with navigation.', async ({ getLocalTestU const url = await getLocalTestUrl({ testDir: __dirname }); await page.route('**/foo', (route: Route) => route.continue({ url })); - await page.route('https://dsn.ingest.sentry.io/**/*', route => { - return route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ id: 'test-id' }), - }); - }); - const initSession = await getFirstSentryEnvelopeRequest(page, url); await page.locator('#navigate').click(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts index c52bf2a6b68c..b55a5fef4d98 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegrationShim/test.ts @@ -24,7 +24,7 @@ sentryTest( }); }); - const url = await getLocalTestPath({ testDir: __dirname }); + const url = await getLocalTestPath({ testDir: __dirname, skipDsnRouteHandler: true }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts index 1b6bc5bc686d..1fee219c8e5b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts @@ -16,14 +16,6 @@ sentryTest('should capture an INP click event span after pageload', async ({ bro 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 url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts index a9d5191b5cf3..18be7b654140 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts @@ -18,14 +18,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts index 87ba1fd8632c..bf5021250b42 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts @@ -17,14 +17,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts index 594bd9904052..e42028f9dff1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts @@ -18,14 +18,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); @@ -109,14 +101,6 @@ sentryTest( 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 url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts index 9d158ea5491e..b62923be0e9b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts @@ -74,14 +74,6 @@ sentryTest('error after navigation has navigation traceId', async ({ getLocalTes 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 url = await getLocalTestUrl({ testDir: __dirname }); // ensure pageload transaction is finished diff --git a/dev-packages/browser-integration-tests/utils/fixtures.ts b/dev-packages/browser-integration-tests/utils/fixtures.ts index e154ddc25988..9455e8c8626c 100644 --- a/dev-packages/browser-integration-tests/utils/fixtures.ts +++ b/dev-packages/browser-integration-tests/utils/fixtures.ts @@ -31,8 +31,12 @@ const getAsset = (assetDir: string, asset: string): string => { export type TestFixtures = { _autoSnapshotSuffix: void; testDir: string; - getLocalTestPath: (options: { testDir: string }) => Promise; - getLocalTestUrl: (options: { testDir: string; skipRouteHandler?: boolean }) => Promise; + getLocalTestPath: (options: { testDir: string; skipDsnRouteHandler?: boolean }) => Promise; + getLocalTestUrl: (options: { + testDir: string; + skipRouteHandler?: boolean; + skipDsnRouteHandler?: boolean; + }) => Promise; forceFlushReplay: () => Promise; enableConsole: () => void; runInChromium: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown; @@ -55,7 +59,7 @@ const sentryTest = base.extend({ ], getLocalTestUrl: ({ page }, use) => { - return use(async ({ testDir, skipRouteHandler = false }) => { + return use(async ({ testDir, skipRouteHandler = false, skipDsnRouteHandler = false }) => { const pagePath = `${TEST_HOST}/index.html`; const tmpDir = path.join(testDir, 'dist', crypto.randomUUID()); @@ -68,6 +72,16 @@ const sentryTest = base.extend({ return tmpDir; } + if (!skipDsnRouteHandler) { + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + } + await page.route(`${TEST_HOST}/*.*`, route => { const file = route.request().url().split('/').pop(); const filePath = path.resolve(tmpDir, `./${file}`); @@ -96,13 +110,23 @@ const sentryTest = base.extend({ }); }, - getLocalTestPath: ({}, use) => { - return use(async ({ testDir }) => { + getLocalTestPath: ({ page }, use) => { + return use(async ({ testDir, skipDsnRouteHandler }) => { const tmpDir = path.join(testDir, 'dist', crypto.randomUUID()); const pagePath = `file:///${path.resolve(tmpDir, './index.html')}`; await build(testDir, tmpDir); + if (!skipDsnRouteHandler) { + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + } + return pagePath; }); }, From a7193fb498261de0f4b9edf415ec4301dc3035c1 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 29 Oct 2024 09:39:32 +0100 Subject: [PATCH 36/44] ci: Use `list` reporter for playwright on CI instead of `line` (#14113) This does not work properly anyhow (e.g. it shows all the tests because of how CI logs are rendered, see https://github.com/getsentry/sentry-javascript/actions/runs/11557223813/job/32202778369), so we may as well also have the added detail of per-test runtime etc. in there. --- dev-packages/browser-integration-tests/playwright.config.ts | 2 +- .../test-applications/ember-classic/playwright.config.ts | 2 +- .../test-applications/ember-embroider/playwright.config.ts | 2 +- .../node-express-send-to-sentry/playwright.config.mjs | 2 +- .../react-send-to-sentry/playwright.config.mjs | 2 +- packages/remix/playwright.config.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-packages/browser-integration-tests/playwright.config.ts b/dev-packages/browser-integration-tests/playwright.config.ts index 498e7529f37a..821c0291ccfb 100644 --- a/dev-packages/browser-integration-tests/playwright.config.ts +++ b/dev-packages/browser-integration-tests/playwright.config.ts @@ -30,7 +30,7 @@ const config: PlaywrightTestConfig = { }, ], - reporter: process.env.CI ? [['line'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', + reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', globalSetup: require.resolve('./playwright.setup.ts'), globalTeardown: require.resolve('./playwright.teardown.ts'), diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/playwright.config.ts b/dev-packages/e2e-tests/test-applications/ember-classic/playwright.config.ts index a092503a9fc2..8d378b127c72 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/ember-classic/playwright.config.ts @@ -35,7 +35,7 @@ const config: PlaywrightTestConfig = { forbidOnly: !!process.env.CI, retries: 0, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? [['line'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', + reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/playwright.config.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/playwright.config.ts index a092503a9fc2..8d378b127c72 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/playwright.config.ts @@ -35,7 +35,7 @@ const config: PlaywrightTestConfig = { forbidOnly: !!process.env.CI, retries: 0, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? [['line'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', + reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ diff --git a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/playwright.config.mjs index f29509db795c..39bf757e0fd8 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-send-to-sentry/playwright.config.mjs @@ -23,7 +23,7 @@ const config = { /* Retry on CI only */ retries: 0, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? [['line'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', + reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/playwright.config.mjs index aa8fc9bfd4b7..566614052236 100644 --- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/playwright.config.mjs @@ -23,7 +23,7 @@ const config = { /* Opt out of parallel tests on CI. */ workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: process.env.CI ? [['line'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', + reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ diff --git a/packages/remix/playwright.config.ts b/packages/remix/playwright.config.ts index a1570f27f50d..142272a44740 100644 --- a/packages/remix/playwright.config.ts +++ b/packages/remix/playwright.config.ts @@ -8,7 +8,7 @@ const config: PlaywrightTestConfig = { }, // Run tests inside of a single file in parallel fullyParallel: true, - reporter: process.env.CI ? [['line'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', + reporter: process.env.CI ? [['list'], ['junit', { outputFile: 'results.junit.xml' }]] : 'list', // Use 3 workers on CI, else use defaults (based on available CPU cores) // Note that 3 is a random number selected to work well with our CI setup workers: process.env.CI ? 3 : undefined, From d396241ca0c57bb2578e30317dd2c5ebc842c02b Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 29 Oct 2024 10:11:34 +0100 Subject: [PATCH 37/44] feat(node): Add breadcrumbs for `child_process` and `worker_thread` (#13896) --- .../scripts/consistentExports.ts | 2 + .../node-integration-tests/suites/anr/test.ts | 4 +- .../suites/breadcrumbs/process-thread/app.mjs | 27 + .../suites/breadcrumbs/process-thread/test.ts | 48 ++ .../breadcrumbs/process-thread/worker.mjs | 1 + packages/astro/src/index.server.ts | 1 + packages/aws-serverless/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 1 + .../src/integrations/diagnostic_channel.d.ts | 556 ++++++++++++++++++ .../node/src/integrations/processThread.ts | 105 ++++ packages/node/src/sdk/index.ts | 2 + packages/node/tsconfig.test.json | 2 +- 13 files changed, 748 insertions(+), 3 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs create mode 100644 dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/test.ts create mode 100644 dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/worker.mjs create mode 100644 packages/node/src/integrations/diagnostic_channel.d.ts create mode 100644 packages/node/src/integrations/processThread.ts diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index a35bf4657c64..546639e8a766 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -50,6 +50,8 @@ const DEPENDENTS: Dependent[] = [ ignoreExports: [ // not supported in bun: 'NodeClient', + // Bun doesn't emit the required diagnostics_channel events + 'processThreadBreadcrumbIntegration', ], }, { diff --git a/dev-packages/node-integration-tests/suites/anr/test.ts b/dev-packages/node-integration-tests/suites/anr/test.ts index c3cb935532b1..78f89d7451c0 100644 --- a/dev-packages/node-integration-tests/suites/anr/test.ts +++ b/dev-packages/node-integration-tests/suites/anr/test.ts @@ -56,12 +56,12 @@ const ANR_EVENT_WITH_SCOPE = { user: { email: 'person@home.com', }, - breadcrumbs: [ + breadcrumbs: expect.arrayContaining([ { timestamp: expect.any(Number), message: 'important message!', }, - ], + ]), }; conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => { diff --git a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs new file mode 100644 index 000000000000..903470806ad9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/app.mjs @@ -0,0 +1,27 @@ +import { spawn } from 'child_process'; +import { join } from 'path'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; +import { Worker } from 'worker_threads'; + +const __dirname = new URL('.', import.meta.url).pathname; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +await new Promise(resolve => { + const child = spawn('sleep', ['a']); + child.on('error', resolve); + child.on('exit', resolve); +}); + +await new Promise(resolve => { + const worker = new Worker(join(__dirname, 'worker.mjs')); + worker.on('error', resolve); + worker.on('exit', resolve); +}); + +throw new Error('This is a test error'); diff --git a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/test.ts b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/test.ts new file mode 100644 index 000000000000..f675ca4250dd --- /dev/null +++ b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/test.ts @@ -0,0 +1,48 @@ +import type { Event } from '@sentry/types'; +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +const EVENT = { + // and an exception that is our ANR + exception: { + values: [ + { + type: 'Error', + value: 'This is a test error', + }, + ], + }, + breadcrumbs: [ + { + timestamp: expect.any(Number), + category: 'child_process', + message: "Child process exited with code '1'", + level: 'warning', + data: { + spawnfile: 'sleep', + }, + }, + { + timestamp: expect.any(Number), + category: 'worker_thread', + message: "Worker thread errored with 'Worker error'", + level: 'error', + data: { + threadId: expect.any(Number), + }, + }, + ], +}; + +conditionalTest({ min: 20 })('should capture process and thread breadcrumbs', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('ESM', done => { + createRunner(__dirname, 'app.mjs') + .withMockSentryServer() + .expect({ event: EVENT as Event }) + .start(done); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/worker.mjs b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/worker.mjs new file mode 100644 index 000000000000..049063bd26b4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/breadcrumbs/process-thread/worker.mjs @@ -0,0 +1 @@ +throw new Error('Worker error'); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index dacb42643b99..b544b71087fc 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -89,6 +89,7 @@ export { parameterize, postgresIntegration, prismaIntegration, + processThreadBreadcrumbIntegration, redisIntegration, requestDataIntegration, rewriteFramesIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index ee70e8956c6f..cc7f783c40fd 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -102,6 +102,7 @@ export { setupNestErrorHandler, postgresIntegration, prismaIntegration, + processThreadBreadcrumbIntegration, hapiIntegration, setupHapiErrorHandler, spotlightIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index bda96f062966..80dba64cef97 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -114,6 +114,7 @@ export { zodErrorsIntegration, profiler, amqplibIntegration, + processThreadBreadcrumbIntegration, } from '@sentry/node'; export { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index f77ef88548f9..2e658f7abc36 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -31,6 +31,7 @@ export { spotlightIntegration } from './integrations/spotlight'; export { genericPoolIntegration } from './integrations/tracing/genericPool'; export { dataloaderIntegration } from './integrations/tracing/dataloader'; export { amqplibIntegration } from './integrations/tracing/amqplib'; +export { processThreadBreadcrumbIntegration } from './integrations/processThread'; export { SentryContextManager } from './otel/contextManager'; export { generateInstrumentOnce } from './otel/instrument'; diff --git a/packages/node/src/integrations/diagnostic_channel.d.ts b/packages/node/src/integrations/diagnostic_channel.d.ts new file mode 100644 index 000000000000..abf3649a617f --- /dev/null +++ b/packages/node/src/integrations/diagnostic_channel.d.ts @@ -0,0 +1,556 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ + +/** + * The `node:diagnostics_channel` module provides an API to create named channels + * to report arbitrary message data for diagnostics purposes. + * + * It can be accessed using: + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * ``` + * + * It is intended that a module writer wanting to report diagnostics messages + * will create one or many top-level channels to report messages through. + * Channels may also be acquired at runtime but it is not encouraged + * due to the additional overhead of doing so. Channels may be exported for + * convenience, but as long as the name is known it can be acquired anywhere. + * + * If you intend for your module to produce diagnostics data for others to + * consume it is recommended that you include documentation of what named + * channels are used along with the shape of the message data. Channel names + * should generally include the module name to avoid collisions with data from + * other modules. + * @since v15.1.0, v14.17.0 + * @see [source](https://github.com/nodejs/node/blob/v22.x/lib/diagnostics_channel.js) + */ +declare module 'diagnostics_channel' { + import type { AsyncLocalStorage } from 'node:async_hooks'; + /** + * Check if there are active subscribers to the named channel. This is helpful if + * the message you want to send might be expensive to prepare. + * + * This API is optional but helpful when trying to publish messages from very + * performance-sensitive code. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * if (diagnostics_channel.hasSubscribers('my-channel')) { + * // There are subscribers, prepare and publish message + * } + * ``` + * @since v15.1.0, v14.17.0 + * @param name The channel name + * @return If there are active subscribers + */ + function hasSubscribers(name: string | symbol): boolean; + /** + * This is the primary entry-point for anyone wanting to publish to a named + * channel. It produces a channel object which is optimized to reduce overhead at + * publish time as much as possible. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * ``` + * @since v15.1.0, v14.17.0 + * @param name The channel name + * @return The named channel object + */ + function channel(name: string | symbol): Channel; + type ChannelListener = (message: unknown, name: string | symbol) => void; + /** + * Register a message handler to subscribe to this channel. This message handler + * will be run synchronously whenever a message is published to the channel. Any + * errors thrown in the message handler will trigger an `'uncaughtException'`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * diagnostics_channel.subscribe('my-channel', (message, name) => { + * // Received data + * }); + * ``` + * @since v18.7.0, v16.17.0 + * @param name The channel name + * @param onMessage The handler to receive channel messages + */ + function subscribe(name: string | symbol, onMessage: ChannelListener): void; + /** + * Remove a message handler previously registered to this channel with {@link subscribe}. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * function onMessage(message, name) { + * // Received data + * } + * + * diagnostics_channel.subscribe('my-channel', onMessage); + * + * diagnostics_channel.unsubscribe('my-channel', onMessage); + * ``` + * @since v18.7.0, v16.17.0 + * @param name The channel name + * @param onMessage The previous subscribed handler to remove + * @return `true` if the handler was found, `false` otherwise. + */ + function unsubscribe(name: string | symbol, onMessage: ChannelListener): boolean; + /** + * Creates a `TracingChannel` wrapper for the given `TracingChannel Channels`. If a name is given, the corresponding tracing + * channels will be created in the form of `tracing:${name}:${eventType}` where `eventType` corresponds to the types of `TracingChannel Channels`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channelsByName = diagnostics_channel.tracingChannel('my-channel'); + * + * // or... + * + * const channelsByCollection = diagnostics_channel.tracingChannel({ + * start: diagnostics_channel.channel('tracing:my-channel:start'), + * end: diagnostics_channel.channel('tracing:my-channel:end'), + * asyncStart: diagnostics_channel.channel('tracing:my-channel:asyncStart'), + * asyncEnd: diagnostics_channel.channel('tracing:my-channel:asyncEnd'), + * error: diagnostics_channel.channel('tracing:my-channel:error'), + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param nameOrChannels Channel name or object containing all the `TracingChannel Channels` + * @return Collection of channels to trace with + */ + function tracingChannel< + StoreType = unknown, + ContextType extends object = StoreType extends object ? StoreType : object, + >(nameOrChannels: string | TracingChannelCollection): TracingChannel; + /** + * The class `Channel` represents an individual named channel within the data + * pipeline. It is used to track subscribers and to publish messages when there + * are subscribers present. It exists as a separate object to avoid channel + * lookups at publish time, enabling very fast publish speeds and allowing + * for heavy use while incurring very minimal cost. Channels are created with {@link channel}, constructing a channel directly + * with `new Channel(name)` is not supported. + * @since v15.1.0, v14.17.0 + */ + class Channel { + readonly name: string | symbol; + /** + * Check if there are active subscribers to this channel. This is helpful if + * the message you want to send might be expensive to prepare. + * + * This API is optional but helpful when trying to publish messages from very + * performance-sensitive code. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * if (channel.hasSubscribers) { + * // There are subscribers, prepare and publish message + * } + * ``` + * @since v15.1.0, v14.17.0 + */ + readonly hasSubscribers: boolean; + private constructor(name: string | symbol); + /** + * Publish a message to any subscribers to the channel. This will trigger + * message handlers synchronously so they will execute within the same context. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.publish({ + * some: 'message', + * }); + * ``` + * @since v15.1.0, v14.17.0 + * @param message The message to send to the channel subscribers + */ + publish(message: unknown): void; + /** + * Register a message handler to subscribe to this channel. This message handler + * will be run synchronously whenever a message is published to the channel. Any + * errors thrown in the message handler will trigger an `'uncaughtException'`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.subscribe((message, name) => { + * // Received data + * }); + * ``` + * @since v15.1.0, v14.17.0 + * @deprecated Since v18.7.0,v16.17.0 - Use {@link subscribe(name, onMessage)} + * @param onMessage The handler to receive channel messages + */ + subscribe(onMessage: ChannelListener): void; + /** + * Remove a message handler previously registered to this channel with `channel.subscribe(onMessage)`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * function onMessage(message, name) { + * // Received data + * } + * + * channel.subscribe(onMessage); + * + * channel.unsubscribe(onMessage); + * ``` + * @since v15.1.0, v14.17.0 + * @deprecated Since v18.7.0,v16.17.0 - Use {@link unsubscribe(name, onMessage)} + * @param onMessage The previous subscribed handler to remove + * @return `true` if the handler was found, `false` otherwise. + */ + unsubscribe(onMessage: ChannelListener): void; + /** + * When `channel.runStores(context, ...)` is called, the given context data + * will be applied to any store bound to the channel. If the store has already been + * bound the previous `transform` function will be replaced with the new one. + * The `transform` function may be omitted to set the given context data as the + * context directly. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const store = new AsyncLocalStorage(); + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.bindStore(store, (data) => { + * return { data }; + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param store The store to which to bind the context data + * @param transform Transform context data before setting the store context + */ + bindStore(store: AsyncLocalStorage, transform?: (context: ContextType) => StoreType): void; + /** + * Remove a message handler previously registered to this channel with `channel.bindStore(store)`. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const store = new AsyncLocalStorage(); + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.bindStore(store); + * channel.unbindStore(store); + * ``` + * @since v19.9.0 + * @experimental + * @param store The store to unbind from the channel. + * @return `true` if the store was found, `false` otherwise. + */ + unbindStore(store: any): void; + /** + * Applies the given data to any AsyncLocalStorage instances bound to the channel + * for the duration of the given function, then publishes to the channel within + * the scope of that data is applied to the stores. + * + * If a transform function was given to `channel.bindStore(store)` it will be + * applied to transform the message data before it becomes the context value for + * the store. The prior storage context is accessible from within the transform + * function in cases where context linking is required. + * + * The context applied to the store should be accessible in any async code which + * continues from execution which began during the given function, however + * there are some situations in which `context loss` may occur. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const store = new AsyncLocalStorage(); + * + * const channel = diagnostics_channel.channel('my-channel'); + * + * channel.bindStore(store, (message) => { + * const parent = store.getStore(); + * return new Span(message, parent); + * }); + * channel.runStores({ some: 'message' }, () => { + * store.getStore(); // Span({ some: 'message' }) + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param context Message to send to subscribers and bind to stores + * @param fn Handler to run within the entered storage context + * @param thisArg The receiver to be used for the function call. + * @param args Optional arguments to pass to the function. + */ + runStores(): void; + } + interface TracingChannelSubscribers { + start: (message: ContextType) => void; + end: ( + message: ContextType & { + error?: unknown; + result?: unknown; + }, + ) => void; + asyncStart: ( + message: ContextType & { + error?: unknown; + result?: unknown; + }, + ) => void; + asyncEnd: ( + message: ContextType & { + error?: unknown; + result?: unknown; + }, + ) => void; + error: ( + message: ContextType & { + error: unknown; + }, + ) => void; + } + interface TracingChannelCollection { + start: Channel; + end: Channel; + asyncStart: Channel; + asyncEnd: Channel; + error: Channel; + } + /** + * The class `TracingChannel` is a collection of `TracingChannel Channels` which + * together express a single traceable action. It is used to formalize and + * simplify the process of producing events for tracing application flow. {@link tracingChannel} is used to construct a `TracingChannel`. As with `Channel` it is recommended to create and reuse a + * single `TracingChannel` at the top-level of the file rather than creating them + * dynamically. + * @since v19.9.0 + * @experimental + */ + class TracingChannel implements TracingChannelCollection { + start: Channel; + end: Channel; + asyncStart: Channel; + asyncEnd: Channel; + error: Channel; + /** + * Helper to subscribe a collection of functions to the corresponding channels. + * This is the same as calling `channel.subscribe(onMessage)` on each channel + * individually. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.subscribe({ + * start(message) { + * // Handle start message + * }, + * end(message) { + * // Handle end message + * }, + * asyncStart(message) { + * // Handle asyncStart message + * }, + * asyncEnd(message) { + * // Handle asyncEnd message + * }, + * error(message) { + * // Handle error message + * }, + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param subscribers Set of `TracingChannel Channels` subscribers + */ + subscribe(subscribers: TracingChannelSubscribers): void; + /** + * Helper to unsubscribe a collection of functions from the corresponding channels. + * This is the same as calling `channel.unsubscribe(onMessage)` on each channel + * individually. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.unsubscribe({ + * start(message) { + * // Handle start message + * }, + * end(message) { + * // Handle end message + * }, + * asyncStart(message) { + * // Handle asyncStart message + * }, + * asyncEnd(message) { + * // Handle asyncEnd message + * }, + * error(message) { + * // Handle error message + * }, + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param subscribers Set of `TracingChannel Channels` subscribers + * @return `true` if all handlers were successfully unsubscribed, and `false` otherwise. + */ + unsubscribe(subscribers: TracingChannelSubscribers): void; + /** + * Trace a synchronous function call. This will always produce a `start event` and `end event` around the execution and may produce an `error event` if the given function throws an error. + * This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all + * events should have any bound stores set to match this trace context. + * + * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions + * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.traceSync(() => { + * // Do something + * }, { + * some: 'thing', + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param fn Function to wrap a trace around + * @param context Shared object to correlate events through + * @param thisArg The receiver to be used for the function call + * @param args Optional arguments to pass to the function + * @return The return value of the given function + */ + traceSync( + fn: (this: ThisArg, ...args: Args) => any, + context?: ContextType, + thisArg?: ThisArg, + ...args: Args + ): void; + /** + * Trace a promise-returning function call. This will always produce a `start event` and `end event` around the synchronous portion of the + * function execution, and will produce an `asyncStart event` and `asyncEnd event` when a promise continuation is reached. It may also + * produce an `error event` if the given function throws an error or the + * returned promise rejects. This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all + * events should have any bound stores set to match this trace context. + * + * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions + * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.tracePromise(async () => { + * // Do something + * }, { + * some: 'thing', + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param fn Promise-returning function to wrap a trace around + * @param context Shared object to correlate trace events through + * @param thisArg The receiver to be used for the function call + * @param args Optional arguments to pass to the function + * @return Chained from promise returned by the given function + */ + tracePromise( + fn: (this: ThisArg, ...args: Args) => Promise, + context?: ContextType, + thisArg?: ThisArg, + ...args: Args + ): void; + /** + * Trace a callback-receiving function call. This will always produce a `start event` and `end event` around the synchronous portion of the + * function execution, and will produce a `asyncStart event` and `asyncEnd event` around the callback execution. It may also produce an `error event` if the given function throws an error or + * the returned + * promise rejects. This will run the given function using `channel.runStores(context, ...)` on the `start` channel which ensures all + * events should have any bound stores set to match this trace context. + * + * The `position` will be -1 by default to indicate the final argument should + * be used as the callback. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * + * channels.traceCallback((arg1, callback) => { + * // Do something + * callback(null, 'result'); + * }, 1, { + * some: 'thing', + * }, thisArg, arg1, callback); + * ``` + * + * The callback will also be run with `channel.runStores(context, ...)` which + * enables context loss recovery in some cases. + * + * To ensure only correct trace graphs are formed, events will only be published if subscribers are present prior to starting the trace. Subscriptions + * which are added after the trace begins will not receive future events from that trace, only future traces will be seen. + * + * ```js + * import diagnostics_channel from 'node:diagnostics_channel'; + * import { AsyncLocalStorage } from 'node:async_hooks'; + * + * const channels = diagnostics_channel.tracingChannel('my-channel'); + * const myStore = new AsyncLocalStorage(); + * + * // The start channel sets the initial store data to something + * // and stores that store data value on the trace context object + * channels.start.bindStore(myStore, (data) => { + * const span = new Span(data); + * data.span = span; + * return span; + * }); + * + * // Then asyncStart can restore from that data it stored previously + * channels.asyncStart.bindStore(myStore, (data) => { + * return data.span; + * }); + * ``` + * @since v19.9.0 + * @experimental + * @param fn callback using function to wrap a trace around + * @param position Zero-indexed argument position of expected callback + * @param context Shared object to correlate trace events through + * @param thisArg The receiver to be used for the function call + * @param args Optional arguments to pass to the function + * @return The return value of the given function + */ + traceCallback any>( + fn: Fn, + position?: number, + context?: ContextType, + thisArg?: any, + ...args: Parameters + ): void; + } +} +declare module 'node:diagnostics_channel' { + export * from 'diagnostics_channel'; +} diff --git a/packages/node/src/integrations/processThread.ts b/packages/node/src/integrations/processThread.ts new file mode 100644 index 000000000000..870a0dc6df64 --- /dev/null +++ b/packages/node/src/integrations/processThread.ts @@ -0,0 +1,105 @@ +import type { ChildProcess } from 'node:child_process'; +import * as diagnosticsChannel from 'node:diagnostics_channel'; +import type { Worker } from 'node:worker_threads'; +import { addBreadcrumb, defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; + +interface Options { + /** + * Whether to include child process arguments in breadcrumbs data. + * + * @default false + */ + includeChildProcessArgs?: boolean; +} + +const INTEGRATION_NAME = 'ProcessAndThreadBreadcrumbs'; + +const _processThreadBreadcrumbIntegration = ((options: Options = {}) => { + return { + name: INTEGRATION_NAME, + setup(_client) { + // eslint-disable-next-line deprecation/deprecation + diagnosticsChannel.channel('child_process').subscribe((event: unknown) => { + if (event && typeof event === 'object' && 'process' in event) { + captureChildProcessEvents(event.process as ChildProcess, options); + } + }); + + // eslint-disable-next-line deprecation/deprecation + diagnosticsChannel.channel('worker_threads').subscribe((event: unknown) => { + if (event && typeof event === 'object' && 'worker' in event) { + captureWorkerThreadEvents(event.worker as Worker); + } + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Capture breadcrumbs for child processes and worker threads. + */ +export const processThreadBreadcrumbIntegration = defineIntegration(_processThreadBreadcrumbIntegration); + +function captureChildProcessEvents(child: ChildProcess, options: Options): void { + let hasExited = false; + let data: Record | undefined; + + child + .on('spawn', () => { + // This is Sentry getting macOS OS context + if (child.spawnfile === '/usr/bin/sw_vers') { + hasExited = true; + return; + } + + data = { spawnfile: child.spawnfile }; + if (options.includeChildProcessArgs) { + data.spawnargs = child.spawnargs; + } + }) + .on('exit', code => { + if (!hasExited) { + hasExited = true; + + // Only log for non-zero exit codes + if (code !== null && code !== 0) { + addBreadcrumb({ + category: 'child_process', + message: `Child process exited with code '${code}'`, + level: 'warning', + data, + }); + } + } + }) + .on('error', error => { + if (!hasExited) { + hasExited = true; + + addBreadcrumb({ + category: 'child_process', + message: `Child process errored with '${error.message}'`, + level: 'error', + data, + }); + } + }); +} + +function captureWorkerThreadEvents(worker: Worker): void { + let threadId: number | undefined; + + worker + .on('online', () => { + threadId = worker.threadId; + }) + .on('error', error => { + addBreadcrumb({ + category: 'worker_thread', + message: `Worker thread errored with '${error.message}'`, + level: 'error', + data: { threadId }, + }); + }); +} diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 7276e809875a..87d61cc908bc 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -36,6 +36,7 @@ import { modulesIntegration } from '../integrations/modules'; import { nativeNodeFetchIntegration } from '../integrations/node-fetch'; import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception'; import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection'; +import { processThreadBreadcrumbIntegration } from '../integrations/processThread'; import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight'; import { getAutoPerformanceIntegrations } from '../integrations/tracing'; import { makeNodeTransport } from '../transports'; @@ -71,6 +72,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { contextLinesIntegration(), localVariablesIntegration(), nodeContextIntegration(), + processThreadBreadcrumbIntegration(), ...getCjsOnlyIntegrations(), ]; } diff --git a/packages/node/tsconfig.test.json b/packages/node/tsconfig.test.json index 87f6afa06b86..b0c6b000999b 100644 --- a/packages/node/tsconfig.test.json +++ b/packages/node/tsconfig.test.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", - "include": ["test/**/*"], + "include": ["test/**/*", "./src/integrations/diagnostic_channel.d.ts"], "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used From 6fefd44bc7bbe026d93dd3121a652cbc7e4ef91c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 29 Oct 2024 10:21:22 +0100 Subject: [PATCH 38/44] chore(dev-deps): Bump some (transitive) dev dependencies to latest (#14115) This is done to fix some security vulnerability warnings, which do not actually affect us/our users, but we may as well update the deps accordingly: * axios * http-proxy-middleware * body-parser * path-to-regexp * dset * requirejs * ws * braces * socks (to get rid of ip package) * lodash * socket.io * @nestjs/platform-express * express --- .../browser-integration-tests/package.json | 2 +- .../node-integration-tests/package.json | 6 +- yarn.lock | 460 +++++++++--------- 3 files changed, 231 insertions(+), 237 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index b791ba4fcca0..09d6800b4310 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -44,7 +44,7 @@ "@playwright/test": "^1.44.1", "@sentry-internal/rrweb": "2.11.0", "@sentry/browser": "8.35.0", - "axios": "1.6.7", + "axios": "1.7.7", "babel-loader": "^8.2.2", "html-webpack-plugin": "^5.5.0", "pako": "^2.1.0", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index b21948783ff4..353f2b231e97 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -29,7 +29,7 @@ "@hapi/hapi": "^21.3.10", "@nestjs/common": "^10.3.7", "@nestjs/core": "^10.3.3", - "@nestjs/platform-express": "^10.3.3", + "@nestjs/platform-express": "^10.4.6", "@prisma/client": "5.9.1", "@sentry/aws-serverless": "8.35.0", "@sentry/node": "8.35.0", @@ -40,12 +40,12 @@ "@types/pg": "^8.6.5", "amqplib": "^0.10.4", "apollo-server": "^3.11.1", - "axios": "^1.6.7", + "axios": "^1.7.7", "connect": "^3.7.0", "cors": "^2.8.5", "cron": "^3.1.6", "dataloader": "2.2.2", - "express": "^4.17.3", + "express": "^4.21.1", "generic-pool": "^3.9.0", "graphql": "^16.3.0", "http-terminator": "^3.2.0", diff --git a/yarn.lock b/yarn.lock index d49a1943fd7e..e077f2dd33f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6426,16 +6426,16 @@ path-to-regexp "3.2.0" tslib "2.6.3" -"@nestjs/platform-express@^10.3.3": - version "10.3.3" - resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.3.3.tgz#c1484d30d1e7666c4c8d0d7cde31cfc0b9d166d7" - integrity sha512-GGKSEU48Os7nYFIsUM0nutuFUGn5AbeP8gzFBiBCAtiuJWrXZXpZ58pMBYxAbMf7IrcOZFInHEukjHGAQU0OZw== +"@nestjs/platform-express@^10.4.6": + version "10.4.6" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-10.4.6.tgz#6c39c522fa66036b4256714fea203fbeb49fc4de" + integrity sha512-HcyCpAKccAasrLSGRTGWv5BKRs0rwTIFOSsk6laNyqfqvgvYcJQAedarnm4jmaemtmSJ0PFI9PmtEZADd2ahCg== dependencies: - body-parser "1.20.2" + body-parser "1.20.3" cors "2.8.5" - express "4.18.2" + express "4.21.1" multer "1.4.4-lts.1" - tslib "2.6.2" + tslib "2.7.0" "@netlify/functions@^2.8.0": version "2.8.1" @@ -8954,11 +8954,6 @@ "@smithy/types" "^2.12.0" tslib "^2.6.2" -"@socket.io/base64-arraybuffer@~1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#568d9beae00b0d835f4f8c53fd55714986492e61" - integrity sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ== - "@socket.io/component-emitter@~3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" @@ -9692,17 +9687,7 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@*": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -10025,15 +10010,7 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -12240,12 +12217,12 @@ aws-ssl-profiles@^1.1.1: resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== -axios@1.6.7, axios@^1.0.0, axios@^1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.7.tgz#7b48c2e27c96f9c68a2f8f31e2ab19f59b06b0a7" - integrity sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA== +axios@1.7.7, axios@^1.0.0, axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== dependencies: - follow-redirects "^1.15.4" + follow-redirects "^1.15.6" form-data "^4.0.0" proxy-from-env "^1.1.0" @@ -12730,28 +12707,10 @@ bluebird@^3.4.6, bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.1, body-parser@^1.18.3, body-parser@^1.19.0: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3, body-parser@^1.18.3, body-parser@^1.19.0: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -12761,7 +12720,7 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -12883,11 +12842,11 @@ braces@^2.3.1: to-regex "^3.0.1" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" broccoli-amd-funnel@^2.0.1: version "2.0.1" @@ -14690,21 +14649,31 @@ cookie-signature@^1.1.0: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.0.tgz#4deed303f5f095e7a02c979e3fcb19157f5eaeea" integrity sha512-R0BOPfLGTitaKhgKROKZQN6iyq2iDQcH1DOF8nJoaWapguX5bC2w+Q/I9NmmM5lfcvEarnLZr+cCvmEYYSXvYA== -cookie@0.5.0, cookie@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== -cookie@^0.4.1, cookie@~0.4.1: +cookie@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +cookie@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + cookie@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cookie@~0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + copy-anything@^2.0.1: version "2.0.6" resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" @@ -15301,7 +15270,7 @@ debug@^4.3.5: dependencies: ms "2.1.2" -debug@^4.3.6: +debug@^4.3.6, debug@~4.3.4: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -15941,9 +15910,9 @@ downlevel-dts@~0.11.0: typescript next dset@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a" - integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q== + version "3.1.4" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248" + integrity sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA== duplexer3@^0.1.4: version "0.1.4" @@ -16802,6 +16771,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + encoding@^0.1.11, encoding@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" @@ -16816,28 +16790,26 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -engine.io-parser@~5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.3.tgz#ca1f0d7b11e290b4bfda251803baea765ed89c09" - integrity sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg== - dependencies: - "@socket.io/base64-arraybuffer" "~1.0.2" +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== -engine.io@~6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.0.tgz#003bec48f6815926f2b1b17873e576acd54f41d0" - integrity sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg== +engine.io@~6.6.0: + version "6.6.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.2.tgz#32bd845b4db708f8c774a4edef4e5c8a98b3da72" + integrity sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw== dependencies: "@types/cookie" "^0.4.1" "@types/cors" "^2.8.12" "@types/node" ">=10.0.0" accepts "~1.3.4" base64id "2.0.0" - cookie "~0.4.1" + cookie "~0.7.2" cors "~2.8.5" debug "~4.3.1" - engine.io-parser "~5.0.3" - ws "~8.2.3" + engine.io-parser "~5.2.1" + ws "~8.17.1" enhanced-resolve@^5.10.0: version "5.16.0" @@ -18149,37 +18121,37 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -express@4.18.2, express@^4.10.7, express@^4.16.4, express@^4.17.1, express@^4.17.3, express@^4.18.1: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== +express@4.21.1, express@^4.10.7, express@^4.16.4, express@^4.17.1, express@^4.17.3, express@^4.18.1, express@^4.21.1: + version "4.21.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" + integrity sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.7.1" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.10" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -18497,10 +18469,10 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -18517,13 +18489,13 @@ finalhandler@1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -18710,10 +18682,10 @@ follow-redirects@^1.0.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== -follow-redirects@^1.15.4: - version "1.15.5" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" - integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== for-each@^0.3.3: version "0.3.3" @@ -20452,9 +20424,9 @@ http-proxy-agent@^5.0.0: debug "4" http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" @@ -20895,10 +20867,13 @@ ioredis@^5.4.1: redis-parser "^3.0.0" standard-as-callback "^2.1.0" -ip@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" ipaddr.js@1.9.1: version "1.9.1" @@ -22185,6 +22160,11 @@ js-yaml@^3.10.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.2.5, js-yaml@^3.2. argparse "^1.0.7" esprima "^4.0.0" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jsdoctypeparser@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz#8c97e2fb69315eb274b0f01377eaa5c940bd7b26" @@ -23847,10 +23827,10 @@ merge-anything@^5.1.7: dependencies: is-what "^4.1.8" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -25874,6 +25854,11 @@ object-inspect@^1.12.2, object-inspect@^1.9.0: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + object-is@^1.0.1: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" @@ -26712,10 +26697,10 @@ path-scurry@^1.6.1: 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" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@0.1.10: + version "0.1.10" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" + integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== path-to-regexp@3.2.0: version "3.2.0" @@ -26723,21 +26708,16 @@ path-to-regexp@3.2.0: integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== path-to-regexp@^1.5.3, path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + version "1.9.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24" + integrity sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g== dependencies: isarray "0.0.1" -path-to-regexp@^6.2.0: - version "6.2.2" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.2.tgz#324377a83e5049cbecadc5554d6a63a9a4866b36" - integrity sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw== - -path-to-regexp@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" - integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== +path-to-regexp@^6.2.0, path-to-regexp@^6.2.1: + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== path-type@^2.0.0: version "2.0.0" @@ -28258,7 +28238,14 @@ pupa@^2.1.1: dependencies: escape-goat "^2.0.0" -qs@6.11.0, qs@^6.4.0: +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== + dependencies: + side-channel "^1.0.6" + +qs@^6.4.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== @@ -28352,16 +28339,6 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - raw-body@2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" @@ -28477,7 +28454,7 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0": +"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -28492,13 +28469,6 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react-router@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" - react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -29117,9 +29087,9 @@ requirejs-config-file@^4.0.0: stringify-object "^3.2.1" requirejs@^2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9" - integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg== + version "2.3.7" + resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.7.tgz#0b22032e51a967900e0ae9f32762c23a87036bd0" + integrity sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw== requires-port@^1.0.0: version "1.0.0" @@ -29902,6 +29872,25 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + seq-queue@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" @@ -29951,7 +29940,17 @@ serve-placeholder@^2.0.2: dependencies: defu "^6.1.4" -serve-static@1.15.0, serve-static@^1.15.0: +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.19.0" + +serve-static@^1.15.0: version "1.15.0" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== @@ -30133,6 +30132,16 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + sift@13.5.2: version "13.5.2" resolved "https://registry.yarnpkg.com/sift/-/sift-13.5.2.tgz#24a715e13c617b086166cd04917d204a591c9da6" @@ -30340,30 +30349,34 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socket.io-adapter@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" - integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== +socket.io-adapter@~2.5.2: + version "2.5.5" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" + integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== + dependencies: + debug "~4.3.4" + ws "~8.17.1" -socket.io-parser@~4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667" - integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ== +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" socket.io@^4.1.2: - version "4.5.3" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.3.tgz#44dffea48d7f5aa41df4a66377c386b953bc521c" - integrity sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg== + version "4.8.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a" + integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg== dependencies: accepts "~1.3.4" base64id "~2.0.0" + cors "~2.8.5" debug "~4.3.2" - engine.io "~6.2.0" - socket.io-adapter "~2.4.0" - socket.io-parser "~4.2.0" + engine.io "~6.6.0" + socket.io-adapter "~2.5.2" + socket.io-parser "~4.2.4" sockjs@^0.3.24: version "0.3.24" @@ -30384,11 +30397,11 @@ socks-proxy-agent@^7.0.0: socks "^2.6.2" socks@^2.6.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ip "^2.0.0" + ip-address "^9.0.5" smart-buffer "^4.2.0" solid-js@^1.8.11: @@ -30699,6 +30712,11 @@ sprintf-js@^1.0.3: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -30886,16 +30904,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -31007,14 +31016,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -32031,6 +32033,11 @@ tslib@2.6.3, tslib@^2.2.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -33618,7 +33625,7 @@ webpack-bundle-analyzer@^4.10.2: sirv "^2.0.3" ws "^7.3.1" -webpack-dev-middleware@5.3.3, webpack-dev-middleware@^5.3.1: +webpack-dev-middleware@5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== @@ -33629,6 +33636,17 @@ webpack-dev-middleware@5.3.3, webpack-dev-middleware@^5.3.1: range-parser "^1.2.1" schema-utils "^4.0.0" +webpack-dev-middleware@^5.3.1: + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + webpack-dev-server@4.11.0: version "4.11.0" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.11.0.tgz#290ee594765cd8260adfe83b2d18115ea04484e7" @@ -34031,16 +34049,7 @@ wrangler@^3.67.1: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@7.0.0, wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -34111,35 +34120,20 @@ write-pkg@4.0.0: type-fest "^0.4.1" write-json-file "^3.2.0" -ws@^7.3.1: - version "7.5.9" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^7.3.1, ws@^7.4.6: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== -ws@^7.4.6: - version "7.5.6" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" - integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== - -ws@^8.13.0: - version "8.17.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" - integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== - -ws@^8.17.1, ws@^8.18.0: +ws@^8.13.0, ws@^8.17.1, ws@^8.18.0, ws@^8.4.2: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== -ws@^8.4.2: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" - integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== - -ws@~8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" - integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xdg-basedir@^4.0.0: version "4.0.0" From 8f65bf70ae4c795cd7c71b239f28d0885618e87e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 29 Oct 2024 10:22:19 +0100 Subject: [PATCH 39/44] ci: Fix CodeQL workflow config (#14117) Reverts https://github.com/getsentry/sentry-javascript/pull/14109 and re-implements this differently. Actually, the problem was dependabot merging to develop (so the fix would not have caught that anyhow), + this was incorrect syntax (oops) as we had ignore-branches _and_ branches, which does not work. Now, instead we just run this always but check if this is a push from dependabot, which hopefully works better. See https://github.com/getsentry/sentry-javascript/actions/runs/11570166519 --- .github/workflows/codeql-analysis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c3a36e5a34f7..2901d92ee12c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,9 +14,6 @@ name: 'CI: CodeQL' on: push: branches: [develop] - branches-ignore: - # Ignore dependabot branches - - "dependabot/**" pull_request: # The branches below must be a subset of the branches above branches: [develop] @@ -36,6 +33,8 @@ jobs: analyze: name: Analyze runs-on: ubuntu-latest + # Skip for pushes from dependabot, which is not supported + if: github.event_name == 'pull_request' || github.actor != 'dependabot[bot]' strategy: fail-fast: false From f18d04fcbf2f101e295066ee21acb984a0c0a25a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 29 Oct 2024 10:48:05 +0100 Subject: [PATCH 40/44] chore(dev-deps): Bump size-limit to 11.1.6 (#14114) Also bump transitive deps (e.g. loader-utils & webpack) to the latest allowed version. --- .../browser-integration-tests/package.json | 2 +- .../bundle-analyzer-scenarios/package.json | 2 +- package.json | 6 +- packages/ember/package.json | 2 +- yarn.lock | 98 +++++++++++-------- 5 files changed, 65 insertions(+), 45 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 09d6800b4310..d093a3531276 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -48,7 +48,7 @@ "babel-loader": "^8.2.2", "html-webpack-plugin": "^5.5.0", "pako": "^2.1.0", - "webpack": "^5.94.0" + "webpack": "^5.95.0" }, "devDependencies": { "@types/glob": "8.0.0", diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index 8733d3b578b0..d3061f63ee2c 100644 --- a/dev-packages/bundle-analyzer-scenarios/package.json +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -9,7 +9,7 @@ "private": true, "dependencies": { "html-webpack-plugin": "^5.6.0", - "webpack": "^5.94.0", + "webpack": "^5.95.0", "webpack-bundle-analyzer": "^4.10.2" }, "scripts": { diff --git a/package.json b/package.json index 419465ec95d4..29a272efcc6c 100644 --- a/package.json +++ b/package.json @@ -105,8 +105,8 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", "@rollup/pluginutils": "^5.1.0", - "@size-limit/file": "~11.1.0", - "@size-limit/webpack": "~11.1.0", + "@size-limit/file": "~11.1.6", + "@size-limit/webpack": "~11.1.6", "@strictsoftware/typedoc-plugin-monorepo": "^0.3.1", "@types/jest": "^27.4.1", "@types/jsdom": "^21.1.6", @@ -127,7 +127,7 @@ "rollup": "^4.24.2", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-license": "^3.3.1", - "size-limit": "~11.1.0", + "size-limit": "~11.1.6", "sucrase": "^3.35.0", "ts-jest": "^27.1.4", "ts-node": "10.9.1", diff --git a/packages/ember/package.json b/packages/ember/package.json index d75df71f7861..c198fa9af1cc 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -80,7 +80,7 @@ "qunit": "~2.19.2", "qunit-dom": "~2.0.0", "sinon": "15.2.0", - "webpack": "~5.94.0" + "webpack": "~5.95.0" }, "engines": { "node": ">=14.18" diff --git a/yarn.lock b/yarn.lock index e077f2dd33f5..9d3139160e51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8477,18 +8477,18 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@size-limit/file@~11.1.0": - version "11.1.0" - resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-11.1.0.tgz#9fe6497f5782cf4d887439b2ded1daf2ad2da620" - integrity sha512-C7Tr9dvw8Jx8If2xyGt0OprB+t03tAf259QJYi4WSl3IRMxH7l8k9qyITa/KCFcmH7tVBduNRSnHAL8Cf90EfQ== +"@size-limit/file@~11.1.6": + version "11.1.6" + resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-11.1.6.tgz#de1244aef06081a93bd594ddc28ef14080ca5b01" + integrity sha512-ojzzJMrTfcSECRnaTjGy0wNIolTCRdyqZTSWG9sG5XEoXG6PNgHXDDS6gf6YNxnqb+rWfCfVe93u6aKi3wEocQ== -"@size-limit/webpack@~11.1.0": - version "11.1.0" - resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-11.1.0.tgz#955fa23defae08697caa924c11df4e510dd03884" - integrity sha512-Tijbk9hgr2fPDz6M7ig7K4AbD8sacEljO+1c0bVVEr0JdbO7fv5dWsjS0lzKt+EM3GiuGPUVPZ4f381t/FzTSw== +"@size-limit/webpack@~11.1.6": + version "11.1.6" + resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-11.1.6.tgz#a73f5b82a88d0896e45697863370e7a56e6cf2b9" + integrity sha512-PTZCgwJsgdzdEj2wPFuLm0cCge8N2WbswMcKWNwMJibxQxPAmiF+sZ2F6GYBS7G7K3Fb4ovCliuN+wnnRACPNg== dependencies: - nanoid "^5.0.6" - webpack "^5.90.3" + nanoid "^5.0.7" + webpack "^5.95.0" "@smithy/abort-controller@^2.2.0": version "2.2.0" @@ -13940,6 +13940,13 @@ chokidar@^3.5.1, chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" +chokidar@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -18393,6 +18400,11 @@ fdir@^6.3.0: resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.3.0.tgz#fcca5a23ea20e767b15e081ee13b3e6488ee0bb0" integrity sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ== +fdir@^6.4.2: + version "6.4.2" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689" + integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ== + fflate@0.8.1, fflate@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.1.tgz#1ed92270674d2ad3c73f077cd0acf26486dae6c9" @@ -22116,6 +22128,11 @@ jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +jiti@^2.0.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.3.3.tgz#39c66fc77476b92a694e65dfe04b294070e2e096" + integrity sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ== + js-cleanup@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/js-cleanup/-/js-cleanup-1.2.0.tgz#8dbc65954b1d38b255f1e8cf02cd17b3f7a053f9" @@ -22711,11 +22728,6 @@ license-webpack-plugin@4.0.2: dependencies: webpack-sources "^3.0.0" -lilconfig@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.1.tgz#9d8a246fa753106cfc205fd2d77042faca56e5e3" - integrity sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ== - lilconfig@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" @@ -22833,18 +22845,18 @@ loader-utils@3.2.1: integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" json5 "^1.0.1" loader-utils@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" - integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" @@ -24833,11 +24845,6 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== -nanoid@^5.0.6: - version "5.0.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.6.tgz#7f99a033aa843e4dcf9778bdaec5eb02f4dc44d5" - integrity sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA== - nanoid@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.7.tgz#6452e8c5a816861fd9d2b898399f7e5fd6944cc6" @@ -28657,6 +28664,11 @@ readdir-glob@^1.1.2: dependencies: minimatch "^5.1.0" +readdirp@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" + integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -30254,18 +30266,18 @@ sisteransi@^1.0.5: resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== -size-limit@~11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-11.1.0.tgz#f0386fac40bfc26f4d8940e9d4824135e8391691" - integrity sha512-HvqVEUDJ3y/XL69pR3vxTavPr6s4S9ScJhHjLEjpctF0Ymgo1StmHgFeQoyQYb/1ip7olkWxJG3wRcZk1hZcPg== +size-limit@~11.1.6: + version "11.1.6" + resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-11.1.6.tgz#75cd54f9326d1b065ebcb6ca9ec27294e7ccdfb1" + integrity sha512-S5ux2IB8rU26xwVgMskmknGMFkieaIAqDLuwgKiypk6oa4lFsie8yFPrzRFV+yrLDY2GddjXuCaVk5PveVOHiQ== dependencies: bytes-iec "^3.1.1" - chokidar "^3.6.0" - globby "^14.0.1" - jiti "^1.21.0" - lilconfig "^3.1.1" + chokidar "^4.0.1" + jiti "^2.0.0" + lilconfig "^3.1.2" nanospinner "^1.1.0" - picocolors "^1.0.0" + picocolors "^1.1.0" + tinyglobby "^0.2.7" skip-regex@^1.0.2: version "1.0.2" @@ -31726,6 +31738,14 @@ tinyglobby@0.2.6, tinyglobby@^0.2.6: fdir "^6.3.0" picomatch "^4.0.2" +tinyglobby@^0.2.7: + version "0.2.10" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.10.tgz#e712cf2dc9b95a1f5c5bbd159720e15833977a0f" + integrity sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew== + dependencies: + fdir "^6.4.2" + picomatch "^4.0.2" + tinypool@^0.8.3: version "0.8.4" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.4.tgz#e217fe1270d941b39e98c625dcecebb1408c9aa8" @@ -33747,10 +33767,10 @@ webpack@5.76.1: watchpack "^2.4.0" webpack-sources "^3.2.3" -webpack@^5.90.3, webpack@^5.94.0, webpack@~5.94.0: - version "5.94.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" - integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== +webpack@^5.95.0, webpack@~5.95.0: + version "5.95.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" + integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== dependencies: "@types/estree" "^1.0.5" "@webassemblyjs/ast" "^1.12.1" From 81b1f9fa580ea193a7008ff6850d7db92dccde23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:26:28 +0100 Subject: [PATCH 41/44] ci(deps): bump github/codeql-action from 2 to 3 (#10004) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
Release notes

Sourced from github/codeql-action's releases.

CodeQL Bundle v2.15.5

Bundles CodeQL CLI v2.15.5

Includes the following CodeQL language packs from github/codeql@codeql-cli/v2.15.5:

CodeQL Bundle v2.15.4

Bundles CodeQL CLI v2.15.4

Includes the following CodeQL language packs from github/codeql@codeql-cli/v2.15.4:

CodeQL Bundle

Bundles CodeQL CLI v2.15.3

Includes the following CodeQL language packs from github/codeql@codeql-cli/v2.15.3:

... (truncated)

Changelog

Sourced from github/codeql-action's changelog.

Commits
  • e0c2b0a change version numbers inside processing function as well
  • 8e4a6c7 improve handling of changelog processing for backports
  • 511f073 Merge pull request #2033 from github/dependabot/npm_and_yarn/npm-0a98872b3d
  • ebf5a83 Merge pull request #2035 from github/mergeback/v3.22.11-to-main-b374143c
  • 7813bda Update checked-in dependencies
  • 2b2fb6b Update changelog and version after v3.22.11
  • b374143 Merge pull request #2034 from github/update-v3.22.11-64e61baea
  • 95591ba Merge branch 'main' into dependabot/npm_and_yarn/npm-0a98872b3d
  • e2b5cc7 Update changelog for v3.22.11
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=2&new-version=3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2901d92ee12c..daeb79c8fa30 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -50,7 +50,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: config-file: ./.github/codeql/codeql-config.yml queries: security-extended @@ -63,7 +63,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions @@ -77,4 +77,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 57ba5a7d5fea61194b085c16a9e22369fd3ae9a0 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 29 Oct 2024 13:06:22 +0100 Subject: [PATCH 42/44] fix(nextjs): Don't leak webpack types into exports (#14116) Fixes https://github.com/getsentry/sentry-javascript/issues/14066 We're just vendoring the types. The chance that anyone is depending on the specifics is lim x -> 0. --- packages/nextjs/package.json | 8 +------- packages/nextjs/src/config/types.ts | 14 ++++++-------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 23047338e280..66032b6ecc52 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -100,13 +100,7 @@ "next": "13.2.0" }, "peerDependencies": { - "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0", - "webpack": ">=5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } + "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 63f678cdbc66..d887369606b6 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -1,12 +1,5 @@ import type { GLOBAL_OBJ } from '@sentry/utils'; import type { SentryWebpackPluginOptions } from '@sentry/webpack-plugin'; -import type { DefinePlugin, WebpackPluginInstance } from 'webpack'; - -// Export this from here because importing something from Webpack (the library) in `webpack.ts` confuses the heck out of -// madge, which we use for circular dependency checking. We've manually excluded this file from the check (which is -// safe, since it only includes types), so we can import it here without causing madge to fail. See -// https://github.com/pahen/madge/issues/306. -export type { WebpackPluginInstance }; // The first argument to `withSentryConfig` (which is the user's next config). export type ExportedNextConfig = NextConfigObject | NextConfigFunction; @@ -17,6 +10,11 @@ type NextRewrite = { destination: string; }; +interface WebpackPluginInstance { + [index: string]: any; + apply: (compiler: any) => void; +} + export type NextConfigObject = { // Custom webpack options webpack?: WebpackConfigFunction | null; @@ -502,7 +500,7 @@ export type BuildContext = { config: any; webpack: { version: string; - DefinePlugin: typeof DefinePlugin; + DefinePlugin: new (values: Record) => WebpackPluginInstance; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any defaultLoaders: any; // needed for type tests (test:types) From 06ef6282bb4cf78bdb6cbe71c0dafc1b918adac1 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 29 Oct 2024 12:33:24 +0000 Subject: [PATCH 43/44] feat(nextjs): Add method and url to route handler request data (#14084) Resolves: https://github.com/getsentry/sentry-javascript/issues/13908 --------- Co-authored-by: Luca Forstner --- .../tests/route-handlers.test.ts | 3 + .../src/common/wrapRouteHandlerWithSentry.ts | 125 +++++++++--------- .../templates/routeHandlerWrapperTemplate.ts | 9 +- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts index 7e6dc5fbe300..648ee81839ac 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/route-handlers.test.ts @@ -60,6 +60,9 @@ test('Should record exceptions and transactions for faulty route handlers', asyn expect(routehandlerError.exception?.values?.[0].value).toBe('route-handler-error'); + expect(routehandlerError.request?.method).toBe('PUT'); + expect(routehandlerError.request?.url).toContain('/route-handlers/baz/error'); + expect(routehandlerError.transaction).toBe('PUT /route-handlers/[param]/error'); }); diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index 6ff4e314b17b..215bb35ce9a5 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -5,6 +5,7 @@ import { captureException, getActiveSpan, getCapturedScopesOnSpan, + getIsolationScope, getRootSpan, handleCallbackErrors, setCapturedScopesOnSpan, @@ -16,9 +17,8 @@ import { import type { RouteHandlerContext } from './types'; import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; - import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; -import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; +import { commonObjectToIsolationScope } from './utils/tracingUtils'; /** * Wraps a Next.js App Router Route handler with Sentry error and performance instrumentation. @@ -34,80 +34,83 @@ export function wrapRouteHandlerWithSentry any>( return new Proxy(routeHandler, { apply: async (originalFunction, thisArg, args) => { - const isolationScope = commonObjectToIsolationScope(headers); - - const completeHeadersDict: Record = headers ? winterCGHeadersToDict(headers) : {}; - - isolationScope.setSDKProcessingMetadata({ - request: { - headers: completeHeadersDict, - }, - }); - - const incomingPropagationContext = propagationContextFromHeaders( - completeHeadersDict['sentry-trace'], - completeHeadersDict['baggage'], - ); - - const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext); - const activeSpan = getActiveSpan(); const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined; - if (rootSpan) { + + let edgeRuntimeIsolationScopeOverride: Scope | undefined; + if (rootSpan && process.env.NEXT_RUNTIME === 'edge') { + const isolationScope = commonObjectToIsolationScope(headers); const { scope } = getCapturedScopesOnSpan(rootSpan); setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); - if (process.env.NEXT_RUNTIME === 'edge') { - rootSpan.updateName(`${method} ${parameterizedRoute}`); - rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); - rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); - } + edgeRuntimeIsolationScopeOverride = isolationScope; + + rootSpan.updateName(`${method} ${parameterizedRoute}`); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server'); } - return withIsolationScope(isolationScope, () => { - return withScope(async scope => { - scope.setTransactionName(`${method} ${parameterizedRoute}`); - scope.setPropagationContext(propagationContext); + return withIsolationScope( + process.env.NEXT_RUNTIME === 'edge' ? edgeRuntimeIsolationScopeOverride : getIsolationScope(), + () => { + return withScope(async scope => { + scope.setTransactionName(`${method} ${parameterizedRoute}`); - const response: Response = await handleCallbackErrors( - () => originalFunction.apply(thisArg, args), - error => { - // Next.js throws errors when calling `redirect()`. We don't wanna report these. - if (isRedirectNavigationError(error)) { - // Don't do anything - } else if (isNotFoundNavigationError(error)) { + if (process.env.NEXT_RUNTIME === 'edge') { + const completeHeadersDict: Record = headers ? winterCGHeadersToDict(headers) : {}; + const incomingPropagationContext = propagationContextFromHeaders( + completeHeadersDict['sentry-trace'], + completeHeadersDict['baggage'], + ); + scope.setPropagationContext(incomingPropagationContext); + scope.setSDKProcessingMetadata({ + request: { + method, + headers: completeHeadersDict, + }, + }); + } + + const response: Response = await handleCallbackErrors( + () => originalFunction.apply(thisArg, args), + error => { + // Next.js throws errors when calling `redirect()`. We don't wanna report these. + if (isRedirectNavigationError(error)) { + // Don't do anything + } else if (isNotFoundNavigationError(error)) { + if (activeSpan) { + setHttpStatus(activeSpan, 404); + } + if (rootSpan) { + setHttpStatus(rootSpan, 404); + } + } else { + captureException(error, { + mechanism: { + handled: false, + }, + }); + } + }, + ); + + try { + if (response.status) { if (activeSpan) { - setHttpStatus(activeSpan, 404); + setHttpStatus(activeSpan, response.status); } if (rootSpan) { - setHttpStatus(rootSpan, 404); + setHttpStatus(rootSpan, response.status); } - } else { - captureException(error, { - mechanism: { - handled: false, - }, - }); - } - }, - ); - - try { - if (response.status) { - if (activeSpan) { - setHttpStatus(activeSpan, response.status); - } - if (rootSpan) { - setHttpStatus(rootSpan, response.status); } + } catch { + // best effort - response may be undefined? } - } catch { - // best effort - response may be undefined? - } - return response; - }); - }); + return response; + }); + }, + ); }, }); } diff --git a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts index 9f955f9f4109..d0b9fba5ce3b 100644 --- a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts @@ -38,27 +38,20 @@ function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | ' return new Proxy(handler, { apply: (originalFunction, thisArg, args) => { - let sentryTraceHeader: string | undefined | null = undefined; - let baggageHeader: string | undefined | null = undefined; let headers: WebFetchHeaders | undefined = undefined; // We try-catch here just in case the API around `requestAsyncStorage` changes unexpectedly since it is not public API try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const requestAsyncStore = requestAsyncStorage?.getStore() as ReturnType; - sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace') ?? undefined; - baggageHeader = requestAsyncStore?.headers.get('baggage') ?? undefined; headers = requestAsyncStore?.headers; } catch (e) { /** empty */ } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any return Sentry.wrapRouteHandlerWithSentry(originalFunction as any, { method, parameterizedRoute: '__ROUTE__', - sentryTraceHeader, - baggageHeader, headers, }).apply(thisArg, args); }, From 8d9322c9f03d5c3beff7f1d77f3b8a4319bb8a01 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 29 Oct 2024 13:11:58 +0000 Subject: [PATCH 44/44] meta(changelog): Update changelog for 8.36.0 --- CHANGELOG.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 8 +++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bb9478862cc..d584d65cff65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,56 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.36.0 + +### Important Changes + +- **feat(nuxt): Add Sentry Pinia plugin ([#14047](https://github.com/getsentry/sentry-javascript/pull/14047))** + +The Nuxt SDK now allows you to track Pinia state for captured errors. To enable the Pinia plugin, set the `trackPinia` option to `true` in your client config: + +```ts +// sentry.client.config.ts + +Sentry.init({ + trackPinia: true, +}); +``` + +Read more about the Pinia plugin in the [Sentry Pinia Documentation](https://docs.sentry.io/platforms/javascript/guides/nuxt/features/pinia/). + +- **feat(nextjs/vercel-edge/cloudflare): Switch to OTEL for performance monitoring ([#13889](https://github.com/getsentry/sentry-javascript/pull/13889))** + +With this release, the Sentry Next.js, and Cloudflare SDKs will now capture performance data based on OpenTelemetry. +Some exceptions apply in cases where Next.js captures inaccurate data itself. + +NOTE: You may experience minor differences in transaction names in Sentry. +Most importantly transactions for serverside pages router invocations will now be named `GET /[param]/my/route` instead of `/[param]/my/route`. +This means that those transactions are now better aligned with the OpenTelemetry semantic conventions. + +### Other Changes + +- deps: Bump bundler plugins and CLI to 2.22.6 and 2.37.0 respectively ([#14050](https://github.com/getsentry/sentry-javascript/pull/14050)) +- feat(deps): bump @opentelemetry/instrumentation-aws-sdk from 0.44.0 to 0.45.0 ([#14099](https://github.com/getsentry/sentry-javascript/pull/14099)) +- feat(deps): bump @opentelemetry/instrumentation-connect from 0.39.0 to 0.40.0 ([#14101](https://github.com/getsentry/sentry-javascript/pull/14101)) +- feat(deps): bump @opentelemetry/instrumentation-express from 0.43.0 to 0.44.0 ([#14102](https://github.com/getsentry/sentry-javascript/pull/14102)) +- feat(deps): bump @opentelemetry/instrumentation-fs from 0.15.0 to 0.16.0 ([#14098](https://github.com/getsentry/sentry-javascript/pull/14098)) +- feat(deps): bump @opentelemetry/instrumentation-kafkajs from 0.3.0 to 0.4.0 ([#14100](https://github.com/getsentry/sentry-javascript/pull/14100)) +- feat(nextjs): Add method and url to route handler request data ([#14084](https://github.com/getsentry/sentry-javascript/pull/14084)) +- feat(node): Add breadcrumbs for `child_process` and `worker_thread` ([#13896](https://github.com/getsentry/sentry-javascript/pull/13896)) +- fix(core): Ensure standalone spans are not sent if SDK is disabled ([#14088](https://github.com/getsentry/sentry-javascript/pull/14088)) +- fix(nextjs): Await flush in api handlers ([#14023](https://github.com/getsentry/sentry-javascript/pull/14023)) +- fix(nextjs): Don't leak webpack types into exports ([#14116](https://github.com/getsentry/sentry-javascript/pull/14116)) +- fix(nextjs): Fix matching logic for file convention type for root level components ([#14038](https://github.com/getsentry/sentry-javascript/pull/14038)) +- fix(nextjs): Respect directives in value injection loader ([#14083](https://github.com/getsentry/sentry-javascript/pull/14083)) +- fix(nuxt): Only wrap `.mjs` entry files in rollup ([#14060](https://github.com/getsentry/sentry-javascript/pull/14060)) +- fix(nuxt): Re-export all exported bindings ([#14086](https://github.com/getsentry/sentry-javascript/pull/14086)) +- fix(nuxt): Server-side setup in readme ([#14049](https://github.com/getsentry/sentry-javascript/pull/14049)) +- fix(profiling-node): Always warn when running on incompatible major version of Node.js ([#14043](https://github.com/getsentry/sentry-javascript/pull/14043)) +- fix(replay): Fix `onError` callback ([#14002](https://github.com/getsentry/sentry-javascript/pull/14002)) +- perf(otel): Only calculate current timestamp once ([#14094](https://github.com/getsentry/sentry-javascript/pull/14094)) +- test(browser-integration): Add sentry DSN route handler by default ([#14095](https://github.com/getsentry/sentry-javascript/pull/14095)) + ## 8.35.0 ### Beta release of the official Nuxt Sentry SDK diff --git a/package.json b/package.json index 29a272efcc6c..bee335619d24 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,12 @@ "printWidth": 120, "proseWrap": "always", "singleQuote": true, - "trailingComma": "all" + "trailingComma": "all", + "overrides": [{ + "files": "CHANGELOG.md", + "options": { + "proseWrap": "preserve" + } + }] } }