promptlayer 1.0.60 → 1.1.0

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 (37) hide show
  1. package/README.md +9 -0
  2. package/dist/esm/chunk-SWBNW72U.js +2 -0
  3. package/dist/esm/chunk-SWBNW72U.js.map +1 -0
  4. package/dist/esm/index.js +2 -2
  5. package/dist/esm/index.js.map +1 -1
  6. package/dist/esm/openai-agents.js +3 -0
  7. package/dist/esm/openai-agents.js.map +1 -0
  8. package/dist/index.d.mts +229 -9
  9. package/dist/index.d.ts +229 -9
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/openai-agents.d.mts +42 -0
  13. package/dist/openai-agents.d.ts +42 -0
  14. package/dist/openai-agents.js +3 -0
  15. package/dist/openai-agents.js.map +1 -0
  16. package/package.json +24 -3
  17. package/src/integrations/openai-agents/helpers.test.ts +254 -0
  18. package/src/integrations/openai-agents/ids.ts +27 -0
  19. package/src/integrations/openai-agents/index.ts +8 -0
  20. package/src/integrations/openai-agents/instrumentation.test.ts +46 -0
  21. package/src/integrations/openai-agents/instrumentation.ts +47 -0
  22. package/src/integrations/openai-agents/mapping.ts +714 -0
  23. package/src/integrations/openai-agents/otlp-json.ts +120 -0
  24. package/src/integrations/openai-agents/processor.test.ts +509 -0
  25. package/src/integrations/openai-agents/processor.ts +388 -0
  26. package/src/integrations/openai-agents/time.ts +56 -0
  27. package/src/integrations/openai-agents/types.ts +49 -0
  28. package/src/integrations/openai-agents/url.ts +9 -0
  29. package/src/openai-agents.ts +1 -0
  30. package/src/types.ts +302 -9
  31. package/src/utils/blueprint-builder.test.ts +727 -0
  32. package/src/utils/blueprint-builder.ts +957 -126
  33. package/src/utils/streaming.test.ts +498 -0
  34. package/src/utils/streaming.ts +471 -43
  35. package/src/utils/utils.ts +4 -0
  36. package/tsup.config.ts +4 -1
  37. package/vitest.config.ts +3 -0
