@glasstrace/sdk 1.3.3 → 1.3.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/README.md CHANGED
@@ -163,6 +163,100 @@ GLASSTRACE_SUPPRESS_ACTION_NUDGE=1
163
163
  The nudge never fires in production (detected via `NODE_ENV` or
164
164
  `VERCEL_ENV`) unless `GLASSTRACE_FORCE_ENABLE=true` is also set.
165
165
 
166
+ ## Production deployment under Next 16
167
+
168
+ As of `@glasstrace/sdk@1.3.5`, auto-attach detection now classifies the
169
+ SDK's own bundled proxy correctly under bundler minification (DISC-1556
170
+ — verified against the `clean-next-sdk130` validation fixture). The
171
+ manual integration documented below remains supported for users who
172
+ prefer explicit configuration.
173
+
174
+ Next 16 (`next build && next start`) registers an OpenTelemetry
175
+ TracerProvider before user code runs. When `registerGlasstrace()` then
176
+ detects that provider, the SDK attempts to attach its span processor to
177
+ the existing pipeline. On most providers this auto-attach succeeds and
178
+ no further action is required; on a small number of provider shapes —
179
+ including Next 16's production-runtime provider in some versions — the
180
+ provider exposes no injection point and auto-attach returns
181
+ unsuccessfully. In that case spans flow through the existing pipeline
182
+ without reaching the Glasstrace exporter, so no traces appear in MCP
183
+ queries or the dashboard.
184
+
185
+ The SDK signals this case in three ways:
186
+
187
+ 1. **Log line.** The SDK logs a guidance message at `warn` level in
188
+ development and `error` level under `NODE_ENV=production`:
189
+
190
+ ```text
191
+ [glasstrace] An existing OTel TracerProvider is registered but
192
+ Glasstrace could not auto-attach its span processor.
193
+ Add Glasstrace to your provider configuration:
194
+ ...
195
+ ```
196
+
197
+ 2. **Programmatic signal.** `getStatus().tracing === "not-configured"`
198
+ after `registerGlasstrace()` has resolved indicates spans are not
199
+ reaching the Glasstrace exporter. Poll this from a health endpoint
200
+ or a startup readiness check:
201
+
202
+ ```ts
203
+ import { getStatus } from "@glasstrace/sdk";
204
+
205
+ const { tracing } = getStatus();
206
+ if (tracing === "not-configured") {
207
+ // Spans are not being exported. Apply the manual workaround below.
208
+ }
209
+ ```
210
+
211
+ 3. **CLI bridge.** `.glasstrace/runtime-state.json` carries a
212
+ structured `lastError` record that downstream tooling (custom
213
+ dashboards, CI assertions, the `npx @glasstrace/sdk status`
214
+ command in future releases) can surface verbatim:
215
+
216
+ ```json
217
+ {
218
+ "otel": { "state": "COEXISTENCE_FAILED", "scenario": "C/F" },
219
+ "lastError": {
220
+ "category": "auto-attach-returned-null",
221
+ "message": "tryAutoAttachGlasstraceProcessor returned null — ...",
222
+ "timestamp": "2026-05-04T12:34:56.789Z",
223
+ "providerClass": "BasicTracerProvider"
224
+ }
225
+ }
226
+ ```
227
+
228
+ The `providerClass` field is the constructor name of the existing
229
+ provider's delegate. URLs, headers, and credentials are never
230
+ captured.
231
+
232
+ ### Manual workaround
233
+
234
+ When auto-attach cannot succeed, register Glasstrace's span processor
235
+ on the provider you already own:
236
+
237
+ ```ts
238
+ import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
239
+ import { createGlasstraceSpanProcessor } from "@glasstrace/sdk";
240
+
241
+ const provider = new BasicTracerProvider({
242
+ spanProcessors: [
243
+ // ... your existing processors,
244
+ createGlasstraceSpanProcessor(),
245
+ ],
246
+ });
247
+ ```
248
+
249
+ `createGlasstraceSpanProcessor()` produces a processor with the same
250
+ branded exporter the auto-attach path uses, so duplicate
251
+ `registerGlasstrace()` calls remain idempotent. `registerGlasstrace()`
252
+ is still required when wiring the processor manually — it handles the
253
+ init handshake, anonymous-key resolution, session management, and
254
+ discovery endpoint, none of which are owned by the span processor.
255
+
256
+ A future SDK release may extend the auto-attach detection to recognize
257
+ additional Next 16 provider shapes; until that ships, the manual path
258
+ above is the production-supported integration.
259
+
166
260
  ## Capturing error response bodies
167
261
 
168
262
  When debugging a 4xx or 5xx, the response body is often the most useful
@@ -3671,6 +3671,27 @@ var OTLPTraceExporter = class extends OTLPExporterBase {
3671
3671
  }
