weflayr 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.0",
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
@@ -7,6 +7,7 @@ const { version: SDK_VERSION } = require('../package.json');
7
7
  const SDK_LANGUAGE = 'javascript';
8
8
 
9
9
  let _config = null;
10
+ let _propagated = {};
10
11
 
11
12
  function configure(intakeUrl, clientId, clientSecret, settings) {
12
13
  if (settings.ignore_fields && settings.allow_fields) {
@@ -173,12 +174,12 @@ async function* _wrapStream(stream, methodName, tags, requestArgs, settings, sta
173
174
  }
174
175
 
175
176
  function _extractTags(args) {
176
- let tags = {};
177
+ let callTags = {};
177
178
 
178
179
  function strip(obj) {
179
180
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return;
180
181
  if ('__weflayr_tags' in obj) {
181
- tags = obj.__weflayr_tags || {};
182
+ callTags = obj.__weflayr_tags || {};
182
183
  delete obj.__weflayr_tags;
183
184
  }
184
185
  for (const key of Object.keys(obj)) {
@@ -191,9 +192,19 @@ function _extractTags(args) {
191
192
  return arg;
192
193
  });
193
194
 
195
+ const tags = {
196
+ ...(_config.settings.default_tags || {}),
197
+ ..._propagated,
198
+ ...callTags,
199
+ };
200
+
194
201
  return { tags, cleanArgs };
195
202
  }
196
203
 
204
+ function weflayr_propagate(key, value) {
205
+ _propagated[key] = value;
206
+ }
207
+
197
208
  function _isAsyncIterable(value) {
198
209
  return value != null && typeof value[Symbol.asyncIterator] === 'function';
199
210
  }
@@ -297,4 +308,4 @@ function send_event(eventType, data) {
297
308
  return _sendEvent(eventType, data);
298
309
  }
299
310
 
300
- module.exports = { configure, weflayr_instrument, send_event };
311
+ 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
+