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);
+}