@glasstrace/sdk 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1344 @@
1
+ import {
2
+ buildImportGraph,
3
+ discoverTestFiles,
4
+ extractImports
5
+ } from "./chunk-BKMITIEZ.js";
6
+
7
+ // src/errors.ts
8
+ var SdkError = class extends Error {
9
+ code;
10
+ constructor(code, message, cause) {
11
+ super(message, { cause });
12
+ this.name = "SdkError";
13
+ this.code = code;
14
+ }
15
+ };
16
+
17
+ // src/env-detection.ts
18
+ var DEFAULT_ENDPOINT = "https://api.glasstrace.dev";
19
+ function readEnvVars() {
20
+ return {
21
+ GLASSTRACE_API_KEY: process.env.GLASSTRACE_API_KEY,
22
+ GLASSTRACE_FORCE_ENABLE: process.env.GLASSTRACE_FORCE_ENABLE,
23
+ GLASSTRACE_ENV: process.env.GLASSTRACE_ENV,
24
+ GLASSTRACE_COVERAGE_MAP: process.env.GLASSTRACE_COVERAGE_MAP,
25
+ NODE_ENV: process.env.NODE_ENV,
26
+ VERCEL_ENV: process.env.VERCEL_ENV
27
+ };
28
+ }
29
+ function resolveConfig(options) {
30
+ const env = readEnvVars();
31
+ return {
32
+ apiKey: options?.apiKey ?? env.GLASSTRACE_API_KEY,
33
+ endpoint: options?.endpoint ?? DEFAULT_ENDPOINT,
34
+ forceEnable: options?.forceEnable ?? env.GLASSTRACE_FORCE_ENABLE === "true",
35
+ verbose: options?.verbose ?? false,
36
+ environment: env.GLASSTRACE_ENV,
37
+ coverageMapEnabled: env.GLASSTRACE_COVERAGE_MAP === "true",
38
+ nodeEnv: env.NODE_ENV,
39
+ vercelEnv: env.VERCEL_ENV
40
+ };
41
+ }
42
+ function isProductionDisabled(config) {
43
+ if (config.forceEnable) {
44
+ return false;
45
+ }
46
+ if (config.nodeEnv === "production") {
47
+ return true;
48
+ }
49
+ if (config.vercelEnv === "production") {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ function isAnonymousMode(config) {
55
+ if (config.apiKey === void 0) {
56
+ return true;
57
+ }
58
+ if (config.apiKey.trim() === "") {
59
+ return true;
60
+ }
61
+ if (config.apiKey.startsWith("gt_anon_")) {
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ // src/session.ts
68
+ import { createHash } from "crypto";
69
+ import { SessionIdSchema } from "@glasstrace/protocol";
70
+ var FOUR_HOURS_MS = 4 * 60 * 60 * 1e3;
71
+ var cachedGlasstraceEnv = process.env.GLASSTRACE_ENV;
72
+ var cachedPort = process.env.PORT ?? "3000";
73
+ function deriveSessionId(apiKey, origin, date, windowIndex) {
74
+ const input = JSON.stringify([apiKey, origin, date, windowIndex]);
75
+ const hash = createHash("sha256").update(input).digest("hex").slice(0, 16);
76
+ return SessionIdSchema.parse(hash);
77
+ }
78
+ function getOrigin() {
79
+ if (cachedGlasstraceEnv) {
80
+ return cachedGlasstraceEnv;
81
+ }
82
+ return `localhost:${cachedPort}`;
83
+ }
84
+ function getDateString() {
85
+ const now = /* @__PURE__ */ new Date();
86
+ const year = now.getUTCFullYear();
87
+ const month = String(now.getUTCMonth() + 1).padStart(2, "0");
88
+ const day = String(now.getUTCDate()).padStart(2, "0");
89
+ return `${year}-${month}-${day}`;
90
+ }
91
+ var SessionManager = class {
92
+ windowIndex = 0;
93
+ lastActivityTimestamp = 0;
94
+ lastDate = "";
95
+ lastApiKey = "";
96
+ currentSessionId = null;
97
+ /**
98
+ * Returns the current session ID, deriving a new one if:
99
+ * - More than 4 hours have elapsed since last activity
100
+ * - The UTC date has changed (resets window index to 0)
101
+ * - The API key has changed (e.g., deferred anonymous key swap)
102
+ * - This is the first call
103
+ *
104
+ * @param apiKey - The project's API key used in session derivation.
105
+ * @returns The current or newly derived SessionId.
106
+ */
107
+ getSessionId(apiKey) {
108
+ const now = Date.now();
109
+ const currentDate = getDateString();
110
+ const origin = getOrigin();
111
+ const elapsed = now - this.lastActivityTimestamp;
112
+ const dateChanged = currentDate !== this.lastDate;
113
+ const apiKeyChanged = apiKey !== this.lastApiKey;
114
+ if (dateChanged) {
115
+ this.windowIndex = 0;
116
+ this.lastDate = currentDate;
117
+ this.lastApiKey = apiKey;
118
+ this.currentSessionId = deriveSessionId(apiKey, origin, currentDate, this.windowIndex);
119
+ } else if (apiKeyChanged) {
120
+ this.lastApiKey = apiKey;
121
+ this.currentSessionId = deriveSessionId(apiKey, origin, currentDate, this.windowIndex);
122
+ } else if (this.currentSessionId === null || elapsed > FOUR_HOURS_MS) {
123
+ if (this.currentSessionId !== null) {
124
+ this.windowIndex++;
125
+ }
126
+ this.lastApiKey = apiKey;
127
+ this.currentSessionId = deriveSessionId(apiKey, origin, currentDate, this.windowIndex);
128
+ this.lastDate = currentDate;
129
+ }
130
+ this.lastActivityTimestamp = now;
131
+ return this.currentSessionId;
132
+ }
133
+ };
134
+
135
+ // src/fetch-classifier.ts
136
+ var cachedPort2 = process.env.PORT ?? "3000";
137
+ function classifyFetchTarget(url) {
138
+ let parsed;
139
+ try {
140
+ parsed = new URL(url);
141
+ } catch {
142
+ return "unknown";
143
+ }
144
+ const hostname = parsed.hostname.toLowerCase();
145
+ if (hostname === "supabase.co" || hostname.endsWith(".supabase.co") || hostname === "supabase.in" || hostname.endsWith(".supabase.in")) {
146
+ return "supabase";
147
+ }
148
+ if (hostname === "stripe.com" || hostname.endsWith(".stripe.com")) {
149
+ return "stripe";
150
+ }
151
+ const internalOrigin = `localhost:${cachedPort2}`;
152
+ const parsedPort = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
153
+ const urlOrigin = `${hostname}:${parsedPort}`;
154
+ if (urlOrigin === internalOrigin) {
155
+ return "internal";
156
+ }
157
+ return "unknown";
158
+ }
159
+
160
+ // src/anon-key.ts
161
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
162
+ import { join } from "path";
163
+ import { AnonApiKeySchema, createAnonApiKey } from "@glasstrace/protocol";
164
+ var GLASSTRACE_DIR = ".glasstrace";
165
+ var ANON_KEY_FILE = "anon_key";
166
+ var ephemeralKeyCache = /* @__PURE__ */ new Map();
167
+ async function readAnonKey(projectRoot) {
168
+ const root = projectRoot ?? process.cwd();
169
+ const keyPath = join(root, GLASSTRACE_DIR, ANON_KEY_FILE);
170
+ try {
171
+ const content = await readFile(keyPath, "utf-8");
172
+ const result = AnonApiKeySchema.safeParse(content);
173
+ if (result.success) {
174
+ return result.data;
175
+ }
176
+ } catch {
177
+ }
178
+ const cached = ephemeralKeyCache.get(root);
179
+ if (cached !== void 0) {
180
+ return cached;
181
+ }
182
+ return null;
183
+ }
184
+ async function getOrCreateAnonKey(projectRoot) {
185
+ const root = projectRoot ?? process.cwd();
186
+ const dirPath = join(root, GLASSTRACE_DIR);
187
+ const keyPath = join(dirPath, ANON_KEY_FILE);
188
+ const existingKey = await readAnonKey(root);
189
+ if (existingKey !== null) {
190
+ return existingKey;
191
+ }
192
+ const cached = ephemeralKeyCache.get(root);
193
+ if (cached !== void 0) {
194
+ return cached;
195
+ }
196
+ const newKey = createAnonApiKey();
197
+ try {
198
+ await mkdir(dirPath, { recursive: true, mode: 448 });
199
+ await writeFile(keyPath, newKey, "utf-8");
200
+ await chmod(keyPath, 384);
201
+ } catch (err) {
202
+ ephemeralKeyCache.set(root, newKey);
203
+ console.warn(
204
+ `[glasstrace] Failed to persist anonymous key to ${keyPath}: ${err instanceof Error ? err.message : String(err)}. Using ephemeral key.`
205
+ );
206
+ }
207
+ return newKey;
208
+ }
209
+
210
+ // src/init-client.ts
211
+ import { readFileSync } from "fs";
212
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
213
+ import { join as join2 } from "path";
214
+ import {
215
+ SdkInitResponseSchema,
216
+ SdkCachedConfigSchema,
217
+ DEFAULT_CAPTURE_CONFIG
218
+ } from "@glasstrace/protocol";
219
+ var GLASSTRACE_DIR2 = ".glasstrace";
220
+ var CONFIG_FILE = "config";
221
+ var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
222
+ var INIT_TIMEOUT_MS = 1e4;
223
+ var currentConfig = null;
224
+ var rateLimitBackoff = false;
225
+ function loadCachedConfig(projectRoot) {
226
+ const root = projectRoot ?? process.cwd();
227
+ const configPath = join2(root, GLASSTRACE_DIR2, CONFIG_FILE);
228
+ try {
229
+ const content = readFileSync(configPath, "utf-8");
230
+ const parsed = JSON.parse(content);
231
+ const cached = SdkCachedConfigSchema.parse(parsed);
232
+ const age = Date.now() - cached.cachedAt;
233
+ if (age > TWENTY_FOUR_HOURS_MS) {
234
+ console.warn(
235
+ `[glasstrace] Cached config is ${Math.round(age / 36e5)}h old. Will refresh on next init.`
236
+ );
237
+ }
238
+ const result = SdkInitResponseSchema.safeParse(cached.response);
239
+ if (result.success) {
240
+ return result.data;
241
+ }
242
+ console.warn("[glasstrace] Cached config failed validation. Using defaults.");
243
+ return null;
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+ async function saveCachedConfig(response, projectRoot) {
249
+ const root = projectRoot ?? process.cwd();
250
+ const dirPath = join2(root, GLASSTRACE_DIR2);
251
+ const configPath = join2(dirPath, CONFIG_FILE);
252
+ try {
253
+ await mkdir2(dirPath, { recursive: true });
254
+ const cached = {
255
+ response,
256
+ cachedAt: Date.now()
257
+ };
258
+ await writeFile2(configPath, JSON.stringify(cached), "utf-8");
259
+ } catch (err) {
260
+ console.warn(
261
+ `[glasstrace] Failed to cache config to ${configPath}: ${err instanceof Error ? err.message : String(err)}`
262
+ );
263
+ }
264
+ }
265
+ async function sendInitRequest(config, anonKey, sdkVersion, importGraph, healthReport, diagnostics, signal) {
266
+ const effectiveKey = config.apiKey ?? anonKey;
267
+ if (!effectiveKey) {
268
+ throw new Error("No API key available for init request");
269
+ }
270
+ const payload = {
271
+ apiKey: effectiveKey,
272
+ sdkVersion
273
+ };
274
+ if (config.apiKey && anonKey) {
275
+ payload.anonKey = anonKey;
276
+ }
277
+ if (config.environment) {
278
+ payload.environment = config.environment;
279
+ }
280
+ if (importGraph) {
281
+ payload.importGraph = importGraph;
282
+ }
283
+ if (healthReport) {
284
+ payload.healthReport = healthReport;
285
+ }
286
+ if (diagnostics) {
287
+ payload.diagnostics = diagnostics;
288
+ }
289
+ const url = `${config.endpoint}/v1/sdk/init`;
290
+ const response = await fetch(url, {
291
+ method: "POST",
292
+ headers: {
293
+ "Content-Type": "application/json",
294
+ Authorization: `Bearer ${effectiveKey}`
295
+ },
296
+ body: JSON.stringify(payload),
297
+ signal
298
+ });
299
+ if (!response.ok) {
300
+ try {
301
+ await response.text();
302
+ } catch {
303
+ }
304
+ const error = new Error(`Init request failed with status ${response.status}`);
305
+ error.status = response.status;
306
+ throw error;
307
+ }
308
+ const body = await response.json();
309
+ return SdkInitResponseSchema.parse(body);
310
+ }
311
+ async function performInit(config, anonKey, sdkVersion) {
312
+ if (rateLimitBackoff) {
313
+ rateLimitBackoff = false;
314
+ return;
315
+ }
316
+ try {
317
+ const effectiveKey = config.apiKey ?? anonKey;
318
+ if (!effectiveKey) {
319
+ console.warn("[glasstrace] No API key available for init request.");
320
+ return;
321
+ }
322
+ const controller = new AbortController();
323
+ const timeoutId = setTimeout(() => controller.abort(), INIT_TIMEOUT_MS);
324
+ try {
325
+ const result = await sendInitRequest(
326
+ config,
327
+ anonKey,
328
+ sdkVersion,
329
+ void 0,
330
+ void 0,
331
+ void 0,
332
+ controller.signal
333
+ );
334
+ clearTimeout(timeoutId);
335
+ currentConfig = result;
336
+ await saveCachedConfig(result);
337
+ } catch (err) {
338
+ clearTimeout(timeoutId);
339
+ if (err instanceof DOMException && err.name === "AbortError") {
340
+ console.warn("[glasstrace] ingestion_unreachable: Init request timed out.");
341
+ return;
342
+ }
343
+ const status = err.status;
344
+ if (status === 401) {
345
+ console.warn(
346
+ "[glasstrace] ingestion_auth_failed: Check your GLASSTRACE_API_KEY."
347
+ );
348
+ return;
349
+ }
350
+ if (status === 429) {
351
+ console.warn("[glasstrace] ingestion_rate_limited: Backing off.");
352
+ rateLimitBackoff = true;
353
+ return;
354
+ }
355
+ if (typeof status === "number" && status >= 400) {
356
+ console.warn(
357
+ `[glasstrace] Init request failed with status ${status}. Using cached config.`
358
+ );
359
+ return;
360
+ }
361
+ if (err instanceof Error && err.name === "ZodError") {
362
+ console.warn(
363
+ "[glasstrace] Init response failed validation (schema version mismatch?). Using cached config."
364
+ );
365
+ return;
366
+ }
367
+ console.warn(
368
+ `[glasstrace] ingestion_unreachable: ${err instanceof Error ? err.message : String(err)}`
369
+ );
370
+ }
371
+ } catch (err) {
372
+ console.warn(
373
+ `[glasstrace] Unexpected init error: ${err instanceof Error ? err.message : String(err)}`
374
+ );
375
+ }
376
+ }
377
+ function getActiveConfig() {
378
+ if (currentConfig) {
379
+ return currentConfig.config;
380
+ }
381
+ const cached = loadCachedConfig();
382
+ if (cached) {
383
+ return cached.config;
384
+ }
385
+ return { ...DEFAULT_CAPTURE_CONFIG };
386
+ }
387
+ function _setCurrentConfig(config) {
388
+ currentConfig = config;
389
+ }
390
+
391
+ // src/span-processor.ts
392
+ var GlasstraceSpanProcessor = class {
393
+ wrappedProcessor;
394
+ /* eslint-disable @typescript-eslint/no-unused-vars -- backward compat signature */
395
+ constructor(wrappedProcessor, _sessionManager, _apiKey, _getConfig, _environment) {
396
+ this.wrappedProcessor = wrappedProcessor;
397
+ }
398
+ onStart(span, parentContext) {
399
+ this.wrappedProcessor.onStart(span, parentContext);
400
+ }
401
+ onEnd(readableSpan) {
402
+ this.wrappedProcessor.onEnd(readableSpan);
403
+ }
404
+ async shutdown() {
405
+ return this.wrappedProcessor.shutdown();
406
+ }
407
+ async forceFlush() {
408
+ return this.wrappedProcessor.forceFlush();
409
+ }
410
+ };
411
+
412
+ // src/enriching-exporter.ts
413
+ import { SpanKind } from "@opentelemetry/api";
414
+ import { GLASSTRACE_ATTRIBUTE_NAMES } from "@glasstrace/protocol";
415
+ var ATTR = GLASSTRACE_ATTRIBUTE_NAMES;
416
+ var API_KEY_PENDING = "pending";
417
+ var MAX_PENDING_SPANS = 1024;
418
+ var GlasstraceExporter = class {
419
+ getApiKey;
420
+ sessionManager;
421
+ getConfig;
422
+ environment;
423
+ endpointUrl;
424
+ createDelegateFn;
425
+ delegate = null;
426
+ delegateKey = null;
427
+ pendingBatches = [];
428
+ pendingSpanCount = 0;
429
+ overflowLogged = false;
430
+ constructor(options) {
431
+ this.getApiKey = options.getApiKey;
432
+ this.sessionManager = options.sessionManager;
433
+ this.getConfig = options.getConfig;
434
+ this.environment = options.environment;
435
+ this.endpointUrl = options.endpointUrl;
436
+ this.createDelegateFn = options.createDelegate;
437
+ }
438
+ export(spans, resultCallback) {
439
+ const currentKey = this.getApiKey();
440
+ if (currentKey === API_KEY_PENDING) {
441
+ this.bufferSpans(spans, resultCallback);
442
+ return;
443
+ }
444
+ const enrichedSpans = spans.map((span) => this.enrichSpan(span));
445
+ const exporter = this.ensureDelegate();
446
+ if (exporter) {
447
+ exporter.export(enrichedSpans, resultCallback);
448
+ } else {
449
+ resultCallback({ code: 0 });
450
+ }
451
+ }
452
+ /**
453
+ * Called when the API key transitions from "pending" to a resolved value.
454
+ * Creates the delegate exporter and flushes all buffered spans.
455
+ */
456
+ notifyKeyResolved() {
457
+ this.flushPending();
458
+ }
459
+ async shutdown() {
460
+ const currentKey = this.getApiKey();
461
+ if (currentKey !== API_KEY_PENDING && this.pendingBatches.length > 0) {
462
+ this.flushPending();
463
+ } else if (this.pendingBatches.length > 0) {
464
+ console.warn(
465
+ `[glasstrace] Shutdown with ${this.pendingSpanCount} buffered spans \u2014 API key never resolved, spans lost.`
466
+ );
467
+ for (const batch of this.pendingBatches) {
468
+ batch.resultCallback({ code: 0 });
469
+ }
470
+ this.pendingBatches = [];
471
+ this.pendingSpanCount = 0;
472
+ }
473
+ if (this.delegate) {
474
+ return this.delegate.shutdown();
475
+ }
476
+ }
477
+ forceFlush() {
478
+ if (this.delegate?.forceFlush) {
479
+ return this.delegate.forceFlush();
480
+ }
481
+ return Promise.resolve();
482
+ }
483
+ /**
484
+ * Enriches a ReadableSpan with all glasstrace.* attributes.
485
+ * Returns a new ReadableSpan wrapper; the original span is not mutated.
486
+ *
487
+ * External function calls (getSessionId, deriveErrorCategory,
488
+ * deriveOrmProvider, classifyFetchTarget) are individually guarded so a
489
+ * failure in one does not prevent the remaining attributes from being set.
490
+ * On total failure, returns the original span unchanged.
491
+ */
492
+ enrichSpan(span) {
493
+ try {
494
+ const attrs = span.attributes ?? {};
495
+ const name = span.name ?? "";
496
+ const extra = {};
497
+ extra[ATTR.TRACE_TYPE] = "server";
498
+ try {
499
+ const sessionId = this.sessionManager.getSessionId(this.getApiKey());
500
+ extra[ATTR.SESSION_ID] = sessionId;
501
+ } catch {
502
+ }
503
+ const env = this.environment ?? process.env.GLASSTRACE_ENV;
504
+ if (env) {
505
+ extra[ATTR.ENVIRONMENT] = env;
506
+ }
507
+ const existingCid = attrs["glasstrace.correlation.id"];
508
+ if (typeof existingCid === "string") {
509
+ extra[ATTR.CORRELATION_ID] = existingCid;
510
+ }
511
+ const route = attrs["http.route"] ?? name;
512
+ if (route) {
513
+ extra[ATTR.ROUTE] = route;
514
+ }
515
+ const method = attrs["http.method"] ?? attrs["http.request.method"];
516
+ if (method) {
517
+ extra[ATTR.HTTP_METHOD] = method;
518
+ }
519
+ const statusCode = attrs["http.status_code"] ?? attrs["http.response.status_code"];
520
+ if (statusCode !== void 0) {
521
+ extra[ATTR.HTTP_STATUS_CODE] = statusCode;
522
+ }
523
+ if (span.startTime && span.endTime) {
524
+ const [startSec, startNano] = span.startTime;
525
+ const [endSec, endNano] = span.endTime;
526
+ const durationMs = (endSec - startSec) * 1e3 + (endNano - startNano) / 1e6;
527
+ if (durationMs >= 0) {
528
+ extra[ATTR.HTTP_DURATION_MS] = durationMs;
529
+ }
530
+ }
531
+ const errorMessage = attrs["exception.message"];
532
+ if (errorMessage) {
533
+ extra[ATTR.ERROR_MESSAGE] = errorMessage;
534
+ }
535
+ try {
536
+ const errorType = attrs["exception.type"];
537
+ if (errorType) {
538
+ extra[ATTR.ERROR_CODE] = errorType;
539
+ extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(errorType);
540
+ }
541
+ } catch {
542
+ }
543
+ const errorField = attrs["error.field"];
544
+ if (errorField) {
545
+ extra[ATTR.ERROR_FIELD] = errorField;
546
+ }
547
+ try {
548
+ const spanAny = span;
549
+ const instrumentationName = spanAny.instrumentationScope?.name ?? spanAny.instrumentationLibrary?.name ?? "";
550
+ const ormProvider = deriveOrmProvider(instrumentationName);
551
+ if (ormProvider) {
552
+ extra[ATTR.ORM_PROVIDER] = ormProvider;
553
+ const model = attrs["db.sql.table"] ?? attrs["db.prisma.model"];
554
+ if (model) {
555
+ extra[ATTR.ORM_MODEL] = model;
556
+ }
557
+ const operation = attrs["db.operation"];
558
+ if (operation) {
559
+ extra[ATTR.ORM_OPERATION] = operation;
560
+ }
561
+ }
562
+ } catch {
563
+ }
564
+ try {
565
+ const url = attrs["http.url"] ?? attrs["url.full"];
566
+ if (url && span.kind === SpanKind.CLIENT) {
567
+ extra[ATTR.FETCH_TARGET] = classifyFetchTarget(url);
568
+ }
569
+ } catch {
570
+ }
571
+ return createEnrichedSpan(span, extra);
572
+ } catch {
573
+ return span;
574
+ }
575
+ }
576
+ /**
577
+ * Lazily creates the delegate OTLP exporter once the API key is resolved.
578
+ * Recreates the delegate if the key has changed (e.g., after key rotation)
579
+ * so the Authorization header stays current.
580
+ */
581
+ ensureDelegate() {
582
+ if (!this.createDelegateFn) return null;
583
+ const currentKey = this.getApiKey();
584
+ if (currentKey === API_KEY_PENDING) return null;
585
+ if (this.delegate && this.delegateKey === currentKey) {
586
+ return this.delegate;
587
+ }
588
+ if (this.delegate) {
589
+ void this.delegate.shutdown?.().catch(() => {
590
+ });
591
+ }
592
+ this.delegate = this.createDelegateFn(this.endpointUrl, {
593
+ Authorization: `Bearer ${currentKey}`
594
+ });
595
+ this.delegateKey = currentKey;
596
+ return this.delegate;
597
+ }
598
+ /**
599
+ * Buffers raw (unenriched) spans while the API key is pending.
600
+ * Evicts oldest batches if the buffer exceeds MAX_PENDING_SPANS.
601
+ * Re-checks the key after buffering to close the race window where
602
+ * the key resolves between the caller's check and this buffer call.
603
+ */
604
+ bufferSpans(spans, resultCallback) {
605
+ this.pendingBatches.push({ spans, resultCallback });
606
+ this.pendingSpanCount += spans.length;
607
+ while (this.pendingSpanCount > MAX_PENDING_SPANS && this.pendingBatches.length > 1) {
608
+ const evicted = this.pendingBatches.shift();
609
+ this.pendingSpanCount -= evicted.spans.length;
610
+ evicted.resultCallback({ code: 0 });
611
+ if (!this.overflowLogged) {
612
+ this.overflowLogged = true;
613
+ console.warn(
614
+ "[glasstrace] Pending span buffer overflow \u2014 oldest spans evicted. This usually means the API key is taking too long to resolve."
615
+ );
616
+ }
617
+ }
618
+ if (this.getApiKey() !== API_KEY_PENDING) {
619
+ this.flushPending();
620
+ }
621
+ }
622
+ /**
623
+ * Flushes all buffered spans through the delegate exporter.
624
+ * Enriches spans at flush time (not buffer time) so that session IDs
625
+ * are computed with the resolved API key instead of the "pending" sentinel.
626
+ */
627
+ flushPending() {
628
+ if (this.pendingBatches.length === 0) return;
629
+ const exporter = this.ensureDelegate();
630
+ if (!exporter) {
631
+ for (const batch of this.pendingBatches) {
632
+ batch.resultCallback({ code: 0 });
633
+ }
634
+ this.pendingBatches = [];
635
+ this.pendingSpanCount = 0;
636
+ return;
637
+ }
638
+ const batches = this.pendingBatches;
639
+ this.pendingBatches = [];
640
+ this.pendingSpanCount = 0;
641
+ for (const batch of batches) {
642
+ const enriched = batch.spans.map((span) => this.enrichSpan(span));
643
+ exporter.export(enriched, batch.resultCallback);
644
+ }
645
+ }
646
+ };
647
+ function createEnrichedSpan(span, extra) {
648
+ const enrichedAttributes = { ...span.attributes, ...extra };
649
+ return Object.create(span, {
650
+ attributes: {
651
+ value: enrichedAttributes,
652
+ enumerable: true
653
+ }
654
+ });
655
+ }
656
+ function deriveOrmProvider(instrumentationName) {
657
+ const lower = instrumentationName.toLowerCase();
658
+ if (lower.includes("prisma")) {
659
+ return "prisma";
660
+ }
661
+ if (lower.includes("drizzle")) {
662
+ return "drizzle";
663
+ }
664
+ return null;
665
+ }
666
+ function deriveErrorCategory(errorType) {
667
+ const lower = errorType.toLowerCase();
668
+ if (lower.includes("validation") || lower.includes("zod")) {
669
+ return "validation";
670
+ }
671
+ if (lower.includes("network") || lower.includes("econnrefused") || lower.includes("fetch") || lower.includes("timeout")) {
672
+ return "network";
673
+ }
674
+ if (lower.includes("auth") || lower.includes("unauthorized") || lower.includes("forbidden")) {
675
+ return "auth";
676
+ }
677
+ if (lower.includes("notfound") || lower.includes("not_found")) {
678
+ return "not-found";
679
+ }
680
+ return "internal";
681
+ }
682
+
683
+ // src/discovery-endpoint.ts
684
+ function isAllowedOrigin(origin) {
685
+ if (origin === null) return true;
686
+ if (origin.startsWith("chrome-extension://")) return true;
687
+ if (origin.startsWith("moz-extension://")) return true;
688
+ if (origin.startsWith("safari-web-extension://")) return true;
689
+ return false;
690
+ }
691
+ function buildCorsHeaders(origin) {
692
+ const headers = {
693
+ "Content-Type": "application/json",
694
+ Vary: "Origin"
695
+ };
696
+ if (origin && isAllowedOrigin(origin)) {
697
+ headers["Access-Control-Allow-Origin"] = origin;
698
+ }
699
+ return headers;
700
+ }
701
+ function createDiscoveryHandler(getAnonKey, getSessionId) {
702
+ return async (request) => {
703
+ let url;
704
+ try {
705
+ url = new URL(request.url);
706
+ } catch {
707
+ return null;
708
+ }
709
+ if (url.pathname !== "/__glasstrace/config") {
710
+ return null;
711
+ }
712
+ const origin = request.headers.get("Origin");
713
+ const corsHeaders = buildCorsHeaders(origin);
714
+ if (request.method === "OPTIONS") {
715
+ return new Response(null, {
716
+ status: 204,
717
+ headers: {
718
+ ...corsHeaders,
719
+ "Access-Control-Allow-Methods": "GET, OPTIONS",
720
+ "Access-Control-Allow-Headers": "Content-Type"
721
+ }
722
+ });
723
+ }
724
+ if (request.method !== "GET") {
725
+ return new Response(
726
+ JSON.stringify({ error: "method_not_allowed" }),
727
+ {
728
+ status: 405,
729
+ headers: corsHeaders
730
+ }
731
+ );
732
+ }
733
+ try {
734
+ const anonKey = await getAnonKey();
735
+ if (anonKey === null) {
736
+ return new Response(
737
+ JSON.stringify({ error: "not_ready" }),
738
+ {
739
+ status: 503,
740
+ headers: corsHeaders
741
+ }
742
+ );
743
+ }
744
+ const sessionId = getSessionId();
745
+ return new Response(
746
+ JSON.stringify({ key: anonKey, sessionId }),
747
+ {
748
+ status: 200,
749
+ headers: corsHeaders
750
+ }
751
+ );
752
+ } catch {
753
+ return new Response(
754
+ JSON.stringify({ error: "internal_error" }),
755
+ {
756
+ status: 500,
757
+ headers: corsHeaders
758
+ }
759
+ );
760
+ }
761
+ };
762
+ }
763
+
764
+ // src/otel-config.ts
765
+ var _resolvedApiKey = API_KEY_PENDING;
766
+ var _activeExporter = null;
767
+ var _shutdownHandler = null;
768
+ function setResolvedApiKey(key) {
769
+ _resolvedApiKey = key;
770
+ }
771
+ function getResolvedApiKey() {
772
+ return _resolvedApiKey;
773
+ }
774
+ function notifyApiKeyResolved() {
775
+ _activeExporter?.notifyKeyResolved();
776
+ }
777
+ async function tryImport(moduleId) {
778
+ try {
779
+ return await Function("id", "return import(id)")(moduleId);
780
+ } catch {
781
+ return null;
782
+ }
783
+ }
784
+ function registerShutdownHooks(provider) {
785
+ if (typeof process === "undefined" || typeof process.once !== "function") {
786
+ return;
787
+ }
788
+ if (_shutdownHandler) {
789
+ process.removeListener("SIGTERM", _shutdownHandler);
790
+ process.removeListener("SIGINT", _shutdownHandler);
791
+ }
792
+ let shutdownCalled = false;
793
+ const shutdown = (signal) => {
794
+ if (shutdownCalled) return;
795
+ shutdownCalled = true;
796
+ void provider.shutdown().catch((err) => {
797
+ console.warn(
798
+ `[glasstrace] Error during OTel shutdown: ${err instanceof Error ? err.message : String(err)}`
799
+ );
800
+ }).finally(() => {
801
+ process.removeListener("SIGTERM", _shutdownHandler);
802
+ process.removeListener("SIGINT", _shutdownHandler);
803
+ process.kill(process.pid, signal);
804
+ });
805
+ };
806
+ const handler = (signal) => shutdown(signal);
807
+ _shutdownHandler = handler;
808
+ process.once("SIGTERM", handler);
809
+ process.once("SIGINT", handler);
810
+ }
811
+ async function configureOtel(config, sessionManager) {
812
+ const exporterUrl = `${config.endpoint}/v1/traces`;
813
+ let createOtlpExporter = null;
814
+ const otlpModule = await tryImport("@opentelemetry/exporter-trace-otlp-http");
815
+ if (otlpModule && typeof otlpModule.OTLPTraceExporter === "function") {
816
+ const OTLPTraceExporter = otlpModule.OTLPTraceExporter;
817
+ createOtlpExporter = (url, headers) => new OTLPTraceExporter({ url, headers });
818
+ }
819
+ const glasstraceExporter = new GlasstraceExporter({
820
+ getApiKey: getResolvedApiKey,
821
+ sessionManager,
822
+ getConfig: () => getActiveConfig(),
823
+ environment: config.environment,
824
+ endpointUrl: exporterUrl,
825
+ createDelegate: createOtlpExporter
826
+ });
827
+ _activeExporter = glasstraceExporter;
828
+ const vercelOtel = await tryImport("@vercel/otel");
829
+ if (vercelOtel && typeof vercelOtel.registerOTel === "function") {
830
+ if (!createOtlpExporter) {
831
+ console.warn(
832
+ "[glasstrace] @opentelemetry/exporter-trace-otlp-http not found for @vercel/otel path. Trace export disabled."
833
+ );
834
+ }
835
+ const otelConfig = {
836
+ serviceName: "glasstrace-sdk",
837
+ traceExporter: glasstraceExporter
838
+ };
839
+ const prismaModule = await tryImport("@prisma/instrumentation");
840
+ if (prismaModule) {
841
+ const PrismaInstrumentation = prismaModule.PrismaInstrumentation;
842
+ if (PrismaInstrumentation) {
843
+ otelConfig.instrumentations = [new PrismaInstrumentation()];
844
+ }
845
+ }
846
+ vercelOtel.registerOTel(otelConfig);
847
+ return;
848
+ }
849
+ try {
850
+ const otelSdk = await import("@opentelemetry/sdk-trace-base");
851
+ const otelApi3 = await import("@opentelemetry/api");
852
+ const existingProvider = otelApi3.trace.getTracerProvider();
853
+ const probeTracer = existingProvider.getTracer("glasstrace-probe");
854
+ if (probeTracer.constructor.name !== "ProxyTracer") {
855
+ console.warn(
856
+ "[glasstrace] An existing OpenTelemetry TracerProvider is already registered. Glasstrace will not overwrite it. To use Glasstrace alongside another tracing tool, add GlasstraceExporter as an additional span processor on your existing provider."
857
+ );
858
+ _activeExporter = null;
859
+ return;
860
+ }
861
+ if (!createOtlpExporter) {
862
+ const consoleExporter = new otelSdk.ConsoleSpanExporter();
863
+ const consoleGlasstraceExporter = new GlasstraceExporter({
864
+ getApiKey: getResolvedApiKey,
865
+ sessionManager,
866
+ getConfig: () => getActiveConfig(),
867
+ environment: config.environment,
868
+ endpointUrl: exporterUrl,
869
+ createDelegate: () => consoleExporter
870
+ });
871
+ _activeExporter = consoleGlasstraceExporter;
872
+ console.warn(
873
+ "[glasstrace] @opentelemetry/exporter-trace-otlp-http not found. Using ConsoleSpanExporter."
874
+ );
875
+ const processor2 = new otelSdk.SimpleSpanProcessor(consoleGlasstraceExporter);
876
+ const provider2 = new otelSdk.BasicTracerProvider({
877
+ spanProcessors: [processor2]
878
+ });
879
+ otelApi3.trace.setGlobalTracerProvider(provider2);
880
+ registerShutdownHooks(provider2);
881
+ return;
882
+ }
883
+ const processor = new otelSdk.BatchSpanProcessor(glasstraceExporter);
884
+ const provider = new otelSdk.BasicTracerProvider({
885
+ spanProcessors: [processor]
886
+ });
887
+ otelApi3.trace.setGlobalTracerProvider(provider);
888
+ registerShutdownHooks(provider);
889
+ } catch {
890
+ console.warn(
891
+ "[glasstrace] Neither @vercel/otel nor @opentelemetry/sdk-trace-base available. Tracing disabled."
892
+ );
893
+ }
894
+ }
895
+
896
+ // src/console-capture.ts
897
+ var isGlasstraceLog = false;
898
+ var originalError = null;
899
+ var originalWarn = null;
900
+ var installed = false;
901
+ var otelApi = null;
902
+ function formatArgs(args) {
903
+ return args.map((arg) => {
904
+ if (typeof arg === "string") return arg;
905
+ if (arg instanceof Error) return arg.stack ?? arg.message;
906
+ try {
907
+ return JSON.stringify(arg);
908
+ } catch {
909
+ return String(arg);
910
+ }
911
+ }).join(" ");
912
+ }
913
+ function isSdkMessage(args) {
914
+ return typeof args[0] === "string" && args[0].startsWith("[glasstrace]");
915
+ }
916
+ async function installConsoleCapture() {
917
+ if (installed) return;
918
+ try {
919
+ otelApi = await import("@opentelemetry/api");
920
+ } catch {
921
+ otelApi = null;
922
+ }
923
+ originalError = console.error;
924
+ originalWarn = console.warn;
925
+ installed = true;
926
+ console.error = (...args) => {
927
+ originalError.apply(console, args);
928
+ if (isGlasstraceLog || isSdkMessage(args)) return;
929
+ if (otelApi) {
930
+ const span = otelApi.trace.getSpan(otelApi.context.active());
931
+ if (span) {
932
+ span.addEvent("console.error", {
933
+ "console.message": formatArgs(args)
934
+ });
935
+ }
936
+ }
937
+ };
938
+ console.warn = (...args) => {
939
+ originalWarn.apply(console, args);
940
+ if (isGlasstraceLog || isSdkMessage(args)) return;
941
+ if (otelApi) {
942
+ const span = otelApi.trace.getSpan(otelApi.context.active());
943
+ if (span) {
944
+ span.addEvent("console.warn", {
945
+ "console.message": formatArgs(args)
946
+ });
947
+ }
948
+ }
949
+ };
950
+ }
951
+
952
+ // src/capture-error.ts
953
+ var otelApi2 = null;
954
+ var otelLoadAttempted = false;
955
+ var otelLoadPromise = null;
956
+ async function _preloadOtelApi() {
957
+ if (otelLoadAttempted) return;
958
+ otelLoadAttempted = true;
959
+ try {
960
+ otelApi2 = await import("@opentelemetry/api");
961
+ } catch {
962
+ otelApi2 = null;
963
+ }
964
+ }
965
+ function captureError(error) {
966
+ if (otelApi2) {
967
+ recordError(otelApi2, error);
968
+ return;
969
+ }
970
+ if (!otelLoadAttempted) {
971
+ otelLoadPromise ??= _preloadOtelApi();
972
+ }
973
+ if (otelLoadPromise) {
974
+ void otelLoadPromise.then(() => {
975
+ if (otelApi2) {
976
+ recordError(otelApi2, error);
977
+ }
978
+ });
979
+ }
980
+ }
981
+ function recordError(api, error) {
982
+ try {
983
+ const span = api.trace.getSpan(api.context.active());
984
+ if (!span) return;
985
+ const attributes = {
986
+ "error.message": String(error)
987
+ };
988
+ if (error instanceof Error) {
989
+ attributes["error.type"] = error.constructor.name;
990
+ }
991
+ span.addEvent("glasstrace.error", attributes);
992
+ } catch {
993
+ }
994
+ }
995
+
996
+ // src/register.ts
997
+ var consoleCaptureInstalled = false;
998
+ var discoveryHandler = null;
999
+ var isRegistered = false;
1000
+ var registrationGeneration = 0;
1001
+ function registerGlasstrace(options) {
1002
+ try {
1003
+ if (isRegistered) {
1004
+ return;
1005
+ }
1006
+ const config = resolveConfig(options);
1007
+ if (config.verbose) {
1008
+ console.info("[glasstrace] Config resolved.");
1009
+ }
1010
+ if (isProductionDisabled(config)) {
1011
+ console.warn(
1012
+ "[glasstrace] Disabled in production. Set GLASSTRACE_FORCE_ENABLE=true to override."
1013
+ );
1014
+ return;
1015
+ }
1016
+ if (config.verbose) {
1017
+ console.info("[glasstrace] Not production-disabled.");
1018
+ }
1019
+ const anonymous = isAnonymousMode(config);
1020
+ let effectiveKey = config.apiKey;
1021
+ if (effectiveKey) {
1022
+ setResolvedApiKey(effectiveKey);
1023
+ }
1024
+ if (config.verbose) {
1025
+ console.info(
1026
+ `[glasstrace] Auth mode = ${anonymous ? "anonymous" : "dev-key"}.`
1027
+ );
1028
+ }
1029
+ const cachedInitResponse = loadCachedConfig();
1030
+ if (cachedInitResponse) {
1031
+ _setCurrentConfig(cachedInitResponse);
1032
+ }
1033
+ if (config.verbose) {
1034
+ console.info(
1035
+ `[glasstrace] Cached config ${cachedInitResponse ? "loaded and applied" : "not found"}.`
1036
+ );
1037
+ }
1038
+ const sessionManager = new SessionManager();
1039
+ if (config.verbose) {
1040
+ console.info("[glasstrace] SessionManager created.");
1041
+ }
1042
+ isRegistered = true;
1043
+ const currentGeneration = registrationGeneration;
1044
+ void configureOtel(config, sessionManager).then(
1045
+ () => {
1046
+ void _preloadOtelApi();
1047
+ maybeInstallConsoleCapture();
1048
+ if (config.verbose) {
1049
+ console.info("[glasstrace] OTel configured.");
1050
+ }
1051
+ },
1052
+ (err) => {
1053
+ console.warn(
1054
+ `[glasstrace] Failed to configure OTel: ${err instanceof Error ? err.message : String(err)}`
1055
+ );
1056
+ }
1057
+ );
1058
+ if (anonymous) {
1059
+ if (isDiscoveryEnabled(config)) {
1060
+ let resolvedAnonKey = null;
1061
+ const anonKeyPromise = getOrCreateAnonKey();
1062
+ discoveryHandler = createDiscoveryHandler(
1063
+ async () => resolvedAnonKey,
1064
+ () => sessionManager.getSessionId(getResolvedApiKey())
1065
+ );
1066
+ if (config.verbose) {
1067
+ console.info("[glasstrace] Discovery endpoint registered (key pending).");
1068
+ }
1069
+ void (async () => {
1070
+ try {
1071
+ if (currentGeneration !== registrationGeneration) return;
1072
+ const anonKey = await anonKeyPromise;
1073
+ resolvedAnonKey = anonKey;
1074
+ setResolvedApiKey(anonKey);
1075
+ notifyApiKeyResolved();
1076
+ effectiveKey = anonKey;
1077
+ if (currentGeneration !== registrationGeneration) return;
1078
+ discoveryHandler = createDiscoveryHandler(
1079
+ () => Promise.resolve(anonKey),
1080
+ () => sessionManager.getSessionId(getResolvedApiKey())
1081
+ );
1082
+ if (config.verbose) {
1083
+ console.info("[glasstrace] Background init firing.");
1084
+ }
1085
+ await performInit(config, anonKey, "0.2.0");
1086
+ maybeInstallConsoleCapture();
1087
+ } catch (err) {
1088
+ console.warn(
1089
+ `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
1090
+ );
1091
+ }
1092
+ })();
1093
+ } else {
1094
+ void (async () => {
1095
+ try {
1096
+ if (currentGeneration !== registrationGeneration) return;
1097
+ const anonKey = await getOrCreateAnonKey();
1098
+ setResolvedApiKey(anonKey);
1099
+ notifyApiKeyResolved();
1100
+ effectiveKey = anonKey;
1101
+ if (currentGeneration !== registrationGeneration) return;
1102
+ if (config.verbose) {
1103
+ console.info("[glasstrace] Background init firing.");
1104
+ }
1105
+ await performInit(config, anonKey, "0.2.0");
1106
+ maybeInstallConsoleCapture();
1107
+ } catch (err) {
1108
+ console.warn(
1109
+ `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
1110
+ );
1111
+ }
1112
+ })();
1113
+ }
1114
+ } else {
1115
+ void (async () => {
1116
+ try {
1117
+ if (currentGeneration !== registrationGeneration) return;
1118
+ let anonKeyForInit = null;
1119
+ try {
1120
+ anonKeyForInit = await readAnonKey();
1121
+ } catch {
1122
+ }
1123
+ if (currentGeneration !== registrationGeneration) return;
1124
+ if (config.verbose) {
1125
+ console.info("[glasstrace] Background init firing.");
1126
+ }
1127
+ await performInit(config, anonKeyForInit, "0.2.0");
1128
+ maybeInstallConsoleCapture();
1129
+ } catch (err) {
1130
+ console.warn(
1131
+ `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`
1132
+ );
1133
+ }
1134
+ })();
1135
+ }
1136
+ if (config.coverageMapEnabled && config.verbose) {
1137
+ console.info("[glasstrace] Import graph building skipped.");
1138
+ }
1139
+ } catch (err) {
1140
+ console.warn(
1141
+ `[glasstrace] Registration failed: ${err instanceof Error ? err.message : String(err)}`
1142
+ );
1143
+ }
1144
+ }
1145
+ function getDiscoveryHandler() {
1146
+ return discoveryHandler;
1147
+ }
1148
+ function maybeInstallConsoleCapture() {
1149
+ if (consoleCaptureInstalled) return;
1150
+ if (getActiveConfig().consoleErrors) {
1151
+ consoleCaptureInstalled = true;
1152
+ void installConsoleCapture();
1153
+ }
1154
+ }
1155
+ function isDiscoveryEnabled(config) {
1156
+ if (process.env.GLASSTRACE_DISCOVERY_ENABLED === "true") return true;
1157
+ if (process.env.GLASSTRACE_DISCOVERY_ENABLED === "false") return false;
1158
+ if (config.nodeEnv === "production") return false;
1159
+ if (config.vercelEnv === "production") return false;
1160
+ if (config.nodeEnv === "development" || config.nodeEnv === void 0) return true;
1161
+ return false;
1162
+ }
1163
+
1164
+ // src/source-map-uploader.ts
1165
+ import * as fs from "fs/promises";
1166
+ import * as path from "path";
1167
+ import * as crypto from "crypto";
1168
+ import { execSync } from "child_process";
1169
+ import {
1170
+ SourceMapUploadResponseSchema
1171
+ } from "@glasstrace/protocol";
1172
+ async function collectSourceMaps(buildDir) {
1173
+ const results = [];
1174
+ try {
1175
+ await walkDir(buildDir, buildDir, results);
1176
+ } catch {
1177
+ return [];
1178
+ }
1179
+ return results;
1180
+ }
1181
+ async function walkDir(baseDir, currentDir, results) {
1182
+ let entries;
1183
+ try {
1184
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
1185
+ } catch {
1186
+ return;
1187
+ }
1188
+ for (const entry of entries) {
1189
+ const fullPath = path.join(currentDir, entry.name);
1190
+ if (entry.isDirectory()) {
1191
+ await walkDir(baseDir, fullPath, results);
1192
+ } else if (entry.isFile() && entry.name.endsWith(".map")) {
1193
+ try {
1194
+ const content = await fs.readFile(fullPath, "utf-8");
1195
+ const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, "/");
1196
+ const compiledPath = relativePath.replace(/\.map$/, "");
1197
+ results.push({ filePath: compiledPath, content });
1198
+ } catch {
1199
+ }
1200
+ }
1201
+ }
1202
+ }
1203
+ async function computeBuildHash(maps) {
1204
+ try {
1205
+ const sha = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
1206
+ if (sha) {
1207
+ return sha;
1208
+ }
1209
+ } catch {
1210
+ }
1211
+ const sortedMaps = [...maps ?? []].sort(
1212
+ (a, b) => a.filePath.localeCompare(b.filePath)
1213
+ );
1214
+ const hashInput = sortedMaps.map((m) => `${m.filePath}
1215
+ ${m.content.length}
1216
+ ${m.content}`).join("");
1217
+ const hash = crypto.createHash("sha256").update(hashInput).digest("hex");
1218
+ return hash;
1219
+ }
1220
+ async function uploadSourceMaps(apiKey, endpoint, buildHash, maps) {
1221
+ const body = {
1222
+ apiKey,
1223
+ buildHash,
1224
+ files: maps.map((m) => ({
1225
+ filePath: m.filePath,
1226
+ sourceMap: m.content
1227
+ }))
1228
+ };
1229
+ const baseUrl = endpoint.replace(/\/+$/, "");
1230
+ const response = await fetch(`${baseUrl}/v1/source-maps`, {
1231
+ method: "POST",
1232
+ headers: {
1233
+ "Content-Type": "application/json",
1234
+ Authorization: `Bearer ${apiKey}`
1235
+ },
1236
+ body: JSON.stringify(body)
1237
+ });
1238
+ if (!response.ok) {
1239
+ try {
1240
+ await response.text();
1241
+ } catch {
1242
+ }
1243
+ throw new Error(
1244
+ `Source map upload failed: ${String(response.status)} ${response.statusText}`
1245
+ );
1246
+ }
1247
+ const json = await response.json();
1248
+ return SourceMapUploadResponseSchema.parse(json);
1249
+ }
1250
+
1251
+ // src/config-wrapper.ts
1252
+ function withGlasstraceConfig(nextConfig) {
1253
+ const config = nextConfig != null ? { ...nextConfig } : {};
1254
+ const existingExperimental = config.experimental ?? {};
1255
+ config.experimental = { ...existingExperimental, serverSourceMaps: true };
1256
+ const distDir = typeof config.distDir === "string" ? config.distDir : ".next";
1257
+ const existingWebpack = config.webpack;
1258
+ config.webpack = (webpackConfig, context) => {
1259
+ let result = webpackConfig;
1260
+ if (typeof existingWebpack === "function") {
1261
+ result = existingWebpack(webpackConfig, context);
1262
+ }
1263
+ const webpackContext = context;
1264
+ if (!webpackContext.isServer && webpackContext.dev === false) {
1265
+ const plugins = result.plugins ?? [];
1266
+ plugins.push({
1267
+ apply(compiler) {
1268
+ const typedCompiler = compiler;
1269
+ if (typedCompiler.hooks?.afterEmit?.tapPromise) {
1270
+ typedCompiler.hooks.afterEmit.tapPromise(
1271
+ "GlasstraceSourceMapUpload",
1272
+ async () => {
1273
+ await handleSourceMapUpload(distDir);
1274
+ }
1275
+ );
1276
+ }
1277
+ }
1278
+ });
1279
+ result.plugins = plugins;
1280
+ }
1281
+ return result;
1282
+ };
1283
+ return config;
1284
+ }
1285
+ async function handleSourceMapUpload(distDir) {
1286
+ try {
1287
+ const apiKey = process.env.GLASSTRACE_API_KEY;
1288
+ const endpoint = process.env.GLASSTRACE_ENDPOINT ?? "https://api.glasstrace.dev";
1289
+ if (!apiKey || apiKey.trim() === "") {
1290
+ console.info(
1291
+ "[glasstrace] Source map upload skipped (no API key). Stack traces will show compiled locations."
1292
+ );
1293
+ return;
1294
+ }
1295
+ const maps = await collectSourceMaps(distDir);
1296
+ if (maps.length === 0) {
1297
+ console.info("[glasstrace] No source map files found. Skipping upload.");
1298
+ return;
1299
+ }
1300
+ const buildHash = await computeBuildHash(maps);
1301
+ await uploadSourceMaps(apiKey, endpoint, buildHash, maps);
1302
+ console.info(
1303
+ `[glasstrace] Uploaded ${String(maps.length)} source map(s) for build ${buildHash}.`
1304
+ );
1305
+ } catch (error) {
1306
+ const message = error instanceof Error ? error.message : "Unknown error";
1307
+ console.warn(
1308
+ `[glasstrace] Source map upload failed: ${message}. Build continues normally.`
1309
+ );
1310
+ }
1311
+ }
1312
+ export {
1313
+ GlasstraceExporter,
1314
+ GlasstraceSpanProcessor,
1315
+ SdkError,
1316
+ SessionManager,
1317
+ buildImportGraph,
1318
+ captureError,
1319
+ classifyFetchTarget,
1320
+ collectSourceMaps,
1321
+ computeBuildHash,
1322
+ createDiscoveryHandler,
1323
+ deriveSessionId,
1324
+ discoverTestFiles,
1325
+ extractImports,
1326
+ getActiveConfig,
1327
+ getDateString,
1328
+ getDiscoveryHandler,
1329
+ getOrCreateAnonKey,
1330
+ getOrigin,
1331
+ isAnonymousMode,
1332
+ isProductionDisabled,
1333
+ loadCachedConfig,
1334
+ performInit,
1335
+ readAnonKey,
1336
+ readEnvVars,
1337
+ registerGlasstrace,
1338
+ resolveConfig,
1339
+ saveCachedConfig,
1340
+ sendInitRequest,
1341
+ uploadSourceMaps,
1342
+ withGlasstraceConfig
1343
+ };
1344
+ //# sourceMappingURL=index.js.map