Skip to content

Commit f3e9b05

Browse files
authored
V0.8.2 proposal (#76)
* chore(crashtracking): bump libdd-crashtracker to v28.0.1 (#72) * bump libdd-crashtracker to v28 * v28.0.1 * feat(crashtracking): add unhandled exception libdatadog binding (#73) * Add unhandled exception libdatadog binding * Use process.on() in the test * uncaughtException -> uncaughtExceptionMonitor * Handle unhandled promise rejections and add test scenarios * We only need uncaughtExceptionMonitor * Clean test cases * Assert library * v0.8.2
1 parent 64a7f46 commit f3e9b05

13 files changed

Lines changed: 458 additions & 90 deletions

Cargo.lock

Lines changed: 5 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/crashtracker/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ path = "src/bin/receiver.rs"
1414

1515
[dependencies]
1616
anyhow = "1"
17-
libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v27.0.0" }
17+
libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v28.0.1" }
1818
napi = { version = "2", features = ["serde-json"] }
1919
napi-derive = { version = "2", default-features = false }
2020
rustls = { version = "*", default-features = false, features = ["aws-lc-rs"] }

crates/crashtracker/src/lib.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use napi::{Env, JsUnknown};
22
use napi_derive::napi;
33

4+
mod unhandled_exception;
5+
46
/// Ensures that if signals is empty, default signals are applied.
57
/// This is necessary because NAPI deserialization bypasses the
6-
/// CrashtrackerConfiguration::new() constructor where the default
8+
/// CrashtrackerConfiguration::new() constructor where the default
79
/// signals logic exists.
810
fn apply_default_signals(
911
config: libdd_crashtracker::CrashtrackerConfiguration,
@@ -15,7 +17,7 @@ fn apply_default_signals(
1517
config.use_alt_stack(),
1618
config.endpoint().clone(),
1719
config.resolve_frames(),
18-
vec![], // Empty vec will be replaced with default_signals() in new() in libdatadog
20+
vec![], // Empty vec will be replaced with default_signals() in new() in libdatadog
1921
Some(config.timeout()),
2022
config.unix_socket_path().clone(),
2123
config.demangle_names(),
@@ -27,7 +29,12 @@ fn apply_default_signals(
2729
}
2830

2931
#[napi]
30-
pub fn init(env: Env, config: JsUnknown, receiver_config: JsUnknown, metadata: JsUnknown) -> napi::Result<()> {
32+
pub fn init(
33+
env: Env,
34+
config: JsUnknown,
35+
receiver_config: JsUnknown,
36+
metadata: JsUnknown,
37+
) -> napi::Result<()> {
3138
let config: libdd_crashtracker::CrashtrackerConfiguration = env.from_js_value(config)?;
3239
let receiver_config = env.from_js_value(receiver_config)?;
3340
let metadata = env.from_js_value(metadata)?;
@@ -40,7 +47,7 @@ pub fn init(env: Env, config: JsUnknown, receiver_config: JsUnknown, metadata: J
4047
}
4148

4249
#[napi]
43-
pub fn update_config (env: Env, config: JsUnknown) -> napi::Result<()> {
50+
pub fn update_config(env: Env, config: JsUnknown) -> napi::Result<()> {
4451
let config: libdd_crashtracker::CrashtrackerConfiguration = env.from_js_value(config)?;
4552

4653
let config = apply_default_signals(config);
@@ -51,7 +58,7 @@ pub fn update_config (env: Env, config: JsUnknown) -> napi::Result<()> {
5158
}
5259

5360
#[napi]
54-
pub fn update_metadata (env: Env, metadata: JsUnknown) -> napi::Result<()> {
61+
pub fn update_metadata(env: Env, metadata: JsUnknown) -> napi::Result<()> {
5562
let metadata = env.from_js_value(metadata)?;
5663

5764
libdd_crashtracker::update_metadata(metadata).unwrap();
@@ -60,14 +67,14 @@ pub fn update_metadata (env: Env, metadata: JsUnknown) -> napi::Result<()> {
6067
}
6168

6269
#[napi]
63-
pub fn begin_profiler_serializing (_env: Env) -> napi::Result<()> {
70+
pub fn begin_profiler_serializing(_env: Env) -> napi::Result<()> {
6471
let _ = libdd_crashtracker::begin_op(libdd_crashtracker::OpTypes::ProfilerSerializing);
6572

6673
Ok(())
6774
}
6875

6976
#[napi]
70-
pub fn end_profiler_serializing (_env: Env) -> napi::Result<()> {
77+
pub fn end_profiler_serializing(_env: Env) -> napi::Result<()> {
7178
let _ = libdd_crashtracker::end_op(libdd_crashtracker::OpTypes::ProfilerSerializing);
7279

7380
Ok(())
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
use napi::{Env, JsFunction, JsObject, JsUnknown};
2+
use napi_derive::napi;
3+
4+
fn get_optional_string_property(obj: &JsObject, key: &str) -> napi::Result<Option<String>> {
5+
match obj.get_named_property::<JsUnknown>(key) {
6+
Ok(val) => {
7+
use napi::ValueType;
8+
if val.get_type()? == ValueType::String {
9+
let s: String = val.coerce_to_string()?.into_utf8()?.as_str()?.to_owned();
10+
if s.is_empty() {
11+
Ok(None)
12+
} else {
13+
Ok(Some(s))
14+
}
15+
} else {
16+
Ok(None)
17+
}
18+
}
19+
Err(_) => Ok(None),
20+
}
21+
}
22+
23+
fn parse_v8_stack(stack: &str) -> libdd_crashtracker::StackTrace {
24+
let mut frames = Vec::new();
25+
26+
for line in stack.lines().skip(1) {
27+
let line = line.trim();
28+
let line = match line.strip_prefix("at ") {
29+
Some(rest) => rest,
30+
None => continue,
31+
};
32+
33+
let mut frame = libdd_crashtracker::StackFrame::new();
34+
35+
// Formats:
36+
// "functionName (file:line:col)"
37+
// "functionName (file:line)"
38+
// "file:line:col"
39+
// "file:line"
40+
if let Some(paren_start) = line.rfind('(') {
41+
let func_name = line[..paren_start].trim();
42+
if !func_name.is_empty() {
43+
frame.function = Some(func_name.to_string());
44+
}
45+
let location = line[paren_start + 1..].trim_end_matches(')');
46+
parse_location(location, &mut frame);
47+
} else {
48+
parse_location(line, &mut frame);
49+
}
50+
51+
frames.push(frame);
52+
}
53+
54+
libdd_crashtracker::StackTrace::from_frames(frames, false)
55+
}
56+
57+
fn parse_location(location: &str, frame: &mut libdd_crashtracker::StackFrame) {
58+
// location is "file:line:col" or "file:line" or just "native" etc.
59+
// The file portion may contain ":" ("node:internal/...")
60+
// so we split from the right.
61+
let parts: Vec<&str> = location.rsplitn(3, ':').collect();
62+
match parts.len() {
63+
3 => {
64+
// col, line, file
65+
frame.column = parts[0].parse().ok();
66+
frame.line = parts[1].parse().ok();
67+
frame.file = Some(parts[2].to_string());
68+
}
69+
2 => {
70+
if let Ok(line_num) = parts[0].parse::<u32>() {
71+
frame.line = Some(line_num);
72+
frame.file = Some(parts[1].to_string());
73+
} else {
74+
frame.file = Some(location.to_string());
75+
}
76+
}
77+
_ => {
78+
frame.file = Some(location.to_string());
79+
}
80+
}
81+
}
82+
83+
fn is_error_instance(env: &Env, value: &JsUnknown) -> napi::Result<bool> {
84+
let global = env.get_global()?;
85+
let error_ctor: JsFunction = global.get_named_property("Error")?;
86+
value.instanceof(error_ctor)
87+
}
88+
89+
fn stringify_js_value(value: JsUnknown) -> napi::Result<String> {
90+
let s = value.coerce_to_string()?.into_utf8()?;
91+
Ok(s.as_str()?.to_owned())
92+
}
93+
94+
fn report_unhandled(env: &Env, error: JsUnknown, fallback_type: &str) -> napi::Result<()> {
95+
let is_error = is_error_instance(env, &error)?;
96+
let (exception_type, exception_message, stacktrace) = if is_error {
97+
let error_obj: JsObject = error.coerce_to_object()?;
98+
let name = get_optional_string_property(&error_obj, "name")?;
99+
let message = get_optional_string_property(&error_obj, "message")?;
100+
let stack_string = get_optional_string_property(&error_obj, "stack")?;
101+
let stacktrace = match &stack_string {
102+
Some(s) => parse_v8_stack(s),
103+
None => libdd_crashtracker::StackTrace::new_incomplete(),
104+
};
105+
(name, message, stacktrace)
106+
} else {
107+
// This only fires for synchronous `throw <non-Error>`; node already
108+
// wraps non-Error unhandled rejections in an Error object
109+
let message = stringify_js_value(error).ok();
110+
(
111+
Some(fallback_type.to_string()),
112+
message,
113+
// libdatadog defines a missing stacktrace as incomplete
114+
libdd_crashtracker::StackTrace::new_incomplete(),
115+
)
116+
};
117+
118+
libdd_crashtracker::report_unhandled_exception(
119+
exception_type.as_deref(),
120+
exception_message.as_deref(),
121+
stacktrace,
122+
)
123+
.unwrap();
124+
125+
Ok(())
126+
}
127+
128+
#[napi]
129+
pub fn report_uncaught_exception_monitor(
130+
env: Env,
131+
error: JsUnknown,
132+
origin: String,
133+
) -> napi::Result<()> {
134+
report_unhandled(&env, error, &origin)
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
141+
#[test]
142+
fn test_parse_v8_stack_typical_error() {
143+
let stack = "\
144+
TypeError: Cannot read properties of undefined (reading 'foo')
145+
at Object.method (/app/src/index.js:10:15)
146+
at Module._compile (node:internal/modules/cjs/loader:1234:14)
147+
at /app/src/helper.js:5:3";
148+
149+
let trace = parse_v8_stack(stack);
150+
assert_eq!(trace.frames.len(), 3);
151+
assert!(!trace.incomplete);
152+
153+
assert_eq!(trace.frames[0].function.as_deref(), Some("Object.method"));
154+
assert_eq!(trace.frames[0].file.as_deref(), Some("/app/src/index.js"));
155+
assert_eq!(trace.frames[0].line, Some(10));
156+
assert_eq!(trace.frames[0].column, Some(15));
157+
158+
assert_eq!(trace.frames[1].function.as_deref(), Some("Module._compile"));
159+
assert_eq!(
160+
trace.frames[1].file.as_deref(),
161+
Some("node:internal/modules/cjs/loader")
162+
);
163+
assert_eq!(trace.frames[1].line, Some(1234));
164+
assert_eq!(trace.frames[1].column, Some(14));
165+
166+
assert_eq!(trace.frames[2].function, None);
167+
assert_eq!(trace.frames[2].file.as_deref(), Some("/app/src/helper.js"));
168+
assert_eq!(trace.frames[2].line, Some(5));
169+
assert_eq!(trace.frames[2].column, Some(3));
170+
}
171+
172+
#[test]
173+
fn test_parse_v8_stack_anonymous_and_native() {
174+
let stack = "\
175+
Error: boom
176+
at <anonymous>:1:1
177+
at native";
178+
179+
let trace = parse_v8_stack(stack);
180+
assert_eq!(trace.frames.len(), 2);
181+
182+
assert_eq!(trace.frames[0].file.as_deref(), Some("<anonymous>"));
183+
assert_eq!(trace.frames[0].line, Some(1));
184+
assert_eq!(trace.frames[0].column, Some(1));
185+
186+
assert_eq!(trace.frames[1].file.as_deref(), Some("native"));
187+
assert_eq!(trace.frames[1].line, None);
188+
}
189+
190+
#[test]
191+
fn test_parse_v8_stack_empty() {
192+
let stack = "Error: something";
193+
let trace = parse_v8_stack(stack);
194+
assert_eq!(trace.frames.len(), 0);
195+
assert!(!trace.incomplete);
196+
}
197+
198+
#[test]
199+
fn test_parse_location_file_line_col() {
200+
let mut frame = libdd_crashtracker::StackFrame::new();
201+
parse_location("/app/index.js:42:7", &mut frame);
202+
assert_eq!(frame.file.as_deref(), Some("/app/index.js"));
203+
assert_eq!(frame.line, Some(42));
204+
assert_eq!(frame.column, Some(7));
205+
}
206+
207+
#[test]
208+
fn test_parse_location_node_internal() {
209+
let mut frame = libdd_crashtracker::StackFrame::new();
210+
parse_location("node:internal/modules/cjs/loader:1234:14", &mut frame);
211+
assert_eq!(
212+
frame.file.as_deref(),
213+
Some("node:internal/modules/cjs/loader")
214+
);
215+
assert_eq!(frame.line, Some(1234));
216+
assert_eq!(frame.column, Some(14));
217+
}
218+
219+
#[test]
220+
fn test_parse_location_no_column() {
221+
let mut frame = libdd_crashtracker::StackFrame::new();
222+
parse_location("/app/index.js:42", &mut frame);
223+
assert_eq!(frame.file.as_deref(), Some("/app/index.js"));
224+
assert_eq!(frame.line, Some(42));
225+
assert_eq!(frame.column, None);
226+
}
227+
228+
#[test]
229+
fn test_parse_location_bare_path() {
230+
let mut frame = libdd_crashtracker::StackFrame::new();
231+
parse_location("native", &mut frame);
232+
assert_eq!(frame.file.as_deref(), Some("native"));
233+
assert_eq!(frame.line, None);
234+
assert_eq!(frame.column, None);
235+
}
236+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@datadog/libdatadog",
3-
"version": "0.8.1",
3+
"version": "0.8.2",
44
"description": "Node.js binding for libdatadog",
55
"main": "index.js",
66
"scripts": {

test/crashtracker/app-seg-fault.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict'
2+
3+
const libdatadog = require('../..')
4+
const crashtracker = libdatadog.load('crashtracker')
5+
const { initTestCrashtracker } = require('./test_utils')
6+
7+
initTestCrashtracker()
8+
crashtracker.beginProfilerSerializing()
9+
require('@datadog/segfaultify').segfaultify()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict'
2+
3+
const libdatadog = require('../..')
4+
const crashtracker = libdatadog.load('crashtracker')
5+
const { initTestCrashtracker } = require('./test_utils')
6+
7+
initTestCrashtracker()
8+
crashtracker.beginProfilerSerializing()
9+
10+
process.on('uncaughtExceptionMonitor', (e, origin) => {
11+
crashtracker.reportUncaughtExceptionMonitor(e, origin)
12+
})
13+
14+
throw 'a plain string error'

0 commit comments

Comments
 (0)