@narumitw/pi-langfuse 0.15.0 → 0.15.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.
Files changed (2) hide show
  1. package/package.json +2 -1
  2. package/src/tracing.ts +43 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-langfuse",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Pi extension that traces LLM generations and tool activity to Langfuse.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  "@langfuse/otel": "^4.0.0",
33
33
  "@langfuse/tracing": "^4.0.0",
34
34
  "@opentelemetry/api": "^1.9.0",
35
+ "@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
35
36
  "@opentelemetry/sdk-trace-base": "^2.9.0",
36
37
  "@opentelemetry/sdk-trace-node": "^2.9.0"
37
38
  },
package/src/tracing.ts CHANGED
@@ -358,9 +358,9 @@ function sanitize(
358
358
  return consume(value, budget);
359
359
  }
360
360
  if (typeof value === "string") {
361
- const redacted = value.replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
362
- const bounded = truncateString(redacted, Math.min(MAX_STRING_LENGTH, budget.remaining));
363
- return consume(bounded, budget);
361
+ const bounded = truncateString(value, Math.min(MAX_STRING_LENGTH, budget.remaining));
362
+ const redacted = bounded.replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
363
+ return consume(redacted, budget);
364
364
  }
365
365
  if (typeof value === "bigint") return consume(value.toString(), budget);
366
366
  if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
@@ -387,30 +387,46 @@ function sanitize(
387
387
  result = items;
388
388
  } else {
389
389
  const record = value as Record<string, unknown>;
390
- let keys: string[];
391
- try {
392
- keys = Object.keys(record);
393
- } catch {
394
- active.delete(value);
395
- return consume("[unreadable object]", budget);
396
- }
397
390
  const output: Record<string, unknown> = {};
398
391
  const objectType = readProperty(record, "type").value;
399
392
  const redactData = objectType === "image" || objectType === "base64";
393
+ let visited = 0;
400
394
  let processed = 0;
401
- for (const key of keys.slice(0, MAX_COLLECTION_LENGTH)) {
402
- if (budget.remaining <= byteLength(TRUNCATED)) break;
403
- budget.remaining -= byteLength(key) + 4;
404
- const property = readProperty(record, key);
405
- if (redactData && key === "data") {
406
- output[key] = consume("[base64 omitted]", budget);
407
- } else if (!property.ok) output[key] = consume("[unreadable property]", budget);
408
- else output[key] = sanitize(property.value, active, depth + 1, budget);
409
- processed += 1;
410
- }
411
- if (processed < keys.length) {
412
- output["$truncated"] = `${keys.length - processed} object entries omitted`;
395
+ let omitted = false;
396
+ try {
397
+ for (const key in record) {
398
+ if (visited >= MAX_COLLECTION_LENGTH || budget.remaining <= byteLength(TRUNCATED)) {
399
+ omitted = true;
400
+ break;
401
+ }
402
+ visited += 1;
403
+ const keyBudget = Math.min(MAX_STRING_LENGTH, Math.max(0, budget.remaining - 4));
404
+ if (key.length > keyBudget) {
405
+ omitted = true;
406
+ break;
407
+ }
408
+ const keyBytes = byteLength(key);
409
+ if (keyBytes > keyBudget) {
410
+ omitted = true;
411
+ break;
412
+ }
413
+ budget.remaining -= keyBytes + 4;
414
+ if (!Object.hasOwn(record, key)) continue;
415
+ const property = readProperty(record, key);
416
+ if (redactData && key === "data") {
417
+ output[key] = consume("[base64 omitted]", budget);
418
+ } else if (!property.ok) output[key] = consume("[unreadable property]", budget);
419
+ else output[key] = sanitize(property.value, active, depth + 1, budget);
420
+ processed += 1;
421
+ }
422
+ } catch {
423
+ if (processed === 0) {
424
+ active.delete(value);
425
+ return consume("[unreadable object]", budget);
426
+ }
427
+ omitted = true;
413
428
  }
429
+ if (omitted) output["$truncated"] = "additional object entries omitted";
414
430
  result = output;
415
431
  }
416
432
  active.delete(value);
@@ -439,12 +455,15 @@ function consume<T>(value: T, budget: { remaining: number }): T | string {
439
455
  }
440
456
 
441
457
  function truncateString(value: string, maxBytes: number): string {
442
- if (byteLength(value) <= maxBytes) return value;
458
+ const boundedPrefix = value.slice(0, maxBytes);
459
+ if (boundedPrefix.length === value.length && byteLength(boundedPrefix) <= maxBytes) {
460
+ return boundedPrefix;
461
+ }
443
462
  const suffix = "… [truncated]";
444
463
  const target = Math.max(0, maxBytes - byteLength(suffix) - 2);
445
464
  let bytes = 0;
446
465
  let output = "";
447
- for (const character of value) {
466
+ for (const character of boundedPrefix) {
448
467
  const size = byteLength(character);
449
468
  if (bytes + size > target) break;
450
469
  output += character;