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 +13 -0
- package/package.json +1 -1
- package/src/index.js +2 -2
- package/src/instrument.js +17 -3
- package/tests/index.test.js +153 -0
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
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
|
|
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
|
-
|
|
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 };
|
package/tests/index.test.js
CHANGED
|
@@ -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
|
+
|