Skip to content

Commit 8db698b

Browse files
committed
fix: Don't send objects across isolates
1 parent 5332dbf commit 8db698b

File tree

8 files changed

+109
-142
lines changed

8 files changed

+109
-142
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules/
22
build/
33
lib/
44
/*.tgz
5+
test/yarn.lock

README.md

Lines changed: 29 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ registerThread();
1616
Watchdog thread:
1717

1818
```ts
19-
const { captureStackTrace } = require("@sentry-internal/node-native-stacktrace");
19+
const { captureStackTrace } = require(
20+
"@sentry-internal/node-native-stacktrace",
21+
);
2022

2123
const stacks = captureStackTrace();
2224
console.log(stacks);
@@ -26,58 +28,32 @@ Results in:
2628

2729
```js
2830
{
29-
'0': [
30-
{
31-
function: 'from',
32-
filename: 'node:buffer',
33-
lineno: 298,
34-
colno: 28
35-
},
36-
{
37-
function: 'pbkdf2Sync',
38-
filename: 'node:internal/crypto/pbkdf2',
39-
lineno: 78,
40-
colno: 17
41-
},
42-
{
43-
function: 'longWork',
44-
filename: '/app/test.js',
45-
lineno: 20,
46-
colno: 29
47-
},
48-
{
49-
function: '?',
50-
filename: '/app/test.js',
51-
lineno: 24,
52-
colno: 1
53-
}
54-
],
55-
'2': [
56-
{
57-
function: 'from',
58-
filename: 'node:buffer',
59-
lineno: 298,
60-
colno: 28
61-
},
62-
{
63-
function: 'pbkdf2Sync',
64-
filename: 'node:internal/crypto/pbkdf2',
65-
lineno: 78,
66-
colno: 17
67-
},
68-
{
69-
function: 'longWork',
70-
filename: '/app/worker.js',
71-
lineno: 10,
72-
colno: 29
73-
},
74-
{
75-
function: '?',
76-
filename: '/app/worker.js',
77-
lineno: 14,
78-
colno: 1
79-
}
80-
]
31+
'0': ' at from (node:buffer:299:28)\n' +
32+
' at pbkdf2Sync (node:internal/crypto/pbkdf2:78:17)\n' +
33+
' at longWork (/Users/tim/test/test/long-work.js:6:25)\n' +
34+
' at ? (/Users/tim/test/test/stack-traces.js:11:1)\n' +
35+
' at ? (node:internal/modules/cjs/loader:1734:14)\n' +
36+
' at ? (node:internal/modules/cjs/loader:1899:10)\n' +
37+
' at ? (node:internal/modules/cjs/loader:1469:32)\n' +
38+
' at ? (node:internal/modules/cjs/loader:1286:12)\n' +
39+
' at traceSync (node:diagnostics_channel:322:14)\n' +
40+
' at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)\n' +
41+
' at executeUserEntryPoint (node:internal/modules/run_main:152:5)\n' +
42+
' at ? (node:internal/main/run_main_module:33:47)',
43+
'2': ' at from (node:buffer:299:28)\n' +
44+
' at pbkdf2Sync (node:internal/crypto/pbkdf2:78:17)\n' +
45+
' at longWork (/Users/tim/test/test/long-work.js:6:25)\n' +
46+
' at ? (/Users/tim/test/test/worker.js:6:1)\n' +
47+
' at ? (node:internal/modules/cjs/loader:1734:14)\n' +
48+
' at ? (node:internal/modules/cjs/loader:1899:10)\n' +
49+
' at ? (node:internal/modules/cjs/loader:1469:32)\n' +
50+
' at ? (node:internal/modules/cjs/loader:1286:12)\n' +
51+
' at traceSync (node:diagnostics_channel:322:14)\n' +
52+
' at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)\n' +
53+
' at executeUserEntryPoint (node:internal/modules/run_main:152:5)\n' +
54+
' at ? (node:internal/main/worker_thread:212:26)\n' +
55+
' at [nodejs.internal.kHybridDispatch] (node:internal/event_target:827:20)\n' +
56+
' at ? (node:internal/per_context/messageport:23:28)'
8157
}
8258
```
8359

module.cc

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -23,76 +23,60 @@ static std::unordered_map<v8::Isolate *, ThreadInfo> threads = {};
2323

2424
// Function to be called when an isolate's execution is interrupted
2525
static void ExecutionInterrupted(Isolate *isolate, void *data) {
26-
auto promise = static_cast<std::promise<Local<Array>> *>(data);
26+
auto promise = static_cast<std::promise<std::string> *>(data);
2727
auto stack = StackTrace::CurrentStackTrace(isolate, kMaxStackFrames,
2828
StackTrace::kDetailed);
2929

3030
if (stack.IsEmpty()) {
31-
promise->set_value(Array::New(isolate, 0));
31+
promise->set_value("");
3232
return;
3333
}
3434

35-
auto frames = Array::New(isolate, stack->GetFrameCount());
35+
std::string stack_string;
3636

3737
for (int i = 0; i < stack->GetFrameCount(); i++) {
3838
auto frame = stack->GetFrame(isolate, i);
3939
auto fn_name = frame->GetFunctionName();
4040

41+
std::string function_name;
4142
if (frame->IsEval()) {
42-
fn_name =
43-
String::NewFromUtf8(isolate, "[eval]", NewStringType::kInternalized)
44-
.ToLocalChecked();
43+
function_name = "[eval]";
4544
} else if (fn_name.IsEmpty() || fn_name->Length() == 0) {
46-
fn_name = String::NewFromUtf8(isolate, "?", NewStringType::kInternalized)
47-
.ToLocalChecked();
45+
function_name = "?";
4846
} else if (frame->IsConstructor()) {
49-
fn_name = String::NewFromUtf8(isolate, "[constructor]",
50-
NewStringType::kInternalized)
51-
.ToLocalChecked();
47+
function_name = "[constructor]";
48+
} else {
49+
v8::String::Utf8Value utf8_fn(isolate, fn_name);
50+
function_name = *utf8_fn ? *utf8_fn : "?";
5251
}
5352

54-
auto frame_obj = Object::New(isolate);
55-
frame_obj
56-
->Set(isolate->GetCurrentContext(),
57-
String::NewFromUtf8(isolate, "function",
58-
NewStringType::kInternalized)
59-
.ToLocalChecked(),
60-
fn_name)
61-
.Check();
62-
63-
frame_obj
64-
->Set(isolate->GetCurrentContext(),
65-
String::NewFromUtf8(isolate, "filename",
66-
NewStringType::kInternalized)
67-
.ToLocalChecked(),
68-
frame->GetScriptName())
69-
.Check();
70-
71-
frame_obj
72-
->Set(
73-
isolate->GetCurrentContext(),
74-
String::NewFromUtf8(isolate, "lineno", NewStringType::kInternalized)
75-
.ToLocalChecked(),
76-
Integer::New(isolate, frame->GetLineNumber()))
77-
.Check();
53+
std::string filename;
54+
auto script_name = frame->GetScriptName();
55+
if (!script_name.IsEmpty()) {
56+
v8::String::Utf8Value utf8_filename(isolate, script_name);
57+
filename = *utf8_filename ? *utf8_filename : "<unknown>";
58+
} else {
59+
filename = "<unknown>";
60+
}
7861

79-
frame_obj
80-
->Set(
81-
isolate->GetCurrentContext(),
82-
String::NewFromUtf8(isolate, "colno", NewStringType::kInternalized)
83-
.ToLocalChecked(),
84-
Integer::New(isolate, frame->GetColumn()))
85-
.Check();
62+
int line_number = frame->GetLineNumber();
63+
int column_number = frame->GetColumn();
64+
65+
// Build stack trace line in JavaScript format: " at functionName (filename:line:column)"
66+
stack_string += " at " + function_name + " (" + filename + ":" +
67+
std::to_string(line_number) + ":" + std::to_string(column_number) + ")";
8668

87-
frames->Set(isolate->GetCurrentContext(), i, frame_obj).Check();
69+
if (i < stack->GetFrameCount() - 1) {
70+
stack_string += "\n";
71+
}
8872
}
8973

90-
promise->set_value(frames);
74+
promise->set_value(stack_string);
9175
}
9276

9377
// Function to capture the stack trace of a single isolate
94-
Local<Array> CaptureStackTrace(Isolate *isolate) {
95-
std::promise<Local<Array>> promise;
78+
std::string CaptureStackTrace(Isolate *isolate) {
79+
std::promise<std::string> promise;
9680
auto future = promise.get_future();
9781

9882
// The v8 isolate must be interrupted to capture the stack trace
@@ -105,7 +89,7 @@ Local<Array> CaptureStackTrace(Isolate *isolate) {
10589
void CaptureStackTraces(const FunctionCallbackInfo<Value> &args) {
10690
auto capture_from_isolate = args.GetIsolate();
10791

108-
using ThreadResult = std::tuple<std::string, Local<Array>>;
92+
using ThreadResult = std::tuple<std::string, std::string>;
10993
std::vector<std::future<ThreadResult>> futures;
11094

11195
// We collect the futures into a vec so they can be processed in parallel
@@ -128,13 +112,17 @@ void CaptureStackTraces(const FunctionCallbackInfo<Value> &args) {
128112
// JavaScript object
129113
Local<Object> result = Object::New(capture_from_isolate);
130114
for (auto &future : futures) {
131-
auto [thread_name, frames] = future.get();
115+
auto [thread_name, stack_string] = future.get();
132116

133117
auto key = String::NewFromUtf8(capture_from_isolate, thread_name.c_str(),
134118
NewStringType::kNormal)
135119
.ToLocalChecked();
136120

137-
result->Set(capture_from_isolate->GetCurrentContext(), key, frames).Check();
121+
auto value = String::NewFromUtf8(capture_from_isolate, stack_string.c_str(),
122+
NewStringType::kNormal)
123+
.ToLocalChecked();
124+
125+
result->Set(capture_from_isolate->GetCurrentContext(), key, value).Check();
138126
}
139127

140128
args.GetReturnValue().Set(result);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"devDependencies": {
3434
"@sentry-internal/eslint-config-sdk": "^9.22.0",
35+
"@sentry/core": "^9.22.0",
3536
"@types/node": "^18.19.1",
3637
"@types/node-abi": "^3.0.3",
3738
"clang-format": "^1.8.0",

src/index.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,9 @@ const arch = process.env['BUILD_ARCH'] || _arch();
1111
const abi = getAbi(versions.node, 'node');
1212
const identifier = [platform, arch, stdlib, abi].filter(c => c !== undefined && c !== null).join('-');
1313

14-
type StackFrame = {
15-
function: string;
16-
filename: string;
17-
lineno: number;
18-
colno: number;
19-
};
20-
2114
interface Native {
2215
registerThread(threadName: string): void;
23-
captureStackTrace(): Record<string, StackFrame[]>;
16+
captureStackTrace(): Record<string, string>;
2417
getThreadsLastSeen(): Record<string, number>;
2518
}
2619

@@ -180,7 +173,7 @@ export function registerThread(threadName: string = String(threadId)): void {
180173
/**
181174
* Captures stack traces for all registered threads.
182175
*/
183-
export function captureStackTrace(): Record<string, StackFrame[]> {
176+
export function captureStackTrace(): Record<string, string> {
184177
return native.captureStackTrace();
185178
}
186179

test/e2e.test.mjs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { spawnSync } from 'node:child_process';
22
import { join } from 'node:path';
3+
import { createStackParser, nodeStackLineParser } from '@sentry/core';
34
import { beforeAll, describe, expect, test } from 'vitest';
45
import { installTarballAsDependency } from './prepare.mjs';
56

67
const __dirname = import.meta.dirname || new URL('.', import.meta.url).pathname;
8+
const defaultStackParser = createStackParser(nodeStackLineParser());
79

810
describe('e2e Tests', { timeout: 20000 }, () => {
911
beforeAll(() => {
@@ -18,45 +20,63 @@ describe('e2e Tests', { timeout: 20000 }, () => {
1820

1921
const stacks = JSON.parse(result.stdout.toString());
2022

21-
expect(stacks['0']).toEqual(expect.arrayContaining([
23+
console.log(stacks);
24+
25+
const stack0 = defaultStackParser(stacks['0']);
26+
27+
expect(stack0).toEqual(expect.arrayContaining([
2228
{
2329
function: 'pbkdf2Sync',
2430
filename: expect.any(String),
2531
lineno: expect.any(Number),
2632
colno: expect.any(Number),
33+
in_app: false,
34+
module: undefined,
2735
},
2836
{
2937
function: 'longWork',
3038
filename: expect.stringMatching(/long-work.js$/),
3139
lineno: expect.any(Number),
3240
colno: expect.any(Number),
41+
in_app: true,
42+
module: undefined,
3343
},
3444
{
3545
function: '?',
3646
filename: expect.stringMatching(/stack-traces.js$/),
3747
lineno: expect.any(Number),
3848
colno: expect.any(Number),
49+
in_app: true,
50+
module: undefined,
3951
},
4052
]));
4153

42-
expect(stacks['2']).toEqual(expect.arrayContaining([
54+
const stack2 = defaultStackParser(stacks['2']);
55+
56+
expect(stack2).toEqual(expect.arrayContaining([
4357
{
4458
function: 'pbkdf2Sync',
4559
filename: expect.any(String),
4660
lineno: expect.any(Number),
4761
colno: expect.any(Number),
62+
in_app: false,
63+
module: undefined,
4864
},
4965
{
5066
function: 'longWork',
5167
filename: expect.stringMatching(/long-work.js$/),
5268
lineno: expect.any(Number),
5369
colno: expect.any(Number),
70+
in_app: true,
71+
module: undefined,
5472
},
5573
{
5674
function: '?',
5775
filename: expect.stringMatching(/worker.js$/),
5876
lineno: expect.any(Number),
5977
colno: expect.any(Number),
78+
in_app: true,
79+
module: undefined,
6080
},
6181
]));
6282
});
@@ -69,27 +89,37 @@ describe('e2e Tests', { timeout: 20000 }, () => {
6989

7090
const stacks = JSON.parse(result.stdout.toString());
7191

72-
expect(stacks['0']).toEqual(expect.arrayContaining([
92+
const stack0 = defaultStackParser(stacks['0']);
93+
94+
expect(stack0).toEqual(expect.arrayContaining([
7395
{
7496
function: 'pbkdf2Sync',
7597
filename: expect.any(String),
7698
lineno: expect.any(Number),
7799
colno: expect.any(Number),
100+
in_app: false,
101+
module: undefined,
78102
},
79103
{
80104
function: 'longWork',
81105
filename: expect.stringMatching(/long-work.js$/),
82106
lineno: expect.any(Number),
83107
colno: expect.any(Number),
108+
in_app: true,
109+
module: undefined,
84110
},
85111
{
86112
function: '?',
87113
filename: expect.stringMatching(/stalled.js$/),
88114
lineno: expect.any(Number),
89115
colno: expect.any(Number),
116+
in_app: true,
117+
module: undefined,
90118
},
91119
]));
92120

93-
expect(stacks['2'].length).toEqual(1);
121+
const stack2 = defaultStackParser(stacks['2']);
122+
123+
expect(stack2.length).toEqual(1);
94124
});
95125
});

0 commit comments

Comments
 (0)