Skip to content

feat(crashtracking): add unhandled exception libdatadog binding#73

Open
gyuheon0h wants to merge 7 commits intomainfrom
gyuheon0h/add-unhandled-exception-binding
Open

feat(crashtracking): add unhandled exception libdatadog binding#73
gyuheon0h wants to merge 7 commits intomainfrom
gyuheon0h/add-unhandled-exception-binding

Conversation

@gyuheon0h
Copy link
Contributor

@gyuheon0h gyuheon0h commented Feb 25, 2026

This PR adds Rust binding for libdatadog's report_unhandled_exception API.

Honestly, I don't know much about JS conventions or style or internals; I am very open to feedback.

The design choice here is that the caller should only ever have to pass in some payload given to them by process.on("uncaughtExceptionMonitor"). The binding side will take care of the work of checking if it is an Error type and parsing into libdatadog StackFrame's and call the Rust library API.

I also added some unit tests for the stacktrace parsing logic, and added new "integration test" types that run the different scenarios:

  1. uncaught exception that throws an error type
  2. uncaught exception that throws a non-error type
  3. unhandled rejection that throws an error type
  4. unhandled rejection that throws a non-error type

Note: node wraps unhandled rejection that throws a non-error type into an Error itself

I also ran cargo fmt on the native extension code, which formatted some of the older code.

This can be used on the dd-trace-js side. All the caller must do is ensure that the crashtracker has been started for the program, and do as such

let = new UnexpectedError("something bad happend")
crashtracker.reportUnhandledException(e)
let r = some reason for an unhandled rejection
crashtracker.reportUnhandledRejection(r)

Here is a crash report from the "integration test"

{
  "counters": {
    "profiler_serializing": 0,
    "profiler_inactive": 0,
    "profiler_collecting_sample": 0,
    "profiler_unwinding": 0
  },
  "data_schema_version": "1.5",
  "error": {
    "is_crash": true,
    "kind": "UnhandledException",
    "message": "Process was terminated due to an unhandled exception of type 'TypeError'. Message: something went wrong",
    "source_type": "Crashtracking",
    "stack": {
      "format": "Datadog Crashtracker 1.0",
      "frames": [
        {
          "column": 9,
          "file": "/Users/gyuheon.oh/go/src/github.com/DataDog/libdatadog-nodejs/test/crashtracker/app-unhandled-exception.js",
          "function": "myFaultyFunction",
          "line": 9
        },
        {
          "column": 3,
          "file": "/Users/gyuheon.oh/go/src/github.com/DataDog/libdatadog-nodejs/test/crashtracker/app-unhandled-exception.js",
          "function": "Object.<anonymous>",
          "line": 13
        },
        {
          "column": 14,
          "file": "node:internal/modules/cjs/loader",
          "function": "Module._compile",
          "line": 1760
        },
        {
          "column": 10,
          "file": "node:internal/modules/cjs/loader",
          "function": "Object..js",
          "line": 1893
        },
        {
          "column": 32,
          "file": "node:internal/modules/cjs/loader",
          "function": "Module.load",
          "line": 1480
        },
        {
          "column": 12,
          "file": "node:internal/modules/cjs/loader",
          "function": "Module._load",
          "line": 1299
        },
        {
          "column": 14,
          "file": "node:diagnostics_channel",
          "function": "TracingChannel.traceSync",
          "line": 322
        },
        {
          "column": 24,
          "file": "node:internal/modules/cjs/loader",
          "function": "wrapModuleLoad",
          "line": 244
        },
        {
          "column": 5,
          "file": "node:internal/modules/run_main",
          "function": "Module.executeUserEntryPoint [as runMain]",
          "line": 154
        },
        {
          "column": 47,
          "file": "node:internal/main/run_main_module",
          "line": 33
        }
      ],
      "incomplete": false
    }
  },
  "experimental": {},
  "incomplete": false,
  "metadata": {
    "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"
    ]
  },
  "os_info": {
    "architecture": "arm64",
    "bitness": "64-bit",
    "os_type": "Mac OS",
    "version": "15.7.0"
  },
  "proc_info": {
    "pid": 21084,
    "tid": 21172342
  },
  "timestamp": "2026-02-25 13:22:41.428719 UTC",
  "uuid": "3556a100-5d13-4009-82ec-476d47d55f22"
}

Copy link
Contributor Author

gyuheon0h commented Feb 25, 2026

@gyuheon0h gyuheon0h changed the title Add unhandled exception libdatadog binding feat(crashtracking): add unhandled exception libdatadog binding Feb 25, 2026
@gyuheon0h gyuheon0h marked this pull request as ready for review February 25, 2026 13:25
@gyuheon0h gyuheon0h requested review from a team as code owners February 25, 2026 13:25
@gyuheon0h gyuheon0h marked this pull request as draft February 25, 2026 13:26
@gyuheon0h gyuheon0h force-pushed the gyuheon0h/add-unhandled-exception-binding branch from 1503cc4 to 7226d08 Compare February 25, 2026 13:28
@gyuheon0h gyuheon0h force-pushed the gyuheon0h/add-unhandled-exception-binding branch 2 times, most recently from 56126dd to d01d35c Compare February 25, 2026 15:41
Base automatically changed from gyuheon0h/bump-libdd-crashtracker-v28 to main February 25, 2026 16:16
@gyuheon0h gyuheon0h force-pushed the gyuheon0h/add-unhandled-exception-binding branch from d01d35c to fbb872e Compare February 25, 2026 18:27
@gyuheon0h gyuheon0h marked this pull request as ready for review February 25, 2026 18:28
Copy link