@@ -0,0 +1,120 @@
1
+ import { SDK_VERSION } from "@/utils/utils";
2
+ import type {
3
+ AttributeValue,
4
+ OtlpJsonPayload,
5
+ OtlpSpanRecord,
6
+ } from "@/integrations/openai-agents/types";
7
+
8
+ const toAnyValue = (value: AttributeValue): Record<string, unknown> => {
9
+ if (typeof value === "string") {
10
+ return { stringValue: value };
11
+ }
12
+
13
+ if (typeof value === "boolean") {
14
+ return { boolValue: value };
15
+ }
16
+
17
+ if (typeof value === "number") {
18
+ if (Number.isInteger(value)) {
19
+ return { intValue: String(value) };
20
+ }
21
+ return { doubleValue: value };
22
+ }
23
+
24
+ if (value === null) {
25
+ return { stringValue: "null" };
26
+ }
27
+
28
+ if (Array.isArray(value)) {
29
+ return {
30
+ arrayValue: {
31
+ values: value.map((item) => toAnyValue(item)),
32
+ },
33
+ };
34
+ }
35
+
36
+ return {
37
+ kvlistValue: {
38
+ values: Object.entries(value).map(([key, nestedValue]) => ({
39
+ key,
40
+ value: toAnyValue(nestedValue),
41
+ })),
42
+ },
43
+ };
44
+ };
45
+
46
+ const toKeyValues = (attributes: Record<string, AttributeValue>) => {
47
+ return Object.entries(attributes).map(([key, value]) => ({
48
+ key,
49
+ value: toAnyValue(value),
50
+ }));
51
+ };
52
+
53
+ export interface BuildOtlpJsonPayloadOptions {
54
+ serviceName?: string;
55
+ scopeName?: string;
56
+ scopeVersion?: string;
57
+ }
58
+
59
+ export const buildOtlpJsonPayload = (
60
+ spans: OtlpSpanRecord[],
61
+ {
62
+ serviceName = "promptlayer-openai-agents-js",
63
+ scopeName = "promptlayer.integrations.openai_agents",
64
+ scopeVersion = SDK_VERSION,
65
+ }: BuildOtlpJsonPayloadOptions = {}
66
+ ): OtlpJsonPayload => {
67
+ return {
68
+ resourceSpans: [
69
+ {
70
+ resource: {
71
+ attributes: toKeyValues({
72
+ "service.name": serviceName,
73
+ }),
74
+ },
75
+ scopeSpans: [
76
+ {
77
+ scope: {
78
+ name: scopeName,
79
+ version: scopeVersion,
80
+ },
81
+ spans: spans.map((span) => {
82
+ const payload: Record<string, unknown> = {
83
+ traceId: span.traceId,
84
+ spanId: span.spanId,
85
+ name: span.name,
86
+ kind: span.kind,
87
+ startTimeUnixNano: span.startTimeUnixNano,
88
+ endTimeUnixNano: span.endTimeUnixNano ?? span.startTimeUnixNano,
89
+ attributes: toKeyValues(span.attributes),
90
+ events: (span.events ?? []).map((event) => ({
91
+ name: event.name,
92
+ timeUnixNano: event.timeUnixNano,
93
+ attributes: toKeyValues(event.attributes ?? {}),
94
+ })),
95
+ links: [],
96
+ };
97
+
98
+ if (span.parentSpanId) {
99
+ payload.parentSpanId = span.parentSpanId;
100
+ }
101
+
102
+ if (span.traceState) {
103
+ payload.traceState = span.traceState;
104
+ }
105
+
106
+ if (span.status) {
107
+ payload.status = {
108
+ code: span.status.code,
109
+ ...(span.status.message ? { message: span.status.message } : {}),
110
+ };
111
+ }
112
+
113
+ return payload;
114
+ }),
115
+ },
116
+ ],
117
+ },
118
+ ],
119
+ };
120
+ };
@@ -0,0 +1,509 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("@/utils/utils", async () => {
4
+ const actual = await vi.importActual<typeof import("@/utils/utils")>(
5
+ "@/utils/utils"
6
+ );
7
+
8
+ return {
9
+ ...actual,
10
+ fetchWithRetry: vi.fn(),
11
+ getCommonHeaders: vi.fn(() => ({
12
+ "User-Agent": "promptlayer-js/test",
13
+ "X-SDK-Version": "test-version",
14
+ })),
15
+ };
16
+ });
17
+
18
+ import { mapSpanId } from "@/integrations/openai-agents/ids";
19
+ import { PromptLayerOpenAIAgentsProcessor } from "@/integrations/openai-agents/processor";
20
+ import { fetchWithRetry } from "@/utils/utils";
21
+
22
+ const fetchWithRetryMock = vi.mocked(fetchWithRetry);
23
+
24
+ const keyValuesToObject = (keyValues: Array<{ key: string; value: any }>) => {
25
+ return Object.fromEntries(
26
+ keyValues.map((entry) => [
27
+ entry.key,
28
+ entry.value.stringValue ??
29
+ entry.value.boolValue ??
30
+ entry.value.intValue ??
31
+ entry.value.doubleValue,
32
+ ])
33
+ );
34
+ };
35
+
36
+ describe("PromptLayerOpenAIAgentsProcessor", () => {
37
+ beforeEach(() => {
38
+ fetchWithRetryMock.mockResolvedValue({
39
+ ok: true,
40
+ status: 200,
41
+ statusText: "OK",
42
+ } as Response);
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ it("exports a finished generation trace as OTLP JSON", async () => {
50
+ const processor = new PromptLayerOpenAIAgentsProcessor({
51
+ apiKey: "pl_test",
52
+ baseURL: "https://api.promptlayer.dev",
53
+ });
54
+ const trace = {
55
+ traceId: "trace_0af7651916cd43dd8448eb211c80319c",
56
+ name: "Support workflow",
57
+ groupId: "group_123",
58
+ metadata: {
59
+ customer_id: "cust_123",
60
+ nested: { ignored: true },
61
+ },
62
+ } as any;
63
+ const span = {
64
+ traceId: trace.traceId,
65
+ spanId: "span_generation_123",
66
+ parentId: null,
67
+ startedAt: "2026-03-17T14:15:16.123456789Z",
68
+ endedAt: "2026-03-17T14:15:17.123456789Z",
69
+ error: null,
70
+ traceMetadata: trace.metadata,
71
+ spanData: {
72
+ type: "generation",
73
+ model: "gpt-4.1",
74
+ input: [{ role: "user", content: "Hello" }],
75
+ output: [{ role: "assistant", content: "Hi" }],
76
+ usage: { input_tokens: 3, output_tokens: 5 },
77
+ },
78
+ } as any;
79
+
80
+ await processor.onTraceStart(trace);
81
+ await processor.onSpanStart(span);
82
+ await processor.onSpanEnd(span);
83
+ await processor.onTraceEnd(trace);
84
+
85
+ expect(fetchWithRetryMock).toHaveBeenCalledTimes(1);
86
+ expect(fetchWithRetryMock).toHaveBeenCalledWith(
87
+ "https://api.promptlayer.dev/v1/traces",
88
+ expect.objectContaining({
89
+ method: "POST",
90
+ headers: expect.objectContaining({
91
+ "Content-Type": "application/json",
92
+ "X-API-KEY": "pl_test",
93
+ "X-PromptLayer-Integration": "openai-agents-js",
94
+ "X-SDK-Version": "test-version",
95
+ }),
96
+ })
97
+ );
98
+
99
+ const [, request] = fetchWithRetryMock.mock.calls[0];
100
+ const payload = JSON.parse(String(request?.body));
101
+ const spans = payload.resourceSpans[0].scopeSpans[0].spans;
102
+
103
+ expect(spans).toHaveLength(2);
104
+
105
+ const generationSpan = spans.find((item: any) => item.name === "Generation");
106
+ expect(generationSpan).toBeDefined();
107
+
108
+ const attrs = keyValuesToObject(generationSpan.attributes);
109
+ expect(attrs["gen_ai.provider.name"]).toBe("openai.responses");
110
+ expect(attrs["gen_ai.request.model"]).toBe("gpt-4.1");
111
+ expect(attrs["gen_ai.usage.input_tokens"]).toBe("3");
112
+ expect(attrs["gen_ai.usage.output_tokens"]).toBe("5");
113
+ expect(attrs["promptlayer.telemetry.source"]).toBe("openai-agents-js");
114
+ });
115
+
116
+ it("records error status and exception events", async () => {
117
+ const processor = new PromptLayerOpenAIAgentsProcessor({
118
+ apiKey: "pl_test",
119
+ baseURL: "https://api.promptlayer.dev",
120
+ });
121
+ const trace = {
122
+ traceId: "trace_0af7651916cd43dd8448eb211c80319c",
123
+ name: "Errored workflow",
124
+ groupId: null,
125
+ metadata: {},
126
+ } as any;
127
+ const span = {
128
+ traceId: trace.traceId,
129
+ spanId: "span_function_123",
130
+ parentId: null,
131
+ startedAt: "2026-03-17T14:15:16.123456789Z",
132
+ endedAt: "2026-03-17T14:15:17.123456789Z",
133
+ error: {
134
+ message: "rate limit exceeded",
135
+ },
136
+ traceMetadata: {},
137
+ spanData: {
138
+ type: "function",
139
+ name: "weather",
140
+ input: "{\"city\":\"Tokyo\"}",
141
+ output: "",
142
+ },
143
+ } as any;
144
+
145
+ await processor.onTraceStart(trace);
146
+ await processor.onSpanStart(span);
147
+ await processor.onSpanEnd(span);
148
+ await processor.onTraceEnd(trace);
149
+
150
+ const [, request] = fetchWithRetryMock.mock.calls[0];
151
+ const payload = JSON.parse(String(request?.body));
152
+ const functionSpan = payload.resourceSpans[0].scopeSpans[0].spans.find(
153
+ (item: any) => item.name === "Function: weather"
154
+ );
155
+
156
+ expect(functionSpan.status).toEqual({
157
+ code: 2,
158
+ message: "rate limit exceeded",
159
+ });
160
+ expect(functionSpan.events).toEqual(
161
+ expect.arrayContaining([
162
+ expect.objectContaining({
163
+ name: "exception",
164
+ }),
165
+ ])
166
+ );
167
+ });
168
+
169
+ it("exports response spans with canonical response attributes", async () => {
170
+ const processor = new PromptLayerOpenAIAgentsProcessor({
171
+ apiKey: "pl_test",
172
+ baseURL: "https://api.promptlayer.dev",
173
+ });
174
+ const trace = {
175
+ traceId: "trace_0af7651916cd43dd8448eb211c80319c",
176
+ name: "Response workflow",
177
+ groupId: null,
178
+ metadata: {},
179
+ } as any;
180
+ const span = {
181
+ traceId: trace.traceId,
182
+ spanId: "span_response_123",
183
+ parentId: null,
184
+ startedAt: "2026-03-17T14:15:16.123456789Z",
185
+ endedAt: "2026-03-17T14:15:17.123456789Z",
186
+ error: null,
187
+ traceMetadata: {},
188
+ spanData: {
189
+ type: "response",
190
+ response_id: "resp_123",
191
+ _input: [
192
+ { role: "user", content: "Hello" },
193
+ {
194
+ type: "function_call",
195
+ call_id: "call_weather",
196
+ name: "weather_lookup",
197
+ arguments: "{\"city\":\"Barcelona\"}",
198
+ },
199
+ {
200
+ type: "tool_call_output_item",
201
+ rawItem: {
202
+ type: "function_call_result",
203
+ callId: "call_weather",
204
+ },
205
+ output: {
206
+ type: "text",
207
+ text: "{\"temp_c\":20,\"condition\":\"Sunny\"}",
208
+ },
209
+ },
210
+ ],
211
+ _response: {
212
+ object: "response",
213
+ id: "resp_123",
214
+ model: "gpt-4.1",
215
+ usage: { input_tokens: 3, output_tokens: 5 },
216
+ output: [{ role: "assistant", content: [{ type: "output_text", text: "Hi" }] }],
217
+ },
218
+ },
219
+ } as any;
220
+
221
+ await processor.onTraceStart(trace);
222
+ await processor.onSpanStart(span);
223
+ await processor.onSpanEnd(span);
224
+ await processor.onTraceEnd(trace);
225
+
226
+ const [, request] = fetchWithRetryMock.mock.calls[0];
227
+ const payload = JSON.parse(String(request?.body));
228
+ const responseSpan = payload.resourceSpans[0].scopeSpans[0].spans.find(
229
+ (item: any) => item.name === "Response"
230
+ );
231
+ const attrs = keyValuesToObject(responseSpan.attributes);
232
+
233
+ expect(attrs["gen_ai.provider.name"]).toBe("openai.responses");
234
+ expect(attrs["gen_ai.request.model"]).toBe("gpt-4.1");
235
+ expect(attrs["gen_ai.response.model"]).toBe("gpt-4.1");
236
+ expect(attrs["gen_ai.response.id"]).toBe("resp_123");
237
+ expect(attrs["gen_ai.prompt.0.content"]).toBe("Hello");
238
+ expect(attrs["gen_ai.prompt.1.tool_calls"]).toBe(
239
+ "[{\"arguments\":{\"city\":\"Barcelona\"},\"id\":\"call_weather\",\"name\":\"weather_lookup\",\"type\":\"tool_call\"}]"
240
+ );
241
+ expect(attrs["gen_ai.prompt.2.role"]).toBe("tool");
242
+ expect(attrs["gen_ai.prompt.2.tool_call_id"]).toBe("call_weather");
243
+ expect(attrs["gen_ai.prompt.2.content"]).toBe(
244
+ "{\"temp_c\":20,\"condition\":\"Sunny\"}"
245
+ );
246
+ expect(attrs["gen_ai.completion.0.content"]).toBe("Hi");
247
+ expect(attrs["openai_agents.response.object"]).toBe("response");
248
+ });
249
+
250
+ it("preserves nested parent-child relationships in exported spans", async () => {
251
+ const processor = new PromptLayerOpenAIAgentsProcessor({
252
+ apiKey: "pl_test",
253
+ baseURL: "https://api.promptlayer.dev",
254
+ });
255
+ const trace = {
256
+ traceId: "trace_0af7651916cd43dd8448eb211c80319c",
257
+ name: "Nested workflow",
258
+ groupId: null,
259
+ metadata: {},
260
+ } as any;
261
+ const parentSpan = {
262
+ traceId: trace.traceId,
263
+ spanId: "span_parent_function",
264
+ parentId: null,
265
+ startedAt: "2026-03-17T14:15:16.123456789Z",
266
+ endedAt: "2026-03-17T14:15:18.123456789Z",
267
+ error: null,
268
+ traceMetadata: {},
269
+ spanData: {
270
+ type: "function",
271
+ name: "weather",
272
+ input: "{\"city\":\"Tokyo\"}",
273
+ output: "72 and sunny",
274
+ },
275
+ } as any;
276
+ const childSpan = {
277
+ traceId: trace.traceId,
278
+ spanId: "span_child_custom",
279
+ parentId: parentSpan.spanId,
280
+ startedAt: "2026-03-17T14:15:17.123456789Z",
281
+ endedAt: "2026-03-17T14:15:17.223456789Z",
282
+ error: null,
283
+ traceMetadata: {},
284
+ spanData: {
285
+ type: "custom",
286
+ name: "child-work",
287
+ data: { ok: true },
288
+ },
289
+ } as any;
290
+
291
+ await processor.onTraceStart(trace);
292
+ await processor.onSpanStart(parentSpan);
293
+ await processor.onSpanStart(childSpan);
294
+ await processor.onSpanEnd(childSpan);
295
+ await processor.onSpanEnd(parentSpan);
296
+ await processor.onTraceEnd(trace);
297
+
298
+ const [, request] = fetchWithRetryMock.mock.calls[0];
299
+ const payload = JSON.parse(String(request?.body));
300
+ const exportedChild = payload.resourceSpans[0].scopeSpans[0].spans.find(
301
+ (item: any) => item.name === "child-work"
302
+ );
303
+
304
+ expect(exportedChild.parentSpanId).toBe(mapSpanId(parentSpan.spanId));
305
+ });
306
+
307
+ it("uses traceparent metadata to parent the synthetic root", async () => {
308
+ const processor = new PromptLayerOpenAIAgentsProcessor({
309
+ apiKey: "pl_test",
310
+ baseURL: "https://api.promptlayer.dev",
311
+ });
312
+ const trace = {
313
+ traceId: "trace_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
314
+ name: "Traceparent workflow",
315
+ groupId: null,
316
+ metadata: {
317
+ traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
318
+ tracestate: "vendor=value",
319
+ tenant: "acme",
320
+ },
321
+ } as any;
322
+ const span = {
323
+ traceId: trace.traceId,
324
+ spanId: "span_generation_traceparent",
325
+ parentId: null,
326
+ startedAt: "2026-03-17T14:15:16.123456789Z",
327
+ endedAt: "2026-03-17T14:15:17.123456789Z",
328
+ error: null,
329
+ traceMetadata: trace.metadata,
330
+ spanData: {
331
+ type: "generation",
332
+ model: "gpt-4.1",
333
+ input: [{ role: "user", content: "Hello" }],
334
+ output: [{ role: "assistant", content: "Hi" }],
335
+ },
336
+ } as any;
337
+
338
+ await processor.onTraceStart(trace);
339
+ await processor.onSpanStart(span);
340
+ await processor.onSpanEnd(span);
341
+ await processor.onTraceEnd(trace);
342
+
343
+ const [, request] = fetchWithRetryMock.mock.calls[0];
344
+ const payload = JSON.parse(String(request?.body));
345
+ const spans = payload.resourceSpans[0].scopeSpans[0].spans;
346
+ const rootSpan = spans.find((item: any) => item.name === "Traceparent workflow");
347
+ const generationSpan = spans.find((item: any) => item.name === "Generation");
348
+
349
+ expect(rootSpan.traceId).toBe("11111111111111111111111111111111");
350
+ expect(generationSpan.traceId).toBe("11111111111111111111111111111111");
351
+ expect(rootSpan.parentSpanId).toBe("2222222222222222");
352
+ expect(rootSpan.traceState).toBe("vendor=value");
353
+ expect(generationSpan.traceState).toBe("vendor=value");
354
+
355
+ const rootAttrs = keyValuesToObject(rootSpan.attributes);
356
+ expect(rootAttrs["openai_agents.trace_id_original"]).toBe(
357
+ "trace_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
358
+ );
359
+ });
360
+
361
+ it("recovers serialized trace metadata when the root trace callback is absent", async () => {
362
+ const processor = new PromptLayerOpenAIAgentsProcessor({
363
+ apiKey: "pl_test",
364
+ baseURL: "https://api.promptlayer.dev",
365
+ });
366
+ const span = {
367
+ traceId: "trace_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
368
+ spanId: "span_generation_orphan",
369
+ parentId: null,
370
+ startedAt: "2026-03-17T14:15:16.123456789Z",
371
+ endedAt: "2026-03-17T14:15:17.123456789Z",
372
+ error: null,
373
+ traceMetadata: {
374
+ workflow_name: "Recovered workflow",
375
+ group_id: "group_456",
376
+ metadata: {
377
+ tenant: "acme",
378
+ },
379
+ },
380
+ spanData: {
381
+ type: "generation",
382
+ model: "gpt-4.1",
383
+ input: [{ role: "user", content: "Hello" }],
384
+ output: [{ role: "assistant", content: "Hi" }],
385
+ },
386
+ } as any;
387
+ const trace = {
388
+ traceId: span.traceId,
389
+ name: "Ignored root name",
390
+ groupId: null,
391
+ metadata: {},
392
+ } as any;
393
+
394
+ await processor.onSpanStart(span);
395
+ await processor.onSpanEnd(span);
396
+ await processor.onTraceEnd(trace);
397
+
398
+ const [, request] = fetchWithRetryMock.mock.calls[0];
399
+ const payload = JSON.parse(String(request?.body));
400
+ const rootSpan = payload.resourceSpans[0].scopeSpans[0].spans.find(
401
+ (item: any) => item.name === "Recovered workflow"
402
+ );
403
+ const rootAttrs = keyValuesToObject(rootSpan.attributes);
404
+
405
+ expect(rootSpan).toBeDefined();
406
+ expect(rootAttrs["openai_agents.group_id"]).toBe("group_456");
407
+ expect(rootAttrs["openai_agents.metadata.tenant"]).toBe("acme");
408
+ });
409
+
410
+ it("keeps the agents trace id when traceparent metadata is absent", async () => {
411
+ const processor = new PromptLayerOpenAIAgentsProcessor({
412
+ apiKey: "pl_test",
413
+ baseURL: "https://api.promptlayer.dev",
414
+ });
415
+ const trace = {
416
+ traceId: "trace_cccccccccccccccccccccccccccccccc",
417
+ name: "No traceparent workflow",
418
+ groupId: null,
419
+ metadata: {
420
+ tenant: "acme",
421
+ },
422
+ } as any;
423
+ const span = {
424
+ traceId: trace.traceId,
425
+ spanId: "span_generation_no_traceparent",
426
+ parentId: null,
427
+ startedAt: "2026-03-17T14:15:16.123456789Z",
428
+ endedAt: "2026-03-17T14:15:17.123456789Z",
429
+ error: null,
430
+ traceMetadata: trace.metadata,
431
+ spanData: {
432
+ type: "generation",
433
+ model: "gpt-4.1",
434
+ input: [{ role: "user", content: "Hello" }],
435
+ output: [{ role: "assistant", content: "Hi" }],
436
+ },
437
+ } as any;
438
+
439
+ await processor.onTraceStart(trace);
440
+ await processor.onSpanStart(span);
441
+ await processor.onSpanEnd(span);
442
+ await processor.onTraceEnd(trace);
443
+
444
+ const [, request] = fetchWithRetryMock.mock.calls[0];
445
+ const payload = JSON.parse(String(request?.body));
446
+ const rootSpan = payload.resourceSpans[0].scopeSpans[0].spans.find(
447
+ (item: any) => item.name === "No traceparent workflow"
448
+ );
449
+
450
+ expect(rootSpan.traceId).toBe("cccccccccccccccccccccccccccccccc");
451
+ expect(rootSpan.parentSpanId).toBeUndefined();
452
+ expect(rootSpan.traceState).toBeUndefined();
453
+ });
454
+
455
+ it("does not throw on export failure and retries on forceFlush", async () => {
456
+ const processor = new PromptLayerOpenAIAgentsProcessor({
457
+ apiKey: "pl_test",
458
+ baseURL: "https://api.promptlayer.dev",
459
+ });
460
+ const trace = {
461
+ traceId: "trace_0af7651916cd43dd8448eb211c80319c",
462
+ name: "Retry workflow",
463
+ groupId: null,
464
+ metadata: {},
465
+ } as any;
466
+ const span = {
467
+ traceId: trace.traceId,
468
+ spanId: "span_generation_retry",
469
+ parentId: null,
470
+ startedAt: "2026-03-17T14:15:16.123456789Z",
471
+ endedAt: "2026-03-17T14:15:17.123456789Z",
472
+ error: null,
473
+ traceMetadata: {},
474
+ spanData: {
475
+ type: "generation",
476
+ model: "gpt-4.1",
477
+ input: [{ role: "user", content: "Hello" }],
478
+ output: [{ role: "assistant", content: "Hi" }],
479
+ usage: { input_tokens: 3, output_tokens: 5 },
480
+ },
481
+ } as any;
482
+
483
+ const consoleErrorSpy = vi
484
+ .spyOn(console, "error")
485
+ .mockImplementation(() => undefined);
486
+
487
+ fetchWithRetryMock
488
+ .mockRejectedValueOnce(new Error("network down"))
489
+ .mockResolvedValueOnce({
490
+ ok: true,
491
+ status: 200,
492
+ statusText: "OK",
493
+ } as Response);
494
+
495
+ await processor.onTraceStart(trace);
496
+ await processor.onSpanStart(span);
497
+ await processor.onSpanEnd(span);
498
+
499
+ await expect(processor.onTraceEnd(trace)).resolves.toBeUndefined();
500
+ expect(fetchWithRetryMock).toHaveBeenCalledTimes(1);
501
+
502
+ await processor.forceFlush();
503
+
504
+ expect(fetchWithRetryMock).toHaveBeenCalledTimes(2);
505
+ expect(consoleErrorSpy).toHaveBeenCalled();
506
+
507
+ consoleErrorSpy.mockRestore();
508
+ });
509
+ });