risicare 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -49,9 +49,11 @@ __export(src_exports, {
49
49
  getCurrentSpan: () => getCurrentSpan,
50
50
  getCurrentSpanId: () => getCurrentSpanId,
51
51
  getCurrentTraceId: () => getCurrentTraceId,
52
+ getMetrics: () => getMetrics,
52
53
  getSpanById: () => getSpanById,
54
+ getTraceContent: () => getTraceContent,
53
55
  getTraceContext: () => getTraceContext,
54
- getTracer: () => getTracer,
56
+ getTracer: () => getTracer2,
55
57
  init: () => init,
56
58
  injectTraceContext: () => injectTraceContext,
57
59
  isEnabled: () => isEnabled,
@@ -85,6 +87,7 @@ function resolveConfig(config) {
85
87
  const traceContent = config?.traceContent ?? parseBool(env.RISICARE_TRACE_CONTENT, true);
86
88
  const sampleRate = config?.sampleRate ?? parseFloat(env.RISICARE_SAMPLE_RATE ?? "1.0");
87
89
  const debug2 = config?.debug ?? parseBool(env.RISICARE_DEBUG, false);
90
+ const compress = config?.compress ?? parseBool(env.RISICARE_COMPRESS, false);
88
91
  const batchSize = config?.batchSize ?? 100;
89
92
  const batchTimeoutMs = config?.batchTimeoutMs ?? 1e3;
90
93
  const maxQueueSize = config?.maxQueueSize ?? 1e4;
@@ -110,6 +113,7 @@ function resolveConfig(config) {
110
113
  batchTimeoutMs,
111
114
  maxQueueSize,
112
115
  debug: debug2,
116
+ compress,
113
117
  metadata: config?.metadata ?? {}
114
118
  };
115
119
  }
@@ -130,7 +134,7 @@ function generateSpanId() {
130
134
  return (0, import_node_crypto.randomBytes)(8).toString("hex");
131
135
  }
132
136
  function generateAgentId(prefix) {
133
- const suffix = (0, import_node_crypto.randomBytes)(6).toString("hex");
137
+ const suffix = (0, import_node_crypto.randomBytes)(8).toString("hex");
134
138
  return prefix ? `${prefix}-${suffix}` : suffix;
135
139
  }
136
140
  function validateTraceId(id) {
@@ -447,16 +451,58 @@ function shouldSample(traceId, sampleRate) {
447
451
  return hash / 4294967295 < sampleRate;
448
452
  }
449
453
 
450
- // src/context/storage.ts
454
+ // src/globals.ts
451
455
  var import_node_async_hooks = require("async_hooks");
452
- var contextStorage = new import_node_async_hooks.AsyncLocalStorage();
456
+ var G = globalThis;
457
+ var PREFIX = "__risicare_";
458
+ function getClient() {
459
+ return G[PREFIX + "client"];
460
+ }
461
+ function setClient(client) {
462
+ G[PREFIX + "client"] = client;
463
+ }
464
+ function getTracer() {
465
+ return G[PREFIX + "tracer"];
466
+ }
467
+ function setTracer(tracer) {
468
+ G[PREFIX + "tracer"] = tracer;
469
+ }
470
+ function getContextStorage() {
471
+ if (!G[PREFIX + "ctx"]) {
472
+ G[PREFIX + "ctx"] = new import_node_async_hooks.AsyncLocalStorage();
473
+ }
474
+ return G[PREFIX + "ctx"];
475
+ }
476
+ function getRegistry() {
477
+ if (!G[PREFIX + "registry"]) {
478
+ G[PREFIX + "registry"] = /* @__PURE__ */ new Map();
479
+ }
480
+ return G[PREFIX + "registry"];
481
+ }
482
+ function getOpCount() {
483
+ return G[PREFIX + "opcount"] ?? 0;
484
+ }
485
+ function setOpCount(n) {
486
+ G[PREFIX + "opcount"] = n;
487
+ }
488
+ function getDebug() {
489
+ return G[PREFIX + "debug"] ?? false;
490
+ }
491
+ function setDebugFlag(enabled) {
492
+ G[PREFIX + "debug"] = enabled;
493
+ }
494
+
495
+ // src/context/storage.ts
496
+ function storage() {
497
+ return getContextStorage();
498
+ }
453
499
  function getContext() {
454
- return contextStorage.getStore() ?? {};
500
+ return storage().getStore() ?? {};
455
501
  }
456
502
  function runWithContext(overrides, fn) {
457
503
  const parent = getContext();
458
504
  const merged = { ...parent, ...overrides };
459
- return contextStorage.run(merged, fn);
505
+ return storage().run(merged, fn);
460
506
  }
461
507
  function getCurrentSession() {
462
508
  return getContext().session;
@@ -497,10 +543,12 @@ var Tracer = class {
497
543
  _onSpanEnd;
498
544
  _sampleRate;
499
545
  _enabled;
546
+ _traceContent;
500
547
  constructor(config) {
501
548
  this._onSpanEnd = config.onSpanEnd;
502
549
  this._sampleRate = config.sampleRate ?? 1;
503
550
  this._enabled = config.enabled ?? true;
551
+ this._traceContent = config.traceContent ?? true;
504
552
  }
505
553
  get enabled() {
506
554
  return this._enabled;
@@ -508,6 +556,9 @@ var Tracer = class {
508
556
  set enabled(value) {
509
557
  this._enabled = value;
510
558
  }
559
+ get traceContent() {
560
+ return this._traceContent;
561
+ }
511
562
  /**
512
563
  * Start a span, run the callback within its context, and auto-end on completion.
513
564
  *
@@ -613,12 +664,11 @@ var Tracer = class {
613
664
  };
614
665
 
615
666
  // src/utils/log.ts
616
- var _debug = false;
617
667
  function setDebug(enabled) {
618
- _debug = enabled;
668
+ setDebugFlag(enabled);
619
669
  }
620
670
  function debug(msg) {
621
- if (_debug) {
671
+ if (getDebug()) {
622
672
  process.stderr.write(`[risicare] ${msg}
623
673
  `);
624
674
  }
@@ -629,7 +679,7 @@ function warn(msg) {
629
679
  }
630
680
 
631
681
  // src/exporters/batch.ts
632
- var BatchSpanProcessor = class {
682
+ var BatchSpanProcessor = class _BatchSpanProcessor {
633
683
  _exporters;
634
684
  _batchSize;
635
685
  _batchTimeoutMs;
@@ -639,6 +689,10 @@ var BatchSpanProcessor = class {
639
689
  _timer = null;
640
690
  _started = false;
641
691
  _flushing = false;
692
+ _beforeExitHandler = null;
693
+ // Retry tracking for failed batches (Audit #5)
694
+ _retryCounts = /* @__PURE__ */ new Map();
695
+ static MAX_RETRIES = 3;
642
696
  // Metrics
643
697
  droppedSpans = 0;
644
698
  exportedSpans = 0;
@@ -657,9 +711,10 @@ var BatchSpanProcessor = class {
657
711
  void this._exportBatch();
658
712
  }, this._batchTimeoutMs);
659
713
  this._timer.unref();
660
- process.on("beforeExit", () => {
714
+ this._beforeExitHandler = () => {
661
715
  void this.shutdown();
662
- });
716
+ };
717
+ process.once("beforeExit", this._beforeExitHandler);
663
718
  }
664
719
  async shutdown(timeoutMs = 5e3) {
665
720
  if (!this._started) return;
@@ -668,6 +723,10 @@ var BatchSpanProcessor = class {
668
723
  clearInterval(this._timer);
669
724
  this._timer = null;
670
725
  }
726
+ if (this._beforeExitHandler) {
727
+ process.removeListener("beforeExit", this._beforeExitHandler);
728
+ this._beforeExitHandler = null;
729
+ }
671
730
  const flushPromise = this._exportBatch();
672
731
  const timeoutPromise = new Promise((resolve) => setTimeout(resolve, timeoutMs));
673
732
  await Promise.race([flushPromise, timeoutPromise]);
@@ -678,6 +737,7 @@ var BatchSpanProcessor = class {
678
737
  debug(`Error shutting down ${exporter.name}: ${e}`);
679
738
  }
680
739
  }
740
+ this._retryCounts.clear();
681
741
  debug(
682
742
  `BatchSpanProcessor shutdown. Exported: ${this.exportedSpans}, Dropped: ${this.droppedSpans}, Failed: ${this.failedExports}`
683
743
  );
@@ -734,6 +794,9 @@ var BatchSpanProcessor = class {
734
794
  if (!batchExported) {
735
795
  this.exportedSpans += batch.length;
736
796
  batchExported = true;
797
+ for (const span of batch) {
798
+ this._retryCounts.delete(span.spanId);
799
+ }
737
800
  }
738
801
  } else {
739
802
  this.failedExports++;
@@ -743,6 +806,22 @@ var BatchSpanProcessor = class {
743
806
  debug(`Export to ${exporter.name} failed: ${e}`);
744
807
  }
745
808
  }
809
+ if (!batchExported) {
810
+ const retryable = batch.filter((span) => {
811
+ const count = (this._retryCounts.get(span.spanId) ?? 0) + 1;
812
+ if (count > _BatchSpanProcessor.MAX_RETRIES) {
813
+ this._retryCounts.delete(span.spanId);
814
+ this.droppedSpans++;
815
+ return false;
816
+ }
817
+ this._retryCounts.set(span.spanId, count);
818
+ return true;
819
+ });
820
+ if (retryable.length > 0) {
821
+ this._queue.unshift(...retryable);
822
+ debug(`Re-queued ${retryable.length} spans for retry (${batch.length - retryable.length} dropped after max retries)`);
823
+ }
824
+ }
746
825
  } finally {
747
826
  this._flushing = false;
748
827
  }
@@ -750,6 +829,7 @@ var BatchSpanProcessor = class {
750
829
  };
751
830
 
752
831
  // src/exporters/http.ts
832
+ var SDK_VERSION = "0.1.1";
753
833
  var HttpExporter = class {
754
834
  name = "http";
755
835
  _endpoint;
@@ -776,23 +856,26 @@ var HttpExporter = class {
776
856
  async export(spans) {
777
857
  if (spans.length === 0) return "success" /* SUCCESS */;
778
858
  const now = Date.now();
859
+ let isHalfOpen = false;
779
860
  if (this._consecutiveFailures >= this._circuitBreakerThreshold) {
780
861
  if (now < this._circuitOpenUntil) {
781
862
  return "failure" /* FAILURE */;
782
863
  }
864
+ isHalfOpen = true;
783
865
  }
784
866
  const body = {
785
867
  spans: spans.map((s) => s.toPayload())
786
868
  };
787
869
  if (this._projectId) body.projectId = this._projectId;
788
870
  if (this._environment) body.environment = this._environment;
789
- for (let attempt = 0; attempt < this._maxRetries; attempt++) {
871
+ const maxAttempts = isHalfOpen ? 1 : this._maxRetries;
872
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
790
873
  const result = await this._sendRequest(body);
791
874
  if (result === "success" /* SUCCESS */) {
792
875
  this._consecutiveFailures = 0;
793
876
  return result;
794
877
  }
795
- if (attempt < this._maxRetries - 1) {
878
+ if (attempt < maxAttempts - 1) {
796
879
  await sleep(100 * Math.pow(2, attempt));
797
880
  }
798
881
  }
@@ -808,7 +891,8 @@ var HttpExporter = class {
808
891
  async _sendRequest(body) {
809
892
  const url = `${this._endpoint}/v1/spans`;
810
893
  const headers = {
811
- "Content-Type": "application/json"
894
+ "Content-Type": "application/json",
895
+ "User-Agent": `risicare-js/${SDK_VERSION} node/${process.version}`
812
896
  };
813
897
  if (this._apiKey) {
814
898
  headers["Authorization"] = `Bearer ${this._apiKey}`;
@@ -819,7 +903,8 @@ var HttpExporter = class {
819
903
  const { gzipSync } = await import("zlib");
820
904
  payload = gzipSync(Buffer.from(payload));
821
905
  headers["Content-Encoding"] = "gzip";
822
- } catch {
906
+ } catch (e) {
907
+ debug(`Gzip compression failed, sending uncompressed: ${e}`);
823
908
  }
824
909
  }
825
910
  try {
@@ -869,15 +954,17 @@ var ConsoleExporter = class {
869
954
  };
870
955
 
871
956
  // src/client.ts
872
- var _client;
873
- var _tracer;
874
957
  var RisicareClient = class {
875
958
  config;
876
959
  processor;
877
960
  tracer;
878
- _shutdownCalled = false;
961
+ _shutdownPromise;
962
+ _shutdownHandlers = [];
879
963
  constructor(config) {
880
964
  this.config = resolveConfig(config);
965
+ if (this.config.apiKey && !this.config.apiKey.startsWith("rsk-")) {
966
+ debug('Warning: API key should start with "rsk-". Got: ' + this.config.apiKey.slice(0, 4) + "...");
967
+ }
881
968
  let exporter;
882
969
  if (this.config.debug && !this.config.apiKey) {
883
970
  exporter = new ConsoleExporter();
@@ -886,7 +973,8 @@ var RisicareClient = class {
886
973
  endpoint: this.config.endpoint,
887
974
  apiKey: this.config.apiKey,
888
975
  projectId: this.config.projectId || void 0,
889
- environment: this.config.environment || void 0
976
+ environment: this.config.environment || void 0,
977
+ compress: this.config.compress
890
978
  });
891
979
  } else {
892
980
  exporter = new ConsoleExporter();
@@ -901,7 +989,8 @@ var RisicareClient = class {
901
989
  this.tracer = new Tracer({
902
990
  onSpanEnd: (span) => this.processor.onSpanEnd(span),
903
991
  sampleRate: this.config.sampleRate,
904
- enabled: this.config.enabled
992
+ enabled: this.config.enabled,
993
+ traceContent: this.config.traceContent
905
994
  });
906
995
  this.processor.start();
907
996
  this._registerShutdownHooks();
@@ -914,10 +1003,18 @@ var RisicareClient = class {
914
1003
  set enabled(value) {
915
1004
  this.tracer.enabled = value;
916
1005
  }
1006
+ // Audit #6: Promise-based shutdown dedup (fixes TOCTOU race condition)
917
1007
  async shutdown() {
918
- if (this._shutdownCalled) return;
919
- this._shutdownCalled = true;
1008
+ if (this._shutdownPromise) return this._shutdownPromise;
1009
+ this._shutdownPromise = this._doShutdown();
1010
+ return this._shutdownPromise;
1011
+ }
1012
+ async _doShutdown() {
920
1013
  debug("Shutting down...");
1014
+ for (const { signal, handler } of this._shutdownHandlers) {
1015
+ process.removeListener(signal, handler);
1016
+ }
1017
+ this._shutdownHandlers = [];
921
1018
  await this.processor.shutdown();
922
1019
  }
923
1020
  async flush() {
@@ -925,43 +1022,68 @@ var RisicareClient = class {
925
1022
  }
926
1023
  _registerShutdownHooks() {
927
1024
  const onShutdown = () => {
1025
+ const timeout = setTimeout(() => process.exit(1), 5e3);
1026
+ timeout.unref();
928
1027
  this.shutdown().catch(() => {
929
- });
1028
+ }).finally(() => clearTimeout(timeout));
930
1029
  };
931
- process.once("beforeExit", onShutdown);
932
- process.once("SIGTERM", onShutdown);
933
- process.once("SIGINT", onShutdown);
1030
+ const signals = ["beforeExit", "SIGTERM", "SIGINT"];
1031
+ for (const signal of signals) {
1032
+ process.once(signal, onShutdown);
1033
+ this._shutdownHandlers.push({ signal, handler: onShutdown });
1034
+ }
934
1035
  }
935
1036
  };
936
1037
  function init(config) {
937
- if (_client) {
1038
+ if (getClient()) {
938
1039
  debug("Already initialized. Call shutdown() first to re-initialize.");
939
1040
  return;
940
1041
  }
941
- _client = new RisicareClient(config);
942
- _tracer = _client.tracer;
1042
+ const client = new RisicareClient(config);
1043
+ setClient(client);
1044
+ setTracer(client.tracer);
943
1045
  }
944
1046
  async function shutdown() {
945
- if (!_client) return;
946
- await _client.shutdown();
947
- _client = void 0;
948
- _tracer = void 0;
1047
+ const client = getClient();
1048
+ if (!client) return;
1049
+ await client.shutdown();
1050
+ setClient(void 0);
1051
+ setTracer(void 0);
949
1052
  }
950
1053
  async function flush() {
951
- if (!_client) return;
952
- await _client.flush();
1054
+ const client = getClient();
1055
+ if (!client) return;
1056
+ await client.flush();
953
1057
  }
954
1058
  function enable() {
955
- if (_client) _client.enabled = true;
1059
+ const client = getClient();
1060
+ if (client) client.enabled = true;
956
1061
  }
957
1062
  function disable() {
958
- if (_client) _client.enabled = false;
1063
+ const client = getClient();
1064
+ if (client) client.enabled = false;
959
1065
  }
960
1066
  function isEnabled() {
961
- return _client?.enabled ?? false;
1067
+ const client = getClient();
1068
+ return client?.enabled ?? false;
962
1069
  }
963
- function getTracer() {
964
- return _tracer;
1070
+ function getTracer2() {
1071
+ return getTracer();
1072
+ }
1073
+ function getTraceContent() {
1074
+ const tracer = getTracer();
1075
+ return tracer?.traceContent ?? true;
1076
+ }
1077
+ function getMetrics() {
1078
+ const client = getClient();
1079
+ return client?.processor.getMetrics() ?? {
1080
+ exportedSpans: 0,
1081
+ droppedSpans: 0,
1082
+ failedExports: 0,
1083
+ queueSize: 0,
1084
+ queueCapacity: 0,
1085
+ queueUtilization: 0
1086
+ };
965
1087
  }
966
1088
 
967
1089
  // src/context/agent.ts
@@ -985,7 +1107,7 @@ function withAgent(options, fn) {
985
1107
  function agent(options, fn) {
986
1108
  const spanName = `agent:${options.name ?? "agent"}`;
987
1109
  return (...args) => {
988
- const tracer = getTracer();
1110
+ const tracer = getTracer2();
989
1111
  if (!tracer) {
990
1112
  return fn(...args);
991
1113
  }
@@ -1027,7 +1149,7 @@ function withPhase(phase, fn) {
1027
1149
  // src/decorators/phase.ts
1028
1150
  function phaseWrapper(phase, fn) {
1029
1151
  return (...args) => {
1030
- const tracer = getTracer();
1152
+ const tracer = getTracer2();
1031
1153
  if (!tracer) {
1032
1154
  return fn(...args);
1033
1155
  }
@@ -1061,7 +1183,7 @@ function namespacedMetadata(metadata) {
1061
1183
  function traceMessage(options, fn) {
1062
1184
  const msgType = options.type ?? "request" /* REQUEST */;
1063
1185
  return (...args) => {
1064
- const tracer = getTracer();
1186
+ const tracer = getTracer2();
1065
1187
  if (!tracer) return fn(...args);
1066
1188
  return tracer.startSpan(
1067
1189
  {
@@ -1080,7 +1202,7 @@ function traceMessage(options, fn) {
1080
1202
  }
1081
1203
  function traceDelegate(options, fn) {
1082
1204
  return (...args) => {
1083
- const tracer = getTracer();
1205
+ const tracer = getTracer2();
1084
1206
  if (!tracer) return fn(...args);
1085
1207
  return tracer.startSpan(
1086
1208
  {
@@ -1099,7 +1221,7 @@ function traceDelegate(options, fn) {
1099
1221
  }
1100
1222
  function traceCoordinate(options, fn) {
1101
1223
  return (...args) => {
1102
- const tracer = getTracer();
1224
+ const tracer = getTracer2();
1103
1225
  if (!tracer) return fn(...args);
1104
1226
  return tracer.startSpan(
1105
1227
  {
@@ -1190,36 +1312,37 @@ function extractTraceContext(headers) {
1190
1312
  var DEFAULT_TTL_MS = 6e4;
1191
1313
  var MAX_ENTRIES = 1e4;
1192
1314
  var CLEANUP_INTERVAL = 100;
1193
- var entries = /* @__PURE__ */ new Map();
1194
- var operationCount = 0;
1315
+ function entries() {
1316
+ return getRegistry();
1317
+ }
1195
1318
  function registerSpan(span, ttlMs = DEFAULT_TTL_MS) {
1196
- entries.set(span.spanId, {
1319
+ entries().set(span.spanId, {
1197
1320
  span,
1198
1321
  registeredAt: Date.now(),
1199
1322
  ttlMs
1200
1323
  });
1201
- operationCount++;
1324
+ setOpCount(getOpCount() + 1);
1202
1325
  maybeCleanup();
1203
1326
  }
1204
1327
  function getSpanById(spanId) {
1205
- const entry = entries.get(spanId);
1328
+ const entry = entries().get(spanId);
1206
1329
  if (!entry) return void 0;
1207
1330
  if (Date.now() - entry.registeredAt > entry.ttlMs) {
1208
- entries.delete(spanId);
1331
+ entries().delete(spanId);
1209
1332
  return void 0;
1210
1333
  }
1211
1334
  return entry.span;
1212
1335
  }
1213
1336
  function unregisterSpan(spanId) {
1214
- entries.delete(spanId);
1337
+ entries().delete(spanId);
1215
1338
  }
1216
1339
  function maybeCleanup() {
1217
- if (operationCount % CLEANUP_INTERVAL !== 0) return;
1218
- if (entries.size <= MAX_ENTRIES) return;
1340
+ if (getOpCount() % CLEANUP_INTERVAL !== 0) return;
1341
+ if (entries().size <= MAX_ENTRIES) return;
1219
1342
  const now = Date.now();
1220
- for (const [id, entry] of entries) {
1343
+ for (const [id, entry] of entries()) {
1221
1344
  if (now - entry.registeredAt > entry.ttlMs) {
1222
- entries.delete(id);
1345
+ entries().delete(id);
1223
1346
  }
1224
1347
  }
1225
1348
  }
@@ -1244,7 +1367,9 @@ function maybeCleanup() {
1244
1367
  getCurrentSpan,
1245
1368
  getCurrentSpanId,
1246
1369
  getCurrentTraceId,
1370
+ getMetrics,
1247
1371
  getSpanById,
1372
+ getTraceContent,
1248
1373
  getTraceContext,
1249
1374
  getTracer,
1250
1375
  init,