@glasstrace/sdk 0.12.3 → 0.12.5

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.d.cts CHANGED
@@ -361,6 +361,7 @@ interface GlasstraceExporterOptions {
361
361
  environment: string | undefined;
362
362
  endpointUrl: string;
363
363
  createDelegate: ((url: string, headers: Record<string, string>) => SpanExporter) | null;
364
+ verbose?: boolean;
364
365
  }
365
366
  /**
366
367
  * A SpanExporter that enriches spans with glasstrace.* attributes at export
@@ -381,6 +382,7 @@ declare class GlasstraceExporter implements SpanExporter {
381
382
  private readonly environment;
382
383
  private readonly endpointUrl;
383
384
  private readonly createDelegateFn;
385
+ private readonly verbose;
384
386
  private delegate;
385
387
  private delegateKey;
386
388
  private pendingBatches;
package/dist/index.d.ts CHANGED
@@ -361,6 +361,7 @@ interface GlasstraceExporterOptions {
361
361
  environment: string | undefined;
362
362
  endpointUrl: string;
363
363
  createDelegate: ((url: string, headers: Record<string, string>) => SpanExporter) | null;
364
+ verbose?: boolean;
364
365
  }
365
366
  /**
366
367
  * A SpanExporter that enriches spans with glasstrace.* attributes at export
@@ -381,6 +382,7 @@ declare class GlasstraceExporter implements SpanExporter {
381
382
  private readonly environment;
382
383
  private readonly endpointUrl;
383
384
  private readonly createDelegateFn;
385
+ private readonly verbose;
384
386
  private delegate;
385
387
  private delegateKey;
386
388
  private pendingBatches;
package/dist/index.js CHANGED
@@ -259,6 +259,7 @@ function loadFsSyncOrNull() {
259
259
  var currentConfig = null;
260
260
  var configCacheChecked = false;
261
261
  var rateLimitBackoff = false;
262
+ var lastInitSucceeded = false;
262
263
  function loadCachedConfig(projectRoot) {
263
264
  const modules = loadFsSyncOrNull();
264
265
  if (!modules) return null;
@@ -428,6 +429,7 @@ async function writeClaimedKey(newApiKey, projectRoot) {
428
429
  }
429
430
  }
430
431
  async function performInit(config, anonKey, sdkVersion, healthReport) {
432
+ lastInitSucceeded = false;
431
433
  if (rateLimitBackoff) {
432
434
  rateLimitBackoff = false;
433
435
  return null;
@@ -456,6 +458,7 @@ async function performInit(config, anonKey, sdkVersion, healthReport) {
456
458
  if (healthReport) {
457
459
  acknowledgeHealthReport(healthReport);
458
460
  }
461
+ lastInitSucceeded = true;
459
462
  await saveCachedConfig(result);
460
463
  if (result.claimResult) {
461
464
  try {
@@ -502,7 +505,6 @@ async function performInit(config, anonKey, sdkVersion, healthReport) {
502
505
  return null;
503
506
  }
504
507
  } catch (err) {
505
- recordInitFailure();
506
508
  console.warn(
507
509
  `[glasstrace] Unexpected init error: ${err instanceof Error ? err.message : String(err)}`
508
510
  );
@@ -532,6 +534,16 @@ function getClaimResult() {
532
534
  function _setCurrentConfig(config) {
533
535
  currentConfig = config;
534
536
  }
537
+ function consumeRateLimitFlag() {
538
+ if (rateLimitBackoff) {
539
+ rateLimitBackoff = false;
540
+ return true;
541
+ }
542
+ return false;
543
+ }
544
+ function didLastInitSucceed() {
545
+ return lastInitSucceeded;
546
+ }
535
547
 
536
548
  // src/span-processor.ts
537
549
  var GlasstraceSpanProcessor = class {
@@ -565,6 +577,7 @@ var GlasstraceExporter = class {
565
577
  environment;
566
578
  endpointUrl;
567
579
  createDelegateFn;
580
+ verbose;
568
581
  delegate = null;
569
582
  delegateKey = null;
570
583
  pendingBatches = [];
@@ -577,6 +590,7 @@ var GlasstraceExporter = class {
577
590
  this.environment = options.environment;
578
591
  this.endpointUrl = options.endpointUrl;
579
592
  this.createDelegateFn = options.createDelegate;
593
+ this.verbose = options.verbose ?? false;
580
594
  }
581
595
  export(spans, resultCallback) {
582
596
  const currentKey = this.getApiKey();
@@ -585,11 +599,16 @@ var GlasstraceExporter = class {
585
599
  return;
586
600
  }
587
601
  const enrichedSpans = spans.map((span) => this.enrichSpan(span));
602
+ if (this.verbose) {
603
+ sdkLog("info", `[glasstrace:diag] Export batch: ${enrichedSpans.length} spans`);
604
+ }
588
605
  const exporter = this.ensureDelegate();
589
606
  if (exporter) {
590
607
  exporter.export(enrichedSpans, (result) => {
591
608
  if (result.code !== 0) {
592
609
  sdkLog("warn", `[glasstrace] Span export failed: ${result.error?.message ?? "unknown error"}`);
610
+ } else if (this.verbose) {
611
+ sdkLog("info", `[glasstrace:diag] Export success: ${enrichedSpans.length} spans delivered`);
593
612
  }
594
613
  resultCallback(result);
595
614
  });
@@ -682,6 +701,21 @@ var GlasstraceExporter = class {
682
701
  if (statusCode !== void 0) {
683
702
  extra[ATTR.HTTP_STATUS_CODE] = statusCode;
684
703
  }
704
+ if (method && span.status?.code === SpanStatusCode.ERROR) {
705
+ if (statusCode === void 0 || statusCode === 0 || statusCode === 200) {
706
+ const httpErrorType = attrs["error.type"];
707
+ if (typeof httpErrorType === "string") {
708
+ const parsed = parseInt(httpErrorType, 10);
709
+ if (!isNaN(parsed) && parsed >= 400 && parsed <= 599) {
710
+ extra[ATTR.HTTP_STATUS_CODE] = parsed;
711
+ } else {
712
+ extra[ATTR.HTTP_STATUS_CODE] = 500;
713
+ }
714
+ } else {
715
+ extra[ATTR.HTTP_STATUS_CODE] = 500;
716
+ }
717
+ }
718
+ }
685
719
  if (span.startTime && span.endTime) {
686
720
  const [startSec, startNano] = span.startTime;
687
721
  const [endSec, endNano] = span.endTime;
@@ -761,11 +795,17 @@ var GlasstraceExporter = class {
761
795
  bufferSpans(spans, resultCallback) {
762
796
  this.pendingBatches.push({ spans, resultCallback });
763
797
  this.pendingSpanCount += spans.length;
798
+ if (this.verbose) {
799
+ sdkLog("info", `[glasstrace:diag] Buffering ${spans.length} spans (key pending, total: ${this.pendingSpanCount})`);
800
+ }
764
801
  while (this.pendingSpanCount > MAX_PENDING_SPANS && this.pendingBatches.length > 1) {
765
802
  const evicted = this.pendingBatches.shift();
766
803
  this.pendingSpanCount -= evicted.spans.length;
767
804
  recordSpansDropped(evicted.spans.length);
768
805
  evicted.resultCallback({ code: 0 });
806
+ if (this.verbose) {
807
+ sdkLog("info", `[glasstrace:diag] Buffer overflow: evicted ${evicted.spans.length} spans (total pending: ${this.pendingSpanCount})`);
808
+ }
769
809
  if (!this.overflowLogged) {
770
810
  this.overflowLogged = true;
771
811
  console.warn(
@@ -804,6 +844,8 @@ var GlasstraceExporter = class {
804
844
  exporter.export(enriched, (result) => {
805
845
  if (result.code !== 0) {
806
846
  sdkLog("warn", `[glasstrace] Span export failed: ${result.error?.message ?? "unknown error"}`);
847
+ } else if (this.verbose) {
848
+ sdkLog("info", `[glasstrace:diag] Flush export success: ${enriched.length} spans delivered`);
807
849
  }
808
850
  batch.resultCallback(result);
809
851
  });
@@ -3569,7 +3611,8 @@ async function configureOtel(config, sessionManager) {
3569
3611
  getConfig: () => getActiveConfig(),
3570
3612
  environment: config.environment,
3571
3613
  endpointUrl: exporterUrl,
3572
- createDelegate: createOtlpExporter
3614
+ createDelegate: createOtlpExporter,
3615
+ verbose: config.verbose
3573
3616
  });
3574
3617
  _activeExporter = glasstraceExporter;
3575
3618
  const vercelOtel = await tryImport("@vercel/otel");
@@ -3612,6 +3655,9 @@ async function configureOtel(config, sessionManager) {
3612
3655
  const processor = new BatchSpanProcessor(glasstraceExporter, {
3613
3656
  scheduledDelayMillis: 1e3
3614
3657
  });
3658
+ if (config.verbose) {
3659
+ sdkLog("info", "[glasstrace:diag] BatchSpanProcessor configured: scheduledDelayMillis=1000");
3660
+ }
3615
3661
  const provider = new BasicTracerProvider({
3616
3662
  spanProcessors: [processor]
3617
3663
  });
@@ -3619,6 +3665,109 @@ async function configureOtel(config, sessionManager) {
3619
3665
  registerShutdownHooks(provider);
3620
3666
  }
3621
3667
 
3668
+ // src/heartbeat.ts
3669
+ var HEARTBEAT_INTERVAL_MS = 5 * 60 * 1e3;
3670
+ var BACKOFF_BASE_MS = HEARTBEAT_INTERVAL_MS;
3671
+ var BACKOFF_MAX_MS = 30 * 60 * 1e3;
3672
+ var BACKOFF_JITTER = 0.2;
3673
+ var heartbeatTimer = null;
3674
+ var heartbeatGeneration = 0;
3675
+ var backoffAttempts = 0;
3676
+ var backoffUntil = 0;
3677
+ var tickInProgress = false;
3678
+ var _shutdownHandler2 = null;
3679
+ function startHeartbeat(config, anonKey, sdkVersion, generation, onClaimTransition) {
3680
+ if (heartbeatTimer !== null) return;
3681
+ heartbeatGeneration = generation;
3682
+ heartbeatTimer = setInterval(() => {
3683
+ void heartbeatTick(config, anonKey, sdkVersion, generation, onClaimTransition);
3684
+ }, HEARTBEAT_INTERVAL_MS);
3685
+ heartbeatTimer.unref();
3686
+ registerShutdownHandlers(config, anonKey, sdkVersion);
3687
+ if (config.verbose) {
3688
+ sdkLog("info", "[glasstrace] Heartbeat started (5-minute interval).");
3689
+ }
3690
+ }
3691
+ function stopHeartbeat() {
3692
+ if (heartbeatTimer !== null) {
3693
+ clearInterval(heartbeatTimer);
3694
+ heartbeatTimer = null;
3695
+ }
3696
+ removeShutdownHandlers();
3697
+ }
3698
+ async function heartbeatTick(config, anonKey, sdkVersion, generation, onClaimTransition) {
3699
+ if (tickInProgress) return;
3700
+ tickInProgress = true;
3701
+ try {
3702
+ if (generation !== heartbeatGeneration) {
3703
+ stopHeartbeat();
3704
+ return;
3705
+ }
3706
+ if (Date.now() < backoffUntil) {
3707
+ if (config.verbose) {
3708
+ sdkLog("info", "[glasstrace] Heartbeat skipped (rate-limit backoff).");
3709
+ }
3710
+ return;
3711
+ }
3712
+ const healthReport = collectHealthReport(sdkVersion);
3713
+ const initResult = await performInit(config, anonKey, sdkVersion, healthReport);
3714
+ if (generation !== heartbeatGeneration) return;
3715
+ if (initResult === null && consumeRateLimitFlag()) {
3716
+ backoffAttempts++;
3717
+ const delay = Math.min(
3718
+ BACKOFF_BASE_MS * Math.pow(2, backoffAttempts - 1),
3719
+ BACKOFF_MAX_MS
3720
+ );
3721
+ const jitter = delay * BACKOFF_JITTER * (Math.random() * 2 - 1);
3722
+ backoffUntil = Date.now() + delay + jitter;
3723
+ if (config.verbose) {
3724
+ sdkLog("info", `[glasstrace] Heartbeat backing off for ${Math.round((delay + jitter) / 1e3)}s.`);
3725
+ }
3726
+ } else {
3727
+ backoffAttempts = 0;
3728
+ backoffUntil = 0;
3729
+ }
3730
+ if (initResult?.claimResult) {
3731
+ onClaimTransition(initResult.claimResult.newApiKey);
3732
+ }
3733
+ if (config.verbose) {
3734
+ sdkLog("info", "[glasstrace] Heartbeat completed.");
3735
+ }
3736
+ } finally {
3737
+ tickInProgress = false;
3738
+ }
3739
+ }
3740
+ function registerShutdownHandlers(config, anonKey, sdkVersion) {
3741
+ if (typeof process === "undefined" || typeof process.once !== "function") {
3742
+ return;
3743
+ }
3744
+ let shutdownFired = false;
3745
+ const handler = (signal) => {
3746
+ if (shutdownFired) return;
3747
+ shutdownFired = true;
3748
+ if (heartbeatTimer !== null) {
3749
+ clearInterval(heartbeatTimer);
3750
+ heartbeatTimer = null;
3751
+ }
3752
+ const healthReport = collectHealthReport(sdkVersion);
3753
+ void performInit(config, anonKey, sdkVersion, healthReport).catch(() => {
3754
+ }).finally(() => {
3755
+ removeShutdownHandlers();
3756
+ process.kill(process.pid, signal);
3757
+ });
3758
+ };
3759
+ _shutdownHandler2 = handler;
3760
+ process.once("SIGTERM", _shutdownHandler2);
3761
+ process.once("SIGINT", _shutdownHandler2);
3762
+ }
3763
+ function removeShutdownHandlers() {
3764
+ if (_shutdownHandler2 && typeof process !== "undefined") {
3765
+ process.removeListener("SIGTERM", _shutdownHandler2);
3766
+ process.removeListener("SIGINT", _shutdownHandler2);
3767
+ _shutdownHandler2 = null;
3768
+ }
3769
+ }
3770
+
3622
3771
  // src/register.ts
3623
3772
  var consoleCaptureInstalled = false;
3624
3773
  var discoveryHandler = null;
@@ -3772,14 +3921,20 @@ async function backgroundInit(config, anonKeyForInit, generation) {
3772
3921
  if (config.verbose) {
3773
3922
  console.info("[glasstrace] Background init firing.");
3774
3923
  }
3775
- const healthReport = collectHealthReport("0.12.3");
3776
- const initResult = await performInit(config, anonKeyForInit, "0.12.3", healthReport);
3924
+ const healthReport = collectHealthReport("0.12.5");
3925
+ const initResult = await performInit(config, anonKeyForInit, "0.12.5", healthReport);
3777
3926
  if (generation !== registrationGeneration) return;
3778
3927
  if (initResult?.claimResult) {
3779
3928
  setResolvedApiKey(initResult.claimResult.newApiKey);
3780
3929
  notifyApiKeyResolved();
3781
3930
  }
3782
3931
  maybeInstallConsoleCapture();
3932
+ if (didLastInitSucceed()) {
3933
+ startHeartbeat(config, anonKeyForInit, "0.12.5", generation, (newApiKey) => {
3934
+ setResolvedApiKey(newApiKey);
3935
+ notifyApiKeyResolved();
3936
+ });
3937
+ }
3783
3938
  }
3784
3939
  function getDiscoveryHandler() {
3785
3940
  return discoveryHandler;