@glassflow-ai/rius 0.1.0 → 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.
package/README.md CHANGED
@@ -233,6 +233,11 @@ variables, then the defaults below.
233
233
  | `sampleRate` | `RIUS_SAMPLE_RATE` | `1.0` |
234
234
  | `captureContent` | `RIUS_CAPTURE_CONTENT` | `true` |
235
235
  | `mask` | (options only) | none |
236
+ | `heartbeat` | `RIUS_HEARTBEAT` | `true` |
237
+ | `heartbeatInterval` | `RIUS_HEARTBEAT_INTERVAL` | `15` (seconds; clamped 5-300) |
238
+ | `agentName` | `RIUS_AGENT_NAME` | `serviceName` |
239
+ | `partialSpans` | `RIUS_PARTIAL_SPANS` | `false` |
240
+ | `partialSpansDelay` | `RIUS_PARTIAL_SPANS_DELAY` | `0` (seconds; clamped 0-60) |
236
241
 
237
242
  Traces are posted to `<endpoint>/v1/traces`.
238
243
 
@@ -255,6 +260,50 @@ optional integration is loaded, so no third-party module is patched in your
255
260
  process. `client.ready` resolves to an empty list. Spans can still be
256
261
  created and are simply dropped.
257
262
 
263
+ ## Agent liveness
264
+
265
+ Traces only leave the process when a span ends, so an idle-but-alive agent
266
+ and a crashed one look identical from the outside. Two independent
267
+ mechanisms close that gap: a heartbeat that reports the process is still
268
+ running, and partial spans that leave a durable trace of in-flight work if
269
+ the process disappears before finishing it.
270
+
271
+ ### Heartbeat
272
+
273
+ With `heartbeat: true` (the default), the SDK posts to `<endpoint>/v1/heartbeat`
274
+ for the life of the process: an immediate first ping when `init()` runs,
275
+ then one every `heartbeatInterval` seconds (default 15, clamped to 5-300).
276
+ Each ping reports the count of currently open root spans, which is what
277
+ lets the backend tell a live-but-idle agent from one that has vanished.
278
+
279
+ The final ping, carrying `stopped: true`, is sent by `client.shutdown()`.
280
+ The SDK also makes a best-effort attempt to send it when the process ends
281
+ naturally, on Node's `beforeExit` event. An application that never calls
282
+ `shutdown()` and exits abruptly, killed, crashed, or via `process.exit()`,
283
+ misses that last ping and reports as gone rather than stopped, the same way
284
+ a hard-killed agent looks in any language. Call `client.shutdown()` on a
285
+ graceful exit if you want that distinction to show up.
286
+
287
+ The heartbeat never keeps the process alive, its timer is unref'd, and never
288
+ throws into your code; delivery failures are logged once per client and
289
+ silent after that. `agentName` identifies the agent in heartbeat payloads
290
+ and defaults to `serviceName`.
291
+
292
+ ### Partial spans
293
+
294
+ With `partialSpans: true` (default `false`), every sampled span additionally
295
+ exports a content-free snapshot the moment it starts, on top of the normal
296
+ export when it ends. The snapshot carries only identity and taxonomy
297
+ attributes, trace id, span id, name and timestamps, never content, whatever
298
+ instrumentation set on the span. The backend stores it as an unfinished row
299
+ and replaces it with the real span at end, so a snapshot that is never
300
+ replaced is the durable record of what a crashed agent was doing.
301
+
302
+ `partialSpansDelay` (default `0`, clamped to 0-60 seconds) debounces the
303
+ snapshot: it is held for that long and only sent if the span is still open
304
+ once the delay elapses, so a span that finishes quickly costs zero network
305
+ calls.
306
+
258
307
  ## Reliability
259
308
 
260
309
  Export is designed to never block or crash your application:
package/dist/index.cjs CHANGED
@@ -20,6 +20,16 @@ var __copyProps = (to, from, except, desc) => {
20
20
  };
21
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
22
 
23
+ // node_modules/tsup/assets/cjs_shims.js
24
+ var getImportMetaUrl, importMetaUrl;
25
+ var init_cjs_shims = __esm({
26
+ "node_modules/tsup/assets/cjs_shims.js"() {
27
+ "use strict";
28
+ getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
29
+ importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
30
+ }
31
+ });
32
+
23
33
  // src/config.ts
