raindrop-ai 0.2.1 → 0.2.2

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.
@@ -35,6 +35,12 @@ __export(tracing_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(tracing_exports);
37
37
 
38
+ // src/lib/inject-als.ts
39
+ var import_node_async_hooks = require("async_hooks");
40
+ if (!globalThis.RAINDROP_ASYNC_LOCAL_STORAGE) {
41
+ globalThis.RAINDROP_ASYNC_LOCAL_STORAGE = import_node_async_hooks.AsyncLocalStorage;
42
+ }
43
+
38
44
  // src/tracing/tracer-core.ts
39
45
  var import_api4 = require("@opentelemetry/api");
40
46
  var import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
@@ -47,10 +53,10 @@ var import_instrumentation_pinecone = require("@traceloop/instrumentation-pineco
47
53
  var import_instrumentation_qdrant = require("@traceloop/instrumentation-qdrant");
48
54
  var import_instrumentation_together = require("@traceloop/instrumentation-together");
49
55
  var import_instrumentation_vertexai = require("@traceloop/instrumentation-vertexai");
50
- var traceloop3 = __toESM(require("@traceloop/node-server-sdk"));
56
+ var traceloop4 = __toESM(require("@traceloop/node-server-sdk"));
51
57
  var import_weakref = require("weakref");
52
58
 
53
- // ../core/dist/chunk-Y5LUB7OE.js
59
+ // ../core/dist/chunk-ITAZONWL.js
54
60
  function normalizeFeatureFlags(input) {
55
61
  if (Array.isArray(input)) {
56
62
  return Object.fromEntries(input.map((name) => [name, "true"]));
@@ -407,6 +413,171 @@ var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
407
413
  "ai.response.providerMetadata"
408
414
  ];
409
415
  var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
416
+ function getAsyncLocalStorageCtor() {
417
+ return globalThis.RAINDROP_ASYNC_LOCAL_STORAGE;
418
+ }
419
+ function getWeakRefCtor() {
420
+ const ctor = globalThis.WeakRef;
421
+ return typeof ctor === "function" ? ctor : void 0;
422
+ }
423
+ var MAX_BINDING_STACK = 128;
424
+ var EMPTY_STACK = [];
425
+ var RoutingContextStorage = class {
426
+ constructor() {
427
+ this._als = null;
428
+ this._enterWithUsable = false;
429
+ this._syncStack = EMPTY_STACK;
430
+ this._adoptionChecked = false;
431
+ this._warnedNoAmbientBind = false;
432
+ }
433
+ maybeAdoptAsyncLocalStorage() {
434
+ if (this._adoptionChecked) return;
435
+ this._adoptionChecked = true;
436
+ const Ctor = getAsyncLocalStorageCtor();
437
+ if (!Ctor) return;
438
+ const als = new Ctor();
439
+ try {
440
+ als.run(EMPTY_STACK, () => {
441
+ });
442
+ } catch (e) {
443
+ return;
444
+ }
445
+ this._als = als;
446
+ if (typeof als.enterWith === "function") {
447
+ try {
448
+ als.enterWith(EMPTY_STACK);
449
+ this._enterWithUsable = true;
450
+ } catch (e) {
451
+ this._enterWithUsable = false;
452
+ }
453
+ }
454
+ }
455
+ getStack() {
456
+ var _a;
457
+ this.maybeAdoptAsyncLocalStorage();
458
+ if (this._als) return (_a = this._als.getStore()) != null ? _a : this._syncStack;
459
+ return this._syncStack;
460
+ }
461
+ /**
462
+ * Persist an ambient stack mutation. Returns false when an ALS is adopted but
463
+ * `enterWith` is unusable (workerd): callers must NOT fall back to the
464
+ * process-shared sync stack, since concurrent isolates would cross-stamp.
465
+ */
466
+ setAmbientStack(stack) {
467
+ if (this._als) {
468
+ if (!this._enterWithUsable) return false;
469
+ this._als.enterWith(stack);
470
+ return true;
471
+ }
472
+ this._syncStack = stack;
473
+ return true;
474
+ }
475
+ push(binding) {
476
+ this.maybeAdoptAsyncLocalStorage();
477
+ if (this._als && !this._enterWithUsable) {
478
+ this.warnNoAmbientBindOnce();
479
+ return;
480
+ }
481
+ let stack = this.getStack();
482
+ if (stack.length >= MAX_BINDING_STACK) {
483
+ stack = stack.filter((b) => b.isLive());
484
+ if (stack.length >= MAX_BINDING_STACK) {
485
+ stack = stack.slice(stack.length - (MAX_BINDING_STACK - 1));
486
+ }
487
+ }
488
+ this.setAmbientStack([...stack, binding]);
489
+ }
490
+ remove(binding) {
491
+ binding.markDead();
492
+ const stack = this.getStack();
493
+ const pruned = stack.filter((b) => b !== binding && b.isLive());
494
+ if (pruned.length !== stack.length) {
495
+ this.setAmbientStack(pruned);
496
+ }
497
+ }
498
+ runWith(binding, callback) {
499
+ this.maybeAdoptAsyncLocalStorage();
500
+ if (this._als) {
501
+ return this._als.run([...this.getStack(), binding], callback);
502
+ }
503
+ const previous = this._syncStack;
504
+ this._syncStack = [...previous, binding];
505
+ try {
506
+ return callback();
507
+ } finally {
508
+ this._syncStack = previous;
509
+ }
510
+ }
511
+ current() {
512
+ const stack = this.getStack();
513
+ if (stack.length === 0) return void 0;
514
+ const top = stack[stack.length - 1];
515
+ if (top.isLive()) return top;
516
+ const pruned = stack.filter((b) => b.isLive());
517
+ this.setAmbientStack(pruned);
518
+ return pruned.length > 0 ? pruned[pruned.length - 1] : void 0;
519
+ }
520
+ warnNoAmbientBindOnce() {
521
+ if (this._warnedNoAmbientBind) return;
522
+ this._warnedNoAmbientBind = true;
523
+ console.warn(
524
+ "[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."
525
+ );
526
+ }
527
+ };
528
+ var ROUTING_STORAGE_KEY = /* @__PURE__ */ Symbol.for("raindrop.tracing.routingContextStorage");
529
+ function routingStorage() {
530
+ const holder = globalThis;
531
+ let storage = holder[ROUTING_STORAGE_KEY];
532
+ if (!storage) {
533
+ storage = new RoutingContextStorage();
534
+ holder[ROUTING_STORAGE_KEY] = storage;
535
+ }
536
+ return storage;
537
+ }
538
+ function makeBoundContext(binding, owner) {
539
+ const WeakRefCtor = owner !== void 0 ? getWeakRefCtor() : void 0;
540
+ const ref = WeakRefCtor && owner !== void 0 ? new WeakRefCtor(owner) : void 0;
541
+ let dead = false;
542
+ return {
543
+ projectId: binding.projectId,
544
+ authHint: binding.authHint,
545
+ isLive() {
546
+ if (dead) return false;
547
+ return ref === void 0 || ref.deref() !== void 0;
548
+ },
549
+ markDead() {
550
+ dead = true;
551
+ }
552
+ };
553
+ }
554
+ function bindRoutingContext(binding, owner) {
555
+ const bound = makeBoundContext(binding, owner);
556
+ try {
557
+ routingStorage().push(bound);
558
+ } catch (e) {
559
+ }
560
+ return bound;
561
+ }
562
+ function unbindRoutingContext(bound) {
563
+ if (!bound || !isBoundContext(bound)) return;
564
+ try {
565
+ routingStorage().remove(bound);
566
+ } catch (e) {
567
+ }
568
+ }
569
+ function withRoutingContext(binding, callback) {
570
+ const bound = makeBoundContext(binding);
571
+ return routingStorage().runWith(bound, callback);
572
+ }
573
+ function currentRoutingBinding() {
574
+ const bound = routingStorage().current();
575
+ if (!bound) return void 0;
576
+ return { projectId: bound.projectId, authHint: bound.authHint };
577
+ }
578
+ function isBoundContext(value) {
579
+ return typeof value.isLive === "function";
580
+ }
410
581
 
411
582
  // ../core/dist/index.node.js
412
583
  var import_async_hooks = require("async_hooks");
@@ -593,7 +764,7 @@ function base64ToHex(base64) {
593
764
  // package.json
594
765
  var package_default = {
595
766
  name: "raindrop-ai",
596
- version: "0.2.1",
767
+ version: "0.2.2",
597
768
  main: "dist/index.js",
598
769
  module: "dist/index.mjs",
599
770
  types: "dist/index.d.ts",
@@ -659,6 +830,7 @@ var package_default = {
659
830
  dependencies: {
660
831
  "@opentelemetry/api": "^1.9.0",
661
832
  "@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
833
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.57.2",
662
834
  "@traceloop/node-server-sdk": "0.19.0-otel-v1",
663
835
  weakref: "^0.2.1",
664
836
  zod: "^3.23.8"
@@ -690,6 +862,7 @@ var package_default = {
690
862
  "@traceloop/instrumentation-together",
691
863
  "@opentelemetry/instrumentation",
692
864
  "@opentelemetry/exporter-trace-otlp-http",
865
+ "@opentelemetry/exporter-trace-otlp-proto",
693
866
  "@opentelemetry/sdk-trace-base"
694
867
  ],
695
868
  noExternal: [
@@ -772,6 +945,190 @@ function getActiveTraceContext() {
772
945
  };
773
946
  }
774
947
 
948
+ // src/tracing/multi-project.ts
949
+ var import_node_crypto = require("crypto");
950
+ var import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
951
+ var PROJECT_ID_SPAN_ATTRIBUTE = "raindrop.project_id";
952
+ var AUTH_HINT_SPAN_ATTRIBUTE = "raindrop.auth_hint";
953
+ var EXPORT_SUCCESS = 0;
954
+ var EXPORT_FAILED = 1;
955
+ function authHintForKey(writeKey) {
956
+ if (!writeKey) return void 0;
957
+ return (0, import_node_crypto.createHash)("sha256").update(writeKey, "utf8").digest("hex").slice(0, 8);
958
+ }
959
+ var STATE_KEY = /* @__PURE__ */ Symbol.for("raindrop.tracing.multiProjectState");
960
+ function getState() {
961
+ const holder = globalThis;
962
+ let state = holder[STATE_KEY];
963
+ if (!state) {
964
+ state = {
965
+ foreignClientSeen: false,
966
+ seenClientHints: /* @__PURE__ */ new Set(),
967
+ ownerAuthHint: void 0,
968
+ ownerProjectId: void 0,
969
+ pipelineClaimed: false,
970
+ traceloopConfiguredWithoutClaim: false,
971
+ multiKeyWarned: false,
972
+ pendingMultiKeyWarning: false
973
+ };
974
+ holder[STATE_KEY] = state;
975
+ }
976
+ return state;
977
+ }
978
+ function emitMultiKeyWarningOnce() {
979
+ const state = getState();
980
+ if (state.multiKeyWarned) return;
981
+ state.multiKeyWarned = true;
982
+ state.pendingMultiKeyWarning = false;
983
+ console.warn(
984
+ "[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."
985
+ );
986
+ }
987
+ function registerClientKey(authHint, tracingEnabled = false) {
988
+ if (!authHint) return;
989
+ const state = getState();
990
+ state.seenClientHints.add(authHint);
991
+ if (state.seenClientHints.size > 1) {
992
+ state.foreignClientSeen = true;
993
+ if (tracingEnabled || state.pipelineClaimed) {
994
+ emitMultiKeyWarningOnce();
995
+ } else {
996
+ state.pendingMultiKeyWarning = true;
997
+ }
998
+ }
999
+ }
1000
+ function markForeignClient() {
1001
+ getState().foreignClientSeen = true;
1002
+ }
1003
+ function foreignClientSeen() {
1004
+ return getState().foreignClientSeen;
1005
+ }
1006
+ function stampSpan(span, projectId, authHint) {
1007
+ try {
1008
+ if (projectId) {
1009
+ span.setAttribute(PROJECT_ID_SPAN_ATTRIBUTE, projectId);
1010
+ }
1011
+ if (authHint && foreignClientSeen()) {
1012
+ span.setAttribute(AUTH_HINT_SPAN_ATTRIBUTE, authHint);
1013
+ }
1014
+ } catch (e) {
1015
+ }
1016
+ }
1017
+ var RaindropContextSpanProcessor = class {
1018
+ onStart(span) {
1019
+ try {
1020
+ const bound = currentRoutingBinding();
1021
+ if (!bound) return;
1022
+ stampSpan(span, bound.projectId, bound.authHint);
1023
+ } catch (e) {
1024
+ }
1025
+ }
1026
+ onEnd() {
1027
+ }
1028
+ async forceFlush() {
1029
+ }
1030
+ async shutdown() {
1031
+ }
1032
+ };
1033
+ var GuardedSpanExporter = class {
1034
+ constructor(inner, ownerAuthHint) {
1035
+ this.droppedTotal = 0;
1036
+ this.inner = inner;
1037
+ this.ownerAuthHint = ownerAuthHint;
1038
+ }
1039
+ partition(spans) {
1040
+ var _a;
1041
+ const allowed = [];
1042
+ let dropped = 0;
1043
+ const requirePositive = foreignClientSeen();
1044
+ for (const span of spans) {
1045
+ const attrs = (_a = span.attributes) != null ? _a : {};
1046
+ const hint = attrs[AUTH_HINT_SPAN_ATTRIBUTE];
1047
+ const keep = requirePositive ? hint === this.ownerAuthHint : hint === void 0 || hint === this.ownerAuthHint;
1048
+ if (keep) {
1049
+ allowed.push(span);
1050
+ } else {
1051
+ dropped += 1;
1052
+ }
1053
+ }
1054
+ return { allowed, dropped };
1055
+ }
1056
+ warnDropped(dropped) {
1057
+ this.droppedTotal += dropped;
1058
+ const total = this.droppedTotal;
1059
+ rateLimitedLog(
1060
+ "js-sdk.multi-project.dropped",
1061
+ () => console.warn(
1062
+ `[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.`
1063
+ )
1064
+ );
1065
+ }
1066
+ export(spans, resultCallback) {
1067
+ let allowed;
1068
+ let dropped;
1069
+ try {
1070
+ ({ allowed, dropped } = this.partition(spans));
1071
+ } catch (e) {
1072
+ if (foreignClientSeen()) {
1073
+ this.warnDropped(spans.length);
1074
+ resultCallback({ code: EXPORT_FAILED });
1075
+ return;
1076
+ }
1077
+ this.inner.export(spans, resultCallback);
1078
+ return;
1079
+ }
1080
+ if (dropped) {
1081
+ this.warnDropped(dropped);
1082
+ }
1083
+ if (allowed.length === 0) {
1084
+ resultCallback({ code: EXPORT_SUCCESS });
1085
+ return;
1086
+ }
1087
+ this.inner.export(allowed, resultCallback);
1088
+ }
1089
+ shutdown() {
1090
+ return this.inner.shutdown();
1091
+ }
1092
+ forceFlush() {
1093
+ return this.inner.forceFlush ? this.inner.forceFlush() : Promise.resolve();
1094
+ }
1095
+ };
1096
+ function buildGuardedExporter(baseUrl, headers, ownerAuthHint) {
1097
+ const inner = new import_exporter_trace_otlp_proto.OTLPTraceExporter({
1098
+ url: `${baseUrl}/v1/traces`,
1099
+ headers
1100
+ });
1101
+ return new GuardedSpanExporter(inner, ownerAuthHint);
1102
+ }
1103
+ function pipelineOwnerHint() {
1104
+ return getState().ownerAuthHint;
1105
+ }
1106
+ function claimPipeline(authHint, projectId) {
1107
+ const state = getState();
1108
+ if (state.pipelineClaimed) {
1109
+ return false;
1110
+ }
1111
+ state.pipelineClaimed = true;
1112
+ state.ownerAuthHint = authHint;
1113
+ state.ownerProjectId = projectId;
1114
+ if (state.foreignClientSeen && state.pendingMultiKeyWarning) {
1115
+ emitMultiKeyWarningOnce();
1116
+ }
1117
+ return true;
1118
+ }
1119
+ function releasePipelineClaim() {
1120
+ const state = getState();
1121
+ state.pipelineClaimed = false;
1122
+ state.ownerAuthHint = void 0;
1123
+ state.ownerProjectId = void 0;
1124
+ }
1125
+ function markTraceloopConfiguredWithoutClaim() {
1126
+ getState().traceloopConfiguredWithoutClaim = true;
1127
+ }
1128
+ function traceloopConfiguredWithoutClaim() {
1129
+ return getState().traceloopConfiguredWithoutClaim;
1130
+ }
1131
+
775
1132
  // src/tracing/direct/shipper.ts
776
1133
  function serializeAssociationValue(value) {
777
1134
  return stringifyBounded(value);
@@ -831,7 +1188,11 @@ var DirectTraceShipper = class {
831
1188
  attrString2("traceloop.entity.input", params.input),
832
1189
  attrString2("traceloop.entity.output", params.output),
833
1190
  // Duration tracking
834
- attrInt2("traceloop.entity.duration_ms", params.durationMs)
1191
+ attrInt2("traceloop.entity.duration_ms", params.durationMs),
1192
+ // Per-span project routing: the ingest boundary routes on this attribute
1193
+ // (org-validated) and falls back to the request's project header. Direct
1194
+ // spans ship on this instance's own key, so no cross-key auth hint.
1195
+ attrString2(PROJECT_ID_SPAN_ATTRIBUTE, this.projectId)
835
1196
  ];
836
1197
  for (const [key, value] of Object.entries(params.properties)) {
837
1198
  if (value === void 0) {
@@ -1129,12 +1490,27 @@ var DirectToolSpan = class {
1129
1490
  }
1130
1491
  };
1131
1492
  var LiveInteraction = class {
1132
- constructor(context5, traceId, analytics, directShipper) {
1493
+ constructor(context5, traceId, analytics, directShipper, routing) {
1133
1494
  this.context = context5;
1134
1495
  this.analytics = analytics;
1135
1496
  this.tracer = import_api2.trace.getTracer("traceloop.tracer");
1136
1497
  this.traceId = traceId;
1137
1498
  this.directShipper = directShipper;
1499
+ this.routing = routing;
1500
+ }
1501
+ /**
1502
+ * Record the routing binding this interaction pushed onto the context stack
1503
+ * (via {@link bindRoutingContext} in `begin()`) so {@link finish} removes
1504
+ * exactly its own entry — non-LIFO safe, and leaving any still-open sibling's
1505
+ * binding intact.
1506
+ */
1507
+ setBoundContext(bound) {
1508
+ this.boundContext = bound;
1509
+ }
1510
+ /** Stamp a manually-created OTel span with this interaction's routing. */
1511
+ stampRouting(span) {
1512
+ if (!this.routing) return;
1513
+ stampSpan(span, this.routing.projectId, this.routing.authHint);
1138
1514
  }
1139
1515
  ensureTraceId(preferred) {
1140
1516
  if (preferred) {
@@ -1203,8 +1579,9 @@ var LiveInteraction = class {
1203
1579
  import_api2.context.active(),
1204
1580
  import_api2.trace.wrapSpanContext(spanContext)
1205
1581
  );
1582
+ const invoke = () => this.routing ? withRoutingContext(this.routing, () => fn.apply(thisArg, args)) : fn.apply(thisArg, args);
1206
1583
  try {
1207
- const result = await import_api2.context.with(wrappedContext, () => fn.apply(thisArg, args));
1584
+ const result = await import_api2.context.with(wrappedContext, invoke);
1208
1585
  const endTimeMs = Date.now();
1209
1586
  this.directShipper.sendSpan({
1210
1587
  name: taskName,
@@ -1239,11 +1616,17 @@ var LiveInteraction = class {
1239
1616
  }
1240
1617
  }
1241
1618
  const wrappedFn = (...fnArgs) => {
1242
- var _a2;
1243
- const activeTraceId = (_a2 = import_api2.trace.getSpan(import_api2.context.active())) == null ? void 0 : _a2.spanContext().traceId;
1619
+ const activeSpan = import_api2.trace.getSpan(import_api2.context.active());
1620
+ const activeTraceId = activeSpan == null ? void 0 : activeSpan.spanContext().traceId;
1244
1621
  if (activeTraceId) {
1245
1622
  this.traceId = activeTraceId;
1246
1623
  }
1624
+ if (activeSpan) {
1625
+ this.stampRouting(activeSpan);
1626
+ }
1627
+ if (this.routing) {
1628
+ return withRoutingContext(this.routing, () => fn.apply(thisArg, fnArgs));
1629
+ }
1247
1630
  return fn.apply(thisArg, fnArgs);
1248
1631
  };
1249
1632
  return traceloop.withTask(
@@ -1255,7 +1638,6 @@ var LiveInteraction = class {
1255
1638
  suppressTracing: params.suppressTracing
1256
1639
  },
1257
1640
  wrappedFn,
1258
- void 0,
1259
1641
  ...args
1260
1642
  );
1261
1643
  }
@@ -1292,10 +1674,11 @@ var LiveInteraction = class {
1292
1674
  import_api2.context.active(),
1293
1675
  import_api2.trace.wrapSpanContext(spanContext)
1294
1676
  );
1677
+ const invoke = () => this.routing ? withRoutingContext(this.routing, () => fn.apply(thisArg, args)) : fn.apply(thisArg, args);
1295
1678
  let result;
1296
1679
  let error;
1297
1680
  try {
1298
- result = await import_api2.context.with(wrappedContext, () => fn.apply(thisArg, args));
1681
+ result = await import_api2.context.with(wrappedContext, invoke);
1299
1682
  } catch (err) {
1300
1683
  error = err instanceof Error ? err.message : String(err);
1301
1684
  const endTimeMs2 = Date.now();
@@ -1363,6 +1746,9 @@ var LiveInteraction = class {
1363
1746
  const activeSpan = import_api2.trace.getSpan(import_api2.context.active());
1364
1747
  const ctx = activeSpan == null ? void 0 : activeSpan.spanContext();
1365
1748
  const liveSpanId = ctx && (0, import_api2.isValidSpanId)(ctx.spanId) ? ctx.spanId : void 0;
1749
+ if (activeSpan) {
1750
+ this.stampRouting(activeSpan);
1751
+ }
1366
1752
  emitLive({
1367
1753
  type: "tool_start",
1368
1754
  spanId: liveSpanId,
@@ -1371,7 +1757,7 @@ var LiveInteraction = class {
1371
1757
  metadata: params.inputParameters ? { args: params.inputParameters } : void 0
1372
1758
  });
1373
1759
  try {
1374
- const r = await fn.apply(thisArg, wrappedArgs);
1760
+ const r = this.routing ? await withRoutingContext(this.routing, () => fn.apply(thisArg, wrappedArgs)) : await fn.apply(thisArg, wrappedArgs);
1375
1761
  emitLive({
1376
1762
  type: "tool_result",
1377
1763
  spanId: liveSpanId,
@@ -1407,7 +1793,6 @@ var LiveInteraction = class {
1407
1793
  suppressTracing: params.suppressTracing
1408
1794
  },
1409
1795
  wrapped,
1410
- void 0,
1411
1796
  ...args
1412
1797
  );
1413
1798
  }
@@ -1418,6 +1803,7 @@ var LiveInteraction = class {
1418
1803
  setAssociationProperties(span, contextProperties);
1419
1804
  span.setAttribute("traceloop.span.kind", "task");
1420
1805
  setAssociationProperties(span, properties);
1806
+ this.stampRouting(span);
1421
1807
  return span;
1422
1808
  }
1423
1809
  startToolSpan(params) {
@@ -1459,6 +1845,7 @@ var LiveInteraction = class {
1459
1845
  setAssociationProperties(span, contextProperties);
1460
1846
  span.setAttribute("traceloop.span.kind", "tool");
1461
1847
  setAssociationProperties(span, properties);
1848
+ this.stampRouting(span);
1462
1849
  if (inputParameters !== void 0) {
1463
1850
  span.setAttribute("traceloop.entity.input", serializeSpanValue(inputParameters));
1464
1851
  }
@@ -1561,6 +1948,8 @@ var LiveInteraction = class {
1561
1948
  }
1562
1949
  async finish(resultEvent) {
1563
1950
  var _a, _b, _c, _d;
1951
+ unbindRoutingContext(this.boundContext);
1952
+ this.boundContext = void 0;
1564
1953
  if (!this.traceId) {
1565
1954
  this.resolveLiveTraceId();
1566
1955
  }
@@ -1648,6 +2037,7 @@ var LiveInteraction = class {
1648
2037
  setAssociationProperties(span, contextProperties);
1649
2038
  span.setAttribute("traceloop.span.kind", "tool");
1650
2039
  setAssociationProperties(span, properties);
2040
+ this.stampRouting(span);
1651
2041
  if (input !== void 0) {
1652
2042
  span.setAttribute("traceloop.entity.input", stringifyBounded(input));
1653
2043
  }
@@ -1693,10 +2083,16 @@ var LiveInteraction = class {
1693
2083
  var import_api3 = require("@opentelemetry/api");
1694
2084
  var traceloop2 = __toESM(require("@traceloop/node-server-sdk"));
1695
2085
  var LiveTracer = class {
1696
- constructor(globalProperties, directShipper) {
2086
+ constructor(globalProperties, directShipper, routing) {
1697
2087
  this.tracer = import_api3.trace.getTracer("traceloop.tracer");
1698
2088
  this.globalProperties = globalProperties || {};
1699
2089
  this.directShipper = directShipper;
2090
+ this.routing = routing;
2091
+ }
2092
+ /** Stamp a manually-created OTel span with this tracer's client routing. */
2093
+ stampRouting(span) {
2094
+ if (!this.routing) return;
2095
+ stampSpan(span, this.routing.projectId, this.routing.authHint);
1700
2096
  }
1701
2097
  emitLiveEvent(event) {
1702
2098
  var _a, _b;
@@ -1719,6 +2115,16 @@ var LiveTracer = class {
1719
2115
  ...params.properties || {}
1720
2116
  };
1721
2117
  const inputParameters = typeof params === "string" ? void 0 : params.inputParameters;
2118
+ const wrappedFn = (...fnArgs) => {
2119
+ const activeSpan = import_api3.trace.getSpan(import_api3.context.active());
2120
+ if (activeSpan) {
2121
+ this.stampRouting(activeSpan);
2122
+ }
2123
+ if (this.routing) {
2124
+ return withRoutingContext(this.routing, () => fn.apply(thisArg, fnArgs));
2125
+ }
2126
+ return fn.apply(thisArg, fnArgs);
2127
+ };
1722
2128
  return traceloop2.withTask(
1723
2129
  {
1724
2130
  name: taskName,
@@ -1727,8 +2133,7 @@ var LiveTracer = class {
1727
2133
  traceContent: params.traceContent,
1728
2134
  suppressTracing: params.suppressTracing
1729
2135
  },
1730
- fn,
1731
- thisArg,
2136
+ wrappedFn,
1732
2137
  ...args
1733
2138
  );
1734
2139
  }
@@ -1777,6 +2182,7 @@ var LiveTracer = class {
1777
2182
  const span = this.tracer.startSpan(name, {
1778
2183
  startTime: startTimeMs
1779
2184
  });
2185
+ this.stampRouting(span);
1780
2186
  Object.entries(this.globalProperties).forEach(([key, value]) => {
1781
2187
  span.setAttribute("traceloop.association.properties." + key, String(value));
1782
2188
  });
@@ -1821,6 +2227,59 @@ var LiveTracer = class {
1821
2227
  }
1822
2228
  };
1823
2229
 
2230
+ // src/tracing/exit-flush.ts
2231
+ var traceloop3 = __toESM(require("@traceloop/node-server-sdk"));
2232
+ var STATE_KEY2 = /* @__PURE__ */ Symbol.for("raindrop.tracing.exitFlushState");
2233
+ function getState2() {
2234
+ const holder = globalThis;
2235
+ let state = holder[STATE_KEY2];
2236
+ if (!state) {
2237
+ state = {
2238
+ clientFlushers: /* @__PURE__ */ new Set(),
2239
+ pipelineFlushNeeded: false,
2240
+ handlerRegistered: false
2241
+ };
2242
+ holder[STATE_KEY2] = state;
2243
+ }
2244
+ return state;
2245
+ }
2246
+ function ensureExitHandler(state) {
2247
+ if (state.handlerRegistered) return;
2248
+ if (typeof process === "undefined" || typeof process.once !== "function") return;
2249
+ state.handlerRegistered = true;
2250
+ process.once("beforeExit", () => {
2251
+ void flushAll();
2252
+ });
2253
+ }
2254
+ function registerClientFlusher(flush) {
2255
+ const state = getState2();
2256
+ state.clientFlushers.add(flush);
2257
+ ensureExitHandler(state);
2258
+ return () => {
2259
+ state.clientFlushers.delete(flush);
2260
+ };
2261
+ }
2262
+ function markPipelineForExitFlush() {
2263
+ const state = getState2();
2264
+ state.pipelineFlushNeeded = true;
2265
+ ensureExitHandler(state);
2266
+ }
2267
+ async function flushAll() {
2268
+ const state = getState2();
2269
+ const tasks = [];
2270
+ for (const flush of state.clientFlushers) {
2271
+ tasks.push(Promise.resolve().then(flush).catch(() => {
2272
+ }));
2273
+ }
2274
+ if (state.pipelineFlushNeeded) {
2275
+ tasks.push(
2276
+ Promise.resolve().then(() => traceloop3.forceFlush()).catch(() => {
2277
+ })
2278
+ );
2279
+ }
2280
+ await Promise.all(tasks);
2281
+ }
2282
+
1824
2283
  // src/tracing/tracer-core.ts
1825
2284
  if (localDebuggerEnabled()) {
1826
2285
  if (!process.env.OTEL_EXPORTER_OTLP_PROTOCOL) {
@@ -1929,6 +2388,29 @@ function resolveLocalDebuggerExporterBaseUrl(baseUrlOpt) {
1929
2388
  function createLocalDebuggerOtlpExporter(config) {
1930
2389
  return new import_exporter_trace_otlp_http.OTLPTraceExporter(config);
1931
2390
  }
2391
+ function parseTraceloopHeaders(value) {
2392
+ const headers = {};
2393
+ for (const entry of value.split(",")) {
2394
+ const keyPairPart = entry.split(";")[0];
2395
+ const sep = keyPairPart.indexOf("=");
2396
+ if (sep <= 0) continue;
2397
+ try {
2398
+ const key = decodeURIComponent(keyPairPart.slice(0, sep).trim());
2399
+ const val = decodeURIComponent(keyPairPart.slice(sep + 1).trim());
2400
+ if (key.length > 0 && val.length > 0) headers[key] = val;
2401
+ } catch (e) {
2402
+ }
2403
+ }
2404
+ return headers;
2405
+ }
2406
+ function traceloopDefaultHeaders(writeKey) {
2407
+ const env = process.env.TRACELOOP_HEADERS;
2408
+ if (env) {
2409
+ const parsed = parseTraceloopHeaders(env);
2410
+ if (Object.keys(parsed).length > 0) return parsed;
2411
+ }
2412
+ return { Authorization: `Bearer ${writeKey}` };
2413
+ }
1932
2414
  function createLocalDebuggerSpanProcessor(options) {
1933
2415
  const localDebuggerUrl = resolveLocalDebuggerExporterBaseUrl(options.localDebuggerUrlOpt);
1934
2416
  if (!localDebuggerUrl) {
@@ -1939,7 +2421,7 @@ function createLocalDebuggerSpanProcessor(options) {
1939
2421
  headers: {}
1940
2422
  });
1941
2423
  try {
1942
- return traceloop3.createSpanProcessor({
2424
+ return traceloop4.createSpanProcessor({
1943
2425
  apiKey: options.writeKey,
1944
2426
  baseUrl: localDebuggerUrl,
1945
2427
  disableBatch: options.disableBatching,
@@ -1955,12 +2437,6 @@ function createLocalDebuggerSpanProcessor(options) {
1955
2437
  return void 0;
1956
2438
  }
1957
2439
  }
1958
- function mergeWithBestEffortLocalDebuggerProcessor(processor, localDebuggerProcessor) {
1959
- if (!localDebuggerProcessor) {
1960
- return processor;
1961
- }
1962
- return new CompositeSpanProcessor(processor ? [processor] : [], [localDebuggerProcessor]);
1963
- }
1964
2440
  var warnedSpanNormalizeFailure = false;
1965
2441
  var INVALID_OTEL_SPAN_ID = "0000000000000000";
1966
2442
  function normalizeSpanForV1Exporter(span) {
@@ -2260,13 +2736,12 @@ function createManualInstrumentations(instrumentModules) {
2260
2736
  }
2261
2737
  return instrumentations;
2262
2738
  }
2263
- var activeInteractions = new import_weakref.WeakValueMap();
2264
- var activeInteractionsByEventId = new import_weakref.WeakValueMap();
2265
2739
  function getCurrentTraceId() {
2266
2740
  var _a;
2267
2741
  return (_a = import_api4.trace.getSpan(import_api4.context.active())) == null ? void 0 : _a.spanContext().traceId;
2268
2742
  }
2269
2743
  var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2744
+ var _a;
2270
2745
  process.env.TRACELOOP_TELEMETRY = "false";
2271
2746
  const {
2272
2747
  logLevel,
@@ -2281,12 +2756,15 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2281
2756
  ...otherOptions
2282
2757
  } = options;
2283
2758
  const cloudEnabledResolved = cloudEnabled != null ? cloudEnabled : true;
2759
+ const authHint = authHintForKey(writeKey);
2760
+ const clientTracingExports = !useExternalOtel && cloudEnabledResolved && tracingEnabled !== false;
2761
+ registerClientKey(authHint, clientTracingExports);
2284
2762
  const withProjectIdHeaders = (existing) => {
2285
2763
  if (!projectId) {
2286
2764
  return existing;
2287
2765
  }
2288
2766
  return {
2289
- Authorization: `Bearer ${writeKey}`,
2767
+ ...traceloopDefaultHeaders(writeKey),
2290
2768
  ...existing != null ? existing : {},
2291
2769
  ...projectIdHeaders(projectId)
2292
2770
  };
@@ -2308,9 +2786,12 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2308
2786
  projectId
2309
2787
  }) : void 0;
2310
2788
  let traceloopInitialized = false;
2789
+ let claimedPipeline = false;
2790
+ const activeInteractions = new import_weakref.WeakValueMap();
2791
+ const activeInteractionsByEventId = new import_weakref.WeakValueMap();
2311
2792
  try {
2312
2793
  if (useExternalOtel) {
2313
- traceloop3.initialize({
2794
+ traceloop4.initialize({
2314
2795
  baseUrl: apiUrl,
2315
2796
  apiKey: writeKey,
2316
2797
  logLevel,
@@ -2322,6 +2803,7 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2322
2803
  // DO NOT create NodeSDK - customer has their own
2323
2804
  });
2324
2805
  traceloopInitialized = true;
2806
+ markTraceloopConfiguredWithoutClaim();
2325
2807
  if (isDebug) {
2326
2808
  console.log(
2327
2809
  "[raindrop] External OTEL mode active. Add raindrop.createSpanProcessor() to your NodeSDK's spanProcessors. Register instrumentations (AnthropicInstrumentation, etc.) on your NodeSDK."
@@ -2338,12 +2820,12 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2338
2820
  const localDebuggerProcessor = createLocalDebuggerSpanProcessor({
2339
2821
  writeKey,
2340
2822
  disableBatching,
2341
- allowedInstrumentationLibraries: traceloop3.ALL_INSTRUMENTATION_LIBRARIES,
2823
+ allowedInstrumentationLibraries: traceloop4.ALL_INSTRUMENTATION_LIBRARIES,
2342
2824
  localDebuggerUrlOpt: localDebuggerUrl
2343
2825
  });
2344
2826
  if (!cloudEnabledResolved) {
2345
2827
  if (localDebuggerProcessor) {
2346
- traceloop3.initialize({
2828
+ traceloop4.initialize({
2347
2829
  baseUrl: apiUrl,
2348
2830
  apiKey: writeKey,
2349
2831
  ...otherOptions,
@@ -2354,8 +2836,9 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2354
2836
  processor: localDebuggerProcessor
2355
2837
  });
2356
2838
  traceloopInitialized = true;
2839
+ markTraceloopConfiguredWithoutClaim();
2357
2840
  } else {
2358
- traceloop3.initialize({
2841
+ traceloop4.initialize({
2359
2842
  baseUrl: apiUrl,
2360
2843
  apiKey: writeKey,
2361
2844
  logLevel,
@@ -2363,33 +2846,91 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2363
2846
  tracingEnabled: false
2364
2847
  });
2365
2848
  traceloopInitialized = true;
2849
+ markTraceloopConfiguredWithoutClaim();
2366
2850
  }
2851
+ } else if (tracingEnabled === false) {
2852
+ traceloopInitialized = false;
2367
2853
  } else {
2368
- const processor = mergeWithBestEffortLocalDebuggerProcessor(
2369
- otherOptions.processor,
2370
- localDebuggerProcessor
2371
- );
2372
- const headers = withProjectIdHeaders(otherOptions.headers);
2373
- traceloop3.initialize({
2374
- baseUrl: apiUrl,
2375
- apiKey: writeKey,
2376
- ...otherOptions,
2377
- logLevel,
2378
- silenceInitializationMessage: true,
2379
- tracingEnabled: tracingEnabled !== false,
2380
- disableBatch: disableBatching,
2381
- ...processor ? { processor } : {},
2382
- ...headers ? { headers } : {}
2383
- });
2384
- traceloopInitialized = true;
2854
+ const claimed = claimPipeline(authHint, projectId);
2855
+ claimedPipeline = claimed;
2856
+ const ownerHint = pipelineOwnerHint();
2857
+ if (!claimed && authHint !== ownerHint) {
2858
+ markForeignClient();
2859
+ console.warn(
2860
+ "[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."
2861
+ );
2862
+ }
2863
+ if (claimed && traceloopConfiguredWithoutClaim()) {
2864
+ releasePipelineClaim();
2865
+ claimedPipeline = false;
2866
+ console.warn(
2867
+ "[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."
2868
+ );
2869
+ } else if (claimed) {
2870
+ const exporterHeaders = (_a = withProjectIdHeaders(otherOptions.headers)) != null ? _a : traceloopDefaultHeaders(writeKey);
2871
+ let guardedExporter;
2872
+ try {
2873
+ const callerExporter = otherOptions.exporter;
2874
+ guardedExporter = callerExporter ? new GuardedSpanExporter(callerExporter, ownerHint) : buildGuardedExporter(apiUrl, exporterHeaders, ownerHint);
2875
+ } catch (guardError) {
2876
+ releasePipelineClaim();
2877
+ claimedPipeline = false;
2878
+ console.warn(
2879
+ `[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.`
2880
+ );
2881
+ }
2882
+ if (guardedExporter) {
2883
+ const contextProcessor = new RaindropContextSpanProcessor();
2884
+ const callerProcessor = otherOptions.processor;
2885
+ const processor = new CompositeSpanProcessor(
2886
+ callerProcessor ? [contextProcessor, callerProcessor] : [contextProcessor],
2887
+ localDebuggerProcessor ? [localDebuggerProcessor] : []
2888
+ );
2889
+ try {
2890
+ traceloop4.initialize({
2891
+ baseUrl: apiUrl,
2892
+ apiKey: writeKey,
2893
+ ...otherOptions,
2894
+ logLevel,
2895
+ silenceInitializationMessage: true,
2896
+ // This branch only runs when tracing is enabled (the disabled
2897
+ // case is handled above), so tracing is always on here.
2898
+ tracingEnabled: true,
2899
+ disableBatch: disableBatching,
2900
+ processor,
2901
+ exporter: guardedExporter
2902
+ });
2903
+ traceloopInitialized = true;
2904
+ } catch (initError) {
2905
+ releasePipelineClaim();
2906
+ claimedPipeline = false;
2907
+ throw initError;
2908
+ }
2909
+ }
2910
+ } else {
2911
+ traceloopInitialized = true;
2912
+ }
2385
2913
  }
2386
2914
  }
2387
2915
  } catch (error) {
2916
+ if (claimedPipeline && !traceloopInitialized) {
2917
+ releasePipelineClaim();
2918
+ claimedPipeline = false;
2919
+ }
2388
2920
  console.warn(
2389
2921
  "[raindrop] Failed to initialize traceloop. Spans will not be sent.",
2390
2922
  error instanceof Error ? error.message : error
2391
2923
  );
2392
2924
  }
2925
+ if (traceloopInitialized) {
2926
+ markPipelineForExitFlush();
2927
+ }
2928
+ const deregisterExitFlusher = directShipper ? registerClientFlusher(async () => {
2929
+ try {
2930
+ await directShipper.flush();
2931
+ } catch (e) {
2932
+ }
2933
+ }) : void 0;
2393
2934
  return {
2394
2935
  /**
2395
2936
  * Returns instrumentation instances for your NodeSDK.
@@ -2477,7 +3018,7 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2477
3018
  );
2478
3019
  }
2479
3020
  const headers = withProjectIdHeaders(processorOptions == null ? void 0 : processorOptions.headers);
2480
- const productionProcessor = traceloop3.createSpanProcessor({
3021
+ const productionProcessor = traceloop4.createSpanProcessor({
2481
3022
  baseUrl: apiUrl,
2482
3023
  apiKey: writeKey,
2483
3024
  disableBatch: disableBatching,
@@ -2490,12 +3031,16 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2490
3031
  allowedInstrumentationLibraries: processorOptions == null ? void 0 : processorOptions.allowedInstrumentationLibraries,
2491
3032
  localDebuggerUrlOpt: localDebuggerUrl
2492
3033
  });
3034
+ const contextProcessor = new RaindropContextSpanProcessor();
2493
3035
  return withV2SpanCompat(
2494
- mergeWithBestEffortLocalDebuggerProcessor(productionProcessor, localDebuggerProcessor)
3036
+ new CompositeSpanProcessor(
3037
+ [contextProcessor, productionProcessor],
3038
+ localDebuggerProcessor ? [localDebuggerProcessor] : []
3039
+ )
2495
3040
  );
2496
3041
  },
2497
3042
  begin(traceContext) {
2498
- var _a;
3043
+ var _a2;
2499
3044
  if (!traceContext.eventId) {
2500
3045
  traceContext.eventId = crypto.randomUUID();
2501
3046
  }
@@ -2504,19 +3049,27 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2504
3049
  traceContext,
2505
3050
  traceId,
2506
3051
  analytics,
2507
- directShipper
3052
+ directShipper,
3053
+ { projectId, authHint }
2508
3054
  );
2509
- analytics._trackAiPartial({
2510
- ...traceContext,
2511
- properties: {
2512
- ...(_a = traceContext.properties) != null ? _a : {},
2513
- ...interaction.getTraceId() ? { trace_id: interaction.getTraceId() } : {}
3055
+ const bound = bindRoutingContext({ projectId, authHint }, interaction);
3056
+ interaction.setBoundContext(bound);
3057
+ try {
3058
+ analytics._trackAiPartial({
3059
+ ...traceContext,
3060
+ properties: {
3061
+ ...(_a2 = traceContext.properties) != null ? _a2 : {},
3062
+ ...interaction.getTraceId() ? { trace_id: interaction.getTraceId() } : {}
3063
+ }
3064
+ });
3065
+ if (interaction.getTraceId()) {
3066
+ activeInteractions.set(interaction.getTraceId(), interaction);
2514
3067
  }
2515
- });
2516
- if (interaction.getTraceId()) {
2517
- activeInteractions.set(interaction.getTraceId(), interaction);
3068
+ activeInteractionsByEventId.set(traceContext.eventId, interaction);
3069
+ } catch (error) {
3070
+ unbindRoutingContext(bound);
3071
+ throw error;
2518
3072
  }
2519
- activeInteractionsByEventId.set(traceContext.eventId, interaction);
2520
3073
  return interaction;
2521
3074
  },
2522
3075
  getActiveInteraction(eventId) {
@@ -2531,12 +3084,15 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2531
3084
  return interactionByTraceId;
2532
3085
  }
2533
3086
  }
2534
- const interaction = new LiveInteraction({ eventId }, traceId, analytics, directShipper);
3087
+ const interaction = new LiveInteraction({ eventId }, traceId, analytics, directShipper, {
3088
+ projectId,
3089
+ authHint
3090
+ });
2535
3091
  activeInteractionsByEventId.set(eventId, interaction);
2536
3092
  return interaction;
2537
3093
  },
2538
3094
  tracer(globalProperties) {
2539
- return new LiveTracer(globalProperties, directShipper);
3095
+ return new LiveTracer(globalProperties, directShipper, { projectId, authHint });
2540
3096
  },
2541
3097
  /**
2542
3098
  * Best-effort drain of in-flight OTel spans and direct shipper batches.
@@ -2551,18 +3107,19 @@ var tracing = (analytics, apiUrl, writeKey, options, isDebug = false) => {
2551
3107
  }
2552
3108
  if (traceloopInitialized) {
2553
3109
  try {
2554
- await traceloop3.forceFlush();
3110
+ await traceloop4.forceFlush();
2555
3111
  } catch (e) {
2556
3112
  }
2557
3113
  }
2558
3114
  },
2559
3115
  async close() {
3116
+ deregisterExitFlusher == null ? void 0 : deregisterExitFlusher();
2560
3117
  activeInteractions.clear();
2561
3118
  activeInteractionsByEventId.clear();
2562
3119
  await (directShipper == null ? void 0 : directShipper.shutdown());
2563
3120
  if (traceloopInitialized) {
2564
3121
  try {
2565
- await traceloop3.forceFlush();
3122
+ await traceloop4.forceFlush();
2566
3123
  } catch (e) {
2567
3124
  }
2568
3125
  }