weflayr 0.3.0 → 0.3.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Weflayr Node.js SDK
2
2
 
3
- Observability for Node.js - instrument any client or method with one line.
3
+ Observability for Node.js - instrument any LLM client or function with one line.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,33 +8,23 @@ Observability for Node.js - instrument any client or method with one line.
8
8
  npm install weflayr
9
9
  ```
10
10
 
11
- ## Environment variables
12
-
13
- ```bash
14
- WEFLAYR_INTAKE_URL=https://api.weflayr.com
15
- WEFLAYR_CLIENT_ID=your-client-id-uuid
16
- WEFLAYR_CLIENT_SECRET=your-client-secret
17
- ```
18
-
19
- ## Usage
11
+ ## Quick start
20
12
 
21
13
  ```js
22
14
  const { weflayr_setup, weflayr_instrument } = require('weflayr');
23
15
 
24
16
  weflayr_setup({
25
- event_mode: 'default', // 'default' | 'light' (light skips before events)
26
- content_policy: 'ignore', // 'ignore' removes ignore_fields | 'allow' keeps only allow_fields
27
- ignore_fields: ['messages[].content', 'choices[].message.content'],
28
- allow_fields: [],
17
+ intake_url: 'https://api.weflayr.com',
18
+ client_id: 'your-client-id-uuid',
19
+ client_secret: 'your-client-secret',
29
20
  methods: [
30
- { call: 'chat.completions.create' }
21
+ { call: 'chat.completions.create' },
31
22
  ],
32
- }, { enabled: true });
23
+ });
33
24
 
34
25
  const OpenAI = require('openai');
35
26
  const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
36
27
 
