weflayr 0.22.0 → 0.22.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.
Files changed (77) hide show
  1. package/.prettierignore +2 -0
  2. package/dist/api/caseConversion.d.ts +16 -0
  3. package/dist/api/caseConversion.js +45 -0
  4. package/dist/api/generated/client/client.gen.d.ts +2 -0
  5. package/dist/api/generated/client/client.gen.js +220 -0
  6. package/dist/api/generated/client/index.d.ts +10 -0
  7. package/dist/api/generated/client/index.js +17 -0
  8. package/dist/api/generated/client/types.gen.d.ts +120 -0
  9. package/dist/api/generated/client/types.gen.js +3 -0
  10. package/dist/api/generated/client/utils.gen.d.ts +37 -0
  11. package/dist/api/generated/client/utils.gen.js +239 -0
  12. package/dist/api/generated/client.gen.d.ts +12 -0
  13. package/dist/api/generated/client.gen.js +6 -0
  14. package/dist/api/generated/core/auth.gen.d.ts +25 -0
  15. package/dist/api/generated/core/auth.gen.js +18 -0
  16. package/dist/api/generated/core/bodySerializer.gen.d.ts +25 -0
  17. package/dist/api/generated/core/bodySerializer.gen.js +60 -0
  18. package/dist/api/generated/core/params.gen.d.ts +43 -0
  19. package/dist/api/generated/core/params.gen.js +112 -0
  20. package/dist/api/generated/core/pathSerializer.gen.d.ts +33 -0
  21. package/dist/api/generated/core/pathSerializer.gen.js +115 -0
  22. package/dist/api/generated/core/queryKeySerializer.gen.d.ts +18 -0
  23. package/dist/api/generated/core/queryKeySerializer.gen.js +98 -0
  24. package/dist/api/generated/core/serverSentEvents.gen.d.ts +71 -0
  25. package/dist/api/generated/core/serverSentEvents.gen.js +135 -0
  26. package/dist/api/generated/core/types.gen.d.ts +83 -0
  27. package/dist/api/generated/core/types.gen.js +3 -0
  28. package/dist/api/generated/core/utils.gen.d.ts +19 -0
  29. package/dist/api/generated/core/utils.gen.js +93 -0
  30. package/dist/api/generated/facade.gen.d.ts +153 -0
  31. package/dist/api/generated/facade.gen.js +49 -0
  32. package/dist/api/generated/index.d.ts +2 -0
  33. package/dist/api/generated/index.js +14 -0
  34. package/dist/api/generated/sdk.gen.d.ts +51 -0
  35. package/dist/api/generated/sdk.gen.js +98 -0
  36. package/dist/api/generated/types.gen.d.ts +652 -0
  37. package/dist/api/generated/types.gen.js +3 -0
  38. package/dist/api/index.d.ts +27 -0
  39. package/dist/api/index.js +48 -0
  40. package/dist/api/makeEndpoint.d.ts +27 -0
  41. package/dist/api/makeEndpoint.js +37 -0
  42. package/index.d.ts +24 -15
  43. package/openapi-ts.config.mjs +9 -0
  44. package/package.json +9 -2
  45. package/scripts/generate_facade.mjs +86 -0
  46. package/src/api/caseConversion.ts +58 -0
  47. package/src/api/generated/client/client.gen.ts +277 -0
  48. package/src/api/generated/client/index.ts +27 -0
  49. package/src/api/generated/client/types.gen.ts +218 -0
  50. package/src/api/generated/client/utils.gen.ts +316 -0
  51. package/src/api/generated/client.gen.ts +16 -0
  52. package/src/api/generated/core/auth.gen.ts +48 -0
  53. package/src/api/generated/core/bodySerializer.gen.ts +82 -0
  54. package/src/api/generated/core/params.gen.ts +178 -0
  55. package/src/api/generated/core/pathSerializer.gen.ts +171 -0
  56. package/src/api/generated/core/queryKeySerializer.gen.ts +117 -0
  57. package/src/api/generated/core/serverSentEvents.gen.ts +242 -0
  58. package/src/api/generated/core/types.gen.ts +110 -0
  59. package/src/api/generated/core/utils.gen.ts +140 -0
  60. package/src/api/generated/facade.gen.ts +62 -0
  61. package/src/api/generated/index.ts +4 -0
  62. package/src/api/generated/sdk.gen.ts +112 -0
  63. package/src/api/generated/types.gen.ts +718 -0
  64. package/src/api/index.ts +45 -0
  65. package/src/api/makeEndpoint.ts +54 -0
  66. package/src/auto-instrument.js +19 -6
  67. package/src/index.js +5 -0
  68. package/src/instrumentation/google/get_provider_name.js +2 -1
  69. package/src/instrumentation/vercel/full_instrumentation.js +44 -0
  70. package/src/otel/propagation.js +21 -12
  71. package/src/otel/span-processor.js +11 -1
  72. package/src/otel/vercel-ai-sdk-span-filter.js +79 -0
  73. package/tests/api/index.test.js +361 -0
  74. package/tests/index.test.js +48 -15
  75. package/tests/instrumentation/vercel/full_instrumentation.test.js +16 -0
  76. package/tests/otel/vercel-ai-sdk-span-filter.test.js +159 -0
  77. package/tsconfig.json +18 -0
