Skip to content

Commit ef7919c

Browse files
committed
Fix APIPromise workaround in bundlers
1 parent 8e5ee96 commit ef7919c

4 files changed

Lines changed: 123 additions & 86 deletions

File tree

js/src/auto-instrumentations/hook.mts

Lines changed: 5 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -20,95 +20,14 @@ import { aiSDKConfigs } from "./configs/ai-sdk.js";
2020
import { claudeAgentSDKConfigs } from "./configs/claude-agent-sdk.js";
2121
import { googleGenAIConfigs } from "./configs/google-genai.js";
2222
import { ModulePatch } from "./loader/cjs-patch.js";
23+
import { patchTracingChannel } from "./patch-tracing-channel.js";
2324

24-
// Patch diagnostics_channel.tracePromise to handle APIPromise correctly
25-
// MUST be done here (before any SDK code runs) to fix Anthropic APIPromise incompatibility
26-
// Construct the module path dynamically to prevent build from stripping "node:" prefix
25+
// Patch diagnostics_channel.tracePromise to handle APIPromise correctly.
26+
// MUST be done here (before any SDK code runs) to fix Anthropic APIPromise incompatibility.
27+
// Construct the module path dynamically to prevent build from stripping "node:" prefix.
2728
const dcPath = ["node", "diagnostics_channel"].join(":");
2829
const dc: any = await import(/* @vite-ignore */ dcPath as any);
29-
30-
// Get TracingChannel class by creating a dummy instance
31-
const dummyChannel = dc.tracingChannel("dummy");
32-
const TracingChannel = dummyChannel.constructor;
33-
34-
if (
35-
TracingChannel &&
36-
!Object.getOwnPropertyDescriptor(TracingChannel.prototype, "hasSubscribers")
37-
) {
38-
Object.defineProperty(TracingChannel.prototype, "hasSubscribers", {
39-
configurable: true,
40-
enumerable: false,
41-
get(this: {
42-
start?: { hasSubscribers?: boolean };
43-
end?: { hasSubscribers?: boolean };
44-
asyncStart?: { hasSubscribers?: boolean };
45-
asyncEnd?: { hasSubscribers?: boolean };
46-
error?: { hasSubscribers?: boolean };
47-
}) {
48-
return Boolean(
49-
this.start?.hasSubscribers ||
50-
this.end?.hasSubscribers ||
51-
this.asyncStart?.hasSubscribers ||
52-
this.asyncEnd?.hasSubscribers ||
53-
this.error?.hasSubscribers,
54-
);
55-
},
56-
});
57-
}
58-
59-
if (TracingChannel && TracingChannel.prototype.tracePromise) {
60-
TracingChannel.prototype.tracePromise = function (
61-
fn: any,
62-
context: any = {},
63-
thisArg: any,
64-
...args: any[]
65-
) {
66-
const { start, end, asyncStart, asyncEnd, error } = this;
67-
68-
function reject(err: any) {
69-
context.error = err;
70-
error?.publish(context);
71-
asyncStart?.publish(context);
72-
asyncEnd?.publish(context);
73-
return Promise.reject(err);
74-
}
75-
76-
function resolve(result: any) {
77-
context.result = result;
78-
asyncStart?.publish(context);
79-
asyncEnd?.publish(context);
80-
return result;
81-
}
82-
83-
start?.publish(context);
84-
85-
try {
86-
// PATCHED: Removed instanceof Promise check and Promise.resolve() wrapper
87-
// This allows APIPromise and other Promise subclasses to work correctly
88-
89-
const result = Reflect.apply(fn, thisArg, args);
90-
91-
if (
92-
result &&
93-
(typeof result === "object" || typeof result === "function") &&
94-
typeof result.then === "function"
95-
) {
96-
return result.then(resolve, reject);
97-
}
98-
99-
context.result = result;
100-
asyncStart?.publish(context);
101-
asyncEnd?.publish(context);
102-
return result;
103-
} catch (err) {
104-
context.error = err;
105-
error?.publish(context);
106-
throw err;
107-
} finally {
108-
end?.publish(context);
109-
}
110-
};
111-
}
30+
patchTracingChannel(dc.tracingChannel);
11231

