@autter/runtime-node 0.1.0 → 1.1.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.js CHANGED
@@ -198,8 +198,19 @@ function createBrowserRelayHandler(opts) {
198
198
  }
199
199
 
200
200
  // src/server.ts
201
- import { trace, SpanStatusCode } from "@opentelemetry/api";
202
- import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
201
+ import { ServerResponse } from "http";
202
+ import {
203
+ context,
204
+ trace,
205
+ SpanKind,
206
+ SpanStatusCode,
207
+ TraceFlags
208
+ } from "@opentelemetry/api";
209
+ import { getRPCMetadata, RPCType } from "@opentelemetry/core";
210
+ import {
211
+ AggregationTemporalityPreference,
212
+ OTLPMetricExporter
213
+ } from "@opentelemetry/exporter-metrics-otlp-http";
203
214
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
204
215
  import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
205
216
  import { Resource } from "@opentelemetry/resources";
@@ -210,13 +221,748 @@ import {
210
221
  ParentBasedSampler,
211
222
  TraceIdRatioBasedSampler,
212
223
  AlwaysOnSampler,
213
- BasicTracerProvider
224
+ BasicTracerProvider,
225
+ SamplingDecision
214
226
  } from "@opentelemetry/sdk-trace-base";
215
227
  import {
216
228
  ATTR_SERVICE_NAME,
217
229
  ATTR_SERVICE_VERSION
218
230
  } from "@opentelemetry/semantic-conventions";
