From ed29e44af00ac684cecb9d01316648da56dda043 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 5 Aug 2026 11:36:25 +0200 Subject: [PATCH] feat(bundler-plugins)!: Migrate from @sentry/cli to the sentry CLI SDK Replace the @sentry/cli v2 binary wrapper with the programmatic SDK (createSentrySDK) exposed by the new sentry CLI package. A SentryCliAdapter in src/core/cli.ts maps the plugin's structured option shapes onto the SDK's typed methods, keeping all translation in one place: - release create/finalize/set-commits/deploy and sourcemap upload now go through the SDK instead of spawning the CLI binary; one SDK client is created per (options, project) pair since project selection is bound at client creation time - the integration-test fixtures replace the @sentry__cli.patch with a local sentry-stub fixture recording CLI invocations to sentry-cli-mock.json, so upload assertions no longer depend on the binary's internals - remix's release/sourcemap scripts and react-router's buildEnd hook are migrated to the same SDK - add a react-router-7-sourcemaps e2e test app covering the new upload path against a mock Sentry server Requires sentry >= 0.41.0: earlier 0.x builds had a link-fatal named import of Node 22.15+ zstd builtins in the ESM entry, which crashed the whole module on Node 18/20. Co-Authored-By: Claude --- .../fixtures/esbuild/basic-cjs.test.ts | 6 +- .../fixtures/esbuild/basic-sourcemaps.test.ts | 8 +- .../fixtures/esbuild/basic.test.ts | 6 +- .../fixtures/esbuild/package.json | 6 +- .../fixtures/esbuild/release-disabled.test.ts | 4 +- .../fixtures/patches/@sentry__cli.patch | 18 -- .../fixtures/rolldown/basic-cjs.test.ts | 6 +- .../rolldown/basic-sourcemaps.test.ts | 8 +- .../fixtures/rolldown/basic.test.ts | 6 +- .../fixtures/rolldown/package.json | 6 +- .../rolldown/release-disabled.test.ts | 4 +- .../fixtures/rollup3/basic-cjs.test.ts | 6 +- .../fixtures/rollup3/basic-sourcemaps.test.ts | 8 +- .../fixtures/rollup3/basic.test.ts | 6 +- .../fixtures/rollup3/package.json | 6 +- .../fixtures/rollup3/release-disabled.test.ts | 4 +- .../fixtures/rollup4/basic-cjs.test.ts | 6 +- .../fixtures/rollup4/basic-sourcemaps.test.ts | 8 +- .../fixtures/rollup4/basic.test.ts | 6 +- .../fixtures/rollup4/package.json | 6 +- .../fixtures/rollup4/release-disabled.test.ts | 4 +- .../fixtures/sentry-stub/index.cjs | 110 +++++++++ .../fixtures/sentry-stub/index.d.cts | 39 +++ .../fixtures/sentry-stub/package.json | 18 ++ .../fixtures/utils.ts | 3 +- .../fixtures/vite4/basic-cjs.test.ts | 6 +- .../fixtures/vite4/basic-sourcemaps.test.ts | 8 +- .../fixtures/vite4/basic.test.ts | 6 +- .../fixtures/vite4/package.json | 6 +- .../fixtures/vite4/release-disabled.test.ts | 4 +- .../fixtures/vite6/package.json | 6 +- .../fixtures/vite7/basic-cjs.test.ts | 6 +- .../fixtures/vite7/basic-sourcemaps.test.ts | 8 +- .../fixtures/vite7/basic.test.ts | 6 +- .../fixtures/vite7/package.json | 6 +- .../fixtures/vite7/release-disabled.test.ts | 4 +- .../fixtures/vite8/basic-cjs.test.ts | 6 +- .../fixtures/vite8/basic-sourcemaps.test.ts | 8 +- .../fixtures/vite8/basic.test.ts | 6 +- .../fixtures/vite8/package.json | 6 +- .../fixtures/vite8/release-disabled.test.ts | 4 +- .../fixtures/webpack5/basic-cjs.test.ts | 6 +- .../webpack5/basic-sourcemaps.test.ts | 8 +- .../fixtures/webpack5/basic.test.ts | 6 +- .../fixtures/webpack5/package.json | 6 +- .../webpack5/release-disabled.test.ts | 4 +- .../sentry-stub-conformance.ts | 48 ++++ dev-packages/e2e-tests/run.ts | 15 +- .../nextjs-sourcemaps/assert-build.ts | 9 +- .../react-router-7-sourcemaps/.gitignore | 6 + .../react-router-7-sourcemaps/app/root.tsx | 23 ++ .../react-router-7-sourcemaps/app/routes.ts | 3 + .../app/routes/home.tsx | 8 + .../react-router-7-sourcemaps/assert-build.ts | 67 ++++++ .../react-router-7-sourcemaps/package.json | 35 +++ .../react-router.config.ts | 7 + .../start-mock-sentry-server.mjs | 3 + .../react-router-7-sourcemaps/tsconfig.json | 19 ++ .../react-router-7-sourcemaps/vite.config.ts | 31 +++ .../react-router-sourcemaps/assert-build.ts | 35 ++- .../test-utils/src/mock-sentry-server.ts | 38 ++- .../test-utils/src/sourcemap-upload-utils.ts | 44 +++- packages/bundler-plugins/package.json | 2 +- .../src/core/build-plugin-manager.ts | 126 ++++------ packages/bundler-plugins/src/core/cli.ts | 172 +++++++++++++ packages/bundler-plugins/src/core/index.ts | 11 - .../src/core/sentry/telemetry.ts | 33 +-- packages/bundler-plugins/src/core/utils.ts | 17 -- packages/bundler-plugins/src/esbuild/index.ts | 1 - packages/bundler-plugins/src/rollup/index.ts | 1 - packages/bundler-plugins/src/vite/index.ts | 1 - packages/bundler-plugins/src/webpack/index.ts | 2 - .../bundler-plugins/src/webpack/webpack5.ts | 2 - .../test/core/build-plugin-manager.test.ts | 227 ++++++++---------- .../test/core/cli-contract.test.ts | 178 ++++++++++++++ .../test/core/sentry/telemetry.test.ts | 16 +- .../bundler-plugins/test/core/utils.test.ts | 23 -- packages/react-router/package.json | 4 +- .../src/vite/buildEnd/handleOnBuildEnd.ts | 29 +-- .../vite/buildEnd/handleOnBuildEnd.test.ts | 70 +++--- packages/remix/package.json | 2 +- packages/remix/scripts/createRelease.js | 19 +- packages/remix/scripts/injectDebugId.js | 10 +- .../remix/scripts/sentry-upload-sourcemaps.js | 15 +- .../test/scripts/upload-sourcemaps.test.ts | 70 +++--- yarn.lock | 75 +----- 86 files changed, 1274 insertions(+), 657 deletions(-) delete mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/patches/@sentry__cli.patch create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.cjs create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.d.cts create mode 100644 dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/package.json create mode 100644 dev-packages/bundler-plugin-integration-tests/sentry-stub-conformance.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/root.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/routes.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/routes/home.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/assert-build.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/package.json create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/react-router.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/start-mock-sentry-server.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/vite.config.ts create mode 100644 packages/bundler-plugins/src/core/cli.ts create mode 100644 packages/bundler-plugins/test/core/cli-contract.test.ts diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic-cjs.test.ts index b6497403fc72..97e3904f8a7a 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic-cjs.test.ts @@ -32,9 +32,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { var basic_default = void 0; })(); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic-sourcemaps.test.ts index 23695e5ccdce..29d92a98538f 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic-sourcemaps.test.ts @@ -34,10 +34,10 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //# sourceMappingURL=basic-sourcemaps.js.map ", "basic-sourcemaps.js.map": "{"version":3,"sources":["../../_sentry-injection-stub","sentry-debug-id-stub:_sentry-debug-id-injection-stub?sentry-module-id=00000000-0000-0000-0000-000000000000","../../src/basic.js","../../src/basic.js"],"sourcesContent":["!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e.SENTRY_RELEASE={id:\\"CURRENT_SHA\\"};}catch(e){}}();","!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"00000000-0000-0000-0000-000000000000\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-00000000-0000-0000-0000-000000000000\\");}catch(e){}}();","// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n","\\n import \\"_sentry-debug-id-injection-stub\\";\\n import * as OriginalModule from \\"./src/basic.js\\";\\n export default OriginalModule.default;\\n export * from \\"./src/basic.js\\";"],"mappings":";;AAAA,IAAC,WAAU;AAAC,QAAG;AAAC,UAAI,IAAE,eAAa,OAAO,SAAO,SAAO,eAAa,OAAO,SAAO,SAAO,eAAa,OAAO,aAAW,aAAW,eAAa,OAAO,OAAK,OAAK,CAAC;AAAE,QAAE,iBAAe,EAAC,IAAG,2CAA0C;AAAA,IAAE,SAAOA,IAAE;AAAA,IAAC;AAAA,EAAC,GAAE;;;ACAnP,IAAC,WAAU;AAAC,QAAG;AAAC,UAAI,IAAE,eAAa,OAAO,SAAO,SAAO,eAAa,OAAO,SAAO,SAAO,eAAa,OAAO,aAAW,aAAW,eAAa,OAAO,OAAK,OAAK,CAAC;AAAE,UAAI,IAAG,IAAI,EAAE,QAAO;AAAM,YAAI,EAAE,kBAAgB,EAAE,mBAAiB,CAAC,GAAE,EAAE,gBAAgB,CAAC,IAAE,wCAAuC,EAAE,2BAAyB;AAAA,IAAoD,SAAOC,IAAE;AAAA,IAAC;AAAA,EAAC,GAAE;;;ACCnY,UAAQ,IAAI,aAAa;;;ACEX,MAAO,gBAAuB;","names":["e","e"]}", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], - ["sourcemaps","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--ignore","node_modules","--no-rewrite"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--no-rewrite"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic.test.ts index b6497403fc72..97e3904f8a7a 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/basic.test.ts @@ -32,9 +32,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { var basic_default = void 0; })(); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/package.json index b1a68dc39eea..cd13efd2a1a3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/package.json @@ -10,10 +10,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/release-disabled.test.ts index 71eaf7b92ce9..95daff0a5b95 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/release-disabled.test.ts @@ -32,8 +32,8 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { var basic_default = void 0; })(); ", - "sentry-cli-mock.json": "["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/patches/@sentry__cli.patch b/dev-packages/bundler-plugin-integration-tests/fixtures/patches/@sentry__cli.patch deleted file mode 100644 index d57a008a264c..000000000000 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/patches/@sentry__cli.patch +++ /dev/null @@ -1,18 +0,0 @@ -diff --git a/js/helper.js b/js/helper.js -index 56f95c9784d32a48e98b8341bfa336f0ed61a6e7..b97e41b4dcf96b9dcd9e171b91b575898b6cfbc3 100644 ---- a/js/helper.js -+++ b/js/helper.js -@@ -295,6 +295,13 @@ function execute(args_1, live_1, silent_1, configFile_1) { - if (config.vcsRemote) { - env.SENTRY_VCS_REMOTE = config.vcsRemote; - } -+ -+ if (process.env['SENTRY_TEST_OUT_DIR']) { -+ const out = path.join(process.env['SENTRY_TEST_OUT_DIR'], 'sentry-cli-mock.json'); -+ fs.appendFileSync(out, JSON.stringify(args) + ',\n'); -+ return Promise.resolve(); -+ } -+ - if (config.customHeader) { - env.CUSTOM_HEADER = config.customHeader; - } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts index c59af2abe5e4..46effbc444c9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-cjs.test.ts @@ -17,9 +17,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { console.log("hello world"); //#endregion ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-sourcemaps.test.ts index 11726184aff9..77a218bcc879 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic-sourcemaps.test.ts @@ -19,10 +19,10 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //# sourceMappingURL=basic.js.map", "basic.js.map": "{"version":3,"file":"basic.js","names":[],"sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"mappings":";;;;;;;;;AACA,QAAQ,IAAI,cAAc"}", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], - ["sourcemaps","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--ignore","node_modules","--no-rewrite"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--no-rewrite"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts index c59af2abe5e4..46effbc444c9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/basic.test.ts @@ -17,9 +17,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { console.log("hello world"); //#endregion ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/package.json index 6448dc45fc8c..81bcfb4ccfe3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/package.json @@ -11,10 +11,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts index 3bf5b9b5f855..468da9e55d09 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/release-disabled.test.ts @@ -17,8 +17,8 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { console.log("hello world"); //#endregion ", - "sentry-cli-mock.json": "["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts index 5667c2ea0b02..38c595ef7a34 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-cjs.test.ts @@ -8,9 +8,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "basic.js": "// eslint-disable-next-line no-console !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-sourcemaps.test.ts index 0f4f0bed3833..c03fb84469df 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic-sourcemaps.test.ts @@ -10,10 +10,10 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //# sourceMappingURL=basic.js.map ", "basic.js.map": "{"version":3,"file":"basic.js","sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"mappings":"AAAA,CAAA,CAAA,CAAA,MAAA,CAAA,OAAA,CAAA,IAAA,CAAA,IAAA,CAAA,EAAA,CAAA;scACA,OAAO,CAAC,GAAG,CAAC,CAAA,KAAA,CAAA,KAAA,CAAa,CAAC"}", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], - ["sourcemaps","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--ignore","node_modules","--no-rewrite"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--no-rewrite"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts index 5667c2ea0b02..38c595ef7a34 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/basic.test.ts @@ -8,9 +8,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "basic.js": "// eslint-disable-next-line no-console !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/package.json index 58d8ada98674..edf910f23e3f 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/package.json @@ -13,10 +13,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts index 051038a480e3..39c7da1959d9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/release-disabled.test.ts @@ -8,8 +8,8 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { "basic.js": "// eslint-disable-next-line no-console !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts index 5667c2ea0b02..38c595ef7a34 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-cjs.test.ts @@ -8,9 +8,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "basic.js": "// eslint-disable-next-line no-console !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-sourcemaps.test.ts index 0f4f0bed3833..c03fb84469df 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic-sourcemaps.test.ts @@ -10,10 +10,10 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //# sourceMappingURL=basic.js.map ", "basic.js.map": "{"version":3,"file":"basic.js","sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"mappings":"AAAA,CAAA,CAAA,CAAA,MAAA,CAAA,OAAA,CAAA,IAAA,CAAA,IAAA,CAAA,EAAA,CAAA;scACA,OAAO,CAAC,GAAG,CAAC,CAAA,KAAA,CAAA,KAAA,CAAa,CAAC"}", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], - ["sourcemaps","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--ignore","node_modules","--no-rewrite"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--no-rewrite"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts index 5667c2ea0b02..38c595ef7a34 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/basic.test.ts @@ -8,9 +8,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { "basic.js": "// eslint-disable-next-line no-console !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/package.json index baeb48f55967..be72e1b2d1f8 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/package.json @@ -13,10 +13,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts index 051038a480e3..39c7da1959d9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/release-disabled.test.ts @@ -8,8 +8,8 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { "basic.js": "// eslint-disable-next-line no-console !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.cjs b/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.cjs new file mode 100644 index 000000000000..1a2dd4ec7839 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.cjs @@ -0,0 +1,110 @@ +'use strict'; + +/** + * Recording stub of the `sentry` CLI SDK for the bundler-plugin integration tests. + * + * The real CLI talks to Sentry over HTTP. In these tests we only care about *which* commands the + * bundler plugin issues and with what arguments, so this stub records each call to + * `sentry-cli-mock.json` (under `SENTRY_TEST_OUT_DIR`) as a flat CLI-style argument array and + * resolves without doing any network work. This mirrors the previous approach of patching + * `@sentry/cli`'s `execute()` to dump its args. + */ + +const fs = require('fs'); +const path = require('path'); + +function record(args) { + const outDir = process.env['SENTRY_TEST_OUT_DIR']; + if (!outDir) { + return; + } + const out = path.join(outDir, 'sentry-cli-mock.json'); + fs.appendFileSync(out, `${JSON.stringify(args)},\n`); +} + +/** Append boolean flags (only when true) and value flags (only when set) to an args array. */ +function pushFlags(args, flags, spec) { + for (const [key, flag] of Object.entries(spec)) { + const value = flags?.[key]; + if (value === undefined || value === false) { + continue; + } + if (value === true) { + args.push(flag); + } else { + args.push(flag, String(value)); + } + } +} + +function createSentrySDK(options = {}) { + const project = options.project; + + return { + release: { + create(params = {}) { + const args = ['release', 'create']; + if (params.orgVersion) { + args.push(params.orgVersion); + } + pushFlags(args, params, { project: '--project', finalize: '--finalize', ref: '--ref', url: '--url' }); + record(args); + return Promise.resolve(undefined); + }, + finalize(params = {}) { + const args = ['release', 'finalize']; + if (params.orgVersion) { + args.push(params.orgVersion); + } + record(args); + return Promise.resolve(undefined); + }, + 'set-commits'(params = {}) { + const args = ['release', 'set-commits']; + if (params.orgVersion) { + args.push(params.orgVersion); + } + pushFlags(args, params, { auto: '--auto', local: '--local', clear: '--clear', commit: '--commit' }); + record(args); + return Promise.resolve(undefined); + }, + }, + sourcemap: { + upload(params = {}) { + const args = ['sourcemap', 'upload']; + if (project) { + args.push('-p', project); + } + pushFlags(args, params, { release: '--release', dist: '--dist' }); + if (params.directory) { + args.push(params.directory); + } + pushFlags(args, params, { + ext: '--ext', + ignore: '--ignore', + urlPrefix: '--url-prefix', + noRewrite: '--no-rewrite', + }); + record(args); + return Promise.resolve(undefined); + }, + inject(params = {}) { + const args = ['sourcemap', 'inject']; + if (params.directory) { + args.push(params.directory); + } + pushFlags(args, params, { ignore: '--ignore' }); + record(args); + return Promise.resolve(undefined); + }, + }, + run(...args) { + record(args); + return Promise.resolve(undefined); + }, + }; +} + +module.exports = createSentrySDK; +module.exports.default = createSentrySDK; +module.exports.createSentrySDK = createSentrySDK; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.d.cts b/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.d.cts new file mode 100644 index 000000000000..98e6fc3bcbbf --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/index.d.cts @@ -0,0 +1,39 @@ +/** + * Types for the recording stub, taken from the real CLI rather than restated here. + * + * The stub used to declare its params as `Record`, which accepted anything: a + * param the CLI had renamed, or one it never had, still produced a green snapshot. Importing the + * real types means the build plugin's calls are checked against the CLI's actual surface, so + * drift fails `check:types` instead of silently passing. + * + * This only covers the arguments going in. How the stub turns them into the recorded CLI-style + * argv is its own invention and cannot be checked this way - see the adapter's contract test, + * which runs those same calls against the real CLI. + */ +import type { + ReleaseCreateParams, + ReleaseFinalizeParams, + ReleaseSetCommitsParams, + SentryOptions, + SourcemapInjectParams, + SourcemapUploadParams, +} from 'sentry'; + +export type { SentryOptions }; + +export type SentrySDK = { + release: { + create(params?: ReleaseCreateParams): Promise; + finalize(params?: ReleaseFinalizeParams): Promise; + 'set-commits'(params?: ReleaseSetCommitsParams): Promise; + }; + sourcemap: { + upload(params?: SourcemapUploadParams): Promise; + inject(params?: SourcemapInjectParams): Promise; + }; + run(...args: string[]): Promise; +}; + +declare function createSentrySDK(options?: SentryOptions): SentrySDK; + +export default createSentrySDK; diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/package.json new file mode 100644 index 000000000000..52000222f7de --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/sentry-stub/package.json @@ -0,0 +1,18 @@ +{ + "name": "sentry", + "version": "0.39.0", + "private": true, + "description": "Recording stub of the Sentry CLI used by the bundler-plugin integration tests. Records every SDK call to sentry-cli-mock.json instead of talking to Sentry.", + "main": "./index.cjs", + "types": "./index.d.cts", + "exports": { + ".": { + "types": "./index.d.cts", + "require": "./index.cjs", + "default": "./index.cjs" + } + }, + "engines": { + "node": ">=18.0" + } +} diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/utils.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/utils.ts index 030b44535d9e..6e23852ff1f6 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/utils.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/utils.ts @@ -15,7 +15,8 @@ type SourceMap = { export function runBundler(command: string, opt: ExecSyncOptions, outDir?: string): void { if (outDir) { - // We've patched the sentry-cli helper to write the args to a file instead of actually executing the command + // The `sentry` CLI dependency is overridden with a recording stub (fixtures/sentry-stub) that + // writes the invoked args to `sentry-cli-mock.json` instead of talking to Sentry. opt.env = { ...opt.env, SENTRY_TEST_OUT_DIR: outDir }; } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-cjs.test.ts index 9e086c0dc020..8ba63d23bb0b 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-cjs.test.ts @@ -16,9 +16,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { }(); console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-sourcemaps.test.ts index 423bc333ea17..5cfa09a8d1e7 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic-sourcemaps.test.ts @@ -18,10 +18,10 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //# sourceMappingURL=basic.js.map ", "basic.js.map": "{"version":3,"file":"basic.js","sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"mappings":";;;;;;;;;AACA,QAAQ,IAAI,aAAa;"}", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], - ["sourcemaps","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--ignore","node_modules","--no-rewrite"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--no-rewrite"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic.test.ts index 9e086c0dc020..8ba63d23bb0b 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/basic.test.ts @@ -16,9 +16,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { }(); console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/package.json index d9f581f72433..5ea315aea92d 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/package.json @@ -12,10 +12,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/release-disabled.test.ts index a71c289a22b8..31ebf5cc9d16 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/release-disabled.test.ts @@ -16,8 +16,8 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { }(); console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite6/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/vite6/package.json index c434f1c92db7..282017b87c29 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite6/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite6/package.json @@ -10,10 +10,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-cjs.test.ts index e6d000c44c49..19b59fda09df 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-cjs.test.ts @@ -16,9 +16,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { })(); console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-sourcemaps.test.ts index 89553dea9a3f..e80d429a039d 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic-sourcemaps.test.ts @@ -18,10 +18,10 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //# sourceMappingURL=basic.js.map ", "basic.js.map": "{"version":3,"file":"basic.js","sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"mappings":";;;;;;;;;AACA,QAAQ,IAAI,aAAa;"}", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], - ["sourcemaps","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--ignore","node_modules","--no-rewrite"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--no-rewrite"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic.test.ts index e6d000c44c49..19b59fda09df 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/basic.test.ts @@ -16,9 +16,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { })(); console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/package.json index 333d89e3bb55..ac70089d8902 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/package.json @@ -12,10 +12,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/release-disabled.test.ts index 453c03f9a040..adc4f30dbebe 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/release-disabled.test.ts @@ -16,8 +16,8 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { })(); console.log("hello world"); ", - "sentry-cli-mock.json": "["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-cjs.test.ts index c59af2abe5e4..46effbc444c9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-cjs.test.ts @@ -17,9 +17,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { console.log("hello world"); //#endregion ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-sourcemaps.test.ts index c2d1a1ab61e9..73bb95d3294c 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic-sourcemaps.test.ts @@ -19,10 +19,10 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //# sourceMappingURL=basic.js.map", "basic.js.map": "{"version":3,"file":"basic.js","names":[],"sources":["../../src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"mappings":";;;;;;;;;AACA,QAAQ,IAAI,aAAa"}", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], - ["sourcemaps","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--ignore","node_modules","--no-rewrite"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--no-rewrite"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic.test.ts index c59af2abe5e4..46effbc444c9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/basic.test.ts @@ -17,9 +17,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { console.log("hello world"); //#endregion ", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/package.json index 5d844920fdcd..28c5437684e9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/package.json @@ -12,10 +12,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/release-disabled.test.ts index 3bf5b9b5f855..468da9e55d09 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/release-disabled.test.ts @@ -17,8 +17,8 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { console.log("hello world"); //#endregion ", - "sentry-cli-mock.json": "["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts index 0bc8f4f2b4e5..8dcfab1b18f4 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts @@ -13,9 +13,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { /******/ })() ;", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-sourcemaps.test.ts index c9fedd3d47f8..7ea9f54ebe6e 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-sourcemaps.test.ts @@ -15,10 +15,10 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { ; //# sourceMappingURL=basic.js.map", "basic.js.map": "{"version":3,"file":"basic.js","mappings":";;;AAAA;AACA","sources":["webpack://webpack5-integration-tests/./src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"sourceRoot":""}", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], - ["sourcemaps","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--ignore","node_modules","--no-rewrite"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], + ["sourcemap","upload","-p","fake-project","--release","CURRENT_SHA","sentry-bundler-plugin-upload-path","--no-rewrite"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts index 0bc8f4f2b4e5..8dcfab1b18f4 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts @@ -13,9 +13,9 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { /******/ })() ;", - "sentry-cli-mock.json": "["releases","new","CURRENT_SHA"], - ["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","create","CURRENT_SHA"], + ["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/package.json b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/package.json index 4de04b5c29b4..33a4b1933834 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/package.json +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/package.json @@ -13,10 +13,8 @@ "pnpm": { "overrides": { "@sentry/bundler-plugins": "file:../../../../packages/bundler-plugins/sentry-bundler-plugins-10.67.0.tgz", - "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz" - }, - "patchedDependencies": { - "@sentry/cli": "../patches/@sentry__cli.patch" + "@sentry/core": "file:../../../../packages/core/sentry-core-10.67.0.tgz", + "sentry": "file:../sentry-stub" } } } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts index 9d180fc54288..5288fc48bcba 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts @@ -13,8 +13,8 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { /******/ })() ;", - "sentry-cli-mock.json": "["releases","set-commits","CURRENT_SHA","--auto","--ignore-missing"], - ["releases","finalize","CURRENT_SHA"], + "sentry-cli-mock.json": "["release","set-commits","CURRENT_SHA","--auto"], + ["release","finalize","CURRENT_SHA"], ", } `); diff --git a/dev-packages/bundler-plugin-integration-tests/sentry-stub-conformance.ts b/dev-packages/bundler-plugin-integration-tests/sentry-stub-conformance.ts new file mode 100644 index 000000000000..ea0506c99e16 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/sentry-stub-conformance.ts @@ -0,0 +1,48 @@ +/** + * Compile-time check that the recording stub still matches the real CLI. + * + * `index.d.cts` derives its params from the real CLI's types, but the root tsconfig sets + * `skipLibCheck`, so declaration files are never checked and that import alone proves nothing. + * This is a regular `.ts` file so it is checked, and referencing each type and method here fails + * `check:types` if the CLI renames or drops any of the surface the build plugin calls. + * + * It has to live outside `fixtures/sentry-stub` because that package is itself named `sentry`, + * so an import of `sentry` from within it self-references the stub and checks nothing. + * + * Only the shape going in is covered. Whether the CLI *accepts* the argv the plugin builds is a + * runtime question - see the adapter's contract test, which issues these calls for real. + */ +import type { + ReleaseCreateParams, + ReleaseFinalizeParams, + ReleaseSetCommitsParams, + SentryOptions, + SourcemapInjectParams, + SourcemapUploadParams, + createSentrySDK, +} from 'sentry'; + +type RealSDK = ReturnType; + +/** Fails to compile if the real SDK ever loses a method the plugin depends on. */ +type UsedSurface = { + releaseCreate: RealSDK['release']['create']; + releaseFinalize: RealSDK['release']['finalize']; + releaseSetCommits: RealSDK['release']['set-commits']; + releaseDeploy: RealSDK['release']['deploy']; + sourcemapUpload: RealSDK['sourcemap']['upload']; + sourcemapInject: RealSDK['sourcemap']['inject']; + run: RealSDK['run']; +}; + +/** Fails to compile if a param type the stub declares is renamed or removed. */ +type UsedParams = { + releaseCreate: ReleaseCreateParams; + releaseFinalize: ReleaseFinalizeParams; + releaseSetCommits: ReleaseSetCommitsParams; + sourcemapUpload: SourcemapUploadParams; + sourcemapInject: SourcemapInjectParams; + options: SentryOptions; +}; + +export type { UsedParams, UsedSurface }; diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index 6957296385fa..5a44ea9a9416 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { spawn } from 'child_process'; import * as dotenv from 'dotenv'; -import { mkdtemp, readFile, rm } from 'fs/promises'; +import { mkdir, mkdtemp, readFile, rm } from 'fs/promises'; import { sync as globSync } from 'glob'; import { tmpdir } from 'os'; import { join, resolve } from 'path'; @@ -217,6 +217,15 @@ async function run(): Promise { await copyToTemp(originalPath, tmpDirPath); await addPnpmOverrides(tmpDirPath, packedDirPath); + // The Sentry CLI keeps its config and a SQLite cache in `~/.sentry`, so without this every + // build reads and writes the machine's global CLI state. That leaks a developer's real auth + // config into the run, and any state the natively installed CLI leaves behind - notably a + // `cli.db` it has switched to WAL journal mode, which the CLI's wasm SQLite build cannot open + // at all - breaks the build with `unable to open database file`. Give each app its own. + const sentryConfigDir = join(tmpDirPath, '.tmp_sentry_home'); + await mkdir(sentryConfigDir, { recursive: true }); + const appEnv = { ...env, SENTRY_CONFIG_DIR: sentryConfigDir }; + const cwd = tmpDirPath; // Resolve variant if needed const { buildCommand, assertCommand, testLabel, matchedVariantLabel } = variantLabel @@ -233,7 +242,7 @@ async function run(): Promise { } console.log(`Building ${testLabel} in ${tmpDirPath}...`); - await asyncExec(`volta run ${buildCommand}`, { env, cwd }); + await asyncExec(`volta run ${buildCommand}`, { env: appEnv, cwd }); console.log(`Testing ${testLabel}...`); // Pass command as a string to support shell features (env vars, operators like &&) @@ -248,7 +257,7 @@ async function run(): Promise { return flag; }); const testCommand = `volta run ${assertCommand}${quotedTestFlags.length > 0 ? ` ${quotedTestFlags.join(' ')}` : ''}`; - await asyncExec(testCommand, { env, cwd }); + await asyncExec(testCommand, { env: appEnv, cwd }); // clean up (although this is tmp, still nice to do) await rm(tmpDirPath, { recursive: true }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/assert-build.ts b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/assert-build.ts index 6bd6b5f92568..116dbd875baa 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/assert-build.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-sourcemaps/assert-build.ts @@ -4,6 +4,7 @@ import { getArtifactBundles, getDebugIdPairs, getSourcemaps, + getSourcemapSources, getChunkUploadPosts, getAssembleRequests, } from '@sentry-internal/test-utils'; @@ -57,9 +58,13 @@ assert.ok( 'Expected at least one sourcemap with non-empty mappings', ); -// At least one sourcemap references app source files +// At least one sourcemap references app source files. +// +// Via `getSourcemapSources`, because Turbopack emits indexed source maps: the app's paths sit in +// `sections[].map.sources` and the top-level `sources` is empty, so reading the latter finds +// nothing however well the upload worked. assert.ok( - sourcemaps.some(s => s.sourcemap.sources?.some(src => /client-page|page\.tsx/.test(src))), + sourcemaps.some(s => getSourcemapSources(s.sourcemap).some(src => /client-page|page\.tsx/.test(src))), 'Expected at least one sourcemap referencing app source files', ); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/.gitignore new file mode 100644 index 000000000000..9ab7d57b7adc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/.gitignore @@ -0,0 +1,6 @@ +build +.react-router +.tmp_mock_uploads.json +.tmp_chunks +.tmp_build_stdout +.tmp_build_stderr diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/root.tsx new file mode 100644 index 000000000000..c09b53b99d46 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/root.tsx @@ -0,0 +1,23 @@ +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/routes.ts new file mode 100644 index 000000000000..205ff3ccb9fd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from '@react-router/dev/routes'; + +export default [index('routes/home.tsx')] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/routes/home.tsx new file mode 100644 index 000000000000..430b74caea40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/app/routes/home.tsx @@ -0,0 +1,8 @@ +export function meta() { + return [{ title: 'Sourcemaps Test' }]; +} + +export default function Home() { + const message = `hello from react-router sourcemaps test at ${new Date().toISOString()}`; + return

{message}

; +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/assert-build.ts b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/assert-build.ts new file mode 100644 index 000000000000..0438187d4fb9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/assert-build.ts @@ -0,0 +1,67 @@ +import * as assert from 'assert/strict'; +import { + getArtifactBundles, + getAssembleRequests, + getDebugIdPairs, + loadMockServerResults, +} from '@sentry-internal/test-utils'; + +const requests = loadMockServerResults(); + +console.log(`Captured ${requests.length} requests to mock Sentry server:\n`); +for (const req of requests) { + console.log(` ${req.method} ${req.url} (${req.bodySize} bytes)`); +} +console.log(''); + +// Auth token is forwarded on the upload requests. +const authenticated = requests.filter(r => r.authorization.includes('fake-auth-token')); +assert.ok(authenticated.length > 0, 'Expected requests carrying the configured auth token'); + +// The buildEnd hook creates and finalizes the release. +assert.ok( + requests.some(r => r.url?.includes('/releases') && r.method === 'POST'), + 'Expected a POST to create the release', +); +assert.ok( + requests.some(r => r.url?.includes('/releases/') && r.method === 'PUT'), + 'Expected a PUT to finalize the release', +); + +// Chunk-upload options are fetched before uploading. +assert.ok( + requests.some(r => r.url?.includes('/chunk-upload/') && r.method === 'GET'), + 'Expected a GET for chunk-upload options', +); + +// The artifact bundle is uploaded via the assemble endpoint. For a bundle this +// small the payload rides in the assemble request itself (no separate +// /chunk-upload/ POST when the server reports no missing chunks), so the +// assemble request with its chunk checksums is the authoritative upload signal. +const assembleReqs = getAssembleRequests(requests); +assert.ok(assembleReqs.length > 0, 'Expected at least one artifact bundle assemble request'); +for (const req of assembleReqs) { + assert.ok(req.assembleBody?.projects?.includes('test-project'), 'Expected assemble request to target test-project'); + assert.ok(req.assembleBody?.version === 'test-release', 'Expected assemble request to reference the release version'); + assert.ok((req.assembleBody?.chunks?.length ?? 0) > 0, 'Expected assemble request to carry chunk checksums'); + const sha1 = /^[\da-f]{40}$/i; + for (const chunk of req.assembleBody?.chunks ?? []) { + assert.match(chunk, sha1, `Expected a SHA-1 chunk checksum, got: ${chunk}`); + } +} +console.log(`Verified ${assembleReqs.length} assemble request(s) with valid chunk checksums\n`); + +// When the server reports missing chunks, the CLI additionally POSTs the raw +// bundle — in that case assert on the richer debug-ID/manifest signal too. +const bundles = getArtifactBundles(requests); +if (bundles.length > 0) { + const debugIdPairs = getDebugIdPairs(bundles); + assert.ok(debugIdPairs.length > 0, 'Expected JS/sourcemap pairs with matching debug IDs'); + const uuidRegex = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i; + for (const pair of debugIdPairs) { + assert.match(pair.debugId, uuidRegex, `Invalid debug ID: ${pair.debugId}`); + console.log(` ${pair.debugId} ${pair.jsUrl}`); + } +} + +console.log('All sourcemap upload assertions passed!'); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/package.json new file mode 100644 index 000000000000..42f33c25cce9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/package.json @@ -0,0 +1,35 @@ +{ + "name": "react-router-7-sourcemaps", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "build": "node start-mock-sentry-server.mjs & react-router build > .tmp_build_stdout 2> .tmp_build_stderr; BUILD_EXIT=$?; kill %1 2>/dev/null; exit $BUILD_EXIT", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm tsx assert-build.ts" + }, + "dependencies": { + "@react-router/node": "^7.13.0", + "@react-router/serve": "^7.13.0", + "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", + "isbot": "^5.1.17", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.13.0" + }, + "devDependencies": { + "@react-router/dev": "^7.13.0", + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/node": "^18.19.1", + "@types/react": "18.3.1", + "@types/react-dom": "18.3.1", + "tsx": "4.19.2", + "typescript": "^5.6.3", + "vite": "^5.4.11" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/react-router.config.ts new file mode 100644 index 000000000000..a5314e0a13b6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from '@react-router/dev/config'; +import { sentryOnBuildEnd } from '@sentry/react-router'; + +export default { + ssr: true, + buildEnd: sentryOnBuildEnd, +} satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/start-mock-sentry-server.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/start-mock-sentry-server.mjs new file mode 100644 index 000000000000..cce37a5f9daf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/start-mock-sentry-server.mjs @@ -0,0 +1,3 @@ +import { startMockSentryServer } from '@sentry-internal/test-utils'; + +startMockSentryServer(); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/tsconfig.json new file mode 100644 index 000000000000..b7683bd20a45 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["**/*", ".react-router/types/**/*"] +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/vite.config.ts new file mode 100644 index 000000000000..da36cae6dbd8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-sourcemaps/vite.config.ts @@ -0,0 +1,31 @@ +import { reactRouter } from '@react-router/dev/vite'; +import { sentryReactRouter } from '@sentry/react-router'; +import { defineConfig } from 'vite'; + +const sentryConfig = { + // Route every CLI/API request at the local mock Sentry server. + unstable_sentryVitePluginOptions: { + url: 'http://localhost:3032', + }, + authToken: 'fake-auth-token', + org: 'test-org', + project: 'test-project', + release: { + name: 'test-release', + }, + sourcemaps: { + // Keep the emitted sourcemaps so we can inspect the upload; don't delete after upload. + filesToDeleteAfterUpload: [], + }, + debug: true, +}; + +export default defineConfig(config => { + return { + build: { + sourcemap: true, + }, + plugins: [reactRouter(), sentryReactRouter(sentryConfig, config)], + sentryConfig, + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-sourcemaps/assert-build.ts b/dev-packages/e2e-tests/test-applications/react-router-sourcemaps/assert-build.ts index 970744c09427..2bc4986220d4 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-sourcemaps/assert-build.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-sourcemaps/assert-build.ts @@ -23,32 +23,35 @@ function getClientChunks(): string[] { const chunks = getClientChunks(); assert.ok(chunks.length > 0, `Expected at least one client chunk in ${CLIENT_ASSETS_DIR}`); -// 1. Every chunk carries exactly one debug ID. +// 1. No chunk carries more than one debug ID. // // Two injections per chunk is the failure mode of // https://github.com/getsentry/sentry-javascript/issues/22929: both snippets run at // runtime, `applyDebugIds` flattens them to a single filename, and the last one wins - // which is the CLI's, the one with no uploaded artifact bundle. Frames arrive minified. +// +// The other half - that a chunk which *should* carry one actually does - is asserted in +// step 3. The CLI only injects into JS it can pair with a source map, so chunks Vite emits +// without one (e.g. the route manifest) legitimately carry none, and which chunks those are +// is only known from the upload requests: the maps are deleted from disk after upload. const injectedDebugIds = new Map(); for (const chunk of chunks) { const code = fs.readFileSync(chunk, 'utf-8'); const ids = [...code.matchAll(DEBUG_ID_ASSIGNMENT)].map(match => match[1] as string); - // Exactly one, not "at most one": zero would mean injection silently skipped a chunk, - // which leaves its frames unresolvable just as surely as injecting twice does. - assert.equal( - ids.length, - 1, - `Expected exactly one debug ID in ${chunk}, found ${ids.length}: ${JSON.stringify([...new Set(ids)])}. ` + - 'More than one means debug IDs were injected twice (Vite plugin *and* sentryOnBuildEnd); ' + - 'none means injection skipped this chunk.', + assert.ok( + ids.length <= 1, + `Expected at most one debug ID in ${chunk}, found ${ids.length}: ${JSON.stringify([...new Set(ids)])}. ` + + 'More than one means debug IDs were injected twice (Vite plugin *and* sentryOnBuildEnd).', ); - injectedDebugIds.set(chunk, ids[0] as string); + if (ids.length === 1) { + injectedDebugIds.set(chunk, ids[0] as string); + } } -console.log(`all ${chunks.length} client chunk(s) carry exactly one debug ID\n`); +console.log(`no client chunk carries more than one debug ID (${injectedDebugIds.size}/${chunks.length} carry one)\n`); const requests = loadMockServerResults(); const bundles = getArtifactBundles(requests); @@ -84,11 +87,19 @@ assert.ok(uploadedDebugIds.size > 0, 'Expected at least one uploaded JS/source m const uploadedJsFiles = new Set(debugIdPairs.map(pair => path.basename(pair.jsUrl))); let crossCheckedChunks = 0; -for (const [chunk, injectedDebugId] of injectedDebugIds) { +for (const chunk of chunks) { if (!uploadedJsFiles.has(path.basename(chunk))) { continue; } + const injectedDebugId = injectedDebugIds.get(chunk); + // Its map reached Sentry, so a missing debug ID here means injection skipped a chunk it + // should have covered - the frames stay minified just as surely as a double injection. + assert.ok( + injectedDebugId, + `Expected exactly one debug ID in ${chunk}, found none, even though its source map was uploaded.`, + ); + const debugId = injectedDebugId.toLowerCase(); assert.ok( uploadedDebugIds.has(debugId), diff --git a/dev-packages/test-utils/src/mock-sentry-server.ts b/dev-packages/test-utils/src/mock-sentry-server.ts index 93ae312905e8..207319700ad2 100644 --- a/dev-packages/test-utils/src/mock-sentry-server.ts +++ b/dev-packages/test-utils/src/mock-sentry-server.ts @@ -127,15 +127,44 @@ function processChunkUpload( return nextChunkIndex; } +/** + * Answer an artifact bundle assemble request. + * + * The CLI is assemble-first: it asks Sentry to assemble a bundle from chunk checksums and only + * uploads the chunks the response reports as missing. Always answering `created` therefore means + * the chunks are never POSTed, and the bundle contents the assertions inspect never reach this + * server. So report every chunk as missing the first time a bundle is seen, and `created` on the + * retry that follows the upload - the same handshake the real API performs. + */ +function sendAssembleResponse(res: http.ServerResponse, record: RequestRecord, assembled: Set): void { + const { checksum, chunks } = record.assembleBody ?? {}; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + + if (!checksum || !chunks?.length || assembled.has(checksum)) { + res.end(JSON.stringify({ state: 'created', missingChunks: [] })); + return; + } + + assembled.add(checksum); + res.end(JSON.stringify({ state: 'not_found', missingChunks: chunks })); +} + /** * Send the appropriate mock response based on the request URL. */ -function sendResponse(req: http.IncomingMessage, res: http.ServerResponse, port: number, org: string): void { +function sendResponse( + req: http.IncomingMessage, + res: http.ServerResponse, + port: number, + org: string, + record: RequestRecord, + assembled: Set, +): void { const url = req.url || ''; if (url.includes('/artifactbundle/assemble/')) { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ state: 'created', missingChunks: [] })); + sendAssembleResponse(res, record, assembled); } else if (url.includes('/chunk-upload/')) { if (req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); @@ -190,6 +219,7 @@ export function startMockSentryServer(options: MockSentryServerOptions = {}): Mo fs.mkdirSync(outputDir); const requests: RequestRecord[] = []; + const assembled = new Set(); let chunkIndex = 0; const server = http.createServer((req, res) => { @@ -232,7 +262,7 @@ export function startMockSentryServer(options: MockSentryServerOptions = {}): Mo // Write all collected requests to the output file after each request fs.writeFileSync(outputFile, JSON.stringify(requests, null, 2)); - sendResponse(req, res, port, org); + sendResponse(req, res, port, org, record, assembled); }); }); diff --git a/dev-packages/test-utils/src/sourcemap-upload-utils.ts b/dev-packages/test-utils/src/sourcemap-upload-utils.ts index 96a621b5f44c..125cef61d236 100644 --- a/dev-packages/test-utils/src/sourcemap-upload-utils.ts +++ b/dev-packages/test-utils/src/sourcemap-upload-utils.ts @@ -56,6 +56,7 @@ export interface ParsedSourcemap { version?: number; sources?: string[]; mappings?: string; + sections?: { map?: ParsedSourcemap }[]; } export interface SourcemapEntry { @@ -92,6 +93,41 @@ export function getArtifactBundles(requests: RequestRecord[]): ArtifactBundleDat return bundles; } +/** + * Every source path a sourcemap refers to, including those inside an indexed map's sections. + * + * Bundlers that emit indexed source maps (Turbopack, for one) leave the top-level `sources` empty + * and put the real paths in `sections[].map.sources`, so reading `sources` alone reports that a + * map covers nothing. Nothing flattens these before upload: the bundler plugin rewrites only the + * top-level `sources` and uploads with `rewrite: false`, so the sections reach Sentry intact. + */ +export function getSourcemapSources(sourcemap: ParsedSourcemap): string[] { + const sources = [...(sourcemap.sources ?? [])]; + + for (const section of sourcemap.sections ?? []) { + if (section.map) { + sources.push(...getSourcemapSources(section.map)); + } + } + + return sources; +} + +/** + * Read a manifest header by name, ignoring case. + * + * These are HTTP header names, so their casing is not contractual: `@sentry/cli` wrote + * `sourcemap`, the CLI SDK writes `Sourcemap`. Matching one spelling exactly silently drops + * every pair when the other CLI produced the bundle. + */ +function getManifestHeader(headers: Record | undefined, name: string): string | undefined { + if (!headers) { + return undefined; + } + const match = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase()); + return match?.[1]; +} + /** * Extract debug ID pairs (JS file + sourcemap with matching debug-id) from artifact bundles. */ @@ -104,11 +140,13 @@ export function getDebugIdPairs(bundles: ArtifactBundleData[]): DebugIdPair[] { for (const [, entry] of fileEntries) { if (entry.type !== 'minified_source') continue; - const debugId = entry.headers?.['debug-id']; - const sourcemapRef = entry.headers?.['sourcemap']; + const debugId = getManifestHeader(entry.headers, 'debug-id'); + const sourcemapRef = getManifestHeader(entry.headers, 'sourcemap'); if (!debugId || !sourcemapRef) continue; - const mapEntry = fileEntries.find(([, e]) => e.type === 'source_map' && e.headers?.['debug-id'] === debugId); + const mapEntry = fileEntries.find( + ([, e]) => e.type === 'source_map' && getManifestHeader(e.headers, 'debug-id') === debugId, + ); if (mapEntry) { pairs.push({ diff --git a/packages/bundler-plugins/package.json b/packages/bundler-plugins/package.json index d66abc03c9f7..41b8722fd9cb 100644 --- a/packages/bundler-plugins/package.json +++ b/packages/bundler-plugins/package.json @@ -109,12 +109,12 @@ }, "dependencies": { "@babel/core": "^7.18.5", - "@sentry/cli": "^2.58.6", "@sentry/core": "10.67.0", "dotenv": "^17.4.2", "find-up": "^5.0.0", "glob": "^13.0.6", "magic-string": "~0.30.8", + "sentry": "^0.42.2", "supports-color": "^8.1.1" }, "peerDependencies": { diff --git a/packages/bundler-plugins/src/core/build-plugin-manager.ts b/packages/bundler-plugins/src/core/build-plugin-manager.ts index 1e2d2b6bad50..6b64e29b13c3 100644 --- a/packages/bundler-plugins/src/core/build-plugin-manager.ts +++ b/packages/bundler-plugins/src/core/build-plugin-manager.ts @@ -1,23 +1,17 @@ /* oxlint-disable max-lines */ -import SentryCli from '@sentry/cli'; -import { closeSession, DEFAULT_ENVIRONMENT, getTraceData, makeSession, setMeasurement, startSpan } from '@sentry/core'; +import { closeSession, DEFAULT_ENVIRONMENT, makeSession, setMeasurement, startSpan } from '@sentry/core'; import * as dotenv from 'dotenv'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { SentryCliAdapter } from './cli'; import type { NormalizedOptions } from './options-mapping'; import { normalizeUserOptions, validateOptions } from './options-mapping'; import type { Logger } from './logger'; import { createLogger } from './logger'; import { allowedToSendTelemetry, createSentryInstance, safeFlushTelemetry } from './sentry/telemetry'; import type { Options, SentrySDKBuildFlags } from './types'; -import { - arrayify, - getProjects, - getTurborepoEnvPassthroughWarning, - serializeIgnoreOptions, - stripQueryAndHashFromPath, -} from './utils'; +import { arrayify, getProjects, getTurborepoEnvPassthroughWarning, stripQueryAndHashFromPath } from './utils'; import { defaultRewriteSourcesHook, prepareBundleForDebugIdUpload } from './debug-id-upload'; import { globFiles } from './glob'; import { LIB_VERSION } from './version'; @@ -96,20 +90,8 @@ export type SentryBuildPluginManager = { createDependencyOnBuildArtifacts: () => () => void; }; -function createCliInstance(options: NormalizedOptions): SentryCli { - return new SentryCli(null, { - authToken: options.authToken, - org: options.org, - // Default to the first project if multiple projects are specified - project: getProjects(options.project)?.[0], - silent: options.silent, - url: options.url, - vcsRemote: options.release.vcsRemote, - headers: { - ...(options.telemetry ? getTraceData() : {}), - ...options.headers, - }, - }); +function createCliInstance(options: NormalizedOptions): SentryCliAdapter { + return new SentryCliAdapter(options); } /** @@ -458,40 +440,39 @@ export function createSentryBuildPluginManager( // Therefore we need to actually register the execution of this hook as dependency on the sourcemap files. const freeWriteBundleInvocationDependencyOnSourcemapFiles = createDependencyOnBuildArtifacts(); + // Guaranteed to be set by the guard clause above. + const releaseName = options.release.name; + try { const cliInstance = createCliInstance(options); if (options.release.create) { - const releaseOutput = await cliInstance.releases.new(options.release.name); + const releaseOutput = await cliInstance.createRelease(releaseName); logger.debug('Release created:', releaseOutput); } if (options.release.uploadLegacySourcemaps) { - const normalizedInclude = arrayify(options.release.uploadLegacySourcemaps) + const uploadTargets = arrayify(options.release.uploadLegacySourcemaps) .map(includeItem => (typeof includeItem === 'string' ? { paths: [includeItem] } : includeItem)) - .map(includeEntry => ({ - ...includeEntry, - validate: includeEntry.validate ?? false, - ext: includeEntry.ext - ? includeEntry.ext.map(extension => `.${extension.replace(/^\./, '')}`) - : ['.js', '.map', '.jsbundle', '.bundle'], - ignore: includeEntry.ignore ? arrayify(includeEntry.ignore) : undefined, - })); - - await cliInstance.releases.uploadSourceMaps(options.release.name, { - include: normalizedInclude, - dist: options.release.dist, - projects: getProjects(options.project), - // We want this promise to throw if the sourcemaps fail to upload so that we know about it. - // see: https://github.com/getsentry/sentry-cli/pull/2605 - live: 'rejectOnError', - }); + .flatMap(includeEntry => + includeEntry.paths.map(directory => ({ + directory, + rewrite: true, + dist: options.release.dist, + ext: includeEntry.ext + ? includeEntry.ext.map(extension => `.${extension.replace(/^\./, '')}`) + : ['.js', '.map', '.jsbundle', '.bundle'], + ignore: includeEntry.ignore ? arrayify(includeEntry.ignore) : undefined, + })), + ); + + await cliInstance.uploadSourcemaps(releaseName, uploadTargets); } if (options.release.setCommits !== false) { try { - await cliInstance.releases.setCommits( - options.release.name, + await cliInstance.setCommits( + releaseName, // set commits always exists due to the normalize function // eslint-disable-next-line @typescript-eslint/no-non-null-assertion options.release.setCommits!, @@ -514,12 +495,12 @@ export function createSentryBuildPluginManager( } if (options.release.finalize) { - await cliInstance.releases.finalize(options.release.name); + await cliInstance.finalizeRelease(releaseName); } - if (options.release.deploy && !_deployedReleases.has(options.release.name)) { - await cliInstance.releases.newDeploy(options.release.name, options.release.deploy); - _deployedReleases.add(options.release.name); + if (options.release.deploy && !_deployedReleases.has(releaseName)) { + await cliInstance.newDeploy(releaseName, options.release.deploy); + _deployedReleases.add(releaseName); } } catch (e) { sentryScope.captureException('Error in "releaseManagementPlugin" writeBundle hook'); @@ -541,10 +522,7 @@ export function createSentryBuildPluginManager( await startSpan({ name: 'inject-debug-ids', scope: sentryScope, forceTransaction: true }, async () => { try { const cliInstance = createCliInstance(options); - await cliInstance.execute( - ['sourcemaps', 'inject', ...serializeIgnoreOptions(options.sourcemaps?.ignore), ...buildArtifactPaths], - options.debug ? 'rejectOnError' : false, - ); + await cliInstance.injectDebugIds(buildArtifactPaths, options.sourcemaps?.ignore); } catch (e) { sentryScope.captureException('Error in "debugIdInjectionPlugin" writeBundle hook'); handleRecoverableError(e, false); @@ -604,25 +582,17 @@ export function createSentryBuildPluginManager( pathsToUpload = buildArtifactPaths; } - const ignorePaths = options.sourcemaps?.ignore - ? Array.isArray(options.sourcemaps?.ignore) - ? options.sourcemaps?.ignore - : [options.sourcemaps?.ignore] - : []; await startSpan({ name: 'upload', scope: sentryScope }, async () => { const cliInstance = createCliInstance(options); - await cliInstance.releases.uploadSourceMaps(options.release.name ?? 'undefined', { - include: [ - { - paths: pathsToUpload, - rewrite: true, - dist: options.release.dist, - }, - ], - ignore: ignorePaths, - projects: getProjects(options.project), - live: 'rejectOnError', - }); + await cliInstance.uploadSourcemaps( + options.release.name ?? 'undefined', + pathsToUpload.map(directory => ({ + directory, + rewrite: true, + dist: options.release.dist, + ignore: options.sourcemaps?.ignore, + })), + ); }); logger.info('Successfully uploaded source maps to Sentry'); @@ -717,17 +687,13 @@ export function createSentryBuildPluginManager( await startSpan({ name: 'upload', scope: sentryScope }, async () => { const cliInstance = createCliInstance(options); - await cliInstance.releases.uploadSourceMaps(options.release.name ?? 'undefined', { - include: [ - { - paths: [tmpUploadFolder], - rewrite: false, - dist: options.release.dist, - }, - ], - projects: getProjects(options.project), - live: 'rejectOnError', - }); + await cliInstance.uploadSourcemaps(options.release.name ?? 'undefined', [ + { + directory: tmpUploadFolder, + rewrite: false, + dist: options.release.dist, + }, + ]); }); // this must be in the method so that the "no sourcemaps" diff --git a/packages/bundler-plugins/src/core/cli.ts b/packages/bundler-plugins/src/core/cli.ts new file mode 100644 index 000000000000..49bd86785ca5 --- /dev/null +++ b/packages/bundler-plugins/src/core/cli.ts @@ -0,0 +1,172 @@ +import { createSentrySDK } from 'sentry'; +import type { NormalizedOptions } from './options-mapping'; +import type { SetCommitsOptions } from './types'; +import { arrayify, getProjects } from './utils'; + +type SentrySDK = ReturnType; + +/** Comma-joined list of ignore globs, or `undefined` when nothing should be ignored. */ +function serializeIgnore(ignore: string | string[] | undefined): string | undefined { + if (!ignore) { + return undefined; + } + const patterns = arrayify(ignore); + return patterns.length > 0 ? patterns.join(',') : undefined; +} + +/** A single sourcemap directory to upload plus the flags that apply to it. */ +interface UploadTarget { + directory: string; + rewrite: boolean; + dist?: string; + ext?: string[]; + ignore?: string | string[]; + urlPrefix?: string; +} + +/** + * Thin wrapper around the Sentry CLI's programmatic SDK. The bundler plugin used to drive the + * old `@sentry/cli` binary through a `SentryCli` class; the new CLI exposes typed methods via + * `createSentrySDK()` instead. This adapter maps the plugin's structured option shapes onto + * those methods and keeps all translation logic in one place. + */ +export class SentryCliAdapter { + readonly #options: NormalizedOptions; + /** Client for every command that is not scoped to a specific project. */ + readonly #sdk: SentrySDK; + + public constructor(options: NormalizedOptions) { + this.#options = options; + this.#sdk = this.#createClient(); + } + + /** + * The CLI binds `org`/`project`/`url` when the client is created rather than per call, so a + * client scoped to a different project needs to be a different client. + * + * Creating one is free: it reads no config and opens no connection, it only closes over these + * options. Everything happens when a command is actually invoked. + */ + #createClient(project?: string): SentrySDK { + return createSentrySDK({ + token: this.#options.authToken, + org: this.#options.org, + project, + url: this.#options.url, + }); + } + + /** Create a release. */ + public async createRelease(name: string): Promise { + return this.#sdk.release.create({ orgVersion: name }); + } + + /** Finalize a release by stamping an end timestamp. */ + public async finalizeRelease(name: string): Promise { + await this.#sdk.release.finalize({ orgVersion: name }); + } + + /** + * Associate commits with a release. Translates the plugin's {@link SetCommitsOptions} `auto` / + * `repo`+`commit` union into the flags accepted by `sentry release set-commits`. The old CLI's + * `ignoreMissing`/`ignoreEmpty` toggles have no equivalent flag on the new CLI; the caller's + * `shouldNotThrowOnFailure` handling still swallows the "no repository" failure case. + */ + public async setCommits(name: string, setCommitsOptions: SetCommitsOptions): Promise { + const { auto, repo, commit, previousCommit } = setCommitsOptions; + + // Manual mode is expressed as `REPO@SHA` (optionally `REPO@PREV..SHA`). + const commitSpec = + repo && commit ? `${repo}@${previousCommit ? `${previousCommit}..${commit}` : commit}` : undefined; + + await this.#sdk.release['set-commits']({ + orgVersion: name, + auto: auto === true, + commit: commitSpec, + }); + } + + /** Create a deploy for a release. */ + public async newDeploy(name: string, deploy: NonNullable): Promise { + if (deploy === false) { + return; + } + + // `sentry release deploy` takes three positionals - ` []` - + // but up to CLI 0.42 the generated `release.deploy()` exposed them as a single + // `orgVersionEnvironmentName` string that it forwarded as one argv token, with no separator + // that splits it back apart. Passing "1.0.0 production" fails with "Expected argument for + // environment", so `run()` is the only way to pass them separately. Once the SDK's minimum + // CLI version includes the split params, this can become `release.deploy({...})`. + const args = ['release', 'deploy', name, deploy.env]; + + if (deploy.name) { + args.push(deploy.name); + } + + if (deploy.url) { + args.push('--url', deploy.url); + } + + if (deploy.started !== undefined) { + args.push('--started', String(deploy.started)); + } + + if (deploy.finished !== undefined) { + args.push('--finished', String(deploy.finished)); + } + + if (deploy.time !== undefined) { + args.push('--time', String(deploy.time)); + } + + await this.#sdk.run(...args); + } + + /** + * Upload sourcemaps for one or more directories. The old CLI accepted a structured `include` + * array; the new `sourcemap upload` command takes a single directory, so we upload each target + * separately. A client is created per project because project selection is bound at client + * creation time. + */ + public async uploadSourcemaps(name: string, targets: UploadTarget[]): Promise { + const projects = getProjects(this.#options.project) ?? [undefined]; + + for (const project of projects) { + const sdk = this.#createClient(project); + for (const target of targets) { + await sdk.sourcemap.upload({ + directory: target.directory, + release: name, + dist: target.dist ?? this.#options.release.dist, + ext: target.ext?.join(','), + ignore: serializeIgnore(target.ignore), + urlPrefix: target.urlPrefix, + noRewrite: !target.rewrite, + }); + } + } + } + + /** Inject debug IDs into the given build artifacts. */ + public async injectDebugIds(directories: string[], ignore: string | string[] | undefined): Promise { + // Preserve the previous CLI's default of ignoring `node_modules` when nothing else is configured. + const serializedIgnore = serializeIgnore(ignore) ?? 'node_modules'; + for (const directory of directories) { + await this.#sdk.sourcemap.inject({ directory, ignore: serializedIgnore }); + } + } + + /** + * Resolve the Sentry server URL the CLI is configured to talk to. Used by the telemetry guard + * to decide whether the current build targets Sentry SaaS. Returns `undefined` on error. + */ + public async getServerUrl(): Promise { + try { + const info = (await this.#sdk.run('info')) as { config?: { url?: string } }; + return info.config?.url; + } catch { + return undefined; + } + } +} diff --git a/packages/bundler-plugins/src/core/index.ts b/packages/bundler-plugins/src/core/index.ts index a72054a86115..8387ea32b88a 100644 --- a/packages/bundler-plugins/src/core/index.ts +++ b/packages/bundler-plugins/src/core/index.ts @@ -1,6 +1,4 @@ -import SentryCli from '@sentry/cli'; import { debug } from '@sentry/core'; -import * as fs from 'fs'; import { CodeInjection, containsOnlyImports, stripQueryAndHashFromPath } from './utils'; import type { transformAsync as babelTransformAsync } from '@babel/core'; import type componentNameAnnotatePlugin from '../babel-plugin'; @@ -32,15 +30,6 @@ function loadBabelAnnotationRuntime(): Promise { return babelAnnotationRuntimePromise; } -/** - * Determines whether the Sentry CLI binary is in its expected location. - * This function is useful since `@sentry/cli` installs the binary via a post-install - * script and post-install scripts may not always run. E.g. with `npm i --ignore-scripts`. - */ -export function sentryCliBinaryExists(): boolean { - return fs.existsSync(SentryCli.getPath()); -} - // We need to be careful not to inject the snippet before any `"use strict";`s. // As an additional complication `"use strict";`s may come after any number of comments. export const COMMENT_USE_STRICT_REGEX = diff --git a/packages/bundler-plugins/src/core/sentry/telemetry.ts b/packages/bundler-plugins/src/core/sentry/telemetry.ts index 962064c383ee..60f59e7e0ff4 100644 --- a/packages/bundler-plugins/src/core/sentry/telemetry.ts +++ b/packages/bundler-plugins/src/core/sentry/telemetry.ts @@ -1,4 +1,3 @@ -import SentryCli from '@sentry/cli'; import type { Client } from '@sentry/core'; import type { ServerRuntimeClientOptions } from '@sentry/core'; import { applySdkMetadata, ServerRuntimeClient } from '@sentry/core'; @@ -7,7 +6,7 @@ import { SENTRY_SAAS_URL } from '../options-mapping'; import { Scope } from '@sentry/core'; import { createStackParser, nodeStackLineParser } from '@sentry/core'; import { makeOptionallyEnabledNodeTransport } from './transports'; -import { getProjects } from '../utils'; +import { SentryCliAdapter } from '../cli'; import { LIB_VERSION } from '../version'; const SENTRY_SAAS_HOSTNAME = 'sentry.io'; @@ -127,7 +126,7 @@ export function setTelemetryDataOnScope( } export async function allowedToSendTelemetry(options: NormalizedOptions): Promise { - const { silent, org, project, authToken, url, headers, telemetry, release } = options; + const { telemetry, url } = options; // `options.telemetry` defaults to true if (telemetry === false) { @@ -138,31 +137,9 @@ export async function allowedToSendTelemetry(options: NormalizedOptions): Promis return true; } - const cli = new SentryCli(null, { - url, - authToken, - org, - project: getProjects(project)?.[0], - vcsRemote: release.vcsRemote, - silent, - headers, - }); - - let cliInfo; - try { - // Makes a call to SentryCLI to get the Sentry server URL the CLI uses. - // We need to check and decide to use telemetry based on the CLI's response to this call - // because only at this time we checked a possibly existing .sentryclirc file. This file - // could point to another URL than the default URL. - cliInfo = await cli.execute(['info'], false); - } catch { - return false; - } - - const cliInfoUrl = cliInfo - .split(/(\r\n|\n|\r)/)[0] - ?.replace(/^Sentry Server: /, '') - ?.trim(); + // Ask the CLI which Sentry server URL it resolves to. This can differ from the default (or the + // configured `url`) because the CLI also honors a possibly existing `.sentryclirc` file. + const cliInfoUrl = await new SentryCliAdapter(options).getServerUrl(); if (cliInfoUrl === undefined) { return false; diff --git a/packages/bundler-plugins/src/core/utils.ts b/packages/bundler-plugins/src/core/utils.ts index 2436147cc610..742e0e5361d7 100644 --- a/packages/bundler-plugins/src/core/utils.ts +++ b/packages/bundler-plugins/src/core/utils.ts @@ -395,23 +395,6 @@ export function getProjects(project: string | string[] | undefined): string[] | return undefined; } -/** - * Inlined functionality from @sentry/cli helper code to add `--ignore` options. - * - * Temporary workaround until we expose a function for injecting debug IDs. Currently, we directly call `execute` with CLI args to inject them. - */ -export function serializeIgnoreOptions(ignoreValue: string | string[] | undefined): string[] { - const DEFAULT_IGNORE = ['node_modules']; - - const ignoreOptions: string[] = Array.isArray(ignoreValue) - ? ignoreValue - : typeof ignoreValue === 'string' - ? [ignoreValue] - : DEFAULT_IGNORE; - - return ignoreOptions.reduce((acc, value) => acc.concat(['--ignore', String(value)]), [] as string[]); -} - /** * Checks if a chunk contains only import/export statements and no substantial code. * diff --git a/packages/bundler-plugins/src/esbuild/index.ts b/packages/bundler-plugins/src/esbuild/index.ts index a1b1c12150ff..e4b1374729e3 100644 --- a/packages/bundler-plugins/src/esbuild/index.ts +++ b/packages/bundler-plugins/src/esbuild/index.ts @@ -299,4 +299,3 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any { // eslint-disable-next-line @typescript-eslint/no-explicit-any export default sentryEsbuildPlugin; export type { Options as SentryEsbuildPluginOptions } from '../core'; -export { sentryCliBinaryExists } from '../core'; diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts index c53ce21245bd..a1ebcb98769d 100644 --- a/packages/bundler-plugins/src/rollup/index.ts +++ b/packages/bundler-plugins/src/rollup/index.ts @@ -353,4 +353,3 @@ export function sentryRollupPlugin(userOptions: Options = {}): any { } export type { Options as SentryRollupPluginOptions } from '../core'; -export { sentryCliBinaryExists } from '../core'; diff --git a/packages/bundler-plugins/src/vite/index.ts b/packages/bundler-plugins/src/vite/index.ts index 450f7d2694fd..ea24f64e5d73 100644 --- a/packages/bundler-plugins/src/vite/index.ts +++ b/packages/bundler-plugins/src/vite/index.ts @@ -31,4 +31,3 @@ export const sentryVitePlugin = (options?: SentryRollupPluginOptions): SentryVit }; export type { Options as SentryVitePluginOptions } from '../core'; -export { sentryCliBinaryExists } from '../core'; diff --git a/packages/bundler-plugins/src/webpack/index.ts b/packages/bundler-plugins/src/webpack/index.ts index de4af49dcc8d..634f2c1e958f 100644 --- a/packages/bundler-plugins/src/webpack/index.ts +++ b/packages/bundler-plugins/src/webpack/index.ts @@ -34,6 +34,4 @@ export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any DefinePlugin, }); -export { sentryCliBinaryExists } from '../core'; - export type { SentryWebpackPluginOptions }; diff --git a/packages/bundler-plugins/src/webpack/webpack5.ts b/packages/bundler-plugins/src/webpack/webpack5.ts index b2bcaf5373b1..063aee71da02 100644 --- a/packages/bundler-plugins/src/webpack/webpack5.ts +++ b/packages/bundler-plugins/src/webpack/webpack5.ts @@ -6,6 +6,4 @@ const createSentryWebpackPlugin = sentryWebpackPluginFactory(); // eslint-disable-next-line @typescript-eslint/no-explicit-any export const sentryWebpackPlugin: (options?: SentryWebpackPluginOptions) => any = createSentryWebpackPlugin; -export { sentryCliBinaryExists } from '../core'; - export type { SentryWebpackPluginOptions }; diff --git a/packages/bundler-plugins/test/core/build-plugin-manager.test.ts b/packages/bundler-plugins/test/core/build-plugin-manager.test.ts index ea88ab067079..302ec29accb3 100644 --- a/packages/bundler-plugins/test/core/build-plugin-manager.test.ts +++ b/packages/bundler-plugins/test/core/build-plugin-manager.test.ts @@ -5,25 +5,38 @@ import { prepareBundleForDebugIdUpload } from '../../src/core/debug-id-upload'; import type { MockedFunction } from 'vitest'; import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; -const { mockCliExecute, mockCliUploadSourceMaps, mockCliNewDeploy, mockCliConstructor } = vi.hoisted(() => ({ - mockCliExecute: vi.fn(), - mockCliUploadSourceMaps: vi.fn(), - mockCliNewDeploy: vi.fn(), - mockCliConstructor: vi.fn(), +const { + mockSdkConstructor, + mockReleaseCreate, + mockReleaseFinalize, + mockReleaseSetCommits, + mockSourcemapUpload, + mockSourcemapInject, + mockRun, +} = vi.hoisted(() => ({ + mockSdkConstructor: vi.fn(), + mockReleaseCreate: vi.fn(), + mockReleaseFinalize: vi.fn(), + mockReleaseSetCommits: vi.fn(), + mockSourcemapUpload: vi.fn(), + mockSourcemapInject: vi.fn(), + mockRun: vi.fn(), })); -vi.mock('@sentry/cli', () => ({ - default: class { - constructor(...args: unknown[]) { - mockCliConstructor(...args); - } - execute = mockCliExecute; - releases = { - uploadSourceMaps: mockCliUploadSourceMaps, - new: vi.fn(), - finalize: vi.fn(), - setCommits: vi.fn(), - newDeploy: mockCliNewDeploy, +vi.mock('sentry', () => ({ + createSentrySDK: (options: unknown) => { + mockSdkConstructor(options); + return { + release: { + create: mockReleaseCreate, + finalize: mockReleaseFinalize, + 'set-commits': mockReleaseSetCommits, + }, + sourcemap: { + upload: mockSourcemapUpload, + inject: mockSourcemapInject, + }, + run: mockRun, }; }, })); @@ -126,7 +139,7 @@ describe('createSentryBuildPluginManager', () => { }); it('should have SENTRY_LOG_LEVEL set when CLI operations are performed with debug enabled', async () => { - mockCliExecute.mockImplementation(() => { + mockSourcemapInject.mockImplementation(() => { // Verify the environment variable is set at the time the CLI is called expect(process.env['SENTRY_LOG_LEVEL']).toBe('debug'); return Promise.resolve(undefined); @@ -151,12 +164,12 @@ describe('createSentryBuildPluginManager', () => { // Perform a CLI operation and verify the env var is still set await buildPluginManager.injectDebugIds(['/path/to/bundle']); - expect(mockCliExecute).toHaveBeenCalled(); + expect(mockSourcemapInject).toHaveBeenCalled(); }); it('should have SENTRY_LOG_LEVEL set during error scenarios with debug enabled', async () => { // Simulate CLI error - mockCliExecute.mockImplementation(() => { + mockSourcemapInject.mockImplementation(() => { // Verify the environment variable is set even when CLI encounters an error // This ensures the CLI won't emit the "Add --log-level=debug" warning expect(process.env['SENTRY_LOG_LEVEL']).toBe('debug'); @@ -188,7 +201,7 @@ describe('createSentryBuildPluginManager', () => { it('should NOT have SENTRY_LOG_LEVEL set during error scenarios when debug is disabled', async () => { // Simulate CLI error - mockCliExecute.mockImplementation(() => { + mockSourcemapInject.mockImplementation(() => { // Verify the environment variable is NOT set // In this case, the CLI WOULD emit the "Add --log-level=debug" warning expect(process.env['SENTRY_LOG_LEVEL']).toBeUndefined(); @@ -269,7 +282,7 @@ describe('createSentryBuildPluginManager', () => { describe('uploadSourcemaps', () => { it('uploads in-place when prepareArtifacts is false', async () => { - mockCliUploadSourceMaps.mockResolvedValue(undefined); + mockSourcemapUpload.mockResolvedValue(undefined); const manager = createSentryBuildPluginManager( { @@ -284,22 +297,15 @@ describe('createSentryBuildPluginManager', () => { await manager.uploadSourcemaps(['/unused'], { prepareArtifacts: false }); - expect(mockCliUploadSourceMaps).toHaveBeenCalledTimes(1); - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - 'some-release-name', + expect(mockSourcemapUpload).toHaveBeenCalledTimes(1); + expect(mockSourcemapUpload).toHaveBeenCalledWith( // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment expect.objectContaining({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - include: expect.arrayContaining([ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ - // User-provided assets should be passed directly to CLI (no globbing) - paths: ['/app/dist/**/*'], - rewrite: true, - dist: '1', - }), - ]), - live: 'rejectOnError', + // User-provided assets should be passed directly to CLI (no globbing) + directory: '/app/dist/**/*', + release: 'some-release-name', + dist: '1', + noRewrite: false, }), ); // Should not glob when prepareArtifacts is false @@ -308,7 +314,7 @@ describe('createSentryBuildPluginManager', () => { }); it('uploads build artifact paths when prepareArtifacts is false and no assets provided', async () => { - mockCliUploadSourceMaps.mockResolvedValue(undefined); + mockSourcemapUpload.mockResolvedValue(undefined); const manager = createSentryBuildPluginManager( { @@ -323,23 +329,15 @@ describe('createSentryBuildPluginManager', () => { await manager.uploadSourcemaps(['.next', 'dist'], { prepareArtifacts: false }); - expect(mockCliUploadSourceMaps).toHaveBeenCalledTimes(1); - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - 'some-release-name', + // One upload per build artifact directory + expect(mockSourcemapUpload).toHaveBeenCalledTimes(2); + expect(mockSourcemapUpload).toHaveBeenCalledWith( // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - include: expect.arrayContaining([ - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expect.objectContaining({ - // Should use buildArtifactPaths directly - paths: ['.next', 'dist'], - rewrite: true, - dist: '1', - }), - ]), - live: 'rejectOnError', - }), + expect.objectContaining({ directory: '.next', release: 'some-release-name', dist: '1', noRewrite: false }), + ); + expect(mockSourcemapUpload).toHaveBeenCalledWith( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + expect.objectContaining({ directory: 'dist', release: 'some-release-name', dist: '1', noRewrite: false }), ); expect(mockGlobFiles).not.toHaveBeenCalled(); expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled(); @@ -359,7 +357,7 @@ describe('createSentryBuildPluginManager', () => { await manager.uploadSourcemaps(['.next'], { prepareArtifacts: false }); - expect(mockCliUploadSourceMaps).not.toHaveBeenCalled(); + expect(mockSourcemapUpload).not.toHaveBeenCalled(); expect(mockGlobFiles).not.toHaveBeenCalled(); expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled(); }); @@ -378,13 +376,13 @@ describe('createSentryBuildPluginManager', () => { await manager.uploadSourcemaps(['.next']); - expect(mockCliUploadSourceMaps).not.toHaveBeenCalled(); + expect(mockSourcemapUpload).not.toHaveBeenCalled(); expect(mockGlobFiles).not.toHaveBeenCalled(); expect(mockPrepareBundleForDebugIdUpload).not.toHaveBeenCalled(); }); it('prepares into temp folder and uploads when prepareArtifacts is true (default)', async () => { - mockCliUploadSourceMaps.mockResolvedValue(undefined); + mockSourcemapUpload.mockResolvedValue(undefined); mockGlobFiles.mockResolvedValue(['/app/dist/a.js', '/app/dist/a.js.map', '/app/dist/other.txt']); @@ -410,18 +408,22 @@ describe('createSentryBuildPluginManager', () => { // Should call prepare for each JS chunk discovered by glob expect(mockPrepareBundleForDebugIdUpload).toHaveBeenCalled(); - // Should upload from temp folder - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith('some-release-name', { - include: [{ paths: ['/tmp/sentry-upload-xyz'], rewrite: false, dist: '1' }], - projects: ['p'], - live: 'rejectOnError', + // Should upload from temp folder without rewriting (debug IDs already injected) + expect(mockSourcemapUpload).toHaveBeenCalledWith({ + directory: '/tmp/sentry-upload-xyz', + release: 'some-release-name', + dist: '1', + ext: undefined, + ignore: undefined, + urlPrefix: undefined, + noRewrite: true, }); }); // Skipping mapless chunks (so the CLI stops warning per stub chunk) must // not swallow the signal that source map generation is disabled. it('warns and skips upload when none of the matched bundles have a source map', async () => { - mockCliUploadSourceMaps.mockResolvedValue(undefined); + mockSourcemapUpload.mockResolvedValue(undefined); // Bundles were found, but preparation produced no artifacts mockGlobFiles.mockResolvedValue(['/app/dist/a.js', '/app/dist/b.js']); @@ -451,15 +453,15 @@ describe('createSentryBuildPluginManager', () => { expect(warnSpy).toHaveBeenCalledTimes(1); expect(warnSpy.mock.calls[0]?.[0]).toMatch(/source map generation is not enabled/); // Nothing to upload, so the CLI upload must be skipped entirely. - expect(mockCliUploadSourceMaps).not.toHaveBeenCalled(); + expect(mockSourcemapUpload).not.toHaveBeenCalled(); vi.restoreAllMocks(); }); }); describe('injectDebugIds', () => { - it('should call CLI with correct sourcemaps inject command', async () => { - mockCliExecute.mockResolvedValue(undefined); + it('should call the CLI inject command for each build artifact path', async () => { + mockSourcemapInject.mockResolvedValue(undefined); const buildPluginManager = createSentryBuildPluginManager( { @@ -476,21 +478,20 @@ describe('createSentryBuildPluginManager', () => { const buildArtifactPaths = ['/path/to/1', '/path/to/2']; await buildPluginManager.injectDebugIds(buildArtifactPaths); - expect(mockCliExecute).toHaveBeenCalledWith( - ['sourcemaps', 'inject', '--ignore', 'node_modules', '/path/to/1', '/path/to/2'], - false, - ); + expect(mockSourcemapInject).toHaveBeenCalledTimes(2); + expect(mockSourcemapInject).toHaveBeenCalledWith({ directory: '/path/to/1', ignore: 'node_modules' }); + expect(mockSourcemapInject).toHaveBeenCalledWith({ directory: '/path/to/2', ignore: 'node_modules' }); }); - it('should pass "rejectOnError" flag when options.debug is true', async () => { - mockCliExecute.mockResolvedValue(undefined); + it('should forward configured ignore globs to the inject command', async () => { + mockSourcemapInject.mockResolvedValue(undefined); const buildPluginManager = createSentryBuildPluginManager( { authToken: 'test-token', org: 'test-org', project: 'test-project', - debug: true, + sourcemaps: { ignore: ['foo', 'bar'] }, }, { buildTool: 'webpack', @@ -498,13 +499,9 @@ describe('createSentryBuildPluginManager', () => { }, ); - const buildArtifactPaths = ['/path/to/bundle']; - await buildPluginManager.injectDebugIds(buildArtifactPaths); + await buildPluginManager.injectDebugIds(['/path/to/bundle']); - expect(mockCliExecute).toHaveBeenCalledWith( - ['sourcemaps', 'inject', '--ignore', 'node_modules', '/path/to/bundle'], - 'rejectOnError', - ); + expect(mockSourcemapInject).toHaveBeenCalledWith({ directory: '/path/to/bundle', ignore: 'foo,bar' }); }); }); @@ -513,7 +510,7 @@ describe('createSentryBuildPluginManager', () => { vi.clearAllMocks(); mockGlobFiles.mockResolvedValue(['/path/to/bundle.js']); mockPrepareBundleForDebugIdUpload.mockResolvedValue(undefined); - mockCliUploadSourceMaps.mockResolvedValue(undefined); + mockSourcemapUpload.mockResolvedValue(undefined); // Mock fs operations needed for temp folder upload path. `readdir` returns a prepared artifact // so the upload path runs (an empty temp folder warns and skips the upload instead). @@ -527,7 +524,7 @@ describe('createSentryBuildPluginManager', () => { vi.restoreAllMocks(); }); - it('should pass projects array to uploadSourceMaps when multiple projects configured', async () => { + it('should create a CLI client per project when multiple projects configured', async () => { const buildPluginManager = createSentryBuildPluginManager( { authToken: 'test-token', @@ -543,15 +540,13 @@ describe('createSentryBuildPluginManager', () => { await buildPluginManager.uploadSourcemaps(['/path/to/bundle.js']); - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - 'test-release', - expect.objectContaining({ - projects: ['proj-a', 'proj-b', 'proj-c'], - }), - ); + const uploadProjects = mockSdkConstructor.mock.calls.map(call => (call[0] as { project?: string }).project); + expect(uploadProjects).toContain('proj-a'); + expect(uploadProjects).toContain('proj-b'); + expect(uploadProjects).toContain('proj-c'); }); - it('should pass single project as array to uploadSourceMaps', async () => { + it('should create a CLI client for a single project', async () => { const buildPluginManager = createSentryBuildPluginManager( { authToken: 'test-token', @@ -567,15 +562,10 @@ describe('createSentryBuildPluginManager', () => { await buildPluginManager.uploadSourcemaps(['/path/to/bundle.js']); - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - 'test-release', - expect.objectContaining({ - projects: ['single-project'], - }), - ); + expect(mockSdkConstructor).toHaveBeenCalledWith(expect.objectContaining({ project: 'single-project' })); }); - it('should pass projects array in direct upload mode', async () => { + it('should create a CLI client per project in direct upload mode', async () => { const buildPluginManager = createSentryBuildPluginManager( { authToken: 'test-token', @@ -593,12 +583,9 @@ describe('createSentryBuildPluginManager', () => { prepareArtifacts: false, }); - expect(mockCliUploadSourceMaps).toHaveBeenCalledWith( - 'test-release', - expect.objectContaining({ - projects: ['proj-a', 'proj-b'], - }), - ); + const uploadProjects = mockSdkConstructor.mock.calls.map(call => (call[0] as { project?: string }).project); + expect(uploadProjects).toContain('proj-a'); + expect(uploadProjects).toContain('proj-b'); }); }); @@ -679,8 +666,8 @@ describe('createSentryBuildPluginManager', () => { }); describe('telemetry option', () => { - it('should not pass sentry-trace or baggage headers to CLI when telemetry is false', async () => { - mockCliExecute.mockResolvedValue(undefined); + it('should not pass sentry-trace or baggage headers to the CLI client', async () => { + mockSourcemapInject.mockResolvedValue(undefined); const buildPluginManager = createSentryBuildPluginManager( { @@ -695,20 +682,16 @@ describe('createSentryBuildPluginManager', () => { }, ); - // Trigger a CLI operation so createCliInstance is called + // Trigger a CLI operation so a client is created await buildPluginManager.injectDebugIds(['/path/to/bundle']); - // Find the CLI constructor call that was made by createCliInstance (not the one from allowedToSendTelemetry) - const cliConstructorCalls = mockCliConstructor.mock.calls; - expect(cliConstructorCalls.length).toBeGreaterThan(0); - - // Check that none of the CLI instances were created with sentry-trace or baggage headers - for (const call of cliConstructorCalls) { - const options = call[1] as { headers?: Record }; - if (options?.headers) { - expect(options.headers).not.toHaveProperty('sentry-trace'); - expect(options.headers).not.toHaveProperty('baggage'); - } + const clientOptionsCalls = mockSdkConstructor.mock.calls; + expect(clientOptionsCalls.length).toBeGreaterThan(0); + + // The new CLI injects trace headers itself; the plugin must never forward them explicitly. + for (const call of clientOptionsCalls) { + const options = call[0] as { headers?: Record }; + expect(options).not.toHaveProperty('headers'); } }); }); @@ -735,8 +718,8 @@ describe('createSentryBuildPluginManager', () => { await manager.createRelease(); - expect(mockCliNewDeploy).toHaveBeenCalledTimes(1); - expect(mockCliNewDeploy).toHaveBeenCalledWith('test-release', { env: 'production' }); + expect(mockRun).toHaveBeenCalledTimes(1); + expect(mockRun).toHaveBeenCalledWith('release', 'deploy', 'test-release', 'production'); }); it('should not create duplicate deploy records when createRelease is called multiple times on the same instance', async () => { @@ -757,7 +740,7 @@ describe('createSentryBuildPluginManager', () => { await manager.createRelease(); await manager.createRelease(); - expect(mockCliNewDeploy).toHaveBeenCalledTimes(1); + expect(mockRun).toHaveBeenCalledTimes(1); }); it('should not create duplicate deploy records across separate plugin instances with the same release name', async () => { @@ -790,7 +773,7 @@ describe('createSentryBuildPluginManager', () => { await managerA.createRelease(); await managerB.createRelease(); - expect(mockCliNewDeploy).toHaveBeenCalledTimes(1); + expect(mockRun).toHaveBeenCalledTimes(1); }); it('should allow deploys for different release names', async () => { @@ -823,9 +806,9 @@ describe('createSentryBuildPluginManager', () => { await managerA.createRelease(); await managerB.createRelease(); - expect(mockCliNewDeploy).toHaveBeenCalledTimes(2); - expect(mockCliNewDeploy).toHaveBeenCalledWith('release-1', { env: 'production' }); - expect(mockCliNewDeploy).toHaveBeenCalledWith('release-2', { env: 'production' }); + expect(mockRun).toHaveBeenCalledTimes(2); + expect(mockRun).toHaveBeenCalledWith('release', 'deploy', 'release-1', 'production'); + expect(mockRun).toHaveBeenCalledWith('release', 'deploy', 'release-2', 'production'); }); it('should not create a deploy when deploy option is not set', async () => { @@ -841,7 +824,7 @@ describe('createSentryBuildPluginManager', () => { await manager.createRelease(); - expect(mockCliNewDeploy).not.toHaveBeenCalled(); + expect(mockRun).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/bundler-plugins/test/core/cli-contract.test.ts b/packages/bundler-plugins/test/core/cli-contract.test.ts new file mode 100644 index 000000000000..e8fbf4e1714f --- /dev/null +++ b/packages/bundler-plugins/test/core/cli-contract.test.ts @@ -0,0 +1,178 @@ +/** + * Contract tests for `SentryCliAdapter` against the *real* Sentry CLI. + * + * Every other test in this package mocks the `sentry` module, so they assert which calls the + * plugin makes but never that the CLI accepts them. That gap is not theoretical: the deploy name + * used to be passed as `--name`, a flag the new CLI does not have, and the mocked tests were + * green the whole time because the mock records whatever it is given. + * + * These tests therefore import the real CLI and point it at a mock Sentry HTTP server, so an + * argument the CLI rejects fails here. They are deliberately few - the argv shapes, not the + * plugin's option mapping, which the mocked tests already cover. + * + * They assert on the requests that reach the server rather than on the calls resolving. `run()` + * resolves with `undefined` when the CLI rejects its arguments instead of throwing, so the + * `--name` regression above produced no deploy, no error, and a successful build. Only the + * absence of the request reveals it. + */ +import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { startMockSentryServer } from '@sentry-internal/test-utils'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { SentryCliAdapter } from '../../src/core/cli'; +import type { NormalizedOptions } from '../../src/core/options-mapping'; + +const PORT = 3037; + +let workDir: string; +let buildDir: string; +let server: ReturnType; +let previousCwd: string; + +/** Paths of every request the mock server has recorded so far, in order. */ +function recordedRequests(): string[] { + const file = join(workDir, '.tmp_mock_uploads.json'); + if (!existsSync(file)) { + return []; + } + const records = JSON.parse(readFileSync(file, 'utf-8')) as { method: string; url: string }[]; + return records.map(record => `${record.method} ${record.url}`); +} + +/** Requests recorded since a marker returned by a previous `recordedRequests()` call. */ +function requestsSince(before: string[]): string[] { + return recordedRequests().slice(before.length); +} + +function createAdapter(overrides: Partial = {}): SentryCliAdapter { + return new SentryCliAdapter({ + authToken: 'fake-auth-token', + org: 'test-org', + project: 'test-project', + url: `http://localhost:${PORT}`, + release: {}, + ...overrides, + } as NormalizedOptions); +} + +beforeAll(() => { + workDir = mkdtempSync(join(tmpdir(), 'sentry-cli-contract-')); + buildDir = join(workDir, 'build'); + mkdirSync(buildDir); + + writeFileSync(join(buildDir, 'app.js'), 'console.log("hello");\n//# sourceMappingURL=app.js.map\n'); + writeFileSync( + join(buildDir, 'app.js.map'), + JSON.stringify({ + version: 3, + file: 'app.js', + sources: ['../src/app.ts'], + sourcesContent: ['console.log("hello");'], + names: [], + mappings: 'AAAA', + }), + ); + + // The CLI keeps config and a SQLite cache in `~/.sentry` unless told otherwise, so give it a + // throwaway directory. Without this the suite reads and writes the developer's global CLI + // state, which also makes it fail on a `cli.db` another CLI left in WAL mode. + process.env['SENTRY_CONFIG_DIR'] = join(workDir, 'sentry-home'); + mkdirSync(process.env['SENTRY_CONFIG_DIR']); + + // The mock server writes its recording relative to cwd. + previousCwd = process.cwd(); + process.chdir(workDir); + + server = startMockSentryServer({ port: PORT }); +}); + +afterAll(() => { + server.close(); + process.chdir(previousCwd); + delete process.env['SENTRY_CONFIG_DIR']; + rmSync(workDir, { recursive: true, force: true }); +}); + +describe('SentryCliAdapter against the real CLI', () => { + it('creates a release', async () => { + const before = recordedRequests(); + + await createAdapter().createRelease('1.0.0'); + + expect(requestsSince(before)).toContain('POST /api/0/organizations/test-org/releases/'); + }); + + it('finalizes a release', async () => { + const before = recordedRequests(); + + await createAdapter().finalizeRelease('1.0.0'); + + expect(requestsSince(before)).toContain('PUT /api/0/organizations/test-org/releases/1.0.0/'); + }); + + it('sets commits from an explicit repo and sha', async () => { + const before = recordedRequests(); + + await createAdapter().setCommits('1.0.0', { repo: 'getsentry/sentry-javascript', commit: 'abc123' }); + + expect(requestsSince(before)).toContain('PUT /api/0/organizations/test-org/releases/1.0.0/'); + }); + + it('creates a deploy with an environment and a name', async () => { + const before = recordedRequests(); + + // The name is the third positional. As `--name` the CLI rejects the argv, `run()` resolves + // with undefined anyway, and no deploy is created - so assert the request, not the promise. + await createAdapter().newDeploy('1.0.0', { + env: 'production', + name: 'Deploy #42', + url: 'https://example.com', + }); + + expect(requestsSince(before)).toContain('POST /api/0/organizations/test-org/releases/1.0.0/deploys/'); + }); + + it('creates a deploy with duration-based timing', async () => { + const before = recordedRequests(); + + // `--time` cannot be combined with `--started`/`--finished`, so this covers the other branch. + await createAdapter().newDeploy('1.0.0', { env: 'production', time: 900 }); + + expect(requestsSince(before)).toContain('POST /api/0/organizations/test-org/releases/1.0.0/deploys/'); + }); + + it('injects debug IDs into a build directory', async () => { + await createAdapter().injectDebugIds([buildDir], undefined); + + // Injection is purely local, so the debug ID snippet in the bundle is the observable effect. + expect(readFileSync(join(buildDir, 'app.js'), 'utf-8')).toContain('_sentryDebugIds'); + }); + + it('uploads source maps with the full option set', async () => { + const before = recordedRequests(); + + await createAdapter().uploadSourcemaps('1.0.0', [ + { + directory: buildDir, + rewrite: false, + dist: 'dist-1', + ext: ['js', 'map'], + ignore: ['node_modules'], + urlPrefix: '~/', + }, + ]); + + expect(requestsSince(before)).toContain('POST /api/0/organizations/test-org/artifactbundle/assemble/'); + }); + + it('resolves the server url, or gives up quietly', async () => { + const adapter = createAdapter(); + + // `info` exits non-zero when the token does not authenticate, and the adapter swallows that + // and returns undefined - which turns telemetry off rather than on. Both outcomes are fine; + // what must not happen is the call throwing. + const url = await adapter.getServerUrl(); + expect(url === undefined || url === `http://localhost:${PORT}`).toBe(true); + }); +}); diff --git a/packages/bundler-plugins/test/core/sentry/telemetry.test.ts b/packages/bundler-plugins/test/core/sentry/telemetry.test.ts index 887a16fdf60e..49cf78b7dffa 100644 --- a/packages/bundler-plugins/test/core/sentry/telemetry.test.ts +++ b/packages/bundler-plugins/test/core/sentry/telemetry.test.ts @@ -4,14 +4,14 @@ import { normalizeUserOptions } from '../../../src/core/options-mapping'; import { allowedToSendTelemetry, setTelemetryDataOnScope } from '../../../src/core/sentry/telemetry'; import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; -const { mockCliExecute } = vi.hoisted(() => ({ - mockCliExecute: vi.fn(), +const { mockRun } = vi.hoisted(() => ({ + mockRun: vi.fn(), })); -vi.mock('@sentry/cli', () => ({ - default: class { - execute = mockCliExecute; - }, +vi.mock('sentry', () => ({ + createSentrySDK: () => ({ + run: mockRun, + }), })); describe('shouldSendTelemetry', () => { @@ -20,12 +20,12 @@ describe('shouldSendTelemetry', () => { }); it('should return false if CLI returns a URL other than sentry.io', async () => { - mockCliExecute.mockImplementation(() => 'Sentry Server: https://selfhostedSentry.io \nsomeotherstuff\netc'); + mockRun.mockResolvedValue({ config: { url: 'https://selfhostedSentry.io' } }); expect(await allowedToSendTelemetry({ release: {} } as NormalizedOptions)).toBe(false); }); it('should return true if CLI returns sentry.io as a URL', async () => { - mockCliExecute.mockImplementation(() => 'Sentry Server: https://sentry.io \nsomeotherstuff\netc'); + mockRun.mockResolvedValue({ config: { url: 'https://sentry.io' } }); expect(await allowedToSendTelemetry({ release: {} } as NormalizedOptions)).toBe(true); }); }); diff --git a/packages/bundler-plugins/test/core/utils.test.ts b/packages/bundler-plugins/test/core/utils.test.ts index 6aeaec85d689..86171c15a827 100644 --- a/packages/bundler-plugins/test/core/utils.test.ts +++ b/packages/bundler-plugins/test/core/utils.test.ts @@ -6,7 +6,6 @@ import { getPackageJson, parseMajorVersion, replaceBooleanFlagsInCode, - serializeIgnoreOptions, stringToUUID, } from '../../src/core/utils'; @@ -270,28 +269,6 @@ describe('generateModuleMetadataInjectorCode', () => { }); }); -describe('serializeIgnoreOptions', () => { - it('returns default ignore options when undefined', () => { - const result = serializeIgnoreOptions(undefined); - expect(result).toEqual(['--ignore', 'node_modules']); - }); - - it('handles array of ignore patterns', () => { - const result = serializeIgnoreOptions(['dist', '**/build/**', '*.log']); - expect(result).toEqual(['--ignore', 'dist', '--ignore', '**/build/**', '--ignore', '*.log']); - }); - - it('handles single string pattern', () => { - const result = serializeIgnoreOptions('dist'); - expect(result).toEqual(['--ignore', 'dist']); - }); - - it('handles empty array', () => { - const result = serializeIgnoreOptions([]); - expect(result).toEqual([]); - }); -}); - describe('determineReleaseName', () => { it('runs `git rev-parse HEAD` with windowsHide so no console window flashes on Windows', () => { // Clear env so the function falls through the CI/git-provider checks to the diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 4ef8f3e90e42..7103eabeaecd 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -47,14 +47,14 @@ "dependencies": { "@opentelemetry/api": "^1.9.1", "@sentry/browser": "10.67.0", - "@sentry/cli": "^2.58.6", "@sentry/conventions": "^0.19.0", "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", "@sentry/react": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/bundler-plugins": "10.67.0", - "glob": "^13.0.6" + "glob": "^13.0.6", + "sentry": "^0.42.2" }, "devDependencies": { "@react-router/dev": "^7.17.0", diff --git a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts index 12de869e7f8c..59a8613a9950 100644 --- a/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts +++ b/packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts @@ -1,6 +1,6 @@ import { rm } from 'node:fs/promises'; import type { Config } from '@react-router/dev/config'; -import SentryCli from '@sentry/cli'; +import { createSentrySDK } from 'sentry'; import type { SentryVitePluginOptions } from '@sentry/bundler-plugins/vite'; import { glob } from 'glob'; import type { SentryReactRouterBuildOptions } from '../types'; @@ -26,7 +26,6 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo const { authToken, - headers, org, project, release, @@ -46,20 +45,17 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo }, }; - // `url` and `headers` previously only reached the CLI through `unstable_sentryVitePluginOptions`, - // so self-hosted setups had no supported way to point this upload at their instance. - const cliInstance = new SentryCli(null, { - authToken, - headers, + const sentry = createSentrySDK({ + token: authToken, org, - project, url: sentryUrl, + project, }); // check if release should be created if (release?.name) { try { - await cliInstance.releases.new(release.name); + await sentry.release.create({ orgVersion: release.name }); } catch (error) { // eslint-disable-next-line no-console console.error('[Sentry] Could not create release', error); @@ -74,10 +70,7 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo if (!sourceMapsFullyDisabled && viteConfig.build.sourcemap !== false) { // inject debugIds try { - await cliInstance.execute( - ['sourcemaps', 'inject', reactRouterConfig.buildDirectory], - debug ? 'rejectOnError' : false, - ); + await sentry.sourcemap.inject({ directory: reactRouterConfig.buildDirectory }); } catch (error) { // eslint-disable-next-line no-console console.error('[Sentry] Could not inject debug ids', error); @@ -86,13 +79,9 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo if (!uploadDisabled) { // upload sourcemaps try { - await cliInstance.releases.uploadSourceMaps(release?.name || 'undefined', { - include: [ - { - paths: [reactRouterConfig.buildDirectory], - }, - ], - live: 'rejectOnError', + await sentry.sourcemap.upload({ + directory: reactRouterConfig.buildDirectory, + release: release?.name || 'undefined', }); } catch (error) { // eslint-disable-next-line no-console diff --git a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts index 45a79c6a694b..ad28a939d0aa 100644 --- a/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts +++ b/packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts @@ -1,4 +1,4 @@ -import SentryCli from '@sentry/cli'; +import { createSentrySDK } from 'sentry'; import * as fs from 'fs'; import { glob } from 'glob'; import type { ResolvedConfig } from 'vite'; @@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { sentryOnBuildEnd } from '../../../src/vite/buildEnd/handleOnBuildEnd'; import type { SentryReactRouterBuildOptions } from '../../../src/vite/types'; -vi.mock('@sentry/cli'); +vi.mock('sentry'); vi.mock('fs', () => ({ promises: { rm: vi.fn().mockResolvedValue(undefined), @@ -19,12 +19,14 @@ type TestConfig = ResolvedConfig & { }; describe('sentryOnBuildEnd', () => { - const mockSentryCliInstance = { - releases: { - new: vi.fn(), - uploadSourceMaps: vi.fn(), + const mockSentrySdkInstance = { + release: { + create: vi.fn(), + }, + sourcemap: { + upload: vi.fn(), + inject: vi.fn(), }, - execute: vi.fn(), }; const defaultConfig = { @@ -57,8 +59,7 @@ describe('sentryOnBuildEnd', () => { beforeEach(() => { vi.clearAllMocks(); - // @ts-expect-error - mocking constructor - SentryCli.mockImplementation(() => mockSentryCliInstance); + vi.mocked(createSentrySDK).mockReturnValue(mockSentrySdkInstance as unknown as ReturnType); vi.mocked(glob).mockResolvedValue(['/build/file1.map', '/build/file2.map']); vi.mocked(fs.promises.rm).mockResolvedValue(undefined); }); @@ -84,7 +85,7 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(config); - expect(mockSentryCliInstance.releases.new).toHaveBeenCalledWith('v1.0.0'); + expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ orgVersion: 'v1.0.0' }); }); it('resolves root-level BuildTimeOptionsBase options for release creation and source map upload', async () => { @@ -107,15 +108,16 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(config); - expect(SentryCli).toHaveBeenCalledWith(null, { - authToken: 'my-token', + expect(createSentrySDK).toHaveBeenCalledWith({ + token: 'my-token', org: 'my-org', project: 'my-project', + url: undefined, }); - expect(mockSentryCliInstance.releases.new).toHaveBeenCalledWith('1.2.3'); - expect(mockSentryCliInstance.releases.uploadSourceMaps).toHaveBeenCalledWith('1.2.3', { - include: [{ paths: ['/build'] }], - live: 'rejectOnError', + expect(mockSentrySdkInstance.release.create).toHaveBeenCalledWith({ orgVersion: '1.2.3' }); + expect(mockSentrySdkInstance.sourcemap.upload).toHaveBeenCalledWith({ + directory: '/build', + release: '1.2.3', }); expect(glob).toHaveBeenCalledWith(['./build/custom/**/*.map'], { absolute: true, @@ -127,10 +129,10 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(defaultConfig); - expect(mockSentryCliInstance.releases.uploadSourceMaps).toHaveBeenCalledTimes(1); - expect(mockSentryCliInstance.releases.uploadSourceMaps).toHaveBeenCalledWith('undefined', { - include: [{ paths: ['/build'] }], - live: 'rejectOnError', + expect(mockSentrySdkInstance.sourcemap.upload).toHaveBeenCalledTimes(1); + expect(mockSentrySdkInstance.sourcemap.upload).toHaveBeenCalledWith({ + directory: '/build', + release: 'undefined', }); }); @@ -149,12 +151,10 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(config); - expect(mockSentryCliInstance.execute).not.toHaveBeenCalled(); - expect(mockSentryCliInstance.releases.uploadSourceMaps).not.toHaveBeenCalled(); + expect(mockSentrySdkInstance.sourcemap.inject).not.toHaveBeenCalled(); + expect(mockSentrySdkInstance.sourcemap.upload).not.toHaveBeenCalled(); }); - // `disable` used to be read from the top-level config only, so this opt-out was - // silently ignored while the Vite plugin honoured it - see #22929. // `'disable-upload'` means "inject debug IDs, but let me upload the maps myself", so // injection must still run and the maps must survive. it('should inject debug IDs but skip upload and deletion when disable is "disable-upload"', async () => { @@ -172,8 +172,8 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(config); - expect(mockSentryCliInstance.execute).toHaveBeenCalledWith(['sourcemaps', 'inject', '/build'], false); - expect(mockSentryCliInstance.releases.uploadSourceMaps).not.toHaveBeenCalled(); + expect(mockSentrySdkInstance.sourcemap.inject).toHaveBeenCalledWith({ directory: '/build' }); + expect(mockSentrySdkInstance.sourcemap.upload).not.toHaveBeenCalled(); expect(glob).not.toHaveBeenCalled(); }); @@ -232,7 +232,7 @@ describe('sentryOnBuildEnd', () => { it('should handle errors during release creation gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - mockSentryCliInstance.releases.new.mockRejectedValueOnce(new Error('Release creation failed')); + mockSentrySdkInstance.release.create.mockRejectedValueOnce(new Error('Release creation failed')); const config = { ...defaultConfig, @@ -258,17 +258,17 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(defaultConfig); - expect(mockSentryCliInstance.execute).toHaveBeenCalledWith(['sourcemaps', 'inject', '/build'], false); + expect(mockSentrySdkInstance.sourcemap.inject).toHaveBeenCalledWith({ directory: '/build' }); }); it('should handle errors during debug ID injection gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - mockSentryCliInstance.execute.mockRejectedValueOnce(new Error('Injection failed')); + mockSentrySdkInstance.sourcemap.inject.mockRejectedValueOnce(new Error('Injection failed')); // @ts-expect-error - mocking the React config await sentryOnBuildEnd(defaultConfig); - expect(mockSentryCliInstance.execute).toHaveBeenCalledTimes(1); - expect(mockSentryCliInstance.execute).toHaveBeenCalledWith(['sourcemaps', 'inject', '/build'], false); + expect(mockSentrySdkInstance.sourcemap.inject).toHaveBeenCalledTimes(1); + expect(mockSentrySdkInstance.sourcemap.inject).toHaveBeenCalledWith({ directory: '/build' }); expect(consoleSpy).toHaveBeenCalledWith('[Sentry] Could not inject debug ids', expect.any(Error)); consoleSpy.mockRestore(); @@ -276,7 +276,7 @@ describe('sentryOnBuildEnd', () => { it('should handle errors during source map upload gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - mockSentryCliInstance.releases.uploadSourceMaps.mockRejectedValueOnce(new Error('Upload failed')); + mockSentrySdkInstance.sourcemap.upload.mockRejectedValueOnce(new Error('Upload failed')); // @ts-expect-error - mocking the React config await sentryOnBuildEnd(defaultConfig); @@ -304,8 +304,7 @@ describe('sentryOnBuildEnd', () => { expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[Sentry] Automatically setting')); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Deleting asset after upload:')); - // rejectOnError is used in debug mode to pipe debug id injection output from the CLI to this process's stdout - expect(mockSentryCliInstance.execute).toHaveBeenCalledWith(['sourcemaps', 'inject', '/build'], 'rejectOnError'); + expect(mockSentrySdkInstance.sourcemap.inject).toHaveBeenCalledWith({ directory: '/build' }); consoleSpy.mockRestore(); }); @@ -330,8 +329,7 @@ describe('sentryOnBuildEnd', () => { // @ts-expect-error - mocking the React config await sentryOnBuildEnd(config); - expect(SentryCli).toHaveBeenCalledWith( - null, + expect(createSentrySDK).toHaveBeenCalledWith( expect.objectContaining({ url: 'https://custom-instance.ejemplo.es', headers: { 'X-Custom-Header': 'test-value' }, diff --git a/packages/remix/package.json b/packages/remix/package.json index f62489630526..4bcbab8abcc4 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -53,12 +53,12 @@ }, "dependencies": { "@remix-run/router": "^1.23.3", - "@sentry/cli": "^2.58.6", "@sentry/conventions": "^0.19.0", "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", "@sentry/react": "10.67.0", "@sentry/server-utils": "10.67.0", + "sentry": "^0.42.2", "yargs": "^17.6.0" }, "devDependencies": { diff --git a/packages/remix/scripts/createRelease.js b/packages/remix/scripts/createRelease.js index 8f74eed5cd1a..c6fe5cb9ee2f 100644 --- a/packages/remix/scripts/createRelease.js +++ b/packages/remix/scripts/createRelease.js @@ -1,10 +1,10 @@ /* eslint-disable no-console */ -const SentryCli = require('@sentry/cli'); +const { createSentrySDK } = require('sentry'); const { deleteSourcemaps } = require('./deleteSourcemaps'); async function createRelease(argv, URL_PREFIX, BUILD_PATH) { - const sentry = new SentryCli(null, { + const sentry = createSentrySDK({ url: argv.url, org: argv.org, project: argv.project, @@ -14,7 +14,7 @@ async function createRelease(argv, URL_PREFIX, BUILD_PATH) { if (!argv.release) { try { - release = await sentry.releases.proposeVersion(); + release = (await sentry.release['propose-version']()).version; } catch (error) { console.warn('[sentry] Failed to propose a release version.'); console.warn('[sentry] You can specify a release version with `--release` flag.'); @@ -25,22 +25,21 @@ async function createRelease(argv, URL_PREFIX, BUILD_PATH) { release = argv.release; } - await sentry.releases.new(release); + await sentry.release.create({ orgVersion: release }); try { - await sentry.releases.uploadSourceMaps(release, { + await sentry.sourcemap.upload({ + directory: BUILD_PATH, + release, urlPrefix: URL_PREFIX, - include: [BUILD_PATH], - // oxlint-disable-next-line typescript/no-deprecated -- kept for older Sentry CLI versions that still honor it - useArtifactBundle: !argv.disableDebugIds, - live: 'rejectOnError', + noRewrite: argv.disableDebugIds, }); } catch { console.warn('[sentry] Failed to upload sourcemaps.'); } try { - await sentry.releases.finalize(release); + await sentry.release.finalize({ orgVersion: release }); } catch { console.warn('[sentry] Failed to finalize release.'); } diff --git a/packages/remix/scripts/injectDebugId.js b/packages/remix/scripts/injectDebugId.js index 2eb05ef87ba6..80adfc0d4701 100644 --- a/packages/remix/scripts/injectDebugId.js +++ b/packages/remix/scripts/injectDebugId.js @@ -1,13 +1,9 @@ /* eslint-disable no-console */ -const { execSync } = require('child_process'); - -const SentryCli = require('@sentry/cli'); - -function injectDebugId(buildPath) { - const cliPath = SentryCli.getPath(); +const { createSentrySDK } = require('sentry'); +async function injectDebugId(buildPath) { try { - execSync(`${cliPath} sourcemaps inject ${buildPath}`); + await createSentrySDK().sourcemap.inject({ directory: buildPath }); } catch (error) { console.warn('[sentry] Failed to inject debug ids.'); console.error(error); diff --git a/packages/remix/scripts/sentry-upload-sourcemaps.js b/packages/remix/scripts/sentry-upload-sourcemaps.js index ceb41d3ac14c..a5394e23b69c 100755 --- a/packages/remix/scripts/sentry-upload-sourcemaps.js +++ b/packages/remix/scripts/sentry-upload-sourcemaps.js @@ -77,8 +77,15 @@ if (!argv.uploadSourcemaps) { const buildPath = argv.buildPath || DEFAULT_BUILD_PATH; const urlPrefix = argv.urlPrefix || DEFAULT_URL_PREFIX; -if (!argv.disableDebugIds) { - injectDebugId(buildPath); -} +// eslint-disable-next-line no-console +(async () => { + if (!argv.disableDebugIds) { + await injectDebugId(buildPath); + } -createRelease(argv, urlPrefix, buildPath); + await createRelease(argv, urlPrefix, buildPath); +})().catch(error => { + // eslint-disable-next-line no-console + console.error(error); + process.exit(1); +}); diff --git a/packages/remix/test/scripts/upload-sourcemaps.test.ts b/packages/remix/test/scripts/upload-sourcemaps.test.ts index 677c602a011f..cce1ed03f269 100644 --- a/packages/remix/test/scripts/upload-sourcemaps.test.ts +++ b/packages/remix/test/scripts/upload-sourcemaps.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -const newMock = vi.fn(); +const createMock = vi.fn(); const uploadSourceMapsMock = vi.fn(); const finalizeMock = vi.fn(); -const proposeVersionMock = vi.fn(() => '0.1.2.3.4'); +const proposeVersionMock = vi.fn(() => ({ version: '0.1.2.3.4' })); const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); @@ -22,27 +22,27 @@ async function mock(mockedUri: string, stub: any) { } await vi.hoisted(async () => - mock( - '@sentry/cli', - vi.fn().mockImplementation(() => { + mock('sentry', { + createSentrySDK: vi.fn().mockImplementation(() => { return { - execute: vi.fn(), - releases: { - new: newMock, - uploadSourceMaps: uploadSourceMapsMock, + release: { + create: createMock, finalize: finalizeMock, - proposeVersion: proposeVersionMock, + 'propose-version': proposeVersionMock, + }, + sourcemap: { + upload: uploadSourceMapsMock, }, }; }), - ), + }), ); // eslint-disable-next-line @typescript-eslint/no-var-requires const { createRelease } = require('../../scripts/createRelease'); beforeEach(() => { - newMock.mockClear(); + createMock.mockClear(); uploadSourceMapsMock.mockClear(); finalizeMock.mockClear(); proposeVersionMock.mockClear(); @@ -53,28 +53,28 @@ describe('createRelease', () => { await createRelease({ release: '0.1.2.3' }, '~/build/', 'public/build'); expect(proposeVersionMock).not.toHaveBeenCalled(); - expect(newMock).toHaveBeenCalledWith('0.1.2.3'); - expect(uploadSourceMapsMock).toHaveBeenCalledWith('0.1.2.3', { + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3' }); + expect(uploadSourceMapsMock).toHaveBeenCalledWith({ + directory: 'public/build', + release: '0.1.2.3', urlPrefix: '~/build/', - include: ['public/build'], - useArtifactBundle: true, - live: 'rejectOnError', + noRewrite: undefined, }); - expect(finalizeMock).toHaveBeenCalledWith('0.1.2.3'); + expect(finalizeMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3' }); }); it('should call `proposeVersion` when release param is not given.', async () => { await createRelease({}, '~/build/', 'public/build'); expect(proposeVersionMock).toHaveBeenCalled(); - expect(newMock).toHaveBeenCalledWith('0.1.2.3.4'); - expect(uploadSourceMapsMock).toHaveBeenCalledWith('0.1.2.3.4', { + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); + expect(uploadSourceMapsMock).toHaveBeenCalledWith({ + directory: 'public/build', + release: '0.1.2.3.4', urlPrefix: '~/build/', - include: ['public/build'], - useArtifactBundle: true, - live: 'rejectOnError', + noRewrite: undefined, }); - expect(finalizeMock).toHaveBeenCalledWith('0.1.2.3.4'); + expect(finalizeMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); }); it('should use given buildPath and urlPrefix over the defaults when given.', async () => { @@ -88,14 +88,14 @@ describe('createRelease', () => { ); expect(proposeVersionMock).toHaveBeenCalled(); - expect(newMock).toHaveBeenCalledWith('0.1.2.3.4'); - expect(uploadSourceMapsMock).toHaveBeenCalledWith('0.1.2.3.4', { + expect(createMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); + expect(uploadSourceMapsMock).toHaveBeenCalledWith({ + directory: 'public/build', + release: '0.1.2.3.4', urlPrefix: '~/build/', - include: ['public/build'], - useArtifactBundle: true, - live: 'rejectOnError', + noRewrite: undefined, }); - expect(finalizeMock).toHaveBeenCalledWith('0.1.2.3.4'); + expect(finalizeMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); }); it('logs an error when uploadSourceMaps fails', async () => { @@ -103,16 +103,16 @@ describe('createRelease', () => { await createRelease({}, '~/build/', 'public/build'); - expect(uploadSourceMapsMock).toHaveBeenCalledWith('0.1.2.3.4', { + expect(uploadSourceMapsMock).toHaveBeenCalledWith({ + directory: 'public/build', + release: '0.1.2.3.4', urlPrefix: '~/build/', - include: ['public/build'], - useArtifactBundle: true, - live: 'rejectOnError', + noRewrite: undefined, }); expect(consoleWarnSpy).toHaveBeenCalledWith('[sentry] Failed to upload sourcemaps.'); - expect(finalizeMock).toHaveBeenCalledWith('0.1.2.3.4'); + expect(finalizeMock).toHaveBeenCalledWith({ orgVersion: '0.1.2.3.4' }); }); it('logs an error when finalize fails', async () => { diff --git a/yarn.lock b/yarn.lock index c370ee3da5e5..5e7b8606444b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7862,66 +7862,6 @@ "@angular-devkit/schematics" "14.2.13" jsonc-parser "3.1.0" -"@sentry/cli-darwin@2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.58.6.tgz#38fd82751014b287e58e99ef948d01ca1e09f41d" - integrity sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA== - -"@sentry/cli-linux-arm64@2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.6.tgz#6e660e457af7928c1be8191c77646801fe3fa6a0" - integrity sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g== - -"@sentry/cli-linux-arm@2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.6.tgz#41256912d636193d2a67985b6c9b3efbbe6a47c9" - integrity sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw== - -"@sentry/cli-linux-i686@2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.6.tgz#278e7696d82e51dfbfd7d82ec125dda65d249a43" - integrity sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg== - -"@sentry/cli-linux-x64@2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.6.tgz#57860d46ac3397c33bbcc6224ac19b7de2502c18" - integrity sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q== - -"@sentry/cli-win32-arm64@2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.6.tgz#9335a5d2411381dca1d6b11fdd71a4342b375fc3" - integrity sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A== - -"@sentry/cli-win32-i686@2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.6.tgz#2afd19536ef111af43538ccc5f9c8c0b179d930e" - integrity sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg== - -"@sentry/cli-win32-x64@2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.6.tgz#8d0e70b5660cc82a7763a4bbe9346cf18e49e07e" - integrity sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA== - -"@sentry/cli@^2.58.6": - version "2.58.6" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.58.6.tgz#72edb4977d822757511b279e006b00f139e24945" - integrity sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg== - dependencies: - https-proxy-agent "^5.0.0" - node-fetch "^2.6.7" - progress "^2.0.3" - proxy-from-env "^1.1.0" - which "^2.0.2" - optionalDependencies: - "@sentry/cli-darwin" "2.58.6" - "@sentry/cli-linux-arm" "2.58.6" - "@sentry/cli-linux-arm64" "2.58.6" - "@sentry/cli-linux-i686" "2.58.6" - "@sentry/cli-linux-x64" "2.58.6" - "@sentry/cli-win32-arm64" "2.58.6" - "@sentry/cli-win32-i686" "2.58.6" - "@sentry/cli-win32-x64" "2.58.6" - "@sentry/conventions@0.19.0", "@sentry/conventions@^0.19.0": version "0.19.0" resolved "https://registry.yarnpkg.com/@sentry/conventions/-/conventions-0.19.0.tgz#da81dd1b8c4f2f42aca2d74251a07ff9f8718e42" @@ -23137,11 +23077,6 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -23216,11 +23151,6 @@ proxy-from-env@2.1.0, proxy-from-env@^2.1.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" integrity sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA== -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - proxy@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/proxy/-/proxy-2.1.1.tgz#45f9b307508ffcae12bdc71678d44a4ab79cbf8b" @@ -24744,6 +24674,11 @@ send@~0.19.0, send@~0.19.1: range-parser "~1.2.1" statuses "~2.0.2" +sentry@^0.42.2: + version "0.42.2" + resolved "https://registry.yarnpkg.com/sentry/-/sentry-0.42.2.tgz#dcc490ef6c1d20f6612c97ec0ca32eca3ea6ee3f" + integrity sha512-k5h0j8oQDVRrX6vSAMvE7p6F0LymyUzAkFeK6yD0wZ7V7APGdkYv2g8jPgq4erh/RrLhohfu0FjLp06sXSlrsg== + serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"