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 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
156 changes: 156 additions & 0 deletions src/commands/sourcemaps/inject.rs
@@ -0,0 +1,156 @@
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.")
// TODO: What are these {n}{n}s? They show up verbatim in the help output.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They suppose to be terminal line-break specific to clap, but for some reason you say they stopped working? 🤔

https://github.com/clap-rs/clap/blob/ad5d67623a89415c7f5f7e1f7a47bf5abdfd0b6c/src/output/help_template.rs#L1018

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placeholders have apparently been in the send_envelope-help test case since the beginning.

https://github.com/getsentry/sentry-cli/blame/master/tests/integration/_cases/send_envelope/send_envelope-help.trycmd

.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."),
)
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
let path = matches.value_of("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<()> {
'paths: 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 'paths;
loewenheim marked this conversation as resolved.
Show resolved Hide resolved
}

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<()> {
make_backup_copy(js_path)?;
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 => {
make_backup_copy(sourcemap_path)?;
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)
}
}
}

// Makes a backup copy of a file that has `.bak` appended to the path.
fn make_backup_copy(path: &Path) -> Result<()> {
let mut file_name = path.file_name().unwrap_or_default().to_os_string();
file_name.push(".bak");
let backup_path = path.with_file_name(file_name);
std::fs::copy(path, backup_path).context("Failed to back up {path}")?;

Ok(())
}
2 changes: 2 additions & 0 deletions src/commands/sourcemaps/mod.rs
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
1 change: 1 addition & 0 deletions tests/integration/_cases/sourcemaps/sourcemaps-help.trycmd
Expand Up @@ -24,6 +24,7 @@ OPTIONS:
SUBCOMMANDS:
explain Explain why sourcemaps are not working for a given event.
help Print this message or the help of the given subcommand(s)
inject Fixes up JavaScript source files and sourcemaps with debug ids.
resolve Resolve sourcemap for a given line/column position.
upload Upload sourcemaps for a release.

Expand Down
47 changes: 47 additions & 0 deletions tests/integration/_cases/sourcemaps/sourcemaps-inject-help.trycmd
@@ -0,0 +1,47 @@
```
$ sentry-cli sourcemaps inject --help
? success
sentry-cli[EXE]-sourcemaps-inject
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.

USAGE:
sentry-cli[EXE] sourcemaps inject [OPTIONS] <PATH>

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

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

-h, --help
Print help information

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

--log-level <LOG_LEVEL>
Set the log output verbosity.

[possible values: trace, debug, info, warn, error]

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

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

--quiet
Do not print any output while preserving correct exit code. This flag is currently
implemented only for selected subcommands.

[aliases: silent]

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

```
Expand Up @@ -24,6 +24,7 @@ OPTIONS:
SUBCOMMANDS:
explain Explain why sourcemaps are not working for a given event.
help Print this message or the help of the given subcommand(s)
inject Fixes up JavaScript source files and sourcemaps with debug ids.
resolve Resolve sourcemap for a given line/column position.
upload Upload sourcemaps for a release.

Expand Down
6 changes: 6 additions & 0 deletions 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");
}
1 change: 1 addition & 0 deletions tests/integration/sourcemaps/mod.rs
@@ -1,6 +1,7 @@
use crate::integration::register_test;

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

Expand Down