@apifuse/connector-sdk 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +44 -0
  2. package/bin/apifuse-check.ts +408 -0
  3. package/bin/apifuse-dev.ts +222 -0
  4. package/bin/apifuse-init.ts +390 -0
  5. package/bin/apifuse-perf.ts +1101 -0
  6. package/bin/apifuse-record.ts +446 -0
  7. package/bin/apifuse-test.ts +688 -0
  8. package/bin/apifuse.ts +51 -0
  9. package/package.json +64 -0
  10. package/src/__tests__/auth.test.ts +396 -0
  11. package/src/__tests__/browser-auth.test.ts +180 -0
  12. package/src/__tests__/browser.test.ts +632 -0
  13. package/src/__tests__/connectors-yaml.test.ts +135 -0
  14. package/src/__tests__/define.test.ts +225 -0
  15. package/src/__tests__/errors.test.ts +69 -0
  16. package/src/__tests__/executor.test.ts +214 -0
  17. package/src/__tests__/http.test.ts +238 -0
  18. package/src/__tests__/insights.test.ts +210 -0
  19. package/src/__tests__/instrumentation.test.ts +290 -0
  20. package/src/__tests__/otlp.test.ts +141 -0
  21. package/src/__tests__/perf.test.ts +60 -0
  22. package/src/__tests__/proxy.test.ts +359 -0
  23. package/src/__tests__/recipes.test.ts +36 -0
  24. package/src/__tests__/serve.test.ts +233 -0
  25. package/src/__tests__/session.test.ts +231 -0
  26. package/src/__tests__/state.test.ts +100 -0
  27. package/src/__tests__/stealth.test.ts +57 -0
  28. package/src/__tests__/testing.test.ts +97 -0
  29. package/src/__tests__/tls.test.ts +345 -0
  30. package/src/__tests__/types.test.ts +142 -0
  31. package/src/__tests__/utils.test.ts +62 -0
  32. package/src/__tests__/waterfall.test.ts +270 -0
  33. package/src/config/connectors-yaml.ts +373 -0
  34. package/src/config/loader.ts +122 -0
  35. package/src/define.ts +137 -0
  36. package/src/dev.ts +38 -0
  37. package/src/errors.ts +68 -0
  38. package/src/index.test.ts +1 -0
  39. package/src/index.ts +100 -0
  40. package/src/protocol.ts +183 -0
  41. package/src/recipes/gov-api.ts +97 -0
  42. package/src/recipes/rest-api.ts +152 -0
  43. package/src/runtime/auth.ts +245 -0
  44. package/src/runtime/browser.ts +724 -0
  45. package/src/runtime/connector.ts +20 -0
  46. package/src/runtime/executor.ts +51 -0
  47. package/src/runtime/http.ts +248 -0
  48. package/src/runtime/insights.ts +456 -0
  49. package/src/runtime/instrumentation.ts +424 -0
  50. package/src/runtime/otlp.ts +171 -0
  51. package/src/runtime/perf.ts +73 -0
  52. package/src/runtime/session.ts +573 -0
  53. package/src/runtime/state.ts +124 -0
  54. package/src/runtime/tls.ts +410 -0
  55. package/src/runtime/trace.ts +261 -0
  56. package/src/runtime/waterfall.ts +245 -0
  57. package/src/serve.ts +665 -0
  58. package/src/stealth/profiles.ts +391 -0
  59. package/src/testing/helpers.ts +144 -0
  60. package/src/testing/index.ts +2 -0
  61. package/src/testing/run.ts +88 -0
  62. package/src/types/playwright-stealth.d.ts +9 -0
  63. package/src/types.ts +243 -0
  64. package/src/utils/date.ts +163 -0
  65. package/src/utils/parse.ts +66 -0
  66. package/src/utils/text.ts +20 -0
  67. package/src/utils/transform.ts +62 -0