11332
// Combine all instrumentation configs
11433
const allConfigs = [
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Patches TracingChannel.prototype to handle APIPromise and other Promise subclasses
3+
* that change the constructor signature (violating the species contract).
4+
*
5+
* node:diagnostics_channel's tracePromise wraps the result with Promise.resolve(),
6+
* which calls the subclass constructor with the wrong signature for classes like
7+
* Anthropic's APIPromise. This patch uses duck-typing (.then check) instead.
8+
*
9+
* This is applied both in the loader hook (hook.mts) for the --import path,
10+
* and in configureNode/configureBrowser for the bundler plugin path.
11+
*/
12+
13+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14+
export function patchTracingChannel(
15+
tracingChannelFn: (name: string) => any,
16+
): void {
17+
const dummyChannel = tracingChannelFn("__braintrust_probe__");
18+
const TracingChannel = dummyChannel?.constructor;
19+
20+
if (!TracingChannel?.prototype) {
21+
return;
22+
}
23+
24+
if (
25+
!Object.getOwnPropertyDescriptor(TracingChannel.prototype, "hasSubscribers")
26+
) {
27+
Object.defineProperty(TracingChannel.prototype, "hasSubscribers", {
28+
configurable: true,
29+
enumerable: false,
30+
get(this: {
31+
start?: { hasSubscribers?: boolean };
32+
end?: { hasSubscribers?: boolean };
33+
asyncStart?: { hasSubscribers?: boolean };
34+
asyncEnd?: { hasSubscribers?: boolean };
35+
error?: { hasSubscribers?: boolean };
36+
}) {
37+
return Boolean(
38+
this.start?.hasSubscribers ||
39+
this.end?.hasSubscribers ||
40+
this.asyncStart?.hasSubscribers ||
41+
this.asyncEnd?.hasSubscribers ||
42+
this.error?.hasSubscribers,
43+
);
44+
},
45+
});
46+
}
47+
48+
if (TracingChannel.prototype.tracePromise) {
49+
TracingChannel.prototype.tracePromise = function (
50+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
51+
fn: any,
52+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
53+
context: any = {},
54+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
55+
thisArg: any,
56+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
57+
...args: any[]
58+
) {
59+
const { start, end, asyncStart, asyncEnd, error } = this;
60+
61+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
62+
function reject(err: any) {
63+
context.error = err;
64+
error?.publish(context);
65+
asyncStart?.publish(context);
66+
asyncEnd?.publish(context);
67+
return Promise.reject(err);
68+
}
69+
70+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71+
function resolve(result: any) {
72+
context.result = result;
73+
asyncStart?.publish(context);
74+
asyncEnd?.publish(context);
75+
return result;
76+
}
77+
78+
start?.publish(context);
79+
80+
try {
81+
// PATCHED: duck-type thenable instead of instanceof Promise + Promise.resolve().
82+
// This allows APIPromise and other Promise subclasses to work correctly —
83+
// Promise.resolve() on a subclass with a non-standard constructor signature
84+
// triggers the species protocol and calls the constructor, which throws.
85+
const result = Reflect.apply(fn, thisArg, args);
86+
87+
if (
88+
result &&
89+
(typeof result === "object" || typeof result === "function") &&
90+
typeof result.then === "function"
91+
) {
92+
return result.then(resolve, reject);
93+
}
94+
95+
context.result = result;
96+
asyncStart?.publish(context);
97+
asyncEnd?.publish(context);
98+
return result;
99+
} catch (err) {
100+
context.error = err;
101+
error?.publish(context);
102+
throw err;
103+
} finally {
104+
end?.publish(context);
105+
}
106+
};
107+
}
108+
}

js/src/browser/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { tracingChannel } from "dc-browser";
22
import iso from "../isomorph";
33
import { _internalSetInitialState } from "../logger";
44
import { registry } from "../instrumentation/registry";
5+
import { patchTracingChannel } from "../auto-instrumentations/patch-tracing-channel";
56

67
// This is copied from next.js. It seems they define AsyncLocalStorage in the edge
78
// environment, even though it's not defined in the browser.
@@ -36,6 +37,10 @@ export function configureBrowser(): void {
3637
iso.newTracingChannel = <_M = any>(nameOrChannels: string | object) =>
3738
tracingChannel(nameOrChannels as any) as any;
3839

40+
// Patch TracingChannel.prototype.tracePromise to handle APIPromise and other
41+
// Promise subclasses (mirrors the fix in hook.mts for the --import loader path).
42+
patchTracingChannel(tracingChannel);
43+
3944
iso.getEnv = (name: string) => {
4045
if (typeof process === "undefined" || typeof process.env === "undefined") {
4146
return undefined;

js/src/node/config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AsyncLocalStorage } from "node:async_hooks";
22
import * as diagnostics_channel from "node:diagnostics_channel";
33
import * as path from "node:path";
4+
import { patchTracingChannel } from "../auto-instrumentations/patch-tracing-channel";
45
import * as fs from "node:fs/promises";
56
import * as os from "node:os";
67
import * as fsSync from "node:fs";
@@ -24,6 +25,10 @@ export function configureNode() {
2425
iso.newAsyncLocalStorage = <T>() => new AsyncLocalStorage<T>();
2526
iso.newTracingChannel = <_M = any>(nameOrChannels: string | object) =>
2627
(diagnostics_channel as any).tracingChannel(nameOrChannels) as any;
28+
29+
// Patch TracingChannel.prototype.tracePromise to handle APIPromise and other
30+
// Promise subclasses (mirrors the fix in hook.mts for the --import loader path).
31+
patchTracingChannel((diagnostics_channel as any).tracingChannel);
2732
iso.processOn = (event: string, handler: (code: unknown) => void) => {
2833
process.on(event, handler);
2934
};

0 commit comments

Comments
 (0)