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(errors): Fingerprinting first pass #25707

Merged
merged 25 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rust/cymbal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ serde_json = { workspace = true }
serde = { workspace = true }
sourcemap = "9.0.0"
reqwest = { workspace = true }
sha2 = "0.10.8"

[dev-dependencies]
httpmock = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions rust/cymbal/src/fingerprinting/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod v1;
264 changes: 264 additions & 0 deletions rust/cymbal/src/fingerprinting/v1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
use crate::{
error::Error,
metric_consts::ERRORS,
types::{frames::Frame, Exception},
};
use reqwest::Url;
use sha2::{Digest, Sha256};

// Given resolved Frames vector and the original Exception, we can now generate a fingerprint for it
pub fn generate_fingerprint(
exception: &Exception,
mut frames: Vec<Frame>,
) -> Result<String, Error> {
let mut fingerprint = format!(
"{}-{}",
exception.exception_type, exception.exception_message
);

let has_resolved_frames: bool = frames.iter().any(|f| f.resolved);
if has_resolved_frames {
frames.retain(|f| f.resolved);
}

let has_in_app_frames: bool = frames.iter().any(|f| f.in_app);
if has_in_app_frames {
frames.retain(|f| f.in_app);
} else {
metrics::counter!(ERRORS, "cause" => "no_in_app_frames").increment(1);
frames = frames.into_iter().take(1).collect()
}

for frame in frames {
let source_fn = match Url::parse(&frame.source.unwrap_or("".to_string())) {
Ok(url) => url.path().to_string(),
Err(_) => "unknown".to_string(),
};

fingerprint.push('-');
fingerprint.push_str(&source_fn);
fingerprint.push(':');
oliverb123 marked this conversation as resolved.
Show resolved Hide resolved
fingerprint.push_str(&frame.resolved_name.unwrap_or(frame.mangled_name));
}
// TODO: Handle anonymous functions somehow? Not sure if these would have a resolved name at all. How would they show up
// as unresolved names?

Ok(fingerprint)
}

// Generate sha256 hash of the fingerprint to get a unique fingerprint identifier
pub fn hash_fingerprint(fingerprint: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(fingerprint.as_bytes());
let result = hasher.finalize();
format!("{:x}", result)
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_fingerprint_generation() {
let exception = Exception {
exception_type: "TypeError".to_string(),
exception_message: "Cannot read property 'foo' of undefined".to_string(),
mechanism: Default::default(),
module: Default::default(),
thread_id: None,
stacktrace: Default::default(),
};

let resolved_frames = vec![
Frame {
mangled_name: "foo".to_string(),
line: Some(10),
column: Some(5),
source: Some("http://example.com/alpha/foo.js".to_string()),
in_app: true,
resolved_name: Some("bar".to_string()),
resolved: true,
lang: "javascript".to_string(),
},
Frame {
mangled_name: "bar".to_string(),
line: Some(20),
column: Some(15),
source: Some("http://example.com/bar.js".to_string()),
in_app: true,
resolved_name: Some("baz".to_string()),
resolved: true,
lang: "javascript".to_string(),
},
Frame {
mangled_name: "xyz".to_string(),
line: Some(30),
column: Some(25),
source: None,
in_app: true,
resolved_name: None,
resolved: true,
lang: "javascript".to_string(),
},
Frame {
mangled_name: "<anonymous>".to_string(),
line: None,
column: None,
source: None,
in_app: false,
resolved_name: None,
resolved: true,
lang: "javascript".to_string(),
},
];

let fingerprint = super::generate_fingerprint(&exception, resolved_frames).unwrap();
assert_eq!(
fingerprint,
"TypeError-Cannot read property 'foo' of undefined-/alpha/foo.js:bar-/bar.js:baz-unknown:xyz"
);
}

#[test]
fn test_some_resolved_frames() {
let exception = Exception {
exception_type: "TypeError".to_string(),
exception_message: "Cannot read property 'foo' of undefined".to_string(),
mechanism: Default::default(),
module: Default::default(),
thread_id: None,
stacktrace: Default::default(),
};

let resolved_frames = vec![
Frame {
mangled_name: "foo".to_string(),
line: Some(10),
column: Some(5),
source: Some("http://example.com/alpha/foo.js".to_string()),
in_app: true,
resolved_name: Some("bar".to_string()),
resolved: true,
lang: "javascript".to_string(),
},
Frame {
mangled_name: "bar".to_string(),
line: Some(20),
column: Some(15),
source: Some("http://example.com/bar.js".to_string()),
in_app: true,
resolved_name: Some("baz".to_string()),
resolved: true,
lang: "javascript".to_string(),
},
Frame {
mangled_name: "xyz".to_string(),
line: Some(30),
column: Some(25),
source: None,
in_app: true,
resolved_name: None,
resolved: false,
lang: "javascript".to_string(),
},
];

let fingerprint = super::generate_fingerprint(&exception, resolved_frames).unwrap();
assert_eq!(
fingerprint,
"TypeError-Cannot read property 'foo' of undefined-/alpha/foo.js:bar-/bar.js:baz"
);
}

#[test]
fn test_no_resolved_frames() {
let exception = Exception {
exception_type: "TypeError".to_string(),
exception_message: "Cannot read property 'foo' of undefined".to_string(),
mechanism: Default::default(),
module: Default::default(),
thread_id: None,
stacktrace: Default::default(),
};

let resolved_frames = vec![
Frame {
mangled_name: "foo".to_string(),
line: Some(10),
column: Some(5),
source: Some("http://example.com/alpha/foo.js".to_string()),
in_app: true,
resolved_name: Some("bar".to_string()),
resolved: false,
lang: "javascript".to_string(),
},
Frame {
mangled_name: "bar".to_string(),
line: Some(20),
column: Some(15),
source: Some("http://example.com/bar.js".to_string()),
in_app: true,
resolved_name: Some("baz".to_string()),
resolved: false,
lang: "javascript".to_string(),
},
Frame {
mangled_name: "xyz".to_string(),
line: Some(30),
column: Some(25),
source: None,
in_app: true,
resolved_name: None,
resolved: false,
lang: "javascript".to_string(),
},
];

let fingerprint = super::generate_fingerprint(&exception, resolved_frames).unwrap();
assert_eq!(
fingerprint,
"TypeError-Cannot read property 'foo' of undefined-/alpha/foo.js:bar-/bar.js:baz-unknown:xyz"
);
}

#[test]
fn test_no_in_app_frames() {
let exception = Exception {
exception_type: "TypeError".to_string(),
exception_message: "Cannot read property 'foo' of undefined".to_string(),
mechanism: Default::default(),
module: Default::default(),
thread_id: None,
stacktrace: Default::default(),
};

let resolved_frames = vec![
Frame {
mangled_name: "foo".to_string(),
line: Some(10),
column: Some(5),
source: Some("http://example.com/alpha/foo.js".to_string()),
in_app: false,
resolved_name: Some("bar".to_string()),
resolved: false,
lang: "javascript".to_string(),
},
Frame {
mangled_name: "bar".to_string(),
line: Some(20),
column: Some(15),
source: Some("http://example.com/bar.js".to_string()),
in_app: false,
resolved_name: Some("baz".to_string()),
resolved: false,
lang: "javascript".to_string(),
},
];

let fingerprint = super::generate_fingerprint(&exception, resolved_frames).unwrap();
assert_eq!(
fingerprint,
"TypeError-Cannot read property 'foo' of undefined-/alpha/foo.js:bar"
);
}
}
4 changes: 3 additions & 1 deletion rust/cymbal/src/langs/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ impl RawJSFrame {
}?)
}

