From 4c29705223c41fc7d4270f78a445761b1a0055d8 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Tue, 3 Mar 2026 09:36:33 -0500 Subject: [PATCH 1/9] Revert "V0.8.2 proposal (#76)" This reverts commit f3e9b05b1dad4c06889075d267d8b34d0dec76f2. --- Cargo.lock | 11 +- crates/crashtracker/Cargo.toml | 2 +- crates/crashtracker/src/lib.rs | 21 +- .../crashtracker/src/unhandled_exception.rs | 236 ------------------ package.json | 2 +- test/crashtracker/app-seg-fault.js | 9 - .../app-uncaught-exception-non-error.js | 14 -- test/crashtracker/app-uncaught-exception.js | 18 -- .../app-unhandled-rejection-non-error.js | 14 -- test/crashtracker/app-unhandled-rejection.js | 18 -- test/crashtracker/app.js | 44 ++++ test/crashtracker/index.js | 114 +++------ test/crashtracker/test_utils.js | 45 ---- 13 files changed, 90 insertions(+), 458 deletions(-) delete mode 100644 crates/crashtracker/src/unhandled_exception.rs delete mode 100644 test/crashtracker/app-seg-fault.js delete mode 100644 test/crashtracker/app-uncaught-exception-non-error.js delete mode 100644 test/crashtracker/app-uncaught-exception.js delete mode 100644 test/crashtracker/app-unhandled-rejection-non-error.js delete mode 100644 test/crashtracker/app-unhandled-rejection.js create mode 100644 test/crashtracker/app.js delete mode 100644 test/crashtracker/test_utils.js diff --git a/Cargo.lock b/Cargo.lock index f95516a..09fab51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,10 +773,9 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libdd-common" version = "1.1.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v28.0.1#647c8ff924070d4e79494ec852d312ff0301fd8a" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" dependencies = [ "anyhow", - "bytes", "cc", "const_format", "futures", @@ -807,7 +806,7 @@ dependencies = [ [[package]] name = "libdd-crashtracker" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v28.0.1#647c8ff924070d4e79494ec852d312ff0301fd8a" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" dependencies = [ "anyhow", "backtrace", @@ -839,7 +838,7 @@ dependencies = [ [[package]] name = "libdd-ddsketch" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v28.0.1#647c8ff924070d4e79494ec852d312ff0301fd8a" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" dependencies = [ "prost", ] @@ -847,7 +846,7 @@ dependencies = [ [[package]] name = "libdd-telemetry" version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v28.0.1#647c8ff924070d4e79494ec852d312ff0301fd8a" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" dependencies = [ "anyhow", "base64", @@ -855,6 +854,8 @@ dependencies = [ "hashbrown", "http", "http-body-util", + "hyper", + "hyper-util", "libc", "libdd-common", "libdd-ddsketch", diff --git a/crates/crashtracker/Cargo.toml b/crates/crashtracker/Cargo.toml index 62877bf..85c806b 100644 --- a/crates/crashtracker/Cargo.toml +++ b/crates/crashtracker/Cargo.toml @@ -14,7 +14,7 @@ path = "src/bin/receiver.rs" [dependencies] anyhow = "1" -libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v28.0.1" } +libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v27.0.0" } napi = { version = "2", features = ["serde-json"] } napi-derive = { version = "2", default-features = false } rustls = { version = "*", default-features = false, features = ["aws-lc-rs"] } diff --git a/crates/crashtracker/src/lib.rs b/crates/crashtracker/src/lib.rs index 28366af..a908776 100644 --- a/crates/crashtracker/src/lib.rs +++ b/crates/crashtracker/src/lib.rs @@ -1,11 +1,9 @@ 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, @@ -17,7 +15,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(), @@ -29,12 +27,7 @@ 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)?; @@ -47,7 +40,7 @@ pub fn init( } #[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); @@ -58,7 +51,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(); @@ -67,14 +60,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(()) diff --git a/crates/crashtracker/src/unhandled_exception.rs b/crates/crashtracker/src/unhandled_exception.rs deleted file mode 100644 index 64dd8fc..0000000 --- a/crates/crashtracker/src/unhandled_exception.rs +++ /dev/null @@ -1,236 +0,0 @@ -use napi::{Env, JsFunction, JsObject, JsUnknown}; -use napi_derive::napi; - -fn get_optional_string_property(obj: &JsObject, key: &str) -> napi::Result> { - match obj.get_named_property::(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::() { - 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 { - 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 { - 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 `; 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 :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("")); - 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); - } -} diff --git a/package.json b/package.json index a12600f..65105ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/libdatadog", - "version": "0.8.2", + "version": "0.8.1", "description": "Node.js binding for libdatadog", "main": "index.js", "scripts": { diff --git a/test/crashtracker/app-seg-fault.js b/test/crashtracker/app-seg-fault.js deleted file mode 100644 index a1471f6..0000000 --- a/test/crashtracker/app-seg-fault.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -const libdatadog = require('../..') -const crashtracker = libdatadog.load('crashtracker') -const { initTestCrashtracker } = require('./test_utils') - -initTestCrashtracker() -crashtracker.beginProfilerSerializing() -require('@datadog/segfaultify').segfaultify() diff --git a/test/crashtracker/app-uncaught-exception-non-error.js b/test/crashtracker/app-uncaught-exception-non-error.js deleted file mode 100644 index 418fa4a..0000000 --- a/test/crashtracker/app-uncaught-exception-non-error.js +++ /dev/null @@ -1,14 +0,0 @@ -'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' diff --git a/test/crashtracker/app-uncaught-exception.js b/test/crashtracker/app-uncaught-exception.js deleted file mode 100644 index bc94be3..0000000 --- a/test/crashtracker/app-uncaught-exception.js +++ /dev/null @@ -1,18 +0,0 @@ -'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() diff --git a/test/crashtracker/app-unhandled-rejection-non-error.js b/test/crashtracker/app-unhandled-rejection-non-error.js deleted file mode 100644 index fc45909..0000000 --- a/test/crashtracker/app-unhandled-rejection-non-error.js +++ /dev/null @@ -1,14 +0,0 @@ -'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') diff --git a/test/crashtracker/app-unhandled-rejection.js b/test/crashtracker/app-unhandled-rejection.js deleted file mode 100644 index 963d60a..0000000 --- a/test/crashtracker/app-unhandled-rejection.js +++ /dev/null @@ -1,18 +0,0 @@ -'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() diff --git a/test/crashtracker/app.js b/test/crashtracker/app.js new file mode 100644 index 0000000..5ec4b86 --- /dev/null +++ b/test/crashtracker/app.js @@ -0,0 +1,44 @@ +'use strict' + +const libdatadog = require('../..') +const crashtracker = libdatadog.load('crashtracker') + +crashtracker.init({ + additional_files: [], + create_alt_stack: true, + use_alt_stack: true, + endpoint: { + url: { + scheme: 'http', + authority: `127.0.0.1:${process.env.PORT || 8126}`, + path_and_query: '' + }, + timeout_ms: 3000 + }, + timeout: { secs: 3, nanos: 0 }, + resolve_frames: 'EnabledWithInprocessSymbols', + wait_for_receiver: true, + demangle_names: false, + signals: [] +}, { + args: [], + env: [], + path_to_receiver_binary: libdatadog.find('crashtracker-receiver', true), + stderr_filename: 'stderr.log', + stdout_filename: 'stdout.log', +}, { + library_name: 'dd-trace-js', + library_version: '6.0.0-pre', + family: 'javascript', + tags: [ + 'language:javascript', + 'runtime:nodejs', + 'runtime-id:8a8fef6433a849b3bc3171198831d102', + 'library_version:6.0.0-pre', + 'is_crash:true', + 'severity:crash' + ] +}) + +crashtracker.beginProfilerSerializing() +require('@datadog/segfaultify').segfaultify() diff --git a/test/crashtracker/index.js b/test/crashtracker/index.js index 1118fdb..92454d8 100644 --- a/test/crashtracker/index.js +++ b/test/crashtracker/index.js @@ -1,6 +1,7 @@ 'use strict' const { execSync, exec } = require('child_process') +const { inspect } = require('util') const cwd = __dirname const stdio = ['inherit', 'inherit', 'inherit'] @@ -12,7 +13,6 @@ execSync('yarn install', opts) const express = require('express') const bodyParser = require('body-parser') -const assert = require('assert') const { existsSync, rmSync } = require('fs') const path = require('path') @@ -26,9 +26,7 @@ let timeout = setTimeout(() => { execSync('cat stderr.log', opts) throw new Error('No crash report received before timing out.') -}, 10000) - -let currentTest = null +}, 10000) // TODO: reduce this when the receiver no longer locks up app.use(bodyParser.json()) @@ -43,92 +41,42 @@ app.post('/telemetry/proxy/api/v2/apmtelemetry', (req, res) => { return } - if (!currentTest) { - throw new Error('Received unexpected crash report with no active test.') - } + clearTimeout(timeout) - currentTest(logPayload, tags) -}) + server.close(() => { + const stackTrace = JSON.parse(logPayload.message).error.stack.frames -let PORT + const boomFrame = stackTrace.find(frame => frame.function?.toLowerCase().includes('segfaultify')) -function runApp (script) { - return new Promise((resolve) => { - exec(`node ${script}`, { - ...opts, - env: { ...process.env, PORT } - }) + if (existsSync('/etc/alpine-release')) { + // TODO: Remove this when supported. + console.log('Received crash report. Skipping stack trace test since it is currently unsupported for Alpine.') + } else if (boomFrame) { + console.log('Stack frame for crashing function successfully received by the mock agent.') + } else { + throw new Error('Could not find a stack frame for the crashing function.') + } - currentTest = (logPayload, tags) => { - currentTest = null - resolve({ logPayload, tags }) + if (tags.includes('profiler_serializing:1')) { + console.log('Stack trace was marked as happened during profile serialization.') + } else { + throw new Error('Stack trace was not marked as happening during profile serialization.') } }) -} - -async function testSegfault () { - const { logPayload, tags } = await runApp('app-seg-fault') - const stackTrace = JSON.parse(logPayload.message).error.stack.frames - const boomFrame = stackTrace.find(frame => frame.function?.toLowerCase().includes('segfaultify')) - - if (existsSync('/etc/alpine-release')) { - console.log('[segfault] Received crash report. Skipping stack trace test since it is currently unsupported for Alpine.') - } else { - assert(boomFrame, '[segfault] Expected stack frame for crashing function not found.') - } - - assert(tags.includes('profiler_serializing:1'), '[segfault] Expected profiler_serializing:1 tag not found.') -} - -async function testUnhandledError (label, script, { expectedType, expectedMessage, expectedFrame }) { - const { logPayload } = await runApp(script) - const crashReport = JSON.parse(logPayload.message) - - assert(crashReport.error.message.includes(expectedType), `[${label}] Expected exception type "${expectedType}" not found in message.`) - assert(crashReport.error.message.includes(expectedMessage), `[${label}] Expected exception message "${expectedMessage}" not found.`) - if (expectedFrame) { - const frame = crashReport.error.stack.frames.find(f => f.function && f.function.includes(expectedFrame)) - assert(frame, `[${label}] Expected stack frame for ${expectedFrame} not found.`) - } -} - -async function testUnhandledNonError (label, script, { expectedFallbackType, expectedValue }) { - const { logPayload } = await runApp(script) - const crashReport = JSON.parse(logPayload.message) - - assert(crashReport.error.message.includes(expectedFallbackType), `[${label}] Expected fallback type "${expectedFallbackType}" not found in message.`) - assert(crashReport.error.message.includes(expectedValue), `[${label}] Expected stringified value "${expectedValue}" not found in message.`) - assert.strictEqual(crashReport.error.stack.frames.length, 0, `[${label}] Expected empty stack trace but got ${crashReport.error.stack.frames.length} frames.`) -} - -const server = app.listen(async () => { - PORT = server.address().port +}) +const server = app.listen(() => { + const PORT = server.address().port - await testSegfault() - await testUnhandledError('uncaught-exception', 'app-uncaught-exception', { - expectedType: 'TypeError', - expectedMessage: 'something went wrong', - expectedFrame: 'myFaultyFunction' - }) - await testUnhandledNonError('uncaught-exception-non-error', 'app-uncaught-exception-non-error', { - expectedFallbackType: 'uncaughtException', - expectedValue: 'a plain string error' - }) - await testUnhandledError('unhandled-rejection', 'app-unhandled-rejection', { - expectedType: 'Error', - expectedMessage: 'async went wrong', - expectedFrame: 'myAsyncFaultyFunction' - }) - // Node wraps non-Error rejections in an Error with name 'UnhandledPromiseRejection' - // before passing to uncaughtExceptionMonitor, so this hits the Error path. - // However, this test case rejects with a plain string, so the wrapped Error object has useless - // stack trace - await testUnhandledError('unhandled-rejection-non-error', 'app-unhandled-rejection-non-error', { - expectedType: 'UnhandledPromiseRejection', - expectedMessage: 'a plain string rejection' + exec('node app', { + ...opts, + env: { + ...process.env, + PORT + } + }, e => { + if (e.signal !== 'SIGSEGV' && e.code !== 139 && e.status !== 139) { + throw e + } }) - - clearTimeout(timeout) - server.close() }) diff --git a/test/crashtracker/test_utils.js b/test/crashtracker/test_utils.js deleted file mode 100644 index 8062d9c..0000000 --- a/test/crashtracker/test_utils.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict' - -const libdatadog = require('../..') -const crashtracker = libdatadog.load('crashtracker') - -function initTestCrashtracker () { - crashtracker.init({ - additional_files: [], - create_alt_stack: true, - use_alt_stack: true, - endpoint: { - url: { - scheme: 'http', - authority: `127.0.0.1:${process.env.PORT || 8126}`, - path_and_query: '' - }, - timeout_ms: 3000 - }, - timeout: { secs: 3, nanos: 0 }, - resolve_frames: 'EnabledWithInprocessSymbols', - wait_for_receiver: true, - demangle_names: false, - signals: [] - }, { - args: [], - env: [], - path_to_receiver_binary: libdatadog.find('crashtracker-receiver', true), - stderr_filename: 'stderr.log', - stdout_filename: 'stdout.log', - }, { - library_name: 'dd-trace-js', - library_version: '6.0.0-pre', - family: 'javascript', - tags: [ - 'language:javascript', - 'runtime:nodejs', - 'runtime-id:8a8fef6433a849b3bc3171198831d102', - 'library_version:6.0.0-pre', - 'is_crash:true', - 'severity:crash' - ] - }) -} - -module.exports = { initTestCrashtracker } From 13a12fafd8b2f82dbbb6d47520e111b54e958c4c Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Tue, 3 Mar 2026 09:36:55 -0500 Subject: [PATCH 2/9] Revert " v0.8.1 (#68)" This reverts commit 64a7f46213a59bc1367f85032366d95e315d807f. --- .github/CODEOWNERS | 1 - .github/workflows/release.yml | 4 +- Cargo.lock | 238 ++---------------------------- crates/crashtracker/Cargo.toml | 2 +- package.json | 2 +- test/crashtracker/index.js | 8 +- test/wasm/library_config/index.js | 2 - 7 files changed, 22 insertions(+), 235 deletions(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index dc8bb6a..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @DataDog/dd-trace-js @DataDog/libdatadog diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6b9ea6..eb820ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,8 +35,9 @@ jobs: needs: build environment: npm permissions: - id-token: write # Required for OIDC contents: write + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} outputs: pkgjson: ${{ steps.pkg.outputs.json }} steps: @@ -44,7 +45,6 @@ jobs: - uses: actions/download-artifact@v4 - uses: actions/setup-node@v4 with: - node-version: '24' registry-url: 'https://registry.npmjs.org' - run: chmod -R +x ./prebuilds - run: npm publish diff --git a/Cargo.lock b/Cargo.lock index 09fab51..39df17d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,15 +146,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - [[package]] name = "bumpalo" version = "3.17.0" @@ -215,7 +206,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -355,16 +346,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "dispatch2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" -dependencies = [ - "bitflags", - "objc2", -] - [[package]] name = "dunce" version = "1.0.5" @@ -773,7 +754,7 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libdd-common" version = "1.1.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v26.0.0#9aa79d219d0fa74ce667c4013005008f69052786" dependencies = [ "anyhow", "cc", @@ -789,7 +770,7 @@ dependencies = [ "hyper-rustls", "hyper-util", "libc", - "nix 0.29.0", + "nix", "pin-project", "regex", "rustls", @@ -806,7 +787,7 @@ dependencies = [ [[package]] name = "libdd-crashtracker" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v26.0.0#9aa79d219d0fa74ce667c4013005008f69052786" dependencies = [ "anyhow", "backtrace", @@ -817,7 +798,7 @@ dependencies = [ "libc", "libdd-common", "libdd-telemetry", - "nix 0.29.0", + "nix", "num-derive", "num-traits", "os_info", @@ -838,7 +819,7 @@ dependencies = [ [[package]] name = "libdd-ddsketch" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v26.0.0#9aa79d219d0fa74ce667c4013005008f69052786" dependencies = [ "prost", ] @@ -846,7 +827,7 @@ dependencies = [ [[package]] name = "libdd-telemetry" version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v26.0.0#9aa79d219d0fa74ce667c4013005008f69052786" dependencies = [ "anyhow", "base64", @@ -1048,18 +1029,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -1090,165 +1059,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "object" version = "0.36.7" @@ -1272,18 +1082,13 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "os_info" -version = "3.14.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +checksum = "41fc863e2ca13dc2d5c34fb22ea4a588248ac14db929616ba65c45f21744b1e9" dependencies = [ - "android_system_properties", "log", - "nix 0.30.1", - "objc2", - "objc2-foundation", - "objc2-ui-kit", "serde", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2202,7 +2007,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface", - "windows-link 0.1.1", + "windows-link", "windows-result", "windows-strings 0.4.2", ] @@ -2246,19 +2051,13 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -2267,7 +2066,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -2276,7 +2075,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -2297,15 +2096,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.48.5" diff --git a/crates/crashtracker/Cargo.toml b/crates/crashtracker/Cargo.toml index 85c806b..c1f7002 100644 --- a/crates/crashtracker/Cargo.toml +++ b/crates/crashtracker/Cargo.toml @@ -14,7 +14,7 @@ path = "src/bin/receiver.rs" [dependencies] anyhow = "1" -libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v27.0.0" } +libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v26.0.0" } napi = { version = "2", features = ["serde-json"] } napi-derive = { version = "2", default-features = false } rustls = { version = "*", default-features = false, features = ["aws-lc-rs"] } diff --git a/package.json b/package.json index 65105ec..35c2ecb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/libdatadog", - "version": "0.8.1", + "version": "0.8.0", "description": "Node.js binding for libdatadog", "main": "index.js", "scripts": { diff --git a/test/crashtracker/index.js b/test/crashtracker/index.js index 92454d8..958ad97 100644 --- a/test/crashtracker/index.js +++ b/test/crashtracker/index.js @@ -33,18 +33,18 @@ app.use(bodyParser.json()) app.post('/telemetry/proxy/api/v2/apmtelemetry', (req, res) => { res.status(200).send() - const logPayload = req.body.payload.logs[0] - const tags = logPayload.tags ? logPayload.tags.split(',') : [] + const payload = req.body.payload[0] + const tags = payload.tags ? payload.tags.split(',') : [] // Only process crash reports (not pings) - if (!logPayload.is_crash) { + if (!tags.some(tag => tag === 'is_crash:true')) { return } clearTimeout(timeout) server.close(() => { - const stackTrace = JSON.parse(logPayload.message).error.stack.frames + const stackTrace = JSON.parse(payload.message).error.stack.frames const boomFrame = stackTrace.find(frame => frame.function?.toLowerCase().includes('segfaultify')) diff --git a/test/wasm/library_config/index.js b/test/wasm/library_config/index.js index 02c0343..71aaac1 100644 --- a/test/wasm/library_config/index.js +++ b/test/wasm/library_config/index.js @@ -40,8 +40,6 @@ function test_service_selector() { }); assert.strictEqual(values.length, 2) - // We can't rely on ordering, so sort it by name to make it deterministic - values.sort((a, b) => a.name.localeCompare(b.name)) assert.strictEqual(values[0].name, 'DD_RUNTIME_METRICS_ENABLED') assert.strictEqual(values[0].value, 'true') assert.strictEqual(values[0].source, 'local_stable_config') From 6f86a0f46e0b69291c57f05bd975f2de28dda238 Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Thu, 12 Feb 2026 18:19:09 +0100 Subject: [PATCH 3/9] Add CODEOWNERS (#64) * Add CODEOWNERS, with dd-trace-js and libdatadog teams --- .github/CODEOWNERS | 1 + test/wasm/library_config/index.js | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..dc8bb6a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @DataDog/dd-trace-js @DataDog/libdatadog diff --git a/test/wasm/library_config/index.js b/test/wasm/library_config/index.js index 71aaac1..02c0343 100644 --- a/test/wasm/library_config/index.js +++ b/test/wasm/library_config/index.js @@ -40,6 +40,8 @@ function test_service_selector() { }); assert.strictEqual(values.length, 2) + // We can't rely on ordering, so sort it by name to make it deterministic + values.sort((a, b) => a.name.localeCompare(b.name)) assert.strictEqual(values[0].name, 'DD_RUNTIME_METRICS_ENABLED') assert.strictEqual(values[0].value, 'true') assert.strictEqual(values[0].source, 'local_stable_config') From 757a27321d2affb4c01e805c2ef1b39974b9bcd3 Mon Sep 17 00:00:00 2001 From: Attila Szegedi Date: Thu, 12 Feb 2026 18:19:28 +0100 Subject: [PATCH 4/9] Enable releasing through OIDC (#65) --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb820ee..c6b9ea6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,9 +35,8 @@ jobs: needs: build environment: npm permissions: + id-token: write # Required for OIDC contents: write - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} outputs: pkgjson: ${{ steps.pkg.outputs.json }} steps: @@ -45,6 +44,7 @@ jobs: - uses: actions/download-artifact@v4 - uses: actions/setup-node@v4 with: + node-version: '24' registry-url: 'https://registry.npmjs.org' - run: chmod -R +x ./prebuilds - run: npm publish From 4fe15ca39c5f14bfa1aa9e8fffff3bb1ec0c1d0d Mon Sep 17 00:00:00 2001 From: Gyuheon Oh <102937919+gyuheon0h@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:14:47 -0500 Subject: [PATCH 5/9] Bump libdd-crashtracker to v27.0.0 (#66) --- Cargo.lock | 238 +++++++++++++++++++++++++++++++-- crates/crashtracker/Cargo.toml | 2 +- test/crashtracker/index.js | 8 +- 3 files changed, 229 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39df17d..09fab51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,15 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -206,7 +215,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -346,6 +355,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "dunce" version = "1.0.5" @@ -754,7 +773,7 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libdd-common" version = "1.1.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v26.0.0#9aa79d219d0fa74ce667c4013005008f69052786" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" dependencies = [ "anyhow", "cc", @@ -770,7 +789,7 @@ dependencies = [ "hyper-rustls", "hyper-util", "libc", - "nix", + "nix 0.29.0", "pin-project", "regex", "rustls", @@ -787,7 +806,7 @@ dependencies = [ [[package]] name = "libdd-crashtracker" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v26.0.0#9aa79d219d0fa74ce667c4013005008f69052786" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" dependencies = [ "anyhow", "backtrace", @@ -798,7 +817,7 @@ dependencies = [ "libc", "libdd-common", "libdd-telemetry", - "nix", + "nix 0.29.0", "num-derive", "num-traits", "os_info", @@ -819,7 +838,7 @@ dependencies = [ [[package]] name = "libdd-ddsketch" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v26.0.0#9aa79d219d0fa74ce667c4013005008f69052786" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" dependencies = [ "prost", ] @@ -827,7 +846,7 @@ dependencies = [ [[package]] name = "libdd-telemetry" version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v26.0.0#9aa79d219d0fa74ce667c4013005008f69052786" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" dependencies = [ "anyhow", "base64", @@ -1029,6 +1048,18 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1059,6 +1090,165 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "object" version = "0.36.7" @@ -1082,13 +1272,18 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "os_info" -version = "3.11.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fc863e2ca13dc2d5c34fb22ea4a588248ac14db929616ba65c45f21744b1e9" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" dependencies = [ + "android_system_properties", "log", + "nix 0.30.1", + "objc2", + "objc2-foundation", + "objc2-ui-kit", "serde", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2007,7 +2202,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface", - "windows-link", + "windows-link 0.1.1", "windows-result", "windows-strings 0.4.2", ] @@ -2051,13 +2246,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -2066,7 +2267,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -2075,7 +2276,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -2096,6 +2297,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" diff --git a/crates/crashtracker/Cargo.toml b/crates/crashtracker/Cargo.toml index c1f7002..85c806b 100644 --- a/crates/crashtracker/Cargo.toml +++ b/crates/crashtracker/Cargo.toml @@ -14,7 +14,7 @@ path = "src/bin/receiver.rs" [dependencies] anyhow = "1" -libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v26.0.0" } +libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v27.0.0" } napi = { version = "2", features = ["serde-json"] } napi-derive = { version = "2", default-features = false } rustls = { version = "*", default-features = false, features = ["aws-lc-rs"] } diff --git a/test/crashtracker/index.js b/test/crashtracker/index.js index 958ad97..92454d8 100644 --- a/test/crashtracker/index.js +++ b/test/crashtracker/index.js @@ -33,18 +33,18 @@ app.use(bodyParser.json()) app.post('/telemetry/proxy/api/v2/apmtelemetry', (req, res) => { res.status(200).send() - const payload = req.body.payload[0] - const tags = payload.tags ? payload.tags.split(',') : [] + const logPayload = req.body.payload.logs[0] + const tags = logPayload.tags ? logPayload.tags.split(',') : [] // Only process crash reports (not pings) - if (!tags.some(tag => tag === 'is_crash:true')) { + if (!logPayload.is_crash) { return } clearTimeout(timeout) server.close(() => { - const stackTrace = JSON.parse(payload.message).error.stack.frames + const stackTrace = JSON.parse(logPayload.message).error.stack.frames const boomFrame = stackTrace.find(frame => frame.function?.toLowerCase().includes('segfaultify')) From 7251a86996e0debe96977102b0521b501d0451ad Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Thu, 12 Feb 2026 14:19:09 -0500 Subject: [PATCH 6/9] v0.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 35c2ecb..65105ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/libdatadog", - "version": "0.8.0", + "version": "0.8.1", "description": "Node.js binding for libdatadog", "main": "index.js", "scripts": { From 37237e71cccdf7dca9166a76ab20c67560930659 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh <102937919+gyuheon0h@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:16:15 -0500 Subject: [PATCH 7/9] chore(crashtracking): bump libdd-crashtracker to v28.0.1 (#72) * bump libdd-crashtracker to v28 * v28.0.1 --- Cargo.lock | 11 +++++------ crates/crashtracker/Cargo.toml | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09fab51..f95516a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,9 +773,10 @@ checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libdd-common" version = "1.1.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v28.0.1#647c8ff924070d4e79494ec852d312ff0301fd8a" dependencies = [ "anyhow", + "bytes", "cc", "const_format", "futures", @@ -806,7 +807,7 @@ dependencies = [ [[package]] name = "libdd-crashtracker" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v28.0.1#647c8ff924070d4e79494ec852d312ff0301fd8a" dependencies = [ "anyhow", "backtrace", @@ -838,7 +839,7 @@ dependencies = [ [[package]] name = "libdd-ddsketch" version = "1.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v28.0.1#647c8ff924070d4e79494ec852d312ff0301fd8a" dependencies = [ "prost", ] @@ -846,7 +847,7 @@ dependencies = [ [[package]] name = "libdd-telemetry" version = "2.0.0" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v27.0.0#72e56a3dcf9189a92db1f177c4c9d844725079f7" +source = "git+https://github.com/DataDog/libdatadog.git?tag=v28.0.1#647c8ff924070d4e79494ec852d312ff0301fd8a" dependencies = [ "anyhow", "base64", @@ -854,8 +855,6 @@ dependencies = [ "hashbrown", "http", "http-body-util", - "hyper", - "hyper-util", "libc", "libdd-common", "libdd-ddsketch", diff --git a/crates/crashtracker/Cargo.toml b/crates/crashtracker/Cargo.toml index 85c806b..62877bf 100644 --- a/crates/crashtracker/Cargo.toml +++ b/crates/crashtracker/Cargo.toml @@ -14,7 +14,7 @@ path = "src/bin/receiver.rs" [dependencies] anyhow = "1" -libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v27.0.0" } +libdd-crashtracker = { git = "https://github.com/DataDog/libdatadog.git", tag = "v28.0.1" } napi = { version = "2", features = ["serde-json"] } napi-derive = { version = "2", default-features = false } rustls = { version = "*", default-features = false, features = ["aws-lc-rs"] } From bbe66b17e8f94771c4c97b5fb4f167c8488b31b2 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh <102937919+gyuheon0h@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:17:11 -0500 Subject: [PATCH 8/9] 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 --- crates/crashtracker/src/lib.rs | 21 +- .../crashtracker/src/unhandled_exception.rs | 236 ++++++++++++++++++ test/crashtracker/app-seg-fault.js | 9 + .../app-uncaught-exception-non-error.js | 14 ++ test/crashtracker/app-uncaught-exception.js | 18 ++ .../app-unhandled-rejection-non-error.js | 14 ++ test/crashtracker/app-unhandled-rejection.js | 18 ++ test/crashtracker/app.js | 44 ---- test/crashtracker/index.js | 114 ++++++--- test/crashtracker/test_utils.js | 45 ++++ 10 files changed, 451 insertions(+), 82 deletions(-) create mode 100644 crates/crashtracker/src/unhandled_exception.rs create mode 100644 test/crashtracker/app-seg-fault.js create mode 100644 test/crashtracker/app-uncaught-exception-non-error.js create mode 100644 test/crashtracker/app-uncaught-exception.js create mode 100644 test/crashtracker/app-unhandled-rejection-non-error.js create mode 100644 test/crashtracker/app-unhandled-rejection.js delete mode 100644 test/crashtracker/app.js create mode 100644 test/crashtracker/test_utils.js diff --git a/crates/crashtracker/src/lib.rs b/crates/crashtracker/src/lib.rs index a908776..28366af 100644 --- a/crates/crashtracker/src/lib.rs +++ b/crates/crashtracker/src/lib.rs @@ -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, @@ -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(), @@ -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)?; @@ -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); @@ -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(); @@ -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(()) diff --git a/crates/crashtracker/src/unhandled_exception.rs b/crates/crashtracker/src/unhandled_exception.rs new file mode 100644 index 0000000..64dd8fc --- /dev/null +++ b/crates/crashtracker/src/unhandled_exception.rs @@ -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> { + match obj.get_named_property::(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::() { + 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 { + 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 { + 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 `; 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 :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("")); + 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); + } +} diff --git a/test/crashtracker/app-seg-fault.js b/test/crashtracker/app-seg-fault.js new file mode 100644 index 0000000..a1471f6 --- /dev/null +++ b/test/crashtracker/app-seg-fault.js @@ -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() diff --git a/test/crashtracker/app-uncaught-exception-non-error.js b/test/crashtracker/app-uncaught-exception-non-error.js new file mode 100644 index 0000000..418fa4a --- /dev/null +++ b/test/crashtracker/app-uncaught-exception-non-error.js @@ -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' diff --git a/test/crashtracker/app-uncaught-exception.js b/test/crashtracker/app-uncaught-exception.js new file mode 100644 index 0000000..bc94be3 --- /dev/null +++ b/test/crashtracker/app-uncaught-exception.js @@ -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() diff --git a/test/crashtracker/app-unhandled-rejection-non-error.js b/test/crashtracker/app-unhandled-rejection-non-error.js new file mode 100644 index 0000000..fc45909 --- /dev/null +++ b/test/crashtracker/app-unhandled-rejection-non-error.js @@ -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') diff --git a/test/crashtracker/app-unhandled-rejection.js b/test/crashtracker/app-unhandled-rejection.js new file mode 100644 index 0000000..963d60a --- /dev/null +++ b/test/crashtracker/app-unhandled-rejection.js @@ -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() diff --git a/test/crashtracker/app.js b/test/crashtracker/app.js deleted file mode 100644 index 5ec4b86..0000000 --- a/test/crashtracker/app.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict' - -const libdatadog = require('../..') -const crashtracker = libdatadog.load('crashtracker') - -crashtracker.init({ - additional_files: [], - create_alt_stack: true, - use_alt_stack: true, - endpoint: { - url: { - scheme: 'http', - authority: `127.0.0.1:${process.env.PORT || 8126}`, - path_and_query: '' - }, - timeout_ms: 3000 - }, - timeout: { secs: 3, nanos: 0 }, - resolve_frames: 'EnabledWithInprocessSymbols', - wait_for_receiver: true, - demangle_names: false, - signals: [] -}, { - args: [], - env: [], - path_to_receiver_binary: libdatadog.find('crashtracker-receiver', true), - stderr_filename: 'stderr.log', - stdout_filename: 'stdout.log', -}, { - library_name: 'dd-trace-js', - library_version: '6.0.0-pre', - family: 'javascript', - tags: [ - 'language:javascript', - 'runtime:nodejs', - 'runtime-id:8a8fef6433a849b3bc3171198831d102', - 'library_version:6.0.0-pre', - 'is_crash:true', - 'severity:crash' - ] -}) - -crashtracker.beginProfilerSerializing() -require('@datadog/segfaultify').segfaultify() diff --git a/test/crashtracker/index.js b/test/crashtracker/index.js index 92454d8..1118fdb 100644 --- a/test/crashtracker/index.js +++ b/test/crashtracker/index.js @@ -1,7 +1,6 @@ 'use strict' const { execSync, exec } = require('child_process') -const { inspect } = require('util') const cwd = __dirname const stdio = ['inherit', 'inherit', 'inherit'] @@ -13,6 +12,7 @@ execSync('yarn install', opts) const express = require('express') const bodyParser = require('body-parser') +const assert = require('assert') const { existsSync, rmSync } = require('fs') const path = require('path') @@ -26,7 +26,9 @@ let timeout = setTimeout(() => { execSync('cat stderr.log', opts) throw new Error('No crash report received before timing out.') -}, 10000) // TODO: reduce this when the receiver no longer locks up +}, 10000) + +let currentTest = null app.use(bodyParser.json()) @@ -41,42 +43,92 @@ app.post('/telemetry/proxy/api/v2/apmtelemetry', (req, res) => { return } - clearTimeout(timeout) + if (!currentTest) { + throw new Error('Received unexpected crash report with no active test.') + } - server.close(() => { - const stackTrace = JSON.parse(logPayload.message).error.stack.frames + currentTest(logPayload, tags) +}) - const boomFrame = stackTrace.find(frame => frame.function?.toLowerCase().includes('segfaultify')) +let PORT - if (existsSync('/etc/alpine-release')) { - // TODO: Remove this when supported. - console.log('Received crash report. Skipping stack trace test since it is currently unsupported for Alpine.') - } else if (boomFrame) { - console.log('Stack frame for crashing function successfully received by the mock agent.') - } else { - throw new Error('Could not find a stack frame for the crashing function.') - } +function runApp (script) { + return new Promise((resolve) => { + exec(`node ${script}`, { + ...opts, + env: { ...process.env, PORT } + }) - if (tags.includes('profiler_serializing:1')) { - console.log('Stack trace was marked as happened during profile serialization.') - } else { - throw new Error('Stack trace was not marked as happening during profile serialization.') + currentTest = (logPayload, tags) => { + currentTest = null + resolve({ logPayload, tags }) } }) -}) +} -const server = app.listen(() => { - const PORT = server.address().port +async function testSegfault () { + const { logPayload, tags } = await runApp('app-seg-fault') + const stackTrace = JSON.parse(logPayload.message).error.stack.frames + const boomFrame = stackTrace.find(frame => frame.function?.toLowerCase().includes('segfaultify')) - exec('node app', { - ...opts, - env: { - ...process.env, - PORT - } - }, e => { - if (e.signal !== 'SIGSEGV' && e.code !== 139 && e.status !== 139) { - throw e - } + if (existsSync('/etc/alpine-release')) { + console.log('[segfault] Received crash report. Skipping stack trace test since it is currently unsupported for Alpine.') + } else { + assert(boomFrame, '[segfault] Expected stack frame for crashing function not found.') + } + + assert(tags.includes('profiler_serializing:1'), '[segfault] Expected profiler_serializing:1 tag not found.') +} + +async function testUnhandledError (label, script, { expectedType, expectedMessage, expectedFrame }) { + const { logPayload } = await runApp(script) + const crashReport = JSON.parse(logPayload.message) + + assert(crashReport.error.message.includes(expectedType), `[${label}] Expected exception type "${expectedType}" not found in message.`) + assert(crashReport.error.message.includes(expectedMessage), `[${label}] Expected exception message "${expectedMessage}" not found.`) + if (expectedFrame) { + const frame = crashReport.error.stack.frames.find(f => f.function && f.function.includes(expectedFrame)) + assert(frame, `[${label}] Expected stack frame for ${expectedFrame} not found.`) + } +} + +async function testUnhandledNonError (label, script, { expectedFallbackType, expectedValue }) { + const { logPayload } = await runApp(script) + const crashReport = JSON.parse(logPayload.message) + + assert(crashReport.error.message.includes(expectedFallbackType), `[${label}] Expected fallback type "${expectedFallbackType}" not found in message.`) + assert(crashReport.error.message.includes(expectedValue), `[${label}] Expected stringified value "${expectedValue}" not found in message.`) + assert.strictEqual(crashReport.error.stack.frames.length, 0, `[${label}] Expected empty stack trace but got ${crashReport.error.stack.frames.length} frames.`) +} + +const server = app.listen(async () => { + PORT = server.address().port + + + await testSegfault() + await testUnhandledError('uncaught-exception', 'app-uncaught-exception', { + expectedType: 'TypeError', + expectedMessage: 'something went wrong', + expectedFrame: 'myFaultyFunction' }) + await testUnhandledNonError('uncaught-exception-non-error', 'app-uncaught-exception-non-error', { + expectedFallbackType: 'uncaughtException', + expectedValue: 'a plain string error' + }) + await testUnhandledError('unhandled-rejection', 'app-unhandled-rejection', { + expectedType: 'Error', + expectedMessage: 'async went wrong', + expectedFrame: 'myAsyncFaultyFunction' + }) + // Node wraps non-Error rejections in an Error with name 'UnhandledPromiseRejection' + // before passing to uncaughtExceptionMonitor, so this hits the Error path. + // However, this test case rejects with a plain string, so the wrapped Error object has useless + // stack trace + await testUnhandledError('unhandled-rejection-non-error', 'app-unhandled-rejection-non-error', { + expectedType: 'UnhandledPromiseRejection', + expectedMessage: 'a plain string rejection' + }) + + clearTimeout(timeout) + server.close() }) diff --git a/test/crashtracker/test_utils.js b/test/crashtracker/test_utils.js new file mode 100644 index 0000000..8062d9c --- /dev/null +++ b/test/crashtracker/test_utils.js @@ -0,0 +1,45 @@ +'use strict' + +const libdatadog = require('../..') +const crashtracker = libdatadog.load('crashtracker') + +function initTestCrashtracker () { + crashtracker.init({ + additional_files: [], + create_alt_stack: true, + use_alt_stack: true, + endpoint: { + url: { + scheme: 'http', + authority: `127.0.0.1:${process.env.PORT || 8126}`, + path_and_query: '' + }, + timeout_ms: 3000 + }, + timeout: { secs: 3, nanos: 0 }, + resolve_frames: 'EnabledWithInprocessSymbols', + wait_for_receiver: true, + demangle_names: false, + signals: [] + }, { + args: [], + env: [], + path_to_receiver_binary: libdatadog.find('crashtracker-receiver', true), + stderr_filename: 'stderr.log', + stdout_filename: 'stdout.log', + }, { + library_name: 'dd-trace-js', + library_version: '6.0.0-pre', + family: 'javascript', + tags: [ + 'language:javascript', + 'runtime:nodejs', + 'runtime-id:8a8fef6433a849b3bc3171198831d102', + 'library_version:6.0.0-pre', + 'is_crash:true', + 'severity:crash' + ] + }) +} + +module.exports = { initTestCrashtracker } From 0509d4495f3850077d3651ce940fd037b4fd0e35 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Tue, 3 Mar 2026 00:41:51 -0500 Subject: [PATCH 9/9] v0.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 65105ec..a12600f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datadog/libdatadog", - "version": "0.8.1", + "version": "0.8.2", "description": "Node.js binding for libdatadog", "main": "index.js", "scripts": {