@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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/env-detection.ts","../src/session.ts","../src/fetch-classifier.ts","../src/anon-key.ts","../src/init-client.ts","../src/span-processor.ts","../src/enriching-exporter.ts","../src/discovery-endpoint.ts","../src/otel-config.ts","../src/register.ts","../src/source-map-uploader.ts","../src/config-wrapper.ts","../src/import-graph.ts"],"sourcesContent":["// @glasstrace/sdk\nexport { SdkError } from \"./errors.js\";\nexport {\n readEnvVars,\n resolveConfig,\n isProductionDisabled,\n isAnonymousMode,\n} from \"./env-detection.js\";\nexport type { ResolvedConfig } from \"./env-detection.js\";\nexport {\n deriveSessionId,\n getOrigin,\n getDateString,\n SessionManager,\n} from \"./session.js\";\nexport { classifyFetchTarget } from \"./fetch-classifier.js\";\nexport type { FetchTarget } from \"./fetch-classifier.js\";\nexport { getOrCreateAnonKey, readAnonKey } from \"./anon-key.js\";\n\n// Init client + config cache\nexport {\n loadCachedConfig,\n saveCachedConfig,\n sendInitRequest,\n performInit,\n getActiveConfig,\n} from \"./init-client.js\";\n\n// Custom span processor (deprecated — now a pass-through)\nexport { GlasstraceSpanProcessor } from \"./span-processor.js\";\n\n// Enriching exporter — performs all glasstrace.* enrichment at export time\nexport { GlasstraceExporter } from \"./enriching-exporter.js\";\nexport type { GlasstraceExporterOptions } from \"./enriching-exporter.js\";\n\n// Discovery endpoint\nexport { createDiscoveryHandler } from \"./discovery-endpoint.js\";\n\n// registerGlasstrace orchestrator\nexport {\n registerGlasstrace,\n getDiscoveryHandler,\n} from \"./register.js\";\n\n// Config wrapper + source map uploader\nexport { withGlasstraceConfig } from \"./config-wrapper.js\";\nexport {\n collectSourceMaps,\n computeBuildHash,\n uploadSourceMaps,\n} from \"./source-map-uploader.js\";\nexport type { SourceMapEntry } from \"./source-map-uploader.js\";\n\n// Import graph builder\nexport {\n discoverTestFiles,\n extractImports,\n buildImportGraph,\n} from \"./import-graph.js\";\n","import type { SdkDiagnosticCode } from \"@glasstrace/protocol\";\n\n/**\n * Internal SDK error class with a typed diagnostic code.\n * Caught at the boundary and converted to a log message + diagnostic entry.\n * Never thrown to the developer.\n */\nexport class SdkError extends Error {\n readonly code: SdkDiagnosticCode;\n\n constructor(code: SdkDiagnosticCode, message: string, cause?: Error) {\n super(message, { cause });\n this.name = \"SdkError\";\n this.code = code;\n }\n}\n","import type { GlasstraceEnvVars, GlasstraceOptions } from \"@glasstrace/protocol\";\n\n/**\n * Resolved configuration after merging explicit options with environment variables.\n */\nexport interface ResolvedConfig {\n apiKey: string | undefined;\n endpoint: string;\n forceEnable: boolean;\n verbose: boolean;\n environment: string | undefined;\n coverageMapEnabled: boolean;\n nodeEnv: string | undefined;\n vercelEnv: string | undefined;\n}\n\nconst DEFAULT_ENDPOINT = \"https://api.glasstrace.dev\";\n\n/**\n * Reads all recognized Glasstrace environment variables from process.env.\n * Returns undefined for any variable not set. Never throws.\n */\nexport function readEnvVars(): GlasstraceEnvVars {\n return {\n GLASSTRACE_API_KEY: process.env.GLASSTRACE_API_KEY,\n GLASSTRACE_FORCE_ENABLE: process.env.GLASSTRACE_FORCE_ENABLE,\n GLASSTRACE_ENV: process.env.GLASSTRACE_ENV,\n GLASSTRACE_COVERAGE_MAP: process.env.GLASSTRACE_COVERAGE_MAP,\n NODE_ENV: process.env.NODE_ENV,\n VERCEL_ENV: process.env.VERCEL_ENV,\n };\n}\n\n/**\n * Merges explicit GlasstraceOptions with environment variables.\n * Explicit options take precedence over environment variables.\n */\nexport function resolveConfig(options?: GlasstraceOptions): ResolvedConfig {\n const env = readEnvVars();\n\n return {\n apiKey: options?.apiKey ?? env.GLASSTRACE_API_KEY,\n endpoint: options?.endpoint ?? DEFAULT_ENDPOINT,\n forceEnable: options?.forceEnable ?? env.GLASSTRACE_FORCE_ENABLE === \"true\",\n verbose: options?.verbose ?? false,\n environment: env.GLASSTRACE_ENV,\n coverageMapEnabled: env.GLASSTRACE_COVERAGE_MAP === \"true\",\n nodeEnv: env.NODE_ENV,\n vercelEnv: env.VERCEL_ENV,\n };\n}\n\n/**\n * Returns true when the SDK should be inactive (production detected without force-enable).\n * Logic order:\n * 1. forceEnable === true → return false (override)\n * 2. NODE_ENV === 'production' → return true\n * 3. VERCEL_ENV === 'production' → return true\n * 4. Otherwise → return false\n */\nexport function isProductionDisabled(config: ResolvedConfig): boolean {\n if (config.forceEnable) {\n return false;\n }\n if (config.nodeEnv === \"production\") {\n return true;\n }\n if (config.vercelEnv === \"production\") {\n return true;\n }\n return false;\n}\n\n/**\n * Returns true when no API key is configured (anonymous mode).\n * Treats undefined, empty string, whitespace-only, and gt_anon_* keys as anonymous.\n */\nexport function isAnonymousMode(config: ResolvedConfig): boolean {\n if (config.apiKey === undefined) {\n return true;\n }\n if (config.apiKey.trim() === \"\") {\n return true;\n }\n if (config.apiKey.startsWith(\"gt_anon_\")) {\n return true;\n }\n return false;\n}\n","import { createHash } from \"node:crypto\";\nimport { SessionIdSchema } from \"@glasstrace/protocol\";\nimport type { SessionId } from \"@glasstrace/protocol\";\n\nconst FOUR_HOURS_MS = 4 * 60 * 60 * 1000;\n\n/**\n * Derives a deterministic session ID from the given inputs using SHA-256.\n * The hash is truncated to 16 hex characters and parsed through SessionIdSchema.\n *\n * @param apiKey - The project's API key (or anonymous placeholder).\n * @param origin - The origin string identifying the deployment environment.\n * @param date - UTC date as YYYY-MM-DD.\n * @param windowIndex - Zero-based index of the 4-hour activity window within the day.\n * @returns A 16-character hex SessionId.\n */\nexport function deriveSessionId(\n apiKey: string,\n origin: string,\n date: string,\n windowIndex: number,\n): SessionId {\n const input = JSON.stringify([apiKey, origin, date, windowIndex]);\n const hash = createHash(\"sha256\").update(input).digest(\"hex\").slice(0, 16);\n return SessionIdSchema.parse(hash);\n}\n\n/**\n * Returns the origin string for the current process.\n * If GLASSTRACE_ENV is set, returns that value.\n * Otherwise returns `localhost:{PORT}` (PORT defaults to 3000).\n *\n * @returns The origin string used as a session derivation input.\n */\nexport function getOrigin(): string {\n if (process.env.GLASSTRACE_ENV) {\n return process.env.GLASSTRACE_ENV;\n }\n const port = process.env.PORT ?? \"3000\";\n return `localhost:${port}`;\n}\n\n/**\n * Returns the current UTC date as a YYYY-MM-DD string.\n *\n * @returns The UTC date formatted as \"YYYY-MM-DD\".\n */\nexport function getDateString(): string {\n const now = new Date();\n const year = now.getUTCFullYear();\n const month = String(now.getUTCMonth() + 1).padStart(2, \"0\");\n const day = String(now.getUTCDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\n/**\n * Tracks the current session state with 4-hour window tracking.\n * Instantiated once by the orchestrator.\n */\nexport class SessionManager {\n private windowIndex: number = 0;\n private lastActivityTimestamp: number = 0;\n private lastDate: string = \"\";\n private lastApiKey: string = \"\";\n private currentSessionId: SessionId | null = null;\n\n /**\n * Returns the current session ID, deriving a new one if:\n * - More than 4 hours have elapsed since last activity\n * - The UTC date has changed (resets window index to 0)\n * - The API key has changed (e.g., deferred anonymous key swap)\n * - This is the first call\n *\n * @param apiKey - The project's API key used in session derivation.\n * @returns The current or newly derived SessionId.\n */\n getSessionId(apiKey: string): SessionId {\n const now = Date.now();\n const currentDate = getDateString();\n const origin = getOrigin();\n\n const elapsed = now - this.lastActivityTimestamp;\n const dateChanged = currentDate !== this.lastDate;\n const apiKeyChanged = apiKey !== this.lastApiKey;\n\n if (dateChanged) {\n // New UTC day: reset window index\n this.windowIndex = 0;\n this.lastDate = currentDate;\n this.lastApiKey = apiKey;\n this.currentSessionId = deriveSessionId(apiKey, origin, currentDate, this.windowIndex);\n } else if (apiKeyChanged) {\n // API key changed (e.g., anonymous key resolved): re-derive with same window\n this.lastApiKey = apiKey;\n this.currentSessionId = deriveSessionId(apiKey, origin, currentDate, this.windowIndex);\n } else if (this.currentSessionId === null || elapsed > FOUR_HOURS_MS) {\n // First call or gap exceeding 4 hours: increment window\n if (this.currentSessionId !== null) {\n this.windowIndex++;\n }\n this.lastApiKey = apiKey;\n this.currentSessionId = deriveSessionId(apiKey, origin, currentDate, this.windowIndex);\n this.lastDate = currentDate;\n }\n\n this.lastActivityTimestamp = now;\n return this.currentSessionId;\n }\n}\n","/**\n * The set of recognized fetch target categories.\n */\nexport type FetchTarget = \"supabase\" | \"stripe\" | \"internal\" | \"unknown\";\n\n/**\n * Classifies an outbound fetch target URL into a known category.\n * Classification is case-insensitive and based on the URL hostname.\n * Uses dot-boundary matching to avoid false positives (e.g. evilstripe.com).\n *\n * Returns one of: 'supabase', 'stripe', 'internal', or 'unknown'.\n */\nexport function classifyFetchTarget(url: string): FetchTarget {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return \"unknown\";\n }\n\n const hostname = parsed.hostname.toLowerCase();\n\n if (\n hostname === \"supabase.co\" ||\n hostname.endsWith(\".supabase.co\") ||\n hostname === \"supabase.in\" ||\n hostname.endsWith(\".supabase.in\")\n ) {\n return \"supabase\";\n }\n\n if (hostname === \"stripe.com\" || hostname.endsWith(\".stripe.com\")) {\n return \"stripe\";\n }\n\n const port = process.env.PORT ?? \"3000\";\n const internalOrigin = `localhost:${port}`;\n const parsedPort = parsed.port || (parsed.protocol === \"https:\" ? \"443\" : \"80\");\n const urlOrigin = `${hostname}:${parsedPort}`;\n\n if (urlOrigin === internalOrigin) {\n return \"internal\";\n }\n\n return \"unknown\";\n}\n","import { readFile, writeFile, mkdir, chmod } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { AnonApiKeySchema, createAnonApiKey } from \"@glasstrace/protocol\";\nimport type { AnonApiKey } from \"@glasstrace/protocol\";\n\nconst GLASSTRACE_DIR = \".glasstrace\";\nconst ANON_KEY_FILE = \"anon_key\";\n\n/**\n * In-memory cache for ephemeral keys when filesystem persistence fails.\n * Keyed by resolved project root to support multiple roots in tests.\n */\nconst ephemeralKeyCache = new Map<string, AnonApiKey>();\n\n/**\n * Reads an existing anonymous key from the filesystem.\n * Returns the key if valid, or null if:\n * - The file does not exist\n * - The file content is invalid\n * - An I/O error occurs\n */\nexport async function readAnonKey(projectRoot?: string): Promise<AnonApiKey | null> {\n const root = projectRoot ?? process.cwd();\n const keyPath = join(root, GLASSTRACE_DIR, ANON_KEY_FILE);\n\n try {\n const content = await readFile(keyPath, \"utf-8\");\n const result = AnonApiKeySchema.safeParse(content);\n if (result.success) {\n return result.data;\n }\n } catch {\n // Fall through to check ephemeral cache\n }\n\n // Check in-memory cache (used when filesystem persistence failed)\n const cached = ephemeralKeyCache.get(root);\n if (cached !== undefined) {\n return cached;\n }\n\n return null;\n}\n\n/**\n * Gets an existing anonymous key from the filesystem, or creates a new one.\n *\n * - If file exists and contains a valid key, returns it\n * - If file does not exist or content is invalid, generates a new key via createAnonApiKey()\n * - Writes the new key to `.glasstrace/anon_key`, creating the directory if needed\n * - On file write failure: logs a warning, caches an ephemeral in-memory key so\n * repeated calls in the same process return the same key\n */\nexport async function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey> {\n const root = projectRoot ?? process.cwd();\n const dirPath = join(root, GLASSTRACE_DIR);\n const keyPath = join(dirPath, ANON_KEY_FILE);\n\n // Try reading existing key from filesystem\n const existingKey = await readAnonKey(root);\n if (existingKey !== null) {\n return existingKey;\n }\n\n // Check in-memory cache (used when filesystem is unavailable)\n const cached = ephemeralKeyCache.get(root);\n if (cached !== undefined) {\n return cached;\n }\n\n // Generate a new key\n const newKey = createAnonApiKey();\n\n // Persist to filesystem\n try {\n await mkdir(dirPath, { recursive: true, mode: 0o700 });\n await writeFile(keyPath, newKey, \"utf-8\");\n await chmod(keyPath, 0o600);\n } catch (err) {\n // Cache in memory so repeated calls get the same ephemeral key\n ephemeralKeyCache.set(root, newKey);\n console.warn(\n `[glasstrace] Failed to persist anonymous key to ${keyPath}: ${err instanceof Error ? err.message : String(err)}. Using ephemeral key.`,\n );\n }\n\n return newKey;\n}\n","import { readFileSync } from \"node:fs\";\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport {\n SdkInitResponseSchema,\n SdkCachedConfigSchema,\n DEFAULT_CAPTURE_CONFIG,\n} from \"@glasstrace/protocol\";\nimport type {\n SdkInitResponse,\n CaptureConfig,\n AnonApiKey,\n ImportGraphPayload,\n SdkHealthReport,\n SdkDiagnosticCode,\n} from \"@glasstrace/protocol\";\nimport type { ResolvedConfig } from \"./env-detection.js\";\n\nconst GLASSTRACE_DIR = \".glasstrace\";\nconst CONFIG_FILE = \"config\";\nconst TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;\nconst INIT_TIMEOUT_MS = 10_000;\n\n/** In-memory config from the latest successful init response. */\nlet currentConfig: SdkInitResponse | null = null;\n\n/** Whether the next init call should be skipped (rate-limit backoff). */\nlet rateLimitBackoff = false;\n\n/**\n * Reads and validates a cached config file from `.glasstrace/config`.\n * Returns the parsed `SdkInitResponse` or `null` on any failure.\n */\nexport function loadCachedConfig(projectRoot?: string): SdkInitResponse | null {\n const root = projectRoot ?? process.cwd();\n const configPath = join(root, GLASSTRACE_DIR, CONFIG_FILE);\n\n try {\n // Use synchronous read for startup performance (this is called during init)\n const content = readFileSync(configPath, \"utf-8\");\n const parsed = JSON.parse(content);\n const cached = SdkCachedConfigSchema.parse(parsed);\n\n // Warn if cache is stale\n const age = Date.now() - cached.cachedAt;\n if (age > TWENTY_FOUR_HOURS_MS) {\n console.warn(\n `[glasstrace] Cached config is ${Math.round(age / 3600000)}h old. Will refresh on next init.`,\n );\n }\n\n // Parse the response through the schema\n const result = SdkInitResponseSchema.safeParse(cached.response);\n if (result.success) {\n return result.data;\n }\n\n console.warn(\"[glasstrace] Cached config failed validation. Using defaults.\");\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Persists the init response to `.glasstrace/config`.\n * On failure, logs a warning and continues.\n */\nexport async function saveCachedConfig(\n response: SdkInitResponse,\n projectRoot?: string,\n): Promise<void> {\n const root = projectRoot ?? process.cwd();\n const dirPath = join(root, GLASSTRACE_DIR);\n const configPath = join(dirPath, CONFIG_FILE);\n\n try {\n await mkdir(dirPath, { recursive: true });\n const cached = {\n response,\n cachedAt: Date.now(),\n };\n await writeFile(configPath, JSON.stringify(cached), \"utf-8\");\n } catch (err) {\n console.warn(\n `[glasstrace] Failed to cache config to ${configPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\n/**\n * Sends a POST request to `/v1/sdk/init`.\n * Validates the response against `SdkInitResponseSchema`.\n */\nexport async function sendInitRequest(\n config: ResolvedConfig,\n anonKey: AnonApiKey | null,\n sdkVersion: string,\n importGraph?: ImportGraphPayload,\n healthReport?: SdkHealthReport,\n diagnostics?: Array<{ code: SdkDiagnosticCode; message: string; timestamp: number }>,\n signal?: AbortSignal,\n): Promise<SdkInitResponse> {\n // Determine the API key for auth\n const effectiveKey = config.apiKey ?? anonKey;\n if (!effectiveKey) {\n throw new Error(\"No API key available for init request\");\n }\n\n // Build the request payload\n const payload: Record<string, unknown> = {\n apiKey: effectiveKey,\n sdkVersion,\n };\n\n // Straggler linking: if dev key is set AND anonKey is provided\n if (config.apiKey && anonKey) {\n payload.anonKey = anonKey;\n }\n\n if (config.environment) {\n payload.environment = config.environment;\n }\n if (importGraph) {\n payload.importGraph = importGraph;\n }\n if (healthReport) {\n payload.healthReport = healthReport;\n }\n if (diagnostics) {\n payload.diagnostics = diagnostics;\n }\n\n const url = `${config.endpoint}/v1/sdk/init`;\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${effectiveKey}`,\n },\n body: JSON.stringify(payload),\n signal,\n });\n\n if (!response.ok) {\n const error = new Error(`Init request failed with status ${response.status}`);\n (error as unknown as Record<string, unknown>).status = response.status;\n throw error;\n }\n\n const body = await response.json();\n return SdkInitResponseSchema.parse(body);\n}\n\n/**\n * Orchestrates the full init flow: send request, update config, cache result.\n * This function MUST NOT throw.\n */\nexport async function performInit(\n config: ResolvedConfig,\n anonKey: AnonApiKey | null,\n sdkVersion: string,\n): Promise<void> {\n // Skip if in rate-limit backoff\n if (rateLimitBackoff) {\n rateLimitBackoff = false; // Reset for next call\n return;\n }\n\n try {\n const effectiveKey = config.apiKey ?? anonKey;\n if (!effectiveKey) {\n console.warn(\"[glasstrace] No API key available for init request.\");\n return;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), INIT_TIMEOUT_MS);\n\n try {\n // Delegate to sendInitRequest to avoid duplicating fetch logic\n const result = await sendInitRequest(\n config,\n anonKey,\n sdkVersion,\n undefined,\n undefined,\n undefined,\n controller.signal,\n );\n\n clearTimeout(timeoutId);\n\n // Update in-memory config\n currentConfig = result;\n\n // Persist to disk\n await saveCachedConfig(result);\n } catch (err) {\n clearTimeout(timeoutId);\n\n if (err instanceof DOMException && err.name === \"AbortError\") {\n console.warn(\"[glasstrace] ingestion_unreachable: Init request timed out.\");\n return;\n }\n\n // Check for HTTP status errors attached by sendInitRequest\n const status = (err as Record<string, unknown>).status;\n if (status === 401) {\n console.warn(\n \"[glasstrace] ingestion_auth_failed: Check your GLASSTRACE_API_KEY.\",\n );\n return;\n }\n\n if (status === 429) {\n console.warn(\"[glasstrace] ingestion_rate_limited: Backing off.\");\n rateLimitBackoff = true;\n return;\n }\n\n if (typeof status === \"number\" && status >= 400) {\n console.warn(\n `[glasstrace] Init request failed with status ${status}. Using cached config.`,\n );\n return;\n }\n\n // Schema validation failure from sendInitRequest.parse\n if (err instanceof Error && err.name === \"ZodError\") {\n console.warn(\n \"[glasstrace] Init response failed validation (schema version mismatch?). Using cached config.\",\n );\n return;\n }\n\n // Network error or other fetch failure\n console.warn(\n `[glasstrace] ingestion_unreachable: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n } catch (err) {\n // Outermost catch -- should never reach here, but safety net\n console.warn(\n `[glasstrace] Unexpected init error: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\n/**\n * Returns the current capture config from the three-tier fallback chain:\n * 1. In-memory config from latest init response\n * 2. File cache\n * 3. DEFAULT_CAPTURE_CONFIG\n */\nexport function getActiveConfig(): CaptureConfig {\n // Tier 1: in-memory\n if (currentConfig) {\n return currentConfig.config;\n }\n\n // Tier 2: file cache\n const cached = loadCachedConfig();\n if (cached) {\n return cached.config;\n }\n\n // Tier 3: defaults\n return { ...DEFAULT_CAPTURE_CONFIG };\n}\n\n/**\n * Resets the in-memory config store. For testing only.\n */\nexport function _resetConfigForTesting(): void {\n currentConfig = null;\n rateLimitBackoff = false;\n}\n\n/**\n * Sets the in-memory config directly. Used by performInit and the orchestrator.\n */\nexport function _setCurrentConfig(config: SdkInitResponse): void {\n currentConfig = config;\n}\n\n/**\n * Returns whether rate-limit backoff is active. For testing only.\n */\nexport function _isRateLimitBackoff(): boolean {\n return rateLimitBackoff;\n}\n","import type { Context } from \"@opentelemetry/api\";\nimport type { SpanProcessor, ReadableSpan } from \"@opentelemetry/sdk-trace-base\";\nimport type { Span } from \"@opentelemetry/sdk-trace-base\";\nimport type { CaptureConfig } from \"@glasstrace/protocol\";\nimport type { SessionManager } from \"./session.js\";\n\n/**\n * Lightweight SpanProcessor that delegates to a wrapped processor.\n *\n * All glasstrace.* attribute enrichment has been moved to {@link GlasstraceExporter}\n * (see enriching-exporter.ts), which enriches spans at export time. This resolves:\n * - Cold-start spans are buffered in the exporter, not dropped\n * - Vercel's CompositeSpanProcessor skips onEnding(); the exporter doesn't need it\n * - Session ID is computed at export time with the resolved API key\n *\n * This class is retained for backward compatibility. New code should use\n * GlasstraceExporter directly.\n *\n * @deprecated Use GlasstraceExporter for span enrichment. This processor is now a pass-through.\n */\nexport class GlasstraceSpanProcessor implements SpanProcessor {\n private readonly wrappedProcessor: SpanProcessor;\n\n /* eslint-disable @typescript-eslint/no-unused-vars -- backward compat signature */\n constructor(\n wrappedProcessor: SpanProcessor,\n _sessionManager?: SessionManager,\n _apiKey?: string | (() => string),\n _getConfig?: () => CaptureConfig,\n _environment?: string,\n ) {\n /* eslint-enable @typescript-eslint/no-unused-vars */\n this.wrappedProcessor = wrappedProcessor;\n }\n\n onStart(span: Span, parentContext: Context): void {\n this.wrappedProcessor.onStart(span, parentContext);\n }\n\n onEnd(readableSpan: ReadableSpan): void {\n this.wrappedProcessor.onEnd(readableSpan);\n }\n\n async shutdown(): Promise<void> {\n return this.wrappedProcessor.shutdown();\n }\n\n async forceFlush(): Promise<void> {\n return this.wrappedProcessor.forceFlush();\n }\n}\n","import { SpanKind } from \"@opentelemetry/api\";\nimport type { ReadableSpan, SpanExporter } from \"@opentelemetry/sdk-trace-base\";\nimport type { ExportResult } from \"@opentelemetry/core\";\nimport { GLASSTRACE_ATTRIBUTE_NAMES } from \"@glasstrace/protocol\";\nimport type { CaptureConfig } from \"@glasstrace/protocol\";\nimport type { SessionManager } from \"./session.js\";\nimport { classifyFetchTarget } from \"./fetch-classifier.js\";\n\nconst ATTR = GLASSTRACE_ATTRIBUTE_NAMES;\n\n/**\n * Sentinel value indicating the API key has not yet been resolved.\n */\nexport const API_KEY_PENDING = \"pending\" as const;\n\n/**\n * Maximum number of spans to buffer while waiting for key resolution.\n * Prevents unbounded memory growth if the key never resolves.\n */\nconst MAX_PENDING_SPANS = 1024;\n\n/**\n * Options for constructing a {@link GlasstraceExporter}.\n */\nexport interface GlasstraceExporterOptions {\n getApiKey: () => string;\n sessionManager: SessionManager;\n getConfig: () => CaptureConfig;\n environment: string | undefined;\n endpointUrl: string;\n createDelegate: ((url: string, headers: Record<string, string>) => SpanExporter) | null;\n}\n\ninterface PendingBatch {\n spans: ReadableSpan[];\n resultCallback: (result: ExportResult) => void;\n}\n\n/**\n * A SpanExporter that enriches spans with glasstrace.* attributes at export\n * time, then delegates to a real OTLP exporter.\n *\n * This design resolves three issues:\n * - Spans emitted before the API key resolves are buffered (not dropped)\n * and flushed once the key is available.\n * - Enrichment happens in the exporter (not onEnding), so it works\n * on Vercel where CompositeSpanProcessor does not forward onEnding().\n * - Session ID is computed at export time using the resolved API key,\n * not the \"pending\" placeholder.\n */\nexport class GlasstraceExporter implements SpanExporter {\n private readonly getApiKey: () => string;\n private readonly sessionManager: SessionManager;\n private readonly getConfig: () => CaptureConfig;\n private readonly environment: string | undefined;\n private readonly endpointUrl: string;\n private readonly createDelegateFn: ((url: string, headers: Record<string, string>) => SpanExporter) | null;\n\n private delegate: SpanExporter | null = null;\n private pendingBatches: PendingBatch[] = [];\n private pendingSpanCount = 0;\n private overflowLogged = false;\n\n constructor(options: GlasstraceExporterOptions) {\n this.getApiKey = options.getApiKey;\n this.sessionManager = options.sessionManager;\n this.getConfig = options.getConfig;\n this.environment = options.environment;\n this.endpointUrl = options.endpointUrl;\n this.createDelegateFn = options.createDelegate;\n }\n\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n const enrichedSpans = spans.map((span) => this.enrichSpan(span));\n\n const currentKey = this.getApiKey();\n if (currentKey === API_KEY_PENDING) {\n // Buffer spans until key resolves\n this.bufferSpans(enrichedSpans, resultCallback);\n return;\n }\n\n // Key is available — ensure delegate exists and export\n const exporter = this.ensureDelegate();\n if (exporter) {\n exporter.export(enrichedSpans, resultCallback);\n } else {\n // No delegate factory — report success so the pipeline does not stall\n resultCallback({ code: 0 });\n }\n }\n\n /**\n * Called when the API key transitions from \"pending\" to a resolved value.\n * Creates the delegate exporter and flushes all buffered spans.\n */\n notifyKeyResolved(): void {\n this.flushPending();\n }\n\n async shutdown(): Promise<void> {\n const currentKey = this.getApiKey();\n if (currentKey !== API_KEY_PENDING && this.pendingBatches.length > 0) {\n this.flushPending();\n } else if (this.pendingBatches.length > 0) {\n console.warn(\n `[glasstrace] Shutdown with ${this.pendingSpanCount} buffered spans — API key never resolved, spans lost.`,\n );\n // Complete pending callbacks so pipeline doesn't hang\n for (const batch of this.pendingBatches) {\n batch.resultCallback({ code: 0 });\n }\n this.pendingBatches = [];\n this.pendingSpanCount = 0;\n }\n\n if (this.delegate) {\n return this.delegate.shutdown();\n }\n }\n\n forceFlush(): Promise<void> {\n if (this.delegate?.forceFlush) {\n return this.delegate.forceFlush();\n }\n return Promise.resolve();\n }\n\n /**\n * Enriches a ReadableSpan with all glasstrace.* attributes.\n * Returns a new ReadableSpan wrapper; the original span is not mutated.\n * Each attribute derivation is wrapped in its own try-catch for partial\n * enrichment resilience.\n */\n private enrichSpan(span: ReadableSpan): ReadableSpan {\n const attrs = span.attributes ?? {};\n const name = span.name ?? \"\";\n const extra: Record<string, string | number> = {};\n\n // glasstrace.trace.type\n try {\n extra[ATTR.TRACE_TYPE] = \"server\";\n } catch {\n // omitted\n }\n\n // glasstrace.session.id — computed at export time with the current API key\n try {\n const sessionId = this.sessionManager.getSessionId(this.getApiKey());\n extra[ATTR.SESSION_ID] = sessionId;\n } catch {\n // Session ID omitted for this span\n }\n\n // glasstrace.environment\n try {\n const env = this.environment ?? process.env.GLASSTRACE_ENV;\n if (env) {\n extra[ATTR.ENVIRONMENT] = env;\n }\n } catch {\n // omitted\n }\n\n // glasstrace.correlation.id\n try {\n const existingCid = attrs[\"glasstrace.correlation.id\"];\n if (typeof existingCid === \"string\") {\n extra[ATTR.CORRELATION_ID] = existingCid;\n }\n } catch {\n // omitted\n }\n\n // glasstrace.route\n try {\n const route =\n (attrs[\"http.route\"] as string | undefined) ?? name;\n if (route) {\n extra[ATTR.ROUTE] = route;\n }\n } catch {\n // omitted\n }\n\n // glasstrace.http.method\n try {\n const method =\n (attrs[\"http.method\"] as string | undefined) ??\n (attrs[\"http.request.method\"] as string | undefined);\n if (method) {\n extra[ATTR.HTTP_METHOD] = method;\n }\n } catch {\n // omitted\n }\n\n // glasstrace.http.status_code\n try {\n const statusCode =\n (attrs[\"http.status_code\"] as number | undefined) ??\n (attrs[\"http.response.status_code\"] as number | undefined);\n if (statusCode !== undefined) {\n extra[ATTR.HTTP_STATUS_CODE] = statusCode;\n }\n } catch {\n // omitted\n }\n\n // glasstrace.http.duration_ms\n try {\n if (span.startTime && span.endTime) {\n const [startSec, startNano] = span.startTime;\n const [endSec, endNano] = span.endTime;\n const durationMs =\n (endSec - startSec) * 1000 + (endNano - startNano) / 1_000_000;\n if (durationMs >= 0) {\n extra[ATTR.HTTP_DURATION_MS] = durationMs;\n }\n }\n } catch {\n // omitted\n }\n\n // glasstrace.error.message\n try {\n const errorMessage = attrs[\"exception.message\"] as string | undefined;\n if (errorMessage) {\n extra[ATTR.ERROR_MESSAGE] = errorMessage;\n }\n } catch {\n // omitted\n }\n\n // glasstrace.error.code + glasstrace.error.category\n try {\n const errorType = attrs[\"exception.type\"] as string | undefined;\n if (errorType) {\n extra[ATTR.ERROR_CODE] = errorType;\n extra[ATTR.ERROR_CATEGORY] = deriveErrorCategory(errorType);\n }\n } catch {\n // omitted\n }\n\n // glasstrace.error.field\n try {\n const errorField = attrs[\"error.field\"] as string | undefined;\n if (errorField) {\n extra[ATTR.ERROR_FIELD] = errorField;\n }\n } catch {\n // omitted\n }\n\n // glasstrace.orm.*\n try {\n // Support both OTel >=1.9 (instrumentationScope) and <1.9 (instrumentationLibrary)\n const spanAny = span as unknown as Record<string, { name?: string } | undefined>;\n const instrumentationName =\n (spanAny.instrumentationScope?.name ?? spanAny.instrumentationLibrary?.name) ?? \"\";\n const ormProvider = deriveOrmProvider(instrumentationName);\n if (ormProvider) {\n extra[ATTR.ORM_PROVIDER] = ormProvider;\n\n const model =\n (attrs[\"db.sql.table\"] as string | undefined) ??\n (attrs[\"db.prisma.model\"] as string | undefined);\n if (model) {\n extra[ATTR.ORM_MODEL] = model;\n }\n\n const operation = attrs[\"db.operation\"] as string | undefined;\n if (operation) {\n extra[ATTR.ORM_OPERATION] = operation;\n }\n }\n } catch {\n // omitted\n }\n\n // glasstrace.fetch.target\n try {\n const url =\n (attrs[\"http.url\"] as string | undefined) ??\n (attrs[\"url.full\"] as string | undefined);\n if (url && span.kind === SpanKind.CLIENT) {\n extra[ATTR.FETCH_TARGET] = classifyFetchTarget(url);\n }\n } catch {\n // omitted\n }\n\n return createEnrichedSpan(span, extra);\n }\n\n /**\n * Lazily creates the delegate OTLP exporter once the API key is resolved.\n */\n private ensureDelegate(): SpanExporter | null {\n if (!this.createDelegateFn) return null;\n if (this.delegate) return this.delegate;\n\n const currentKey = this.getApiKey();\n if (currentKey === API_KEY_PENDING) return null;\n\n this.delegate = this.createDelegateFn(this.endpointUrl, {\n Authorization: `Bearer ${currentKey}`,\n });\n return this.delegate;\n }\n\n /**\n * Buffers enriched spans while the API key is pending.\n * Evicts oldest batches if the buffer exceeds MAX_PENDING_SPANS.\n */\n private bufferSpans(\n spans: ReadableSpan[],\n resultCallback: (result: ExportResult) => void,\n ): void {\n this.pendingBatches.push({ spans, resultCallback });\n this.pendingSpanCount += spans.length;\n\n // Evict oldest batches if over limit\n while (this.pendingSpanCount > MAX_PENDING_SPANS && this.pendingBatches.length > 1) {\n const evicted = this.pendingBatches.shift()!;\n this.pendingSpanCount -= evicted.spans.length;\n // Complete callback so pipeline doesn't hang\n evicted.resultCallback({ code: 0 });\n\n if (!this.overflowLogged) {\n this.overflowLogged = true;\n console.warn(\n \"[glasstrace] Pending span buffer overflow — oldest spans evicted. \" +\n \"This usually means the API key is taking too long to resolve.\",\n );\n }\n }\n }\n\n /**\n * Flushes all buffered spans through the delegate exporter.\n * Called when the API key resolves.\n */\n private flushPending(): void {\n if (this.pendingBatches.length === 0) return;\n\n const exporter = this.ensureDelegate();\n if (!exporter) {\n // No delegate factory — complete callbacks and discard\n for (const batch of this.pendingBatches) {\n batch.resultCallback({ code: 0 });\n }\n this.pendingBatches = [];\n this.pendingSpanCount = 0;\n return;\n }\n\n const batches = this.pendingBatches;\n this.pendingBatches = [];\n this.pendingSpanCount = 0;\n\n for (const batch of batches) {\n exporter.export(batch.spans, batch.resultCallback);\n }\n }\n}\n\n/**\n * Creates a ReadableSpan wrapper that inherits all properties from the\n * original span but overrides `attributes` to include additional entries.\n * The original span is not mutated.\n */\nfunction createEnrichedSpan(\n span: ReadableSpan,\n extra: Record<string, string | number>,\n): ReadableSpan {\n const enrichedAttributes = { ...span.attributes, ...extra };\n return Object.create(span, {\n attributes: {\n value: enrichedAttributes,\n enumerable: true,\n },\n }) as ReadableSpan;\n}\n\n/**\n * Derives ORM provider from the instrumentation library name.\n */\nexport function deriveOrmProvider(instrumentationName: string): string | null {\n const lower = instrumentationName.toLowerCase();\n if (lower.includes(\"prisma\")) {\n return \"prisma\";\n }\n if (lower.includes(\"drizzle\")) {\n return \"drizzle\";\n }\n return null;\n}\n\n/**\n * Derives error category from error type string.\n */\nexport function deriveErrorCategory(errorType: string): string {\n const lower = errorType.toLowerCase();\n if (lower.includes(\"validation\") || lower.includes(\"zod\")) {\n return \"validation\";\n }\n if (\n lower.includes(\"network\") ||\n lower.includes(\"econnrefused\") ||\n lower.includes(\"fetch\") ||\n lower.includes(\"timeout\")\n ) {\n return \"network\";\n }\n if (lower.includes(\"auth\") || lower.includes(\"unauthorized\") || lower.includes(\"forbidden\")) {\n return \"auth\";\n }\n if (lower.includes(\"notfound\") || lower.includes(\"not_found\")) {\n return \"not-found\";\n }\n return \"internal\";\n}\n","import type { AnonApiKey, SessionId } from \"@glasstrace/protocol\";\n\n/**\n * Checks whether the given Origin header is allowed for CORS access.\n *\n * Allowed origins:\n * - `chrome-extension://*` — any Chrome extension\n * - `moz-extension://*` — any Firefox extension\n * - `safari-web-extension://*` — any Safari extension\n * - Absent origin (same-origin / non-browser request)\n *\n * Replaced wildcard `*` to prevent arbitrary websites from\n * reading the anonymous API key from localhost.\n */\nfunction isAllowedOrigin(origin: string | null): boolean {\n if (origin === null) return true;\n if (origin.startsWith(\"chrome-extension://\")) return true;\n if (origin.startsWith(\"moz-extension://\")) return true;\n if (origin.startsWith(\"safari-web-extension://\")) return true;\n return false;\n}\n\n/**\n * Builds CORS headers for a given request origin.\n * Returns headers with `Access-Control-Allow-Origin` set to the origin\n * if allowed, otherwise omits that header entirely.\n */\nfunction buildCorsHeaders(origin: string | null): Record<string, string> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Vary: \"Origin\",\n };\n\n if (origin && isAllowedOrigin(origin)) {\n headers[\"Access-Control-Allow-Origin\"] = origin;\n }\n\n return headers;\n}\n\n/**\n * Creates a request handler for the `/__glasstrace/config` discovery endpoint.\n *\n * The returned handler checks if the request URL path is `/__glasstrace/config`.\n * If not, returns `null` (pass-through). If it matches, returns a `DiscoveryResponse`\n * with the anonymous key and current session ID.\n *\n * The triple guard (anonymous + dev + active) is enforced by the caller,\n * not by this module. If the handler is registered, it serves.\n */\nexport function createDiscoveryHandler(\n getAnonKey: () => Promise<AnonApiKey | null>,\n getSessionId: () => SessionId,\n): (request: Request) => Promise<Response | null> {\n return async (request: Request): Promise<Response | null> => {\n // Check path match\n let url: URL;\n try {\n url = new URL(request.url);\n } catch {\n return null;\n }\n\n if (url.pathname !== \"/__glasstrace/config\") {\n return null;\n }\n\n // Restrict CORS to known extension origins instead of wildcard\n const origin = request.headers.get(\"Origin\");\n const corsHeaders = buildCorsHeaders(origin);\n\n // Handle CORS preflight\n if (request.method === \"OPTIONS\") {\n return new Response(null, {\n status: 204,\n headers: {\n ...corsHeaders,\n \"Access-Control-Allow-Methods\": \"GET, OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type\",\n },\n });\n }\n\n // Only allow GET requests\n if (request.method !== \"GET\") {\n return new Response(\n JSON.stringify({ error: \"method_not_allowed\" }),\n {\n status: 405,\n headers: corsHeaders,\n },\n );\n }\n\n try {\n // Get the anonymous key\n const anonKey = await getAnonKey();\n\n if (anonKey === null) {\n return new Response(\n JSON.stringify({ error: \"not_ready\" }),\n {\n status: 503,\n headers: corsHeaders,\n },\n );\n }\n\n // Get the current session ID\n const sessionId = getSessionId();\n\n return new Response(\n JSON.stringify({ key: anonKey, sessionId }),\n {\n status: 200,\n headers: corsHeaders,\n },\n );\n } catch {\n return new Response(\n JSON.stringify({ error: \"internal_error\" }),\n {\n status: 500,\n headers: corsHeaders,\n },\n );\n }\n };\n}\n\n// Exported for testing\nexport { isAllowedOrigin, buildCorsHeaders };\n","import type { ResolvedConfig } from \"./env-detection.js\";\nimport type { SessionManager } from \"./session.js\";\nimport type { SpanExporter } from \"@opentelemetry/sdk-trace-base\";\nimport { GlasstraceExporter, API_KEY_PENDING } from \"./enriching-exporter.js\";\nimport { getActiveConfig } from \"./init-client.js\";\n\n/** Module-level resolved API key, updated when the anon key resolves. */\nlet _resolvedApiKey: string = API_KEY_PENDING;\n\n/** Module-level reference to the active exporter for key-resolution notification. */\nlet _activeExporter: GlasstraceExporter | null = null;\n\n/**\n * Sets the resolved API key for OTel export authentication.\n * Called once the anonymous key or dev key is available.\n * @param key - The resolved API key (anonymous or developer).\n */\nexport function setResolvedApiKey(key: string): void {\n _resolvedApiKey = key;\n}\n\n/**\n * Returns the current resolved API key.\n * Returns the {@link API_KEY_PENDING} sentinel if the key has not yet resolved.\n */\nexport function getResolvedApiKey(): string {\n return _resolvedApiKey;\n}\n\n/**\n * Notifies the active exporter that the API key has transitioned from\n * \"pending\" to a resolved value. This triggers flushing of any buffered spans.\n */\nexport function notifyApiKeyResolved(): void {\n _activeExporter?.notifyKeyResolved();\n}\n\n/**\n * Resets OTel configuration state to initial values. For testing only.\n */\nexport function resetOtelConfigForTesting(): void {\n _resolvedApiKey = API_KEY_PENDING;\n _activeExporter = null;\n}\n\n/**\n * Dynamically imports an optional peer dependency at runtime.\n *\n * Uses `Function()` constructor to build the `import()` expression so that\n * bundlers (webpack, esbuild, turbopack) do not attempt to resolve the module\n * at compile time. This is intentional -- the SDK's peer dependencies are\n * optional, and static analysis would cause missing-module build errors for\n * users who have not installed them.\n *\n * @param moduleId - The npm package name to import (e.g. \"@vercel/otel\").\n * @returns The module namespace object, or `null` if the module is not installed.\n */\nasync function tryImport(moduleId: string): Promise<Record<string, unknown> | null> {\n try {\n return await (Function(\"id\", \"return import(id)\")(moduleId) as Promise<Record<string, unknown>>);\n } catch {\n return null;\n }\n}\n\n/**\n * Configures OpenTelemetry with the GlasstraceExporter.\n * The exporter handles all span enrichment (glasstrace.* attributes) at\n * export time, solving buffering, no-onEnding,\n * and session-ID-uses-resolved-key concerns.\n *\n * Attempts to use `@vercel/otel` first, falls back to bare OTel SDK.\n *\n * @param config - The resolved SDK configuration (endpoint, environment, etc.).\n * @param sessionManager - Provides session IDs for span enrichment.\n */\nexport async function configureOtel(\n config: ResolvedConfig,\n sessionManager: SessionManager,\n): Promise<void> {\n // Build OTLP exporter configuration\n const exporterUrl = `${config.endpoint}/v1/traces`;\n\n // Build the exporter factory from the optional OTLP peer dependency\n let createOtlpExporter: ((url: string, headers: Record<string, string>) => SpanExporter) | null = null;\n const otlpModule = await tryImport(\"@opentelemetry/exporter-trace-otlp-http\");\n if (otlpModule && typeof otlpModule.OTLPTraceExporter === \"function\") {\n const OTLPTraceExporter = otlpModule.OTLPTraceExporter as new (opts: {\n url: string;\n headers: Record<string, string>;\n }) => SpanExporter;\n createOtlpExporter = (url: string, headers: Record<string, string>) =>\n new OTLPTraceExporter({ url, headers });\n }\n\n // Create the GlasstraceExporter that enriches + buffers + delegates\n const glasstraceExporter = new GlasstraceExporter({\n getApiKey: getResolvedApiKey,\n sessionManager,\n getConfig: () => getActiveConfig(),\n environment: config.environment,\n endpointUrl: exporterUrl,\n createDelegate: createOtlpExporter,\n });\n _activeExporter = glasstraceExporter;\n\n // Try @vercel/otel first\n const vercelOtel = await tryImport(\"@vercel/otel\");\n if (vercelOtel && typeof vercelOtel.registerOTel === \"function\") {\n if (!createOtlpExporter) {\n console.warn(\n \"[glasstrace] @opentelemetry/exporter-trace-otlp-http not found for @vercel/otel path. Trace export disabled.\",\n );\n }\n\n const otelConfig: Record<string, unknown> = {\n serviceName: \"glasstrace-sdk\",\n traceExporter: glasstraceExporter,\n };\n\n // Try @prisma/instrumentation\n const prismaModule = await tryImport(\"@prisma/instrumentation\");\n if (prismaModule) {\n const PrismaInstrumentation = prismaModule.PrismaInstrumentation as\n (new () => unknown) | undefined;\n if (PrismaInstrumentation) {\n otelConfig.instrumentations = [new PrismaInstrumentation()];\n }\n }\n\n (vercelOtel.registerOTel as (opts: Record<string, unknown>) => void)(otelConfig);\n return;\n }\n\n // Fallback: bare OTel SDK with BasicTracerProvider\n try {\n const otelSdk = await import(\"@opentelemetry/sdk-trace-base\");\n const otelApi = await import(\"@opentelemetry/api\");\n\n if (!createOtlpExporter) {\n // No OTLP exporter available -- rebuild GlasstraceExporter with a\n // ConsoleSpanExporter delegate so spans still get glasstrace.* enrichment.\n const consoleExporter = new otelSdk.ConsoleSpanExporter();\n const consoleGlasstraceExporter = new GlasstraceExporter({\n getApiKey: getResolvedApiKey,\n sessionManager,\n getConfig: () => getActiveConfig(),\n environment: config.environment,\n endpointUrl: exporterUrl,\n createDelegate: () => consoleExporter,\n });\n _activeExporter = consoleGlasstraceExporter;\n\n console.warn(\n \"[glasstrace] @opentelemetry/exporter-trace-otlp-http not found. Using ConsoleSpanExporter.\",\n );\n\n const processor = new otelSdk.SimpleSpanProcessor(consoleGlasstraceExporter);\n const provider = new otelSdk.BasicTracerProvider({\n spanProcessors: [processor],\n });\n otelApi.trace.setGlobalTracerProvider(provider);\n return;\n }\n\n const processor = new otelSdk.SimpleSpanProcessor(glasstraceExporter);\n const provider = new otelSdk.BasicTracerProvider({\n spanProcessors: [processor],\n });\n\n // Warn if another OTel provider is already registered to avoid\n // silently overwriting existing tracing (e.g., Datadog, New Relic).\n const existingProvider = otelApi.trace.getTracerProvider();\n if (existingProvider && existingProvider.constructor.name !== \"ProxyTracerProvider\") {\n console.warn(\n \"[glasstrace] An existing OpenTelemetry TracerProvider was detected and will be replaced. \" +\n \"If you use another tracing tool, configure Glasstrace as an additional exporter instead.\",\n );\n }\n\n otelApi.trace.setGlobalTracerProvider(provider);\n } catch {\n console.warn(\n \"[glasstrace] Neither @vercel/otel nor @opentelemetry/sdk-trace-base available. Tracing disabled.\",\n );\n }\n}\n","declare const __SDK_VERSION__: string;\n\nimport type { GlasstraceOptions, AnonApiKey } from \"@glasstrace/protocol\";\nimport { resolveConfig, isProductionDisabled, isAnonymousMode } from \"./env-detection.js\";\nimport type { ResolvedConfig } from \"./env-detection.js\";\nimport { SessionManager } from \"./session.js\";\nimport { getOrCreateAnonKey, readAnonKey } from \"./anon-key.js\";\nimport { loadCachedConfig, performInit, _setCurrentConfig } from \"./init-client.js\";\nimport { createDiscoveryHandler } from \"./discovery-endpoint.js\";\nimport { configureOtel, setResolvedApiKey, getResolvedApiKey, notifyApiKeyResolved, resetOtelConfigForTesting } from \"./otel-config.js\";\n\n/** Module-level state tracking for the registered discovery handler. */\nlet discoveryHandler: ((request: Request) => Promise<Response | null>) | null = null;\n\n/** Module-level flag to prevent double registration. */\nlet isRegistered = false;\n\n/** Generation counter to invalidate stale background promises after reset. */\nlet registrationGeneration = 0;\n\n/**\n * The primary SDK entry point called by developers in their `instrumentation.ts`.\n * Orchestrates OTel setup, span processor, init client, anon key, and discovery endpoint.\n *\n * This function is synchronous and MUST NOT throw. The developer's server is never blocked.\n * Background work (key resolution, init call) happens via fire-and-forget promises.\n *\n * @param options - Optional SDK configuration. Environment variables are used as fallbacks.\n *\n * @example\n * ```ts\n * // instrumentation.ts\n * import { registerGlasstrace } from \"@glasstrace/sdk\";\n * registerGlasstrace(); // uses env vars\n * ```\n */\nexport function registerGlasstrace(options?: GlasstraceOptions): void {\n try {\n // Prevent double registration\n if (isRegistered) {\n return;\n }\n\n // Step 1: Resolve config\n const config = resolveConfig(options);\n if (config.verbose) {\n console.info(\"[glasstrace] Step 1: Config resolved.\");\n }\n\n // Step 2: Production check\n if (isProductionDisabled(config)) {\n console.warn(\n \"[glasstrace] Disabled in production. Set GLASSTRACE_FORCE_ENABLE=true to override.\",\n );\n return;\n }\n if (config.verbose) {\n console.info(\"[glasstrace] Step 2: Not production-disabled.\");\n }\n\n // Step 3: Determine auth mode\n const anonymous = isAnonymousMode(config);\n let effectiveKey: string | undefined = config.apiKey;\n\n if (effectiveKey) {\n setResolvedApiKey(effectiveKey);\n }\n\n if (config.verbose) {\n console.info(\n `[glasstrace] Step 3: Auth mode = ${anonymous ? \"anonymous\" : \"dev-key\"}.`,\n );\n }\n\n // Step 4: Load cached config and apply to in-memory store\n const cachedInitResponse = loadCachedConfig();\n if (cachedInitResponse) {\n _setCurrentConfig(cachedInitResponse);\n }\n if (config.verbose) {\n console.info(\n `[glasstrace] Step 4: Cached config ${cachedInitResponse ? \"loaded and applied\" : \"not found\"}.`,\n );\n }\n\n // Step 5: Create SessionManager\n const sessionManager = new SessionManager();\n if (config.verbose) {\n console.info(\"[glasstrace] Step 5: SessionManager created.\");\n }\n\n isRegistered = true;\n const currentGeneration = registrationGeneration;\n\n // Step 6: Configure OTel IMMEDIATELY in all modes.\n // OTel is registered before the anon key resolves so that\n // spans are captured from cold start. GlasstraceExporter buffers spans\n // while the key is \"pending\" and flushes them once notifyApiKeyResolved()\n // is called after anonymous key resolution.\n // This is fire-and-forget -- OTel failure must not block init.\n void configureOtel(config, sessionManager).then(\n () => {\n if (config.verbose) {\n console.info(\"[glasstrace] Step 6: OTel configured.\");\n }\n },\n (err: unknown) => {\n console.warn(\n `[glasstrace] Failed to configure OTel: ${err instanceof Error ? err.message : String(err)}`,\n );\n },\n );\n\n // Step 7 + 8 + anonymous key resolution -- all in background\n if (anonymous) {\n // Step 8: Register discovery endpoint IMMEDIATELY with async key resolution\n if (isDiscoveryEnabled(config)) {\n let resolvedAnonKey: AnonApiKey | null = null;\n const anonKeyPromise = getOrCreateAnonKey();\n\n // Use getResolvedApiKey() for session ID instead of\n // capturing the mutable effectiveKey closure variable.\n discoveryHandler = createDiscoveryHandler(\n async () => resolvedAnonKey,\n () => sessionManager.getSessionId(getResolvedApiKey()),\n );\n\n if (config.verbose) {\n console.info(\"[glasstrace] Step 8: Discovery endpoint registered (key pending).\");\n }\n\n // Background: resolve key, update API key, then init\n void (async () => {\n try {\n if (currentGeneration !== registrationGeneration) return;\n\n const anonKey = await anonKeyPromise;\n resolvedAnonKey = anonKey;\n setResolvedApiKey(anonKey);\n notifyApiKeyResolved();\n effectiveKey = anonKey;\n\n if (currentGeneration !== registrationGeneration) return;\n\n // Update the discovery handler to serve the resolved key\n // Use getResolvedApiKey() for canonical key state\n discoveryHandler = createDiscoveryHandler(\n () => Promise.resolve(anonKey),\n () => sessionManager.getSessionId(getResolvedApiKey()),\n );\n\n if (config.verbose) {\n console.info(\"[glasstrace] Step 7: Background init firing.\");\n }\n\n await performInit(config, anonKey, __SDK_VERSION__);\n } catch (err) {\n console.warn(\n `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n })();\n } else {\n // Anonymous + non-dev: no discovery endpoint, just background init\n void (async () => {\n try {\n if (currentGeneration !== registrationGeneration) return;\n\n const anonKey = await getOrCreateAnonKey();\n setResolvedApiKey(anonKey);\n notifyApiKeyResolved();\n effectiveKey = anonKey;\n\n if (currentGeneration !== registrationGeneration) return;\n\n if (config.verbose) {\n console.info(\"[glasstrace] Step 7: Background init firing.\");\n }\n\n await performInit(config, anonKey, __SDK_VERSION__);\n } catch (err) {\n console.warn(\n `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n })();\n }\n } else {\n // Dev key mode: read straggler anon key, then fire init\n void (async () => {\n try {\n if (currentGeneration !== registrationGeneration) return;\n\n let anonKeyForInit: AnonApiKey | null = null;\n try {\n anonKeyForInit = await readAnonKey();\n } catch {\n // Expected when no prior anon key file exists on disk (first run).\n }\n\n if (currentGeneration !== registrationGeneration) return;\n\n if (config.verbose) {\n console.info(\"[glasstrace] Step 7: Background init firing.\");\n }\n await performInit(config, anonKeyForInit, __SDK_VERSION__);\n } catch (err) {\n console.warn(\n `[glasstrace] Background init failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n })();\n }\n\n // Step 9: Import graph (coverageMapEnabled) -- placeholder\n if (config.coverageMapEnabled && config.verbose) {\n console.info(\"[glasstrace] Step 9: Import graph building skipped.\");\n }\n } catch (err) {\n console.warn(\n `[glasstrace] Registration failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\n/**\n * Returns the registered discovery handler, or null if not registered.\n */\nexport function getDiscoveryHandler(): ((request: Request) => Promise<Response | null>) | null {\n return discoveryHandler;\n}\n\n/**\n * Returns `true` if the discovery endpoint should be enabled for this environment.\n *\n * Tightened from \"not production\" to explicit development conditions:\n * 1. `GLASSTRACE_DISCOVERY_ENABLED=true` -- explicit override (highest priority)\n * 2. `GLASSTRACE_DISCOVERY_ENABLED=false` -- explicit disable (highest priority)\n * 3. `NODE_ENV` is `\"development\"` or unset, AND `VERCEL_ENV` is not `\"production\"`\n *\n * Environments like `staging`, `test`, and `ci` do not expose the discovery\n * endpoint by default, since it serves the anonymous API key over CORS.\n *\n * @param config - The resolved SDK configuration.\n * @returns Whether the discovery endpoint should be registered.\n */\nfunction isDiscoveryEnabled(config: ResolvedConfig): boolean {\n // Explicit flag takes precedence\n if (process.env.GLASSTRACE_DISCOVERY_ENABLED === \"true\") return true;\n if (process.env.GLASSTRACE_DISCOVERY_ENABLED === \"false\") return false;\n\n // Block production environments\n if (config.nodeEnv === \"production\") return false;\n if (config.vercelEnv === \"production\") return false;\n\n // Only allow in development or when NODE_ENV is unset\n if (config.nodeEnv === \"development\" || config.nodeEnv === undefined) return true;\n\n return false;\n}\n\n/**\n * Resets registration state. For testing only.\n */\nexport function _resetRegistrationForTesting(): void {\n isRegistered = false;\n discoveryHandler = null;\n registrationGeneration++;\n resetOtelConfigForTesting();\n}\n","import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport { execSync } from \"node:child_process\";\nimport {\n SourceMapUploadResponseSchema,\n type SourceMapUploadResponse,\n} from \"@glasstrace/protocol\";\n\nexport interface SourceMapEntry {\n filePath: string;\n content: string;\n}\n\n/**\n * Recursively finds all .map files in the given build directory.\n * Returns relative paths and file contents.\n */\nexport async function collectSourceMaps(\n buildDir: string,\n): Promise<SourceMapEntry[]> {\n const results: SourceMapEntry[] = [];\n\n try {\n await walkDir(buildDir, buildDir, results);\n } catch {\n // Directory doesn't exist or is unreadable — return empty\n return [];\n }\n\n return results;\n}\n\nasync function walkDir(\n baseDir: string,\n currentDir: string,\n results: SourceMapEntry[],\n): Promise<void> {\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = await fs.readdir(currentDir, { withFileTypes: true });\n } catch {\n return;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(currentDir, entry.name);\n\n if (entry.isDirectory()) {\n await walkDir(baseDir, fullPath, results);\n } else if (entry.isFile() && entry.name.endsWith(\".map\")) {\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n const relativePath = path.relative(baseDir, fullPath).replace(/\\\\/g, \"/\");\n // Strip the trailing .map extension so the key matches the compiled\n // JS path that the runtime uses for stack-frame lookups (e.g.\n // \"static/chunks/main.js\" instead of \"static/chunks/main.js.map\").\n const compiledPath = relativePath.replace(/\\.map$/, \"\");\n results.push({ filePath: compiledPath, content });\n } catch {\n // Skip unreadable files\n }\n }\n }\n}\n\n/**\n * Computes a build hash for source map uploads.\n *\n * First tries `git rev-parse HEAD` to get the git commit SHA.\n * On failure, falls back to a deterministic content hash:\n * sort source map file paths alphabetically, concatenate each as\n * `{relativePath}\\n{fileLength}\\n{fileContent}`, then SHA-256 the result.\n */\nexport async function computeBuildHash(\n maps?: SourceMapEntry[],\n): Promise<string> {\n // Try git first\n try {\n const sha = execSync(\"git rev-parse HEAD\", { encoding: \"utf-8\" }).trim();\n if (sha) {\n return sha;\n }\n } catch {\n // Git not available, fall through to content hash\n }\n\n // Fallback: content-based hash\n const sortedMaps = [...(maps ?? [])].sort((a, b) =>\n a.filePath.localeCompare(b.filePath),\n );\n\n const hashInput = sortedMaps\n .map((m) => `${m.filePath}\\n${m.content.length}\\n${m.content}`)\n .join(\"\");\n\n const hash = crypto.createHash(\"sha256\").update(hashInput).digest(\"hex\");\n return hash;\n}\n\n/**\n * Uploads source maps to the ingestion API.\n *\n * POSTs to `{endpoint}/v1/source-maps` with the API key, build hash,\n * and file entries. Validates the response against SourceMapUploadResponseSchema.\n */\nexport async function uploadSourceMaps(\n apiKey: string,\n endpoint: string,\n buildHash: string,\n maps: SourceMapEntry[],\n): Promise<SourceMapUploadResponse> {\n const body = {\n apiKey,\n buildHash,\n files: maps.map((m) => ({\n filePath: m.filePath,\n sourceMap: m.content,\n })),\n };\n\n const baseUrl = endpoint.replace(/\\/+$/, \"\");\n const response = await fetch(`${baseUrl}/v1/source-maps`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n throw new Error(\n `Source map upload failed: ${String(response.status)} ${response.statusText}`,\n );\n }\n\n const json: unknown = await response.json();\n return SourceMapUploadResponseSchema.parse(json);\n}\n","import { collectSourceMaps, computeBuildHash, uploadSourceMaps } from \"./source-map-uploader.js\";\n\ntype NextConfig = Record<string, unknown>;\ntype WebpackConfigFn = (config: Record<string, unknown>, context: Record<string, unknown>) => Record<string, unknown>;\n\n/** Subset of the webpack context object passed by Next.js to the webpack config function. */\ninterface WebpackContext {\n isServer: boolean;\n dev: boolean;\n [key: string]: unknown;\n}\n\n/** Minimal webpack compiler shape for the afterEmit hook. */\ninterface WebpackCompiler {\n hooks?: {\n afterEmit?: {\n tapPromise?: (name: string, fn: (compilation: unknown) => Promise<void>) => void;\n };\n };\n}\n\n/**\n * Wraps the developer's Next.js config to enable source map generation\n * and upload .map files to the ingestion API at build time.\n *\n * The build NEVER fails because of Glasstrace — all errors are caught\n * and logged as warnings.\n *\n * @param nextConfig - The developer's existing Next.js configuration object.\n * @returns A new config object with source map generation and upload enabled.\n */\nexport function withGlasstraceConfig(nextConfig: NextConfig): NextConfig {\n // Handle null/undefined gracefully\n const config: NextConfig = nextConfig != null ? { ...nextConfig } : {};\n\n // Enable server-side source map generation for Glasstrace uploads.\n // Intentionally does NOT set productionBrowserSourceMaps — that exposes\n // full source code publicly via browser DevTools. Users who want public\n // browser source maps can set it explicitly in their Next.js config.\n\n // Enable server-side source maps (Next.js experimental feature)\n const existingExperimental = (config.experimental as Record<string, unknown>) ?? {};\n config.experimental = { ...existingExperimental, serverSourceMaps: true };\n\n // Capture distDir for source map collection (default: .next)\n const distDir = typeof config.distDir === \"string\" ? config.distDir : \".next\";\n\n // Capture existing webpack config if any\n const existingWebpack = config.webpack as WebpackConfigFn | undefined;\n\n // Register webpack config modifier\n config.webpack = (\n webpackConfig: Record<string, unknown>,\n context: Record<string, unknown>,\n ): Record<string, unknown> => {\n // Call existing webpack config first\n let result = webpackConfig;\n if (typeof existingWebpack === \"function\") {\n result = existingWebpack(webpackConfig, context);\n }\n\n const webpackContext = context as WebpackContext;\n\n // Only run source map upload on client-side production builds (not server, not dev)\n if (!webpackContext.isServer && webpackContext.dev === false) {\n // Register a plugin to collect and upload source maps after compilation\n const plugins = (result.plugins as Array<Record<string, unknown>>) ?? [];\n plugins.push({\n apply(compiler: Record<string, unknown>) {\n const typedCompiler = compiler as WebpackCompiler;\n if (typedCompiler.hooks?.afterEmit?.tapPromise) {\n typedCompiler.hooks.afterEmit.tapPromise(\n \"GlasstraceSourceMapUpload\",\n async () => {\n await handleSourceMapUpload(distDir);\n },\n );\n }\n },\n });\n result.plugins = plugins;\n }\n\n return result;\n };\n\n return config;\n}\n\n/**\n * Collects source map files from the build output directory and uploads\n * them to the Glasstrace ingestion API. Never throws — all errors are\n * caught and logged as warnings so the build is never blocked.\n *\n * Exported for testing only; not part of the public API.\n *\n * @param distDir - The Next.js build output directory (e.g. \".next\").\n */\nexport async function handleSourceMapUpload(distDir: string): Promise<void> {\n try {\n const apiKey = process.env.GLASSTRACE_API_KEY;\n const endpoint =\n process.env.GLASSTRACE_ENDPOINT ?? \"https://api.glasstrace.dev\";\n\n // Anonymous mode: skip upload\n if (!apiKey || apiKey.trim() === \"\") {\n console.info(\n \"[glasstrace] Source map upload skipped (no API key). Stack traces will show compiled locations.\",\n );\n return;\n }\n\n const maps = await collectSourceMaps(distDir);\n\n if (maps.length === 0) {\n console.info(\"[glasstrace] No source map files found. Skipping upload.\");\n return;\n }\n\n const buildHash = await computeBuildHash(maps);\n\n await uploadSourceMaps(apiKey, endpoint, buildHash, maps);\n console.info(\n `[glasstrace] Uploaded ${String(maps.length)} source map(s) for build ${buildHash}.`,\n );\n } catch (error: unknown) {\n // Build must NEVER fail because of Glasstrace\n const message =\n error instanceof Error ? error.message : \"Unknown error\";\n console.warn(\n `[glasstrace] Source map upload failed: ${message}. Build continues normally.`,\n );\n }\n}\n","import * as fs from \"node:fs/promises\";\nimport * as fsSync from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport { createBuildHash, type ImportGraphPayload } from \"@glasstrace/protocol\";\n\n/** Maximum number of test files to process to prevent runaway in large projects */\nconst MAX_TEST_FILES = 5000;\n\n/** Directories to exclude from test file discovery */\nconst EXCLUDED_DIRS = new Set([\"node_modules\", \".next\", \".git\", \"dist\", \".turbo\"]);\n\n/** Conventional test file patterns */\nconst DEFAULT_TEST_PATTERNS = [\n /\\.test\\.tsx?$/,\n /\\.spec\\.tsx?$/,\n];\n\n/**\n * Converts a glob pattern (e.g. \"e2e/**\\/*.ts\") to an anchored RegExp.\n * Uses a placeholder to avoid `*` replacement corrupting the `**\\/` output.\n *\n * @param glob - A file glob pattern such as \"src/**\\/*.test.ts\".\n * @returns A RegExp that matches paths against the glob from start to end.\n */\nfunction globToRegExp(glob: string): RegExp {\n const DOUBLE_STAR_PLACEHOLDER = \"\\0DSTAR\\0\";\n const regexStr = glob\n .replace(/\\*\\*\\//g, DOUBLE_STAR_PLACEHOLDER) // protect **/ first\n .replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\") // escape all regex metacharacters (except *)\n .replace(/\\*/g, \"[^/]+\")\n .replace(new RegExp(DOUBLE_STAR_PLACEHOLDER.replace(/\\0/g, \"\\\\0\"), \"g\"), \"(?:.+/)?\");\n return new RegExp(\"^\" + regexStr + \"$\");\n}\n\n/**\n * Attempts to read include patterns from vitest.config.*, vite.config.*,\n * or jest.config.* files. Returns additional RegExp patterns extracted\n * from the config, or an empty array if no config is found or parsing fails.\n * This is best-effort — it reads the config as text and extracts patterns\n * via regex, without evaluating the JS.\n *\n * For Vitest/Vite configs, looks for `test.include` arrays.\n * For Jest configs, looks for `testMatch` arrays.\n * Does not support `testRegex` (string-based Jest pattern) — that is\n * left as future work.\n */\nfunction loadCustomTestPatterns(projectRoot: string): RegExp[] {\n const configNames = [\n \"vitest.config.ts\",\n \"vitest.config.js\",\n \"vitest.config.mts\",\n \"vitest.config.mjs\",\n \"vite.config.ts\",\n \"vite.config.js\",\n \"vite.config.mts\",\n \"vite.config.mjs\",\n \"jest.config.ts\",\n \"jest.config.js\",\n \"jest.config.mts\",\n \"jest.config.mjs\",\n ];\n\n for (const name of configNames) {\n const configPath = path.join(projectRoot, name);\n let content: string;\n try {\n content = fsSync.readFileSync(configPath, \"utf-8\");\n } catch {\n // Config file does not exist at this path — try next candidate\n continue;\n }\n\n try {\n const isJest = name.startsWith(\"jest.\");\n let includeMatch: RegExpExecArray | null = null;\n\n if (isJest) {\n // Jest: look for testMatch: [...]\n includeMatch = /testMatch\\s*:\\s*\\[([^\\]]*)\\]/s.exec(content);\n } else {\n // Vitest/Vite: look for `test` block's `include` to avoid\n // matching `coverage.include` or other unrelated arrays.\n // Strategy: find `test` property, then look for `include` within\n // the next ~500 chars (heuristic to stay within the test block).\n const testBlockMatch = /\\btest\\s*[:{]\\s*/s.exec(content);\n if (testBlockMatch) {\n const afterTest = content.slice(testBlockMatch.index, testBlockMatch.index + 500);\n includeMatch = /include\\s*:\\s*\\[([^\\]]*)\\]/s.exec(afterTest);\n }\n }\n\n if (!includeMatch) {\n continue;\n }\n\n const arrayContent = includeMatch[1];\n const stringRegex = /['\"]([^'\"]+)['\"]/g;\n const patterns: RegExp[] = [];\n let match: RegExpExecArray | null;\n match = stringRegex.exec(arrayContent);\n while (match !== null) {\n patterns.push(globToRegExp(match[1]));\n match = stringRegex.exec(arrayContent);\n }\n\n if (patterns.length > 0) {\n return patterns;\n }\n } catch {\n // Regex-based config parsing failed — fall through to next config file\n continue;\n }\n }\n\n return [];\n}\n\n/**\n * Discovers test files by scanning the project directory for conventional\n * test file patterns. Also reads vitest/jest config files for custom include\n * patterns and merges them with the defaults. Excludes node_modules/ and .next/.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns Relative POSIX paths from projectRoot, capped at {@link MAX_TEST_FILES}.\n */\nexport async function discoverTestFiles(\n projectRoot: string,\n): Promise<string[]> {\n const customPatterns = loadCustomTestPatterns(projectRoot);\n const testPatterns = [...DEFAULT_TEST_PATTERNS, ...customPatterns];\n const results: string[] = [];\n\n try {\n await walkForTests(projectRoot, projectRoot, results, testPatterns);\n } catch {\n // Project root directory does not exist or is unreadable — return empty\n return [];\n }\n\n return results.slice(0, MAX_TEST_FILES);\n}\n\n/** Recursively walks directories, collecting test file paths into `results`. */\nasync function walkForTests(\n baseDir: string,\n currentDir: string,\n results: string[],\n testPatterns: RegExp[],\n): Promise<void> {\n if (results.length >= MAX_TEST_FILES) {\n return;\n }\n\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = await fs.readdir(currentDir, { withFileTypes: true });\n } catch {\n // Directory is unreadable (permissions, broken symlink) — skip subtree\n return;\n }\n\n for (const entry of entries) {\n if (results.length >= MAX_TEST_FILES) {\n return;\n }\n\n const fullPath = path.join(currentDir, entry.name);\n\n if (entry.isDirectory()) {\n if (EXCLUDED_DIRS.has(entry.name)) {\n continue;\n }\n await walkForTests(baseDir, fullPath, results, testPatterns);\n } else if (entry.isFile()) {\n const relativePath = path.relative(baseDir, fullPath).replace(/\\\\/g, \"/\");\n\n // Check if it matches test patterns or is in __tests__\n const isTestFile =\n testPatterns.some((p) => p.test(entry.name) || p.test(relativePath)) ||\n relativePath.includes(\"__tests__\");\n\n if (isTestFile && (entry.name.endsWith(\".ts\") || entry.name.endsWith(\".tsx\"))) {\n results.push(relativePath);\n }\n }\n }\n}\n\n/**\n * Extracts import paths from file content using regex.\n * Handles ES module imports, CommonJS requires, and dynamic imports.\n *\n * @param fileContent - The full text content of a TypeScript/JavaScript file.\n * @returns An array of import path strings as written in the source (e.g. \"./foo\", \"react\").\n */\nexport function extractImports(fileContent: string): string[] {\n const seen = new Set<string>();\n const imports: string[] = [];\n\n /** Adds a path to the result if not already present. */\n const addUnique = (importPath: string): void => {\n if (!seen.has(importPath)) {\n seen.add(importPath);\n imports.push(importPath);\n }\n };\n\n // ES module: import ... from 'path' or import 'path'\n const esImportRegex = /import\\s+(?:(?:[\\w*{}\\s,]+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\n let match: RegExpExecArray | null;\n\n match = esImportRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = esImportRegex.exec(fileContent);\n }\n\n // CommonJS: require('path')\n const requireRegex = /require\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n match = requireRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = requireRegex.exec(fileContent);\n }\n\n // Dynamic import: import('path')\n const dynamicImportRegex = /import\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g;\n match = dynamicImportRegex.exec(fileContent);\n while (match !== null) {\n addUnique(match[1]);\n match = dynamicImportRegex.exec(fileContent);\n }\n\n return imports;\n}\n\n/**\n * Builds an import graph mapping test file paths to their imported module paths.\n *\n * Discovers test files, reads each, extracts imports, and builds a graph.\n * Computes a deterministic buildHash from the serialized graph content.\n * Individual file read failures are silently skipped.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @returns An {@link ImportGraphPayload} containing the graph and a deterministic buildHash.\n */\nexport async function buildImportGraph(\n projectRoot: string,\n): Promise<ImportGraphPayload> {\n const testFiles = await discoverTestFiles(projectRoot);\n const graph: Record<string, string[]> = {};\n\n for (const testFile of testFiles) {\n const fullPath = path.join(projectRoot, testFile);\n try {\n const content = await fs.readFile(fullPath, \"utf-8\");\n const imports = extractImports(content);\n graph[testFile] = imports;\n } catch {\n // File is unreadable (permissions, deleted between discovery and read) — skip\n continue;\n }\n }\n\n // Compute deterministic build hash from graph content\n const sortedKeys = Object.keys(graph).sort();\n const serialized = sortedKeys\n .map((key) => `${key}:${JSON.stringify(graph[key])}`)\n .join(\"\\n\");\n const hashHex = crypto\n .createHash(\"sha256\")\n .update(serialized)\n .digest(\"hex\");\n const buildHash = createBuildHash(hashHex);\n\n return { buildHash, graph };\n}\n"],"mappings":"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","names":["import_protocol","import_promises","import_node_path","import_protocol","GLASSTRACE_DIR","import_protocol","processor","provider","import_protocol","fs","path","crypto","import_protocol"]}
@@ -0,0 +1,357 @@
1
+ import { SdkDiagnosticCode, GlasstraceEnvVars, GlasstraceOptions, SessionId, AnonApiKey, CaptureConfig, SdkInitResponse, ImportGraphPayload, SdkHealthReport, SourceMapUploadResponse } from '@glasstrace/protocol';
2
+ import { Context } from '@opentelemetry/api';
3
+ import { SpanProcessor, Span, ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
4
+ import { ExportResult } from '@opentelemetry/core';
5
+
6
+ /**
7
+ * Internal SDK error class with a typed diagnostic code.
8
+ * Caught at the boundary and converted to a log message + diagnostic entry.
9
+ * Never thrown to the developer.
10
+ */
11
+ declare class SdkError extends Error {
12
+ readonly code: SdkDiagnosticCode;
13
+ constructor(code: SdkDiagnosticCode, message: string, cause?: Error);
14
+ }
15
+
16
+ /**
17
+ * Resolved configuration after merging explicit options with environment variables.
18
+ */
19
+ interface ResolvedConfig {
20
+ apiKey: string | undefined;
21
+ endpoint: string;
22
+ forceEnable: boolean;
23
+ verbose: boolean;
24
+ environment: string | undefined;
25
+ coverageMapEnabled: boolean;
26
+ nodeEnv: string | undefined;
27
+ vercelEnv: string | undefined;
28
+ }
29
+ /**
30
+ * Reads all recognized Glasstrace environment variables from process.env.
31
+ * Returns undefined for any variable not set. Never throws.
32
+ */
33
+ declare function readEnvVars(): GlasstraceEnvVars;
34
+ /**
35
+ * Merges explicit GlasstraceOptions with environment variables.
36
+ * Explicit options take precedence over environment variables.
37
+ */
38
+ declare function resolveConfig(options?: GlasstraceOptions): ResolvedConfig;
39
+ /**
40
+ * Returns true when the SDK should be inactive (production detected without force-enable).
41
+ * Logic order:
42
+ * 1. forceEnable === true → return false (override)
43
+ * 2. NODE_ENV === 'production' → return true
44
+ * 3. VERCEL_ENV === 'production' → return true
45
+ * 4. Otherwise → return false
46
+ */
47
+ declare function isProductionDisabled(config: ResolvedConfig): boolean;
48
+ /**
49
+ * Returns true when no API key is configured (anonymous mode).
50
+ * Treats undefined, empty string, whitespace-only, and gt_anon_* keys as anonymous.
51
+ */
52
+ declare function isAnonymousMode(config: ResolvedConfig): boolean;
53
+
54
+ /**
55
+ * Derives a deterministic session ID from the given inputs using SHA-256.
56
+ * The hash is truncated to 16 hex characters and parsed through SessionIdSchema.
57
+ *
58
+ * @param apiKey - The project's API key (or anonymous placeholder).
59
+ * @param origin - The origin string identifying the deployment environment.
60
+ * @param date - UTC date as YYYY-MM-DD.
61
+ * @param windowIndex - Zero-based index of the 4-hour activity window within the day.
62
+ * @returns A 16-character hex SessionId.
63
+ */
64
+ declare function deriveSessionId(apiKey: string, origin: string, date: string, windowIndex: number): SessionId;
65
+ /**
66
+ * Returns the origin string for the current process.
67
+ * If GLASSTRACE_ENV is set, returns that value.
68
+ * Otherwise returns `localhost:{PORT}` (PORT defaults to 3000).
69
+ *
70
+ * @returns The origin string used as a session derivation input.
71
+ */
72
+ declare function getOrigin(): string;
73
+ /**
74
+ * Returns the current UTC date as a YYYY-MM-DD string.
75
+ *
76
+ * @returns The UTC date formatted as "YYYY-MM-DD".
77
+ */
78
+ declare function getDateString(): string;
79
+ /**
80
+ * Tracks the current session state with 4-hour window tracking.
81
+ * Instantiated once by the orchestrator.
82
+ */
83
+ declare class SessionManager {
84
+ private windowIndex;
85
+ private lastActivityTimestamp;
86
+ private lastDate;
87
+ private lastApiKey;
88
+ private currentSessionId;
89
+ /**
90
+ * Returns the current session ID, deriving a new one if:
91
+ * - More than 4 hours have elapsed since last activity
92
+ * - The UTC date has changed (resets window index to 0)
93
+ * - The API key has changed (e.g., deferred anonymous key swap)
94
+ * - This is the first call
95
+ *
96
+ * @param apiKey - The project's API key used in session derivation.
97
+ * @returns The current or newly derived SessionId.
98
+ */
99
+ getSessionId(apiKey: string): SessionId;
100
+ }
101
+
102
+ /**
103
+ * The set of recognized fetch target categories.
104
+ */
105
+ type FetchTarget = "supabase" | "stripe" | "internal" | "unknown";
106
+ /**
107
+ * Classifies an outbound fetch target URL into a known category.
108
+ * Classification is case-insensitive and based on the URL hostname.
109
+ * Uses dot-boundary matching to avoid false positives (e.g. evilstripe.com).
110
+ *
111
+ * Returns one of: 'supabase', 'stripe', 'internal', or 'unknown'.
112
+ */
113
+ declare function classifyFetchTarget(url: string): FetchTarget;
114
+
115
+ /**
116
+ * Reads an existing anonymous key from the filesystem.
117
+ * Returns the key if valid, or null if:
118
+ * - The file does not exist
119
+ * - The file content is invalid
120
+ * - An I/O error occurs
121
+ */
122
+ declare function readAnonKey(projectRoot?: string): Promise<AnonApiKey | null>;
123
+ /**
124
+ * Gets an existing anonymous key from the filesystem, or creates a new one.
125
+ *
126
+ * - If file exists and contains a valid key, returns it
127
+ * - If file does not exist or content is invalid, generates a new key via createAnonApiKey()
128
+ * - Writes the new key to `.glasstrace/anon_key`, creating the directory if needed
129
+ * - On file write failure: logs a warning, caches an ephemeral in-memory key so
130
+ * repeated calls in the same process return the same key
131
+ */
132
+ declare function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey>;
133
+
134
+ /**
135
+ * Reads and validates a cached config file from `.glasstrace/config`.
136
+ * Returns the parsed `SdkInitResponse` or `null` on any failure.
137
+ */
138
+ declare function loadCachedConfig(projectRoot?: string): SdkInitResponse | null;
139
+ /**
140
+ * Persists the init response to `.glasstrace/config`.
141
+ * On failure, logs a warning and continues.
142
+ */
143
+ declare function saveCachedConfig(response: SdkInitResponse, projectRoot?: string): Promise<void>;
144
+ /**
145
+ * Sends a POST request to `/v1/sdk/init`.
146
+ * Validates the response against `SdkInitResponseSchema`.
147
+ */
148
+ declare function sendInitRequest(config: ResolvedConfig, anonKey: AnonApiKey | null, sdkVersion: string, importGraph?: ImportGraphPayload, healthReport?: SdkHealthReport, diagnostics?: Array<{
149
+ code: SdkDiagnosticCode;
150
+ message: string;
151
+ timestamp: number;
152
+ }>, signal?: AbortSignal): Promise<SdkInitResponse>;
153
+ /**
154
+ * Orchestrates the full init flow: send request, update config, cache result.
155
+ * This function MUST NOT throw.
156
+ */
157
+ declare function performInit(config: ResolvedConfig, anonKey: AnonApiKey | null, sdkVersion: string): Promise<void>;
158
+ /**
159
+ * Returns the current capture config from the three-tier fallback chain:
160
+ * 1. In-memory config from latest init response
161
+ * 2. File cache
162
+ * 3. DEFAULT_CAPTURE_CONFIG
163
+ */
164
+ declare function getActiveConfig(): CaptureConfig;
165
+
166
+ /**
167
+ * Lightweight SpanProcessor that delegates to a wrapped processor.
168
+ *
169
+ * All glasstrace.* attribute enrichment has been moved to {@link GlasstraceExporter}
170
+ * (see enriching-exporter.ts), which enriches spans at export time. This resolves:
171
+ * - Cold-start spans are buffered in the exporter, not dropped
172
+ * - Vercel's CompositeSpanProcessor skips onEnding(); the exporter doesn't need it
173
+ * - Session ID is computed at export time with the resolved API key
174
+ *
175
+ * This class is retained for backward compatibility. New code should use
176
+ * GlasstraceExporter directly.
177
+ *
178
+ * @deprecated Use GlasstraceExporter for span enrichment. This processor is now a pass-through.
179
+ */
180
+ declare class GlasstraceSpanProcessor implements SpanProcessor {
181
+ private readonly wrappedProcessor;
182
+ constructor(wrappedProcessor: SpanProcessor, _sessionManager?: SessionManager, _apiKey?: string | (() => string), _getConfig?: () => CaptureConfig, _environment?: string);
183
+ onStart(span: Span, parentContext: Context): void;
184
+ onEnd(readableSpan: ReadableSpan): void;
185
+ shutdown(): Promise<void>;
186
+ forceFlush(): Promise<void>;
187
+ }
188
+
189
+ /**
190
+ * Options for constructing a {@link GlasstraceExporter}.
191
+ */
192
+ interface GlasstraceExporterOptions {
193
+ getApiKey: () => string;
194
+ sessionManager: SessionManager;
195
+ getConfig: () => CaptureConfig;
196
+ environment: string | undefined;
197
+ endpointUrl: string;
198
+ createDelegate: ((url: string, headers: Record<string, string>) => SpanExporter) | null;
199
+ }
200
+ /**
201
+ * A SpanExporter that enriches spans with glasstrace.* attributes at export
202
+ * time, then delegates to a real OTLP exporter.
203
+ *
204
+ * This design resolves three issues:
205
+ * - Spans emitted before the API key resolves are buffered (not dropped)
206
+ * and flushed once the key is available.
207
+ * - Enrichment happens in the exporter (not onEnding), so it works
208
+ * on Vercel where CompositeSpanProcessor does not forward onEnding().
209
+ * - Session ID is computed at export time using the resolved API key,
210
+ * not the "pending" placeholder.
211
+ */
212
+ declare class GlasstraceExporter implements SpanExporter {
213
+ private readonly getApiKey;
214
+ private readonly sessionManager;
215
+ private readonly getConfig;
216
+ private readonly environment;
217
+ private readonly endpointUrl;
218
+ private readonly createDelegateFn;
219
+ private delegate;
220
+ private pendingBatches;
221
+ private pendingSpanCount;
222
+ private overflowLogged;
223
+ constructor(options: GlasstraceExporterOptions);
224
+ export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
225
+ /**
226
+ * Called when the API key transitions from "pending" to a resolved value.
227
+ * Creates the delegate exporter and flushes all buffered spans.
228
+ */
229
+ notifyKeyResolved(): void;
230
+ shutdown(): Promise<void>;
231
+ forceFlush(): Promise<void>;
232
+ /**
233
+ * Enriches a ReadableSpan with all glasstrace.* attributes.
234
+ * Returns a new ReadableSpan wrapper; the original span is not mutated.
235
+ * Each attribute derivation is wrapped in its own try-catch for partial
236
+ * enrichment resilience.
237
+ */
238
+ private enrichSpan;
239
+ /**
240
+ * Lazily creates the delegate OTLP exporter once the API key is resolved.
241
+ */
242
+ private ensureDelegate;
243
+ /**
244
+ * Buffers enriched spans while the API key is pending.
245
+ * Evicts oldest batches if the buffer exceeds MAX_PENDING_SPANS.
246
+ */
247
+ private bufferSpans;
248
+ /**
249
+ * Flushes all buffered spans through the delegate exporter.
250
+ * Called when the API key resolves.
251
+ */
252
+ private flushPending;
253
+ }
254
+
255
+ /**
256
+ * Creates a request handler for the `/__glasstrace/config` discovery endpoint.
257
+ *
258
+ * The returned handler checks if the request URL path is `/__glasstrace/config`.
259
+ * If not, returns `null` (pass-through). If it matches, returns a `DiscoveryResponse`
260
+ * with the anonymous key and current session ID.
261
+ *
262
+ * The triple guard (anonymous + dev + active) is enforced by the caller,
263
+ * not by this module. If the handler is registered, it serves.
264
+ */
265
+ declare function createDiscoveryHandler(getAnonKey: () => Promise<AnonApiKey | null>, getSessionId: () => SessionId): (request: Request) => Promise<Response | null>;
266
+
267
+ /**
268
+ * The primary SDK entry point called by developers in their `instrumentation.ts`.
269
+ * Orchestrates OTel setup, span processor, init client, anon key, and discovery endpoint.
270
+ *
271
+ * This function is synchronous and MUST NOT throw. The developer's server is never blocked.
272
+ * Background work (key resolution, init call) happens via fire-and-forget promises.
273
+ *
274
+ * @param options - Optional SDK configuration. Environment variables are used as fallbacks.
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * // instrumentation.ts
279
+ * import { registerGlasstrace } from "@glasstrace/sdk";
280
+ * registerGlasstrace(); // uses env vars
281
+ * ```
282
+ */
283
+ declare function registerGlasstrace(options?: GlasstraceOptions): void;
284
+ /**
285
+ * Returns the registered discovery handler, or null if not registered.
286
+ */
287
+ declare function getDiscoveryHandler(): ((request: Request) => Promise<Response | null>) | null;
288
+
289
+ type NextConfig = Record<string, unknown>;
290
+ /**
291
+ * Wraps the developer's Next.js config to enable source map generation
292
+ * and upload .map files to the ingestion API at build time.
293
+ *
294
+ * The build NEVER fails because of Glasstrace — all errors are caught
295
+ * and logged as warnings.
296
+ *
297
+ * @param nextConfig - The developer's existing Next.js configuration object.
298
+ * @returns A new config object with source map generation and upload enabled.
299
+ */
300
+ declare function withGlasstraceConfig(nextConfig: NextConfig): NextConfig;
301
+
302
+ interface SourceMapEntry {
303
+ filePath: string;
304
+ content: string;
305
+ }
306
+ /**
307
+ * Recursively finds all .map files in the given build directory.
308
+ * Returns relative paths and file contents.
309
+ */
310
+ declare function collectSourceMaps(buildDir: string): Promise<SourceMapEntry[]>;
311
+ /**
312
+ * Computes a build hash for source map uploads.
313
+ *
314
+ * First tries `git rev-parse HEAD` to get the git commit SHA.
315
+ * On failure, falls back to a deterministic content hash:
316
+ * sort source map file paths alphabetically, concatenate each as
317
+ * `{relativePath}\n{fileLength}\n{fileContent}`, then SHA-256 the result.
318
+ */
319
+ declare function computeBuildHash(maps?: SourceMapEntry[]): Promise<string>;
320
+ /**
321
+ * Uploads source maps to the ingestion API.
322
+ *
323
+ * POSTs to `{endpoint}/v1/source-maps` with the API key, build hash,
324
+ * and file entries. Validates the response against SourceMapUploadResponseSchema.
325
+ */
326
+ declare function uploadSourceMaps(apiKey: string, endpoint: string, buildHash: string, maps: SourceMapEntry[]): Promise<SourceMapUploadResponse>;
327
+
328
+ /**
329
+ * Discovers test files by scanning the project directory for conventional
330
+ * test file patterns. Also reads vitest/jest config files for custom include
331
+ * patterns and merges them with the defaults. Excludes node_modules/ and .next/.
332
+ *
333
+ * @param projectRoot - Absolute path to the project root directory.
334
+ * @returns Relative POSIX paths from projectRoot, capped at {@link MAX_TEST_FILES}.
335
+ */
336
+ declare function discoverTestFiles(projectRoot: string): Promise<string[]>;
337
+ /**
338
+ * Extracts import paths from file content using regex.
339
+ * Handles ES module imports, CommonJS requires, and dynamic imports.
340
+ *
341
+ * @param fileContent - The full text content of a TypeScript/JavaScript file.
342
+ * @returns An array of import path strings as written in the source (e.g. "./foo", "react").
343
+ */
344
+ declare function extractImports(fileContent: string): string[];
345
+ /**
346
+ * Builds an import graph mapping test file paths to their imported module paths.
347
+ *
348
+ * Discovers test files, reads each, extracts imports, and builds a graph.
349
+ * Computes a deterministic buildHash from the serialized graph content.
350
+ * Individual file read failures are silently skipped.
351
+ *
352
+ * @param projectRoot - Absolute path to the project root directory.
353
+ * @returns An {@link ImportGraphPayload} containing the graph and a deterministic buildHash.
354
+ */
355
+ declare function buildImportGraph(projectRoot: string): Promise<ImportGraphPayload>;
356
+
357
+ export { type FetchTarget, GlasstraceExporter, type GlasstraceExporterOptions, GlasstraceSpanProcessor, type ResolvedConfig, SdkError, SessionManager, type SourceMapEntry, buildImportGraph, classifyFetchTarget, collectSourceMaps, computeBuildHash, createDiscoveryHandler, deriveSessionId, discoverTestFiles, extractImports, getActiveConfig, getDateString, getDiscoveryHandler, getOrCreateAnonKey, getOrigin, isAnonymousMode, isProductionDisabled, loadCachedConfig, performInit, readAnonKey, readEnvVars, registerGlasstrace, resolveConfig, saveCachedConfig, sendInitRequest, uploadSourceMaps, withGlasstraceConfig };