3672
3672
  };
3673
3673
 
3674
+ // src/proxy-detection.ts
3675
+ function isProxyTracerProvider(value) {
3676
+ if (value === null || value === void 0 || typeof value !== "object") {
3677
+ return false;
3678
+ }
3679
+ return "getTracer" in value && typeof value.getTracer === "function" && "getDelegate" in value && typeof value.getDelegate === "function" && "setDelegate" in value && typeof value.setDelegate === "function" && "getDelegateTracer" in value && typeof value.getDelegateTracer === "function";
3680
+ }
3681
+ function isProxyTracer(value, ownerProvider) {
3682
+ if (value === null || value === void 0 || typeof value !== "object") {
3683
+ return false;
3684
+ }
3685
+ const structurallyShaped = "_getTracer" in value && typeof value._getTracer === "function" && "startSpan" in value && typeof value.startSpan === "function" && "startActiveSpan" in value && typeof value.startActiveSpan === "function";
3686
+ if (!structurallyShaped) {
3687
+ return false;
3688
+ }
3689
+ if (!Object.hasOwn(value, "_provider")) {
3690
+ return false;
3691
+ }
3692
+ return value._provider === ownerProvider;
3693
+ }
3694
+
3674
3695
  // src/otel-config.ts
3675
3696
  var resolvedApiKey = API_KEY_PENDING;
3676
3697
  var activeExporter = null;
@@ -3709,7 +3730,7 @@ async function configureOtel(config, sessionManager) {
3709
3730
  });
3710
3731
  const existingProvider = trace.getTracerProvider();
3711
3732
  const probeTracer = existingProvider.getTracer("glasstrace-probe");