231
+
232
+ // src/lifecycle.ts
233
+ import { ExportResultCode } from "@opentelemetry/core";
234
+ var debugEnabled = process.env.AUTTER_DEBUG === "1" || process.env.AUTTER_DEBUG === "true";
235
+ function setDebugMode(enabled) {
236
+ debugEnabled = enabled;
237
+ }
238
+ function isDebugEnabled() {
239
+ return debugEnabled;
240
+ }
241
+ function debugLog(...args) {
242
+ if (debugEnabled) {
243
+ console.error("[autter]", ...args);
244
+ }
245
+ }
246
+ var TelemetryStats = class {
247
+ /** Captures queued via our APIs and not yet seen leaving an exporter. */
248
+ pendingCaptures = 0;
249
+ markCaptured(count = 1) {
250
+ this.pendingCaptures += count;
251
+ }
252
+ /** An exporter batch left the process successfully. */
253
+ onExported(spanCount) {
254
+ this.pendingCaptures = Math.max(0, this.pendingCaptures - spanCount);
255
+ if (debugEnabled && spanCount > 0) {
256
+ debugLog(`exported ${spanCount} span${spanCount === 1 ? "" : "s"}`);
257
+ }
258
+ }
259
+ markAllFlushed() {
260
+ this.pendingCaptures = 0;
261
+ }
262
+ };
263
+ var telemetryStats = new TelemetryStats();
264
+ var CountingExporter = class {
265
+ constructor(inner) {
266
+ this.inner = inner;
267
+ }
268
+ inner;
269
+ export(spans, resultCallback) {
270
+ this.inner.export(spans, (result) => {
271
+ if (result.code === ExportResultCode.SUCCESS) {
272
+ telemetryStats.onExported(spans.length);
273
+ }
274
+ resultCallback(result);
275
+ });
276
+ }
277
+ shutdown() {
278
+ return this.inner.shutdown();
279
+ }
280
+ forceFlush() {
281
+ return this.inner.forceFlush?.() ?? Promise.resolve();
282
+ }
283
+ };
284
+ var flushTargets = /* @__PURE__ */ new Map();
285
+ function registerFlushTarget(key, target) {
286
+ flushTargets.set(key, target);
287
+ }
288
+ function unregisterFlushTargets() {
289
+ flushTargets.clear();
290
+ }
291
+ function activeFlushTargets() {
292
+ return [...flushTargets.values()];
293
+ }
294
+ var DEFAULT_SIGNALS = ["SIGINT", "SIGTERM"];
295
+ var SIGNAL_EXIT_CODES = {
296
+ SIGINT: 130,
297
+ SIGTERM: 143
298
+ };
299
+ function installAutterAutoFlush(options = {}) {
300
+ const logger = options.logger ?? console;
301
+ const log = options.log !== false;
302
+ const warnOnUnflushedExit = options.warnOnUnflushedExit !== false;
303
+ const timeoutMs = Math.max(100, options.timeoutMs ?? 3e3);
304
+ const signals = options.signals ?? DEFAULT_SIGNALS;
305
+ let disposed = false;
306
+ let inFlight = null;
307
+ const doFlush = async () => {
308
+ const targets = options.targets ?? activeFlushTargets();
309
+ if (targets.length === 0) return true;
310
+ let timer;
311
+ const timedOut = new Promise((resolve) => {
312
+ timer = setTimeout(() => resolve(false), timeoutMs);
313
+ });
314
+ const drained = Promise.allSettled(
315
+ targets.map(
316
+ (target) => Promise.resolve().then(() => target.forceFlush())
317
+ )
318
+ ).then(
319
+ (results) => results.every((result) => result.status === "fulfilled")
320
+ );
321
+ let ok;
322
+ try {
323
+ ok = await Promise.race([drained, timedOut]);
324
+ } finally {
325
+ clearTimeout(timer);
326
+ }
327
+ if (ok) {
328
+ telemetryStats.markAllFlushed();
329
+ if (log) {
330
+ logger.log("[autter] telemetry flushed");
331
+ }
332
+ } else {
333
+ logger.error(
334
+ `[autter] flush did not finish within ${timeoutMs}ms \u2014 some telemetry may be lost`
335
+ );
336
+ }
337
+ return ok;
338
+ };
339
+ const flush = (reason = "exit") => {
340
+ if (disposed) return Promise.resolve(false);
341
+ if (!inFlight) {
342
+ inFlight = doFlush().finally(() => {
343
+ inFlight = null;
344
+ });
345
+ }
346
+ if (isDebugEnabled()) debugLog(`auto-flush requested on ${reason}`);
347
+ return inFlight;
348
+ };
349
+ const signalCounts = /* @__PURE__ */ new Map();
350
+ const onSignal = (signal) => {
351
+ const count = (signalCounts.get(signal) ?? 0) + 1;
352
+ signalCounts.set(signal, count);
353
+ const exitCode = SIGNAL_EXIT_CODES[signal] ?? 1;
354
+ if (count > 1) {
355
+ process.exit(exitCode);
356
+ }
357
+ const otherHandlers = process.listenerCount(signal) - 1;
358
+ if (otherHandlers > 0) {
359
+ void flush(signal);
360
+ return;
361
+ }
362
+ void flush(signal).finally(() => {
363
+ process.exit(exitCode);
364
+ });
365
+ };
366
+ const MAX_BEFORE_EXIT_FLUSHES = 10;
367
+ let beforeExitFlushes = 0;
368
+ const onBeforeExit = () => {
369
+ if (disposed) return;
370
+ const targets = options.targets ?? activeFlushTargets();
371
+ const worthIt = beforeExitFlushes === 0 ? targets.length > 0 : telemetryStats.pendingCaptures > 0;
372
+ if (!worthIt || beforeExitFlushes >= MAX_BEFORE_EXIT_FLUSHES) return;
373
+ beforeExitFlushes++;
374
+ const keepAlive = setTimeout(() => {
375
+ }, timeoutMs);
376
+ void flush("beforeExit").finally(() => clearTimeout(keepAlive));
377
+ };
378
+ const onExit = () => {
379
+ if (!warnOnUnflushedExit || disposed) return;
380
+ const pending = telemetryStats.pendingCaptures;
381
+ if (pending > 0) {
382
+ logger.error(
383
+ `[autter] process exiting with ~${pending} captured item(s) never confirmed exported \u2014 await autter.shutdown() on graceful shutdown`
384
+ );
385
+ }
386
+ };
387
+ const signalHandlers = /* @__PURE__ */ new Map();
388
+ process.on("beforeExit", onBeforeExit);
389
+ for (const signal of signals) {
390
+ const handler = () => onSignal(signal);
391
+ signalHandlers.set(signal, handler);
392
+ process.on(signal, handler);
393
+ }
394
+ process.on("exit", onExit);
395
+ return {
396
+ flush,
397
+ dispose() {
398
+ disposed = true;
399
+ process.removeListener("beforeExit", onBeforeExit);
400
+ process.removeListener("exit", onExit);
401
+ for (const [signal, handler] of signalHandlers) {
402
+ process.removeListener(signal, handler);
403
+ }
404
+ signalHandlers.clear();
405
+ }
406
+ };
407
+ }
408
+
409
+ // src/redact.ts
410
+ var SENSITIVE_KEY_PATTERNS = [
411
+ /e-?mail/,
412
+ /pass(word|wd|phrase)|^pass$/,
413
+ /token/,
414
+ /secret/,
415
+ /credential/,
416
+ /(api|access|secret|private|consumer|client|signing|encryption)-?[_.]?key/,
417
+ /^-?x?-?authorization$|^auth(-|_|$)|bearer/,
418
+ /cookie/,
419
+ /phone|msisdn/,
420
+ /\bssn\b|social[-_ ]?security/,
421
+ /cvv|cvc|card([-_. ]?(number|num|no))?$/,
422
+ /credit[-_.]?card/,
423
+ /connection[-_.]?string/,
424
+ /recovery[-_.]?code|\botp\b|magic[-_.]?link/
425
+ ];
426
+ var PRIVATE_KEY_BLOCK_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g;
427
+ var EMAIL_VALUE_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
428
+ var JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
429
+ var OPENAI_KEY_RE = /\bsk-[A-Za-z0-9]{20,}\b/g;
430
+ var GITHUB_TOKEN_RE = /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g;
431
+ var AWS_KEY_RE = /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g;
432
+ var SLACK_TOKEN_RE = /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g;
433
+ var BEARER_RE = /\bbearer\s+[A-Za-z0-9._~+/=-]{10,}/gi;
434
+ var URL_CREDENTIALS_RE = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
435
+ function toCaseInsensitive(pattern) {
436
+ return typeof pattern === "string" ? new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(pattern.source, pattern.flags.includes("i") ? pattern.flags : `${pattern.flags}i`);
437
+ }
438
+ function compile(options) {
439
+ const keyPatterns = [...SENSITIVE_KEY_PATTERNS];
440
+ const valuePatterns = [
441
+ JWT_RE,
442
+ OPENAI_KEY_RE,
443
+ GITHUB_TOKEN_RE,
444
+ AWS_KEY_RE,
445
+ SLACK_TOKEN_RE,
446
+ BEARER_RE
447
+ ];
448
+ if (options?.additionalKeyPatterns) {
449
+ for (const p of options.additionalKeyPatterns) {
450
+ keyPatterns.push(toCaseInsensitive(p));
451
+ }
452
+ }
453
+ if (options?.additionalValuePatterns) {
454
+ for (const p of options.additionalValuePatterns) {
455
+ if (!valuePatterns.some((existing) => existing.source === (typeof p === "string" ? p : p.source))) {
456
+ valuePatterns.push(typeof p === "string" ? new RegExp(p, "gi") : p);
457
+ }
458
+ }
459
+ }
460
+ return {
461
+ keyPatterns,
462
+ valuePatterns,
463
+ urlCredentials: URL_CREDENTIALS_RE,
464
+ privateKeyBlock: PRIVATE_KEY_BLOCK_RE,
465
+ mask: options?.mask ?? "[redacted]",
466
+ scrubEmailValues: options?.scrubEmailValues !== false
467
+ };
468
+ }
469
+ function redactString(value, r) {
470
+ let out = value.replace(r.privateKeyBlock, r.mask);
471
+ for (const re of r.valuePatterns) {
472
+ out = out.replace(re, r.mask);
473
+ }
474
+ out = out.replace(r.urlCredentials, "$1" + r.mask + "@");
475
+ if (r.scrubEmailValues) {
476
+ out = out.replace(EMAIL_VALUE_RE, r.mask);
477
+ }
478
+ return out;
479
+ }
480
+ var MAX_REDACTION_DEPTH = 64;
481
+ var MAX_REDACTION_WORK = 1e4;
482
+ var MAX_COLLECTION_ENTRIES = 1e3;
483
+ function canTraverse(depth, state) {
484
+ if (depth > MAX_REDACTION_DEPTH || state.remainingWork <= 0) return false;
485
+ state.remainingWork -= 1;
486
+ return true;
487
+ }
488
+ function redactValue(value, r, state, depth) {
489
+ if (typeof value === "string") return redactString(value, r);
490
+ let isArray = false;
491
+ try {
492
+ isArray = Array.isArray(value);
493
+ } catch {
494
+ return r.mask;
495
+ }
496
+ if (isArray) {
497
+ return redactArray(value, r, state, depth);
498
+ }
499
+ if (typeof value === "object" && value !== null) {
500
+ return redactObject(value, r, state, depth);
501
+ }
502
+ return value;
503
+ }
504
+ function redactArray(value, r, state, depth) {
505
+ const existing = state.ancestors.get(value);
506
+ if (existing) return r.mask;
507
+ if (!canTraverse(depth, state)) return r.mask;
508
+ const out = [];
509
+ state.ancestors.set(value, out);
510
+ let length;
511
+ try {
512
+ length = value.length;
513
+ } catch {
514
+ state.ancestors.delete(value);
515
+ return r.mask;
516
+ }
517
+ const limit = Math.min(length, MAX_COLLECTION_ENTRIES);
518
+ for (let i = 0; i < limit; i += 1) {
519
+ let item;
520
+ try {
521
+ item = value[i];
522
+ } catch {
523
+ out.push(r.mask);
524
+ continue;
525
+ }
526
+ out.push(redactValue(item, r, state, depth + 1));
527
+ }
528
+ if (length > limit) {
529
+ out.push(r.mask);
530
+ }
531
+ state.ancestors.delete(value);
532
+ return out;
533
+ }
534
+ function redactObject(value, r, state, depth) {
535
+ const existing = state.ancestors.get(value);
536
+ if (existing) return r.mask;
537
+ if (!canTraverse(depth, state)) return r.mask;
538
+ const out = {};
539
+ state.ancestors.set(value, out);
540
+ let count = 0;
541
+ let truncated = false;
542
+ try {
543
+ for (const key in value) {
544
+ if (!Object.prototype.propertyIsEnumerable.call(
545
+ value,
546
+ key
547
+ )) {
548
+ continue;
549
+ }
550
+ if (count >= MAX_COLLECTION_ENTRIES) {
551
+ truncated = true;
552
+ break;
553
+ }
554
+ let nestedValue;
555
+ try {
556
+ nestedValue = value[key];
557
+ } catch {
558
+ out[key] = r.mask;
559
+ count += 1;
560
+ continue;
561
+ }
562
+ count += 1;
563
+ out[key] = isSensitiveKey(key, nestedValue, r) ? r.mask : redactValue(
564
+ nestedValue,
565
+ r,
566
+ state,
567
+ depth + 1
568
+ );
569
+ }
570
+ } catch {
571
+ truncated = true;
572
+ }
573
+ if (truncated) {
574
+ out.__redaction_truncated__ = r.mask;
575
+ }
576
+ state.ancestors.delete(value);
577
+ return out;
578
+ }
579
+ var USAGE_TOKEN_KEYS = /* @__PURE__ */ new Set([
580
+ "input_tokens",
581
+ "output_tokens",
582
+ "prompt_tokens",
583
+ "completion_tokens",
584
+ "total_tokens",
585
+ "token_count",
586
+ "gen_ai.usage.input_tokens",
587
+ "gen_ai.usage.output_tokens",
588
+ "gen_ai.usage.prompt_tokens",
589
+ "gen_ai.usage.completion_tokens",
590
+ "gen_ai.usage.total_tokens",
591
+ "gen_ai.usage.token_count"
592
+ ]);
593
+ function isSensitiveKey(key, value, r) {
594
+ const lowered = key.toLowerCase();
595
+ if (USAGE_TOKEN_KEYS.has(lowered)) {
596
+ return !(typeof value === "number" && Number.isFinite(value) && value >= 0);
597
+ }
598
+ return r.keyPatterns.some((re) => re.test(lowered));
599
+ }
600
+ function redactAttributes(attributes, options) {
601
+ const r = compile(options);
602
+ return redactWith(attributes, r);
603
+ }
604
+ function redactWith(attributes, r) {
605
+ const out = {};
606
+ if (!attributes) return out;
607
+ const state = {
608
+ ancestors: /* @__PURE__ */ new WeakMap(),
609
+ remainingWork: MAX_REDACTION_WORK
610
+ };
611
+ let count = 0;
612
+ let truncated = false;
613
+ try {
614
+ for (const key in attributes) {
615
+ if (!Object.prototype.propertyIsEnumerable.call(
616
+ attributes,
617
+ key
618
+ )) {
619
+ continue;
620
+ }
621
+ if (count >= MAX_COLLECTION_ENTRIES || state.remainingWork <= 0) {
622
+ truncated = true;
623
+ break;
624
+ }
625
+ let value;
626
+ try {
627
+ value = attributes[key];
628
+ } catch {
629
+ out[key] = r.mask;
630
+ count += 1;
631
+ state.remainingWork -= 1;
632
+ continue;
633
+ }
634
+ count += 1;
635
+ state.remainingWork -= 1;
636
+ if (value === void 0) continue;
637
+ out[key] = isSensitiveKey(key, value, r) ? r.mask : redactValue(value, r, state, 0);
638
+ }
639
+ } catch {
640
+ truncated = true;
641
+ }
642
+ if (truncated) {
643
+ out.__redaction_truncated__ = r.mask;
644
+ }
645
+ return out;
646
+ }
647
+ function makeRedactor(options) {
648
+ if (options === false) {
649
+ return (attributes) => ({ ...attributes ?? {} });
650
+ }
651
+ const r = compile(options === true ? void 0 : options);
652
+ return (attributes) => redactWith(attributes, r);
653
+ }
654
+
655
+ // src/server.ts
656
+ var LLM_MARKER_ATTRIBUTES = [
657
+ "gen_ai.operation.name",
658
+ "gen_ai.system",
659
+ "gen_ai.provider.name",
660
+ "gen_ai.request.model",
661
+ "autter.llm.provider"
662
+ ];
663
+ function isLlmSpan(name, attributes) {
664
+ if (name.startsWith("ai.") || name.startsWith("gen_ai.")) return true;
665
+ return LLM_MARKER_ATTRIBUTES.some((key) => attributes[key] !== void 0);
666
+ }
667
+ var LlmAwareSampler = class {
668
+ constructor(delegate) {
669
+ this.delegate = delegate;
670
+ }
671
+ delegate;
672
+ shouldSample(ctx, traceId, spanName, spanKind, attributes, links) {
673
+ if (isLlmSpan(spanName, attributes)) {
674
+ return { decision: SamplingDecision.RECORD_AND_SAMPLED };
675
+ }
676
+ return this.delegate.shouldSample(
677
+ ctx,
678
+ traceId,
679
+ spanName,
680
+ spanKind,
681
+ attributes,
682
+ links
683
+ );
684
+ }
685
+ toString() {
686
+ return `LlmAware(${this.delegate.toString()})`;
687
+ }
688
+ };
689
+ var RecordUnsampledSampler = class {
690
+ constructor(delegate) {
691
+ this.delegate = delegate;
692
+ }
693
+ delegate;
694
+ shouldSample(ctx, traceId, spanName, spanKind, attributes, links) {
695
+ const result = this.delegate.shouldSample(
696
+ ctx,
697
+ traceId,
698
+ spanName,
699
+ spanKind,
700
+ attributes,
701
+ links
702
+ );
703
+ if (result.decision === SamplingDecision.NOT_RECORD) {
704
+ return { ...result, decision: SamplingDecision.RECORD };
705
+ }
706
+ return result;
707
+ }
708
+ toString() {
709
+ return `RecordUnsampled(${this.delegate.toString()})`;
710
+ }
711
+ };
712
+ var RETENTION_MAX_SPANS_PER_TRACE = 256;
713
+ var RETENTION_MAX_BUFFERED_SPANS = 5e3;
714
+ var RETENTION_SWEEP_INTERVAL_MS = 1e4;
715
+ var RETENTION_TRACE_TTL_MS = 3e4;
716
+ function spanIndicatesError(span) {
717
+ if (span.status.code === SpanStatusCode.ERROR) return true;
718
+ return span.events.some((event) => event.name === "exception");
719
+ }
720
+ var TAIL_RETAINED_ATTR = "autter.tail_retained";
721
+ function withSampledFlag(span) {
722
+ const spanContext = {
723
+ ...span.spanContext(),
724
+ traceFlags: span.spanContext().traceFlags | TraceFlags.SAMPLED
725
+ };
726
+ return {
727
+ name: span.name,
728
+ kind: span.kind,
729
+ spanContext: () => spanContext,
730
+ parentSpanId: span.parentSpanId,
731
+ startTime: span.startTime,
732
+ endTime: span.endTime,
733
+ status: span.status,
734
+ attributes: { ...span.attributes, [TAIL_RETAINED_ATTR]: true },
735
+ links: span.links,
736
+ events: span.events,
737
+ duration: span.duration,
738
+ ended: span.ended,
739
+ resource: span.resource,
740
+ instrumentationLibrary: span.instrumentationLibrary,
741
+ droppedAttributesCount: span.droppedAttributesCount,
742
+ droppedEventsCount: span.droppedEventsCount,
743
+ droppedLinksCount: span.droppedLinksCount
744
+ };
745
+ }
746
+ var ErrorTraceRetentionProcessor = class {
747
+ constructor(inner) {
748
+ this.inner = inner;
749
+ this.sweeper = setInterval(() => this.sweep(), RETENTION_SWEEP_INTERVAL_MS);
750
+ this.sweeper.unref?.();
751
+ }
752
+ inner;
753
+ traces = /* @__PURE__ */ new Map();
754
+ bufferedSpans = 0;
755
+ sweeper;
756
+ onStart(_span, _parentContext) {
757
+ }
758
+ onEnd(span) {
759
+ const ctx = span.spanContext();
760
+ if ((ctx.traceFlags & TraceFlags.SAMPLED) !== 0) return;
761
+ const entry = this.entryFor(ctx.traceId);
762
+ if (entry.retained) {
763
+ this.forward(span);
764
+ } else if (spanIndicatesError(span)) {
765
+ entry.retained = true;
766
+ this.flush(entry);
767
+ this.forward(span);
768
+ } else if (entry.spans.length < RETENTION_MAX_SPANS_PER_TRACE) {
769
+ entry.spans.push(span);
770
+ this.bufferedSpans++;
771
+ this.evictIfOverBudget();
772
+ }
773
+ if (span.parentSpanId === void 0 && !entry.retained) {
774
+ this.drop(ctx.traceId);
775
+ }
776
+ }
777
+ /** Promote the active trace, if any — called on captured errors. */
778
+ retainActiveTrace() {
779
+ const active2 = trace.getActiveSpan()?.spanContext();
780
+ if (!active2 || (active2.traceFlags & TraceFlags.SAMPLED) !== 0) return;
781
+ const entry = this.entryFor(active2.traceId);
782
+ if (entry.retained) return;
783
+ entry.retained = true;
784
+ this.flush(entry);
785
+ }
786
+ forceFlush() {
787
+ return this.inner.forceFlush();
788
+ }
789
+ shutdown() {
790
+ clearInterval(this.sweeper);
791
+ this.traces.clear();
792
+ this.bufferedSpans = 0;
793
+ return this.inner.shutdown();
794
+ }
795
+ entryFor(traceId) {
796
+ let entry = this.traces.get(traceId);
797
+ if (!entry) {
798
+ entry = { spans: [], retained: false, firstSeenAtMs: Date.now() };
799
+ this.traces.set(traceId, entry);
800
+ }
801
+ return entry;
802
+ }
803
+ forward(span) {
804
+ this.inner.onEnd(withSampledFlag(span));
805
+ }
806
+ flush(entry) {
807
+ for (const span of entry.spans) this.forward(span);
808
+ this.bufferedSpans -= entry.spans.length;
809
+ entry.spans = [];
810
+ }
811
+ drop(traceId) {
812
+ const entry = this.traces.get(traceId);
813
+ if (!entry) return;
814
+ this.bufferedSpans -= entry.spans.length;
815
+ this.traces.delete(traceId);
816
+ }
817
+ evictIfOverBudget() {
818
+ if (this.bufferedSpans <= RETENTION_MAX_BUFFERED_SPANS) return;
819
+ for (const [traceId, entry] of this.traces) {
820
+ if (entry.spans.length === 0) continue;
821
+ this.drop(traceId);
822
+ if (this.bufferedSpans <= RETENTION_MAX_BUFFERED_SPANS) return;
823
+ }
824
+ }
825
+ sweep() {
826
+ const cutoff = Date.now() - RETENTION_TRACE_TTL_MS;
827
+ for (const [traceId, entry] of this.traces) {
828
+ if (entry.firstSeenAtMs <= cutoff) this.drop(traceId);
829
+ }
830
+ }
831
+ };
832
+ function llmBaseAttributes(info) {
833
+ return {
834
+ "gen_ai.operation.name": info.operation ?? "chat",
835
+ // Both keys: gen_ai.system is the pre-1.37 semconv name and what
836
+ // most tooling still matches on.
837
+ "gen_ai.provider.name": info.provider,
838
+ "gen_ai.system": info.provider,
839
+ "gen_ai.request.model": info.model,
840
+ ...info.userId ? { "autter.user_id": info.userId } : {},
841
+ ...info.sessionId ? { "autter.session_id": info.sessionId } : {},
842
+ // Caller-supplied extras go through redaction like every other
843
+ // custom attribute family (prompts can contain emails/keys).
844
+ ...info.attributes ? activeRedactor(info.attributes) : {}
845
+ };
846
+ }
847
+ function usageAttributes(usage) {
848
+ const attrs = {};
849
+ if (usage.inputTokens !== void 0) {
850
+ attrs["gen_ai.usage.input_tokens"] = Math.max(0, Math.round(usage.inputTokens));
851
+ }
852
+ if (usage.outputTokens !== void 0) {
853
+ attrs["gen_ai.usage.output_tokens"] = Math.max(
854
+ 0,
855
+ Math.round(usage.outputTokens)
856
+ );
857
+ }
858
+ if (usage.costUsd !== void 0) {
859
+ attrs["autter.llm.cost_usd"] = usage.costUsd;
860
+ }
861
+ return attrs;
862
+ }
863
+ async function runLlmSpan(tracer, info, fn) {
864
+ const span = tracer.startSpan(`${info.operation ?? "chat"} ${info.model}`, {
865
+ kind: SpanKind.CLIENT,
866
+ attributes: llmBaseAttributes(info)
867
+ });
868
+ const setResponseModel = (model) => span.setAttribute("gen_ai.response.model", model);
869
+ const handle = {
870
+ setUsage: (usage) => span.setAttributes(usageAttributes(usage)),
871
+ setCost: (usd) => span.setAttribute("autter.llm.cost_usd", usd),
872
+ setResponseModel,
873
+ setModel: setResponseModel,
874
+ setAttributes: (attributes) => span.setAttributes(attributes)
875
+ };
876
+ try {
877
+ const result = await context.with(
878
+ trace.setSpan(context.active(), span),
879
+ () => fn(handle)
880
+ );
881
+ span.setStatus({ code: SpanStatusCode.OK });
882
+ return result;
883
+ } catch (err) {
884
+ if (err instanceof Error) {
885
+ span.recordException(err);
886
+ span.setAttribute("error.type", err.name);
887
+ }
888
+ span.setStatus({
889
+ code: SpanStatusCode.ERROR,
890
+ message: err instanceof Error ? err.message : String(err)
891
+ });
892
+ throw err;
893
+ } finally {
894
+ span.end();
895
+ }
896
+ }
897
+ function recordLlmCall(tracer, call) {
898
+ const durationMs = Math.max(0, call.durationMs ?? 0);
899
+ const endTime = /* @__PURE__ */ new Date();
900
+ const startTime = new Date(endTime.getTime() - durationMs);
901
+ const span = tracer.startSpan(
902
+ `${call.operation ?? "chat"} ${call.model}`,
903
+ {
904
+ kind: SpanKind.CLIENT,
905
+ startTime,
906
+ attributes: { ...llmBaseAttributes(call), ...usageAttributes(call) }
907
+ }
908
+ );
909
+ if (call.error !== void 0 && call.error !== null) {
910
+ if (call.error instanceof Error) {
911
+ span.recordException(call.error);
912
+ span.setAttribute("error.type", call.error.name);
913
+ }
914
+ span.setStatus({
915
+ code: SpanStatusCode.ERROR,
916
+ message: call.error instanceof Error ? call.error.message : String(call.error)
917
+ });
918
+ } else {
919
+ span.setStatus({ code: SpanStatusCode.OK });
920
+ }
921
+ span.end(endTime);
922
+ }
923
+ async function runWithSpan(tracer, name, fn, attributes) {
924
+ const span = tracer.startSpan(name, { attributes });
925
+ try {
926
+ const result = await context.with(
927
+ trace.setSpan(context.active(), span),
928
+ fn
929
+ );
930
+ span.setStatus({ code: SpanStatusCode.OK });
931
+ return result;
932
+ } catch (err) {
933
+ if (err instanceof Error) span.recordException(err);
934
+ span.setStatus({
935
+ code: SpanStatusCode.ERROR,
936
+ message: err instanceof Error ? err.message : String(err)
937
+ });
938
+ throw err;
939
+ } finally {
940
+ span.end();
941
+ }
942
+ }
943
+ function expressRouteOf(req) {
944
+ const props = req;
945
+ const path = props?.route?.path;
946
+ if (typeof path !== "string" || path === "") return null;
947
+ const base = typeof props?.baseUrl === "string" ? props.baseUrl : "";
948
+ const route = base + path;
949
+ return route.startsWith("/") ? route : null;
950
+ }
951
+ function captureExpressRoute(_span, response) {
952
+ if (!(response instanceof ServerResponse)) return;
953
+ const rpcMetadata = getRPCMetadata(context.active());
954
+ if (rpcMetadata?.type !== RPCType.HTTP) return;
955
+ const setRoute = () => {
956
+ if (rpcMetadata.route) return;
957
+ const route = expressRouteOf(response.req);
958
+ if (route) rpcMetadata.route = route;
959
+ };
960
+ response.prependListener("finish", setRoute);
961
+ response.prependListener("close", setRoute);
962
+ }
219
963
  var active = null;