@@ -0,0 +1,361 @@
1
+ 'use strict';
2
+
3
+ // Tests for the public-API client: auth and base-URL wiring of the
4
+ // client facade, plus the request/response wiring of every generated
5
+ // endpoint (server behavior is covered by the dashboard's app_api tests).
6
+
7
+ const { test } = require('node:test');
8
+ const assert = require('node:assert/strict');
9
+ const http = require('node:http');
10
+
11
+ const weflayr = require('../../src/index');
12
+ const { api } = weflayr;
13
+
14
+ // A client whose server records requests and returns a canned response.
15
+ async function withClientReturning(responseBody, statusCode, run, clientOptions = {}) {
16
+ const seenRequests = [];
17
+ const server = http.createServer((request, response) => {
18
+ let requestBody = '';
19
+ request.on('data', (chunk) => {
20
+ requestBody += chunk;
21
+ });
22
+ request.on('end', () => {
23
+ seenRequests.push({
24
+ method: request.method,
25
+ url: request.url,
26
+ authorization: request.headers.authorization,
27
+ body: requestBody === '' ? null : JSON.parse(requestBody),
28
+ });
29
+ response.statusCode = statusCode;
30
+ response.setHeader('content-type', 'application/json');
31
+ response.end(JSON.stringify(responseBody));
32
+ });
33
+ });
34
+ await new Promise((resolve) => server.listen(0, resolve));
35
+ const client = api.client({
36
+ apiKey: 'wf-test-key',
37
+ ...clientOptions,
38
+ baseUrl: `http://127.0.0.1:${server.address().port}/api`,
39
+ });
40
+ try {
41
+ await run(client, seenRequests);
42
+ } finally {
43
+ server.close();
44
+ }
45
+ }
46
+
47
+ // ── client facade ─────────────────────────────────────────────────────────
48
+
49
+ test('client sends the bearer key to the configured host', async () => {
50
+ await withClientReturning({ rows: [] }, 200, async (client, seenRequests) => {
51
+ const { data } = await api.getRevenue({ client, throwOnError: true });
52
+ assert.deepEqual(data, { rows: [] });
53
+ assert.deepEqual(seenRequests, [
54
+ { method: 'GET', url: '/api/revenue/', authorization: 'Bearer wf-test-key', body: null },
55
+ ]);
56
+ });
57
+ });
58
+
59
+ test('weflayr.client is the same function as api.client', () => {
60
+ assert.equal(weflayr.client, api.client);
61
+ });
62
+
63
+ test('client defaults to the production API host', () => {
64
+ const client = api.client({ apiKey: 'wf-test-key' });
65
+ assert.equal(client.getConfig().baseUrl, 'https://app.weflayr.com/api');
66
+ });
67
+
68
+ test('client throws without apiKey', () => {
69
+ const saved = process.env.WEFLAYR_API_KEY;
70
+ delete process.env.WEFLAYR_API_KEY;
71
+ try {
72
+ assert.throws(() => api.client(), /apiKey/);
73
+ } finally {
74
+ if (saved !== undefined) process.env.WEFLAYR_API_KEY = saved;
75
+ }
76
+ });
77
+
78
+ test('client picks up apiKey from WEFLAYR_API_KEY env var', async () => {
79
+ process.env.WEFLAYR_API_KEY = 'env-key';
80
+ try {
81
+ await withClientReturning(
82
+ { rows: [] },
83
+ 200,
84
+ async (client, seenRequests) => {
85
+ await api.getRevenue({ client, throwOnError: true });
86
+ assert.equal(seenRequests[0].authorization, 'Bearer env-key');
87
+ },
88
+ { apiKey: undefined }
89
+ );
90
+ } finally {
91
+ delete process.env.WEFLAYR_API_KEY;
92
+ }
93
+ });
94
+
95
+ // ── Revenue ───────────────────────────────────────────────────────────────────
96
+
97
+ test('setRevenue posts the rows in snake_case and parses the booked count', async () => {
98
+ await withClientReturning({ ok: true, rows_inserted: 2 }, 200, async (client, seenRequests) => {
99
+ const rows = [
100
+ { customerName: 'c_1', amount: 1200.5, month: '2026-06' },
101
+ { customerName: 'c_2', amount: 800, month: '2026-06' },
102
+ ];
103
+ const { data } = await api.setRevenue({ client, throwOnError: true, body: { rows } });
104
+ assert.deepEqual(data, { ok: true, rowsInserted: 2 });
105
+ const request = seenRequests[0];
106
+ assert.equal(request.method, 'POST');
107
+ assert.equal(request.url, '/api/revenue/');
108
+ assert.deepEqual(request.body, {
109
+ rows: [
110
+ { customer_name: 'c_1', amount: 1200.5, month: '2026-06' },
111
+ { customer_name: 'c_2', amount: 800, month: '2026-06' },
112
+ ],
113
+ });
114
+ });
115
+ });
116
+
117
+ test('setRevenue posts the revenueId in snake_case and the upsert flag', async () => {
118
+ await withClientReturning({ ok: true, rows_inserted: 1 }, 200, async (client, seenRequests) => {
119
+ const rows = [
120
+ {
121
+ revenueId: 'b3f2b160-6e12-4f1a-9c3d-0a2e7c9b5d41',
122
+ customerName: 'c_1',
123
+ amount: 1300,
124
+ month: '2026-06',
125
+ },
126
+ ];
127
+ const { data } = await api.setRevenue({
128
+ client,
129
+ throwOnError: true,
130
+ body: { upsert: true, rows },
131
+ });
132
+ assert.deepEqual(data, { ok: true, rowsInserted: 1 });
133
+ assert.deepEqual(seenRequests[0].body, {
134
+ upsert: true,
135
+ rows: [
136
+ {
137
+ revenue_id: 'b3f2b160-6e12-4f1a-9c3d-0a2e7c9b5d41',
138
+ customer_name: 'c_1',
139
+ amount: 1300,
140
+ month: '2026-06',
141
+ },
142
+ ],
143
+ });
144
+ });
145
+ });
146
+
147
+ test('getRevenue parses the returned rows into camelCase', async () => {
148
+ const wireRow = {
149
+ customer_name: 'c_1',
150
+ metric_name: 'revenue',
151
+ month: '2026-06',
152
+ amount: 1200.5,
153
+ source: 'api',
154
+ };
155
+ await withClientReturning({ rows: [wireRow] }, 200, async (client) => {
156
+ const { data } = await api.getRevenue({ client, throwOnError: true });
157
+ assert.deepEqual(data.rows, [
158
+ {
159
+ customerName: 'c_1',
160
+ metricName: 'revenue',
161
+ month: '2026-06',
162
+ amount: 1200.5,
163
+ source: 'api',
164
+ },
165
+ ]);
166
+ });
167
+ });
168
+
169
+ test('getRevenue sends the customerNames filter as repeated params', async () => {
170
+ await withClientReturning({ rows: [] }, 200, async (client, seenRequests) => {
171
+ const { data } = await api.getRevenue({
172
+ client,
173
+ throwOnError: true,
174
+ query: { customerNames: ['c_1', 'c_2'] },
175
+ });
176
+ assert.deepEqual(data, { rows: [] });
177
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
178
+ assert.deepEqual(query.getAll('customer_names'), ['c_1', 'c_2']);
179
+ });
180
+ });
181
+
182
+ test('deleteRevenue sends the customerNames filter and parses the deleted count', async () => {
183
+ await withClientReturning({ ok: true, rows_deleted: 2 }, 200, async (client, seenRequests) => {
184
+ const { data } = await api.deleteRevenue({
185
+ client,
186
+ throwOnError: true,
187
+ query: { customerNames: ['c_1', 'c_2'] },
188
+ });
189
+ assert.deepEqual(data, { ok: true, rowsDeleted: 2 });
190
+ const request = seenRequests[0];
191
+ assert.equal(request.method, 'DELETE');
192
+ const url = new URL(request.url, 'http://x');
193
+ assert.equal(url.pathname, '/api/revenue/');
194
+ assert.deepEqual(url.searchParams.getAll('customer_names'), ['c_1', 'c_2']);
195
+ });
196
+ });
197
+
198
+ // ── Key metrics ───────────────────────────────────────────────────────────────
199
+
200
+ test('setKeyMetrics posts the rows in snake_case and parses the booked count', async () => {
201
+ await withClientReturning({ ok: true, rows_inserted: 1 }, 200, async (client, seenRequests) => {
202
+ const rows = [{ userName: 'u_1', metricName: 'Meetings booked', amount: 3, month: '2026-06' }];
203
+ const { data } = await api.setKeyMetrics({ client, throwOnError: true, body: { rows } });
204
+ assert.deepEqual(data, { ok: true, rowsInserted: 1 });
205
+ const request = seenRequests[0];
206
+ assert.equal(request.method, 'POST');
207
+ assert.equal(request.url, '/api/key-metrics/');
208
+ assert.deepEqual(request.body, {
209
+ rows: [{ user_name: 'u_1', metric_name: 'Meetings booked', amount: 3, month: '2026-06' }],
210
+ });
211
+ });
212
+ });
213
+
214
+ test('getKeyMetrics parses the returned rows into camelCase', async () => {
215
+ const wireRow = {
216
+ user_name: 'u_1',
217
+ metric_name: 'Meetings booked',
218
+ month: '2026-06',
219
+ amount: 3.0,
220
+ source: 'api',
221
+ };
222
+ await withClientReturning({ rows: [wireRow] }, 200, async (client) => {
223
+ const { data } = await api.getKeyMetrics({ client, throwOnError: true });
224
+ assert.deepEqual(data.rows, [
225
+ {
226
+ userName: 'u_1',
227
+ metricName: 'Meetings booked',
228
+ month: '2026-06',
229
+ amount: 3.0,
230
+ source: 'api',
231
+ },
232
+ ]);
233
+ });
234
+ });
235
+
236
+ test('getKeyMetrics sends the userNames filter as repeated params', async () => {
237
+ await withClientReturning({ rows: [] }, 200, async (client, seenRequests) => {
238
+ const { data } = await api.getKeyMetrics({
239
+ client,
240
+ throwOnError: true,
241
+ query: { userNames: ['u_1', 'u_2'] },
242
+ });
243
+ assert.deepEqual(data, { rows: [] });
244
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
245
+ assert.deepEqual(query.getAll('user_names'), ['u_1', 'u_2']);
246
+ });
247
+ });
248
+
249
+ test('setKeyMetrics surfaces a 422 and its details', async () => {
250
+ const rejection = {
251
+ error: 'Invalid payload.',
252
+ detail: [{ field: 'rows.0.metric_name', error: 'Unknown metric.' }],
253
+ };
254
+ await withClientReturning(rejection, 422, async (client) => {
255
+ const { error, response } = await api.setKeyMetrics({ client, body: { rows: [] } });
256
+ assert.equal(response.status, 422);
257
+ assert.deepEqual(error, rejection);
258
+ });
259
+ });
260
+
261
+ test('deleteKeyMetrics sends the userNames filter and parses the deleted count', async () => {
262
+ await withClientReturning({ ok: true, rows_deleted: 2 }, 200, async (client, seenRequests) => {
263
+ const { data } = await api.deleteKeyMetrics({
264
+ client,
265
+ throwOnError: true,
266
+ query: { userNames: ['u_1', 'u_2'] },
267
+ });
268
+ assert.deepEqual(data, { ok: true, rowsDeleted: 2 });
269
+ const request = seenRequests[0];
270
+ assert.equal(request.method, 'DELETE');
271
+ const url = new URL(request.url, 'http://x');
272
+ assert.equal(url.pathname, '/api/key-metrics/');
273
+ assert.deepEqual(url.searchParams.getAll('user_names'), ['u_1', 'u_2']);
274
+ });
275
+ });
276
+
277
+ // ── Customer tags ─────────────────────────────────────────────────────────────
278
+
279
+ test('setCustomersTags posts the customers in snake_case and parses the updated count', async () => {
280
+ await withClientReturning(
281
+ { ok: true, customers_updated: 1 },
282
+ 200,
283
+ async (client, seenRequests) => {
284
+ const customers = [{ customerName: 'c_1', tags: { plan: 'pro' } }];
285
+ const { data } = await api.setCustomersTags({
286
+ client,
287
+ throwOnError: true,
288
+ body: { customers },
289
+ });
290
+ assert.deepEqual(data, { ok: true, customersUpdated: 1 });
291
+ const request = seenRequests[0];
292
+ assert.equal(request.method, 'POST');
293
+ assert.equal(request.url, '/api/customer-tags/');
294
+ assert.deepEqual(request.body, {
295
+ customers: [{ customer_name: 'c_1', tags: { plan: 'pro' } }],
296
+ });
297
+ }
298
+ );
299
+ });
300
+
301
+ test('setCustomersTags leaves tag keys untouched, even when they contain underscores', async () => {
302
+ await withClientReturning(
303
+ { ok: true, customers_updated: 1 },
304
+ 200,
305
+ async (client, seenRequests) => {
306
+ const customers = [{ customerName: 'c_1', tags: { utm_source: 'ads', plan_tier: 'pro' } }];
307
+ await api.setCustomersTags({ client, throwOnError: true, body: { customers } });
308
+ assert.deepEqual(seenRequests[0].body, {
309
+ customers: [{ customer_name: 'c_1', tags: { utm_source: 'ads', plan_tier: 'pro' } }],
310
+ });
311
+ }
312
+ );
313
+ });
314
+
315
+ test('getCustomersTags parses the returned customers into camelCase', async () => {
316
+ const wireCustomer = { customer_name: 'c_1', tags: { plan: 'pro' } };
317
+ await withClientReturning(
318
+ { customers: [wireCustomer], next_cursor: null, has_more: false },
319
+ 200,
320
+ async (client) => {
321
+ const { data } = await api.getCustomersTags({ client, throwOnError: true });
322
+ assert.deepEqual(data.customers, [{ customerName: 'c_1', tags: { plan: 'pro' } }]);
323
+ }
324
+ );
325
+ });
326
+
327
+ test('getCustomersTags sends the customerNames filter, leaving tag keys untouched', async () => {
328
+ const wireCustomer = { customer_name: 'c_1', tags: { plan: 'pro', utm_source: 'ads' } };
329
+ await withClientReturning(
330
+ { customers: [wireCustomer], next_cursor: null, has_more: false },
331
+ 200,
332
+ async (client, seenRequests) => {
333
+ const { data } = await api.getCustomersTags({
334
+ client,
335
+ throwOnError: true,
336
+ query: { customerNames: ['c_1', 'c_2'] },
337
+ });
338
+ assert.deepEqual(data.customers, [
339
+ { customerName: 'c_1', tags: { plan: 'pro', utm_source: 'ads' } },
340
+ ]);
341
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
342
+ assert.deepEqual(query.getAll('customer_names'), ['c_1', 'c_2']);
343
+ }
344
+ );
345
+ });
346
+
347
+ test('deleteCustomersTags sends the customerNames filter and parses the deleted count', async () => {
348
+ await withClientReturning({ ok: true, tags_deleted: 3 }, 200, async (client, seenRequests) => {
349
+ const { data } = await api.deleteCustomersTags({
350
+ client,
351
+ throwOnError: true,
352
+ query: { customerNames: ['c_1', 'c_2'] },
353
+ });
354
+ assert.deepEqual(data, { ok: true, tagsDeleted: 3 });
355
+ const request = seenRequests[0];
356
+ assert.equal(request.method, 'DELETE');
357
+ const url = new URL(request.url, 'http://x');
358
+ assert.equal(url.pathname, '/api/customer-tags/');
359
+ assert.deepEqual(url.searchParams.getAll('customer_names'), ['c_1', 'c_2']);
360
+ });
361
+ });
@@ -405,7 +405,7 @@ test('propagateMetadata writes structured fields and extra tags', async () => {
405
405
  await weflayr.propagateMetadata(
406
406
  {
407
407
  featureName: 'onboarding',
408
- customerId: '#3756',
408
+ customerName: '#3756',
409
409
  extraTags: { variant: 'control' },
410
410
  },
411
411
  async () => {
@@ -419,7 +419,7 @@ test('propagateMetadata writes structured fields and extra tags', async () => {
419
419
  assert.equal(exporter.exported.length, 1);
420
420
  const attrs = exporter.exported[0].attributes;
421
421
  assert.equal(attrs['weflayr.feature_name'], 'onboarding');
422
- assert.equal(attrs['weflayr.customer_id'], '#3756');
422
+ assert.equal(attrs['weflayr.customer_name'], '#3756');
423
423
  assert.equal(attrs['weflayr.tag.variant'], 'control');
424
424
  assert.ok(attrs['weflayr.uuid']);
425
425
  assert.equal(attrs['weflayr.provider_name'], undefined);
@@ -429,7 +429,7 @@ test('propagateMetadata writes structured fields and extra tags', async () => {
429
429
  test('propagateMetadata uuid is stable within scope and changes across calls', async () => {
430
430
  const { exporter, proc, provider, tracer } = _makeMetadataHarness();
431
431
 
432
- await weflayr.propagateMetadata({ featureName: 'f', customerId: 'c' }, async () => {
432
+ await weflayr.propagateMetadata({ featureName: 'f', customerName: 'c' }, async () => {
433
433
  const spanA = tracer.startSpan('a');
434
434
  spanA.setAttribute('gen_ai.system', 'openai');
435
435
  spanA.end();
@@ -438,7 +438,7 @@ test('propagateMetadata uuid is stable within scope and changes across calls', a
438
438
  spanB.end();
439
439
  });
440
440
 
441
- await weflayr.propagateMetadata({ featureName: 'f', customerId: 'c' }, async () => {
441
+ await weflayr.propagateMetadata({ featureName: 'f', customerName: 'c' }, async () => {
442
442
  const spanC = tracer.startSpan('c_span');
443
443
  spanC.setAttribute('gen_ai.system', 'openai');
444
444
  spanC.end();
@@ -459,14 +459,14 @@ test('propagateMetadata nested scopes inner overrides and extra tags merge', asy
459
459
  await weflayr.propagateMetadata(
460
460
  {
461
461
  featureName: 'outer',
462
- customerId: 'outer_c',
462
+ customerName: 'outer_c',
463
463
  extraTags: { variant: 'A', shared: 'outer' },
464
464
  },
465
465
  async () => {
466
466
  await weflayr.propagateMetadata(
467
467
  {
468
468
  featureName: 'inner',
469
- customerId: 'inner_c',
469
+ customerName: 'inner_c',
470
470
  extraTags: { shared: 'inner', experiment: 'B' },
471
471
  },
472
472
  async () => {
@@ -481,7 +481,7 @@ test('propagateMetadata nested scopes inner overrides and extra tags merge', asy
481
481
  await proc.forceFlush();
482
482
  const attrs = exporter.exported[0].attributes;
483
483
  assert.equal(attrs['weflayr.feature_name'], 'inner');
484
- assert.equal(attrs['weflayr.customer_id'], 'inner_c');
484
+ assert.equal(attrs['weflayr.customer_name'], 'inner_c');
485
485
  // Outer extra tag not shadowed survives
486
486
  assert.equal(attrs['weflayr.tag.variant'], 'A');
487
487
  // Inner extra tag wins on conflict
@@ -493,7 +493,7 @@ test('propagateMetadata nested scopes inner overrides and extra tags merge', asy
493
493
  test('propagateMetadata decorator mints fresh uuid per invocation', async () => {
494
494
  const { exporter, proc, provider, tracer } = _makeMetadataHarness();
495
495
 
496
- const wrap = weflayr.propagateMetadata({ featureName: 'f', customerId: 'c' });
496
+ const wrap = weflayr.propagateMetadata({ featureName: 'f', customerName: 'c' });
497
497
  const leaf = wrap(async function leaf() {
498
498
  const span = tracer.startSpan('child');
499
499
  span.setAttribute('gen_ai.system', 'openai');
@@ -515,7 +515,7 @@ test('propagateMetadata decorator form wraps a function', async () => {
515
515
 
516
516
  const wrap = weflayr.propagateMetadata({
517
517
  featureName: 'chat',
518
- customerId: 'c_42',
518
+ customerName: 'c_42',
519
519
  extraTags: { experiment: 'B' },
520
520
  });
521
521
  const leaf = wrap(async function leaf() {
@@ -528,7 +528,7 @@ test('propagateMetadata decorator form wraps a function', async () => {
528
528
  await proc.forceFlush();
529
529
  const attrs = exporter.exported[0].attributes;
530
530
  assert.equal(attrs['weflayr.feature_name'], 'chat');
531
- assert.equal(attrs['weflayr.customer_id'], 'c_42');
531
+ assert.equal(attrs['weflayr.customer_name'], 'c_42');
532
532
  assert.equal(attrs['weflayr.tag.experiment'], 'B');
533
533
  await provider.shutdown();
534
534
  });
@@ -539,7 +539,7 @@ test('propagateMetadata callable form runs function', async () => {
539
539
  const result = await weflayr.propagateMetadata(
540
540
  {
541
541
  featureName: 'callable',
542
- customerId: 'c_42',
542
+ customerName: 'c_42',
543
543
  extraTags: { variant: 'A' },
544
544
  },
545
545
  async () => {
@@ -564,7 +564,7 @@ test('propagateMetadata providerName writes attribute', async () => {
564
564
  await weflayr.propagateMetadata(
565
565
  {
566
566
  featureName: 'f',
567
- customerId: 'c',
567
+ customerName: 'c',
568
568
  providerName: weflayr.AIProviderName.VERCEL_AI_GATEWAY,
569
569
  },
570
570
  async () => {
@@ -579,10 +579,34 @@ test('propagateMetadata providerName writes attribute', async () => {
579
579
  await provider.shutdown();
580
580
  });
581
581
 
582
- test('propagateMetadata requires featureName and customerId', () => {
583
- assert.throws(() => weflayr.propagateMetadata({}), /featureName.*customerId.*required/);
582
+ test('propagateMetadata requires featureName and customerName', () => {
583
+ assert.throws(() => weflayr.propagateMetadata({}), /featureName.*customerName.*required/);
584
584
  assert.throws(() => weflayr.propagateMetadata({ featureName: 'f' }), /required/);
585
- assert.throws(() => weflayr.propagateMetadata({ customerId: 'c' }), /required/);
585
+ assert.throws(() => weflayr.propagateMetadata({ customerName: 'c' }), /required/);
586
+ });
587
+
588
+ test('propagateMetadata deprecated customerId still works', async () => {
589
+ const { exporter, proc, provider, tracer } = _makeMetadataHarness();
590
+
591
+ const warnings = [];
592
+ const onWarning = (warning) => warnings.push(warning);
593
+ process.on('warning', onWarning);
594
+
595
+ await weflayr.propagateMetadata({ featureName: 'f', customerId: 'Acme Corp' }, async () => {
596
+ const span = tracer.startSpan('child');
597
+ span.setAttribute('gen_ai.system', 'openai');
598
+ span.end();
599
+ });
600
+
601
+ // process.emitWarning delivers on a later tick; drain it before asserting.
602
+ await new Promise((resolve) => process.nextTick(resolve));
603
+ process.removeListener('warning', onWarning);
604
+ assert.ok(warnings.some((warning) => /customerId.*deprecated/.test(warning.message)));
605
+
606
+ await proc.forceFlush();
607
+ const attrs = exporter.exported[0].attributes;
608
+ assert.equal(attrs['weflayr.customer_name'], 'Acme Corp');
609
+ await provider.shutdown();
586
610
  });
587
611
 
588
612
  // ── autoInstrument ────────────────────────────────────────────────────────────
@@ -688,6 +712,15 @@ test('autoInstrument throws when no apiKey is reachable (env nor argument)', asy
688
712
  }
689
713
  });
690
714
 
715
+ test('autoInstrument with an empty aiSdks list still runs setup', async () => {
716
+ // The vercel instrumentor's ai <= 6 error message tells users to keep their
717
+ // autoInstrument call with 'vercel_ai_gateway' removed (possibly leaving an
718
+ // empty list) — that must still install the processor.
719
+ _state.processor = null;
720
+ await weflayr.autoInstrument({ aiSdks: [], apiKey: 'empty-list-key' });
721
+ assert.ok(_state.processor, 'processor should be installed by the embedded setup() call');
722
+ });
723
+
691
724
  test('autoInstrument runs setup when apiKey passed', async () => {
692
725
  _state.processor = null;
693
726
  class FakeOpenAIInstrumentation {
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const {
7
+ VercelAiSdkInstrumentor,
8
+ } = require('../../../src/instrumentation/vercel/full_instrumentation');
9
+
10
+ // The `ai` / `@ai-sdk/otel` packages are app dependencies (not installed in
11
+ // this repo), so instrument() must fail with an actionable install message.
12
+ // The success path (registerTelemetry wiring) is covered by the vercel e2e
13
+ // ingestion scenario.
14
+ test('instrument() without the ai packages installed throws an install hint', () => {
15
+ assert.throws(() => new VercelAiSdkInstrumentor().instrument(), /npm install ai @ai-sdk\/otel/);
16
+ });