3712
- const anotherProviderRegistered = probeTracer.constructor.name !== "ProxyTracer";
3733
+ const anotherProviderRegistered = !isProxyTracerProvider(existingProvider) || !isProxyTracer(probeTracer, existingProvider);
3713
3734
  if (anotherProviderRegistered) {
3714
3735
  setCoexistenceState("coexisting");
3715
3736
  await runCoexistencePath(existingProvider, config);
@@ -3763,11 +3784,27 @@ async function runCoexistencePath(existingProvider, config) {
3763
3784
  setOtelState(OtelState.COEXISTENCE_FAILED);
3764
3785
  emitLifecycleEvent("otel:configured", { state: OtelState.COEXISTENCE_FAILED, scenario: "C/F" });
3765
3786
  emitLifecycleEvent("otel:injection_failed", { reason: "provider internals inaccessible" });
3787
+ emitLifecycleEvent("otel:failed", {
3788
+ category: "auto-attach-returned-null",
3789
+ message: "tryAutoAttachGlasstraceProcessor returned null \u2014 the existing OTel TracerProvider exposed no injection point. Spans are not reaching the Glasstrace exporter. Apply the manual createGlasstraceSpanProcessor() workaround documented in the SDK README.",
3790
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3791
+ providerClass: readProviderClass(existingProvider)
3792
+ });
3766
3793
  const coreState = getCoreState();
3767
3794
  if (coreState === CoreState.ACTIVE || coreState === CoreState.KEY_RESOLVED) {
3768
3795
  setCoreState(CoreState.ACTIVE_DEGRADED);
3769
3796
  }
3770
3797
  }
3798
+ function readProviderClass(tracerProvider) {
3799
+ try {
3800
+ const proxy = tracerProvider;
3801
+ const delegate = typeof proxy.getDelegate === "function" ? proxy.getDelegate() : tracerProvider;
3802
+ const name = delegate?.constructor?.name;
3803
+ return typeof name === "string" && name.length > 0 ? name : void 0;
3804
+ } catch {
3805
+ return void 0;
3806
+ }
3807
+ }
3771
3808
  async function runRegistrationPath(config, sessionManager) {
3772
3809
  const exporterUrl = `${config.endpoint}/v1/traces`;
3773
3810
  const createOtlpExporter = (url, headers) => new OTLPTraceExporter({ url, headers });
@@ -4152,6 +4189,7 @@ import { join } from "node:path";
4152
4189
  var _projectRoot = null;
4153
4190
  var _sdkVersion = "unknown";
4154
4191
  var _lastScenario;
4192
+ var _lastError;
4155
4193
  var _debounceTimer = null;
4156
4194
  var _started = false;
4157
4195
  function startRuntimeStateWriter(options) {
@@ -4174,6 +4212,10 @@ function startRuntimeStateWriter(options) {
4174
4212
  _lastScenario = scenario;
4175
4213
  debouncedWrite();
4176
4214
  });
4215
+ onLifecycleEvent("otel:failed", (payload) => {
4216
+ _lastError = { ...payload };
4217
+ debouncedWrite();
4218
+ });
4177
4219
  onLifecycleEvent("auth:key_resolved", () => debouncedWrite());
4178
4220
  onLifecycleEvent("auth:claim_started", () => debouncedWrite());
4179
4221
  onLifecycleEvent("auth:claim_completed", () => debouncedWrite());
@@ -4201,6 +4243,9 @@ function writeStateNow() {
4201
4243
  auth: { state: state.auth },
4202
4244
  otel: { state: state.otel, scenario: _lastScenario }
4203
4245
  };
4246
+ if (_lastError) {
4247
+ runtimeState.lastError = _lastError;
4248
+ }
4204
4249
  const dir = join(_projectRoot, ".glasstrace");
4205
4250
  const filePath = join(dir, "runtime-state.json");
4206
4251
  mkdirSync(dir, { recursive: true, mode: 448 });
@@ -4245,7 +4290,7 @@ function registerGlasstrace(options) {
4245
4290
  setCoreState(CoreState.REGISTERING);
4246
4291
  startRuntimeStateWriter({
4247
4292
  projectRoot: process.cwd(),
4248
- sdkVersion: "1.3.3"
4293
+ sdkVersion: "1.3.5"
4249
4294
  });
4250
4295
  const config = resolveConfig(options);
4251
4296
  if (config.verbose) {
@@ -4261,8 +4306,9 @@ function registerGlasstrace(options) {
4261
4306
  if (config.verbose) {
4262
4307
  console.info("[glasstrace] Not production-disabled.");
4263
4308
  }
4264
- const existingProbe = trace.getTracerProvider().getTracer("glasstrace-probe");
4265
- const anotherProviderRegistered = existingProbe.constructor.name !== "ProxyTracer";
4309
+ const existingTracerProvider = trace.getTracerProvider();
4310
+ const existingProbe = existingTracerProvider.getTracer("glasstrace-probe");
4311
+ const anotherProviderRegistered = !isProxyTracerProvider(existingTracerProvider) || !isProxyTracer(existingProbe, existingTracerProvider);
4266
4312
  if (anotherProviderRegistered) {
4267
4313
  setCoexistenceState("coexisting");
4268
4314
  }
@@ -4411,8 +4457,8 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4411
4457
  if (config.verbose) {
4412
4458
  console.info("[glasstrace] Background init firing.");
4413
4459
  }
4414
- const healthReport = collectHealthReport("1.3.3");
4415
- const initResult = await performInit(config, anonKeyForInit, "1.3.3", healthReport);
4460
+ const healthReport = collectHealthReport("1.3.5");
4461
+ const initResult = await performInit(config, anonKeyForInit, "1.3.5", healthReport);
4416
4462
  if (generation !== registrationGeneration) return;
4417
4463
  const currentState = getCoreState();
4418
4464
  if (currentState === CoreState.SHUTTING_DOWN || currentState === CoreState.SHUTDOWN) {
@@ -4435,7 +4481,7 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4435
4481
  }
4436
4482
  maybeInstallConsoleCapture();
4437
4483
  if (didLastInitSucceed()) {
4438
- startHeartbeat(config, anonKeyForInit, "1.3.3", generation, (newApiKey, accountId) => {
4484
+ startHeartbeat(config, anonKeyForInit, "1.3.5", generation, (newApiKey, accountId) => {
4439
4485
  setAuthState(AuthState.CLAIMING);
4440
4486
  emitLifecycleEvent("auth:claim_started", { accountId });
4441
4487
  setResolvedApiKey(newApiKey);
@@ -4572,9 +4618,11 @@ This message will not appear once Glasstrace is added to your provider config.`
4572
4618
  }
4573
4619
  function emitGuidanceMessage() {
4574
4620
  const isSentry = detectSentry();
4621
+ const isProduction = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
4622
+ const level = isProduction ? "error" : "warn";
4575
4623
  if (isSentry) {
4576
4624
  sdkLog(
4577
- "warn",
4625
+ level,
4578
4626
  `[glasstrace] An existing OTel TracerProvider is registered but Glasstrace could not auto-attach its span processor.
4579
4627
  Add Glasstrace to your Sentry config:
4580
4628
 
@@ -4587,7 +4635,7 @@ Add Glasstrace to your Sentry config:
4587
4635
  );
4588
4636
  } else {
4589
4637
  sdkLog(
4590
- "warn",
4638
+ level,
4591
4639
  `[glasstrace] An existing OTel TracerProvider is registered but Glasstrace could not auto-attach its span processor.
4592
4640
  Add Glasstrace to your provider configuration:
4593
4641
 
@@ -4833,4 +4881,4 @@ export {
4833
4881
  withGlasstraceConfig,
4834
4882
  captureError
4835
4883
  };
4836
- //# sourceMappingURL=chunk-XNKG4WNQ.js.map
4884
+ //# sourceMappingURL=chunk-EK6MYHR2.js.map