Skip to content

Commit c1e1b3c

Browse files
committed
remove unnecessary handle_scope
1 parent b3c5cd6 commit c1e1b3c

File tree

4 files changed

+214
-45
lines changed

4 files changed

+214
-45
lines changed

README.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,22 +174,33 @@ thread you want to capture stack traces from.
174174

175175
- `threadName` (optional): Name for the thread. Defaults to the current thread
176176
ID.
177-
- `asyncStorage`: `AsyncStorageArgs` to fetch state from `AsyncLocalStorage` on
178-
stack trace capture.
177+
- `asyncStorage` (optional): `AsyncStorageArgs` to fetch state from
178+
`AsyncLocalStorage` on stack trace capture.
179179

180180
```ts
181181
type AsyncStorageArgs = {
182-
// AsyncLocalStorage instance to fetch state from
182+
/** AsyncLocalStorage instance to fetch state from */
183183
asyncLocalStorage: AsyncLocalStorage<unknown>;
184-
// Optional key to fetch specific property from the store object
185-
storageKey?: string | symbol;
184+
/**
185+
* Optional array of keys to pick a specific property from the store.
186+
* Key will be traversed in order through Objects/Maps to reach the desired property.
187+
*
188+
* This is useful if you want to capture Open Telemetry context values as state.
189+
*
190+
* To get this value:
191+
* context.getValue(MY_UNIQUE_SYMBOL_REF)
192+
*
193+
* You would set:
194+
* stateLookup: ['_currentContext', MY_UNIQUE_SYMBOL_REF]
195+
*/
196+
stateLookup?: Array<string | symbol>;
186197
};
187198
```
188199

189200
#### `captureStackTrace<State>(): Record<string, Thread<A, P>>`
190201

191202
Captures stack traces from all registered threads. Can be called from any thread
192-
but will not capture the stack trace of the calling thread itself.
203+
but will not capture a stack trace for the calling thread itself.
193204

