Skip to content

Commit a38ecaa

Browse files
fix(sourcemaps): Fix double association bug (#2764)
Fixes a bug where the same sourcemap can be associated with mulitple source files. ### Change description We solve this problem by keeping track of the sourcemaps we have already associated with a source file. If we guess a sourcemap which has already been associated, we do not associate the given source file with any sourcemap. Also, we iterate first through all the sources which explicitly reference the sourcemap – i.e., for which, we don't have to guess the sourcemap. These are associated first. As they are explicitly linked to a sourcemap, we intentionally **do not prevent duplicates** here. On the second iteration, we guess the sourcemap reference for all remaining sources. **We enforce the no duplicates rule on the second iteration**. Sources cannot be associated with a sourcemap that was explicitly referenced by another source on the first iteration. If multiple sources guess the same sourcemap, and that sourcemap is not explicitly associated with any source, then none of the sources will be associated with that sourcemap (as it is unclear how that sourcemap should be associated). In all cases, we warn the users that we failed to associate the sources properly. ### Issues Fixes #2445 Fixes [CLI-5](https://linear.app/getsentry/issue/CLI-5/ensure-collect-sourcemap-references-does-not-associate-the-same)
1 parent 9b8b012 commit a38ecaa

26 files changed

+293
-43
lines changed

src/utils/file_upload.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ impl<'a> TryFrom<&'a UploadContext<'_>> for LegacyUploadContext<'a> {
211211
}
212212
}
213213

214-
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
214+
#[derive(Eq, PartialEq, Debug, Copy, Clone, Hash)]
215215
pub enum LogLevel {
216216
Warning,
217217
Error,
@@ -226,7 +226,7 @@ impl fmt::Display for LogLevel {
226226
}
227227
}
228228

