diff --git a/src/commands/sourcemaps/inject.rs b/src/commands/sourcemaps/inject.rs new file mode 100644 index 0000000000..d126d2b446 --- /dev/null +++ b/src/commands/sourcemaps/inject.rs @@ -0,0 +1,144 @@ +use std::fs::{self, File}; +use std::io::{Seek, Write}; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use clap::{Arg, ArgMatches, Command}; +use glob::glob; +use log::{debug, warn}; +use serde_json::Value; +use symbolic::debuginfo::js; +use uuid::Uuid; + +const CODE_SNIPPET_TEMPLATE: &str = r#"!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]="__SENTRY_DEBUG_ID__")}catch(e){}}()"#; +const DEBUGID_PLACEHOLDER: &str = "__SENTRY_DEBUG_ID__"; +const SOURCEMAP_DEBUGID_KEY: &str = "debug_id"; +const DEBUGID_COMMENT_PREFIX: &str = "//# debugId"; + +pub fn make_command(command: Command) -> Command { + command + .about("Fixes up JavaScript source files and sourcemaps with debug ids.") + .long_about( + "Fixes up JavaScript source files and sourcemaps with debug ids.{n}{n}\ + For every JS source file that references a sourcemap, a debug id is generated and \ + inserted into both files. If the referenced sourcemap already contains a debug id, \ + that id is used instead.", + ) + .arg( + Arg::new("path") + .value_name("PATH") + .required(true) + .help("The path or glob to the javascript files."), + ) + .hide(true) +} + +pub fn execute(matches: &ArgMatches) -> Result<()> { + let path = matches.get_one::("path").unwrap(); + + let collected_paths: Vec = glob(path) + .unwrap() + .flatten() + .filter(|path| path.extension().map_or(false, |ext| ext == "js")) + .collect(); + + if collected_paths.is_empty() { + warn!("Did not match any JavaScript files for pattern: {}", path); + return Ok(()); + } + + fixup_files(&collected_paths) +} + +fn fixup_files(paths: &[PathBuf]) -> Result<()> { + for path in paths { + let js_path = path.as_path(); + + debug!("Processing js file {}", js_path.display()); + + let file = + fs::read_to_string(js_path).context(format!("Failed to open {}", js_path.display()))?; + + if js::discover_debug_id(&file).is_some() { + debug!("File {} was previously processed", js_path.display()); + continue; + } + + let Some(sourcemap_url) = js::discover_sourcemaps_location(&file) else { + debug!("File {} does not contain a sourcemap url", js_path.display()); + continue; + }; + + let sourcemap_path = js_path.with_file_name(sourcemap_url); + + if !sourcemap_path.exists() { + warn!("Sourcemap file {} not found", sourcemap_path.display()); + continue; + } + + let debug_id = fixup_sourcemap(&sourcemap_path) + .context(format!("Failed to process {}", sourcemap_path.display()))?; + + fixup_js_file(js_path, debug_id) + .context(format!("Failed to process {}", js_path.display()))?; + } + + Ok(()) +} + +/// Appends the following text to a file: +/// ``` +/// +/// [] +/// //# sentryDebugId= +///``` +/// where `[]` +/// is `CODE_SNIPPET_TEMPLATE` with `debug_id` substituted for the `__SENTRY_DEBUG_ID__` +/// placeholder. +fn fixup_js_file(js_path: &Path, debug_id: Uuid) -> Result<()> { + let mut js_file = File::options().append(true).open(js_path)?; + let to_inject = + CODE_SNIPPET_TEMPLATE.replace(DEBUGID_PLACEHOLDER, &debug_id.hyphenated().to_string()); + writeln!(js_file)?; + writeln!(js_file, "{to_inject}")?; + write!(js_file, "{DEBUGID_COMMENT_PREFIX}={debug_id}")?; + + Ok(()) +} + +/// Fixes up a sourcemap file with a debug id. +/// +/// If the file already contains a debug id under the `debugID` key, it is left unmodified. +/// Otherwise, a fresh debug id is inserted under that key. +/// +/// In either case, the value of the `debugID` key is returned. +fn fixup_sourcemap(sourcemap_path: &Path) -> Result { + let mut sourcemap_file = File::options() + .read(true) + .write(true) + .open(sourcemap_path)?; + let mut sourcemap: Value = serde_json::from_reader(&sourcemap_file)?; + + sourcemap_file.rewind()?; + + let Some(map) = sourcemap.as_object_mut() else { + bail!("Invalid sourcemap"); + }; + + match map.get(SOURCEMAP_DEBUGID_KEY) { + Some(id) => { + let debug_id = serde_json::from_value(id.clone())?; + debug!("Sourcemap already has a debug id"); + Ok(debug_id) + } + + None => { + let debug_id = Uuid::new_v4(); + let id = serde_json::to_value(debug_id)?; + map.insert(SOURCEMAP_DEBUGID_KEY.to_string(), id); + + serde_json::to_writer(&mut sourcemap_file, &sourcemap)?; + Ok(debug_id) + } + } +} diff --git a/src/commands/sourcemaps/mod.rs b/src/commands/sourcemaps/mod.rs index 2407763e89..b37f3b3601 100644 --- a/src/commands/sourcemaps/mod.rs +++ b/src/commands/sourcemaps/mod.rs @@ -4,12 +4,14 @@ use clap::{ArgMatches, Command}; use crate::utils::args::ArgExt; pub mod explain; +pub mod inject; pub mod resolve; pub mod upload; macro_rules! each_subcommand { ($mac:ident) => { $mac!(explain); + $mac!(inject); $mac!(resolve); $mac!(upload); }; diff --git a/tests/integration/_cases/sourcemaps/sourcemaps-inject-help.trycmd b/tests/integration/_cases/sourcemaps/sourcemaps-inject-help.trycmd new file mode 100644 index 0000000000..867762db1b --- /dev/null +++ b/tests/integration/_cases/sourcemaps/sourcemaps-inject-help.trycmd @@ -0,0 +1,46 @@ +``` +$ sentry-cli[EXE] sourcemaps inject --help +? success +Fixes up JavaScript source files and sourcemaps with debug ids. + +For every JS source file that references a sourcemap, a debug id is generated and inserted into both +files. If the referenced sourcemap already contains a debug id, that id is used instead. + +Usage: sentry-cli[EXE] sourcemaps inject [OPTIONS] + +Arguments: + + The path or glob to the javascript files. + +Options: + -o, --org + The organization slug + + --header + Custom headers that should be attached to all requests + in key:value format. + + -p, --project + The project slug. + + --auth-token + Use the given Sentry auth token. + + -r, --release + The release slug. + + --log-level + Set the log output verbosity. + + [possible values: trace, debug, info, warn, error] + + --quiet + Do not print any output while preserving correct exit code. This flag is currently + implemented only for selected subcommands. + + [aliases: silent] + + -h, --help + Print help (see a summary with '-h') + +``` \ No newline at end of file diff --git a/tests/integration/sourcemaps/inject.rs b/tests/integration/sourcemaps/inject.rs new file mode 100644 index 0000000000..a4eafc2abb --- /dev/null +++ b/tests/integration/sourcemaps/inject.rs @@ -0,0 +1,6 @@ +use crate::integration::register_test; + +#[test] +fn command_sourcemaps_inject_help() { + register_test("sourcemaps/sourcemaps-inject-help.trycmd"); +} diff --git a/tests/integration/sourcemaps/mod.rs b/tests/integration/sourcemaps/mod.rs index c2c374da5c..5e1a263042 100644 --- a/tests/integration/sourcemaps/mod.rs +++ b/tests/integration/sourcemaps/mod.rs @@ -1,6 +1,7 @@ use crate::integration::register_test; mod explain; +mod inject; mod resolve; mod upload;