@glasstrace/sdk 1.7.0 → 1.8.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.
@@ -143,1114 +143,1422 @@ function classifyFetchTarget(url) {
143
143
  return "unknown";
144
144
  }
145
145
 
146
- // src/error-response-body.ts
147
- var ERROR_RESPONSE_BODY_MAX_BYTES = 4096;
148
- var ERROR_RESPONSE_BODY_TRUNCATION_MARKER = "...[truncated]";
149
- var REDACTED = "[REDACTED]";
150
- var ERROR_STATUS_MIN = 400;
151
- var ERROR_STATUS_MAX = 599;
152
- function coerceHttpStatus(value) {
153
- let numeric;
154
- if (typeof value === "number") {
155
- numeric = value;
156
- } else if (typeof value === "string") {
157
- const trimmed = value.trim();
158
- if (trimmed.length === 0) return void 0;
159
- numeric = Number(trimmed);
160
- } else {
161
- return void 0;
162
- }
163
- return Number.isFinite(numeric) ? numeric : void 0;
146
+ // src/lifecycle.ts
147
+ import { EventEmitter } from "node:events";
148
+
149
+ // src/signal-handler.ts
150
+ var coexistenceState = "unknown";
151
+ function setCoexistenceState(s) {
152
+ coexistenceState = s;
164
153
  }
165
- function isHttpErrorStatus(status) {
166
- const numeric = coerceHttpStatus(status);
167
- if (numeric === void 0) return false;
168
- return numeric >= ERROR_STATUS_MIN && numeric <= ERROR_STATUS_MAX;
154
+ function getCoexistenceState() {
155
+ return coexistenceState;
169
156
  }
170
- var REDACTION_PATTERNS = [
171
- // Order matters: redact specific token shapes BEFORE the generic
172
- // key=value catcher so a literal `Bearer eyJ…` collapses into a single
173
- // [REDACTED] and the JWT regex does not separately match the suffix.
174
- {
175
- name: "bearer",
176
- // Case-insensitive on the scheme: HTTP frameworks and proxies
177
- // round-trip the auth scheme with inconsistent casing
178
- // (`Bearer`, `bearer`, `BEARER`), and a real token leaks just as
179
- // badly under any of them.
180
- pattern: /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi
181
- },
182
- {
183
- name: "jwt",
184
- // Three base64url segments separated by dots. Real JWTs encode at
185
- // minimum a small JSON header in the first segment, which alone is
186
- // typically ≥10 chars after base64url; a 16-char floor avoids false
187
- // positives on dotted text like a stack-trace frame
188
- // (`react.dom.server`) while still catching every real JWT we have
189
- // seen in the wild. Anchored with word boundaries on both sides so
190
- // a 3-dot semantic version like "next@15.4.1.2" does not match.
191
- pattern: /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
192
- },
193
- {
194
- name: "glasstrace-api-key",
195
- // gt_dev_* and gt_anon_* keys are >=24 chars of [A-Za-z0-9].
196
- pattern: /\bgt_(?:dev|anon)_[A-Za-z0-9]{16,}\b/g
197
- },
198
- {
199
- name: "aws-access-key",
200
- // 20-char prefix-fixed identifier.
201
- pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g
202
- },
203
- {
204
- name: "key-value-secret-quoted",
205
- // Quoted-string variant: (key) [:=] "<value>". The value runs to
206
- // the next unescaped closing quote so a multi-word secret like
207
- // `password="my secret phrase"` is fully consumed instead of
208
- // splitting at the first space and leaving the tail visible.
209
- // The leading `(?<![A-Za-z0-9_])` prevents matching inside
210
- // identifiers like `passwordless`. The trailing `"?` after the
211
- // keyword absorbs the closing quote in JSON-style `"apikey":
212
- // "value"` so the colon is still seen as the separator.
213
- pattern: /(?<![A-Za-z0-9_])(?:api[_-]?key|apikey|secret|password|token)"?\s*[:=]\s*"(?:[^"\\]|\\.)*"/gi
214
- },
215
- {
216
- name: "key-value-secret-bare",
217
- // Unquoted variant: (key) [:=] <bare-value>. The bare value
218
- // capture stops at common JSON/text delimiters so we redact only
219
- // the value, not surrounding structure. Listed AFTER the quoted
220
- // variant so a quoted value's surrounding `"` are consumed by
221
- // the first pattern and we never fall through here for a quoted
222
- // secret.
223
- pattern: /(?<![A-Za-z0-9_])(?:api[_-]?key|apikey|secret|password|token)"?\s*[:=]\s*[^\s,;}\]"]+/gi
224
- }
225
- ];
226
- function sanitizeErrorResponseBody(body) {
227
- let out = body;
228
- for (const { pattern } of REDACTION_PATTERNS) {
229
- out = out.replace(pattern, REDACTED);
157
+
158
+ // src/lifecycle.ts
159
+ var CoreState = {
160
+ IDLE: "IDLE",
161
+ REGISTERING: "REGISTERING",
162
+ KEY_PENDING: "KEY_PENDING",
163
+ KEY_RESOLVED: "KEY_RESOLVED",
164
+ ACTIVE: "ACTIVE",
165
+ ACTIVE_DEGRADED: "ACTIVE_DEGRADED",
166
+ SHUTTING_DOWN: "SHUTTING_DOWN",
167
+ SHUTDOWN: "SHUTDOWN",
168
+ PRODUCTION_DISABLED: "PRODUCTION_DISABLED",
169
+ REGISTRATION_FAILED: "REGISTRATION_FAILED"
170
+ };
171
+ var AuthState = {
172
+ ANONYMOUS: "ANONYMOUS",
173
+ AUTHENTICATED: "AUTHENTICATED",
174
+ CLAIMING: "CLAIMING",
175
+ CLAIMED: "CLAIMED"
176
+ };
177
+ var OtelState = {
178
+ UNCONFIGURED: "UNCONFIGURED",
179
+ CONFIGURING: "CONFIGURING",
180
+ OWNS_PROVIDER: "OWNS_PROVIDER",
181
+ AUTO_ATTACHED: "AUTO_ATTACHED",
182
+ PROCESSOR_PRESENT: "PROCESSOR_PRESENT",
183
+ COEXISTENCE_FAILED: "COEXISTENCE_FAILED"
184
+ };
185
+ var VALID_CORE_TRANSITIONS = {
186
+ [CoreState.IDLE]: [CoreState.REGISTERING, CoreState.REGISTRATION_FAILED, CoreState.SHUTTING_DOWN],
187
+ [CoreState.REGISTERING]: [
188
+ CoreState.KEY_PENDING,
189
+ CoreState.PRODUCTION_DISABLED,
190
+ CoreState.REGISTRATION_FAILED,
191
+ CoreState.SHUTTING_DOWN
192
+ ],
193
+ [CoreState.KEY_PENDING]: [
194
+ CoreState.KEY_RESOLVED,
195
+ CoreState.REGISTRATION_FAILED,
196
+ CoreState.SHUTTING_DOWN
197
+ ],
198
+ [CoreState.KEY_RESOLVED]: [
199
+ CoreState.ACTIVE,
200
+ CoreState.ACTIVE_DEGRADED,
201
+ CoreState.SHUTTING_DOWN
202
+ ],
203
+ [CoreState.ACTIVE]: [
204
+ CoreState.ACTIVE_DEGRADED,
205
+ CoreState.SHUTTING_DOWN
206
+ ],
207
+ [CoreState.ACTIVE_DEGRADED]: [
208
+ CoreState.ACTIVE,
209
+ CoreState.SHUTTING_DOWN
210
+ ],
211
+ [CoreState.SHUTTING_DOWN]: [CoreState.SHUTDOWN],
212
+ [CoreState.SHUTDOWN]: [],
213
+ [CoreState.PRODUCTION_DISABLED]: [],
214
+ [CoreState.REGISTRATION_FAILED]: []
215
+ };
216
+ var VALID_AUTH_TRANSITIONS = {
217
+ [AuthState.ANONYMOUS]: [AuthState.CLAIMING],
218
+ [AuthState.AUTHENTICATED]: [AuthState.CLAIMING],
219
+ [AuthState.CLAIMING]: [AuthState.CLAIMED],
220
+ [AuthState.CLAIMED]: [AuthState.CLAIMING]
221
+ };
222
+ var VALID_OTEL_TRANSITIONS = {
223
+ [OtelState.UNCONFIGURED]: [OtelState.CONFIGURING],
224
+ [OtelState.CONFIGURING]: [
225
+ OtelState.OWNS_PROVIDER,
226
+ OtelState.AUTO_ATTACHED,
227
+ OtelState.PROCESSOR_PRESENT,
228
+ OtelState.COEXISTENCE_FAILED
229
+ ],
230
+ [OtelState.OWNS_PROVIDER]: [],
231
+ [OtelState.AUTO_ATTACHED]: [],
232
+ [OtelState.PROCESSOR_PRESENT]: [],
233
+ [OtelState.COEXISTENCE_FAILED]: []
234
+ };
235
+ var _coreState = CoreState.IDLE;
236
+ var _authState = AuthState.ANONYMOUS;
237
+ var _otelState = OtelState.UNCONFIGURED;
238
+ var _emitter = new EventEmitter();
239
+ var _logger = null;
240
+ var _initialized = false;
241
+ var _initWarned = false;
242
+ var _coreReadyEmitted = false;
243
+ var _authInitialized = false;
244
+ var _emitting = false;
245
+ function initLifecycle(options) {
246
+ if (_initialized) {
247
+ options.logger("warn", "[glasstrace] initLifecycle() called twice \u2014 ignored.");
248
+ return;
230
249
  }
231
- return out;
250
+ _logger = options.logger;
251
+ _initialized = true;
232
252
  }
233
- function truncateErrorResponseBody(body) {
234
- const encoder = new TextEncoder();
235
- const encoded = encoder.encode(body);
236
- if (encoded.byteLength <= ERROR_RESPONSE_BODY_MAX_BYTES) {
237
- return body;
253
+ function warnIfNotInitialized() {
254
+ if (!_initialized && !_initWarned) {
255
+ _initWarned = true;
256
+ console.warn(
257
+ "[glasstrace] Lifecycle state changed before initLifecycle() was called. Logger not available \u2014 errors will be silent."
258
+ );
238
259
  }
239
- let cut = ERROR_RESPONSE_BODY_MAX_BYTES;
240
- let scan = cut - 1;
241
- while (scan >= 0 && (encoded[scan] & 192) === 128) {
242
- scan -= 1;
260
+ }
261
+ function setCoreState(to) {
262
+ warnIfNotInitialized();
263
+ const from = _coreState;
264
+ if (from === to) return;
265
+ const valid = VALID_CORE_TRANSITIONS[from];
266
+ if (!valid.includes(to)) {
267
+ _logger?.(
268
+ "warn",
269
+ `[glasstrace] Invalid core state transition: ${from} \u2192 ${to}. Ignored.`
270
+ );
271
+ return;
243
272
  }
244
- if (scan >= 0) {
245
- const leading = encoded[scan];
246
- let expected = 1;
247
- if ((leading & 128) === 0) {
248
- expected = 1;
249
- } else if ((leading & 224) === 192) {
250
- expected = 2;
251
- } else if ((leading & 240) === 224) {
252
- expected = 3;
253
- } else if ((leading & 248) === 240) {
254
- expected = 4;
273
+ _coreState = to;
274
+ if (_emitting) return;
275
+ _emitting = true;
276
+ try {
277
+ emitSafe("core:state_changed", { from, to });
278
+ const current = _coreState;
279
+ if (!_coreReadyEmitted && (current === CoreState.ACTIVE || current === CoreState.ACTIVE_DEGRADED)) {
280
+ _coreReadyEmitted = true;
281
+ emitSafe("core:ready", {});
255
282
  }
256
- if (scan + expected > cut) {
257
- cut = scan;
283
+ if (current === CoreState.SHUTTING_DOWN) {
284
+ emitSafe("core:shutdown_started", {});
285
+ }
286
+ if (current === CoreState.SHUTDOWN) {
287
+ emitSafe("core:shutdown_completed", {});
258
288
  }
289
+ } finally {
290
+ _emitting = false;
291
+ }
292
+ if (to === CoreState.ACTIVE && _degradationSources.size > 0) {
293
+ recomputeCoreFromDegradationSources();
259
294
  }
260
- const decoder = new TextDecoder("utf-8", { fatal: false });
261
- const sliced = encoded.subarray(0, cut);
262
- const decoded = decoder.decode(sliced);
263
- return decoded + ERROR_RESPONSE_BODY_TRUNCATION_MARKER;
264
- }
265
- function prepareErrorResponseBody(body) {
266
- if (body.length === 0) return null;
267
- if (body.trim().length === 0) return null;
268
- const sanitized = sanitizeErrorResponseBody(body);
269
- return truncateErrorResponseBody(sanitized);
270
295
  }
271
-
272
- // src/error-stack.ts
273
- var ERROR_STACK_MAX_BYTES = 8192;
274
- var ERROR_STACK_TRUNCATION_MARKER = "...[stack truncated]";
275
- var PATH_REDACTED = "<path>";
276
- var PATH_KEEP_MARKERS = [
277
- "node_modules",
278
- ".next",
279
- ".glasstrace",
280
- "src",
281
- "dist",
282
- "build",
283
- "lib",
284
- "app",
285
- "pages"
286
- ];
287
- var PATH_TOKEN_RE = /(?<=^|[\s(])(\/[^\s()<>]+|[A-Za-z]:\\[^\s()<>]+|file:\/\/\/[^\s()<>]+|webpack-internal:\/\/[^\s()<>]+|node:[^\s()<>]+)/g;
288
- var URL_QUERY_FRAGMENT_RE = /(\bhttps?:\/\/[^\s?#()<>]+)([?#][^\s()<>]*)/g;
289
- function normalizePathToken(token) {
290
- let work = token;
291
- if (work.startsWith("file:///")) {
292
- work = work.slice("file://".length);
293
- }
294
- if (work.startsWith("webpack-internal:") || work.startsWith("node:")) {
295
- return { token, changed: false };
296
- }
297
- const isPosixAbs = work.startsWith("/");
298
- const isWinAbs = /^[A-Za-z]:\\/.test(work);
299
- if (!isPosixAbs && !isWinAbs) {
300
- return { token, changed: false };
296
+ function initAuthState(state) {
297
+ if (_authInitialized) {
298
+ _logger?.(
299
+ "warn",
300
+ "[glasstrace] initAuthState() called after auth state already initialized. Ignored."
301
+ );
302
+ return;
301
303
  }
302
- const sep = isWinAbs ? "\\" : "/";
303
- let bestIdx = -1;
304
- for (const marker of PATH_KEEP_MARKERS) {
305
- const needle = `${sep}${marker}${sep}`;
306
- const idx = work.lastIndexOf(needle);
307
- if (idx >= 0) {
308
- bestIdx = idx;
309
- break;
310
- }
304
+ _authInitialized = true;
305
+ _authState = state;
306
+ }
307
+ function setAuthState(to) {
308
+ warnIfNotInitialized();
309
+ const from = _authState;
310
+ if (from === to) return;
311
+ const valid = VALID_AUTH_TRANSITIONS[from];
312
+ if (!valid.includes(to)) {
313
+ _logger?.(
314
+ "warn",
315
+ `[glasstrace] Invalid auth state transition: ${from} \u2192 ${to}. Ignored.`
316
+ );
317
+ return;
311
318
  }
312
- if (bestIdx >= 0) {
313
- const kept = work.slice(bestIdx + sep.length);
314
- const rebuilt2 = `${PATH_REDACTED}/${kept.replace(/\\/g, "/")}`;
315
- return { token: rebuilt2, changed: true };
319
+ _authState = to;
320
+ }
321
+ function setOtelState(to) {
322
+ warnIfNotInitialized();
323
+ const from = _otelState;
324
+ if (from === to) return;
325
+ const valid = VALID_OTEL_TRANSITIONS[from];
326
+ if (!valid.includes(to)) {
327
+ _logger?.(
328
+ "warn",
329
+ `[glasstrace] Invalid OTel state transition: ${from} \u2192 ${to}. Ignored.`
330
+ );
331
+ return;
316
332
  }
317
- const colonLineRe = /:\d+(?::\d+)?$/;
318
- const lineMatch = colonLineRe.exec(work);
319
- const pathBody = lineMatch ? work.slice(0, lineMatch.index) : work;
320
- const lineSuffix = lineMatch ? work.slice(lineMatch.index) : "";
321
- const lastSep = Math.max(pathBody.lastIndexOf("/"), pathBody.lastIndexOf("\\"));
322
- const basename = lastSep >= 0 ? pathBody.slice(lastSep + 1) : pathBody;
323
- const rebuilt = `${PATH_REDACTED}/${basename}${lineSuffix}`;
324
- return { token: rebuilt, changed: true };
333
+ _otelState = to;
325
334
  }
326
- function sanitizeStack(stack) {
327
- let changed = false;
328
- const pathNormalized = stack.replace(PATH_TOKEN_RE, (token) => {
329
- const out = normalizePathToken(token);
330
- if (out.changed) changed = true;
331
- return out.token;
332
- });
333
- const urlStripped = pathNormalized.replace(URL_QUERY_FRAGMENT_RE, (match, prefix) => {
334
- if (match !== prefix) changed = true;
335
- return prefix;
336
- });
337
- const credentialRedacted = sanitizeErrorResponseBody(urlStripped);
338
- if (credentialRedacted !== urlStripped) changed = true;
339
- return { stack: credentialRedacted, redacted: changed };
335
+ var _degradationSources = /* @__PURE__ */ new Set();
336
+ function pushDegradationSource(key) {
337
+ _degradationSources.add(key);
338
+ recomputeCoreFromDegradationSources();
340
339
  }
341
- function truncateStack(stack) {
342
- const encoder = new TextEncoder();
343
- const encoded = encoder.encode(stack);
344
- if (encoded.byteLength <= ERROR_STACK_MAX_BYTES) {
345
- return { stack, truncated: false };
346
- }
347
- let cut = ERROR_STACK_MAX_BYTES;
348
- let scan = cut - 1;
349
- while (scan >= 0 && (encoded[scan] & 192) === 128) {
350
- scan -= 1;
340
+ function clearDegradationSource(key) {
341
+ _degradationSources.delete(key);
342
+ recomputeCoreFromDegradationSources();
343
+ }
344
+ function recomputeCoreFromDegradationSources() {
345
+ const hasDegradation = _degradationSources.size > 0;
346
+ if (hasDegradation && _coreState === CoreState.ACTIVE) {
347
+ setCoreState(CoreState.ACTIVE_DEGRADED);
348
+ return;
351
349
  }
352
- if (scan >= 0) {
353
- const leading = encoded[scan];
354
- let expected = 1;
355
- if ((leading & 128) === 0) {
356
- expected = 1;
357
- } else if ((leading & 224) === 192) {
358
- expected = 2;
359
- } else if ((leading & 240) === 224) {
360
- expected = 3;
361
- } else if ((leading & 248) === 240) {
362
- expected = 4;
363
- }
364
- if (scan + expected > cut) {
365
- cut = scan;
366
- }
350
+ if (!hasDegradation && _coreState === CoreState.ACTIVE_DEGRADED) {
351
+ setCoreState(CoreState.ACTIVE);
367
352
  }
368
- const decoder = new TextDecoder("utf-8", { fatal: false });
369
- const sliced = encoded.subarray(0, cut);
370
- const decoded = decoder.decode(sliced);
371
- return { stack: decoded + ERROR_STACK_TRUNCATION_MARKER, truncated: true };
372
353
  }
373
- function prepareStack(stack) {
374
- if (stack.length === 0) return null;
375
- if (stack.trim().length === 0) return null;
376
- const sanitized = sanitizeStack(stack);
377
- const truncated = truncateStack(sanitized.stack);
354
+ function getCoreState() {
355
+ return _coreState;
356
+ }
357
+ function getSdkState() {
378
358
  return {
379
- stack: truncated.stack,
380
- truncated: truncated.truncated,
381
- redacted: sanitized.redacted
359
+ core: _coreState,
360
+ auth: _authState,
361
+ otel: _otelState
382
362
  };
383
363
  }
384
-
385
- // src/build-info.ts
386
- var UNSET = "";
387
- var SHA_SHAPE = /^[0-9a-f]{7,64}$/i;
388
- function redactBuildHash(value) {
389
- const sanitize = (s) => s.replace(/[\x00-\x1F\x7F]/g, "?");
390
- if (value.length <= 12) return sanitize(value.slice(0, 4)) + "...";
391
- return sanitize(value.slice(0, 8)) + "..." + sanitize(value.slice(-4));
364
+ function onLifecycleEvent(event, listener) {
365
+ _emitter.on(event, listener);
392
366
  }
393
- function readBuildHashFromEnv() {
394
- const raw = process.env.GLASSTRACE_BUILD_HASH;
395
- if (typeof raw !== "string") return UNSET;
396
- const trimmed = raw.trim();
397
- if (trimmed.length === 0) return UNSET;
398
- if (!SHA_SHAPE.test(trimmed)) {
399
- sdkLog(
400
- "warn",
401
- `[glasstrace] warning: GLASSTRACE_BUILD_HASH=${redactBuildHash(trimmed)} does not match expected SHA shape (7-64 hex characters); source-map enrichment may not work as expected.`
402
- );
403
- }
404
- return trimmed;
367
+ function emitLifecycleEvent(event, payload) {
368
+ emitSafe(event, payload);
405
369
  }
406
- var cachedBuildHash = null;
407
- function getBuildHash() {
408
- if (cachedBuildHash === null) {
409
- cachedBuildHash = readBuildHashFromEnv();
410
- }
411
- return cachedBuildHash === UNSET ? void 0 : cachedBuildHash;
370
+ function offLifecycleEvent(event, listener) {
371
+ _emitter.off(event, listener);
412
372
  }
413
-
414
- // src/enriching-exporter.ts
415
- var ATTR = GLASSTRACE_ATTRIBUTE_NAMES;
416
- var API_KEY_PENDING = "pending";
417
- var MAX_PENDING_SPANS = 1024;
418
- var GlasstraceExporter = class {
419
- getApiKey;
420
- sessionManager;
421
- getConfig;
422
- environment;
423
- endpointUrl;
424
- createDelegateFn;
425
- verbose;
426
- delegate = null;
427
- delegateKey = null;
428
- pendingBatches = [];
429
- pendingSpanCount = 0;
430
- overflowLogged = false;
431
- constructor(options) {
432
- this.getApiKey = options.getApiKey;
433
- this.sessionManager = options.sessionManager;
434
- this.getConfig = options.getConfig;
435
- this.environment = options.environment;
436
- this.endpointUrl = options.endpointUrl;
437
- this.createDelegateFn = options.createDelegate;
438
- this.verbose = options.verbose ?? false;
439
- this[/* @__PURE__ */ Symbol.for("glasstrace.exporter")] = true;
440
- }
441
- export(spans, resultCallback) {
442
- const currentKey = this.getApiKey();
443
- if (currentKey === API_KEY_PENDING) {
444
- this.bufferSpans(spans, resultCallback);
445
- return;
446
- }
447
- const enrichedSpans = spans.map((span) => this.enrichSpan(span));
448
- const exporter = this.ensureDelegate();
449
- if (exporter) {
450
- exporter.export(enrichedSpans, (result) => {
451
- if (result.code !== 0) {
452
- sdkLog("warn", `[glasstrace] Span export failed: ${result.error?.message ?? "unknown error"}`);
453
- }
454
- resultCallback(result);
455
- });
456
- recordSpansExported(enrichedSpans.length);
457
- } else {
458
- recordSpansDropped(enrichedSpans.length);
459
- resultCallback({ code: 0 });
373
+ function emitSafe(event, payload) {
374
+ const listeners = _emitter.listeners(event);
375
+ for (const listener of listeners) {
376
+ try {
377
+ const result = listener(payload);
378
+ if (result && typeof result.catch === "function") {
379
+ result.catch((err) => {
380
+ _logger?.(
381
+ "error",
382
+ `[glasstrace] Async error in lifecycle event listener for "${event}": ${err instanceof Error ? err.message : String(err)}`
383
+ );
384
+ });
385
+ }
386
+ } catch (err) {
387
+ _logger?.(
388
+ "error",
389
+ `[glasstrace] Error in lifecycle event listener for "${event}": ${err instanceof Error ? err.message : String(err)}`
390
+ );
460
391
  }
461
392
  }
462
- /**
463
- * Called when the API key transitions from "pending" to a resolved value.
464
- * Creates the delegate exporter and flushes all buffered spans.
465
- */
466
- notifyKeyResolved() {
467
- this.flushPending();
393
+ }
394
+ function isReady() {
395
+ return _coreState === CoreState.ACTIVE || _coreState === CoreState.ACTIVE_DEGRADED;
396
+ }
397
+ function waitForReady(timeoutMs = 3e4) {
398
+ if (isReady()) {
399
+ return Promise.resolve();
468
400
  }
469
- async shutdown() {
470
- const currentKey = this.getApiKey();
471
- if (currentKey !== API_KEY_PENDING && this.pendingBatches.length > 0) {
472
- this.flushPending();
473
- } else if (this.pendingBatches.length > 0) {
474
- console.warn(
475
- `[glasstrace] Shutdown with ${this.pendingSpanCount} buffered spans \u2014 API key never resolved, spans lost.`
476
- );
477
- recordSpansDropped(this.pendingSpanCount);
478
- for (const batch of this.pendingBatches) {
479
- batch.resultCallback({ code: 0 });
401
+ if (_coreState === CoreState.PRODUCTION_DISABLED || _coreState === CoreState.REGISTRATION_FAILED || _coreState === CoreState.SHUTTING_DOWN || _coreState === CoreState.SHUTDOWN) {
402
+ return Promise.reject(new Error(`SDK is in terminal state: ${_coreState}`));
403
+ }
404
+ return new Promise((resolve2, reject) => {
405
+ let settled = false;
406
+ const listener = ({ to }) => {
407
+ if (settled) return;
408
+ if (to === CoreState.ACTIVE || to === CoreState.ACTIVE_DEGRADED) {
409
+ settled = true;
410
+ offLifecycleEvent("core:state_changed", listener);
411
+ resolve2();
412
+ } else if (to === CoreState.PRODUCTION_DISABLED || to === CoreState.REGISTRATION_FAILED || to === CoreState.SHUTTING_DOWN || to === CoreState.SHUTDOWN) {
413
+ settled = true;
414
+ offLifecycleEvent("core:state_changed", listener);
415
+ reject(new Error(`SDK reached terminal state: ${to}`));
416
+ }
417
+ };
418
+ onLifecycleEvent("core:state_changed", listener);
419
+ if (timeoutMs > 0) {
420
+ const timer = setTimeout(() => {
421
+ if (settled) return;
422
+ settled = true;
423
+ offLifecycleEvent("core:state_changed", listener);
424
+ reject(new Error(`waitForReady timed out after ${timeoutMs}ms (state: ${_coreState})`));
425
+ }, timeoutMs);
426
+ if (typeof timer === "object" && "unref" in timer) {
427
+ timer.unref();
480
428
  }
481
- this.pendingBatches = [];
482
- this.pendingSpanCount = 0;
483
- }
484
- if (this.delegate) {
485
- return this.delegate.shutdown();
486
429
  }
430
+ });
431
+ }
432
+ function getStatus() {
433
+ let mode;
434
+ if (_coreState === CoreState.PRODUCTION_DISABLED) {
435
+ mode = "disabled";
436
+ } else if (_authState === AuthState.CLAIMING || _authState === AuthState.CLAIMED) {
437
+ mode = "claiming";
438
+ } else if (_authState === AuthState.AUTHENTICATED) {
439
+ mode = "authenticated";
440
+ } else {
441
+ mode = "anonymous";
487
442
  }
488
- /**
489
- * Flushes any pending buffered spans (if the API key has resolved) and
490
- * delegates to the underlying exporter's forceFlush to drain its queue.
491
- */
492
- forceFlush() {
493
- if (this.getApiKey() !== API_KEY_PENDING && this.pendingBatches.length > 0) {
494
- this.flushPending();
495
- }
496
- if (this.delegate?.forceFlush) {
497
- return this.delegate.forceFlush();
498
- }
499
- return Promise.resolve();
443
+ let tracing;
444
+ if (_otelState === OtelState.COEXISTENCE_FAILED || _otelState === OtelState.UNCONFIGURED || _otelState === OtelState.CONFIGURING) {
445
+ tracing = "not-configured";
446
+ } else if (_coreState === CoreState.ACTIVE_DEGRADED) {
447
+ tracing = "degraded";
448
+ } else if (_otelState === OtelState.AUTO_ATTACHED || _otelState === OtelState.PROCESSOR_PRESENT) {
449
+ tracing = "coexistence";
450
+ } else {
451
+ tracing = "active";
500
452
  }
501
- /**
502
- * Enriches a ReadableSpan with all glasstrace.* attributes.
503
- * Returns a new ReadableSpan wrapper; the original span is not mutated.
504
- *
505
- * Only {@link SessionManager.getSessionId} is individually guarded because
506
- * it calls into crypto and schema validation — a session ID failure should
507
- * not prevent the rest of enrichment. The other helper calls
508
- * ({@link deriveErrorCategory}, {@link deriveOrmProvider},
509
- * {@link classifyFetchTarget}) are pure functions on typed string inputs
510
- * and rely on the outer catch for any unexpected failure.
511
- *
512
- * On total failure, returns the original span unchanged.
513
- */
514
- enrichSpan(span) {
453
+ return {
454
+ ready: isReady(),
455
+ mode,
456
+ tracing
457
+ };
458
+ }
459
+ var _shutdownHooks = [];
460
+ var _signalHandlersRegistered = false;
461
+ var _signalHandler = null;
462
+ var _beforeExitRegistered = false;
463
+ var _beforeExitHandler = null;
464
+ var _shutdownExecuted = false;
465
+ function registerShutdownHook(hook) {
466
+ _shutdownHooks.push(hook);
467
+ _shutdownHooks.sort((a, b) => a.priority - b.priority);
468
+ }
469
+ async function executeShutdown(timeoutMs = 5e3) {
470
+ if (_shutdownExecuted) return;
471
+ _shutdownExecuted = true;
472
+ setCoreState(CoreState.SHUTTING_DOWN);
473
+ for (const hook of _shutdownHooks) {
515
474
  try {
516
- const attrs = span.attributes ?? {};
517
- const name = span.name ?? "";
518
- const extra = {};
519
- extra[ATTR.TRACE_TYPE] = "server";
520
- try {
521
- const sessionId = this.sessionManager.getSessionId(this.getApiKey());
522
- extra[ATTR.SESSION_ID] = sessionId;
523
- } catch {
524
- }
525
- const env = this.environment ?? process.env.GLASSTRACE_ENV;
526
- if (env) {
527
- extra[ATTR.ENVIRONMENT] = env;
528
- }
529
- const buildHash = getBuildHash();
530
- if (buildHash) {
531
- extra[ATTR.BUILD_HASH] = buildHash;
532
- }
533
- const existingCid = attrs["glasstrace.correlation.id"];
534
- if (typeof existingCid === "string") {
535
- extra[ATTR.CORRELATION_ID] = existingCid;
536
- }
537
- const rawRoute = attrs["http.route"];
538
- const route = typeof rawRoute === "string" ? rawRoute : name;
539
- if (route) {
540
- extra[ATTR.ROUTE] = route;
541
- }
542
- const rawUrlAttr = attrs["http.url"] ?? attrs["url.full"] ?? attrs["http.target"];
543
- const rawHttpUrl = typeof rawUrlAttr === "string" ? rawUrlAttr : void 0;
544
- if (rawHttpUrl) {
545
- const trpcMatch = rawHttpUrl.match(/\/api\/trpc\/([^/?#]+)/);
546
- if (trpcMatch) {
547
- let procedure;
548
- try {
549
- procedure = decodeURIComponent(trpcMatch[1]);
550
- } catch {
551
- procedure = trpcMatch[1];
552
- }
553
- if (procedure) {
554
- extra[ATTR.TRPC_PROCEDURE] = procedure;
555
- }
556
- }
557
- }
558
- const method = attrs["http.method"] ?? attrs["http.request.method"];
559
- if (method) {
560
- extra[ATTR.HTTP_METHOD] = method;
561
- }
562
- const actionRoute = extractLeadingPath(route);
563
- if (method === "POST" && actionRoute) {
564
- const isApiRoute = actionRoute === "/api" || actionRoute.startsWith("/api/");
565
- const isInternalRoute = actionRoute.startsWith("/_next/");
566
- if (!isApiRoute && !isInternalRoute) {
567
- extra[ATTR.NEXT_ACTION_DETECTED] = true;
568
- if (typeof extra[ATTR.CORRELATION_ID] !== "string") {
569
- maybeShowServerActionNudge();
570
- }
571
- }
572
- }
573
- const statusCode = coerceHttpStatus(attrs["http.status_code"]) ?? coerceHttpStatus(attrs["http.response.status_code"]);
574
- if (statusCode !== void 0) {
575
- extra[ATTR.HTTP_STATUS_CODE] = statusCode;
576
- }
577
- const isErrorByStatus = span.status?.code === SpanStatusCode.ERROR;
578
- const isErrorByEvent = hasExceptionEvent(span);
579
- const isErrorByAttrs = typeof attrs["exception.type"] === "string" || typeof attrs["exception.message"] === "string";
580
- const statusNotExplicitlyOK = span.status?.code !== SpanStatusCode.OK;
581
- if (this.verbose && method) {
582
- sdkLog(
583
- "info",
584
- `[glasstrace] enrichSpan "${name}": status.code=${span.status?.code}, http.status_code=${statusCode}, isErrorByStatus=${isErrorByStatus}, isErrorByEvent=${isErrorByEvent}, isErrorByAttrs=${isErrorByAttrs}`
585
- );
586
- }
587
- if (method && statusNotExplicitlyOK && (isErrorByStatus || isErrorByEvent || isErrorByAttrs)) {
588
- if (statusCode === void 0 || statusCode === 0 || statusCode === 200) {
589
- const httpErrorType = attrs["error.type"];
590
- if (typeof httpErrorType === "string") {
591
- const parsed = parseInt(httpErrorType, 10);
592
- if (!isNaN(parsed) && parsed >= 400 && parsed <= 599) {
593
- extra[ATTR.HTTP_STATUS_CODE] = parsed;
594
- } else {
595
- extra[ATTR.HTTP_STATUS_CODE] = 500;
596
- }
597
- } else {
598
- extra[ATTR.HTTP_STATUS_CODE] = 500;
599
- }
600
- if (this.verbose) {
601
- sdkLog(
602
- "info",
603
- `[glasstrace] enrichSpan "${name}": inferred status_code=${extra[ATTR.HTTP_STATUS_CODE]} (was ${statusCode}), error.type=${attrs["error.type"]}`
604
- );
475
+ const hookPromise = hook.fn();
476
+ hookPromise.catch(() => {
477
+ });
478
+ await Promise.race([
479
+ hookPromise,
480
+ new Promise((_, reject) => {
481
+ const timer = setTimeout(() => reject(new Error(`Shutdown hook "${hook.name}" timed out`)), timeoutMs);
482
+ if (typeof timer === "object" && "unref" in timer) {
483
+ timer.unref();
605
484
  }
606
- }
607
- }
608
- if (span.startTime && span.endTime) {
609
- const [startSec, startNano] = span.startTime;
610
- const [endSec, endNano] = span.endTime;
611
- const durationMs = (endSec - startSec) * 1e3 + (endNano - startNano) / 1e6;
612
- if (durationMs >= 0) {
613
- extra[ATTR.HTTP_DURATION_MS] = durationMs;
614
- }
485
+ })
486
+ ]);
487
+ } catch (err) {
488
+ _logger?.(
489
+ "warn",
490
+ `[glasstrace] Shutdown hook "${hook.name}" failed: ${err instanceof Error ? err.message : String(err)}`
491
+ );
492
+ }
493
+ }
494
+ setCoreState(CoreState.SHUTDOWN);
495
+ }
496
+ function registerSignalHandlers() {
497
+ if (_signalHandlersRegistered) return;
498
+ if (typeof process === "undefined" || typeof process.once !== "function") return;
499
+ _signalHandlersRegistered = true;
500
+ const otherSigtermListeners = process.listenerCount("SIGTERM");
501
+ const otherSigintListeners = process.listenerCount("SIGINT");
502
+ const handler = (signal) => {
503
+ void executeShutdown().finally(() => {
504
+ if (_signalHandler) {
505
+ process.removeListener("SIGTERM", _signalHandler);
506
+ process.removeListener("SIGINT", _signalHandler);
615
507
  }
616
- const eventDetails = statusNotExplicitlyOK ? getExceptionEventDetails(span) : { type: void 0, message: void 0, stacktrace: void 0 };
617
- let errorSource;
618
- const attrMessage = attrs["exception.message"];
619
- if (eventDetails.message) {
620
- extra[ATTR.ERROR_MESSAGE] = eventDetails.message;
621
- errorSource = "otel_exception";
622
- } else if (typeof attrMessage === "string") {
623
- extra[ATTR.ERROR_MESSAGE] = attrMessage;
624
- errorSource = "otel_event";
508
+ const otherListeners = signal === "SIGTERM" ? otherSigtermListeners : otherSigintListeners;
509
+ const otherProviderOwnsSignal = getCoexistenceState() === "coexisting" && otherListeners > 0;
510
+ if (!otherProviderOwnsSignal) {
511
+ process.kill(process.pid, signal);
625
512
  }
626
- const attrType = attrs["exception.type"];
627
- if (eventDetails.type) {
628
- extra[ATTR.ERROR_CODE] = eventDetails.type;
629
- extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(eventDetails.type);
630
- errorSource = errorSource ?? "otel_exception";
631
- } else if (typeof attrType === "string") {
632
- extra[ATTR.ERROR_CODE] = attrType;
633
- extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(attrType);
634
- errorSource = errorSource ?? "otel_event";
635
- }
636
- if (statusNotExplicitlyOK) {
637
- const rawStack = eventDetails.stacktrace ?? (typeof attrs["exception.stacktrace"] === "string" ? attrs["exception.stacktrace"] : void 0);
638
- if (rawStack) {
639
- const prepared = prepareStack(rawStack);
640
- if (prepared !== null) {
641
- extra[ATTR.ERROR_STACK] = prepared.stack;
642
- extra[ATTR.ERROR_STACK_TRUNCATED] = prepared.truncated;
643
- extra[ATTR.ERROR_STACK_REDACTED] = prepared.redacted;
644
- errorSource = errorSource ?? (eventDetails.stacktrace ? "otel_exception" : "otel_event");
645
- }
646
- }
647
- }
648
- const routeIsFallback = route === "/_error" || route === "/_not-found" || route === "/_404" || route === "/_500";
649
- if (routeIsFallback && rawHttpUrl) {
650
- const originalPath = extractPathOnly(rawHttpUrl);
651
- const normOriginal = stripTrailingSlash(originalPath);
652
- const normRoute = stripTrailingSlash(route);
653
- if (normOriginal && normOriginal !== normRoute) {
654
- extra[ATTR.ERROR_ORIGINAL_PATH] = normOriginal;
655
- extra[ATTR.ERROR_FALLBACK_ROUTE] = route;
656
- extra[ATTR.ERROR_FRAMEWORK_KIND] = "fallback";
657
- errorSource = errorSource ?? "framework_fallback";
658
- }
659
- }
660
- if (errorSource !== void 0) {
661
- extra[ATTR.ERROR_SOURCE] = errorSource;
662
- }
663
- if (this.verbose && (extra[ATTR.ERROR_MESSAGE] || extra[ATTR.ERROR_CODE])) {
664
- const msgSource = eventDetails.message ? "event" : typeof attrMessage === "string" ? "attrs" : "none";
665
- const typeSource = eventDetails.type ? "event" : typeof attrType === "string" ? "attrs" : "none";
666
- sdkLog(
667
- "info",
668
- `[glasstrace] enrichSpan "${name}": error.message source=${msgSource}, error.code source=${typeSource}`
669
- );
670
- }
671
- const errorField = attrs["error.field"];
672
- if (typeof errorField === "string") {
673
- extra[ATTR.ERROR_FIELD] = errorField;
674
- }
675
- if (this.getConfig().errorResponseBodies) {
676
- const responseBody = attrs["glasstrace.internal.response_body"];
677
- if (typeof responseBody === "string") {
678
- const enrichedStatus = extra[ATTR.HTTP_STATUS_CODE];
679
- const effectiveStatus = typeof enrichedStatus === "number" ? enrichedStatus : statusCode;
680
- if (isHttpErrorStatus(effectiveStatus)) {
681
- const prepared = prepareErrorResponseBody(responseBody);
682
- if (prepared !== null) {
683
- extra[ATTR.ERROR_RESPONSE_BODY] = prepared;
684
- }
685
- }
686
- }
687
- }
688
- const spanAny = span;
689
- const instrumentationName = spanAny.instrumentationScope?.name ?? spanAny.instrumentationLibrary?.name ?? "";
690
- const ormProvider = deriveOrmProvider(instrumentationName);
691
- if (ormProvider) {
692
- extra[ATTR.ORM_PROVIDER] = ormProvider;
693
- const table = attrs["db.sql.table"];
694
- const prismaModel = attrs["db.prisma.model"];
695
- const model = typeof table === "string" ? table : typeof prismaModel === "string" ? prismaModel : void 0;
696
- if (model) {
697
- extra[ATTR.ORM_MODEL] = model;
698
- }
699
- const operation = attrs["db.operation"];
700
- if (typeof operation === "string") {
701
- extra[ATTR.ORM_OPERATION] = operation;
702
- }
703
- }
704
- const httpUrl = attrs["http.url"];
705
- const fullUrl = attrs["url.full"];
706
- const url = typeof httpUrl === "string" ? httpUrl : typeof fullUrl === "string" ? fullUrl : void 0;
707
- if (url && span.kind === SpanKind.CLIENT) {
708
- extra[ATTR.FETCH_TARGET] = classifyFetchTarget(url);
709
- }
710
- return createEnrichedSpan(span, extra);
711
- } catch {
712
- return span;
713
- }
714
- }
715
- /**
716
- * Lazily creates the delegate OTLP exporter once the API key is resolved.
717
- * Recreates the delegate if the key has changed (e.g., after key rotation)
718
- * so the Authorization header stays current.
719
- */
720
- ensureDelegate() {
721
- if (!this.createDelegateFn) return null;
722
- const currentKey = this.getApiKey();
723
- if (currentKey === API_KEY_PENDING) return null;
724
- if (this.delegate && this.delegateKey === currentKey) {
725
- return this.delegate;
726
- }
727
- if (this.delegate) {
728
- void this.delegate.shutdown?.().catch(() => {
729
- });
730
- }
731
- this.delegate = this.createDelegateFn(this.endpointUrl, {
732
- Authorization: `Bearer ${currentKey}`
733
513
  });
734
- this.delegateKey = currentKey;
735
- return this.delegate;
736
- }
737
- /**
738
- * Buffers raw (unenriched) spans while the API key is pending.
739
- * Evicts oldest batches if the buffer exceeds MAX_PENDING_SPANS.
740
- * Re-checks the key after buffering to close the race window where
741
- * the key resolves between the caller's check and this buffer call.
742
- */
743
- bufferSpans(spans, resultCallback) {
744
- this.pendingBatches.push({ spans, resultCallback });
745
- this.pendingSpanCount += spans.length;
746
- while (this.pendingSpanCount > MAX_PENDING_SPANS && this.pendingBatches.length > 1) {
747
- const evicted = this.pendingBatches.shift();
748
- this.pendingSpanCount -= evicted.spans.length;
749
- recordSpansDropped(evicted.spans.length);
750
- evicted.resultCallback({ code: 0 });
751
- if (!this.overflowLogged) {
752
- this.overflowLogged = true;
753
- console.warn(
754
- "[glasstrace] Pending span buffer overflow \u2014 oldest spans evicted. This usually means the API key is taking too long to resolve."
755
- );
756
- }
757
- }
758
- if (this.getApiKey() !== API_KEY_PENDING) {
759
- this.flushPending();
760
- }
761
- }
762
- /**
763
- * Flushes all buffered spans through the delegate exporter.
764
- * Enriches spans at flush time (not buffer time) so that session IDs
765
- * are computed with the resolved API key instead of the "pending" sentinel.
766
- */
767
- flushPending() {
768
- if (this.pendingBatches.length === 0) return;
769
- const exporter = this.ensureDelegate();
770
- if (!exporter) {
771
- let discardedCount = 0;
772
- for (const batch of this.pendingBatches) {
773
- discardedCount += batch.spans.length;
774
- batch.resultCallback({ code: 0 });
775
- }
776
- recordSpansDropped(discardedCount);
777
- this.pendingBatches = [];
778
- this.pendingSpanCount = 0;
779
- return;
780
- }
781
- const batches = this.pendingBatches;
782
- this.pendingBatches = [];
783
- this.pendingSpanCount = 0;
784
- for (const batch of batches) {
785
- const enriched = batch.spans.map((span) => this.enrichSpan(span));
786
- exporter.export(enriched, (result) => {
787
- if (result.code !== 0) {
788
- sdkLog("warn", `[glasstrace] Span export failed: ${result.error?.message ?? "unknown error"}`);
789
- }
790
- batch.resultCallback(result);
791
- });
792
- recordSpansExported(enriched.length);
793
- }
794
- }
795
- };
796
- function createEnrichedSpan(span, extra) {
797
- const enrichedAttributes = { ...span.attributes, ...extra };
798
- return Object.create(span, {
799
- attributes: {
800
- value: enrichedAttributes,
801
- enumerable: true
802
- }
803
- });
804
- }
805
- function hasExceptionEvent(span) {
806
- return span.events?.some((e) => e.name === "exception") ?? false;
514
+ };
515
+ _signalHandler = handler;
516
+ process.once("SIGTERM", handler);
517
+ process.once("SIGINT", handler);
807
518
  }
808
- function getExceptionEventDetails(span) {
809
- const event = span.events?.find((e) => e.name === "exception");
810
- if (!event?.attributes) {
811
- return { type: void 0, message: void 0, stacktrace: void 0 };
812
- }
813
- const type = event.attributes["exception.type"];
814
- const message = event.attributes["exception.message"];
815
- const stacktrace = event.attributes["exception.stacktrace"];
816
- return {
817
- type: typeof type === "string" ? type : void 0,
818
- message: typeof message === "string" ? message : void 0,
819
- stacktrace: typeof stacktrace === "string" ? stacktrace : void 0
519
+ function registerBeforeExitTrigger() {
520
+ if (_beforeExitRegistered) return;
521
+ if (typeof process === "undefined" || typeof process.once !== "function") return;
522
+ _beforeExitRegistered = true;
523
+ const handler = () => {
524
+ void executeShutdown();
820
525
  };
526
+ _beforeExitHandler = handler;
527
+ process.once("beforeExit", handler);
821
528
  }
822
- function extractLeadingPath(raw) {
823
- if (!raw) return void 0;
824
- const trimmed = raw.trim();
825
- if (trimmed.length === 0) return void 0;
826
- if (trimmed.startsWith("/")) {
827
- const firstSpace = trimmed.indexOf(" ");
828
- return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
829
- }
830
- for (const token of trimmed.split(/\s+/)) {
831
- if (token.startsWith("/")) {
832
- return token;
833
- }
529
+
530
+ // src/error-response-body.ts
531
+ var ERROR_RESPONSE_BODY_MAX_BYTES = 4096;
532
+ var ERROR_RESPONSE_BODY_TRUNCATION_MARKER = "...[truncated]";
533
+ var REDACTED = "[REDACTED]";
534
+ var ERROR_STATUS_MIN = 400;
535
+ var ERROR_STATUS_MAX = 599;
536
+ function coerceHttpStatus(value) {
537
+ let numeric;
538
+ if (typeof value === "number") {
539
+ numeric = value;
540
+ } else if (typeof value === "string") {
541
+ const trimmed = value.trim();
542
+ if (trimmed.length === 0) return void 0;
543
+ numeric = Number(trimmed);
544
+ } else {
545
+ return void 0;
834
546
  }
835
- return void 0;
547
+ return Number.isFinite(numeric) ? numeric : void 0;
836
548
  }
837
- function stripTrailingSlash(path3) {
838
- if (!path3) return path3;
839
- if (path3 === "/") return path3;
840
- return path3.endsWith("/") ? path3.slice(0, -1) : path3;
549
+ function isHttpErrorStatus(status) {
550
+ const numeric = coerceHttpStatus(status);
551
+ if (numeric === void 0) return false;
552
+ return numeric >= ERROR_STATUS_MIN && numeric <= ERROR_STATUS_MAX;
841
553
  }
842
- function extractPathOnly(raw) {
843
- if (!raw) return void 0;
844
- const trimmed = raw.trim();
845
- if (trimmed.length === 0) return void 0;
846
- const isAbsoluteUrl = /^https?:\/\//i.test(trimmed);
847
- const isProtocolRelative = trimmed.startsWith("//");
848
- if (isAbsoluteUrl || isProtocolRelative) {
849
- try {
850
- const parsed = new URL(trimmed, "http://_/");
851
- if (parsed.pathname && parsed.pathname.startsWith("/")) {
852
- return parsed.pathname;
853
- }
854
- } catch {
855
- }
856
- }
857
- if (trimmed.startsWith("/")) {
858
- const queryIdx = trimmed.indexOf("?");
859
- const fragIdx = trimmed.indexOf("#");
860
- let cut = trimmed.length;
861
- if (queryIdx >= 0) cut = Math.min(cut, queryIdx);
862
- if (fragIdx >= 0) cut = Math.min(cut, fragIdx);
863
- return trimmed.slice(0, cut);
554
+ var REDACTION_PATTERNS = [
555
+ // Order matters: redact specific token shapes BEFORE the generic
556
+ // key=value catcher so a literal `Bearer eyJ…` collapses into a single
557
+ // [REDACTED] and the JWT regex does not separately match the suffix.
558
+ {
559
+ name: "bearer",
560
+ // Case-insensitive on the scheme: HTTP frameworks and proxies
561
+ // round-trip the auth scheme with inconsistent casing
562
+ // (`Bearer`, `bearer`, `BEARER`), and a real token leaks just as
563
+ // badly under any of them.
564
+ pattern: /\bBearer\s+[A-Za-z0-9._\-+/=]+/gi
565
+ },
566
+ {
567
+ name: "jwt",
568
+ // Three base64url segments separated by dots. Real JWTs encode at
569
+ // minimum a small JSON header in the first segment, which alone is
570
+ // typically ≥10 chars after base64url; a 16-char floor avoids false
571
+ // positives on dotted text like a stack-trace frame
572
+ // (`react.dom.server`) while still catching every real JWT we have
573
+ // seen in the wild. Anchored with word boundaries on both sides so
574
+ // a 3-dot semantic version like "next@15.4.1.2" does not match.
575
+ pattern: /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
576
+ },
577
+ {
578
+ name: "glasstrace-api-key",
579
+ // gt_dev_* and gt_anon_* keys are >=24 chars of [A-Za-z0-9].
580
+ pattern: /\bgt_(?:dev|anon)_[A-Za-z0-9]{16,}\b/g
581
+ },
582
+ {
583
+ name: "aws-access-key",
584
+ // 20-char prefix-fixed identifier.
585
+ pattern: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g
586
+ },
587
+ {
588
+ name: "key-value-secret-quoted",
589
+ // Quoted-string variant: (key) [:=] "<value>". The value runs to
590
+ // the next unescaped closing quote so a multi-word secret like
591
+ // `password="my secret phrase"` is fully consumed instead of
592
+ // splitting at the first space and leaving the tail visible.
593
+ // The leading `(?<![A-Za-z0-9_])` prevents matching inside
594
+ // identifiers like `passwordless`. The trailing `"?` after the
595
+ // keyword absorbs the closing quote in JSON-style `"apikey":
596
+ // "value"` so the colon is still seen as the separator.
597
+ pattern: /(?<![A-Za-z0-9_])(?:api[_-]?key|apikey|secret|password|token)"?\s*[:=]\s*"(?:[^"\\]|\\.)*"/gi
598
+ },
599
+ {
600
+ name: "key-value-secret-bare",
601
+ // Unquoted variant: (key) [:=] <bare-value>. The bare value
602
+ // capture stops at common JSON/text delimiters so we redact only
603
+ // the value, not surrounding structure. Listed AFTER the quoted
604
+ // variant so a quoted value's surrounding `"` are consumed by
605
+ // the first pattern and we never fall through here for a quoted
606
+ // secret.
607
+ pattern: /(?<![A-Za-z0-9_])(?:api[_-]?key|apikey|secret|password|token)"?\s*[:=]\s*[^\s,;}\]"]+/gi
864
608
  }
865
- return void 0;
609
+ ];
610
+ function sanitizeErrorResponseBody(body) {
611
+ let out = body;
612
+ for (const { pattern } of REDACTION_PATTERNS) {
613
+ out = out.replace(pattern, REDACTED);
614
+ }
615
+ return out;
866
616
  }
867
- function deriveOrmProvider(instrumentationName) {
868
- const lower = instrumentationName.toLowerCase();
869
- if (lower.includes("prisma")) {
870
- return "prisma";
617
+ function truncateErrorResponseBody(body) {
618
+ const encoder = new TextEncoder();
619
+ const encoded = encoder.encode(body);
620
+ if (encoded.byteLength <= ERROR_RESPONSE_BODY_MAX_BYTES) {
621
+ return body;
871
622
  }
872
- if (lower.includes("drizzle")) {
873
- return "drizzle";
623
+ let cut = ERROR_RESPONSE_BODY_MAX_BYTES;
624
+ let scan = cut - 1;
625
+ while (scan >= 0 && (encoded[scan] & 192) === 128) {
626
+ scan -= 1;
874
627
  }
875
- return null;
628
+ if (scan >= 0) {
629
+ const leading = encoded[scan];
630
+ let expected = 1;
631
+ if ((leading & 128) === 0) {
632
+ expected = 1;
633
+ } else if ((leading & 224) === 192) {
634
+ expected = 2;
635
+ } else if ((leading & 240) === 224) {
636
+ expected = 3;
637
+ } else if ((leading & 248) === 240) {
638
+ expected = 4;
639
+ }
640
+ if (scan + expected > cut) {
641
+ cut = scan;
642
+ }
643
+ }
644
+ const decoder = new TextDecoder("utf-8", { fatal: false });
645
+ const sliced = encoded.subarray(0, cut);
646
+ const decoded = decoder.decode(sliced);
647
+ return decoded + ERROR_RESPONSE_BODY_TRUNCATION_MARKER;
876
648
  }
877
- function deriveErrorCategory(errorType) {
878
- const lower = errorType.toLowerCase();
879
- if (lower.includes("validation") || lower.includes("zod")) {
880
- return "validation";
649
+ function prepareErrorResponseBody(body) {
650
+ if (body.length === 0) return null;
651
+ if (body.trim().length === 0) return null;
652
+ const sanitized = sanitizeErrorResponseBody(body);
653
+ return truncateErrorResponseBody(sanitized);
654
+ }
655
+
656
+ // src/error-stack.ts
657
+ var ERROR_STACK_MAX_BYTES = 8192;
658
+ var ERROR_STACK_TRUNCATION_MARKER = "...[stack truncated]";
659
+ var PATH_REDACTED = "<path>";
660
+ var PATH_KEEP_MARKERS = [
661
+ "node_modules",
662
+ ".next",
663
+ ".glasstrace",
664
+ "src",
665
+ "dist",
666
+ "build",
667
+ "lib",
668
+ "app",
669
+ "pages"
670
+ ];
671
+ var PATH_TOKEN_RE = /(?<=^|[\s(])(\/[^\s()<>]+|[A-Za-z]:\\[^\s()<>]+|file:\/\/\/[^\s()<>]+|webpack-internal:\/\/[^\s()<>]+|node:[^\s()<>]+)/g;
672
+ var URL_QUERY_FRAGMENT_RE = /(\bhttps?:\/\/[^\s?#()<>]+)([?#][^\s()<>]*)/g;
673
+ function normalizePathToken(token) {
674
+ let work = token;
675
+ if (work.startsWith("file:///")) {
676
+ work = work.slice("file://".length);
881
677
  }
882
- if (lower.includes("network") || lower.includes("econnrefused") || lower.includes("fetch") || lower.includes("timeout")) {
883
- return "network";
678
+ if (work.startsWith("webpack-internal:") || work.startsWith("node:")) {
679
+ return { token, changed: false };
884
680
  }
885
- if (lower.includes("auth") || lower.includes("unauthorized") || lower.includes("forbidden")) {
886
- return "auth";
681
+ const isPosixAbs = work.startsWith("/");
682
+ const isWinAbs = /^[A-Za-z]:\\/.test(work);
683
+ if (!isPosixAbs && !isWinAbs) {
684
+ return { token, changed: false };
887
685
  }
888
- if (lower.includes("notfound") || lower.includes("not_found")) {
889
- return "not-found";
686
+ const sep = isWinAbs ? "\\" : "/";
687
+ let bestIdx = -1;
688
+ for (const marker of PATH_KEEP_MARKERS) {
689
+ const needle = `${sep}${marker}${sep}`;
690
+ const idx = work.lastIndexOf(needle);
691
+ if (idx >= 0) {
692
+ bestIdx = idx;
693
+ break;
694
+ }
890
695
  }
891
- return "internal";
696
+ if (bestIdx >= 0) {
697
+ const kept = work.slice(bestIdx + sep.length);
698
+ const rebuilt2 = `${PATH_REDACTED}/${kept.replace(/\\/g, "/")}`;
699
+ return { token: rebuilt2, changed: true };
700
+ }
701
+ const colonLineRe = /:\d+(?::\d+)?$/;
702
+ const lineMatch = colonLineRe.exec(work);
703
+ const pathBody = lineMatch ? work.slice(0, lineMatch.index) : work;
704
+ const lineSuffix = lineMatch ? work.slice(lineMatch.index) : "";
705
+ const lastSep = Math.max(pathBody.lastIndexOf("/"), pathBody.lastIndexOf("\\"));
706
+ const basename = lastSep >= 0 ? pathBody.slice(lastSep + 1) : pathBody;
707
+ const rebuilt = `${PATH_REDACTED}/${basename}${lineSuffix}`;
708
+ return { token: rebuilt, changed: true };
892
709
  }
893
-
894
- // src/lifecycle.ts
895
- import { EventEmitter } from "node:events";
896
-
897
- // src/signal-handler.ts
898
- var coexistenceState = "unknown";
899
- function setCoexistenceState(s) {
900
- coexistenceState = s;
710
+ function sanitizeStack(stack) {
711
+ let changed = false;
712
+ const pathNormalized = stack.replace(PATH_TOKEN_RE, (token) => {
713
+ const out = normalizePathToken(token);
714
+ if (out.changed) changed = true;
715
+ return out.token;
716
+ });
717
+ const urlStripped = pathNormalized.replace(URL_QUERY_FRAGMENT_RE, (match, prefix) => {
718
+ if (match !== prefix) changed = true;
719
+ return prefix;
720
+ });
721
+ const credentialRedacted = sanitizeErrorResponseBody(urlStripped);
722
+ if (credentialRedacted !== urlStripped) changed = true;
723
+ return { stack: credentialRedacted, redacted: changed };
901
724
  }
902
- function getCoexistenceState() {
903
- return coexistenceState;
725
+ function truncateStack(stack) {
726
+ const encoder = new TextEncoder();
727
+ const encoded = encoder.encode(stack);
728
+ if (encoded.byteLength <= ERROR_STACK_MAX_BYTES) {
729
+ return { stack, truncated: false };
730
+ }
731
+ let cut = ERROR_STACK_MAX_BYTES;
732
+ let scan = cut - 1;
733
+ while (scan >= 0 && (encoded[scan] & 192) === 128) {
734
+ scan -= 1;
735
+ }
736
+ if (scan >= 0) {
737
+ const leading = encoded[scan];
738
+ let expected = 1;
739
+ if ((leading & 128) === 0) {
740
+ expected = 1;
741
+ } else if ((leading & 224) === 192) {
742
+ expected = 2;
743
+ } else if ((leading & 240) === 224) {
744
+ expected = 3;
745
+ } else if ((leading & 248) === 240) {
746
+ expected = 4;
747
+ }
748
+ if (scan + expected > cut) {
749
+ cut = scan;
750
+ }
751
+ }
752
+ const decoder = new TextDecoder("utf-8", { fatal: false });
753
+ const sliced = encoded.subarray(0, cut);
754
+ const decoded = decoder.decode(sliced);
755
+ return { stack: decoded + ERROR_STACK_TRUNCATION_MARKER, truncated: true };
756
+ }
757
+ function prepareStack(stack) {
758
+ if (stack.length === 0) return null;
759
+ if (stack.trim().length === 0) return null;
760
+ const sanitized = sanitizeStack(stack);
761
+ const truncated = truncateStack(sanitized.stack);
762
+ return {
763
+ stack: truncated.stack,
764
+ truncated: truncated.truncated,
765
+ redacted: sanitized.redacted
766
+ };
904
767
  }
905
768
 
906
- // src/lifecycle.ts
907
- var CoreState = {
908
- IDLE: "IDLE",
909
- REGISTERING: "REGISTERING",
910
- KEY_PENDING: "KEY_PENDING",
911
- KEY_RESOLVED: "KEY_RESOLVED",
912
- ACTIVE: "ACTIVE",
913
- ACTIVE_DEGRADED: "ACTIVE_DEGRADED",
914
- SHUTTING_DOWN: "SHUTTING_DOWN",
915
- SHUTDOWN: "SHUTDOWN",
916
- PRODUCTION_DISABLED: "PRODUCTION_DISABLED",
917
- REGISTRATION_FAILED: "REGISTRATION_FAILED"
918
- };
919
- var AuthState = {
920
- ANONYMOUS: "ANONYMOUS",
921
- AUTHENTICATED: "AUTHENTICATED",
922
- CLAIMING: "CLAIMING",
923
- CLAIMED: "CLAIMED"
924
- };
925
- var OtelState = {
926
- UNCONFIGURED: "UNCONFIGURED",
927
- CONFIGURING: "CONFIGURING",
928
- OWNS_PROVIDER: "OWNS_PROVIDER",
929
- AUTO_ATTACHED: "AUTO_ATTACHED",
930
- PROCESSOR_PRESENT: "PROCESSOR_PRESENT",
931
- COEXISTENCE_FAILED: "COEXISTENCE_FAILED"
932
- };
933
- var VALID_CORE_TRANSITIONS = {
934
- [CoreState.IDLE]: [CoreState.REGISTERING, CoreState.REGISTRATION_FAILED, CoreState.SHUTTING_DOWN],
935
- [CoreState.REGISTERING]: [
936
- CoreState.KEY_PENDING,
937
- CoreState.PRODUCTION_DISABLED,
938
- CoreState.REGISTRATION_FAILED,
939
- CoreState.SHUTTING_DOWN
940
- ],
941
- [CoreState.KEY_PENDING]: [
942
- CoreState.KEY_RESOLVED,
943
- CoreState.REGISTRATION_FAILED,
944
- CoreState.SHUTTING_DOWN
945
- ],
946
- [CoreState.KEY_RESOLVED]: [
947
- CoreState.ACTIVE,
948
- CoreState.ACTIVE_DEGRADED,
949
- CoreState.SHUTTING_DOWN
950
- ],
951
- [CoreState.ACTIVE]: [
952
- CoreState.ACTIVE_DEGRADED,
953
- CoreState.SHUTTING_DOWN
954
- ],
955
- [CoreState.ACTIVE_DEGRADED]: [
956
- CoreState.ACTIVE,
957
- CoreState.SHUTTING_DOWN
958
- ],
959
- [CoreState.SHUTTING_DOWN]: [CoreState.SHUTDOWN],
960
- [CoreState.SHUTDOWN]: [],
961
- [CoreState.PRODUCTION_DISABLED]: [],
962
- [CoreState.REGISTRATION_FAILED]: []
963
- };
964
- var VALID_AUTH_TRANSITIONS = {
965
- [AuthState.ANONYMOUS]: [AuthState.CLAIMING],
966
- [AuthState.AUTHENTICATED]: [AuthState.CLAIMING],
967
- [AuthState.CLAIMING]: [AuthState.CLAIMED],
968
- [AuthState.CLAIMED]: [AuthState.CLAIMING]
969
- };
970
- var VALID_OTEL_TRANSITIONS = {
971
- [OtelState.UNCONFIGURED]: [OtelState.CONFIGURING],
972
- [OtelState.CONFIGURING]: [
973
- OtelState.OWNS_PROVIDER,
974
- OtelState.AUTO_ATTACHED,
975
- OtelState.PROCESSOR_PRESENT,
976
- OtelState.COEXISTENCE_FAILED
977
- ],
978
- [OtelState.OWNS_PROVIDER]: [],
979
- [OtelState.AUTO_ATTACHED]: [],
980
- [OtelState.PROCESSOR_PRESENT]: [],
981
- [OtelState.COEXISTENCE_FAILED]: []
982
- };
983
- var _coreState = CoreState.IDLE;
984
- var _authState = AuthState.ANONYMOUS;
985
- var _otelState = OtelState.UNCONFIGURED;
986
- var _emitter = new EventEmitter();
987
- var _logger = null;
988
- var _initialized = false;
989
- var _initWarned = false;
990
- var _coreReadyEmitted = false;
991
- var _authInitialized = false;
992
- var _emitting = false;
993
- function initLifecycle(options) {
994
- if (_initialized) {
995
- options.logger("warn", "[glasstrace] initLifecycle() called twice \u2014 ignored.");
996
- return;
997
- }
998
- _logger = options.logger;
999
- _initialized = true;
769
+ // src/build-info.ts
770
+ var UNSET = "";
771
+ var SHA_SHAPE = /^[0-9a-f]{7,64}$/i;
772
+ function redactBuildHash(value) {
773
+ const sanitize = (s) => s.replace(/[\x00-\x1F\x7F]/g, "?");
774
+ if (value.length <= 12) return sanitize(value.slice(0, 4)) + "...";
775
+ return sanitize(value.slice(0, 8)) + "..." + sanitize(value.slice(-4));
1000
776
  }
1001
- function warnIfNotInitialized() {
1002
- if (!_initialized && !_initWarned) {
1003
- _initWarned = true;
1004
- console.warn(
1005
- "[glasstrace] Lifecycle state changed before initLifecycle() was called. Logger not available \u2014 errors will be silent."
777
+ function readBuildHashFromEnv() {
778
+ const raw = process.env.GLASSTRACE_BUILD_HASH;
779
+ if (typeof raw !== "string") return UNSET;
780
+ const trimmed = raw.trim();
781
+ if (trimmed.length === 0) return UNSET;
782
+ if (!SHA_SHAPE.test(trimmed)) {
783
+ sdkLog(
784
+ "warn",
785
+ `[glasstrace] warning: GLASSTRACE_BUILD_HASH=${redactBuildHash(trimmed)} does not match expected SHA shape (7-64 hex characters); source-map enrichment may not work as expected.`
1006
786
  );
1007
787
  }
788
+ return trimmed;
1008
789
  }
1009
- function setCoreState(to) {
1010
- warnIfNotInitialized();
1011
- const from = _coreState;
1012
- if (from === to) return;
1013
- const valid = VALID_CORE_TRANSITIONS[from];
1014
- if (!valid.includes(to)) {
1015
- _logger?.(
1016
- "warn",
1017
- `[glasstrace] Invalid core state transition: ${from} \u2192 ${to}. Ignored.`
1018
- );
1019
- return;
790
+ var cachedBuildHash = null;
791
+ function getBuildHash() {
792
+ if (cachedBuildHash === null) {
793
+ cachedBuildHash = readBuildHashFromEnv();
1020
794
  }
1021
- _coreState = to;
1022
- if (_emitting) return;
1023
- _emitting = true;
1024
- try {
1025
- emitSafe("core:state_changed", { from, to });
1026
- const current = _coreState;
1027
- if (!_coreReadyEmitted && (current === CoreState.ACTIVE || current === CoreState.ACTIVE_DEGRADED)) {
1028
- _coreReadyEmitted = true;
1029
- emitSafe("core:ready", {});
1030
- }
1031
- if (current === CoreState.SHUTTING_DOWN) {
1032
- emitSafe("core:shutdown_started", {});
1033
- }
1034
- if (current === CoreState.SHUTDOWN) {
1035
- emitSafe("core:shutdown_completed", {});
795
+ return cachedBuildHash === UNSET ? void 0 : cachedBuildHash;
796
+ }
797
+
798
+ // src/export-circuit-breaker.ts
799
+ var INITIAL_BACKOFF_MS = 3e4;
800
+ var BACKOFF_FACTOR = 2;
801
+ var MAX_BACKOFF_MS = 30 * 60 * 1e3;
802
+ var FAILURE_THRESHOLD = 5;
803
+ function classifyExportFailure(info) {
804
+ const status = readStatus(info);
805
+ if (status === 401 || status === 403) return "auth";
806
+ if (status === 429) return "rate_limit";
807
+ if (typeof status === "number" && status >= 500 && status <= 599) return "server_error";
808
+ if (typeof status === "number" && status >= 400 && status <= 499) return "client_error";
809
+ return "network";
810
+ }
811
+ function readStatus(info) {
812
+ if (typeof info.status === "number") return info.status;
813
+ const err = info.error;
814
+ if (!err || typeof err !== "object") return void 0;
815
+ const record = err;
816
+ const direct = record.status;
817
+ if (typeof direct === "number") return direct;
818
+ if (typeof direct === "string") {
819
+ const parsed = Number.parseInt(direct, 10);
820
+ if (Number.isFinite(parsed)) return parsed;
821
+ }
822
+ const nested = record.response;
823
+ if (nested && typeof nested === "object") {
824
+ const nestedStatus = nested.status;
825
+ if (typeof nestedStatus === "number") return nestedStatus;
826
+ if (typeof nestedStatus === "string") {
827
+ const parsed = Number.parseInt(nestedStatus, 10);
828
+ if (Number.isFinite(parsed)) return parsed;
1036
829
  }
1037
- } finally {
1038
- _emitting = false;
1039
830
  }
831
+ return void 0;
1040
832
  }
1041
- function initAuthState(state) {
1042
- if (_authInitialized) {
1043
- _logger?.(
1044
- "warn",
1045
- "[glasstrace] initAuthState() called after auth state already initialized. Ignored."
1046
- );
1047
- return;
833
+ function buildOpenedMessage(category, count) {
834
+ return `[glasstrace] Export circuit opened after ${count} consecutive failures (category: ${category}). Subsequent spans dropped until probe succeeds.`;
835
+ }
836
+ var _singleton = null;
837
+ function getExportCircuitBreaker(options) {
838
+ if (_singleton === null) {
839
+ _singleton = createExportCircuitBreaker(options);
1048
840
  }
1049
- _authInitialized = true;
1050
- _authState = state;
841
+ return _singleton;
1051
842
  }
1052
- function setAuthState(to) {
1053
- warnIfNotInitialized();
1054
- const from = _authState;
1055
- if (from === to) return;
1056
- const valid = VALID_AUTH_TRANSITIONS[from];
1057
- if (!valid.includes(to)) {
1058
- _logger?.(
1059
- "warn",
1060
- `[glasstrace] Invalid auth state transition: ${from} \u2192 ${to}. Ignored.`
1061
- );
1062
- return;
843
+ function peekExportCircuitBreaker() {
844
+ return _singleton;
845
+ }
846
+ function createExportCircuitBreaker(options) {
847
+ const events = options.events;
848
+ const recordDropped = options.recordDropped;
849
+ const fsm = options.fsm;
850
+ const now = options.now ?? (() => Date.now());
851
+ const setTimer = options.setTimer ?? ((fn, delayMs) => {
852
+ const handle = setTimeout(fn, delayMs);
853
+ if (typeof handle === "object" && handle && "unref" in handle) {
854
+ handle.unref();
855
+ }
856
+ return handle;
857
+ });
858
+ const clearTimer = options.clearTimer ?? ((handle) => clearTimeout(handle));
859
+ let state = "CLOSED";
860
+ let consecutiveFailures = 0;
861
+ let currentBackoffMs = INITIAL_BACKOFF_MS;
862
+ let openedAtMs = null;
863
+ let pendingTimer = null;
864
+ let halfOpenProbeInFlight = false;
865
+ let generation = 0;
866
+ function clearPendingTimer() {
867
+ if (pendingTimer !== null) {
868
+ clearTimer(pendingTimer);
869
+ pendingTimer = null;
870
+ }
871
+ }
872
+ function scheduleHalfOpen(delayMs) {
873
+ clearPendingTimer();
874
+ pendingTimer = setTimer(() => {
875
+ pendingTimer = null;
876
+ if (state !== "OPEN") return;
877
+ transitionToHalfOpen(delayMs);
878
+ }, delayMs);
879
+ }
880
+ function transitionToOpen(category) {
881
+ const wasNonOpen = state !== "OPEN";
882
+ state = "OPEN";
883
+ halfOpenProbeInFlight = false;
884
+ if (openedAtMs === null) {
885
+ openedAtMs = now();
886
+ }
887
+ if (wasNonOpen) {
888
+ const timestamp = new Date(now()).toISOString();
889
+ const message = buildOpenedMessage(category, consecutiveFailures);
890
+ try {
891
+ events.emitOpened({
892
+ category,
893
+ message,
894
+ timestamp,
895
+ consecutiveFailures,
896
+ nextProbeMs: currentBackoffMs
897
+ });
898
+ } catch {
899
+ }
900
+ try {
901
+ fsm?.onCircuitOpened();
902
+ } catch {
903
+ }
904
+ }
905
+ scheduleHalfOpen(currentBackoffMs);
1063
906
  }
1064
- _authState = to;
907
+ function transitionToHalfOpen(previousTimerMs) {
908
+ state = "HALF_OPEN";
909
+ halfOpenProbeInFlight = false;
910
+ try {
911
+ events.emitHalfOpen({
912
+ timestamp: new Date(now()).toISOString(),
913
+ previousTimerMs
914
+ });
915
+ } catch {
916
+ }
917
+ }
918
+ function transitionToClosed() {
919
+ const startedAt = openedAtMs;
920
+ state = "CLOSED";
921
+ consecutiveFailures = 0;
922
+ currentBackoffMs = INITIAL_BACKOFF_MS;
923
+ openedAtMs = null;
924
+ halfOpenProbeInFlight = false;
925
+ clearPendingTimer();
926
+ try {
927
+ events.emitClosed({
928
+ timestamp: new Date(now()).toISOString(),
929
+ outageDurationMs: startedAt === null ? 0 : Math.max(0, now() - startedAt)
930
+ });
931
+ } catch {
932
+ }
933
+ try {
934
+ fsm?.onCircuitClosed();
935
+ } catch {
936
+ }
937
+ }
938
+ return {
939
+ shouldExport() {
940
+ if (state === "OPEN") return false;
941
+ if (state === "HALF_OPEN") {
942
+ if (halfOpenProbeInFlight) return false;
943
+ halfOpenProbeInFlight = true;
944
+ return true;
945
+ }
946
+ return true;
947
+ },
948
+ recordSuccess() {
949
+ if (state === "HALF_OPEN") {
950
+ halfOpenProbeInFlight = false;
951
+ transitionToClosed();
952
+ return;
953
+ }
954
+ consecutiveFailures = 0;
955
+ },
956
+ recordFailure(info) {
957
+ const category = classifyExportFailure(info);
958
+ if (state === "HALF_OPEN") {
959
+ currentBackoffMs = Math.min(currentBackoffMs * BACKOFF_FACTOR, MAX_BACKOFF_MS);
960
+ halfOpenProbeInFlight = false;
961
+ state = "OPEN";
962
+ scheduleHalfOpen(currentBackoffMs);
963
+ return;
964
+ }
965
+ if (state === "CLOSED") {
966
+ consecutiveFailures += 1;
967
+ if (consecutiveFailures >= FAILURE_THRESHOLD) {
968
+ transitionToOpen(category);
969
+ }
970
+ return;
971
+ }
972
+ },
973
+ onSpansDropped(count) {
974
+ if (!Number.isFinite(count) || count <= 0) return;
975
+ try {
976
+ recordDropped(count);
977
+ } catch {
978
+ }
979
+ },
980
+ getState() {
981
+ return state;
982
+ },
983
+ resetForKeyRotation() {
984
+ generation += 1;
985
+ const wasNonClosed = state !== "CLOSED";
986
+ consecutiveFailures = 0;
987
+ currentBackoffMs = INITIAL_BACKOFF_MS;
988
+ clearPendingTimer();
989
+ if (wasNonClosed) {
990
+ transitionToClosed();
991
+ }
992
+ },
993
+ getGeneration() {
994
+ return generation;
995
+ }
996
+ };
1065
997
  }
1066
- function setOtelState(to) {
1067
- warnIfNotInitialized();
1068
- const from = _otelState;
1069
- if (from === to) return;
1070
- const valid = VALID_OTEL_TRANSITIONS[from];
1071
- if (!valid.includes(to)) {
1072
- _logger?.(
1073
- "warn",
1074
- `[glasstrace] Invalid OTel state transition: ${from} \u2192 ${to}. Ignored.`
1075
- );
1076
- return;
998
+
999
+ // src/enriching-exporter.ts
1000
+ var ATTR = GLASSTRACE_ATTRIBUTE_NAMES;
1001
+ var API_KEY_PENDING = "pending";
1002
+ var MAX_PENDING_SPANS = 1024;
1003
+ var GlasstraceExporter = class {
1004
+ getApiKey;
1005
+ sessionManager;
1006
+ getConfig;
1007
+ environment;
1008
+ endpointUrl;
1009
+ createDelegateFn;
1010
+ verbose;
1011
+ delegate = null;
1012
+ delegateKey = null;
1013
+ pendingBatches = [];
1014
+ pendingSpanCount = 0;
1015
+ overflowLogged = false;
1016
+ /**
1017
+ * Lazily-bound reference to the export-path circuit breaker
1018
+ * (DISC-1568 / Wave 15C). Resolved on first export so this
1019
+ * constructor stays side-effect-free. The breaker is a module-
1020
+ * singleton — every `GlasstraceExporter` instance shares the same
1021
+ * one so a rotation event observed in `init-client.ts` reaches
1022
+ * every active exporter.
1023
+ */
1024
+ circuitBreaker = null;
1025
+ constructor(options) {
1026
+ this.getApiKey = options.getApiKey;
1027
+ this.sessionManager = options.sessionManager;
1028
+ this.getConfig = options.getConfig;
1029
+ this.environment = options.environment;
1030
+ this.endpointUrl = options.endpointUrl;
1031
+ this.createDelegateFn = options.createDelegate;
1032
+ this.verbose = options.verbose ?? false;
1033
+ this[/* @__PURE__ */ Symbol.for("glasstrace.exporter")] = true;
1034
+ }
1035
+ /**
1036
+ * Returns the export-path circuit breaker, lazily wiring it on
1037
+ * first call. The breaker is a module-singleton so all exporter
1038
+ * instances share state — a credential rotation observed once
1039
+ * resets the breaker for every active exporter, and a single
1040
+ * outage at the OTLP endpoint trips a single breaker rather than
1041
+ * one per exporter copy.
1042
+ *
1043
+ * The wiring binds:
1044
+ * - the lifecycle event sink to the SDK's lifecycle bus
1045
+ * (`emitLifecycleEvent`) so the `otel:circuit_*` events surface
1046
+ * to runtime-state, the CLI bridge, and any user-installed
1047
+ * subscribers.
1048
+ * - the dropped-span counter to {@link recordSpansDropped} so OPEN-
1049
+ * state drops show up in the existing health surface.
1050
+ * - the FSM hooks to {@link pushDegradationSource} /
1051
+ * {@link clearDegradationSource} keyed on `"export-circuit"`,
1052
+ * which routes the OPEN/CLOSED transitions through the
1053
+ * centralised `recomputeCoreFromDegradationSources()` helper.
1054
+ * That helper guards `ACTIVE ↔ ACTIVE_DEGRADED` so a circuit
1055
+ * recovery never clobbers an unrelated `OtelState.COEXISTENCE_FAILED`
1056
+ * degradation source.
1057
+ */
1058
+ getCircuitBreaker() {
1059
+ if (this.circuitBreaker !== null) return this.circuitBreaker;
1060
+ this.circuitBreaker = getExportCircuitBreaker({
1061
+ events: {
1062
+ emitOpened: (payload) => emitLifecycleEvent("otel:circuit_opened", payload),
1063
+ emitHalfOpen: (payload) => emitLifecycleEvent("otel:circuit_half_open", payload),
1064
+ emitClosed: (payload) => emitLifecycleEvent("otel:circuit_closed", payload)
1065
+ },
1066
+ recordDropped: (count) => recordSpansDropped(count),
1067
+ fsm: {
1068
+ onCircuitOpened: () => pushDegradationSource("export-circuit"),
1069
+ onCircuitClosed: () => clearDegradationSource("export-circuit")
1070
+ }
1071
+ });
1072
+ return this.circuitBreaker;
1073
+ }
1074
+ export(spans, resultCallback) {
1075
+ const currentKey = this.getApiKey();
1076
+ if (currentKey === API_KEY_PENDING) {
1077
+ this.bufferSpans(spans, resultCallback);
1078
+ return;
1079
+ }
1080
+ const breaker = this.getCircuitBreaker();
1081
+ if (!breaker.shouldExport()) {
1082
+ breaker.onSpansDropped(spans.length);
1083
+ resultCallback({ code: 0 });
1084
+ return;
1085
+ }
1086
+ const enrichedSpans = spans.map((span) => this.enrichSpan(span));
1087
+ const exporter = this.ensureDelegate();
1088
+ if (exporter) {
1089
+ const generationAtIssue = breaker.getGeneration();
1090
+ exporter.export(enrichedSpans, (result) => {
1091
+ if (result.code !== 0) {
1092
+ sdkLog("warn", `[glasstrace] Span export failed: ${result.error?.message ?? "unknown error"}`);
1093
+ }
1094
+ if (breaker.getGeneration() !== generationAtIssue) {
1095
+ resultCallback(result);
1096
+ return;
1097
+ }
1098
+ if (result.code === 0) {
1099
+ breaker.recordSuccess();
1100
+ } else {
1101
+ breaker.recordFailure({ error: result.error });
1102
+ }
1103
+ resultCallback(result);
1104
+ });
1105
+ recordSpansExported(enrichedSpans.length);
1106
+ } else {
1107
+ recordSpansDropped(enrichedSpans.length);
1108
+ resultCallback({ code: 0 });
1109
+ }
1110
+ }
1111
+ /**
1112
+ * Called when the API key transitions from "pending" to a resolved value.
1113
+ * Creates the delegate exporter and flushes all buffered spans.
1114
+ */
1115
+ notifyKeyResolved() {
1116
+ this.flushPending();
1117
+ }
1118
+ async shutdown() {
1119
+ const currentKey = this.getApiKey();
1120
+ if (currentKey !== API_KEY_PENDING && this.pendingBatches.length > 0) {
1121
+ this.flushPending();
1122
+ } else if (this.pendingBatches.length > 0) {
1123
+ console.warn(
1124
+ `[glasstrace] Shutdown with ${this.pendingSpanCount} buffered spans \u2014 API key never resolved, spans lost.`
1125
+ );
1126
+ recordSpansDropped(this.pendingSpanCount);
1127
+ for (const batch of this.pendingBatches) {
1128
+ batch.resultCallback({ code: 0 });
1129
+ }
1130
+ this.pendingBatches = [];
1131
+ this.pendingSpanCount = 0;
1132
+ }
1133
+ if (this.delegate) {
1134
+ return this.delegate.shutdown();
1135
+ }
1136
+ }
1137
+ /**
1138
+ * Flushes any pending buffered spans (if the API key has resolved) and
1139
+ * delegates to the underlying exporter's forceFlush to drain its queue.
1140
+ */
1141
+ forceFlush() {
1142
+ if (this.getApiKey() !== API_KEY_PENDING && this.pendingBatches.length > 0) {
1143
+ this.flushPending();
1144
+ }
1145
+ if (this.delegate?.forceFlush) {
1146
+ return this.delegate.forceFlush();
1147
+ }
1148
+ return Promise.resolve();
1149
+ }
1150
+ /**
1151
+ * Enriches a ReadableSpan with all glasstrace.* attributes.
1152
+ * Returns a new ReadableSpan wrapper; the original span is not mutated.
1153
+ *
1154
+ * Only {@link SessionManager.getSessionId} is individually guarded because
1155
+ * it calls into crypto and schema validation — a session ID failure should
1156
+ * not prevent the rest of enrichment. The other helper calls
1157
+ * ({@link deriveErrorCategory}, {@link deriveOrmProvider},
1158
+ * {@link classifyFetchTarget}) are pure functions on typed string inputs
1159
+ * and rely on the outer catch for any unexpected failure.
1160
+ *
1161
+ * On total failure, returns the original span unchanged.
1162
+ */
1163
+ enrichSpan(span) {
1164
+ try {
1165
+ const attrs = span.attributes ?? {};
1166
+ const name = span.name ?? "";
1167
+ const extra = {};
1168
+ extra[ATTR.TRACE_TYPE] = "server";
1169
+ try {
1170
+ const sessionId = this.sessionManager.getSessionId(this.getApiKey());
1171
+ extra[ATTR.SESSION_ID] = sessionId;
1172
+ } catch {
1173
+ }
1174
+ const env = this.environment ?? process.env.GLASSTRACE_ENV;
1175
+ if (env) {
1176
+ extra[ATTR.ENVIRONMENT] = env;
1177
+ }
1178
+ const buildHash = getBuildHash();
1179
+ if (buildHash) {
1180
+ extra[ATTR.BUILD_HASH] = buildHash;
1181
+ }
1182
+ const existingCid = attrs["glasstrace.correlation.id"];
1183
+ if (typeof existingCid === "string") {
1184
+ extra[ATTR.CORRELATION_ID] = existingCid;
1185
+ }
1186
+ const rawRoute = attrs["http.route"];
1187
+ const route = typeof rawRoute === "string" ? rawRoute : name;
1188
+ if (route) {
1189
+ extra[ATTR.ROUTE] = route;
1190
+ }
1191
+ const rawUrlAttr = attrs["http.url"] ?? attrs["url.full"] ?? attrs["http.target"];
1192
+ const rawHttpUrl = typeof rawUrlAttr === "string" ? rawUrlAttr : void 0;
1193
+ if (rawHttpUrl) {
1194
+ const trpcMatch = rawHttpUrl.match(/\/api\/trpc\/([^/?#]+)/);
1195
+ if (trpcMatch) {
1196
+ let procedure;
1197
+ try {
1198
+ procedure = decodeURIComponent(trpcMatch[1]);
1199
+ } catch {
1200
+ procedure = trpcMatch[1];
1201
+ }
1202
+ if (procedure) {
1203
+ extra[ATTR.TRPC_PROCEDURE] = procedure;
1204
+ }
1205
+ }
1206
+ }
1207
+ const method = attrs["http.method"] ?? attrs["http.request.method"];
1208
+ if (method) {
1209
+ extra[ATTR.HTTP_METHOD] = method;
1210
+ }
1211
+ const actionRoute = extractLeadingPath(route);
1212
+ if (method === "POST" && actionRoute) {
1213
+ const isApiRoute = actionRoute === "/api" || actionRoute.startsWith("/api/");
1214
+ const isInternalRoute = actionRoute.startsWith("/_next/");
1215
+ if (!isApiRoute && !isInternalRoute) {
1216
+ extra[ATTR.NEXT_ACTION_DETECTED] = true;
1217
+ if (typeof extra[ATTR.CORRELATION_ID] !== "string") {
1218
+ maybeShowServerActionNudge();
1219
+ }
1220
+ }
1221
+ }
1222
+ const statusCode = coerceHttpStatus(attrs["http.status_code"]) ?? coerceHttpStatus(attrs["http.response.status_code"]);
1223
+ if (statusCode !== void 0) {
1224
+ extra[ATTR.HTTP_STATUS_CODE] = statusCode;
1225
+ }
1226
+ const isErrorByStatus = span.status?.code === SpanStatusCode.ERROR;
1227
+ const isErrorByEvent = hasExceptionEvent(span);
1228
+ const isErrorByAttrs = typeof attrs["exception.type"] === "string" || typeof attrs["exception.message"] === "string";
1229
+ const statusNotExplicitlyOK = span.status?.code !== SpanStatusCode.OK;
1230
+ if (this.verbose && method) {
1231
+ sdkLog(
1232
+ "info",
1233
+ `[glasstrace] enrichSpan "${name}": status.code=${span.status?.code}, http.status_code=${statusCode}, isErrorByStatus=${isErrorByStatus}, isErrorByEvent=${isErrorByEvent}, isErrorByAttrs=${isErrorByAttrs}`
1234
+ );
1235
+ }
1236
+ if (method && statusNotExplicitlyOK && (isErrorByStatus || isErrorByEvent || isErrorByAttrs)) {
1237
+ if (statusCode === void 0 || statusCode === 0 || statusCode === 200) {
1238
+ const httpErrorType = attrs["error.type"];
1239
+ if (typeof httpErrorType === "string") {
1240
+ const parsed = parseInt(httpErrorType, 10);
1241
+ if (!isNaN(parsed) && parsed >= 400 && parsed <= 599) {
1242
+ extra[ATTR.HTTP_STATUS_CODE] = parsed;
1243
+ } else {
1244
+ extra[ATTR.HTTP_STATUS_CODE] = 500;
1245
+ }
1246
+ } else {
1247
+ extra[ATTR.HTTP_STATUS_CODE] = 500;
1248
+ }
1249
+ if (this.verbose) {
1250
+ sdkLog(
1251
+ "info",
1252
+ `[glasstrace] enrichSpan "${name}": inferred status_code=${extra[ATTR.HTTP_STATUS_CODE]} (was ${statusCode}), error.type=${attrs["error.type"]}`
1253
+ );
1254
+ }
1255
+ }
1256
+ }
1257
+ if (span.startTime && span.endTime) {
1258
+ const [startSec, startNano] = span.startTime;
1259
+ const [endSec, endNano] = span.endTime;
1260
+ const durationMs = (endSec - startSec) * 1e3 + (endNano - startNano) / 1e6;
1261
+ if (durationMs >= 0) {
1262
+ extra[ATTR.HTTP_DURATION_MS] = durationMs;
1263
+ }
1264
+ }
1265
+ const eventDetails = statusNotExplicitlyOK ? getExceptionEventDetails(span) : { type: void 0, message: void 0, stacktrace: void 0 };
1266
+ let errorSource;
1267
+ const attrMessage = attrs["exception.message"];
1268
+ if (eventDetails.message) {
1269
+ extra[ATTR.ERROR_MESSAGE] = eventDetails.message;
1270
+ errorSource = "otel_exception";
1271
+ } else if (typeof attrMessage === "string") {
1272
+ extra[ATTR.ERROR_MESSAGE] = attrMessage;
1273
+ errorSource = "otel_event";
1274
+ }
1275
+ const attrType = attrs["exception.type"];
1276
+ if (eventDetails.type) {
1277
+ extra[ATTR.ERROR_CODE] = eventDetails.type;
1278
+ extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(eventDetails.type);
1279
+ errorSource = errorSource ?? "otel_exception";
1280
+ } else if (typeof attrType === "string") {
1281
+ extra[ATTR.ERROR_CODE] = attrType;
1282
+ extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(attrType);
1283
+ errorSource = errorSource ?? "otel_event";
1284
+ }
1285
+ if (statusNotExplicitlyOK) {
1286
+ const rawStack = eventDetails.stacktrace ?? (typeof attrs["exception.stacktrace"] === "string" ? attrs["exception.stacktrace"] : void 0);
1287
+ if (rawStack) {
1288
+ const prepared = prepareStack(rawStack);
1289
+ if (prepared !== null) {
1290
+ extra[ATTR.ERROR_STACK] = prepared.stack;
1291
+ extra[ATTR.ERROR_STACK_TRUNCATED] = prepared.truncated;
1292
+ extra[ATTR.ERROR_STACK_REDACTED] = prepared.redacted;
1293
+ errorSource = errorSource ?? (eventDetails.stacktrace ? "otel_exception" : "otel_event");
1294
+ }
1295
+ }
1296
+ }
1297
+ const routeIsFallback = route === "/_error" || route === "/_not-found" || route === "/_404" || route === "/_500";
1298
+ if (routeIsFallback && rawHttpUrl) {
1299
+ const originalPath = extractPathOnly(rawHttpUrl);
1300
+ const normOriginal = stripTrailingSlash(originalPath);
1301
+ const normRoute = stripTrailingSlash(route);
1302
+ if (normOriginal && normOriginal !== normRoute) {
1303
+ extra[ATTR.ERROR_ORIGINAL_PATH] = normOriginal;
1304
+ extra[ATTR.ERROR_FALLBACK_ROUTE] = route;
1305
+ extra[ATTR.ERROR_FRAMEWORK_KIND] = "fallback";
1306
+ errorSource = errorSource ?? "framework_fallback";
1307
+ }
1308
+ }
1309
+ if (errorSource !== void 0) {
1310
+ extra[ATTR.ERROR_SOURCE] = errorSource;
1311
+ }
1312
+ if (this.verbose && (extra[ATTR.ERROR_MESSAGE] || extra[ATTR.ERROR_CODE])) {
1313
+ const msgSource = eventDetails.message ? "event" : typeof attrMessage === "string" ? "attrs" : "none";
1314
+ const typeSource = eventDetails.type ? "event" : typeof attrType === "string" ? "attrs" : "none";
1315
+ sdkLog(
1316
+ "info",
1317
+ `[glasstrace] enrichSpan "${name}": error.message source=${msgSource}, error.code source=${typeSource}`
1318
+ );
1319
+ }
1320
+ const errorField = attrs["error.field"];
1321
+ if (typeof errorField === "string") {
1322
+ extra[ATTR.ERROR_FIELD] = errorField;
1323
+ }
1324
+ if (this.getConfig().errorResponseBodies) {
1325
+ const responseBody = attrs["glasstrace.internal.response_body"];
1326
+ if (typeof responseBody === "string") {
1327
+ const enrichedStatus = extra[ATTR.HTTP_STATUS_CODE];
1328
+ const effectiveStatus = typeof enrichedStatus === "number" ? enrichedStatus : statusCode;
1329
+ if (isHttpErrorStatus(effectiveStatus)) {
1330
+ const prepared = prepareErrorResponseBody(responseBody);
1331
+ if (prepared !== null) {
1332
+ extra[ATTR.ERROR_RESPONSE_BODY] = prepared;
1333
+ }
1334
+ }
1335
+ }
1336
+ }
1337
+ const spanAny = span;
1338
+ const instrumentationName = spanAny.instrumentationScope?.name ?? spanAny.instrumentationLibrary?.name ?? "";
1339
+ const ormProvider = deriveOrmProvider(instrumentationName);
1340
+ if (ormProvider) {
1341
+ extra[ATTR.ORM_PROVIDER] = ormProvider;
1342
+ const table = attrs["db.sql.table"];
1343
+ const prismaModel = attrs["db.prisma.model"];
1344
+ const model = typeof table === "string" ? table : typeof prismaModel === "string" ? prismaModel : void 0;
1345
+ if (model) {
1346
+ extra[ATTR.ORM_MODEL] = model;
1347
+ }
1348
+ const operation = attrs["db.operation"];
1349
+ if (typeof operation === "string") {
1350
+ extra[ATTR.ORM_OPERATION] = operation;
1351
+ }
1352
+ }
1353
+ const httpUrl = attrs["http.url"];
1354
+ const fullUrl = attrs["url.full"];
1355
+ const url = typeof httpUrl === "string" ? httpUrl : typeof fullUrl === "string" ? fullUrl : void 0;
1356
+ if (url && span.kind === SpanKind.CLIENT) {
1357
+ extra[ATTR.FETCH_TARGET] = classifyFetchTarget(url);
1358
+ }
1359
+ return createEnrichedSpan(span, extra);
1360
+ } catch {
1361
+ return span;
1362
+ }
1363
+ }
1364
+ /**
1365
+ * Lazily creates the delegate OTLP exporter once the API key is resolved.
1366
+ * Recreates the delegate if the key has changed (e.g., after key rotation)
1367
+ * so the Authorization header stays current.
1368
+ */
1369
+ ensureDelegate() {
1370
+ if (!this.createDelegateFn) return null;
1371
+ const currentKey = this.getApiKey();
1372
+ if (currentKey === API_KEY_PENDING) return null;
1373
+ if (this.delegate && this.delegateKey === currentKey) {
1374
+ return this.delegate;
1375
+ }
1376
+ if (this.delegate) {
1377
+ void this.delegate.shutdown?.().catch(() => {
1378
+ });
1379
+ }
1380
+ this.delegate = this.createDelegateFn(this.endpointUrl, {
1381
+ Authorization: `Bearer ${currentKey}`
1382
+ });
1383
+ this.delegateKey = currentKey;
1384
+ return this.delegate;
1385
+ }
1386
+ /**
1387
+ * Buffers raw (unenriched) spans while the API key is pending.
1388
+ * Evicts oldest batches if the buffer exceeds MAX_PENDING_SPANS.
1389
+ * Re-checks the key after buffering to close the race window where
1390
+ * the key resolves between the caller's check and this buffer call.
1391
+ */
1392
+ bufferSpans(spans, resultCallback) {
1393
+ this.pendingBatches.push({ spans, resultCallback });
1394
+ this.pendingSpanCount += spans.length;
1395
+ while (this.pendingSpanCount > MAX_PENDING_SPANS && this.pendingBatches.length > 1) {
1396
+ const evicted = this.pendingBatches.shift();
1397
+ this.pendingSpanCount -= evicted.spans.length;
1398
+ recordSpansDropped(evicted.spans.length);
1399
+ evicted.resultCallback({ code: 0 });
1400
+ if (!this.overflowLogged) {
1401
+ this.overflowLogged = true;
1402
+ console.warn(
1403
+ "[glasstrace] Pending span buffer overflow \u2014 oldest spans evicted. This usually means the API key is taking too long to resolve."
1404
+ );
1405
+ }
1406
+ }
1407
+ if (this.getApiKey() !== API_KEY_PENDING) {
1408
+ this.flushPending();
1409
+ }
1410
+ }
1411
+ /**
1412
+ * Flushes all buffered spans through the delegate exporter.
1413
+ * Enriches spans at flush time (not buffer time) so that session IDs
1414
+ * are computed with the resolved API key instead of the "pending" sentinel.
1415
+ *
1416
+ * Honors the circuit breaker symmetrically with {@link export}: if the
1417
+ * breaker is OPEN at flush time, every buffered batch is dropped via
1418
+ * `recordSpansDropped` and its callback completed with `{ code: 0 }`,
1419
+ * preserving the bounded-memory contract during outages.
1420
+ */
1421
+ flushPending() {
1422
+ if (this.pendingBatches.length === 0) return;
1423
+ const exporter = this.ensureDelegate();
1424
+ if (!exporter) {
1425
+ let discardedCount = 0;
1426
+ for (const batch of this.pendingBatches) {
1427
+ discardedCount += batch.spans.length;
1428
+ batch.resultCallback({ code: 0 });
1429
+ }
1430
+ recordSpansDropped(discardedCount);
1431
+ this.pendingBatches = [];
1432
+ this.pendingSpanCount = 0;
1433
+ return;
1434
+ }
1435
+ const breaker = this.getCircuitBreaker();
1436
+ const batches = this.pendingBatches;
1437
+ this.pendingBatches = [];
1438
+ this.pendingSpanCount = 0;
1439
+ for (const batch of batches) {
1440
+ if (!breaker.shouldExport()) {
1441
+ breaker.onSpansDropped(batch.spans.length);
1442
+ batch.resultCallback({ code: 0 });
1443
+ continue;
1444
+ }
1445
+ const enriched = batch.spans.map((span) => this.enrichSpan(span));
1446
+ const generationAtIssue = breaker.getGeneration();
1447
+ exporter.export(enriched, (result) => {
1448
+ if (result.code !== 0) {
1449
+ sdkLog("warn", `[glasstrace] Span export failed: ${result.error?.message ?? "unknown error"}`);
1450
+ }
1451
+ if (breaker.getGeneration() !== generationAtIssue) {
1452
+ batch.resultCallback(result);
1453
+ return;
1454
+ }
1455
+ if (result.code === 0) {
1456
+ breaker.recordSuccess();
1457
+ } else {
1458
+ breaker.recordFailure({ error: result.error });
1459
+ }
1460
+ batch.resultCallback(result);
1461
+ });
1462
+ recordSpansExported(enriched.length);
1463
+ }
1077
1464
  }
1078
- _otelState = to;
1465
+ };
1466
+ function createEnrichedSpan(span, extra) {
1467
+ const enrichedAttributes = { ...span.attributes, ...extra };
1468
+ return Object.create(span, {
1469
+ attributes: {
1470
+ value: enrichedAttributes,
1471
+ enumerable: true
1472
+ }
1473
+ });
1079
1474
  }
1080
- function getCoreState() {
1081
- return _coreState;
1475
+ function hasExceptionEvent(span) {
1476
+ return span.events?.some((e) => e.name === "exception") ?? false;
1082
1477
  }
1083
- function getSdkState() {
1478
+ function getExceptionEventDetails(span) {
1479
+ const event = span.events?.find((e) => e.name === "exception");
1480
+ if (!event?.attributes) {
1481
+ return { type: void 0, message: void 0, stacktrace: void 0 };
1482
+ }
1483
+ const type = event.attributes["exception.type"];
1484
+ const message = event.attributes["exception.message"];
1485
+ const stacktrace = event.attributes["exception.stacktrace"];
1084
1486
  return {
1085
- core: _coreState,
1086
- auth: _authState,
1087
- otel: _otelState
1487
+ type: typeof type === "string" ? type : void 0,
1488
+ message: typeof message === "string" ? message : void 0,
1489
+ stacktrace: typeof stacktrace === "string" ? stacktrace : void 0
1088
1490
  };
1089
1491
  }
1090
- function onLifecycleEvent(event, listener) {
1091
- _emitter.on(event, listener);
1092
- }
1093
- function emitLifecycleEvent(event, payload) {
1094
- emitSafe(event, payload);
1492
+ function extractLeadingPath(raw) {
1493
+ if (!raw) return void 0;
1494
+ const trimmed = raw.trim();
1495
+ if (trimmed.length === 0) return void 0;
1496
+ if (trimmed.startsWith("/")) {
1497
+ const firstSpace = trimmed.indexOf(" ");
1498
+ return firstSpace === -1 ? trimmed : trimmed.slice(0, firstSpace);
1499
+ }
1500
+ for (const token of trimmed.split(/\s+/)) {
1501
+ if (token.startsWith("/")) {
1502
+ return token;
1503
+ }
1504
+ }
1505
+ return void 0;
1095
1506
  }
1096
- function offLifecycleEvent(event, listener) {
1097
- _emitter.off(event, listener);
1507
+ function stripTrailingSlash(path3) {
1508
+ if (!path3) return path3;
1509
+ if (path3 === "/") return path3;
1510
+ return path3.endsWith("/") ? path3.slice(0, -1) : path3;
1098
1511
  }
1099
- function emitSafe(event, payload) {
1100
- const listeners = _emitter.listeners(event);
1101
- for (const listener of listeners) {
1512
+ function extractPathOnly(raw) {
1513
+ if (!raw) return void 0;
1514
+ const trimmed = raw.trim();
1515
+ if (trimmed.length === 0) return void 0;
1516
+ const isAbsoluteUrl = /^https?:\/\//i.test(trimmed);
1517
+ const isProtocolRelative = trimmed.startsWith("//");
1518
+ if (isAbsoluteUrl || isProtocolRelative) {
1102
1519
  try {
1103
- const result = listener(payload);
1104
- if (result && typeof result.catch === "function") {
1105
- result.catch((err) => {
1106
- _logger?.(
1107
- "error",
1108
- `[glasstrace] Async error in lifecycle event listener for "${event}": ${err instanceof Error ? err.message : String(err)}`
1109
- );
1110
- });
1520
+ const parsed = new URL(trimmed, "http://_/");
1521
+ if (parsed.pathname && parsed.pathname.startsWith("/")) {
1522
+ return parsed.pathname;
1111
1523
  }
1112
- } catch (err) {
1113
- _logger?.(
1114
- "error",
1115
- `[glasstrace] Error in lifecycle event listener for "${event}": ${err instanceof Error ? err.message : String(err)}`
1116
- );
1524
+ } catch {
1117
1525
  }
1118
1526
  }
1527
+ if (trimmed.startsWith("/")) {
1528
+ const queryIdx = trimmed.indexOf("?");
1529
+ const fragIdx = trimmed.indexOf("#");
1530
+ let cut = trimmed.length;
1531
+ if (queryIdx >= 0) cut = Math.min(cut, queryIdx);
1532
+ if (fragIdx >= 0) cut = Math.min(cut, fragIdx);
1533
+ return trimmed.slice(0, cut);
1534
+ }
1535
+ return void 0;
1119
1536
  }
1120
- function isReady() {
1121
- return _coreState === CoreState.ACTIVE || _coreState === CoreState.ACTIVE_DEGRADED;
1122
- }
1123
- function waitForReady(timeoutMs = 3e4) {
1124
- if (isReady()) {
1125
- return Promise.resolve();
1537
+ function deriveOrmProvider(instrumentationName) {
1538
+ const lower = instrumentationName.toLowerCase();
1539
+ if (lower.includes("prisma")) {
1540
+ return "prisma";
1126
1541
  }
1127
- if (_coreState === CoreState.PRODUCTION_DISABLED || _coreState === CoreState.REGISTRATION_FAILED || _coreState === CoreState.SHUTTING_DOWN || _coreState === CoreState.SHUTDOWN) {
1128
- return Promise.reject(new Error(`SDK is in terminal state: ${_coreState}`));
1542
+ if (lower.includes("drizzle")) {
1543
+ return "drizzle";
1129
1544
  }
1130
- return new Promise((resolve2, reject) => {
1131
- let settled = false;
1132
- const listener = ({ to }) => {
1133
- if (settled) return;
1134
- if (to === CoreState.ACTIVE || to === CoreState.ACTIVE_DEGRADED) {
1135
- settled = true;
1136
- offLifecycleEvent("core:state_changed", listener);
1137
- resolve2();
1138
- } else if (to === CoreState.PRODUCTION_DISABLED || to === CoreState.REGISTRATION_FAILED || to === CoreState.SHUTTING_DOWN || to === CoreState.SHUTDOWN) {
1139
- settled = true;
1140
- offLifecycleEvent("core:state_changed", listener);
1141
- reject(new Error(`SDK reached terminal state: ${to}`));
1142
- }
1143
- };
1144
- onLifecycleEvent("core:state_changed", listener);
1145
- if (timeoutMs > 0) {
1146
- const timer = setTimeout(() => {
1147
- if (settled) return;
1148
- settled = true;
1149
- offLifecycleEvent("core:state_changed", listener);
1150
- reject(new Error(`waitForReady timed out after ${timeoutMs}ms (state: ${_coreState})`));
1151
- }, timeoutMs);
1152
- if (typeof timer === "object" && "unref" in timer) {
1153
- timer.unref();
1154
- }
1155
- }
1156
- });
1545
+ return null;
1157
1546
  }
1158
- function getStatus() {
1159
- let mode;
1160
- if (_coreState === CoreState.PRODUCTION_DISABLED) {
1161
- mode = "disabled";
1162
- } else if (_authState === AuthState.CLAIMING || _authState === AuthState.CLAIMED) {
1163
- mode = "claiming";
1164
- } else if (_authState === AuthState.AUTHENTICATED) {
1165
- mode = "authenticated";
1166
- } else {
1167
- mode = "anonymous";
1547
+ function deriveErrorCategory(errorType) {
1548
+ const lower = errorType.toLowerCase();
1549
+ if (lower.includes("validation") || lower.includes("zod")) {
1550
+ return "validation";
1168
1551
  }
1169
- let tracing;
1170
- if (_otelState === OtelState.COEXISTENCE_FAILED || _otelState === OtelState.UNCONFIGURED || _otelState === OtelState.CONFIGURING) {
1171
- tracing = "not-configured";
1172
- } else if (_coreState === CoreState.ACTIVE_DEGRADED) {
1173
- tracing = "degraded";
1174
- } else if (_otelState === OtelState.AUTO_ATTACHED || _otelState === OtelState.PROCESSOR_PRESENT) {
1175
- tracing = "coexistence";
1176
- } else {
1177
- tracing = "active";
1552
+ if (lower.includes("network") || lower.includes("econnrefused") || lower.includes("fetch") || lower.includes("timeout")) {
1553
+ return "network";
1178
1554
  }
1179
- return {
1180
- ready: isReady(),
1181
- mode,
1182
- tracing
1183
- };
1184
- }
1185
- var _shutdownHooks = [];
1186
- var _signalHandlersRegistered = false;
1187
- var _signalHandler = null;
1188
- var _beforeExitRegistered = false;
1189
- var _beforeExitHandler = null;
1190
- var _shutdownExecuted = false;
1191
- function registerShutdownHook(hook) {
1192
- _shutdownHooks.push(hook);
1193
- _shutdownHooks.sort((a, b) => a.priority - b.priority);
1194
- }
1195
- async function executeShutdown(timeoutMs = 5e3) {
1196
- if (_shutdownExecuted) return;
1197
- _shutdownExecuted = true;
1198
- setCoreState(CoreState.SHUTTING_DOWN);
1199
- for (const hook of _shutdownHooks) {
1200
- try {
1201
- const hookPromise = hook.fn();
1202
- hookPromise.catch(() => {
1203
- });
1204
- await Promise.race([
1205
- hookPromise,
1206
- new Promise((_, reject) => {
1207
- const timer = setTimeout(() => reject(new Error(`Shutdown hook "${hook.name}" timed out`)), timeoutMs);
1208
- if (typeof timer === "object" && "unref" in timer) {
1209
- timer.unref();
1210
- }
1211
- })
1212
- ]);
1213
- } catch (err) {
1214
- _logger?.(
1215
- "warn",
1216
- `[glasstrace] Shutdown hook "${hook.name}" failed: ${err instanceof Error ? err.message : String(err)}`
1217
- );
1218
- }
1555
+ if (lower.includes("auth") || lower.includes("unauthorized") || lower.includes("forbidden")) {
1556
+ return "auth";
1219
1557
  }
1220
- setCoreState(CoreState.SHUTDOWN);
1221
- }
1222
- function registerSignalHandlers() {
1223
- if (_signalHandlersRegistered) return;
1224
- if (typeof process === "undefined" || typeof process.once !== "function") return;
1225
- _signalHandlersRegistered = true;
1226
- const otherSigtermListeners = process.listenerCount("SIGTERM");
1227
- const otherSigintListeners = process.listenerCount("SIGINT");
1228
- const handler = (signal) => {
1229
- void executeShutdown().finally(() => {
1230
- if (_signalHandler) {
1231
- process.removeListener("SIGTERM", _signalHandler);
1232
- process.removeListener("SIGINT", _signalHandler);
1233
- }
1234
- const otherListeners = signal === "SIGTERM" ? otherSigtermListeners : otherSigintListeners;
1235
- const otherProviderOwnsSignal = getCoexistenceState() === "coexisting" && otherListeners > 0;
1236
- if (!otherProviderOwnsSignal) {
1237
- process.kill(process.pid, signal);
1238
- }
1239
- });
1240
- };
1241
- _signalHandler = handler;
1242
- process.once("SIGTERM", handler);
1243
- process.once("SIGINT", handler);
1244
- }
1245
- function registerBeforeExitTrigger() {
1246
- if (_beforeExitRegistered) return;
1247
- if (typeof process === "undefined" || typeof process.once !== "function") return;
1248
- _beforeExitRegistered = true;
1249
- const handler = () => {
1250
- void executeShutdown();
1251
- };
1252
- _beforeExitHandler = handler;
1253
- process.once("beforeExit", handler);
1558
+ if (lower.includes("notfound") || lower.includes("not_found")) {
1559
+ return "not-found";
1560
+ }
1561
+ return "internal";
1254
1562
  }
1255
1563
 
1256
1564
  // ../../node_modules/@opentelemetry/core/build/esm/trace/suppress-tracing.js
@@ -3852,6 +4160,31 @@ var OTLPTraceExporter = class extends OTLPExporterBase {
3852
4160
  }
3853
4161
  };
3854
4162
 
4163
+ // src/api-key-hash.ts
4164
+ var cryptoModule;
4165
+ function loadCrypto() {
4166
+ if (cryptoModule !== void 0) return cryptoModule;
4167
+ try {
4168
+ cryptoModule = __require("node:crypto");
4169
+ } catch {
4170
+ cryptoModule = null;
4171
+ }
4172
+ return cryptoModule;
4173
+ }
4174
+ function hashApiKey(key) {
4175
+ if (typeof key !== "string" || key.length === 0) return "";
4176
+ const crypto = loadCrypto();
4177
+ if (crypto !== null) {
4178
+ return crypto.createHash("sha256").update(key, "utf8").digest("hex").slice(0, 32);
4179
+ }
4180
+ let hash = 2166136261;
4181
+ for (let i = 0; i < key.length; i++) {
4182
+ hash ^= key.charCodeAt(i);
4183
+ hash = hash + ((hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)) >>> 0;
4184
+ }
4185
+ return `fnv:${hash.toString(16).padStart(8, "0")}`;
4186
+ }
4187
+
3855
4188
  // src/proxy-detection.ts
3856
4189
  function isProxyTracerProvider(value) {
3857
4190
  if (value === null || value === void 0 || typeof value !== "object") {
@@ -3878,8 +4211,15 @@ var resolvedApiKey = API_KEY_PENDING;
3878
4211
  var activeExporter = null;
3879
4212
  var additionalExporters = [];
3880
4213
  var injectedProcessor = null;
4214
+ var resolvedApiKeyHash = "";
3881
4215
  function setResolvedApiKey(key) {
4216
+ const newHash = hashApiKey(key);
4217
+ const isRotation = resolvedApiKeyHash !== "" && resolvedApiKeyHash !== newHash;
3882
4218
  resolvedApiKey = key;
4219
+ resolvedApiKeyHash = newHash;
4220
+ if (isRotation) {
4221
+ peekExportCircuitBreaker()?.resetForKeyRotation();
4222
+ }
3883
4223
  }
3884
4224
  function getResolvedApiKey() {
3885
4225
  return resolvedApiKey;
@@ -3971,10 +4311,7 @@ async function runCoexistencePath(existingProvider, config) {
3971
4311
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3972
4312
  providerClass: readProviderClass(existingProvider)
3973
4313
  });
3974
- const coreState = getCoreState();
3975
- if (coreState === CoreState.ACTIVE || coreState === CoreState.KEY_RESOLVED) {
3976
- setCoreState(CoreState.ACTIVE_DEGRADED);
3977
- }
4314
+ pushDegradationSource("otel-coexistence-failed");
3978
4315
  }
3979
4316
  function readProviderClass(tracerProvider) {
3980
4317
  try {
@@ -4404,6 +4741,21 @@ function startRuntimeStateWriter(options) {
4404
4741
  _lastError = { ...payload };
4405
4742
  debouncedWrite();
4406
4743
  });
4744
+ onLifecycleEvent("otel:circuit_opened", (payload) => {
4745
+ _lastError = {
4746
+ category: "export-circuit-open",
4747
+ message: payload.message,
4748
+ timestamp: payload.timestamp,
4749
+ exportCircuitCategory: payload.category
4750
+ };
4751
+ debouncedWrite();
4752
+ });
4753
+ onLifecycleEvent("otel:circuit_closed", () => {
4754
+ if (_lastError?.category === "export-circuit-open") {
4755
+ _lastError = void 0;
4756
+ debouncedWrite();
4757
+ }
4758
+ });
4407
4759
  onLifecycleEvent("auth:key_resolved", () => debouncedWrite());
4408
4760
  onLifecycleEvent("auth:claim_started", () => debouncedWrite());
4409
4761
  onLifecycleEvent("auth:claim_completed", () => debouncedWrite());
@@ -4616,11 +4968,11 @@ function registerGlasstrace(options) {
4616
4968
  setCoreState(CoreState.REGISTERING);
4617
4969
  maybeWarnStaleAgentInstructions({
4618
4970
  projectRoot: process.cwd(),
4619
- sdkVersion: "1.7.0"
4971
+ sdkVersion: "1.8.0"
4620
4972
  });
4621
4973
  startRuntimeStateWriter({
4622
4974
  projectRoot: process.cwd(),
4623
- sdkVersion: "1.7.0"
4975
+ sdkVersion: "1.8.0"
4624
4976
  });
4625
4977
  const config = resolveConfig(options);
4626
4978
  if (config.verbose) {
@@ -4787,8 +5139,8 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4787
5139
  if (config.verbose) {
4788
5140
  console.info("[glasstrace] Background init firing.");
4789
5141
  }
4790
- const healthReport = collectHealthReport("1.7.0");
4791
- const initResult = await performInit(config, anonKeyForInit, "1.7.0", healthReport);
5142
+ const healthReport = collectHealthReport("1.8.0");
5143
+ const initResult = await performInit(config, anonKeyForInit, "1.8.0", healthReport);
4792
5144
  if (generation !== registrationGeneration) return;
4793
5145
  const currentState = getCoreState();
4794
5146
  if (currentState === CoreState.SHUTTING_DOWN || currentState === CoreState.SHUTDOWN) {
@@ -4811,7 +5163,7 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4811
5163
  }
4812
5164
  maybeInstallConsoleCapture();
4813
5165
  if (didLastInitSucceed()) {
4814
- startHeartbeat(config, anonKeyForInit, "1.7.0", generation, (newApiKey, accountId) => {
5166
+ startHeartbeat(config, anonKeyForInit, "1.8.0", generation, (newApiKey, accountId) => {
4815
5167
  setAuthState(AuthState.CLAIMING);
4816
5168
  emitLifecycleEvent("auth:claim_started", { accountId });
4817
5169
  setResolvedApiKey(newApiKey);
@@ -5201,14 +5553,14 @@ export {
5201
5553
  getDateString,
5202
5554
  SessionManager,
5203
5555
  classifyFetchTarget,
5204
- GlasstraceExporter,
5205
5556
  isReady,
5206
5557
  waitForReady,
5207
5558
  getStatus,
5559
+ GlasstraceExporter,
5208
5560
  createGlasstraceSpanProcessor,
5209
5561
  registerGlasstrace,
5210
5562
  getDiscoveryHandler,
5211
5563
  withGlasstraceConfig,
5212
5564
  captureError
5213
5565
  };
5214
- //# sourceMappingURL=chunk-OWPA7GHV.js.map
5566
+ //# sourceMappingURL=chunk-JJL2M64Z.js.map