diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/telemetry.test.ts index fe5649ca57dc..2585278adc54 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/telemetry.test.ts @@ -6,7 +6,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { expect(readOutputFiles()).toMatchInlineSnapshot(` { "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"esbuild","bundler-major-version":"28"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"delete-sourcemaps-after-upload":"auto","sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"esbuild","bundler-major-version":"28"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", "telemetry.js": "(() => { diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/telemetry.test.ts index 77ac826d8d70..f995682ca797 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rolldown/telemetry.test.ts @@ -18,7 +18,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //#endregion ", "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"delete-sourcemaps-after-upload":"auto","sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/telemetry.test.ts index 329f3b6969ed..72ae140b7b42 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup3/telemetry.test.ts @@ -9,7 +9,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { !function(){try{var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"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-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"3"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"delete-sourcemaps-after-upload":"auto","sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"3"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/telemetry.test.ts index 4e0c4d969660..d4bde5c071ae 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/rollup4/telemetry.test.ts @@ -9,7 +9,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { !function(){try{var e="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"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-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"delete-sourcemaps-after-upload":"auto","sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"rollup","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/telemetry.test.ts index d6ea3f3a91d5..f61dde5478e3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite4/telemetry.test.ts @@ -17,7 +17,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { console.log("hello world"); ", "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"delete-sourcemaps-after-upload":"auto","sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"4"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/telemetry.test.ts index 3b5c50c6a113..9ad711e030fc 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite7/telemetry.test.ts @@ -17,7 +17,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { console.log("hello world"); ", "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"7"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"delete-sourcemaps-after-upload":"auto","sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"7"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/telemetry.test.ts index d09ee8bc094a..6d832d2ae117 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/vite8/telemetry.test.ts @@ -18,7 +18,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { //#endregion ", "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"8"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"delete-sourcemaps-after-upload":"auto","sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"vite","bundler-major-version":"8"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/telemetry.test.ts index 0854328dd4a8..22d582168680 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/telemetry.test.ts @@ -13,7 +13,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { /******/ })() ;", "sentry-telemetry.json": "[{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":true,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"ok","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], - [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"webpack","bundler-major-version":"5"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], + [{"event_id":"UUID","sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"},"trace":{"environment":"production","release":"PLUGIN_VERSION","public_key":"UUID","trace_id":"UUID","org_id":"1","transaction":"Sentry Bundler Plugin execution","sampled":"true","sample_rand":"SAMPLE_RAND","sample_rate":"0.3"}},[[{"type":"transaction"},{"contexts":{"trace":{"span_id":"SHORT_UUID","trace_id":"UUID","data":{"sentry.origin":"manual","sentry.segment.name.source":"custom","sentry.sample_rate":0.3},"status":"ok","origin":"manual"},"runtime":{"name":"node","version":"NODE_VERSION"}},"spans":[],"start_timestamp":START_TIMESTAMP,"timestamp":TIMESTAMP,"transaction":"Sentry Bundler Plugin execution","type":"transaction","transaction_info":{"source":"custom"},"platform":"PLATFORM","event_id":"UUID","environment":"production","release":"PLUGIN_VERSION","tags":{"upload-legacy-sourcemaps":false,"module-metadata":false,"inject-build-information":false,"set-commits":"auto","finalize-release":true,"deploy-options":false,"custom-error-handler":false,"sourcemaps-assets":false,"delete-after-upload":false,"delete-sourcemaps-after-upload":"auto","sourcemaps-disabled":false,"react-annotate":false,"node":"NODE_VERSION","platform":"PLATFORM","meta-framework":"none","application-key-set":false,"ci":true,"project":"undefined","bundler":"webpack","bundler-major-version":"5"},"user":{},"sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION","integrations":[],"packages":[{"name":"npm:@sentry/node","version":"SDK_VERSION"}]}}]]], [{"sent_at":"TIMESTAMP","sdk":{"name":"sentry.javascript.node","version":"SDK_VERSION"}},[[{"type":"session"},{"sid":"UUID","init":false,"started":"TIMESTAMP","timestamp":"TIMESTAMP","status":"exited","errors":0,"duration":DURATION,"attrs":{"release":"PLUGIN_VERSION","environment":"production"}}]]], ", } diff --git a/packages/bundler-plugins/src/core/build-plugin-manager.ts b/packages/bundler-plugins/src/core/build-plugin-manager.ts index 242a77d73f0d..d0cf2f0d706e 100644 --- a/packages/bundler-plugins/src/core/build-plugin-manager.ts +++ b/packages/bundler-plugins/src/core/build-plugin-manager.ts @@ -12,8 +12,15 @@ import { createLogger } from './logger'; import { allowedToSendTelemetry, createSentryInstance, safeFlushTelemetry } from './sentry/telemetry'; import type { Options, SentrySDKBuildFlags } from './types'; import { arrayify, getProjects, getTurborepoEnvPassthroughWarning, stripQueryAndHashFromPath } from './utils'; -import { defaultRewriteSourcesHook, prepareBundleForDebugIdUpload } from './debug-id-upload'; +import { + defaultRewriteSourcesHook, + prepareArtifactForUpload, + prepareBundleForDebugIdUpload, + writePreparedArtifact, +} from './debug-id-upload'; import { globFiles } from './glob'; +import type { SourceMapDecision, SourceMapKind } from './sourcemap-settings'; +import { decideSourceMapHandling } from './sourcemap-settings'; import { LIB_VERSION } from './version'; // Module-level guard to prevent duplicate deploy records when multiple bundler plugin @@ -26,6 +33,31 @@ export function _resetDeployedReleasesForTesting(): void { _deployedReleases.clear(); } +/** + * A temporary folder that build artifacts are written into straight from the bundler's output, + * before the bundler writes them to disk. The upload reads from this folder, so the bundler can + * drop the source map assets from its own output. + */ +export type UploadStaging = { + readonly dir: string; + /** Number of artifacts written so far. */ + readonly count: number; + /** + * Stamps the debug ID into the chunk and its source map and writes both into the folder. + * + * `mapSource` is `undefined` when the chunk has no separate source map. `mapDir` is the directory + * the map would be written to and is passed to the `rewriteSources` hook. + */ + stage(input: { + code: string; + mapSource: string | undefined; + mapDir: string; + fileName: string; + }): Promise<'staged' | 'skipped' | 'failed'>; + /** Removes the folder. Call this when no upload will consume it. */ + discard(): Promise; +}; + export type SentryBuildPluginManager = { /** * A logger instance that takes the options passed to the build plugin manager into account. (for silencing and log level etc.) @@ -80,7 +112,28 @@ export type SentryBuildPluginManager = { /** * Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded */ - uploadSourcemaps(buildArtifactPaths: string[], opts?: { prepareArtifacts?: boolean }): Promise; + uploadSourcemaps( + buildArtifactPaths: string[], + opts?: { prepareArtifacts?: boolean; staging?: UploadStaging }, + ): Promise<{ uploaded: boolean }>; + + /** + * Creates a staging folder that `uploadSourcemaps` uploads together with the build artifacts + * found on disk, in both the prepared and the direct (`prepareArtifacts: false`) mode. + */ + createUploadStaging(): Promise; + + sourceMaps: { + /** + * Whether a source map upload can run with the current options and environment. + */ + uploadPossible: boolean; + /** + * Decides how the plugin treats the user's source map setting (see `decideSourceMapHandling`). + * Logs the decision's message once per message. + */ + decide(kind: SourceMapKind): SourceMapDecision; + }; /** * Will delete artifacts based on the passed `sourcemaps.filesToDeleteAfterUpload` option. @@ -155,8 +208,18 @@ export function createSentryBuildPluginManager( createRelease: async () => { /* noop */ }, - uploadSourcemaps: async () => { - /* noop */ + uploadSourcemaps: async () => ({ uploaded: false }), + createUploadStaging: async () => ({ + dir: '', + count: 0, + stage: async () => 'skipped' as const, + discard: async () => { + /* noop */ + }, + }), + sourceMaps: { + uploadPossible: false, + decide: () => ({ enableHidden: false, deleteAfterUpload: false }), }, deleteArtifacts: async () => { /* noop */ @@ -283,6 +346,9 @@ export function createSentryBuildPluginManager( }; } + const sourceMapUploadPossible = getSourceMapUploadBlocker(options, isDevMode) === undefined; + const loggedSourceMapMessages = new Set(); + /** * Returns a Promise that resolves when all the currently active dependencies are freed again. * @@ -548,17 +614,19 @@ export function createSentryBuildPluginManager( * @param buildArtifactPaths - The paths of the build artifacts to upload * @param opts - Optional flags to control temp folder usage and preparation */ - async uploadSourcemaps(buildArtifactPaths: string[], opts?: { prepareArtifacts?: boolean }) { + async uploadSourcemaps( + buildArtifactPaths: string[], + opts?: { prepareArtifacts?: boolean; staging?: UploadStaging }, + ): Promise<{ uploaded: boolean }> { + const staging = opts?.staging; + if (!canUploadSourceMaps(options, logger, isDevMode)) { - return; + await staging?.discard(); + return { uploaded: false }; } - // Early exit if assets is explicitly set to an empty array const assets = options.sourcemaps?.assets; - if (Array.isArray(assets) && assets.length === 0) { - logger.debug('Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID.'); - return; - } + let uploaded = false; await startSpan( // This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions. @@ -568,7 +636,7 @@ export function createSentryBuildPluginManager( // If we're not using a temp folder, we must not prepare artifacts in-place (to avoid mutating user files) const shouldPrepare = opts?.prepareArtifacts ?? true; - let folderToCleanUp: string | undefined; + let folderToCleanUp: string | undefined = staging?.dir; // It is possible that this writeBundle hook (which calls this function) is called multiple times in one build (for example when reusing the plugin, or when using build tooling like `@vitejs/plugin-legacy`) // Therefore we need to actually register the execution of this hook as dependency on the sourcemap files. @@ -589,6 +657,11 @@ export function createSentryBuildPluginManager( pathsToUpload = buildArtifactPaths; } + // Staged artifacts are already prepared, so the CLI can take the folder as it is. + if (staging && staging.count > 0) { + pathsToUpload = [...pathsToUpload, staging.dir]; + } + await startSpan({ name: 'upload', scope: sentryScope }, async () => { const cliInstance = new SentryCliAdapter(options); await cliInstance.uploadSourcemaps( @@ -601,6 +674,7 @@ export function createSentryBuildPluginManager( ); }); + uploaded = true; logger.info('Successfully uploaded source maps to Sentry'); } else { // Prepare artifacts in temp folder before uploading @@ -626,17 +700,16 @@ export function createSentryBuildPluginManager( // Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent debugIdChunkFilePaths.sort(); - if (debugIdChunkFilePaths.length === 0) { + const stagedCount = staging?.count ?? 0; + + if (debugIdChunkFilePaths.length === 0 && stagedCount === 0) { logger.warn( "Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option.", ); } else { - const tmpUploadFolder = await startSpan({ name: 'mkdtemp', scope: sentryScope }, async () => { - return ( - process.env?.['SENTRY_TEST_OVERRIDE_TEMP_DIR'] || - (await fs.promises.mkdtemp(path.join(os.tmpdir(), 'sentry-bundler-plugin-upload-'))) - ); - }); + const tmpUploadFolder = + staging?.dir ?? + (await startSpan({ name: 'mkdtemp', scope: sentryScope }, () => createTempUploadFolder())); folderToCleanUp = tmpUploadFolder; // Prepare into temp folder, then upload @@ -647,7 +720,7 @@ export function createSentryBuildPluginManager( await prepareBundleForDebugIdUpload( chunkFilePath, tmpUploadFolder, - chunkIndex, + stagedCount + chunkIndex, logger, options.sourcemaps?.rewriteSources ?? defaultRewriteSourcesHook, options.sourcemaps?.resolveSourceMap, @@ -684,7 +757,7 @@ export function createSentryBuildPluginManager( // bundler, so warn instead of silently reporting success. if (files.length === 0) { logger.warn( - `No source maps found for any of the ${debugIdChunkFilePaths.length} matched build ` + + `No source maps found for any of the ${debugIdChunkFilePaths.length + stagedCount} matched build ` + 'artifacts, so no source maps were uploaded to Sentry. This usually means source map ' + 'generation is not enabled in your bundler. Enable it so Sentry can un-minify your stack traces.', ); @@ -703,11 +776,21 @@ export function createSentryBuildPluginManager( // this must be in the method so that the "no sourcemaps" // early return doesn't also log success. + uploaded = true; logger.info('Successfully uploaded source maps to Sentry'); }); } } } catch (e) { + if (staging && folderToCleanUp) { + // The bundler drops the staged source maps from its output, so + // this folder is the only copy left. + logger.error( + `Source map upload failed. The source maps were kept in ${folderToCleanUp}. ` + + `Upload them with \`sentry sourcemap upload ${folderToCleanUp}\`.`, + ); + folderToCleanUp = undefined; + } sentryScope.captureException('Error in "debugIdUploadPlugin" writeBundle hook'); handleRecoverableError(e, false); } finally { @@ -734,6 +817,76 @@ export function createSentryBuildPluginManager( } }, ); + + return { uploaded }; + }, + + async createUploadStaging(): Promise { + const dir = await createTempUploadFolder(); + const rewriteSourcesHook = options.sourcemaps?.rewriteSources ?? defaultRewriteSourcesHook; + let count = 0; + let discarded = false; + + return { + dir, + get count() { + return count; + }, + async stage(input) { + const result = prepareArtifactForUpload({ + code: input.code, + mapSource: input.mapSource, + mapDir: input.mapDir, + rewriteSourcesHook, + }); + + if (!result.ok) { + logger.debug(`Not staging ${input.fileName} for upload: ${result.reason}`); + return 'skipped'; + } + + const index = count++; + + try { + await writePreparedArtifact(dir, `${result.artifact.debugId}-${index}`, result.artifact); + return 'staged'; + } catch (e) { + sentryScope.captureException('Error staging build artifact for upload'); + // Throwing by default: the bundler drops the source map from its + // output once staged, so a lost copy would leave no map at all. + handleRecoverableError(e, true); + return 'failed'; + } + }, + async discard() { + if (discarded) { + return; + } + discarded = true; + + if (process.env['SENTRY_TEST_OVERRIDE_TEMP_DIR']) { + return; + } + + try { + await fs.promises.rm(dir, { recursive: true, force: true }); + } catch (e) { + logger.debug(`Failed to remove staging folder ${dir}:`, e); + } + }, + }; + }, + + sourceMaps: { + uploadPossible: sourceMapUploadPossible, + decide(kind) { + const decision = decideSourceMapHandling(kind, options, sourceMapUploadPossible); + if (decision.log && !loggedSourceMapMessages.has(decision.log.message)) { + loggedSourceMapMessages.add(decision.log.message); + logger[decision.log.level](decision.log.message); + } + return decision; + }, }, /** @@ -774,33 +927,64 @@ export function createSentryBuildPluginManager( }; } -function canUploadSourceMaps(options: NormalizedOptions, logger: Logger, isDevMode: boolean): boolean { +async function createTempUploadFolder(): Promise { + return ( + process.env['SENTRY_TEST_OVERRIDE_TEMP_DIR'] || + (await fs.promises.mkdtemp(path.join(os.tmpdir(), 'sentry-bundler-plugin-upload-'))) + ); +} + +type SourceMapUploadBlocker = 'disabled' | 'no-assets' | 'dev-mode' | 'no-auth-token' | 'no-org' | 'no-project'; + +function getSourceMapUploadBlocker(options: NormalizedOptions, isDevMode: boolean): SourceMapUploadBlocker | undefined { if (options.sourcemaps?.disable) { - logger.debug('Source map upload was disabled. Will not upload sourcemaps using debug ID process.'); - return false; + return 'disabled'; + } + if (Array.isArray(options.sourcemaps?.assets) && options.sourcemaps.assets.length === 0) { + return 'no-assets'; } if (isDevMode) { - logger.debug('Running in development mode. Will not upload sourcemaps.'); - return false; + return 'dev-mode'; } if (!options.authToken) { - logger.warn( - `No auth token provided. Will not upload source maps. Please set the \`authToken\` option. You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/${getTurborepoEnvPassthroughWarning('SENTRY_AUTH_TOKEN')}`, - ); - return false; + return 'no-auth-token'; } if (!options.org && !options.authToken.startsWith('sntrys_')) { - logger.warn( - `No org provided. Will not upload source maps. Please set the \`org\` option to your Sentry organization slug.${getTurborepoEnvPassthroughWarning('SENTRY_ORG')}`, - ); - return false; + return 'no-org'; } if (!getProjects(options.project)?.[0]) { - logger.warn( - `No project provided. Will not upload source maps. Please set the \`project\` option to your Sentry project slug.${getTurborepoEnvPassthroughWarning('SENTRY_PROJECT')}`, - ); - return false; + return 'no-project'; } + return undefined; +} - return true; +function canUploadSourceMaps(options: NormalizedOptions, logger: Logger, isDevMode: boolean): boolean { + switch (getSourceMapUploadBlocker(options, isDevMode)) { + case 'disabled': + logger.debug('Source map upload was disabled. Will not upload sourcemaps using debug ID process.'); + return false; + case 'no-assets': + logger.debug('Empty `sourcemaps.assets` option provided. Will not upload sourcemaps with debug ID.'); + return false; + case 'dev-mode': + logger.debug('Running in development mode. Will not upload sourcemaps.'); + return false; + case 'no-auth-token': + logger.warn( + `No auth token provided. Will not upload source maps. Please set the \`authToken\` option. You can find information on how to generate a Sentry auth token here: https://docs.sentry.io/api/auth/${getTurborepoEnvPassthroughWarning('SENTRY_AUTH_TOKEN')}`, + ); + return false; + case 'no-org': + logger.warn( + `No org provided. Will not upload source maps. Please set the \`org\` option to your Sentry organization slug.${getTurborepoEnvPassthroughWarning('SENTRY_ORG')}`, + ); + return false; + case 'no-project': + logger.warn( + `No project provided. Will not upload source maps. Please set the \`project\` option to your Sentry project slug.${getTurborepoEnvPassthroughWarning('SENTRY_PROJECT')}`, + ); + return false; + default: + return true; + } } diff --git a/packages/bundler-plugins/src/core/debug-id-upload.ts b/packages/bundler-plugins/src/core/debug-id-upload.ts index 8041e8c99d0f..2d9e16d49dd1 100644 --- a/packages/bundler-plugins/src/core/debug-id-upload.ts +++ b/packages/bundler-plugins/src/core/debug-id-upload.ts @@ -2,8 +2,7 @@ import fs from 'fs'; import path from 'path'; import * as url from 'url'; import * as util from 'util'; -import { promisify } from 'util'; -import type { SentryBuildPluginManager } from './build-plugin-manager'; +import type { SentryBuildPluginManager, UploadStaging } from './build-plugin-manager'; import type { Logger } from './logger'; import type { ResolveSourceMapHook, RewriteSourcesHook } from './types'; import { stripQueryAndHashFromPath } from './utils'; @@ -13,11 +12,11 @@ interface DebugIdUploadPluginOptions { } export function createDebugIdUploadFunction({ sentryBuildPluginManager }: DebugIdUploadPluginOptions) { - return async (buildArtifactPaths: string[]) => { + return async (buildArtifactPaths: string[], opts?: { staging?: UploadStaging }) => { // Webpack and perhaps other bundlers allow you to append query strings to // filenames for cache busting purposes. We should strip these before upload. const cleanedPaths = buildArtifactPaths.map(stripQueryAndHashFromPath); - await sentryBuildPluginManager.uploadSourcemaps(cleanedPaths); + return sentryBuildPluginManager.uploadSourcemaps(cleanedPaths, opts); }; } @@ -29,26 +28,21 @@ export async function prepareBundleForDebugIdUpload( rewriteSourcesHook: RewriteSourcesHook, resolveSourceMapHook: ResolveSourceMapHook | undefined, ): Promise { - let bundleContent; + let bundleContent: string; try { - bundleContent = await promisify(fs.readFile)(bundleFilePath, 'utf8'); + bundleContent = await fs.promises.readFile(bundleFilePath, 'utf8'); } catch (e) { logger.error(`Could not read bundle to determine debug ID and source map: ${bundleFilePath}`, e); return; } - const debugId = determineDebugIdFromBundleSource(bundleContent); - if (debugId === undefined) { + if (determineDebugIdFromBundleSource(bundleContent) === undefined) { logger.debug( `Could not determine debug ID from bundle. This can happen if you did not clean your output folder before installing the Sentry plugin. File will not be source mapped: ${bundleFilePath}`, ); return; } - const uniqueUploadName = `${debugId}-${chunkIndex}`; - - bundleContent = addDebugIdToBundleSource(bundleContent, debugId); - const sourceMapPath = await determineSourceMapPathFromBundle( bundleFilePath, bundleContent, @@ -56,35 +50,118 @@ export async function prepareBundleForDebugIdUpload( resolveSourceMapHook, ); - // A chunk with a debug ID but no resolvable source map - // (e.g. framework-generated stub chunks that never emit one) can't be - // symbolicated, so uploading its minified source alone accomplishes - // nothing and only makes the uploader warn about a missing source map ref. - // Skip it, unless the source map is inlined into the bundle, in which case - // the source file carries the map itself and must still be uploaded. - if (!sourceMapPath && !bundleHasInlineSourceMap(bundleContent)) { - logger.debug(`Not uploading bundle without a source map: ${bundleFilePath}`); + let sourceMapSource: string | undefined; + if (sourceMapPath) { + try { + sourceMapSource = await fs.promises.readFile(sourceMapPath, 'utf8'); + } catch (e) { + logger.error(`Failed to read source map for debug ID upload: ${sourceMapPath}`, e); + return; + } + } + + const result = prepareArtifactForUpload({ + code: bundleContent, + mapSource: sourceMapSource, + mapDir: path.dirname(sourceMapPath ?? bundleFilePath), + rewriteSourcesHook, + }); + + if (!result.ok) { + if (result.reason === 'invalid-source-map') { + logger.error(`Failed to parse source map for debug ID upload: ${sourceMapPath}`); + } else { + // A chunk with a debug ID but no resolvable source map + // (e.g. framework-generated stub chunks that never emit one) can't be + // symbolicated, so uploading its minified source alone accomplishes + // nothing and only makes the uploader warn about a missing source map ref. + logger.debug(`Not uploading bundle without a source map: ${bundleFilePath}`); + } return; } - const writeSourceFilePromise = fs.promises.writeFile( - path.join(uploadFolder, `${uniqueUploadName}.js`), - bundleContent, - 'utf-8', - ); + try { + await writePreparedArtifact(uploadFolder, `${result.artifact.debugId}-${chunkIndex}`, result.artifact); + } catch (e) { + logger.error(`Failed to prepare bundle for debug ID upload: ${bundleFilePath}`, e); + } +} + +export type PreparedUploadArtifact = { + debugId: string; + bundleSource: string; + /** `undefined` when the bundle has no separate source map (i.e. the map is inlined). */ + sourceMapSource: string | undefined; +}; + +export type PrepareArtifactResult = + | { ok: true; artifact: PreparedUploadArtifact } + | { ok: false; reason: 'no-debug-id' | 'no-source-map' | 'invalid-source-map' }; + +/** + * Prepares a bundle and its source map for the debug ID upload: stamps the debug ID into both + * and runs the `rewriteSources` hook over the map's `sources`. + * + * Pass `mapSource: undefined` when the bundle has no separate source map. A bundle with an + * inlined map is still prepared; a bundle without any map is skipped because it cannot be + * symbolicated. + */ +export function prepareArtifactForUpload(input: { + code: string; + mapSource: string | undefined; + /** Directory the source map lives in, passed to the `rewriteSources` hook. */ + mapDir: string; + rewriteSourcesHook: RewriteSourcesHook; +}): PrepareArtifactResult { + const debugId = determineDebugIdFromBundleSource(input.code); + if (debugId === undefined) { + return { ok: false, reason: 'no-debug-id' }; + } + + const bundleSource = addDebugIdToBundleSource(input.code, debugId); + + if (input.mapSource === undefined) { + if (!bundleHasInlineSourceMap(input.code)) { + return { ok: false, reason: 'no-source-map' }; + } + return { ok: true, artifact: { debugId, bundleSource, sourceMapSource: undefined } }; + } + + const map = parseSourceMap(input.mapSource); + if (!map) { + return { ok: false, reason: 'invalid-source-map' }; + } + + setDebugIdOnSourceMap(map, debugId); + + if (Array.isArray(map['sources'])) { + map['sources'] = map['sources'].map((source: string) => + input.rewriteSourcesHook(source, map, { mapDir: input.mapDir }), + ); + } + + return { ok: true, artifact: { debugId, bundleSource, sourceMapSource: JSON.stringify(map) } }; +} + +/** + * Writes a prepared artifact as `.js` (and `.js.map`) into the upload folder. + */ +export async function writePreparedArtifact( + uploadFolder: string, + uniqueUploadName: string, + artifact: PreparedUploadArtifact, +): Promise { + const writes = [ + fs.promises.writeFile(path.join(uploadFolder, `${uniqueUploadName}.js`), artifact.bundleSource, 'utf-8'), + ]; + + if (artifact.sourceMapSource !== undefined) { + writes.push( + fs.promises.writeFile(path.join(uploadFolder, `${uniqueUploadName}.js.map`), artifact.sourceMapSource, 'utf-8'), + ); + } - const writeSourceMapFilePromise = sourceMapPath - ? prepareSourceMapForDebugIdUpload( - sourceMapPath, - path.join(uploadFolder, `${uniqueUploadName}.js.map`), - debugId, - rewriteSourcesHook, - logger, - ) - : Promise.resolve(); - - await writeSourceFilePromise; - await writeSourceMapFilePromise; + await Promise.all(writes); } /** @@ -321,50 +398,6 @@ export async function determineSourceMapPathFromBundle( return undefined; } -/** - * Reads a source map, injects debug ID fields, and writes the source map to the target path. - */ -async function prepareSourceMapForDebugIdUpload( - sourceMapPath: string, - targetPath: string, - debugId: string, - rewriteSourcesHook: RewriteSourcesHook, - logger: Logger, -): Promise { - let sourceMapFileContent: string; - try { - sourceMapFileContent = await util.promisify(fs.readFile)(sourceMapPath, { - encoding: 'utf8', - }); - } catch (e) { - logger.error(`Failed to read source map for debug ID upload: ${sourceMapPath}`, e); - return; - } - - let map: Record; - try { - map = JSON.parse(sourceMapFileContent) as { sources: unknown; [key: string]: unknown }; - setDebugIdOnSourceMap(map, debugId); - } catch { - logger.error(`Failed to parse source map for debug ID upload: ${sourceMapPath}`); - return; - } - - if (map['sources'] && Array.isArray(map['sources'])) { - const mapDir = path.dirname(sourceMapPath); - map['sources'] = map['sources'].map((source: string) => rewriteSourcesHook(source, map, { mapDir })); - } - - try { - await util.promisify(fs.writeFile)(targetPath, JSON.stringify(map), { - encoding: 'utf8', - }); - } catch (e) { - logger.error(`Failed to prepare source map for debug ID upload: ${sourceMapPath}`, e); - return; - } -} - const PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//; export function defaultRewriteSourcesHook(source: string): string { if (source.match(PROTOCOL_REGEX)) { diff --git a/packages/bundler-plugins/src/core/index.ts b/packages/bundler-plugins/src/core/index.ts index 4edc5ebbed91..e46caaaec203 100644 --- a/packages/bundler-plugins/src/core/index.ts +++ b/packages/bundler-plugins/src/core/index.ts @@ -110,4 +110,7 @@ export { generateModuleMetadataInjectorCode, } from './utils'; export { createSentryBuildPluginManager } from './build-plugin-manager'; +export type { SentryBuildPluginManager, UploadStaging } from './build-plugin-manager'; export { createDebugIdUploadFunction, addDebugIdToEmittedArtifacts, stampDebugId } from './debug-id-upload'; +export { decideSourceMapHandling } from './sourcemap-settings'; +export type { SourceMapDecision, SourceMapKind } from './sourcemap-settings'; diff --git a/packages/bundler-plugins/src/core/options-mapping.ts b/packages/bundler-plugins/src/core/options-mapping.ts index 4847b11b0415..c190d3efc5cc 100644 --- a/packages/bundler-plugins/src/core/options-mapping.ts +++ b/packages/bundler-plugins/src/core/options-mapping.ts @@ -30,6 +30,7 @@ export type NormalizedOptions = { rewriteSources?: RewriteSourcesHook; resolveSourceMap?: ResolveSourceMapHook; filesToDeleteAfterUpload?: string | string[] | Promise; + deleteSourcemapsAfterUpload?: boolean; } | undefined; release: { @@ -214,6 +215,13 @@ export function validateOptions(options: NormalizedOptions, logger: Logger): boo return false; } + if (options.sourcemaps?.deleteSourcemapsAfterUpload && options.sourcemaps.disable) { + logger.warn( + 'The `sourcemaps.deleteSourcemapsAfterUpload` option is set but source map upload is disabled via `sourcemaps.disable`.', + 'Source maps will not be deleted.', + ); + } + if (options.project && Array.isArray(options.project)) { if (options.project.length === 0) { logger.error( diff --git a/packages/bundler-plugins/src/core/sentry/telemetry.ts b/packages/bundler-plugins/src/core/sentry/telemetry.ts index 72ea587bfef8..c9da77637d90 100644 --- a/packages/bundler-plugins/src/core/sentry/telemetry.ts +++ b/packages/bundler-plugins/src/core/sentry/telemetry.ts @@ -106,6 +106,10 @@ export function setTelemetryDataOnScope( scope.setTag('custom-error-handler', !!errorHandler); scope.setTag('sourcemaps-assets', !!sourcemaps?.assets); scope.setTag('delete-after-upload', !!sourcemaps?.filesToDeleteAfterUpload); + scope.setTag( + 'delete-sourcemaps-after-upload', + sourcemaps?.deleteSourcemapsAfterUpload === undefined ? 'auto' : String(sourcemaps.deleteSourcemapsAfterUpload), + ); scope.setTag('sourcemaps-disabled', !!sourcemaps?.disable); scope.setTag('react-annotate', !!reactComponentAnnotation?.enabled); diff --git a/packages/bundler-plugins/src/core/sourcemap-settings.ts b/packages/bundler-plugins/src/core/sourcemap-settings.ts new file mode 100644 index 000000000000..9c8b720ca85e --- /dev/null +++ b/packages/bundler-plugins/src/core/sourcemap-settings.ts @@ -0,0 +1,94 @@ +import type { NormalizedOptions } from './options-mapping'; + +/** + * The user's source map setting in the bundler, reduced to what matters for upload and deletion. + * + * - `unset`: the user did not configure source maps + * - `off`: source map generation is turned off + * - `visible`: separate map files with a `sourceMappingURL` comment + * - `hidden`: separate map files without the comment + * - `inline`: the map is embedded into the bundle + */ +export type SourceMapKind = 'unset' | 'off' | 'visible' | 'hidden' | 'inline'; + +export type SourceMapDecision = { + /** Switch the bundler to hidden source maps. */ + enableHidden: boolean; + /** Remove the source maps from the build output once they are uploaded. */ + deleteAfterUpload: boolean; + log?: { level: 'info' | 'warn'; message: string }; +}; + +const NO_CHANGE: SourceMapDecision = { enableHidden: false, deleteAfterUpload: false }; + +/** + * Decides whether the plugin turns hidden source maps on and whether it removes the maps from the + * build output after the upload. The plugin only turns maps on when it also removes them, and only + * when an upload can run. + */ +export function decideSourceMapHandling( + kind: SourceMapKind, + options: NormalizedOptions, + uploadPossible: boolean, +): SourceMapDecision { + const deleteOption = options.sourcemaps?.deleteSourcemapsAfterUpload; + + // `false` keeps the plugin away from source maps, warnings included. + if (!uploadPossible || options.sourcemaps?.disable || deleteOption === false) { + return NO_CHANGE; + } + + switch (kind) { + case 'off': + return { + ...NO_CHANGE, + log: { + level: 'warn', + message: + 'Source map generation is turned off in your bundler, so Sentry cannot un-minify your stack traces. ' + + 'Remove the setting to let the plugin generate hidden source maps, upload them to Sentry and remove them from the build output.', + }, + }; + + case 'inline': + return { + ...NO_CHANGE, + log: { + level: 'warn', + message: + 'Source maps are inlined into your bundles. They are uploaded to Sentry, but inline source maps ship your source code to users and cannot be removed after the upload. ' + + 'Remove the setting to let the plugin generate hidden source maps instead.', + }, + }; + + case 'unset': + return { + enableHidden: true, + deleteAfterUpload: true, + log: { + level: 'info', + message: + 'Enabling hidden source maps. They are uploaded to Sentry and removed from the build output. ' + + 'Set `sourcemaps.deleteSourcemapsAfterUpload: false` to keep this plugin from touching source maps.', + }, + }; + + case 'hidden': + return { enableHidden: false, deleteAfterUpload: deleteOption === true }; + + case 'visible': + if (deleteOption !== true) { + return NO_CHANGE; + } + return { + enableHidden: true, + deleteAfterUpload: true, + log: { + level: 'info', + message: + 'Switching to hidden source maps because `sourcemaps.deleteSourcemapsAfterUpload` is set. ' + + 'Without the `sourceMappingURL` comment browsers do not request the removed map files.', + }, + }; + } +} diff --git a/packages/bundler-plugins/src/core/types.ts b/packages/bundler-plugins/src/core/types.ts index bd3761c12dca..6562b20b5e19 100644 --- a/packages/bundler-plugins/src/core/types.ts +++ b/packages/bundler-plugins/src/core/types.ts @@ -192,6 +192,18 @@ export interface Options { * Use the `debug` option to print information about which files end up being deleted. */ filesToDeleteAfterUpload?: string | string[] | Promise; + + /** + * Whether source maps should be removed from the build output after they are uploaded to Sentry. + * + * When unset, source maps are removed only if the plugin turned source map generation on itself, that is, when + * the bundler's source map setting was not set. Set to `true` to also remove source maps you enabled yourself, or + * to `false` to keep the plugin from changing or warning about your source map setting. + * + * With Rollup, Vite and webpack the source map files are never written to the output directory. With esbuild they + * are deleted after the upload. + */ + deleteSourcemapsAfterUpload?: boolean; }; /** diff --git a/packages/bundler-plugins/test/core/debug-id-upload.test.ts b/packages/bundler-plugins/test/core/debug-id-upload.test.ts index ddeeff073fc3..6765e10bea9a 100644 --- a/packages/bundler-plugins/test/core/debug-id-upload.test.ts +++ b/packages/bundler-plugins/test/core/debug-id-upload.test.ts @@ -4,8 +4,10 @@ import * as path from 'path'; import * as os from 'os'; import { addDebugIdToEmittedArtifacts, + prepareArtifactForUpload, prepareBundleForDebugIdUpload, stampDebugId, + writePreparedArtifact, } from '../../src/core/debug-id-upload'; import type { RewriteSourcesHook } from '../../src/core/types'; import type { Logger } from '../../src/core'; @@ -112,6 +114,117 @@ describe('prepareBundleForDebugIdUpload', () => { }); }); +describe('prepareArtifactForUpload', () => { + const debugId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; + const noopRewriteHook: RewriteSourcesHook = source => source; + const map = JSON.stringify({ version: 3, sources: ['../src/a.ts'], mappings: 'AAAA' }); + + it('stamps the debug ID into the bundle and the map and rewrites the sources', () => { + const rewriteHook: RewriteSourcesHook = (source, _map, context) => `${context?.mapDir}|${source}`; + + const result = prepareArtifactForUpload({ + code: `code();\n${debugIdSnippet(debugId)}`, + mapSource: map, + mapDir: '/out/assets', + rewriteSourcesHook: rewriteHook, + }); + + expect(result.ok).toBe(true); + if (!result.ok) { + return; + } + expect(result.artifact.debugId).toBe(debugId); + expect(result.artifact.bundleSource).toBe(`code();\n${debugIdSnippet(debugId)}\n//# debugId=${debugId}`); + expect(JSON.parse(result.artifact.sourceMapSource!)).toEqual({ + version: 3, + sources: ['/out/assets|../src/a.ts'], + mappings: 'AAAA', + debug_id: debugId, + debugId, + }); + }); + + it('replaces an existing spec comment', () => { + const result = prepareArtifactForUpload({ + code: `${debugIdSnippet(debugId)}\n//# debugId=00000000-0000-0000-0000-000000000000`, + mapSource: map, + mapDir: '/out', + rewriteSourcesHook: noopRewriteHook, + }); + + expect(result.ok && result.artifact.bundleSource).toBe(`${debugIdSnippet(debugId)}\n//# debugId=${debugId}`); + }); + + it('prepares a bundle with an inlined map without a separate map', () => { + const result = prepareArtifactForUpload({ + code: `${debugIdSnippet(debugId)}\n//# sourceMappingURL=data:application/json;base64,e30=`, + mapSource: undefined, + mapDir: '/out', + rewriteSourcesHook: noopRewriteHook, + }); + + expect(result.ok && result.artifact.sourceMapSource).toBeUndefined(); + }); + + it('reports a bundle without a debug ID', () => { + expect( + prepareArtifactForUpload({ + code: 'code();', + mapSource: map, + mapDir: '/out', + rewriteSourcesHook: noopRewriteHook, + }), + ).toEqual({ ok: false, reason: 'no-debug-id' }); + }); + + it('reports a bundle without any source map', () => { + expect( + prepareArtifactForUpload({ + code: debugIdSnippet(debugId), + mapSource: undefined, + mapDir: '/out', + rewriteSourcesHook: noopRewriteHook, + }), + ).toEqual({ ok: false, reason: 'no-source-map' }); + }); + + it('reports an invalid source map', () => { + expect( + prepareArtifactForUpload({ + code: debugIdSnippet(debugId), + mapSource: '{not json', + mapDir: '/out', + rewriteSourcesHook: noopRewriteHook, + }), + ).toEqual({ ok: false, reason: 'invalid-source-map' }); + }); +}); + +describe('writePreparedArtifact', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-test-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('writes the bundle and the map', async () => { + await writePreparedArtifact(tmpDir, 'abc-0', { debugId: 'abc', bundleSource: 'js', sourceMapSource: '{}' }); + + expect(fs.readFileSync(path.join(tmpDir, 'abc-0.js'), 'utf8')).toBe('js'); + expect(fs.readFileSync(path.join(tmpDir, 'abc-0.js.map'), 'utf8')).toBe('{}'); + }); + + it('writes only the bundle when there is no separate map', async () => { + await writePreparedArtifact(tmpDir, 'abc-0', { debugId: 'abc', bundleSource: 'js', sourceMapSource: undefined }); + + expect(fs.readdirSync(tmpDir)).toEqual(['abc-0.js']); + }); +}); + describe('stampDebugId', () => { const debugId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; const bundleSource = `"use strict";\n${debugIdSnippet(debugId)}\n//# sourceMappingURL=bundle.js.map`; diff --git a/packages/bundler-plugins/test/core/option-mappings.test.ts b/packages/bundler-plugins/test/core/option-mappings.test.ts index 341aad2f21e6..1c1bbbe42b82 100644 --- a/packages/bundler-plugins/test/core/option-mappings.test.ts +++ b/packages/bundler-plugins/test/core/option-mappings.test.ts @@ -351,6 +351,25 @@ describe('validateOptions', () => { expect(mockedLogger.error).not.toHaveBeenCalled(); }); + it('warns when `deleteSourcemapsAfterUpload` is set together with `sourcemaps.disable`', () => { + const options = normalizeUserOptions({ + sourcemaps: { disable: 'disable-upload', deleteSourcemapsAfterUpload: true }, + }); + + expect(validateOptions(options, mockedLogger)).toBe(true); + expect(mockedLogger.warn).toHaveBeenCalledWith( + expect.stringContaining('deleteSourcemapsAfterUpload'), + expect.any(String), + ); + }); + + it('does not warn when `deleteSourcemapsAfterUpload` is set and uploads are enabled', () => { + const options = normalizeUserOptions({ sourcemaps: { deleteSourcemapsAfterUpload: true } }); + + expect(validateOptions(options, mockedLogger)).toBe(true); + expect(mockedLogger.warn).not.toHaveBeenCalled(); + }); + it('should return `true` for options without special cases', () => { const options = { org: 'my-org', diff --git a/packages/bundler-plugins/test/core/sentry/telemetry.test.ts b/packages/bundler-plugins/test/core/sentry/telemetry.test.ts index 49cf78b7dffa..7de02ab79bc0 100644 --- a/packages/bundler-plugins/test/core/sentry/telemetry.test.ts +++ b/packages/bundler-plugins/test/core/sentry/telemetry.test.ts @@ -68,6 +68,19 @@ describe('addPluginOptionTagsToScope', () => { expect(mockedScope.setTag).toHaveBeenCalledWith('deploy-options', true); }); + it.each([ + [undefined, 'auto'], + [true, 'true'], + [false, 'false'], + ])('should set the delete-sourcemaps-after-upload tag for `%s` to `%s`', (option, tag) => { + setTelemetryDataOnScope( + normalizeUserOptions({ ...defaultOptions, sourcemaps: { deleteSourcemapsAfterUpload: option } }), + mockedScope as unknown as Scope, + 'rollup', + ); + expect(mockedScope.setTag).toHaveBeenCalledWith('delete-sourcemaps-after-upload', tag); + }); + it('should set errorHandler tag to `custom` if the errorHandler option is specified', () => { setTelemetryDataOnScope( // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/packages/bundler-plugins/test/core/sourcemap-settings.test.ts b/packages/bundler-plugins/test/core/sourcemap-settings.test.ts new file mode 100644 index 000000000000..033f9e26ab36 --- /dev/null +++ b/packages/bundler-plugins/test/core/sourcemap-settings.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest'; +import { normalizeUserOptions } from '../../src/core/options-mapping'; +import type { Options } from '../../src/core'; +import { decideSourceMapHandling } from '../../src/core/sourcemap-settings'; +import type { SourceMapKind } from '../../src/core/sourcemap-settings'; + +function options(sourcemaps?: Options['sourcemaps']) { + return normalizeUserOptions({ org: 'o', project: 'p', authToken: 't', release: { name: 'r' }, sourcemaps }); +} + +const NO_CHANGE = { enableHidden: false, deleteAfterUpload: false }; + +describe('decideSourceMapHandling', () => { + describe('with the option unset (auto)', () => { + it('enables hidden source maps and deletion when the bundler setting is unset', () => { + const decision = decideSourceMapHandling('unset', options(), true); + + expect(decision).toMatchObject({ enableHidden: true, deleteAfterUpload: true }); + expect(decision.log?.level).toBe('info'); + }); + + it.each(['hidden', 'visible'])('leaves a `%s` setting alone', kind => { + expect(decideSourceMapHandling(kind, options(), true)).toEqual(NO_CHANGE); + }); + + it('warns when source maps are turned off', () => { + const decision = decideSourceMapHandling('off', options(), true); + + expect(decision).toMatchObject(NO_CHANGE); + expect(decision.log?.level).toBe('warn'); + expect(decision.log?.message).toMatch(/turned off/); + }); + + it('warns when source maps are inlined', () => { + const decision = decideSourceMapHandling('inline', options(), true); + + expect(decision).toMatchObject(NO_CHANGE); + expect(decision.log?.level).toBe('warn'); + expect(decision.log?.message).toMatch(/inlined/); + }); + }); + + describe('with the option set to true', () => { + it('enables hidden source maps and deletion when the bundler setting is unset', () => { + expect(decideSourceMapHandling('unset', options({ deleteSourcemapsAfterUpload: true }), true)).toMatchObject({ + enableHidden: true, + deleteAfterUpload: true, + }); + }); + + it('deletes hidden source maps without changing the setting', () => { + expect(decideSourceMapHandling('hidden', options({ deleteSourcemapsAfterUpload: true }), true)).toEqual({ + enableHidden: false, + deleteAfterUpload: true, + }); + }); + + it('switches visible source maps to hidden and deletes them', () => { + const decision = decideSourceMapHandling('visible', options({ deleteSourcemapsAfterUpload: true }), true); + + expect(decision).toMatchObject({ enableHidden: true, deleteAfterUpload: true }); + expect(decision.log?.level).toBe('info'); + }); + + it.each(['off', 'inline'])('warns and does not delete for a `%s` setting', kind => { + const decision = decideSourceMapHandling(kind, options({ deleteSourcemapsAfterUpload: true }), true); + + expect(decision).toMatchObject(NO_CHANGE); + expect(decision.log?.level).toBe('warn'); + }); + }); + + describe('with the option set to false', () => { + it.each(['unset', 'off', 'visible', 'hidden', 'inline'])( + 'never changes or warns about a `%s` setting', + kind => { + expect(decideSourceMapHandling(kind, options({ deleteSourcemapsAfterUpload: false }), true)).toEqual(NO_CHANGE); + }, + ); + }); + + it.each(['unset', 'off', 'visible', 'hidden', 'inline'])( + 'does nothing for a `%s` setting when no upload can run', + kind => { + expect(decideSourceMapHandling(kind, options({ deleteSourcemapsAfterUpload: true }), false)).toEqual(NO_CHANGE); + }, + ); + + it.each([true, 'disable-upload'] as const)('does nothing when `sourcemaps.disable` is `%s`', disable => { + expect(decideSourceMapHandling('unset', options({ disable, deleteSourcemapsAfterUpload: true }), true)).toEqual( + NO_CHANGE, + ); + }); +}); diff --git a/packages/bundler-plugins/test/core/upload-staging.test.ts b/packages/bundler-plugins/test/core/upload-staging.test.ts new file mode 100644 index 000000000000..3a4577d90bc2 --- /dev/null +++ b/packages/bundler-plugins/test/core/upload-staging.test.ts @@ -0,0 +1,314 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { createSentryBuildPluginManager } from '../../src/core/build-plugin-manager'; + +const { mockSourcemapUpload } = vi.hoisted(() => ({ mockSourcemapUpload: vi.fn() })); + +vi.mock('sentry', () => ({ + createSentrySDK: () => ({ + release: { create: vi.fn(), finalize: vi.fn(), 'set-commits': vi.fn(), deploy: vi.fn() }, + sourcemap: { upload: mockSourcemapUpload, inject: vi.fn() }, + run: vi.fn(), + }), +})); + +vi.mock('../../src/core/sentry/telemetry', async () => ({ + ...(await vi.importActual('../../src/core/sentry/telemetry')), + safeFlushTelemetry: vi.fn(), +})); + +const DEBUG_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; +const OTHER_DEBUG_ID = '11111111-2222-3333-4444-555555555555'; + +function chunk(debugId: string): string { + return `console.log(1);\n;!function(){try{var e={},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}}();`; +} + +const MAP = JSON.stringify({ version: 3, sources: ['/abs/src/index.ts'], mappings: 'AAAA' }); + +function createManager(rewriteSources?: (source: string) => string) { + return createSentryBuildPluginManager( + { + authToken: 't', + org: 'o', + project: 'p', + release: { name: 'r', create: false, finalize: false }, + telemetry: false, + sourcemaps: { rewriteSources }, + }, + { buildTool: 'rollup', loggerPrefix: '[test]' }, + ); +} + +describe('createUploadStaging', () => { + let tmpDir: string; + + beforeEach(() => { + vi.clearAllMocks(); + mockSourcemapUpload.mockResolvedValue(undefined); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-staging-test-')); + }); + + afterEach(() => { + delete process.env['SENTRY_TEST_OVERRIDE_TEMP_DIR']; + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('writes the stamped chunk and map into the staging folder', async () => { + const manager = createManager(source => path.basename(source)); + const staging = await manager.createUploadStaging(); + + try { + const result = await staging.stage({ + code: chunk(DEBUG_ID), + mapSource: MAP, + mapDir: '/out', + fileName: 'index.js', + }); + + expect(result).toBe('staged'); + expect(staging.count).toBe(1); + expect(fs.readdirSync(staging.dir).sort()).toEqual([`${DEBUG_ID}-0.js`, `${DEBUG_ID}-0.js.map`]); + expect(fs.readFileSync(path.join(staging.dir, `${DEBUG_ID}-0.js`), 'utf8')).toContain(`//# debugId=${DEBUG_ID}`); + + const map = JSON.parse(fs.readFileSync(path.join(staging.dir, `${DEBUG_ID}-0.js.map`), 'utf8')); + expect(map).toMatchObject({ debug_id: DEBUG_ID, debugId: DEBUG_ID, sources: ['index.ts'] }); + } finally { + await staging.discard(); + } + }); + + it('passes the map directory to the rewriteSources hook', async () => { + const rewriteSources = vi.fn((source: string) => source); + const manager = createManager(rewriteSources); + const staging = await manager.createUploadStaging(); + + try { + await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out/assets', fileName: 'a.js' }); + } finally { + await staging.discard(); + } + + expect(rewriteSources).toHaveBeenCalledWith('/abs/src/index.ts', expect.any(Object), { mapDir: '/out/assets' }); + }); + + it('skips chunks without a debug ID or without a source map and does not count them', async () => { + const manager = createManager(); + const staging = await manager.createUploadStaging(); + + try { + expect(await staging.stage({ code: 'console.log(1)', mapSource: MAP, mapDir: '/out', fileName: 'a.js' })).toBe( + 'skipped', + ); + expect( + await staging.stage({ code: chunk(DEBUG_ID), mapSource: undefined, mapDir: '/out', fileName: 'b.js' }), + ).toBe('skipped'); + expect(staging.count).toBe(0); + expect(fs.readdirSync(staging.dir)).toEqual([]); + } finally { + await staging.discard(); + } + }); + + it('removes the folder on discard', async () => { + const manager = createManager(); + const staging = await manager.createUploadStaging(); + + await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + await staging.discard(); + await staging.discard(); + + expect(fs.existsSync(staging.dir)).toBe(false); + }); + + it('keeps the folder on discard when SENTRY_TEST_OVERRIDE_TEMP_DIR is set', async () => { + process.env['SENTRY_TEST_OVERRIDE_TEMP_DIR'] = tmpDir; + const manager = createManager(); + const staging = await manager.createUploadStaging(); + + expect(staging.dir).toBe(tmpDir); + await staging.discard(); + + expect(fs.existsSync(tmpDir)).toBe(true); + }); + + describe('uploadSourcemaps with a staging folder', () => { + it('uploads the staging folder and removes it afterwards', async () => { + const manager = createManager(); + const staging = await manager.createUploadStaging(); + await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + + const result = await manager.uploadSourcemaps([], { staging }); + + expect(result).toEqual({ uploaded: true }); + expect(mockSourcemapUpload).toHaveBeenCalledWith(expect.objectContaining({ directory: staging.dir })); + expect(fs.existsSync(staging.dir)).toBe(false); + }); + + it('numbers artifacts prepared from disk after the staged ones', async () => { + const outDir = path.join(tmpDir, 'out'); + fs.mkdirSync(outDir); + fs.writeFileSync(path.join(outDir, 'disk.js'), `${chunk(OTHER_DEBUG_ID)}\n//# sourceMappingURL=disk.js.map`); + fs.writeFileSync(path.join(outDir, 'disk.js.map'), MAP); + + const manager = createManager(); + const staging = await manager.createUploadStaging(); + await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: outDir, fileName: 'staged.js' }); + + let uploadedFiles: string[] = []; + mockSourcemapUpload.mockImplementation(async ({ directory }: { directory: string }) => { + uploadedFiles = fs.readdirSync(directory).sort(); + }); + + await manager.uploadSourcemaps([path.join(outDir, 'disk.js')], { staging }); + + expect(uploadedFiles).toEqual([ + `${OTHER_DEBUG_ID}-1.js`, + `${OTHER_DEBUG_ID}-1.js.map`, + `${DEBUG_ID}-0.js`, + `${DEBUG_ID}-0.js.map`, + ]); + }); + + it('does not warn about missing sources when artifacts were staged', async () => { + const manager = createManager(); + const warnSpy = vi.spyOn(manager.logger, 'warn'); + const staging = await manager.createUploadStaging(); + await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + + await manager.uploadSourcemaps([], { staging }); + + expect(warnSpy).not.toHaveBeenCalled(); + expect(mockSourcemapUpload).toHaveBeenCalledTimes(1); + }); + + it('discards an empty staging folder and warns when nothing matched on disk', async () => { + const manager = createManager(); + const warnSpy = vi.spyOn(manager.logger, 'warn'); + const staging = await manager.createUploadStaging(); + + const result = await manager.uploadSourcemaps([], { staging }); + + expect(result).toEqual({ uploaded: false }); + expect(warnSpy).toHaveBeenCalledWith(expect.stringMatching(/matching sources/)); + expect(mockSourcemapUpload).not.toHaveBeenCalled(); + expect(fs.existsSync(staging.dir)).toBe(false); + }); + + it('discards the staging folder when the upload cannot run', async () => { + const manager = createSentryBuildPluginManager( + { org: 'o', project: 'p', release: { name: 'r' }, telemetry: false }, + { buildTool: 'rollup', loggerPrefix: '[test]' }, + ); + const staging = await manager.createUploadStaging(); + await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + + const result = await manager.uploadSourcemaps([], { staging }); + + expect(result).toEqual({ uploaded: false }); + expect(mockSourcemapUpload).not.toHaveBeenCalled(); + expect(fs.existsSync(staging.dir)).toBe(false); + }); + + it('keeps the staging folder and logs its path when the upload fails', async () => { + mockSourcemapUpload.mockRejectedValue(new Error('upload failed')); + const manager = createManager(); + const errorSpy = vi.spyOn(manager.logger, 'error'); + const staging = await manager.createUploadStaging(); + await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + + try { + const result = await manager.uploadSourcemaps([], { staging }); + + expect(result).toEqual({ uploaded: false }); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining(`sentry sourcemap upload ${staging.dir}`)); + expect(fs.existsSync(staging.dir)).toBe(true); + } finally { + await staging.discard(); + } + }); + + it('uploads the staging folder next to the direct paths when prepareArtifacts is false', async () => { + const manager = createManager(); + const staging = await manager.createUploadStaging(); + await staging.stage({ code: chunk(DEBUG_ID), mapSource: MAP, mapDir: '/out', fileName: 'a.js' }); + + const result = await manager.uploadSourcemaps(['.next/server'], { staging, prepareArtifacts: false }); + + expect(result).toEqual({ uploaded: true }); + expect(mockSourcemapUpload).toHaveBeenCalledTimes(2); + expect(mockSourcemapUpload).toHaveBeenCalledWith(expect.objectContaining({ directory: '.next/server' })); + expect(mockSourcemapUpload).toHaveBeenCalledWith(expect.objectContaining({ directory: staging.dir })); + expect(fs.existsSync(staging.dir)).toBe(false); + }); + + it('skips an empty staging folder when prepareArtifacts is false', async () => { + const manager = createManager(); + const staging = await manager.createUploadStaging(); + + await manager.uploadSourcemaps(['.next/server'], { staging, prepareArtifacts: false }); + + expect(mockSourcemapUpload).toHaveBeenCalledTimes(1); + expect(mockSourcemapUpload).toHaveBeenCalledWith(expect.objectContaining({ directory: '.next/server' })); + expect(fs.existsSync(staging.dir)).toBe(false); + }); + }); +}); + +describe('sourceMaps', () => { + afterEach(() => { + delete process.env['NODE_ENV']; + }); + + it('reports that an upload is possible with credentials', () => { + expect(createManager().sourceMaps.uploadPossible).toBe(true); + }); + + it.each([ + ['no auth token', { org: 'o', project: 'p' }], + ['no project', { org: 'o', authToken: 't' }], + ['upload disabled', { org: 'o', project: 'p', authToken: 't', sourcemaps: { disable: true as const } }], + ['an empty assets list', { org: 'o', project: 'p', authToken: 't', sourcemaps: { assets: [] } }], + ])('reports that no upload is possible with %s', (_, options) => { + const manager = createSentryBuildPluginManager( + { ...options, release: { name: 'r' }, telemetry: false }, + { buildTool: 'rollup', loggerPrefix: '[test]' }, + ); + + expect(manager.sourceMaps.uploadPossible).toBe(false); + expect(manager.sourceMaps.decide('unset')).toEqual({ enableHidden: false, deleteAfterUpload: false }); + }); + + it('reports that no upload is possible in development mode', () => { + process.env['NODE_ENV'] = 'development'; + + expect(createManager().sourceMaps.uploadPossible).toBe(false); + }); + + it('logs the decision message once', () => { + const manager = createManager(); + const infoSpy = vi.spyOn(manager.logger, 'info'); + const { decide } = manager.sourceMaps; + + expect(decide('unset')).toMatchObject({ enableHidden: true, deleteAfterUpload: true }); + expect(decide('unset')).toMatchObject({ enableHidden: true, deleteAfterUpload: true }); + + expect(infoSpy).toHaveBeenCalledTimes(1); + }); + + it('is inert on a disabled plugin', async () => { + const manager = createSentryBuildPluginManager( + { disable: true, org: 'o', project: 'p', authToken: 't' }, + { buildTool: 'rollup', loggerPrefix: '[test]' }, + ); + + expect(manager.sourceMaps.uploadPossible).toBe(false); + expect(manager.sourceMaps.decide('unset')).toEqual({ enableHidden: false, deleteAfterUpload: false }); + + const staging = await manager.createUploadStaging(); + expect(await staging.stage({ code: '', mapSource: undefined, mapDir: '', fileName: 'a.js' })).toBe('skipped'); + expect(await manager.uploadSourcemaps([], { staging })).toEqual({ uploaded: false }); + }); +}); diff --git a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts index 36a8967c8105..1d345c633c9c 100644 --- a/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts +++ b/packages/core/src/build-time-plugins/buildTimeOptionsBase.ts @@ -248,6 +248,15 @@ interface SourceMapsOptions { */ filesToDeleteAfterUpload?: string | Array; + /** + * Whether source maps should be removed from the build output after they are uploaded to Sentry. + * + * When unset, source maps are removed only if the plugin turned source map generation on itself, that is, when + * the bundler's source map setting was not set. Set to `true` to also remove source maps you enabled yourself, or + * to `false` to keep the plugin from changing or warning about your source map setting. + */ + deleteSourcemapsAfterUpload?: boolean; + /** * Hook to rewrite the `sources` field inside the source map before being uploaded to Sentry. Does not modify the actual source map. *