// Returns none if the frame is
// Returns none if the frame is minified
fn try_assume_unminified(&self) -> Option<Frame> {
// TODO - we should include logic here that uses some kind of heuristic to determine
// if this frame is minified or not. Right now, we simply assume it isn't if this is
Expand All @@ -115,6 +115,7 @@ impl RawJSFrame {
source: self.source_url.clone(), // Maybe we have one?
in_app: self.in_app,
resolved_name: Some(self.fn_name.clone()), // This is the bit we'd want to check
resolved: false,
lang: "javascript".to_string(),
})
}
Expand All @@ -131,6 +132,7 @@ impl From<(&RawJSFrame, Token<'_>)> for Frame {
source: token.get_source().map(String::from),
in_app: raw_frame.in_app,
resolved_name: token.get_name().map(String::from),
resolved: true,
lang: "javascript".to_string(),
}
}
Expand Down
1 change: 1 addition & 0 deletions rust/cymbal/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod app_context;
pub mod config;
pub mod error;
pub mod fingerprinting;
pub mod langs;
pub mod metric_consts;
pub mod resolver;
Expand Down
9 changes: 9 additions & 0 deletions rust/cymbal/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use cymbal::{
app_context::AppContext,
config::Config,
error::Error,
fingerprinting,
metric_consts::{ERRORS, EVENT_RECEIVED, STACK_PROCESSED},
types::{frames::RawFrame, ErrProps},
};
Expand Down Expand Up @@ -129,6 +130,14 @@ async fn main() -> Result<(), Error> {
resolved_frames.push(resolved);
}

let Ok(_fingerprint) = fingerprinting::v1::generate_fingerprint(
&properties.exception_list[0],
resolved_frames,
) else {
metrics::counter!(ERRORS, "cause" => "fingerprint_generation_failed").increment(1);
continue;
};

metrics::counter!(STACK_PROCESSED).increment(1);
}
}
5 changes: 3 additions & 2 deletions rust/cymbal/src/types/frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ pub struct Frame {
pub line: Option<u32>, // Line the function is define on, if known
pub column: Option<u32>, // Column the function is defined on, if known
pub source: Option<String>, // Generally, the file the function is defined in
pub in_app: bool, // We hard-require clients to tell us this?
pub resolved_name: Option<String>, // The name of the function, after symbolification
pub lang: String, // The language of the frame. Always known (I guess?)
pub lang: String, // The language of the frame. Always known
pub in_app: bool, // We hard-require clients to tell us this?
pub resolved: bool, // Whether the frame was successfully demangled (always true if it does not apply)
}
Loading