weflayr 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -66,6 +66,11 @@ export interface WeflayrSettings {
66
66
  * Mutually exclusive with `ignore_fields` — setting both blocks all events.
67
67
  */
68
68
  allow_fields?: ContentPolicyFn;
69
+ /**
70
+ * Static tags attached to every instrumented call. Can be overridden per-call
71
+ * via `__weflayr_tags` or at runtime via `weflayr_propagate`.
72
+ */
73
+ default_tags?: Tags;
69
74
  /** Methods to instrument on the proxied object. */
70
75
  methods?: MethodConfig[];
71
76
  }
@@ -80,6 +85,14 @@ export function weflayr_setup(settings: WeflayrSettings): void;
80
85
  */
81
86
  export function weflayr_instrument<T>(target: T): T;
82
87
 
88
+ /**
89
+ * Adds or overrides a single key in the runtime default tags.
90
+ * Equivalent to adding the key to `default_tags` in `weflayr_setup`, but callable at any
91
+ * point during execution (e.g. inside a request handler to propagate a customer ID).
92
+ * Propagated tags are overridden by per-call `__weflayr_tags`.
93
+ */
94
+ export function weflayr_propagate(key: string, value: string | number | boolean): void;
95
+
83
96
  /** Sends a custom event to the intake API from user code. No-ops if `weflayr_setup` hasn't been called. */
