Skip to content

Commit 66c7970

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

File tree

8 files changed

+106
-141
lines changed

8 files changed

+106
-141
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: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -23,76 +23,62 @@ 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
66+
// (filename:line:column)"
67+
stack_string += " at " + function_name + " (" + filename + ":" +
68+
std::to_string(line_number) + ":" +
69+
std::to_string(column_number) + ")";
8670

87-
frames->Set(isolate->GetCurrentContext(), i, frame_obj).Check();
71+
if (i < stack->GetFrameCount() - 1) {
72+
stack_string += "\n";
73+
}
8874
}
8975

90-
promise->set_value(frames);
76+
promise->set_value(stack_string);
9177
}
9278

9379
// Function to capture the stack trace of a single isolate
94-
Local<Array> CaptureStackTrace(Isolate *isolate) {
95-
std::promise<Local<Array>> promise;
80+
std::string CaptureStackTrace(Isolate *isolate) {
81+
std::promise<std::string> promise;
9682
auto future = promise.get_future();
9783

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

108-
using ThreadResult = std::tuple<std::string, Local<Array>>;
94+
using ThreadResult = std::tuple<std::string, std::string>;
10995
std::vector<std::future<ThreadResult>> futures;
11096

11197
// We collect the futures into a vec so they can be processed in parallel
@@ -128,13 +114,17 @@ void CaptureStackTraces(const FunctionCallbackInfo<Value> &args) {
128114
// JavaScript object
129115
Local<Object> result = Object::New(capture_from_isolate);
130116
for (auto &future : futures) {
131-
auto [thread_name, frames] = future.get();
117+
auto [thread_name, stack_string] = future.get();
132118

133119
auto key = String::NewFromUtf8(capture_from_isolate, thread_name.c_str(),
134120
NewStringType::kNormal)
135121
.ToLocalChecked();
136122

137-
result->Set(capture_from_isolate->GetCurrentContext(), key, frames).Check();
123+
auto value = String::NewFromUtf8(capture_from_isolate, stack_string.c_str(),
124+
NewStringType::kNormal)
125+
.ToLocalChecked();
126+
127+
result->Set(capture_from_isolate->GetCurrentContext(), key, value).Check();
138128
}
139129

140130
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: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
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());
9+
10+
function parseStacks(stacks) {
11+
return Object.fromEntries(
12+
Object.entries(stacks).map(([id, stack]) => [id, defaultStackParser(stack)]),
13+
);
14+
}
715

816
describe('e2e Tests', { timeout: 20000 }, () => {
917
beforeAll(() => {
@@ -16,26 +24,32 @@ describe('e2e Tests', { timeout: 20000 }, () => {
1624

1725
expect(result.status).toBe(0);
1826

19-
const stacks = JSON.parse(result.stdout.toString());
27+
const stacks = parseStacks(JSON.parse(result.stdout.toString()));
2028

2129
expect(stacks['0']).toEqual(expect.arrayContaining([
2230
{
2331
function: 'pbkdf2Sync',
2432
filename: expect.any(String),
2533
lineno: expect.any(Number),
2634
colno: expect.any(Number),
35+
in_app: false,
36+
module: undefined,
2737
},
2838
{
2939
function: 'longWork',
3040
filename: expect.stringMatching(/long-work.js$/),
3141
lineno: expect.any(Number),
3242
colno: expect.any(Number),
43+
in_app: true,
44+
module: undefined,
3345
},
3446
{
3547
function: '?',
3648
filename: expect.stringMatching(/stack-traces.js$/),
3749
lineno: expect.any(Number),
3850
colno: expect.any(Number),
51+
in_app: true,
52+
module: undefined,
3953
},
4054
]));
4155

@@ -45,48 +59,60 @@ describe('e2e Tests', { timeout: 20000 }, () => {
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
});
6383

64-
test('detect stalled thread', { timeout: 20000 }, () => {
84+
test('Detect stalled thread', { timeout: 20000 }, () => {
6585
const testFile = join(__dirname, 'stalled.js');
6686
const result = spawnSync('node', [testFile]);
6787

6888
expect(result.status).toBe(0);
6989

70-
const stacks = JSON.parse(result.stdout.toString());
90+
const stacks = parseStacks(JSON.parse(result.stdout.toString()));
7191

7292
expect(stacks['0']).toEqual(expect.arrayContaining([
7393
{
7494
function: 'pbkdf2Sync',
7595
filename: expect.any(String),
7696
lineno: expect.any(Number),
7797
colno: expect.any(Number),
98+
in_app: false,
99+
module: undefined,
78100
},
79101
{
80102
function: 'longWork',
81103
filename: expect.stringMatching(/long-work.js$/),
82104
lineno: expect.any(Number),
83105
colno: expect.any(Number),
106+
in_app: true,
107+
module: undefined,
84108
},
85109
{
86110
function: '?',
87111
filename: expect.stringMatching(/stalled.js$/),
88112
lineno: expect.any(Number),
89113
colno: expect.any(Number),
114+
in_app: true,
115+
module: undefined,
90116
},
91117
]));
92118

0 commit comments

Comments
 (0)