raindrop-ai 0.2.1 → 0.2.2-otelv2

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.
@@ -1,4 +1,10 @@
1
- // ../core/dist/chunk-Y5LUB7OE.js
1
+ // src/lib/inject-als.ts
2
+ import { AsyncLocalStorage } from "async_hooks";
3
+ if (!globalThis.RAINDROP_ASYNC_LOCAL_STORAGE) {
4
+ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
5
+ }
6
+
7
+ // ../core/dist/chunk-ITAZONWL.js
2
8
  function normalizeFeatureFlags(input) {
3
9
  if (Array.isArray(input)) {
4
10
  return Object.fromEntries(input.map((name) => [name, "true"]));
@@ -393,10 +399,175 @@ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
393
399
  "ai.response.providerMetadata"
394
400
  ];
395
401
  var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
402
+ function getAsyncLocalStorageCtor() {
403
+ return globalThis.RAINDROP_ASYNC_LOCAL_STORAGE;
404
+ }
405
+ function getWeakRefCtor() {
406
+ const ctor = globalThis.WeakRef;
407
+ return typeof ctor === "function" ? ctor : void 0;
408
+ }
409
+ var MAX_BINDING_STACK = 128;
410
+ var EMPTY_STACK = [];
411
+ var RoutingContextStorage = class {
412
+ constructor() {
413
+ this._als = null;
414
+ this._enterWithUsable = false;
415
+ this._syncStack = EMPTY_STACK;
416
+ this._adoptionChecked = false;
417
+ this._warnedNoAmbientBind = false;
418
+ }
419
+ maybeAdoptAsyncLocalStorage() {
420
+ if (this._adoptionChecked) return;
421
+ this._adoptionChecked = true;
422
+ const Ctor = getAsyncLocalStorageCtor();
423
+ if (!Ctor) return;
424
+ const als = new Ctor();
425
+ try {
426
+ als.run(EMPTY_STACK, () => {
427
+ });
428
+ } catch (e) {
429
+ return;
430
+ }
431
+ this._als = als;
432
+ if (typeof als.enterWith === "function") {
433
+ try {
434
+ als.enterWith(EMPTY_STACK);
435
+ this._enterWithUsable = true;
436
+ } catch (e) {
437
+ this._enterWithUsable = false;
438
+ }
439
+ }
440
+ }
441
+ getStack() {
442
+ var _a;
443
+ this.maybeAdoptAsyncLocalStorage();
444
+ if (this._als) return (_a = this._als.getStore()) != null ? _a : this._syncStack;
445
+ return this._syncStack;
446
+ }
447
+ /**
448
+ * Persist an ambient stack mutation. Returns false when an ALS is adopted but
449
+ * `enterWith` is unusable (workerd): callers must NOT fall back to the
450
+ * process-shared sync stack, since concurrent isolates would cross-stamp.
451
+ */
452
+ setAmbientStack(stack) {
453
+ if (this._als) {
454
+ if (!this._enterWithUsable) return false;
455
+ this._als.enterWith(stack);
456
+ return true;
457
+ }
458
+ this._syncStack = stack;
459
+ return true;
460
+ }
461
+ push(binding) {
462
+ this.maybeAdoptAsyncLocalStorage();
463
+ if (this._als && !this._enterWithUsable) {
464
+ this.warnNoAmbientBindOnce();
465
+ return;
466
+ }
467
+ let stack = this.getStack();
468
+ if (stack.length >= MAX_BINDING_STACK) {
469
+ stack = stack.filter((b) => b.isLive());
470
+ if (stack.length >= MAX_BINDING_STACK) {
471
+ stack = stack.slice(stack.length - (MAX_BINDING_STACK - 1));
472
+ }
473
+ }
474
+ this.setAmbientStack([...stack, binding]);
475
+ }
476
+ remove(binding) {
477
+ binding.markDead();
478
+ const stack = this.getStack();
479
+ const pruned = stack.filter((b) => b !== binding && b.isLive());
480
+ if (pruned.length !== stack.length) {
481
+ this.setAmbientStack(pruned);
482
+ }
483
+ }
484
+ runWith(binding, callback) {
485
+ this.maybeAdoptAsyncLocalStorage();
486
+ if (this._als) {
487
+ return this._als.run([...this.getStack(), binding], callback);
488
+ }
489
+ const previous = this._syncStack;
490
+ this._syncStack = [...previous, binding];
491
+ try {
492
+ return callback();
493
+ } finally {
494
+ this._syncStack = previous;
495
+ }
496
+ }
497
+ current() {
498
+ const stack = this.getStack();
499
+ if (stack.length === 0) return void 0;
500
+ const top = stack[stack.length - 1];
501
+ if (top.isLive()) return top;
502
+ const pruned = stack.filter((b) => b.isLive());
503
+ this.setAmbientStack(pruned);
504
+ return pruned.length > 0 ? pruned[pruned.length - 1] : void 0;
505
+ }
506
+ warnNoAmbientBindOnce() {
507
+ if (this._warnedNoAmbientBind) return;
508
+ this._warnedNoAmbientBind = true;
509
+ console.warn(
510
+ "[raindrop] This runtime's AsyncLocalStorage does not implement enterWith (e.g. Cloudflare Workers), so begin()/finish() cannot scope auto-instrumented spans to a project without cross-contaminating concurrent requests. Manual events and explicit span routing are unaffected; use asCurrent()/withSpan (run-based) to route auto-instrumented spans on this runtime."
511
+ );
512
+ }
513
+ };
514
+ var ROUTING_STORAGE_KEY = /* @__PURE__ */ Symbol.for("raindrop.tracing.routingContextStorage");
515
+ function routingStorage() {
516
+ const holder = globalThis;
517
+ let storage = holder[ROUTING_STORAGE_KEY];
518
+ if (!storage) {
519
+ storage = new RoutingContextStorage();
520
+ holder[ROUTING_STORAGE_KEY] = storage;
521
+ }
522
+ return storage;
523
+ }
524
+ function makeBoundContext(binding, owner) {
525
+ const WeakRefCtor = owner !== void 0 ? getWeakRefCtor() : void 0;
526
+ const ref = WeakRefCtor && owner !== void 0 ? new WeakRefCtor(owner) : void 0;
527
+ let dead = false;
528
+ return {
529
+ projectId: binding.projectId,
530
+ authHint: binding.authHint,
531
+ isLive() {
532
+ if (dead) return false;
533
+ return ref === void 0 || ref.deref() !== void 0;
534
+ },
535
+ markDead() {
536
+ dead = true;
537
+ }
538
+ };
539
+ }
540
+ function bindRoutingContext(binding, owner) {
541
+ const bound = makeBoundContext(binding, owner);
542
+ try {
543
+ routingStorage().push(bound);
544
+ } catch (e) {
545
+ }
546
+ return bound;
547
+ }
548
+ function unbindRoutingContext(bound) {
549
+ if (!bound || !isBoundContext(bound)) return;
550
+ try {
551
+ routingStorage().remove(bound);
552
+ } catch (e) {
553
+ }
554
+ }
555
+ function withRoutingContext(binding, callback) {
556
+ const bound = makeBoundContext(binding);
557
+ return routingStorage().runWith(bound, callback);
558
+ }
559
+ function currentRoutingBinding() {
560
+ const bound = routingStorage().current();
561
+ if (!bound) return void 0;
562
+ return { projectId: bound.projectId, authHint: bound.authHint };
563
+ }
564
+ function isBoundContext(value) {
565
+ return typeof value.isLive === "function";
566
+ }
396
567
 
397
568
  // ../core/dist/index.node.js
398
- import { AsyncLocalStorage } from "async_hooks";
399
- globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage;
569
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
570
+ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = AsyncLocalStorage2;
400
571
  var SUPPRESS_TRACING_KEY = /* @__PURE__ */ Symbol.for(
401
572
  "OpenTelemetry SDK Context Key SUPPRESS_TRACING"
402
573
  );
@@ -426,7 +597,7 @@ installTracingSuppressionHook();
426
597
  // package.json
427
598
  var package_default = {
428
599
  name: "raindrop-ai",
429
- version: "0.2.1",
600
+ version: "0.2.2",
430
601
  main: "dist/index.js",
431
602
  module: "dist/index.mjs",
432
603
  types: "dist/index.d.ts",
@@ -492,6 +663,7 @@ var package_default = {
492
663
  dependencies: {
493
664
  "@opentelemetry/api": "^1.9.0",
494
665
  "@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
666
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.57.2",
495
667
  "@traceloop/node-server-sdk": "0.19.0-otel-v1",
496
668
  weakref: "^0.2.1",
497
669
  zod: "^3.23.8"
@@ -523,6 +695,7 @@ var package_default = {
523
695
  "@traceloop/instrumentation-together",
524
696
  "@opentelemetry/instrumentation",
525
697
  "@opentelemetry/exporter-trace-otlp-http",
698
+ "@opentelemetry/exporter-trace-otlp-proto",
526
699
  "@opentelemetry/sdk-trace-base"
527
700
  ],
528
701
  noExternal: [
@@ -537,6 +710,190 @@ var package_default = {
537
710
  }
538
711
  };
539
712
 
713
+ // src/tracing/multi-project.ts
714
+ import { createHash } from "crypto";
715
+ import { OTLPTraceExporter as OTLPProtoTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
716
+ var PROJECT_ID_SPAN_ATTRIBUTE = "raindrop.project_id";
717
+ var AUTH_HINT_SPAN_ATTRIBUTE = "raindrop.auth_hint";
718
+ var EXPORT_SUCCESS = 0;
719
+ var EXPORT_FAILED = 1;
720
+ function authHintForKey(writeKey) {
721
+ if (!writeKey) return void 0;
722
+ return createHash("sha256").update(writeKey, "utf8").digest("hex").slice(0, 8);
723
+ }
724
+ var STATE_KEY = /* @__PURE__ */ Symbol.for("raindrop.tracing.multiProjectState");
725
+ function getState() {
726
+ const holder = globalThis;
727
+ let state = holder[STATE_KEY];
728
+ if (!state) {
729
+ state = {
730
+ foreignClientSeen: false,
731
+ seenClientHints: /* @__PURE__ */ new Set(),
732
+ ownerAuthHint: void 0,
733
+ ownerProjectId: void 0,
734
+ pipelineClaimed: false,
735
+ traceloopConfiguredWithoutClaim: false,
736
+ multiKeyWarned: false,
737
+ pendingMultiKeyWarning: false
738
+ };
739
+ holder[STATE_KEY] = state;
740
+ }
741
+ return state;
742
+ }
743
+ function emitMultiKeyWarningOnce() {
744
+ const state = getState();
745
+ if (state.multiKeyWarned) return;
746
+ state.multiKeyWarned = true;
747
+ state.pendingMultiKeyWarning = false;
748
+ console.warn(
749
+ "[raindrop] Multiple API keys detected in one process. The OTel export guard now requires positive attribution: spans not bound to any client (no begin()/asCurrent()) are dropped, and spans bound to a non-owner key are dropped. Manual events are unaffected."
750
+ );
751
+ }
752
+ function registerClientKey(authHint, tracingEnabled = false) {
753
+ if (!authHint) return;
754
+ const state = getState();
755
+ state.seenClientHints.add(authHint);
756
+ if (state.seenClientHints.size > 1) {
757
+ state.foreignClientSeen = true;
758
+ if (tracingEnabled || state.pipelineClaimed) {
759
+ emitMultiKeyWarningOnce();
760
+ } else {
761
+ state.pendingMultiKeyWarning = true;
762
+ }
763
+ }
764
+ }
765
+ function markForeignClient() {
766
+ getState().foreignClientSeen = true;
767
+ }
768
+ function foreignClientSeen() {
769
+ return getState().foreignClientSeen;
770
+ }
771
+ function stampSpan(span, projectId, authHint) {
772
+ try {
773
+ if (projectId) {
774
+ span.setAttribute(PROJECT_ID_SPAN_ATTRIBUTE, projectId);
775
+ }
776
+ if (authHint && foreignClientSeen()) {
777
+ span.setAttribute(AUTH_HINT_SPAN_ATTRIBUTE, authHint);
778
+ }
779
+ } catch (e) {
780
+ }
781
+ }
782
+ var RaindropContextSpanProcessor = class {
783
+ onStart(span) {
784
+ try {
785
+ const bound = currentRoutingBinding();
786
+ if (!bound) return;
787
+ stampSpan(span, bound.projectId, bound.authHint);
788
+ } catch (e) {
789
+ }
790
+ }
791
+ onEnd() {
792
+ }
793
+ async forceFlush() {
794
+ }
795
+ async shutdown() {
796
+ }
797
+ };
798
+ var GuardedSpanExporter = class {
799
+ constructor(inner, ownerAuthHint) {
800
+ this.droppedTotal = 0;
801
+ this.inner = inner;
802
+ this.ownerAuthHint = ownerAuthHint;
803
+ }
804
+ partition(spans) {
805
+ var _a;
806
+ const allowed = [];
807
+ let dropped = 0;
808
+ const requirePositive = foreignClientSeen();
809
+ for (const span of spans) {
810
+ const attrs = (_a = span.attributes) != null ? _a : {};
811
+ const hint = attrs[AUTH_HINT_SPAN_ATTRIBUTE];
812
+ const keep = requirePositive ? hint === this.ownerAuthHint : hint === void 0 || hint === this.ownerAuthHint;
813
+ if (keep) {
814
+ allowed.push(span);
815
+ } else {
816
+ dropped += 1;
817
+ }
818
+ }
819
+ return { allowed, dropped };
820
+ }
821
+ warnDropped(dropped) {
822
+ this.droppedTotal += dropped;
823
+ const total = this.droppedTotal;
824
+ rateLimitedLog(
825
+ "js-sdk.multi-project.dropped",
826
+ () => console.warn(
827
+ `[raindrop] Dropped ${dropped} span(s) produced under a different API key than the tracing pipeline's (total dropped: ${total}). One process has ONE OTel pipeline authenticating with the first tracing-enabled client's key; tracing for a different key/org in the same process is unsupported \u2014 run it in its own process. Manual events (track_ai/begin/finish/signals/identify) are unaffected.`
828
+ )
829
+ );
830
+ }
831
+ export(spans, resultCallback) {
832
+ let allowed;
833
+ let dropped;
834
+ try {
835
+ ({ allowed, dropped } = this.partition(spans));
836
+ } catch (e) {
837
+ if (foreignClientSeen()) {
838
+ this.warnDropped(spans.length);
839
+ resultCallback({ code: EXPORT_FAILED });
840
+ return;
841
+ }
842
+ this.inner.export(spans, resultCallback);
843
+ return;
844
+ }
845
+ if (dropped) {
846
+ this.warnDropped(dropped);
847
+ }
848
+ if (allowed.length === 0) {
849
+ resultCallback({ code: EXPORT_SUCCESS });
850
+ return;
851
+ }
852
+ this.inner.export(allowed, resultCallback);
853
+ }
854
+ shutdown() {
855
+ return this.inner.shutdown();
856
+ }
857
+ forceFlush() {
858
+ return this.inner.forceFlush ? this.inner.forceFlush() : Promise.resolve();
859
+ }
860
+ };
861
+ function buildGuardedExporter(baseUrl, headers, ownerAuthHint) {
862
+ const inner = new OTLPProtoTraceExporter({
863
+ url: `${baseUrl}/v1/traces`,
864
+ headers
865
+ });
866
+ return new GuardedSpanExporter(inner, ownerAuthHint);
867
+ }
868
+ function pipelineOwnerHint() {
869
+ return getState().ownerAuthHint;
870
+ }
871
+ function claimPipeline(authHint, projectId) {
872
+ const state = getState();
873
+ if (state.pipelineClaimed) {
874
+ return false;
875
+ }
876
+ state.pipelineClaimed = true;
877
+ state.ownerAuthHint = authHint;
878
+ state.ownerProjectId = projectId;
879
+ if (state.foreignClientSeen && state.pendingMultiKeyWarning) {
880
+ emitMultiKeyWarningOnce();
881
+ }
882
+ return true;
883
+ }
884
+ function releasePipelineClaim() {
885
+ const state = getState();
886
+ state.pipelineClaimed = false;
887
+ state.ownerAuthHint = void 0;
888
+ state.ownerProjectId = void 0;
889
+ }
890
+ function markTraceloopConfiguredWithoutClaim() {
891
+ getState().traceloopConfiguredWithoutClaim = true;
892
+ }
893
+ function traceloopConfiguredWithoutClaim() {
894
+ return getState().traceloopConfiguredWithoutClaim;
895
+ }
896
+
540
897
  // src/tracing/tracer-core.ts
541
898
  import { context as context4, SpanStatusCode as SpanStatusCode4, trace as trace4 } from "@opentelemetry/api";
542
899
  import { OTLPTraceExporter as OTLPHttpTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
@@ -552,7 +909,7 @@ import {
552
909
  AIPlatformInstrumentation,
553
910
  VertexAIInstrumentation
554
911
  } from "@traceloop/instrumentation-vertexai";
555
- import * as traceloop3 from "@traceloop/node-server-sdk";
912
+ import * as traceloop4 from "@traceloop/node-server-sdk";
556
913
  import { WeakValueMap } from "weakref";
557
914
 
558
915
  // src/tracing/direct/http.ts
@@ -835,7 +1192,11 @@ var DirectTraceShipper = class {
835
1192
  attrString2("traceloop.entity.input", params.input),
836
1193
  attrString2("traceloop.entity.output", params.output),
837
1194
  // Duration tracking
838
- attrInt2("traceloop.entity.duration_ms", params.durationMs)
1195
+ attrInt2("traceloop.entity.duration_ms", params.durationMs),
1196
+ // Per-span project routing: the ingest boundary routes on this attribute
1197
+ // (org-validated) and falls back to the request's project header. Direct
1198
+ // spans ship on this instance's own key, so no cross-key auth hint.
1199
+ attrString2(PROJECT_ID_SPAN_ATTRIBUTE, this.projectId)
839
1200
  ];
840
1201
  for (const [key, value] of Object.entries(params.properties)) {
841
1202
  if (value === void 0) {
@@ -1139,12 +1500,27 @@ var DirectToolSpan = class {
1139
1500
  }
1140
1501
  };
1141
1502
  var LiveInteraction = class {
1142
- constructor(context5, traceId, analytics, directShipper) {
1503
+ constructor(context5, traceId, analytics, directShipper, routing) {
1143
1504
  this.context = context5;
1144
1505
  this.analytics = analytics;
1145
1506
  this.tracer = trace2.getTracer("traceloop.tracer");
1146
1507
  this.traceId = traceId;
1147
1508
  this.directShipper = directShipper;
1509
+ this.routing = routing;
1510
+ }
1511
+ /**
1512
+ * Record the routing binding this interaction pushed onto the context stack
1513
+ * (via {@link bindRoutingContext} in `begin()`) so {@link finish} removes
1514
+ * exactly its own entry — non-LIFO safe, and leaving any still-open sibling's
1515
+ * binding intact.
1516
+ */
1517
+ setBoundContext(bound) {
1518
+ this.boundContext = bound;
1519
+ }
1520
+ /** Stamp a manually-created OTel span with this interaction's routing. */
1521
+ stampRouting(span) {
1522
+ if (!this.routing) return;
1523
+ stampSpan(span, this.routing.projectId, this.routing.authHint);
1148
1524
  }
1149
1525
  ensureTraceId(preferred) {
1150
1526
  if (preferred) {
@@ -1213,8 +1589,9 @@ var LiveInteraction = class {
1213
1589
  context2.active(),
1214
1590
  trace2.wrapSpanContext(spanContext)
1215
1591
  );
1592
+ const invoke = () => this.routing ? withRoutingContext(this.routing, () => fn.apply(thisArg, args)) : fn.apply(thisArg, args);
1216
1593
  try {
1217
- const result = await context2.with(wrappedContext, () => fn.apply(thisArg, args));
1594
+ const result = await context2.with(wrappedContext, invoke);
1218
1595
  const endTimeMs = Date.now();
1219
1596
  this.directShipper.sendSpan({
1220
1597
  name: taskName,
@@ -1249,11 +1626,17 @@ var LiveInteraction = class {
1249
1626
  }
1250
1627
  }
1251
1628
  const wrappedFn = (...fnArgs) => {
1252
- var _a2;
1253
- const activeTraceId = (_a2 = trace2.getSpan(context2.active())) == null ? void 0 : _a2.spanContext().traceId;
1629
+ const activeSpan = trace2.getSpan(context2.active());
1630
+ const activeTraceId = activeSpan == null ? void 0 : activeSpan.spanContext().traceId;
1254
1631
  if (activeTraceId) {
1255
1632
  this.traceId = activeTraceId;
1256
1633
  }
1634
+ if (activeSpan) {
1635
+ this.stampRouting(activeSpan);
1636
+ }
1637
+ if (this.routing) {
1638
+ return withRoutingContext(this.routing, () => fn.apply(thisArg, fnArgs));
1639
+ }
1257
1640
  return fn.apply(thisArg, fnArgs);
1258
1641
  };
1259
1642
  return traceloop.withTask(
@@ -1265,7 +1648,6 @@ var LiveInteraction = class {
1265
1648
  suppressTracing: params.suppressTracing
1266
1649
  },
1267
1650
  wrappedFn,
1268
- void 0,
1269
1651
  ...args
1270
1652
  );
1271
1653
  }
@@ -1302,10 +1684,11 @@ var LiveInteraction = class {
1302
1684
  context2.active(),
1303
1685
  trace2.wrapSpanContext(spanContext)
1304
1686
  );
1687
+ const invoke = () => this.routing ? withRoutingContext(this.routing, () => fn.apply(thisArg, args)) : fn.apply(thisArg, args);
1305
1688
  let result;
1306
1689
  let error;
1307
1690
  try {
1308
- result = await context2.with(wrappedContext, () => fn.apply(thisArg, args));
1691
+ result = await context2.with(wrappedContext, invoke);
1309
1692
  } catch (err) {
1310
1693
  error = err instanceof Error ? err.message : String(err);
1311
1694
  const endTimeMs2 = Date.now();
@@ -1373,6 +1756,9 @@ var LiveInteraction = class {
1373
1756
  const activeSpan = trace2.getSpan(context2.active());
1374
1757
  const ctx = activeSpan == null ? void 0 : activeSpan.spanContext();
1375
1758
  const liveSpanId = ctx && isValidSpanId(ctx.spanId) ? ctx.spanId : void 0;
1759
+ if (activeSpan) {
1760
+ this.stampRouting(activeSpan);
1761
+ }
1376
1762
  emitLive({
1377
1763
  type: "tool_start",
1378
1764
  spanId: liveSpanId,
@@ -1381,7 +1767,7 @@ var LiveInteraction = class {
1381
1767
  metadata: params.inputParameters ? { args: params.inputParameters } : void 0
1382
1768
  });
1383
1769
  try {
1384
- const r = await fn.apply(thisArg, wrappedArgs);
1770
+ const r = this.routing ? await withRoutingContext(this.routing, () => fn.apply(thisArg, wrappedArgs)) : await fn.apply(thisArg, wrappedArgs);
1385
1771
  emitLive({
1386
1772
  type: "tool_result",
1387
1773
  spanId: liveSpanId,
@@ -1417,7 +1803,6 @@ var LiveInteraction = class {
1417
1803
  suppressTracing: params.suppressTracing
1418
1804
  },
1419
1805
  wrapped,
1420
- void 0,
1421
1806
  ...args
1422
1807
  );
1423
1808
  }
@@ -1428,6 +1813,7 @@ var LiveInteraction = class {
1428
1813
  setAssociationProperties(span, contextProperties);
1429
1814
  span.setAttribute("traceloop.span.kind", "task");
1430
1815
  setAssociationProperties(span, properties);
1816
+ this.stampRouting(span);
1431
1817
  return span;
1432
1818
  }
1433
1819
  startToolSpan(params) {
@@ -1469,6 +1855,7 @@ var LiveInteraction = class {
1469
1855
  setAssociationProperties(span, contextProperties);
1470
1856
  span.setAttribute("traceloop.span.kind", "tool");
1471
1857
  setAssociationProperties(span, properties);
1858
+ this.stampRouting(span);
1472
1859
  if (inputParameters !== void 0) {
1473
1860
  span.setAttribute("traceloop.entity.input", serializeSpanValue(inputParameters));
1474
1861
  }
@@ -1571,6 +1958,8 @@ var LiveInteraction = class {
1571
1958
  }
1572
1959
  async finish(resultEvent) {
1573
1960
  var _a, _b, _c, _d;
1961
+ unbindRoutingContext(this.boundContext);
1962
+ this.boundContext = void 0;
1574
1963
  if (!this.traceId) {
1575
1964
  this.resolveLiveTraceId();
1576
1965
  }
@@ -1658,6 +2047,7 @@ var LiveInteraction = class {
1658
2047
  setAssociationProperties(span, contextProperties);
1659
2048
  span.setAttribute("traceloop.span.kind", "tool");
1660
2049
  setAssociationProperties(span, properties);
2050
+ this.stampRouting(span);
1661
2051
  if (input !== void 0) {
1662
2052
  span.setAttribute("traceloop.entity.input", stringifyBounded(input));
1663
2053
  }
@@ -1703,10 +2093,16 @@ var LiveInteraction = class {
1703
2093
  import { context as context3, isValidSpanId as isValidSpanId2, SpanStatusCode as SpanStatusCode3, trace as trace3 } from "@opentelemetry/api";
1704
2094
  import * as traceloop2 from "@traceloop/node-server-sdk";
1705
2095
  var LiveTracer = class {
1706
- constructor(globalProperties, directShipper) {
2096
+ constructor(globalProperties, directShipper, routing) {
1707
2097
  this.tracer = trace3.getTracer("traceloop.tracer");
1708
2098
  this.globalProperties = globalProperties || {};
1709
2099
  this.directShipper = directShipper;
2100
+ this.routing = routing;
2101
+ }
2102
+ /** Stamp a manually-created OTel span with this tracer's client routing. */
2103
+ stampRouting(span) {
2104
+ if (!this.routing) return;
2105
+ stampSpan(span, this.routing.projectId, this.routing.authHint);
1710
2106
  }
1711
2107
  emitLiveEvent(event) {
1712
2108
  var _a, _b;
@@ -1729,6 +2125,16 @@ var LiveTracer = class {
1729
2125
  ...params.properties || {}
1730
2126
  };
1731
2127
  const inputParameters = typeof params === "string" ? void 0 : params.inputParameters;
2128
+ const wrappedFn = (...fnArgs) => {
2129
+ const activeSpan = trace3.getSpan(context3.active());
2130
+ if (activeSpan) {
2131
+ this.stampRouting(activeSpan);
2132
+ }
2133
+ if (this.routing) {
2134
+ return withRoutingContext(this.routing, () => fn.apply(thisArg, fnArgs));
2135
+ }
2136
+ return fn.apply(thisArg, fnArgs);
2137
+ };
1732
2138
  return traceloop2.withTask(
1733
2139
  {
1734
2140
  name: taskName,
@@ -1737,8 +2143,7 @@ var LiveTracer = class {
1737
2143
  traceContent: params.traceContent,
1738
2144
  suppressTracing: params.suppressTracing
1739
2145
  },
1740
- fn,
1741
- thisArg,
2146
+ wrappedFn,
1742
2147
  ...args
1743
2148
  );
1744
2149
  }
@@ -1787,6 +2192,7 @@ var LiveTracer = class {
1787
2192
  const span = this.tracer.startSpan(name, {
1788
2193
  startTime: startTimeMs
1789
2194
  });
2195
+ this.stampRouting(span);
1790
2196
  Object.entries(this.globalProperties).forEach(([key, value]) => {
1791
2197
  span.setAttribute("traceloop.association.properties." + key, String(value));
1792
2198
  });
@@ -1831,6 +2237,59 @@ var LiveTracer = class {
1831
2237
  }
1832
2238
  };
1833
2239
 
2240
+ // src/tracing/exit-flush.ts
2241
+ import * as traceloop3 from "@traceloop/node-server-sdk";
2242
+ var STATE_KEY2 = /* @__PURE__ */ Symbol.for("raindrop.tracing.exitFlushState");
2243
+ function getState2() {
2244
+ const holder = globalThis;
2245
+ let state = holder[STATE_KEY2];
2246
+ if (!state) {
2247
+ state = {
2248
+ clientFlushers: /* @__PURE__ */ new Set(),
2249
+ pipelineFlushNeeded: false,
2250
+ handlerRegistered: false
2251
+ };
2252
+ holder[STATE_KEY2] = state;
2253
+ }
2254
+ return state;
2255
+ }
2256
+ function ensureExitHandler(state) {
2257
+ if (state.handlerRegistered) return;
2258
+ if (typeof process === "undefined" || typeof process.once !== "function") return;
2259
+ state.handlerRegistered = true;
2260
+ process.once("beforeExit", () => {
2261
+ void flushAll();
2262
+ });
2263
+ }
2264
+ function registerClientFlusher(flush) {
2265
+ const state = getState2();
2266
+ state.clientFlushers.add(flush);
2267
+ ensureExitHandler(state);
2268
+ return () => {
2269
+ state.clientFlushers.delete(flush);
2270
+ };
2271
+ }
2272
+ function markPipelineForExitFlush() {
2273
+ const state = getState2();
2274
+ state.pipelineFlushNeeded = true;
2275
+ ensureExitHandler(state);
2276
+ }
2277
+ async function flushAll() {
2278
+ const state = getState2();
2279
+ const tasks = [];
2280
+ for (const flush of state.clientFlushers) {
2281
+ tasks.push(Promise.resolve().then(flush).catch(() => {
2282
+ }));
2283
+ }
2284
+ if (state.pipelineFlushNeeded) {
2285
+ tasks.push(
2286
+ Promise.resolve().then(() => traceloop3.forceFlush()).catch(() => {
2287
+ })
2288
+ );
2289
+ }
2290
+ await Promise.all(tasks);
2291
+ }
2292
+
1834
2293
  // src/tracing/tracer-core.ts
1835
2294
  if (localDebuggerEnabled()) {
1836
2295
  if (!process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
@@ -1939,6 +2398,29 @@ function resolveLocalDebuggerExporterBaseUrl(baseUrlOpt) {
1939
2398
  function createLocalDebuggerOtlpExporter(config) {
1940
2399
  return new OTLPHttpTraceExporter(config);
1941
2400
  }
2401
+ function parseTraceloopHeaders(value) {
2402
+ const headers = {};
2403
+ for (const entry of value.split(",")) {
2404
+ const keyPairPart = entry.split(";")[0];
2405
+ const sep = keyPairPart.indexOf("=");
2406
+ if (sep <= 0) continue;
2407
+ try {
2408
+ const key = decodeURIComponent(keyPairPart.slice(0, sep).trim());
2409
+ const val = decodeURIComponent(keyPairPart.slice(sep + 1).trim());
2410
+ if (key.length > 0 && val.length > 0) headers[key] = val;
2411
+ } catch (e) {
2412
+ }
2413
+ }
2414
+ return headers;
2415
+ }
2416
+ function traceloopDefaultHeaders(writeKey) {
2417
+ const env = process.env.TRACELOOP_HEADERS;
2418
+ if (env) {
2419
+ const parsed = parseTraceloopHeaders(env);
2420
+ if (Object.keys(parsed).length > 0) return parsed;
2421
+ }
2422
+ return { Authorization: `Bearer ${writeKey}` };
2423
+ }
1942
2424
  function createLocalDebuggerSpanProcessor(options) {
1943
2425
  const localDebuggerUrl = resolveLocalDebuggerExporterBaseUrl(options.localDebuggerUrlOpt);
1944
2426
  if (!localDebuggerUrl) {
@@ -1949,7 +2431,7 @@ function createLocalDebuggerSpanProcessor(options) {
1949
2431
  headers: {}
1950
2432
  });
1951
2433
  try {
1952
- return traceloop3.createSpanProcessor({
2434
+ return traceloop4.createSpanProcessor({
1953
2435
  apiKey: options.writeKey,
1954
2436
  baseUrl: localDebuggerUrl,
1955
2437
  disableBatch: options.disableBatching,
@@ -1965,12 +2447,6 @@ function createLocalDebuggerSpanProcessor(options) {
1965
2447
  return void 0;
1966
2448
  }
1967
2449
  }
1968
- function mergeWithBestEffortLocalDebuggerProcessor(processor, localDebuggerProcessor) {
1969
- if (!localDebuggerProcessor) {
1970
- return processor;
1971
- }
1972
- return new CompositeSpanProcessor(processor ? [processor] : [], [localDebuggerProcessor]);
1973
- }
1974
2450
  var warnedSpanNormalizeFailure = false;
1975
2451
  var INVALID_OTEL_SPAN_ID = "0000000000000000";
1976
2452
  function normalizeSpanForV1Exporter(span) {
@@ -2270,13 +2746,12 @@ function createManualInstrumentations(instrumentModules) {
2270
2746
  }
2271
2747
  return instrumentations;
2272
2748
  }
2273
- var activeInteractions = new WeakValueMap();
2274
- var activeInteractionsByEventId = new WeakValueMap();
2275
2749
  function getCurrentTraceId() {
2276
2750
  var _a;
2277
2751
  return (_a = trace4.getSpan(context4.active())) == null ? void 0 : _a.spanContext().traceId;
2278
2752
  }
2279
2753
  var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2754
+ var _a;
2280
2755
  process.env.TRACELOOP_TELEMETRY = "false";
2281
2756
  const {
2282
2757
  logLevel,
@@ -2291,12 +2766,15 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2291
2766
  ...otherOptions
2292
2767
  } = options;
2293
2768
  const cloudEnabledResolved = cloudEnabled != null ? cloudEnabled : true;
2769
+ const authHint = authHintForKey(writeKey);
2770
+ const clientTracingExports = !useExternalOtel && cloudEnabledResolved && tracingEnabled !== false;
2771
+ registerClientKey(authHint, clientTracingExports);
2294
2772
  const withProjectIdHeaders = (existing) => {
2295
2773
  if (!projectId) {
2296
2774
  return existing;
2297
2775
  }
2298
2776
  return {
2299
- Authorization: `Bearer ${writeKey}`,
2777
+ ...traceloopDefaultHeaders(writeKey),
2300
2778
  ...existing != null ? existing : {},
2301
2779
  ...projectIdHeaders(projectId)
2302
2780
  };
@@ -2318,9 +2796,12 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2318
2796
  projectId
2319
2797
  }) : void 0;
2320
2798
  let traceloopInitialized = false;
2799
+ let claimedPipeline = false;
2800
+ const activeInteractions = new WeakValueMap();
2801
+ const activeInteractionsByEventId = new WeakValueMap();
2321
2802
  try {
2322
2803
  if (useExternalOtel) {
2323
- traceloop3.initialize({
2804
+ traceloop4.initialize({
2324
2805
  baseUrl: apiUrl,
2325
2806
  apiKey: writeKey,
2326
2807
  logLevel,
@@ -2332,6 +2813,7 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2332
2813
  // DO NOT create NodeSDK - customer has their own
2333
2814
  });
2334
2815
  traceloopInitialized = true;
2816
+ markTraceloopConfiguredWithoutClaim();
2335
2817
  if (isDebug) {
2336
2818
  console.log(
2337
2819
  "[raindrop] External OTEL mode active. Add raindrop.createSpanProcessor() to your NodeSDK's spanProcessors. Register instrumentations (AnthropicInstrumentation, etc.) on your NodeSDK."
@@ -2348,12 +2830,12 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2348
2830
  const localDebuggerProcessor = createLocalDebuggerSpanProcessor({
2349
2831
  writeKey,
2350
2832
  disableBatching,
2351
- allowedInstrumentationLibraries: traceloop3.ALL_INSTRUMENTATION_LIBRARIES,
2833
+ allowedInstrumentationLibraries: traceloop4.ALL_INSTRUMENTATION_LIBRARIES,
2352
2834
  localDebuggerUrlOpt: localDebuggerUrl
2353
2835
  });
2354
2836
  if (!cloudEnabledResolved) {
2355
2837
  if (localDebuggerProcessor) {
2356
- traceloop3.initialize({
2838
+ traceloop4.initialize({
2357
2839
  baseUrl: apiUrl,
2358
2840
  apiKey: writeKey,
2359
2841
  ...otherOptions,
@@ -2364,8 +2846,9 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2364
2846
  processor: localDebuggerProcessor
2365
2847
  });
2366
2848
  traceloopInitialized = true;
2849
+ markTraceloopConfiguredWithoutClaim();
2367
2850
  } else {
2368
- traceloop3.initialize({
2851
+ traceloop4.initialize({
2369
2852
  baseUrl: apiUrl,
2370
2853
  apiKey: writeKey,
2371
2854
  logLevel,
@@ -2373,33 +2856,91 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2373
2856
  tracingEnabled: false
2374
2857
  });
2375
2858
  traceloopInitialized = true;
2859
+ markTraceloopConfiguredWithoutClaim();
2376
2860
  }
2861
+ } else if (tracingEnabled === false) {
2862
+ traceloopInitialized = false;
2377
2863
  } else {
2378
- const processor = mergeWithBestEffortLocalDebuggerProcessor(
2379
- otherOptions.processor,
2380
- localDebuggerProcessor
2381
- );
2382
- const headers = withProjectIdHeaders(otherOptions.headers);
2383
- traceloop3.initialize({
2384
- baseUrl: apiUrl,
2385
- apiKey: writeKey,
2386
- ...otherOptions,
2387
- logLevel,
2388
- silenceInitializationMessage: true,
2389
- tracingEnabled: tracingEnabled !== false,
2390
- disableBatch: disableBatching,
2391
- ...processor ? { processor } : {},
2392
- ...headers ? { headers } : {}
2393
- });
2394
- traceloopInitialized = true;
2864
+ const claimed = claimPipeline(authHint, projectId);
2865
+ claimedPipeline = claimed;
2866
+ const ownerHint = pipelineOwnerHint();
2867
+ if (!claimed && authHint !== ownerHint) {
2868
+ markForeignClient();
2869
+ console.warn(
2870
+ "[raindrop] The OTel tracing pipeline is already initialized with a DIFFERENT api key. One process supports one tracing key/org: auto-instrumented spans from this client will be dropped at export, and spans not bound to any client (no begin()/asCurrent()) will be dropped as unattributable (manual events are unaffected and route normally). Run this client in its own process for tracing."
2871
+ );
2872
+ }
2873
+ if (claimed && traceloopConfiguredWithoutClaim()) {
2874
+ releasePipelineClaim();
2875
+ claimedPipeline = false;
2876
+ console.warn(
2877
+ "[raindrop] Cannot initialize the cloud tracing pipeline: OpenTelemetry was already configured in this process by an earlier Raindrop client (external-OTEL mode, or a local-only/Workshop client with no write key). Traceloop's tracer provider is fixed once per process, so this client's auto-instrumented spans will NOT be exported. Construct the cloud tracing-enabled client FIRST, or run it in its own process. Manual events (track_ai/begin/finish/signals/identify) are unaffected."
2878
+ );
2879
+ } else if (claimed) {
2880
+ const exporterHeaders = (_a = withProjectIdHeaders(otherOptions.headers)) != null ? _a : traceloopDefaultHeaders(writeKey);
2881
+ let guardedExporter;
2882
+ try {
2883
+ const callerExporter = otherOptions.exporter;
2884
+ guardedExporter = callerExporter ? new GuardedSpanExporter(callerExporter, ownerHint) : buildGuardedExporter(apiUrl, exporterHeaders, ownerHint);
2885
+ } catch (guardError) {
2886
+ releasePipelineClaim();
2887
+ claimedPipeline = false;
2888
+ console.warn(
2889
+ `[raindrop] could not install the guarded span exporter (${guardError instanceof Error ? guardError.message : guardError}); disabling tracing rather than running an unguarded pipeline. Manual events are unaffected.`
2890
+ );
2891
+ }
2892
+ if (guardedExporter) {
2893
+ const contextProcessor = new RaindropContextSpanProcessor();
2894
+ const callerProcessor = otherOptions.processor;
2895
+ const processor = new CompositeSpanProcessor(
2896
+ callerProcessor ? [contextProcessor, callerProcessor] : [contextProcessor],
2897
+ localDebuggerProcessor ? [localDebuggerProcessor] : []
2898
+ );
2899
+ try {
2900
+ traceloop4.initialize({
2901
+ baseUrl: apiUrl,
2902
+ apiKey: writeKey,
2903
+ ...otherOptions,
2904
+ logLevel,
2905
+ silenceInitializationMessage: true,
2906
+ // This branch only runs when tracing is enabled (the disabled
2907
+ // case is handled above), so tracing is always on here.
2908
+ tracingEnabled: true,
2909
+ disableBatch: disableBatching,
2910
+ processor,
2911
+ exporter: guardedExporter
2912
+ });
2913
+ traceloopInitialized = true;
2914
+ } catch (initError) {
2915
+ releasePipelineClaim();
2916
+ claimedPipeline = false;
2917
+ throw initError;
2918
+ }
2919
+ }
2920
+ } else {
2921
+ traceloopInitialized = true;
2922
+ }
2395
2923
  }
2396
2924
  }
2397
2925
  } catch (error) {
2926
+ if (claimedPipeline && !traceloopInitialized) {
2927
+ releasePipelineClaim();
2928
+ claimedPipeline = false;
2929
+ }
2398
2930
  console.warn(
2399
2931
  "[raindrop] Failed to initialize traceloop. Spans will not be sent.",
2400
2932
  error instanceof Error ? error.message : error
2401
2933
  );
2402
2934
  }
2935
+ if (traceloopInitialized) {
2936
+ markPipelineForExitFlush();
2937
+ }
2938
+ const deregisterExitFlusher = directShipper ? registerClientFlusher(async () => {
2939
+ try {
2940
+ await directShipper.flush();
2941
+ } catch (e) {
2942
+ }
2943
+ }) : void 0;
2403
2944
  return {
2404
2945
  /**
2405
2946
  * Returns instrumentation instances for your NodeSDK.
@@ -2487,7 +3028,7 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2487
3028
  );
2488
3029
  }
2489
3030
  const headers = withProjectIdHeaders(processorOptions == null ? void 0 : processorOptions.headers);
2490
- const productionProcessor = traceloop3.createSpanProcessor({
3031
+ const productionProcessor = traceloop4.createSpanProcessor({
2491
3032
  baseUrl: apiUrl,
2492
3033
  apiKey: writeKey,
2493
3034
  disableBatch: disableBatching,
@@ -2500,12 +3041,16 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2500
3041
  allowedInstrumentationLibraries: processorOptions == null ? void 0 : processorOptions.allowedInstrumentationLibraries,
2501
3042
  localDebuggerUrlOpt: localDebuggerUrl
2502
3043
  });
3044
+ const contextProcessor = new RaindropContextSpanProcessor();
2503
3045
  return withV2SpanCompat(
2504
- mergeWithBestEffortLocalDebuggerProcessor(productionProcessor, localDebuggerProcessor)
3046
+ new CompositeSpanProcessor(
3047
+ [contextProcessor, productionProcessor],
3048
+ localDebuggerProcessor ? [localDebuggerProcessor] : []
3049
+ )
2505
3050
  );
2506
3051
  },
2507
3052
  begin(traceContext) {
2508
- var _a;
3053
+ var _a2;
2509
3054
  if (!traceContext.eventId) {
2510
3055
  traceContext.eventId = crypto.randomUUID();
2511
3056
  }
@@ -2514,19 +3059,27 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2514
3059
  traceContext,
2515
3060
  traceId,
2516
3061
  analytics,
2517
- directShipper
3062
+ directShipper,
3063
+ { projectId, authHint }
2518
3064
  );
2519
- analytics._trackAiPartial({
2520
- ...traceContext,
2521
- properties: {
2522
- ...(_a = traceContext.properties) != null ? _a : {},
2523
- ...interaction.getTraceId() ? { trace_id: interaction.getTraceId() } : {}
3065
+ const bound = bindRoutingContext({ projectId, authHint }, interaction);
3066
+ interaction.setBoundContext(bound);
3067
+ try {
3068
+ analytics._trackAiPartial({
3069
+ ...traceContext,
3070
+ properties: {
3071
+ ...(_a2 = traceContext.properties) != null ? _a2 : {},
3072
+ ...interaction.getTraceId() ? { trace_id: interaction.getTraceId() } : {}
3073
+ }
3074
+ });
3075
+ if (interaction.getTraceId()) {
3076
+ activeInteractions.set(interaction.getTraceId(), interaction);
2524
3077
  }
2525
- });
2526
- if (interaction.getTraceId()) {
2527
- activeInteractions.set(interaction.getTraceId(), interaction);
3078
+ activeInteractionsByEventId.set(traceContext.eventId, interaction);
3079
+ } catch (error) {
3080
+ unbindRoutingContext(bound);
3081
+ throw error;
2528
3082
  }
2529
- activeInteractionsByEventId.set(traceContext.eventId, interaction);
2530
3083
  return interaction;
2531
3084
  },
2532
3085
  getActiveInteraction(eventId) {
@@ -2541,12 +3094,15 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2541
3094
  return interactionByTraceId;
2542
3095
  }
2543
3096
  }
2544
- const interaction = new LiveInteraction({ eventId }, traceId, analytics, directShipper);
3097
+ const interaction = new LiveInteraction({ eventId }, traceId, analytics, directShipper, {
3098
+ projectId,
3099
+ authHint
3100
+ });
2545
3101
  activeInteractionsByEventId.set(eventId, interaction);
2546
3102
  return interaction;
2547
3103
  },
2548
3104
  tracer(globalProperties) {
2549
- return new LiveTracer(globalProperties, directShipper);
3105
+ return new LiveTracer(globalProperties, directShipper, { projectId, authHint });
2550
3106
  },
2551
3107
  /**
2552
3108
  * Best-effort drain of in-flight OTel spans and direct shipper batches.
@@ -2561,18 +3117,19 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2561
3117
  }
2562
3118
  if (traceloopInitialized) {
2563
3119
  try {
2564
- await traceloop3.forceFlush();
3120
+ await traceloop4.forceFlush();
2565
3121
  } catch (e) {
2566
3122
  }
2567
3123
  }
2568
3124
  },
2569
3125
  async close() {
3126
+ deregisterExitFlusher == null ? void 0 : deregisterExitFlusher();
2570
3127
  activeInteractions.clear();
2571
3128
  activeInteractionsByEventId.clear();
2572
3129
  await (directShipper == null ? void 0 : directShipper.shutdown());
2573
3130
  if (traceloopInitialized) {
2574
3131
  try {
2575
- await traceloop3.forceFlush();
3132
+ await traceloop4.forceFlush();
2576
3133
  } catch (e) {
2577
3134
  }
2578
3135
  }
@@ -2598,6 +3155,8 @@ export {
2598
3155
  projectIdHeaders,
2599
3156
  SHUTDOWN_DEADLINE_MS,
2600
3157
  POST_SHUTDOWN_TIMEOUT_MS,
3158
+ withRoutingContext,
2601
3159
  package_default,
3160
+ authHintForKey,
2602
3161
  tracing
2603
3162
  };