diff --git a/rust/cymbal/src/frames/mod.rs b/rust/cymbal/src/frames/mod.rs index da2b7080fc266..7d9aed7ac4271 100644 --- a/rust/cymbal/src/frames/mod.rs +++ b/rust/cymbal/src/frames/mod.rs @@ -5,7 +5,9 @@ use serde_json::Value; use sha2::{Digest, Sha512}; use crate::{ - error::UnhandledError, langs::js::RawJSFrame, metric_consts::PER_FRAME_TIME, + error::UnhandledError, + langs::{js::RawJSFrame, python::RawPythonFrame}, + metric_consts::PER_FRAME_TIME, symbol_store::Catalog, }; @@ -15,17 +17,27 @@ pub mod resolver; // 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. #[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(untagged)] +#[serde(tag = "platform")] pub enum RawFrame { + #[serde(rename = "python")] + Python(RawPythonFrame), + #[serde(rename = "web:javascript")] JavaScript(RawJSFrame), + // TODO - remove once we're happy no clients are using this anymore + #[serde(rename = "javascript")] + LegacyJS(RawJSFrame), } impl RawFrame { pub async fn resolve(&self, team_id: i32, catalog: &Catalog) -> Result { - let RawFrame::JavaScript(raw) = self; - let frame_resolve_time = common_metrics::timing_guard(PER_FRAME_TIME, &[]); - let res = raw.resolve(team_id, catalog).await; + let (res, lang_tag) = match self { + RawFrame::JavaScript(frame) | RawFrame::LegacyJS(frame) => { + (frame.resolve(team_id, catalog).await, "javascript") + } + + RawFrame::Python(frame) => (Ok(frame.into()), "python"), + }; // The raw id of the frame is set after it's resolved let res = res.map(|mut f| { @@ -38,19 +50,26 @@ impl RawFrame { } else { frame_resolve_time.label("outcome", "success") } + .label("lang", lang_tag) .fin(); res } pub fn symbol_set_ref(&self) -> Option { - let RawFrame::JavaScript(raw) = self; - raw.source_url().map(String::from).ok() + match self { + RawFrame::JavaScript(frame) | RawFrame::LegacyJS(frame) => { + frame.source_url().map(String::from).ok() + } + RawFrame::Python(_) => None, // Python frames don't have symbol sets + } } pub fn frame_id(&self) -> String { - let RawFrame::JavaScript(raw) = self; - raw.frame_id() + match self { + RawFrame::JavaScript(raw) | RawFrame::LegacyJS(raw) => raw.frame_id(), + RawFrame::Python(raw) => raw.frame_id(), + } } } @@ -89,8 +108,8 @@ pub struct Context { #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct ContextLine { - number: u32, - line: String, + pub number: u32, + pub line: String, } impl Frame { diff --git a/rust/cymbal/src/frames/resolver.rs b/rust/cymbal/src/frames/resolver.rs index 5f2f2b478cb31..11564c4950a21 100644 --- a/rust/cymbal/src/frames/resolver.rs +++ b/rust/cymbal/src/frames/resolver.rs @@ -143,12 +143,16 @@ mod test { // 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; + let RawFrame::JavaScript(s) = s else { + return false; + }; s.source_url.as_ref().unwrap().contains(CHUNK_PATH) }); for frame in test_stack.iter_mut() { - let RawFrame::JavaScript(frame) = frame; + let RawFrame::JavaScript(frame) = frame else { + panic!("Expected a JavaScript 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 diff --git a/rust/cymbal/src/langs/js.rs b/rust/cymbal/src/langs/js.rs index f98902bddea1d..0390e37222440 100644 --- a/rust/cymbal/src/langs/js.rs +++ b/rust/cymbal/src/langs/js.rs @@ -15,7 +15,6 @@ use crate::{ // A minifed JS stack frame. Just the minimal information needed to lookup some // sourcemap for it and produce a "real" stack frame. -// TODO - how do we know if this frame is minified? If it isn't, we can skip a lot of work, but I think we have to guess? Based on whether we can get a sourcemap for it? #[derive(Debug, Clone, Deserialize, Serialize)] pub struct RawJSFrame { #[serde(flatten)] diff --git a/rust/cymbal/src/langs/mod.rs b/rust/cymbal/src/langs/mod.rs index cb71828f95d73..bb4bac0b9a69e 100644 --- a/rust/cymbal/src/langs/mod.rs +++ b/rust/cymbal/src/langs/mod.rs @@ -1 +1,2 @@ pub mod js; +pub mod python; diff --git a/rust/cymbal/src/langs/python.rs b/rust/cymbal/src/langs/python.rs new file mode 100644 index 0000000000000..cdf6405047c25 --- /dev/null +++ b/rust/cymbal/src/langs/python.rs @@ -0,0 +1,94 @@ +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha512}; + +use crate::frames::{Context, ContextLine, Frame}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RawPythonFrame { + #[serde(rename = "abs_path")] + pub path: Option, // Absolute path to the file - unused for now + pub context_line: Option, // The line of code the exception came from + pub filename: String, // The relative path of the file the context line is in + pub function: String, // The name of the function the exception came from + pub lineno: Option, // The line number of the context line + pub module: Option, // The python-import style module name the function is in + #[serde(default)] + pub pre_context: Vec, // The lines of code before the context line + #[serde(default)] + pub post_context: Vec, // The lines of code after the context line + // Default to false as sometimes not present on library code + #[serde(default)] + pub in_app: bool, // Whether the frame is in the user's code +} + +impl RawPythonFrame { + pub fn frame_id(&self) -> String { + // We don't have version info for python frames, so we rely on + // the module, function, line number and surrounding context to + // uniquely identify a frame, with the intuition being that even + // if two frames are from two different library versions, if the + // files they're in are sufficiently similar we can consider + // them to be the same frame + let mut hasher = Sha512::new(); + self.context_line + .as_ref() + .inspect(|c| hasher.update(c.as_bytes())); + hasher.update(self.filename.as_bytes()); + hasher.update(self.function.as_bytes()); + hasher.update(self.lineno.unwrap_or_default().to_be_bytes()); + self.module + .as_ref() + .inspect(|m| hasher.update(m.as_bytes())); + self.pre_context + .iter() + .chain(self.post_context.iter()) + .for_each(|line| { + hasher.update(line.as_bytes()); + }); + format!("{:x}", hasher.finalize()) + } + + pub fn get_context(&self) -> Option { + let context_line = self.context_line.as_ref()?; + let lineno = self.lineno?; + + let line = ContextLine::new(lineno, context_line); + + let before = self + .pre_context + .iter() + .enumerate() + .map(|(i, line)| ContextLine::new(lineno - i as u32 - 1, line.clone())) + .collect(); + let after = self + .post_context + .iter() + .enumerate() + .map(|(i, line)| ContextLine::new(lineno + i as u32 + 1, line.clone())) + .collect(); + Some(Context { + before, + line, + after, + }) + } +} + +impl From<&RawPythonFrame> for Frame { + fn from(raw: &RawPythonFrame) -> Self { + Frame { + raw_id: String::new(), + mangled_name: raw.function.clone(), + line: raw.lineno, + column: None, + source: Some(raw.filename.clone()), + in_app: raw.in_app, + resolved_name: Some(raw.function.clone()), + lang: "python".to_string(), + resolved: true, + resolve_failure: None, + junk_drawer: None, + context: raw.get_context(), + } + } +} diff --git a/rust/cymbal/src/types/mod.rs b/rust/cymbal/src/types/mod.rs index 3cdc401a3ebbe..9f09e5cf83cd9 100644 --- a/rust/cymbal/src/types/mod.rs +++ b/rust/cymbal/src/types/mod.rs @@ -197,7 +197,9 @@ mod test { panic!("Expected a Raw stacktrace") }; assert_eq!(frames.len(), 2); - let RawFrame::JavaScript(frame) = &frames[0]; + let RawFrame::JavaScript(frame) = &frames[0] else { + panic!("Expected a JavaScript frame") + }; assert_eq!( frame.source_url, @@ -208,7 +210,9 @@ mod test { assert_eq!(frame.location.as_ref().unwrap().line, 64); assert_eq!(frame.location.as_ref().unwrap().column, 25112); - let RawFrame::JavaScript(frame) = &frames[1]; + let RawFrame::JavaScript(frame) = &frames[1] else { + panic!("Expected a JavaScript frame") + }; assert_eq!( frame.source_url, Some("https://app-static.eu.posthog.com/static/chunk-PGUQKT6S.js".to_string()) diff --git a/rust/cymbal/tests/resolve.rs b/rust/cymbal/tests/resolve.rs index 31533c3f297aa..808afb2369a7d 100644 --- a/rust/cymbal/tests/resolve.rs +++ b/rust/cymbal/tests/resolve.rs @@ -46,12 +46,16 @@ async fn end_to_end_resolver_test() { // 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; + let RawFrame::JavaScript(s) = s else { + panic!("Expected a JavaScript frame") + }; s.source_url.as_ref().unwrap().contains(CHUNK_PATH) }); for frame in test_stack.iter_mut() { - let RawFrame::JavaScript(frame) = frame; + let RawFrame::JavaScript(frame) = frame else { + panic!("Expected a JavaScript 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 diff --git a/rust/cymbal/tests/static/python_err_props.json b/rust/cymbal/tests/static/python_err_props.json new file mode 100644 index 0000000000000..1a110b81964b1 --- /dev/null +++ b/rust/cymbal/tests/static/python_err_props.json @@ -0,0 +1,738 @@ +{ + "$exception_list": [ + { + "type": "ConnectionRefusedError", + "value": "[Errno 111] Connection refused", + "mechanism": { + "handled": true, + "type": "generic" + }, + "stacktrace": { + "type": "raw", + "frames": [ + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/connection.py", + "context_line": " conn = connection.create_connection(", + "filename": "urllib3/connection.py", + "function": "_new_conn", + "lineno": 174, + "module": "urllib3.connection", + "pre_context": [ + "", + " if self.socket_options:", + " extra_kw[\"socket_options\"] = self.socket_options", + "", + " try:" + ], + "post_context": [ + " (self._dns_host, self.port), self.timeout, **extra_kw", + " )", + "", + " except SocketTimeout:", + " raise ConnectTimeoutError(" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/util/connection.py", + "context_line": " raise err", + "filename": "urllib3/util/connection.py", + "function": "create_connection", + "lineno": 95, + "module": "urllib3.util.connection", + "pre_context": [ + " if sock is not None:", + " sock.close()", + " sock = None", + "", + " if err is not None:" + ], + "post_context": [ + "", + " raise socket.error(\"getaddrinfo returns an empty list\")", + "", + "", + "def _set_socket_options(sock, options):" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/util/connection.py", + "context_line": " sock.connect(sa)", + "filename": "urllib3/util/connection.py", + "function": "create_connection", + "lineno": 85, + "module": "urllib3.util.connection", + "pre_context": [ + "", + " if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:", + " sock.settimeout(timeout)", + " if source_address:", + " sock.bind(source_address)" + ], + "post_context": [ + " return sock", + "", + " except socket.error as e:", + " err = e", + " if sock is not None:" + ], + "in_app": false + } + ] + } + }, + { + "type": "NewConnectionError", + "value": ": Failed to establish a new connection: [Errno 111] Connection refused", + "mechanism": { + "handled": true, + "type": "generic" + }, + "module": "urllib3.exceptions", + "stacktrace": { + "type": "raw", + "frames": [ + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/connectionpool.py", + "context_line": " httplib_response = self._make_request(", + "filename": "urllib3/connectionpool.py", + "function": "urlopen", + "lineno": 715, + "module": "urllib3.connectionpool", + "pre_context": [ + " )", + " if is_new_proxy_conn and http_tunnel_required:", + " self._prepare_proxy(conn)", + "", + " # Make the request on the httplib connection object." + ], + "post_context": [ + " conn,", + " method,", + " url,", + " timeout=timeout_obj,", + " body=body," + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/connectionpool.py", + "context_line": " conn.request(method, url, **httplib_request_kw)", + "filename": "urllib3/connectionpool.py", + "function": "_make_request", + "lineno": 416, + "module": "urllib3.connectionpool", + "pre_context": [ + " # urllib3.request. It also calls makefile (recv) on the socket.", + " try:", + " if chunked:", + " conn.request_chunked(method, url, **httplib_request_kw)", + " else:" + ], + "post_context": [ + "", + " # We are swallowing BrokenPipeError (errno.EPIPE) since the server is", + " # legitimately able to close the connection after sending a valid response.", + " # With this behaviour, the received response is still readable.", + " except BrokenPipeError:" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/connection.py", + "context_line": " super(HTTPConnection, self).request(method, url, body=body, headers=headers)", + "filename": "urllib3/connection.py", + "function": "request", + "lineno": 244, + "module": "urllib3.connection", + "pre_context": [ + " else:", + " # Avoid modifying the headers passed into .request()", + " headers = headers.copy()", + " if \"user-agent\" not in (six.ensure_str(k.lower()) for k in headers):", + " headers[\"User-Agent\"] = _get_default_user_agent()" + ], + "post_context": [ + "", + " def request_chunked(self, method, url, body=None, headers=None):", + " \"\"\"", + " Alternative to the common request method, which sends the", + " body with chunked encoding and not as one block" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/usr/local/lib/python3.11/http/client.py", + "context_line": " self._send_request(method, url, body, headers, encode_chunked)", + "filename": "http/client.py", + "function": "request", + "lineno": 1298, + "module": "http.client", + "pre_context": [ + " self._send_output(message_body, encode_chunked=encode_chunked)", + "", + " def request(self, method, url, body=None, headers={}, *,", + " encode_chunked=False):", + " \"\"\"Send a complete request to the server.\"\"\"" + ], + "post_context": [ + "", + " def _send_request(self, method, url, body, headers, encode_chunked):", + " # Honor explicitly requested Host: and Accept-Encoding: headers.", + " header_names = frozenset(k.lower() for k in headers)", + " skips = {}" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/usr/local/lib/python3.11/http/client.py", + "context_line": " self.endheaders(body, encode_chunked=encode_chunked)", + "filename": "http/client.py", + "function": "_send_request", + "lineno": 1344, + "module": "http.client", + "pre_context": [ + " self.putheader(hdr, value)", + " if isinstance(body, str):", + " # RFC 2616 Section 3.7.1 says that text default has a", + " # default charset of iso-8859-1.", + " body = _encode(body, 'body')" + ], + "post_context": [ + "", + " def getresponse(self):", + " \"\"\"Get the response from the server.", + "", + " If the HTTPConnection is in the correct state, returns an" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/usr/local/lib/python3.11/http/client.py", + "context_line": " self._send_output(message_body, encode_chunked=encode_chunked)", + "filename": "http/client.py", + "function": "endheaders", + "lineno": 1293, + "module": "http.client", + "pre_context": [ + " \"\"\"", + " if self.__state == _CS_REQ_STARTED:", + " self.__state = _CS_REQ_SENT", + " else:", + " raise CannotSendHeader()" + ], + "post_context": [ + "", + " def request(self, method, url, body=None, headers={}, *,", + " encode_chunked=False):", + " \"\"\"Send a complete request to the server.\"\"\"", + " self._send_request(method, url, body, headers, encode_chunked)" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/usr/local/lib/python3.11/http/client.py", + "context_line": " self.send(msg)", + "filename": "http/client.py", + "function": "_send_output", + "lineno": 1052, + "module": "http.client", + "pre_context": [ + " A message_body may be specified, to be appended to the request.", + " \"\"\"", + " self._buffer.extend((b\"\", b\"\"))", + " msg = b\"\\r\\n\".join(self._buffer)", + " del self._buffer[:]" + ], + "post_context": [ + "", + " if message_body is not None:", + "", + " # create a consistent interface to message_body", + " if hasattr(message_body, 'read'):" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/usr/local/lib/python3.11/http/client.py", + "context_line": " self.connect()", + "filename": "http/client.py", + "function": "send", + "lineno": 990, + "module": "http.client", + "pre_context": [ + " file-like object that supports a .read() method, or an iterable object.", + " \"\"\"", + "", + " if self.sock is None:", + " if self.auto_open:" + ], + "post_context": [ + " else:", + " raise NotConnected()", + "", + " if self.debuglevel > 0:", + " print(\"send:\", repr(data))" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/connection.py", + "context_line": " conn = self._new_conn()", + "filename": "urllib3/connection.py", + "function": "connect", + "lineno": 205, + "module": "urllib3.connection", + "pre_context": [ + " self._tunnel()", + " # Mark this connection as not reusable", + " self.auto_open = 0", + "", + " def connect(self):" + ], + "post_context": [ + " self._prepare_conn(conn)", + "", + " def putrequest(self, method, url, *args, **kwargs):", + " \"\"\" \"\"\"", + " # Empty docstring because the indentation of CPython's implementation" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/connection.py", + "context_line": " raise NewConnectionError(", + "filename": "urllib3/connection.py", + "function": "_new_conn", + "lineno": 186, + "module": "urllib3.connection", + "pre_context": [ + " \"Connection to %s timed out. (connect timeout=%s)\"", + " % (self.host, self.timeout),", + " )", + "", + " except SocketError as e:" + ], + "post_context": [ + " self, \"Failed to establish a new connection: %s\" % e", + " )", + "", + " return conn", + "" + ], + "in_app": false + } + ] + } + }, + { + "type": "MaxRetryError", + "value": "HTTPConnectionPool(host='clickhouse', port=8123): Max retries exceeded with url: / (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 111] Connection refused'))", + "mechanism": { + "handled": true, + "type": "generic" + }, + "module": "urllib3.exceptions", + "stacktrace": { + "type": "raw", + "frames": [ + { + "platform": "python", + "abs_path": "/python-runtime/requests/adapters.py", + "context_line": " resp = conn.urlopen(", + "filename": "requests/adapters.py", + "function": "send", + "lineno": 564, + "module": "requests.adapters", + "pre_context": [ + " pass", + " else:", + " timeout = TimeoutSauce(connect=timeout, read=timeout)", + "", + " try:" + ], + "post_context": [ + " method=request.method,", + " url=url,", + " body=request.body,", + " headers=request.headers,", + " redirect=False," + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/connectionpool.py", + "context_line": " retries = retries.increment(", + "filename": "urllib3/connectionpool.py", + "function": "urlopen", + "lineno": 799, + "module": "urllib3.connectionpool", + "pre_context": [ + " elif isinstance(e, (SocketError, NewConnectionError)) and self.proxy:", + " e = ProxyError(\"Cannot connect to proxy.\", e)", + " elif isinstance(e, (SocketError, HTTPException)):", + " e = ProtocolError(\"Connection aborted.\", e)", + "" + ], + "post_context": [ + " method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]", + " )", + " retries.sleep()", + "", + " # Keep track of the error for the retry warning." + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/urllib3/util/retry.py", + "context_line": " raise MaxRetryError(_pool, url, error or ResponseError(cause))", + "filename": "urllib3/util/retry.py", + "function": "increment", + "lineno": 592, + "module": "urllib3.util.retry", + "pre_context": [ + " other=other,", + " history=history,", + " )", + "", + " if new_retry.is_exhausted():" + ], + "post_context": [ + "", + " log.debug(\"Incremented Retry for (url='%s'): %r\", url, new_retry)", + "", + " return new_retry", + "" + ], + "in_app": false + } + ] + } + }, + { + "type": "ConnectionError", + "value": "HTTPConnectionPool(host='clickhouse', port=8123): Max retries exceeded with url: / (Caused by NewConnectionError(': Failed to establish a new connection: [Errno 111] Connection refused'))", + "mechanism": { + "handled": true, + "type": "generic" + }, + "module": "requests.exceptions", + "stacktrace": { + "type": "raw", + "frames": [ + { + "platform": "python", + "filename": "manage.py", + "in_app": true, + "function": "" + }, + { + "platform": "python", + "filename": "manage.py", + "in_app": true, + "function": "main" + }, + { + "platform": "python", + "abs_path": "/python-runtime/django/core/management/__init__.py", + "context_line": " utility.execute()", + "filename": "django/core/management/__init__.py", + "function": "execute_from_command_line", + "lineno": 442, + "module": "django.core.management", + "pre_context": [ + "", + "", + "def execute_from_command_line(argv=None):", + " \"\"\"Run a ManagementUtility.\"\"\"", + " utility = ManagementUtility(argv)" + ], + "post_context": [], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/django/core/management/__init__.py", + "context_line": " self.fetch_command(subcommand).run_from_argv(self.argv)", + "filename": "django/core/management/__init__.py", + "function": "execute", + "lineno": 436, + "module": "django.core.management", + "pre_context": [ + " elif subcommand == \"version\" or self.argv[1:] == [\"--version\"]:", + " sys.stdout.write(django.get_version() + \"\\n\")", + " elif self.argv[1:] in ([\"--help\"], [\"-h\"]):", + " sys.stdout.write(self.main_help_text() + \"\\n\")", + " else:" + ], + "post_context": [ + "", + "", + "def execute_from_command_line(argv=None):", + " \"\"\"Run a ManagementUtility.\"\"\"", + " utility = ManagementUtility(argv)" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/django/core/management/base.py", + "context_line": " self.execute(*args, **cmd_options)", + "filename": "django/core/management/base.py", + "function": "run_from_argv", + "lineno": 412, + "module": "django.core.management.base", + "pre_context": [ + " cmd_options = vars(options)", + " # Move positional args out of options to mimic legacy optparse", + " args = cmd_options.pop(\"args\", ())", + " handle_default_options(options)", + " try:" + ], + "post_context": [ + " except CommandError as e:", + " if options.traceback:", + " raise", + "", + " # SystemCheckError takes care of its own formatting." + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/django/core/management/base.py", + "context_line": " output = self.handle(*args, **options)", + "filename": "django/core/management/base.py", + "function": "execute", + "lineno": 458, + "module": "django.core.management.base", + "pre_context": [ + " self.check()", + " else:", + " self.check(tags=self.requires_system_checks)", + " if self.requires_migrations_checks:", + " self.check_migrations()" + ], + "post_context": [ + " if output:", + " if self.output_transaction:", + " connection = connections[options.get(\"database\", DEFAULT_DB_ALIAS)]", + " output = \"%s\\n%s\\n%s\" % (", + " self.style.SQL_KEYWORD(connection.ops.start_transaction_sql())," + ], + "in_app": false + }, + { + "platform": "python", + "filename": "posthog/management/commands/migrate_clickhouse.py", + "in_app": true, + "function": "handle" + }, + { + "platform": "python", + "filename": "posthog/management/commands/migrate_clickhouse.py", + "in_app": true, + "function": "migrate" + }, + { + "platform": "python", + "abs_path": "/python-runtime/infi/clickhouse_orm/database.py", + "context_line": " self.db_exists = self._is_existing_database()", + "filename": "/python-runtime/infi/clickhouse_orm/database.py", + "function": "__init__", + "lineno": 118, + "module": "infi.clickhouse_orm.database", + "pre_context": [ + " self.request_session.auth = (username, password or '')", + " self.log_statements = log_statements", + " self.randomize_replica_paths = randomize_replica_paths", + " self.settings = {}", + " self.db_exists = False # this is required before running _is_existing_database" + ], + "post_context": [ + " if readonly:", + " if not self.db_exists:", + " raise DatabaseException('Database does not exist, and cannot be created under readonly connection')", + " self.connection_readonly = self._is_connection_readonly()", + " self.readonly = True" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/infi/clickhouse_orm/database.py", + "context_line": " r = self._send(\"SELECT count() FROM system.databases WHERE name = '%s'\" % self.db_name)", + "filename": "/python-runtime/infi/clickhouse_orm/database.py", + "function": "_is_existing_database", + "lineno": 440, + "module": "infi.clickhouse_orm.database", + "pre_context": [ + " # :TRICKY: Altinity cloud uses a non-numeric suffix for the version, which this removes.", + " ver = re.sub(r\"[.\\D]+$\", '', ver)", + " return tuple(int(n) for n in ver.split('.')) if as_tuple else ver", + "", + " def _is_existing_database(self):" + ], + "post_context": [ + " return r.text.strip() == '1'", + "", + " def _is_connection_readonly(self):", + " r = self._send(\"SELECT value FROM system.settings WHERE name = 'readonly'\")", + " return r.text.strip() != '0'" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/infi/clickhouse_orm/database.py", + "context_line": " r = self.request_session.post(self.db_url, params=params, data=data, stream=stream, timeout=self.timeout)", + "filename": "/python-runtime/infi/clickhouse_orm/database.py", + "function": "_send", + "lineno": 391, + "module": "infi.clickhouse_orm.database", + "pre_context": [ + " if isinstance(data, str):", + " data = data.encode('utf-8')", + " if self.log_statements:", + " logger.info(data)", + " params = self._build_params(settings)" + ], + "post_context": [ + " if r.status_code != 200:", + " raise ServerError(r.text)", + " return r", + "", + " def _build_params(self, settings):" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/requests/sessions.py", + "context_line": " return self.request(\"POST\", url, data=data, json=json, **kwargs)", + "filename": "requests/sessions.py", + "function": "post", + "lineno": 637, + "module": "requests.sessions", + "pre_context": [ + " :param json: (optional) json to send in the body of the :class:`Request`.", + " :param \\*\\*kwargs: Optional arguments that ``request`` takes.", + " :rtype: requests.Response", + " \"\"\"", + "" + ], + "post_context": [ + "", + " def put(self, url, data=None, **kwargs):", + " r\"\"\"Sends a PUT request. Returns :class:`Response` object.", + "", + " :param url: URL for the new :class:`Request` object." + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/requests/sessions.py", + "context_line": " resp = self.send(prep, **send_kwargs)", + "filename": "requests/sessions.py", + "function": "request", + "lineno": 589, + "module": "requests.sessions", + "pre_context": [ + " send_kwargs = {", + " \"timeout\": timeout,", + " \"allow_redirects\": allow_redirects,", + " }", + " send_kwargs.update(settings)" + ], + "post_context": [ + "", + " return resp", + "", + " def get(self, url, **kwargs):", + " r\"\"\"Sends a GET request. Returns :class:`Response` object." + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/requests/sessions.py", + "context_line": " r = adapter.send(request, **kwargs)", + "filename": "requests/sessions.py", + "function": "send", + "lineno": 703, + "module": "requests.sessions", + "pre_context": [ + "", + " # Start time (approximately) of the request", + " start = preferred_clock()", + "", + " # Send the request" + ], + "post_context": [ + "", + " # Total elapsed time of the request (approximately)", + " elapsed = preferred_clock() - start", + " r.elapsed = timedelta(seconds=elapsed)", + "" + ], + "in_app": false + }, + { + "platform": "python", + "abs_path": "/python-runtime/requests/adapters.py", + "context_line": " raise ConnectionError(e, request=request)", + "filename": "requests/adapters.py", + "function": "send", + "lineno": 597, + "module": "requests.adapters", + "pre_context": [ + "", + " if isinstance(e.reason, _SSLError):", + " # This branch is for urllib3 v1.22 and later.", + " raise SSLError(e, request=request)", + "" + ], + "post_context": [ + "", + " except ClosedPoolError as e:", + " raise ConnectionError(e, request=request)", + "", + " except _ProxyError as e:" + ], + "in_app": false + } + ] + } + } + ], + "$exception_message": "[Errno 111] Connection refused", + "$exception_type": "ConnectionRefusedError", + "$plugins_failed": [], + "$geoip_disable": true, + "$lib_version__patch": 6, + "$lib_version": "3.6.6", + "$plugins_succeeded": ["posthog-semver-flattener (12449)"], + "$exception_personURL": "https://us.i.posthog.com/project/sTMFPsFhdP1Ssg/person/python-exceptions", + "$ip": "185.140.230.43", + "$lib_version__minor": 6, + "$lib": "posthog-python", + "$lib_version__major": 3, + "$exception_fingerprint": ["ConnectionRefusedError", "[Errno 111] Connection refused", "_new_conn"] +} diff --git a/rust/cymbal/tests/static/raw_ch_exception.json b/rust/cymbal/tests/static/raw_ch_exception.json deleted file mode 100644 index 382926b869708..0000000000000 --- a/rust/cymbal/tests/static/raw_ch_exception.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "uuid": "01925277-f069-7eb2-9236-bf86c4c3ed7d", - "event": "$exception", - "properties": "{\n \"$os\": \"Mac OS X\",\n \"$os_version\": \"10.15.7\",\n \"$browser\": \"Chrome\",\n \"$device_type\": \"Desktop\",\n \"$current_url\": \"https://us.posthog.com/project/2/feature_flags/50888\",\n \"$host\": \"us.posthog.com\",\n \"$pathname\": \"/project/2/feature_flags/50888\",\n \"$raw_user_agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36\",\n \"$browser_version\": 129,\n \"$browser_language\": \"en-US\",\n \"$screen_height\": 982,\n \"$screen_width\": 1512,\n \"$viewport_height\": 882,\n \"$viewport_width\": 1086,\n \"$lib\": \"web\",\n \"$lib_version\": \"1.166.0\",\n \"$insert_id\": \"xa7aywslwzwsilxn\",\n \"$time\": 1727960445.033,\n \"distinct_id\": \"UAnHBWWjwZPH8Ky85ZJ4BfYpMD45CvIVohyeJk97Wy9\",\n \"$device_id\": \"018fc4d4-4fc5-7e78-843d-f2d478a82f74\",\n \"$console_log_recording_enabled_server_side\": true,\n \"$session_recording_network_payload_capture\": {\n \"capturePerformance\": {\n \"network_timing\": true,\n \"web_vitals\": true,\n \"web_vitals_allowed_metrics\": null\n },\n \"recordBody\": true,\n \"recordHeaders\": true\n },\n \"$session_recording_canvas_recording\": {\n \"enabled\": false,\n \"fps\": null,\n \"quality\": null\n },\n \"$replay_sample_rate\": null,\n \"$replay_minimum_duration\": 2000,\n \"$initial_person_info\": {\n \"r\": \"$direct\",\n \"u\": \"https://us.posthog.com/project/84498/replay/01922131-3fc4-7c41-bc7a-fc4cd3cb5b12\"\n },\n \"$active_feature_flags\": [\n \"artificial-hog\",\n \"alerts\",\n \"hog-functions\",\n \"heatmaps-ui\",\n \"web-analytics-lcp-score\",\n \"error-tracking\"\n ],\n \"$feature/artificial-hog\": true,\n \"$feature/alerts\": true,\n \"$feature/hog-functions\": true,\n \"$feature/purchase-credits\": false,\n \"$feature/environments\": false,\n \"$feature/test-trend-juraj-1\": \"test\",\n \"$feature/onboarding-dashboard-templates\": \"control\",\n \"$feature_flag_payloads\": {\n \"changelog-notification\": \"{\\n \\\"notificationDate\\\": \\\"2024-08-26\\\", \\n \\\"markdown\\\": \\\"New this week: [React Native session replays](https://posthog.com/changelog/2024#react-native-session-replay-now-in-beta), [link Vitally to your data warehouse](https://posthog.com/changelog/2024#vitally-now-supported-as-a-warehouse-source), and [more](https://posthog.com/changelog)!\\\"\\n}\",\n \"string-payload\": \"true\",\n \"survey-test-target\": \"[1]\",\n \"product-ctas\": \"{\\\"title\\\": \\\"View pricing\\\", \\\"url\\\": \\\"pricing\\\"}\",\n \"available-plans\": \"{\\n \\\"planKeys\\\": {\\n \\\"marketing-website\\\": [\\\"123\\\", \\\"456\\\"],\\n \\\"app\\\": [\\\"789\\\", \\\"101112\\\"]\\n }\\n}\"\n },\n \"$user_id\": \"UAnHBWWjwZPH8Ky85ZJ4BfYpMD45CvIVohyeJk97Wy9\",\n \"is_demo_project\": false,\n \"$groups\": {\n \"project\": \"fc445b88-e2c4-488e-bb52-aa80cd7918c9\",\n \"organization\": \"4dc8564d-bd82-1065-2f40-97f7c50f67cf\",\n \"customer\": \"cus_IK2DWsWVn2ZM16\",\n \"instance\": \"https://us.posthog.com\"\n },\n \"realm\": \"cloud\",\n \"email_service_available\": true,\n \"slack_service_available\": true,\n \"commit_sha\": \"a71b50163b\",\n \"$autocapture_disabled_server_side\": false,\n \"$web_vitals_enabled_server_side\": true,\n \"$web_vitals_allowed_metrics\": null,\n \"$exception_capture_endpoint_suffix\": \"/e/\",\n \"$exception_capture_enabled_server_side\": true,\n \"has_billing_plan\": true,\n \"customer_deactivated\": false,\n \"current_total_amount_usd\": \"0.00\",\n \"custom_limits_usd.surveys\": 500,\n \"percentage_usage.product_analytics\": 0,\n \"current_amount_usd.product_analytics\": \"0.00\",\n \"unit_amount_usd.product_analytics\": null,\n \"usage_limit.product_analytics\": null,\n \"current_usage.product_analytics\": 13531521,\n \"projected_usage.product_analytics\": 225037109,\n \"free_allocation.product_analytics\": 0,\n \"percentage_usage.session_replay\": 0,\n \"current_amount_usd.session_replay\": \"0.00\",\n \"unit_amount_usd.session_replay\": null,\n \"usage_limit.session_replay\": null,\n \"current_usage.session_replay\": 70886,\n \"projected_usage.session_replay\": 1268192,\n \"free_allocation.session_replay\": 0,\n \"percentage_usage.feature_flags\": 0,\n \"current_amount_usd.feature_flags\": \"0.00\",\n \"unit_amount_usd.feature_flags\": null,\n \"usage_limit.feature_flags\": null,\n \"current_usage.feature_flags\": 16287759,\n \"projected_usage.feature_flags\": 461418955,\n \"free_allocation.feature_flags\": 0,\n \"percentage_usage.surveys\": 0,\n \"current_amount_usd.surveys\": \"0.00\",\n \"unit_amount_usd.surveys\": null,\n \"usage_limit.surveys\": null,\n \"current_usage.surveys\": 110,\n \"projected_usage.surveys\": 3047,\n \"free_allocation.surveys\": 0,\n \"percentage_usage.data_warehouse\": 0,\n \"current_amount_usd.data_warehouse\": \"0.00\",\n \"unit_amount_usd.data_warehouse\": null,\n \"usage_limit.data_warehouse\": null,\n \"current_usage.data_warehouse\": 109714956,\n \"projected_usage.data_warehouse\": 2121050491,\n \"free_allocation.data_warehouse\": 0,\n \"percentage_usage.integrations\": 0,\n \"current_amount_usd.integrations\": null,\n \"unit_amount_usd.integrations\": null,\n \"usage_limit.integrations\": 0,\n \"current_usage.integrations\": 0,\n \"projected_usage.integrations\": 0,\n \"free_allocation.integrations\": 0,\n \"percentage_usage.platform_and_support\": 0,\n \"current_amount_usd.platform_and_support\": null,\n \"unit_amount_usd.platform_and_support\": null,\n \"usage_limit.platform_and_support\": 0,\n \"current_usage.platform_and_support\": 0,\n \"projected_usage.platform_and_support\": 0,\n \"free_allocation.platform_and_support\": 0,\n \"billing_period_start\": {\n \"$L\": \"en\",\n \"$d\": {},\n \"$y\": 2024,\n \"$M\": 9,\n \"$D\": 2,\n \"$W\": 3,\n \"$H\": 17,\n \"$m\": 9,\n \"$s\": 56,\n \"$ms\": 0,\n \"$x\": {},\n \"$isDayjsObject\": true\n },\n \"billing_period_end\": {\n \"$L\": \"en\",\n \"$d\": {},\n \"$y\": 2024,\n \"$M\": 10,\n \"$D\": 2,\n \"$W\": 6,\n \"$H\": 16,\n \"$m\": 9,\n \"$s\": 56,\n \"$ms\": 0,\n \"$x\": {},\n \"$isDayjsObject\": true\n },\n \"$surveys_activated\": [\"0190fe51-92a0-0000-4ba3-f85f5f0ef78f\"],\n \"custom_limits_usd.data_warehouse\": 0,\n \"custom_limits_usd.feature_flags\": 0,\n \"custom_limits_usd.session_replay\": 0,\n \"custom_limits_usd.product_analytics\": 0,\n \"$referrer\": \"https://us.posthog.com/project/2/error_tracking\",\n \"$referring_domain\": \"us.posthog.com\",\n \"$exception_type\": \"Error\",\n \"$exception_message\": \"Unexpected usage\\n\\nError: Unexpected usage\\n at n.loadForeignModule (https://app-static-prod.posthog.com/static/chunk-PGUQKT6S.js:64:15003)\\n at https://app-static-prod.posthog.com/static/chunk-PGUQKT6S.js:64:25112\",\n \"$exception_stack_trace_raw\": \"[{\\\"filename\\\":\\\"https://app-static-prod.posthog.com/static/chunk-3LWGKVZR.js\\\",\\\"function\\\":\\\"r\\\",\\\"in_app\\\":true,\\\"lineno\\\":19,\\\"colno\\\":2044},{\\\"filename\\\":\\\"https://app-static-prod.posthog.com/static/chunk-PGUQKT6S.js\\\",\\\"function\\\":\\\"?\\\",\\\"in_app\\\":true,\\\"lineno\\\":3,\\\"colno\\\":12},{\\\"filename\\\":\\\"https://app-static-prod.posthog.com/static/chunk-PGUQKT6S.js\\\",\\\"function\\\":\\\"?\\\",\\\"in_app\\\":true,\\\"lineno\\\":64,\\\"colno\\\":25112},{\\\"filename\\\":\\\"https://app-static-prod.posthog.com/static/chunk-PGUQKT6S.js\\\",\\\"function\\\":\\\"n.loadForeignModule\\\",\\\"in_app\\\":true,\\\"lineno\\\":64,\\\"colno\\\":15003}]\",\n \"$exception_level\": \"error\",\n \"$exception_source\": \"https://app-static-prod.posthog.com/static/chunk-3LWGKVZR.js\",\n \"$exception_lineno\": 19,\n \"$exception_colno\": 2067,\n \"$exception_personURL\": \"https://us.posthog.com/project/sTMFPsFhdP1Ssg/person/UAnHBWWjwZPH8Ky85ZJ4BfYpMD45CvIVohyeJk97Wy9\",\n \"token\": \"sYTIHGHJKJHGFHJG\",\n \"$session_id\": \"01925255-fc87-771a-8aef-607444f71b4c\",\n \"$window_id\": \"01925273-b871-75c4-bd8f-54a978ead4f9\",\n \"$lib_custom_api_host\": \"https://internal-t.posthog.com\",\n \"$is_identified\": true,\n \"$lib_rate_limit_remaining_tokens\": 98.02,\n \"$set_once\": {\n \"$initial_os\": \"Mac OS X\",\n \"$initial_os_version\": \"10.15.7\",\n \"$initial_browser\": \"Chrome\",\n \"$initial_device_type\": \"Desktop\",\n \"$initial_current_url\": \"https://us.posthog.com/project/84498/replay/01922131-3fc4-7c41-bc7a-fc4cd3cb5b12\",\n \"$initial_pathname\": \"/project/84498/replay/01922131-3fc4-7c41-bc7a-fc4cd3cb5b12\",\n \"$initial_browser_version\": 129,\n \"$initial_referrer\": \"$direct\",\n \"$initial_referring_domain\": \"$direct\",\n \"$initial_geoip_city_name\": \"London\",\n \"$initial_geoip_city_confidence\": null,\n \"$initial_geoip_subdivision_2_name\": null,\n \"$initial_geoip_subdivision_2_code\": null,\n \"$initial_geoip_subdivision_2_confidence\": null,\n \"$initial_geoip_subdivision_1_name\": \"England\",\n \"$initial_geoip_subdivision_1_code\": \"ENG\",\n \"$initial_geoip_subdivision_1_confidence\": null,\n \"$initial_geoip_country_name\": \"United Kingdom\",\n \"$initial_geoip_country_code\": \"GB\",\n \"$initial_geoip_country_confidence\": null,\n \"$initial_geoip_continent_name\": \"Europe\",\n \"$initial_geoip_continent_code\": \"EU\",\n \"$initial_geoip_postal_code\": \"EC4R\",\n \"$initial_geoip_postal_code_confidence\": null,\n \"$initial_geoip_latitude\": 51.5088,\n \"$initial_geoip_longitude\": -0.093,\n \"$initial_geoip_accuracy_radius\": 20,\n \"$initial_geoip_time_zone\": \"Europe/London\",\n \"$initial_host\": \"us.posthog.com\"\n },\n \"$ip\": \"62.23.207.10\",\n \"$set\": {\n \"$os\": \"Mac OS X\",\n \"$os_version\": \"10.15.7\",\n \"$browser\": \"Chrome\",\n \"$device_type\": \"Desktop\",\n \"$current_url\": \"https://us.posthog.com/project/2/feature_flags/50888\",\n \"$pathname\": \"/project/2/feature_flags/50888\",\n \"$browser_version\": 129,\n \"$referrer\": \"https://us.posthog.com/project/2/error_tracking\",\n \"$referring_domain\": \"us.posthog.com\",\n \"$geoip_city_name\": \"London\",\n \"$geoip_city_confidence\": null,\n \"$geoip_subdivision_2_name\": null,\n \"$geoip_subdivision_2_code\": null,\n \"$geoip_subdivision_2_confidence\": null,\n \"$geoip_subdivision_1_name\": \"England\",\n \"$geoip_subdivision_1_code\": \"ENG\",\n \"$geoip_subdivision_1_confidence\": null,\n \"$geoip_country_name\": \"United Kingdom\",\n \"$geoip_country_code\": \"GB\",\n \"$geoip_country_confidence\": null,\n \"$geoip_continent_name\": \"Europe\",\n \"$geoip_continent_code\": \"EU\",\n \"$geoip_postal_code\": \"EC4R\",\n \"$geoip_postal_code_confidence\": null,\n \"$geoip_latitude\": 51.5088,\n \"$geoip_longitude\": -0.093,\n \"$geoip_accuracy_radius\": 20,\n \"$geoip_time_zone\": \"Europe/London\"\n },\n \"$sent_at\": \"2024-10-03T13:00:45.158000+00:00\",\n \"$geoip_city_name\": \"London\",\n \"$geoip_city_confidence\": null,\n \"$geoip_country_name\": \"United Kingdom\",\n \"$geoip_country_code\": \"GB\",\n \"$geoip_country_confidence\": null,\n \"$geoip_continent_name\": \"Europe\",\n \"$geoip_continent_code\": \"EU\",\n \"$geoip_postal_code\": \"EC4R\",\n \"$geoip_postal_code_confidence\": null,\n \"$geoip_latitude\": 51.5088,\n \"$geoip_longitude\": -0.093,\n \"$geoip_accuracy_radius\": 20,\n \"$geoip_time_zone\": \"Europe/London\",\n \"$geoip_subdivision_1_code\": \"ENG\",\n \"$geoip_subdivision_1_name\": \"England\",\n \"$geoip_subdivision_1_confidence\": null,\n \"$lib_version__major\": 1,\n \"$lib_version__minor\": 166,\n \"$lib_version__patch\": 0,\n \"$group_2\": \"fc445b88-e2c4-488e-bb52-aa80cd7918c9\",\n \"$group_0\": \"4dc8564d-bd82-1065-2f40-97f7c50f67cf\",\n \"$group_3\": \"cus_IK2DWsWVn2ZM16\",\n \"$group_1\": \"https://us.posthog.com\",\n \"$exception_fingerprint\": [\n \"Error\",\n \"Unexpected usage\\n\\nError: Unexpected usage\\n at n.loadForeignModule (https://app-static-prod.posthog.com/static/chunk-PGUQKT6S.js:64:15003)\\n at https://app-static-prod.posthog.com/static/chunk-PGUQKT6S.js:64:25112\",\n \"r\"\n ]\n }", - "timestamp": "2024-10-03T06:00:45.069000-07:00", - "team_id": 2, - "project_id": 2, - "distinct_id": "UAnHBWWjwZPH8Ky85ZJ4BfYpMD45CvIVohyeJk97Wy9", - "elements_chain": "", - "created_at": "2024-10-03T06:01:25.266000-07:00", - "person_mode": "full" -} diff --git a/rust/cymbal/tests/static/raw_ch_exception_list.json b/rust/cymbal/tests/static/raw_ch_exception_list.json index 78b961dfdbcf7..bc84058e5d461 100644 --- a/rust/cymbal/tests/static/raw_ch_exception_list.json +++ b/rust/cymbal/tests/static/raw_ch_exception_list.json @@ -1,7 +1,7 @@ { "uuid": "019295b1-519f-71a6-aacf-c97b5db73696", "event": "$exception", - "properties": "{\"$os\":\"Mac OS X\",\"$os_version\":\"10.15.7\",\"$browser\":\"Chrome\",\"$device_type\":\"Desktop\",\"$current_url\":\"https://eu.posthog.com/project/12557/feature_flags/31624\",\"$host\":\"eu.posthog.com\",\"$pathname\":\"/project/12557/feature_flags/31624\",\"$raw_user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36\",\"$browser_version\":129,\"$browser_language\":\"en-GB\",\"$screen_height\":1080,\"$screen_width\":1920,\"$viewport_height\":934,\"$viewport_width\":1920,\"$lib\":\"web\",\"$lib_version\":\"1.170.1\",\"$insert_id\":\"xjfjg606eo2x7n4x\",\"$time\":1729088278.943,\"distinct_id\":\"pQC9X9Fe7BPzJXVxpY0fx37UwFOCd1vXHzh8rjUPv1G\",\"$device_id\":\"018ccedb-d598-79bb-94e0-4751a3b956f4\",\"$console_log_recording_enabled_server_side\":true,\"$autocapture_disabled_server_side\":false,\"$web_vitals_enabled_server_side\":true,\"$exception_capture_enabled_server_side\":true,\"$exception_capture_endpoint\":\"/e/\",\"realm\":\"cloud\",\"email_service_available\":true,\"slack_service_available\":true,\"commit_sha\":\"bafa32953e\",\"$user_id\":\"pQC9X9Fe7BPzJXVxpY0fx37UwFOCd1vXHzh8rjUPv1G\",\"is_demo_project\":false,\"$groups\":{\"project\":\"018c1057-288d-0000-93bb-3bd44c845f22\",\"organization\":\"018afaa6-8b2e-0000-2311-d58d2df832ad\",\"customer\":\"cus_P5B9QmoUKLAUlx\",\"instance\":\"https://eu.posthog.com\"},\"has_billing_plan\":true,\"$referrer\":\"$direct\",\"$referring_domain\":\"$direct\",\"$session_recording_start_reason\":\"session_id_changed\",\"$exception_list\":[{\"type\":\"UnhandledRejection\",\"value\":\"Unexpected usage\",\"stacktrace\":{\"type\": \"raw\", \"frames\":[{\"filename\":\"https://app-static.eu.posthog.com/static/chunk-PGUQKT6S.js\",\"function\":\"?\",\"in_app\":true,\"lineno\":64,\"colno\":25112},{\"filename\":\"https://app-static.eu.posthog.com/static/chunk-PGUQKT6S.js\",\"function\":\"n.loadForeignModule\",\"in_app\":true,\"lineno\":64,\"colno\":15003}]},\"mechanism\":{\"handled\":false,\"synthetic\":false}}],\"$exception_level\":\"error\",\"$exception_personURL\":\"https://us.posthog.com/project/sTMFPsFhdP1Ssg/person/pQC9X9Fe7BPzJXVxpY0fx37UwFOCd1vXHzh8rjUPv1G\",\"token\":\"sTMFPsFhdP1Ssg\",\"$session_id\":\"019295b0-db2b-7e02-8010-0a1c4db680df\",\"$window_id\":\"019295b0-db2b-7e02-8010-0a1dee88e5f5\",\"$lib_custom_api_host\":\"https://internal-t.posthog.com\",\"$is_identified\":true,\"$lib_rate_limit_remaining_tokens\":97.28999999999999,\"$sent_at\":\"2024-10-16T14:17:59.543000+00:00\",\"$geoip_city_name\":\"Lisbon\",\"$geoip_city_confidence\":null,\"$geoip_country_name\":\"Portugal\",\"$geoip_country_code\":\"PT\",\"$geoip_country_confidence\":null,\"$geoip_continent_name\":\"Europe\",\"$geoip_continent_code\":\"EU\",\"$geoip_postal_code\":\"1269-001\",\"$geoip_postal_code_confidence\":null,\"$geoip_latitude\":38.731,\"$geoip_longitude\":-9.1373,\"$geoip_accuracy_radius\":100,\"$geoip_time_zone\":\"Europe/Lisbon\",\"$geoip_subdivision_1_code\":\"11\",\"$geoip_subdivision_1_name\":\"Lisbon\",\"$geoip_subdivision_1_confidence\":null,\"$lib_version__major\":1,\"$lib_version__minor\":170,\"$lib_version__patch\":1,\"$group_2\":\"018c1057-288d-0000-93bb-3bd44c845f22\",\"$group_0\":\"018afaa6-8b2e-0000-2311-d58d2df832ad\",\"$group_3\":\"cus_P5B9QmoUKLAUlx\",\"$group_1\":\"https://eu.posthog.com\"}", + "properties": "{\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.7\", \"$browser\": \"Chrome\", \"$device_type\": \"Desktop\", \"$current_url\": \"https://eu.posthog.com/project/12557/feature_flags/31624\", \"$host\": \"eu.posthog.com\", \"$pathname\": \"/project/12557/feature_flags/31624\", \"$raw_user_agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36\", \"$browser_version\": 129, \"$browser_language\": \"en-GB\", \"$screen_height\": 1080, \"$screen_width\": 1920, \"$viewport_height\": 934, \"$viewport_width\": 1920, \"$lib\": \"web\", \"$lib_version\": \"1.170.1\", \"$insert_id\": \"xjfjg606eo2x7n4x\", \"$time\": 1729088278.943, \"distinct_id\": \"pQC9X9Fe7BPzJXVxpY0fx37UwFOCd1vXHzh8rjUPv1G\", \"$device_id\": \"018ccedb-d598-79bb-94e0-4751a3b956f4\", \"$console_log_recording_enabled_server_side\": true, \"$autocapture_disabled_server_side\": false, \"$web_vitals_enabled_server_side\": true, \"$exception_capture_enabled_server_side\": true, \"$exception_capture_endpoint\": \"/e/\", \"realm\": \"cloud\", \"email_service_available\": true, \"slack_service_available\": true, \"commit_sha\": \"bafa32953e\", \"$user_id\": \"pQC9X9Fe7BPzJXVxpY0fx37UwFOCd1vXHzh8rjUPv1G\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018c1057-288d-0000-93bb-3bd44c845f22\", \"organization\": \"018afaa6-8b2e-0000-2311-d58d2df832ad\", \"customer\": \"cus_P5B9QmoUKLAUlx\", \"instance\": \"https://eu.posthog.com\"}, \"has_billing_plan\": true, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$session_recording_start_reason\": \"session_id_changed\", \"$exception_list\": [{\"type\": \"UnhandledRejection\", \"value\": \"Unexpected usage\", \"stacktrace\": {\"type\": \"raw\", \"frames\": [{\"filename\": \"https://app-static.eu.posthog.com/static/chunk-PGUQKT6S.js\", \"function\": \"?\", \"in_app\": true, \"lineno\": 64, \"colno\": 25112, \"platform\": \"web:javascript\"}, {\"filename\": \"https://app-static.eu.posthog.com/static/chunk-PGUQKT6S.js\", \"function\": \"n.loadForeignModule\", \"in_app\": true, \"lineno\": 64, \"colno\": 15003, \"platform\": \"web:javascript\"}]}, \"mechanism\": {\"handled\": false, \"synthetic\": false}}], \"$exception_level\": \"error\", \"$exception_personURL\": \"https://us.posthog.com/project/sTMFPsFhdP1Ssg/person/pQC9X9Fe7BPzJXVxpY0fx37UwFOCd1vXHzh8rjUPv1G\", \"token\": \"sTMFPsFhdP1Ssg\", \"$session_id\": \"019295b0-db2b-7e02-8010-0a1c4db680df\", \"$window_id\": \"019295b0-db2b-7e02-8010-0a1dee88e5f5\", \"$lib_custom_api_host\": \"https://internal-t.posthog.com\", \"$is_identified\": true, \"$lib_rate_limit_remaining_tokens\": 97.28999999999999, \"$sent_at\": \"2024-10-16T14:17:59.543000+00:00\", \"$geoip_city_name\": \"Lisbon\", \"$geoip_city_confidence\": null, \"$geoip_country_name\": \"Portugal\", \"$geoip_country_code\": \"PT\", \"$geoip_country_confidence\": null, \"$geoip_continent_name\": \"Europe\", \"$geoip_continent_code\": \"EU\", \"$geoip_postal_code\": \"1269-001\", \"$geoip_postal_code_confidence\": null, \"$geoip_latitude\": 38.731, \"$geoip_longitude\": -9.1373, \"$geoip_accuracy_radius\": 100, \"$geoip_time_zone\": \"Europe/Lisbon\", \"$geoip_subdivision_1_code\": \"11\", \"$geoip_subdivision_1_name\": \"Lisbon\", \"$geoip_subdivision_1_confidence\": null, \"$lib_version__major\": 1, \"$lib_version__minor\": 170, \"$lib_version__patch\": 1, \"$group_2\": \"018c1057-288d-0000-93bb-3bd44c845f22\", \"$group_0\": \"018afaa6-8b2e-0000-2311-d58d2df832ad\", \"$group_3\": \"cus_P5B9QmoUKLAUlx\", \"$group_1\": \"https://eu.posthog.com\"}", "timestamp": "2024-10-16T07:17:59.088000-07:00", "team_id": 2, "project_id": 2, @@ -9,4 +9,4 @@ "elements_chain": "", "created_at": "2024-10-16T07:18:00.100000-07:00", "person_mode": "full" -} +} \ No newline at end of file diff --git a/rust/cymbal/tests/types.rs b/rust/cymbal/tests/types.rs index 0404398a2b66b..a320b7a08f5e1 100644 --- a/rust/cymbal/tests/types.rs +++ b/rust/cymbal/tests/types.rs @@ -1,7 +1,10 @@ use std::str::FromStr; use common_types::ClickHouseEvent; -use cymbal::types::RawErrProps; +use cymbal::{ + frames::{Frame, RawFrame}, + types::{RawErrProps, Stacktrace}, +}; use serde_json::Value; #[test] @@ -24,3 +27,30 @@ fn serde_passthrough() { assert_eq!(before, after) } + +#[test] +fn python_exceptions() { + let props: RawErrProps = + serde_json::from_str(include_str!("./static/python_err_props.json")).unwrap(); + + let frames = props + .exception_list + .into_iter() + .map(|e| e.stack.unwrap()) + .flat_map(|t| { + let Stacktrace::Raw { frames } = t else { + panic!("Expected a Raw stacktrace") + }; + frames + }) + .map(|f| { + let RawFrame::Python(f) = f else { + panic!("Expected a Python frame") + }; + let f: Frame = (&f).into(); + f + }) + .collect::>(); + + assert_eq!(frames.len(), 31); +}