From 34708fd69282f2c648a05b4c7dab99efa0bc209d Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:36:57 +0200 Subject: [PATCH 01/14] ref(nuxt): Only use rollup for build (#13138) Previously, the Nuxt SDK was built with `@nuxt/module-builder` and rollup. This made things complicated, as it created two separate folders in the build output which could not share code between each other. This setup removes the `@nuxt/module-builder` and adds the module config to rollup. --- packages/nuxt/package.json | 13 +- packages/nuxt/rollup.npm.config.mjs | 38 +++- .../nuxt/src/{module.ts => module/index.ts} | 8 +- .../plugins/sentry.client.ts | 0 .../plugins/sentry.server.ts | 0 .../nuxt/src/{runtime => module}/utils.ts | 0 .../nuxt/test/client/runtime/utils.test.ts | 2 +- .../nuxt/test/server/runtime/plugin.test.ts | 2 +- yarn.lock | 170 +++--------------- 9 files changed, 69 insertions(+), 164 deletions(-) rename packages/nuxt/src/{module.ts => module/index.ts} (87%) rename packages/nuxt/src/{runtime => module}/plugins/sentry.client.ts (100%) rename packages/nuxt/src/{runtime => module}/plugins/sentry.server.ts (100%) rename packages/nuxt/src/{runtime => module}/utils.ts (100%) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 49540d91f1e0..a0a9ec715527 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -30,9 +30,9 @@ } }, "./module": { - "types": "./build/module/types.d.ts", - "import": "./build/module/module.mjs", - "require": "./build/module/module.cjs" + "types": "./build/types/module/index.d.ts", + "import": "./build/esm/module/index.js", + "require": "./build/cjs/module/index.js" } }, "publishConfig": { @@ -53,14 +53,12 @@ "@sentry/vue": "8.21.0" }, "devDependencies": { - "@nuxt/module-builder": "0.8.0", "nuxt": "^3.12.2" }, "scripts": { "build": "run-s build:types build:transpile", "build:dev": "yarn build", - "build:nuxt-module": "nuxt-module-build build --outDir build/module", - "build:transpile": "rollup -c rollup.npm.config.mjs && yarn build:nuxt-module", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", @@ -90,7 +88,8 @@ "outputs": [ "{projectRoot}/build/cjs", "{projectRoot}/build/esm", - "{projectRoot}/build/module" + "{projectRoot}/build/cjs/module", + "{projectRoot}/build/esm/module" ] } } diff --git a/packages/nuxt/rollup.npm.config.mjs b/packages/nuxt/rollup.npm.config.mjs index a672e9e43eb3..63db9a45e9d6 100644 --- a/packages/nuxt/rollup.npm.config.mjs +++ b/packages/nuxt/rollup.npm.config.mjs @@ -1,7 +1,35 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -export default makeNPMConfigVariants( - makeBaseNPMConfig({ - entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'], - }), -); +export default [ + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: [ + 'src/index.server.ts', + 'src/index.client.ts', + 'src/client/index.ts', + 'src/server/index.ts', + 'src/module/index.ts', + ], + packageSpecificConfig: { + external: ['nuxt/app'], + }, + }), + ), + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/module/plugins/sentry.client.ts', 'src/module/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', + }, + }, + }), + ), +]; + +/* + + */ diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module/index.ts similarity index 87% rename from packages/nuxt/src/module.ts rename to packages/nuxt/src/module/index.ts index 6cfccfbd2714..2b38d413d06f 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module/index.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('./runtime/plugins/sentry.client'), mode: 'client' }); + addPlugin({ src: moduleDirResolver.resolve('./plugins/sentry.client'), mode: 'client' }); } const serverConfigFile = findDefaultSdkInitFile('server'); @@ -46,7 +46,7 @@ export default defineNuxtModule({ 'export default defineNuxtPlugin(() => {})', }); - addServerPlugin(moduleDirResolver.resolve('./runtime/plugins/sentry.server')); + addServerPlugin(moduleDirResolver.resolve('./plugins/sentry.server')); } if (clientConfigFile || serverConfigFile) { diff --git a/packages/nuxt/src/runtime/plugins/sentry.client.ts b/packages/nuxt/src/module/plugins/sentry.client.ts similarity index 100% rename from packages/nuxt/src/runtime/plugins/sentry.client.ts rename to packages/nuxt/src/module/plugins/sentry.client.ts diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/module/plugins/sentry.server.ts similarity index 100% rename from packages/nuxt/src/runtime/plugins/sentry.server.ts rename to packages/nuxt/src/module/plugins/sentry.server.ts diff --git a/packages/nuxt/src/runtime/utils.ts b/packages/nuxt/src/module/utils.ts similarity index 100% rename from packages/nuxt/src/runtime/utils.ts rename to packages/nuxt/src/module/utils.ts diff --git a/packages/nuxt/test/client/runtime/utils.test.ts b/packages/nuxt/test/client/runtime/utils.test.ts index b0b039d52e54..ceb10b9bb7fa 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/runtime/utils'; +import { extractErrorContext } from '../../../src/module/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 518b20026cbd..407eec41eb59 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/runtime/utils'; +import { addSentryTracingMetaTags } from '../../../src/module/utils'; const mockReturns = vi.hoisted(() => { return { diff --git a/yarn.lock b/yarn.lock index 650c78d792aa..067cbb38765b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6437,21 +6437,6 @@ unimport "^3.7.2" untyped "^1.4.2" -"@nuxt/module-builder@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@nuxt/module-builder/-/module-builder-0.8.0.tgz#39955cc224df61adceebf0ba4478f4ab202e00df" - integrity sha512-r8zsnTus4I2zv4jbVljTb2DPULfqBTQfUzfCsZUolaTYz/qJW1NfVOd9juVbIJFHJR+4ZQzxoxL9zScjzS0YNg== - 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.0" - 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" @@ -7580,7 +7565,7 @@ dependencies: web-streams-polyfill "^3.1.1" -"@rollup/plugin-alias@^5.0.0", "@rollup/plugin-alias@^5.1.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== @@ -7599,18 +7584,6 @@ 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" @@ -7646,7 +7619,7 @@ dependencies: "@rollup/pluginutils" "^3.0.8" -"@rollup/plugin-json@^6.0.0", "@rollup/plugin-json@^6.1.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== @@ -7665,7 +7638,7 @@ is-module "^1.0.0" resolve "^1.19.0" -"@rollup/plugin-node-resolve@^15.2.1", "@rollup/plugin-node-resolve@^15.2.3": +"@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== @@ -7677,14 +7650,6 @@ is-module "^1.0.0" resolve "^1.22.1" -"@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.5": version "5.0.5" resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz#33d5653dce6d03cb24ef98bef7f6d25b57faefdf" @@ -7693,6 +7658,14 @@ "@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== + dependencies: + "@rollup/pluginutils" "^5.0.1" + magic-string "^0.30.3" + "@rollup/plugin-sucrase@^5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@rollup/plugin-sucrase/-/plugin-sucrase-5.0.2.tgz#f8b8b54ad789a47fa882b968a76cede0194eea6e" @@ -7752,7 +7725,7 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@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": +"@rollup/pluginutils@^5.0.2", "@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== @@ -13964,7 +13937,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.2, citty@^0.1.5, citty@^0.1.6: +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== @@ -19351,7 +19324,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.2.2: +globby@^13.1.1: version "13.2.2" resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== @@ -21997,16 +21970,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" @@ -23198,19 +23171,6 @@ 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" @@ -24407,27 +24367,6 @@ 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.2" - resolved "https://registry.yarnpkg.com/mkdist/-/mkdist-1.5.2.tgz#ff81c60c0a865394952becbc236f5c55b0011654" - integrity sha512-Xa6+CSzw6N338+vfWZcM5B5GEkZRmtWd2zFdoegNGnoF6p5o0je5lBfCKKCIo8jSQ9yG3hVbFOoz3G0ZmLfAjg== - dependencies: - autoprefixer "^10.4.19" - citty "^0.1.6" - cssnano "^7.0.2" - defu "^6.1.4" - esbuild "^0.21.5" - fs-extra "^11.2.0" - globby "^14.0.1" - jiti "^1.21.6" - mlly "^1.7.1" - mri "^1.2.0" - pathe "^1.1.2" - pkg-types "^1.1.1" - postcss "^8.4.38" - 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" @@ -24443,7 +24382,7 @@ mlly@^1.2.0, mlly@^1.4.2: pkg-types "^1.0.3" ufo "^1.3.2" -mlly@^1.3.0, mlly@^1.4.0, mlly@^1.6.1, mlly@^1.7.0, mlly@^1.7.1: +mlly@^1.3.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== @@ -27270,13 +27209,6 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nested@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" - integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== - dependencies: - postcss-selector-parser "^6.0.11" - 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" @@ -27531,7 +27463,7 @@ postcss-selector-parser@^6.0.10: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.1.0: +postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ== @@ -28675,11 +28607,6 @@ 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" @@ -29254,15 +29181,6 @@ 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" @@ -29328,7 +29246,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.28.1: +rollup@3.29.4, rollup@^3.27.1: version "3.29.4" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== @@ -31875,11 +31793,6 @@ tsconfck@^3.0.0: resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.0.tgz#b469f1ced12973bbec3209a55ed8de3bb04223c9" integrity sha512-w3wnsIrJNi7avf4Zb0VjOoodoO0woEqGgZGQm+LHH9przdUI+XDKsWAXwxHA1DaRTjeuZNcregSzr7RaA8zG9A== -tsconfck@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.0.tgz#30c63b15972b591adb41dc9a339a02743d090c81" - integrity sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w== - tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -32047,11 +31960,6 @@ 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" @@ -32183,36 +32091,6 @@ 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" @@ -32572,7 +32450,7 @@ unplugin@1.0.1: webpack-sources "^3.2.3" webpack-virtual-modules "^0.5.0" -unplugin@^1.10.0, unplugin@^1.10.1, unplugin@^1.3.1, unplugin@^1.5.0, unplugin@^1.8.3: +unplugin@^1.10.0, unplugin@^1.10.1, unplugin@^1.3.1, unplugin@^1.5.0: version "1.10.1" resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.10.1.tgz#8ceda065dc71bc67d923dea0920f05c67f2cd68c" integrity sha512-d6Mhq8RJeGA8UfKCu54Um4lFA0eSaRa3XxdAJg8tIdxbu1ubW0hBCZUL7yI2uGyYCRndvbK8FLHzqy2XKfeMsg== @@ -32627,7 +32505,7 @@ untun@^0.1.3: consola "^3.2.3" pathe "^1.1.1" -untyped@^1.4.0, untyped@^1.4.2: +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== From 2306458c6c7dba0920005f47eebcc728e27c13ec Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 1 Aug 2024 10:05:17 +0200 Subject: [PATCH 02/14] ci: Only run affected unit tests on PRs (#13052) This PR updates our unit test runner on CI to only run unit tests for packages that have been changed. In addition, it also updates the list of packages to run in node/browser only unit test envs - we've been running some in both. We only actually need to run the fully-browser-only unit tests in the "Browser Unit Tests" job, the rest (including e.g. core, ...) runs in the node unit tests in all node versions. In a follow up step, maybe we can further streamline this and simply run only unit tests and ensure that e.g. the browser ones only run in one of the node versions, not all of them. Or find another way that does not require us to keep 2 lists of packages in separate places. But for now, this should be an improvement. I opened a test PR to show this in action, where only something in the react package was changed: * Browser Unit Tests: https://github.com/getsentry/sentry-javascript/actions/runs/10180894649/job/28160138970 * Node Unit Tests: https://github.com/getsentry/sentry-javascript/actions/runs/10180894649/job/28160141152 --- .github/workflows/build.yml | 35 ++++++++++++++++++++++++++++++----- package.json | 9 ++++++--- scripts/node-unit-tests.ts | 20 +++++++++++++++++++- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29dcba922106..d653671e2801 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -434,6 +434,11 @@ jobs: timeout-minutes: 10 runs-on: ubuntu-20.04 steps: + - name: Check out base commit (${{ github.event.pull_request.base.sha }}) + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 with: @@ -446,8 +451,15 @@ jobs: uses: ./.github/actions/restore-cache env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Run tests - run: yarn test-ci-browser + + - name: Run affected tests + run: yarn test:pr:browser --base=${{ github.event.pull_request.base.sha }} + if: github.event_name == 'pull_request' + + - name: Run all tests + run: yarn test:ci:browser + if: github.event_name != 'pull_request' + - name: Compute test coverage uses: codecov/codecov-action@v4 with: @@ -478,7 +490,7 @@ jobs: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - name: Run tests run: | - yarn test-ci-bun + yarn test:ci:bun job_deno_unit_tests: name: Deno Unit Tests @@ -521,6 +533,10 @@ jobs: matrix: node: [14, 16, 18, 20, 22] steps: + - name: Check out base commit (${{ github.event.pull_request.base.sha }}) + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.base.sha }} - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) uses: actions/checkout@v4 with: @@ -533,10 +549,19 @@ jobs: uses: ./.github/actions/restore-cache env: DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Run tests + + - name: Run affected tests + run: yarn test:pr:node --base=${{ github.event.pull_request.base.sha }} + if: github.event_name == 'pull_request' env: NODE_VERSION: ${{ matrix.node }} - run: yarn test-ci-node + + - name: Run all tests + run: yarn test:ci:node + if: github.event_name != 'pull_request' + env: + NODE_VERSION: ${{ matrix.node }} + - name: Compute test coverage uses: codecov/codecov-action@v4 with: diff --git a/package.json b/package.json index 3d2a137ab650..604d67e9ef5b 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,13 @@ "postpublish": "lerna run --stream --concurrency 1 postpublish", "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test", "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test:unit", - "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,deno,node,profiling-node,serverless,google-cloud,nextjs,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", "test:update-snapshots": "lerna run test:update-snapshots", + "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"", + "test:pr: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:node": "ts-node ./scripts/node-unit-tests.ts", + "test:ci:bun": "lerna run test --scope @sentry/bun", "yalc:publish": "lerna run yalc:publish" }, "volta": { diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts index d5fe581f2a82..e9f22da1309a 100644 --- a/scripts/node-unit-tests.ts +++ b/scripts/node-unit-tests.ts @@ -8,6 +8,8 @@ interface VersionConfig { const CURRENT_NODE_VERSION = process.version.replace('v', '').split('.')[0] as NodeVersion; +const RUN_AFFECTED = process.argv.includes('--affected'); + const DEFAULT_SKIP_TESTS_PACKAGES = [ '@sentry-internal/eslint-plugin-sdk', '@sentry/ember', @@ -70,6 +72,18 @@ function runWithIgnores(skipPackages: string[] = []): void { run(`yarn test ${ignoreFlags}`); } +/** + * Run affected tests, ignoring the given packages + */ +function runAffectedWithIgnores(skipPackages: string[] = []): void { + const additionalArgs = process.argv + .slice(2) + .filter(arg => arg !== '--affected') + .join(' '); + const ignoreFlags = skipPackages.map(dep => `--exclude="${dep}"`).join(' '); + run(`yarn test:pr ${ignoreFlags} ${additionalArgs}`); +} + /** * Run the tests, accounting for compatibility problems in older versions of Node. */ @@ -83,7 +97,11 @@ function runTests(): void { versionConfig.ignoredPackages.forEach(dep => ignores.add(dep)); } - runWithIgnores(Array.from(ignores)); + if (RUN_AFFECTED) { + runAffectedWithIgnores(Array.from(ignores)); + } else { + runWithIgnores(Array.from(ignores)); + } } runTests(); From bfb7ec4f91e0a8cfd2a369db736cc1f1268467db Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Thu, 1 Aug 2024 10:25:15 +0200 Subject: [PATCH 03/14] feat(nestjs): Automatic instrumentation of nestjs pipes (#13137) Adds automatic instrumentation of pipes to `@sentry/nestjs`. Pipes in nest have a `@Injectable` decorator and implement a `transform` function. So we can simply extend the existing instrumentation to add a proxy for `transform`. --- .../nestjs-basic/src/app.controller.ts | 7 +- .../nestjs-basic/tests/transactions.test.ts | 72 +++++++++++++++++++ .../node-nestjs-basic/src/app.controller.ts | 7 +- .../tests/transactions.test.ts | 72 +++++++++++++++++++ .../node/src/integrations/tracing/nest.ts | 26 +++++++ 5 files changed, 182 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts index eb0ead5e32d0..40ee93adaa90 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; @@ -22,6 +22,11 @@ export class AppController { return {}; } + @Get('test-pipe-instrumentation/:id') + testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { + return { value: id }; + } + @Get('test-exception/:id') async testException(@Param('id') id: string) { return this.appService.testException(id); 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 ebd8503e1d42..33e56cd5695e 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 @@ -266,3 +266,75 @@ test('API route transaction includes nest guard span and span started in guard i // 'ExampleGuard' is the parent of 'test-guard-span' expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId); }); + +test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + ); + }); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`); + expect(response.status).toBe(200); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); + +test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + ); + }); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`); + expect(response.status).toBe(400); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'unknown_error', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts index eb0ead5e32d0..40ee93adaa90 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; @@ -22,6 +22,11 @@ export class AppController { return {}; } + @Get('test-pipe-instrumentation/:id') + testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { + return { value: id }; + } + @Get('test-exception/:id') async testException(@Param('id') id: string) { return this.appService.testException(id); 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 c646ac9aea74..754d545979e5 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 @@ -266,3 +266,75 @@ test('API route transaction includes nest guard span and span started in guard i // 'ExampleGuard' is the parent of 'test-guard-span' expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId); }); + +test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + ); + }); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`); + expect(response.status).toBe(200); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); + +test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('nestjs', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' + ); + }); + + const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`); + expect(response.status).toBe(400); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ParseIntPipe', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'unknown_error', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); +}); diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts index dbf3c40ab171..cb3097b06228 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -72,6 +72,8 @@ export interface InjectableTarget { 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; }; } @@ -212,6 +214,30 @@ export class SentryNestInstrumentation extends InstrumentationBase { }); } + // patch pipes + if (typeof target.prototype.transform === 'function') { + if (isPatched(target)) { + return original(options)(target); + } + + target.prototype.transform = new Proxy(target.prototype.transform, { + apply: (originalTransform, thisArgTransform, argsTransform) => { + return startSpan( + { + name: target.name, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', + }, + }, + () => { + return originalTransform.apply(thisArgTransform, argsTransform); + }, + ); + }, + }); + } + return original(options)(target); }; }; From 29f070f92cf43b07f632e0310e955d63b42b6103 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 1 Aug 2024 12:38:38 +0200 Subject: [PATCH 04/14] feat(nuxt): Filter out Nuxt build assets (#13148) Filtering out events of the `_nuxt` [buildAssetDir](https://nuxt.com/docs/api/nuxt-config#buildassetsdir). Next step would be to change the regex based on the folder name (as this could be changed in the nuxt config during build time - but probably doesn't happen too often). Also added some unit tests for the server-side of the SDK. Before: image After: image --- .../test-applications/nuxt-3/package.json | 2 +- .../nuxt-3/public/instrument.server.mjs | 8 ++++ .../nuxt-3/tests/performance.server.test.ts | 46 +++++++++++++++++++ packages/nuxt/src/server/sdk.ts | 31 +++++++++++-- packages/nuxt/test/server/sdk.test.ts | 42 +++++++++++++++++ 5 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/public/instrument.server.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.server.test.ts create mode 100644 packages/nuxt/test/server/sdk.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index 72acea9f33b6..a487d61a144b 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -6,7 +6,7 @@ "build": "nuxt build", "dev": "nuxt dev", "generate": "nuxt generate", - "preview": "nuxt preview", + "preview": "NODE_OPTIONS='--import ./public/instrument.server.mjs' nuxt preview", "clean": "npx nuxi cleanup", "test": "playwright test", "test:build": "pnpm install && npx playwright install && pnpm build", diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/public/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/nuxt-3/public/instrument.server.mjs new file mode 100644 index 000000000000..729b2296c683 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/public/instrument.server.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/nuxt'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.server.test.ts new file mode 100644 index 000000000000..5b78e235e564 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/performance.server.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transaction = await transactionPromise; + + expect(transaction.contexts.trace).toEqual( + expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.otel.http', + }), + }), + ); +}); + +test('does not send transactions for build asset folder "_nuxt"', async ({ page }) => { + let buildAssetFolderOccurred = false; + + waitForTransaction('nuxt-3', transactionEvent => { + if (transactionEvent.transaction?.match(/^GET \/_nuxt\//)) { + buildAssetFolderOccurred = true; + } + return false; // expects to return a boolean (but not relevant here) + }); + + const transactionEventPromise = waitForTransaction('nuxt-3', transactionEvent => { + return transactionEvent.transaction.includes('GET /test-param/'); + }); + + await page.goto('/test-param/1234'); + + const transactionEvent = await transactionEventPromise; + + expect(buildAssetFolderOccurred).toBe(false); + + // todo: url not yet parametrized + expect(transactionEvent.transaction).toBe('GET /test-param/1234'); +}); diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 4faae8e6ef1a..f14cc23ab8cd 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -1,6 +1,6 @@ -import { applySdkMetadata } from '@sentry/core'; +import { applySdkMetadata, getGlobalScope } from '@sentry/core'; import { init as initNode } from '@sentry/node'; -import type { Client } from '@sentry/types'; +import type { Client, EventProcessor } from '@sentry/types'; import type { SentryNuxtOptions } from '../common/types'; /** @@ -15,5 +15,30 @@ export function init(options: SentryNuxtOptions): Client | undefined { applySdkMetadata(sentryOptions, 'nuxt', ['nuxt', 'node']); - return initNode(sentryOptions); + const client = initNode(sentryOptions); + + getGlobalScope().addEventProcessor( + Object.assign( + (event => { + if (event.type === 'transaction') { + // Filter out transactions for Nuxt build assets + // This regex matches the default path to the nuxt-generated build assets (`_nuxt`). + // 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); + return null; + } + + return event; + } else { + return event; + } + }) satisfies EventProcessor, + { id: 'NuxtLowQualityTransactionsFilter' }, + ), + ); + + return client; } diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts new file mode 100644 index 000000000000..8d84dc8b15c8 --- /dev/null +++ b/packages/nuxt/test/server/sdk.test.ts @@ -0,0 +1,42 @@ +import * as SentryNode from '@sentry/node'; +import { SDK_VERSION } from '@sentry/node'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { init } from '../../src/server'; + +const nodeInit = vi.spyOn(SentryNode, 'init'); + +describe('Nuxt Server SDK', () => { + describe('init', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('Adds Nuxt metadata to the SDK options', () => { + expect(nodeInit).not.toHaveBeenCalled(); + + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + const expectedMetadata = { + _metadata: { + sdk: { + name: 'sentry.javascript.nuxt', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/nuxt', version: SDK_VERSION }, + { name: 'npm:@sentry/node', version: SDK_VERSION }, + ], + }, + }, + }; + + expect(nodeInit).toHaveBeenCalledTimes(1); + expect(nodeInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + }); + + it('returns client from init', () => { + expect(init({})).not.toBeUndefined(); + }); + }); +}); From 26c81e8fb15b0b840338d8f266a66122833ee6bd Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 1 Aug 2024 14:33:01 +0200 Subject: [PATCH 05/14] test(e2e): Add `nextjs-13` e2e test app (#13154) --- .github/workflows/build.yml | 1 + .github/workflows/canary.yml | 6 + .../test-applications/nextjs-13/.gitignore | 42 +++++ .../test-applications/nextjs-13/.npmrc | 2 + .../nextjs-13/app/layout.tsx | 7 + .../app/pageload-transaction/page.tsx | 3 + .../nextjs-13/app/rsc-error/page.tsx | 6 + .../test-applications/nextjs-13/globals.d.ts | 4 + .../nextjs-13/instrumentation.ts | 17 ++ .../test-applications/nextjs-13/next-env.d.ts | 6 + .../nextjs-13/next.config.js | 17 ++ .../test-applications/nextjs-13/package.json | 45 +++++ .../nextjs-13/pages/[param]/click-error.tsx | 12 ++ .../pages/[param]/navigation-start-page.tsx | 9 + .../pages/[param]/navigation-target-page.tsx | 3 + .../pages/[param]/pages-pageload.tsx | 3 + .../pages/[param]/withInitialProps.tsx | 7 + .../pages/[param]/withServerSideProps.tsx | 7 + .../nextjs-13/pages/_app.tsx | 19 ++ .../pages/api/[param]/failure-api-route.ts | 5 + .../nextjs-13/pages/api/[param]/index.ts | 5 + .../pages/api/[param]/success-api-route.ts | 5 + .../api/cjs-api-endpoint-with-require.ts | 14 ++ .../nextjs-13/pages/api/cjs-api-endpoint.ts | 7 + .../pages/api/endpoint-excluded-with-regex.ts | 7 + .../api/endpoint-excluded-with-string.ts | 7 + .../nextjs-13/pages/api/no-params.ts | 5 + .../pages/api/params/[...pathParts].ts | 5 + .../nextjs-13/pages/crashed-session-page.tsx | 13 ++ .../pages/customPageExtension.page.tsx | 12 ++ .../pages/error-getServerSideProps.tsx | 7 + .../nextjs-13/pages/fetch.tsx | 12 ++ .../nextjs-13/pages/healthy-session-page.tsx | 3 + .../misconfigured-_app-getInitialProps.tsx | 5 + .../nextjs-13/pages/reportDialog.tsx | 15 ++ ...unmatchedCustomPageExtension.someExtension | 3 + .../nextjs-13/playwright.config.ts | 19 ++ .../nextjs-13/sentry.client.config.ts | 9 + .../nextjs-13/start-event-proxy.mjs | 6 + .../tests/client/app-dir-pageloads.test.ts | 48 +++++ .../tests/client/click-error.test.ts | 48 +++++ ...ltyAppGetInitialPropsConfiguration.test.ts | 13 ++ .../nextjs-13/tests/client/fetch.test.ts | 58 ++++++ .../tests/client/pages-dir-navigation.test.ts | 56 ++++++ .../tests/client/pages-dir-pageload.test.ts | 48 +++++ .../tests/client/reportDialog.test.ts | 17 ++ .../nextjs-13/tests/client/sessions.test.ts | 26 +++ .../tests/isomorphic/getInitialProps.test.ts | 60 ++++++ .../isomorphic/getServerSideProps.test.ts | 60 ++++++ .../tests/server/cjs-api-endpoints.test.ts | 122 ++++++++++++ .../server/excluded-api-endpoints.test.ts | 46 +++++ .../tests/server/getServerSideProps.test.ts | 173 ++++++++++++++++++ .../server/pages-router-api-endpoints.test.ts | 123 +++++++++++++ .../server/server-component-error.test.ts | 39 ++++ .../nextjs-13/tests/server/utils/throw.js | 1 + .../server/wrapApiHandlerWithSentry.test.ts | 60 ++++++ .../test-applications/nextjs-13/tsconfig.json | 25 +++ .../test-utils/src/event-proxy-server.ts | 26 ++- dev-packages/test-utils/src/index.ts | 1 + .../test-utils/src/playwright-config.ts | 2 +- 60 files changed, 1429 insertions(+), 3 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/app/layout.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/app/pageload-transaction/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/app/rsc-error/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/next-env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/next.config.js create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/click-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/navigation-start-page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/navigation-target-page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/pages-pageload.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withInitialProps.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withServerSideProps.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/_app.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/failure-api-route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/index.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/success-api-route.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/cjs-api-endpoint-with-require.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/cjs-api-endpoint.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-regex.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-string.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/no-params.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/params/[...pathParts].ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/crashed-session-page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/customPageExtension.page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/error-getServerSideProps.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/fetch.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/healthy-session-page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/misconfigured-_app-getInitialProps.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/reportDialog.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/pages/unmatchedCustomPageExtension.someExtension create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/click-error.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/faultyAppGetInitialPropsConfiguration.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/reportDialog.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/utils/throw.js create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-13/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d653671e2801..76d2ade83d15 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1019,6 +1019,7 @@ jobs: 'ember-classic', 'ember-embroider', 'nextjs-app-dir', + 'nextjs-13', 'nextjs-14', 'nextjs-15', 'react-17', diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 42c1594ebc5b..1f584d2a921c 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -78,6 +78,12 @@ jobs: - test-application: 'nextjs-app-dir' build-command: 'test:build-latest' label: 'nextjs-app-dir (latest)' + - test-application: 'nextjs-13' + build-command: 'test:build-canary' + label: 'nextjs-13 (canary)' + - test-application: 'nextjs-13' + build-command: 'test:build-latest' + label: 'nextjs-13 (latest)' - test-application: 'nextjs-14' build-command: 'test:build-canary' label: 'nextjs-14 (canary)' diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore new file mode 100644 index 000000000000..b7a8bf3b3701 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +!*.d.ts + +test-results + +.vscode diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/app/layout.tsx new file mode 100644 index 000000000000..c8f9cee0b787 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/app/pageload-transaction/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/app/pageload-transaction/page.tsx new file mode 100644 index 000000000000..b8109689f986 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/app/pageload-transaction/page.tsx @@ -0,0 +1,3 @@ +export default function PageloadTransactionPage() { + return

Pageload Transaction Page

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/app/rsc-error/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/app/rsc-error/page.tsx new file mode 100644 index 000000000000..9328f85142a8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/app/rsc-error/page.tsx @@ -0,0 +1,6 @@ +export const dynamic = 'force-dynamic'; + +export default async function Page() { + throw new Error('RSC error'); + return

Hello World

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/globals.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/globals.d.ts new file mode 100644 index 000000000000..109dbcd55648 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/globals.d.ts @@ -0,0 +1,4 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts new file mode 100644 index 000000000000..1f889238427c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/nextjs'; + +export function register() { + if (process.env.NEXT_RUNTIME === 'nodejs' || process.env.NEXT_RUNTIME === 'edge') { + Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + sendDefaultPii: true, + transportOptions: { + // We are doing a lot of events at once in this test app + bufferSize: 1000, + }, + }); + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/next-env.d.ts new file mode 100644 index 000000000000..fd36f9494e2c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-13/next.config.js new file mode 100644 index 000000000000..a08502723262 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/next.config.js @@ -0,0 +1,17 @@ +const { withSentryConfig } = require('@sentry/nextjs'); + +/** @type {import('next').NextConfig} */ +const moduleExports = { + typescript: { + ignoreBuildErrors: true, // TODO: Remove this + }, + experimental: { + appDir: true, + }, + pageExtensions: ['jsx', 'js', 'tsx', 'ts', 'page.tsx'], +}; + +module.exports = withSentryConfig(moduleExports, { + silent: true, + excludeServerRoutes: ['/api/endpoint-excluded-with-string', /\/api\/endpoint-excluded-with-regex/], +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json new file mode 100644 index 000000000000..3f1219f210a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json @@ -0,0 +1,45 @@ +{ + "name": "create-next-app", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)", + "clean": "npx rimraf node_modules pnpm-lock.yaml .next", + "test:prod": "TEST_ENV=production playwright test", + "test:dev": "TEST_ENV=development playwright test", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build", + "test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod && pnpm test:dev" + }, + "dependencies": { + "@sentry/nextjs": "latest || *", + "@types/node": "18.11.17", + "@types/react": "18.0.26", + "@types/react-dom": "18.0.9", + "next": "13.2.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "4.9.5" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry-internal/feedback": "latest || *", + "@sentry-internal/replay-canvas": "latest || *", + "@sentry-internal/browser-utils": "latest || *", + "@sentry/browser": "latest || *", + "@sentry/core": "latest || *", + "@sentry/nextjs": "latest || *", + "@sentry/node": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@sentry/react": "latest || *", + "@sentry-internal/replay": "latest || *", + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *", + "@sentry/vercel-edge": "latest || *" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/click-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/click-error.tsx new file mode 100644 index 000000000000..c0b3dc70edec --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/click-error.tsx @@ -0,0 +1,12 @@ +export default function ClickErrorPage() { + return ( + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/navigation-start-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/navigation-start-page.tsx new file mode 100644 index 000000000000..4a344176db31 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/navigation-start-page.tsx @@ -0,0 +1,9 @@ +import Link from 'next/link'; + +export default function Page() { + return ( + + Navigate + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/navigation-target-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/navigation-target-page.tsx new file mode 100644 index 000000000000..c49ff17fd490 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/navigation-target-page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

arrived

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/pages-pageload.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/pages-pageload.tsx new file mode 100644 index 000000000000..5b0847bb89fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/pages-pageload.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

pageload test page

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withInitialProps.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withInitialProps.tsx new file mode 100644 index 000000000000..01b557bdd09f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withInitialProps.tsx @@ -0,0 +1,7 @@ +const WithInitialPropsPage = ({ data }: { data: string }) =>

WithInitialPropsPage {data}

; + +WithInitialPropsPage.getInitialProps = () => { + return { data: '[some getInitialProps data]' }; +}; + +export default WithInitialPropsPage; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withServerSideProps.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withServerSideProps.tsx new file mode 100644 index 000000000000..0379cc202436 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/[param]/withServerSideProps.tsx @@ -0,0 +1,7 @@ +export default function WithServerSidePropsPage({ data }: { data: string }) { + return

WithServerSidePropsPage {data}

; +} + +export async function getServerSideProps() { + return { props: { data: '[some getServerSideProps data]' } }; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/_app.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/_app.tsx new file mode 100644 index 000000000000..d6dfa41828d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/_app.tsx @@ -0,0 +1,19 @@ +import App, { AppContext, AppProps } from 'next/app'; + +const MyApp = ({ Component, pageProps }: AppProps) => { + // @ts-ignore I don't know why TS complains here + return ; +}; + +MyApp.getInitialProps = async (appContext: AppContext) => { + // This simulates user misconfiguration. Users should always call `App.getInitialProps(appContext)`, but they don't, + // so we have a test for this so we don't break their apps. + if (appContext.ctx.pathname === '/misconfigured-_app-getInitialProps') { + return {}; + } + + const appProps = await App.getInitialProps(appContext); + return { ...appProps }; +}; + +export default MyApp; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/failure-api-route.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/failure-api-route.ts new file mode 100644 index 000000000000..8a0d5f537aa6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/failure-api-route.ts @@ -0,0 +1,5 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async (_req: NextApiRequest, res: NextApiResponse) => { + throw new Error('api route error'); +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/index.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/index.ts new file mode 100644 index 000000000000..faa9a571ca10 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/index.ts @@ -0,0 +1,5 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async (_req: NextApiRequest, res: NextApiResponse) => { + res.status(200).json({ success: true }); +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/success-api-route.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/success-api-route.ts new file mode 100644 index 000000000000..faa9a571ca10 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/[param]/success-api-route.ts @@ -0,0 +1,5 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async (_req: NextApiRequest, res: NextApiResponse) => { + res.status(200).json({ success: true }); +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/cjs-api-endpoint-with-require.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/cjs-api-endpoint-with-require.ts new file mode 100644 index 000000000000..63a5176101ce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/cjs-api-endpoint-with-require.ts @@ -0,0 +1,14 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +if (process.env.NEXT_PUBLIC_SOME_FALSE_ENV_VAR === 'enabled') { + require('../../tests/server/utils/throw'); // Should not throw unless the hoisting in the wrapping loader is messed up! +} + +const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { + require('@sentry/nextjs').captureException; // Should not throw unless the wrapping loader messes up cjs imports + // @ts-expect-error + require.context('.'); // This is a webpack utility call. Should not throw unless the wrapping loader messes it up by mangling. + res.status(200).json({ success: true }); +}; + +module.exports = handler; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/cjs-api-endpoint.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/cjs-api-endpoint.ts new file mode 100644 index 000000000000..6ae521fa5cb4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/cjs-api-endpoint.ts @@ -0,0 +1,7 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { + res.status(200).json({ success: true }); +}; + +module.exports = handler; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-regex.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-regex.ts new file mode 100644 index 000000000000..6ae521fa5cb4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-regex.ts @@ -0,0 +1,7 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { + res.status(200).json({ success: true }); +}; + +module.exports = handler; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-string.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-string.ts new file mode 100644 index 000000000000..6ae521fa5cb4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-string.ts @@ -0,0 +1,7 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { + res.status(200).json({ success: true }); +}; + +module.exports = handler; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/no-params.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/no-params.ts new file mode 100644 index 000000000000..faa9a571ca10 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/no-params.ts @@ -0,0 +1,5 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async (_req: NextApiRequest, res: NextApiResponse) => { + res.status(200).json({ success: true }); +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/params/[...pathParts].ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/params/[...pathParts].ts new file mode 100644 index 000000000000..faa9a571ca10 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/params/[...pathParts].ts @@ -0,0 +1,5 @@ +import { NextApiRequest, NextApiResponse } from 'next'; + +export default async (_req: NextApiRequest, res: NextApiResponse) => { + res.status(200).json({ success: true }); +}; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/crashed-session-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/crashed-session-page.tsx new file mode 100644 index 000000000000..277293e77aed --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/crashed-session-page.tsx @@ -0,0 +1,13 @@ +export default function CrashedPage() { + // Magic to naively trigger onerror to make session crashed and allow for SSR + try { + if (typeof window !== 'undefined' && typeof window.onerror === 'function') { + // Lovely oldschool browsers syntax with 5 arguments <3 + // @ts-expect-error + window.onerror(null, null, null, null, new Error('Crashed')); + } + } catch (_e) { + // no-empty + } + return

Crashed

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/customPageExtension.page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/customPageExtension.page.tsx new file mode 100644 index 000000000000..5f25223a9b4d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/customPageExtension.page.tsx @@ -0,0 +1,12 @@ +export default function BasicPage() { + return ( +

+ This page simply exists to test the compatibility of Next.js' `pageExtensions` option with our auto wrapping + process. This file should be turned into a page by Next.js and our webpack loader should process it. +

+ ); +} + +export async function getServerSideProps() { + throw new Error('custom page extension error'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/error-getServerSideProps.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/error-getServerSideProps.tsx new file mode 100644 index 000000000000..9bc737cf7a7d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/error-getServerSideProps.tsx @@ -0,0 +1,7 @@ +export default function WithServerSidePropsPage({ data }: { data: string }) { + return

WithServerSidePropsPage {data}

; +} + +export async function getServerSideProps() { + throw new Error('getServerSideProps Error'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/fetch.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/fetch.tsx new file mode 100644 index 000000000000..0493d4e508f3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/fetch.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; + +export default function FetchPage() { + useEffect(() => { + // test that a span is created in the pageload transaction for this fetch request + fetch('http://example.com').catch(() => { + // no-empty + }); + }, []); + + return

Hello world!

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/healthy-session-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/healthy-session-page.tsx new file mode 100644 index 000000000000..6a30e4f8b3a8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/healthy-session-page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

healthy page

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/misconfigured-_app-getInitialProps.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/misconfigured-_app-getInitialProps.tsx new file mode 100644 index 000000000000..3627c5088af8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/misconfigured-_app-getInitialProps.tsx @@ -0,0 +1,5 @@ +// See _app.tsx for more information why this file exists. + +export default function Page() { + return

faulty _app getInitialProps

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/reportDialog.tsx b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/reportDialog.tsx new file mode 100644 index 000000000000..a8e097c769a9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/reportDialog.tsx @@ -0,0 +1,15 @@ +import { captureException, showReportDialog } from '@sentry/nextjs'; + +export default function ReportDialogPage() { + return ( + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/unmatchedCustomPageExtension.someExtension b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/unmatchedCustomPageExtension.someExtension new file mode 100644 index 000000000000..e8d58e47f18e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/unmatchedCustomPageExtension.someExtension @@ -0,0 +1,3 @@ +This page simply exists to test the compatibility of Next.js' `pageExtensions` option with our auto wrapping +process. This file should not be turned into a page by Next.js and our webpack loader also shouldn't process it. +This page should not contain valid JavaScript. diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/playwright.config.ts new file mode 100644 index 000000000000..8448829443d6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/playwright.config.ts @@ -0,0 +1,19 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; +const testEnv = process.env.TEST_ENV; + +if (!testEnv) { + throw new Error('No test env defined'); +} + +const config = getPlaywrightConfig( + { + startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030' : 'pnpm next start -p 3030', + port: 3030, + }, + { + // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize + workers: '100%', + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/sentry.client.config.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs new file mode 100644 index 000000000000..9983d484bcbc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nextjs-13', +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts new file mode 100644 index 000000000000..d7d08eb22773 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/app-dir-pageloads.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create a pageload transaction when the `app` directory is used', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/pageload-transaction' && transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/pageload-transaction`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + breadcrumbs: expect.arrayContaining([ + { + category: 'navigation', + data: { from: '/pageload-transaction', to: '/pageload-transaction' }, + timestamp: expect.any(Number), + }, + ]), + contexts: { + react: { version: expect.any(String) }, + trace: { + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', + 'sentry.source': 'url', + }, + op: 'pageload', + origin: 'auto.pageload.nextjs.app_router_instrumentation', + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + environment: 'qa', + request: { + headers: expect.any(Object), + url: expect.stringMatching(/\/pageload-transaction$/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/pageload-transaction', + transaction_info: { source: 'url' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/click-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/click-error.test.ts new file mode 100644 index 000000000000..a9fbcdb69a45 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/click-error.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('should send error for faulty click handlers', async ({ page }) => { + const errorPromise = waitForError('nextjs-13', async errorEvent => { + return errorEvent.exception?.values?.[0].value === 'click error'; + }); + + await page.goto('/42/click-error'); + await page.click('#error-button'); + + const errorEvent = await errorPromise; + + expect(errorEvent).toBeDefined(); + + const frames = errorEvent?.exception?.values?.[0]?.stacktrace?.frames; + + await test.step('error should have a non-url-encoded top frame in route with parameter', () => { + if (process.env.TEST_ENV === 'development') { + // In dev mode we want to check local source mapping + expect(frames?.[frames.length - 1].filename).toMatch(/\/\[param\]\/click-error.tsx$/); + } else { + expect(frames?.[frames.length - 1].filename).toMatch(/\/\[param\]\/click-error-[a-f0-9]+\.js$/); + } + }); + + await test.step('error should have `in_app`: false for nextjs internal frames', () => { + if (process.env.TEST_ENV !== 'development') { + expect(frames).toContainEqual( + expect.objectContaining({ + filename: expect.stringMatching( + /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, + ), + in_app: false, + }), + ); + + expect(frames).not.toContainEqual( + expect.objectContaining({ + filename: expect.stringMatching( + /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, + ), + in_app: true, + }), + ); + } + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/faultyAppGetInitialPropsConfiguration.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/faultyAppGetInitialPropsConfiguration.test.ts new file mode 100644 index 000000000000..68336c3e5c4e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/faultyAppGetInitialPropsConfiguration.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from '@playwright/test'; + +// This test verifies that a faulty configuration of `getInitialProps` in `_app` will not cause our +// auto - wrapping / instrumentation to throw an error. +// See `_app.tsx` for more information. + +test('should not fail auto-wrapping when `getInitialProps` configuration is faulty.', async ({ page }) => { + await page.goto('/misconfigured-_app-getInitialProps'); + + const serverErrorText = await page.$('//*[contains(text(), "Internal Server Error")]'); + + expect(serverErrorText).toBeFalsy(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts new file mode 100644 index 000000000000..5b42e14269bc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/fetch.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should correctly instrument `fetch` for performance tracing', async ({ page }) => { + await page.route('http://example.com/**/*', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + foo: 'bar', + }), + }); + }); + + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return transactionEvent.transaction === '/fetch' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto(`/fetch`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + transaction: '/fetch', + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + }, + }, + }); + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + data: { + 'http.method': 'GET', + url: 'http://example.com', + 'http.url': 'http://example.com/', + 'server.address': 'example.com', + type: 'fetch', + 'http.response_content_length': expect.any(Number), + 'http.response.status_code': 200, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + }, + description: 'GET http://example.com', + op: 'http.client', + parent_span_id: expect.any(String), + span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: expect.any(String), + status: expect.any(String), + origin: 'auto.http.browser', + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts new file mode 100644 index 000000000000..8bd128896b6c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-navigation.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should report a navigation transaction for pages router navigations', async ({ page }) => { + test.skip(process.env.TEST_ENV === 'development', 'Test is flakey in dev mode'); + const navigationTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/navigation-target-page' && + transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + await page.goto('/foo/navigation-start-page'); + await page.click('#navigation-link'); + + expect(await navigationTransactionPromise).toMatchObject({ + breadcrumbs: expect.arrayContaining([ + { + category: 'navigation', + data: { from: '/foo/navigation-start-page', to: '/foo/navigation-start-page' }, + timestamp: expect.any(Number), + }, + { category: 'ui.click', message: 'body > div#__next > a#navigation-link', timestamp: expect.any(Number) }, + { + category: 'navigation', + data: { from: '/foo/navigation-start-page', to: '/foo/navigation-target-page' }, + timestamp: expect.any(Number), + }, + ]), + contexts: { + trace: { + data: { + 'sentry.idle_span_finish_reason': 'idleTimeout', + 'sentry.op': 'navigation', + 'sentry.origin': 'auto.navigation.nextjs.pages_router_instrumentation', + 'sentry.source': 'route', + }, + op: 'navigation', + origin: 'auto.navigation.nextjs.pages_router_instrumentation', + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + platform: 'javascript', + request: { + headers: expect.any(Object), + url: expect.stringMatching(/\/foo\/navigation-target-page$/), + }, + spans: expect.arrayContaining([]), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/[param]/navigation-target-page', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts new file mode 100644 index 000000000000..8c74b2c99427 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts @@ -0,0 +1,48 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create a pageload transaction when the `pages` directory is used', async ({ page }) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/pages-pageload' && transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + await page.goto(`/foo/pages-pageload`); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + breadcrumbs: expect.arrayContaining([ + { + category: 'navigation', + data: { from: '/foo/pages-pageload', to: '/foo/pages-pageload' }, + timestamp: expect.any(Number), + }, + ]), + contexts: { + react: { version: expect.any(String) }, + trace: { + data: { + 'sentry.op': 'pageload', + 'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation', + 'sentry.source': 'route', + }, + op: 'pageload', + origin: 'auto.pageload.nextjs.pages_router_instrumentation', + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + environment: 'qa', + request: { + headers: expect.any(Object), + url: expect.stringMatching(/\/foo\/pages-pageload$/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/[param]/pages-pageload', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/reportDialog.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/reportDialog.test.ts new file mode 100644 index 000000000000..386d228ebf0c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/reportDialog.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test'; + +test('should show a dialog', async ({ page }) => { + // *= means "containing" + const dialogScriptSelector = 'head > script[src*="/api/embed/error-page"]'; + + await page.goto('/reportDialog'); + + expect(await page.locator(dialogScriptSelector).count()).toEqual(0); + + await page.click('#open-report-dialog'); + + const dialogScript = await page.waitForSelector(dialogScriptSelector, { state: 'attached' }); + const dialogScriptSrc = await (await dialogScript.getProperty('src')).jsonValue(); + + expect(dialogScriptSrc).toMatch(/^http.*\/api\/embed\/error-page\/\?.*/); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts new file mode 100644 index 000000000000..8fbe8ac8b7b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/sessions.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from '@playwright/test'; +import { waitForSession } from '@sentry-internal/test-utils'; + +test('should report healthy sessions', async ({ page }) => { + test.skip(process.env.TEST_ENV === 'development', 'test is flakey in dev mode'); + + const sessionPromise = waitForSession('nextjs-13', session => { + return session.init === true && session.status === 'ok' && session.errors === 0; + }); + + await page.goto('/healthy-session-page'); + + expect(await sessionPromise).toBeDefined(); +}); + +test('should report crashed sessions', async ({ page }) => { + test.skip(process.env.TEST_ENV === 'development', 'test is flakey in dev mode'); + + const sessionPromise = waitForSession('nextjs-13', session => { + return session.init === false && session.status === 'crashed' && session.errors === 1; + }); + + await page.goto('/crashed-session-page'); + + expect(await sessionPromise).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts new file mode 100644 index 000000000000..22da2071d533 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should propagate serverside `getInitialProps` trace to client', async ({ page }) => { + const pageloadTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/withInitialProps' && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/withInitialProps' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await page.goto(`/42/withInitialProps`); + + const pageloadTransaction = await pageloadTransactionPromise; + + expect(pageloadTransaction).toBeDefined(); + + await test.step('should propagate tracing data from server to client', async () => { + const nextDataTag = await page.waitForSelector('#__NEXT_DATA__', { state: 'attached' }); + const nextDataTagValue = JSON.parse(await nextDataTag.evaluate(tag => (tag as HTMLElement).innerText)); + + const traceId = pageloadTransaction?.contexts?.trace?.trace_id; + + expect(traceId).toBeDefined(); + + expect(nextDataTagValue.props.pageProps.data).toBe('[some getInitialProps data]'); + expect(nextDataTagValue.props.pageProps._sentryTraceData).toBeTruthy(); + expect(nextDataTagValue.props.pageProps._sentryBaggage).toBeTruthy(); + + expect(nextDataTagValue.props.pageProps._sentryTraceData.split('-')[0]).toBe(traceId); + + expect(nextDataTagValue.props.pageProps._sentryBaggage.match(/sentry-trace_id=([a-f0-9]*),/)[1]).toBe(traceId); + }); + + await test.step('should record serverside performance', async () => { + expect(await serverTransactionPromise).toMatchObject({ + contexts: { + trace: { + op: 'http.server', + status: 'ok', + }, + }, + transaction: '/[param]/withInitialProps', + transaction_info: { + source: 'route', + }, + type: 'transaction', + request: { + url: expect.stringMatching(/http.*\/42\/withInitialProps$/), + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts new file mode 100644 index 000000000000..20bbbc9437f6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should record performance for getServerSideProps', async ({ page }) => { + const pageloadTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/withServerSideProps' && + transactionEvent.contexts?.trace?.op === 'pageload' + ); + }); + + const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === '/[param]/withServerSideProps' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await page.goto(`/1337/withServerSideProps`); + + const pageloadTransaction = await pageloadTransactionPromise; + + expect(pageloadTransaction).toBeDefined(); + + await test.step('should propagate tracing data from server to client', async () => { + const nextDataTag = await page.waitForSelector('#__NEXT_DATA__', { state: 'attached' }); + const nextDataTagValue = JSON.parse(await nextDataTag.evaluate(tag => (tag as HTMLElement).innerText)); + + const traceId = pageloadTransaction?.contexts?.trace?.trace_id; + + expect(traceId).toBeDefined(); + + expect(nextDataTagValue.props.pageProps.data).toBe('[some getServerSideProps data]'); + expect(nextDataTagValue.props.pageProps._sentryTraceData).toBeTruthy(); + expect(nextDataTagValue.props.pageProps._sentryBaggage).toBeTruthy(); + + expect(nextDataTagValue.props.pageProps._sentryTraceData.split('-')[0]).toBe(traceId); + + expect(nextDataTagValue.props.pageProps._sentryBaggage.match(/sentry-trace_id=([a-f0-9]*),/)[1]).toBe(traceId); + }); + + await test.step('should record serverside performance', async () => { + expect(await serverTransactionPromise).toMatchObject({ + contexts: { + trace: { + op: 'http.server', + status: 'ok', + }, + }, + transaction: '/[param]/withServerSideProps', + transaction_info: { + source: 'route', + }, + type: 'transaction', + request: { + url: expect.stringMatching(/http.*\/1337\/withServerSideProps$/), + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts new file mode 100644 index 000000000000..8a0ed1176142 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts @@ -0,0 +1,122 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create a transaction for a CJS pages router API endpoint', async ({ request }) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/cjs-api-endpoint' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + const result = (await request.get(`/api/cjs-api-endpoint`)).json(); + + expect(await result).toMatchObject({ success: true }); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + contexts: { + otel: { + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + environment: 'qa', + event_id: expect.any(String), + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/cjs-api-endpoint$/), + }, + spans: expect.arrayContaining([]), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /api/cjs-api-endpoint', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); + +test('should not mess up require statements in CJS API endpoints', async ({ request }) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/cjs-api-endpoint-with-require' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + const result = (await request.get(`/api/cjs-api-endpoint-with-require`)).json(); + + expect(await result).toMatchObject({ success: true }); + + const transaction = await transactionPromise; + + expect(transaction).toMatchObject({ + contexts: { + otel: { + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.sample_rate': 1, + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + environment: 'qa', + event_id: expect.any(String), + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/cjs-api-endpoint-with-require$/), + }, + spans: expect.arrayContaining([]), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /api/cjs-api-endpoint-with-require', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts new file mode 100644 index 000000000000..b148accf1450 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should not automatically create transactions for routes that were excluded from auto wrapping (string)', async ({ + request, +}) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/endpoint-excluded-with-string' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await (await request.get(`/api/endpoint-excluded-with-string`)).json(); + + let transactionPromiseReceived = false; + transactionPromise.then(() => { + transactionPromiseReceived = true; + }); + + await new Promise(resolve => setTimeout(resolve, 5_000)); + + expect(transactionPromiseReceived).toBe(false); +}); + +test('should not automatically create transactions for routes that were excluded from auto wrapping (regex)', async ({ + request, +}) => { + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/endpoint-excluded-with-regex' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await (await request.get(`/api/endpoint-excluded-with-regex`)).json(); + + let transactionPromiseReceived = false; + transactionPromise.then(() => { + transactionPromiseReceived = true; + }); + + await new Promise(resolve => setTimeout(resolve, 5_000)); + + expect(transactionPromiseReceived).toBe(false); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts new file mode 100644 index 000000000000..0c99ba302dfa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts @@ -0,0 +1,173 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should report an error event for errors thrown in getServerSideProps', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-13', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'getServerSideProps Error'; + }); + + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + return ( + transactionEvent.transaction === '/error-getServerSideProps' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await page.goto('/error-getServerSideProps'); + + expect(await errorEventPromise).toMatchObject({ + contexts: { + trace: { span_id: expect.any(String), trace_id: expect.any(String) }, + }, + event_id: expect.any(String), + exception: { + values: [ + { + mechanism: { handled: false, type: 'generic' }, + type: 'Error', + value: 'getServerSideProps Error', + stacktrace: { + frames: expect.arrayContaining([]), + }, + }, + ], + }, + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/error-getServerSideProps/), + }, + timestamp: expect.any(Number), + transaction: 'getServerSideProps (/error-getServerSideProps)', + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + otel: { + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 500, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.function.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.function.nextjs', + span_id: expect.any(String), + status: 'internal_error', + trace_id: expect.any(String), + }, + }, + event_id: expect.any(String), + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/error-getServerSideProps/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/error-getServerSideProps', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); + +test('Should report an error event for errors thrown in getServerSideProps in pages with custom page extensions', async ({ + page, +}) => { + const errorEventPromise = waitForError('nextjs-13', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'custom page extension error'; + }); + + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + return ( + transactionEvent.transaction === '/customPageExtension' && transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + await page.goto('/customPageExtension'); + + expect(await errorEventPromise).toMatchObject({ + contexts: { + trace: { span_id: expect.any(String), trace_id: expect.any(String) }, + }, + event_id: expect.any(String), + exception: { + values: [ + { + mechanism: { handled: false, type: 'generic' }, + type: 'Error', + value: 'custom page extension error', + stacktrace: { + frames: expect.arrayContaining([]), + }, + }, + ], + }, + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/customPageExtension/), + }, + timestamp: expect.any(Number), + transaction: 'getServerSideProps (/customPageExtension)', + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + otel: { + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }, + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 500, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.function.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.function.nextjs', + span_id: expect.any(String), + status: 'internal_error', + trace_id: expect.any(String), + }, + }, + event_id: expect.any(String), + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/customPageExtension/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/customPageExtension', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts new file mode 100644 index 000000000000..12196c08fcc1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/pages-router-api-endpoints.test.ts @@ -0,0 +1,123 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should report an error event for errors thrown in pages router api routes', async ({ request }) => { + const errorEventPromise = waitForError('nextjs-13', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'api route error'; + }); + + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/[param]/failure-api-route' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + request.get('/api/foo/failure-api-route').catch(e => { + // expected to crash + }); + + expect(await errorEventPromise).toMatchObject({ + contexts: { + runtime: { name: 'node', version: expect.any(String) }, + trace: { span_id: expect.any(String), trace_id: expect.any(String) }, + }, + exception: { + values: [ + { + mechanism: { + data: { + function: 'withSentry', + }, + handled: false, + type: 'instrument', + }, + stacktrace: { frames: expect.arrayContaining([]) }, + type: 'Error', + value: 'api route error', + }, + ], + }, + platform: 'node', + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/foo\/failure-api-route$/), + }, + timestamp: expect.any(Number), + transaction: 'GET /api/[param]/failure-api-route', + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 500, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'internal_error', + trace_id: (await errorEventPromise).contexts?.trace?.trace_id, + }, + }, + platform: 'node', + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/foo\/failure-api-route$/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /api/[param]/failure-api-route', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); + +test('Should report a transaction event for a successful pages router api route', async ({ request }) => { + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + return ( + transactionEvent.transaction === 'GET /api/[param]/success-api-route' && + transactionEvent.contexts?.trace?.op === 'http.server' + ); + }); + + request.get('/api/foo/success-api-route').catch(e => { + // we don't care about crashes + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + runtime: { name: 'node', version: expect.any(String) }, + trace: { + data: { + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + platform: 'node', + request: { + headers: expect.any(Object), + method: 'GET', + url: expect.stringMatching(/^http.*\/api\/foo\/success-api-route$/), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'GET /api/[param]/success-api-route', + transaction_info: { source: 'route' }, + type: 'transaction', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts new file mode 100644 index 000000000000..600e2b6eda53 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Should capture an error thrown in a server component', async ({ page }) => { + const errorEventPromise = waitForError('nextjs-13', errorEvent => { + return errorEvent.exception?.values?.[0].value === 'RSC error'; + }); + + await page.goto('/rsc-error'); + + expect(await errorEventPromise).toMatchObject({ + contexts: { + runtime: { name: 'node', version: expect.any(String) }, + trace: { + parent_span_id: expect.any(String), + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + event_id: expect.any(String), + exception: { + values: [ + { + mechanism: { handled: false, type: 'generic' }, + type: 'Error', + value: 'RSC error', + }, + ], + }, + modules: { next: '13.2.0' }, + platform: 'node', + request: { + cookies: expect.any(Object), + headers: expect.any(Object), + }, + timestamp: expect.any(Number), + transaction: 'Page Server Component (/rsc-error)', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/utils/throw.js b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/utils/throw.js new file mode 100644 index 000000000000..0e37a4135be4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/utils/throw.js @@ -0,0 +1 @@ +throw new Error('I am throwing'); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts new file mode 100644 index 000000000000..27bf728b42a5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts @@ -0,0 +1,60 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +const cases = [ + { + name: 'wrappedNoParamURL', + url: `/api/no-params`, + transactionName: 'GET /api/no-params', + }, + { + name: 'wrappedDynamicURL', + url: `/api/dog`, + transactionName: 'GET /api/[param]', + }, + { + name: 'wrappedCatchAllURL', + url: `/api/params/dog/bug`, + transactionName: 'GET /api/params/[...pathParts]', + }, +]; + +cases.forEach(({ name, url, transactionName }) => { + test(`Should capture transactions for routes with various shapes (${name})`, async ({ request }) => { + const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { + console.log({ t: transactionEvent.transaction }); + return transactionEvent.transaction === transactionName && transactionEvent.contexts?.trace?.op === 'http.server'; + }); + + request.get(url).catch(() => { + // we don't care about crashes + }); + + expect(await transactionEventPromise).toMatchObject({ + contexts: { + trace: { + data: { + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.nextjs', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.nextjs', + span_id: expect.any(String), + status: 'ok', + trace_id: expect.any(String), + }, + }, + platform: 'node', + request: { + url: expect.stringContaining(url), + }, + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: transactionName, + transaction_info: { source: 'route' }, + type: 'transaction', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-13/tsconfig.json new file mode 100644 index 000000000000..ef9e351d7a7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ], + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"], + "exclude": ["node_modules", "playwright.config.ts"] +} diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index a6f0822da0fa..01395202d990 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -7,7 +7,7 @@ import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import type { Envelope, EnvelopeItem, Event, SerializedSession } from '@sentry/types'; import { parseEnvelope } from '@sentry/utils'; const readFile = util.promisify(fs.readFile); @@ -368,7 +368,7 @@ export function waitForEnvelopeItem( /** Wait for an error to be sent. */ export function waitForError( proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, + callback: (errorEvent: Event) => Promise | boolean, ): Promise { const timestamp = Date.now(); return new Promise((resolve, reject) => { @@ -387,6 +387,28 @@ export function waitForError( }); } +/** Wait for an session to be sent. */ +export function waitForSession( + proxyServerName: string, + callback: (session: SerializedSession) => Promise | boolean, +): Promise { + const timestamp = Date.now(); + return new Promise((resolve, reject) => { + waitForEnvelopeItem( + proxyServerName, + async envelopeItem => { + const [envelopeItemHeader, envelopeItemBody] = envelopeItem; + if (envelopeItemHeader.type === 'session' && (await callback(envelopeItemBody as SerializedSession))) { + resolve(envelopeItemBody as SerializedSession); + return true; + } + return false; + }, + timestamp, + ).catch(reject); + }); +} + /** Wait for a transaction to be sent. */ export function waitForTransaction( proxyServerName: string, diff --git a/dev-packages/test-utils/src/index.ts b/dev-packages/test-utils/src/index.ts index 49685a6b18c2..6cfbe61d9306 100644 --- a/dev-packages/test-utils/src/index.ts +++ b/dev-packages/test-utils/src/index.ts @@ -5,6 +5,7 @@ export { waitForError, waitForRequest, waitForTransaction, + waitForSession, waitForPlainRequest, } from './event-proxy-server'; diff --git a/dev-packages/test-utils/src/playwright-config.ts b/dev-packages/test-utils/src/playwright-config.ts index a48ca969ad06..da2a10d0b477 100644 --- a/dev-packages/test-utils/src/playwright-config.ts +++ b/dev-packages/test-utils/src/playwright-config.ts @@ -22,7 +22,7 @@ export function getPlaywrightConfig( const config: PlaywrightTestConfig = { testDir: './tests', /* Maximum time one test can run for. */ - timeout: 150_000, + timeout: 30_000, expect: { /** * Maximum time expect() should wait for the condition to be met. From 1320f2d52a89c50bba6772744a9577dd6f1619ad Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Thu, 1 Aug 2024 15:28:49 +0200 Subject: [PATCH 06/14] test(nextjs): Remove Next.js integration tests (#13155) --- .github/workflows/build.yml | 51 ------- .../test-applications/nextjs-13/package.json | 6 +- .../pages/api/endpoint-excluded-with-regex.ts | 4 +- .../api/endpoint-excluded-with-string.ts | 4 +- .../tests/server/cjs-api-endpoints.test.ts | 8 ++ .../server/excluded-api-endpoints.test.ts | 4 +- .../server/server-component-error.test.ts | 2 - .../server/wrapApiHandlerWithSentry.test.ts | 1 - packages/nextjs/.eslintrc.js | 1 - packages/nextjs/jest.config.js | 1 - packages/nextjs/package.json | 6 +- packages/nextjs/playwright.config.ts | 23 ---- packages/nextjs/test/integration/.gitignore | 39 ------ .../app/clientcomponent/layout.tsx | 3 - .../integration/app/clientcomponent/page.tsx | 5 - .../nextjs/test/integration/app/layout.tsx | 7 - .../app/servercomponent/layout.tsx | 3 - .../integration/app/servercomponent/page.tsx | 8 -- .../test/integration/components/Layout.tsx | 44 ------ .../test/integration/components/List.tsx | 20 --- .../integration/components/ListDetail.tsx | 17 --- .../test/integration/components/ListItem.tsx | 19 --- .../test/integration/instrumentation.ts | 21 --- .../test/integration/interfaces/index.ts | 10 -- .../nextjs/test/integration/jest.config.js | 11 -- .../nextjs/test/integration/jest.setup.js | 8 -- .../nextjs/test/integration/next-env.d.ts | 5 - .../nextjs/test/integration/next.config.js | 16 --- .../integration/next13.appdir.config.template | 16 --- .../test/integration/next13.config.template | 13 -- packages/nextjs/test/integration/package.json | 41 ------ .../integration/pages/[id]/errorClick.tsx | 11 -- .../pages/[id]/withInitialProps.tsx | 17 --- .../pages/[id]/withServerSideProps.tsx | 17 --- .../nextjs/test/integration/pages/_app.tsx | 18 --- .../test/integration/pages/_document.tsx | 18 --- .../nextjs/test/integration/pages/about.tsx | 3 - .../test/integration/pages/alsoHealthy.tsx | 10 -- .../integration/pages/api/broken/index.ts | 7 - .../pages/api/doubleEndMethodOnVercel.ts | 9 -- .../test/integration/pages/api/error/index.ts | 7 - .../excludedEndpoints/excludedWithRegExp.tsx | 6 - .../excludedEndpoints/excludedWithString.tsx | 6 - .../test/integration/pages/api/http/index.ts | 5 - .../test/integration/pages/api/requireTest.ts | 14 -- .../test/integration/pages/api/users/index.ts | 17 --- .../unwrapped/[...pathParts].ts | 7 - .../unwrapped/[animal].ts | 7 - .../unwrapped/cjsExport.ts | 7 - .../unwrapped/noParams.ts | 7 - .../wrapped/[...pathParts].ts | 7 - .../wrapped/[animal].ts | 7 - .../wrapped/cjsExport.ts | 7 - .../wrapped/noParams.ts | 7 - .../nextjs/test/integration/pages/crashed.tsx | 15 --- .../pages/customPageExtension.page.tsx | 12 -- .../test/integration/pages/errorClick.tsx | 11 -- .../pages/faultyAppGetInitialProps.tsx | 4 - .../nextjs/test/integration/pages/fetch.tsx | 14 -- .../nextjs/test/integration/pages/healthy.tsx | 10 -- .../nextjs/test/integration/pages/index.tsx | 3 - .../test/integration/pages/reportDialog.tsx | 14 -- ...unmatchedCustomPageExtension.someExtension | 3 - .../test/integration/pages/users/[id].tsx | 57 -------- .../test/integration/pages/users/index.tsx | 39 ------ .../pages/withErrorServerSideProps.tsx | 7 - .../test/integration/sentry.client.config.js | 7 - .../src/pages/someNamedComponent.tsx | 6 - .../test/integration/test/.eslintrc.json | 6 - ...pDirTracingPageloadClientcomponent.test.ts | 26 ---- ...pDirTracingPageloadServercomponent.test.ts | 26 ---- .../test/client/errorClick.test.ts | 59 -------- .../test/client/errorGlobal.test.ts | 12 -- ...ltyAppGetInitialPropsConfiguration.test.ts | 13 -- .../test/client/reportDialog.test.ts | 14 -- .../test/client/sessionCrashed.test.ts | 21 --- .../test/client/sessionHealthy.test.ts | 15 --- .../test/client/sessionNavigate.test.ts | 56 -------- .../tracingClientGetInitialProps.test.ts | 37 ----- .../tracingClientGetServerSideProps.test.ts | 38 ------ .../test/client/tracingDynamicRoute.test.ts | 22 --- .../test/client/tracingFetch.test.ts | 58 -------- .../test/client/tracingNavigate.test.ts | 60 --------- .../test/client/tracingPageLoad.test.ts | 20 --- .../integration/test/client/utils/helpers.ts | 1 - .../test/server/cjsApiEndpoints.test.ts | 108 --------------- .../server/doubleEndMethodOnVercel.test.ts | 14 -- .../test/server/errorApiEndpoint.test.ts | 66 --------- .../test/server/errorServerSideProps.test.ts | 66 --------- .../test/server/excludedApiEndpoints.test.ts | 29 ---- .../test/server/serverComponent.test.ts | 28 ---- .../test/server/tracing200.test.ts | 43 ------ .../test/server/tracing500.test.ts | 43 ------ .../test/server/tracingHttp.test.ts | 43 ------ .../tracingServerGetInitialProps.test.ts | 40 ------ .../tracingServerGetServerSideProps.test.ts | 40 ------ ...ServerSidePropsCustomPageExtension.test.ts | 40 ------ .../test/server/tracingWithSentryAPI.test.ts | 64 --------- .../integration/test/server/utils/helpers.ts | 67 --------- .../integration/test/server/utils/throw.js | 1 - .../test/integration/test/tsconfig.json | 4 - .../nextjs/test/integration/tsconfig.json | 30 ----- .../test/integration/tsconfig.test.json | 10 -- .../test/integration/utils/sample-data.ts | 9 -- .../nextjs/test/integration_test_utils.sh | 79 ----------- packages/nextjs/test/run-integration-tests.sh | 123 ----------------- .../vercel/install-sentry-from-branch.sh | 127 ------------------ packages/nextjs/vercel/instructions.md | 53 -------- .../vercel/make-project-use-current-branch.sh | 63 --------- packages/nextjs/vercel/post-app-build.sh | 16 --- .../vercel/set-up-branch-for-test-app-use.sh | 59 -------- 111 files changed, 16 insertions(+), 2573 deletions(-) delete mode 100644 packages/nextjs/playwright.config.ts delete mode 100644 packages/nextjs/test/integration/.gitignore delete mode 100644 packages/nextjs/test/integration/app/clientcomponent/layout.tsx delete mode 100644 packages/nextjs/test/integration/app/clientcomponent/page.tsx delete mode 100644 packages/nextjs/test/integration/app/layout.tsx delete mode 100644 packages/nextjs/test/integration/app/servercomponent/layout.tsx delete mode 100644 packages/nextjs/test/integration/app/servercomponent/page.tsx delete mode 100644 packages/nextjs/test/integration/components/Layout.tsx delete mode 100644 packages/nextjs/test/integration/components/List.tsx delete mode 100644 packages/nextjs/test/integration/components/ListDetail.tsx delete mode 100644 packages/nextjs/test/integration/components/ListItem.tsx delete mode 100644 packages/nextjs/test/integration/instrumentation.ts delete mode 100644 packages/nextjs/test/integration/interfaces/index.ts delete mode 100644 packages/nextjs/test/integration/jest.config.js delete mode 100644 packages/nextjs/test/integration/jest.setup.js delete mode 100644 packages/nextjs/test/integration/next-env.d.ts delete mode 100644 packages/nextjs/test/integration/next.config.js delete mode 100644 packages/nextjs/test/integration/next13.appdir.config.template delete mode 100644 packages/nextjs/test/integration/next13.config.template delete mode 100644 packages/nextjs/test/integration/package.json delete mode 100644 packages/nextjs/test/integration/pages/[id]/errorClick.tsx delete mode 100644 packages/nextjs/test/integration/pages/[id]/withInitialProps.tsx delete mode 100644 packages/nextjs/test/integration/pages/[id]/withServerSideProps.tsx delete mode 100644 packages/nextjs/test/integration/pages/_app.tsx delete mode 100644 packages/nextjs/test/integration/pages/_document.tsx delete mode 100644 packages/nextjs/test/integration/pages/about.tsx delete mode 100644 packages/nextjs/test/integration/pages/alsoHealthy.tsx delete mode 100644 packages/nextjs/test/integration/pages/api/broken/index.ts delete mode 100644 packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts delete mode 100644 packages/nextjs/test/integration/pages/api/error/index.ts delete mode 100644 packages/nextjs/test/integration/pages/api/excludedEndpoints/excludedWithRegExp.tsx delete mode 100644 packages/nextjs/test/integration/pages/api/excludedEndpoints/excludedWithString.tsx delete mode 100644 packages/nextjs/test/integration/pages/api/http/index.ts delete mode 100644 packages/nextjs/test/integration/pages/api/requireTest.ts delete mode 100644 packages/nextjs/test/integration/pages/api/users/index.ts delete mode 100644 packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/[...pathParts].ts delete mode 100644 packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/[animal].ts delete mode 100644 packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/cjsExport.ts delete mode 100644 packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/noParams.ts delete mode 100644 packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/[...pathParts].ts delete mode 100644 packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/[animal].ts delete mode 100644 packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/cjsExport.ts delete mode 100644 packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/noParams.ts delete mode 100644 packages/nextjs/test/integration/pages/crashed.tsx delete mode 100644 packages/nextjs/test/integration/pages/customPageExtension.page.tsx delete mode 100644 packages/nextjs/test/integration/pages/errorClick.tsx delete mode 100644 packages/nextjs/test/integration/pages/faultyAppGetInitialProps.tsx delete mode 100644 packages/nextjs/test/integration/pages/fetch.tsx delete mode 100644 packages/nextjs/test/integration/pages/healthy.tsx delete mode 100644 packages/nextjs/test/integration/pages/index.tsx delete mode 100644 packages/nextjs/test/integration/pages/reportDialog.tsx delete mode 100644 packages/nextjs/test/integration/pages/unmatchedCustomPageExtension.someExtension delete mode 100644 packages/nextjs/test/integration/pages/users/[id].tsx delete mode 100644 packages/nextjs/test/integration/pages/users/index.tsx delete mode 100644 packages/nextjs/test/integration/pages/withErrorServerSideProps.tsx delete mode 100644 packages/nextjs/test/integration/sentry.client.config.js delete mode 100644 packages/nextjs/test/integration/src/pages/someNamedComponent.tsx delete mode 100644 packages/nextjs/test/integration/test/.eslintrc.json delete mode 100644 packages/nextjs/test/integration/test/client/appDirTracingPageloadClientcomponent.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/appDirTracingPageloadServercomponent.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/errorClick.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/errorGlobal.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/faultyAppGetInitialPropsConfiguration.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/reportDialog.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/sessionCrashed.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/sessionHealthy.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/sessionNavigate.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/tracingClientGetInitialProps.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/tracingClientGetServerSideProps.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/tracingDynamicRoute.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/tracingFetch.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/tracingNavigate.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/tracingPageLoad.test.ts delete mode 100644 packages/nextjs/test/integration/test/client/utils/helpers.ts delete mode 100644 packages/nextjs/test/integration/test/server/cjsApiEndpoints.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/doubleEndMethodOnVercel.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/errorApiEndpoint.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/errorServerSideProps.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/excludedApiEndpoints.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/serverComponent.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/tracing200.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/tracing500.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/tracingHttp.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/tracingServerGetInitialProps.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/tracingServerGetServerSideProps.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/tracingServerGetServerSidePropsCustomPageExtension.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/tracingWithSentryAPI.test.ts delete mode 100644 packages/nextjs/test/integration/test/server/utils/helpers.ts delete mode 100644 packages/nextjs/test/integration/test/server/utils/throw.js delete mode 100644 packages/nextjs/test/integration/test/tsconfig.json delete mode 100644 packages/nextjs/test/integration/tsconfig.json delete mode 100644 packages/nextjs/test/integration/tsconfig.test.json delete mode 100644 packages/nextjs/test/integration/utils/sample-data.ts delete mode 100644 packages/nextjs/test/integration_test_utils.sh delete mode 100755 packages/nextjs/test/run-integration-tests.sh delete mode 100644 packages/nextjs/vercel/install-sentry-from-branch.sh delete mode 100644 packages/nextjs/vercel/instructions.md delete mode 100644 packages/nextjs/vercel/make-project-use-current-branch.sh delete mode 100644 packages/nextjs/vercel/post-app-build.sh delete mode 100644 packages/nextjs/vercel/set-up-branch-for-test-app-use.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76d2ade83d15..9afeaec5947a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -595,56 +595,6 @@ jobs: - name: Unit Test run: yarn lerna run test --scope @sentry/profiling-node - job_nextjs_integration_test: - name: Nextjs (Node ${{ matrix.node }}) Tests - needs: [job_get_metadata, job_build] - if: needs.job_get_metadata.outputs.changed_nextjs == 'true' || github.event_name != 'pull_request' - timeout-minutes: 25 - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - node: [14, 16, 18, 20, 22] - steps: - - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) - uses: actions/checkout@v4 - with: - ref: ${{ env.HEAD_COMMIT }} - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node }} - - name: Restore caches - uses: ./.github/actions/restore-cache - env: - DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Get npm cache directory - id: npm-cache-dir - run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT - - name: Get Playwright version - id: playwright-version - run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> $GITHUB_OUTPUT - - uses: actions/cache@v4 - name: Check if Playwright browser is cached - id: playwright-cache - with: - path: ${{ steps.npm-cache-dir.outputs.dir }} - key: ${{ runner.os }}-Playwright-${{steps.playwright-version.outputs.version}} - - name: Install Playwright browser if not cached - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npx playwright install --with-deps - env: - PLAYWRIGHT_BROWSERS_PATH: ${{steps.npm-cache-dir.outputs.dir}} - - name: Install OS dependencies of Playwright if cache hit - if: steps.playwright-cache.outputs.cache-hit == 'true' - run: npx playwright install-deps - - name: Run tests - env: - NODE_VERSION: ${{ matrix.node }} - run: | - cd packages/nextjs - yarn test:integration - job_browser_playwright_tests: name: Playwright (${{ matrix.bundle }}${{ matrix.shard && format(' {0}/{1}', matrix.shard, matrix.shards) || ''}}) Tests needs: [job_get_metadata, job_build] @@ -1328,7 +1278,6 @@ jobs: job_deno_unit_tests, job_node_unit_tests, job_profiling_node_unit_tests, - job_nextjs_integration_test, job_node_integration_tests, job_browser_playwright_tests, job_browser_loader_tests, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json index 3f1219f210a2..5be9ecbfc32c 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/package.json @@ -24,17 +24,17 @@ }, "devDependencies": { "@playwright/test": "^1.44.1", - "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry-internal/browser-utils": "latest || *", "@sentry-internal/feedback": "latest || *", + "@sentry-internal/replay": "latest || *", "@sentry-internal/replay-canvas": "latest || *", - "@sentry-internal/browser-utils": "latest || *", + "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/browser": "latest || *", "@sentry/core": "latest || *", "@sentry/nextjs": "latest || *", "@sentry/node": "latest || *", "@sentry/opentelemetry": "latest || *", "@sentry/react": "latest || *", - "@sentry-internal/replay": "latest || *", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", "@sentry/vercel-edge": "latest || *" diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-regex.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-regex.ts index 6ae521fa5cb4..5bb9ddca1270 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-regex.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-regex.ts @@ -1,7 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { +export default async (_req: NextApiRequest, res: NextApiResponse): Promise => { res.status(200).json({ success: true }); }; - -module.exports = handler; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-string.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-string.ts index 6ae521fa5cb4..5bb9ddca1270 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-string.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/pages/api/endpoint-excluded-with-string.ts @@ -1,7 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next'; -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { +export default async (_req: NextApiRequest, res: NextApiResponse): Promise => { res.status(200).json({ success: true }); }; - -module.exports = handler; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts index 8a0ed1176142..8ef79f8cf146 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/cjs-api-endpoints.test.ts @@ -1,7 +1,13 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +const packageJson = require('../../package.json'); +const nextjsVersion = packageJson.dependencies.next; +const nextjsMajor = Number(nextjsVersion.split('.')[0]); + test('should create a transaction for a CJS pages router API endpoint', async ({ request }) => { + test.skip(nextjsMajor > 13, 'Next.js does not like CJS routes after a certain point.'); + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { return ( transactionEvent.transaction === 'GET /api/cjs-api-endpoint' && @@ -62,6 +68,8 @@ test('should create a transaction for a CJS pages router API endpoint', async ({ }); test('should not mess up require statements in CJS API endpoints', async ({ request }) => { + test.skip(nextjsMajor > 13, 'Next.js does not like CJS routes after a certain point.'); + const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => { return ( transactionEvent.transaction === 'GET /api/cjs-api-endpoint-with-require' && diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts index b148accf1450..63082fee6e07 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/excluded-api-endpoints.test.ts @@ -11,7 +11,7 @@ test('should not automatically create transactions for routes that were excluded ); }); - await (await request.get(`/api/endpoint-excluded-with-string`)).json(); + expect(await (await request.get(`/api/endpoint-excluded-with-string`)).text()).toBe('{"success":true}'); let transactionPromiseReceived = false; transactionPromise.then(() => { @@ -33,7 +33,7 @@ test('should not automatically create transactions for routes that were excluded ); }); - await (await request.get(`/api/endpoint-excluded-with-regex`)).json(); + expect(await (await request.get(`/api/endpoint-excluded-with-regex`)).text()).toBe('{"success":true}'); let transactionPromiseReceived = false; transactionPromise.then(() => { diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts index 600e2b6eda53..0303fe6e583f 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/server-component-error.test.ts @@ -12,7 +12,6 @@ test('Should capture an error thrown in a server component', async ({ page }) => contexts: { runtime: { name: 'node', version: expect.any(String) }, trace: { - parent_span_id: expect.any(String), span_id: expect.any(String), trace_id: expect.any(String), }, @@ -27,7 +26,6 @@ test('Should capture an error thrown in a server component', async ({ page }) => }, ], }, - modules: { next: '13.2.0' }, platform: 'node', request: { cookies: expect.any(Object), diff --git a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts index 27bf728b42a5..6b25cb6f74f7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/wrapApiHandlerWithSentry.test.ts @@ -22,7 +22,6 @@ const cases = [ cases.forEach(({ name, url, transactionName }) => { test(`Should capture transactions for routes with various shapes (${name})`, async ({ request }) => { const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => { - console.log({ t: transactionEvent.transaction }); return transactionEvent.transaction === transactionName && transactionEvent.contexts?.trace?.op === 'http.server'; }); diff --git a/packages/nextjs/.eslintrc.js b/packages/nextjs/.eslintrc.js index 885839f52e18..95ce15bc668f 100644 --- a/packages/nextjs/.eslintrc.js +++ b/packages/nextjs/.eslintrc.js @@ -6,7 +6,6 @@ module.exports = { parserOptions: { jsx: true, }, - ignorePatterns: ['test/integration/**', 'playwright.config.ts'], extends: ['../../.eslintrc.js'], rules: { '@sentry-internal/sdk/no-optional-chaining': 'off', diff --git a/packages/nextjs/jest.config.js b/packages/nextjs/jest.config.js index edaa219fa7be..fd23311e1656 100644 --- a/packages/nextjs/jest.config.js +++ b/packages/nextjs/jest.config.js @@ -2,5 +2,4 @@ const baseConfig = require('../../jest/jest.config.js'); module.exports = { ...baseConfig, - testPathIgnorePatterns: ['/test/integration/'], }; diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index d44d5872ff08..1a651c7c6e43 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -115,12 +115,8 @@ "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", "test": "yarn test:unit", - "test:all": "run-s test:unit test:integration", + "test:all": "run-s test:unit", "test:unit": "jest", - "test:integration": "./test/run-integration-tests.sh && yarn test:types", - "test:integration:clean": "(cd test/integration && rimraf .cache node_modules build)", - "test:integration:client": "yarn playwright test test/integration/test/client/", - "test:integration:server": "(cd test/integration && yarn test:server)", "test:types": "cd test/types && yarn test", "test:watch": "jest --watch", "vercel:branch": "source vercel/set-up-branch-for-test-app-use.sh", diff --git a/packages/nextjs/playwright.config.ts b/packages/nextjs/playwright.config.ts deleted file mode 100644 index 97dfdd760da1..000000000000 --- a/packages/nextjs/playwright.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as path from 'path'; -import type { PlaywrightTestConfig } from '@playwright/test'; - -const config: PlaywrightTestConfig = { - retries: 0, // We do not accept flakes. - use: { - baseURL: 'http://localhost:3000', - }, - // Run tests inside of a single file in parallel - fullyParallel: true, - // Use 3 workers on CI, else use defaults (based on available CPU cores) - // Note that 3 is a random number selected to work well with our CI setup - workers: process.env.CI ? 3 : undefined, - webServer: { - cwd: path.join(__dirname, 'test', 'integration'), - command: 'yarn start', - port: 3000, - stdout: 'pipe', - stderr: 'pipe', - }, -}; - -export default config; diff --git a/packages/nextjs/test/integration/.gitignore b/packages/nextjs/test/integration/.gitignore deleted file mode 100644 index b302c94ed40c..000000000000 --- a/packages/nextjs/test/integration/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -yarn.lock -package-lock.json - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env.local -.env.development.local -.env.test.local -.env.production.local - -# vercel -.vercel - -# Generated by Next.js 13 -.vscode diff --git a/packages/nextjs/test/integration/app/clientcomponent/layout.tsx b/packages/nextjs/test/integration/app/clientcomponent/layout.tsx deleted file mode 100644 index 71beffba2865..000000000000 --- a/packages/nextjs/test/integration/app/clientcomponent/layout.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ({ children }: { children: React.ReactNode }) { - return <>{children}; -} diff --git a/packages/nextjs/test/integration/app/clientcomponent/page.tsx b/packages/nextjs/test/integration/app/clientcomponent/page.tsx deleted file mode 100644 index a1d45b38a939..000000000000 --- a/packages/nextjs/test/integration/app/clientcomponent/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -'use client'; - -export default function () { - return

I am a client component!

; -} diff --git a/packages/nextjs/test/integration/app/layout.tsx b/packages/nextjs/test/integration/app/layout.tsx deleted file mode 100644 index c8f9cee0b787..000000000000 --- a/packages/nextjs/test/integration/app/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Layout({ children }: { children: React.ReactNode }) { - return ( - - {children} - - ); -} diff --git a/packages/nextjs/test/integration/app/servercomponent/layout.tsx b/packages/nextjs/test/integration/app/servercomponent/layout.tsx deleted file mode 100644 index 71beffba2865..000000000000 --- a/packages/nextjs/test/integration/app/servercomponent/layout.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ({ children }: { children: React.ReactNode }) { - return <>{children}; -} diff --git a/packages/nextjs/test/integration/app/servercomponent/page.tsx b/packages/nextjs/test/integration/app/servercomponent/page.tsx deleted file mode 100644 index e3c4e8d06f61..000000000000 --- a/packages/nextjs/test/integration/app/servercomponent/page.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import * as Sentry from '@sentry/nextjs'; - -export default async function () { - // do some request so that next will render this component serverside for each new pageload - await fetch('http://example.com', { cache: 'no-store' }); - Sentry.captureException(new Error('I am an Error captured inside a server component')); - return

I am a server component!

; -} diff --git a/packages/nextjs/test/integration/components/Layout.tsx b/packages/nextjs/test/integration/components/Layout.tsx deleted file mode 100644 index 36b99f04720f..000000000000 --- a/packages/nextjs/test/integration/components/Layout.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import Head from 'next/head'; -import Link from 'next/link'; -import { ReactNode } from 'react'; - -type Props = { - children?: ReactNode; - title?: string; -}; - -const Layout = ({ children, title = 'This is the default title' }: Props) => ( -
- - {title} - - - -
- -
- {children} -
-
- Im here to stay (Footer) -
-
-); - -export default Layout; diff --git a/packages/nextjs/test/integration/components/List.tsx b/packages/nextjs/test/integration/components/List.tsx deleted file mode 100644 index fbcdfeb504c9..000000000000 --- a/packages/nextjs/test/integration/components/List.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX -import * as React from 'react'; -import { User } from '../interfaces'; -import ListItem from './ListItem'; - -type Props = { - items: User[]; -}; - -const List = ({ items }: Props) => ( -
    - {items.map(item => ( -
  • - -
  • - ))} -
-); - -export default List; diff --git a/packages/nextjs/test/integration/components/ListDetail.tsx b/packages/nextjs/test/integration/components/ListDetail.tsx deleted file mode 100644 index 9b1417333c9f..000000000000 --- a/packages/nextjs/test/integration/components/ListDetail.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX -import * as React from 'react'; - -import { User } from '../interfaces'; - -type ListDetailProps = { - item: User; -}; - -const ListDetail = ({ item: user }: ListDetailProps) => ( -
-

Detail for {user.name}

-

ID: {user.id}

-
-); - -export default ListDetail; diff --git a/packages/nextjs/test/integration/components/ListItem.tsx b/packages/nextjs/test/integration/components/ListItem.tsx deleted file mode 100644 index 9de97d32c7fb..000000000000 --- a/packages/nextjs/test/integration/components/ListItem.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Link from 'next/link'; -// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX -import React from 'react'; - -import { User } from '../interfaces'; - -type Props = { - data: User; -}; - -const ListItem = ({ data }: Props) => ( - - - {data.id}: {data.name} - - -); - -export default ListItem; diff --git a/packages/nextjs/test/integration/instrumentation.ts b/packages/nextjs/test/integration/instrumentation.ts deleted file mode 100644 index d3bf16c5b957..000000000000 --- a/packages/nextjs/test/integration/instrumentation.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Sentry from '@sentry/nextjs'; - -export function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - tracesSampleRate: 1.0, - debug: !!process.env.SDK_DEBUG, - integrations: defaults => [ - ...defaults.filter( - integration => - // filter out `Console` since the tests are happening in the console and we don't need to record what's printed - // there, because we can see it (this makes debug logging much less noisy, since intercepted events which are - // printed to the console no longer create console breadcrumbs, which then get printed, creating even longer - // console breadcrumbs, which get printed, etc, etc) - integration.name !== 'Console', - ), - ], - }); - } -} diff --git a/packages/nextjs/test/integration/interfaces/index.ts b/packages/nextjs/test/integration/interfaces/index.ts deleted file mode 100644 index 0a4f06952a9c..000000000000 --- a/packages/nextjs/test/integration/interfaces/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// You can include shared interfaces/types in a separate file -// and then use them in any component by importing them. For -// example, to import the interface below do: -// -// import { User } from 'path/to/interfaces'; - -export type User = { - id: number; - name: string; -}; diff --git a/packages/nextjs/test/integration/jest.config.js b/packages/nextjs/test/integration/jest.config.js deleted file mode 100644 index e9df7b66dc9f..000000000000 --- a/packages/nextjs/test/integration/jest.config.js +++ /dev/null @@ -1,11 +0,0 @@ -const baseConfig = require('../../jest.config.js'); - -module.exports = { - ...baseConfig, - testMatch: [`${__dirname}/test/server/**/*.test.ts`], - testPathIgnorePatterns: [`${__dirname}/test/client`], - forceExit: true, - testTimeout: 30000, - setupFilesAfterEnv: [`${__dirname}/jest.setup.js`], - collectCoverage: false, -}; diff --git a/packages/nextjs/test/integration/jest.setup.js b/packages/nextjs/test/integration/jest.setup.js deleted file mode 100644 index 6360e753c4a5..000000000000 --- a/packages/nextjs/test/integration/jest.setup.js +++ /dev/null @@ -1,8 +0,0 @@ -global.console = { - ...console, - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - // console.debug is available -}; diff --git a/packages/nextjs/test/integration/next-env.d.ts b/packages/nextjs/test/integration/next-env.d.ts deleted file mode 100644 index 4f11a03dc6cc..000000000000 --- a/packages/nextjs/test/integration/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/nextjs/test/integration/next.config.js b/packages/nextjs/test/integration/next.config.js deleted file mode 100644 index e9e4e4e04b2e..000000000000 --- a/packages/nextjs/test/integration/next.config.js +++ /dev/null @@ -1,16 +0,0 @@ -const { withSentryConfig } = require('@sentry/nextjs'); - -const moduleExports = { - eslint: { - ignoreDuringBuilds: true, - }, - experimental: { - appDir: Number(process.env.NODE_MAJOR) >= 16, // experimental.appDir requires Node v16.8.0 or later. - }, - pageExtensions: ['jsx', 'js', 'tsx', 'ts', 'page.tsx'], -}; - -module.exports = withSentryConfig(moduleExports, { - debug: true, - excludeServerRoutes: ['/api/excludedEndpoints/excludedWithString', /\/api\/excludedEndpoints\/excludedWithRegExp/], -}); diff --git a/packages/nextjs/test/integration/next13.appdir.config.template b/packages/nextjs/test/integration/next13.appdir.config.template deleted file mode 100644 index e9e4e4e04b2e..000000000000 --- a/packages/nextjs/test/integration/next13.appdir.config.template +++ /dev/null @@ -1,16 +0,0 @@ -const { withSentryConfig } = require('@sentry/nextjs'); - -const moduleExports = { - eslint: { - ignoreDuringBuilds: true, - }, - experimental: { - appDir: Number(process.env.NODE_MAJOR) >= 16, // experimental.appDir requires Node v16.8.0 or later. - }, - pageExtensions: ['jsx', 'js', 'tsx', 'ts', 'page.tsx'], -}; - -module.exports = withSentryConfig(moduleExports, { - debug: true, - excludeServerRoutes: ['/api/excludedEndpoints/excludedWithString', /\/api\/excludedEndpoints\/excludedWithRegExp/], -}); diff --git a/packages/nextjs/test/integration/next13.config.template b/packages/nextjs/test/integration/next13.config.template deleted file mode 100644 index 815eba98e889..000000000000 --- a/packages/nextjs/test/integration/next13.config.template +++ /dev/null @@ -1,13 +0,0 @@ -const { withSentryConfig } = require('@sentry/nextjs'); - -const moduleExports = { - eslint: { - ignoreDuringBuilds: true, - }, - pageExtensions: ['jsx', 'js', 'tsx', 'ts', 'page.tsx'], -}; - -module.exports = withSentryConfig(moduleExports, { - debug: true, - excludeServerRoutes: ['/api/excludedEndpoints/excludedWithString', /\/api\/excludedEndpoints\/excludedWithRegExp/], -}); diff --git a/packages/nextjs/test/integration/package.json b/packages/nextjs/test/integration/package.json deleted file mode 100644 index f4c547b5b687..000000000000 --- a/packages/nextjs/test/integration/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "with-typescript", - "license": "MIT", - "scripts": { - "dev": "next", - "build": "next build", - "predebug": "source ../integration_test_utils.sh && link_monorepo_packages '../../..' && yarn build", - "start": "next start", - "pretest": "yarn build", - "test:client": "playwright test", - "test:server": "jest --forceExit --runInBand" - }, - "dependencies": { - "@sentry/nextjs": "file:../../", - "next": "latest", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/node": "^15.3.1", - "@types/react": "17.0.47", - "@types/react-dom": "17.0.17", - "nock": "^13.1.0", - "typescript": "4.9.5", - "yargs": "^16.2.0" - }, - "resolutions": { - "@sentry/browser": "file:../../../browser", - "@sentry/core": "file:../../../core", - "@sentry/node": "file:../../../node", - "@sentry/opentelemetry": "file:../../../opentelemetry", - "@sentry/react": "file:../../../react", - "@sentry-internal/browser-utils": "file:../../../browser-utils", - "@sentry-internal/replay": "file:../../../replay-internal", - "@sentry-internal/replay-canvas": "file:../../../replay-canvas", - "@sentry-internal/feedback": "file:../../../feedback", - "@sentry/types": "file:../../../types", - "@sentry/utils": "file:../../../utils", - "@sentry/vercel-edge": "file:../../../vercel-edge" - } -} diff --git a/packages/nextjs/test/integration/pages/[id]/errorClick.tsx b/packages/nextjs/test/integration/pages/[id]/errorClick.tsx deleted file mode 100644 index df55a4b32967..000000000000 --- a/packages/nextjs/test/integration/pages/[id]/errorClick.tsx +++ /dev/null @@ -1,11 +0,0 @@ -const ButtonPage = (): JSX.Element => ( - -); - -export default ButtonPage; diff --git a/packages/nextjs/test/integration/pages/[id]/withInitialProps.tsx b/packages/nextjs/test/integration/pages/[id]/withInitialProps.tsx deleted file mode 100644 index 8fa2f228e90d..000000000000 --- a/packages/nextjs/test/integration/pages/[id]/withInitialProps.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import Link from 'next/link'; - -const WithInitialPropsPage = ({ data }: { data: string }) => ( - <> -

WithInitialPropsPage {data}

- {/* @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag */} - - Go to withServerSideProps - - -); - -WithInitialPropsPage.getInitialProps = () => { - return { data: '[some getInitialProps data]' }; -}; - -export default WithInitialPropsPage; diff --git a/packages/nextjs/test/integration/pages/[id]/withServerSideProps.tsx b/packages/nextjs/test/integration/pages/[id]/withServerSideProps.tsx deleted file mode 100644 index 420f88856656..000000000000 --- a/packages/nextjs/test/integration/pages/[id]/withServerSideProps.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import Link from 'next/link'; - -const WithServerSidePropsPage = ({ data }: { data: string }) => ( - <> -

WithServerSidePropsPage {data}

- {/* @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag */} - - Go to withInitialProps - - -); - -export async function getServerSideProps() { - return { props: { data: '[some getServerSideProps data]' } }; -} - -export default WithServerSidePropsPage; diff --git a/packages/nextjs/test/integration/pages/_app.tsx b/packages/nextjs/test/integration/pages/_app.tsx deleted file mode 100644 index da1ab1154f15..000000000000 --- a/packages/nextjs/test/integration/pages/_app.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import App, { AppContext, AppProps } from 'next/app'; - -const MyApp = ({ Component, pageProps }: AppProps) => { - return ; -}; - -MyApp.getInitialProps = async (appContext: AppContext) => { - // This simulates user misconfiguration. Users should always call `App.getInitialProps(appContext)`, but they don't, - // so we have a test for this so we don't break their apps. - if (appContext.ctx.pathname === '/faultyAppGetInitialProps') { - return {}; - } - - const appProps = await App.getInitialProps(appContext); - return { ...appProps }; -}; - -export default MyApp; diff --git a/packages/nextjs/test/integration/pages/_document.tsx b/packages/nextjs/test/integration/pages/_document.tsx deleted file mode 100644 index 9479e30f712b..000000000000 --- a/packages/nextjs/test/integration/pages/_document.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Document, { DocumentContext } from 'next/document'; - -class MyDocument extends Document { - static async getInitialProps(ctx: DocumentContext) { - // Verify that wrapping correctly passes `this` - this.testFunction(); - - const initialProps = await Document.getInitialProps(ctx); - - return initialProps; - } - - static testFunction() { - // noop - } -} - -export default MyDocument; diff --git a/packages/nextjs/test/integration/pages/about.tsx b/packages/nextjs/test/integration/pages/about.tsx deleted file mode 100644 index 30cfcf130c54..000000000000 --- a/packages/nextjs/test/integration/pages/about.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const AboutPage = () =>

About

; - -export default AboutPage; diff --git a/packages/nextjs/test/integration/pages/alsoHealthy.tsx b/packages/nextjs/test/integration/pages/alsoHealthy.tsx deleted file mode 100644 index 84eb4c60c4d6..000000000000 --- a/packages/nextjs/test/integration/pages/alsoHealthy.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Link from 'next/link'; - -const HealthyPage = (): JSX.Element => ( - // @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag - - Healthy - -); - -export default HealthyPage; diff --git a/packages/nextjs/test/integration/pages/api/broken/index.ts b/packages/nextjs/test/integration/pages/api/broken/index.ts deleted file mode 100644 index bc1bbcd5c241..000000000000 --- a/packages/nextjs/test/integration/pages/api/broken/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(500).json({ statusCode: 500, message: 'Something went wrong' }); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts b/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts deleted file mode 100644 index b0cfca8651be..000000000000 --- a/packages/nextjs/test/integration/pages/api/doubleEndMethodOnVercel.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - // This handler calls .end twice. We test this to verify that this still doesn't throw because we're wrapping `.end`. - res.status(200).json({ success: true }); - res.end(); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/error/index.ts b/packages/nextjs/test/integration/pages/api/error/index.ts deleted file mode 100644 index ce65576656de..000000000000 --- a/packages/nextjs/test/integration/pages/api/error/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, _res: NextApiResponse): Promise => { - throw new Error('API Error'); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/excludedEndpoints/excludedWithRegExp.tsx b/packages/nextjs/test/integration/pages/api/excludedEndpoints/excludedWithRegExp.tsx deleted file mode 100644 index 49099819c843..000000000000 --- a/packages/nextjs/test/integration/pages/api/excludedEndpoints/excludedWithRegExp.tsx +++ /dev/null @@ -1,6 +0,0 @@ -// This file will test the `excludeServerRoutes` option when a route is provided as a RegExp. -const handler = async (): Promise => { - throw new Error('API Error'); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/excludedEndpoints/excludedWithString.tsx b/packages/nextjs/test/integration/pages/api/excludedEndpoints/excludedWithString.tsx deleted file mode 100644 index 9e6bde70c490..000000000000 --- a/packages/nextjs/test/integration/pages/api/excludedEndpoints/excludedWithString.tsx +++ /dev/null @@ -1,6 +0,0 @@ -// This file will test the `excludeServerRoutes` option when a route is provided as a string. -const handler = async (): Promise => { - throw new Error('API Error'); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/http/index.ts b/packages/nextjs/test/integration/pages/api/http/index.ts deleted file mode 100644 index e5fe7f576723..000000000000 --- a/packages/nextjs/test/integration/pages/api/http/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -export default (_req: NextApiRequest, res: NextApiResponse) => { - res.status(200).json({}); -}; diff --git a/packages/nextjs/test/integration/pages/api/requireTest.ts b/packages/nextjs/test/integration/pages/api/requireTest.ts deleted file mode 100644 index 353dd632927e..000000000000 --- a/packages/nextjs/test/integration/pages/api/requireTest.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -if (process.env.NEXT_PUBLIC_SOME_FALSE_ENV_VAR === 'enabled') { - require('../../test/server/utils/throw'); // Should not throw unless the hoisting in the wrapping loader is messed up! -} - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - require('@sentry/nextjs').captureException; // Should not throw unless the wrapping loader messes up cjs imports - // @ts-expect-error - require.context('.'); // This is a webpack utility call. Should not throw unless the wrapping loader messes it up by mangling. - res.status(200).json({ success: true }); -}; - -module.exports = handler; diff --git a/packages/nextjs/test/integration/pages/api/users/index.ts b/packages/nextjs/test/integration/pages/api/users/index.ts deleted file mode 100644 index a46c4bd12879..000000000000 --- a/packages/nextjs/test/integration/pages/api/users/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -import { sampleUserData } from '../../../utils/sample-data'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - try { - if (!Array.isArray(sampleUserData)) { - throw new Error('Cannot find user data'); - } - - res.status(200).json(sampleUserData); - } catch (err) { - res.status(500).json({ statusCode: 500, message: (err as Error).message }); - } -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/[...pathParts].ts b/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/[...pathParts].ts deleted file mode 100644 index 3307b12037d5..000000000000 --- a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/[...pathParts].ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(200).json({}); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/[animal].ts b/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/[animal].ts deleted file mode 100644 index 3307b12037d5..000000000000 --- a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/[animal].ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(200).json({}); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/cjsExport.ts b/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/cjsExport.ts deleted file mode 100644 index 6ae521fa5cb4..000000000000 --- a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/cjsExport.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(200).json({ success: true }); -}; - -module.exports = handler; diff --git a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/noParams.ts b/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/noParams.ts deleted file mode 100644 index 3307b12037d5..000000000000 --- a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/unwrapped/noParams.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(200).json({}); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/[...pathParts].ts b/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/[...pathParts].ts deleted file mode 100644 index 3307b12037d5..000000000000 --- a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/[...pathParts].ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(200).json({}); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/[animal].ts b/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/[animal].ts deleted file mode 100644 index 3307b12037d5..000000000000 --- a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/[animal].ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(200).json({}); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/cjsExport.ts b/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/cjsExport.ts deleted file mode 100644 index 6ae521fa5cb4..000000000000 --- a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/cjsExport.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(200).json({ success: true }); -}; - -module.exports = handler; diff --git a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/noParams.ts b/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/noParams.ts deleted file mode 100644 index 3307b12037d5..000000000000 --- a/packages/nextjs/test/integration/pages/api/wrapApiHandlerWithSentry/wrapped/noParams.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { - res.status(200).json({}); -}; - -export default handler; diff --git a/packages/nextjs/test/integration/pages/crashed.tsx b/packages/nextjs/test/integration/pages/crashed.tsx deleted file mode 100644 index bddf97b5cb6c..000000000000 --- a/packages/nextjs/test/integration/pages/crashed.tsx +++ /dev/null @@ -1,15 +0,0 @@ -const CrashedPage = (): JSX.Element => { - // Magic to naively trigger onerror to make session crashed and allow for SSR - try { - if (typeof window !== 'undefined' && typeof window.onerror === 'function') { - // Lovely oldschool browsers syntax with 5 arguments <3 - // @ts-expect-error - window.onerror(null, null, null, null, new Error('Crashed')); - } - } catch (_e) { - // no-empty - } - return

Crashed

; -}; - -export default CrashedPage; diff --git a/packages/nextjs/test/integration/pages/customPageExtension.page.tsx b/packages/nextjs/test/integration/pages/customPageExtension.page.tsx deleted file mode 100644 index 302326cdbc96..000000000000 --- a/packages/nextjs/test/integration/pages/customPageExtension.page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -const BasicPage = (): JSX.Element => ( -

- This page simply exists to test the compatibility of Next.js' `pageExtensions` option with our auto wrapping - process. This file should be turned into a page by Next.js and our webpack loader should process it. -

-); - -export async function getServerSideProps() { - return { props: { data: '[some getServerSideProps data]' } }; -} - -export default BasicPage; diff --git a/packages/nextjs/test/integration/pages/errorClick.tsx b/packages/nextjs/test/integration/pages/errorClick.tsx deleted file mode 100644 index df55a4b32967..000000000000 --- a/packages/nextjs/test/integration/pages/errorClick.tsx +++ /dev/null @@ -1,11 +0,0 @@ -const ButtonPage = (): JSX.Element => ( - -); - -export default ButtonPage; diff --git a/packages/nextjs/test/integration/pages/faultyAppGetInitialProps.tsx b/packages/nextjs/test/integration/pages/faultyAppGetInitialProps.tsx deleted file mode 100644 index fabe09909a44..000000000000 --- a/packages/nextjs/test/integration/pages/faultyAppGetInitialProps.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// See _app.tsx for more information why this file exists. -const Page = (): JSX.Element =>

Hello World!

; - -export default Page; diff --git a/packages/nextjs/test/integration/pages/fetch.tsx b/packages/nextjs/test/integration/pages/fetch.tsx deleted file mode 100644 index 1e34e41986b9..000000000000 --- a/packages/nextjs/test/integration/pages/fetch.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useEffect } from 'react'; - -const FetchPage = (): JSX.Element => { - useEffect(() => { - // test that a span is created in the pageload transaction for this fetch request - fetch('http://example.com').catch(() => { - // no-empty - }); - }, []); - - return

Hello world!

; -}; - -export default FetchPage; diff --git a/packages/nextjs/test/integration/pages/healthy.tsx b/packages/nextjs/test/integration/pages/healthy.tsx deleted file mode 100644 index 2a254b825d4b..000000000000 --- a/packages/nextjs/test/integration/pages/healthy.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Link from 'next/link'; - -const HealthyPage = (): JSX.Element => ( - // @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag - - AlsoHealthy - -); - -export default HealthyPage; diff --git a/packages/nextjs/test/integration/pages/index.tsx b/packages/nextjs/test/integration/pages/index.tsx deleted file mode 100644 index e9549c4f1604..000000000000 --- a/packages/nextjs/test/integration/pages/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -const IndexPage = (): JSX.Element =>

Hello Next.js

; - -export default IndexPage; diff --git a/packages/nextjs/test/integration/pages/reportDialog.tsx b/packages/nextjs/test/integration/pages/reportDialog.tsx deleted file mode 100644 index bfc9704c3aa9..000000000000 --- a/packages/nextjs/test/integration/pages/reportDialog.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { captureException, showReportDialog } from '@sentry/nextjs'; - -const ReportDialogPage = (): JSX.Element => ( - -); - -export default ReportDialogPage; diff --git a/packages/nextjs/test/integration/pages/unmatchedCustomPageExtension.someExtension b/packages/nextjs/test/integration/pages/unmatchedCustomPageExtension.someExtension deleted file mode 100644 index e8d58e47f18e..000000000000 --- a/packages/nextjs/test/integration/pages/unmatchedCustomPageExtension.someExtension +++ /dev/null @@ -1,3 +0,0 @@ -This page simply exists to test the compatibility of Next.js' `pageExtensions` option with our auto wrapping -process. This file should not be turned into a page by Next.js and our webpack loader also shouldn't process it. -This page should not contain valid JavaScript. diff --git a/packages/nextjs/test/integration/pages/users/[id].tsx b/packages/nextjs/test/integration/pages/users/[id].tsx deleted file mode 100644 index 20f79eb0ecd7..000000000000 --- a/packages/nextjs/test/integration/pages/users/[id].tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { GetStaticPaths, GetStaticProps } from 'next'; - -import Layout from '../../components/Layout'; -import ListDetail from '../../components/ListDetail'; -import { User } from '../../interfaces'; -import { sampleUserData } from '../../utils/sample-data'; - -type Props = { - item?: User; - errors?: string; -}; - -const StaticPropsDetail = ({ item, errors }: Props) => { - if (errors) { - return ( - -

- Error: {errors} -

-
- ); - } - - return ( - - {item && } - - ); -}; - -export default StaticPropsDetail; - -export const getStaticPaths: GetStaticPaths = async () => { - // Get the paths we want to pre-render based on users - const paths = sampleUserData.map(user => ({ - params: { id: user.id.toString() }, - })); - - // We'll pre-render only these paths at build time. - // { fallback: false } means other routes should 404. - return { paths, fallback: false }; -}; - -// This function gets called at build time on server-side. -// It won't be called on client-side, so you can even do -// direct database queries. -export const getStaticProps: GetStaticProps = async ({ params }) => { - try { - const id = params?.id; - const item = sampleUserData.find(data => data.id === Number(id)); - // By returning { props: item }, the StaticPropsDetail component - // will receive `item` as a prop at build time - return { props: { item } }; - } catch (err) { - return { props: { errors: (err as Error).message } }; - } -}; diff --git a/packages/nextjs/test/integration/pages/users/index.tsx b/packages/nextjs/test/integration/pages/users/index.tsx deleted file mode 100644 index 90e959ff440e..000000000000 --- a/packages/nextjs/test/integration/pages/users/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { GetStaticProps } from 'next'; -import Link from 'next/link'; - -import Layout from '../../components/Layout'; -import List from '../../components/List'; -import { User } from '../../interfaces'; -import { sampleUserData } from '../../utils/sample-data'; - -type Props = { - items: User[]; -}; - -const WithStaticProps = ({ items }: Props) => ( - -

Users List

-

- Example fetching data from inside getStaticProps(). -

-

You are currently on: /users

- -

- {/* - // @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag */} - - Go home - -

-
-); - -export const getStaticProps: GetStaticProps = async () => { - // Example for including static props in a Next.js function component page. - // Don't forget to include the respective types for any props passed into - // the component. - const items: User[] = sampleUserData; - return { props: { items } }; -}; - -export default WithStaticProps; diff --git a/packages/nextjs/test/integration/pages/withErrorServerSideProps.tsx b/packages/nextjs/test/integration/pages/withErrorServerSideProps.tsx deleted file mode 100644 index dda4f8f31dd2..000000000000 --- a/packages/nextjs/test/integration/pages/withErrorServerSideProps.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const WithServerSidePropsPage = ({ data }: { data: string }) =>

WithServerSidePropsPage {data}

; - -export async function getServerSideProps() { - throw new Error('ServerSideProps Error'); -} - -export default WithServerSidePropsPage; diff --git a/packages/nextjs/test/integration/sentry.client.config.js b/packages/nextjs/test/integration/sentry.client.config.js deleted file mode 100644 index 4345033e9cdc..000000000000 --- a/packages/nextjs/test/integration/sentry.client.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as Sentry from '@sentry/nextjs'; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - tracesSampler: () => true, - debug: process.env.SDK_DEBUG, -}); diff --git a/packages/nextjs/test/integration/src/pages/someNamedComponent.tsx b/packages/nextjs/test/integration/src/pages/someNamedComponent.tsx deleted file mode 100644 index c6b4a0859769..000000000000 --- a/packages/nextjs/test/integration/src/pages/someNamedComponent.tsx +++ /dev/null @@ -1,6 +0,0 @@ -export const MyNamedPage = () => ( -

- This page exists to test the compatibility of our auto-wrapper with the option of having the `pages` directory - inside a `src` directory (https://nextjs.org/docs/advanced-features/src-directory) -

-); diff --git a/packages/nextjs/test/integration/test/.eslintrc.json b/packages/nextjs/test/integration/test/.eslintrc.json deleted file mode 100644 index 1abfc6ab4acc..000000000000 --- a/packages/nextjs/test/integration/test/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-console": "off", - "guard-for-in": "off" - } -} diff --git a/packages/nextjs/test/integration/test/client/appDirTracingPageloadClientcomponent.test.ts b/packages/nextjs/test/integration/test/client/appDirTracingPageloadClientcomponent.test.ts deleted file mode 100644 index 019c84438c72..000000000000 --- a/packages/nextjs/test/integration/test/client/appDirTracingPageloadClientcomponent.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should create a pageload transaction when the `app` directory is used with a client component.', async ({ - page, -}) => { - if (process.env.USE_APPDIR !== 'true') { - return; - } - - const [transaction] = await getMultipleSentryEnvelopeRequests(page, 1, { - url: '/clientcomponent', - envelopeType: 'transaction', - }); - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - }, - }, - transaction: '/clientcomponent', - }); - - expect(await countEnvelopes(page, { url: '/clientcomponent', envelopeType: 'transaction', timeout: 2000 })).toBe(1); -}); diff --git a/packages/nextjs/test/integration/test/client/appDirTracingPageloadServercomponent.test.ts b/packages/nextjs/test/integration/test/client/appDirTracingPageloadServercomponent.test.ts deleted file mode 100644 index aa77dc5c9afb..000000000000 --- a/packages/nextjs/test/integration/test/client/appDirTracingPageloadServercomponent.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should create a pageload transaction when the `app` directory is used with a server component.', async ({ - page, -}) => { - if (process.env.USE_APPDIR !== 'true') { - return; - } - - const [transaction] = await getMultipleSentryEnvelopeRequests(page, 1, { - url: '/servercomponent', - envelopeType: 'transaction', - }); - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - }, - }, - transaction: '/servercomponent', - }); - - expect(await countEnvelopes(page, { url: '/servercomponent', envelopeType: 'transaction', timeout: 2000 })).toBe(1); -}); diff --git a/packages/nextjs/test/integration/test/client/errorClick.test.ts b/packages/nextjs/test/integration/test/client/errorClick.test.ts deleted file mode 100644 index ad1b94463ed7..000000000000 --- a/packages/nextjs/test/integration/test/client/errorClick.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Event } from '@sentry/types'; -import { getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should capture error triggered on click', async ({ page }) => { - await page.goto('/errorClick'); - - const [, events] = await Promise.all([ - page.click('button'), - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'event' }), - ]); - - expect(events[0]?.exception?.values?.[0]).toMatchObject({ - type: 'Error', - value: 'Sentry Frontend Error', - }); -}); - -test('should have a non-url-encoded top frame in route with parameter', async ({ page }) => { - await page.goto('/some-param/errorClick'); - - const [, events] = await Promise.all([ - page.click('button'), - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'event' }), - ]); - - const frames = events[0]?.exception?.values?.[0]?.stacktrace?.frames; - - expect(frames?.[frames.length - 1].filename).toMatch(/\/\[id\]\/errorClick-[a-f0-9]+\.js$/); -}); - -test('should mark nextjs internal frames as `in_app`: false', async ({ page }) => { - await page.goto('/some-param/errorClick'); - - const [, events] = await Promise.all([ - page.click('button'), - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'event' }), - ]); - - const frames = events[0]?.exception?.values?.[0]?.stacktrace?.frames; - - expect(frames).toContainEqual( - expect.objectContaining({ - filename: expect.stringMatching( - /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, - ), - in_app: false, - }), - ); - - expect(frames).not.toContainEqual( - expect.objectContaining({ - filename: expect.stringMatching( - /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, - ), - in_app: true, - }), - ); -}); diff --git a/packages/nextjs/test/integration/test/client/errorGlobal.test.ts b/packages/nextjs/test/integration/test/client/errorGlobal.test.ts deleted file mode 100644 index e31daac08be1..000000000000 --- a/packages/nextjs/test/integration/test/client/errorGlobal.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Event } from '@sentry/types'; -import { getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should capture a globally triggered event', async ({ page }) => { - const event = await getMultipleSentryEnvelopeRequests(page, 1, { url: '/crashed', envelopeType: 'event' }); - - expect(event[0]?.exception?.values?.[0]).toMatchObject({ - type: 'Error', - value: 'Crashed', - }); -}); diff --git a/packages/nextjs/test/integration/test/client/faultyAppGetInitialPropsConfiguration.test.ts b/packages/nextjs/test/integration/test/client/faultyAppGetInitialPropsConfiguration.test.ts deleted file mode 100644 index b03c1f9f705f..000000000000 --- a/packages/nextjs/test/integration/test/client/faultyAppGetInitialPropsConfiguration.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expect, test } from '@playwright/test'; - -// This test verifies that a faulty configuration of `getInitialProps` in `_app` will not cause our -// auto - wrapping / instrumentation to throw an error. -// See `_app.tsx` for more information. - -test('should not fail auto-wrapping when `getInitialProps` configuration is faulty.', async ({ page }) => { - await page.goto('/faultyAppGetInitialProps'); - - const serverErrorText = await page.$('//*[contains(text(), "Internal Server Error")]'); - - expect(serverErrorText).toBeFalsy(); -}); diff --git a/packages/nextjs/test/integration/test/client/reportDialog.test.ts b/packages/nextjs/test/integration/test/client/reportDialog.test.ts deleted file mode 100644 index bfbb54d775ad..000000000000 --- a/packages/nextjs/test/integration/test/client/reportDialog.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('should show a dialog', async ({ page }) => { - await page.goto('/reportDialog'); - - await page.click('button'); - - const dialogScriptSelector = 'head > script[src^="https://dsn.ingest.sentry.io/api/embed/error-page"]'; - - const dialogScript = await page.waitForSelector(dialogScriptSelector, { state: 'attached' }); - const dialogScriptSrc = await (await dialogScript.getProperty('src')).jsonValue(); - - expect(dialogScriptSrc).toMatch(/^https:\/\/dsn\.ingest\.sentry\.io\/api\/embed\/error-page\/\?.*/); -}); diff --git a/packages/nextjs/test/integration/test/client/sessionCrashed.test.ts b/packages/nextjs/test/integration/test/client/sessionCrashed.test.ts deleted file mode 100644 index a16615cb7afb..000000000000 --- a/packages/nextjs/test/integration/test/client/sessionCrashed.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Session } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should report crashed sessions', async ({ page }) => { - const event = await getMultipleSentryEnvelopeRequests(page, 2, { url: '/crashed', envelopeType: 'session' }); - - expect(event[0]).toMatchObject({ - init: true, - status: 'ok', - errors: 0, - }); - - expect(event[1]).toMatchObject({ - init: false, - status: 'crashed', - errors: 1, - }); - - expect(await countEnvelopes(page, { url: '/crashed', envelopeType: 'session' })).toBe(2); -}); diff --git a/packages/nextjs/test/integration/test/client/sessionHealthy.test.ts b/packages/nextjs/test/integration/test/client/sessionHealthy.test.ts deleted file mode 100644 index ffbc238fe7ae..000000000000 --- a/packages/nextjs/test/integration/test/client/sessionHealthy.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Session } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should report healthy sessions', async ({ page }) => { - const event = await getMultipleSentryEnvelopeRequests(page, 1, { url: '/healthy', envelopeType: 'session' }); - - expect(event[0]).toMatchObject({ - init: true, - status: 'ok', - errors: 0, - }); - - expect(await countEnvelopes(page, { url: '/healthy', envelopeType: 'session' })).toBe(1); -}); diff --git a/packages/nextjs/test/integration/test/client/sessionNavigate.test.ts b/packages/nextjs/test/integration/test/client/sessionNavigate.test.ts deleted file mode 100644 index 05b201f99609..000000000000 --- a/packages/nextjs/test/integration/test/client/sessionNavigate.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Session } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should report navigation sessions', async ({ page }) => { - const event = await getMultipleSentryEnvelopeRequests(page, 1, { url: '/healthy', envelopeType: 'session' }); - - expect(event[0]).toMatchObject({ - init: true, - status: 'ok', - errors: 0, - }); - - await page.waitForTimeout(250); - - const [, events] = await Promise.all([ - page.click('a#alsoHealthy'), - getMultipleSentryEnvelopeRequests(page, 2, { envelopeType: 'session' }), - ]); - - expect(events[0]).toMatchObject({ - init: false, - status: 'exited', - errors: 0, - }); - - await page.waitForTimeout(250); - - expect(events[1]).toMatchObject({ - init: true, - status: 'ok', - errors: 0, - }); - - await page.waitForTimeout(250); - - const [, events_2] = await Promise.all([ - page.click('a#healthy'), - getMultipleSentryEnvelopeRequests(page, 2, { envelopeType: 'session' }), - ]); - - expect(events_2[0]).toMatchObject({ - init: false, - status: 'exited', - errors: 0, - }); - - expect(events_2[1]).toMatchObject({ - init: true, - status: 'ok', - errors: 0, - }); - - expect(await countEnvelopes(page, { url: '/healthy', envelopeType: 'session' })).toBe(1); - expect(await countEnvelopes(page, { url: '/healthy#alsoHealthy', envelopeType: 'session' })).toBe(4); -}); diff --git a/packages/nextjs/test/integration/test/client/tracingClientGetInitialProps.test.ts b/packages/nextjs/test/integration/test/client/tracingClientGetInitialProps.test.ts deleted file mode 100644 index 4f44d6762f46..000000000000 --- a/packages/nextjs/test/integration/test/client/tracingClientGetInitialProps.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { TransactionEvent } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should instrument `getInitialProps` for performance tracing', async ({ page }) => { - const transaction = await getMultipleSentryEnvelopeRequests(page, 1, { - url: '/42/withInitialProps', - envelopeType: 'transaction', - }); - - expect(transaction[0]).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - }, - }, - }); - - const nextDataTag = await page.waitForSelector('#__NEXT_DATA__', { state: 'attached' }); - const nextDataTagValue = JSON.parse(await nextDataTag.evaluate(tag => (tag as HTMLElement).innerText)); - - const traceId = transaction[0]?.contexts?.trace?.trace_id; - - expect(traceId).toBeDefined(); - - expect(nextDataTagValue.props.pageProps.data).toBe('[some getInitialProps data]'); - expect(nextDataTagValue.props.pageProps._sentryTraceData).toBeTruthy(); - expect(nextDataTagValue.props.pageProps._sentryBaggage).toBeTruthy(); - - expect(nextDataTagValue.props.pageProps._sentryTraceData.split('-')[0]).toBe(traceId); - - expect(nextDataTagValue.props.pageProps._sentryBaggage.match(/sentry-trace_id=([a-f0-9]*),/)[1]).toBe(traceId); - - expect(await countEnvelopes(page, { url: '/42/withInitialProps', envelopeType: 'transaction', timeout: 2500 })).toBe( - 1, - ); -}); diff --git a/packages/nextjs/test/integration/test/client/tracingClientGetServerSideProps.test.ts b/packages/nextjs/test/integration/test/client/tracingClientGetServerSideProps.test.ts deleted file mode 100644 index ee8990462d47..000000000000 --- a/packages/nextjs/test/integration/test/client/tracingClientGetServerSideProps.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Transaction } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should instrument `getServerSideProps` for performance tracing', async ({ page }) => { - const transaction = await getMultipleSentryEnvelopeRequests(page, 1, { - url: '/1337/withServerSideProps', - envelopeType: 'transaction', - }); - - expect(transaction[0]).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - }, - }, - }); - - const nextDataTag = await page.waitForSelector('#__NEXT_DATA__', { state: 'attached' }); - const nextDataTagValue = JSON.parse(await nextDataTag.evaluate(tag => (tag as HTMLElement).innerText)); - - // @ts-expect-error - We know `contexts` is defined in the Transaction envelope - const traceId = transaction[0]?.contexts.trace.trace_id; - - expect(traceId).toBeDefined(); - - expect(nextDataTagValue.props.pageProps.data).toBe('[some getServerSideProps data]'); - expect(nextDataTagValue.props.pageProps._sentryTraceData).toBeTruthy(); - expect(nextDataTagValue.props.pageProps._sentryBaggage).toBeTruthy(); - - expect(nextDataTagValue.props.pageProps._sentryTraceData.split('-')[0]).toBe(traceId); - - expect(nextDataTagValue.props.pageProps._sentryBaggage.match(/sentry-trace_id=([a-f0-9]*),/)[1]).toBe(traceId); - - expect( - await countEnvelopes(page, { url: '/1337/withServerSideProps', envelopeType: 'transaction', timeout: 2500 }), - ).toBe(1); -}); diff --git a/packages/nextjs/test/integration/test/client/tracingDynamicRoute.test.ts b/packages/nextjs/test/integration/test/client/tracingDynamicRoute.test.ts deleted file mode 100644 index 015668852f40..000000000000 --- a/packages/nextjs/test/integration/test/client/tracingDynamicRoute.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Transaction } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should correctly instrument dynamic routes for tracing', async ({ page }) => { - const transaction = await getMultipleSentryEnvelopeRequests(page, 1, { - url: '/users/102', - envelopeType: 'transaction', - }); - - expect(transaction[0]).toMatchObject({ - transaction: '/users/[id]', - type: 'transaction', - contexts: { - trace: { - op: 'pageload', - }, - }, - }); - - expect(await countEnvelopes(page, { url: '/users/102', envelopeType: 'transaction', timeout: 2500 })).toBe(1); -}); diff --git a/packages/nextjs/test/integration/test/client/tracingFetch.test.ts b/packages/nextjs/test/integration/test/client/tracingFetch.test.ts deleted file mode 100644 index 1ad8d3859518..000000000000 --- a/packages/nextjs/test/integration/test/client/tracingFetch.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Event } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should correctly instrument `fetch` for performance tracing', async ({ page }) => { - await page.route('http://example.com/**/*', route => { - return route.fulfill({ - status: 200, - body: JSON.stringify({ - foo: 'bar', - }), - }); - }); - - const transaction = await getMultipleSentryEnvelopeRequests(page, 1, { - url: '/fetch', - envelopeType: 'transaction', - }); - - expect(transaction[0]).toMatchObject({ - transaction: '/fetch', - type: 'transaction', - contexts: { - trace: { - op: 'pageload', - }, - }, - }); - - expect(transaction[0]?.spans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - data: { - 'http.method': 'GET', - url: 'http://example.com', - 'http.url': 'http://example.com/', - 'server.address': 'example.com', - type: 'fetch', - 'http.response_content_length': expect.any(Number), - 'http.response.status_code': 200, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - }, - description: 'GET http://example.com', - op: 'http.client', - parent_span_id: expect.any(String), - span_id: expect.any(String), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.any(String), - status: expect.any(String), - origin: 'auto.http.browser', - }), - ]), - ); - - expect(await countEnvelopes(page, { url: '/fetch', envelopeType: 'transaction', timeout: 2500 })).toBe(1); -}); diff --git a/packages/nextjs/test/integration/test/client/tracingNavigate.test.ts b/packages/nextjs/test/integration/test/client/tracingNavigate.test.ts deleted file mode 100644 index 434f12ecca22..000000000000 --- a/packages/nextjs/test/integration/test/client/tracingNavigate.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Transaction } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should report navigation transactions', async ({ page }) => { - const transaction = await getMultipleSentryEnvelopeRequests(page, 1, { - url: '/42/withInitialProps', - envelopeType: 'transaction', - }); - - expect(transaction[0]).toMatchObject({ - transaction: '/[id]/withInitialProps', - type: 'transaction', - contexts: { - trace: { - op: 'pageload', - }, - }, - }); - - await page.waitForTimeout(250); - - const [, transactions] = await Promise.all([ - page.click('a#server-side-props-page'), - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'transaction' }), - ]); - - expect(transactions[0]).toMatchObject({ - transaction: '/[id]/withServerSideProps', - type: 'transaction', - contexts: { - trace: { - op: 'navigation', - data: {}, - }, - }, - }); - - await page.waitForTimeout(250); - - const [, transactions_2] = await Promise.all([ - page.click('a#initial-props-page'), - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'transaction' }), - ]); - - expect(transactions_2[0]).toMatchObject({ - transaction: '/[id]/withInitialProps', - type: 'transaction', - contexts: { - trace: { - op: 'navigation', - data: {}, - }, - }, - }); - - expect(await countEnvelopes(page, { url: '/42/withInitialProps', envelopeType: 'transaction', timeout: 4000 })).toBe( - 1, - ); -}); diff --git a/packages/nextjs/test/integration/test/client/tracingPageLoad.test.ts b/packages/nextjs/test/integration/test/client/tracingPageLoad.test.ts deleted file mode 100644 index 65e1eaf93df6..000000000000 --- a/packages/nextjs/test/integration/test/client/tracingPageLoad.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { Transaction } from '@sentry/types'; -import { countEnvelopes, getMultipleSentryEnvelopeRequests } from './utils/helpers'; - -test('should report a `pageload` transaction', async ({ page }) => { - const transaction = await getMultipleSentryEnvelopeRequests(page, 1, { - url: '/testy', - envelopeType: 'transaction', - }); - - expect(transaction[0]).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - }, - }, - }); - - expect(await countEnvelopes(page, { url: '/testy', envelopeType: 'transaction', timeout: 4000 })).toBe(1); -}); diff --git a/packages/nextjs/test/integration/test/client/utils/helpers.ts b/packages/nextjs/test/integration/test/client/utils/helpers.ts deleted file mode 100644 index 37cffb1cd898..000000000000 --- a/packages/nextjs/test/integration/test/client/utils/helpers.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@sentry-internal/browser-integration-tests/utils/helpers'; diff --git a/packages/nextjs/test/integration/test/server/cjsApiEndpoints.test.ts b/packages/nextjs/test/integration/test/server/cjsApiEndpoints.test.ts deleted file mode 100644 index 25bbed240baa..000000000000 --- a/packages/nextjs/test/integration/test/server/cjsApiEndpoints.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('CommonJS API Endpoints', () => { - it('should not intercept unwrapped request', async () => { - const env = await NextTestEnv.init(); - const unwrappedRoute = '/api/wrapApiHandlerWithSentry/unwrapped/cjsExport'; - const url = `${env.url}${unwrappedRoute}`; - - const unwrappedEnvelope = await env.getEnvelopeRequest({ - url, - envelopeType: 'transaction', - endServer: false, - }); - - expect(unwrappedEnvelope[2]).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - data: { - 'http.response.status_code': 200, - }, - }, - }, - transaction: `GET ${unwrappedRoute}`, - type: 'transaction', - request: { - url, - }, - }); - - const response = await env.getAPIResponse(url); - - expect(response).toMatchObject({ - success: true, - }); - }); - - it('should intercept wrapped request', async () => { - const env = await NextTestEnv.init(); - const wrappedRoute = '/api/wrapApiHandlerWithSentry/wrapped/cjsExport'; - const url = `${env.url}${wrappedRoute}`; - - const wrappedEnvelope = await env.getEnvelopeRequest({ - url, - envelopeType: 'transaction', - endServer: false, - }); - - expect(wrappedEnvelope[2]).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - data: { - 'http.response.status_code': 200, - }, - }, - }, - transaction: `GET ${wrappedRoute}`, - type: 'transaction', - request: { - url, - }, - }); - - const response = await env.getAPIResponse(url); - - expect(response).toMatchObject({ - success: true, - }); - }); - - it('should not mess up require statements', async () => { - const env = await NextTestEnv.init(); - const route = '/api/requireTest'; - const url = `${env.url}${route}`; - - const wrappedEnvelope = await env.getEnvelopeRequest({ - url, - envelopeType: 'transaction', - endServer: false, - }); - - expect(wrappedEnvelope[2]).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - data: { - 'http.response.status_code': 200, - }, - }, - }, - transaction: `GET ${route}`, - type: 'transaction', - request: { - url, - }, - }); - - const response = await env.getAPIResponse(url); - - expect(response).toMatchObject({ - success: true, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/doubleEndMethodOnVercel.test.ts b/packages/nextjs/test/integration/test/server/doubleEndMethodOnVercel.test.ts deleted file mode 100644 index 5bc1aed6536e..000000000000 --- a/packages/nextjs/test/integration/test/server/doubleEndMethodOnVercel.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -// This test asserts that our wrapping of `res.end` doesn't break API routes on Vercel if people call `res.json` or -// `res.send` multiple times in one request handler. -// https://github.com/getsentry/sentry-javascript/issues/6670 -it.skip('should not break API routes on Vercel if people call res.json or res.send multiple times in one request handler', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/api/doubleEndMethodOnVercel`; - const response = await env.getAPIResponse(url); - - expect(response).toMatchObject({ - success: true, - }); -}); diff --git a/packages/nextjs/test/integration/test/server/errorApiEndpoint.test.ts b/packages/nextjs/test/integration/test/server/errorApiEndpoint.test.ts deleted file mode 100644 index b99058ecfe64..000000000000 --- a/packages/nextjs/test/integration/test/server/errorApiEndpoint.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('Error API Endpoints', () => { - it('should capture an error event', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/api/error`; - - const envelope = await env.getEnvelopeRequest({ - url, - envelopeType: 'event', - }); - - expect(envelope[2]).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: 'API Error', - }, - ], - }, - request: { - url, - method: 'GET', - }, - transaction: 'GET /api/error', - }); - }); - - it('should capture an erroneous transaction', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/api/error`; - - const envelopes = await env.getMultipleEnvelopeRequest({ - url, - envelopeType: 'transaction', - count: 1, - }); - - const sentryTransactionEnvelope = envelopes.find(envelope => { - const envelopeItem = envelope[2]!; - return envelopeItem.transaction === 'GET /api/error'; - }); - - expect(sentryTransactionEnvelope).toBeDefined(); - - const envelopeItem = sentryTransactionEnvelope![2]; - - expect(envelopeItem).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'internal_error', - data: { - 'http.response.status_code': 500, - }, - }, - }, - transaction: 'GET /api/error', - type: 'transaction', - request: { - url, - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/errorServerSideProps.test.ts b/packages/nextjs/test/integration/test/server/errorServerSideProps.test.ts deleted file mode 100644 index b777653af526..000000000000 --- a/packages/nextjs/test/integration/test/server/errorServerSideProps.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('Error Server-side Props', () => { - it('should capture an error event', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/withErrorServerSideProps`; - - const envelope = await env.getEnvelopeRequest({ - url, - envelopeType: 'event', - }); - - expect(envelope[2]).toMatchObject({ - transaction: `getServerSideProps (/withErrorServerSideProps)`, - exception: { - values: [ - { - type: 'Error', - value: 'ServerSideProps Error', - }, - ], - }, - request: { - url, - method: 'GET', - }, - }); - }); - - it('should capture an erroneous transaction', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/withErrorServerSideProps`; - - const envelopes = await env.getMultipleEnvelopeRequest({ - url, - envelopeType: 'transaction', - count: 1, - }); - - const sentryTransactionEnvelope = envelopes.find(envelope => { - const envelopeItem = envelope[2]!; - return envelopeItem.transaction === '/withErrorServerSideProps'; - }); - - expect(sentryTransactionEnvelope).toBeDefined(); - - const envelopeItem = sentryTransactionEnvelope![2]; - - expect(envelopeItem).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'internal_error', - }, - }, - transaction: '/withErrorServerSideProps', - transaction_info: { - source: 'route', - }, - type: 'transaction', - request: { - url: expect.stringMatching(/http:\/\/localhost:[0-9]+\/withErrorServerSideProps/), - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/excludedApiEndpoints.test.ts b/packages/nextjs/test/integration/test/server/excludedApiEndpoints.test.ts deleted file mode 100644 index 810fa899e926..000000000000 --- a/packages/nextjs/test/integration/test/server/excludedApiEndpoints.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('Excluded API Endpoints', () => { - it('Should exclude API endpoint via RegExp', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/api/excludedEndpoints/excludedWithRegExp`; - - const count = await env.countEnvelopes({ - url, - envelopeType: 'event', - timeout: 3000, - }); - - expect(count).toBe(0); - }); - - it('Should exclude API endpoint via string', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/api/excludedEndpoints/excludedWithString`; - - const count = await env.countEnvelopes({ - url, - envelopeType: 'event', - timeout: 3000, - }); - - expect(count).toBe(0); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/serverComponent.test.ts b/packages/nextjs/test/integration/test/server/serverComponent.test.ts deleted file mode 100644 index e178def81d7b..000000000000 --- a/packages/nextjs/test/integration/test/server/serverComponent.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('Loading the server component', () => { - it('should capture an error event', async () => { - if (process.env.USE_APPDIR !== 'true') { - return; - } - - const env = await NextTestEnv.init(); - const url = `${env.url}/servercomponent`; - - const envelope = await env.getEnvelopeRequest({ - url, - envelopeType: 'event', - }); - - expect(envelope[2]).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: 'I am an Error captured inside a server component', - }, - ], - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/tracing200.test.ts b/packages/nextjs/test/integration/test/server/tracing200.test.ts deleted file mode 100644 index dec2af2ef086..000000000000 --- a/packages/nextjs/test/integration/test/server/tracing200.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('Tracing 200', () => { - it('should capture a transaction', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/api/users`; - - const envelopes = await env.getMultipleEnvelopeRequest({ - url, - envelopeType: 'transaction', - count: 1, - }); - - const sentryTransactionEnvelope = envelopes.find(envelope => { - const envelopeItem = envelope[2]!; - return envelopeItem.transaction === 'GET /api/users'; - }); - - expect(sentryTransactionEnvelope).toBeDefined(); - - const envelopeItem = sentryTransactionEnvelope![2]; - - expect(envelopeItem).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - data: { - 'http.response.status_code': 200, - }, - }, - }, - transaction: 'GET /api/users', - transaction_info: { - source: 'route', - }, - type: 'transaction', - request: { - url, - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/tracing500.test.ts b/packages/nextjs/test/integration/test/server/tracing500.test.ts deleted file mode 100644 index d98c36d61db3..000000000000 --- a/packages/nextjs/test/integration/test/server/tracing500.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('Tracing 500', () => { - it('should capture an erroneous transaction', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/api/broken`; - - const envelopes = await env.getMultipleEnvelopeRequest({ - url, - envelopeType: 'transaction', - count: 1, - }); - - const sentryTransactionEnvelope = envelopes.find(envelope => { - const envelopeItem = envelope[2]!; - return envelopeItem.transaction === 'GET /api/broken'; - }); - - expect(sentryTransactionEnvelope).toBeDefined(); - - const envelopeItem = sentryTransactionEnvelope![2]; - - expect(envelopeItem).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'internal_error', - data: { - 'http.response.status_code': 500, - }, - }, - }, - transaction: 'GET /api/broken', - transaction_info: { - source: 'route', - }, - type: 'transaction', - request: { - url, - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/tracingHttp.test.ts b/packages/nextjs/test/integration/test/server/tracingHttp.test.ts deleted file mode 100644 index 1d84c2309d53..000000000000 --- a/packages/nextjs/test/integration/test/server/tracingHttp.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('Tracing HTTP', () => { - it('should capture a transaction', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/api/http`; - - const envelopes = await env.getMultipleEnvelopeRequest({ - url, - envelopeType: 'transaction', - count: 1, - }); - - const sentryTransactionEnvelope = envelopes.find(envelope => { - const envelopeItem = envelope[2]!; - return envelopeItem.transaction === 'GET /api/http'; - }); - - expect(sentryTransactionEnvelope).toBeDefined(); - - const envelopeItem = sentryTransactionEnvelope![2]!; - - expect(envelopeItem).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - data: { - 'http.response.status_code': 200, - }, - }, - }, - transaction: 'GET /api/http', - transaction_info: { - source: 'route', - }, - type: 'transaction', - request: { - url, - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/tracingServerGetInitialProps.test.ts b/packages/nextjs/test/integration/test/server/tracingServerGetInitialProps.test.ts deleted file mode 100644 index 4e8c1a2975c4..000000000000 --- a/packages/nextjs/test/integration/test/server/tracingServerGetInitialProps.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('getInitialProps', () => { - it('should capture a transaction', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/239/withInitialProps`; - - const envelopes = await env.getMultipleEnvelopeRequest({ - url, - envelopeType: 'transaction', - count: 1, - }); - - const sentryTransactionEnvelope = envelopes.find(envelope => { - const envelopeItem = envelope[2]!; - return envelopeItem.transaction === `/[id]/withInitialProps`; - }); - - expect(sentryTransactionEnvelope).toBeDefined(); - - const envelopeItem = sentryTransactionEnvelope![2]; - - expect(envelopeItem).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - }, - }, - transaction: '/[id]/withInitialProps', - transaction_info: { - source: 'route', - }, - type: 'transaction', - request: { - url, - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/tracingServerGetServerSideProps.test.ts b/packages/nextjs/test/integration/test/server/tracingServerGetServerSideProps.test.ts deleted file mode 100644 index 88e0bcb2b550..000000000000 --- a/packages/nextjs/test/integration/test/server/tracingServerGetServerSideProps.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('getServerSideProps', () => { - it('should capture a transaction', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/193/withServerSideProps`; - - const envelopes = await env.getMultipleEnvelopeRequest({ - url, - envelopeType: 'transaction', - count: 1, - }); - - const sentryTransactionEnvelope = envelopes.find(envelope => { - const envelopeItem = envelope[2]!; - return envelopeItem.transaction === '/[id]/withServerSideProps'; - }); - - expect(sentryTransactionEnvelope).toBeDefined(); - - const envelopeItem = sentryTransactionEnvelope![2]; - - expect(envelopeItem).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - }, - }, - transaction: '/[id]/withServerSideProps', - transaction_info: { - source: 'route', - }, - type: 'transaction', - request: { - url, - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/tracingServerGetServerSidePropsCustomPageExtension.test.ts b/packages/nextjs/test/integration/test/server/tracingServerGetServerSidePropsCustomPageExtension.test.ts deleted file mode 100644 index 54dec6ae5153..000000000000 --- a/packages/nextjs/test/integration/test/server/tracingServerGetServerSidePropsCustomPageExtension.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -describe('tracingServerGetServerSidePropsCustomPageExtension', () => { - it('should capture a transaction', async () => { - const env = await NextTestEnv.init(); - const url = `${env.url}/customPageExtension`; - - const envelopes = await env.getMultipleEnvelopeRequest({ - url, - envelopeType: 'transaction', - count: 1, - }); - - const sentryTransactionEnvelope = envelopes.find(envelope => { - const envelopeItem = envelope[2]!; - return envelopeItem.transaction === '/customPageExtension'; - }); - - expect(sentryTransactionEnvelope).toBeDefined(); - - const envelopeItem = sentryTransactionEnvelope![2]; - - expect(envelopeItem).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - }, - }, - transaction: '/customPageExtension', - transaction_info: { - source: 'route', - }, - type: 'transaction', - request: { - url, - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/tracingWithSentryAPI.test.ts b/packages/nextjs/test/integration/test/server/tracingWithSentryAPI.test.ts deleted file mode 100644 index 73302842e15e..000000000000 --- a/packages/nextjs/test/integration/test/server/tracingWithSentryAPI.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { NextTestEnv } from './utils/helpers'; - -const cases = [ - { - name: 'unwrappedNoParamURL', - url: `/api/wrapApiHandlerWithSentry/unwrapped/noParams`, - transactionName: '/api/wrapApiHandlerWithSentry/unwrapped/noParams', - }, - { - name: 'unwrappedDynamicURL', - url: `/api/wrapApiHandlerWithSentry/unwrapped/dog`, - transactionName: '/api/wrapApiHandlerWithSentry/unwrapped/[animal]', - }, - { - name: 'unwrappedCatchAllURL', - url: `/api/wrapApiHandlerWithSentry/unwrapped/dog/facts`, - transactionName: '/api/wrapApiHandlerWithSentry/unwrapped/[...pathParts]', - }, - { - name: 'wrappedNoParamURL', - url: `/api/wrapApiHandlerWithSentry/wrapped/noParams`, - transactionName: '/api/wrapApiHandlerWithSentry/wrapped/noParams', - }, - { - name: 'wrappedDynamicURL', - url: `/api/wrapApiHandlerWithSentry/wrapped/dog`, - transactionName: '/api/wrapApiHandlerWithSentry/wrapped/[animal]', - }, - { - name: 'wrappedCatchAllURL', - url: `/api/wrapApiHandlerWithSentry/wrapped/dog/facts`, - transactionName: '/api/wrapApiHandlerWithSentry/wrapped/[...pathParts]', - }, -]; - -describe('getServerSideProps', () => { - it.each(cases)(`should capture a transaction for %s`, async ({ url, transactionName }) => { - const env = await NextTestEnv.init(); - - const fullUrl = `${env.url}${url}`; - - const envelope = await env.getEnvelopeRequest({ - url: fullUrl, - envelopeType: 'transaction', - }); - - expect(envelope[2]).toMatchObject({ - contexts: { - trace: { - op: 'http.server', - status: 'ok', - }, - }, - transaction: `GET ${transactionName}`, - transaction_info: { - source: 'route', - }, - type: 'transaction', - request: { - url: fullUrl, - }, - }); - }); -}); diff --git a/packages/nextjs/test/integration/test/server/utils/helpers.ts b/packages/nextjs/test/integration/test/server/utils/helpers.ts deleted file mode 100644 index badc22c18424..000000000000 --- a/packages/nextjs/test/integration/test/server/utils/helpers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as http from 'http'; -import { Server, createServer } from 'http'; -import { AddressInfo } from 'net'; -import * as path from 'path'; -import { parse } from 'url'; -import next from 'next'; -import { TestEnv } from '../../../../../../../dev-packages/node-integration-tests/utils'; -import { register } from '../../../instrumentation'; - -let initializedSdk = false; - -// Type not exported from NextJS -// @ts-expect-error -export const createNextServer = async config => { - const app = next({ ...config, customServer: false }); // customServer: false because: https://github.com/vercel/next.js/pull/49805#issuecomment-1557321794 - const handle = app.getRequestHandler(); - await app.prepare(); - - return createServer((req, res) => { - const { url } = req; - - if (!url) { - throw new Error('No url'); - } - - handle(req, res, parse(url, true)); - }); -}; - -export const startServer = async (server: Server) => { - return new Promise<{ server: http.Server; url: string }>(resolve => { - server.listen(0, () => { - const port = (server.address() as AddressInfo).port; - const url = `http://localhost:${port}`; - resolve({ server, url }); - }); - }); -}; - -export class NextTestEnv extends TestEnv { - private constructor(public readonly server: http.Server, public readonly url: string) { - super(server, url); - } - - public static async init(): Promise { - if (!initializedSdk) { - // Normally, Next.js calls the `register` hook by itself, but since we are using a custom server for the tests we need to do it manually. - process.env.NEXT_RUNTIME = 'nodejs'; - await register(); - initializedSdk = true; - } - - const server = await createNextServer({ - dev: false, - dir: path.resolve(__dirname, '../../..'), - - // This needs to be explicitly passed to the server - // Otherwise it causes Segmentation Fault with NextJS >= 12 - // https://github.com/vercel/next.js/issues/33008 - conf: path.resolve(__dirname, '../../next.config.js'), - }); - - const { url } = await startServer(server); - - return new NextTestEnv(server, url); - } -} diff --git a/packages/nextjs/test/integration/test/server/utils/throw.js b/packages/nextjs/test/integration/test/server/utils/throw.js deleted file mode 100644 index 0e37a4135be4..000000000000 --- a/packages/nextjs/test/integration/test/server/utils/throw.js +++ /dev/null @@ -1 +0,0 @@ -throw new Error('I am throwing'); diff --git a/packages/nextjs/test/integration/test/tsconfig.json b/packages/nextjs/test/integration/test/tsconfig.json deleted file mode 100644 index e48928457b7c..000000000000 --- a/packages/nextjs/test/integration/test/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../../tsconfig.test.json", - -} diff --git a/packages/nextjs/test/integration/tsconfig.json b/packages/nextjs/test/integration/tsconfig.json deleted file mode 100644 index ed3ebdb2baea..000000000000 --- a/packages/nextjs/test/integration/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "allowSyntheticDefaultImports": true, - "alwaysStrict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "isolatedModules": true, - "jsx": "preserve", - "lib": ["dom", "es2018"], - "module": "esnext", - "moduleResolution": "node", - "noEmit": true, - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "strict": true, - "target": "esnext", - "incremental": true, // automatically set by Next.js 13 - "plugins": [ - { - "name": "next" - } - ], - }, - "exclude": ["node_modules"], - "include": ["**/*.ts", "**/*.tsx", "../../playwright.config.ts", ".next/types/**/*.ts"] -} diff --git a/packages/nextjs/test/integration/tsconfig.test.json b/packages/nextjs/test/integration/tsconfig.test.json deleted file mode 100644 index 3b04de6d5bc0..000000000000 --- a/packages/nextjs/test/integration/tsconfig.test.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.test.json", - - "include": ["test/**/*"], - - "compilerOptions": { - "types": ["node", "jest"], - "esModuleInterop": true - } -} diff --git a/packages/nextjs/test/integration/utils/sample-data.ts b/packages/nextjs/test/integration/utils/sample-data.ts deleted file mode 100644 index 504268d70797..000000000000 --- a/packages/nextjs/test/integration/utils/sample-data.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { User } from '../interfaces'; - -/** Dummy user data. */ -export const sampleUserData: User[] = [ - { id: 101, name: 'Alice' }, - { id: 102, name: 'Bob' }, - { id: 103, name: 'Caroline' }, - { id: 104, name: 'Dave' }, -]; diff --git a/packages/nextjs/test/integration_test_utils.sh b/packages/nextjs/test/integration_test_utils.sh deleted file mode 100644 index c067ab9c227d..000000000000 --- a/packages/nextjs/test/integration_test_utils.sh +++ /dev/null @@ -1,79 +0,0 @@ -function link_package() { - local package_abs_path=$1 - # strip the 'sentry-' prefix from the repo name of packages not in the monorepo (`cli`, `webpack-plugin`, and `wizard`) - local package_name=$(basename $package_abs_path | sed s/sentry-//) - - echo "Setting up @sentry/${package_name} for linking" - pushd $package_abs_path - yarn link - popd - - echo "Linking @sentry/$package_name" - yarn link "@sentry/$package_name" - -} - -# Note: LINKED_CLI_REPO and LINKED_PLUGIN_REPO in the functions below should be set to the absolute path of each local repo - -function linkcli() { - if [[ ! $LINKED_CLI_REPO ]]; then - return - fi - - # check to make sure the repo directory exists - if [[ -d $LINKED_CLI_REPO ]]; then - link_package $LINKED_CLI_REPO - else - # the $1 lets us insert a string in that spot if one is passed to `linkcli` (useful for when we're calling this from - # within another linking function) - echo "ERROR: Can't link @sentry/cli $1because directory $LINKED_CLI_REPO does not exist." - fi -} - -function linkplugin() { - if [[ ! $LINKED_PLUGIN_REPO ]]; then - return - fi - - # check to make sure the repo directory exists - if [[ -d $LINKED_PLUGIN_REPO ]]; then - link_package $LINKED_PLUGIN_REPO - - # the webpack plugin depends on `@sentry/cli`, so if we're also using a linked version of the cli package, the - # plugin needs to link to it, too - if [[ $LINKED_CLI_REPO ]]; then - pushd $LINKED_PLUGIN_REPO - link_cli "in webpack plugin repo " - popd - fi - else - echo "ERROR: Can't link @sentry/wepack-plugin because $LINKED_PLUGIN_REPO does not exist." - fi -} - -# This is only really useful for running tests in the debugger, as the normal test runner reinstalls all SDK packages -# from the local files on each test run -function link_monorepo_packages() { - local repo_packages_dir=$1 - - for abs_package_path in ${repo_packages_dir}/*; do - local package_name=$(basename $abs_package_path) - - # Skip packages under the `@sentry-internal` namespace (our function is only linking packages in the `@sentry` - # namespace, and besides, there's no reason to link such packages, as they're purely SDK dev dependencies). - # - # (The regex test ( `=~` ) is a sneaky way of testing if `package_name` is any of the three packages listed: if the - # string containing all of the packages containes a match to the regex solely consisting of the current package - # name, the current package must be in the list.) - if [[ "eslint-config-sdk eslint-plugin-sdk typescript" =~ $package_name ]]; then - continue - fi - - # `-L` tests if the given file is a symbolic link, to see if linking has already been done - if [[ ! -L node_modules/@sentry/$package_name ]]; then - echo "Linking @sentry/$package_name" - link_package $abs_package_path >/dev/null 2>&1 - fi - - done -} diff --git a/packages/nextjs/test/run-integration-tests.sh b/packages/nextjs/test/run-integration-tests.sh deleted file mode 100755 index 17e8ace8f446..000000000000 --- a/packages/nextjs/test/run-integration-tests.sh +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash - -source test/integration_test_utils.sh - -set -e - -START_TIME=$(date -R) - -function cleanup { - echo "[nextjs] Cleaning up..." - mv next.config.js.bak next.config.js 2>/dev/null || true - mv -f package.json.bak package.json 2>/dev/null || true - rm -rf node_modules 2>/dev/null || true - - # Delete yarn's cached versions of sentry packages added during this test run, since every test run installs multiple - # copies of each package. Without this, the cache can balloon in size quickly if integration tests are being run - # multiple times in a row. - find "$(yarn cache dir)" -iname "npm-@sentry*" -newermt "$START_TIME" -mindepth 1 -maxdepth 1 -exec rm -rf {} \; - - echo "[nextjs] Test run complete" -} - -trap cleanup EXIT - -cd "$(dirname "$0")/integration" - -NODE_VERSION=$(node -v) -NODE_MAJOR=$(echo "$NODE_VERSION" | cut -c2- | cut -d. -f1) -echo "Running integration tests on Node $NODE_VERSION" - -# make a backup of our config file so we can restore it when we're done -mv next.config.js next.config.js.bak - -for NEXTJS_VERSION in 13; do - for USE_APPDIR in true false; do - if ([ "$NODE_MAJOR" -lt "16" ]) && [ "$USE_APPDIR" == true ]; then - # App dir doesn not work on Node.js < 16 - continue - fi - - # export this to the env so that we can behave differently depending on which version of next we're testing, without - # having to pass this value from function to function to function to the one spot, deep in some callstack, where we - # actually need it - export NEXTJS_VERSION=$NEXTJS_VERSION - export NODE_MAJOR=$NODE_MAJOR - export USE_APPDIR=$USE_APPDIR - - echo "[nextjs@$NEXTJS_VERSION] Preparing environment..." - rm -rf node_modules .next .env.local 2>/dev/null || true - - echo "[nextjs@$NEXTJS_VERSION] Installing dependencies..." - - # Pin to a specific version - if [ "$NEXTJS_VERSION" -eq "13" ]; then - NEXTJS_PACKAGE_JSON_VERSION="13.2.0" - else - NEXTJS_PACKAGE_JSON_VERSION="$NEXTJS_VERSION.x" - fi - - # set the desired version of next long enough to run yarn, and then restore the old version (doing the restoration now - # rather than during overall cleanup lets us look for "latest" in every loop) - cp package.json package.json.bak - if [[ $(uname) == "Darwin" ]]; then - sed -i "" /"next.*latest"/s/latest/"${NEXTJS_PACKAGE_JSON_VERSION}"/ package.json - else - sed -i /"next.*latest"/s/latest/"${NEXTJS_PACKAGE_JSON_VERSION}"/ package.json - fi - - # Yarn install randomly started failing because it couldn't find some cache so for now we need to run these two commands which seem to fix it. - # It was pretty much this issue: https://github.com/yarnpkg/yarn/issues/5275 - rm -rf node_modules - yarn cache clean - - # We have to use `--ignore-engines` because sucrase claims to need Node 12, even though tests pass just fine on Node 10 - yarn --no-lockfile --ignore-engines - - # if applicable, use local versions of `@sentry/cli` and/or `@sentry/webpack-plugin` (these commands no-op unless - # LINKED_CLI_REPO and/or LINKED_PLUGIN_REPO are set) - linkcli && linkplugin - mv -f package.json.bak package.json 2>/dev/null || true - - if [ "$NEXTJS_VERSION" -eq "13" ]; then - if [ "$USE_APPDIR" == true ]; then - cat next13.appdir.config.template > next.config.js - else - cat next13.config.template > next.config.js - fi - fi - - echo "[nextjs@$NEXTJS_VERSION] Building..." - yarn build - - # we keep this updated as we run the tests, so that if it's ever non-zero, we can bail - EXIT_CODE=0 - - if [ "$USE_APPDIR" == true ]; then - echo "Skipping server tests for appdir" - else - echo "[nextjs@$NEXTJS_VERSION] Running server tests with options: $args" - (cd .. && yarn test:integration:server) || EXIT_CODE=$? - fi - - if [ $EXIT_CODE -eq 0 ]; then - echo "[nextjs@$NEXTJS_VERSION] Server integration tests passed" - else - echo "[nextjs@$NEXTJS_VERSION] Server integration tests failed" - exit 1 - fi - - if [ "$NODE_MAJOR" -lt "14" ]; then - echo "[nextjs@$NEXTJS_VERSION] Skipping client tests on Node $NODE_MAJOR" - else - echo "[nextjs@$NEXTJS_VERSION] Running client tests with options: $args" - (cd .. && yarn test:integration:client) || EXIT_CODE=$? - if [ $EXIT_CODE -eq 0 ]; then - echo "[nextjs@$NEXTJS_VERSION] Client integration tests passed" - else - echo "[nextjs@$NEXTJS_VERSION] Client integration tests failed" - exit 1 - fi - fi - done -done diff --git a/packages/nextjs/vercel/install-sentry-from-branch.sh b/packages/nextjs/vercel/install-sentry-from-branch.sh deleted file mode 100644 index b36983b898dc..000000000000 --- a/packages/nextjs/vercel/install-sentry-from-branch.sh +++ /dev/null @@ -1,127 +0,0 @@ -# SCRIPT TO INCLUDE AS PART OF A VERCEL-DEPLOYED PROJECT, SO THAT IT USES A BRANCH FROM THE SDK REPO -# USE `yarn vercel:project ` TO HAVE IT AUTOMATICALLY ADDED TO YOUR PROJECT - -# CUSTOM INSTALL COMMAND FOR PROJECT ON VERCEL: `bash .sentry/install-sentry-from-branch.sh` - -PROJECT_DIR=$(pwd) -REPO_DIR="${PROJECT_DIR}/sentry-javascript" - -# Set BRANCH_NAME as an environment variable -source .sentry/set-branch-name.sh - -echo " " -echo "CLONING SDK REPO" -git clone https://github.com/getsentry/sentry-javascript.git - -echo " " -echo "MOVING INTO REPO DIRECTORY AND CHECKING OUT BRANCH" -cd $REPO_DIR -git checkout $BRANCH_NAME - -echo "LATEST COMMIT: $(git log --format="%C(auto) %h - %s" | head -n 1)" - -echo " " -echo "INSTALLING SDK DEPENDENCIES" -# We need dev dependencies so that we can build the SDK -yarn --prod false - -echo " " -echo "BUILDING SDK" -# build types, cjs, and esm -yarn build:dev - -# Set all packages in the repo to point to their siblings as file dependencies. That way, when we install the local copy -# of @sentry/nextjs, it'll pull the local copy of each of its @sentry/* dependents. This mimics what Lerna does with -# symlinks, just with file dependencies (which we have to use because linking seems to lead to module resolution -# errors). -echo " " -echo "POINTING SIBLING DEPENDENCIES IN PACKAGE.JSON AT LOCAL DIRECTORIES" -PACKAGES_DIR="$REPO_DIR/packages" -# Escape all of the slashes in the path for use in sed -ESCAPED_PACKAGES_DIR=$(echo $PACKAGES_DIR | sed s/'\/'/'\\\/'/g) - -PACKAGE_NAMES=$(ls $PACKAGES_DIR) - -# Modify each package's package.json file by searching in it for sentry dependencies from the monorepo and, for each -# sibling dependency found, replacing the version number with a file dependency pointing to the sibling itself (so -# `"@sentry/utils": "6.9.0"` becomes `"@sentry/utils": "file:/abs/path/to/sentry-javascript/packages/utils"`) -for package in ${PACKAGE_NAMES[@]}; do - # Within a given package.json file, search for each of the other packages in turn, and if found, make the replacement - for package_dep in ${PACKAGE_NAMES[@]}; do - sed -Ei /"@sentry\/${package_dep}"/s/"[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?"/"file:${ESCAPED_PACKAGES_DIR}\/${package_dep}"/ ${PACKAGES_DIR}/${package}/package.json - done -done - -echo " " -echo "MOVING BACK TO PROJECT DIRECTORY" -cd $PROJECT_DIR - -# TODO move this into `yarn vercel:project` script, accounting for differences in SDK repo location between running the -# test app locally and on vercel -echo " " -echo "PATCHING SENTRY.SERVER.CONFIG.JS AND SENTRY.CLIENT.CONFIG.JS" -echo "Removing frame limit on stacktraces" -echo "Tagging events with $(vercel) tag" -echo "Tagging events with SDK repo's most recent commit message" -echo "Tagging events with test project repo's most recent commit message" - -INFINITE_STACKTRACE_CODE=" -Error.stackTraceLimit = Infinity; - " - -SDK_COMMIT_MESSAGE=$(cd sentry-javascript && git log --format="%C(auto)%s" | head -n 1) -CONFIGURE_SCOPE_CODE=" -if (process.env.VERCEL) { - Sentry.setTag('vercel', true); -} -Sentry.setTag('commitMessage', process.env.VERCEL_GIT_COMMIT_MESSAGE); -Sentry.setTag('sdkCommitMessage', \"$SDK_COMMIT_MESSAGE\"); -" - -echo "$INFINITE_STACKTRACE_CODE" "$CONFIGURE_SCOPE_CODE" >>sentry.server.config.js -echo "$INFINITE_STACKTRACE_CODE" "$CONFIGURE_SCOPE_CODE" >>sentry.client.config.js - -# Add built SDK as a file dependency. This has the side effect of forcing yarn to install all of the other dependencies, -# saving us the trouble of needing to call `yarn` separately after this -echo " " -echo "SUBSTITUTING LOCAL SDK FOR PUBLISHED ONE AND INSTALLING PROJECT DEPENDENCIES" -echo "yarn add file:sentry-javascript/packages/nextjs" -yarn add file:sentry-javascript/packages/nextjs - -# In case for any reason we ever need to link the local SDK rather than adding it as a file dependency: - -# echo " " -# echo "LINKING LOCAL SDK INTO PROJECT" - -# for abs_package_path in sentry-javascript/packages/*; do -# package=$(basename $abs_package_path) - -# # this one will error out because it's not called @sentry/typescript, it's -# # called @sentry-internal/typescript, but we don't need it, so just move on -# if [ "$package" = "typescript" ]; then -# continue -# fi - -# echo " " -# echo "Linking @sentry/${package}" - -# cd $abs_package_path -# yarn link - -# cd $PROJECT_DIR -# yarn link "@sentry/$package" -# done - -# # These aren't in the repo and therefore have to be done separately (we link these even though they're not in the repo -# # because the branch might specify a different version of either than the published SDK does) -# for package in "cli" "webpack-plugin"; do - -# echo " " -# echo "Linking @sentry/${package}" - -# cd sentry-javascript/node_modules/@sentry/$package -# yarn link - -# cd $PROJECT_DIR -# yarn link "@sentry/$package" -# done diff --git a/packages/nextjs/vercel/instructions.md b/packages/nextjs/vercel/instructions.md deleted file mode 100644 index 6a1890494ffc..000000000000 --- a/packages/nextjs/vercel/instructions.md +++ /dev/null @@ -1,53 +0,0 @@ -# Testing an SDK Branch on Vercel - -Follow the instructions below to test a branch of the SDK against a test app deployed to Vercel. This assumes you -already have such an app set up, and modifies both it and the SDK branch such that the dependency installation process -run on Vercel includes cloning the repo, building the current branch of the SDK, and setting the test app's -`@sentry/next` dependency to point to the newly-built local version. - -(The clone-build-link step is necessary because you can't point a `package.json` dependency to a sub-folder of a git -repo, only a full repo itself. Since we run a monorepo, this won't work in our case.) - -### To prepare your SDK branch for deploying on Vercel - -From `packages/nextjs`, run - -`yarn vercel:branch`. - -This will delete unneeded packages (angular, vue, etc) in order to speed up deployment. It will then commit that change. -When your branch is ready to PR, just rebase and drop that commit. - -### To prepare your test app for using current SDK branch - -First, make sure the branch you want to test is checked out in your `sentry-javascript` repo, and that all changes you -want to test are pushed to GitHub. - -From `packages/nextjs`, run - -`yarn vercel:project `. - -This will copy the `install-sentry-from-branch.sh` script into a `.sentry` folder at the root level of your test app, -and create a `set-branch-name.sh` script in the same location. (The first script is the one you'll run on Vercel. The -second is called by the first, and just sets an environment variable with the current (SDK) branch name.) It will then -commit (but not push) this change. - -Go into your project settings on Vercel and change the install command to - -`bash .sentry/install-sentry-from-branch.sh` - -and the build command to - -`yarn build && bash .sentry/post-app-build.sh`. - -If you're using bundle analyzer, the post-build script will move the visualizations it creates so that they're available -on your deployed site at `/client.html` and `/server.html`. - -NOTE: You don't need to change the `@sentry/nextjs` dependency in your project's `package.json` file. That will happen -on the fly each time your app is deployed. - -### To test the SDK - -Once you have pushed the changes made by `yarn vercel:project` to GitHub, just make changes (either to the SDK or your -test app) and push them. Vercel will always use the latest version of both the SDK and your test app each time it -deploys. Pushing changes to your test app will trigger a new build in Vercel; for changes to the SDK, you'll need to -manually redeploy, either by kicking off a new build or simply choosing 'Redeploy' on your most recent existing build. diff --git a/packages/nextjs/vercel/make-project-use-current-branch.sh b/packages/nextjs/vercel/make-project-use-current-branch.sh deleted file mode 100644 index 57d21ac968c8..000000000000 --- a/packages/nextjs/vercel/make-project-use-current-branch.sh +++ /dev/null @@ -1,63 +0,0 @@ -# SCRIPT TO MAKE TEST APP USE THIS BRANCH - -# CALL THIS BY RUNNING `yarn vercel:project ` - -NEXTJS_SDK_DIR=$(pwd) -PROJECT_DIR=$1 -SDK_BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) - -if [ ! -n "${PROJECT_DIR}" ]; then - echo " " - echo "ERROR: Missing project directory. Please supply the path to your project as an argument to the command." - exit 1 -fi - -# make sure branch is already set up -echo " " -echo "Making sure branch is set up for vercel deployment." -yarn vercel:branch - -cd $PROJECT_DIR - -# make sure we're dealing with a clean test app repo -STASHED_CHANGES=$(git status --porcelain) -if [ -n "${STASHED_CHANGES}" ]; then - echo "Found uncommitted changes in your project. Stashing them." - git stash --quiet --include-untracked -fi - -# make sure we have a clean directory into which to put our scripts -echo " " -if [ -d .sentry ]; then - echo "Clearing .sentry directory" - rm -rf .sentry -else - echo "Creating .sentry directory" -fi -mkdir .sentry - -# set up scripts for use in vercel deployment -echo " " -echo "Creating install scripts and committing the changes" -cp $NEXTJS_SDK_DIR/vercel/install-sentry-from-branch.sh .sentry -cp $NEXTJS_SDK_DIR/vercel/post-app-build.sh .sentry -echo "export BRANCH_NAME=${SDK_BRANCH_NAME}" >>.sentry/set-branch-name.sh -git add . -git commit -m "add scripts for using ${SDK_BRANCH_NAME} branch of @sentry/nextjs" - -# restore working directory, if necessary -if [ -n "${STASHED_CHANGES}" ]; then - echo " " - echo "Restoring changes from earlier stash:" - git stash pop --quiet - git status --porcelain - echo " " -fi - -cd $NEXTJS_SDK_DIR - -echo " " -echo "SUCCESS!" -echo "Your project will now use this branch of the SDK repo when deployed to Vercel. If you haven't done so already, go to your project settings in Vercel and set a custom install command:" -echo " bash .sentry/install-sentry-from-branch.sh" -echo " " diff --git a/packages/nextjs/vercel/post-app-build.sh b/packages/nextjs/vercel/post-app-build.sh deleted file mode 100644 index d4f98319f528..000000000000 --- a/packages/nextjs/vercel/post-app-build.sh +++ /dev/null @@ -1,16 +0,0 @@ -# SCRIPT TO INCLUDE AS PART OF A VERCEL-DEPLOYED PROJECT, FOR WORK TO BE DONE AFTER THE NEXTJS APP IS BUILT -# USE `yarn vercel:project ` TO HAVE IT AUTOMATICALLY ADDED TO YOUR PROJECT - -# CUSTOM BUILD COMMAND FOR PROJECT ON VERCEL: `yarn build && bash .sentry/post-app-build.sh` - -if [[ -e .next/analyze/ ]]; then - echo " " - echo "Moving bundle analysis graphs from \`.next/analyze/\` to \`/public\`" - mv .next/analyze/* public -fi -if [[ -e .next/server/analyze/ ]]; then - echo " " - echo "Moving bundle analysis graphs from \`.next/server/analyze/\` to \`/public\`" - mv .next/server/analyze/* public - echo " " -fi diff --git a/packages/nextjs/vercel/set-up-branch-for-test-app-use.sh b/packages/nextjs/vercel/set-up-branch-for-test-app-use.sh deleted file mode 100644 index 324ad21412ee..000000000000 --- a/packages/nextjs/vercel/set-up-branch-for-test-app-use.sh +++ /dev/null @@ -1,59 +0,0 @@ -# SCRIPT TO SET UP BRANCH FOR USE IN VERCEL-DEPLOYED TEST APPS - -# CALL THIS WITH `yarn vercel:branch` - -echo " " - -# This puts us in the packages directory -cd .. - -# Make sure we're dealing with a clean SDK repo -STASHED_CHANGES=$(git status --porcelain) -if [ -n "${STASHED_CHANGES}" ]; then - echo "Found uncommitted changes. Stashing them." - git stash --quiet --include-untracked -fi - -# If this hasn't already been done, get rid of irrelevant packages to speed up deploy process -PACKAGES_DELETED=false -for package in *; do - # Delete all packages which aren't either runtime or dev dependencies of the nextjs SDK - case $package in - # Runtime depependencies - "nextjs" | "core" | "hub" | "browser" | "node" | "react" | "tracing" | "utils" | "integrations") - continue - ;; - # Dev dependencies - "eslint-config-sdk" | "eslint-plugin-sdk" | "types" | "typescript") - continue - ;; - # Everything else - *) - echo "Deleting ${package}" - rm -rf ${package} - PACKAGES_DELETED=true - ;; - esac -done - -echo " " - -# If we deleted anything, commit the result -if [ "$PACKAGES_DELETED" = true ]; then - echo "Committing deletions. Don't forget to push this commit before you deploy." - git add . - git commit -m "delete unneeded packages" -else - echo "Branch already set up for vercel deployment" -fi - -# Restore working directory, if necessary -if [ -n "${STASHED_CHANGES}" ]; then - echo " " - echo "Restoring changes from earlier stash:" - git stash pop --quiet - git status --porcelain - echo " " -fi - -cd nextjs From 0be98947dbe7ba6118fd8f618157bebfef888de0 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 1 Aug 2024 10:39:57 -0400 Subject: [PATCH 07/14] feat(cloudflare): Add Cloudflare D1 instrumentation (#13142) This PR adds a new method to the cloudflare SDK, `instrumentD1WithSentry`. This method can be used to instrument [Cloudflare D1](https://developers.cloudflare.com/d1/), Cloudflare's serverless SQL database. ```js // 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(); ``` The reason this has to be a standalone wrapper method instead of an integration is because the cloudflare d1 instance can be bound to any arbitrary environmental variable as per user config. This is why the snippet above shows `env.DB` being passed into `instrumentD1WithSentry`. `env.DB` can easily be `env.COOL_DB` or `env.HAPPY_DB`. I am planning to ask the cloudflare team to expose some APIs to make this better, but in the meantime this is the best we can do. --- packages/cloudflare/README.md | 12 ++ packages/cloudflare/src/d1.ts | 154 +++++++++++++++++ packages/cloudflare/src/index.ts | 2 + packages/cloudflare/test/d1.test.ts | 253 ++++++++++++++++++++++++++++ 4 files changed, 421 insertions(+) create mode 100644 packages/cloudflare/src/d1.ts create mode 100644 packages/cloudflare/test/d1.test.ts diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index dc0d6de01274..7c7512e2ed1d 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -136,3 +136,15 @@ Sentry.captureEvent({ ], }); ``` + +## Cloudflare D1 Instrumentation + +You can use the `instrumentD1WithSentry` method to instrument [Cloudflare D1](https://developers.cloudflare.com/d1/), +Cloudflare's serverless SQL database with Sentry. + +```javascript +// 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(); +``` diff --git a/packages/cloudflare/src/d1.ts b/packages/cloudflare/src/d1.ts new file mode 100644 index 000000000000..a67574fabb9e --- /dev/null +++ b/packages/cloudflare/src/d1.ts @@ -0,0 +1,154 @@ +import type { D1Database, D1PreparedStatement, D1Response } from '@cloudflare/workers-types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, addBreadcrumb, startSpan } from '@sentry/core'; +import type { Span, SpanAttributes, StartSpanOptions } from '@sentry/types'; + +// Patching is based on internal Cloudflare D1 API +// https://github.com/cloudflare/workerd/blob/cd5279e7b305003f1d9c851e73efa9d67e4b68b2/src/cloudflare/internal/d1-api.ts + +const patchedStatement = new WeakSet(); + +/** + * Patches the query methods of a Cloudflare D1 prepared statement with Sentry. + */ +function instrumentD1PreparedStatementQueries(statement: D1PreparedStatement, query: string): D1PreparedStatement { + if (patchedStatement.has(statement)) { + return statement; + } + + // eslint-disable-next-line @typescript-eslint/unbound-method + statement.first = new Proxy(statement.first, { + apply(target, thisArg, args: Parameters) { + return startSpan(createStartSpanOptions(query, 'first'), async () => { + const res = await Reflect.apply(target, thisArg, args); + createD1Breadcrumb(query, 'first'); + return res; + }); + }, + }); + + // eslint-disable-next-line @typescript-eslint/unbound-method + statement.run = new Proxy(statement.run, { + apply(target, thisArg, args: Parameters) { + return startSpan(createStartSpanOptions(query, 'run'), async span => { + const d1Response = await Reflect.apply(target, thisArg, args); + applyD1ReturnObjectToSpan(span, d1Response); + createD1Breadcrumb(query, 'run', d1Response); + return d1Response; + }); + }, + }); + + // eslint-disable-next-line @typescript-eslint/unbound-method + statement.all = new Proxy(statement.all, { + apply(target, thisArg, args: Parameters) { + return startSpan(createStartSpanOptions(query, 'all'), async span => { + const d1Result = await Reflect.apply(target, thisArg, args); + applyD1ReturnObjectToSpan(span, d1Result); + createD1Breadcrumb(query, 'all', d1Result); + return d1Result; + }); + }, + }); + + // eslint-disable-next-line @typescript-eslint/unbound-method + statement.raw = new Proxy(statement.raw, { + apply(target, thisArg, args: Parameters) { + return startSpan(createStartSpanOptions(query, 'raw'), async () => { + const res = await Reflect.apply(target, thisArg, args); + createD1Breadcrumb(query, 'raw'); + return res; + }); + }, + }); + + patchedStatement.add(statement); + + return statement; +} + +/** + * Instruments a Cloudflare D1 prepared statement with Sentry. + * + * This is meant to be used as a top-level call, under the hood it calls `instrumentD1PreparedStatementQueries` + * to patch the query methods. The reason for this abstraction is to ensure that the `bind` method is also patched. + */ +function instrumentD1PreparedStatement(statement: D1PreparedStatement, query: string): D1PreparedStatement { + // statement.bind() returns a new instance of D1PreparedStatement, so we have to patch it as well. + // eslint-disable-next-line @typescript-eslint/unbound-method + statement.bind = new Proxy(statement.bind, { + apply(target, thisArg, args: Parameters) { + return instrumentD1PreparedStatementQueries(Reflect.apply(target, thisArg, args), query); + }, + }); + + return instrumentD1PreparedStatementQueries(statement, query); +} + +/** + * Add D1Response meta information to a span. + * + * See: https://developers.cloudflare.com/d1/build-with-d1/d1-client-api/#return-object + */ +function applyD1ReturnObjectToSpan(span: Span, d1Result: D1Response): void { + if (!d1Result.success) { + span.setStatus({ code: SPAN_STATUS_ERROR }); + } + + span.setAttributes(getAttributesFromD1Response(d1Result)); +} + +function getAttributesFromD1Response(d1Result: D1Response): SpanAttributes { + return { + 'cloudflare.d1.duration': d1Result.meta.duration, + 'cloudflare.d1.rows_read': d1Result.meta.rows_read, + 'cloudflare.d1.rows_written': d1Result.meta.rows_written, + }; +} + +function createD1Breadcrumb(query: string, type: 'first' | 'run' | 'all' | 'raw', d1Result?: D1Response): void { + addBreadcrumb({ + category: 'query', + message: query, + data: { + ...(d1Result ? getAttributesFromD1Response(d1Result) : {}), + 'cloudflare.d1.query_type': type, + }, + }); +} + +function createStartSpanOptions(query: string, type: 'first' | 'run' | 'all' | 'raw'): StartSpanOptions { + return { + op: 'db.query', + name: query, + attributes: { + 'cloudflare.d1.query_type': type, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.cloudflare.d1', + }, + }; +} + +/** + * Instruments Cloudflare D1 bindings with Sentry. + * + * Currently, only prepared statements are instrumented. `db.exec` and `db.batch` are not instrumented. + * + * @example + * + * ```js + * // 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(); + * ``` + */ +export function instrumentD1WithSentry(db: D1Database): D1Database { + // eslint-disable-next-line @typescript-eslint/unbound-method + db.prepare = new Proxy(db.prepare, { + apply(target, thisArg, args: Parameters) { + const [query] = args; + return instrumentD1PreparedStatement(Reflect.apply(target, thisArg, args), query); + }, + }); + + return db; +} diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 3708d3ae9382..a4a466fa5bb5 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -91,3 +91,5 @@ export { CloudflareClient } from './client'; export { getDefaultIntegrations } from './sdk'; export { fetchIntegration } from './integrations/fetch'; + +export { instrumentD1WithSentry } from './d1'; diff --git a/packages/cloudflare/test/d1.test.ts b/packages/cloudflare/test/d1.test.ts new file mode 100644 index 000000000000..c86538b96208 --- /dev/null +++ b/packages/cloudflare/test/d1.test.ts @@ -0,0 +1,253 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; + +import * as SentryCore from '@sentry/core'; + +import type { D1Database, D1PreparedStatement } from '@cloudflare/workers-types'; + +import { instrumentD1WithSentry } from '../src/d1'; + +const MOCK_FIRST_RETURN_VALUE = { id: 1, name: 'Foo' }; + +const MOCK_RAW_RETURN_VALUE = [ + { id: 1, name: 'Foo' }, + { id: 2, name: 'Bar' }, +]; + +const MOCK_D1_RESPONSE = { + success: true, + meta: { + duration: 1, + size_after: 2, + rows_read: 3, + rows_written: 4, + last_row_id: 5, + changed_db: false, + changes: 7, + }, +}; + +describe('instrumentD1WithSentry', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + const startSpanSpy = vi.spyOn(SentryCore, 'startSpan'); + const addBreadcrumbSpy = vi.spyOn(SentryCore, 'addBreadcrumb'); + + function createMockD1Statement(): D1PreparedStatement { + return { + bind: vi.fn().mockImplementation(createMockD1Statement), + first: vi.fn().mockImplementation(() => Promise.resolve(MOCK_FIRST_RETURN_VALUE)), + run: vi.fn().mockImplementation(() => Promise.resolve(MOCK_D1_RESPONSE)), + all: vi.fn().mockImplementation(() => Promise.resolve(MOCK_D1_RESPONSE)), + raw: vi.fn().mockImplementation(() => Promise.resolve(MOCK_RAW_RETURN_VALUE)), + }; + } + + function createMockD1Database(): D1Database { + return { + prepare: vi.fn().mockImplementation(createMockD1Statement), + dump: vi.fn(), + batch: vi.fn(), + exec: vi.fn(), + }; + } + + describe('statement.first()', () => { + test('does not change return value', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + const response = await instrumentedDb.prepare('SELECT * FROM users').first(); + expect(response).toEqual(MOCK_FIRST_RETURN_VALUE); + }); + + test('instruments with spans', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('SELECT * FROM users').first(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + { + attributes: { + 'cloudflare.d1.query_type': 'first', + 'sentry.origin': 'auto.db.cloudflare.d1', + }, + name: 'SELECT * FROM users', + op: 'db.query', + }, + expect.any(Function), + ); + }); + + test('instruments with breadcrumbs', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('SELECT * FROM users').first(); + + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenLastCalledWith({ + category: 'query', + message: 'SELECT * FROM users', + data: { + 'cloudflare.d1.query_type': 'first', + }, + }); + }); + + test('works with statement.bind()', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('SELECT * FROM users').bind().first(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('statement.run()', () => { + test('does not change return value', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + const response = await instrumentedDb.prepare('INSERT INTO users (name) VALUES (?)').run(); + expect(response).toEqual(MOCK_D1_RESPONSE); + }); + + test('instruments with spans', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('INSERT INTO users (name) VALUES (?)').run(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + { + attributes: { + 'cloudflare.d1.query_type': 'run', + 'sentry.origin': 'auto.db.cloudflare.d1', + }, + name: 'INSERT INTO users (name) VALUES (?)', + op: 'db.query', + }, + expect.any(Function), + ); + }); + + test('instruments with breadcrumbs', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('INSERT INTO users (name) VALUES (?)').run(); + + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenLastCalledWith({ + category: 'query', + message: 'INSERT INTO users (name) VALUES (?)', + data: { + 'cloudflare.d1.query_type': 'run', + 'cloudflare.d1.duration': 1, + 'cloudflare.d1.rows_read': 3, + 'cloudflare.d1.rows_written': 4, + }, + }); + }); + + test('works with statement.bind()', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('SELECT * FROM users').bind().run(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('statement.all()', () => { + test('does not change return value', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + const response = await instrumentedDb.prepare('INSERT INTO users (name) VALUES (?)').run(); + expect(response).toEqual(MOCK_D1_RESPONSE); + }); + + test('instruments with spans', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('INSERT INTO users (name) VALUES (?)').all(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + { + attributes: { + 'cloudflare.d1.query_type': 'all', + 'sentry.origin': 'auto.db.cloudflare.d1', + }, + name: 'INSERT INTO users (name) VALUES (?)', + op: 'db.query', + }, + expect.any(Function), + ); + }); + + test('instruments with breadcrumbs', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('INSERT INTO users (name) VALUES (?)').all(); + + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenLastCalledWith({ + category: 'query', + message: 'INSERT INTO users (name) VALUES (?)', + data: { + 'cloudflare.d1.query_type': 'all', + 'cloudflare.d1.duration': 1, + 'cloudflare.d1.rows_read': 3, + 'cloudflare.d1.rows_written': 4, + }, + }); + }); + + test('works with statement.bind()', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('SELECT * FROM users').bind().all(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('statement.raw()', () => { + test('does not change return value', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + const response = await instrumentedDb.prepare('SELECT * FROM users').raw(); + expect(response).toEqual(MOCK_RAW_RETURN_VALUE); + }); + + test('instruments with spans', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('SELECT * FROM users').raw(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(startSpanSpy).toHaveBeenLastCalledWith( + { + attributes: { + 'cloudflare.d1.query_type': 'raw', + 'sentry.origin': 'auto.db.cloudflare.d1', + }, + name: 'SELECT * FROM users', + op: 'db.query', + }, + expect.any(Function), + ); + }); + + test('instruments with breadcrumbs', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('SELECT * FROM users').raw(); + + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenLastCalledWith({ + category: 'query', + message: 'SELECT * FROM users', + data: { + 'cloudflare.d1.query_type': 'raw', + }, + }); + }); + + test('works with statement.bind()', async () => { + const instrumentedDb = instrumentD1WithSentry(createMockD1Database()); + await instrumentedDb.prepare('SELECT * FROM users').bind().raw(); + + expect(startSpanSpy).toHaveBeenCalledTimes(1); + expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); + }); + }); +}); From 6285808eb60afff0356c053a09593d5c77f3fec6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 1 Aug 2024 10:40:16 -0400 Subject: [PATCH 08/14] ref(cloudflare): Clean up pages plugin docs comment (#13147) Leftover from auto-generated JSDoc, can be removed --- packages/cloudflare/src/pages-plugin.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/cloudflare/src/pages-plugin.ts b/packages/cloudflare/src/pages-plugin.ts index 7f7070ddfbf7..f2c46efd86f2 100644 --- a/packages/cloudflare/src/pages-plugin.ts +++ b/packages/cloudflare/src/pages-plugin.ts @@ -17,9 +17,6 @@ import { wrapRequestHandler } from './request'; * tracesSampleRate: 1.0, * }); * ``` - * - * @param _options - * @returns */ export function sentryPagesPlugin< Env = unknown, From 21b0dae13e61bfae958f30bb0949f7c59480f8a3 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 1 Aug 2024 11:37:11 -0400 Subject: [PATCH 09/14] test(browser): Switch from jest to vitest (#13092) Before: `Time: 10.517 s` After: `Duration 3.65s (transform 2.76s, setup 7ms, collect 15.44s, tests 1.36s, environment 4.96s, prepare 3.63s)` We also change the folder structure of the browser unit tests, because we've removed the in-package integration tests. This change also removes `environment: 'jsdom'` from the central config in favour of explicitly adding jsdom environment via the `@vitest-environment` pragma to the specific test file that needs it. This should means that our tests are not polluted with jsdom globals, and that future writers have to explicitly opt-in to the behaviour. --- packages/browser/jest.config.js | 7 - packages/browser/package.json | 5 +- .../test/{unit => }/eventbuilder.test.ts | 17 ++- .../helper/browser-client-options.ts | 2 +- .../{unit => }/index.bundle.feedback.test.ts | 6 +- .../{unit => }/index.bundle.replay.test.ts | 6 +- .../test/{unit => }/index.bundle.test.ts | 4 +- ...dex.bundle.tracing.replay.feedback.test.ts | 6 +- .../index.bundle.tracing.replay.test.ts | 6 +- .../{unit => }/index.bundle.tracing.test.ts | 6 +- .../browser/test/{unit => }/index.test.ts | 123 ++++++++++-------- .../integrations/breadcrumbs.test.ts | 6 +- .../integrations/contextlines.test.ts | 4 +- .../{unit => }/integrations/helpers.test.ts | 8 +- .../integrations/reportingobserver.test.ts | 22 ++-- .../test/{unit => }/mocks/simpletransport.ts | 0 .../{unit => }/profiling/integration.test.ts | 14 +- .../test/{unit => }/profiling/utils.test.ts | 10 +- packages/browser/test/{unit => }/sdk.test.ts | 58 +++++---- .../test/{unit => }/tracekit/chromium.test.ts | 6 +- .../test/{unit => }/tracekit/firefox.test.ts | 6 +- .../test/{unit => }/tracekit/ie.test.ts | 6 +- .../test/{unit => }/tracekit/misc.test.ts | 6 +- .../test/{unit => }/tracekit/opera.test.ts | 6 +- .../{unit => }/tracekit/react-native.test.ts | 6 +- .../test/{unit => }/tracekit/react.test.ts | 6 +- .../test/{unit => }/tracekit/safari.test.ts | 6 +- .../{unit => }/tracing/backgroundtab.test.ts | 12 +- .../tracing/browserTracingIntegration.test.ts | 77 +++++------ .../test/{unit => }/tracing/request.test.ts | 24 ++-- .../test/{unit => }/transports/fetch.test.ts | 21 +-- .../{unit => }/transports/offline.test.ts | 7 +- .../test/{unit => }/userfeedback.test.ts | 4 +- .../utils/lazyLoadIntegration.test.ts | 10 +- packages/browser/tsconfig.test.json | 6 +- packages/browser/vite.config.ts | 10 ++ 36 files changed, 303 insertions(+), 226 deletions(-) delete mode 100644 packages/browser/jest.config.js rename packages/browser/test/{unit => }/eventbuilder.test.ts (92%) rename packages/browser/test/{unit => }/helper/browser-client-options.ts (87%) rename packages/browser/test/{unit => }/index.bundle.feedback.test.ts (76%) rename packages/browser/test/{unit => }/index.bundle.replay.test.ts (77%) rename packages/browser/test/{unit => }/index.bundle.test.ts (84%) rename packages/browser/test/{unit => }/index.bundle.tracing.replay.feedback.test.ts (75%) rename packages/browser/test/{unit => }/index.bundle.tracing.replay.test.ts (81%) rename packages/browser/test/{unit => }/index.bundle.tracing.test.ts (75%) rename packages/browser/test/{unit => }/index.test.ts (83%) rename packages/browser/test/{unit => }/integrations/breadcrumbs.test.ts (82%) rename packages/browser/test/{unit => }/integrations/contextlines.test.ts (96%) rename packages/browser/test/{unit => }/integrations/helpers.test.ts (97%) rename packages/browser/test/{unit => }/integrations/reportingobserver.test.ts (94%) rename packages/browser/test/{unit => }/mocks/simpletransport.ts (100%) rename packages/browser/test/{unit => }/profiling/integration.test.ts (84%) rename packages/browser/test/{unit => }/profiling/utils.test.ts (94%) rename packages/browser/test/{unit => }/sdk.test.ts (78%) rename packages/browser/test/{unit => }/tracekit/chromium.test.ts (99%) rename packages/browser/test/{unit => }/tracekit/firefox.test.ts (98%) rename packages/browser/test/{unit => }/tracekit/ie.test.ts (95%) rename packages/browser/test/{unit => }/tracekit/misc.test.ts (96%) rename packages/browser/test/{unit => }/tracekit/opera.test.ts (98%) rename packages/browser/test/{unit => }/tracekit/react-native.test.ts (99%) rename packages/browser/test/{unit => }/tracekit/react.test.ts (96%) rename packages/browser/test/{unit => }/tracekit/safari.test.ts (98%) rename packages/browser/test/{unit => }/tracing/backgroundtab.test.ts (84%) rename packages/browser/test/{unit => }/tracing/browserTracingIntegration.test.ts (94%) rename packages/browser/test/{unit => }/tracing/request.test.ts (95%) rename packages/browser/test/{unit => }/transports/fetch.test.ts (90%) rename packages/browser/test/{unit => }/transports/offline.test.ts (96%) rename packages/browser/test/{unit => }/userfeedback.test.ts (92%) rename packages/browser/test/{unit => }/utils/lazyLoadIntegration.test.ts (92%) create mode 100644 packages/browser/vite.config.ts diff --git a/packages/browser/jest.config.js b/packages/browser/jest.config.js deleted file mode 100644 index f9cd8056a454..000000000000 --- a/packages/browser/jest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -const baseConfig = require('../../jest/jest.config.js'); - -module.exports = { - ...baseConfig, - testEnvironment: 'jsdom', - testMatch: ['/test/unit/**/*.test.ts'], -}; diff --git a/packages/browser/package.json b/packages/browser/package.json index ff719729e534..221f8a492a02 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -70,9 +70,8 @@ "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", "size:check": "cat build/bundles/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES2017: \",$1,\"kB\";}'", - "test": "yarn test:unit", - "test:unit": "jest", - "test:unit:watch": "jest --watch", + "test": "vitest run", + "test:watch": "vitest --watch", "yalc:publish": "yalc publish --push --sig" }, "volta": { diff --git a/packages/browser/test/unit/eventbuilder.test.ts b/packages/browser/test/eventbuilder.test.ts similarity index 92% rename from packages/browser/test/unit/eventbuilder.test.ts rename to packages/browser/test/eventbuilder.test.ts index e28b283107c3..0f43e7495efd 100644 --- a/packages/browser/test/unit/eventbuilder.test.ts +++ b/packages/browser/test/eventbuilder.test.ts @@ -1,10 +1,15 @@ -import { defaultStackParser } from '../../src'; -import { eventFromUnknownInput } from '../../src/eventbuilder'; +/** + * @vitest-environment jsdom + */ -jest.mock('@sentry/core', () => { - const original = jest.requireActual('@sentry/core'); +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { defaultStackParser } from '../src'; +import { eventFromUnknownInput } from '../src/eventbuilder'; + +vi.mock('@sentry/core', async requireActual => { return { - ...original, + ...((await requireActual()) as any), getClient() { return { getOptions(): any { @@ -21,7 +26,7 @@ class MyTestClass { } afterEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); describe('eventFromUnknownInput', () => { diff --git a/packages/browser/test/unit/helper/browser-client-options.ts b/packages/browser/test/helper/browser-client-options.ts similarity index 87% rename from packages/browser/test/unit/helper/browser-client-options.ts rename to packages/browser/test/helper/browser-client-options.ts index 867e6a9e6e6e..619baab13bad 100644 --- a/packages/browser/test/unit/helper/browser-client-options.ts +++ b/packages/browser/test/helper/browser-client-options.ts @@ -1,7 +1,7 @@ import { createTransport } from '@sentry/core'; import { resolvedSyncPromise } from '@sentry/utils'; -import type { BrowserClientOptions } from '../../../src/client'; +import type { BrowserClientOptions } from '../../src/client'; export function getDefaultBrowserClientOptions(options: Partial = {}): BrowserClientOptions { return { diff --git a/packages/browser/test/unit/index.bundle.feedback.test.ts b/packages/browser/test/index.bundle.feedback.test.ts similarity index 76% rename from packages/browser/test/unit/index.bundle.feedback.test.ts rename to packages/browser/test/index.bundle.feedback.test.ts index 99516cca295e..47beaa33d07c 100644 --- a/packages/browser/test/unit/index.bundle.feedback.test.ts +++ b/packages/browser/test/index.bundle.feedback.test.ts @@ -1,7 +1,9 @@ +import { describe, expect, it } from 'vitest'; + import { browserTracingIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; -import { feedbackAsyncIntegration } from '../../src'; +import { feedbackAsyncIntegration } from '../src'; -import * as FeedbackBundle from '../../src/index.bundle.feedback'; +import * as FeedbackBundle from '../src/index.bundle.feedback'; describe('index.bundle.feedback', () => { it('has correct exports', () => { diff --git a/packages/browser/test/unit/index.bundle.replay.test.ts b/packages/browser/test/index.bundle.replay.test.ts similarity index 77% rename from packages/browser/test/unit/index.bundle.replay.test.ts rename to packages/browser/test/index.bundle.replay.test.ts index 0fdbf95fe3e4..7efbe9ffb5b8 100644 --- a/packages/browser/test/unit/index.bundle.replay.test.ts +++ b/packages/browser/test/index.bundle.replay.test.ts @@ -1,7 +1,9 @@ +import { describe, expect, it } from 'vitest'; + import { browserTracingIntegrationShim, feedbackIntegrationShim } from '@sentry-internal/integration-shims'; -import { replayIntegration } from '../../src'; +import { replayIntegration } from '../src'; -import * as ReplayBundle from '../../src/index.bundle.replay'; +import * as ReplayBundle from '../src/index.bundle.replay'; describe('index.bundle.replay', () => { it('has correct exports', () => { diff --git a/packages/browser/test/unit/index.bundle.test.ts b/packages/browser/test/index.bundle.test.ts similarity index 84% rename from packages/browser/test/unit/index.bundle.test.ts rename to packages/browser/test/index.bundle.test.ts index 1535d74d6b6a..4b4618e2f4eb 100644 --- a/packages/browser/test/unit/index.bundle.test.ts +++ b/packages/browser/test/index.bundle.test.ts @@ -1,10 +1,12 @@ +import { describe, expect, it } from 'vitest'; + import { browserTracingIntegrationShim, feedbackIntegrationShim, replayIntegrationShim, } from '@sentry-internal/integration-shims'; -import * as Bundle from '../../src/index.bundle'; +import * as Bundle from '../src/index.bundle'; describe('index.bundle', () => { it('has correct exports', () => { diff --git a/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts similarity index 75% rename from packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts rename to packages/browser/test/index.bundle.tracing.replay.feedback.test.ts index 2d62f247a1da..e5a454b532e9 100644 --- a/packages/browser/test/unit/index.bundle.tracing.replay.feedback.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts @@ -1,6 +1,8 @@ -import { browserTracingIntegration, feedbackAsyncIntegration, replayIntegration } from '../../src'; +import { describe, expect, it } from 'vitest'; -import * as TracingReplayFeedbackBundle from '../../src/index.bundle.tracing.replay.feedback'; +import { browserTracingIntegration, feedbackAsyncIntegration, replayIntegration } from '../src'; + +import * as TracingReplayFeedbackBundle from '../src/index.bundle.tracing.replay.feedback'; describe('index.bundle.tracing.replay.feedback', () => { it('has correct exports', () => { diff --git a/packages/browser/test/unit/index.bundle.tracing.replay.test.ts b/packages/browser/test/index.bundle.tracing.replay.test.ts similarity index 81% rename from packages/browser/test/unit/index.bundle.tracing.replay.test.ts rename to packages/browser/test/index.bundle.tracing.replay.test.ts index 2cd3f5dca0f0..fa81d1b19aae 100644 --- a/packages/browser/test/unit/index.bundle.tracing.replay.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.test.ts @@ -1,7 +1,9 @@ +import { describe, expect, it } from 'vitest'; + import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; -import { browserTracingIntegration, replayIntegration } from '../../src'; +import { browserTracingIntegration, replayIntegration } from '../src'; -import * as TracingReplayBundle from '../../src/index.bundle.tracing.replay'; +import * as TracingReplayBundle from '../src/index.bundle.tracing.replay'; describe('index.bundle.tracing.replay', () => { it('has correct exports', () => { diff --git a/packages/browser/test/unit/index.bundle.tracing.test.ts b/packages/browser/test/index.bundle.tracing.test.ts similarity index 75% rename from packages/browser/test/unit/index.bundle.tracing.test.ts rename to packages/browser/test/index.bundle.tracing.test.ts index 942d185b2e91..dc786fd31a61 100644 --- a/packages/browser/test/unit/index.bundle.tracing.test.ts +++ b/packages/browser/test/index.bundle.tracing.test.ts @@ -1,7 +1,9 @@ +import { describe, expect, it } from 'vitest'; + import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; -import { browserTracingIntegration } from '../../src'; +import { browserTracingIntegration } from '../src'; -import * as TracingBundle from '../../src/index.bundle.tracing'; +import * as TracingBundle from '../src/index.bundle.tracing'; describe('index.bundle.tracing', () => { it('has correct exports', () => { diff --git a/packages/browser/test/unit/index.test.ts b/packages/browser/test/index.test.ts similarity index 83% rename from packages/browser/test/unit/index.test.ts rename to packages/browser/test/index.test.ts index 34c4845cac13..eb2fb6104b11 100644 --- a/packages/browser/test/unit/index.test.ts +++ b/packages/browser/test/index.test.ts @@ -1,3 +1,10 @@ +/** + * @vitest-environment jsdom + */ + +import type { Mock } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { SDK_VERSION, getGlobalScope, @@ -8,7 +15,7 @@ import { } from '@sentry/core'; import * as utils from '@sentry/utils'; -import { setCurrentClient } from '../../src'; +import { setCurrentClient } from '../src'; import { BrowserClient, Scope, @@ -22,7 +29,7 @@ import { getCurrentScope, init, showReportDialog, -} from '../../src'; +} from '../src'; import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; import { makeSimpleTransport } from './mocks/simpletransport'; @@ -31,16 +38,15 @@ const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; // eslint-disable-next-line no-var declare var global: any; -jest.mock('@sentry/core', () => { - const original = jest.requireActual('@sentry/core'); +vi.mock('@sentry/core', async requireActual => { return { - ...original, - getReportDialogEndpoint: jest.fn(), + ...((await requireActual()) as any), + getReportDialogEndpoint: vi.fn(), }; }); describe('SentryBrowser', () => { - const beforeSend = jest.fn(event => event); + const beforeSend = vi.fn(event => event); beforeEach(() => { getGlobalScope().clear(); @@ -84,7 +90,7 @@ describe('SentryBrowser', () => { describe('showReportDialog', () => { beforeEach(() => { - (getReportDialogEndpoint as jest.Mock).mockReset(); + (getReportDialogEndpoint as Mock).mockReset(); }); describe('user', () => { @@ -145,14 +151,14 @@ describe('SentryBrowser', () => { }); describe('onClose', () => { - const dummyErrorHandler = jest.fn(); + const dummyErrorHandler = vi.fn(); beforeEach(() => { - // this prevents jest-environment-jsdom from failing the test + // this prevents vi-environment-jsdom from failing the test // when an error in `onClose` is thrown // it does not prevent errors thrown directly inside the test, // so we don't have to worry about tests passing that should // otherwise fail - // see: https://github.com/jestjs/jest/blob/main/packages/jest-environment-jsdom/src/index.ts#L95-L115 + // see: https://github.com/vijs/vi/blob/main/packages/vi-environment-jsdom/src/index.ts#L95-L115 WINDOW.addEventListener('error', dummyErrorHandler); }); @@ -166,7 +172,7 @@ describe('SentryBrowser', () => { }; it('should call `onClose` when receiving `__sentry_reportdialog_closed__` MessageEvent', async () => { - const onClose = jest.fn(); + const onClose = vi.fn(); showReportDialog({ onClose }); @@ -179,7 +185,7 @@ describe('SentryBrowser', () => { }); it('should call `onClose` only once even if it throws', async () => { - const onClose = jest.fn(() => { + const onClose = vi.fn(() => { throw new Error(); }); @@ -194,7 +200,7 @@ describe('SentryBrowser', () => { }); it('should not call `onClose` for other MessageEvents', async () => { - const onClose = jest.fn(); + const onClose = vi.fn(); showReportDialog({ onClose }); @@ -236,49 +242,52 @@ describe('SentryBrowser', () => { expect(event.exception.values[0]?.stacktrace.frames).not.toHaveLength(0); }); - it('should capture a message', done => { - const options = getDefaultBrowserClientOptions({ - beforeSend: event => { - expect(event.message).toBe('test'); - expect(event.exception).toBeUndefined(); - done(); - return event; - }, - dsn, - }); - setCurrentClient(new BrowserClient(options)); - captureMessage('test'); - }); - - it('should capture an event', done => { - const options = getDefaultBrowserClientOptions({ - beforeSend: event => { - expect(event.message).toBe('event'); - expect(event.exception).toBeUndefined(); - done(); - return event; - }, - dsn, - }); - setCurrentClient(new BrowserClient(options)); - captureEvent({ message: 'event' }); - }); - - it('should set `platform` on events', done => { - const options = getDefaultBrowserClientOptions({ - beforeSend: event => { - expect(event.platform).toBe('javascript'); - done(); - return event; - }, - dsn, - }); - setCurrentClient(new BrowserClient(options)); - captureEvent({ message: 'event' }); - }); + it('should capture a message', () => + new Promise(resolve => { + const options = getDefaultBrowserClientOptions({ + beforeSend: event => { + expect(event.message).toBe('test'); + expect(event.exception).toBeUndefined(); + resolve(); + return event; + }, + dsn, + }); + setCurrentClient(new BrowserClient(options)); + captureMessage('test'); + })); + + it('should capture an event', () => + new Promise(resolve => { + const options = getDefaultBrowserClientOptions({ + beforeSend: event => { + expect(event.message).toBe('event'); + expect(event.exception).toBeUndefined(); + resolve(); + return event; + }, + dsn, + }); + setCurrentClient(new BrowserClient(options)); + captureEvent({ message: 'event' }); + })); + + it('should set `platform` on events', () => + new Promise(resolve => { + const options = getDefaultBrowserClientOptions({ + beforeSend: event => { + expect(event.platform).toBe('javascript'); + resolve(); + return event; + }, + dsn, + }); + setCurrentClient(new BrowserClient(options)); + captureEvent({ message: 'event' }); + })); it('should not dedupe an event on bound client', async () => { - const localBeforeSend = jest.fn(); + const localBeforeSend = vi.fn(); const options = getDefaultBrowserClientOptions({ beforeSend: localBeforeSend, dsn, @@ -295,7 +304,7 @@ describe('SentryBrowser', () => { }); it('should use inboundfilter rules of bound client', async () => { - const localBeforeSend = jest.fn(); + const localBeforeSend = vi.fn(); const options = getDefaultBrowserClientOptions({ beforeSend: localBeforeSend, dsn, @@ -374,7 +383,7 @@ describe('SentryBrowser initialization', () => { }); it('uses SDK source from global for package name', () => { - const spy = jest.spyOn(utils, 'getSDKSource').mockReturnValue('cdn'); + const spy = vi.spyOn(utils, 'getSDKSource').mockReturnValue('cdn'); init({ dsn }); const sdkData = getClient()?.getOptions()._metadata?.sdk || {}; diff --git a/packages/browser/test/unit/integrations/breadcrumbs.test.ts b/packages/browser/test/integrations/breadcrumbs.test.ts similarity index 82% rename from packages/browser/test/unit/integrations/breadcrumbs.test.ts rename to packages/browser/test/integrations/breadcrumbs.test.ts index 15ad5f3e4261..770578ef77b5 100644 --- a/packages/browser/test/unit/integrations/breadcrumbs.test.ts +++ b/packages/browser/test/integrations/breadcrumbs.test.ts @@ -1,6 +1,8 @@ +import { describe, expect, it, vi } from 'vitest'; + import * as SentryCore from '@sentry/core'; -import { BrowserClient, breadcrumbsIntegration, flush } from '../../../src'; +import { BrowserClient, breadcrumbsIntegration, flush } from '../../src'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; describe('Breadcrumbs', () => { @@ -14,7 +16,7 @@ describe('Breadcrumbs', () => { SentryCore.setCurrentClient(client); client.init(); - const addBreadcrumbSpy = jest.spyOn(SentryCore, 'addBreadcrumb').mockImplementation(() => {}); + const addBreadcrumbSpy = vi.spyOn(SentryCore, 'addBreadcrumb').mockImplementation(() => {}); client.captureMessage('test'); await flush(2000); diff --git a/packages/browser/test/unit/integrations/contextlines.test.ts b/packages/browser/test/integrations/contextlines.test.ts similarity index 96% rename from packages/browser/test/unit/integrations/contextlines.test.ts rename to packages/browser/test/integrations/contextlines.test.ts index 5f19bd2b41a7..783cc36fec89 100644 --- a/packages/browser/test/unit/integrations/contextlines.test.ts +++ b/packages/browser/test/integrations/contextlines.test.ts @@ -1,6 +1,8 @@ +import { describe, expect, it } from 'vitest'; + import type { StackFrame } from '@sentry/types'; -import { applySourceContextToFrame } from '../../../src/integrations/contextlines'; +import { applySourceContextToFrame } from '../../src/integrations/contextlines'; const lines = ['line1', 'line2', 'line3', 'line4', 'line5', 'line6', 'line7', 'line8', 'line9']; describe('ContextLines', () => { diff --git a/packages/browser/test/unit/integrations/helpers.test.ts b/packages/browser/test/integrations/helpers.test.ts similarity index 97% rename from packages/browser/test/unit/integrations/helpers.test.ts rename to packages/browser/test/integrations/helpers.test.ts index 080ab4c5cd0f..37806e06f8a9 100644 --- a/packages/browser/test/unit/integrations/helpers.test.ts +++ b/packages/browser/test/integrations/helpers.test.ts @@ -1,6 +1,8 @@ +import { describe, expect, it, vi } from 'vitest'; + import type { WrappedFunction } from '@sentry/types'; -import { wrap } from '../../../src/helpers'; +import { wrap } from '../../src/helpers'; describe('internal wrap()', () => { it('should wrap only functions', () => { @@ -56,7 +58,7 @@ describe('internal wrap()', () => { it('calls "before" function when invoking wrapped function', () => { const fn = (() => 1337) as WrappedFunction; - const before = jest.fn(); + const before = vi.fn(); const wrapped = wrap(fn, {}, before); wrapped(); @@ -115,7 +117,7 @@ describe('internal wrap()', () => { }); it('calls the original function', () => { - const fn = jest.fn(); + const fn = vi.fn(); wrap(fn)(123, 'Rick'); diff --git a/packages/browser/test/unit/integrations/reportingobserver.test.ts b/packages/browser/test/integrations/reportingobserver.test.ts similarity index 94% rename from packages/browser/test/unit/integrations/reportingobserver.test.ts rename to packages/browser/test/integrations/reportingobserver.test.ts index aba669286f44..9571d59d03d0 100644 --- a/packages/browser/test/unit/integrations/reportingobserver.test.ts +++ b/packages/browser/test/integrations/reportingobserver.test.ts @@ -1,20 +1,22 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import * as SentryCore from '@sentry/core'; import type { Client } from '@sentry/types'; -import { reportingObserverIntegration } from '../../../src/integrations/reportingobserver'; +import { reportingObserverIntegration } from '../../src/integrations/reportingobserver'; const mockScope = { - setExtra: jest.fn(), + setExtra: vi.fn(), }; -const withScope = jest.fn(callback => { +const withScope = vi.fn(callback => { return callback(mockScope); }); -const captureMessage = jest.fn(); +const captureMessage = vi.fn(); -const mockReportingObserverConstructor = jest.fn(); -const mockObserve = jest.fn(); +const mockReportingObserverConstructor = vi.fn(); +const mockObserve = vi.fn(); class MockReportingObserver { public observe: () => void = mockObserve; @@ -32,13 +34,13 @@ describe('ReportingObserver', () => { mockClient = {} as Client; - jest.spyOn(SentryCore, 'captureMessage').mockImplementation(captureMessage); - jest.spyOn(SentryCore, 'getClient').mockImplementation(() => mockClient); - jest.spyOn(SentryCore, 'withScope').mockImplementation(withScope); + vi.spyOn(SentryCore, 'captureMessage').mockImplementation(captureMessage); + vi.spyOn(SentryCore, 'getClient').mockImplementation(() => mockClient); + vi.spyOn(SentryCore, 'withScope').mockImplementation(withScope); }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); delete (global as any).ReportingObserver; }); diff --git a/packages/browser/test/unit/mocks/simpletransport.ts b/packages/browser/test/mocks/simpletransport.ts similarity index 100% rename from packages/browser/test/unit/mocks/simpletransport.ts rename to packages/browser/test/mocks/simpletransport.ts diff --git a/packages/browser/test/unit/profiling/integration.test.ts b/packages/browser/test/profiling/integration.test.ts similarity index 84% rename from packages/browser/test/unit/profiling/integration.test.ts rename to packages/browser/test/profiling/integration.test.ts index b5c4ad7c82a5..2f653563fc1a 100644 --- a/packages/browser/test/unit/profiling/integration.test.ts +++ b/packages/browser/test/profiling/integration.test.ts @@ -1,11 +1,17 @@ +/** + * @vitest-environment jsdom + */ + +import { describe, expect, it, vi } from 'vitest'; + import type { BrowserClient } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; -import type { JSSelfProfile } from '../../../src/profiling/jsSelfProfiling'; +import type { JSSelfProfile } from '../../src/profiling/jsSelfProfiling'; describe('BrowserProfilingIntegration', () => { it('pageload profiles follow regular transaction code path', async () => { - const stopProfile = jest.fn().mockImplementation((): Promise => { + const stopProfile = vi.fn().mockImplementation((): Promise => { return Promise.resolve({ frames: [{ name: 'pageload_fn', line: 1, column: 1 }], stacks: [{ frameId: 0, parentId: undefined }], @@ -30,8 +36,8 @@ describe('BrowserProfilingIntegration', () => { // @ts-expect-error this is a mock constructor window.Profiler = MockProfiler; - const flush = jest.fn().mockImplementation(() => Promise.resolve(true)); - const send = jest.fn().mockImplementation(() => Promise.resolve()); + const flush = vi.fn().mockImplementation(() => Promise.resolve(true)); + const send = vi.fn().mockImplementation(() => Promise.resolve()); Sentry.init({ tracesSampleRate: 1, profilesSampleRate: 1, diff --git a/packages/browser/test/unit/profiling/utils.test.ts b/packages/browser/test/profiling/utils.test.ts similarity index 94% rename from packages/browser/test/unit/profiling/utils.test.ts rename to packages/browser/test/profiling/utils.test.ts index 573d9dbb335e..27e81c3208dc 100644 --- a/packages/browser/test/unit/profiling/utils.test.ts +++ b/packages/browser/test/profiling/utils.test.ts @@ -1,3 +1,9 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'; + import { TextDecoder, TextEncoder } from 'util'; const patchedEncoder = (!global.window.TextEncoder && (global.window.TextEncoder = TextEncoder)) || true; // @ts-expect-error patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) @@ -5,8 +11,8 @@ const patchedDecoder = (!global.window.TextDecoder && (global.window.TextDecoder import { JSDOM } from 'jsdom'; -import type { JSSelfProfile } from '../../../src/profiling/jsSelfProfiling'; -import { convertJSSelfProfileToSampledFormat } from '../../../src/profiling/utils'; +import type { JSSelfProfile } from '../../src/profiling/jsSelfProfiling'; +import { convertJSSelfProfileToSampledFormat } from '../../src/profiling/utils'; const makeJSProfile = (partial: Partial = {}): JSSelfProfile => { return { diff --git a/packages/browser/test/unit/sdk.test.ts b/packages/browser/test/sdk.test.ts similarity index 78% rename from packages/browser/test/unit/sdk.test.ts rename to packages/browser/test/sdk.test.ts index 667ad850fb36..31178bc84423 100644 --- a/packages/browser/test/unit/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -1,11 +1,18 @@ +/** + * @vitest-environment jsdom + */ + /* eslint-disable @typescript-eslint/unbound-method */ +import type { Mock } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { Scope, createTransport } from '@sentry/core'; import type { Client, Integration } from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; -import type { BrowserOptions } from '../../src'; -import { WINDOW } from '../../src'; -import { init } from '../../src/sdk'; +import type { BrowserOptions } from '../src'; +import { WINDOW } from '../src'; +import { init } from '../src/sdk'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -20,16 +27,15 @@ function getDefaultBrowserOptions(options: Partial = {}): Browse export class MockIntegration implements Integration { public name: string; - public setupOnce: () => void = jest.fn(); + public setupOnce: () => void = vi.fn(); public constructor(name: string) { this.name = name; } } -jest.mock('@sentry/core', () => { - const original = jest.requireActual('@sentry/core'); +vi.mock('@sentry/core', async requireActual => { return { - ...original, + ...((await requireActual()) as any), getCurrentHub(): { bindClient(client: Client): boolean; getClient(): boolean; @@ -53,11 +59,11 @@ jest.mock('@sentry/core', () => { describe('init', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); afterAll(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); test('installs default integrations', () => { @@ -69,8 +75,8 @@ describe('init', () => { init(options); - expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as Mock).toHaveBeenCalledTimes(1); + expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as Mock).toHaveBeenCalledTimes(1); }); test("doesn't install default integrations if told not to", () => { @@ -81,8 +87,8 @@ describe('init', () => { const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: false }); init(options); - expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(0); - expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as Mock).toHaveBeenCalledTimes(0); + expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as Mock).toHaveBeenCalledTimes(0); }); it('installs merged default integrations, with overrides provided through options', () => { @@ -100,10 +106,10 @@ describe('init', () => { init(options); // 'MockIntegration 1' should be overridden by the one with the same name provided through options - expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(0); - expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(integrations[0]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(integrations[1]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); + expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as Mock).toHaveBeenCalledTimes(0); + expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as Mock).toHaveBeenCalledTimes(1); + expect(integrations[0]!.setupOnce as Mock).toHaveBeenCalledTimes(1); + expect(integrations[1]!.setupOnce as Mock).toHaveBeenCalledTimes(1); }); it('installs integrations returned from a callback function', () => { @@ -124,9 +130,9 @@ describe('init', () => { init(options); - expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); - expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as jest.Mock).toHaveBeenCalledTimes(0); + expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as Mock).toHaveBeenCalledTimes(1); + expect(newIntegration.setupOnce as Mock).toHaveBeenCalledTimes(1); + expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as Mock).toHaveBeenCalledTimes(0); }); describe('initialization error in browser extension', () => { @@ -146,7 +152,7 @@ describe('init', () => { }); it('logs a browser extension error if executed inside a Chrome extension', () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'chrome', { value: { runtime: { id: 'mock-extension-id' } }, @@ -164,7 +170,7 @@ describe('init', () => { }); it('logs a browser extension error if executed inside a Firefox/Safari extension', () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'browser', { value: { runtime: { id: 'mock-extension-id' } }, writable: true }); @@ -181,7 +187,7 @@ describe('init', () => { it.each(['chrome-extension', 'moz-extension', 'ms-browser-extension'])( "doesn't log a browser extension error if executed inside an extension running in a dedicated page (%s)", extensionProtocol => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); // @ts-expect-error - this is a hack to simulate a dedicated page in a browser extension delete WINDOW.location; @@ -202,7 +208,7 @@ describe('init', () => { ); it("doesn't log a browser extension error if executed inside regular browser environment", () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); init(options); @@ -212,7 +218,7 @@ describe('init', () => { }); it("doesn't log a browser extension error if executed inside an NW.js environment", () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'nw', { value: {} }); @@ -224,7 +230,7 @@ describe('init', () => { }); it("doesn't return a client on initialization error", () => { - const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'chrome', { value: { runtime: { id: 'mock-extension-id' } }, diff --git a/packages/browser/test/unit/tracekit/chromium.test.ts b/packages/browser/test/tracekit/chromium.test.ts similarity index 99% rename from packages/browser/test/unit/tracekit/chromium.test.ts rename to packages/browser/test/tracekit/chromium.test.ts index 790f36e0ddc3..36d728f9cbea 100644 --- a/packages/browser/test/unit/tracekit/chromium.test.ts +++ b/packages/browser/test/tracekit/chromium.test.ts @@ -1,5 +1,7 @@ -import { exceptionFromError } from '../../../src/eventbuilder'; -import { defaultStackParser as parser } from '../../../src/stack-parsers'; +import { describe, expect, it } from 'vitest'; + +import { exceptionFromError } from '../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../src/stack-parsers'; describe('Tracekit - Chrome Tests', () => { it('should parse Chrome error with no location', () => { diff --git a/packages/browser/test/unit/tracekit/firefox.test.ts b/packages/browser/test/tracekit/firefox.test.ts similarity index 98% rename from packages/browser/test/unit/tracekit/firefox.test.ts rename to packages/browser/test/tracekit/firefox.test.ts index 5e05930f8078..4a0d3d90e7dc 100644 --- a/packages/browser/test/unit/tracekit/firefox.test.ts +++ b/packages/browser/test/tracekit/firefox.test.ts @@ -1,5 +1,7 @@ -import { exceptionFromError } from '../../../src/eventbuilder'; -import { defaultStackParser as parser } from '../../../src/stack-parsers'; +import { describe, expect, it } from 'vitest'; + +import { exceptionFromError } from '../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../src/stack-parsers'; describe('Tracekit - Firefox Tests', () => { it('should parse Firefox 3 error', () => { diff --git a/packages/browser/test/unit/tracekit/ie.test.ts b/packages/browser/test/tracekit/ie.test.ts similarity index 95% rename from packages/browser/test/unit/tracekit/ie.test.ts rename to packages/browser/test/tracekit/ie.test.ts index 53e96b96371f..95d88a6bef34 100644 --- a/packages/browser/test/unit/tracekit/ie.test.ts +++ b/packages/browser/test/tracekit/ie.test.ts @@ -1,6 +1,8 @@ +import { describe, expect, it } from 'vitest'; + import { createStackParser } from '@sentry/utils'; -import { exceptionFromError } from '../../../src/eventbuilder'; -import { chromeStackLineParser, geckoStackLineParser, winjsStackLineParser } from '../../../src/stack-parsers'; +import { exceptionFromError } from '../../src/eventbuilder'; +import { chromeStackLineParser, geckoStackLineParser, winjsStackLineParser } from '../../src/stack-parsers'; const parser = createStackParser(chromeStackLineParser, geckoStackLineParser, winjsStackLineParser); diff --git a/packages/browser/test/unit/tracekit/misc.test.ts b/packages/browser/test/tracekit/misc.test.ts similarity index 96% rename from packages/browser/test/unit/tracekit/misc.test.ts rename to packages/browser/test/tracekit/misc.test.ts index 8cb31f9a7868..7e501b8abeef 100644 --- a/packages/browser/test/unit/tracekit/misc.test.ts +++ b/packages/browser/test/tracekit/misc.test.ts @@ -1,5 +1,7 @@ -import { exceptionFromError } from '../../../src/eventbuilder'; -import { defaultStackParser as parser } from '../../../src/stack-parsers'; +import { describe, expect, it } from 'vitest'; + +import { exceptionFromError } from '../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../src/stack-parsers'; describe('Tracekit - Misc Tests', () => { it('should parse PhantomJS 1.19 error', () => { diff --git a/packages/browser/test/unit/tracekit/opera.test.ts b/packages/browser/test/tracekit/opera.test.ts similarity index 98% rename from packages/browser/test/unit/tracekit/opera.test.ts rename to packages/browser/test/tracekit/opera.test.ts index e86855dc172a..352737fba5d0 100644 --- a/packages/browser/test/unit/tracekit/opera.test.ts +++ b/packages/browser/test/tracekit/opera.test.ts @@ -1,7 +1,9 @@ +import { describe, expect, it } from 'vitest'; + import { createStackParser } from '@sentry/utils'; -import { exceptionFromError } from '../../../src/eventbuilder'; -import { defaultStackParser, opera10StackLineParser, opera11StackLineParser } from '../../../src/stack-parsers'; +import { exceptionFromError } from '../../src/eventbuilder'; +import { defaultStackParser, opera10StackLineParser, opera11StackLineParser } from '../../src/stack-parsers'; const operaParser = createStackParser(opera10StackLineParser, opera11StackLineParser); const chromiumParser = defaultStackParser; diff --git a/packages/browser/test/unit/tracekit/react-native.test.ts b/packages/browser/test/tracekit/react-native.test.ts similarity index 99% rename from packages/browser/test/unit/tracekit/react-native.test.ts rename to packages/browser/test/tracekit/react-native.test.ts index 9a74e46007b1..c69703fe0c34 100644 --- a/packages/browser/test/unit/tracekit/react-native.test.ts +++ b/packages/browser/test/tracekit/react-native.test.ts @@ -1,5 +1,7 @@ -import { exceptionFromError } from '../../../src/eventbuilder'; -import { defaultStackParser as parser } from '../../../src/stack-parsers'; +import { describe, expect, it } from 'vitest'; + +import { exceptionFromError } from '../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../src/stack-parsers'; describe('Tracekit - React Native Tests', () => { it('should parse exceptions for react-native-v8', () => { diff --git a/packages/browser/test/unit/tracekit/react.test.ts b/packages/browser/test/tracekit/react.test.ts similarity index 96% rename from packages/browser/test/unit/tracekit/react.test.ts rename to packages/browser/test/tracekit/react.test.ts index 55ffdc34c537..dd737b798d03 100644 --- a/packages/browser/test/unit/tracekit/react.test.ts +++ b/packages/browser/test/tracekit/react.test.ts @@ -1,5 +1,7 @@ -import { exceptionFromError } from '../../../src/eventbuilder'; -import { defaultStackParser as parser } from '../../../src/stack-parsers'; +import { describe, expect, it } from 'vitest'; + +import { exceptionFromError } from '../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../src/stack-parsers'; describe('Tracekit - React Tests', () => { it('should correctly parse Invariant Violation errors and use framesToPop to drop the invariant frame', () => { diff --git a/packages/browser/test/unit/tracekit/safari.test.ts b/packages/browser/test/tracekit/safari.test.ts similarity index 98% rename from packages/browser/test/unit/tracekit/safari.test.ts rename to packages/browser/test/tracekit/safari.test.ts index 470ed1d7b8dc..ca94df924910 100644 --- a/packages/browser/test/unit/tracekit/safari.test.ts +++ b/packages/browser/test/tracekit/safari.test.ts @@ -1,5 +1,7 @@ -import { exceptionFromError } from '../../../src/eventbuilder'; -import { defaultStackParser as parser } from '../../../src/stack-parsers'; +import { describe, expect, it } from 'vitest'; + +import { exceptionFromError } from '../../src/eventbuilder'; +import { defaultStackParser as parser } from '../../src/stack-parsers'; describe('Tracekit - Safari Tests', () => { it('should parse Safari 6 error', () => { diff --git a/packages/browser/test/unit/tracing/backgroundtab.test.ts b/packages/browser/test/tracing/backgroundtab.test.ts similarity index 84% rename from packages/browser/test/unit/tracing/backgroundtab.test.ts rename to packages/browser/test/tracing/backgroundtab.test.ts index 2c998744a723..c939da8b4d03 100644 --- a/packages/browser/test/unit/tracing/backgroundtab.test.ts +++ b/packages/browser/test/tracing/backgroundtab.test.ts @@ -1,3 +1,9 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { getCurrentScope } from '@sentry/core'; import { setCurrentClient } from '@sentry/core'; @@ -8,8 +14,8 @@ const patchedDecoder = (!global.window.TextDecoder && (global.window.TextDecoder import { JSDOM } from 'jsdom'; -import { BrowserClient } from '../../../src/client'; -import { registerBackgroundTabDetection } from '../../../src/tracing/backgroundtab'; +import { BrowserClient } from '../../src/client'; +import { registerBackgroundTabDetection } from '../../src/tracing/backgroundtab'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; describe('registerBackgroundTabDetection', () => { @@ -30,7 +36,7 @@ describe('registerBackgroundTabDetection', () => { setCurrentClient(client); client.init(); - global.document.addEventListener = jest.fn((event, callback) => { + global.document.addEventListener = vi.fn((event, callback) => { events[event] = callback; }); }); diff --git a/packages/browser/test/unit/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts similarity index 94% rename from packages/browser/test/unit/tracing/browserTracingIntegration.test.ts rename to packages/browser/test/tracing/browserTracingIntegration.test.ts index 86e46fe0ccc8..dbdebe473335 100644 --- a/packages/browser/test/unit/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -1,3 +1,9 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + import { TextDecoder, TextEncoder } from 'util'; const oldTextEncoder = global.window.TextEncoder; const oldTextDecoder = global.window.TextDecoder; @@ -25,15 +31,14 @@ import { startInactiveSpan, } from '@sentry/core'; import type { Span, StartSpanOptions } from '@sentry/types'; -import { timestampInSeconds } from '@sentry/utils'; import { JSDOM } from 'jsdom'; -import { BrowserClient } from '../../../src/client'; -import { WINDOW } from '../../../src/helpers'; +import { BrowserClient } from '../../src/client'; +import { WINDOW } from '../../src/helpers'; import { browserTracingIntegration, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan, -} from '../../../src/tracing/browserTracingIntegration'; +} from '../../src/tracing/browserTracingIntegration'; import { getDefaultBrowserClientOptions } from '../helper/browser-client-options'; // We're setting up JSDom here because the Next.js routing instrumentations requires a few things to be present on pageload: @@ -55,6 +60,11 @@ afterAll(() => { Object.defineProperty(WINDOW, 'history', { value: originalGlobalHistory }); }); +afterEach(() => { + vi.useRealTimers(); + performance.clearMarks(); +}); + describe('browserTracingIntegration', () => { beforeEach(() => { getCurrentScope().clear(); @@ -226,29 +236,6 @@ describe('browserTracingIntegration', () => { }); }); - it("trims pageload transactions to the max duration of the transaction's children", async () => { - const client = new BrowserClient( - getDefaultBrowserClientOptions({ - tracesSampleRate: 1, - integrations: [browserTracingIntegration({ idleTimeout: 10 })], - }), - ); - - setCurrentClient(client); - client.init(); - - const pageloadSpan = getActiveSpan(); - const childSpan = startInactiveSpan({ name: 'pageload-child' }); - const timestamp = timestampInSeconds(); - - childSpan.end(timestamp); - - // Wait for 10ms for idle timeout - await new Promise(resolve => setTimeout(resolve, 10)); - - expect(spanToJSON(pageloadSpan!).timestamp).toBe(timestamp); - }); - describe('startBrowserTracingPageLoadSpan', () => { it('works without integration setup', () => { const client = new BrowserClient( @@ -347,7 +334,7 @@ describe('browserTracingIntegration', () => { }); it('calls before beforeStartSpan', () => { - const mockBeforeStartSpan = jest.fn((options: StartSpanOptions) => options); + const mockBeforeStartSpan = vi.fn((options: StartSpanOptions) => options); const client = new BrowserClient( getDefaultBrowserClientOptions({ @@ -371,7 +358,7 @@ describe('browserTracingIntegration', () => { }); it('uses options overridden with beforeStartSpan', () => { - const mockBeforeStartSpan = jest.fn((options: StartSpanOptions) => ({ + const mockBeforeStartSpan = vi.fn((options: StartSpanOptions) => ({ ...options, op: 'test op', })); @@ -414,7 +401,7 @@ describe('browserTracingIntegration', () => { }); it('sets source to "custom" if name is changed in beforeStartSpan', () => { - const mockBeforeStartSpan = jest.fn((options: StartSpanOptions) => ({ + const mockBeforeStartSpan = vi.fn((options: StartSpanOptions) => ({ ...options, name: 'changed', })); @@ -540,7 +527,7 @@ describe('browserTracingIntegration', () => { }); it('calls before beforeStartSpan', () => { - const mockBeforeStartSpan = jest.fn((options: StartSpanOptions) => options); + const mockBeforeStartSpan = vi.fn((options: StartSpanOptions) => options); const client = new BrowserClient( getDefaultBrowserClientOptions({ @@ -568,7 +555,7 @@ describe('browserTracingIntegration', () => { }); it('uses options overridden with beforeStartSpan', () => { - const mockBeforeStartSpan = jest.fn((options: StartSpanOptions) => ({ + const mockBeforeStartSpan = vi.fn((options: StartSpanOptions) => ({ ...options, op: 'test op', })); @@ -596,7 +583,7 @@ describe('browserTracingIntegration', () => { }); it('sets source to "custom" if name is changed in beforeStartSpan', () => { - const mockBeforeStartSpan = jest.fn((options: StartSpanOptions) => ({ + const mockBeforeStartSpan = vi.fn((options: StartSpanOptions) => ({ ...options, name: 'changed', })); @@ -929,7 +916,7 @@ describe('browserTracingIntegration', () => { describe('idleTimeout', () => { it('is created by default', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const client = new BrowserClient( getDefaultBrowserClientOptions({ tracesSampleRate: 1, @@ -955,15 +942,16 @@ describe('browserTracingIntegration', () => { // inner1 is now ended, all good expect(spans).toHaveLength(1); - jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); + vi.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); // idle span itself is now ended - expect(spans).toHaveLength(2); - expect(spans[1]).toBe(idleSpan); + // there is also the `sentry-tracing-init` span included + expect(spans).toHaveLength(3); + expect(spans[2]).toBe(idleSpan); }); it('can be a custom value', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const client = new BrowserClient( getDefaultBrowserClientOptions({ @@ -990,18 +978,19 @@ describe('browserTracingIntegration', () => { // inner1 is now ended, all good expect(spans).toHaveLength(1); - jest.advanceTimersByTime(2000); + vi.advanceTimersByTime(2000); // idle span itself is now ended - expect(spans).toHaveLength(2); - expect(spans[1]).toBe(idleSpan); + // there is also the `sentry-tracing-init` span included + expect(spans).toHaveLength(3); + expect(spans[2]).toBe(idleSpan); }); }); // TODO(lforst): I cannot manage to get this test to pass. /* it('heartbeatInterval can be a custom value', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); const interval = 200; @@ -1015,7 +1004,7 @@ describe('browserTracingIntegration', () => { setCurrentClient(client); client.init(); - const mockFinish = jest.fn(); + const mockFinish = vi.fn(); // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction() as IdleTransaction; transaction.sendAutoFinishSignal(); @@ -1025,7 +1014,7 @@ describe('browserTracingIntegration', () => { span!.end(); // activities = 0 expect(mockFinish).toHaveBeenCalledTimes(0); - jest.advanceTimersByTime(interval * 3); + vi.advanceTimersByTime(interval * 3); expect(mockFinish).toHaveBeenCalledTimes(1); }); */ diff --git a/packages/browser/test/unit/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts similarity index 95% rename from packages/browser/test/unit/tracing/request.test.ts rename to packages/browser/test/tracing/request.test.ts index 384d62bf52e2..f1067c9e4b52 100644 --- a/packages/browser/test/unit/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -1,13 +1,15 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; + import * as browserUtils from '@sentry-internal/browser-utils'; import type { Client } from '@sentry/types'; import * as utils from '@sentry/utils'; -import { WINDOW } from '../../../src/helpers'; +import { WINDOW } from '../../src/helpers'; -import { extractNetworkProtocol, instrumentOutgoingRequests, shouldAttachHeaders } from '../../../src/tracing/request'; +import { extractNetworkProtocol, instrumentOutgoingRequests, shouldAttachHeaders } from '../../src/tracing/request'; beforeAll(() => { - // @ts-expect-error need to override global Request because it's not in the jest environment (even with an - // `@jest-environment jsdom` directive, for some reason) + // @ts-expect-error need to override global Request because it's not in the vi environment (even with an + // `@vi-environment jsdom` directive, for some reason) global.Request = {}; }); @@ -15,7 +17,7 @@ class MockClient implements Partial { public addEventProcessor: () => void; constructor() { // Mock addEventProcessor function - this.addEventProcessor = jest.fn(); + this.addEventProcessor = vi.fn(); } } @@ -23,13 +25,13 @@ describe('instrumentOutgoingRequests', () => { let client: Client; beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); client = new MockClient() as unknown as Client; }); it('instruments fetch and xhr requests', () => { - const addFetchSpy = jest.spyOn(utils, 'addFetchInstrumentationHandler'); - const addXhrSpy = jest.spyOn(browserUtils, 'addXhrInstrumentationHandler'); + const addFetchSpy = vi.spyOn(utils, 'addFetchInstrumentationHandler'); + const addXhrSpy = vi.spyOn(browserUtils, 'addXhrInstrumentationHandler'); instrumentOutgoingRequests(client); @@ -38,7 +40,7 @@ describe('instrumentOutgoingRequests', () => { }); it('does not instrument fetch requests if traceFetch is false', () => { - const addFetchSpy = jest.spyOn(utils, 'addFetchInstrumentationHandler'); + const addFetchSpy = vi.spyOn(utils, 'addFetchInstrumentationHandler'); instrumentOutgoingRequests(client, { traceFetch: false }); @@ -46,7 +48,7 @@ describe('instrumentOutgoingRequests', () => { }); it('does not instrument xhr requests if traceXHR is false', () => { - const addXhrSpy = jest.spyOn(browserUtils, 'addXhrInstrumentationHandler'); + const addXhrSpy = vi.spyOn(browserUtils, 'addXhrInstrumentationHandler'); instrumentOutgoingRequests(client, { traceXHR: false }); @@ -60,7 +62,7 @@ interface ProtocolInfo { } describe('HTTPTimings', () => { - describe('Extracting version from ALPN protocol', () => { + test('Extracting version from ALPN protocol', () => { const nextHopToNetworkVersion: Record = { 'http/0.9': { name: 'http', version: '0.9' }, 'http/1.0': { name: 'http', version: '1.0' }, diff --git a/packages/browser/test/unit/transports/fetch.test.ts b/packages/browser/test/transports/fetch.test.ts similarity index 90% rename from packages/browser/test/unit/transports/fetch.test.ts rename to packages/browser/test/transports/fetch.test.ts index ae8a5d43a6e0..ccba3282251c 100644 --- a/packages/browser/test/unit/transports/fetch.test.ts +++ b/packages/browser/test/transports/fetch.test.ts @@ -1,8 +1,11 @@ +import type { Mock } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; + import type { EventEnvelope, EventItem } from '@sentry/types'; import { createEnvelope, serializeEnvelope } from '@sentry/utils'; -import { makeFetchTransport } from '../../../src/transports/fetch'; -import type { BrowserTransportOptions } from '../../../src/transports/types'; +import { makeFetchTransport } from '../../src/transports/fetch'; +import type { BrowserTransportOptions } from '../../src/transports/types'; const DEFAULT_FETCH_TRANSPORT_OPTIONS: BrowserTransportOptions = { url: 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7', @@ -30,7 +33,7 @@ class Headers { describe('NewFetchTransport', () => { it('calls fetch with the given URL', async () => { - const mockFetch = jest.fn(() => + const mockFetch = vi.fn(() => Promise.resolve({ headers: new Headers(), status: 200, @@ -53,10 +56,10 @@ describe('NewFetchTransport', () => { it('sets rate limit headers', async () => { const headers = { - get: jest.fn(), + get: vi.fn(), }; - const mockFetch = jest.fn(() => + const mockFetch = vi.fn(() => Promise.resolve({ headers, status: 200, @@ -74,7 +77,7 @@ describe('NewFetchTransport', () => { }); it('allows for custom options to be passed in', async () => { - const mockFetch = jest.fn(() => + const mockFetch = vi.fn(() => Promise.resolve({ headers: new Headers(), status: 200, @@ -102,7 +105,7 @@ describe('NewFetchTransport', () => { }); it('handles when `getNativetypeof window.fetchementation` is undefined', async () => { - const mockFetch = jest.fn(() => undefined) as unknown as typeof window.fetch; + const mockFetch = vi.fn(() => undefined) as unknown as typeof window.fetch; const transport = makeFetchTransport(DEFAULT_FETCH_TRANSPORT_OPTIONS, mockFetch); expect(mockFetch).toHaveBeenCalledTimes(0); @@ -111,7 +114,7 @@ describe('NewFetchTransport', () => { }); it('correctly sets keepalive flag', async () => { - const mockFetch = jest.fn(() => + const mockFetch = vi.fn(() => Promise.resolve({ headers: new Headers(), status: 200, @@ -143,7 +146,7 @@ describe('NewFetchTransport', () => { expect(mockFetch).toHaveBeenNthCalledWith(i, expect.any(String), expect.objectContaining({ keepalive })); } - (mockFetch as jest.Mock).mockClear(); + (mockFetch as Mock).mockClear(); // Limit resets when requests have resolved // Now try based on # of pending requests diff --git a/packages/browser/test/unit/transports/offline.test.ts b/packages/browser/test/transports/offline.test.ts similarity index 96% rename from packages/browser/test/unit/transports/offline.test.ts rename to packages/browser/test/transports/offline.test.ts index ed4cd770101d..a8352cb4102d 100644 --- a/packages/browser/test/unit/transports/offline.test.ts +++ b/packages/browser/test/transports/offline.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import 'fake-indexeddb/auto'; import { TextDecoder, TextEncoder } from 'util'; @@ -10,8 +12,9 @@ import type { } from '@sentry/types'; import { createEnvelope } from '@sentry/utils'; -import { MIN_DELAY } from '../../../../core/src/transports/offline'; -import { createStore, makeBrowserOfflineTransport, push, shift, unshift } from '../../../src/transports/offline'; +import { createStore, makeBrowserOfflineTransport, push, shift, unshift } from '../../src/transports/offline'; + +const MIN_DELAY = 100; function deleteDatabase(name: string): Promise { return new Promise((resolve, reject) => { diff --git a/packages/browser/test/unit/userfeedback.test.ts b/packages/browser/test/userfeedback.test.ts similarity index 92% rename from packages/browser/test/unit/userfeedback.test.ts rename to packages/browser/test/userfeedback.test.ts index cb498cac7893..93a8de33690b 100644 --- a/packages/browser/test/unit/userfeedback.test.ts +++ b/packages/browser/test/userfeedback.test.ts @@ -1,4 +1,6 @@ -import { createUserFeedbackEnvelope } from '../../src/userfeedback'; +import { describe, expect, test } from 'vitest'; + +import { createUserFeedbackEnvelope } from '../src/userfeedback'; describe('userFeedback', () => { test('creates user feedback envelope header', () => { diff --git a/packages/browser/test/unit/utils/lazyLoadIntegration.test.ts b/packages/browser/test/utils/lazyLoadIntegration.test.ts similarity index 92% rename from packages/browser/test/unit/utils/lazyLoadIntegration.test.ts rename to packages/browser/test/utils/lazyLoadIntegration.test.ts index fa6bc62fb4fd..ec88ae49a1a9 100644 --- a/packages/browser/test/unit/utils/lazyLoadIntegration.test.ts +++ b/packages/browser/test/utils/lazyLoadIntegration.test.ts @@ -1,6 +1,12 @@ +/** + * @vitest-environment jsdom + */ + +import { afterAll, afterEach, beforeEach, describe, expect, test } from 'vitest'; + import { TextDecoder, TextEncoder } from 'util'; -import { SDK_VERSION, lazyLoadIntegration } from '../../../src'; -import * as Sentry from '../../../src'; +import { SDK_VERSION, lazyLoadIntegration } from '../../src'; +import * as Sentry from '../../src'; const patchedEncoder = (!global.window.TextEncoder && (global.window.TextEncoder = TextEncoder)) || true; // @ts-expect-error patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) const patchedDecoder = (!global.window.TextDecoder && (global.window.TextDecoder = TextDecoder)) || true; diff --git a/packages/browser/tsconfig.test.json b/packages/browser/tsconfig.test.json index 9bdd2aa76dab..00cada2d8bcf 100644 --- a/packages/browser/tsconfig.test.json +++ b/packages/browser/tsconfig.test.json @@ -1,13 +1,9 @@ { "extends": "./tsconfig.json", - "include": ["test/**/*"], - "exclude": ["test/integration/**/*"], + "include": ["test/**/*", "vite.config.ts"], "compilerOptions": { - // should include all types from `./tsconfig.json` plus types for all test frameworks used - "types": ["node", "jest"] - // other package-specific, test-specific options } } diff --git a/packages/browser/vite.config.ts b/packages/browser/vite.config.ts new file mode 100644 index 000000000000..a5523c61f601 --- /dev/null +++ b/packages/browser/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +import baseConfig from '../../vite/vite.config'; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + }, +}); From 98160a5e4dba726719d79a878e9da1e3300d98d0 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 1 Aug 2024 16:41:02 -0400 Subject: [PATCH 10/14] feat(profiling): attach sdk info to chunks (#13145) Attach SDK info to individual profile chunks --- packages/profiling-node/src/integration.ts | 18 ++++++-- packages/profiling-node/src/utils.ts | 8 ++++ .../test/spanProfileUtils.test.ts | 46 ++++++++++++++++++- packages/types/src/profiling.ts | 4 ++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index b05a919fc949..50f240bff732 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -1,3 +1,5 @@ +/* eslint-disable max-lines */ + import { defineIntegration, getCurrentScope, @@ -231,11 +233,17 @@ class ContinuousProfiler { } DEBUG_BUILD && logger.log(`[Profiling] Profile chunk ${this._chunkData.id} sent to Sentry.`); - const chunk = createProfilingChunkEvent(this._client, this._client.getOptions(), profile, { - chunk_id: this._chunkData.id, - trace_id: this._chunkData.startTraceID, - profiler_id: this._profilerId, - }); + const chunk = createProfilingChunkEvent( + this._client, + this._client.getOptions(), + profile, + this._client.getSdkMetadata()?.sdk, + { + chunk_id: this._chunkData.id, + trace_id: this._chunkData.startTraceID, + profiler_id: this._profilerId, + }, + ); if (!chunk) { DEBUG_BUILD && logger.log(`[Profiling] Failed to create profile chunk for: ${this._chunkData.id}`); diff --git a/packages/profiling-node/src/utils.ts b/packages/profiling-node/src/utils.ts index 673901990d90..0eb4fe4281b4 100644 --- a/packages/profiling-node/src/utils.ts +++ b/packages/profiling-node/src/utils.ts @@ -194,12 +194,14 @@ function createProfileChunkPayload( trace_id, profiler_id, chunk_id, + sdk, }: { release: string; environment: string; trace_id: string | undefined; chunk_id: string; profiler_id: string; + sdk: SdkInfo | undefined; }, ): ProfileChunk { // Log a warning if the profile has an invalid traceId (should be uuidv4). @@ -213,6 +215,10 @@ function createProfileChunkPayload( const profile: ProfileChunk = { chunk_id: chunk_id, + client_sdk: { + name: sdk?.name ?? 'sentry.javascript.node', + version: sdk?.version ?? '0.0.0', + }, profiler_id: profiler_id, platform: 'node', version: CONTINUOUS_FORMAT_VERSION, @@ -235,6 +241,7 @@ export function createProfilingChunkEvent( client: Client, options: { release?: string; environment?: string }, profile: RawChunkCpuProfile, + sdk: SdkInfo | undefined, identifiers: { trace_id: string | undefined; chunk_id: string; profiler_id: string }, ): ProfileChunk | null { if (!isValidProfileChunk(profile)) { @@ -247,6 +254,7 @@ export function createProfilingChunkEvent( trace_id: identifiers.trace_id ?? '', chunk_id: identifiers.chunk_id, profiler_id: identifiers.profiler_id, + sdk, }); } diff --git a/packages/profiling-node/test/spanProfileUtils.test.ts b/packages/profiling-node/test/spanProfileUtils.test.ts index 766a0059d02e..4a90caa0f353 100644 --- a/packages/profiling-node/test/spanProfileUtils.test.ts +++ b/packages/profiling-node/test/spanProfileUtils.test.ts @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node'; import { getMainCarrier } from '@sentry/core'; import type { NodeClientOptions } from '@sentry/node/build/types/types'; -import type { Transport } from '@sentry/types'; +import type { ProfileChunk, Transport } from '@sentry/types'; import { GLOBAL_OBJ, createEnvelope, logger } from '@sentry/utils'; import { CpuProfilerBindings } from '../src/cpu_profiler'; import { type ProfilingIntegration, _nodeProfilingIntegration } from '../src/integration'; @@ -402,6 +402,50 @@ describe('continuous profiling', () => { delete getMainCarrier().__SENTRY__; }); + it('attaches sdk metadata to chunks', () => { + // @ts-expect-error we just mock the return type and ignore the signature + jest.spyOn(CpuProfilerBindings, 'stopProfiling').mockImplementation(() => { + return { + samples: [ + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + { + stack_id: 0, + thread_id: '0', + elapsed_since_start_ns: '10', + }, + ], + measurements: {}, + stacks: [[0]], + frames: [], + resources: [], + profiler_logging_mode: 'lazy', + }; + }); + + const [client, transport] = makeContinuousProfilingClient(); + Sentry.setCurrentClient(client); + client.init(); + + const transportSpy = jest.spyOn(transport, 'send').mockReturnValue(Promise.resolve({})); + + const integration = client.getIntegrationByName('ProfilingIntegration'); + if (!integration) { + throw new Error('Profiling integration not found'); + } + integration._profiler.start(); + jest.advanceTimersByTime(1000); + integration._profiler.stop(); + jest.advanceTimersByTime(1000); + + const profile = transportSpy.mock.calls?.[0]?.[0]?.[1]?.[0]?.[1] as ProfileChunk; + expect(profile.client_sdk.name).toBe('sentry.javascript.node'); + expect(profile.client_sdk.version).toEqual(expect.stringMatching(/\d+\.\d+\.\d+/)); + }); + it('initializes the continuous profiler and binds the sentry client', () => { const startProfilingSpy = jest.spyOn(CpuProfilerBindings, 'startProfiling'); diff --git a/packages/types/src/profiling.ts b/packages/types/src/profiling.ts index 48dd797492bf..8f5f4cc2e890 100644 --- a/packages/types/src/profiling.ts +++ b/packages/types/src/profiling.ts @@ -126,4 +126,8 @@ export interface Profile extends BaseProfile { export interface ProfileChunk extends BaseProfile { chunk_id: string; profiler_id: string; + client_sdk: { + name: string; + version: string; + }; } From 964d050ce242bee1d3b95fe6d806f955171e944f Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 2 Aug 2024 09:24:27 +0200 Subject: [PATCH 11/14] feat(nestjs): Automatic instrumentation of nestjs interceptors before route execution (#13153) Adds automatic instrumentation of interceptors to `@sentry/nestjs`. Interceptors in nest have a `@Injectable` decorator and implement a `intercept` function. So we can simply extend the existing instrumentation to add a proxy for `intercept`. Remark: Interceptors allow users to add functionality before and after a route handler is called. This PR adds tracing to whatever happens before the route is executed. I am still figuring out how to trace any instructions after the route was executed. Will do that in a separate PR. --- .../nestjs-basic/src/app.controller.ts | 11 +- .../nestjs-basic/src/app.service.ts | 2 +- .../nestjs-basic/src/example.interceptor.ts | 10 ++ .../nestjs-basic/tests/transactions.test.ts | 80 +++++++++++ .../node-nestjs-basic/src/app.controller.ts | 11 +- .../node-nestjs-basic/src/app.service.ts | 2 +- .../src/example.interceptor.ts | 10 ++ .../tests/transactions.test.ts | 80 +++++++++++ packages/nestjs/src/setup.ts | 3 + .../node/src/integrations/tracing/nest.ts | 135 +++++++++++------- 10 files changed, 288 insertions(+), 56 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts index 40ee93adaa90..c04fd5613e95 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, UseGuards, UseInterceptors } from '@nestjs/common'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; +import { ExampleInterceptor } from './example.interceptor'; @Controller() export class AppController { @@ -13,7 +14,7 @@ export class AppController { @Get('test-middleware-instrumentation') testMiddlewareInstrumentation() { - return this.appService.testMiddleware(); + return this.appService.testSpan(); } @Get('test-guard-instrumentation') @@ -22,6 +23,12 @@ export class AppController { return {}; } + @Get('test-interceptor-instrumentation') + @UseInterceptors(ExampleInterceptor) + testInterceptorInstrumentation() { + return this.appService.testSpan(); + } + @Get('test-pipe-instrumentation/:id') testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { return { value: id }; diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts index 1ae4c50d8901..b2dadbb0a269 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts @@ -21,7 +21,7 @@ export class AppService { }); } - testMiddleware() { + testSpan() { // span that should not be a child span of the middleware span Sentry.startSpan({ name: 'test-controller-span' }, () => {}); } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts new file mode 100644 index 000000000000..75c301b4cffc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/example.interceptor.ts @@ -0,0 +1,10 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; + +@Injectable() +export class ExampleInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-interceptor-span' }, () => {}); + return next.handle().pipe(); + } +} 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 33e56cd5695e..78b3e0d3102a 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 @@ -338,3 +338,83 @@ 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 => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + const exampleInterceptorSpan = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor'); + const exampleInterceptorSpanId = exampleInterceptorSpan?.span_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptorSpan = transactionEvent.spans.find(span => span.description === 'test-interceptor-span'); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'ExampleInterceptor' is the parent of 'test-interceptor-span' + expect(testInterceptorSpan.parent_span_id).toBe(exampleInterceptorSpanId); + + // 'ExampleInterceptor' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanId); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts index 40ee93adaa90..c04fd5613e95 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts @@ -1,6 +1,7 @@ -import { Controller, Get, Param, ParseIntPipe, UseGuards } from '@nestjs/common'; +import { Controller, Get, Param, ParseIntPipe, UseGuards, UseInterceptors } from '@nestjs/common'; import { AppService } from './app.service'; import { ExampleGuard } from './example.guard'; +import { ExampleInterceptor } from './example.interceptor'; @Controller() export class AppController { @@ -13,7 +14,7 @@ export class AppController { @Get('test-middleware-instrumentation') testMiddlewareInstrumentation() { - return this.appService.testMiddleware(); + return this.appService.testSpan(); } @Get('test-guard-instrumentation') @@ -22,6 +23,12 @@ export class AppController { return {}; } + @Get('test-interceptor-instrumentation') + @UseInterceptors(ExampleInterceptor) + testInterceptorInstrumentation() { + return this.appService.testSpan(); + } + @Get('test-pipe-instrumentation/:id') testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) { return { value: id }; diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts index 1ae4c50d8901..b2dadbb0a269 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts @@ -21,7 +21,7 @@ export class AppService { }); } - testMiddleware() { + testSpan() { // span that should not be a child span of the middleware span Sentry.startSpan({ name: 'test-controller-span' }, () => {}); } diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts new file mode 100644 index 000000000000..75c301b4cffc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts @@ -0,0 +1,10 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; +import * as Sentry from '@sentry/nestjs'; + +@Injectable() +export class ExampleInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + Sentry.startSpan({ name: 'test-interceptor-span' }, () => {}); + return next.handle().pipe(); + } +} 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 754d545979e5..62c882eb7f4b 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 @@ -338,3 +338,83 @@ 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 => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' + ); + }); + + const response = await fetch(`${baseURL}/test-interceptor-instrumentation`); + expect(response.status).toBe(200); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'middleware.nestjs', + 'sentry.origin': 'auto.middleware.nestjs', + }, + description: 'ExampleInterceptor', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + op: 'middleware.nestjs', + origin: 'auto.middleware.nestjs', + }, + ]), + }), + ); + + const exampleInterceptorSpan = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor'); + const exampleInterceptorSpanId = exampleInterceptorSpan?.span_id; + + expect(transactionEvent).toEqual( + expect.objectContaining({ + spans: expect.arrayContaining([ + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-controller-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + { + span_id: expect.any(String), + trace_id: expect.any(String), + data: expect.any(Object), + description: 'test-interceptor-span', + parent_span_id: expect.any(String), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + status: 'ok', + origin: 'manual', + }, + ]), + }), + ); + + // verify correct span parent-child relationships + const testInterceptorSpan = transactionEvent.spans.find(span => span.description === 'test-interceptor-span'); + const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span'); + + // 'ExampleInterceptor' is the parent of 'test-interceptor-span' + expect(testInterceptorSpan.parent_span_id).toBe(exampleInterceptorSpanId); + + // 'ExampleInterceptor' is NOT the parent of 'test-controller-span' + expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanId); +}); diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 7402d3f374f0..f788ccb9b67c 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -32,6 +32,9 @@ import type { Observable } from 'rxjs'; * Interceptor to add Sentry tracing capabilities to Nest.js applications. */ class SentryTracingInterceptor implements NestInterceptor { + // used to exclude this class from being auto-instrumented + public static readonly __SENTRY_INTERNAL__ = true; + /** * Intercepts HTTP requests to set the transaction name for Sentry tracing. */ diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts index cb3097b06228..6b452ab3add3 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -61,12 +61,21 @@ const supportedVersions = ['>=8.0.0 <11']; const sentryPatched = 'sentryPatched'; +/** + * 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; @@ -74,6 +83,8 @@ export interface InjectableTarget { 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; }; } @@ -92,6 +103,17 @@ export function isPatched(target: InjectableTarget): boolean { 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. * @@ -148,7 +170,7 @@ export class SentryNestInstrumentation extends InstrumentationBase { return function wrappedInjectable(options?: unknown) { return function (target: InjectableTarget) { // patch middleware - if (typeof target.prototype.use === 'function') { + if (typeof target.prototype.use === 'function' && !target.__SENTRY_INTERNAL__) { // patch only once if (isPatched(target)) { return original(options)(target); @@ -159,38 +181,29 @@ export class SentryNestInstrumentation extends InstrumentationBase { const [req, res, next, ...args] = argsUse; const prevSpan = getActiveSpan(); - return startSpanManual( - { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', - }, - }, - (span: Span) => { - const nextProxy = new Proxy(next, { - apply: (originalNext, thisArgNext, argsNext) => { - span.end(); - - if (prevSpan) { - withActiveSpan(prevSpan, () => { - return Reflect.apply(originalNext, thisArgNext, argsNext); - }); - } else { + return startSpanManual(getMiddlewareSpanOptions(target), (span: Span) => { + const nextProxy = new Proxy(next, { + apply: (originalNext, thisArgNext, argsNext) => { + span.end(); + + if (prevSpan) { + return withActiveSpan(prevSpan, () => { return Reflect.apply(originalNext, thisArgNext, argsNext); - } - }, - }); + }); + } else { + return Reflect.apply(originalNext, thisArgNext, argsNext); + } + }, + }); - return originalUse.apply(thisArgUse, [req, res, nextProxy, args]); - }, - ); + return originalUse.apply(thisArgUse, [req, res, nextProxy, args]); + }); }, }); } // patch guards - if (typeof target.prototype.canActivate === 'function') { + if (typeof target.prototype.canActivate === 'function' && !target.__SENTRY_INTERNAL__) { // patch only once if (isPatched(target)) { return original(options)(target); @@ -198,42 +211,64 @@ export class SentryNestInstrumentation extends InstrumentationBase { target.prototype.canActivate = new Proxy(target.prototype.canActivate, { apply: (originalCanActivate, thisArgCanActivate, argsCanActivate) => { - return startSpan( - { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', - }, - }, - () => { - return originalCanActivate.apply(thisArgCanActivate, argsCanActivate); - }, - ); + return startSpan(getMiddlewareSpanOptions(target), () => { + return originalCanActivate.apply(thisArgCanActivate, argsCanActivate); + }); }, }); } // patch pipes - if (typeof target.prototype.transform === 'function') { + if (typeof target.prototype.transform === 'function' && !target.__SENTRY_INTERNAL__) { if (isPatched(target)) { return original(options)(target); } target.prototype.transform = new Proxy(target.prototype.transform, { apply: (originalTransform, thisArgTransform, argsTransform) => { - return startSpan( - { - name: target.name, - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', + return startSpan(getMiddlewareSpanOptions(target), () => { + return originalTransform.apply(thisArgTransform, argsTransform); + }); + }, + }); + } + + // patch interceptors + if (typeof target.prototype.intercept === 'function' && !target.__SENTRY_INTERNAL__) { + if (isPatched(target)) { + return original(options)(target); + } + + target.prototype.intercept = new Proxy(target.prototype.intercept, { + apply: (originalIntercept, thisArgIntercept, argsIntercept) => { + const [executionContext, next, args] = argsIntercept; + const prevSpan = getActiveSpan(); + + return startSpanManual(getMiddlewareSpanOptions(target), (span: Span) => { + const nextProxy = new Proxy(next, { + get: (thisArgNext, property, receiver) => { + if (property === 'handle') { + const originalHandle = Reflect.get(thisArgNext, property, receiver); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (...args: any[]) => { + span.end(); + + if (prevSpan) { + return withActiveSpan(prevSpan, () => { + return Reflect.apply(originalHandle, thisArgNext, args); + }); + } else { + return Reflect.apply(originalHandle, thisArgNext, args); + } + }; + } + + return Reflect.get(target, property, receiver); }, - }, - () => { - return originalTransform.apply(thisArgTransform, argsTransform); - }, - ); + }); + + return originalIntercept.apply(thisArgIntercept, [executionContext, nextProxy, args]); + }); }, }); } From 8a55ab21e1792773be5e54adf102a74ba8f60468 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 2 Aug 2024 10:54:32 +0200 Subject: [PATCH 12/14] fix(nestjs): Inline Observable type to resolve missing 'rxjs' dependency (#13166) --- .../node-nestjs-basic/src/example.interceptor.ts | 3 ++- packages/node/src/integrations/tracing/nest.ts | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts index 75c301b4cffc..260c1798449f 100644 --- a/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts +++ b/dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/example.interceptor.ts @@ -1,9 +1,10 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import * as Sentry from '@sentry/nestjs'; +import { Observable } from 'rxjs'; @Injectable() export class ExampleInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler) { + intercept(context: ExecutionContext, next: CallHandler): Observable { Sentry.startSpan({ name: 'test-interceptor-span' }, () => {}); return next.handle().pipe(); } diff --git a/packages/node/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts index 6b452ab3add3..b3d1b3547118 100644 --- a/packages/node/src/integrations/tracing/nest.ts +++ b/packages/node/src/integrations/tracing/nest.ts @@ -23,7 +23,6 @@ import { } from '@sentry/core'; import type { IntegrationFn, Span } from '@sentry/types'; import { addNonEnumerableProperty, logger } from '@sentry/utils'; -import type { Observable } from 'rxjs'; import { generateInstrumentOnce } from '../../otel/instrument'; interface MinimalNestJsExecutionContext { @@ -61,6 +60,13 @@ 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. */ From 73ca138145bc4a3aa09dd397dc5d41f3bf2aaaf5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 2 Aug 2024 11:29:51 +0200 Subject: [PATCH 13/14] fix(browser): Avoid showing browser extension error message in non-`window` global scopes (#13156) Relax our browser extension detection check to avoid showing the error message and blocking `Sentry.init` if the SDK is not initialized in a `window` global scope. For instance, this will allow `Sentry.init` to be executed in (service) workers. We likely don't need to worry about multiple SDK instance collisions in non-`window` global scopes. --- packages/browser/src/sdk.ts | 7 ++++++- packages/browser/test/sdk.test.ts | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 4dd323c6aa02..1421814ae9e5 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -72,7 +72,12 @@ type Runtime = { }; function shouldShowBrowserExtensionError(): boolean { - const windowWithMaybeExtension = WINDOW as typeof WINDOW & ExtensionProperties; + const windowWithMaybeExtension = + typeof WINDOW.window !== 'undefined' && (WINDOW as typeof WINDOW & ExtensionProperties); + if (!windowWithMaybeExtension) { + // No need to show the error if we're not in a browser window environment (e.g. service workers) + return false; + } const extensionKey = windowWithMaybeExtension.chrome ? 'chrome' : 'browser'; const extensionObject = windowWithMaybeExtension[extensionKey]; diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index 31178bc84423..80e54e3d49d2 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -149,6 +149,7 @@ describe('init', () => { Object.defineProperty(WINDOW, 'chrome', { value: undefined, writable: true }); Object.defineProperty(WINDOW, 'browser', { value: undefined, writable: true }); Object.defineProperty(WINDOW, 'nw', { value: undefined, writable: true }); + Object.defineProperty(WINDOW, 'window', { value: WINDOW, writable: true }); }); it('logs a browser extension error if executed inside a Chrome extension', () => { @@ -229,6 +230,18 @@ describe('init', () => { consoleErrorSpy.mockRestore(); }); + it("doesn't log a browser extension error if the `window` object isn't defined", () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + Object.defineProperty(WINDOW, 'window', { value: undefined }); + + init(options); + + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); + it("doesn't return a client on initialization error", () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); From c1aefca55e7689a5518cd06cd3be5f8aed00eecc Mon Sep 17 00:00:00 2001 From: nicohrubec Date: Fri, 2 Aug 2024 11:37:32 +0200 Subject: [PATCH 14/14] meta: Update CHANGELOG for 8.23.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef06025545f1..80a592cd4575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 8.23.0 + +- feat(cloudflare): Add Cloudflare D1 instrumentation (#13142) +- 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) +- fix(browser): Avoid showing browser extension error message in non-`window` global scopes (#13156) +- fix(nestjs): Inline Observable type to resolve missing 'rxjs' dependency (#13166) + ## 8.22.0 ### Important Changes