@glasstrace/sdk 1.6.1 → 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.
- package/README.md +13 -1
- package/dist/{capture-error-BmQz7xF6.d.cts → capture-error-C95zvSvF.d.cts} +38 -0
- package/dist/{capture-error-CTgSYxek.d.ts → capture-error-Dzz7_-v4.d.ts} +38 -0
- package/dist/{chunk-SYNQJZZY.js → chunk-JJL2M64Z.js} +1385 -1033
- package/dist/chunk-JJL2M64Z.js.map +1 -0
- package/dist/cli/init.cjs +21 -5
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.d.cts +13 -1
- package/dist/cli/init.d.ts +13 -1
- package/dist/cli/init.js +18 -3
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp-add.cjs +1 -1
- package/dist/cli/mcp-add.js +1 -1
- package/dist/cli/upgrade-instructions.cjs +1 -1
- package/dist/cli/upgrade-instructions.js +1 -1
- package/dist/index.cjs +3545 -3193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node-entry.cjs +3545 -3193
- package/dist/node-entry.cjs.map +1 -1
- package/dist/node-entry.d.cts +1 -1
- package/dist/node-entry.d.ts +1 -1
- package/dist/node-entry.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-SYNQJZZY.js.map +0 -1
|
@@ -143,1114 +143,1422 @@ function classifyFetchTarget(url) {
|
|
|
143
143
|
return "unknown";
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
// src/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
var
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
]
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
250
|
+
_logger = options.logger;
|
|
251
|
+
_initialized = true;
|
|
232
252
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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 (
|
|
257
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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 (
|
|
353
|
-
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const truncated = truncateStack(sanitized.stack);
|
|
354
|
+
function getCoreState() {
|
|
355
|
+
return _coreState;
|
|
356
|
+
}
|
|
357
|
+
function getSdkState() {
|
|
378
358
|
return {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
359
|
+
core: _coreState,
|
|
360
|
+
auth: _authState,
|
|
361
|
+
otel: _otelState
|
|
382
362
|
};
|
|
383
363
|
}
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
394
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
735
|
-
|
|
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
|
|
809
|
-
|
|
810
|
-
if (
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
|
838
|
-
|
|
839
|
-
if (
|
|
840
|
-
return
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
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
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
873
|
-
|
|
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
|
-
|
|
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
|
|
878
|
-
|
|
879
|
-
if (
|
|
880
|
-
|
|
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 (
|
|
883
|
-
return
|
|
678
|
+
if (work.startsWith("webpack-internal:") || work.startsWith("node:")) {
|
|
679
|
+
return { token, changed: false };
|
|
884
680
|
}
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
889
|
-
|
|
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
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
903
|
-
|
|
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/
|
|
907
|
-
var
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1050
|
-
_authState = state;
|
|
841
|
+
return _singleton;
|
|
1051
842
|
}
|
|
1052
|
-
function
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
);
|
|
1062
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
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
|
|
1081
|
-
return
|
|
1475
|
+
function hasExceptionEvent(span) {
|
|
1476
|
+
return span.events?.some((e) => e.name === "exception") ?? false;
|
|
1082
1477
|
}
|
|
1083
|
-
function
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
|
1097
|
-
|
|
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
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
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
|
|
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
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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 (
|
|
1128
|
-
return
|
|
1542
|
+
if (lower.includes("drizzle")) {
|
|
1543
|
+
return "drizzle";
|
|
1129
1544
|
}
|
|
1130
|
-
return
|
|
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
|
|
1159
|
-
|
|
1160
|
-
if (
|
|
1161
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
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
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
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.
|
|
4971
|
+
sdkVersion: "1.8.0"
|
|
4620
4972
|
});
|
|
4621
4973
|
startRuntimeStateWriter({
|
|
4622
4974
|
projectRoot: process.cwd(),
|
|
4623
|
-
sdkVersion: "1.
|
|
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.
|
|
4791
|
-
const initResult = await performInit(config, anonKeyForInit, "1.
|
|
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.
|
|
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-
|
|
5566
|
+
//# sourceMappingURL=chunk-JJL2M64Z.js.map
|