229-
#[derive(Clone, Debug)]
229+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
230230
pub struct SourceFile {
231231
pub url: String,
232232
pub path: PathBuf,

src/utils/sourcemaps.rs

Lines changed: 194 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use crate::utils::file_search::ReleaseFileMatch;
2525
use crate::utils::file_upload::{
2626
initialize_legacy_release_upload, FileUpload, SourceFile, SourceFiles, UploadContext,
2727
};
28+
use crate::utils::fs;
2829
use crate::utils::logging::is_quiet_mode;
2930
use crate::utils::progress::ProgressBar;
3031
use crate::utils::sourcemaps::inject::{InjectReportBuilder, ReportItem};
@@ -172,6 +173,7 @@ fn guess_sourcemap_reference(
172173
/// and original url with which the file was added to the processor.
173174
/// This enable us to look up the source map file based on the original url.
174175
/// Which can be used for example for debug id referencing.
176+
#[derive(Eq, Hash, PartialEq, Debug)]
175177
pub struct SourceMapReference {
176178
url: String,
177179
original_url: Option<String>,
@@ -289,51 +291,44 @@ impl SourceMapProcessor {
289291
fn collect_sourcemap_references(&mut self) {
290292
let sourcemaps = self
291293
.sources
292-
.iter()
293-
.map(|x| x.1)
294+
.values()
294295
.filter(|x| x.ty == SourceFileType::SourceMap)
295296
.map(|x| x.url.clone())
296-
.collect();
297-
298-
for source in self.sources.values_mut() {
299-
// Skip everything but minified JS files.
300-
if source.ty != SourceFileType::MinifiedSource {
301-
continue;
302-
}
303-
304-
if self.sourcemap_references.contains_key(&source.url) {
305-
continue;
306-
}
307-
308-
let Ok(contents) = std::str::from_utf8(&source.contents) else {
309-
continue;
310-
};
297+
.collect::<HashSet<_>>();
311298

312-
// If this is a full external URL, the code below is going to attempt
313-
// to "normalize" it with the source path, resulting in a bogus path
314-
// like "path/to/source/dir/https://some-static-host.example.com/path/to/foo.js.map"
315-
// that can't be resolved to a source map file.
316-
// Instead, we pretend we failed to discover the location, and we fall back to
317-
// guessing the source map location based on the source location.
318-
let location =
319-
discover_sourcemaps_location(contents).filter(|loc| !is_remote_sourcemap(loc));
320-
let sourcemap_reference = match location {
321-
Some(url) => SourceMapReference::from_url(url.to_owned()),
322-
None => match guess_sourcemap_reference(&sourcemaps, &source.url) {
323-
Ok(target) => target,
324-
Err(err) => {
325-
source.warn(format!(
326-
"could not determine a source map reference ({err})"
327-
));
328-
self.sourcemap_references.insert(source.url.clone(), None);
329-
continue;
330-
}
331-
},
332-
};
299+
let (sources_with_location, sources_without_location) = self
300+
.sources
301+
.values_mut()
302+
.filter(|source| {
303+
source.ty == SourceFileType::MinifiedSource
304+
&& !self.sourcemap_references.contains_key(&source.url)
305+
})
306+
.collect_sourcemap_locations();
307+
308+
// First pass: if location discovered, add to sourcemap_references. Also, keep track of
309+
// the sourcemaps we associate, and the source file we associate them with.
310+
let explicitly_associated_sourcemaps = sources_with_location
311+
.iter()
312+
.map(|(source, location)| {
313+
let sourcemap_reference = SourceMapReference::from_url(location.clone());
314+
let sourcemap_path = full_sourcemap_path(source, location);
315+
316+
self.sourcemap_references
317+
.insert(source.url.clone(), Some(sourcemap_reference));
318+
319+
(fs::path_as_url(&sourcemap_path), source.url.clone())
320+
})
321+
.collect::<HashMap<_, _>>();
322+
323+
// Second pass: for remaining sourcemaps, try to guess the location
324+
let guessed_sourcemap_references = guess_sourcemap_references(
325+
sources_without_location,
326+
sourcemaps,
327+
explicitly_associated_sourcemaps,
328+
);
333329

334-
self.sourcemap_references
335-
.insert(source.url.clone(), Some(sourcemap_reference));
336-
}
330+
self.sourcemap_references
331+
.extend(guessed_sourcemap_references);
337332
}
338333

339334
pub fn dump_log(&self, title: &str) {
@@ -1086,6 +1081,164 @@ impl SourceMapProcessor {
10861081
}
10871082
}
10881083

1084+
/// For a set of source files without a sourcemap location, guess the sourcemap references.
1085+
///
1086+
/// Parameters:
1087+
/// - `sources_without_location`: The set of source files without a sourcemap location.
1088+
/// - `sourcemaps`: The set of available sourcemaps.
1089+
/// - `explicitly_associated_sourcemaps`: The set of sourcemaps that are explicitly associated
1090+
/// with a source file, and thus, cannot be guessed. If we guess such a sourcemap, we will
1091+
/// warn the user. This is stored in a map, where the key is the sourcemap URL, and the value
1092+
/// is the source file URL that is explicitly associated with the sourcemap.
1093+
///
1094+
/// Returns:
1095+
/// - A map from sourcemap URLs to the sourcemap references, which may be `None` if we couldn't
1096+
/// guess the sourcemap reference.
1097+
fn guess_sourcemap_references(
1098+
sources_without_location: HashSet<&mut SourceFile>,
1099+
sourcemaps: HashSet<String>,
1100+
explicitly_associated_sourcemaps: HashMap<String, String>,
1101+
) -> HashMap<String, Option<SourceMapReference>> {
1102+
let mut sourcemap_references = HashMap::new();
1103+
1104+
sources_without_location
1105+
.into_iter()
1106+
.fold(
1107+
// Collect sources guessed as associated with each sourcemap. This way, we ensure
1108+
// we only associate the sourcemap with any sources if it is only guessed once.
1109+
HashMap::new(),
1110+
|mut sources_associated_with_sm, source| {
1111+
let sourcemap_reference = guess_sourcemap_reference(&sourcemaps, &source.url)
1112+
.inspect_err(|err| {
1113+
source.warn(format!(
1114+
"could not determine a source map reference ({err})"
1115+
));
1116+
})
1117+
.ok()
1118+
.filter(|sourcemap_reference| {
1119+
explicitly_associated_sourcemaps
1120+
.get(
1121+
sourcemap_reference
1122+
.original_url
1123+
.as_ref()
1124+
.expect("original url set in guess_sourcemap_reference"),
1125+
)
1126+
.inspect(|url| {
1127+
source.warn(format!(
1128+
"based on the file name, we guessed a source map \
1129+
reference ({}), which is already associated with source \
1130+
{url}. Please explicitly set the sourcemap URL with a \
1131+
`//# sourceMappingURL=...` comment in the source file.",
1132+
sourcemap_reference.url
1133+
));
1134+
})
1135+
.is_none()
1136+
});
1137+
1138+
if let Some(sourcemap_reference) = sourcemap_reference {
1139+
sources_associated_with_sm
1140+
.entry(sourcemap_reference)
1141+
.or_insert_with(Vec::new)
1142+
.push(source);
1143+
} else {
1144+
sourcemap_references.insert(source.url.clone(), None);
1145+
}
1146+
1147+
sources_associated_with_sm
1148+
},
1149+
)
1150+
.into_iter()
1151+
.for_each(|(sourcemap_reference, mut sources)| {
1152+
if let [source] = sources.as_slice() {
1153+
// One source -> we can safely associate the sourcemap with it.
1154+
sourcemap_references.insert(source.url.clone(), Some(sourcemap_reference));
1155+
} else {
1156+
// Multiple sources -> it is unclear which source we should associate
1157+
// the sourcemap with, so don't associate it with any of them.
1158+
sources.iter_mut().for_each(|source| {
1159+
source.warn(format!(
1160+
"Could not associate this source with a source map. We \
1161+
guessed the sourcemap reference {} for multiple sources, including \
1162+
this one. Please explicitly set the sourcemap URL with a \
1163+
`//# sourceMappingURL=...` comment in the source file, to make the \
1164+
association clear.",
1165+
sourcemap_reference.url
1166+
));
1167+
sourcemap_references.insert(source.url.clone(), None);
1168+
});
1169+
}
1170+
});
1171+
1172+
sourcemap_references
1173+
}
1174+
1175+
/// Compute the full path to a sourcemap file, given the sourcemap's source file and the relative
1176+
/// path to the sourcemap from the source file.
1177+
fn full_sourcemap_path(source: &SourceFile, sourcemap_relative_path: &String) -> PathBuf {
1178+
let full_sourcemap_path = source
1179+
.path
1180+
.parent()
1181+
.expect("source path has a parent")
1182+
.join(sourcemap_relative_path);
1183+
1184+
full_sourcemap_path
1185+
}
1186+
1187+
/// A tuple of a map and a set, returned by `collect_sourcemap_locations`.
1188+
/// The map contains the sourcefiles for which we found a sourcemap location listed in the file, with
1189+
/// the sourcemap location as the value.
1190+
/// The set contains the sourcefiles for which we did not find a sourcemap location listed in the file.
1191+
type SourcemapLocations<'s> = (
1192+
HashMap<&'s mut SourceFile, String>,
1193+
HashSet<&'s mut SourceFile>,
1194+
);
1195+
1196+
trait SourcesIteratorExt<'s> {
1197+
fn collect_sourcemap_locations(self) -> SourcemapLocations<'s>;
1198+
}
1199+
1200+
impl<'s, I> SourcesIteratorExt<'s> for I
1201+
where
1202+
I: IntoIterator<Item = &'s mut SourceFile>,
1203+
{
1204+
/// Consume this iterator of sources, collecting the sourcemap locations for them.
1205+
fn collect_sourcemap_locations(self) -> SourcemapLocations<'s> {
1206+
self.into_iter()
1207+
.map(|source| {
1208+
let location = location_from_contents(&source.contents).map(String::from);
1209+
(source, location)
1210+
})
1211+
.fold(
1212+
(HashMap::new(), HashSet::new()),
1213+
|(mut sources_with_location, mut sources_without_location), (source, location)| {
1214+
match location {
1215+
Some(location) => {
1216+
sources_with_location.insert(source, location);
1217+
}
1218+
None => {
1219+
sources_without_location.insert(source);
1220+
}
1221+
}
1222+
(sources_with_location, sources_without_location)
1223+
},
1224+
)
1225+
}
1226+
}
1227+
1228+
/// Extract the sourcemap location from the contents of a source file.
1229+
fn location_from_contents(contents: &[u8]) -> Option<&str> {
1230+
str::from_utf8(contents)
1231+
.map(discover_sourcemaps_location)
1232+
.ok()
1233+
.flatten()
1234+
// If this is a full external URL, the code above is going to attempt
1235+
// to "normalize" it with the source path, resulting in a bogus path
1236+
// like "path/to/source/dir/https://some-static-host.example.com/path/to/foo.js.map"
1237+
// that can't be resolved to a source map file.
1238+
// So, we filter out such locations.
1239+
.filter(|loc| !is_remote_sourcemap(loc))
1240+
}
1241+
10891242
fn adjust_regular_sourcemap(
10901243
sourcemap: &mut SourceMap,
10911244
source_file: &mut SourceFile,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
```
2+
$ sentry-cli sourcemaps inject ./
3+
? success
4+
> Searching ./
5+
> Found 11 files
6+
> Analyzing 11 sources
7+
> Injecting debug ids
8+
9+
Source Map Debug ID Injection Report
10+
Modified: The following source files have been modified to have debug ids
11+
0fb38a98-fdda-5fd2-8e58-d8370f1596fb - ./app.min.js
12+
- warning: based on the file name, we guessed a source map reference (app.min.js.map), which is already associated with source ./references_app.min.js. Please explicitly set the sourcemap URL with a `//# sourceMappingURL=...` comment in the source file.
13+
2de47eef-a93e-5d79-8c45-afb649b15f9e - ./mapInSubdirectory.min.js
14+
f369af08-3e09-58d4-92a9-7f432f7d51cb - ./otherApp.js
15+
- warning: Could not associate this source with a source map. We guessed the sourcemap reference otherApp.map for multiple sources, including this one. Please explicitly set the sourcemap URL with a `//# sourceMappingURL=...` comment in the source file, to make the association clear.
16+
4ee42454-c84d-5d2a-b0b9-8cda74c57838 - ./otherApp.min.js
17+
- warning: Could not associate this source with a source map. We guessed the sourcemap reference otherApp.map for multiple sources, including this one. Please explicitly set the sourcemap URL with a `//# sourceMappingURL=...` comment in the source file, to make the association clear.
18+
44931b59-c632-5017-a624-3bb0ac2a0317 - ./references_app.min.js
19+
edefb3dc-9f7a-5540-9db0-270bbd81b8fd - ./subdirectory/app.min.js
20+
228cdb31-bb0a-533c-ac4c-c2ab111ab148 - ./subdirectory/mapForFileInParentDirectory.min.js
21+
- warning: based on the file name, we guessed a source map reference (mapForFileInParentDirectory.min.js.map), which is already associated with source ./mapInSubdirectory.min.js. Please explicitly set the sourcemap URL with a `//# sourceMappingURL=...` comment in the source file.
22+
Modified: The following sourcemap files have been modified to have debug ids
23+
44931b59-c632-5017-a624-3bb0ac2a0317 - ./app.min.js.map
24+
edefb3dc-9f7a-5540-9db0-270bbd81b8fd - ./subdirectory/app.min.js.map
25+
2de47eef-a93e-5d79-8c45-afb649b15f9e - ./subdirectory/mapForFileInParentDirectory.min.js.map
26+
27+
28+
```

tests/integration/_expected_outputs/sourcemaps/sourcemaps-inject-double-association/app.min.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/_expected_outputs/sourcemaps/sourcemaps-inject-double-association/app.min.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2de47eef-a93e-5d79-8c45-afb649b15f9e")}catch(e){}}();
3+
!function(e,t){"use strict";function n(e){return document.getElementById(e)}function r(e,t){e.addEventListener("click",t)}var o=n("btn");r(o,function(){console.log("mapInSubdirectory.min.js!")})}(window,document);
4+
5+
//# sourceMappingURL=subdirectory/mapForFileInParentDirectory.min.js.map
6+
//# debugId=2de47eef-a93e-5d79-8c45-afb649b15f9e

tests/integration/_expected_outputs/sourcemaps/sourcemaps-inject-double-association/otherApp.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/_expected_outputs/sourcemaps/sourcemaps-inject-double-association/otherApp.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/_expected_outputs/sourcemaps/sourcemaps-inject-double-association/otherApp.min.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="44931b59-c632-5017-a624-3bb0ac2a0317")}catch(e){}}();
3+
!function(e,t){"use strict";function n(e){return document.querySelector(e)}function r(e,t){e.textContent=t}var o=n("#message");r(o,"Hello World!")}(window,document);
4+
//# sourceMappingURL=app.min.js.map
5+
6+
//# debugId=44931b59-c632-5017-a624-3bb0ac2a0317

0 commit comments

Comments
 (0)