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