@wytness/sdk 0.5.0 → 0.7.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/dist/index.cjs +86 -24
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +86 -24
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -163,7 +163,7 @@ async function sendOne(apiUrl, apiKey, body) {
|
|
|
163
163
|
body,
|
|
164
164
|
signal: AbortSignal.timeout(1e4)
|
|
165
165
|
});
|
|
166
|
-
if (resp.status !== 201) {
|
|
166
|
+
if (resp.status !== 201 && resp.status !== 202) {
|
|
167
167
|
throw new Error(`HTTP ${resp.status}`);
|
|
168
168
|
}
|
|
169
169
|
return true;
|
|
@@ -284,7 +284,7 @@ var SessionTokenizer = class {
|
|
|
284
284
|
pseudonymizeValue(value, fieldPath = "") {
|
|
285
285
|
if (!value || !value.trim())
|
|
286
286
|
return value;
|
|
287
|
-
if (this.
|
|
287
|
+
if (this.matchPiiField(fieldPath)) {
|
|
288
288
|
if (this._reverseMap[value])
|
|
289
289
|
return this._reverseMap[value];
|
|
290
290
|
const pseudonym = hmacPseudonym(value, this._hmacSecret);
|
|
@@ -293,6 +293,19 @@ var SessionTokenizer = class {
|
|
|
293
293
|
}
|
|
294
294
|
return value;
|
|
295
295
|
}
|
|
296
|
+
matchPiiField(path) {
|
|
297
|
+
if (!path)
|
|
298
|
+
return false;
|
|
299
|
+
if (this._piiFields.includes(path))
|
|
300
|
+
return true;
|
|
301
|
+
const stripped = path.replace(/\[\d+\]/g, "");
|
|
302
|
+
if (this._piiFields.includes(stripped))
|
|
303
|
+
return true;
|
|
304
|
+
const wildcarded = path.replace(/\[\d+\]/g, "[]");
|
|
305
|
+
if (this._piiFields.includes(wildcarded))
|
|
306
|
+
return true;
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
296
309
|
pseudonymizeText(text) {
|
|
297
310
|
if (!text)
|
|
298
311
|
return text;
|
|
@@ -316,20 +329,46 @@ var SessionTokenizer = class {
|
|
|
316
329
|
}
|
|
317
330
|
return text;
|
|
318
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Pseudonymize tool parameters, preserving nested structure.
|
|
334
|
+
*
|
|
335
|
+
* Walks objects and arrays recursively. Keys whose names match
|
|
336
|
+
* SECRET_PATTERN are replaced with "[REDACTED]". Primitive string values
|
|
337
|
+
* at paths listed in piiFields are fully pseudonymized; other strings are
|
|
338
|
+
* scanned with regex patterns.
|
|
339
|
+
*
|
|
340
|
+
* Paths use dot notation ("fields.abn") and [index] for arrays
|
|
341
|
+
* ("entities[0].name"). A piiFields entry matches by exact path, by
|
|
342
|
+
* index-stripped path ("entities.name"), or wildcard ("entities[].name").
|
|
343
|
+
*/
|
|
319
344
|
pseudonymizeParams(params) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
345
|
+
return this.walk(params, "");
|
|
346
|
+
}
|
|
347
|
+
walk(value, path) {
|
|
348
|
+
if (value === null || value === void 0)
|
|
349
|
+
return value;
|
|
350
|
+
if (typeof value === "boolean" || typeof value === "number")
|
|
351
|
+
return value;
|
|
352
|
+
if (Array.isArray(value)) {
|
|
353
|
+
return value.map((item, i) => this.walk(item, `${path}[${i}]`));
|
|
354
|
+
}
|
|
355
|
+
if (typeof value === "object") {
|
|
356
|
+
const out = {};
|
|
357
|
+
for (const [k, v] of Object.entries(value)) {
|
|
358
|
+
if (SECRET_PATTERN.test(k)) {
|
|
359
|
+
out[k] = "[REDACTED]";
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const childPath = path ? `${path}.${k}` : k;
|
|
363
|
+
out[k] = this.walk(v, childPath);
|
|
330
364
|
}
|
|
365
|
+
return out;
|
|
331
366
|
}
|
|
332
|
-
|
|
367
|
+
const text = String(value).slice(0, 500);
|
|
368
|
+
if (this.matchPiiField(path)) {
|
|
369
|
+
return this.pseudonymizeValue(text, path);
|
|
370
|
+
}
|
|
371
|
+
return this.pseudonymizeText(text);
|
|
333
372
|
}
|
|
334
373
|
/**
|
|
335
374
|
* Encrypt the token map with the customer's X25519 public key.
|
|
@@ -430,10 +469,21 @@ var AuditClient = class {
|
|
|
430
469
|
get tokenizer() {
|
|
431
470
|
return this._tokenizer;
|
|
432
471
|
}
|
|
433
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* Record an audit event. Never throws — errors are logged and swallowed.
|
|
474
|
+
*
|
|
475
|
+
* If the client was configured with pseudonymization (piiHmacSecret +
|
|
476
|
+
* encryptionPublicKey), the event is automatically pseudonymized before
|
|
477
|
+
* signing — tool_parameters and response/prompt are walked and declared
|
|
478
|
+
* piiFields (supports dotted paths for nested keys) are replaced with
|
|
479
|
+
* deterministic tokens. The encrypted token map is attached.
|
|
480
|
+
*/
|
|
434
481
|
record(event) {
|
|
435
482
|
try {
|
|
436
|
-
|
|
483
|
+
let d = { ...event };
|
|
484
|
+
if (this._tokenizer) {
|
|
485
|
+
d = this.applyTokenizer(d);
|
|
486
|
+
}
|
|
437
487
|
d["prev_event_hash"] = this._prevEventHash;
|
|
438
488
|
d["signature"] = signEvent(d, this._secretKey);
|
|
439
489
|
this._prevEventHash = computeEventHash(d);
|
|
@@ -442,6 +492,22 @@ var AuditClient = class {
|
|
|
442
492
|
console.error(`wytness: failed to record event: ${e}`);
|
|
443
493
|
}
|
|
444
494
|
}
|
|
495
|
+
applyTokenizer(d) {
|
|
496
|
+
const tokenizer = this._tokenizer;
|
|
497
|
+
const params = d["tool_parameters"] ?? {};
|
|
498
|
+
d["tool_parameters"] = tokenizer.pseudonymizeParams(params);
|
|
499
|
+
const prompt = d["prompt"];
|
|
500
|
+
if (prompt) {
|
|
501
|
+
d["prompt"] = tokenizer.pseudonymizeText(String(prompt));
|
|
502
|
+
}
|
|
503
|
+
const response = d["response"];
|
|
504
|
+
if (response) {
|
|
505
|
+
d["response"] = tokenizer.pseudonymizeText(String(response).slice(0, 5e3));
|
|
506
|
+
}
|
|
507
|
+
d["encrypted_token_map"] = tokenizer.encryptTokenMap();
|
|
508
|
+
d["pseudonymization_version"] = d["pseudonymization_version"] || "1.0";
|
|
509
|
+
return d;
|
|
510
|
+
}
|
|
445
511
|
/**
|
|
446
512
|
* Wait for all pending HTTP requests to complete.
|
|
447
513
|
* Call this before process exit in serverless/short-lived environments
|
|
@@ -492,9 +558,7 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
|
|
|
492
558
|
const outputsHash = result != null ? hashValue(result) : "";
|
|
493
559
|
const tokenizer = client.tokenizer;
|
|
494
560
|
if (tokenizer) {
|
|
495
|
-
const
|
|
496
|
-
const pseudonymizedPrompt = prompt ? tokenizer.pseudonymizeText(prompt) : "";
|
|
497
|
-
const response = result != null ? tokenizer.pseudonymizeText(String(result).slice(0, RESPONSE_MAX_CHARS)) : "";
|
|
561
|
+
const responseRaw = result != null ? String(result).slice(0, RESPONSE_MAX_CHARS) : "";
|
|
498
562
|
const event = AuditEventSchema.parse({
|
|
499
563
|
agent_id: client.agentId,
|
|
500
564
|
agent_version: client.agentVersion,
|
|
@@ -502,16 +566,14 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
|
|
|
502
566
|
task_id: taskId,
|
|
503
567
|
session_id: client.sessionId,
|
|
504
568
|
tool_name: toolName,
|
|
505
|
-
tool_parameters:
|
|
506
|
-
prompt
|
|
569
|
+
tool_parameters: params,
|
|
570
|
+
prompt,
|
|
507
571
|
inputs_hash: hashValue(args),
|
|
508
572
|
outputs_hash: outputsHash,
|
|
509
|
-
response,
|
|
573
|
+
response: responseRaw,
|
|
510
574
|
status,
|
|
511
575
|
error_code: errorCode,
|
|
512
|
-
duration_ms: Math.round(performance.now() - start)
|
|
513
|
-
encrypted_token_map: tokenizer.encryptTokenMap(),
|
|
514
|
-
pseudonymization_version: "1.0"
|
|
576
|
+
duration_ms: Math.round(performance.now() - start)
|
|
515
577
|
});
|
|
516
578
|
client.record(event);
|
|
517
579
|
} else {
|
package/dist/index.d.cts
CHANGED
|
@@ -134,8 +134,22 @@ declare class SessionTokenizer {
|
|
|
134
134
|
get tokenMap(): Record<string, string>;
|
|
135
135
|
private addMapping;
|
|
136
136
|
pseudonymizeValue(value: string, fieldPath?: string): string;
|
|
137
|
+
private matchPiiField;
|
|
137
138
|
pseudonymizeText(text: string): string;
|
|
138
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Pseudonymize tool parameters, preserving nested structure.
|
|
141
|
+
*
|
|
142
|
+
* Walks objects and arrays recursively. Keys whose names match
|
|
143
|
+
* SECRET_PATTERN are replaced with "[REDACTED]". Primitive string values
|
|
144
|
+
* at paths listed in piiFields are fully pseudonymized; other strings are
|
|
145
|
+
* scanned with regex patterns.
|
|
146
|
+
*
|
|
147
|
+
* Paths use dot notation ("fields.abn") and [index] for arrays
|
|
148
|
+
* ("entities[0].name"). A piiFields entry matches by exact path, by
|
|
149
|
+
* index-stripped path ("entities.name"), or wildcard ("entities[].name").
|
|
150
|
+
*/
|
|
151
|
+
pseudonymizeParams(params: unknown): unknown;
|
|
152
|
+
private walk;
|
|
139
153
|
/**
|
|
140
154
|
* Encrypt the token map with the customer's X25519 public key.
|
|
141
155
|
*
|
|
@@ -178,8 +192,17 @@ declare class AuditClient {
|
|
|
178
192
|
constructor(options: AuditClientOptions);
|
|
179
193
|
get sessionId(): string;
|
|
180
194
|
get tokenizer(): SessionTokenizer | null;
|
|
181
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Record an audit event. Never throws — errors are logged and swallowed.
|
|
197
|
+
*
|
|
198
|
+
* If the client was configured with pseudonymization (piiHmacSecret +
|
|
199
|
+
* encryptionPublicKey), the event is automatically pseudonymized before
|
|
200
|
+
* signing — tool_parameters and response/prompt are walked and declared
|
|
201
|
+
* piiFields (supports dotted paths for nested keys) are replaced with
|
|
202
|
+
* deterministic tokens. The encrypted token map is attached.
|
|
203
|
+
*/
|
|
182
204
|
record(event: AuditEvent): void;
|
|
205
|
+
private applyTokenizer;
|
|
183
206
|
/**
|
|
184
207
|
* Wait for all pending HTTP requests to complete.
|
|
185
208
|
* Call this before process exit in serverless/short-lived environments
|
package/dist/index.d.ts
CHANGED
|
@@ -134,8 +134,22 @@ declare class SessionTokenizer {
|
|
|
134
134
|
get tokenMap(): Record<string, string>;
|
|
135
135
|
private addMapping;
|
|
136
136
|
pseudonymizeValue(value: string, fieldPath?: string): string;
|
|
137
|
+
private matchPiiField;
|
|
137
138
|
pseudonymizeText(text: string): string;
|
|
138
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Pseudonymize tool parameters, preserving nested structure.
|
|
141
|
+
*
|
|
142
|
+
* Walks objects and arrays recursively. Keys whose names match
|
|
143
|
+
* SECRET_PATTERN are replaced with "[REDACTED]". Primitive string values
|
|
144
|
+
* at paths listed in piiFields are fully pseudonymized; other strings are
|
|
145
|
+
* scanned with regex patterns.
|
|
146
|
+
*
|
|
147
|
+
* Paths use dot notation ("fields.abn") and [index] for arrays
|
|
148
|
+
* ("entities[0].name"). A piiFields entry matches by exact path, by
|
|
149
|
+
* index-stripped path ("entities.name"), or wildcard ("entities[].name").
|
|
150
|
+
*/
|
|
151
|
+
pseudonymizeParams(params: unknown): unknown;
|
|
152
|
+
private walk;
|
|
139
153
|
/**
|
|
140
154
|
* Encrypt the token map with the customer's X25519 public key.
|
|
141
155
|
*
|
|
@@ -178,8 +192,17 @@ declare class AuditClient {
|
|
|
178
192
|
constructor(options: AuditClientOptions);
|
|
179
193
|
get sessionId(): string;
|
|
180
194
|
get tokenizer(): SessionTokenizer | null;
|
|
181
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Record an audit event. Never throws — errors are logged and swallowed.
|
|
197
|
+
*
|
|
198
|
+
* If the client was configured with pseudonymization (piiHmacSecret +
|
|
199
|
+
* encryptionPublicKey), the event is automatically pseudonymized before
|
|
200
|
+
* signing — tool_parameters and response/prompt are walked and declared
|
|
201
|
+
* piiFields (supports dotted paths for nested keys) are replaced with
|
|
202
|
+
* deterministic tokens. The encrypted token map is attached.
|
|
203
|
+
*/
|
|
182
204
|
record(event: AuditEvent): void;
|
|
205
|
+
private applyTokenizer;
|
|
183
206
|
/**
|
|
184
207
|
* Wait for all pending HTTP requests to complete.
|
|
185
208
|
* Call this before process exit in serverless/short-lived environments
|
package/dist/index.js
CHANGED
|
@@ -115,7 +115,7 @@ async function sendOne(apiUrl, apiKey, body) {
|
|
|
115
115
|
body,
|
|
116
116
|
signal: AbortSignal.timeout(1e4)
|
|
117
117
|
});
|
|
118
|
-
if (resp.status !== 201) {
|
|
118
|
+
if (resp.status !== 201 && resp.status !== 202) {
|
|
119
119
|
throw new Error(`HTTP ${resp.status}`);
|
|
120
120
|
}
|
|
121
121
|
return true;
|
|
@@ -236,7 +236,7 @@ var SessionTokenizer = class {
|
|
|
236
236
|
pseudonymizeValue(value, fieldPath = "") {
|
|
237
237
|
if (!value || !value.trim())
|
|
238
238
|
return value;
|
|
239
|
-
if (this.
|
|
239
|
+
if (this.matchPiiField(fieldPath)) {
|
|
240
240
|
if (this._reverseMap[value])
|
|
241
241
|
return this._reverseMap[value];
|
|
242
242
|
const pseudonym = hmacPseudonym(value, this._hmacSecret);
|
|
@@ -245,6 +245,19 @@ var SessionTokenizer = class {
|
|
|
245
245
|
}
|
|
246
246
|
return value;
|
|
247
247
|
}
|
|
248
|
+
matchPiiField(path) {
|
|
249
|
+
if (!path)
|
|
250
|
+
return false;
|
|
251
|
+
if (this._piiFields.includes(path))
|
|
252
|
+
return true;
|
|
253
|
+
const stripped = path.replace(/\[\d+\]/g, "");
|
|
254
|
+
if (this._piiFields.includes(stripped))
|
|
255
|
+
return true;
|
|
256
|
+
const wildcarded = path.replace(/\[\d+\]/g, "[]");
|
|
257
|
+
if (this._piiFields.includes(wildcarded))
|
|
258
|
+
return true;
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
248
261
|
pseudonymizeText(text) {
|
|
249
262
|
if (!text)
|
|
250
263
|
return text;
|
|
@@ -268,20 +281,46 @@ var SessionTokenizer = class {
|
|
|
268
281
|
}
|
|
269
282
|
return text;
|
|
270
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Pseudonymize tool parameters, preserving nested structure.
|
|
286
|
+
*
|
|
287
|
+
* Walks objects and arrays recursively. Keys whose names match
|
|
288
|
+
* SECRET_PATTERN are replaced with "[REDACTED]". Primitive string values
|
|
289
|
+
* at paths listed in piiFields are fully pseudonymized; other strings are
|
|
290
|
+
* scanned with regex patterns.
|
|
291
|
+
*
|
|
292
|
+
* Paths use dot notation ("fields.abn") and [index] for arrays
|
|
293
|
+
* ("entities[0].name"). A piiFields entry matches by exact path, by
|
|
294
|
+
* index-stripped path ("entities.name"), or wildcard ("entities[].name").
|
|
295
|
+
*/
|
|
271
296
|
pseudonymizeParams(params) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
297
|
+
return this.walk(params, "");
|
|
298
|
+
}
|
|
299
|
+
walk(value, path) {
|
|
300
|
+
if (value === null || value === void 0)
|
|
301
|
+
return value;
|
|
302
|
+
if (typeof value === "boolean" || typeof value === "number")
|
|
303
|
+
return value;
|
|
304
|
+
if (Array.isArray(value)) {
|
|
305
|
+
return value.map((item, i) => this.walk(item, `${path}[${i}]`));
|
|
306
|
+
}
|
|
307
|
+
if (typeof value === "object") {
|
|
308
|
+
const out = {};
|
|
309
|
+
for (const [k, v] of Object.entries(value)) {
|
|
310
|
+
if (SECRET_PATTERN.test(k)) {
|
|
311
|
+
out[k] = "[REDACTED]";
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const childPath = path ? `${path}.${k}` : k;
|
|
315
|
+
out[k] = this.walk(v, childPath);
|
|
282
316
|
}
|
|
317
|
+
return out;
|
|
283
318
|
}
|
|
284
|
-
|
|
319
|
+
const text = String(value).slice(0, 500);
|
|
320
|
+
if (this.matchPiiField(path)) {
|
|
321
|
+
return this.pseudonymizeValue(text, path);
|
|
322
|
+
}
|
|
323
|
+
return this.pseudonymizeText(text);
|
|
285
324
|
}
|
|
286
325
|
/**
|
|
287
326
|
* Encrypt the token map with the customer's X25519 public key.
|
|
@@ -382,10 +421,21 @@ var AuditClient = class {
|
|
|
382
421
|
get tokenizer() {
|
|
383
422
|
return this._tokenizer;
|
|
384
423
|
}
|
|
385
|
-
/**
|
|
424
|
+
/**
|
|
425
|
+
* Record an audit event. Never throws — errors are logged and swallowed.
|
|
426
|
+
*
|
|
427
|
+
* If the client was configured with pseudonymization (piiHmacSecret +
|
|
428
|
+
* encryptionPublicKey), the event is automatically pseudonymized before
|
|
429
|
+
* signing — tool_parameters and response/prompt are walked and declared
|
|
430
|
+
* piiFields (supports dotted paths for nested keys) are replaced with
|
|
431
|
+
* deterministic tokens. The encrypted token map is attached.
|
|
432
|
+
*/
|
|
386
433
|
record(event) {
|
|
387
434
|
try {
|
|
388
|
-
|
|
435
|
+
let d = { ...event };
|
|
436
|
+
if (this._tokenizer) {
|
|
437
|
+
d = this.applyTokenizer(d);
|
|
438
|
+
}
|
|
389
439
|
d["prev_event_hash"] = this._prevEventHash;
|
|
390
440
|
d["signature"] = signEvent(d, this._secretKey);
|
|
391
441
|
this._prevEventHash = computeEventHash(d);
|
|
@@ -394,6 +444,22 @@ var AuditClient = class {
|
|
|
394
444
|
console.error(`wytness: failed to record event: ${e}`);
|
|
395
445
|
}
|
|
396
446
|
}
|
|
447
|
+
applyTokenizer(d) {
|
|
448
|
+
const tokenizer = this._tokenizer;
|
|
449
|
+
const params = d["tool_parameters"] ?? {};
|
|
450
|
+
d["tool_parameters"] = tokenizer.pseudonymizeParams(params);
|
|
451
|
+
const prompt = d["prompt"];
|
|
452
|
+
if (prompt) {
|
|
453
|
+
d["prompt"] = tokenizer.pseudonymizeText(String(prompt));
|
|
454
|
+
}
|
|
455
|
+
const response = d["response"];
|
|
456
|
+
if (response) {
|
|
457
|
+
d["response"] = tokenizer.pseudonymizeText(String(response).slice(0, 5e3));
|
|
458
|
+
}
|
|
459
|
+
d["encrypted_token_map"] = tokenizer.encryptTokenMap();
|
|
460
|
+
d["pseudonymization_version"] = d["pseudonymization_version"] || "1.0";
|
|
461
|
+
return d;
|
|
462
|
+
}
|
|
397
463
|
/**
|
|
398
464
|
* Wait for all pending HTTP requests to complete.
|
|
399
465
|
* Call this before process exit in serverless/short-lived environments
|
|
@@ -444,9 +510,7 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
|
|
|
444
510
|
const outputsHash = result != null ? hashValue(result) : "";
|
|
445
511
|
const tokenizer = client.tokenizer;
|
|
446
512
|
if (tokenizer) {
|
|
447
|
-
const
|
|
448
|
-
const pseudonymizedPrompt = prompt ? tokenizer.pseudonymizeText(prompt) : "";
|
|
449
|
-
const response = result != null ? tokenizer.pseudonymizeText(String(result).slice(0, RESPONSE_MAX_CHARS)) : "";
|
|
513
|
+
const responseRaw = result != null ? String(result).slice(0, RESPONSE_MAX_CHARS) : "";
|
|
450
514
|
const event = AuditEventSchema.parse({
|
|
451
515
|
agent_id: client.agentId,
|
|
452
516
|
agent_version: client.agentVersion,
|
|
@@ -454,16 +518,14 @@ function recordEvent(client, toolName, taskId, prompt, args, start, status, erro
|
|
|
454
518
|
task_id: taskId,
|
|
455
519
|
session_id: client.sessionId,
|
|
456
520
|
tool_name: toolName,
|
|
457
|
-
tool_parameters:
|
|
458
|
-
prompt
|
|
521
|
+
tool_parameters: params,
|
|
522
|
+
prompt,
|
|
459
523
|
inputs_hash: hashValue(args),
|
|
460
524
|
outputs_hash: outputsHash,
|
|
461
|
-
response,
|
|
525
|
+
response: responseRaw,
|
|
462
526
|
status,
|
|
463
527
|
error_code: errorCode,
|
|
464
|
-
duration_ms: Math.round(performance.now() - start)
|
|
465
|
-
encrypted_token_map: tokenizer.encryptTokenMap(),
|
|
466
|
-
pseudonymization_version: "1.0"
|
|
528
|
+
duration_ms: Math.round(performance.now() - start)
|
|
467
529
|
});
|
|
468
530
|
client.record(event);
|
|
469
531
|
} else {
|
package/package.json
CHANGED