@@ -77,7 +77,10 @@ export type SentryBuildPluginManager = {
7777 /**
7878 * Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded
7979 */
80- uploadSourcemaps ( buildArtifactPaths : string [ ] ) : Promise < void > ;
80+ uploadSourcemaps (
81+ buildArtifactPaths : string [ ] ,
82+ opts ?: { prepareArtifacts ?: boolean }
83+ ) : Promise < void > ;
8184
8285 /**
8386 * Will delete artifacts based on the passed `sourcemaps.filesToDeleteAfterUpload` option.
@@ -546,9 +549,16 @@ export function createSentryBuildPluginManager(
546549 } ,
547550
548551 /**
549- * Uploads sourcemaps using the "Debug ID" method. This function takes a list of build artifact paths that will be uploaded
552+ * Uploads sourcemaps using the "Debug ID" method.
553+ *
554+ * By default, this prepares bundles in a temporary folder before uploading. You can opt into an
555+ * in-place, direct upload path by setting `prepareArtifacts` to `false`. If `prepareArtifacts` is set to
556+ * `false`, no preparation (e.g. adding `//# debugId=...` and writing adjusted source maps) is performed and no temp folder is used.
557+ *
558+ * @param buildArtifactPaths - The paths of the build artifacts to upload
559+ * @param opts - Optional flags to control temp folder usage and preparation
550560 */
551- async uploadSourcemaps ( buildArtifactPaths : string [ ] ) {
561+ async uploadSourcemaps ( buildArtifactPaths : string [ ] , opts ?: { prepareArtifacts ?: boolean } ) {
552562 if ( ! canUploadSourceMaps ( options , logger , isDevMode ) ) {
553563 return ;
554564 }
@@ -557,26 +567,16 @@ export function createSentryBuildPluginManager(
557567 // This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions.
558568 { name : "debug-id-sourcemap-upload" , scope : sentryScope , forceTransaction : true } ,
559569 async ( ) => {
570+ // If we're not using a temp folder, we must not prepare artifacts in-place (to avoid mutating user files)
571+ const shouldPrepare = opts ?. prepareArtifacts ?? true ;
572+
560573 let folderToCleanUp : string | undefined ;
561574
562575 // 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`)
563576 // Therefore we need to actually register the execution of this hook as dependency on the sourcemap files.
564577 const freeUploadDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts ( ) ;
565578
566579 try {
567- const tmpUploadFolder = await startSpan (
568- { name : "mkdtemp" , scope : sentryScope } ,
569- async ( ) => {
570- return (
571- process . env ?. [ "SENTRY_TEST_OVERRIDE_TEMP_DIR" ] ||
572- ( await fs . promises . mkdtemp (
573- path . join ( os . tmpdir ( ) , "sentry-bundler-plugin-upload-" )
574- ) )
575- ) ;
576- }
577- ) ;
578-
579- folderToCleanUp = tmpUploadFolder ;
580580 const assets = options . sourcemaps ?. assets ;
581581
582582 let globAssets : string | string [ ] ;
@@ -594,14 +594,17 @@ export function createSentryBuildPluginManager(
594594 async ( ) =>
595595 await glob ( globAssets , {
596596 absolute : true ,
597- nodir : true ,
597+ // If we do not use a temp folder, we allow directories and files; CLI will traverse as needed when given paths.
598+ nodir : shouldPrepare ,
598599 ignore : options . sourcemaps ?. ignore ,
599600 } )
600601 ) ;
601602
602- const debugIdChunkFilePaths = globResult . filter ( ( debugIdChunkFilePath ) => {
603- return ! ! stripQueryAndHashFromPath ( debugIdChunkFilePath ) . match ( / \. ( j s | m j s | c j s ) $ / ) ;
604- } ) ;
603+ const debugIdChunkFilePaths = shouldPrepare
604+ ? globResult . filter ( ( debugIdChunkFilePath ) => {
605+ return ! ! stripQueryAndHashFromPath ( debugIdChunkFilePath ) . match ( / \. ( j s | m j s | c j s ) $ / ) ;
606+ } )
607+ : globResult ;
605608
606609 // The order of the files output by glob() is not deterministic
607610 // Ensure order within the files so that {debug-id}-{chunkIndex} coupling is consistent
@@ -616,75 +619,101 @@ export function createSentryBuildPluginManager(
616619 "Didn't find any matching sources for debug ID upload. Please check the `sourcemaps.assets` option."
617620 ) ;
618621 } else {
619- const numUploadedFiles = await startSpan (
620- { name : "prepare-bundles" , scope : sentryScope } ,
621- async ( prepBundlesSpan ) => {
622- // Preparing the bundles can be a lot of work and doing it all at once has the potential of nuking the heap so
623- // instead we do it with a maximum of 16 concurrent workers
624- const preparationTasks = debugIdChunkFilePaths . map (
625- ( chunkFilePath , chunkIndex ) => async ( ) => {
626- await prepareBundleForDebugIdUpload (
627- chunkFilePath ,
628- tmpUploadFolder ,
629- chunkIndex ,
630- logger ,
631- options . sourcemaps ?. rewriteSources ?? defaultRewriteSourcesHook ,
632- options . sourcemaps ?. resolveSourceMap
633- ) ;
634- }
635- ) ;
636- const workers : Promise < void > [ ] = [ ] ;
637- const worker = async ( ) : Promise < void > => {
638- while ( preparationTasks . length > 0 ) {
639- const task = preparationTasks . shift ( ) ;
640- if ( task ) {
641- await task ( ) ;
622+ if ( ! shouldPrepare ) {
623+ // Direct CLI upload from existing artifact paths (no preparation or temp copies)
624+ await startSpan ( { name : "upload" , scope : sentryScope } , async ( ) => {
625+ const cliInstance = createCliInstance ( options ) ;
626+ await cliInstance . releases . uploadSourceMaps ( options . release . name ?? "undefined" , {
627+ include : [
628+ {
629+ paths : debugIdChunkFilePaths ,
630+ rewrite : false ,
631+ dist : options . release . dist ,
632+ } ,
633+ ] ,
634+ live : "rejectOnError" ,
635+ } ) ;
636+ } ) ;
637+
638+ logger . info ( "Successfully uploaded source maps to Sentry" ) ;
639+ } else {
640+ const tmpUploadFolder = await startSpan (
641+ { name : "mkdtemp" , scope : sentryScope } ,
642+ async ( ) => {
643+ return (
644+ process . env ?. [ "SENTRY_TEST_OVERRIDE_TEMP_DIR" ] ||
645+ ( await fs . promises . mkdtemp (
646+ path . join ( os . tmpdir ( ) , "sentry-bundler-plugin-upload-" )
647+ ) )
648+ ) ;
649+ }
650+ ) ;
651+ folderToCleanUp = tmpUploadFolder ;
652+
653+ // Prepare into temp folder, then upload
654+ await startSpan (
655+ { name : "prepare-bundles" , scope : sentryScope } ,
656+ async ( prepBundlesSpan ) => {
657+ // Preparing the bundles can be a lot of work and doing it all at once has the potential of nuking the heap so
658+ // instead we do it with a maximum of 16 concurrent workers
659+ const preparationTasks = debugIdChunkFilePaths . map (
660+ ( chunkFilePath , chunkIndex ) => async ( ) => {
661+ await prepareBundleForDebugIdUpload (
662+ chunkFilePath ,
663+ tmpUploadFolder ,
664+ chunkIndex ,
665+ logger ,
666+ options . sourcemaps ?. rewriteSources ?? defaultRewriteSourcesHook ,
667+ options . sourcemaps ?. resolveSourceMap
668+ ) ;
669+ }
670+ ) ;
671+ const workers : Promise < void > [ ] = [ ] ;
672+ const worker = async ( ) : Promise < void > => {
673+ while ( preparationTasks . length > 0 ) {
674+ const task = preparationTasks . shift ( ) ;
675+ if ( task ) {
676+ await task ( ) ;
677+ }
642678 }
679+ } ;
680+ for ( let workerIndex = 0 ; workerIndex < 16 ; workerIndex ++ ) {
681+ workers . push ( worker ( ) ) ;
643682 }
644- } ;
645- for ( let workerIndex = 0 ; workerIndex < 16 ; workerIndex ++ ) {
646- workers . push ( worker ( ) ) ;
647- }
648683
649- await Promise . all ( workers ) ;
684+ await Promise . all ( workers ) ;
650685
651- const files = await fs . promises . readdir ( tmpUploadFolder ) ;
652- const stats = files . map ( ( file ) =>
653- fs . promises . stat ( path . join ( tmpUploadFolder , file ) )
654- ) ;
655- const uploadSize = ( await Promise . all ( stats ) ) . reduce (
656- ( accumulator , { size } ) => accumulator + size ,
657- 0
658- ) ;
659-
660- setMeasurement ( "files" , files . length , "none" , prepBundlesSpan ) ;
661- setMeasurement ( "upload_size" , uploadSize , "byte" , prepBundlesSpan ) ;
662-
663- await startSpan ( { name : "upload" , scope : sentryScope } , async ( ) => {
664- const cliInstance = createCliInstance ( options ) ;
665-
666- await cliInstance . releases . uploadSourceMaps (
667- options . release . name ?? "undefined" , // unfortunately this needs a value for now but it will not matter since debug IDs overpower releases anyhow
668- {
669- include : [
670- {
671- paths : [ tmpUploadFolder ] ,
672- rewrite : false ,
673- dist : options . release . dist ,
674- } ,
675- ] ,
676- // We want this promise to throw if the sourcemaps fail to upload so that we know about it.
677- // see: https://github.com/getsentry/sentry-cli/pull/2605
678- live : "rejectOnError" ,
679- }
686+ const files = await fs . promises . readdir ( tmpUploadFolder ) ;
687+ const stats = files . map ( ( file ) =>
688+ fs . promises . stat ( path . join ( tmpUploadFolder , file ) )
689+ ) ;
690+ const uploadSize = ( await Promise . all ( stats ) ) . reduce (
691+ ( accumulator , { size } ) => accumulator + size ,
692+ 0
680693 ) ;
681- } ) ;
682694
683- return files . length ;
684- }
685- ) ;
695+ setMeasurement ( "files" , files . length , "none" , prepBundlesSpan ) ;
696+ setMeasurement ( "upload_size" , uploadSize , "byte" , prepBundlesSpan ) ;
697+
698+ await startSpan ( { name : "upload" , scope : sentryScope } , async ( ) => {
699+ const cliInstance = createCliInstance ( options ) ;
700+ await cliInstance . releases . uploadSourceMaps (
701+ options . release . name ?? "undefined" ,
702+ {
703+ include : [
704+ {
705+ paths : [ tmpUploadFolder ] ,
706+ rewrite : false ,
707+ dist : options . release . dist ,
708+ } ,
709+ ] ,
710+ live : "rejectOnError" ,
711+ }
712+ ) ;
713+ } ) ;
714+ }
715+ ) ;
686716
687- if ( numUploadedFiles > 0 ) {
688717 logger . info ( "Successfully uploaded source maps to Sentry" ) ;
689718 }
690719 }
0 commit comments