84
97
  export function send_event(
85
98
  eventType: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Weflayr Node.js SDK — instrument any LLM client via JS Proxy",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { configure, weflayr_instrument, send_event } = require('./instrument');
3
+ const { configure, weflayr_instrument, weflayr_propagate, send_event } = require('./instrument');
4
4
 
5
5
  /**
6
6
  *
@@ -32,4 +32,4 @@ function flayred(options) {
32
32
  return options;
33
33
  }
34
34
 
35
- module.exports = { weflayr_setup, weflayr_instrument, send_event, flayred };
35
+ module.exports = { weflayr_setup, weflayr_instrument, weflayr_propagate, send_event, flayred };
package/src/instrument.js CHANGED
@@ -3,12 +3,15 @@
3
3
  const https = require('https');
4
4
  const http = require('http');
5
5
  const { randomUUID } = require('crypto');
6
+ const { AsyncLocalStorage } = require('async_hooks');
6
7
  const { version: SDK_VERSION } = require('../package.json');
7
8
  const SDK_LANGUAGE = 'javascript';
8
9
 
9
10
  let _config = null;
11
+ const _asyncStore = new AsyncLocalStorage();
10
12
 
11
13
  function configure(intakeUrl, clientId, clientSecret, settings) {
14
+ _asyncStore.enterWith({});
12
15
  if (settings.ignore_fields && settings.allow_fields) {
13
16
  console.warn('[weflayr] both ignore_fields and allow_fields are set - no events will be sent to the intake API');
14
17
  _config = { intakeUrl, clientId, clientSecret, settings, _blocked: true };
@@ -173,12 +176,12 @@ async function* _wrapStream(stream, methodName, tags, requestArgs, settings, sta
173
176
  }
174
177
 
175
178
  function _extractTags(args) {
176
- let tags = {};
179
+ let callTags = {};
177
180
 
178
181
  function strip(obj) {
179
182
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return;
180
183
  if ('__weflayr_tags' in obj) {
181
- tags = obj.__weflayr_tags || {};
184
+ callTags = obj.__weflayr_tags || {};
182
185
  delete obj.__weflayr_tags;
183
186
  }
184
187
  for (const key of Object.keys(obj)) {
@@ -191,9 +194,20 @@ function _extractTags(args) {
191
194
  return arg;
192
195
  });
193
196
 
197
+ const tags = {
198
+ ...(_config.settings.default_tags || {}),
199
+ ..._asyncStore.getStore(),
200
+ ...callTags,
201
+ };
202
+
194
203
  return { tags, cleanArgs };
195
204
  }
196
205
 
206
+ function weflayr_propagate(key, value) {
207
+ _asyncStore.getStore()[key] = value;
208
+ }
209
+
210
+
197
211
  function _isAsyncIterable(value) {
198
212
  return value != null && typeof value[Symbol.asyncIterator] === 'function';
199
213
  }
@@ -297,4 +311,4 @@ function send_event(eventType, data) {
297
311
  return _sendEvent(eventType, data);
298
312
  }
299
313
 
300
- module.exports = { configure, weflayr_instrument, send_event };
314
+ module.exports = { configure, weflayr_instrument, weflayr_propagate, send_event };
@@ -268,6 +268,10 @@ test('snippet quickstart_setup: wraps client, sends before+after events', async
268
268
  intake_url: process.env.WEFLAYR_INTAKE_URL,
269
269
  client_id: process.env.WEFLAYR_CLIENT_ID,
270
270
  client_secret: process.env.WEFLAYR_CLIENT_SECRET,
271
+ default_tags: {
272
+ app: 'my-app',
273
+ version: '1.0.0',
274
+ },
271
275
  methods: [
272
276
  { call: 'chat.completions.create' },
273
277
  ],
@@ -283,6 +287,8 @@ test('snippet quickstart_setup: wraps client, sends before+after events', async
283
287
  assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
284
288
  assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
285
289
  assert.equal(sent[0].method, 'chat.completions.create');
290
+ assert.equal(sent[0].tags.app, 'my-app');
291
+ assert.equal(sent[0].tags.version, '1.0.0');
286
292
  });
287
293
 
288
294
  test('snippet quickstart_call: tags stripped from call, tags in telemetry', async () => {
@@ -427,3 +433,150 @@ test('snippet openai_image_generation: b64_json stripped from telemetry, url pre
427
433
  assert.ok(after.response.data[0].url, 'url preserved in telemetry');
428
434
  });
429
435
 
436
+ // ---------------------------------------------------------------------------
437
+ // default_tags and weflayr_propagate — behavioural tests
438
+ // ---------------------------------------------------------------------------
439
+
440
+ test('default_tags appear in before and after events', async () => {
441
+ const { weflayr_setup, weflayr_instrument } = freshSDK();
442
+
443
+ weflayr_setup({
444
+ ...TEST_CREDS,
445
+ default_tags: { app: 'my-app', version: '1.2.0' },
446
+ methods: [{ call: 'chat.completions.create' }],
447
+ });
448
+
449
+ const client = weflayr_instrument(fakeClient());
450
+ const sent = await captureHttp(async () => {
451
+ await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
452
+ });
453
+
454
+ const before = sent.find(e => e.event_type === 'before');
455
+ assert.equal(before.tags.app, 'my-app');
456
+ assert.equal(before.tags.version, '1.2.0');
457
+ const after = sent.find(e => e.event_type === 'after');
458
+ assert.equal(after.tags.app, 'my-app');
459
+ assert.equal(after.tags.version, '1.2.0');
460
+ });
461
+
462
+ test('weflayr_propagate adds a tag to before and after events', async () => {
463
+ const { weflayr_setup, weflayr_instrument, weflayr_propagate } = freshSDK();
464
+
465
+ weflayr_setup({
466
+ ...TEST_CREDS,
467
+ methods: [{ call: 'chat.completions.create' }],
468
+ });
469
+
470
+ weflayr_propagate('customer_id', 'acme-corp');
471
+
472
+ const client = weflayr_instrument(fakeClient());
473
+ const sent = await captureHttp(async () => {
474
+ await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
475
+ });
476
+
477
+ const before = sent.find(e => e.event_type === 'before');
478
+ assert.equal(before.tags.customer_id, 'acme-corp');
479
+ const after = sent.find(e => e.event_type === 'after');
480
+ assert.equal(after.tags.customer_id, 'acme-corp');
481
+ });
482
+
483
+ test('tag priority: __weflayr_tags > weflayr_propagate > default_tags', async () => {
484
+ const { weflayr_setup, weflayr_instrument, weflayr_propagate } = freshSDK();
485
+
486
+ weflayr_setup({
487
+ ...TEST_CREDS,
488
+ default_tags: { env: 'production', app: 'my-app' },
489
+ methods: [{ call: 'chat.completions.create' }],
490
+ });
491
+
492
+ weflayr_propagate('env', 'staging'); // overrides default_tags.env
493
+
494
+ const client = weflayr_instrument(fakeClient());
495
+ const sent = await captureHttp(async () => {
496
+ await client.chat.completions.create({
497
+ model: 'gpt-4o-mini',
498
+ messages: [],
499
+ __weflayr_tags: { env: 'test' }, // overrides both
500
+ });
501
+ });
502
+
503
+ const before = sent.find(e => e.event_type === 'before');
504
+ assert.equal(before.tags.env, 'test'); // per-call wins
505
+ assert.equal(before.tags.app, 'my-app'); // from default_tags, untouched
506
+ });
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // Propagate — documentation snippets
510
+ // ---------------------------------------------------------------------------
511
+
512
+ test('snippet propagate_default_tags: default_tags in every event', async () => {
513
+ freshSDK();
514
+ setSnippetEnv();
515
+
516
+ const sent = await captureHttp(async () => {
517
+ // SNIPPET_START: propagate_default_tags
518
+ const { weflayr_setup, weflayr_instrument } = require('weflayr');
519
+
520
+ weflayr_setup({
521
+ intake_url: process.env.WEFLAYR_INTAKE_URL,
522
+ client_id: process.env.WEFLAYR_CLIENT_ID,
523
+ client_secret: process.env.WEFLAYR_CLIENT_SECRET,
524
+ default_tags: {
525
+ app: 'my-app',
526
+ version: '1.2.0',
527
+ env: 'production',
528
+ },
529
+ methods: [{ call: 'chat.completions.create' }],
530
+ });
531
+
532
+ const OpenAI = require('openai');
533
+ const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
534
+ // SNIPPET_END: propagate_default_tags
535
+
536
+ await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
537
+ });
538
+
539
+ const before = sent.find(e => e.event_type === 'before');
540
+ assert.ok(before, 'before event sent');
541
+ assert.equal(before.tags.app, 'my-app');
542
+ assert.equal(before.tags.version, '1.2.0');
543
+ assert.equal(before.tags.env, 'production');
544
+ const after = sent.find(e => e.event_type === 'after');
545
+ assert.ok(after, 'after event sent');
546
+ assert.equal(after.tags.app, 'my-app');
547
+ });
548
+
549
+ test('snippet propagate_runtime: propagated tag in before and after', async () => {
550
+ const { weflayr_setup, weflayr_instrument, weflayr_propagate } = freshSDK();
551
+ setSnippetEnv();
552
+
553
+ let capturedArgs;
554
+ const raw = {
555
+ chat: { completions: { create: async (args) => { capturedArgs = args; return { id: 'res-1' }; } } },
556
+ };
557
+ weflayr_setup({ ...TEST_CREDS, methods: [{ call: 'chat.completions.create' }] });
558
+ const client = weflayr_instrument(raw);
559
+
560
+ const sent = await captureHttp(async () => {
561
+ // SNIPPET_START: propagate_runtime
562
+ const { weflayr_propagate } = require('weflayr');
563
+
564
+ // In a request handler — tags every LLM call made during this execution
565
+ weflayr_propagate('customer_id', 'acme-corp');
566
+
567
+ await client.chat.completions.create({
568
+ model: 'gpt-4o-mini',
569
+ messages: [{ role: 'user', content: 'Hello!' }],
570
+ });
571
+ // SNIPPET_END: propagate_runtime
572
+ });
573
+
574
+ assert.ok(!('__weflayr_tags' in capturedArgs), 'no tags leaked to provider');
575
+ const before = sent.find(e => e.event_type === 'before');
576
+ assert.ok(before, 'before event sent');
577
+ assert.equal(before.tags.customer_id, 'acme-corp');
578
+ const after = sent.find(e => e.event_type === 'after');
579
+ assert.ok(after, 'after event sent');
580
+ assert.equal(after.tags.customer_id, 'acme-corp');
581
+ });
582
+