24
34
  function bool(name, raw, fallback) {
25
35
  if (raw === void 0) return fallback;
@@ -37,24 +47,64 @@ function rate(raw, fallback) {
37
47
  if (Number.isNaN(parsed)) return fallback;
38
48
  return Math.min(1, Math.max(0, parsed));
39
49
  }
50
+ function seconds(raw, fallback) {
51
+ if (raw === void 0) return fallback;
52
+ const parsed = Number.parseFloat(raw);
53
+ if (Number.isNaN(parsed)) return fallback;
54
+ return parsed;
55
+ }
56
+ function clamp(name, value, min, max) {
57
+ if (value >= min && value <= max) return value;
58
+ const clamped = Math.min(Math.max(value, min), max);
59
+ console.warn(`[rius] ${name}=${value} is outside [${min}, ${max}]; clamped to ${clamped}.`);
60
+ return clamped;
61
+ }
40
62
  function resolveConfig(options = {}, env = process.env) {
41
63
  const endpoint = (options.endpoint ?? env.RIUS_ENDPOINT ?? DEFAULT_ENDPOINT).replace(/\/+$/, "");
64
+ const serviceName = options.serviceName ?? env.RIUS_SERVICE_NAME ?? DEFAULT_SERVICE_NAME;
65
+ const heartbeatIntervalSeconds = clamp(
66
+ "heartbeatInterval",
67
+ options.heartbeatInterval ?? seconds(env.RIUS_HEARTBEAT_INTERVAL, DEFAULT_HEARTBEAT_INTERVAL),
68
+ HEARTBEAT_INTERVAL_MIN,
69
+ HEARTBEAT_INTERVAL_MAX
70
+ );
71
+ const partialSpansDelaySeconds = clamp(
72
+ "partialSpansDelay",
73
+ options.partialSpansDelay ?? seconds(env.RIUS_PARTIAL_SPANS_DELAY, DEFAULT_PARTIAL_SPANS_DELAY),
74
+ PARTIAL_SPANS_DELAY_MIN,
75
+ PARTIAL_SPANS_DELAY_MAX
76
+ );
42
77
  return {
43
78
  endpoint,
44
79
  apiKey: options.apiKey ?? env.RIUS_API_KEY,
45
- serviceName: options.serviceName ?? env.RIUS_SERVICE_NAME ?? DEFAULT_SERVICE_NAME,
80
+ serviceName,
46
81
  disabled: options.disabled ?? bool("RIUS_DISABLED", env.RIUS_DISABLED, false),
47
82
  sampleRate: options.sampleRate ?? rate(env.RIUS_SAMPLE_RATE, 1),
48
83
  captureContent: options.captureContent ?? bool("RIUS_CAPTURE_CONTENT", env.RIUS_CAPTURE_CONTENT, true),
49
- mask: options.mask
84
+ mask: options.mask,
85
+ heartbeat: options.heartbeat ?? bool("RIUS_HEARTBEAT", env.RIUS_HEARTBEAT, true),
86
+ heartbeatIntervalMs: heartbeatIntervalSeconds * 1e3,
87
+ heartbeatEndpoint: `${endpoint}/v1/heartbeat`,
88
+ // Empty falls through to serviceName rather than shipping a blank identity
89
+ // on every heartbeat payload, so `??` would be wrong here.
90
+ agentName: options.agentName || env.RIUS_AGENT_NAME || serviceName,
91
+ partialSpans: options.partialSpans ?? bool("RIUS_PARTIAL_SPANS", env.RIUS_PARTIAL_SPANS, false),
92
+ partialSpansDelayMs: partialSpansDelaySeconds * 1e3
50
93
  };
51
94
  }
52
- var DEFAULT_ENDPOINT, DEFAULT_SERVICE_NAME, AFFIRMATIVE, NEGATIVE;
95
+ var DEFAULT_ENDPOINT, DEFAULT_SERVICE_NAME, HEARTBEAT_INTERVAL_MIN, HEARTBEAT_INTERVAL_MAX, DEFAULT_HEARTBEAT_INTERVAL, PARTIAL_SPANS_DELAY_MIN, PARTIAL_SPANS_DELAY_MAX, DEFAULT_PARTIAL_SPANS_DELAY, AFFIRMATIVE, NEGATIVE;
53
96
  var init_config = __esm({
54
97
  "src/config.ts"() {
55
98
  "use strict";
99
+ init_cjs_shims();
56
100
  DEFAULT_ENDPOINT = "https://ingest.eu.console.rius-glassflow.com";
57
101
  DEFAULT_SERVICE_NAME = "unknown_service";
102
+ HEARTBEAT_INTERVAL_MIN = 5;
103
+ HEARTBEAT_INTERVAL_MAX = 300;
104
+ DEFAULT_HEARTBEAT_INTERVAL = 15;
105
+ PARTIAL_SPANS_DELAY_MIN = 0;
106
+ PARTIAL_SPANS_DELAY_MAX = 60;
107
+ DEFAULT_PARTIAL_SPANS_DELAY = 0;
58
108
  AFFIRMATIVE = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
59
109
  NEGATIVE = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
60
110
  }
@@ -65,6 +115,7 @@ var DelegatingSpanProcessor;
65
115
  var init_delegatingProcessor = __esm({
66
116
  "src/delegatingProcessor.ts"() {
67
117
  "use strict";
118
+ init_cjs_shims();
68
119
  DelegatingSpanProcessor = class {
69
120
  delegates = [];
70
121
  add(processor) {
@@ -102,6 +153,7 @@ var ExportResultCode;
102
153
  var init_ExportResult = __esm({
103
154
  "node_modules/@opentelemetry/core/build/esm/ExportResult.js"() {
104
155
  "use strict";
156
+ init_cjs_shims();
105
157
  (function(ExportResultCode2) {
106
158
  ExportResultCode2[ExportResultCode2["SUCCESS"] = 0] = "SUCCESS";
107
159
  ExportResultCode2[ExportResultCode2["FAILED"] = 1] = "FAILED";
@@ -113,6 +165,7 @@ var init_ExportResult = __esm({
113
165
  var init_esm = __esm({
114
166
  "node_modules/@opentelemetry/core/build/esm/index.js"() {
115
167
  "use strict";
168
+ init_cjs_shims();
116
169
  init_ExportResult();
117
170
  }
118
171
  });
@@ -122,6 +175,7 @@ var ExportOutcomeExporter;
122
175
  var init_exportHealth = __esm({
123
176
  "src/exportHealth.ts"() {
124
177
  "use strict";
178
+ init_cjs_shims();
125
179
  init_esm();
126
180
  ExportOutcomeExporter = class {
127
181
  constructor(inner, onFirstFailure = (m) => console.warn(m)) {
@@ -164,6 +218,134 @@ var init_exportHealth = __esm({
164
218
  }
165
219
  });
166
220
 
221
+ // src/heartbeat.ts
222
+ function httpTransport(url, headers) {
223
+ return async (payload, timeoutMs) => {
224
+ const response = await fetch(url, {
225
+ method: "POST",
226
+ headers: { "content-type": "application/json", ...headers },
227
+ body: JSON.stringify(payload),
228
+ signal: AbortSignal.timeout(timeoutMs)
229
+ });
230
+ await response.body?.cancel().catch(() => {
231
+ });
232
+ if (!response.ok) {
233
+ throw new Error(`heartbeat POST ${url} returned ${response.status}`);
234
+ }
235
+ };
236
+ }
237
+ var import_node_crypto, import_node_module, PAYLOAD_VERSION, OPEN_TRACES_CAP, DEFAULT_PING_TIMEOUT_MS, DEFAULT_FINAL_PING_TIMEOUT_MS, SDK_VERSION, OpenRootSpanTracker, HeartbeatSender;
238
+ var init_heartbeat = __esm({
239
+ "src/heartbeat.ts"() {
240
+ "use strict";
241
+ init_cjs_shims();
242
+ import_node_crypto = require("crypto");
243
+ import_node_module = require("module");
244
+ PAYLOAD_VERSION = 1;
245
+ OPEN_TRACES_CAP = 32;
246
+ DEFAULT_PING_TIMEOUT_MS = 3e3;
247
+ DEFAULT_FINAL_PING_TIMEOUT_MS = 1e3;
248
+ SDK_VERSION = (() => {
249
+ try {
250
+ const pkg = (0, import_node_module.createRequire)(importMetaUrl)("../package.json");
251
+ return pkg.version ?? "0.0.0";
252
+ } catch {
253
+ return "0.0.0";
254
+ }
255
+ })();
256
+ OpenRootSpanTracker = class {
257
+ // trace id (32-hex) -> count of open root spans in that trace. A trace id
258
+ // normally has one root, but the count guards against duplicates.
259
+ open = /* @__PURE__ */ new Map();
260
+ onStart(span, _parentContext) {
261
+ if (span.parentSpanContext !== void 0) return;
262
+ const traceId = span.spanContext().traceId;
263
+ this.open.set(traceId, (this.open.get(traceId) ?? 0) + 1);
264
+ }
265
+ onEnd(span) {
266
+ if (span.parentSpanContext !== void 0) return;
267
+ const traceId = span.spanContext().traceId;
268
+ const count = (this.open.get(traceId) ?? 0) - 1;
269
+ if (count <= 0) {
270
+ this.open.delete(traceId);
271
+ } else {
272
+ this.open.set(traceId, count);
273
+ }
274
+ }
275
+ /** Trace ids of currently-open root spans, in the order they opened. */
276
+ openTraceIds() {
277
+ return [...this.open.keys()];
278
+ }
279
+ async forceFlush() {
280
+ }
281
+ async shutdown() {
282
+ }
283
+ };
284
+ HeartbeatSender = class {
285
+ /** Identity of one process lifetime, fresh per sender. */
286
+ instanceId = (0, import_node_crypto.randomUUID)();
287
+ agentName;
288
+ tracker;
289
+ intervalMs;
290
+ pingTimeoutMs;
291
+ finalPingTimeoutMs;
292
+ send;
293
+ timer;
294
+ stopped = false;
295
+ deliveryWarned = false;
296
+ constructor(opts) {
297
+ this.agentName = opts.agentName;
298
+ this.tracker = opts.tracker;
299
+ this.intervalMs = opts.intervalMs;
300
+ this.pingTimeoutMs = opts.pingTimeoutMs ?? DEFAULT_PING_TIMEOUT_MS;
301
+ this.finalPingTimeoutMs = opts.finalPingTimeoutMs ?? DEFAULT_FINAL_PING_TIMEOUT_MS;
302
+ this.send = opts.transport ?? httpTransport(opts.url, opts.headers);
303
+ }
304
+ /** Starts pinging: an immediate first ping, then every `intervalMs`. */
305
+ start() {
306
+ void this.ping(this.pingTimeoutMs, false);
307
+ const timer = setInterval(() => void this.ping(this.pingTimeoutMs, false), this.intervalMs);
308
+ if (typeof timer.unref === "function") timer.unref();
309
+ this.timer = timer;
310
+ }
311
+ /** Stops the interval and sends the final `stopped: true` ping. Idempotent. */
312
+ async stop() {
313
+ if (this.stopped) return;
314
+ this.stopped = true;
315
+ if (this.timer !== void 0) clearInterval(this.timer);
316
+ await this.ping(this.finalPingTimeoutMs, true);
317
+ }
318
+ buildPayload(stopped) {
319
+ const openIds = this.tracker.openTraceIds();
320
+ const payload = {
321
+ v: PAYLOAD_VERSION,
322
+ instance_id: this.instanceId,
323
+ agent_name: this.agentName,
324
+ // RFC3339 UTC with millisecond precision and a Z suffix, already.
325
+ sent_at: (/* @__PURE__ */ new Date()).toISOString(),
326
+ sdk_language: "typescript",
327
+ sdk_version: SDK_VERSION,
328
+ open_traces: openIds.slice(0, OPEN_TRACES_CAP),
329
+ open_trace_count: openIds.length
330
+ };
331
+ if (stopped) payload.stopped = true;
332
+ return payload;
333
+ }
334
+ async ping(timeoutMs, stopped) {
335
+ try {
336
+ await this.send(this.buildPayload(stopped), timeoutMs);
337
+ } catch (error) {
338
+ if (!this.deliveryWarned) {
339
+ this.deliveryWarned = true;
340
+ const message = error instanceof Error ? error.message : String(error);
341
+ console.warn(`[rius] heartbeat delivery failed (${message}); further failures are silent.`);
342
+ }
343
+ }
344
+ }
345
+ };
346
+ }
347
+ });
348
+
167
349
  // src/semconv.ts
168
350
  function kindAttributes(kind) {
169
351
  const attributes = { [OPENINFERENCE_SPAN_KIND]: kind };
@@ -171,10 +353,11 @@ function kindAttributes(kind) {
171
353
  if (operation !== void 0) attributes[GEN_AI_OPERATION_NAME] = operation;
172
354
  return attributes;
173
355
  }
174
- var TRACER_NAME, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES;
356
+ var TRACER_NAME, OPENINFERENCE_SPAN_KIND, INPUT_VALUE, OUTPUT_VALUE, GEN_AI_OPERATION_NAME, GEN_AI_PROVIDER_NAME, GEN_AI_REQUEST_MODEL, GEN_AI_RESPONSE_MODEL, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, GEN_AI_INPUT_MESSAGES, GEN_AI_OUTPUT_MESSAGES, GEN_AI_RESPONSE_FINISH_REASONS, GEN_AI_TOOL_NAME, GEN_AI_REQUEST_PREFIX, MCP_RESULT_TYPE, GEN_AI_FIRST_TOKEN_EVENT, SpanKind, OPERATION_BY_KIND, CONTENT_ATTRIBUTES, CONTENT_ATTRIBUTE_PREFIXES, CONTENT_ATTRIBUTE_SUFFIXES, GLASSFLOW_SPAN_PENDING, PENDING_IDENTITY_ATTRIBUTES, PENDING_IDENTITY_PREFIXES;
175
357
  var init_semconv = __esm({
176
358
  "src/semconv.ts"() {
177
359
  "use strict";
360
+ init_cjs_shims();
178
361
  TRACER_NAME = "glassflow";
179
362
  OPENINFERENCE_SPAN_KIND = "openinference.span.kind";
180
363
  INPUT_VALUE = "input.value";
@@ -239,6 +422,14 @@ var init_semconv = __esm({
239
422
  ".document.content",
240
423
  ".embedding.text"
241
424
  ];
425
+ GLASSFLOW_SPAN_PENDING = "glassflow.span.pending";
426
+ PENDING_IDENTITY_ATTRIBUTES = /* @__PURE__ */ new Set([
427
+ OPENINFERENCE_SPAN_KIND,
428
+ GEN_AI_OPERATION_NAME,
429
+ GEN_AI_PROVIDER_NAME,
430
+ GEN_AI_TOOL_NAME
431
+ ]);
432
+ PENDING_IDENTITY_PREFIXES = [GEN_AI_REQUEST_PREFIX];
242
433
  }
243
434
  });
244
435
 
@@ -267,6 +458,7 @@ function toAttributeValue(value) {
267
458
  var init_serde = __esm({
268
459
  "src/serde.ts"() {
269
460
  "use strict";
461
+ init_cjs_shims();
270
462
  }
271
463
  });
272
464
 
@@ -303,6 +495,7 @@ var import_api, Observation;
303
495
  var init_spans = __esm({
304
496
  "src/spans.ts"() {
305
497
  "use strict";
498
+ init_cjs_shims();
306
499
  import_api = require("@opentelemetry/api");
307
500
  init_client();
308
501
  init_semconv();
@@ -396,6 +589,7 @@ var INPUT_REQUIRED;
396
589
  var init_instrumentationMcp = __esm({
397
590
  "src/instrumentationMcp.ts"() {
398
591
  "use strict";
592
+ init_cjs_shims();
399
593
  init_semconv();
400
594
  init_spans();
401
595
  INPUT_REQUIRED = "input_required";
@@ -446,7 +640,7 @@ function warnBroken(subject, error) {
446
640
  function cachedCjsExports(pkg, toPatchable) {
447
641
  let cache;
448
642
  try {
449
- cache = (0, import_node_module.createRequire)((0, import_node_path.join)(process.cwd(), "noop.js")).cache;
643
+ cache = (0, import_node_module2.createRequire)((0, import_node_path.join)(process.cwd(), "noop.js")).cache;
450
644
  } catch {
451
645
  return void 0;
452
646
  }
@@ -511,11 +705,12 @@ async function enableInstrumentations(sink, tracerProvider, names) {
511
705
  }
512
706
  return enabled;
513
707
  }
514
- var import_node_module, import_node_path, import_instrumentation, dynamicImport, REGISTRY;
708
+ var import_node_module2, import_node_path, import_instrumentation, dynamicImport, REGISTRY;
515
709
  var init_instrumentation = __esm({
516
710
  "src/instrumentation.ts"() {
517
711
  "use strict";
518
- import_node_module = require("module");
712
+ init_cjs_shims();
713
+ import_node_module2 = require("module");
519
714
  import_node_path = require("path");
520
715
  import_instrumentation = require("@opentelemetry/instrumentation");
521
716
  dynamicImport = new Function("s", "return import(s)");
@@ -611,6 +806,7 @@ var import_api2, EXCEPTION_EVENT_NAME, EXCEPTION_CONTENT_KEYS, MaskingSpanExport
611
806
  var init_masking = __esm({
612
807
  "src/masking.ts"() {
613
808
  "use strict";
809
+ init_cjs_shims();
614
810
  import_api2 = require("@opentelemetry/api");
615
811
  init_semconv();
616
812
  init_serde();
@@ -673,6 +869,93 @@ var init_masking = __esm({
673
869
  }
674
870
  });
675
871
 
872
+ // src/pending.ts
873
+ function identityAttributes(attributes) {
874
+ if (!attributes) return {};
875
+ const result = {};
876
+ for (const [key, value] of Object.entries(attributes)) {
877
+ if (PENDING_IDENTITY_ATTRIBUTES.has(key) || PENDING_IDENTITY_PREFIXES.some((p) => key.startsWith(p))) {
878
+ result[key] = value;
879
+ }
880
+ }
881
+ return result;
882
+ }
883
+ function buildSnapshot(span) {
884
+ const attributes = identityAttributes(span.attributes);
885
+ attributes[GLASSFLOW_SPAN_PENDING] = true;
886
+ return {
887
+ name: span.name,
888
+ kind: span.kind,
889
+ spanContext: () => span.spanContext(),
890
+ parentSpanContext: span.parentSpanContext,
891
+ startTime: span.startTime,
892
+ endTime: span.startTime,
893
+ duration: [0, 0],
894
+ status: { code: import_api3.SpanStatusCode.UNSET },
895
+ attributes,
896
+ links: [],
897
+ events: [],
898
+ resource: span.resource,
899
+ instrumentationScope: span.instrumentationScope,
900
+ droppedAttributesCount: 0,
901
+ droppedEventsCount: 0,
902
+ droppedLinksCount: 0,
903
+ ended: true
904
+ };
905
+ }
906
+ function spanKey(context) {
907
+ return `${context.traceId}:${context.spanId}`;
908
+ }
909
+ var import_api3, PendingSpanProcessor;
910
+ var init_pending = __esm({
911
+ "src/pending.ts"() {
912
+ "use strict";
913
+ init_cjs_shims();
914
+ import_api3 = require("@opentelemetry/api");
915
+ init_semconv();
916
+ PendingSpanProcessor = class {
917
+ delegate;
918
+ delayMs;
919
+ scheduled = /* @__PURE__ */ new Map();
920
+ constructor(delegate, opts) {
921
+ this.delegate = delegate;
922
+ this.delayMs = opts?.delayMs ?? 0;
923
+ }
924
+ onStart(span, _parentContext) {
925
+ if (!span.isRecording()) return;
926
+ const snapshot = buildSnapshot(span);
927
+ if (this.delayMs <= 0) {
928
+ this.delegate.onEnd(snapshot);
929
+ return;
930
+ }
931
+ const key = spanKey(span.spanContext());
932
+ const timer = setTimeout(() => {
933
+ this.scheduled.delete(key);
934
+ try {
935
+ this.delegate.onEnd(snapshot);
936
+ } catch {
937
+ }
938
+ }, this.delayMs);
939
+ if (typeof timer.unref === "function") timer.unref();
940
+ this.scheduled.set(key, timer);
941
+ }
942
+ onEnd(span) {
943
+ const key = spanKey(span.spanContext());
944
+ const timer = this.scheduled.get(key);
945
+ if (timer === void 0) return;
946
+ clearTimeout(timer);
947
+ this.scheduled.delete(key);
948
+ }
949
+ async shutdown() {
950
+ for (const timer of this.scheduled.values()) clearTimeout(timer);
951
+ this.scheduled.clear();
952
+ }
953
+ async forceFlush() {
954
+ }
955
+ };
956
+ }
957
+ });
958
+
676
959
  // src/client.ts
677
960
  function init(options = {}) {
678
961
  if (globalClient !== void 0) {
@@ -683,18 +966,23 @@ function init(options = {}) {
683
966
  }
684
967
  const config = resolveConfig(options);
685
968
  const processors = new DelegatingSpanProcessor();
969
+ const authHeaders = config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {};
686
970
  let health;
687
971
  if (!config.disabled) {
688
972
  const base = options.spanExporter ?? new import_exporter_trace_otlp_http.OTLPTraceExporter({
689
973
  url: `${config.endpoint}/v1/traces`,
690
- headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : void 0
974
+ headers: config.apiKey ? authHeaders : void 0
691
975
  });
692
976
  health = new ExportOutcomeExporter(base);
693
977
  const exporter = !config.captureContent || config.mask !== void 0 ? new MaskingSpanExporter(health, {
694
978
  captureContent: config.captureContent,
695
979
  mask: config.mask
696
980
  }) : health;
697
- processors.add(new import_sdk_trace_base.BatchSpanProcessor(exporter));
981
+ const batch = new import_sdk_trace_base.BatchSpanProcessor(exporter);
982
+ if (config.partialSpans) {
983
+ processors.add(new PendingSpanProcessor(batch, { delayMs: config.partialSpansDelayMs }));
984
+ }
985
+ processors.add(batch);
698
986
  }
699
987
  const provider = new import_sdk_trace_node.NodeTracerProvider({
700
988
  resource: (0, import_resources.resourceFromAttributes)({ [import_semantic_conventions.ATTR_SERVICE_NAME]: config.serviceName }),
@@ -708,17 +996,37 @@ function init(options = {}) {
708
996
  });
709
997
  const ready = config.disabled ? Promise.resolve([]) : enableInstrumentations(processors, provider).catch(() => []);
710
998
  provider.register();
711
- globalClient = createClient({ provider, processors, health, ready });
999
+ let heartbeat;
1000
+ if (config.heartbeat && !config.disabled) {
1001
+ const tracker = new OpenRootSpanTracker();
1002
+ processors.add(tracker);
1003
+ const sender = new HeartbeatSender({
1004
+ url: config.heartbeatEndpoint,
1005
+ headers: authHeaders,
1006
+ intervalMs: config.heartbeatIntervalMs,
1007
+ agentName: config.agentName,
1008
+ tracker,
1009
+ transport: options.heartbeatTransport
1010
+ });
1011
+ sender.start();
1012
+ const beforeExitHandler = () => {
1013
+ void sender.stop();
1014
+ };
1015
+ process.once("beforeExit", beforeExitHandler);
1016
+ heartbeat = { sender, beforeExitHandler };
1017
+ }
1018
+ globalClient = createClient({ provider, processors, health, ready, heartbeat });
712
1019
  return globalClient;
713
1020
  }
714
1021
  function getTracer() {
715
- return import_api3.trace.getTracer(TRACER_NAME);
1022
+ return import_api4.trace.getTracer(TRACER_NAME);
716
1023
  }
717
- var import_api3, import_exporter_trace_otlp_http, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, createClient, RiusClient, globalClient;
1024
+ var import_api4, import_exporter_trace_otlp_http, import_resources, import_sdk_trace_base, import_sdk_trace_node, import_semantic_conventions, sinks, heartbeats, createClient, RiusClient, globalClient;
718
1025
  var init_client = __esm({
719
1026
  "src/client.ts"() {
720
1027
  "use strict";
721
- import_api3 = require("@opentelemetry/api");
1028
+ init_cjs_shims();
1029
+ import_api4 = require("@opentelemetry/api");
722
1030
  import_exporter_trace_otlp_http = require("@opentelemetry/exporter-trace-otlp-http");
723
1031
  import_resources = require("@opentelemetry/resources");
724
1032
  import_sdk_trace_base = require("@opentelemetry/sdk-trace-base");
@@ -727,10 +1035,13 @@ var init_client = __esm({
727
1035
  init_config();
728
1036
  init_delegatingProcessor();
729
1037
  init_exportHealth();
1038
+ init_heartbeat();
730
1039
  init_instrumentation();
731
1040
  init_masking();
1041
+ init_pending();
732
1042
  init_semconv();
733
1043
  sinks = /* @__PURE__ */ new WeakMap();
1044
+ heartbeats = /* @__PURE__ */ new WeakMap();
734
1045
  RiusClient = class _RiusClient {
735
1046
  provider;
736
1047
  health;
@@ -741,6 +1052,7 @@ var init_client = __esm({
741
1052
  this.health = parts.health;
742
1053
  this.ready = parts.ready;
743
1054
  sinks.set(this, parts.processors);
1055
+ if (parts.heartbeat) heartbeats.set(this, parts.heartbeat);
744
1056
  }
745
1057
  static {
746
1058
  createClient = (parts) => new _RiusClient(parts);
@@ -753,14 +1065,25 @@ var init_client = __esm({
753
1065
  /**
754
1066
  * Drains and tears down the provider, then releases the global registration
755
1067
  * so a later init() can reconfigure the SDK.
1068
+ *
1069
+ * The heartbeat's final `stopped: true` ping is sent before the provider
1070
+ * shuts down, so the backend hears "stopped" while the trace pipeline can
1071
+ * still export it. Idempotent: `sender.stop()` no-ops on a second call, and
1072
+ * the `beforeExit` listener is removed here so repeated init/shutdown
1073
+ * cycles never leak listeners.
756
1074
  */
757
1075
  async shutdown() {
1076
+ const heartbeat = heartbeats.get(this);
1077
+ if (heartbeat) {
1078
+ process.removeListener("beforeExit", heartbeat.beforeExitHandler);
1079
+ await heartbeat.sender.stop();
1080
+ }
758
1081
  try {
759
1082
  await this.provider.shutdown();
760
1083
  } finally {
761
1084
  if (globalClient === this) {
762
1085
  globalClient = void 0;
763
- import_api3.trace.disable();
1086
+ import_api4.trace.disable();
764
1087
  }
765
1088
  }
766
1089
  }
@@ -785,9 +1108,11 @@ __export(index_exports, {
785
1108
  startSpan: () => startSpan
786
1109
  });
787
1110
  module.exports = __toCommonJS(index_exports);
1111
+ init_cjs_shims();
788
1112
  init_client();
789
1113
 
790
1114
  // src/generation.ts
1115
+ init_cjs_shims();
791
1116
  init_client();
792
1117
  init_semconv();
793
1118
  init_serde();
@@ -875,6 +1200,7 @@ function startAsCurrentGeneration(name, optionsOrFn, maybeFn) {
875
1200
  }
876
1201
 
877
1202
  // src/observe.ts
1203
+ init_cjs_shims();
878
1204
  init_spans();
879
1205
  function observe(fn, options = {}) {
880
1206
  const name = options.name ?? (fn.name || "anonymous");
@@ -896,7 +1222,7 @@ function observe(fn, options = {}) {
896
1222
  // src/index.ts
897
1223
  init_semconv();
898
1224
  init_spans();
899
- var VERSION = "0.1.0";
1225
+ var VERSION = "0.2.2";
900
1226
  // Annotate the CommonJS export names for ESM import in node:
901
1227
  0 && (module.exports = {
902
1228
  Generation,