Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions crates/crashtracker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use napi::{Env, JsUnknown};
use napi_derive::napi;

mod unhandled_exception;

/// Ensures that if signals is empty, default signals are applied.
/// This is necessary because NAPI deserialization bypasses the
/// CrashtrackerConfiguration::new() constructor where the default
/// CrashtrackerConfiguration::new() constructor where the default
/// signals logic exists.
fn apply_default_signals(
config: libdd_crashtracker::CrashtrackerConfiguration,
Expand All @@ -15,7 +17,7 @@ fn apply_default_signals(
config.use_alt_stack(),
config.endpoint().clone(),
config.resolve_frames(),
vec![], // Empty vec will be replaced with default_signals() in new() in libdatadog
vec![], // Empty vec will be replaced with default_signals() in new() in libdatadog
Some(config.timeout()),
config.unix_socket_path().clone(),
config.demangle_names(),
Expand All @@ -27,7 +29,12 @@ fn apply_default_signals(
}

#[napi]
pub fn init(env: Env, config: JsUnknown, receiver_config: JsUnknown, metadata: JsUnknown) -> napi::Result<()> {
pub fn init(
env: Env,
config: JsUnknown,
receiver_config: JsUnknown,
metadata: JsUnknown,
) -> napi::Result<()> {
let config: libdd_crashtracker::CrashtrackerConfiguration = env.from_js_value(config)?;
let receiver_config = env.from_js_value(receiver_config)?;
let metadata = env.from_js_value(metadata)?;
Expand All @@ -40,7 +47,7 @@ pub fn init(env: Env, config: JsUnknown, receiver_config: JsUnknown, metadata: J
}

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

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

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

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

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

Ok(())
}

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

Ok(())
Expand Down
236 changes: 236 additions & 0 deletions crates/crashtracker/src/unhandled_exception.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use napi::{Env, JsFunction, JsObject, JsUnknown};
use napi_derive::napi;

fn get_optional_string_property(obj: &JsObject, key: &str) -> napi::Result<Option<String>> {
match obj.get_named_property::<JsUnknown>(key) {
Ok(val) => {
use napi::ValueType;
if val.get_type()? == ValueType::String {
let s: String = val.coerce_to_string()?.into_utf8()?.as_str()?.to_owned();
if s.is_empty() {
Ok(None)
} else {
Ok(Some(s))
}
} else {
Ok(None)
}
}
Err(_) => Ok(None),
}
}

fn parse_v8_stack(stack: &str) -> libdd_crashtracker::StackTrace {
let mut frames = Vec::new();

for line in stack.lines().skip(1) {
let line = line.trim();
let line = match line.strip_prefix("at ") {
Some(rest) => rest,
None => continue,
};

let mut frame = libdd_crashtracker::StackFrame::new();

// Formats:
// "functionName (file:line:col)"
// "functionName (file:line)"
// "file:line:col"
// "file:line"
if let Some(paren_start) = line.rfind('(') {
let func_name = line[..paren_start].trim();
if !func_name.is_empty() {
frame.function = Some(func_name.to_string());
}
let location = line[paren_start + 1..].trim_end_matches(')');
parse_location(location, &mut frame);
} else {
parse_location(line, &mut frame);
}

frames.push(frame);
}

libdd_crashtracker::StackTrace::from_frames(frames, false)
}

fn parse_location(location: &str, frame: &mut libdd_crashtracker::StackFrame) {
// location is "file:line:col" or "file:line" or just "native" etc.
// The file portion may contain ":" ("node:internal/...")
// so we split from the right.
let parts: Vec<&str> = location.rsplitn(3, ':').collect();
match parts.len() {
3 => {
// col, line, file
frame.column = parts[0].parse().ok();
frame.line = parts[1].parse().ok();
frame.file = Some(parts[2].to_string());
}
2 => {
if let Ok(line_num) = parts[0].parse::<u32>() {
frame.line = Some(line_num);
frame.file = Some(parts[1].to_string());
} else {
frame.file = Some(location.to_string());
}
}
_ => {
frame.file = Some(location.to_string());
}
}
}

fn is_error_instance(env: &Env, value: &JsUnknown) -> napi::Result<bool> {
let global = env.get_global()?;
let error_ctor: JsFunction = global.get_named_property("Error")?;
value.instanceof(error_ctor)
}

fn stringify_js_value(value: JsUnknown) -> napi::Result<String> {
let s = value.coerce_to_string()?.into_utf8()?;
Ok(s.as_str()?.to_owned())
}

fn report_unhandled(env: &Env, error: JsUnknown, fallback_type: &str) -> napi::Result<()> {
let is_error = is_error_instance(env, &error)?;
let (exception_type, exception_message, stacktrace) = if is_error {
let error_obj: JsObject = error.coerce_to_object()?;
let name = get_optional_string_property(&error_obj, "name")?;
let message = get_optional_string_property(&error_obj, "message")?;
let stack_string = get_optional_string_property(&error_obj, "stack")?;
let stacktrace = match &stack_string {
Some(s) => parse_v8_stack(s),
None => libdd_crashtracker::StackTrace::new_incomplete(),
};
(name, message, stacktrace)
} else {
// This only fires for synchronous `throw <non-Error>`; node already
// wraps non-Error unhandled rejections in an Error object
let message = stringify_js_value(error).ok();
(
Some(fallback_type.to_string()),
message,
// libdatadog defines a missing stacktrace as incomplete
libdd_crashtracker::StackTrace::new_incomplete(),
)
};

libdd_crashtracker::report_unhandled_exception(
exception_type.as_deref(),
exception_message.as_deref(),
stacktrace,
)
.unwrap();

Ok(())
}

#[napi]
pub fn report_uncaught_exception_monitor(
env: Env,
error: JsUnknown,
origin: String,
) -> napi::Result<()> {
report_unhandled(&env, error, &origin)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_v8_stack_typical_error() {
let stack = "\
TypeError: Cannot read properties of undefined (reading 'foo')
at Object.method (/app/src/index.js:10:15)
at Module._compile (node:internal/modules/cjs/loader:1234:14)
at /app/src/helper.js:5:3";

let trace = parse_v8_stack(stack);
assert_eq!(trace.frames.len(), 3);
assert!(!trace.incomplete);

assert_eq!(trace.frames[0].function.as_deref(), Some("Object.method"));
assert_eq!(trace.frames[0].file.as_deref(), Some("/app/src/index.js"));
assert_eq!(trace.frames[0].line, Some(10));
assert_eq!(trace.frames[0].column, Some(15));

assert_eq!(trace.frames[1].function.as_deref(), Some("Module._compile"));
assert_eq!(
trace.frames[1].file.as_deref(),
Some("node:internal/modules/cjs/loader")
);
assert_eq!(trace.frames[1].line, Some(1234));
assert_eq!(trace.frames[1].column, Some(14));

assert_eq!(trace.frames[2].function, None);
assert_eq!(trace.frames[2].file.as_deref(), Some("/app/src/helper.js"));
assert_eq!(trace.frames[2].line, Some(5));
assert_eq!(trace.frames[2].column, Some(3));
}

#[test]
fn test_parse_v8_stack_anonymous_and_native() {
let stack = "\
Error: boom
at <anonymous>:1:1
at native";

let trace = parse_v8_stack(stack);
assert_eq!(trace.frames.len(), 2);

assert_eq!(trace.frames[0].file.as_deref(), Some("<anonymous>"));
assert_eq!(trace.frames[0].line, Some(1));
assert_eq!(trace.frames[0].column, Some(1));

assert_eq!(trace.frames[1].file.as_deref(), Some("native"));
assert_eq!(trace.frames[1].line, None);
}

#[test]
fn test_parse_v8_stack_empty() {
let stack = "Error: something";
let trace = parse_v8_stack(stack);
assert_eq!(trace.frames.len(), 0);
assert!(!trace.incomplete);
}

#[test]
fn test_parse_location_file_line_col() {
let mut frame = libdd_crashtracker::StackFrame::new();
parse_location("/app/index.js:42:7", &mut frame);
assert_eq!(frame.file.as_deref(), Some("/app/index.js"));
assert_eq!(frame.line, Some(42));
assert_eq!(frame.column, Some(7));
}

#[test]
fn test_parse_location_node_internal() {
let mut frame = libdd_crashtracker::StackFrame::new();
parse_location("node:internal/modules/cjs/loader:1234:14", &mut frame);
assert_eq!(
frame.file.as_deref(),
Some("node:internal/modules/cjs/loader")
);
assert_eq!(frame.line, Some(1234));
assert_eq!(frame.column, Some(14));
}

#[test]
fn test_parse_location_no_column() {
let mut frame = libdd_crashtracker::StackFrame::new();
parse_location("/app/index.js:42", &mut frame);
assert_eq!(frame.file.as_deref(), Some("/app/index.js"));
assert_eq!(frame.line, Some(42));
assert_eq!(frame.column, None);
}

#[test]
fn test_parse_location_bare_path() {
let mut frame = libdd_crashtracker::StackFrame::new();
parse_location("native", &mut frame);
assert_eq!(frame.file.as_deref(), Some("native"));
assert_eq!(frame.line, None);
assert_eq!(frame.column, None);
}
}
9 changes: 9 additions & 0 deletions test/crashtracker/app-seg-fault.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict'

const libdatadog = require('../..')
const crashtracker = libdatadog.load('crashtracker')
const { initTestCrashtracker } = require('./test_utils')

initTestCrashtracker()
crashtracker.beginProfilerSerializing()
require('@datadog/segfaultify').segfaultify()
14 changes: 14 additions & 0 deletions test/crashtracker/app-uncaught-exception-non-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict'

const libdatadog = require('../..')
const crashtracker = libdatadog.load('crashtracker')
const { initTestCrashtracker } = require('./test_utils')

initTestCrashtracker()
crashtracker.beginProfilerSerializing()

process.on('uncaughtExceptionMonitor', (e, origin) => {
crashtracker.reportUncaughtExceptionMonitor(e, origin)
})

throw 'a plain string error'
18 changes: 18 additions & 0 deletions test/crashtracker/app-uncaught-exception.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'

const libdatadog = require('../..')
const crashtracker = libdatadog.load('crashtracker')
const { initTestCrashtracker } = require('./test_utils')

initTestCrashtracker()
crashtracker.beginProfilerSerializing()

process.on('uncaughtExceptionMonitor', (e, origin) => {
crashtracker.reportUncaughtExceptionMonitor(e, origin)
})

function myFaultyFunction () {
throw new TypeError('something went wrong')
}

myFaultyFunction()
14 changes: 14 additions & 0 deletions test/crashtracker/app-unhandled-rejection-non-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict'

const libdatadog = require('../..')
const crashtracker = libdatadog.load('crashtracker')
const { initTestCrashtracker } = require('./test_utils')

initTestCrashtracker()
crashtracker.beginProfilerSerializing()

process.on('uncaughtExceptionMonitor', (e, origin) => {
crashtracker.reportUncaughtExceptionMonitor(e, origin)
})

Promise.reject('a plain string rejection')
18 changes: 18 additions & 0 deletions test/crashtracker/app-unhandled-rejection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'

const libdatadog = require('../..')
const crashtracker = libdatadog.load('crashtracker')
const { initTestCrashtracker } = require('./test_utils')

initTestCrashtracker()
crashtracker.beginProfilerSerializing()

process.on('uncaughtExceptionMonitor', (e, origin) => {
crashtracker.reportUncaughtExceptionMonitor(e, origin)
})

async function myAsyncFaultyFunction () {
throw new Error('async went wrong')
}

myAsyncFaultyFunction()
Loading
Loading