@glasstrace/sdk 0.1.0 → 0.2.0

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.js CHANGED
@@ -68,17 +68,18 @@ function isAnonymousMode(config) {
68
68
  import { createHash } from "crypto";
69
69
  import { SessionIdSchema } from "@glasstrace/protocol";
70
70
  var FOUR_HOURS_MS = 4 * 60 * 60 * 1e3;
71
+ var cachedGlasstraceEnv = process.env.GLASSTRACE_ENV;
72
+ var cachedPort = process.env.PORT ?? "3000";
71
73
  function deriveSessionId(apiKey, origin, date, windowIndex) {
72
74
  const input = JSON.stringify([apiKey, origin, date, windowIndex]);
73
75
  const hash = createHash("sha256").update(input).digest("hex").slice(0, 16);
74
76
  return SessionIdSchema.parse(hash);
75
77
  }
76
78
  function getOrigin() {
77
- if (process.env.GLASSTRACE_ENV) {
78
- return process.env.GLASSTRACE_ENV;
79
+ if (cachedGlasstraceEnv) {
80
+ return cachedGlasstraceEnv;
79
81
  }
80
- const port = process.env.PORT ?? "3000";
81
- return `localhost:${port}`;
82
+ return `localhost:${cachedPort}`;
82
83
  }
83
84
  function getDateString() {
84
85
  const now = /* @__PURE__ */ new Date();
@@ -132,6 +133,7 @@ var SessionManager = class {
132
133
  };
133
134
 
134
135
  // src/fetch-classifier.ts
136
+ var cachedPort2 = process.env.PORT ?? "3000";
135
137
  function classifyFetchTarget(url) {
136
138
  let parsed;
137
139
  try {
@@ -146,8 +148,7 @@ function classifyFetchTarget(url) {
146
148
  if (hostname === "stripe.com" || hostname.endsWith(".stripe.com")) {
147
149
  return "stripe";
148
150
  }
149
- const port = process.env.PORT ?? "3000";
150
- const internalOrigin = `localhost:${port}`;
151
+ const internalOrigin = `localhost:${cachedPort2}`;
151
152
  const parsedPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
152
153
  const urlOrigin = `${hostname}:${parsedPort}`;
153
154
  if (urlOrigin === internalOrigin) {
@@ -296,6 +297,10 @@ async function sendInitRequest(config, anonKey, sdkVersion, importGraph, healthR
296
297
  signal
297
298
  });
298
299
  if (!response.ok) {
300
+ try {
301
+ await response.text();
302
+ } catch {
303
+ }
299
304
  const error = new Error(`Init request failed with status ${response.status}`);
300
305
  error.status = response.status;
301
306
  throw error;
@@ -418,6 +423,7 @@ var GlasstraceExporter = class {
418
423
  endpointUrl;
419
424
  createDelegateFn;
420
425
  delegate = null;
426
+ delegateKey = null;
421
427
  pendingBatches = [];
422
428
  pendingSpanCount = 0;
423
429
  overflowLogged = false;
@@ -430,12 +436,12 @@ var GlasstraceExporter = class {
430
436
  this.createDelegateFn = options.createDelegate;
431
437
  }
432
438
  export(spans, resultCallback) {
433
- const enrichedSpans = spans.map((span) => this.enrichSpan(span));
434
439
  const currentKey = this.getApiKey();
435
440
  if (currentKey === API_KEY_PENDING) {
436
- this.bufferSpans(enrichedSpans, resultCallback);
441
+ this.bufferSpans(spans, resultCallback);
437
442
  return;
438
443
  }
444
+ const enrichedSpans = spans.map((span) => this.enrichSpan(span));
439
445
  const exporter = this.ensureDelegate();
440
446
  if (exporter) {
441
447
  exporter.export(enrichedSpans, resultCallback);
@@ -477,58 +483,43 @@ var GlasstraceExporter = class {
477
483
  /**
478
484
  * Enriches a ReadableSpan with all glasstrace.* attributes.
479
485
  * Returns a new ReadableSpan wrapper; the original span is not mutated.
480
- * Each attribute derivation is wrapped in its own try-catch for partial
481
- * enrichment resilience.
486
+ *
487
+ * External function calls (getSessionId, deriveErrorCategory,
488
+ * deriveOrmProvider, classifyFetchTarget) are individually guarded so a
489
+ * failure in one does not prevent the remaining attributes from being set.
490
+ * On total failure, returns the original span unchanged.
482
491
  */
483
492
  enrichSpan(span) {
484
- const attrs = span.attributes ?? {};
485
- const name = span.name ?? "";
486
- const extra = {};
487
493
  try {
494
+ const attrs = span.attributes ?? {};
495
+ const name = span.name ?? "";
496
+ const extra = {};
488
497
  extra[ATTR.TRACE_TYPE] = "server";
489
- } catch {
490
- }
491
- try {
492
- const sessionId = this.sessionManager.getSessionId(this.getApiKey());
493
- extra[ATTR.SESSION_ID] = sessionId;
494
- } catch {
495
- }
496
- try {
498
+ try {
499
+ const sessionId = this.sessionManager.getSessionId(this.getApiKey());
500
+ extra[ATTR.SESSION_ID] = sessionId;
501
+ } catch {
502
+ }
497
503
  const env = this.environment ?? process.env.GLASSTRACE_ENV;
498
504
  if (env) {
499
505
  extra[ATTR.ENVIRONMENT] = env;
500
506
  }
501
- } catch {
502
- }
503
- try {
504
507
  const existingCid = attrs["glasstrace.correlation.id"];
505
508
  if (typeof existingCid === "string") {
506
509
  extra[ATTR.CORRELATION_ID] = existingCid;
507
510
  }
508
- } catch {
509
- }
510
- try {
511
511
  const route = attrs["http.route"] ?? name;
512
512
  if (route) {
513
513
  extra[ATTR.ROUTE] = route;
514
514
  }
515
- } catch {
516
- }
517
- try {
518
515
  const method = attrs["http.method"] ?? attrs["http.request.method"];
519
516
  if (method) {
520
517
  extra[ATTR.HTTP_METHOD] = method;
521
518
  }
522
- } catch {
523
- }
524
- try {
525
519
  const statusCode = attrs["http.status_code"] ?? attrs["http.response.status_code"];
526
520
  if (statusCode !== void 0) {
527
521
  extra[ATTR.HTTP_STATUS_CODE] = statusCode;
528
522
  }
529
- } catch {
530
- }
531
- try {
532
523
  if (span.startTime && span.endTime) {
533
524
  const [startSec, startNano] = span.startTime;
534
525
  const [endSec, endNano] = span.endTime;
@@ -537,72 +528,78 @@ var GlasstraceExporter = class {
537
528
  extra[ATTR.HTTP_DURATION_MS] = durationMs;
538
529
  }
539
530
  }
540
- } catch {
541
- }
542
- try {
543
531
  const errorMessage = attrs["exception.message"];
544
532
  if (errorMessage) {
545
533
  extra[ATTR.ERROR_MESSAGE] = errorMessage;
546
534
  }
547
- } catch {
548
- }
549
- try {
550
- const errorType = attrs["exception.type"];
551
- if (errorType) {
552
- extra[ATTR.ERROR_CODE] = errorType;
553
- extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(errorType);
535
+ try {
536
+ const errorType = attrs["exception.type"];
537
+ if (errorType) {
538
+ extra[ATTR.ERROR_CODE] = errorType;
539
+ extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(errorType);
540
+ }
541
+ } catch {
554
542
  }
555
- } catch {
556
- }
557
- try {
558
543
  const errorField = attrs["error.field"];
559
544
  if (errorField) {
560
545
  extra[ATTR.ERROR_FIELD] = errorField;
561
546
  }
562
- } catch {
563
- }
564
- try {
565
- const spanAny = span;
566
- const instrumentationName = spanAny.instrumentationScope?.name ?? spanAny.instrumentationLibrary?.name ?? "";
567
- const ormProvider = deriveOrmProvider(instrumentationName);
568
- if (ormProvider) {
569
- extra[ATTR.ORM_PROVIDER] = ormProvider;
570
- const model = attrs["db.sql.table"] ?? attrs["db.prisma.model"];
571
- if (model) {
572
- extra[ATTR.ORM_MODEL] = model;
573
- }
574
- const operation = attrs["db.operation"];
575
- if (operation) {
576
- extra[ATTR.ORM_OPERATION] = operation;
547
+ try {
548
+ const spanAny = span;
549
+ const instrumentationName = spanAny.instrumentationScope?.name ?? spanAny.instrumentationLibrary?.name ?? "";
550
+ const ormProvider = deriveOrmProvider(instrumentationName);
551
+ if (ormProvider) {
552
+ extra[ATTR.ORM_PROVIDER] = ormProvider;
553
+ const model = attrs["db.sql.table"] ?? attrs["db.prisma.model"];
554
+ if (model) {
555
+ extra[ATTR.ORM_MODEL] = model;
556
+ }
557
+ const operation = attrs["db.operation"];
558
+ if (operation) {
559
+ extra[ATTR.ORM_OPERATION] = operation;
560
+ }
577
561
  }
562
+ } catch {
578
563
  }
579
- } catch {
580
- }
581
- try {
582
- const url = attrs["http.url"] ?? attrs["url.full"];
583
- if (url && span.kind === SpanKind.CLIENT) {
584
- extra[ATTR.FETCH_TARGET] = classifyFetchTarget(url);
564
+ try {
565
+ const url = attrs["http.url"] ?? attrs["url.full"];
566
+ if (url && span.kind === SpanKind.CLIENT) {
567
+ extra[ATTR.FETCH_TARGET] = classifyFetchTarget(url);
568
+ }
569
+ } catch {
585
570
  }
571
+ return createEnrichedSpan(span, extra);
586
572
  } catch {
573
+ return span;
587
574
  }
588
- return createEnrichedSpan(span, extra);
589
575
  }
590
576
  /**
591
577
  * Lazily creates the delegate OTLP exporter once the API key is resolved.
578
+ * Recreates the delegate if the key has changed (e.g., after key rotation)
579
+ * so the Authorization header stays current.
592
580
  */
593
581
  ensureDelegate() {
594
582
  if (!this.createDelegateFn) return null;
595
- if (this.delegate) return this.delegate;
596
583
  const currentKey = this.getApiKey();
597
584
  if (currentKey === API_KEY_PENDING) return null;
585
+ if (this.delegate && this.delegateKey === currentKey) {
586
+ return this.delegate;
587
+ }
588
+ if (this.delegate) {
589
+ void this.delegate.shutdown?.().catch(() => {
590
+ });
591
+ }
598
592
  this.delegate = this.createDelegateFn(this.endpointUrl, {
599
593
  Authorization: `Bearer ${currentKey}`
600
594
  });
595
+ this.delegateKey = currentKey;
601
596
  return this.delegate;
602
597
  }
603
598
  /**
604
- * Buffers enriched spans while the API key is pending.
599
+ * Buffers raw (unenriched) spans while the API key is pending.
605
600
  * Evicts oldest batches if the buffer exceeds MAX_PENDING_SPANS.
601
+ * Re-checks the key after buffering to close the race window where
602
+ * the key resolves between the caller's check and this buffer call.
606
603
  */
607
604
  bufferSpans(spans, resultCallback) {
608
605
  this.pendingBatches.push({ spans, resultCallback });
@@ -618,10 +615,14 @@ var GlasstraceExporter = class {
618
615
  );
619
616
  }
620
617
  }
618
+ if (this.getApiKey() !== API_KEY_PENDING) {
619
+ this.flushPending();
620
+ }
621
621
  }
622
622
  /**
623
623
  * Flushes all buffered spans through the delegate exporter.
624
- * Called when the API key resolves.
624
+ * Enriches spans at flush time (not buffer time) so that session IDs
625
+ * are computed with the resolved API key instead of the "pending" sentinel.
625
626
  */
626
627
  flushPending() {
627
628
  if (this.pendingBatches.length === 0) return;
@@ -638,7 +639,8 @@ var GlasstraceExporter = class {
638
639
  this.pendingBatches = [];
639
640
  this.pendingSpanCount = 0;
640
641
  for (const batch of batches) {
641
- exporter.export(batch.spans, batch.resultCallback);
642
+ const enriched = batch.spans.map((span) => this.enrichSpan(span));
643
+ exporter.export(enriched, batch.resultCallback);
642
644
  }
643
645
  }
644
646
  };
@@ -762,6 +764,7 @@ function createDiscoveryHandler(getAnonKey, getSessionId) {
762
764
  // src/otel-config.ts
763
765
  var _resolvedApiKey = API_KEY_PENDING;
764
766
  var _activeExporter = null;
767
+ var _shutdownHandler = null;
765
768
  function setResolvedApiKey(key) {
766
769
  _resolvedApiKey = key;
767
770
  }
@@ -778,6 +781,33 @@ async function tryImport(moduleId) {
778
781
  return null;
779
782
  }
780
783
  }
784
+ function registerShutdownHooks(provider) {
785
+ if (typeof process === "undefined" || typeof process.once !== "function") {
786
+ return;
787
+ }
788
+ if (_shutdownHandler) {
789
+ process.removeListener("SIGTERM", _shutdownHandler);
790
+ process.removeListener("SIGINT", _shutdownHandler);
791
+ }
792
+ let shutdownCalled = false;
793
+ const shutdown = (signal) => {
794
+ if (shutdownCalled) return;
795
+ shutdownCalled = true;
796
+ void provider.shutdown().catch((err) => {
797
+ console.warn(
798
+ `[glasstrace] Error during OTel shutdown: ${err instanceof Error ? err.message : String(err)}`
799
+ );
800
+ }).finally(() => {
801
+ process.removeListener("SIGTERM", _shutdownHandler);
802
+ process.removeListener("SIGINT", _shutdownHandler);
803
+ process.kill(process.pid, signal);
804
+ });
805
+ };
806
+ const handler = (signal) => shutdown(signal);
807
+ _shutdownHandler = handler;
808
+ process.once("SIGTERM", handler);
809
+ process.once("SIGINT", handler);
810
+ }
781
811
  async function configureOtel(config, sessionManager) {
782
812
  const exporterUrl = `${config.endpoint}/v1/traces`;
783
813
  let createOtlpExporter = null;
@@ -818,7 +848,16 @@ async function configureOtel(config, sessionManager) {
818
848
  }
819
849
  try {
820
850
  const otelSdk = await import("@opentelemetry/sdk-trace-base");
821
- const otelApi = await import("@opentelemetry/api");
851
+ const otelApi3 = await import("@opentelemetry/api");
852
+ const existingProvider = otelApi3.trace.getTracerProvider();
853
+ const probeTracer = existingProvider.getTracer("glasstrace-probe");
854
+ if (probeTracer.constructor.name !== "ProxyTracer") {
855
+ console.warn(
856
+ "[glasstrace] An existing OpenTelemetry TracerProvider is already registered. Glasstrace will not overwrite it. To use Glasstrace alongside another tracing tool, add GlasstraceExporter as an additional span processor on your existing provider."
857
+ );
858
+ _activeExporter = null;
859
+ return;
860
+ }
822
861
  if (!createOtlpExporter) {
823
862
  const consoleExporter = new otelSdk.ConsoleSpanExporter();
824
863
  const consoleGlasstraceExporter = new GlasstraceExporter({
@@ -837,20 +876,16 @@ async function configureOtel(config, sessionManager) {
837
876
  const provider2 = new otelSdk.BasicTracerProvider({
838
877
  spanProcessors: [processor2]
839
878
  });
840
- otelApi.trace.setGlobalTracerProvider(provider2);
879
+ otelApi3.trace.setGlobalTracerProvider(provider2);
880
+ registerShutdownHooks(provider2);
841
881
  return;
842
882
  }
843
- const processor = new otelSdk.SimpleSpanProcessor(glasstraceExporter);
883
+ const processor = new otelSdk.BatchSpanProcessor(glasstraceExporter);
844
884
  const provider = new otelSdk.BasicTracerProvider({
845
885
  spanProcessors: [processor]
846
886
  });
847
- const existingProvider = otelApi.trace.getTracerProvider();
848
- if (existingProvider && existingProvider.constructor.name !== "ProxyTracerProvider") {
849
- console.warn(
850
- "[glasstrace] An existing OpenTelemetry TracerProvider was detected and will be replaced. If you use another tracing tool, configure Glasstrace as an additional exporter instead."
851
- );
852
- }
853
- otelApi.trace.setGlobalTracerProvider(provider);
887
+ otelApi3.trace.setGlobalTracerProvider(provider);
888
+ registerShutdownHooks(provider);
854
889
  } catch {
855
890
  console.warn(
856
891
  "[glasstrace] Neither @vercel/otel nor @opentelemetry/sdk-trace-base available. Tracing disabled."
@@ -858,7 +893,108 @@ async function configureOtel(config, sessionManager) {
858
893
  }
859
894
  }
860
895
 
896
+ // src/console-capture.ts
897
+ var isGlasstraceLog = false;
898
+ var originalError = null;
899
+ var originalWarn = null;
900
+ var installed = false;
901
+ var otelApi = null;
902
+ function formatArgs(args) {
903
+ return args.map((arg) => {
904
+ if (typeof arg === "string") return arg;
905
+ if (arg instanceof Error) return arg.stack ?? arg.message;
906
+ try {
907
+ return JSON.stringify(arg);
908
+ } catch {
909
+ return String(arg);
910
+ }
911
+ }).join(" ");
912
+ }
913
+ function isSdkMessage(args) {
914
+ return typeof args[0] === "string" && args[0].startsWith("[glasstrace]");
915
+ }
916
+ async function installConsoleCapture() {
917
+ if (installed) return;
918
+ try {
919
+ otelApi = await import("@opentelemetry/api");
920
+ } catch {
921
+ otelApi = null;
922
+ }
923
+ originalError = console.error;
924
+ originalWarn = console.warn;
925
+ installed = true;
926
+ console.error = (...args) => {
927
+ originalError.apply(console, args);
928
+ if (isGlasstraceLog || isSdkMessage(args)) return;
929
+ if (otelApi) {
930
+ const span = otelApi.trace.getSpan(otelApi.context.active());
931
+ if (span) {
932
+ span.addEvent("console.error", {
933
+ "console.message": formatArgs(args)
934
+ });
935
+ }
936
+ }
937
+ };
938
+ console.warn = (...args) => {
939
+ originalWarn.apply(console, args);
940
+ if (isGlasstraceLog || isSdkMessage(args)) return;
941
+ if (otelApi) {
942
+ const span = otelApi.trace.getSpan(otelApi.context.active());
943
+ if (span) {
944
+ span.addEvent("console.warn", {
945
+ "console.message": formatArgs(args)
946
+ });
947
+ }
948
+ }
949
+ };
950
+ }
951
+
952
+ // src/capture-error.ts
953
+ var otelApi2 = null;
954
+ var otelLoadAttempted = false;
955
+ var otelLoadPromise = null;
956
+ async function _preloadOtelApi() {
957
+ if (otelLoadAttempted) return;
958
+ otelLoadAttempted = true;
959
+ try {
960
+ otelApi2 = await import("@opentelemetry/api");
961
+ } catch {
962
+ otelApi2 = null;
963
+ }
964
+ }
965
+ function captureError(error) {
966
+ if (otelApi2) {
967
+ recordError(otelApi2, error);
968
+ return;
969
+ }
970
+ if (!otelLoadAttempted) {
971
+ otelLoadPromise ??= _preloadOtelApi();
972
+ }
973
+ if (otelLoadPromise) {
974
+ void otelLoadPromise.then(() => {
975
+ if (otelApi2) {
976
+ recordError(otelApi2, error);
977
+ }
978
+ });
979
+ }
980
+ }
981
+ function recordError(api, error) {
982
+ try {
983
+ const span = api.trace.getSpan(api.context.active());
984
+ if (!span) return;
985
+ const attributes = {
986
+ "error.message": String(error)
987
+ };
988
+ if (error instanceof Error) {
989
+ attributes["error.type"] = error.constructor.name;
990
+ }
991
+ span.addEvent("glasstrace.error", attributes);
992
+ } catch {
993
+ }
994
+ }
995
+
861
996
  // src/register.ts
997
+ var consoleCaptureInstalled = false;
862
998
  var discoveryHandler = null;
863
999
  var isRegistered = false;
864
1000
  var registrationGeneration = 0;
@@ -869,7 +1005,7 @@ function registerGlasstrace(options) {
869
1005
  }
870
1006
  const config = resolveConfig(options);
871
1007
  if (config.verbose) {
872
- console.info("[glasstrace] Step 1: Config resolved.");
1008
+ console.info("[glasstrace] Config resolved.");
873
1009
  }
874
1010
  if (isProductionDisabled(config)) {
875
1011
  console.warn(
@@ -878,7 +1014,7 @@ function registerGlasstrace(options) {
878
1014
  return;
879
1015
  }
880
1016
  if (config.verbose) {
881
- console.info("[glasstrace] Step 2: Not production-disabled.");
1017
+ console.info("[glasstrace] Not production-disabled.");
882
1018
  }
883
1019
  const anonymous = isAnonymousMode(config);
884
1020
  let effectiveKey = config.apiKey;
@@ -887,7 +1023,7 @@ function registerGlasstrace(options) {
887
1023
  }
888
1024
  if (config.verbose) {
889
1025
  console.info(
890
- `[glasstrace] Step 3: Auth mode = ${anonymous ? "anonymous" : "dev-key"}.`
1026
+ `[glasstrace] Auth mode = ${anonymous ? "anonymous" : "dev-key"}.`
891
1027
  );
892
1028
  }
893
1029
  const cachedInitResponse = loadCachedConfig();
@@ -896,19 +1032,21 @@ function registerGlasstrace(options) {
896
1032
  }
897
1033
  if (config.verbose) {
898
1034
  console.info(
899
- `[glasstrace] Step 4: Cached config ${cachedInitResponse ? "loaded and applied" : "not found"}.`
1035
+ `[glasstrace] Cached config ${cachedInitResponse ? "loaded and applied" : "not found"}.`
900
1036
  );
901
1037
  }
902
1038
  const sessionManager = new SessionManager();
903
1039
  if (config.verbose) {
904
- console.info("[glasstrace] Step 5: SessionManager created.");
1040
+ console.info("[glasstrace] SessionManager created.");
905
1041
  }
906
1042
  isRegistered = true;
907
1043
  const currentGeneration = registrationGeneration;
908
1044
  void configureOtel(config, sessionManager).then(
909
1045
  () => {
1046
+ void _preloadOtelApi();
1047
+ maybeInstallConsoleCapture();
910
1048
  if (config.verbose) {
911
- console.info("[glasstrace] Step 6: OTel configured.");
1049
+ console.info("[glasstrace] OTel configured.");
912
1050
  }
913
1051
  },
914
1052
  (err) => {
@@ -926,7 +1064,7 @@ function registerGlasstrace(options) {
926
1064
  () => sessionManager.getSessionId(getResolvedApiKey())
927
1065
  );
928
1066
  if (config.verbose) {
929
- console.info("[glasstrace] Step 8: Discovery endpoint registered (key pending).");
1067
+ console.info("[glasstrace] Discovery endpoint registered (key pending).");
930
1068
  }
931
1069
  void (async () => {
932
1070
  try {
@@ -942,9 +1080,10 @@ function registerGlasstrace(options) {
942
1080
  () => sessionManager.getSessionId(getResolvedApiKey())
943
1081
  );
944
1082
  if (config.verbose) {
945
- console.info("[glasstrace] Step 7: Background init firing.");
1083
+ console.info("[glasstrace] Background init firing.");
946
1084
  }
947
- await performInit(config, anonKey, "0.1.0");
1085
+ await performInit(config, anonKey, "0.2.0");
1086
+ maybeInstallConsoleCapture();
948
1087
  } catch (err) {
949
1088
  console.warn(
950
1089
  `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
@@ -961,9 +1100,10 @@ function registerGlasstrace(options) {
961
1100
  effectiveKey = anonKey;
962
1101
  if (currentGeneration !== registrationGeneration) return;
963
1102
  if (config.verbose) {
964
- console.info("[glasstrace] Step 7: Background init firing.");
1103
+ console.info("[glasstrace] Background init firing.");
965
1104
  }
966
- await performInit(config, anonKey, "0.1.0");
1105
+ await performInit(config, anonKey, "0.2.0");
1106
+ maybeInstallConsoleCapture();
967
1107
  } catch (err) {
968
1108
  console.warn(
969
1109
  `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
@@ -982,9 +1122,10 @@ function registerGlasstrace(options) {
982
1122
  }
983
1123
  if (currentGeneration !== registrationGeneration) return;
984
1124
  if (config.verbose) {
985
- console.info("[glasstrace] Step 7: Background init firing.");
1125
+ console.info("[glasstrace] Background init firing.");
986
1126
  }
987
- await performInit(config, anonKeyForInit, "0.1.0");
1127
+ await performInit(config, anonKeyForInit, "0.2.0");
1128
+ maybeInstallConsoleCapture();
988
1129
  } catch (err) {
989
1130
  console.warn(
990
1131
  `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
@@ -993,7 +1134,7 @@ function registerGlasstrace(options) {
993
1134
  })();
994
1135
  }
995
1136
  if (config.coverageMapEnabled && config.verbose) {
996
- console.info("[glasstrace] Step 9: Import graph building skipped.");
1137
+ console.info("[glasstrace] Import graph building skipped.");
997
1138
  }
998
1139
  } catch (err) {
999
1140
  console.warn(
@@ -1004,6 +1145,13 @@ function registerGlasstrace(options) {
1004
1145
  function getDiscoveryHandler() {
1005
1146
  return discoveryHandler;
1006
1147
  }
1148
+ function maybeInstallConsoleCapture() {
1149
+ if (consoleCaptureInstalled) return;
1150
+ if (getActiveConfig().consoleErrors) {
1151
+ consoleCaptureInstalled = true;
1152
+ void installConsoleCapture();
1153
+ }
1154
+ }
1007
1155
  function isDiscoveryEnabled(config) {
1008
1156
  if (process.env.GLASSTRACE_DISCOVERY_ENABLED === "true") return true;
1009
1157
  if (process.env.GLASSTRACE_DISCOVERY_ENABLED === "false") return false;
@@ -1088,6 +1236,10 @@ async function uploadSourceMaps(apiKey, endpoint, buildHash, maps) {
1088
1236
  body: JSON.stringify(body)
1089
1237
  });
1090
1238
  if (!response.ok) {
1239
+ try {
1240
+ await response.text();
1241
+ } catch {
1242
+ }
1091
1243
  throw new Error(
1092
1244
  `Source map upload failed: ${String(response.status)} ${response.statusText}`
1093
1245
  );
@@ -1163,6 +1315,7 @@ export {
1163
1315
  SdkError,
1164
1316
  SessionManager,
1165
1317
  buildImportGraph,
1318
+ captureError,
1166
1319
  classifyFetchTarget,
1167
1320
  collectSourceMaps,
1168
1321
  computeBuildHash,