tauri_plugin_tracing/callstack.rs
1//! Call stack parsing and filtering utilities.
2//!
3//! This module provides types for parsing JavaScript call stacks and extracting
4//! meaningful location information for log messages.
5
6use serde::{Deserialize, Serialize};
7
8/// A single line from a JavaScript call stack.
9///
10/// This type wraps a string and provides methods for extracting location
11/// information while filtering out noise like `node_modules` paths.
12///
13/// # Examples
14///
15/// ```
16/// use tauri_plugin_tracing::CallStackLine;
17///
18/// // Create from a string
19/// let line = CallStackLine::from("at foo (src/app.ts:10:5)");
20/// assert!(line.contains("foo"));
21///
22/// // Default is "unknown"
23/// let default_line = CallStackLine::default();
24/// assert_eq!(default_line.as_str(), "unknown");
25///
26/// // Create from None defaults to "unknown"
27/// let none_line = CallStackLine::from(None);
28/// assert_eq!(none_line.as_str(), "unknown");
29/// ```
30#[derive(Deserialize, Serialize, Clone)]
31#[cfg_attr(feature = "specta", derive(specta::Type))]
32pub struct CallStackLine(String);
33
34impl std::ops::Deref for CallStackLine {
35 type Target = String;
36
37 fn deref(&self) -> &Self::Target {
38 &self.0
39 }
40}
41
42impl From<&str> for CallStackLine {
43 fn from(value: &str) -> Self {
44 Self(value.to_string())
45 }
46}
47
48impl From<Option<&str>> for CallStackLine {
49 fn from(value: Option<&str>) -> Self {
50 Self(value.unwrap_or("unknown").to_string())
51 }
52}
53
54impl Default for CallStackLine {
55 fn default() -> Self {
56 Self("unknown".to_string())
57 }
58}
59
60impl std::ops::DerefMut for CallStackLine {
61 fn deref_mut(&mut self) -> &mut Self::Target {
62 &mut self.0
63 }
64}
65
66impl std::fmt::Display for CallStackLine {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 write!(f, "{}", self.0)
69 }
70}
71
72impl std::fmt::Debug for CallStackLine {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 write!(f, "{}", self)
75 }
76}
77
78impl CallStackLine {
79 /// Replaces occurrences of a substring with another string.
80 ///
81 /// # Examples
82 ///
83 /// ```
84 /// use tauri_plugin_tracing::CallStackLine;
85 ///
86 /// let line = CallStackLine::from("at foo (src/old.ts:10:5)");
87 /// let replaced = line.replace("old", "new");
88 /// assert!(replaced.contains("new.ts"));
89 /// ```
90 pub fn replace(&self, from: &str, to: &str) -> Self {
91 CallStackLine(self.0.replace(from, to))
92 }
93
94 /// Removes the `localhost:PORT/` prefix from URLs for cleaner output.
95 fn strip_localhost(&self) -> String {
96 let mut result = self.to_string();
97 if let Some(start) = result.find("localhost:")
98 && let Some(slash_pos) = result[start..].find('/')
99 {
100 result.replace_range(0..start + slash_pos + 1, "");
101 }
102 result
103 }
104}
105
106/// A parsed JavaScript call stack.
107///
108/// This type parses a newline-separated call stack string and provides methods
109/// to extract different levels of location detail for log messages.
110///
111/// # Examples
112///
113/// ```
114/// use tauri_plugin_tracing::CallStack;
115///
116/// // Parse a simple call stack
117/// let stack = CallStack::new(Some("Error\n at foo (src/app.ts:10:5)\n at bar (src/lib.ts:20:3)"));
118///
119/// // Get just the filename (last component after '/')
120/// assert_eq!(stack.file_name().as_str(), "lib.ts:20:3)");
121///
122/// // Get the full path of the last frame
123/// assert_eq!(stack.path().as_str(), " at bar (src/lib.ts:20:3)");
124/// ```
125///
126/// ```
127/// use tauri_plugin_tracing::CallStack;
128///
129/// // node_modules paths are filtered out
130/// let stack = CallStack::new(Some("Error\n at node_modules/lib/index.js:1:1\n at src/app.ts:10:5"));
131/// let location = stack.location();
132/// assert!(!location.contains("node_modules"));
133/// ```
134#[derive(Debug, Deserialize, Serialize, Clone)]
135#[cfg_attr(feature = "specta", derive(specta::Type))]
136pub struct CallStack(pub Vec<CallStackLine>);
137
138impl From<Option<&str>> for CallStack {
139 fn from(value: Option<&str>) -> Self {
140 let lines = value
141 .unwrap_or("")
142 .split("\n")
143 .map(|line| CallStackLine(line.to_string()))
144 .collect();
145 Self(lines)
146 }
147}
148
149impl From<Option<String>> for CallStack {
150 fn from(value: Option<String>) -> Self {
151 let lines = value
152 .unwrap_or("".to_string())
153 .split("\n")
154 .map(|line| CallStackLine(line.to_string()))
155 .collect();
156 Self(lines)
157 }
158}
159
160impl CallStack {
161 /// Creates a new `CallStack` from an optional string.
162 pub fn new(value: Option<&str>) -> Self {
163 CallStack::from(value)
164 }
165
166 /// Returns the full filtered location as a `#`-separated string.
167 ///
168 /// This includes all stack frames that pass the filter (excluding
169 /// `node_modules` and native code), joined with `#`.
170 /// Used for `trace` and `error` log levels.
171 pub fn location(&self) -> CallStackLine {
172 CallStackLine(
173 self.0
174 .iter()
175 .filter_map(fmap_location)
176 .collect::<Vec<String>>()
177 .clone()
178 .join("#"),
179 )
180 }
181
182 /// Returns the path of the last (most recent) stack frame.
183 ///
184 /// This extracts just the last location from the full call stack.
185 /// Used for `debug` and `warn` log levels.
186 pub fn path(&self) -> CallStackLine {
187 match self.location().split("#").last() {
188 Some(file_name) => CallStackLine(file_name.to_string()),
189 None => CallStackLine("unknown".to_string()),
190 }
191 }
192
193 /// Returns just the filename (without path) of the most recent stack frame.
194 ///
195 /// This is the most concise location format.
196 /// Used for `info` log level.
197 pub fn file_name(&self) -> CallStackLine {
198 match self.location().split("/").last() {
199 Some(file_name) => CallStackLine(file_name.to_string()),
200 None => CallStackLine("unknown".to_string()),
201 }
202 }
203}
204
205/// Substrings that indicate a stack frame should be filtered out.
206const FILTERED_LINES: [&str; 2] = ["node_modules", "forEach@[native code]"];
207
208/// Filters and transforms a call stack line.
209///
210/// Returns `None` if the line should be filtered out (e.g., `node_modules`),
211/// otherwise returns the line with localhost URLs stripped.
212fn fmap_location(line: &CallStackLine) -> Option<String> {
213 if FILTERED_LINES
214 .iter()
215 .any(|filtered| line.contains(filtered))
216 {
217 return None;
218 }
219 Some(line.strip_localhost())
220}