From 273f64aef10297ba886bbfdc76f080d3387a21c5 Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Thu, 10 Oct 2024 16:52:02 +0300 Subject: [PATCH] feat(err): Wire up resolver (#25464) --- rust/cymbal/src/error.rs | 55 ++++++-- rust/cymbal/src/langs/js.rs | 117 +++++++++++++++--- rust/cymbal/src/resolver.rs | 117 +++++++++++++++--- rust/cymbal/src/symbol_store/basic.rs | 55 ++++---- rust/cymbal/src/symbol_store/caching.rs | 35 ++++++ rust/cymbal/src/symbol_store/mod.rs | 15 ++- rust/cymbal/src/types/error.rs | 4 - rust/cymbal/src/types/frames.rs | 41 +++--- rust/cymbal/src/types/mod.rs | 8 +- rust/cymbal/src/types/stacks.rs | 19 --- ...aw_js_stack.json => raw_ch_exception.json} | 0 rust/cymbal/tests/types.rs | 2 +- 12 files changed, 351 insertions(+), 117 deletions(-) delete mode 100644 rust/cymbal/src/types/error.rs delete mode 100644 rust/cymbal/src/types/stacks.rs rename rust/cymbal/tests/static/{raw_js_stack.json => raw_ch_exception.json} (100%) diff --git a/rust/cymbal/src/error.rs b/rust/cymbal/src/error.rs index 75bfbaffc0837..b3ddb4556a14c 100644 --- a/rust/cymbal/src/error.rs +++ b/rust/cymbal/src/error.rs @@ -11,12 +11,51 @@ pub enum Error { SqlxError(#[from] sqlx::Error), #[error("Reqwest error: {0}")] ReqwestError(#[from] reqwest::Error), - #[error("Not implemented error: {0}")] - NotImplementedError(String), - #[error("Lookup failed: {0}")] - LookupFailed(String), - #[error("Could not get source ref from: {0}")] - InvalidSourceRef(String), - #[error("sourcemap error: {0}")] - SourceMapError(#[from] sourcemap::Error), + #[error(transparent)] + ResolutionError(#[from] ResolutionError), +} + +// These are errors that occur during frame resolution. This excludes e.g. network errors, +// which are handled by the store - this is the error type that's handed to the frame to see +// if it can still make something useful out of it. +#[derive(Debug, Error)] +pub enum ResolutionError { + #[error(transparent)] + JavaScript(#[from] JsResolveErr), +} + +#[derive(Debug, Error)] +pub enum JsResolveErr { + // The frame has no source url. This might indicate it needs no further processing, who knows + #[error("No source url found")] + NoSourceUrl, + // We failed to parse a found source map + #[error("Invalid source map: {0}")] + InvalidSourceMap(#[from] sourcemap::Error), + // We found and parsed the source map, but couldn't find our frames token in it + #[error("Token not found for frame: {0}:{1}:{2}")] + TokenNotFound(String, u32, u32), + // We couldn't parse the source url of the frame + #[error("Invalid source url: {0}")] + InvalidSourceUrl(String), + // We couldn't find a sourcemap associated with the frames source url, after + // fetching the source, in either the headers or body. This might indicate + // the frame is not minified + #[error("Could not find sourcemap for source url: {0}")] + NoSourcemap(String), + // We made a request to the source URL, and got a source + // map header, but couldn't parse it to a utf8 string + #[error("Could not parse source-map header from url {0}")] + InvalidSourceMapHeader(String), + // We found a source map url, in the headers or body + // of the response from the source url, but couldn't + // parse it to a URL to actually fetch the source map + #[error("Invalid source url: {0}")] + InvalidSourceMapUrl(String), +} + +impl From for Error { + fn from(e: JsResolveErr) -> Self { + ResolutionError::JavaScript(e).into() + } } diff --git a/rust/cymbal/src/langs/js.rs b/rust/cymbal/src/langs/js.rs index fa5c272af1f34..a349eb2ea73a5 100644 --- a/rust/cymbal/src/langs/js.rs +++ b/rust/cymbal/src/langs/js.rs @@ -1,7 +1,11 @@ use reqwest::Url; use serde::Deserialize; +use sourcemap::Token; -use crate::error::Error; +use crate::{ + error::{Error, JsResolveErr}, + types::frames::Frame, +}; // A minifed JS stack frame. Just the minimal information needed to lookup some // sourcemap for it and produce a "real" stack frame. @@ -13,27 +17,104 @@ pub struct RawJSFrame { #[serde(rename = "colno")] pub column: u32, #[serde(rename = "filename")] - pub script_url: String, + pub source_url: Option, // The url the the script the frame was in was fetched from pub in_app: bool, #[serde(rename = "function")] pub fn_name: String, } impl RawJSFrame { - pub fn source_ref(&self) -> Result { - // Frame scrupt URLs are in the form: `:///::`. We - // want to strip the line and column, if they're present, and then return the rest - let to_protocol_end = self - .script_url - .find("://") - .ok_or(Error::InvalidSourceRef(self.script_url.clone()))? - + 3; - - let (protocol, rest) = self.script_url.split_at(to_protocol_end); - let to_end_of_path = rest.find(':').unwrap_or(rest.len()); - let useful = protocol.len() + to_end_of_path; - let url = &self.script_url[..useful]; - Url::parse(url).map_err(|_| Error::InvalidSourceRef(self.script_url.clone())) + pub fn source_ref(&self) -> Result { + // We can't resolve a frame without a source ref, and are forced + // to assume this frame is not minified + let Some(source_url) = &self.source_url else { + return Err(JsResolveErr::NoSourceUrl); + }; + + // We outright reject relative URLs, or ones that are not HTTP + if !source_url.starts_with("http://") && !source_url.starts_with("https://") { + return Err(JsResolveErr::InvalidSourceUrl(source_url.clone())); + } + + // TODO - we assume these are always absolute urls, and maybe should handle cases where + // they aren't? We control this on the client side, and I'd prefer to enforce it here. + + // These urls can have a trailing line and column number, formatted like: http://example.com/path/to/file.js:1:2. + // We need to strip that off to get the actual source url + // If the last colon is after the last slash, remove it, under the assumption that it's a column number. + // If there is no colon, or it's before the last slash, we assume the whole thing is a url, + // with no trailing line and column numbers + let last_colon = source_url.rfind(':'); + let last_slash = source_url.rfind('/'); + let useful = match (last_colon, last_slash) { + (Some(colon), Some(slash)) if colon > slash => colon, + _ => source_url.len(), + }; + + // We do this check one more time, to account for possible line number + let source_url = &source_url[..useful]; + let last_colon = source_url.rfind(':'); + let last_slash = source_url.rfind('/'); + let useful = match (last_colon, last_slash) { + (Some(colon), Some(slash)) if colon > slash => colon, + _ => source_url.len(), + }; + + Url::parse(&source_url[..useful]) + .map_err(|_| JsResolveErr::InvalidSourceUrl(source_url.to_string())) + } + + // JS frames can only handle JS resolution errors - errors at the network level + pub fn try_handle_resolution_error(&self, e: JsResolveErr) -> Result { + // A bit naughty, but for code like this, justified I think + use JsResolveErr::{ + InvalidSourceMap, InvalidSourceMapHeader, InvalidSourceMapUrl, InvalidSourceUrl, + NoSourceUrl, NoSourcemap, TokenNotFound, + }; + + // I break out all the cases individually here, rather than using an _ to match all, + // because I want this to stop compiling if we add new cases + Ok(match e { + NoSourceUrl => self.try_assume_unminified().ok_or(NoSourceUrl), // We assume we're not minified + NoSourcemap(u) => self.try_assume_unminified().ok_or(NoSourcemap(u)), + InvalidSourceMap(e) => Err(JsResolveErr::from(e)), + TokenNotFound(s, l, c) => Err(TokenNotFound(s, l, c)), + InvalidSourceUrl(u) => Err(InvalidSourceUrl(u)), + InvalidSourceMapHeader(u) => Err(InvalidSourceMapHeader(u)), + InvalidSourceMapUrl(u) => Err(InvalidSourceMapUrl(u)), + }?) + } + + // Returns none if the frame is + fn try_assume_unminified(&self) -> Option { + // 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 + // being called (and all the cases where it's called are above) + Some(Frame { + mangled_name: self.fn_name.clone(), + line: Some(self.line), + column: Some(self.column), + 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 + lang: "javascript".to_string(), + }) + } +} + +impl From<(&RawJSFrame, Token<'_>)> for Frame { + fn from(src: (&RawJSFrame, Token)) -> Self { + let (raw_frame, token) = src; + + Self { + mangled_name: raw_frame.fn_name.clone(), + line: Some(token.get_src_line()), + column: Some(token.get_src_col()), + source: token.get_source().map(String::from), + in_app: raw_frame.in_app, + resolved_name: token.get_name().map(String::from), + lang: "javascript".to_string(), + } } } @@ -44,7 +125,7 @@ mod test { let frame = super::RawJSFrame { line: 1, column: 2, - script_url: "http://example.com/path/to/file.js:1:2".to_string(), + source_url: Some("http://example.com/path/to/file.js:1:2".to_string()), in_app: true, fn_name: "main".to_string(), }; @@ -57,7 +138,7 @@ mod test { let frame = super::RawJSFrame { line: 1, column: 2, - script_url: "http://example.com/path/to/file.js".to_string(), + source_url: Some("http://example.com/path/to/file.js".to_string()), in_app: true, fn_name: "main".to_string(), }; diff --git a/rust/cymbal/src/resolver.rs b/rust/cymbal/src/resolver.rs index a11ded8623e65..71c47e350ce13 100644 --- a/rust/cymbal/src/resolver.rs +++ b/rust/cymbal/src/resolver.rs @@ -1,5 +1,5 @@ use crate::{ - error::Error, + error::{Error, JsResolveErr}, symbol_store::SymbolStore, types::frames::{Frame, RawFrame}, }; @@ -8,9 +8,14 @@ use sourcemap::SourceMap; #[async_trait] pub trait Resolver: Send + Sync + 'static { - // Resolvers work on a per-frame basis, so we can be clever about the order - // in which we resolve them. We also force any resolver to handle all frame - // types + // TODO - I'm not totally convinced this resolver interface shouldn't enforce + // some kind of batch-style use (take a symbol set ref and a list of frame + // explicitly? I'm not sure)... right now this interface is prone to "holding it + // wrong" type performance issues. Resolvers should maybe even encode a "submit" + // style interface, where users are expected to send them work in a stream and + // asynchronously get back results - which would permit internal batching etc. + // Idk, that's a lot of complexity. I'm a lot less happy with this interface + // than I am with the store one, though. async fn resolve(&self, raw: RawFrame, team_id: i32) -> Result; } @@ -18,25 +23,109 @@ pub struct ResolverImpl { pub store: Box, } -#[async_trait] -impl Resolver for ResolverImpl { - async fn resolve(&self, raw: RawFrame, team_id: i32) -> Result { +impl ResolverImpl { + pub fn new(store: Box) -> Self { + Self { store } + } + + async fn resolve_impl(&self, raw: &RawFrame, team_id: i32) -> Result { let source_ref = raw.source_ref()?; let source = self.store.fetch(team_id, source_ref).await?; // Since we only support js right now, this is all we do. Everything from here // is js specific let RawFrame::JavaScript(raw) = raw; - let sm = SourceMap::from_reader(source.as_slice())?; - let token = sm - .lookup_token(raw.line, raw.column) - .ok_or_else(|| Error::LookupFailed(String::from("Token not found")))?; + let sm = SourceMap::from_reader(source.as_slice()).map_err(JsResolveErr::from)?; + let token = sm.lookup_token(raw.line, raw.column).ok_or_else(|| { + // Unwrap is safe because, if this frame couldn't give us a source ref, we'd know already + JsResolveErr::TokenNotFound(raw.source_ref().unwrap().to_string(), raw.line, raw.column) + })?; + Ok((raw, token).into()) } } -impl ResolverImpl { - pub fn new(store: Box) -> Self { - Self { store } +#[async_trait] +impl Resolver for ResolverImpl { + async fn resolve(&self, raw: RawFrame, team_id: i32) -> Result { + match self.resolve_impl(&raw, team_id).await { + Ok(frame) => Ok(frame), + Err(e) => raw.try_handle_resolve_error(e), + } + } +} + +#[cfg(test)] +mod test { + use common_types::ClickHouseEvent; + use httpmock::MockServer; + + use crate::{ + config::Config, + resolver::Resolver, + symbol_store::{basic::BasicStore, caching::CachingStore}, + types::{frames::RawFrame, ErrProps}, + }; + + use super::ResolverImpl; + + const CHUNK_PATH: &str = "/static/chunk-PGUQKT6S.js"; + const MINIFIED: &[u8] = include_bytes!("../tests/static/chunk-PGUQKT6S.js"); + const MAP: &[u8] = include_bytes!("../tests/static/chunk-PGUQKT6S.js.map"); + const EXAMPLE_EXCEPTION: &str = include_str!("../tests/static/raw_ch_exception.json"); + + #[tokio::test] + async fn end_to_end_resolver_test() { + let server = MockServer::start(); + + let source_mock = server.mock(|when, then| { + when.method("GET").path(CHUNK_PATH); + then.status(200).body(MINIFIED); + }); + + let map_mock = server.mock(|when, then| { + // Our minified example source uses a relative URL, formatted like this + when.method("GET").path(format!("{}.map", CHUNK_PATH)); + then.status(200).body(MAP); + }); + + let exception: ClickHouseEvent = serde_json::from_str(EXAMPLE_EXCEPTION).unwrap(); + let props: ErrProps = serde_json::from_str(&exception.properties.unwrap()).unwrap(); + let mut test_stack: Vec = + serde_json::from_str(props.exception_stack_trace_raw.as_ref().unwrap()).unwrap(); + + // We're going to pretend out stack consists exclusively of JS frames whose source + // we have locally + test_stack.retain(|s| { + let RawFrame::JavaScript(s) = s; + s.source_url.as_ref().unwrap().contains(CHUNK_PATH) + }); + + for frame in &mut test_stack { + let RawFrame::JavaScript(frame) = frame; + // Our test data contains our /actual/ source urls - we need to swap that to localhost + // When I first wrote this test, I forgot to do this, and it took me a while to figure out + // why the test was passing before I'd even set up the mockserver - which was pretty cool, tbh + frame.source_url = Some(server.url(CHUNK_PATH).to_string()); + } + + let mut config = Config::init_with_defaults().unwrap(); + config.allow_internal_ips = true; // We're hitting localhost for the tests + + let store = BasicStore::new(&config).unwrap(); + // We're even going to assert we only hit the mockserver once for the source and sourcemap + let store = CachingStore::new(Box::new(store), 10_000_000); + + let resolver = ResolverImpl::new(Box::new(store)); + + let mut resolved_frames = Vec::new(); + for frame in test_stack { + let resolved = resolver.resolve(frame, 1).await.unwrap(); + resolved_frames.push(resolved); + } + + // The use of the caching layer is tested here - we should only have hit the server once + source_mock.assert_hits(1); + map_mock.assert_hits(1); } } diff --git a/rust/cymbal/src/symbol_store/basic.rs b/rust/cymbal/src/symbol_store/basic.rs index ce60b86357952..d55a41633c2c6 100644 --- a/rust/cymbal/src/symbol_store/basic.rs +++ b/rust/cymbal/src/symbol_store/basic.rs @@ -6,7 +6,7 @@ use tracing::{info, warn}; use crate::{ config::Config, - error::Error, + error::{Error, JsResolveErr}, metric_consts::{ BASIC_FETCHES, SOURCEMAP_BODY_FETCHES, SOURCEMAP_BODY_REF_FOUND, SOURCEMAP_HEADER_FOUND, SOURCEMAP_NOT_FOUND, SOURCE_REF_BODY_FETCHES, @@ -25,9 +25,8 @@ pub struct BasicStore { impl BasicStore { pub fn new(config: &Config) -> Result { - let mut client = reqwest::Client::builder(); - let timeout = Duration::from_secs(config.sourcemap_timeout_seconds); + let mut client = reqwest::Client::builder().timeout(timeout); if !config.allow_internal_ips { client = client.dns_resolver(Arc::new(common_dns::PublicIPv4Resolver {})); @@ -35,7 +34,7 @@ impl BasicStore { warn!("Internal IPs are allowed, this is a security risk"); } - let client = client.timeout(timeout).build()?; + let client = client.build()?; Ok(Self { client }) } @@ -46,23 +45,18 @@ impl SymbolStore for BasicStore { async fn fetch(&self, _: i32, r: SymbolSetRef) -> Result>, Error> { metrics::counter!(BASIC_FETCHES).increment(1); let SymbolSetRef::Js(sref) = r; // We only support this - let Some(sourcemap_url) = find_sourcemap_url(&self.client, sref.clone()).await? else { - warn!("No sourcemap URL found for {}", sref); - // TODO - this might not actually count as an error, and simply means we don't /need/ a sourcemap - // for a give frame, but I haven't decided how to handle that yet - return Err(Error::InvalidSourceRef(format!( - "No sourcemap URL found for {}", - sref - ))); - }; + let sourcemap_url = find_sourcemap_url(&self.client, sref.clone()).await?; fetch_source_map(&self.client, sourcemap_url) .await .map(Arc::new) } } -async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result, Error> { +async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result { info!("Fetching sourcemap from {}", start); + + // If this request fails, we cannot resolve the frame, and do not hand this error to the frames + // failure-case handling - it just didn't work. We should tell the user about it, somehow, though. let res = client.get(start).send().await?; // we use the final URL of the response in the relative case, to account for any redirects @@ -77,25 +71,30 @@ async fn find_sourcemap_url(client: &reqwest::Client, start: Url) -> Result Result, Error> { @@ -149,7 +150,7 @@ mod test { let res = find_sourcemap_url(&client, url).await.unwrap(); // We're doing relative-URL resolution here, so we have to account for that - let expected = Some(server.url("/static/chunk-PGUQKT6S.js.map").parse().unwrap()); + let expected = server.url("/static/chunk-PGUQKT6S.js.map").parse().unwrap(); assert_eq!(res, expected); mock.assert_hits(1); } diff --git a/rust/cymbal/src/symbol_store/caching.rs b/rust/cymbal/src/symbol_store/caching.rs index c6e74f1f33955..a2279b1d30076 100644 --- a/rust/cymbal/src/symbol_store/caching.rs +++ b/rust/cymbal/src/symbol_store/caching.rs @@ -43,6 +43,41 @@ impl SymbolStore for CachingStore { } metrics::counter!(STORE_CACHE_MISSES).increment(1); + /* + Implementation decision re: saving of frame resolution results: + We don't cache failed symbol set lookups, which is a risk - for errors where we're + failing at a parsing step (so we do the fetch and collect the body), we'll + end up doing a LOT of work on every single frame emitting that setref, every + time we see it. + + We could cache the error, with some short TTL - and that might get us 80% of + the benefit, but it's complicated by how we save the results of frame resolution. + + We have talked about saving the entire resolved stack trace - so mapping from a + "raw" stack to a resolved one in totality, and that's the most straightforward + route, but /if/ we instead stored every, say, {team_id, setref, raw_frame, Option}, + we get some nice benefits - we can skip the resolving step for frames we've seen before, + even if we're seeing them in a new stack, and if someone comes along and gives us + a symbol set we didn't have before, rather than invalidating every resolution + result that contained a frame referencing the new symbol set, we just invalidate + the frames specifically, and re-resolve only them (not the full stack). The downside of this + is, when we get a stack, rather than fetching one value from our store, we have + to fetch N, but this isn't the end of the world I think. + + The link between save-per-stack and save-per-frame and this decision about caching failed + symbol set lookups is that, if we save per-frame, it becomes very easy to simply cache failed + lookups, with some short TTL (think seconds/minutes) - we can drop the failed symbol set + lookup result fairly aggressively, because we know we're permanently (barring a user giving + us a new symbol set) saving the failed frame resolve, and each set only contains symbols for + so many frames. If instead we save per-stack, for every new stack that frame shows up in, + we have to re-resolve it, so we'd want to be more complete in our approach to caching failed + symbol set lookups. Basically, the more granular saving approach lets us be more confident about + how often we'll be re-resolving the same frame, and that tells us how hard we need to try and + handle the failed symbol set lookup case "perfectly". + + I'm leaning towards saving on a per-frame basis. We'd still be mapping to an error group + based on the total stack trace, of course. + */ // We hold the cache lock across the underlying fetch, so that if two threads // are racing to fetch the same item, we don't end up doing the request/data transfer twice. let res = self.inner.fetch(team_id, r.clone()).await?; diff --git a/rust/cymbal/src/symbol_store/mod.rs b/rust/cymbal/src/symbol_store/mod.rs index b52e42b0ce689..a22eb55df4bf3 100644 --- a/rust/cymbal/src/symbol_store/mod.rs +++ b/rust/cymbal/src/symbol_store/mod.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::{ + fmt::{Display, Formatter}, + sync::Arc, +}; use axum::async_trait; use reqwest::Url; @@ -18,3 +21,13 @@ pub trait SymbolStore: Send + Sync + 'static { pub enum SymbolSetRef { Js(Url), } + +// We provide this to allow for using these refs as primary keys in some arbitrary storage system, +// like s3 or a database. The result should be ~unique, per team. +impl Display for SymbolSetRef { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SymbolSetRef::Js(url) => write!(f, "{}", url), + } + } +} diff --git a/rust/cymbal/src/types/error.rs b/rust/cymbal/src/types/error.rs deleted file mode 100644 index a01b36fbfb297..0000000000000 --- a/rust/cymbal/src/types/error.rs +++ /dev/null @@ -1,4 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Clone, Error)] -pub enum Error {} diff --git a/rust/cymbal/src/types/frames.rs b/rust/cymbal/src/types/frames.rs index 395e223c7df15..aa5b4fd265f95 100644 --- a/rust/cymbal/src/types/frames.rs +++ b/rust/cymbal/src/types/frames.rs @@ -1,7 +1,10 @@ use serde::{Deserialize, Serialize}; -use sourcemap::Token; -use crate::{error::Error, langs::js::RawJSFrame, symbol_store::SymbolSetRef}; +use crate::{ + error::{Error, ResolutionError}, + langs::js::RawJSFrame, + symbol_store::SymbolSetRef, +}; // We consume a huge variety of differently shaped stack frames, which we have special-case // transformation for, to produce a single, unified representation of a frame. @@ -15,7 +18,23 @@ impl RawFrame { pub fn source_ref(&self) -> Result { let RawFrame::JavaScript(raw) = self; let id = raw.source_ref(); - id.map(SymbolSetRef::Js) + id.map(SymbolSetRef::Js).map_err(Error::from) + } + + // We expect different exception types to handle failure to resolve differently, + // so raw frames are handed the error in the event of one to see if they can + // turn it into a Frame anyway. E.g. for JS frames, if the failure is that + // we didn't manage to find a sourcemap, that indicates we should treat the + // frame as a "real" frame, and just pass it through. + pub fn try_handle_resolve_error(&self, e: Error) -> Result { + let RawFrame::JavaScript(raw) = self; + + // We unpack the general resolution error into the specific one our inner frame can + // handle + let Error::ResolutionError(ResolutionError::JavaScript(e)) = e else { + return Err(e); + }; + raw.try_handle_resolution_error(e) } } @@ -30,19 +49,3 @@ pub struct Frame { pub resolved_name: Option, // The name of the function, after symbolification pub lang: String, // The language of the frame. Always known (I guess?) } - -impl From<(RawJSFrame, Token<'_>)> for Frame { - fn from(src: (RawJSFrame, Token)) -> Self { - let (raw_frame, token) = src; - - Self { - mangled_name: raw_frame.fn_name, - line: Some(token.get_src_line()), - column: Some(token.get_src_col()), - source: token.get_source().map(String::from), - in_app: raw_frame.in_app, - resolved_name: token.get_name().map(String::from), - lang: "javascript".to_string(), - } - } -} diff --git a/rust/cymbal/src/types/mod.rs b/rust/cymbal/src/types/mod.rs index 2b129c130720e..e899ad8481591 100644 --- a/rust/cymbal/src/types/mod.rs +++ b/rust/cymbal/src/types/mod.rs @@ -3,9 +3,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use serde_json::Value; -pub mod error; pub mod frames; -pub mod stacks; // Given a Clickhouse Event's properties, we care about the contents // of only a small subset. This struct is used to give us a strongly-typed @@ -40,15 +38,13 @@ mod test { #[test] fn it_symbolifies() { - let raw: &'static str = include_str!("../../tests/static/raw_js_stack.json"); + let raw: &'static str = include_str!("../../tests/static/raw_ch_exception.json"); let raw: ClickHouseEvent = serde_json::from_str(raw).unwrap(); let props: ErrProps = serde_json::from_str(&raw.properties.unwrap()).unwrap(); - let stack_trace: Vec = + let _stack_trace: Vec = serde_json::from_str(props.exception_stack_trace_raw.as_ref().unwrap()).unwrap(); - - println!("{:?}", stack_trace); } } diff --git a/rust/cymbal/src/types/stacks.rs b/rust/cymbal/src/types/stacks.rs deleted file mode 100644 index c60198dad2a7a..0000000000000 --- a/rust/cymbal/src/types/stacks.rs +++ /dev/null @@ -1,19 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::frames::{Frame, RawFrame}; - -// The stacks we consume are a list of frames. This structure is very flexible, so that -// we support stacks of intermingled languages or types. We only case about special-cased -// handling on a per-frame basis, not a per-stack basis. All the "logic" lives at the frame -// level -#[derive(Debug, Deserialize)] -pub struct RawStack { - pub frames: Vec, -} - -// Our resolved stacks are, as you'd expect, just a vecs of frames. We might add -// "stack-level" information at some point, if we find a need. -#[derive(Debug, Serialize)] -pub struct Stack { - pub frames: Vec, -} diff --git a/rust/cymbal/tests/static/raw_js_stack.json b/rust/cymbal/tests/static/raw_ch_exception.json similarity index 100% rename from rust/cymbal/tests/static/raw_js_stack.json rename to rust/cymbal/tests/static/raw_ch_exception.json diff --git a/rust/cymbal/tests/types.rs b/rust/cymbal/tests/types.rs index 22c8b12112e9e..8262182374575 100644 --- a/rust/cymbal/tests/types.rs +++ b/rust/cymbal/tests/types.rs @@ -6,7 +6,7 @@ use serde_json::Value; #[test] fn serde_passthrough() { - let raw: &'static str = include_str!("./static/raw_js_stack.json"); + let raw: &'static str = include_str!("./static/raw_ch_exception.json"); let before: Value = serde_json::from_str(raw).unwrap(); let raw: ClickHouseEvent = serde_json::from_str(raw).unwrap();