diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37bc5d8..5d0529a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,3 +42,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: test + args: --all-features diff --git a/Cargo.toml b/Cargo.toml index eedcf89..7f413fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,19 @@ license = "MIT OR Apache-2.0" name = "sentry-rust-minidump" readme = "README.md" repository = "https://github.com/timfish/sentry-rust-minidump" -version = "0.6.4" +version = "0.7.0" [dependencies] minidumper-child = "0.2" -sentry = "0.31" +sentry = "0.32" thiserror = "1" +serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1", optional = true } [dev-dependencies] actix-rt = "2.7" sadness-generator = "0.5" -sentry-test-server = {git = "https://github.com/timfish/sentry-test-server.git", rev = "134c30e"} +sentry-test-server = {git = "https://github.com/timfish/sentry-test-server.git", rev = "df19c85"} + +[features] +ipc = [ "dep:serde", "dep:serde_json"] \ No newline at end of file diff --git a/README.md b/README.md index f6e1fb8..9bcd147 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ your application code. ```toml [dependencies] -sentry = "0.31" -sentry-rust-minidump = "0.6" +sentry = "0.32" +sentry-rust-minidump = "0.7" ``` ```rust @@ -39,3 +39,29 @@ fn main() { } } ``` + +## `ipc` feature + +By default there is no scope synchronisation from the app process to the crash +reporter process. This means that native crash event will be missing +breadcrumbs, user, tags or extra added to the scope in the app. + +When the `ipc` feature is enabled, you can send scope updates to the crash +reporter process: + +```rust +fn main() { + let client = sentry::init("__YOUR_DSN__"); + + // Everything before here runs in both app and crash reporter processes + let crash_reporter = sentry_rust_minidump::init(&client).expect("crash reported didn't start"); + // Everything after here runs in only the app process + + crash_reporter.add_breadcrumb(...); + crash_reporter.set_user(...); + crash_reporter.set_extra(...); + crash_reporter.set_tag(...); + + // Don't drop crash_reporter or the reporter process will close! +} +``` \ No newline at end of file diff --git a/examples/app.rs b/examples/app.rs index 6ae6113..300a7ca 100644 --- a/examples/app.rs +++ b/examples/app.rs @@ -2,9 +2,16 @@ fn main() { let client = sentry::init("http://abc123@127.0.0.1:8123/12345"); // Everything before here runs in both app and crash reporter processes - let _guard = sentry_rust_minidump::init(&client); + let crash_handler = + sentry_rust_minidump::init(&client).expect("could not initialize crash reporter"); // Everything after here runs in only the app process + crash_handler.set_user(Some(sentry::User { + username: Some("john_doe".into()), + email: Some("john@doe.town".into()), + ..Default::default() + })); + std::thread::sleep(std::time::Duration::from_secs(10)); unsafe { sadness_generator::raise_segfault() }; diff --git a/src/lib.rs b/src/lib.rs index 12049d2..f1c9fae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,76 @@ -pub use minidumper_child::MinidumperChild; +use minidumper_child::{ClientHandle, Error, MinidumperChild}; use sentry::{ protocol::{Attachment, AttachmentType, Event, Value}, Level, }; -pub use minidumper_child::{ClientHandle, Error}; +#[cfg(feature = "ipc")] +use sentry::{Breadcrumb, User}; + +pub struct Handle { + _handle: ClientHandle, +} + +#[cfg(feature = "ipc")] +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq)] +pub enum ScopeUpdate { + AddBreadcrumb(Breadcrumb), + SetUser(Option), + SetExtra(String, Option), + SetTag(String, Option), +} + +#[cfg(feature = "ipc")] +impl Handle { + fn send_message(&self, update: &ScopeUpdate) { + let buffer = serde_json::to_vec(update).expect("could not serialize scope update"); + self._handle.send_message(0, buffer).ok(); + } + + pub fn add_breadcrumb(&self, breadcrumb: Breadcrumb) { + self.send_message(&ScopeUpdate::AddBreadcrumb(breadcrumb)); + } + + pub fn set_user(&self, user: Option) { + self.send_message(&ScopeUpdate::SetUser(user)); + } + + pub fn set_extra(&self, key: String, value: Option) { + self.send_message(&ScopeUpdate::SetExtra(key, value)); + } + + pub fn set_tag(&self, key: String, value: Option) { + self.send_message(&ScopeUpdate::SetTag(key, value)); + } +} #[must_use = "The return value from init() should not be dropped until the program exits"] -pub fn init(sentry_client: &sentry::Client) -> Result { +pub fn init(sentry_client: &sentry::Client) -> Result { let sentry_client = sentry_client.clone(); - let child = MinidumperChild::new().on_minidump(move |buffer, path| { + let child = MinidumperChild::new(); + + #[cfg(feature = "ipc")] + let child = child.on_message(|_kind, buffer| { + if let Ok(update) = serde_json::from_slice::(&buffer[..]) { + match update { + ScopeUpdate::AddBreadcrumb(b) => sentry::add_breadcrumb(b), + ScopeUpdate::SetUser(u) => sentry::configure_scope(|scope| { + scope.set_user(u); + }), + ScopeUpdate::SetExtra(k, v) => sentry::configure_scope(|scope| match v { + Some(v) => scope.set_extra(&k, v), + None => scope.remove_extra(&k), + }), + ScopeUpdate::SetTag(k, v) => match v { + Some(v) => sentry::configure_scope(|scope| scope.set_tag(&k, &v)), + None => sentry::configure_scope(|scope| scope.remove_tag(&k)), + }, + } + } + }); + + let child = child.on_minidump(move |buffer, path| { sentry::with_scope( |scope| { // Remove event.process because this event came from the @@ -49,5 +109,5 @@ pub fn init(sentry_client: &sentry::Client) -> Result { }); } - child.spawn() + child.spawn().map(|handle| Handle { _handle: handle }) } diff --git a/tests/e2e.rs b/tests/e2e.rs index 254f69f..498daa2 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -6,25 +6,40 @@ async fn test_example_app() -> Result<(), Box> { let envelope_rx = sentry_test_server::server(("127.0.0.1", 8123))?; // We need to await at some point otherwise the server doesn't seem to start - actix_rt::time::sleep(Duration::from_secs(1)).await; + actix_rt::time::sleep(Duration::from_secs(2)).await; Command::new("cargo") - .args(["run", "--example", "app"]) + .args(["run", "--example", "app", "--all-features"]) .spawn()? .wait()?; - let env = envelope_rx.recv_timeout(Duration::from_secs(2))?; + let env = envelope_rx.recv_timeout(Duration::from_secs(15))?; if let Ok(json) = sentry_test_server::to_json_pretty(&env) { println!("{}", json); } - let item = env + let env_item = env + .items() + .find(|item| matches!(item, EnvelopeItem::Event(_))) + .expect("envelope should have an event"); + + let event = match env_item { + EnvelopeItem::Event(event) => event.clone(), + _ => unreachable!("envelope should have an event"), + }; + + let user = event.user.expect("event should have a user"); + + assert_eq!(user.email, Some("john@doe.town".into())); + assert_eq!(user.username, Some("john_doe".into())); + + let env_item = env .items() .find(|item| matches!(item, EnvelopeItem::Attachment(_))) .expect("envelope should have an attachment"); - let attachment = match item { + let attachment = match env_item { EnvelopeItem::Attachment(attachment) => attachment, _ => unreachable!("envelope should have an attachment"), };