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