weflayr 0.16.1 → 0.20.2

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.
@@ -1,1102 +1,691 @@
1
1
  'use strict';
2
2
 
3
- // ---------------------------------------------------------------------------
4
- // Module doubles — registered before any test code so snippet require() calls
5
- // work without openai installed and without weflayr in node_modules.
6
- // ---------------------------------------------------------------------------
7
- {
8
- const Module = require('module');
9
- const path = require('path');
10
- const _origLoad = Module._load;
11
- Module._load = function(request, parent, isMain) {
12
- if (request === 'openai') {
13
- // The real openai package is required as `const OpenAI = require('openai')`,
14
- // so the module export IS the constructor (not a namespace object).
15
- function OpenAI(_opts) {
16
- return {
17
- chat: {
18
- completions: {
19
- create: async (args) => ({ id: 'mock-res', model: args.model || 'gpt-4o-mini', choices: [] }),
20
- },
21
- },
22
- images: {
23
- generate: async () => ({ data: [{ b64_json: 'abc123', url: 'https://example.com/img.png' }] }),
24
- },
25
- };
26
- }
27
- return OpenAI;
28
- }
29
- if (request === '@mastra/core') {
30
- function Agent(opts) {
31
- this._name = opts.name;
32
- }
33
- Agent.prototype.generate = async function(_messages, _options) {
34
- return {
35
- text: 'Hello from the Mastra fake agent.',
36
- modelId: 'gpt-4o-mini',
37
- finishReason: 'stop',
38
- usage: { promptTokens: 10, completionTokens: 15, totalTokens: 25 },
39
- request: { body: '{"messages":[{"role":"user","content":"Hello"}]}' },
40
- response: {
41
- id: 'fake-res',
42
- modelId: 'gpt-4o-mini',
43
- messages: [{ role: 'assistant', content: [{ type: 'text', text: 'Hello.' }] }],
44
- body: { choices: [{ message: { role: 'assistant', content: 'Hello.' } }] },
45
- },
46
- steps: [{ text: 'Hello.', request: { body: '...' }, finishReason: 'stop' }],
47
- };
48
- };
49
- // stream() returns a StreamTextResult-like object (NOT directly iterable).
50
- // usage and response are DelayedPromise wrappers matching real Mastra behaviour.
51
- Agent.prototype.stream = function(_messages, _options) {
52
- return {
53
- textStream: (async function* () {
54
- yield 'Hello'; yield ' Mastra'; yield ' stream.';
55
- })(),
56
- usage: { status: { type: 'resolved', value: { promptTokens: 10, completionTokens: 15, totalTokens: 25 } } },
57
- response: { status: { type: 'resolved', value: { modelId: 'gpt-4o-mini', id: 'fake-stream-res' } } },
58
- };
59
- };
60
- return { Agent };
61
- }
62
- if (request === '@ai-sdk/openai') {
63
- return { openai: (modelId) => ({ _provider: 'openai', modelId }) };
64
- }
65
- if (request === '@ai-sdk/openai-compatible') {
66
- function createOpenAICompatible({ name }) {
67
- return function(modelId) {
68
- return {
69
- specificationVersion: 'v1',
70
- provider: name,
71
- modelId,
72
- defaultObjectGenerationMode: undefined,
73
- doGenerate: async () => ({
74
- text: 'Hello!',
75
- usage: { promptTokens: 10, completionTokens: 5 },
76
- finishReason: 'stop',
77
- rawCall: { rawPrompt: [], rawSettings: {} },
78
- rawResponse: { headers: {} },
79
- warnings: [],
80
- }),
81
- doStream: async () => ({
82
- stream: new ReadableStream({
83
- start(ctrl) {
84
- ctrl.enqueue({ type: 'text-delta', textDelta: 'Hello!' });
85
- ctrl.enqueue({ type: 'finish', finishReason: 'stop', usage: { promptTokens: 10, completionTokens: 5 } });
86
- ctrl.close();
87
- },
88
- }),
89
- rawCall: { rawPrompt: [], rawSettings: {} },
90
- rawResponse: { headers: {} },
91
- warnings: [],
92
- }),
93
- };
94
- };
95
- }
96
- return { createOpenAICompatible };
97
- }
98
- if (request === 'ai') {
99
- function wrapLanguageModel({ model, middleware }) {
100
- return {
101
- modelId: model.modelId,
102
- doGenerate: async (params) =>
103
- middleware.wrapGenerate({ doGenerate: () => model.doGenerate(params), params, model }),
104
- doStream: async (params) =>
105
- middleware.wrapStream({ doStream: () => model.doStream(params), params, model }),
106
- };
107
- }
108
- async function generateText({ model, prompt }) {
109
- const params = { prompt: [{ role: 'user', content: [{ type: 'text', text: prompt }] }] };
110
- return model.doGenerate(params);
111
- }
112
- function streamText({ model, prompt }) {
113
- const params = { prompt: [{ role: 'user', content: [{ type: 'text', text: prompt }] }] };
114
- const streamPromise = model.doStream(params);
115
- return {
116
- get textStream() {
117
- return (async function* () {
118
- const { stream } = await streamPromise;
119
- const reader = stream.getReader();
120
- while (true) {
121
- const { done, value } = await reader.read();
122
- if (done) break;
123
- if (value && value.type === 'text-delta') yield value.textDelta;
124
- }
125
- })();
126
- },
127
- };
128
- }
129
- return { wrapLanguageModel, generateText, streamText };
130
- }
131
- if (request === 'weflayr') {
132
- return _origLoad.call(this, path.resolve(__dirname, '../src/index.js'), parent, isMain);
133
- }
134
- return _origLoad.call(this, request, parent, isMain);
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const Module = require('node:module');
6
+
7
+ const { trace, context: otelContext } = require('@opentelemetry/api');
8
+ const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base');
9
+ const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
10
+ const { AsyncHooksContextManager } = require('@opentelemetry/context-async-hooks');
11
+
12
+ const weflayr = require('../src');
13
+ const { _state } = require('../src/setup');
14
+ const { isDefaultExportSpan } = require('../src/otel/span-filter');
15
+ const { WeflayrSpanProcessor } = require('../src/otel/span-processor');
16
+ const { buildExporter, _exportWithRetry } = require('../src/otel/exporter');
17
+ const { patchOpenAIStreamingUsage } = require('../src/otel/openai-streaming-usage');
18
+
19
+ const contextManager = new AsyncHooksContextManager();
20
+ contextManager.enable();
21
+ otelContext.setGlobalContextManager(contextManager);
22
+
23
+ // ── Helpers ───────────────────────────────────────────────────────────────────
24
+
25
+ function makeExporterStub() {
26
+ const exported = [];
27
+ return {
28
+ exported,
29
+ export(spans, cb) {
30
+ exported.push(...spans);
31
+ cb({ code: 0 });
32
+ },
33
+ shutdown() {
34
+ return Promise.resolve();
35
+ },
36
+ forceFlush() {
37
+ return Promise.resolve();
38
+ },
135
39
  };
136
40
  }
137
41
 
138
- const { test } = require('node:test');
139
- const assert = require('assert/strict');
140
-
141
- const TEST_CREDS = {
142
- intake_url: 'http://localhost:19999', // nothing listening — fire-and-forget is fine
143
- client_id: 'test-client-id',
144
- client_secret: 'test-secret',
145
- };
146
-
147
- // Reset module cache between tests so _config doesn't leak
148
- function freshSDK() {
149
- delete require.cache[require.resolve('../src/instrument')];
150
- delete require.cache[require.resolve('../src/vercel-ai-middleware')];
151
- delete require.cache[require.resolve('../src/index')];
152
- return require('../src/index');
153
- }
154
-
155
- // Minimal fake client: one async method at a nested path
156
- function fakeClient() {
42
+ function makeFakeSpan({
43
+ attributes = {},
44
+ instrumentationScope = { name: 'opentelemetry.instrumentation.openai' },
45
+ name = 'openai.chat',
46
+ } = {}) {
47
+ const attrs = { ...attributes };
48
+ const ctx = {
49
+ traceId: '11111111111111111111111111111111',
50
+ spanId: '2222222222222222',
51
+ traceFlags: 1,
52
+ };
157
53
  return {
158
- chat: {
159
- completions: {
160
- create: async (args) => ({ id: 'res-1', model: args.model, choices: [] }),
161
- },
54
+ name,
55
+ attributes: attrs,
56
+ instrumentationScope,
57
+ instrumentationLibrary: instrumentationScope,
58
+ resource: { attributes: {} },
59
+ kind: 0,
60
+ status: { code: 0 },
61
+ startTime: [0, 0],
62
+ endTime: [0, 0],
63
+ duration: [0, 0],
64
+ events: [],
65
+ links: [],
66
+ droppedAttributesCount: 0,
67
+ droppedEventsCount: 0,
68
+ droppedLinksCount: 0,
69
+ ended: true,
70
+ spanContext: () => ctx,
71
+ setAttribute(k, v) {
72
+ attrs[k] = v;
73
+ },
74
+ setAttributes(obj) {
75
+ Object.assign(attrs, obj);
162
76
  },
77
+ isRecording: () => true,
163
78
  };
164
79
  }
165
80
 
166
- // ---------------------------------------------------------------------------
167
- // Content policy — function-based middleware
168
- // ---------------------------------------------------------------------------
81
+ // ── setup ─────────────────────────────────────────────────────────────────────
169
82
 
170
- test('ignore_fields function filters event data', async () => {
171
- const { weflayr_setup, weflayr_instrument } = freshSDK();
172
-
173
- weflayr_setup({
174
- ...TEST_CREDS,
175
- event_mode: 'default',
176
- ignore_fields: (data) => { delete data.messages; return data; },
177
- methods: [{ call: 'chat.completions.create' }],
178
- });
179
-
180
- const client = weflayr_instrument(fakeClient());
181
- const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
182
-
183
- assert.equal(result.id, 'res-1');
83
+ test('setup throws without apiKey', () => {
84
+ const saved = process.env.WEFLAYR_API_KEY;
85
+ delete process.env.WEFLAYR_API_KEY;
86
+ try {
87
+ assert.throws(() => weflayr.setup({}), /apiKey/);
88
+ } finally {
89
+ if (saved !== undefined) process.env.WEFLAYR_API_KEY = saved;
90
+ }
184
91
  });
185
92
 
186
- test('allow_fields function filters event data', async () => {
187
- const { weflayr_setup, weflayr_instrument } = freshSDK();
188
-
189
- weflayr_setup({
190
- ...TEST_CREDS,
191
- event_mode: 'default',
192
- allow_fields: (data) => ({ model: data.model }),
193
- methods: [{ call: 'chat.completions.create' }],
194
- });
195
-
196
- const client = weflayr_instrument(fakeClient());
197
- const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
198
-
199
- assert.equal(result.id, 'res-1');
93
+ test('setup picks up apiKey from WEFLAYR_API_KEY env var', () => {
94
+ trace.disable();
95
+ _state.processor = null;
96
+ process.env.WEFLAYR_API_KEY = 'env-key';
97
+ try {
98
+ weflayr.setup({});
99
+ assert.ok(_state.processor, 'processor must be installed using the env-var key');
100
+ } finally {
101
+ delete process.env.WEFLAYR_API_KEY;
102
+ }
200
103
  });
201
104
 
202
- test('both ignore_fields and allow_fields set no events sent, function still works', async () => {
203
- const { weflayr_setup, weflayr_instrument } = freshSDK();
204
-
205
- weflayr_setup({
206
- ...TEST_CREDS,
207
- event_mode: 'default',
208
- ignore_fields: (data) => data,
209
- allow_fields: (data) => data,
210
- methods: [{ call: 'chat.completions.create' }],
211
- });
212
-
213
- const client = weflayr_instrument(fakeClient());
214
- const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
215
-
216
- assert.equal(result.id, 'res-1');
105
+ test('setup sets the content-capture env var when captureMessageContent=false', () => {
106
+ delete process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT;
107
+ weflayr.setup({ apiKey: 'test-key', captureMessageContent: false });
108
+ assert.equal(process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, 'false');
217
109
  });
218
110
 
219
- test('no content policy pass-through, function still works', async () => {
220
- const { weflayr_setup, weflayr_instrument } = freshSDK();
221
-
222
- weflayr_setup({
223
- ...TEST_CREDS,
224
- event_mode: 'default',
225
- methods: [{ call: 'chat.completions.create' }],
226
- });
227
-
228
- const client = weflayr_instrument(fakeClient());
229
- const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
230
-
231
- assert.equal(result.id, 'res-1');
111
+ test('setup with captureMessageContent=true sets env var to true', () => {
112
+ delete process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT;
113
+ weflayr.setup({ apiKey: 'test-key', captureMessageContent: true });
114
+ assert.equal(process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, 'true');
232
115
  });
233
116
 
234
- test('ignore_fields does not mutate actual call args', async () => {
235
- const { weflayr_setup, weflayr_instrument } = freshSDK();
236
-
237
- let capturedArgs;
238
- const client = {
239
- chat: {
240
- completions: {
241
- create: async (args) => { capturedArgs = args; return { id: 'res-1' }; },
242
- },
243
- },
244
- };
245
-
246
- weflayr_setup({
247
- ...TEST_CREDS,
248
- event_mode: 'default',
249
- ignore_fields: (data) => { delete data.messages; return data; },
250
- methods: [{ call: 'chat.completions.create' }],
251
- });
252
-
253
- const instrumented = weflayr_instrument(client);
254
- await instrumented.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hello' }] });
255
-
256
- assert.ok(Array.isArray(capturedArgs.messages));
257
- assert.equal(capturedArgs.messages[0].content, 'hello');
117
+ test('setup without captureMessageContent leaves env var untouched', () => {
118
+ delete process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT;
119
+ weflayr.setup({ apiKey: 'test-key' });
120
+ assert.equal(process.env.OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, undefined);
258
121
  });
259
122
 
260
- test('missing methods does not crash on object instrumentation', async () => {
261
- const { weflayr_setup, weflayr_instrument } = freshSDK();
262
-
263
- weflayr_setup({
264
- ...TEST_CREDS,
265
- event_mode: 'default',
266
- // methods intentionally omitted
267
- });
268
-
269
- const client = weflayr_instrument(fakeClient());
270
- const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
271
-
272
- assert.equal(result.id, 'res-1');
123
+ test('setup adopts a pre-existing TracerProvider', () => {
124
+ // Reset the global proxy and our _state so the test owns the registry.
125
+ trace.disable();
126
+ _state.processor = null;
127
+
128
+ // The user (or another OTel instrumentation) registers a provider first.
129
+ const foreign = new NodeTracerProvider({ spanProcessors: [] });
130
+ foreign.register();
131
+
132
+ weflayr.setup({ apiKey: 'test-key' });
133
+
134
+ // weflayr must have attached its processor to `foreign`, not created a new
135
+ // provider — otherwise provider.register() would have silently no-op'd and
136
+ // the LLM spans wouldn't reach our exporter.
137
+ const attached = foreign._activeSpanProcessor?._spanProcessors ?? [];
138
+ assert.ok(
139
+ attached.some((sp) => sp instanceof WeflayrSpanProcessor),
140
+ 'WeflayrSpanProcessor must be attached to the existing TracerProvider'
141
+ );
142
+ assert.strictEqual(_state.tracerProvider, foreign);
273
143
  });
274
144
 
275
- test('missing methods does not crash on function instrumentation', async () => {
276
- const { weflayr_setup, weflayr_instrument } = freshSDK();
145
+ test('setup called twice replaces the prior processor', () => {
146
+ // Stacking would double-export every span and leak HTTP pools in
147
+ // long-running processes that reconfigure (e.g. API-key rotation).
148
+ trace.disable();
149
+ _state.processor = null;
150
+
151
+ weflayr.setup({ apiKey: 'k1' });
152
+ const firstProcessor = _state.processor;
153
+ let shutdownCalled = false;
154
+ const originalShutdown = firstProcessor.shutdown.bind(firstProcessor);
155
+ firstProcessor.shutdown = () => {
156
+ shutdownCalled = true;
157
+ return originalShutdown();
158
+ };
277
159
 
278
- weflayr_setup({
279
- ...TEST_CREDS,
280
- event_mode: 'default',
281
- // methods intentionally omitted
282
- });
160
+ weflayr.setup({ apiKey: 'k2' });
283
161
 
284
- async function myFn(x) { return x * 2; }
285
- const instrumented = weflayr_instrument(myFn);
162
+ assert.ok(_state.processor);
163
+ assert.notStrictEqual(_state.processor, firstProcessor);
164
+ assert.equal(shutdownCalled, true);
286
165
 
287
- assert.equal(await instrumented(5), 10);
166
+ const tp = _state.tracerProvider;
167
+ const attached = tp._activeSpanProcessor?._spanProcessors ?? [];
168
+ assert.ok(!attached.includes(firstProcessor));
169
+ assert.ok(attached.includes(_state.processor));
288
170
  });
289
171
 
290
- // ---------------------------------------------------------------------------
291
- // enabled: false — target passes through unchanged, calls still work
292
- // ---------------------------------------------------------------------------
172
+ // ── buildExporter ─────────────────────────────────────────────────────────────
293
173
 
294
- test('enabled:false returns the original object untouched', () => {
295
- const { weflayr_setup, weflayr_instrument } = freshSDK();
296
-
297
- weflayr_setup({ methods: [{ call: 'chat.completions.create' }] , enabled: false });
298
-
299
- const client = fakeClient();
300
- const result = weflayr_instrument(client);
174
+ function _exporterParams(exporter) {
175
+ return exporter._delegate._transport._transport._parameters;
176
+ }
301
177
 
302
- assert.strictEqual(result, client);
178
+ test('buildExporter sets the bearer header and base URL', () => {
179
+ const exporter = buildExporter({ baseUrl: 'https://api.weflayr.com', apiKey: 'sk-x' });
180
+ assert.equal(_exporterParams(exporter).url, 'https://api.weflayr.com/');
303
181
  });
304
182
 
305
- test('enabled:false returns the original function untouched', () => {
306
- const { weflayr_setup, weflayr_instrument } = freshSDK();
307
-
308
- async function myFn(x) { return x * 2; }
309
- weflayr_setup({ methods: [{ call: 'myFn' }] , enabled: false });
310
-
311
- assert.strictEqual(weflayr_instrument(myFn), myFn);
183
+ test('buildExporter strips trailing slash on baseUrl', () => {
184
+ const exporter = buildExporter({ baseUrl: 'https://api.weflayr.com/', apiKey: 'k' });
185
+ assert.equal(_exporterParams(exporter).url, 'https://api.weflayr.com/');
312
186
  });
313
187
 
314
- test('enabled:false original function still executes correctly', async () => {
315
- const { weflayr_setup, weflayr_instrument } = freshSDK();
316
-
317
- weflayr_setup({ methods: [{ call: 'chat.completions.create' }] , enabled: false });
318
-
319
- const client = weflayr_instrument(fakeClient());
320
- const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
188
+ // ── span-filter ───────────────────────────────────────────────────────────────
321
189
 
322
- assert.equal(result.id, 'res-1');
190
+ test('isDefaultExportSpan accepts spans with gen_ai.* attributes', () => {
191
+ const span = makeFakeSpan({
192
+ attributes: { 'gen_ai.system': 'openai' },
193
+ instrumentationScope: { name: 'unknown' },
194
+ });
195
+ assert.equal(isDefaultExportSpan(span), true);
323
196
  });
324
197
 
325
- // ---------------------------------------------------------------------------
326
- // Documentation snippets
327
- // All snippet code runs verbatim — Module._load at top of file provides
328
- // require('openai') and require('weflayr') doubles.
329
- // update_doc_snippets.py extracts between: // SNIPPET_START: <name> ... // SNIPPET_END: <name>
330
- // ---------------------------------------------------------------------------
331
-
332
- // Intercept http.request; returns array of parsed event JSON bodies.
333
- async function captureHttp(fn) {
334
- const http = require('http');
335
- const sent = [];
336
- const origRequest = http.request;
337
- http.request = function(_opts, responseCb) {
338
- const chunks = [];
339
- const req = {
340
- write: (d) => chunks.push(typeof d === 'string' ? d : d.toString()),
341
- end: () => {
342
- try { sent.push(JSON.parse(chunks.join(''))); } catch { /* skip malformed */ }
343
- if (responseCb) responseCb({ resume: () => {}, on: () => {}, statusCode: 200 });
344
- },
345
- on: () => req,
346
- };
347
- return req;
348
- };
349
- try { await fn(); } finally { http.request = origRequest; }
350
- return sent;
351
- }
352
-
353
- const SNIPPET_ENV = {
354
- WEFLAYR_INTAKE_URL: TEST_CREDS.intake_url,
355
- WEFLAYR_CLIENT_ID: TEST_CREDS.client_id,
356
- WEFLAYR_CLIENT_SECRET: TEST_CREDS.client_secret,
357
- OPENAI_API_KEY: 'sk-test',
358
- };
359
- function setSnippetEnv() { Object.assign(process.env, SNIPPET_ENV); }
360
-
361
- test('snippet quickstart_setup: wraps client, sends before+after events', async () => {
362
- freshSDK();
363
- setSnippetEnv();
364
-
365
- const sent = await captureHttp(async () => {
366
- // SNIPPET_START: quickstart_setup
367
- const OpenAI = require('openai');
368
- const { weflayr_setup, weflayr_instrument } = require('weflayr');
369
-
370
- weflayr_setup({
371
- intake_url: process.env.WEFLAYR_INTAKE_URL,
372
- client_id: process.env.WEFLAYR_CLIENT_ID,
373
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
374
- default_tags: {
375
- app: 'my-app',
376
- version: '1.0.0',
377
- },
378
- methods: [
379
- { call: 'chat.completions.create' },
380
- ],
381
- });
382
-
383
- const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
384
- // SNIPPET_END: quickstart_setup
385
-
386
- assert.equal(typeof client.chat.completions.create, 'function');
387
- await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
198
+ test('isDefaultExportSpan accepts spans from known instrumentation scopes', () => {
199
+ const span = makeFakeSpan({
200
+ attributes: {},
201
+ instrumentationScope: { name: 'opentelemetry.instrumentation.openai' },
388
202
  });
389
-
390
- assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
391
- assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
392
- assert.equal(sent[0].method, 'chat.completions.create');
393
- assert.equal(sent[0].tags.app, 'my-app');
394
- assert.equal(sent[0].tags.version, '1.0.0');
203
+ assert.equal(isDefaultExportSpan(span), true);
395
204
  });
396
205
 
397
- test('snippet quickstart_call: tags stripped from call, tags in telemetry', async () => {
398
- const { weflayr_setup, weflayr_instrument } = freshSDK();
399
- setSnippetEnv();
400
-
401
- let capturedArgs;
402
- const raw = {
403
- chat: { completions: { create: async (args) => { capturedArgs = args; return { id: 'res-1', choices: [] }; } } },
404
- };
405
- weflayr_setup({ ...TEST_CREDS, methods: [{ call: 'chat.completions.create' }] });
406
- const client = weflayr_instrument(raw);
407
-
408
- const sent = await captureHttp(async () => {
409
- // SNIPPET_START: quickstart_call
410
- const response = await client.chat.completions.create({
411
- model: 'gpt-4o-mini',
412
- messages: [{ role: 'user', content: 'Hello!' }],
413
- __weflayr_tags: {
414
- provider: 'openai',
415
- feature: 'onboarding',
416
- customer_id: '#3756'
417
- },
418
- });
419
- // response is the standard OpenAI response object — unchanged
420
- // SNIPPET_END: quickstart_call
421
-
422
- assert.equal(response.id, 'res-1');
206
+ test('isDefaultExportSpan rejects unrelated spans', () => {
207
+ const span = makeFakeSpan({
208
+ attributes: { 'http.method': 'GET' },
209
+ instrumentationScope: { name: 'http' },
423
210
  });
424
-
425
- assert.ok(!('__weflayr_tags' in capturedArgs), 'tags stripped from underlying call');
426
- const before = sent.find(e => e.event_type === 'before');
427
- assert.ok(before, 'before event sent');
428
- assert.deepEqual(before.tags, { provider: 'openai', feature: 'onboarding', customer_id: '#3756' });
211
+ assert.equal(isDefaultExportSpan(span), false);
429
212
  });
430
213
 
431
- test('snippet openai_full_coverage: 7 methods configured, events sent', async () => {
432
- freshSDK();
433
- setSnippetEnv();
434
-
435
- const sent = await captureHttp(async () => {
436
- // SNIPPET_START: openai_full_coverage
437
- const { weflayr_setup, weflayr_instrument } = require('weflayr');
438
-
439
- weflayr_setup({
440
- intake_url: process.env.WEFLAYR_INTAKE_URL,
441
- client_id: process.env.WEFLAYR_CLIENT_ID,
442
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
443
- methods: [
444
- { call: 'chat.completions.create' },
445
- { call: 'completions.create' },
446
- { call: 'embeddings.create' },
447
- { call: 'responses.create' },
448
- {
449
- call: 'audio.speech.create',
450
- middleware: (args) => ({ char_count: args?.input?.length ?? 0 }),
451
- },
452
- { call: 'audio.transcriptions.create' },
453
- { call: 'audio.translations.create' },
454
- ],
455
- });
456
-
457
- const OpenAI = require('openai');
458
- const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
459
- // SNIPPET_END: openai_full_coverage
214
+ // ── WeflayrSpanProcessor ──────────────────────────────────────────────────────
460
215
 
461
- assert.equal(typeof client.chat.completions.create, 'function');
462
- await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
216
+ test('WeflayrSpanProcessor stamps defaultTags on span start', () => {
217
+ const exporter = makeExporterStub();
218
+ const proc = new WeflayrSpanProcessor({
219
+ exporter,
220
+ defaultTags: { app: 'svc-A', env: 'prod' },
463
221
  });
464
-
465
- assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
466
- assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
467
- assert.equal(sent[0].method, 'chat.completions.create');
222
+ const span = makeFakeSpan();
223
+ proc.onStart(span, otelContext.active());
224
+ assert.equal(span.attributes['weflayr.tag.app'], 'svc-A');
225
+ assert.equal(span.attributes['weflayr.tag.env'], 'prod');
468
226
  });
469
227
 
470
- test('snippet openai_tagging: tags stripped from call, all 4 tags in telemetry', async () => {
471
- const { weflayr_setup, weflayr_instrument } = freshSDK();
472
- setSnippetEnv();
473
-
474
- let capturedArgs;
475
- const raw = {
476
- chat: { completions: { create: async (args) => { capturedArgs = args; return { id: 'res-1' }; } } },
477
- };
478
- weflayr_setup({ ...TEST_CREDS, methods: [{ call: 'chat.completions.create' }] });
479
- const client = weflayr_instrument(raw);
480
-
481
- const sent = await captureHttp(async () => {
482
- // SNIPPET_START: openai_tagging
483
- await client.chat.completions.create({
484
- model: 'gpt-4o-mini',
485
- messages: [{ role: 'user', content: 'Hello' }],
486
- __weflayr_tags: {
487
- provider: 'openai',
488
- feature: 'support-chat',
489
- customer_id: 'acme-corp',
490
- env: 'production',
491
- },
492
- });
493
- // SNIPPET_END: openai_tagging
494
- });
228
+ test('WeflayrSpanProcessor forwards GenAI spans to the exporter', async () => {
229
+ const exporter = makeExporterStub();
230
+ const proc = new WeflayrSpanProcessor({ exporter, exportMode: 'immediate' });
231
+ const span = makeFakeSpan({ attributes: { 'gen_ai.system': 'openai' } });
232
+ proc.onEnd(span);
233
+ await proc.forceFlush();
234
+ assert.equal(exporter.exported.length, 1);
235
+ });
495
236
 
496
- assert.ok(!('__weflayr_tags' in capturedArgs), 'tags stripped from underlying call');
497
- assert.equal(capturedArgs.model, 'gpt-4o-mini');
498
- const before = sent.find(e => e.event_type === 'before');
499
- assert.ok(before, 'before event sent');
500
- assert.deepEqual(before.tags, { provider: 'openai', feature: 'support-chat', customer_id: 'acme-corp', env: 'production' });
237
+ test('WeflayrSpanProcessor drops non-GenAI spans', async () => {
238
+ const exporter = makeExporterStub();
239
+ const proc = new WeflayrSpanProcessor({ exporter, exportMode: 'immediate' });
240
+ proc.onEnd(
241
+ makeFakeSpan({
242
+ attributes: { 'http.method': 'GET' },
243
+ instrumentationScope: { name: 'http' },
244
+ })
245
+ );
246
+ await proc.forceFlush();
247
+ assert.equal(exporter.exported.length, 0);
501
248
  });
502
249
 
503
- test('snippet openai_image_generation: b64_json stripped from telemetry, url preserved', async () => {
504
- freshSDK();
505
- setSnippetEnv();
506
-
507
- const sent = await captureHttp(async () => {
508
- // SNIPPET_START: openai_image_generation
509
- const { weflayr_setup, weflayr_instrument } = require('weflayr');
510
-
511
- weflayr_setup({
512
- intake_url: process.env.WEFLAYR_INTAKE_URL,
513
- client_id: process.env.WEFLAYR_CLIENT_ID,
514
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
515
- ignore_fields: (data) => {
516
- (data.data ?? []).forEach(img => delete img.b64_json);
517
- return data;
518
- },
519
- methods: [
520
- { call: 'images.generate' },
521
- ],
522
- });
250
+ test('WeflayrSpanProcessor ends orphaned LLM spans on forceFlush', async () => {
251
+ // LLM spans that started but never ended should be force-ended with ERROR on
252
+ // flush — covers openllmetry-style leaks (e.g. anthropic streaming auth error).
253
+ const exporter = makeExporterStub();
254
+ const proc = new WeflayrSpanProcessor({ exporter, exportMode: 'immediate' });
255
+ const provider = new BasicTracerProvider({ spanProcessors: [proc] });
256
+ const tracer = provider.getTracer('opentelemetry.instrumentation.anthropic');
523
257
 
524
- const OpenAI = require('openai');
525
- const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
526
- // SNIPPET_END: openai_image_generation
258
+ const leaked = tracer.startSpan('anthropic.chat');
259
+ leaked.setAttribute('gen_ai.system', 'anthropic');
260
+ assert.equal(exporter.exported.length, 0, 'span should not have been exported yet');
527
261
 
528
- await client.images.generate({ model: 'gpt-image-1', prompt: 'a cat' });
529
- });
262
+ await proc.forceFlush();
530
263
 
531
- const after = sent.find(e => e.event_type === 'after');
532
- assert.ok(after, 'after event sent');
533
- for (const img of (after.response?.data ?? [])) {
534
- assert.ok(!('b64_json' in img), 'b64_json stripped from telemetry');
535
- }
536
- assert.ok(after.response.data[0].url, 'url preserved in telemetry');
264
+ assert.equal(exporter.exported.length, 1);
265
+ // SpanStatusCode.ERROR === 2 in @opentelemetry/api
266
+ assert.equal(exporter.exported[0].status.code, 2);
267
+ await provider.shutdown();
537
268
  });
538
269
 
539
- // ---------------------------------------------------------------------------
540
- // default_tags and weflayr_propagate — behavioural tests
541
- // ---------------------------------------------------------------------------
542
-
543
- test('default_tags appear in before and after events', async () => {
544
- const { weflayr_setup, weflayr_instrument } = freshSDK();
545
-
546
- weflayr_setup({
547
- ...TEST_CREDS,
548
- default_tags: { app: 'my-app', version: '1.2.0' },
549
- methods: [{ call: 'chat.completions.create' }],
270
+ test('WeflayrSpanProcessor applies mask before export', async () => {
271
+ const exporter = makeExporterStub();
272
+ let received = null;
273
+ const proc = new WeflayrSpanProcessor({
274
+ exporter,
275
+ exportMode: 'immediate',
276
+ mask: (attrs) => {
277
+ received = attrs;
278
+ attrs['gen_ai.prompt'] = '[REDACTED]';
279
+ return attrs;
280
+ },
550
281
  });
551
-
552
- const client = weflayr_instrument(fakeClient());
553
- const sent = await captureHttp(async () => {
554
- await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
282
+ const span = makeFakeSpan({
283
+ attributes: { 'gen_ai.system': 'openai', 'gen_ai.prompt': 'Hi Noe' },
555
284
  });
556
-
557
- const before = sent.find(e => e.event_type === 'before');
558
- assert.equal(before.tags.app, 'my-app');
559
- assert.equal(before.tags.version, '1.2.0');
560
- const after = sent.find(e => e.event_type === 'after');
561
- assert.equal(after.tags.app, 'my-app');
562
- assert.equal(after.tags.version, '1.2.0');
285
+ proc.onEnd(span);
286
+ await proc.forceFlush();
287
+ assert.ok(received);
288
+ assert.equal(span.attributes['gen_ai.prompt'], '[REDACTED]');
563
289
  });
564
290
 
565
- test('weflayr_propagate adds a tag to before and after events', async () => {
566
- const { weflayr_setup, weflayr_instrument, weflayr_propagate } = freshSDK();
567
-
568
- weflayr_setup({
569
- ...TEST_CREDS,
570
- methods: [{ call: 'chat.completions.create' }],
571
- });
291
+ // ── propagateMetadata ────────────────────────────────────────────────────────
572
292
 
573
- weflayr_propagate('customer_id', 'acme-corp');
293
+ function _makeMetadataHarness() {
294
+ const exporter = makeExporterStub();
295
+ const proc = new WeflayrSpanProcessor({ exporter, exportMode: 'immediate' });
296
+ const provider = new BasicTracerProvider({ spanProcessors: [proc] });
297
+ const tracer = provider.getTracer('test');
298
+ return { exporter, proc, provider, tracer };
299
+ }
574
300
 
575
- const client = weflayr_instrument(fakeClient());
576
- const sent = await captureHttp(async () => {
577
- await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
578
- });
301
+ test('propagateMetadata writes structured fields and extra tags', async () => {
302
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
579
303
 
580
- const before = sent.find(e => e.event_type === 'before');
581
- assert.equal(before.tags.customer_id, 'acme-corp');
582
- const after = sent.find(e => e.event_type === 'after');
583
- assert.equal(after.tags.customer_id, 'acme-corp');
304
+ await weflayr.propagateMetadata(
305
+ {
306
+ featureName: 'onboarding',
307
+ customerId: '#3756',
308
+ extraTags: { variant: 'control' },
309
+ },
310
+ async () => {
311
+ const span = tracer.startSpan('child');
312
+ span.setAttribute('gen_ai.system', 'openai');
313
+ span.end();
314
+ }
315
+ );
316
+
317
+ await proc.forceFlush();
318
+ assert.equal(exporter.exported.length, 1);
319
+ const attrs = exporter.exported[0].attributes;
320
+ assert.equal(attrs['weflayr.feature_name'], 'onboarding');
321
+ assert.equal(attrs['weflayr.customer_id'], '#3756');
322
+ assert.equal(attrs['weflayr.tag.variant'], 'control');
323
+ assert.ok(attrs['weflayr.uuid']);
324
+ assert.equal(attrs['weflayr.provider_name'], undefined);
325
+ await provider.shutdown();
584
326
  });
585
327
 
586
- test('tag priority: __weflayr_tags > weflayr_propagate > default_tags', async () => {
587
- const { weflayr_setup, weflayr_instrument, weflayr_propagate } = freshSDK();
328
+ test('propagateMetadata uuid is stable within scope and changes across calls', async () => {
329
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
588
330
 
589
- weflayr_setup({
590
- ...TEST_CREDS,
591
- default_tags: { env: 'production', app: 'my-app' },
592
- methods: [{ call: 'chat.completions.create' }],
331
+ await weflayr.propagateMetadata({ featureName: 'f', customerId: 'c' }, async () => {
332
+ const spanA = tracer.startSpan('a');
333
+ spanA.setAttribute('gen_ai.system', 'openai');
334
+ spanA.end();
335
+ const spanB = tracer.startSpan('b');
336
+ spanB.setAttribute('gen_ai.system', 'openai');
337
+ spanB.end();
593
338
  });
594
339
 
595
- weflayr_propagate('env', 'staging'); // overrides default_tags.env
596
-
597
- const client = weflayr_instrument(fakeClient());
598
- const sent = await captureHttp(async () => {
599
- await client.chat.completions.create({
600
- model: 'gpt-4o-mini',
601
- messages: [],
602
- __weflayr_tags: { env: 'test' }, // overrides both
603
- });
340
+ await weflayr.propagateMetadata({ featureName: 'f', customerId: 'c' }, async () => {
341
+ const spanC = tracer.startSpan('c_span');
342
+ spanC.setAttribute('gen_ai.system', 'openai');
343
+ spanC.end();
604
344
  });
605
345
 
606
- const before = sent.find(e => e.event_type === 'before');
607
- assert.equal(before.tags.env, 'test'); // per-call wins
608
- assert.equal(before.tags.app, 'my-app'); // from default_tags, untouched
346
+ await proc.forceFlush();
347
+ const uuidA = exporter.exported[0].attributes['weflayr.uuid'];
348
+ const uuidB = exporter.exported[1].attributes['weflayr.uuid'];
349
+ const uuidC = exporter.exported[2].attributes['weflayr.uuid'];
350
+ assert.equal(uuidA, uuidB);
351
+ assert.notEqual(uuidA, uuidC);
352
+ await provider.shutdown();
609
353
  });
610
354
 
611
- // ---------------------------------------------------------------------------
612
- // Propagate documentation snippets
613
- // ---------------------------------------------------------------------------
614
-
615
- test('snippet propagate_default_tags: default_tags in every event', async () => {
616
- freshSDK();
617
- setSnippetEnv();
618
-
619
- const sent = await captureHttp(async () => {
620
- // SNIPPET_START: propagate_default_tags
621
- const { weflayr_setup, weflayr_instrument } = require('weflayr');
622
-
623
- weflayr_setup({
624
- intake_url: process.env.WEFLAYR_INTAKE_URL,
625
- client_id: process.env.WEFLAYR_CLIENT_ID,
626
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
627
- default_tags: {
628
- app: 'my-app',
629
- version: '1.2.0',
630
- env: 'production',
631
- },
632
- methods: [{ call: 'chat.completions.create' }],
633
- });
634
-
635
- const OpenAI = require('openai');
636
- const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
637
- // SNIPPET_END: propagate_default_tags
355
+ test('propagateMetadata nested scopes inner overrides and extra tags merge', async () => {
356
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
638
357
 
639
- await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
640
- });
641
-
642
- const before = sent.find(e => e.event_type === 'before');
643
- assert.ok(before, 'before event sent');
644
- assert.equal(before.tags.app, 'my-app');
645
- assert.equal(before.tags.version, '1.2.0');
646
- assert.equal(before.tags.env, 'production');
647
- const after = sent.find(e => e.event_type === 'after');
648
- assert.ok(after, 'after event sent');
649
- assert.equal(after.tags.app, 'my-app');
358
+ await weflayr.propagateMetadata(
359
+ {
360
+ featureName: 'outer',
361
+ customerId: 'outer_c',
362
+ extraTags: { variant: 'A', shared: 'outer' },
363
+ },
364
+ async () => {
365
+ await weflayr.propagateMetadata(
366
+ {
367
+ featureName: 'inner',
368
+ customerId: 'inner_c',
369
+ extraTags: { shared: 'inner', experiment: 'B' },
370
+ },
371
+ async () => {
372
+ const span = tracer.startSpan('child');
373
+ span.setAttribute('gen_ai.system', 'openai');
374
+ span.end();
375
+ }
376
+ );
377
+ }
378
+ );
379
+
380
+ await proc.forceFlush();
381
+ const attrs = exporter.exported[0].attributes;
382
+ assert.equal(attrs['weflayr.feature_name'], 'inner');
383
+ assert.equal(attrs['weflayr.customer_id'], 'inner_c');
384
+ // Outer extra tag not shadowed survives
385
+ assert.equal(attrs['weflayr.tag.variant'], 'A');
386
+ // Inner extra tag wins on conflict
387
+ assert.equal(attrs['weflayr.tag.shared'], 'inner');
388
+ assert.equal(attrs['weflayr.tag.experiment'], 'B');
389
+ await provider.shutdown();
650
390
  });
651
391
 
652
- test('snippet propagate_runtime: propagated tag in before and after', async () => {
653
- const { weflayr_setup, weflayr_instrument, weflayr_propagate } = freshSDK();
654
- setSnippetEnv();
392
+ test('propagateMetadata decorator mints fresh uuid per invocation', async () => {
393
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
655
394
 
656
- let capturedArgs;
657
- const raw = {
658
- chat: { completions: { create: async (args) => { capturedArgs = args; return { id: 'res-1' }; } } },
659
- };
660
- weflayr_setup({ ...TEST_CREDS, methods: [{ call: 'chat.completions.create' }] });
661
- const client = weflayr_instrument(raw);
662
-
663
- const sent = await captureHttp(async () => {
664
- // SNIPPET_START: propagate_runtime
665
- const { weflayr_propagate } = require('weflayr');
666
-
667
- // In a request handler — tags every LLM call made during this execution
668
- weflayr_propagate('customer_id', 'acme-corp');
669
-
670
- await client.chat.completions.create({
671
- model: 'gpt-4o-mini',
672
- messages: [{ role: 'user', content: 'Hello!' }],
673
- });
674
- // SNIPPET_END: propagate_runtime
395
+ const wrap = weflayr.propagateMetadata({ featureName: 'f', customerId: 'c' });
396
+ const leaf = wrap(async function leaf() {
397
+ const span = tracer.startSpan('child');
398
+ span.setAttribute('gen_ai.system', 'openai');
399
+ span.end();
675
400
  });
676
401
 
677
- assert.ok(!('__weflayr_tags' in capturedArgs), 'no tags leaked to provider');
678
- const before = sent.find(e => e.event_type === 'before');
679
- assert.ok(before, 'before event sent');
680
- assert.equal(before.tags.customer_id, 'acme-corp');
681
- const after = sent.find(e => e.event_type === 'after');
682
- assert.ok(after, 'after event sent');
683
- assert.equal(after.tags.customer_id, 'acme-corp');
684
- });
402
+ await leaf();
403
+ await leaf();
685
404
 
686
- // ---------------------------------------------------------------------------
687
- // Mastra integration — documentation snippets
688
- // @mastra/core and @ai-sdk/openai are faked by Module._load at top of file.
689
- // ---------------------------------------------------------------------------
690
-
691
- test('snippet mastra_setup: wraps Mastra Agent, sends before+after events on generate()', async () => {
692
- freshSDK();
693
- setSnippetEnv();
694
-
695
- const sent = await captureHttp(async () => {
696
- // SNIPPET_START: mastra_setup
697
- const { weflayr_setup, weflayr_instrument } = require('weflayr');
698
- const { Agent } = require('@mastra/core');
699
- const { openai } = require('@ai-sdk/openai');
700
-
701
- weflayr_setup({
702
- intake_url: process.env.WEFLAYR_INTAKE_URL,
703
- client_id: process.env.WEFLAYR_CLIENT_ID,
704
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
705
- methods: [{ call: 'generate' }],
706
- });
707
-
708
- const agent = weflayr_instrument(
709
- new Agent({
710
- name: 'my-agent',
711
- model: openai('gpt-4o-mini'),
712
- instructions: 'You are a helpful assistant.',
713
- })
714
- );
715
- // SNIPPET_END: mastra_setup
716
-
717
- await agent.generate([{ role: 'user', content: 'Hello' }]);
718
- });
719
-
720
- assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
721
- assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
722
- assert.equal(sent[0].method, 'generate');
405
+ await proc.forceFlush();
406
+ const uuidFirst = exporter.exported[0].attributes['weflayr.uuid'];
407
+ const uuidSecond = exporter.exported[1].attributes['weflayr.uuid'];
408
+ assert.notEqual(uuidFirst, uuidSecond);
409
+ await provider.shutdown();
723
410
  });
724
411
 
725
- test('snippet mastra_tags: per-call tags stripped from generate args, appear in events', async () => {
726
- const { weflayr_setup, weflayr_instrument } = freshSDK();
727
- setSnippetEnv();
728
-
729
- const { Agent } = require('@mastra/core');
730
- const { openai } = require('@ai-sdk/openai');
731
- weflayr_setup({ ...TEST_CREDS, methods: [{ call: 'generate' }] });
732
- const agent = weflayr_instrument(new Agent({ name: 'test-agent', model: openai('gpt-4o-mini'), instructions: '...' }));
412
+ test('propagateMetadata decorator form wraps a function', async () => {
413
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
733
414
 
734
- const sent = await captureHttp(async () => {
735
- // SNIPPET_START: mastra_tags
736
- await agent.generate(
737
- [{ role: 'user', content: 'Hello' }],
738
- { __weflayr_tags: { feature: 'chat', customer_id: '42', env: 'prod' } },
739
- );
740
- // SNIPPET_END: mastra_tags
415
+ const wrap = weflayr.propagateMetadata({
416
+ featureName: 'chat',
417
+ customerId: 'c_42',
418
+ extraTags: { experiment: 'B' },
741
419
  });
742
-
743
- const before = sent.find(e => e.event_type === 'before');
744
- assert.ok(before, 'before event sent');
745
- assert.deepEqual(before.tags, { feature: 'chat', customer_id: '42', env: 'prod' });
746
- });
747
-
748
- test('snippet mastra_content_privacy: text/request/steps/response content stripped from events', async () => {
749
- freshSDK();
750
- setSnippetEnv();
751
-
752
- const sent = await captureHttp(async () => {
753
- // SNIPPET_START: mastra_content_privacy
754
- const { weflayr_setup, weflayr_instrument } = require('weflayr');
755
- const { Agent } = require('@mastra/core');
756
- const { openai } = require('@ai-sdk/openai');
757
-
758
- weflayr_setup({
759
- intake_url: process.env.WEFLAYR_INTAKE_URL,
760
- client_id: process.env.WEFLAYR_CLIENT_ID,
761
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
762
- methods: [{ call: 'generate' }],
763
- ignore_fields: (data) => {
764
- delete data.text;
765
- delete data.request;
766
- delete data.steps;
767
- if (data.response) {
768
- delete data.response.body;
769
- delete data.response.messages;
770
- }
771
- return data;
772
- },
773
- });
774
-
775
- const agent = weflayr_instrument(
776
- new Agent({
777
- name: 'my-agent',
778
- model: openai('gpt-4o-mini'),
779
- instructions: 'You are a helpful assistant.',
780
- })
781
- );
782
- // SNIPPET_END: mastra_content_privacy
783
-
784
- await agent.generate([{ role: 'user', content: 'Hello' }]);
420
+ const leaf = wrap(async function leaf() {
421
+ const span = tracer.startSpan('child');
422
+ span.setAttribute('gen_ai.system', 'openai');
423
+ span.end();
785
424
  });
786
425
 
787
- const after = sent.find(e => e.event_type === 'after');
788
- assert.ok(after, 'after event sent');
789
- assert.ok(!after.response || !('text' in after.response), 'text stripped');
790
- assert.ok(!after.response || !('request' in after.response), 'request stripped');
791
- assert.ok(!after.response || !('steps' in after.response), 'steps stripped');
792
- assert.ok(!after.response?.response?.body, 'response.body stripped');
793
- assert.ok(!after.response?.response?.messages, 'response.messages stripped');
426
+ await leaf();
427
+ await proc.forceFlush();
428
+ const attrs = exporter.exported[0].attributes;
429
+ assert.equal(attrs['weflayr.feature_name'], 'chat');
430
+ assert.equal(attrs['weflayr.customer_id'], 'c_42');
431
+ assert.equal(attrs['weflayr.tag.experiment'], 'B');
432
+ await provider.shutdown();
794
433
  });
795
434
 
796
- test('snippet mastra_stream: stream_start + after emitted, model/usage in after via streamMiddleware', async () => {
797
- freshSDK();
798
- setSnippetEnv();
799
-
800
- const sent = await captureHttp(async () => {
801
- // SNIPPET_START: mastra_stream
802
- const { weflayr_setup, weflayr_instrument } = require('weflayr');
803
- const { Agent } = require('@mastra/core');
804
- const { openai } = require('@ai-sdk/openai');
805
-
806
- weflayr_setup({
807
- intake_url: process.env.WEFLAYR_INTAKE_URL,
808
- client_id: process.env.WEFLAYR_CLIENT_ID,
809
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
810
- methods: [
811
- { call: 'generate' },
812
- {
813
- call: 'stream',
814
- streamPath: 'textStream',
815
- streamMiddleware: (fullResult) => ({
816
- onChunk: () => false,
817
- async finalize() {
818
- const rawUsage = await fullResult.usage;
819
- const rawResponse = await fullResult.response;
820
- const response = rawResponse?.status?.value ?? rawResponse;
821
- return {
822
- usage: rawUsage,
823
- model: response?.modelId ?? null,
824
- };
825
- },
826
- }),
827
- },
828
- ],
829
- ignore_fields: (data) => {
830
- delete data.text;
831
- delete data.request;
832
- delete data.steps;
833
- if (data.response) {
834
- delete data.response.body;
835
- delete data.response.messages;
836
- }
837
- return data;
838
- },
839
- });
435
+ test('propagateMetadata callable form runs function', async () => {
436
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
840
437
 
841
- const agent = weflayr_instrument(
842
- new Agent({
843
- name: 'my-agent',
844
- model: openai('gpt-4o-mini'),
845
- instructions: 'You are a helpful assistant.',
846
- })
847
- );
848
-
849
- const streamResult = await agent.stream(
850
- [{ role: 'user', content: 'Hello' }],
851
- { __weflayr_tags: { feature: 'chat', customer_id: '42' } },
852
- );
853
-
854
- let text = '';
855
- for await (const chunk of streamResult.textStream) {
856
- text += chunk;
438
+ const result = await weflayr.propagateMetadata(
439
+ {
440
+ featureName: 'callable',
441
+ customerId: 'c_42',
442
+ extraTags: { variant: 'A' },
443
+ },
444
+ async () => {
445
+ const span = tracer.startSpan('child');
446
+ span.setAttribute('gen_ai.system', 'openai');
447
+ span.end();
448
+ return 'done';
857
449
  }
858
- // SNIPPET_END: mastra_stream
859
- });
860
-
861
- const types = sent.map(e => e.event_type);
862
- assert.ok(types.includes('before'), 'before event sent');
863
- assert.ok(types.includes('stream_start'), 'stream_start event sent');
864
- assert.ok(types.includes('after'), 'after event sent');
865
-
866
- const after = sent.find(e => e.event_type === 'after');
867
- assert.equal(after.method, 'stream');
868
- assert.equal(after.model, 'gpt-4o-mini');
869
- assert.ok(after.usage, 'usage present in after event');
870
- assert.equal(after.tags.feature, 'chat');
871
- assert.equal(after.tags.customer_id, '42');
450
+ );
451
+
452
+ await proc.forceFlush();
453
+ assert.equal(result, 'done');
454
+ const attrs = exporter.exported[0].attributes;
455
+ assert.equal(attrs['weflayr.feature_name'], 'callable');
456
+ assert.equal(attrs['weflayr.tag.variant'], 'A');
457
+ await provider.shutdown();
872
458
  });
873
459
 
874
- // ---------------------------------------------------------------------------
875
- // Vercel AI middleware behavioural
876
- // ---------------------------------------------------------------------------
877
-
878
- test('weflayr_vercel_ai_middleware: wrapGenerate sends before + after with model and tags', async () => {
879
- const { weflayr_setup, weflayr_vercel_ai_middleware } = freshSDK();
880
-
881
- weflayr_setup({ ...TEST_CREDS });
460
+ test('propagateMetadata providerName writes attribute', async () => {
461
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
882
462
 
883
- const middleware = weflayr_vercel_ai_middleware({
884
- model: 'openai/gpt-4o-mini',
885
- tags: { feature: 'chat' },
886
- });
887
-
888
- const sent = await captureHttp(async () => {
889
- await middleware.wrapGenerate({
890
- doGenerate: async () => ({
891
- text: 'Hello!',
892
- usage: { promptTokens: 10, completionTokens: 5 },
893
- finishReason: 'stop',
894
- rawCall: {}, rawResponse: {}, warnings: [],
895
- }),
896
- params: {},
897
- model: {},
898
- });
899
- });
463
+ await weflayr.propagateMetadata(
464
+ {
465
+ featureName: 'f',
466
+ customerId: 'c',
467
+ providerName: weflayr.AIProviderName.VERCEL_AI_GATEWAY,
468
+ },
469
+ async () => {
470
+ const span = tracer.startSpan('child');
471
+ span.setAttribute('gen_ai.system', 'openai');
472
+ span.end();
473
+ }
474
+ );
900
475
 
901
- assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
902
- assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
903
- const before = sent.find(e => e.event_type === 'before');
904
- assert.equal(before.method, 'vercel.generate');
905
- assert.equal(before.args.model, 'openai/gpt-4o-mini');
906
- assert.equal(before.tags.feature, 'chat');
907
- const after = sent.find(e => e.event_type === 'after');
908
- assert.deepEqual(after.response.usage, { promptTokens: 10, completionTokens: 5 });
909
- assert.equal(after.response.finish_reason, 'stop');
910
- assert.ok(after.elapsed_ms >= 0);
476
+ await proc.forceFlush();
477
+ assert.equal(exporter.exported[0].attributes['weflayr.provider_name'], 'vercel_ai_gateway');
478
+ await provider.shutdown();
911
479
  });
912
480
 
913
- test('weflayr_vercel_ai_middleware: wrapStream sends before + stream_start + after with usage', async () => {
914
- const { weflayr_setup, weflayr_vercel_ai_middleware } = freshSDK();
481
+ // ── autoInstrument ────────────────────────────────────────────────────────────
915
482
 
916
- weflayr_setup({ ...TEST_CREDS });
483
+ function _ensureSetupDone() {
484
+ if (!_state.processor) weflayr.setup({ apiKey: 'test-key' });
485
+ }
917
486
 
918
- const middleware = weflayr_vercel_ai_middleware({
919
- model: 'openai/gpt-4o-mini',
920
- tags: { feature: 'chat' },
921
- });
487
+ test('autoInstrument throws "install package X" when the dep is missing', async () => {
488
+ _ensureSetupDone();
489
+ await assert.rejects(
490
+ () => weflayr.autoInstrument({ aiSdks: weflayr.AiSdk.OPENAI }),
491
+ /Install it with: npm install @traceloop\/instrumentation-openai/
492
+ );
493
+ });
922
494
 
923
- const sent = await captureHttp(async () => {
924
- const { stream } = await middleware.wrapStream({
925
- doStream: async () => ({
926
- stream: new ReadableStream({
927
- start(ctrl) {
928
- ctrl.enqueue({ type: 'text-delta', textDelta: 'Hello!' });
929
- ctrl.enqueue({ type: 'finish', finishReason: 'stop', usage: { promptTokens: 10, completionTokens: 5 } });
930
- ctrl.close();
931
- },
932
- }),
933
- rawCall: {}, rawResponse: {}, warnings: [],
934
- }),
935
- params: {},
936
- model: {},
937
- });
495
+ test('autoInstrument rejects unknown providers', async () => {
496
+ // JS keeps this runtime check because `AiSdk` is a frozen object — it
497
+ // doesn't enforce membership at the value level the way Python's `Enum` does.
498
+ _ensureSetupDone();
499
+ await assert.rejects(() => weflayr.autoInstrument({ aiSdks: 'unicorn-llm' }), /unknown provider/);
500
+ });
938
501
 
939
- const reader = stream.getReader();
940
- while (true) {
941
- const { done } = await reader.read();
942
- if (done) break;
502
+ test('autoInstrument loads a stubbed Traceloop package', async () => {
503
+ _ensureSetupDone();
504
+ let constructed = false;
505
+ class FakeOpenAIInstrumentation {
506
+ constructor() {
507
+ constructed = true;
943
508
  }
944
- });
945
-
946
- const types = sent.map(e => e.event_type);
947
- assert.ok(types.includes('before'), 'before event sent');
948
- assert.ok(types.includes('stream_start'), 'stream_start event sent');
949
- assert.ok(types.includes('after'), 'after event sent');
950
- const after = sent.find(e => e.event_type === 'after');
951
- assert.equal(after.method, 'vercel.stream');
952
- assert.deepEqual(after.response.usage, { promptTokens: 10, completionTokens: 5 });
953
- assert.equal(after.response.finish_reason, 'stop');
509
+ enable() {}
510
+ disable() {}
511
+ setTracerProvider() {}
512
+ setMeterProvider() {}
513
+ setConfig() {}
514
+ getConfig() {
515
+ return {};
516
+ }
517
+ }
518
+ const origResolve = Module._resolveFilename;
519
+ Module._resolveFilename = function patched(req, ...rest) {
520
+ if (req === '@traceloop/instrumentation-openai') return req;
521
+ return origResolve.call(this, req, ...rest);
522
+ };
523
+ require.cache['@traceloop/instrumentation-openai'] = {
524
+ id: '@traceloop/instrumentation-openai',
525
+ filename: '@traceloop/instrumentation-openai',
526
+ loaded: true,
527
+ exports: { OpenAIInstrumentation: FakeOpenAIInstrumentation },
528
+ };
529
+ try {
530
+ await weflayr.autoInstrument({ aiSdks: weflayr.AiSdk.OPENAI });
531
+ assert.equal(constructed, true);
532
+ } finally {
533
+ Module._resolveFilename = origResolve;
534
+ delete require.cache['@traceloop/instrumentation-openai'];
535
+ }
954
536
  });
955
537
 
956
- test('weflayr_vercel_ai_middleware: weflayr_propagate tags appear in events', async () => {
957
- const { weflayr_setup, weflayr_vercel_ai_middleware, weflayr_propagate } = freshSDK();
958
-
959
- weflayr_setup({ ...TEST_CREDS });
960
- weflayr_propagate('customer_id', 'acme-corp');
961
-
962
- const middleware = weflayr_vercel_ai_middleware({ model: 'openai/gpt-4o-mini' });
538
+ test('autoInstrument throws when no apiKey is reachable (env nor argument)', async () => {
539
+ _state.processor = null;
540
+ const saved = process.env.WEFLAYR_API_KEY;
541
+ delete process.env.WEFLAYR_API_KEY;
542
+ try {
543
+ await assert.rejects(
544
+ () => weflayr.autoInstrument({ aiSdks: weflayr.AiSdk.OPENAI }),
545
+ /apiKey.*required/
546
+ );
547
+ } finally {
548
+ if (saved !== undefined) process.env.WEFLAYR_API_KEY = saved;
549
+ }
550
+ });
963
551
 
964
- const sent = await captureHttp(async () => {
965
- await middleware.wrapGenerate({
966
- doGenerate: async () => ({ text: 'ok', usage: { promptTokens: 1, completionTokens: 1 }, finishReason: 'stop', rawCall: {}, rawResponse: {}, warnings: [] }),
967
- params: {},
968
- model: {},
552
+ test('autoInstrument runs setup when apiKey passed', async () => {
553
+ _state.processor = null;
554
+ class FakeOpenAIInstrumentation {
555
+ constructor() {}
556
+ enable() {}
557
+ disable() {}
558
+ setTracerProvider() {}
559
+ setMeterProvider() {}
560
+ setConfig() {}
561
+ getConfig() {
562
+ return {};
563
+ }
564
+ }
565
+ const origResolve = Module._resolveFilename;
566
+ Module._resolveFilename = function patched(req, ...rest) {
567
+ if (req === '@traceloop/instrumentation-openai') return req;
568
+ return origResolve.call(this, req, ...rest);
569
+ };
570
+ require.cache['@traceloop/instrumentation-openai'] = {
571
+ id: '@traceloop/instrumentation-openai',
572
+ filename: '@traceloop/instrumentation-openai',
573
+ loaded: true,
574
+ exports: { OpenAIInstrumentation: FakeOpenAIInstrumentation },
575
+ };
576
+ try {
577
+ await weflayr.autoInstrument({
578
+ aiSdks: weflayr.AiSdk.OPENAI,
579
+ apiKey: 'convenience-key',
969
580
  });
970
- });
971
-
972
- const before = sent.find(e => e.event_type === 'before');
973
- assert.equal(before.tags.customer_id, 'acme-corp');
974
- const after = sent.find(e => e.event_type === 'after');
975
- assert.equal(after.tags.customer_id, 'acme-corp');
581
+ assert.ok(_state.processor, 'processor should be installed by the embedded setup() call');
582
+ } finally {
583
+ Module._resolveFilename = origResolve;
584
+ delete require.cache['@traceloop/instrumentation-openai'];
585
+ }
976
586
  });
977
587
 
978
- // ---------------------------------------------------------------------------
979
- // Vercel AI middleware — documentation snippets
980
- // @ai-sdk/openai-compatible and ai are faked by Module._load at top of file.
981
- // ---------------------------------------------------------------------------
982
-
983
- test('snippet vercel_ai_setup: before+after via generateText, method and args correct', async () => {
984
- freshSDK();
985
- setSnippetEnv();
986
-
987
- const sent = await captureHttp(async () => {
988
- // SNIPPET_START: vercel_ai_setup
989
- const { weflayr_setup, weflayr_vercel_ai_middleware } = require('weflayr');
990
- const { wrapLanguageModel, generateText } = require('ai');
991
- const { createOpenAICompatible } = require('@ai-sdk/openai-compatible');
992
-
993
- weflayr_setup({
994
- intake_url: process.env.WEFLAYR_INTAKE_URL,
995
- client_id: process.env.WEFLAYR_CLIENT_ID,
996
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
997
- });
588
+ test('autoInstrument requires aiSdks', async () => {
589
+ _state.processor = null;
590
+ await assert.rejects(() => weflayr.autoInstrument({ apiKey: 'k' }), /`aiSdks` is required/);
591
+ });
998
592
 
999
- const provider = createOpenAICompatible({
1000
- name: 'openai',
1001
- baseURL: 'https://api.openai.com/v1',
1002
- headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
1003
- });
593
+ // ── exporter retry ────────────────────────────────────────────────────────────
1004
594
 
1005
- const model = wrapLanguageModel({
1006
- model: provider('gpt-4o-mini'),
1007
- middleware: weflayr_vercel_ai_middleware({
1008
- model: 'openai/gpt-4o-mini',
1009
- tags: { env: 'production' },
1010
- }),
1011
- });
595
+ function _failResult(status) {
596
+ return { code: 1, error: { code: status, message: `HTTP ${status}` } };
597
+ }
1012
598
 
1013
- await generateText({ model, prompt: 'Hello!' });
1014
- // SNIPPET_END: vercel_ai_setup
1015
- });
599
+ test('exporter retries on 503 then succeeds', async () => {
600
+ const responses = [_failResult(503), _failResult(503), { code: 0 }];
601
+ let i = 0;
602
+ const fakeExport = (spans, cb) => cb(responses[i++]);
603
+ const wrapped = _exportWithRetry(fakeExport, { maxRetries: 3, maxRetryMs: 5000 });
604
+ const result = await new Promise((resolve) => wrapped([], resolve));
605
+ assert.equal(result.code, 0);
606
+ assert.equal(i, 3);
607
+ });
1016
608
 
1017
- assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
1018
- assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
1019
- const before = sent.find(e => e.event_type === 'before');
1020
- assert.equal(before.method, 'vercel.generate');
1021
- assert.equal(before.args.model, 'openai/gpt-4o-mini');
1022
- assert.equal(before.tags.env, 'production');
1023
- const after = sent.find(e => e.event_type === 'after');
1024
- assert.ok(after.response.usage, 'usage in after event');
1025
- assert.ok(after.elapsed_ms >= 0);
609
+ test('exporter does not retry on 401', async () => {
610
+ let calls = 0;
611
+ const fakeExport = (spans, cb) => {
612
+ calls++;
613
+ cb(_failResult(401));
614
+ };
615
+ const wrapped = _exportWithRetry(fakeExport, { maxRetries: 3, maxRetryMs: 5000 });
616
+ const result = await new Promise((resolve) => wrapped([], resolve));
617
+ assert.equal(result.code, 1);
618
+ assert.equal(result.error.code, 401);
619
+ assert.equal(calls, 1);
1026
620
  });
1027
621
 
1028
- test('snippet vercel_ai_propagate: propagated customer_id in before+after events', async () => {
1029
- const { weflayr_setup, weflayr_vercel_ai_middleware } = freshSDK();
1030
- setSnippetEnv();
622
+ test('exporter swallows unexpected exceptions', async () => {
623
+ const fakeExport = () => {
624
+ throw new Error('boom');
625
+ };
626
+ const wrapped = _exportWithRetry(fakeExport, { maxRetries: 1, maxRetryMs: 5000 });
627
+ const result = await new Promise((resolve) => wrapped([], resolve));
628
+ assert.equal(result.code, 1);
629
+ });
1031
630
 
1032
- const { wrapLanguageModel, generateText } = require('ai');
1033
- const { createOpenAICompatible } = require('@ai-sdk/openai-compatible');
631
+ test('exporter gives up after maxRetries on persistent 503', async () => {
632
+ let calls = 0;
633
+ const fakeExport = (spans, cb) => {
634
+ calls++;
635
+ cb(_failResult(503));
636
+ };
637
+ const wrapped = _exportWithRetry(fakeExport, { maxRetries: 3, maxRetryMs: 5000 });
638
+ const result = await new Promise((resolve) => wrapped([], resolve));
639
+ assert.equal(result.code, 1);
640
+ assert.equal(result.error.code, 503);
641
+ assert.equal(calls, 3);
642
+ });
1034
643
 
1035
- weflayr_setup({ ...TEST_CREDS });
644
+ // ── OpenAI streaming usage backfill ─────────────────────────────────────────────
1036
645
 
1037
- const provider = createOpenAICompatible({ name: 'openai', baseURL: 'https://api.openai.com/v1', headers: {} });
1038
- const model = wrapLanguageModel({
1039
- model: provider('gpt-4o-mini'),
1040
- middleware: weflayr_vercel_ai_middleware({ model: 'openai/gpt-4o-mini' }),
1041
- });
646
+ // Build a fake OpenAI instrumentor whose streaming handler mimics traceloop's:
647
+ // it yields content chunks, then a final usage chunk (as OpenAI sends when
648
+ // `stream_options.include_usage` is set) without recording usage itself. The
649
+ // patch should read the usage chunk and set the token attributes on the span.
650
+ function makeFakeOpenAIInstrumentation() {
651
+ return {
652
+ _streamingWrapPromise() {
653
+ return (async function* originalStream() {
654
+ yield { id: 'chatcmpl-1', model: 'gpt-4o-mini', choices: [{ delta: { content: 'Hi' } }] };
655
+ yield {
656
+ id: 'chatcmpl-1',
657
+ model: 'gpt-4o-mini',
658
+ choices: [],
659
+ usage: { prompt_tokens: 13, completion_tokens: 2, total_tokens: 15 },
660
+ };
661
+ })();
662
+ },
663
+ };
664
+ }
1042
665
 
1043
- const sent = await captureHttp(async () => {
1044
- // SNIPPET_START: vercel_ai_propagate
1045
- const { weflayr_propagate } = require('weflayr');
666
+ test('patchOpenAIStreamingUsage sets token attributes from the streamed usage chunk', async () => {
667
+ const span = makeFakeSpan();
668
+ const instrumentation = makeFakeOpenAIInstrumentation();
669
+ patchOpenAIStreamingUsage(instrumentation);
1046
670
 
1047
- weflayr_propagate('customer_id', 'acme-corp');
1048
- await generateText({ model, prompt: 'Hello!' });
1049
- // SNIPPET_END: vercel_ai_propagate
1050
- });
671
+ const stream = instrumentation._streamingWrapPromise({ span });
672
+ for await (const chunk of stream) void chunk;
1051
673
 
1052
- const before = sent.find(e => e.event_type === 'before');
1053
- assert.ok(before, 'before event sent');
1054
- assert.equal(before.tags.customer_id, 'acme-corp');
1055
- const after = sent.find(e => e.event_type === 'after');
1056
- assert.equal(after.tags.customer_id, 'acme-corp');
674
+ assert.equal(span.attributes['gen_ai.usage.input_tokens'], 13);
675
+ assert.equal(span.attributes['gen_ai.usage.output_tokens'], 2);
676
+ assert.equal(span.attributes['gen_ai.usage.total_tokens'], 15);
1057
677
  });
1058
678
 
1059
- test('snippet vercel_ai_stream: before + stream_start + after via streamText', async () => {
1060
- freshSDK();
1061
- setSnippetEnv();
1062
-
1063
- const sent = await captureHttp(async () => {
1064
- // SNIPPET_START: vercel_ai_stream
1065
- const { weflayr_setup, weflayr_vercel_ai_middleware } = require('weflayr');
1066
- const { wrapLanguageModel, streamText } = require('ai');
1067
- const { createOpenAICompatible } = require('@ai-sdk/openai-compatible');
1068
-
1069
- weflayr_setup({
1070
- intake_url: process.env.WEFLAYR_INTAKE_URL,
1071
- client_id: process.env.WEFLAYR_CLIENT_ID,
1072
- client_secret: process.env.WEFLAYR_CLIENT_SECRET,
1073
- });
1074
-
1075
- const provider = createOpenAICompatible({
1076
- name: 'openai',
1077
- baseURL: 'https://api.openai.com/v1',
1078
- headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
1079
- });
1080
-
1081
- const model = wrapLanguageModel({
1082
- model: provider('gpt-4o-mini'),
1083
- middleware: weflayr_vercel_ai_middleware({
1084
- model: 'openai/gpt-4o-mini',
1085
- tags: { feature: 'chat' },
1086
- }),
1087
- });
679
+ test('patchOpenAIStreamingUsage is a no-op when the streaming handler is absent', () => {
680
+ const instrumentation = {};
681
+ patchOpenAIStreamingUsage(instrumentation);
682
+ assert.equal(instrumentation._streamingWrapPromise, undefined);
683
+ });
1088
684
 
1089
- const { textStream } = streamText({ model, prompt: 'Hello!' });
1090
- for await (const _chunk of textStream) {}
1091
- // SNIPPET_END: vercel_ai_stream
1092
- });
685
+ // ── flush ─────────────────────────────────────────────────────────────────────
1093
686
 
1094
- const types = sent.map(e => e.event_type);
1095
- assert.ok(types.includes('before'), 'before event sent');
1096
- assert.ok(types.includes('stream_start'), 'stream_start event sent');
1097
- assert.ok(types.includes('after'), 'after event sent');
1098
- const after = sent.find(e => e.event_type === 'after');
1099
- assert.equal(after.method, 'vercel.stream');
1100
- assert.equal(after.tags.feature, 'chat');
1101
- assert.ok(after.response.usage, 'usage in after event');
687
+ test('flush does not throw', async () => {
688
+ await weflayr.flush();
1102
689
  });
690
+
691
+ void trace;