37
- // Tags are stripped from args before the API call and attached to the event
38
28
  const response = await client.chat.completions.create({
39
29
  model: 'gpt-4o-mini',
40
30
  messages: [{ role: 'user', content: 'Hello' }],
@@ -42,6 +32,108 @@ const response = await client.chat.completions.create({
42
32
  });
43
33
  ```
44
34
 
35
+ Credentials are typically read from environment variables via `dotenv`:
36
+
37
+ ```js
38
+ weflayr_setup({
39
+ intake_url: process.env.WEFLAYR_INTAKE_URL,
40
+ client_id: process.env.WEFLAYR_CLIENT_ID,
41
+ client_secret: process.env.WEFLAYR_CLIENT_SECRET,
42
+ methods: [...],
43
+ });
44
+ ```
45
+
46
+ ```bash
47
+ # .env
48
+ WEFLAYR_INTAKE_URL=https://api.weflayr.com
49
+ WEFLAYR_CLIENT_ID=your-client-id-uuid
50
+ WEFLAYR_CLIENT_SECRET=your-client-secret
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Settings reference
56
+
57
+ | Field | Type | Required | Description |
58
+ |---|---|---|---|
59
+ | `intake_url` | `string` | ✓ | Base URL of the Weflayr intake API |
60
+ | `client_id` | `string` | ✓ | UUID identifying your Flare credential pair |
61
+ | `client_secret` | `string` | ✓ | Bearer token used to authenticate events |
62
+ | `event_mode` | `'default'` \| `'light'` | | `light` skips `before` events. Default: `'default'` |
63
+ | `enabled` | `boolean` | | Set to `false` to disable instrumentation entirely. Default: `true` |
64
+ | `ignore_fields` | `function` | | Middleware to strip sensitive fields from event payloads. Mutually exclusive with `allow_fields`. |
65
+ | `allow_fields` | `function` | | Middleware to keep only approved fields in event payloads. Mutually exclusive with `ignore_fields`. |
66
+ | `methods` | `MethodConfig[]` | | Methods to instrument on the proxied object |
67
+
68
+ ---
69
+
70
+ ## Filtering event payloads
71
+
72
+ `ignore_fields` and `allow_fields` are **middleware functions**, not field lists. Each receives a deep clone of the event payload (request args or response body) and returns the filtered version. The original args forwarded to the real provider call are never affected.
73
+
74
+ Only one may be set. Setting both logs a warning and blocks all events.
75
+
76
+ ### ignore_fields strip specific fields
77
+
78
+ ```js
79
+ weflayr_setup({
80
+ // ...
81
+ ignore_fields: (data) => {
82
+ (data.messages ?? []).forEach(m => delete m.content);
83
+ (data.choices ?? []).forEach(c => { if (c.message) delete c.message.content; });
84
+ return data;
85
+ },
86
+ methods: [{ call: 'chat.completions.create' }],
87
+ });
88
+ ```
89
+
90
+ ### allow_fields keep only specific fields
91
+
92
+ ```js
93
+ weflayr_setup({
94
+ // ...
95
+ allow_fields: (data) => ({
96
+ model: data.model,
97
+ usage: data.usage,
98
+ }),
99
+ methods: [{ call: 'chat.completions.create' }],
100
+ });
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Metadata tags
106
+
107
+ Pass `__weflayr_tags` on any instrumented call to attach arbitrary key-value metadata to the event. Tags are stripped before the real provider call is made.
108
+
109
+ ```js
110
+ await client.chat.completions.create({
111
+ model: 'gpt-4o-mini',
112
+ messages: [...],
113
+ __weflayr_tags: { feature: 'summarise', customer_id: '42' },
114
+ });
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Middleware
120
+
121
+ Middleware extracts structured fields that are merged into the event payload. `response` is `null` for `before` events.
122
+
123
+ ```js
124
+ methods: [
125
+ {
126
+ call: 'audio.speech.create',
127
+ middleware: (args, response) => ({
128
+ char_count: args?.input?.length ?? 0,
129
+ result_count: response?.data?.length ?? 0,
130
+ }),
131
+ },
132
+ ],
133
+ ```
134
+
135
+ ---
136
+
45
137
  ## Instrumenting a plain function
46
138
 
47
139
  ```js
@@ -51,42 +143,83 @@ async function fetchModels() {
51
143
  }
52
144
 
53
145
  weflayr_setup({
54
- event_mode: 'default',
55
- content_policy: 'ignore',
56
- ignore_fields: ['data'],
146
+ // ...
57
147
  methods: [
58
148
  {
59
149
  call: 'fetchModels',
60
150
  middleware: (_args, response) => ({ count: response?.data?.length ?? 0 }),
61
151
  },
62
152
  ],
63
- }, { enabled: true });
153
+ });
64
154
 
65
155
  const instrumented = weflayr_instrument(fetchModels);
66
156
  const models = await instrumented({ __weflayr_tags: { app: 'my-app' } });
67
157
  ```
68
158
 
69
- ## Settings reference
159
+ ---
70
160
 
71
- | Field | Type | Description |
72
- |---|---|---|
73
- | `event_mode` | `'default'` \| `'light'` | `light` skips before events |
74
- | `content_policy` | `'ignore'` \| `'allow'` | Which filtering mode to apply |
75
- | `ignore_fields` | `string[]` | Fields removed from payloads (used when `content_policy: 'ignore'`) |
76
- | `allow_fields` | `string[]` | Fields kept in payloads; everything else removed (used when `content_policy: 'allow'`) |
77
- | `methods` | `{ call, middleware? }[]` | Whitelisted method paths to instrument |
161
+ ## Disabling instrumentation
78
162
 
79
- Field paths support dot notation (`foo.bar`), and array traversal (`messages[].content`).
163
+ ```js
164
+ weflayr_setup({
165
+ // ...
166
+ enabled: false,
167
+ });
168
+ ```
80
169
 
81
- ## Middleware
170
+ `weflayr_instrument` returns the original object untouched when `enabled` is `false`.
82
171
 
83
- Middleware runs on both **before** and **after** events. `response` is `null` for before events.
172
+ ---
84
173
 
85
- ```js
86
- middleware: (args, response) => ({
87
- char_count: args?.input?.length ?? 0,
88
- result_count: response?.data?.length ?? 0,
89
- })
174
+ ## TypeScript
175
+
176
+ The SDK ships with full type declarations. No additional `@types` package is needed.
177
+
178
+ ```ts
179
+ import { weflayr_setup, weflayr_instrument, flayred } from 'weflayr';
180
+ import OpenAI from 'openai';
181
+
182
+ weflayr_setup({
183
+ intake_url: process.env.WEFLAYR_INTAKE_URL!,
184
+ client_id: process.env.WEFLAYR_CLIENT_ID!,
185
+ client_secret: process.env.WEFLAYR_CLIENT_SECRET!,
186
+ methods: [{ call: 'chat.completions.create' }],
187
+ });
188
+
189
+ const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
90
190
  ```
91
191
 
92
- The returned object is merged into the event payload.
192
+ ### Adding tags in TypeScript
193
+
194
+ Because `__weflayr_tags` is not part of the provider SDK's types, use the `flayred` helper to attach tags without casting:
195
+
196
+ ```ts
197
+ const response = await client.chat.completions.create(flayred({
198
+ model: 'gpt-4o-mini',
199
+ messages: [{ role: 'user', content: 'Hello' }],
200
+ __weflayr_tags: { feature: 'chat', customer_id: '42' },
201
+ }));
202
+ ```
203
+
204
+ `flayred<T>(options: T & { __weflayr_tags? }): T` TypeScript sees the return type as `T`, so it is fully compatible with the provider SDK's expected parameter type.
205
+
206
+ ### Typing middleware
207
+
208
+ Cast inside the middleware body the SDK is provider-agnostic so it cannot infer the concrete response type:
209
+
210
+ ```ts
211
+ import OpenAI from 'openai';
212
+
213
+ methods: [
214
+ {
215
+ call: 'chat.completions.create',
216
+ middleware: (_args, response) => {
217
+ const r = response as OpenAI.ChatCompletion | null;
218
+ return {
219
+ prompt_tokens: r?.usage?.prompt_tokens ?? 0,
220
+ completion_tokens: r?.usage?.completion_tokens ?? 0,
221
+ };
222
+ },
223
+ },
224
+ ],
225
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,97 @@
1
+ /** Arbitrary key-value metadata attached to an LLM call. */
2
+ export type Tags = Record<string, string | number | boolean>;
3
+
4
+ /**
5
+ * Receives a deep clone of the request args or response, mutates or replaces
6
+ * fields, and returns the filtered object sent to the intake API.
7
+ */
8
+ export type ContentPolicyFn = (data: Record<string, unknown>) => Record<string, unknown>;
9
+
10
+ /**
11
+ * Extracts extra structured fields merged into the intake event.
12
+ * Called with `(args, null)` for the `before` event and `(args, result)` for `after`.
13
+ */
14
+ export type MiddlewareFn = (
15
+ args: Record<string, unknown>,
16
+ response: Record<string, unknown> | null,
17
+ ) => Record<string, unknown>;
18
+
19
+ /** Per-chunk accumulator returned by a `streamMiddleware` factory. */
20
+ export interface StreamAccumulator {
21
+ /** Return `true` to emit a `stream_pending` event immediately. */
22
+ onChunk: (chunk: unknown) => boolean;
23
+ /** Called once the stream ends; returns extra fields for the `after` event. */
24
+ finalize: () => Record<string, unknown>;
25
+ }
26
+
27
+ /** Factory called once per stream invocation. */
28
+ export type StreamMiddlewareFn = () => StreamAccumulator;
29
+
30
+ /** Instrumentation configuration for a single method path. */
31
+ export interface MethodConfig {
32
+ /**
33
+ * Dot-separated path to the method on the instrumented object
34
+ * (e.g. `'chat.completions.create'`), or the function name for bare functions.
35
+ */
36
+ call: string;
37
+ middleware?: MiddlewareFn;
38
+ streamMiddleware?: StreamMiddlewareFn;
39
+ }
40
+
41
+ /** Full configuration object passed to `weflayr_setup`. */
42
+ export interface WeflayrSettings {
43
+ /** Base URL of the Weflayr intake API (e.g. `'https://api.weflayr.com'`). */
44
+ intake_url: string;
45
+ /** UUID identifying the Flare credential pair. */
46
+ client_id: string;
47
+ /** Bearer token used when calling the intake API. */
48
+ client_secret: string;
49
+ /** `'default'` emits before + after events; `'light'` emits only after. */
50
+ event_mode?: 'default' | 'light';
51
+ /** Set to `false` to disable instrumentation entirely. Defaults to `true`. */
52
+ enabled?: boolean;
53
+ /**
54
+ * Middleware to strip sensitive fields from event payloads before sending to the intake API.
55
+ * Mutually exclusive with `allow_fields` — setting both blocks all events.
56
+ */
57
+ ignore_fields?: ContentPolicyFn;
58
+ /**
59
+ * Middleware to keep only approved fields in event payloads before sending to the intake API.
60
+ * Mutually exclusive with `ignore_fields` — setting both blocks all events.
61
+ */
62
+ allow_fields?: ContentPolicyFn;
63
+ /** Methods to instrument on the proxied object. */
64
+ methods?: MethodConfig[];
65
+ }
66
+
67
+ /** Configures weflayr with your credentials and instrumentation settings. */
68
+ export function weflayr_setup(settings: WeflayrSettings): void;
69
+
70
+ /**
71
+ * Wraps an LLM client object or async function so matching method calls emit
72
+ * events to the intake API. Returns the original value unchanged when weflayr
73
+ * is disabled or the target has no matching method config.
74
+ */
75
+ export function weflayr_instrument<T>(target: T): T;
76
+
77
+ /** Sends a custom event to the intake API from user code. No-ops if `weflayr_setup` hasn't been called. */
78
+ export function send_event(
79
+ eventType: string,
80
+ data: Record<string, unknown>,
81
+ ): Promise<void> | undefined;
82
+
83
+ /**
84
+ * Attaches weflayr metadata tags to any SDK call options object.
85
+ *
86
+ * `__weflayr_tags` is stripped by the instrumented proxy before the real provider call
87
+ * is made, so it never reaches the underlying SDK. At runtime this is the identity
88
+ * function — the generic signature is its only purpose.
89
+ *
90
+ * @example
91
+ * await openai.chat.completions.create(flayred({
92
+ * model: 'gpt-4o-mini',
93
+ * messages: [...],
94
+ * __weflayr_tags: { feature: 'haiku', customer_id: '42' },
95
+ * }));
96
+ */
97
+ export function flayred<T>(options: T & { __weflayr_tags?: Record<string, string | number | boolean> }): T;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Weflayr Node.js SDK — instrument any LLM client via JS Proxy",
5
5
  "main": "src/index.js",
6
+ "types": "index.d.ts",
6
7
  "scripts": {
7
8
  "test": "node --test tests/index.test.js"
8
9
  },
@@ -12,7 +13,5 @@
12
13
  "engines": {
13
14
  "node": ">=18.0.0"
14
15
  },
15
- "dependencies": {
16
- "dotenv": "^16.0.0"
17
- }
16
+ "dependencies": {}
18
17
  }
package/src/index.js CHANGED
@@ -1,29 +1,35 @@
1
1
  'use strict';
2
2
 
3
- require('dotenv').config();
4
-
5
3
  const { configure, weflayr_instrument, send_event } = require('./instrument');
6
4
 
7
5
  /**
8
- *
6
+ *
9
7
  * @param {*} settings: settings for weflayr
10
- * @param {*} on/off: easily enable or disable weflayr
11
- * @returns
8
+ * @returns
12
9
  */
13
- function weflayr_setup(settings, { enabled = true } = {}) {
14
- if (!enabled) return;
10
+ function weflayr_setup(settings) {
11
+ if (settings.enabled === false) return;
15
12
 
16
- const intakeUrl = process.env.WEFLAYR_INTAKE_URL;
17
- const clientId = process.env.WEFLAYR_CLIENT_ID;
18
- const clientSecret = process.env.WEFLAYR_CLIENT_SECRET;
13
+ const { intake_url, client_id, client_secret } = settings;
19
14
 
20
- if (!intakeUrl || !clientId || !clientSecret) {
15
+ if (!intake_url || !client_id || !client_secret) {
21
16
  throw new Error(
22
- 'Weflayr: WEFLAYR_INTAKE_URL, WEFLAYR_CLIENT_ID and WEFLAYR_CLIENT_SECRET must be set'
17
+ 'Weflayr: intake_url, client_id and client_secret must be set in settings'
23
18
  );
24
19
  }
25
20
 
26
- configure(intakeUrl, clientId, clientSecret, settings);
21
+ configure(intake_url, client_id, client_secret, settings);
22
+ }
23
+
24
+ /**
25
+ * Attaches weflayr metadata tags to any SDK call options object.
26
+ *
27
+ * @template T
28
+ * @param {T & { __weflayr_tags?: Record<string, string | number | boolean> }} options
29
+ * @returns {T}
30
+ */
31
+ function flayred(options) {
32
+ return options;
27
33
  }
28
34
 
29
- module.exports = { weflayr_setup, weflayr_instrument, send_event };
35
+ module.exports = { weflayr_setup, weflayr_instrument, send_event, flayred };
package/src/instrument.js CHANGED
@@ -7,7 +7,12 @@ const { randomUUID } = require('crypto');
7
7
  let _config = null;
8
8
 
9
9
  function configure(intakeUrl, clientId, clientSecret, settings) {
10
- _config = { intakeUrl, clientId, clientSecret, settings };
10
+ if (settings.ignore_fields && settings.allow_fields) {
11
+ console.warn('[weflayr] both ignore_fields and allow_fields are set - no events will be sent to the intake API');
12
+ _config = { intakeUrl, clientId, clientSecret, settings, _blocked: true };
13
+ return;
14
+ }
15
+ _config = { intakeUrl, clientId, clientSecret, settings, _blocked: false };
11
16
  }
12
17
 
13
18
  function weflayr_instrument(target) {
@@ -185,73 +190,29 @@ function _toPlain(value) {
185
190
 
186
191
  function _applyContentPolicy(data, settings) {
187
192
  if (!data) return data;
188
-
193
+ const fn = settings.ignore_fields || settings.allow_fields;
194
+ if (!fn) return data;
189
195
  let clone;
190
196
  try {
191
197
  clone = JSON.parse(JSON.stringify(data));
192
198
  } catch {
193
199
  return data;
194
200
  }
195
-
196
- if (settings.content_policy === 'ignore') {
197
- for (const field of (settings.ignore_fields || [])) {
198
- _deleteField(clone, field);
199
- }
201
+ try {
202
+ return fn(clone) ?? clone;
203
+ } catch (err) {
204
+ console.warn('[weflayr] content policy function error:', err.message);
200
205
  return clone;
201
206
  }
202
-
203
- return _pickFields(clone, settings.allow_fields || []);
204
- }
205
-
206
- function _pickFields(obj, allowFields) {
207
- if (!obj || typeof obj !== 'object' || allowFields.length === 0) return obj;
208
-
209
- const result = {};
210
- for (const field of allowFields) {
211
- const arrayMatch = field.match(/^(\w+)\[\]\.(.+)$/);
212
- if (arrayMatch) {
213
- const [, arrayKey, rest] = arrayMatch;
214
- if (Array.isArray(obj[arrayKey])) {
215
- result[arrayKey] = obj[arrayKey].map(item => _pickFields(item, [rest]));
216
- }
217
- continue;
218
- }
219
-
220
- const dotIdx = field.indexOf('.');
221
- if (dotIdx === -1) {
222
- if (field in obj) result[field] = obj[field];
223
- } else {
224
- const key = field.slice(0, dotIdx);
225
- const rest = field.slice(dotIdx + 1);
226
- if (key in obj) result[key] = _pickFields(obj[key], [rest]);
227
- }
228
- }
229
- return result;
230
- }
231
-
232
- function _deleteField(obj, path) {
233
- if (!obj || typeof obj !== 'object') return;
234
-
235
- const arrayMatch = path.match(/^(\w+)\[\]\.(.+)$/);
236
- if (arrayMatch) {
237
- const [, arrayKey, rest] = arrayMatch;
238
- if (Array.isArray(obj[arrayKey])) {
239
- obj[arrayKey].forEach(item => _deleteField(item, rest));
240
- }
241
- return;
242
- }
243
-
244
- const dotIdx = path.indexOf('.');
245
- if (dotIdx === -1) {
246
- delete obj[path];
247
- } else {
248
- _deleteField(obj[path.slice(0, dotIdx)], path.slice(dotIdx + 1));
249
- }
250
207
  }
251
208
 
252
209
  const _debug = process.env.WEFLAYR_DEBUG === 'true';
253
210
 
254
211
  function _sendEvent(eventType, data) {
212
+ if (_config._blocked) {
213
+ console.warn(`[weflayr] is blocked and can't send event, ensure your configuration is valid`);
214
+ return;
215
+ }
255
216
  const { intakeUrl, clientId, clientSecret } = _config;
256
217
  const endpoint = `${intakeUrl.replace(/\/$/, '')}/${clientId}/`;
257
218
 
@@ -3,13 +3,16 @@
3
3
  const { test } = require('node:test');
4
4
  const assert = require('assert/strict');
5
5
 
6
+ const TEST_CREDS = {
7
+ intake_url: 'http://localhost:19999', // nothing listening — fire-and-forget is fine
8
+ client_id: 'test-client-id',
9
+ client_secret: 'test-secret',
10
+ };
11
+
6
12
  // Reset module cache between tests so _config doesn't leak
7
13
  function freshSDK() {
8
14
  delete require.cache[require.resolve('../src/instrument')];
9
15
  delete require.cache[require.resolve('../src/index')];
10
- process.env.WEFLAYR_INTAKE_URL = 'http://localhost:19999'; // nothing listening — fire-and-forget is fine
11
- process.env.WEFLAYR_CLIENT_ID = 'test-client-id';
12
- process.env.WEFLAYR_CLIENT_SECRET = 'test-secret';
13
16
  return require('../src/index');
14
17
  }
15
18
 
@@ -25,16 +28,16 @@ function fakeClient() {
25
28
  }
26
29
 
27
30
  // ---------------------------------------------------------------------------
28
- // Missing options don't crash
31
+ // Content policy function-based middleware
29
32
  // ---------------------------------------------------------------------------
30
33
 
31
- test('missing ignore_fields does not crash', async () => {
34
+ test('ignore_fields function filters event data', async () => {
32
35
  const { weflayr_setup, weflayr_instrument } = freshSDK();
33
36
 
34
37
  weflayr_setup({
38
+ ...TEST_CREDS,
35
39
  event_mode: 'default',
36
- content_policy: 'ignore',
37
- // ignore_fields intentionally omitted
40
+ ignore_fields: (data) => { delete data.messages; return data; },
38
41
  methods: [{ call: 'chat.completions.create' }],
39
42
  });
40
43
 
@@ -44,13 +47,13 @@ test('missing ignore_fields does not crash', async () => {
44
47
  assert.equal(result.id, 'res-1');
45
48
  });
46
49
 
47
- test('missing allow_fields does not crash', async () => {
50
+ test('allow_fields function filters event data', async () => {
48
51
  const { weflayr_setup, weflayr_instrument } = freshSDK();
49
52
 
50
53
  weflayr_setup({
54
+ ...TEST_CREDS,
51
55
  event_mode: 'default',
52
- content_policy: 'allow',
53
- // allow_fields intentionally omitted
56
+ allow_fields: (data) => ({ model: data.model }),
54
57
  methods: [{ call: 'chat.completions.create' }],
55
58
  });
56
59
 
@@ -60,13 +63,15 @@ test('missing allow_fields does not crash', async () => {
60
63
  assert.equal(result.id, 'res-1');
61
64
  });
62
65
 
63
- test('missing methods does not crash on object instrumentation', async () => {
66
+ test('both ignore_fields and allow_fields set no events sent, function still works', async () => {
64
67
  const { weflayr_setup, weflayr_instrument } = freshSDK();
65
68
 
66
69
  weflayr_setup({
70
+ ...TEST_CREDS,
67
71
  event_mode: 'default',
68
- content_policy: 'ignore',
69
- // methods intentionally omitted
72
+ ignore_fields: (data) => data,
73
+ allow_fields: (data) => data,
74
+ methods: [{ call: 'chat.completions.create' }],
70
75
  });
71
76
 
72
77
  const client = weflayr_instrument(fakeClient());
@@ -75,49 +80,55 @@ test('missing methods does not crash on object instrumentation', async () => {
75
80
  assert.equal(result.id, 'res-1');
76
81
  });
77
82
 
78
- test('missing methods does not crash on function instrumentation', async () => {
83
+ test('no content policy pass-through, function still works', async () => {
79
84
  const { weflayr_setup, weflayr_instrument } = freshSDK();
80
85
 
81
86
  weflayr_setup({
87
+ ...TEST_CREDS,
82
88
  event_mode: 'default',
83
- content_policy: 'ignore',
84
- // methods intentionally omitted
89
+ methods: [{ call: 'chat.completions.create' }],
85
90
  });
86
91
 
87
- async function myFn(x) { return x * 2; }
88
- const instrumented = weflayr_instrument(myFn);
92
+ const client = weflayr_instrument(fakeClient());
93
+ const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
89
94
 
90
- assert.equal(await instrumented(5), 10);
95
+ assert.equal(result.id, 'res-1');
91
96
  });
92
97
 
93
- // ---------------------------------------------------------------------------
94
- // enabled: false — target passes through unchanged, calls still work
95
- // ---------------------------------------------------------------------------
96
-
97
- test('enabled:false returns the original object untouched', () => {
98
+ test('ignore_fields does not mutate actual call args', async () => {
98
99
  const { weflayr_setup, weflayr_instrument } = freshSDK();
99
100
 
100
- weflayr_setup({ methods: [{ call: 'chat.completions.create' }] }, { enabled: false });
101
-
102
- const client = fakeClient();
103
- const result = weflayr_instrument(client);
104
-
105
- assert.strictEqual(result, client);
106
- });
101
+ let capturedArgs;
102
+ const client = {
103
+ chat: {
104
+ completions: {
105
+ create: async (args) => { capturedArgs = args; return { id: 'res-1' }; },
106
+ },
107
+ },
108
+ };
107
109
 
108
- test('enabled:false returns the original function untouched', () => {
109
- const { weflayr_setup, weflayr_instrument } = freshSDK();
110
+ weflayr_setup({
111
+ ...TEST_CREDS,
112
+ event_mode: 'default',
113
+ ignore_fields: (data) => { delete data.messages; return data; },
114
+ methods: [{ call: 'chat.completions.create' }],
115
+ });
110
116
 
111
- async function myFn(x) { return x * 2; }
112
- weflayr_setup({ methods: [{ call: 'myFn' }] }, { enabled: false });
117
+ const instrumented = weflayr_instrument(client);
118
+ await instrumented.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hello' }] });
113
119
 
114
- assert.strictEqual(weflayr_instrument(myFn), myFn);
120
+ assert.ok(Array.isArray(capturedArgs.messages));
121
+ assert.equal(capturedArgs.messages[0].content, 'hello');
115
122
  });
116
123
 
117
- test('enabled:false original function still executes correctly', async () => {
124
+ test('missing methods does not crash on object instrumentation', async () => {
118
125
  const { weflayr_setup, weflayr_instrument } = freshSDK();
119
126
 
120
- weflayr_setup({ methods: [{ call: 'chat.completions.create' }] }, { enabled: false });
127
+ weflayr_setup({
128
+ ...TEST_CREDS,
129
+ event_mode: 'default',
130
+ // methods intentionally omitted
131
+ });
121
132
 
122
133
  const client = weflayr_instrument(fakeClient());
123
134
  const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
@@ -125,67 +136,53 @@ test('enabled:false — original function still executes correctly', async () =>
125
136
  assert.equal(result.id, 'res-1');
126
137
  });
127
138
 
128
- // ---------------------------------------------------------------------------
129
- // Invalid content_policy falls back to allow — function still works correctly
130
- // ---------------------------------------------------------------------------
131
-
132
- test('invalid content_policy does not crash', async () => {
139
+ test('missing methods does not crash on function instrumentation', async () => {
133
140
  const { weflayr_setup, weflayr_instrument } = freshSDK();
134
141
 
135
142
  weflayr_setup({
143
+ ...TEST_CREDS,
136
144
  event_mode: 'default',
137
- content_policy: 'not_a_real_policy',
138
- allow_fields: [],
139
- methods: [{ call: 'chat.completions.create' }],
145
+ // methods intentionally omitted
140
146
  });
141
147
 
142
- const client = weflayr_instrument(fakeClient());
143
- const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
148
+ async function myFn(x) { return x * 2; }
149
+ const instrumented = weflayr_instrument(myFn);
144
150
 
145
- assert.equal(result.id, 'res-1');
151
+ assert.equal(await instrumented(5), 10);
146
152
  });
147
153
 
148
- test('invalid content_policy does not apply ignore_fields', async () => {
154
+ // ---------------------------------------------------------------------------
155
+ // enabled: false — target passes through unchanged, calls still work
156
+ // ---------------------------------------------------------------------------
157
+
158
+ test('enabled:false returns the original object untouched', () => {
149
159
  const { weflayr_setup, weflayr_instrument } = freshSDK();
150
160
 
151
- let capturedArgs;
152
- const client = {
153
- chat: {
154
- completions: {
155
- create: async (args) => { capturedArgs = args; return { id: 'res-1' }; },
156
- },
157
- },
158
- };
161
+ weflayr_setup({ methods: [{ call: 'chat.completions.create' }] , enabled: false });
159
162
 
160
- weflayr_setup({
161
- event_mode: 'default',
162
- content_policy: 'not_a_real_policy',
163
- ignore_fields: ['messages'],
164
- allow_fields: [],
165
- methods: [{ call: 'chat.completions.create' }],
166
- });
163
+ const client = fakeClient();
164
+ const result = weflayr_instrument(client);
167
165
 
168
- const instrumented = weflayr_instrument(client);
169
- await instrumented.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hello' }] });
166
+ assert.strictEqual(result, client);
167
+ });
170
168
 
171
- // ignore_fields must NOT have been applied — the real call receives the original args
172
- assert.ok(Array.isArray(capturedArgs.messages));
173
- assert.equal(capturedArgs.messages[0].content, 'hello');
169
+ test('enabled:false returns the original function untouched', () => {
170
+ const { weflayr_setup, weflayr_instrument } = freshSDK();
171
+
172
+ async function myFn(x) { return x * 2; }
173
+ weflayr_setup({ methods: [{ call: 'myFn' }] , enabled: false });
174
+
175
+ assert.strictEqual(weflayr_instrument(myFn), myFn);
174
176
  });
175
177
 
176
- test('undefined content_policy falls back to allow (pass-through)', async () => {
178
+ test('enabled:false original function still executes correctly', async () => {
177
179
  const { weflayr_setup, weflayr_instrument } = freshSDK();
178
180
 
179
- weflayr_setup({
180
- event_mode: 'default',
181
- // content_policy intentionally omitted
182
- ignore_fields: ['messages'],
183
- allow_fields: [],
184
- methods: [{ call: 'chat.completions.create' }],
185
- });
181
+ weflayr_setup({ methods: [{ call: 'chat.completions.create' }] , enabled: false });
186
182
 
187
183
  const client = weflayr_instrument(fakeClient());
188
184
  const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
189
185
 
190
186
  assert.equal(result.id, 'res-1');
191
187
  });
188
+