Skip to main content

parse_stack_trace

Function parse_stack_trace 

Source
pub fn parse_stack_trace(input: &str) -> Vec<StackFrame>
Expand description

Parse a stack trace string into individual frames.

Supports V8 (Chrome, Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) stack trace formats.

Examples found in repository?
examples/error_symbolication.rs (line 124)
69fn main() {
70    println!("=== Error Monitoring Symbolication Demo ===\n");
71
72    let source_map_json = build_source_map_json();
73
74    // -----------------------------------------------------------------------
75    // 1. Parse a V8-format stack trace (Chrome / Node.js)
76    // -----------------------------------------------------------------------
77    //
78    // V8 format: `at functionName (file:line:column)` with 1-based positions.
79    // The error message appears on the first line.
80
81    println!("--- Step 1: Parse V8 stack trace ---\n");
82
83    let v8_stack = "\
84TypeError: Cannot read property 'x' of null
85    at processData (bundle.js:3:10)
86    at handleClick (bundle.js:1:1)";
87
88    let v8_parsed = parse_stack_trace_full(v8_stack);
89
90    println!("  Message: {:?}", v8_parsed.message);
91    println!("  Frames:  {}", v8_parsed.frames.len());
92    for (i, frame) in v8_parsed.frames.iter().enumerate() {
93        println!(
94            "    [{i}] {}  {}:{}:{}",
95            frame.function_name.as_deref().unwrap_or("<anonymous>"),
96            frame.file,
97            frame.line,
98            frame.column,
99        );
100    }
101
102    assert_eq!(v8_parsed.message.as_deref(), Some("TypeError: Cannot read property 'x' of null"));
103    assert_eq!(v8_parsed.frames.len(), 2);
104    assert_eq!(v8_parsed.frames[0].function_name.as_deref(), Some("processData"));
105    assert_eq!(v8_parsed.frames[0].file, "bundle.js");
106    assert_eq!(v8_parsed.frames[0].line, 3);
107    assert_eq!(v8_parsed.frames[0].column, 10);
108    assert_eq!(v8_parsed.frames[1].function_name.as_deref(), Some("handleClick"));
109    assert_eq!(v8_parsed.frames[1].line, 1);
110    assert_eq!(v8_parsed.frames[1].column, 1);
111
112    // -----------------------------------------------------------------------
113    // 2. Parse a SpiderMonkey-format stack trace (Firefox)
114    // -----------------------------------------------------------------------
115    //
116    // SpiderMonkey format: `functionName@file:line:column` (no message line).
117
118    println!("\n--- Step 2: Parse SpiderMonkey stack trace ---\n");
119
120    let sm_stack = "\
121processData@bundle.js:3:10
122handleClick@bundle.js:1:1";
123
124    let sm_frames = parse_stack_trace(sm_stack);
125
126    println!("  Frames: {}", sm_frames.len());
127    for (i, frame) in sm_frames.iter().enumerate() {
128        println!(
129            "    [{i}] {}  {}:{}:{}",
130            frame.function_name.as_deref().unwrap_or("<anonymous>"),
131            frame.file,
132            frame.line,
133            frame.column,
134        );
135    }
136
137    assert_eq!(sm_frames.len(), 2);
138    assert_eq!(sm_frames[0].function_name.as_deref(), Some("processData"));
139    assert_eq!(sm_frames[0].file, "bundle.js");
140    assert_eq!(sm_frames[0].line, 3);
141    assert_eq!(sm_frames[0].column, 10);
142
143    // -----------------------------------------------------------------------
144    // 3. Verify both formats produce the same parsed frames
145    // -----------------------------------------------------------------------
146
147    println!("\n--- Step 3: Compare parsed frames ---\n");
148
149    assert_eq!(v8_parsed.frames.len(), sm_frames.len());
150    for (v8_frame, sm_frame) in v8_parsed.frames.iter().zip(sm_frames.iter()) {
151        assert_eq!(v8_frame.function_name, sm_frame.function_name);
152        assert_eq!(v8_frame.file, sm_frame.file);
153        assert_eq!(v8_frame.line, sm_frame.line);
154        assert_eq!(v8_frame.column, sm_frame.column);
155    }
156    println!("  V8 and SpiderMonkey frames match.");
157
158    // -----------------------------------------------------------------------
159    // 4. Symbolicate the V8 stack trace
160    // -----------------------------------------------------------------------
161    //
162    // The loader closure receives a filename ("bundle.js") and returns the
163    // parsed SourceMap. Internally, the symbolicate function:
164    //   1. Subtracts 1 from the 1-based stack trace positions to get 0-based
165    //   2. Calls SourceMap::original_position_for(line, column)
166    //   3. Adds 1 back to the resolved 0-based original positions
167
168    println!("\n--- Step 4: Symbolicate V8 stack trace ---\n");
169
170    let v8_result = symbolicate(v8_stack, |file| {
171        if file == "bundle.js" { SourceMap::from_json(&source_map_json).ok() } else { None }
172    });
173
174    println!("  Symbolicated stack (Display):\n");
175    // SymbolicatedStack implements Display, printing a V8-style resolved trace
176    print!("{v8_result}");
177
178    assert_eq!(v8_result.message.as_deref(), Some("TypeError: Cannot read property 'x' of null"));
179    assert_eq!(v8_result.frames.len(), 2);
180
181    // Frame 0: bundle.js:3:10 (1-based) -> 0-based (2, 9) -> src/app.ts (9, 4) -> 1-based (10, 5)
182    assert!(v8_result.frames[0].symbolicated);
183    assert_eq!(v8_result.frames[0].file, "src/app.ts");
184    assert_eq!(v8_result.frames[0].line, 10);
185    assert_eq!(v8_result.frames[0].column, 5);
186    assert_eq!(v8_result.frames[0].function_name.as_deref(), Some("processData"));
187
188    // Frame 1: bundle.js:1:1 (1-based) -> 0-based (0, 0) -> src/app.ts (4, 0) -> 1-based (5, 1)
189    assert!(v8_result.frames[1].symbolicated);
190    assert_eq!(v8_result.frames[1].file, "src/app.ts");
191    assert_eq!(v8_result.frames[1].line, 5);
192    assert_eq!(v8_result.frames[1].column, 1);
193    assert_eq!(v8_result.frames[1].function_name.as_deref(), Some("handleClick"));
194
195    // -----------------------------------------------------------------------
196    // 5. Symbolicate the SpiderMonkey stack trace
197    // -----------------------------------------------------------------------
198
199    println!("\n--- Step 5: Symbolicate SpiderMonkey stack trace ---\n");
200
201    let sm_result = symbolicate(sm_stack, |file| {
202        if file == "bundle.js" { SourceMap::from_json(&source_map_json).ok() } else { None }
203    });
204
205    print!("{sm_result}");
206
207    // Both engine formats should resolve to the same original positions
208    assert_eq!(v8_result.frames.len(), sm_result.frames.len());
209    for (v8_frame, sm_frame) in v8_result.frames.iter().zip(sm_result.frames.iter()) {
210        assert_eq!(v8_frame.file, sm_frame.file);
211        assert_eq!(v8_frame.line, sm_frame.line);
212        assert_eq!(v8_frame.column, sm_frame.column);
213        assert_eq!(v8_frame.function_name, sm_frame.function_name);
214        assert_eq!(v8_frame.symbolicated, sm_frame.symbolicated);
215    }
216    println!("\n  V8 and SpiderMonkey symbolicated frames match.");
217
218    // -----------------------------------------------------------------------
219    // 6. JSON output
220    // -----------------------------------------------------------------------
221
222    println!("\n--- Step 6: JSON output ---\n");
223
224    let json_output = to_json(&v8_result);
225    println!("{json_output}");
226
227    assert!(json_output.contains("\"symbolicated\": true"));
228    assert!(json_output.contains("src/app.ts"));
229    assert!(json_output.contains("processData"));
230    assert!(json_output.contains("handleClick"));
231    assert!(json_output.contains("Cannot read property"));
232
233    // -----------------------------------------------------------------------
234    // 7. Batch symbolication
235    // -----------------------------------------------------------------------
236    //
237    // In production error monitoring, you often need to symbolicate many stack
238    // traces against a pre-loaded set of source maps. symbolicate_batch avoids
239    // re-parsing source maps for each trace.
240
241    println!("\n--- Step 7: Batch symbolication ---\n");
242
243    let sm = SourceMap::from_json(&source_map_json).expect("valid source map");
244    let mut maps: HashMap<String, SourceMap> = HashMap::new();
245    maps.insert("bundle.js".to_string(), sm);
246
247    let error_stack_1 = "\
248TypeError: Cannot read property 'x' of null
249    at processData (bundle.js:3:10)
250    at handleClick (bundle.js:1:1)";
251
252    let error_stack_2 = "\
253ReferenceError: foo is not defined
254    at handleClick (bundle.js:1:1)";
255
256    let stacks = vec![error_stack_1, error_stack_2];
257    let batch_results = symbolicate_batch(&stacks, &maps);
258
259    assert_eq!(batch_results.len(), 2);
260    println!("  Batch result 1 ({} frames):", batch_results[0].frames.len());
261    print!("{}", batch_results[0]);
262
263    println!("  Batch result 2 ({} frames):", batch_results[1].frames.len());
264    print!("{}", batch_results[1]);
265
266    // First stack: same as individual symbolication
267    assert_eq!(batch_results[0].frames.len(), 2);
268    assert!(batch_results[0].frames[0].symbolicated);
269    assert_eq!(batch_results[0].frames[0].file, "src/app.ts");
270
271    // Second stack: single frame
272    assert_eq!(batch_results[1].frames.len(), 1);
273    assert!(batch_results[1].frames[0].symbolicated);
274    assert_eq!(batch_results[1].message.as_deref(), Some("ReferenceError: foo is not defined"));
275
276    // -----------------------------------------------------------------------
277    // 8. Debug ID resolution
278    // -----------------------------------------------------------------------
279    //
280    // In production, source maps are often stored by their debug ID (a UUID
281    // embedded in both the generated file and the source map). This allows
282    // matching without relying on filename conventions.
283
284    println!("\n--- Step 8: Debug ID resolution ---\n");
285
286    let debug_id = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
287
288    let resolved = resolve_by_debug_id(debug_id, &maps);
289    assert!(resolved.is_some(), "should find source map by debug ID");
290
291    let resolved_map = resolved.unwrap();
292    assert_eq!(resolved_map.debug_id.as_deref(), Some(debug_id));
293    println!("  Found source map for debug ID: {debug_id}");
294    println!("  Sources: {:?}", resolved_map.sources);
295    println!("  Names:   {:?}", resolved_map.names);
296
297    // Non-existent debug ID returns None
298    let missing = resolve_by_debug_id("00000000-0000-0000-0000-000000000000", &maps);
299    assert!(missing.is_none(), "unknown debug ID should return None");
300    println!("  Unknown debug ID correctly returns None.");
301
302    println!("\n=== All assertions passed. ===");
303}