194205
```ts
195206
type Thread<A = unknown, P = unknown> = {

module.cc

Lines changed: 180 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <node.h>
55
#include <node_version.h>
66
#include <optional>
7+
#include <vector>
78

89
// Platform-specific includes for time functions
910
#ifdef _WIN32
@@ -30,8 +31,9 @@ static const int kMaxStackFrames = 50;
3031
struct AsyncLocalStorageLookup {
3132
// Async local storage instance associated with this thread
3233
v8::Global<v8::Value> async_local_storage;
33-
// Optional key used to look up specific data in an async local storage object
34-
std::optional<v8::Global<v8::Value>> storage_key;
34+
// Optional ordered array of keys (string | symbol) to traverse nested
35+
// Map/Object structures to fetch the final state object
36+
std::optional<std::vector<v8::Global<v8::Value>>> storage_keys;
3537
};
3638

3739
// Structure to hold information for each thread/isolate
@@ -75,11 +77,126 @@ struct ThreadResult {
7577
std::string poll_state;
7678
};
7779

78-
std::string JSONStringify(Isolate *isolate, Local<Value> value) {
79-
HandleScope handle_scope(isolate);
80+
// Recursively sanitize a value to be safely JSON-stringifiable by:
81+
// - Removing properties whose values are BigInt, Function, or Symbol
82+
// (dropped for objects, omitted from arrays)
83+
// - Breaking cycles by omitting repeated objects (undefined -> dropped/omitted)
84+
// - Preserving primitives and traversing arrays/objects
85+
static v8::Local<v8::Value>
86+
SanitizeForJSON(v8::Isolate *isolate, v8::Local<v8::Context> context,
87+
v8::Local<v8::Value> value,
88+
std::vector<v8::Local<v8::Object>> &ancestors) {
89+
// Fast-path for primitives that are always JSON-compatible
90+
if (value->IsNull() || value->IsBoolean() || value->IsNumber() ||
91+
value->IsString()) {
92+
return value;
93+
}
94+
95+
// Values that JSON.stringify cannot handle directly
96+
if (value->IsBigInt() || value->IsSymbol() || value->IsFunction() ||
97+
value->IsUndefined()) {
98+
// Returning undefined here lets callers decide to drop (object) or null
99+
// (array)
100+
return v8::Undefined(isolate);
101+
}
102+
103+
// Arrays
104+
if (value->IsArray()) {
105+
auto arr = value.As<v8::Array>();
106+
// Cycle detection
107+
auto arr_obj = value.As<v8::Object>();
108+
for (auto &a : ancestors) {
109+
if (a->StrictEquals(arr_obj)) {
110+
return v8::Undefined(isolate);
111+
}
112+
}
113+
114+
auto length = arr->Length();
115+
auto out = v8::Array::New(isolate, 0);
116+
ancestors.push_back(arr_obj);
117+
118+
uint32_t out_index = 0;
119+
for (uint32_t i = 0; i < length; ++i) {
120+
auto maybeEl = arr->Get(context, i);
121+
v8::Local<v8::Value> el =
122+
maybeEl.IsEmpty() ? v8::Undefined(isolate) : maybeEl.ToLocalChecked();
123+
124+
auto sanitized = SanitizeForJSON(isolate, context, el, ancestors);
125+
if (!sanitized->IsUndefined()) {
126+
out->Set(context, out_index++, sanitized)
127+
.Check(); // omit undefined entries entirely
128+
}
129+
}
130+
ancestors.pop_back();
131+
return out;
132+
}
133+
134+
// Objects (including Dates, RegExps, Maps as objects; we only traverse
135+
// enumerable own props)
136+
if (value->IsObject()) {
137+
auto obj = value.As<v8::Object>();
138+
// Cycle detection
139+
for (auto &a : ancestors) {
140+
if (a->StrictEquals(obj)) {
141+
return v8::Undefined(isolate);
142+
}
143+
}
144+
145+
ancestors.push_back(obj);
146+
147+
// Collect own enumerable property names (string-keyed)
148+
auto maybe_props = obj->GetPropertyNames(context);
149+
if (maybe_props.IsEmpty()) {
150+
ancestors.pop_back();
151+
return obj; // Nothing enumerable to sanitize
152+
}
153+
154+
auto props = maybe_props.ToLocalChecked();
155+
auto out = v8::Object::New(isolate);
156+
auto len = props->Length();
157+
for (uint32_t i = 0; i < len; ++i) {
158+
auto maybeKey = props->Get(context, i);
159+
if (maybeKey.IsEmpty())
160+
continue;
161+
162+
auto key = maybeKey.ToLocalChecked();
163+
if (!key->IsString()) {
164+
// Skip symbol and non-string keys to match JSON behavior
165+
continue;
166+
}
167+
168+
auto maybeVal = obj->Get(context, key);
169+
if (maybeVal.IsEmpty())
170+
continue;
171+
172+
auto val = maybeVal.ToLocalChecked();
173+
auto sanitized = SanitizeForJSON(isolate, context, val, ancestors);
174+
if (!sanitized->IsUndefined()) {
175+
out->Set(context, key, sanitized).Check();
176+
}
177+
// else: undefined -> drop property
178+
}
179+
180+
ancestors.pop_back();
181+
return out;
182+
}
183+
184+
// Fallback: return as-is (shouldn't hit here for other exotic types)
185+
return value;
186+
}
80187

188+
std::string JSONStringify(Isolate *isolate, Local<Value> value) {
81189
auto context = isolate->GetCurrentContext();
82-
auto maybe_json = v8::JSON::Stringify(context, value);
190+
191+
// Sanitize the value first to avoid JSON failures (e.g., BigInt, cycles)
192+
std::vector<v8::Local<v8::Object>> ancestors;
193+
auto sanitized = SanitizeForJSON(isolate, context, value, ancestors);
194+
if (sanitized->IsUndefined()) {
195+
// Nothing serializable
196+
return "";
197+
}
198+
199+
auto maybe_json = v8::JSON::Stringify(context, sanitized);
83200
if (maybe_json.IsEmpty()) {
84201
return "";
85202
}
@@ -89,8 +206,6 @@ std::string JSONStringify(Isolate *isolate, Local<Value> value) {
89206

90207
// Function to get stack frames from a V8 stack trace
91208
JsStackFrames GetStackFrames(Isolate *isolate) {
92-
HandleScope handle_scope(isolate);
93-
94209
auto stack = StackTrace::CurrentStackTrace(isolate, kMaxStackFrames,
95210
StackTrace::kDetailed);
96211

@@ -135,12 +250,11 @@ JsStackFrames GetStackFrames(Isolate *isolate) {
135250
// Function to fetch the thread state from the async context store
136251
std::string GetThreadState(Isolate *isolate,
137252
const AsyncLocalStorageLookup &store) {
138-
HandleScope handle_scope(isolate);
139253

140-
// Node.js stores the async local storage in the isolate's
141-
// "ContinuationPreservedEmbedderData" map, keyed by the
142-
// AsyncLocalStorage instance.
143-
// https://github.com/nodejs/node/blob/c6316f9db9869864cea84e5f07585fa08e3e06d2/src/async_context_frame.cc#L37
254+
// Node.js stores the async local storage in the isolate's
255+
// "ContinuationPreservedEmbedderData" map, keyed by the
256+
// AsyncLocalStorage instance.
257+
// https://github.com/nodejs/node/blob/c6316f9db9869864cea84e5f07585fa08e3e06d2/src/async_context_frame.cc#L37
144258
#if GET_CONTINUATION_PRESERVED_EMBEDDER_DATA_V2
145259
auto data = isolate->GetContinuationPreservedEmbedderDataV2().As<Value>();
146260
#else
@@ -162,18 +276,36 @@ std::string GetThreadState(Isolate *isolate,
162276

163277
auto root_store = maybe_root_store.ToLocalChecked();
164278

165-
if (store.storage_key.has_value() && root_store->IsObject()) {
166-
auto local_key = store.storage_key->Get(isolate);
279+
if (store.storage_keys.has_value()) {
280+
// Walk the keys to get the desired nested value
281+
const auto &keys = store.storage_keys.value();
282+
auto current = root_store;
283+
284+
for (auto &gkey : keys) {
285+
auto local_key = gkey.Get(isolate);
286+
if (!(local_key->IsString() || local_key->IsSymbol())) {
287+
continue;
288+
}
289+
290+
v8::MaybeLocal<v8::Value> maybeValue;
291+
if (current->IsMap()) {
292+
auto map_val = current.As<v8::Map>();
293+
maybeValue = map_val->Get(context, local_key);
294+
} else if (current->IsObject()) {
295+
auto obj_val = current.As<v8::Object>();
296+
maybeValue = obj_val->Get(context, local_key);
297+
} else {
298+
return "";
299+
}
167300

168-
if (local_key->IsString() || local_key->IsSymbol()) {
169-
auto root_obj = root_store.As<v8::Object>();
170-
auto maybeValue = root_obj->Get(context, local_key);
171301
if (maybeValue.IsEmpty()) {
172302
return "";
173303
}
174304

175-
root_store = maybeValue.ToLocalChecked();
305+
current = maybeValue.ToLocalChecked();
176306
}
307+
308+
root_store = current;
177309
}
178310

179311
return JSONStringify(isolate, root_store);
@@ -232,7 +364,6 @@ CaptureStackTrace(Isolate *isolate,
232364
// Function to capture stack traces from all registered threads
233365
void CaptureStackTraces(const FunctionCallbackInfo<Value> &args) {
234366
auto capture_from_isolate = args.GetIsolate();
235-
HandleScope handle_scope(capture_from_isolate);
236367

237368
std::vector<ThreadResult> results;
238369

@@ -401,7 +532,6 @@ void RegisterThreadInternal(
401532
// Function to register a thread and update its last seen time
402533
void RegisterThread(const FunctionCallbackInfo<Value> &args) {
403534
auto isolate = args.GetIsolate();
404-
HandleScope handle_scope(isolate);
405535

406536
if (args.Length() == 1 && args[0]->IsString()) {
407537
v8::String::Utf8Value utf8(isolate, args[0]);
@@ -430,24 +560,45 @@ void RegisterThread(const FunctionCallbackInfo<Value> &args) {
430560
return;
431561
}
432562

433-
std::optional<v8::Global<v8::Value>> storage_key = std::nullopt;
563+
std::optional<std::vector<v8::Global<v8::Value>>> storage_keys =
564+
std::nullopt;
434565

435-
auto storage_key_val = obj->Get(
436-
isolate->GetCurrentContext(),
437-
String::NewFromUtf8(isolate, "storageKey", NewStringType::kInternalized)
438-
.ToLocalChecked());
566+
auto storage_key_val =
567+
obj->Get(isolate->GetCurrentContext(),
568+
String::NewFromUtf8(isolate, "stateLookup",
569+
NewStringType::kInternalized)
570+
.ToLocalChecked());
439571

440572
if (!storage_key_val.IsEmpty()) {
573+
441574
auto local_val = storage_key_val.ToLocalChecked();
442575
if (!local_val->IsUndefined() && !local_val->IsNull()) {
443-
storage_key = v8::Global<v8::Value>(isolate, local_val);
576+
if (local_val->IsArray()) {
577+
578+
auto arr = local_val.As<v8::Array>();
579+
std::vector<v8::Global<v8::Value>> keys_vec;
580+
uint32_t length = arr->Length();
581+
for (uint32_t i = 0; i < length; ++i) {
582+
auto maybeEl = arr->Get(isolate->GetCurrentContext(), i);
583+
if (maybeEl.IsEmpty())
584+
continue;
585+
auto el = maybeEl.ToLocalChecked();
586+
if (el->IsString() || el->IsSymbol()) {
587+
588+
keys_vec.emplace_back(isolate, el);
589+
}
590+
}
591+
if (!keys_vec.empty()) {
592+
storage_keys = std::move(keys_vec);
593+
}
594+
}
444595
}
445596
}
446597

447598
auto store = AsyncLocalStorageLookup{
448599
v8::Global<v8::Value>(isolate,
449600
async_local_storage_val.ToLocalChecked()),
450-
std::move(storage_key)};
601+
std::move(storage_keys)};
451602

452603
RegisterThreadInternal(isolate, thread_name, std::move(store));
453604
} else {
@@ -457,7 +608,8 @@ void RegisterThread(const FunctionCallbackInfo<Value> &args) {
457608
"Incorrect arguments. Expected: \n"
458609
"- registerThread(threadName: string) or \n"
459610
"- registerThread(storage: {asyncLocalStorage: AsyncLocalStorage; "
460-
"storageKey?: string | symbol}, threadName: string)",
611+
"stateLookup?: Array<string | symbol>}, "
612+
"threadName: string)",
461613
NewStringType::kInternalized)
462614
.ToLocalChecked()));
463615
}
@@ -491,7 +643,6 @@ steady_clock::time_point GetUnbiasedMonotonicTime() {
491643
// Function to track a thread and set its state
492644
void ThreadPoll(const FunctionCallbackInfo<Value> &args) {
493645
auto isolate = args.GetIsolate();
494-
HandleScope handle_scope(isolate);
495646

496647
bool enable_last_seen = true;
497648
if (args.Length() > 0 && args[0]->IsBoolean()) {
@@ -524,7 +675,6 @@ void ThreadPoll(const FunctionCallbackInfo<Value> &args) {
524675
// Function to get the last seen time of all registered threads
525676
void GetThreadsLastSeen(const FunctionCallbackInfo<Value> &args) {
526677
Isolate *isolate = args.GetIsolate();
527-
HandleScope handle_scope(isolate);
528678

529679
Local<Object> result = Object::New(isolate);
530680
milliseconds now = duration_cast<milliseconds>(

src/index.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@ type AsyncStorageArgs = {
1616
/** The AsyncLocalStorage instance used to fetch the store */
1717
asyncLocalStorage: AsyncLocalStorage<unknown>;
1818
/**
19-
* Optional key in the store to fetch the state from. If not provided, the entire store will be returned.
19+
* Optional array of keys to fetch a specific property from the store
20+
* Key will be traversed in order through Objects/Maps to reach the desired property.
2021
*
21-
* This can be useful to fetch only a specific part of the state or in the
22-
* case of Open Telemetry, where it stores context under a symbol key.
22+
* This is useful if you want to capture Open Telemetry context values as state.
23+
*
24+
* To get this value:
25+
* context.getValue(my_unique_symbol_ref)
26+
*
27+
* You would set:
28+
* stateLookup: ['_currentContext', my_unique_symbol_ref]
2329
*/
24-
storageKey?: string | symbol;
30+
stateLookup?: Array<string | symbol>;
2531
}
2632

2733
type Thread<A = unknown, P = unknown> = {

test/async-storage.mjs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { registerThread } from '@sentry-internal/node-native-stacktrace';
44
import { longWork } from './long-work.js';
55

66
const asyncLocalStorage = new AsyncLocalStorage();
7-
const storageKey = Symbol.for('sentry_scopes');
7+
const SOME_UNIQUE_SYMBOL = Symbol.for('sentry_scopes');
88

9-
registerThread({ asyncLocalStorage, storageKey });
9+
registerThread({ asyncLocalStorage, stateLookup: ['_currentContext', SOME_UNIQUE_SYMBOL] });
1010

1111
function withTraceId(traceId, fn) {
12-
return asyncLocalStorage.run({
13-
[storageKey]: { traceId },
14-
}, fn);
12+
// This is a decent approximation of how Otel stores context in the ALS store
13+
const store = {
14+
_currentContext: new Map([ [SOME_UNIQUE_SYMBOL, { traceId }] ])
15+
};
16+
return asyncLocalStorage.run(store, fn);
1517
}
1618

1719
const watchdog = new Worker('./test/watchdog.js');

0 commit comments

Comments
 (0)