Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add sourcemaps inject command #1469

Merged
merged 20 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions src/commands/sourcemaps/inject.rs
Original file line number Diff line number Diff line change
@@ -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::<String>("path").unwrap();

let collected_paths: Vec<PathBuf> = 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:
/// ```
///
/// <CODE_SNIPPET>[<debug_id>]
/// //# sentryDebugId=<debug_id>
///```
/// where `<CODE_SNIPPET>[<debug_id>]`
/// 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<Uuid> {
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)
}
}
}
2 changes: 2 additions & 0 deletions src/commands/sourcemaps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
46 changes: 46 additions & 0 deletions tests/integration/_cases/sourcemaps/sourcemaps-inject-help.trycmd
Original file line number Diff line number Diff line change
@@ -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] <PATH>

Arguments:
<PATH>
The path or glob to the javascript files.

Options:
-o, --org <ORG>
The organization slug

--header <KEY:VALUE>
Custom headers that should be attached to all requests
in key:value format.

-p, --project <PROJECT>
The project slug.

--auth-token <AUTH_TOKEN>
Use the given Sentry auth token.

-r, --release <RELEASE>
The release slug.

--log-level <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')

```
6 changes: 6 additions & 0 deletions tests/integration/sourcemaps/inject.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use crate::integration::register_test;

#[test]
fn command_sourcemaps_inject_help() {
register_test("sourcemaps/sourcemaps-inject-help.trycmd");
}
1 change: 1 addition & 0 deletions tests/integration/sourcemaps/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::integration::register_test;

mod explain;
mod inject;
mod resolve;
mod upload;

Expand Down