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}