@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.cjs CHANGED
@@ -35,6 +35,7 @@ __export(src_exports, {
35
35
  SdkError: () => SdkError,
36
36
  SessionManager: () => SessionManager,
37
37
  buildImportGraph: () => buildImportGraph,
38
+ captureError: () => captureError,
38
39
  classifyFetchTarget: () => classifyFetchTarget,
39
40
  collectSourceMaps: () => collectSourceMaps,
40
41
  computeBuildHash: () => computeBuildHash,
@@ -126,17 +127,18 @@ function isAnonymousMode(config) {
126
127
  var import_node_crypto = require("crypto");
127
128
  var import_protocol = require("@glasstrace/protocol");
128
129
  var FOUR_HOURS_MS = 4 * 60 * 60 * 1e3;
130
+ var cachedGlasstraceEnv = process.env.GLASSTRACE_ENV;
131
+ var cachedPort = process.env.PORT ?? "3000";
129
132
  function deriveSessionId(apiKey, origin, date, windowIndex) {
130
133
  const input = JSON.stringify([apiKey, origin, date, windowIndex]);
131
134
  const hash = (0, import_node_crypto.createHash)("sha256").update(input).digest("hex").slice(0, 16);
132
135
  return import_protocol.SessionIdSchema.parse(hash);
133
136
  }
134
137
  function getOrigin() {
135
- if (process.env.GLASSTRACE_ENV) {
136
- return process.env.GLASSTRACE_ENV;
138
+ if (cachedGlasstraceEnv) {
139
+ return cachedGlasstraceEnv;
137
140
  }
138
- const port = process.env.PORT ?? "3000";
139
- return `localhost:${port}`;
141
+ return `localhost:${cachedPort}`;
140
142
  }
141
143
  function getDateString() {
142
144
  const now = /* @__PURE__ */ new Date();
@@ -190,6 +192,7 @@ var SessionManager = class {
190
192
  };
191
193
 
192
194
  // src/fetch-classifier.ts
195
+ var cachedPort2 = process.env.PORT ?? "3000";
193
196
  function classifyFetchTarget(url) {
194
197
  let parsed;
195
198
  try {
@@ -204,8 +207,7 @@ function classifyFetchTarget(url) {
204
207
  if (hostname === "stripe.com" || hostname.endsWith(".stripe.com")) {
205
208
  return "stripe";
206
209
  }
207
- const port = process.env.PORT ?? "3000";
208
- const internalOrigin = `localhost:${port}`;
210
+ const internalOrigin = `localhost:${cachedPort2}`;
209
211
  const parsedPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
210
212
  const urlOrigin = `${hostname}:${parsedPort}`;
211
213
  if (urlOrigin === internalOrigin) {
@@ -350,6 +352,10 @@ async function sendInitRequest(config, anonKey, sdkVersion, importGraph, healthR
350
352
  signal
351
353
  });
352
354
  if (!response.ok) {
355
+ try {
356
+ await response.text();
357
+ } catch {
358
+ }
353
359
  const error = new Error(`Init request failed with status ${response.status}`);
354
360
  error.status = response.status;
355
361
  throw error;
@@ -472,6 +478,7 @@ var GlasstraceExporter = class {
472
478
  endpointUrl;
473
479
  createDelegateFn;
474
480
  delegate = null;
481
+ delegateKey = null;
475
482
  pendingBatches = [];
476
483
  pendingSpanCount = 0;
477
484
  overflowLogged = false;
@@ -484,12 +491,12 @@ var GlasstraceExporter = class {
484
491
  this.createDelegateFn = options.createDelegate;
485
492
  }
486
493
  export(spans, resultCallback) {
487
- const enrichedSpans = spans.map((span) => this.enrichSpan(span));
488
494
  const currentKey = this.getApiKey();
489
495
  if (currentKey === API_KEY_PENDING) {
490
- this.bufferSpans(enrichedSpans, resultCallback);
496
+ this.bufferSpans(spans, resultCallback);
491
497
  return;
492
498
  }
499
+ const enrichedSpans = spans.map((span) => this.enrichSpan(span));
493
500
  const exporter = this.ensureDelegate();
494
501
  if (exporter) {
495
502
  exporter.export(enrichedSpans, resultCallback);
@@ -531,58 +538,43 @@ var GlasstraceExporter = class {
531
538
  /**
532
539
  * Enriches a ReadableSpan with all glasstrace.* attributes.
533
540
  * Returns a new ReadableSpan wrapper; the original span is not mutated.
534
- * Each attribute derivation is wrapped in its own try-catch for partial
535
- * enrichment resilience.
541
+ *
542
+ * External function calls (getSessionId, deriveErrorCategory,
543
+ * deriveOrmProvider, classifyFetchTarget) are individually guarded so a
544
+ * failure in one does not prevent the remaining attributes from being set.
545
+ * On total failure, returns the original span unchanged.
536
546
  */
537
547
  enrichSpan(span) {
538
- const attrs = span.attributes ?? {};
539
- const name = span.name ?? "";
540
- const extra = {};
541
548
  try {
549
+ const attrs = span.attributes ?? {};
550
+ const name = span.name ?? "";
551
+ const extra = {};
542
552
  extra[ATTR.TRACE_TYPE] = "server";
543
- } catch {
544
- }
545
- try {
546
- const sessionId = this.sessionManager.getSessionId(this.getApiKey());
547
- extra[ATTR.SESSION_ID] = sessionId;
548
- } catch {
549
- }
550
- try {
553
+ try {
554
+ const sessionId = this.sessionManager.getSessionId(this.getApiKey());
555
+ extra[ATTR.SESSION_ID] = sessionId;
556
+ } catch {
557
+ }
551
558
  const env = this.environment ?? process.env.GLASSTRACE_ENV;
552
559
  if (env) {
553
560
  extra[ATTR.ENVIRONMENT] = env;
554
561
  }
555
- } catch {
556
- }
557
- try {
558
562
  const existingCid = attrs["glasstrace.correlation.id"];
559
563
  if (typeof existingCid === "string") {
560
564
  extra[ATTR.CORRELATION_ID] = existingCid;
561
565
  }
562
- } catch {
563
- }
564
- try {
565
566
  const route = attrs["http.route"] ?? name;
566
567
  if (route) {
567
568
  extra[ATTR.ROUTE] = route;
568
569
  }
569
- } catch {
570
- }
571
- try {
572
570
  const method = attrs["http.method"] ?? attrs["http.request.method"];
573
571
  if (method) {
574
572
  extra[ATTR.HTTP_METHOD] = method;
575
573
  }
576
- } catch {
577
- }
578
- try {
579
574
  const statusCode = attrs["http.status_code"] ?? attrs["http.response.status_code"];
580
575
  if (statusCode !== void 0) {
581
576
  extra[ATTR.HTTP_STATUS_CODE] = statusCode;
582
577
  }
583
- } catch {
584
- }
585
- try {
586
578
  if (span.startTime && span.endTime) {
587
579
  const [startSec, startNano] = span.startTime;
588
580
  const [endSec, endNano] = span.endTime;
@@ -591,72 +583,78 @@ var GlasstraceExporter = class {
591
583
  extra[ATTR.HTTP_DURATION_MS] = durationMs;
592
584
  }
593
585
  }
594
- } catch {
595
- }
596
- try {
597
586
  const errorMessage = attrs["exception.message"];
598
587
  if (errorMessage) {
599
588
  extra[ATTR.ERROR_MESSAGE] = errorMessage;
600
589
  }
601
- } catch {
602
- }
603
- try {
604
- const errorType = attrs["exception.type"];
605
- if (errorType) {
606
- extra[ATTR.ERROR_CODE] = errorType;
607
- extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(errorType);
590
+ try {
591
+ const errorType = attrs["exception.type"];
592
+ if (errorType) {
593
+ extra[ATTR.ERROR_CODE] = errorType;
594
+ extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(errorType);
595
+ }
596
+ } catch {
608
597
  }
609
- } catch {
610
- }
611
- try {
612
598
  const errorField = attrs["error.field"];
613
599
  if (errorField) {
614
600
  extra[ATTR.ERROR_FIELD] = errorField;
615
601
  }
616
- } catch {
617
- }
618
- try {
619
- const spanAny = span;
620
- const instrumentationName = spanAny.instrumentationScope?.name ?? spanAny.instrumentationLibrary?.name ?? "";
621
- const ormProvider = deriveOrmProvider(instrumentationName);
622
- if (ormProvider) {
623
- extra[ATTR.ORM_PROVIDER] = ormProvider;
624
- const model = attrs["db.sql.table"] ?? attrs["db.prisma.model"];
625
- if (model) {
626
- extra[ATTR.ORM_MODEL] = model;
627
- }
628
- const operation = attrs["db.operation"];
629
- if (operation) {
630
- extra[ATTR.ORM_OPERATION] = operation;
602
+ try {
603
+ const spanAny = span;
604
+ const instrumentationName = spanAny.instrumentationScope?.name ?? spanAny.instrumentationLibrary?.name ?? "";
605
+ const ormProvider = deriveOrmProvider(instrumentationName);
606
+ if (ormProvider) {
607
+ extra[ATTR.ORM_PROVIDER] = ormProvider;
608
+ const model = attrs["db.sql.table"] ?? attrs["db.prisma.model"];
609
+ if (model) {
610
+ extra[ATTR.ORM_MODEL] = model;
611
+ }
612
+ const operation = attrs["db.operation"];
613
+ if (operation) {
614
+ extra[ATTR.ORM_OPERATION] = operation;
615
+ }
631
616
  }
617
+ } catch {
632
618
  }
633
- } catch {
634
- }
635
- try {
636
- const url = attrs["http.url"] ?? attrs["url.full"];
637
- if (url && span.kind === import_api.SpanKind.CLIENT) {
638
- extra[ATTR.FETCH_TARGET] = classifyFetchTarget(url);
619
+ try {
620
+ const url = attrs["http.url"] ?? attrs["url.full"];
621
+ if (url && span.kind === import_api.SpanKind.CLIENT) {
622
+ extra[ATTR.FETCH_TARGET] = classifyFetchTarget(url);
623
+ }
624
+ } catch {
639
625
  }
626
+ return createEnrichedSpan(span, extra);
640
627
  } catch {
628
+ return span;
641
629
  }
642
- return createEnrichedSpan(span, extra);
643
630
  }
644
631
  /**
645
632
  * Lazily creates the delegate OTLP exporter once the API key is resolved.
633
+ * Recreates the delegate if the key has changed (e.g., after key rotation)
634
+ * so the Authorization header stays current.
646
635
  */
647
636
  ensureDelegate() {
648
637
  if (!this.createDelegateFn) return null;
649
- if (this.delegate) return this.delegate;
650
638
  const currentKey = this.getApiKey();
651
639
  if (currentKey === API_KEY_PENDING) return null;
640
+ if (this.delegate && this.delegateKey === currentKey) {
641
+ return this.delegate;
642
+ }
643
+ if (this.delegate) {
644
+ void this.delegate.shutdown?.().catch(() => {
645
+ });
646
+ }
652
647
  this.delegate = this.createDelegateFn(this.endpointUrl, {
653
648
  Authorization: `Bearer ${currentKey}`
654
649
  });
650
+ this.delegateKey = currentKey;
655
651
  return this.delegate;
656
652
  }
657
653
  /**
658
- * Buffers enriched spans while the API key is pending.
654
+ * Buffers raw (unenriched) spans while the API key is pending.
659
655
  * Evicts oldest batches if the buffer exceeds MAX_PENDING_SPANS.
656
+ * Re-checks the key after buffering to close the race window where
657
+ * the key resolves between the caller's check and this buffer call.
660
658
  */
661
659
  bufferSpans(spans, resultCallback) {
662
660
  this.pendingBatches.push({ spans, resultCallback });
@@ -672,10 +670,14 @@ var GlasstraceExporter = class {
672
670
  );
673
671
  }
674
672
  }
673
+ if (this.getApiKey() !== API_KEY_PENDING) {
674
+ this.flushPending();
675
+ }
675
676
  }
676
677
  /**
677
678
  * Flushes all buffered spans through the delegate exporter.
678
- * Called when the API key resolves.
679
+ * Enriches spans at flush time (not buffer time) so that session IDs
680
+ * are computed with the resolved API key instead of the "pending" sentinel.
679
681
  */
680
682
  flushPending() {
681
683
  if (this.pendingBatches.length === 0) return;
@@ -692,7 +694,8 @@ var GlasstraceExporter = class {
692
694
  this.pendingBatches = [];
693
695
  this.pendingSpanCount = 0;
694
696
  for (const batch of batches) {
695
- exporter.export(batch.spans, batch.resultCallback);
697
+ const enriched = batch.spans.map((span) => this.enrichSpan(span));
698
+ exporter.export(enriched, batch.resultCallback);
696
699
  }
697
700
  }
698
701
  };
@@ -816,6 +819,7 @@ function createDiscoveryHandler(getAnonKey, getSessionId) {
816
819
  // src/otel-config.ts
817
820
  var _resolvedApiKey = API_KEY_PENDING;
818
821
  var _activeExporter = null;
822
+ var _shutdownHandler = null;
819
823
  function setResolvedApiKey(key) {
820
824
  _resolvedApiKey = key;
821
825
  }
@@ -832,6 +836,33 @@ async function tryImport(moduleId) {
832
836
  return null;
833
837
  }
834
838
  }
839
+ function registerShutdownHooks(provider) {
840
+ if (typeof process === "undefined" || typeof process.once !== "function") {
841
+ return;
842
+ }
843
+ if (_shutdownHandler) {
844
+ process.removeListener("SIGTERM", _shutdownHandler);
845
+ process.removeListener("SIGINT", _shutdownHandler);
846
+ }
847
+ let shutdownCalled = false;
848
+ const shutdown = (signal) => {
849
+ if (shutdownCalled) return;
850
+ shutdownCalled = true;
851
+ void provider.shutdown().catch((err) => {
852
+ console.warn(
853
+ `[glasstrace] Error during OTel shutdown: ${err instanceof Error ? err.message : String(err)}`
854
+ );
855
+ }).finally(() => {
856
+ process.removeListener("SIGTERM", _shutdownHandler);
857
+ process.removeListener("SIGINT", _shutdownHandler);
858
+ process.kill(process.pid, signal);
859
+ });
860
+ };
861
+ const handler = (signal) => shutdown(signal);
862
+ _shutdownHandler = handler;
863
+ process.once("SIGTERM", handler);
864
+ process.once("SIGINT", handler);
865
+ }
835
866
  async function configureOtel(config, sessionManager) {
836
867
  const exporterUrl = `${config.endpoint}/v1/traces`;
837
868
  let createOtlpExporter = null;
@@ -872,7 +903,16 @@ async function configureOtel(config, sessionManager) {
872
903
  }
873
904
  try {
874
905
  const otelSdk = await import("@opentelemetry/sdk-trace-base");
875
- const otelApi = await import("@opentelemetry/api");
906
+ const otelApi3 = await import("@opentelemetry/api");
907
+ const existingProvider = otelApi3.trace.getTracerProvider();
908
+ const probeTracer = existingProvider.getTracer("glasstrace-probe");
909
+ if (probeTracer.constructor.name !== "ProxyTracer") {
910
+ console.warn(
911
+ "[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."
912
+ );
913
+ _activeExporter = null;
914
+ return;
915
+ }
876
916
  if (!createOtlpExporter) {
877
917
  const consoleExporter = new otelSdk.ConsoleSpanExporter();
878
918
  const consoleGlasstraceExporter = new GlasstraceExporter({
@@ -891,20 +931,16 @@ async function configureOtel(config, sessionManager) {
891
931
  const provider2 = new otelSdk.BasicTracerProvider({
892
932
  spanProcessors: [processor2]
893
933
  });
894
- otelApi.trace.setGlobalTracerProvider(provider2);
934
+ otelApi3.trace.setGlobalTracerProvider(provider2);
935
+ registerShutdownHooks(provider2);
895
936
  return;
896
937
  }
897
- const processor = new otelSdk.SimpleSpanProcessor(glasstraceExporter);
938
+ const processor = new otelSdk.BatchSpanProcessor(glasstraceExporter);
898
939
  const provider = new otelSdk.BasicTracerProvider({
899
940
  spanProcessors: [processor]
900
941
  });
901
- const existingProvider = otelApi.trace.getTracerProvider();
902
- if (existingProvider && existingProvider.constructor.name !== "ProxyTracerProvider") {
903
- console.warn(
904
- "[glasstrace] An existing OpenTelemetry TracerProvider was detected and will be replaced. If you use another tracing tool, configure Glasstrace as an additional exporter instead."
905
- );
906
- }
907
- otelApi.trace.setGlobalTracerProvider(provider);
942
+ otelApi3.trace.setGlobalTracerProvider(provider);
943
+ registerShutdownHooks(provider);
908
944
  } catch {
909
945
  console.warn(
910
946
  "[glasstrace] Neither @vercel/otel nor @opentelemetry/sdk-trace-base available. Tracing disabled."
@@ -912,7 +948,108 @@ async function configureOtel(config, sessionManager) {
912
948
  }
913
949
  }
914
950
 
951
+ // src/console-capture.ts
952
+ var isGlasstraceLog = false;
953
+ var originalError = null;
954
+ var originalWarn = null;
955
+ var installed = false;
956
+ var otelApi = null;
957
+ function formatArgs(args) {
958
+ return args.map((arg) => {
959
+ if (typeof arg === "string") return arg;
960
+ if (arg instanceof Error) return arg.stack ?? arg.message;
961
+ try {
962
+ return JSON.stringify(arg);
963
+ } catch {
964
+ return String(arg);
965
+ }
966
+ }).join(" ");
967
+ }
968
+ function isSdkMessage(args) {
969
+ return typeof args[0] === "string" && args[0].startsWith("[glasstrace]");
970
+ }
971
+ async function installConsoleCapture() {
972
+ if (installed) return;
973
+ try {
974
+ otelApi = await import("@opentelemetry/api");
975
+ } catch {
976
+ otelApi = null;
977
+ }
978
+ originalError = console.error;
979
+ originalWarn = console.warn;
980
+ installed = true;
981
+ console.error = (...args) => {
982
+ originalError.apply(console, args);
983
+ if (isGlasstraceLog || isSdkMessage(args)) return;
984
+ if (otelApi) {
985
+ const span = otelApi.trace.getSpan(otelApi.context.active());
986
+ if (span) {
987
+ span.addEvent("console.error", {
988
+ "console.message": formatArgs(args)
989
+ });
990
+ }
991
+ }
992
+ };
993
+ console.warn = (...args) => {
994
+ originalWarn.apply(console, args);
995
+ if (isGlasstraceLog || isSdkMessage(args)) return;
996
+ if (otelApi) {
997
+ const span = otelApi.trace.getSpan(otelApi.context.active());
998
+ if (span) {
999
+ span.addEvent("console.warn", {
1000
+ "console.message": formatArgs(args)
1001
+ });
1002
+ }
1003
+ }
1004
+ };
1005
+ }
1006
+
1007
+ // src/capture-error.ts
1008
+ var otelApi2 = null;
1009
+ var otelLoadAttempted = false;
1010
+ var otelLoadPromise = null;
1011
+ async function _preloadOtelApi() {
1012
+ if (otelLoadAttempted) return;
1013
+ otelLoadAttempted = true;
1014
+ try {
1015
+ otelApi2 = await import("@opentelemetry/api");
1016
+ } catch {
1017
+ otelApi2 = null;
1018
+ }
1019
+ }
1020
+ function captureError(error) {
1021
+ if (otelApi2) {
1022
+ recordError(otelApi2, error);
1023
+ return;
1024
+ }
1025
+ if (!otelLoadAttempted) {
1026
+ otelLoadPromise ??= _preloadOtelApi();
1027
+ }
1028
+ if (otelLoadPromise) {
1029
+ void otelLoadPromise.then(() => {
1030
+ if (otelApi2) {
1031
+ recordError(otelApi2, error);
1032
+ }
1033
+ });
1034
+ }
1035
+ }
1036
+ function recordError(api, error) {
1037
+ try {
1038
+ const span = api.trace.getSpan(api.context.active());
1039
+ if (!span) return;
1040
+ const attributes = {
1041
+ "error.message": String(error)
1042
+ };
1043
+ if (error instanceof Error) {
1044
+ attributes["error.type"] = error.constructor.name;
1045
+ }
1046
+ span.addEvent("glasstrace.error", attributes);
1047
+ } catch {
1048
+ }
1049
+ }
1050
+
915
1051
  // src/register.ts
1052
+ var consoleCaptureInstalled = false;
916
1053
  var discoveryHandler = null;
917
1054
  var isRegistered = false;
918
1055
  var registrationGeneration = 0;
@@ -923,7 +1060,7 @@ function registerGlasstrace(options) {
923
1060
  }
924
1061
  const config = resolveConfig(options);
925
1062
  if (config.verbose) {
926
- console.info("[glasstrace] Step 1: Config resolved.");
1063
+ console.info("[glasstrace] Config resolved.");
927
1064
  }
928
1065
  if (isProductionDisabled(config)) {
929
1066
  console.warn(
@@ -932,7 +1069,7 @@ function registerGlasstrace(options) {
932
1069
  return;
933
1070
  }
934
1071
  if (config.verbose) {
935
- console.info("[glasstrace] Step 2: Not production-disabled.");
1072
+ console.info("[glasstrace] Not production-disabled.");
936
1073
  }
937
1074
  const anonymous = isAnonymousMode(config);
938
1075
  let effectiveKey = config.apiKey;
@@ -941,7 +1078,7 @@ function registerGlasstrace(options) {
941
1078
  }
942
1079
  if (config.verbose) {
943
1080
  console.info(
944
- `[glasstrace] Step 3: Auth mode = ${anonymous ? "anonymous" : "dev-key"}.`
1081
+ `[glasstrace] Auth mode = ${anonymous ? "anonymous" : "dev-key"}.`
945
1082
  );
946
1083
  }
947
1084
  const cachedInitResponse = loadCachedConfig();
@@ -950,19 +1087,21 @@ function registerGlasstrace(options) {
950
1087
  }
951
1088
  if (config.verbose) {
952
1089
  console.info(
953
- `[glasstrace] Step 4: Cached config ${cachedInitResponse ? "loaded and applied" : "not found"}.`
1090
+ `[glasstrace] Cached config ${cachedInitResponse ? "loaded and applied" : "not found"}.`
954
1091
  );
955
1092
  }
956
1093
  const sessionManager = new SessionManager();
957
1094
  if (config.verbose) {
958
- console.info("[glasstrace] Step 5: SessionManager created.");
1095
+ console.info("[glasstrace] SessionManager created.");
959
1096
  }
960
1097
  isRegistered = true;
961
1098
  const currentGeneration = registrationGeneration;
962
1099
  void configureOtel(config, sessionManager).then(
963
1100
  () => {
1101
+ void _preloadOtelApi();
1102
+ maybeInstallConsoleCapture();
964
1103
  if (config.verbose) {
965
- console.info("[glasstrace] Step 6: OTel configured.");
1104
+ console.info("[glasstrace] OTel configured.");
966
1105
  }
967
1106
  },
968
1107
  (err) => {
@@ -980,7 +1119,7 @@ function registerGlasstrace(options) {
980
1119
  () => sessionManager.getSessionId(getResolvedApiKey())
981
1120
  );
982
1121
  if (config.verbose) {
983
- console.info("[glasstrace] Step 8: Discovery endpoint registered (key pending).");
1122
+ console.info("[glasstrace] Discovery endpoint registered (key pending).");
984
1123
  }
985
1124
  void (async () => {
986
1125
  try {
@@ -996,9 +1135,10 @@ function registerGlasstrace(options) {
996
1135
  () => sessionManager.getSessionId(getResolvedApiKey())
997
1136
  );
998
1137
  if (config.verbose) {
999
- console.info("[glasstrace] Step 7: Background init firing.");
1138
+ console.info("[glasstrace] Background init firing.");
1000
1139
  }
1001
- await performInit(config, anonKey, "0.1.0");
1140
+ await performInit(config, anonKey, "0.2.0");
1141
+ maybeInstallConsoleCapture();
1002
1142
  } catch (err) {
1003
1143
  console.warn(
1004
1144
  `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
@@ -1015,9 +1155,10 @@ function registerGlasstrace(options) {
1015
1155
  effectiveKey = anonKey;
1016
1156
  if (currentGeneration !== registrationGeneration) return;
1017
1157
  if (config.verbose) {
1018
- console.info("[glasstrace] Step 7: Background init firing.");
1158
+ console.info("[glasstrace] Background init firing.");
1019
1159
  }
1020
- await performInit(config, anonKey, "0.1.0");
1160
+ await performInit(config, anonKey, "0.2.0");
1161
+ maybeInstallConsoleCapture();
1021
1162
  } catch (err) {
1022
1163
  console.warn(
1023
1164
  `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
@@ -1036,9 +1177,10 @@ function registerGlasstrace(options) {
1036
1177
  }
1037
1178
  if (currentGeneration !== registrationGeneration) return;
1038
1179
  if (config.verbose) {
1039
- console.info("[glasstrace] Step 7: Background init firing.");
1180
+ console.info("[glasstrace] Background init firing.");
1040
1181
  }
1041
- await performInit(config, anonKeyForInit, "0.1.0");
1182
+ await performInit(config, anonKeyForInit, "0.2.0");
1183
+ maybeInstallConsoleCapture();
1042
1184
  } catch (err) {
1043
1185
  console.warn(
1044
1186
  `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
@@ -1047,7 +1189,7 @@ function registerGlasstrace(options) {
1047
1189
  })();
1048
1190
  }
1049
1191
  if (config.coverageMapEnabled && config.verbose) {
1050
- console.info("[glasstrace] Step 9: Import graph building skipped.");
1192
+ console.info("[glasstrace] Import graph building skipped.");
1051
1193
  }
1052
1194
  } catch (err) {
1053
1195
  console.warn(
@@ -1058,6 +1200,13 @@ function registerGlasstrace(options) {
1058
1200
  function getDiscoveryHandler() {
1059
1201
  return discoveryHandler;
1060
1202
  }
1203
+ function maybeInstallConsoleCapture() {
1204
+ if (consoleCaptureInstalled) return;
1205
+ if (getActiveConfig().consoleErrors) {
1206
+ consoleCaptureInstalled = true;
1207
+ void installConsoleCapture();
1208
+ }
1209
+ }
1061
1210
  function isDiscoveryEnabled(config) {
1062
1211
  if (process.env.GLASSTRACE_DISCOVERY_ENABLED === "true") return true;
1063
1212
  if (process.env.GLASSTRACE_DISCOVERY_ENABLED === "false") return false;
@@ -1140,6 +1289,10 @@ async function uploadSourceMaps(apiKey, endpoint, buildHash, maps) {
1140
1289
  body: JSON.stringify(body)
1141
1290
  });
1142
1291
  if (!response.ok) {
1292
+ try {
1293
+ await response.text();
1294
+ } catch {
1295
+ }
1143
1296
  throw new Error(
1144
1297
  `Source map upload failed: ${String(response.status)} ${response.statusText}`
1145
1298
  );
@@ -1379,6 +1532,7 @@ async function buildImportGraph(projectRoot) {
1379
1532
  SdkError,
1380
1533
  SessionManager,
1381
1534
  buildImportGraph,
1535
+ captureError,
1382
1536
  classifyFetchTarget,
1383
1537
  collectSourceMaps,
1384
1538
  computeBuildHash,