@@ -0,0 +1,410 @@
1
+ import { ModuleClient, SessionClient } from "tlsclientwrapper";
2
+
3
+ import type { ProxyResolutionOptions } from "../config/loader";
4
+ import { resolveProxyConfig } from "../config/loader";
5
+ import { TransportError } from "../errors";
6
+ import { getStealthProfile } from "../stealth/profiles";
7
+ import type {
8
+ CookieJar,
9
+ TlsClient,
10
+ TlsFetchOptions,
11
+ TlsResponse,
12
+ TlsSession,
13
+ } from "../types";
14
+
15
+ const DEFAULT_PROFILE = "chrome-146";
16
+
17
+ const MISSING_PROXY_WARNING =
18
+ "[connector-sdk] Connector requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
19
+
20
+ export type TlsClientOptions = ProxyResolutionOptions & {
21
+ warn?: (message: string) => void;
22
+ };
23
+
24
+ const TLS_PROFILE_MAP: Record<string, string> = {
25
+ "chrome-131": "chrome_131",
26
+ "chrome-133": "chrome_133",
27
+ "chrome-144": "chrome_144",
28
+ "chrome-146": "chrome_146",
29
+ "firefox-132": "firefox_132",
30
+ "firefox-133": "firefox_133",
31
+ "firefox-135": "firefox_135",
32
+ "firefox-147": "firefox_147",
33
+ "safari-15": "safari_15_6_1",
34
+ "safari-16": "safari_16_0",
35
+ "ios-safari-17": "safari_ios_17_0",
36
+ "ios-safari-18": "safari_ios_18_0",
37
+ "ios-safari-26": "safari_ios_26_0",
38
+ };
39
+
40
+ type TlsClientWrapperResponse = {
41
+ status: number;
42
+ body: string;
43
+ headers?: Record<string, string>;
44
+ rawHeaders?: Record<string, string> | [string, string][];
45
+ usedProtocol?: string;
46
+ httpVersion?: string;
47
+ cookies?:
48
+ | Record<string, { name?: string; value?: string }>
49
+ | Array<{ name?: string; value?: string }>;
50
+ };
51
+
52
+ class CookieJarImpl implements CookieJar {
53
+ private readonly cookies: Record<string, string>;
54
+
55
+ constructor(cookieStrings: string[]) {
56
+ this.cookies = {};
57
+
58
+ for (const cookieString of cookieStrings) {
59
+ const [nameValue] = cookieString.split(";");
60
+ if (!nameValue) {
61
+ continue;
62
+ }
63
+
64
+ const separatorIndex = nameValue.indexOf("=");
65
+ if (separatorIndex === -1) {
66
+ continue;
67
+ }
68
+
69
+ const name = nameValue.slice(0, separatorIndex).trim();
70
+ const value = nameValue.slice(separatorIndex + 1).trim();
71
+ this.cookies[name] = value;
72
+ }
73
+ }
74
+
75
+ get(name: string): string | undefined {
76
+ return this.cookies[name];
77
+ }
78
+
79
+ getAll(): Record<string, string> {
80
+ return { ...this.cookies };
81
+ }
82
+
83
+ toString(): string {
84
+ return Object.entries(this.cookies)
85
+ .map(([name, value]) => `${name}=${value}`)
86
+ .join("; ");
87
+ }
88
+
89
+ find(predicate: (cookie: string) => boolean): string | undefined {
90
+ for (const [name, value] of Object.entries(this.cookies)) {
91
+ const cookie = `${name}=${value}`;
92
+ if (predicate(cookie)) {
93
+ return cookie;
94
+ }
95
+ }
96
+
97
+ return undefined;
98
+ }
99
+ }
100
+
101
+ function resolveIdentifier(profileName: string): string {
102
+ try {
103
+ const profile = getStealthProfile(profileName);
104
+ if (profile.tlsClientIdentifier) {
105
+ return profile.tlsClientIdentifier;
106
+ }
107
+ } catch {
108
+ // Unknown profile name falls through to static mapping and heuristic.
109
+ }
110
+
111
+ return TLS_PROFILE_MAP[profileName] ?? profileName.replaceAll("-", "_");
112
+ }
113
+
114
+ function resolveUrl(baseUrl: string, url: string): string {
115
+ return new URL(url, baseUrl).toString();
116
+ }
117
+
118
+ function toCookieStrings(
119
+ response: TlsClientWrapperResponse,
120
+ ): string[] | undefined {
121
+ const cookies = response.cookies;
122
+ if (!cookies) {
123
+ return undefined;
124
+ }
125
+
126
+ const values = Array.isArray(cookies) ? cookies : Object.values(cookies);
127
+ const normalized = values
128
+ .map((cookie) => {
129
+ if (!cookie?.name) {
130
+ return null;
131
+ }
132
+
133
+ return `${cookie.name}=${cookie.value ?? ""}`;
134
+ })
135
+ .filter((cookie): cookie is string => cookie !== null);
136
+
137
+ return normalized.length > 0 ? normalized : undefined;
138
+ }
139
+
140
+ function normalizeRawHeaders(
141
+ rawHeaders: TlsClientWrapperResponse["rawHeaders"],
142
+ headers: Record<string, string>,
143
+ ): [string, string][] {
144
+ if (Array.isArray(rawHeaders)) {
145
+ return rawHeaders;
146
+ }
147
+
148
+ if (rawHeaders && typeof rawHeaders === "object") {
149
+ return Object.entries(rawHeaders);
150
+ }
151
+
152
+ return Object.entries(headers);
153
+ }
154
+
155
+ export function normalizeResponse(
156
+ response: TlsClientWrapperResponse,
157
+ ): TlsResponse {
158
+ const headers = response.headers ?? {};
159
+ const cookies = new CookieJarImpl(toCookieStrings(response) ?? []);
160
+
161
+ return {
162
+ status: response.status,
163
+ ok: response.status >= 200 && response.status < 300,
164
+ headers,
165
+ rawHeaders: normalizeRawHeaders(response.rawHeaders, headers),
166
+ body: response.body,
167
+ httpVersion: response.httpVersion ?? response.usedProtocol,
168
+ cookies,
169
+ json<T>(): Promise<T> {
170
+ return Promise.resolve(JSON.parse(response.body) as T);
171
+ },
172
+ };
173
+ }
174
+
175
+ function getErrorMessage(error: unknown): string {
176
+ if (error instanceof Error) {
177
+ return error.toString();
178
+ }
179
+
180
+ return String(error);
181
+ }
182
+
183
+ function normalizeBody(body: TlsFetchOptions["body"]): string | null {
184
+ if (body === undefined) {
185
+ return null;
186
+ }
187
+
188
+ if (typeof body === "string") {
189
+ return body;
190
+ }
191
+
192
+ if (Buffer.isBuffer(body)) {
193
+ return body.toString();
194
+ }
195
+
196
+ return String(body);
197
+ }
198
+
199
+ async function sendRequest(
200
+ session: SessionClient,
201
+ method: string,
202
+ url: string,
203
+ options: TlsFetchOptions,
204
+ ): Promise<TlsClientWrapperResponse> {
205
+ const requestOptions = {
206
+ headers: options.headers,
207
+ proxy: options.proxy,
208
+ ...(options.headerOrder ? { headerOrder: options.headerOrder } : {}),
209
+ };
210
+
211
+ switch (method) {
212
+ case "POST":
213
+ return (await session.post(
214
+ url,
215
+ normalizeBody(options.body),
216
+ requestOptions,
217
+ )) as TlsClientWrapperResponse;
218
+ case "PUT":
219
+ return (await session.put(
220
+ url,
221
+ normalizeBody(options.body),
222
+ requestOptions,
223
+ )) as TlsClientWrapperResponse;
224
+ case "PATCH":
225
+ return (await session.patch(
226
+ url,
227
+ normalizeBody(options.body),
228
+ requestOptions,
229
+ )) as TlsClientWrapperResponse;
230
+ case "DELETE":
231
+ return (await session.delete(
232
+ url,
233
+ requestOptions,
234
+ )) as TlsClientWrapperResponse;
235
+ case "HEAD":
236
+ return (await session.head(
237
+ url,
238
+ requestOptions,
239
+ )) as TlsClientWrapperResponse;
240
+ case "OPTIONS":
241
+ return (await session.options(
242
+ url,
243
+ requestOptions,
244
+ )) as TlsClientWrapperResponse;
245
+ default:
246
+ return (await session.get(
247
+ url,
248
+ requestOptions,
249
+ )) as TlsClientWrapperResponse;
250
+ }
251
+ }
252
+
253
+ function createSessionFetcher(
254
+ baseUrl: string,
255
+ defaultProfile: string,
256
+ moduleClient: ModuleClient,
257
+ clientOptions: TlsClientOptions,
258
+ ): TlsSession {
259
+ let sessionClient: SessionClient | null = null;
260
+ let activeProxy: string | undefined;
261
+ let activeTlsIdentifier: string | undefined;
262
+ let hasWarnedMissingProxy = false;
263
+ const warn = clientOptions.warn ?? console.warn;
264
+
265
+ function resolveRequestProxy(options?: TlsFetchOptions): string | undefined {
266
+ const resolvedProxy = resolveProxyConfig({
267
+ proxy: options?.proxy ?? clientOptions.proxy,
268
+ upstream: clientOptions.upstream,
269
+ apifuseConfig: clientOptions.apifuseConfig,
270
+ });
271
+
272
+ if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
273
+ hasWarnedMissingProxy = true;
274
+ warn(MISSING_PROXY_WARNING);
275
+ }
276
+
277
+ return resolvedProxy.url;
278
+ }
279
+
280
+ function closeCurrentSession(): void {
281
+ const client = sessionClient as
282
+ | (SessionClient & {
283
+ destroySession?: () => Promise<unknown>;
284
+ close?: () => void;
285
+ })
286
+ | null;
287
+
288
+ void client?.destroySession?.();
289
+ client?.close?.();
290
+ sessionClient = null;
291
+ activeProxy = undefined;
292
+ activeTlsIdentifier = undefined;
293
+ }
294
+
295
+ function getSessionClient(
296
+ profile?: string,
297
+ proxy?: string,
298
+ ja3?: string,
299
+ ): SessionClient {
300
+ const tlsIdentifier = ja3 ?? resolveIdentifier(profile ?? defaultProfile);
301
+
302
+ if (
303
+ !sessionClient ||
304
+ activeProxy !== proxy ||
305
+ activeTlsIdentifier !== tlsIdentifier
306
+ ) {
307
+ if (sessionClient) {
308
+ closeCurrentSession();
309
+ }
310
+
311
+ sessionClient = new SessionClient(moduleClient, {
312
+ tlsClientIdentifier: tlsIdentifier,
313
+ ...(proxy ? { proxyUrl: proxy } : {}),
314
+ timeoutSeconds: 30,
315
+ } as ConstructorParameters<typeof SessionClient>[1]);
316
+ activeProxy = proxy;
317
+ activeTlsIdentifier = tlsIdentifier;
318
+ }
319
+
320
+ return sessionClient;
321
+ }
322
+
323
+ return {
324
+ async fetch(url, options = {}) {
325
+ const proxy = resolveRequestProxy(options);
326
+ const session = getSessionClient(
327
+ options.profile,
328
+ proxy,
329
+ options.tls?.ja3,
330
+ );
331
+ const requestUrl = resolveUrl(baseUrl, url);
332
+
333
+ try {
334
+ const method = (options.method ?? "GET").toUpperCase();
335
+ const response = await sendRequest(session, method, requestUrl, {
336
+ ...options,
337
+ proxy,
338
+ });
339
+
340
+ if (response.status >= 400) {
341
+ throw new TransportError(
342
+ `HTTP ${response.status}: ${response.body || "Request failed"}`,
343
+ {
344
+ status: response.status,
345
+ },
346
+ );
347
+ }
348
+
349
+ return normalizeResponse(response);
350
+ } catch (error) {
351
+ if (error instanceof TransportError) {
352
+ throw error;
353
+ }
354
+
355
+ throw new TransportError(`Network error: ${getErrorMessage(error)}`, {
356
+ status: 0,
357
+ cause: error instanceof Error ? error : undefined,
358
+ });
359
+ }
360
+ },
361
+ close() {
362
+ closeCurrentSession();
363
+ },
364
+ };
365
+ }
366
+
367
+ export function createTlsClient(
368
+ baseUrl: string,
369
+ defaultProfileOrOptions: string | TlsClientOptions = DEFAULT_PROFILE,
370
+ clientOptions: TlsClientOptions = {},
371
+ ): TlsClient {
372
+ const defaultProfile =
373
+ typeof defaultProfileOrOptions === "string"
374
+ ? defaultProfileOrOptions
375
+ : DEFAULT_PROFILE;
376
+ const resolvedClientOptions =
377
+ typeof defaultProfileOrOptions === "string"
378
+ ? clientOptions
379
+ : defaultProfileOrOptions;
380
+ const moduleClient = new ModuleClient();
381
+ let sharedSession: TlsSession | null = null;
382
+
383
+ function getSharedSession(): TlsSession {
384
+ if (!sharedSession) {
385
+ sharedSession = createSessionFetcher(
386
+ baseUrl,
387
+ defaultProfile,
388
+ moduleClient,
389
+ resolvedClientOptions,
390
+ );
391
+ }
392
+
393
+ return sharedSession;
394
+ }
395
+
396
+ return {
397
+ fetch(url: string, options?: TlsFetchOptions) {
398
+ return getSharedSession().fetch(url, options);
399
+ },
400
+ createSession(opts?: { profile?: string }) {
401
+ const sessionProfile = opts?.profile ?? defaultProfile;
402
+ return createSessionFetcher(
403
+ baseUrl,
404
+ sessionProfile,
405
+ moduleClient,
406
+ resolvedClientOptions,
407
+ );
408
+ },
409
+ };
410
+ }
@@ -0,0 +1,261 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ import type {
4
+ TraceContext as BaseTraceContext,
5
+ TraceAttributeValue,
6
+ TraceConfig,
7
+ TraceSpan,
8
+ } from "../types";
9
+ import { exportSpansOTLP, type OTLPExportOptions } from "./otlp";
10
+
11
+ export type SpanAttributeValue = TraceAttributeValue;
12
+
13
+ export type Span = TraceSpan;
14
+
15
+ export interface TraceContext extends BaseTraceContext {
16
+ getSpans(): Span[];
17
+ }
18
+
19
+ export interface CreateTraceContextOptions {
20
+ maxSpans?: number;
21
+ onSpan?: (span: Span) => void;
22
+ exportOptions?: OTLPExportOptions;
23
+ resourceAttributes?: Record<string, string>;
24
+ }
25
+
26
+ type SpanHookOptions<T> = {
27
+ attributes?: Record<string, unknown>;
28
+ onSuccess?: (value: T) => Record<string, unknown> | undefined;
29
+ onError?: (error: unknown) => Record<string, unknown> | undefined;
30
+ };
31
+
32
+ type PendingSpan = {
33
+ id: string;
34
+ name: string;
35
+ startedAt: number;
36
+ parentId?: string;
37
+ sequence: number;
38
+ attributes: Record<string, SpanAttributeValue>;
39
+ };
40
+
41
+ type CompletedSpanEntry = {
42
+ sequence: number;
43
+ span: Span;
44
+ };
45
+
46
+ export interface TraceRecorder {
47
+ runSpan<T>(
48
+ name: string,
49
+ fn: () => Promise<T> | T,
50
+ options?: SpanHookOptions<T>,
51
+ ): Promise<T>;
52
+ }
53
+
54
+ export const TRACE_RECORDER = Symbol.for(
55
+ "@apifuse/connector-sdk/runtime/trace-recorder",
56
+ );
57
+
58
+ type InternalTraceContext = TraceContext & {
59
+ [TRACE_RECORDER]: TraceRecorder;
60
+ _exportOptions?: OTLPExportOptions;
61
+ _resourceAttributes?: Record<string, string>;
62
+ };
63
+
64
+ function buildOTLPExportOptions(
65
+ config?: TraceConfig,
66
+ ): OTLPExportOptions | undefined {
67
+ if (!config || config.exporter !== "otlp") {
68
+ return undefined;
69
+ }
70
+
71
+ const endpoint = config.otlp?.endpoint ?? config.endpoint;
72
+ if (!endpoint) {
73
+ return undefined;
74
+ }
75
+
76
+ return {
77
+ endpoint,
78
+ headers: config.otlp?.headers,
79
+ timeout: config.otlp?.timeout,
80
+ };
81
+ }
82
+
83
+ export function resolveTraceContextOptions(
84
+ config?: TraceConfig,
85
+ ): CreateTraceContextOptions {
86
+ return {
87
+ maxSpans: config?.maxSpans,
88
+ onSpan: config?.onSpan,
89
+ exportOptions: buildOTLPExportOptions(config),
90
+ };
91
+ }
92
+
93
+ function normalizeAttributeValue(
94
+ value: unknown,
95
+ ): SpanAttributeValue | undefined {
96
+ if (
97
+ typeof value === "string" ||
98
+ typeof value === "number" ||
99
+ typeof value === "boolean"
100
+ ) {
101
+ return value;
102
+ }
103
+
104
+ if (value === null || value === undefined) {
105
+ return undefined;
106
+ }
107
+
108
+ return String(value);
109
+ }
110
+
111
+ function normalizeAttributes(
112
+ attributes?: Record<string, unknown>,
113
+ ): Record<string, SpanAttributeValue> {
114
+ if (!attributes) {
115
+ return {};
116
+ }
117
+
118
+ const normalizedEntries = Object.entries(attributes)
119
+ .map(([key, value]) => [key, normalizeAttributeValue(value)] as const)
120
+ .filter(
121
+ (entry): entry is readonly [string, SpanAttributeValue] =>
122
+ entry[1] !== undefined,
123
+ );
124
+
125
+ return Object.fromEntries(normalizedEntries);
126
+ }
127
+
128
+ function insertCompletedSpan(
129
+ completed: CompletedSpanEntry[],
130
+ entry: CompletedSpanEntry,
131
+ maxSpans: number,
132
+ ): void {
133
+ const insertAt = completed.findIndex(
134
+ (existingEntry) => existingEntry.sequence > entry.sequence,
135
+ );
136
+
137
+ if (insertAt === -1) {
138
+ completed.push(entry);
139
+ } else {
140
+ completed.splice(insertAt, 0, entry);
141
+ }
142
+
143
+ if (completed.length > maxSpans) {
144
+ completed.splice(0, completed.length - maxSpans);
145
+ }
146
+ }
147
+
148
+ export function getTraceRecorder(
149
+ trace: BaseTraceContext,
150
+ ): TraceRecorder | null {
151
+ return (trace as Partial<InternalTraceContext>)[TRACE_RECORDER] ?? null;
152
+ }
153
+
154
+ export function createTraceContext(
155
+ options: CreateTraceContextOptions = {},
156
+ ): TraceContext {
157
+ const maxSpans = options.maxSpans ?? 1000;
158
+ const completed: CompletedSpanEntry[] = [];
159
+ const activeSpanStorage = new AsyncLocalStorage<PendingSpan | undefined>();
160
+ let sequence = 0;
161
+ let traceContext!: InternalTraceContext;
162
+
163
+ const scheduleExport = () => {
164
+ if (!traceContext._exportOptions) {
165
+ return;
166
+ }
167
+
168
+ const spans = completed.map((entry) => ({ ...entry.span }));
169
+ setImmediate(() => {
170
+ void exportSpansOTLP(
171
+ spans,
172
+ traceContext._exportOptions as OTLPExportOptions,
173
+ traceContext._resourceAttributes,
174
+ );
175
+ });
176
+ };
177
+
178
+ const recorder: TraceRecorder = {
179
+ async runSpan(name, fn, spanOptions = {}) {
180
+ const pendingSpan: PendingSpan = {
181
+ id: crypto.randomUUID(),
182
+ name,
183
+ startedAt: Date.now(),
184
+ parentId: activeSpanStorage.getStore()?.id,
185
+ sequence: sequence++,
186
+ attributes: normalizeAttributes(spanOptions.attributes),
187
+ };
188
+
189
+ const finalize = (
190
+ status: Span["status"],
191
+ extraAttributes?: Record<string, unknown>,
192
+ error?: string,
193
+ ) => {
194
+ const endedAt = Date.now();
195
+ const duration = endedAt - pendingSpan.startedAt;
196
+ const attributes = {
197
+ ...pendingSpan.attributes,
198
+ ...normalizeAttributes(extraAttributes),
199
+ };
200
+
201
+ if (attributes.duration_ms === undefined) {
202
+ attributes.duration_ms = duration;
203
+ }
204
+
205
+ const span: Span = {
206
+ id: pendingSpan.id,
207
+ name: pendingSpan.name,
208
+ startedAt: pendingSpan.startedAt,
209
+ endedAt,
210
+ duration_ms: duration,
211
+ status,
212
+ attributes,
213
+ ...(error ? { error } : {}),
214
+ ...(pendingSpan.parentId ? { parentId: pendingSpan.parentId } : {}),
215
+ };
216
+
217
+ insertCompletedSpan(
218
+ completed,
219
+ { sequence: pendingSpan.sequence, span },
220
+ maxSpans,
221
+ );
222
+ options.onSpan?.(span);
223
+
224
+ if (!pendingSpan.parentId) {
225
+ scheduleExport();
226
+ }
227
+ };
228
+
229
+ return activeSpanStorage.run(pendingSpan, async () => {
230
+ try {
231
+ const value = await fn();
232
+ const successAttributes = spanOptions.onSuccess?.(value);
233
+ finalize("ok", successAttributes ?? undefined);
234
+ return value;
235
+ } catch (error) {
236
+ const errorMessage =
237
+ error instanceof Error ? error.message : String(error);
238
+ const errorAttributes = spanOptions.onError?.(error);
239
+ finalize("error", errorAttributes ?? undefined, errorMessage);
240
+ throw error;
241
+ }
242
+ });
243
+ },
244
+ };
245
+
246
+ traceContext = {
247
+ span(name, fn) {
248
+ return recorder.runSpan(name, fn);
249
+ },
250
+ getSpans() {
251
+ return completed.map((entry) => ({ ...entry.span }));
252
+ },
253
+ ...(options.exportOptions ? { _exportOptions: options.exportOptions } : {}),
254
+ ...(options.resourceAttributes
255
+ ? { _resourceAttributes: { ...options.resourceAttributes } }
256
+ : {}),
257
+ [TRACE_RECORDER]: recorder,
258
+ };
259
+
260
+ return traceContext;
261
+ }