964
+ var activeAlwaysOnProvider = null;
965
+ var activeRedactor = makeRedactor(true);
220
966
  function initAutterServer(options) {
221
967
  if (active) return active;
222
968
  const endpoint = (options.endpoint ?? "https://otlp.autter.dev").replace(
@@ -225,65 +971,112 @@ function initAutterServer(options) {
225
971
  );
226
972
  const headers = { authorization: `Bearer ${options.apiKey}` };
227
973
  const environment = options.environment ?? process.env.NODE_ENV ?? "production";
974
+ if (isDebugEnabled() || options.debug === true) setDebugMode(true);
975
+ debugLog(`initialising service=${options.service} endpoint=${endpoint}`);
976
+ activeRedactor = makeRedactor(options.redactAttributes ?? true);
228
977
  const resource = new Resource({
229
978
  [ATTR_SERVICE_NAME]: options.service,
230
979
  ...options.release ? { [ATTR_SERVICE_VERSION]: options.release } : {},
231
- "deployment.environment": environment
980
+ "deployment.environment": environment,
981
+ // Tells the ingester request metrics arrive on the metrics pipe, so
982
+ // it must not also fold our server spans into usage rollups (that
983
+ // would double-count every sampled request).
984
+ "autter.metrics_wired": true
985
+ });
986
+ const headSampler = new ParentBasedSampler({
987
+ root: new TraceIdRatioBasedSampler(options.traceSampleRate ?? 0.01)
988
+ });
989
+ const retainOnError = options.retainTracesOnError !== false;
990
+ let sampler = retainOnError ? new RecordUnsampledSampler(headSampler) : headSampler;
991
+ if (options.llmTracing !== false) sampler = new LlmAwareSampler(sampler);
992
+ const errorTraceBuffer = retainOnError ? new ErrorTraceRetentionProcessor(
993
+ new BatchSpanProcessor(
994
+ new CountingExporter(
995
+ new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
996
+ ),
997
+ { scheduledDelayMillis: 2e3 }
998
+ )
999
+ ) : null;
1000
+ const mainSpanProcessor = new BatchSpanProcessor(
1001
+ new CountingExporter(
1002
+ new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1003
+ )
1004
+ );
1005
+ const metricReader = new PeriodicExportingMetricReader({
1006
+ exporter: new OTLPMetricExporter({
1007
+ url: `${endpoint}/v1/metrics`,
1008
+ headers,
1009
+ // Deltas, not lifetime totals: the ingester SUMs data points
1010
+ // into runtime_metrics_1m, and the default (cumulative)
1011
+ // temporality would re-count every past request on each
1012
+ // 60 s export.
1013
+ temporalityPreference: AggregationTemporalityPreference.DELTA
1014
+ }),
1015
+ exportIntervalMillis: options.metricIntervalMs ?? 6e4
232
1016
  });
233
1017
  const sdk = new NodeSDK({
234
1018
  resource,
235
- sampler: new ParentBasedSampler({
236
- root: new TraceIdRatioBasedSampler(options.traceSampleRate ?? 0.01)
237
- }),
238
- traceExporter: new OTLPTraceExporter({
239
- url: `${endpoint}/v1/traces`,
240
- headers
241
- }),
242
- metricReader: new PeriodicExportingMetricReader({
243
- exporter: new OTLPMetricExporter({
244
- url: `${endpoint}/v1/metrics`,
245
- headers
246
- }),
247
- exportIntervalMillis: options.metricIntervalMs ?? 6e4
248
- }),
1019
+ sampler,
1020
+ spanProcessors: [
1021
+ mainSpanProcessor,
1022
+ ...errorTraceBuffer ? [errorTraceBuffer] : []
1023
+ ],
1024
+ metricReader,
249
1025
  instrumentations: [
250
- new HttpInstrumentation(),
1026
+ new HttpInstrumentation({ responseHook: captureExpressRoute }),
251
1027
  ...options.instrumentations ?? []
252
1028
  ]
253
1029
  });
254
1030
  sdk.start();
255
- const errorProvider = new BasicTracerProvider({
1031
+ const alwaysOnProvider = new BasicTracerProvider({
256
1032
  resource,
257
1033
  sampler: new AlwaysOnSampler(),
258
1034
  spanProcessors: [
259
1035
  new BatchSpanProcessor(
260
- new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers }),
1036
+ new CountingExporter(
1037
+ new OTLPTraceExporter({ url: `${endpoint}/v1/traces`, headers })
1038
+ ),
261
1039
  { scheduledDelayMillis: 2e3 }
262
1040
  )
263
1041
  ]
264
1042
  });
265
- const errorTracer = errorProvider.getTracer("autter-errors");
1043
+ const errorTracer = alwaysOnProvider.getTracer("autter-errors");
1044
+ const processTracer = alwaysOnProvider.getTracer("autter-processes");
1045
+ const llmTracer = alwaysOnProvider.getTracer("autter-llm");
266
1046
  function captureException2(error, attributes) {
1047
+ errorTraceBuffer?.retainActiveTrace();
1048
+ telemetryStats.markCaptured();
267
1049
  const isError = error instanceof Error;
268
1050
  const message = isError ? error.message : String(error);
269
1051
  const span = errorTracer.startSpan(isError ? error.name : "Error", {
270
- attributes: { "autter.severity": "error", ...attributes }
1052
+ attributes: {
1053
+ "autter.severity": "error",
1054
+ // Redaction is applied here, not at export time: PII must not
1055
+ // leave the process even if an exporter misbehaves.
1056
+ ...activeRedactor(attributes)
1057
+ }
271
1058
  });
272
- if (isError) {
1059
+ if (isError && error.stack) {
273
1060
  span.recordException(error);
274
1061
  } else {
1062
+ const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureException")).join("\n");
275
1063
  span.addEvent("exception", {
276
- "exception.type": "Error",
277
- "exception.message": message
1064
+ "exception.type": isError ? error.name : "Error",
1065
+ "exception.message": message,
1066
+ ...stack ? { "exception.stacktrace": stack } : {}
278
1067
  });
279
1068
  }
280
1069
  span.setStatus({ code: SpanStatusCode.ERROR, message });
281
1070
  span.end();
282
1071
  }
283
1072
  function captureMessage2(message, severity = "warning", attributes) {
1073
+ if (severity === "error" || severity === "fatal") {
1074
+ errorTraceBuffer?.retainActiveTrace();
1075
+ }
1076
+ telemetryStats.markCaptured();
284
1077
  const stack = new Error().stack?.split("\n").filter((line, i) => i === 0 || !line.includes("captureMessage")).join("\n");
285
1078
  const span = errorTracer.startSpan("Message", {
286
- attributes: { "autter.severity": severity, ...attributes }
1079
+ attributes: { "autter.severity": severity, ...activeRedactor(attributes) }
287
1080
  });
288
1081
  span.addEvent("exception", {
289
1082
  "exception.type": "Message",
@@ -297,19 +1090,54 @@ function initAutterServer(options) {
297
1090
  if (options.captureGlobalErrors !== false) {
298
1091
  process.on("uncaughtExceptionMonitor", (err) => {
299
1092
  captureException2(err, { "autter.unhandled": true });
300
- void errorProvider.forceFlush().catch(() => {
1093
+ void flushTarget.forceFlush();
1094
+ });
1095
+ process.on("unhandledRejection", (reason) => {
1096
+ captureException2(reason, {
1097
+ "autter.unhandled": true,
1098
+ ...reason instanceof Error ? {} : { "autter.severity": "warning" }
301
1099
  });
302
1100
  });
303
1101
  }
1102
+ const flushTarget = {
1103
+ forceFlush: async () => {
1104
+ const results = await Promise.allSettled([
1105
+ Promise.resolve().then(() => alwaysOnProvider.forceFlush()),
1106
+ Promise.resolve().then(() => mainSpanProcessor.forceFlush()),
1107
+ ...errorTraceBuffer ? [Promise.resolve().then(() => errorTraceBuffer.forceFlush())] : [],
1108
+ Promise.resolve().then(() => metricReader.forceFlush())
1109
+ ]);
1110
+ const failed = results.find(
1111
+ (result) => result.status === "rejected"
1112
+ );
1113
+ if (failed) {
1114
+ throw failed.reason;
1115
+ }
1116
+ }
1117
+ };
1118
+ registerFlushTarget("active-server", flushTarget);
1119
+ let autoFlushHandle = null;
1120
+ if (options.autoFlush !== false) {
1121
+ autoFlushHandle = installAutterAutoFlush();
1122
+ }
304
1123
  const server = {
305
1124
  captureException: captureException2,
306
1125
  captureMessage: captureMessage2,
1126
+ withProcessSpan: (name, fn, attributes) => runWithSpan(processTracer, name, fn, activeRedactor(attributes)),
1127
+ withLlmCall: (info, fn) => runLlmSpan(llmTracer, info, fn),
1128
+ trackLlmCall: (call) => recordLlmCall(llmTracer, call),
307
1129
  shutdown: async () => {
308
1130
  active = null;
309
- await Promise.allSettled([errorProvider.shutdown(), sdk.shutdown()]);
1131
+ activeAlwaysOnProvider = null;
1132
+ autoFlushHandle?.dispose();
1133
+ autoFlushHandle = null;
1134
+ unregisterFlushTargets();
1135
+ telemetryStats.markAllFlushed();
1136
+ await Promise.allSettled([alwaysOnProvider.shutdown(), sdk.shutdown()]);
310
1137
  }
311
1138
  };
312
1139
  active = server;
1140
+ activeAlwaysOnProvider = alwaysOnProvider;
313
1141
  return server;
314
1142
  }
315
1143
  function captureException(error, attributes) {
@@ -325,6 +1153,21 @@ function captureException(error, attributes) {
325
1153
  });
326
1154
  span.end();
327
1155
  }
1156
+ function withProcessSpan(name, fn, attributes) {
1157
+ if (active) return active.withProcessSpan(name, fn, attributes);
1158
+ return runWithSpan(trace.getTracer("autter-processes"), name, fn, attributes);
1159
+ }
1160
+ function withLlmCall(info, fn) {
1161
+ if (active) return active.withLlmCall(info, fn);
1162
+ return runLlmSpan(trace.getTracer("autter-llm"), info, fn);
1163
+ }
1164
+ function trackLlmCall(call) {
1165
+ if (active) {
1166
+ active.trackLlmCall(call);
1167
+ return;
1168
+ }
1169
+ recordLlmCall(trace.getTracer("autter-llm"), call);
1170
+ }
328
1171
  function captureMessage(message, severity = "warning", attributes) {
329
1172
  if (active) {
330
1173
  active.captureMessage(message, severity, attributes);
@@ -341,11 +1184,271 @@ function captureMessage(message, severity = "warning", attributes) {
341
1184
  span.setStatus({ code: SpanStatusCode.ERROR, message });
342
1185
  span.end();
343
1186
  }
1187
+ function makeSafeCapture(options) {
1188
+ const clean = (attributes) => attributes ? { ...redactAttributes(attributes, options) } : void 0;
1189
+ return {
1190
+ captureException: (error, attributes) => captureException(error, clean(attributes)),
1191
+ captureMessage: (message, severity, attributes) => captureMessage(message, severity ?? "warning", clean(attributes))
1192
+ };
1193
+ }
1194
+ function alwaysOnTracer(name) {
1195
+ return activeAlwaysOnProvider ? activeAlwaysOnProvider.getTracer(name) : trace.getTracer(name);
1196
+ }
1197
+ function autterLlmTracer() {
1198
+ return alwaysOnTracer("autter-llm");
1199
+ }
1200
+ async function emitLlmSelftestTrace() {
1201
+ const provider = activeAlwaysOnProvider;
1202
+ if (!provider) {
1203
+ throw new Error(
1204
+ "emitLlmSelftestTrace() requires initAutterServer() to have been called"
1205
+ );
1206
+ }
1207
+ let traceId = "";
1208
+ await withLlmCall(
1209
+ {
1210
+ provider: "autter-selftest",
1211
+ model: "autter-selftest",
1212
+ attributes: { "autter.selftest": true }
1213
+ },
1214
+ (llm) => {
1215
+ traceId = trace.getActiveSpan()?.spanContext().traceId ?? "";
1216
+ llm.setUsage({ inputTokens: 1, outputTokens: 1 });
1217
+ llm.setCost(0);
1218
+ }
1219
+ );
1220
+ await provider.forceFlush();
1221
+ return { traceId };
1222
+ }
1223
+
1224
+ // src/llm-instrument.ts
1225
+ import {
1226
+ SpanKind as SpanKind2,
1227
+ SpanStatusCode as SpanStatusCode2
1228
+ } from "@opentelemetry/api";
1229
+ var CALL_METHODS = /* @__PURE__ */ new Set([
1230
+ "create",
1231
+ // openai chat/completions/embeddings/responses, anthropic messages
1232
+ "stream",
1233
+ // anthropic messages.stream
1234
+ "generateContent",
1235
+ "generateContentStream",
1236
+ // google genai
1237
+ "embedContent",
1238
+ "batchEmbedContents",
1239
+ "parse"
1240
+ // openai beta structured outputs
1241
+ ]);
1242
+ function detectProvider(client) {
1243
+ const name = client.constructor?.name?.toLowerCase() ?? "";
1244
+ for (const known of ["openai", "anthropic", "mistral", "groq", "cohere"]) {
1245
+ if (name.includes(known)) return known;
1246
+ }
1247
+ if (name.includes("google") || name.includes("genai")) return "google";
1248
+ const baseUrl = String(
1249
+ client.baseURL ?? client.baseUrl ?? ""
1250
+ );
1251
+ const host = baseUrl.match(/https?:\/\/([^/]+)/)?.[1] ?? "";
1252
+ if (host.includes("openai")) return "openai";
1253
+ if (host.includes("anthropic")) return "anthropic";
1254
+ if (host.includes("googleapis")) return "google";
1255
+ if (host.includes("mistral")) return "mistral";
1256
+ if (host.includes("groq")) return "groq";
1257
+ return "unknown";
1258
+ }
1259
+ function extractUsage(result) {
1260
+ const r = result;
1261
+ const usage = r?.usage ?? r?.message?.usage;
1262
+ const meta = r?.usageMetadata;
1263
+ const input = usage?.prompt_tokens ?? usage?.input_tokens ?? meta?.promptTokenCount ?? void 0;
1264
+ const output = usage?.completion_tokens ?? usage?.output_tokens ?? meta?.candidatesTokenCount ?? void 0;
1265
+ const responseModel = r?.model ?? r?.modelVersion;
1266
+ return {
1267
+ inputTokens: typeof input === "number" ? input : void 0,
1268
+ outputTokens: typeof output === "number" ? output : void 0,
1269
+ responseModel: typeof responseModel === "string" ? responseModel : void 0
1270
+ };
1271
+ }
1272
+ function applyUsage(span, usage) {
1273
+ if (usage.inputTokens !== void 0) {
1274
+ span.setAttribute("gen_ai.usage.input_tokens", usage.inputTokens);
1275
+ }
1276
+ if (usage.outputTokens !== void 0) {
1277
+ span.setAttribute("gen_ai.usage.output_tokens", usage.outputTokens);
1278
+ }
1279
+ if (usage.responseModel) {
1280
+ span.setAttribute("gen_ai.response.model", usage.responseModel);
1281
+ }
1282
+ }
1283
+ function endWithError(span, err) {
1284
+ if (err instanceof Error) span.recordException(err);
1285
+ span.setStatus({
1286
+ code: SpanStatusCode2.ERROR,
1287
+ message: err instanceof Error ? err.message : String(err)
1288
+ });
1289
+ span.end();
1290
+ }
1291
+ function wrapStream(stream, span) {
1292
+ let ended = false;
1293
+ const seen = {
1294
+ inputTokens: void 0,
1295
+ outputTokens: void 0,
1296
+ responseModel: void 0
1297
+ };
1298
+ const finish = (err) => {
1299
+ if (ended) return;
1300
+ ended = true;
1301
+ applyUsage(span, seen);
1302
+ span.setAttribute("autter.llm.streamed", true);
1303
+ if (err !== void 0) {
1304
+ endWithError(span, err);
1305
+ } else {
1306
+ span.setStatus({ code: SpanStatusCode2.OK });
1307
+ span.end();
1308
+ }
1309
+ };
1310
+ return new Proxy(stream, {
1311
+ get(target, prop, receiver) {
1312
+ if (prop === Symbol.asyncIterator) {
1313
+ return () => {
1314
+ const inner = target[Symbol.asyncIterator]();
1315
+ return {
1316
+ async next() {
1317
+ try {
1318
+ const step = await inner.next();
1319
+ if (!step.done) {
1320
+ const usage = extractUsage(step.value);
1321
+ if (usage.inputTokens !== void 0) seen.inputTokens = usage.inputTokens;
1322
+ if (usage.outputTokens !== void 0) seen.outputTokens = usage.outputTokens;
1323
+ if (usage.responseModel) seen.responseModel = usage.responseModel;
1324
+ } else {
1325
+ finish();
1326
+ }
1327
+ return step;
1328
+ } catch (err) {
1329
+ finish(err);
1330
+ throw err;
1331
+ }
1332
+ },
1333
+ async return(value2) {
1334
+ finish();
1335
+ return inner.return ? inner.return(value2) : { done: true, value: void 0 };
1336
+ },
1337
+ async throw(err) {
1338
+ finish(err);
1339
+ if (inner.throw) return inner.throw(err);
1340
+ throw err;
1341
+ }
1342
+ };
1343
+ };
1344
+ }
1345
+ const value = Reflect.get(target, prop, receiver);
1346
+ return typeof value === "function" ? value.bind(target) : value;
1347
+ }
1348
+ });
1349
+ }
1350
+ function instrumentLlmClient(client, options) {
1351
+ const provider = (options?.provider ?? detectProvider(client)).toLowerCase();
1352
+ const proxied = /* @__PURE__ */ new WeakMap();
1353
+ function resolveUserId() {
1354
+ return typeof options?.userId === "function" ? options.userId() : options?.userId;
1355
+ }
1356
+ function traceCall(fn, self, callPath, args) {
1357
+ const request = args[0];
1358
+ const operation = /embed/i.test(callPath) ? "embeddings" : "chat";
1359
+ const userId = resolveUserId();
1360
+ const span = autterLlmTracer().startSpan(`${operation} ${request.model}`, {
1361
+ kind: SpanKind2.CLIENT,
1362
+ attributes: {
1363
+ "gen_ai.operation.name": operation,
1364
+ "gen_ai.provider.name": provider,
1365
+ "gen_ai.system": provider,
1366
+ "gen_ai.request.model": request.model,
1367
+ ...userId ? { "autter.user_id": userId } : {},
1368
+ ...options?.attributes
1369
+ }
1370
+ });
1371
+ let result;
1372
+ try {
1373
+ result = fn.apply(self, args);
1374
+ } catch (err) {
1375
+ endWithError(span, err);
1376
+ throw err;
1377
+ }
1378
+ if (!(result instanceof Promise)) {
1379
+ if (result && typeof result === "object" && Symbol.asyncIterator in result) {
1380
+ return wrapStream(result, span);
1381
+ }
1382
+ applyUsage(span, extractUsage(result));
1383
+ span.setStatus({ code: SpanStatusCode2.OK });
1384
+ span.end();
1385
+ return result;
1386
+ }
1387
+ return result.then(
1388
+ (resolved) => {
1389
+ if (resolved && typeof resolved === "object" && Symbol.asyncIterator in resolved) {
1390
+ return wrapStream(resolved, span);
1391
+ }
1392
+ applyUsage(span, extractUsage(resolved));
1393
+ span.setStatus({ code: SpanStatusCode2.OK });
1394
+ span.end();
1395
+ return resolved;
1396
+ },
1397
+ (err) => {
1398
+ endWithError(span, err);
1399
+ throw err;
1400
+ }
1401
+ );
1402
+ }
1403
+ function deepProxy(target, path = "") {
1404
+ const cached = proxied.get(target);
1405
+ if (cached) return cached;
1406
+ const proxy = new Proxy(target, {
1407
+ get(t, prop, receiver) {
1408
+ const value = Reflect.get(t, prop, receiver);
1409
+ if (typeof prop === "symbol") return value;
1410
+ const propPath = path ? `${path}.${prop}` : prop;
1411
+ if (typeof value === "function") {
1412
+ if (CALL_METHODS.has(prop)) {
1413
+ return (...args) => {
1414
+ const first = args[0];
1415
+ if (first && typeof first === "object" && typeof first.model === "string") {
1416
+ return traceCall(
1417
+ value,
1418
+ t,
1419
+ propPath,
1420
+ args
1421
+ );
1422
+ }
1423
+ return value.apply(t, args);
1424
+ };
1425
+ }
1426
+ return value.bind(t);
1427
+ }
1428
+ if (value !== null && typeof value === "object" && !(value instanceof Promise)) {
1429
+ return deepProxy(value, propPath);
1430
+ }
1431
+ return value;
1432
+ }
1433
+ });
1434
+ proxied.set(target, proxy);
1435
+ return proxy;
1436
+ }
1437
+ return deepProxy(client);
1438
+ }
344
1439
  export {
345
1440
  captureException,
346
1441
  captureMessage,
347
1442
  createBrowserRelayFetchHandler,
348
1443
  createBrowserRelayHandler,
1444
+ emitLlmSelftestTrace,
349
1445
  initAutterServer,
350
- sanitizeBrowserPayload
1446
+ installAutterAutoFlush,
1447
+ instrumentLlmClient,
1448
+ makeSafeCapture,
1449
+ redactAttributes,
1450
+ sanitizeBrowserPayload,
1451
+ trackLlmCall,
1452
+ withLlmCall,
1453
+ withProcessSpan
351
1454
  };