@watson watson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the name of the function reportUnhandledException set in stone? I ask because I think all we care about is that it's an error right? By calling it something with an "unhandled exception" we bind it very tightly to the concept of unhandled exceptions in Node.js, where in fact both the uncaughtException handler and the uncaughtExceptionMonitor handler is fired for both uncaught exceptions and unhandled promise rejections (they both provide a 2nd argument to their callback functions called origin being either the string uncaughtException or unhandledRejection).

When the origin is uncaughtException, the 1st argument is guarenteed to be an instance of Error, but for unhandledRejection it might be a non-Error. So we need to ensure that either crashtracker.reportUnhandledException is able to ignore those non-Error values, or that when we implement this in the Node.js client lib, that we don't call crashtracker. reportUnhandledException with a non-Error value.

@gyuheon0h
Copy link
Contributor Author

@watson

Is the name of the function reportUnhandledException set in stone?

On the library side, it is called ddog_crasht_report_unhandled_exception, and since the binding here is basically a wrapper around it, I named it the same.

However, I agree that this API should probably be steered towards supporting all crashes from different types of "errors". I can make an update on the library side, so that the name of the function is not limiting.

@gyuheon0h
Copy link
Contributor Author

gyuheon0h commented Feb 26, 2026

@watson

When the origin is uncaughtException, the 1st argument is guarenteed to be an instance of Error, but for unhandledRejection it might be a non-Error. So we need to ensure that either crashtracker.reportUnhandledException is able to ignore those non-Error values, or that when we implement this in the Node.js client lib, that we don't call crashtracker. reportUnhandledException with a non-Error value.

Ah, yeah. I was thinking about this here. We also do want to support unhandledrejections and I think the check whether the reason is an Error or not should be in the binding function here. I assume if it is not an Error, we can just report an empty stacktrace and potentially use whatever object reasonis stringified as the error message. WDYT?

@github-actions
Copy link

github-actions bot commented Feb 26, 2026

Overall package size

Self size: 37.28 MB
Deduped: 37.28 MB
No deduping: 37.28 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------|

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@watson
Copy link

watson commented Feb 26, 2026

@gyuheon0h

We also do want to support unhandledrejections and I think the check whether the reason is an Error or not should be in the binding function here. I assume if it is not an Error, we can just report an empty stacktrace and potentially use whatever object reason is stringified as the error message. WDYT?

I just looked into this, and actually if I reject('foo') and that promise isn't handled, the err argument to uncaughtException / uncaughtExceptionMonitor isn't the string foo, but a custom error object:

UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "foo".
    at throwUnhandledRejectionsMode (node:internal/process/promises:392:7)
    at processPromiseRejections (node:internal/process/promises:475:17)
    at process.processTicksAndRejections (node:internal/process/task_queues:106:32) {
  code: 'ERR_UNHANDLED_REJECTION'
}

So it's not an issue for us in that case.

However, there's still a slight possibility that err isn't an object. See example:

process.on('uncaughtExceptionMonitor', (err, origin) => {
  console.error('type:', typeof err)
  console.error('isError:', Error.isError(err))
  console.error('Origin:', origin)
  console.error(err)
})

throw 'foo'

Output:

type: string
isError: false
Origin: uncaughtException
foo

In that case I think we should do generate a fallback error message, eg: 'UncaughtException: ' + err

@gyuheon0h gyuheon0h requested review from szegedi and watson February 27, 2026 12:42
@gyuheon0h gyuheon0h force-pushed the gyuheon0h/add-unhandled-exception-binding branch from 7b7956b to c17bd04 Compare February 27, 2026 12:48
@gyuheon0h gyuheon0h marked this pull request as draft February 27, 2026 13:04
@gyuheon0h gyuheon0h marked this pull request as ready for review February 27, 2026 13:16
@gyuheon0h gyuheon0h force-pushed the gyuheon0h/add-unhandled-exception-binding branch from 1fdf204 to fdabc55 Compare February 27, 2026 14:09
@gyuheon0h gyuheon0h force-pushed the gyuheon0h/add-unhandled-exception-binding branch from fdabc55 to a38c404 Compare February 27, 2026 14:19
@gyuheon0h gyuheon0h marked this pull request as draft February 27, 2026 14:56
@gyuheon0h gyuheon0h force-pushed the gyuheon0h/add-unhandled-exception-binding branch from a38c404 to 4f77fef Compare February 27, 2026 15:18
@gyuheon0h gyuheon0h marked this pull request as ready for review February 27, 2026 15:18
Copy link

@watson watson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job! 💯

I've reviewed all files except crates/crashtracker/src/unhandled_exception.rs, as I'm not familiar with that part of the code base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants