1+ import SentryCli from "@sentry/cli" ;
2+ import { makeMain } from "@sentry/node" ;
3+ import { Span , Transaction } from "@sentry/types" ;
4+ import fs from "fs" ;
5+ import { glob } from "glob" ;
6+ import MagicString from "magic-string" ;
7+ import os from "os" ;
8+ import path from "path" ;
19import { createUnplugin , UnpluginOptions } from "unplugin" ;
2- import { Options , BuildContext } from "./types" ;
10+ import { promisify } from "util" ;
11+ import { prepareBundleForDebugIdUpload } from "./debug-id" ;
12+ import { NormalizedOptions , normalizeUserOptions , validateOptions } from "./options-mapping" ;
13+ import { getSentryCli } from "./sentry/cli" ;
14+ import { createLogger , Logger } from "./sentry/logger" ;
315import {
4- createNewRelease ,
5- cleanArtifacts ,
616 addDeploy ,
17+ cleanArtifacts ,
18+ createNewRelease ,
719 finalizeRelease ,
820 setCommits ,
9- uploadSourceMaps ,
1021 uploadDebugIdSourcemaps ,
22+ uploadSourceMaps ,
1123} from "./sentry/releasePipeline" ;
12- import SentryCli from "@sentry/cli" ;
1324import {
1425 addPluginOptionInformationToHub ,
1526 addSpanToTransaction ,
1627 makeSentryClient ,
1728 shouldSendTelemetry ,
1829} from "./sentry/telemetry" ;
19- import { Span , Transaction } from "@sentry/types" ;
20- import { createLogger , Logger } from "./sentry/logger" ;
21- import { NormalizedOptions , normalizeUserOptions , validateOptions } from "./options-mapping" ;
22- import { getSentryCli } from "./sentry/cli" ;
23- import { makeMain } from "@sentry/node" ;
24- import os from "os" ;
25- import path from "path" ;
26- import fs from "fs" ;
27- import { createRequire } from "module" ;
28- import { promisify } from "util" ;
30+ import { BuildContext , Options } from "./types" ;
2931import {
3032 determineReleaseName ,
3133 generateGlobalInjectorCode ,
@@ -34,22 +36,10 @@ import {
3436 parseMajorVersion ,
3537 stringToUUID ,
3638} from "./utils" ;
37- import { glob } from "glob" ;
38- import { injectDebugIdSnippetIntoChunk , prepareBundleForDebugIdUpload } from "./debug-id" ;
39- import webpackSources from "webpack-sources" ;
40- import type { sources } from "webpack" ;
41- import type { Plugin } from "rollup" ;
42- import MagicString from "magic-string" ;
43-
44- // Use createRequire because esm doesn't like built-in require.resolve
45- const require = createRequire ( import . meta. url ) ;
46-
47- const esbuildDebugIdInjectionFilePath = require . resolve (
48- "@sentry/bundler-plugin-core/sentry-esbuild-debugid-injection-file"
49- ) ;
5039
5140interface SentryUnpluginFactoryOptions {
5241 releaseInjectionPlugin : ( injectionCode : string ) => UnpluginOptions ;
42+ debugIdInjectionPlugin : ( ) => UnpluginOptions ;
5343}
5444/**
5545 * The sentry bundler plugin concerns itself with two things:
@@ -78,7 +68,10 @@ interface SentryUnpluginFactoryOptions {
7868 *
7969 * This release creation pipeline relies on Sentry CLI to execute the different steps.
8070 */
81- export function sentryUnpluginFactory ( { releaseInjectionPlugin } : SentryUnpluginFactoryOptions ) {
71+ export function sentryUnpluginFactory ( {
72+ releaseInjectionPlugin,
73+ debugIdInjectionPlugin,
74+ } : SentryUnpluginFactoryOptions ) {
8275 return createUnplugin < Options , true > ( ( userOptions , unpluginMetaContext ) => {
8376 const options = normalizeUserOptions ( userOptions ) ;
8477
@@ -279,108 +272,8 @@ export function sentryUnpluginFactory({ releaseInjectionPlugin }: SentryUnplugin
279272 level : "info" ,
280273 } ) ;
281274 } ,
282- rollup : {
283- renderChunk ( code , chunk ) {
284- if (
285- options . sourcemaps ?. assets &&
286- [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) => chunk . fileName . endsWith ( ending ) ) // chunks could be any file (html, md, ...)
287- ) {
288- return injectDebugIdSnippetIntoChunk ( code ) ;
289- } else {
290- return null ; // returning null means not modifying the chunk at all
291- }
292- } ,
293- } ,
294- vite : {
295- renderChunk ( code , chunk ) {
296- if (
297- options . sourcemaps ?. assets &&
298- [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) => chunk . fileName . endsWith ( ending ) ) // chunks could be any file (html, md, ...)
299- ) {
300- return injectDebugIdSnippetIntoChunk ( code ) ;
301- } else {
302- return null ; // returning null means not modifying the chunk at all
303- }
304- } ,
305- } ,
306- webpack ( compiler ) {
307- if ( options . sourcemaps ?. assets ) {
308- // Cache inspired by https://github.com/webpack/webpack/pull/15454
309- const cache = new WeakMap < sources . Source , sources . Source > ( ) ;
310-
311- compiler . hooks . compilation . tap ( "sentry-plugin" , ( compilation ) => {
312- compilation . hooks . optimizeChunkAssets . tap ( "sentry-plugin" , ( chunks ) => {
313- chunks . forEach ( ( chunk ) => {
314- const fileNames = chunk . files ;
315- fileNames . forEach ( ( fileName ) => {
316- const source = compilation . assets [ fileName ] ;
317-
318- if ( ! source ) {
319- logger . warn (
320- "Unable to access compilation assets. If you see this warning, it is likely a bug in the Sentry webpack plugin. Feel free to open an issue at https://github.com/getsentry/sentry-javascript-bundler-plugins with reproduction steps."
321- ) ;
322- return ;
323- }
324-
325- compilation . updateAsset ( fileName , ( oldSource ) => {
326- const cached = cache . get ( oldSource ) ;
327- if ( cached ) {
328- return cached ;
329- }
330-
331- const originalCode = source . source ( ) . toString ( ) ;
332-
333- // The source map type is very annoying :(
334- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
335- const originalSourceMap = source . map ( ) as any ;
336-
337- const { code : newCode , map : newSourceMap } = injectDebugIdSnippetIntoChunk (
338- originalCode ,
339- fileName
340- ) ;
341-
342- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
343- newSourceMap . sources = originalSourceMap . sources as string [ ] ;
344-
345- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
346- newSourceMap . sourcesContent = originalSourceMap . sourcesContent as string [ ] ;
347-
348- const newSource = new webpackSources . SourceMapSource (
349- newCode ,
350- fileName ,
351- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
352- originalSourceMap ,
353- originalCode ,
354- newSourceMap ,
355- false
356- ) as sources . Source ;
357-
358- cache . set ( oldSource , newSource ) ;
359-
360- return newSource ;
361- } ) ;
362- } ) ;
363- } ) ;
364- } ) ;
365- } ) ;
366- }
367- } ,
368275 } ) ;
369276
370- if ( unpluginMetaContext . framework === "esbuild" ) {
371- if ( options . sourcemaps ?. assets ) {
372- plugins . push ( {
373- name : "sentry-esbuild-debug-id-plugin" ,
374- esbuild : {
375- setup ( { initialOptions } ) {
376- initialOptions . inject = initialOptions . inject || [ ] ;
377- initialOptions . inject . push ( esbuildDebugIdInjectionFilePath ) ;
378- } ,
379- } ,
380- } ) ;
381- }
382- }
383-
384277 if ( options . injectRelease && releaseName ) {
385278 const injectionCode = generateGlobalInjectorCode ( {
386279 release : releaseName ,
@@ -393,6 +286,10 @@ export function sentryUnpluginFactory({ releaseInjectionPlugin }: SentryUnplugin
393286 plugins . push ( releaseInjectionPlugin ( injectionCode ) ) ;
394287 }
395288
289+ if ( options . sourcemaps ?. assets ) {
290+ plugins . push ( debugIdInjectionPlugin ( ) ) ;
291+ }
292+
396293 return plugins ;
397294 } ) ;
398295}
@@ -442,13 +339,11 @@ export function sentryCliBinaryExists(): boolean {
442339 return fs . existsSync ( SentryCli . getPath ( ) ) ;
443340}
444341
445- export function createRollupReleaseInjectionHooks (
446- injectionCode : string
447- ) : Pick < Plugin , "resolveId" | "load" | "transform" > {
342+ export function createRollupReleaseInjectionHooks ( injectionCode : string ) {
448343 const virtualReleaseInjectionFileId = "\0sentry-release-injection-file" ;
449344
450345 return {
451- resolveId ( id ) {
346+ resolveId ( id : string ) {
452347 if ( id === virtualReleaseInjectionFileId ) {
453348 return {
454349 id : virtualReleaseInjectionFileId ,
@@ -460,15 +355,15 @@ export function createRollupReleaseInjectionHooks(
460355 }
461356 } ,
462357
463- load ( id ) {
358+ load ( id : string ) {
464359 if ( id === virtualReleaseInjectionFileId ) {
465360 return injectionCode ;
466361 } else {
467362 return null ;
468363 }
469364 } ,
470365
471- transform ( code , id ) {
366+ transform ( code : string , id : string ) {
472367 if ( id === virtualReleaseInjectionFileId ) {
473368 return null ;
474369 }
@@ -495,4 +390,46 @@ export function createRollupReleaseInjectionHooks(
495390 } ;
496391}
497392
393+ export function createRollupDebugIdInjectionHooks ( ) {
394+ return {
395+ renderChunk ( code : string , chunk : { fileName : string } ) {
396+ if (
397+ [ ".js" , ".mjs" , ".cjs" ] . some ( ( ending ) => chunk . fileName . endsWith ( ending ) ) // chunks could be any file (html, md, ...)
398+ ) {
399+ const debugId = stringToUUID ( code ) ; // generate a deterministic debug ID
400+ const codeToInject = getDebugIdSnippet ( debugId ) ;
401+
402+ const ms = new MagicString ( code , { filename : chunk . fileName } ) ;
403+
404+ // We need to be careful not to inject the snippet before any `"use strict";`s.
405+ // As an additional complication `"use strict";`s may come after any number of comments.
406+ const commentUseStrictRegex =
407+ // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files.
408+ / ^ (?: \s * | \/ \* (?: .| \r | \n ) * \* \/ | \/ \/ .* [ \n \r ] ) * (?: " [ ^ " ] * " ; | ' [ ^ ' ] * ' ; ) ? / ;
409+
410+ if ( code . match ( commentUseStrictRegex ) ?. [ 0 ] ) {
411+ // Add injected code after any comments or "use strict" at the beginning of the bundle.
412+ ms . replace ( commentUseStrictRegex , ( match ) => `${ match } ${ codeToInject } ` ) ;
413+ } else {
414+ // ms.replace() doesn't work when there is an empty string match (which happens if
415+ // there is neither, a comment, nor a "use strict" at the top of the chunk) so we
416+ // need this special case here.
417+ ms . prepend ( codeToInject ) ;
418+ }
419+
420+ return {
421+ code : ms . toString ( ) ,
422+ map : ms . generateMap ( { file : chunk . fileName } ) ,
423+ } ;
424+ } else {
425+ return null ; // returning null means not modifying the chunk at all
426+ }
427+ } ,
428+ } ;
429+ }
430+
431+ export function getDebugIdSnippet ( debugId : string ) : string {
432+ return `;!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=(new Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${ debugId } ",e._sentryDebugIdIdentifier="sentry-dbid-${ debugId } ")}catch(e){}}();` ;
433+ }
434+
498435export type { Options } from "./types" ;
0 commit comments