@scoperat/tracker 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/index.d.ts +30 -0
- package/dist/index.js +194 -46
- package/dist/index.js.map +1 -1
- package/dist/widget/index.js +194 -46
- package/dist/widget/index.js.map +1 -1
- package/dist/widget.global.js +97 -96
- package/dist/widget.global.js.map +1 -1
- package/package.json +29 -17
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/breadcrumbs.ts","../src/context.ts","../src/errors.ts","../src/flag-client.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["export interface Breadcrumb {\n type: string;\n category: string;\n message: string;\n timestamp: number;\n data?: Record<string, unknown>;\n}\n\nconst MAX_BREADCRUMBS = 20;\nconst MAX_TEXT_LENGTH = 120;\n\nlet buffer: Breadcrumb[] = [];\nlet installed = false;\n\nfunction push(crumb: Breadcrumb): void {\n buffer.push(crumb);\n if (buffer.length > MAX_BREADCRUMBS) {\n buffer = buffer.slice(-MAX_BREADCRUMBS);\n }\n}\n\nfunction truncate(str: string, max = MAX_TEXT_LENGTH): string {\n return str.length > max ? str.slice(0, max) + \"…\" : str;\n}\n\nexport function getBreadcrumbs(): Breadcrumb[] {\n return [...buffer];\n}\n\nexport function addBreadcrumb(crumb: Breadcrumb): void {\n push(crumb);\n}\n\nexport function startBreadcrumbCollection(): void {\n if (installed || typeof window === \"undefined\") return;\n installed = true;\n\n document.addEventListener(\n \"click\",\n (e) => {\n const target = e.target as HTMLElement | null;\n if (!target) return;\n const text =\n target.textContent?.trim() || target.getAttribute(\"aria-label\") || \"\";\n push({\n type: \"user\",\n category: \"click\",\n message: truncate(\n `${target.tagName.toLowerCase()}${text ? ` \"${text}\"` : \"\"}`,\n ),\n timestamp: Date.now(),\n data: {\n tagName: target.tagName.toLowerCase(),\n className: target.className\n ? truncate(target.className, 80)\n : undefined,\n },\n });\n },\n { capture: true },\n );\n\n const origPushState = history.pushState;\n history.pushState = function (...args) {\n const from = location.pathname;\n origPushState.apply(this, args);\n push({\n type: \"navigation\",\n category: \"navigation\",\n message: `${from} → ${location.pathname}`,\n timestamp: Date.now(),\n });\n };\n\n window.addEventListener(\"popstate\", () => {\n push({\n type: \"navigation\",\n category: \"navigation\",\n message: `popstate → ${location.pathname}`,\n timestamp: Date.now(),\n });\n });\n\n const wrapConsole = (level: \"error\" | \"warn\") => {\n const orig = console[level];\n console[level] = (...args: unknown[]) => {\n push({\n type: \"console\",\n category: `console.${level}`,\n message: truncate(\n args.map((a) => (typeof a === \"string\" ? a : String(a))).join(\" \"),\n ),\n timestamp: Date.now(),\n });\n orig.apply(console, args);\n };\n };\n\n wrapConsole(\"error\");\n wrapConsole(\"warn\");\n}\n","/**\n * Auto-capture browser context: page, referrer, screen, locale, user agent.\n */\nexport function getBrowserContext(): Record<string, unknown> {\n if (typeof window === \"undefined\") {\n return {};\n }\n\n return {\n page: {\n url: window.location.href,\n path: window.location.pathname,\n title: document.title,\n referrer: document.referrer || undefined,\n },\n screen: {\n width: window.screen.width,\n height: window.screen.height,\n },\n locale: navigator.language,\n userAgent: navigator.userAgent,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n };\n}\n","interface TrackerLike {\n track(event: string, properties?: Record<string, unknown>): void;\n}\n\ninterface ErrorProperties {\n type: string;\n message: string;\n stack: string | undefined;\n handled: boolean;\n mechanism: string;\n source: \"browser\";\n filename?: string;\n lineno?: number;\n colno?: number;\n}\n\ntype BreadcrumbGetter = () => Array<{\n type: string;\n category: string;\n message: string;\n timestamp: number;\n data?: Record<string, unknown>;\n}>;\n\nconst DEDUP_COOLDOWN_MS = 60_000;\nconst dedupMap = new Map<string, number>();\n\nfunction simpleFingerprint(type: string, stack?: string): string {\n const firstFrame = stack?.split(\"\\n\")[1]?.trim() ?? \"\";\n return `${type}:${firstFrame}`;\n}\n\nfunction isDuplicate(fingerprint: string): boolean {\n const lastSent = dedupMap.get(fingerprint);\n if (lastSent && Date.now() - lastSent < DEDUP_COOLDOWN_MS) return true;\n dedupMap.set(fingerprint, Date.now());\n return false;\n}\n\nfunction sendException(\n tracker: TrackerLike,\n props: ErrorProperties,\n getBreadcrumbs: BreadcrumbGetter,\n): void {\n const fp = simpleFingerprint(props.type, props.stack);\n if (isDuplicate(fp)) return;\n\n tracker.track(\"$exception\", {\n ...props,\n context: { breadcrumbs: getBreadcrumbs() as unknown[] },\n });\n}\n\nexport function installGlobalHandlers(\n tracker: TrackerLike,\n getBreadcrumbs: BreadcrumbGetter,\n): void {\n if (typeof window === \"undefined\") return;\n\n window.onerror = (\n _message: string | Event,\n filename?: string,\n lineno?: number,\n colno?: number,\n error?: Error,\n ) => {\n const err = error ?? new Error(String(_message));\n sendException(\n tracker,\n {\n type: err.name || \"Error\",\n message: err.message || String(_message),\n stack: err.stack,\n handled: false,\n mechanism: \"onerror\",\n source: \"browser\",\n filename,\n lineno,\n colno,\n },\n getBreadcrumbs,\n );\n };\n\n window.onunhandledrejection = (event: PromiseRejectionEvent) => {\n const reason = event.reason;\n const err = reason instanceof Error ? reason : new Error(String(reason));\n sendException(\n tracker,\n {\n type: err.name || \"UnhandledRejection\",\n message: err.message,\n stack: err.stack,\n handled: false,\n mechanism: \"onunhandledrejection\",\n source: \"browser\",\n },\n getBreadcrumbs,\n );\n };\n}\n","import type { EvaluatedFlag } from \"./types.js\";\n\nconst DEFAULT_FLAG_POLL_INTERVAL = 60_000;\n\nexport interface FlagClientConfig {\n endpoint: string;\n writeKey: string;\n timeout?: number;\n environment?: string;\n pollInterval?: number;\n getIdentity: () => {\n userId?: string;\n anonymousId?: string;\n orgId?: string;\n };\n /** Called when a flag is first evaluated (for impression tracking). */\n onEvaluation?: (key: string, flag: EvaluatedFlag) => void;\n}\n\nexport class FlagClient {\n private endpoint: string;\n private writeKey: string;\n private timeout: number;\n private environment: string | undefined;\n private getIdentity: FlagClientConfig[\"getIdentity\"];\n private onEvaluation: FlagClientConfig[\"onEvaluation\"];\n\n private cache = new Map<string, EvaluatedFlag>();\n private ready = false;\n private readyCallbacks: Array<() => void> = [];\n private changeListeners = new Set<() => void>();\n private evalTracked = new Set<string>();\n private timer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config: FlagClientConfig) {\n this.endpoint = config.endpoint;\n this.writeKey = config.writeKey;\n this.timeout = config.timeout ?? 10_000;\n this.environment = config.environment;\n this.getIdentity = config.getIdentity;\n this.onEvaluation = config.onEvaluation;\n\n const interval = config.pollInterval ?? DEFAULT_FLAG_POLL_INTERVAL;\n this.fetch();\n this.timer = setInterval(() => this.fetch(), interval);\n }\n\n getFlag<T = unknown>(key: string, defaultValue: T): T {\n const flag = this.cache.get(key);\n if (!flag) return defaultValue;\n\n if (!this.evalTracked.has(key)) {\n this.evalTracked.add(key);\n this.onEvaluation?.(key, flag);\n }\n\n return flag.value as T;\n }\n\n onFlagsReady(callback: () => void): void {\n if (this.ready) {\n callback();\n } else {\n this.readyCallbacks.push(callback);\n }\n }\n\n onFlagChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => this.changeListeners.delete(listener);\n }\n\n async refresh(): Promise<void> {\n await this.fetch();\n }\n\n destroy(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n private async fetch(): Promise<void> {\n if (!this.writeKey || !this.endpoint) return;\n\n const params = new URLSearchParams();\n if (this.environment) params.set(\"environment\", this.environment);\n const identity = this.getIdentity();\n const identifier = identity.userId ?? identity.anonymousId;\n if (identifier) params.set(\"identifier\", identifier);\n if (identity.orgId) params.set(\"orgId\", identity.orgId);\n\n const qs = params.toString();\n const url = `${this.endpoint}/api/flags${qs ? `?${qs}` : \"\"}`;\n\n try {\n const res = await globalThis.fetch(url, {\n headers: { Authorization: `Bearer ${this.writeKey}` },\n signal: AbortSignal.timeout(this.timeout),\n });\n if (!res.ok) return;\n\n const json = (await res.json()) as {\n flags: Record<string, EvaluatedFlag>;\n };\n\n this.cache.clear();\n for (const [key, flag] of Object.entries(json.flags)) {\n this.cache.set(key, flag);\n }\n\n if (!this.ready) {\n this.ready = true;\n for (const cb of this.readyCallbacks) {\n try {\n cb();\n } catch {\n // ignore callback errors\n }\n }\n this.readyCallbacks = [];\n }\n\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch {\n // ignore listener errors\n }\n }\n } catch {\n // Silently fail — flags use cached values or defaults\n }\n }\n}\n","import {\n addBreadcrumb,\n getBreadcrumbs,\n startBreadcrumbCollection,\n} from \"./breadcrumbs.js\";\nimport { getBrowserContext } from \"./context.js\";\nimport { installGlobalHandlers } from \"./errors.js\";\nimport { FlagClient } from \"./flag-client.js\";\nimport type {\n FlushResult,\n IdentifyOptions,\n IdentifyTraits,\n InitOptions,\n TrackEvent,\n} from \"./types.js\";\n\nconst DEFAULT_FLUSH_AT = 10;\nconst DEFAULT_FLUSH_INTERVAL = 3_000;\nconst DEFAULT_TIMEOUT = 10_000;\nconst DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1_000; // 30 minutes\nconst MAX_BATCH_SIZE = 100;\nconst ANON_ID_KEY = \"__scoperat_anon_id\";\nconst SESSION_ID_KEY = \"__scoperat_session_id\";\nconst LAST_ACTIVITY_KEY = \"__scoperat_last_activity\";\n/**\n * Last identified user id, persisted so we can tell a genuine login /\n * account switch (→ start a fresh session) apart from a re-identify of\n * the same user across a page reload (→ keep the current session).\n */\nconst USER_ID_KEY = \"__scoperat_user_id\";\n\n/**\n * Lifecycle events emitted as a session begins and ends, mirroring the\n * `session_start` / `session_end` convention used by analytics platforms\n * like GA4 and Amplitude. A session ends on logout, on an explicit\n * `startSession`, or after `sessionTimeout` of inactivity (detected lazily\n * on the next activity, backdated to the last recorded activity).\n */\nconst SESSION_START_EVENT = \"$session_start\";\nconst SESSION_END_EVENT = \"$session_end\";\n\n/** Why a session ended / began, attached as the event's `reason` property. */\nexport type SessionReason = \"initial\" | \"login\" | \"logout\" | \"timeout\";\n/**\n * Storage key for the anonymous support-ticket session token minted\n * by the server via `POST /ticket/session`. Stored as\n * `{ anonymousId, token, expiresAt }` JSON. The widget replays this\n * token on every ticket call so the server never has to trust a\n * client-supplied anonymous id (see `lib/ticket-session.ts` in the\n * API).\n */\nconst TICKET_SESSION_KEY = \"__scoperat_ticket_session\";\n\ninterface StoredTicketSession {\n anonymousId: string;\n token: string;\n expiresAt: number;\n}\n\nfunction generateAnonId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\nfunction getOrCreateAnonId(): string | undefined {\n if (typeof window === \"undefined\" || typeof localStorage === \"undefined\") {\n return undefined;\n }\n\n try {\n let id = localStorage.getItem(ANON_ID_KEY);\n if (!id) {\n id = generateAnonId();\n localStorage.setItem(ANON_ID_KEY, id);\n }\n return id;\n } catch {\n return generateAnonId();\n }\n}\n\nclass TrackerImpl {\n private writeKey: string | null = null;\n private endpoint: string = \"\";\n private flushAt: number = DEFAULT_FLUSH_AT;\n private flushInterval: number = DEFAULT_FLUSH_INTERVAL;\n private timeout: number = DEFAULT_TIMEOUT;\n private onError?: (error: Error, events: TrackEvent[]) => void;\n\n private buffer: TrackEvent[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private visibilityHandler: (() => void) | null = null;\n private userId: string | undefined;\n private orgId: string | undefined;\n private traits: IdentifyTraits = {};\n private userHash: string | undefined;\n private anonymousId: string | undefined;\n private sessionId: string | undefined;\n private sessionTimeout: number = DEFAULT_SESSION_TIMEOUT;\n private ticketSessionFetch: Promise<StoredTicketSession | null> | null = null;\n\n private _flagClient: FlagClient | null = null;\n\n init(writeKey: string, options: InitOptions = {}): void {\n this.writeKey = writeKey;\n this.endpoint = (\n options.endpoint ??\n globalThis.location?.origin ??\n \"\"\n ).replace(/\\/+$/, \"\");\n this.flushAt = options.flushAt ?? DEFAULT_FLUSH_AT;\n this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n this.timeout = options.timeout ?? DEFAULT_TIMEOUT;\n this.onError = options.onError;\n this.sessionTimeout = options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;\n\n this.anonymousId = getOrCreateAnonId();\n\n if (this.timer) clearInterval(this.timer);\n this.timer = setInterval(() => {\n if (this.buffer.length > 0) {\n this.flush().catch(() => {});\n }\n }, this.flushInterval);\n\n if (typeof window !== \"undefined\") {\n if (this.visibilityHandler) {\n window.removeEventListener(\"visibilitychange\", this.visibilityHandler);\n }\n this.visibilityHandler = () => {\n if (document.visibilityState === \"hidden\" && this.buffer.length > 0) {\n this.flushSync();\n }\n };\n window.addEventListener(\"visibilitychange\", this.visibilityHandler);\n }\n\n if (options.captureErrors) {\n installGlobalHandlers(this, getBreadcrumbs);\n startBreadcrumbCollection();\n }\n\n this._flagClient?.destroy();\n this._flagClient = new FlagClient({\n endpoint: this.endpoint,\n writeKey,\n timeout: this.timeout,\n environment: options.environment,\n pollInterval: options.flagPollInterval,\n getIdentity: () => ({\n userId: this.userId,\n anonymousId: this.anonymousId,\n orgId: this.orgId,\n }),\n // Flag evaluations are an internal system signal, not a product\n // event — we deliberately don't emit `$flag_evaluation` into the\n // user's event stream (the server also refuses to persist it).\n });\n }\n\n /** The underlying flag evaluation client. Use for direct SDK integration. */\n get flags(): FlagClient {\n if (!this._flagClient) {\n throw new Error(\"Tracker not initialized. Call init() first.\");\n }\n return this._flagClient;\n }\n\n identify(\n userId: string,\n traits?: IdentifyTraits,\n options?: IdentifyOptions,\n ): void {\n // Compare against the last identified user (in-memory, falling back to\n // the persisted id so a fresh page load doesn't look like a new login).\n const previousUserId = this.userId ?? this.readStoredUserId();\n\n this.userId = userId;\n if (traits) {\n this.traits = { ...traits };\n if (traits.orgId) this.orgId = traits.orgId;\n }\n // Switching from one identified user to another invalidates the\n // prior userHash — callers must either pass a new one or accept\n // that ticket calls degrade to the anonymous session channel.\n this.userHash = options?.userHash;\n\n for (const evt of this.buffer) {\n if (!evt.user_id) evt.user_id = userId;\n if (!evt.org_id && this.orgId) evt.org_id = this.orgId;\n }\n\n // A genuine login or account switch starts a fresh analytics session.\n // Re-identifying the same user (e.g. after a reload) keeps the\n // existing session so reloads don't fragment a single visit.\n if (userId !== previousUserId) {\n this.writeStoredUserId(userId);\n this.startSession(\"login\");\n }\n\n this._flagClient?.refresh();\n }\n\n reset(): void {\n // End the active session (logout) and best-effort deliver it before\n // the caller tears down / navigates away.\n this.endSession(\"logout\", true);\n this.userId = undefined;\n this.orgId = undefined;\n this.traits = {};\n this.userHash = undefined;\n this.anonymousId = getOrCreateAnonId();\n this.clearStoredUserId();\n this.clearTicketSession();\n }\n\n /**\n * Begin a new analytics session, ending the current one first if it\n * exists. Emits `$session_end` (for the old session) and\n * `$session_start` (for the new). Called automatically on login via\n * {@link identify}; also exposed for explicit control.\n */\n startSession(reason: SessionReason = \"login\"): string | undefined {\n if (typeof window === \"undefined\") return this.sessionId;\n\n const now = Date.now();\n const existingId = this.readStoredSession();\n if (existingId) {\n this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);\n }\n\n const newId = generateAnonId();\n this.persistSession(newId, now);\n this.emitSessionEvent(SESSION_START_EVENT, newId, reason);\n return newId;\n }\n\n /**\n * End the current analytics session (if any), emitting `$session_end`,\n * and clear the stored session. The next tracked event will lazily\n * start a fresh session. Called automatically on logout via\n * {@link reset}; also exposed for explicit control.\n *\n * @param immediate - flush buffered events synchronously (via\n * `sendBeacon`) so the end event isn't lost if the page unloads.\n */\n endSession(reason: SessionReason = \"logout\", immediate = false): void {\n if (typeof window === \"undefined\") return;\n\n const existingId = this.readStoredSession();\n if (existingId) {\n this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);\n }\n\n this.sessionId = undefined;\n this.clearSession();\n\n if (immediate) {\n this.flushSync();\n }\n }\n\n private readStoredUserId(): string | undefined {\n if (typeof localStorage === \"undefined\") return undefined;\n try {\n return localStorage.getItem(USER_ID_KEY) ?? undefined;\n } catch {\n return undefined;\n }\n }\n\n private writeStoredUserId(userId: string): void {\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.setItem(USER_ID_KEY, userId);\n } catch {\n // ignore storage errors\n }\n }\n\n private clearStoredUserId(): void {\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.removeItem(USER_ID_KEY);\n } catch {\n // ignore storage errors\n }\n }\n\n private clearTicketSession(): void {\n this.ticketSessionFetch = null;\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.removeItem(TICKET_SESSION_KEY);\n } catch {\n // ignore storage errors\n }\n }\n\n private readStoredTicketSession(): StoredTicketSession | null {\n if (typeof localStorage === \"undefined\") return null;\n try {\n const raw = localStorage.getItem(TICKET_SESSION_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as Partial<StoredTicketSession>;\n if (\n typeof parsed.anonymousId === \"string\" &&\n typeof parsed.token === \"string\" &&\n typeof parsed.expiresAt === \"number\" &&\n parsed.expiresAt > Date.now() + 60_000\n ) {\n return parsed as StoredTicketSession;\n }\n localStorage.removeItem(TICKET_SESSION_KEY);\n } catch {\n // fall through to network fetch\n }\n return null;\n }\n\n /**\n * @internal Used by the widget API helpers. Returns a cached or\n * newly-minted anonymous session token, or `null` if the server\n * has anonymous sessions disabled. Coalesces concurrent callers\n * onto a single in-flight network request so mount-time parallel\n * API calls don't each trigger a `POST /ticket/session`.\n */\n _ensureTicketSession(): Promise<StoredTicketSession | null> {\n const cached = this.readStoredTicketSession();\n if (cached) return Promise.resolve(cached);\n\n if (this.ticketSessionFetch) return this.ticketSessionFetch;\n if (!this.writeKey) return Promise.resolve(null);\n\n this.ticketSessionFetch = (async () => {\n try {\n const res = await fetch(`${this.endpoint}/api/ingest/ticket/session`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.writeKey}`,\n },\n body: \"{}\",\n signal: AbortSignal.timeout(this.timeout),\n });\n if (!res.ok) return null;\n const json = (await res.json()) as StoredTicketSession;\n if (\n typeof json?.anonymousId === \"string\" &&\n typeof json?.token === \"string\" &&\n typeof json?.expiresAt === \"number\"\n ) {\n try {\n localStorage?.setItem(TICKET_SESSION_KEY, JSON.stringify(json));\n } catch {\n // ignore storage errors\n }\n // Keep our in-memory anonymousId in sync with the\n // server-issued one so any code that reads `_getIdentity`\n // outside the ticket flow (e.g. analytics) sees a\n // consistent value.\n this.anonymousId = json.anonymousId;\n return json;\n }\n return null;\n } catch {\n return null;\n } finally {\n this.ticketSessionFetch = null;\n }\n })();\n\n return this.ticketSessionFetch;\n }\n\n /**\n * Return the id of the current session, rotating it when the previous\n * session has expired through inactivity. On rotation it emits a\n * `$session_end` for the timed-out session (backdated to the last\n * recorded activity) and a `$session_start` for the new one. Called on\n * every tracked event so sessions advance with real user activity.\n */\n private ensureSession(now: number): string | undefined {\n if (typeof window === \"undefined\") return this.sessionId;\n\n try {\n const lastActivity = this.readLastActivity();\n const existingId = this.readStoredSession();\n const expired =\n lastActivity === undefined || now - lastActivity > this.sessionTimeout;\n\n if (existingId && !expired) {\n this.sessionId = existingId;\n try {\n localStorage.setItem(LAST_ACTIVITY_KEY, String(now));\n } catch {\n // ignore storage errors\n }\n return existingId;\n }\n\n if (existingId) {\n // Previous session lapsed — close it, backdated to the moment of\n // last activity so the session duration reflects reality rather\n // than the gap until the user returned.\n this.emitSessionEvent(\n SESSION_END_EVENT,\n existingId,\n \"timeout\",\n lastActivity !== undefined\n ? new Date(lastActivity).toISOString()\n : undefined,\n );\n }\n\n const newId = generateAnonId();\n this.persistSession(newId, now);\n this.emitSessionEvent(\n SESSION_START_EVENT,\n newId,\n existingId ? \"timeout\" : \"initial\",\n );\n return newId;\n } catch {\n return this.sessionId ?? generateAnonId();\n }\n }\n\n private readStoredSession(): string | undefined {\n if (this.sessionId) return this.sessionId;\n if (typeof sessionStorage === \"undefined\") return undefined;\n try {\n return sessionStorage.getItem(SESSION_ID_KEY) ?? undefined;\n } catch {\n return undefined;\n }\n }\n\n private readLastActivity(): number | undefined {\n if (typeof localStorage === \"undefined\") return undefined;\n try {\n const raw = localStorage.getItem(LAST_ACTIVITY_KEY);\n return raw ? Number(raw) : undefined;\n } catch {\n return undefined;\n }\n }\n\n private persistSession(id: string, now: number): void {\n this.sessionId = id;\n if (typeof window === \"undefined\") return;\n try {\n sessionStorage.setItem(SESSION_ID_KEY, id);\n localStorage.setItem(LAST_ACTIVITY_KEY, String(now));\n } catch {\n // ignore storage errors\n }\n }\n\n /**\n * Enqueue a session lifecycle event already stamped with its session id\n * (so it never re-enters {@link ensureSession} and risks recursion).\n */\n private emitSessionEvent(\n event: string,\n sessionId: string,\n reason: SessionReason,\n occurredAt?: string,\n ): void {\n if (!this.writeKey) return;\n\n const traitsContext =\n Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};\n\n this.buffer.push({\n event,\n properties: { reason },\n occurred_at: occurredAt ?? new Date().toISOString(),\n source: \"browser\",\n org_id: this.orgId,\n user_id: this.userId,\n anonymous_id: this.anonymousId,\n session_id: sessionId,\n context: {\n ...getBrowserContext(),\n ...traitsContext,\n },\n });\n }\n\n private clearSession(): void {\n if (typeof window === \"undefined\") return;\n try {\n sessionStorage.removeItem(SESSION_ID_KEY);\n localStorage.removeItem(LAST_ACTIVITY_KEY);\n } catch {\n // Ignore storage errors\n }\n }\n\n getSessionId(): string | undefined {\n return this.ensureSession(Date.now());\n }\n\n /** @internal Used by the widget to read tracker config. */\n _getEndpoint(): string {\n return this.endpoint;\n }\n\n /** @internal Used by the widget to read the write key. */\n _getWriteKey(): string | null {\n return this.writeKey;\n }\n\n /** @internal Used by the widget to read the current user identity. */\n _getIdentity(): {\n userId?: string;\n anonymousId?: string;\n email?: string;\n name?: string;\n userHash?: string;\n } {\n return {\n userId: this.userId,\n anonymousId: this.anonymousId,\n email: this.traits.email as string | undefined,\n name: this.traits.name as string | undefined,\n userHash: this.userHash,\n };\n }\n\n captureException(error: Error, context?: Record<string, unknown>): void {\n if (!this.writeKey) return;\n\n const breadcrumbs = getBreadcrumbs();\n this.track(\"$exception\", {\n type: error.name || \"Error\",\n message: error.message,\n stack: error.stack,\n handled: true,\n mechanism: (context?.mechanism as string) ?? \"manual\",\n source: \"browser\",\n ...context,\n context: { breadcrumbs: breadcrumbs as unknown[] },\n });\n }\n\n addBreadcrumb(\n category: string,\n message: string,\n data?: Record<string, unknown>,\n ): void {\n addBreadcrumb({\n type: \"custom\",\n category,\n message,\n timestamp: Date.now(),\n data,\n });\n }\n\n track(event: string, properties?: Record<string, unknown>): void;\n track(event: TrackEvent): void;\n track(\n eventOrName: string | TrackEvent,\n properties?: Record<string, unknown>,\n ): void {\n if (!this.writeKey) return;\n\n const evt: TrackEvent =\n typeof eventOrName === \"string\"\n ? { event: eventOrName, properties }\n : eventOrName;\n\n const traitsContext =\n Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};\n\n const enriched: TrackEvent = {\n ...evt,\n occurred_at: evt.occurred_at ?? new Date().toISOString(),\n source: evt.source ?? \"browser\",\n org_id: evt.org_id ?? this.orgId,\n user_id: evt.user_id ?? this.userId,\n anonymous_id: evt.anonymous_id ?? this.anonymousId,\n session_id: evt.session_id ?? this.ensureSession(Date.now()),\n context: {\n ...getBrowserContext(),\n ...traitsContext,\n ...evt.context,\n },\n };\n\n this.buffer.push(enriched);\n\n if (this.buffer.length >= this.flushAt) {\n this.flush().catch(() => {});\n }\n }\n\n async flush(): Promise<FlushResult> {\n if (!this.writeKey || this.buffer.length === 0) {\n return { success: true, ingested: 0 };\n }\n\n const batch = this.buffer.splice(0, MAX_BATCH_SIZE);\n\n try {\n const res = await fetch(`${this.endpoint}/api/ingest/track`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.writeKey}`,\n },\n body: JSON.stringify({ events: batch }),\n signal: AbortSignal.timeout(this.timeout),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(`API returned ${res.status}: ${body}`);\n }\n\n const json = (await res.json()) as { ingested: number };\n return { success: true, ingested: json.ingested };\n } catch (error) {\n this.buffer.unshift(...batch);\n const err = error instanceof Error ? error : new Error(String(error));\n this.onError?.(err, batch);\n return { success: false, ingested: 0 };\n }\n }\n\n /**\n * Fire-and-forget flush that survives page unload.\n *\n * Uses `fetch` with `keepalive` rather than `navigator.sendBeacon`:\n * beacons are always dispatched with credentials, and a JSON beacon is\n * preflighted — a credentialed preflighted request fails the CORS check\n * against the wildcard (`Access-Control-Allow-Origin: *`) ingest policy,\n * so the browser silently drops the beacon before it is ever sent. A\n * keepalive fetch is non-credentialed by default and carries the normal\n * Bearer header, giving it the exact CORS profile of the regular\n * `flush()` path while still outliving the page.\n */\n private flushSync(): void {\n if (!this.writeKey) return;\n if (typeof fetch === \"undefined\") return;\n if (this.buffer.length === 0) return;\n\n const batch = this.buffer.splice(0, MAX_BATCH_SIZE);\n\n try {\n void fetch(`${this.endpoint}/api/ingest/track`, {\n method: \"POST\",\n keepalive: true,\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.writeKey}`,\n },\n body: JSON.stringify({ events: batch }),\n }).catch(() => {\n // Best effort on unload\n });\n } catch {\n // Best effort on unload\n }\n }\n\n getFlag<T = unknown>(key: string, defaultValue: T): T {\n return this._flagClient?.getFlag(key, defaultValue) ?? defaultValue;\n }\n\n onFlagsReady(callback: () => void): void {\n this._flagClient?.onFlagsReady(callback);\n }\n\n onFlagChange(listener: () => void): () => void {\n return this._flagClient?.onFlagChange(listener) ?? (() => {});\n }\n\n async refreshFlags(): Promise<void> {\n await this._flagClient?.refresh();\n }\n\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n\n this._flagClient?.destroy();\n this._flagClient = null;\n\n if (typeof window !== \"undefined\" && this.visibilityHandler) {\n window.removeEventListener(\"visibilitychange\", this.visibilityHandler);\n this.visibilityHandler = null;\n }\n\n while (this.buffer.length > 0) {\n await this.flush();\n }\n }\n}\n\nconst scoperat = new TrackerImpl();\nexport default scoperat;\n","import scoperat from \"./client.js\";\n\nexport default scoperat;\nexport { FlagClient } from \"./flag-client.js\";\nexport type {\n EvaluatedFlag,\n EventSource,\n FlushResult,\n IdentifyOptions,\n IdentifyTraits,\n InitOptions,\n TrackEvent,\n} from \"./types.js\";\n"],"mappings":"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","names":["getBreadcrumbs"]}
|
|
1
|
+
{"version":3,"sources":["../src/breadcrumbs.ts","../src/context.ts","../src/errors.ts","../src/flag-client.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["export interface Breadcrumb {\n type: string;\n category: string;\n message: string;\n timestamp: number;\n data?: Record<string, unknown>;\n}\n\nconst MAX_BREADCRUMBS = 20;\nconst MAX_TEXT_LENGTH = 120;\n\nlet buffer: Breadcrumb[] = [];\nlet installed = false;\n\nfunction push(crumb: Breadcrumb): void {\n buffer.push(crumb);\n if (buffer.length > MAX_BREADCRUMBS) {\n buffer = buffer.slice(-MAX_BREADCRUMBS);\n }\n}\n\nfunction truncate(str: string, max = MAX_TEXT_LENGTH): string {\n return str.length > max ? str.slice(0, max) + \"…\" : str;\n}\n\nexport function getBreadcrumbs(): Breadcrumb[] {\n return [...buffer];\n}\n\nexport function addBreadcrumb(crumb: Breadcrumb): void {\n push(crumb);\n}\n\nexport function startBreadcrumbCollection(): void {\n if (installed || typeof window === \"undefined\") return;\n installed = true;\n\n document.addEventListener(\n \"click\",\n (e) => {\n const target = e.target as HTMLElement | null;\n if (!target) return;\n const text =\n target.textContent?.trim() || target.getAttribute(\"aria-label\") || \"\";\n push({\n type: \"user\",\n category: \"click\",\n message: truncate(\n `${target.tagName.toLowerCase()}${text ? ` \"${text}\"` : \"\"}`,\n ),\n timestamp: Date.now(),\n data: {\n tagName: target.tagName.toLowerCase(),\n className: target.className\n ? truncate(target.className, 80)\n : undefined,\n },\n });\n },\n { capture: true },\n );\n\n const origPushState = history.pushState;\n history.pushState = function (...args) {\n const from = location.pathname;\n origPushState.apply(this, args);\n push({\n type: \"navigation\",\n category: \"navigation\",\n message: `${from} → ${location.pathname}`,\n timestamp: Date.now(),\n });\n };\n\n window.addEventListener(\"popstate\", () => {\n push({\n type: \"navigation\",\n category: \"navigation\",\n message: `popstate → ${location.pathname}`,\n timestamp: Date.now(),\n });\n });\n\n const wrapConsole = (level: \"error\" | \"warn\") => {\n const orig = console[level];\n console[level] = (...args: unknown[]) => {\n push({\n type: \"console\",\n category: `console.${level}`,\n message: truncate(\n args.map((a) => (typeof a === \"string\" ? a : String(a))).join(\" \"),\n ),\n timestamp: Date.now(),\n });\n orig.apply(console, args);\n };\n };\n\n wrapConsole(\"error\");\n wrapConsole(\"warn\");\n}\n","/**\n * Auto-capture browser context: page, referrer, screen, locale, user agent.\n */\nexport function getBrowserContext(): Record<string, unknown> {\n if (typeof window === \"undefined\") {\n return {};\n }\n\n return {\n page: {\n url: window.location.href,\n path: window.location.pathname,\n title: document.title,\n referrer: document.referrer || undefined,\n },\n screen: {\n width: window.screen.width,\n height: window.screen.height,\n },\n locale: navigator.language,\n userAgent: navigator.userAgent,\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n };\n}\n","interface TrackerLike {\n track(event: string, properties?: Record<string, unknown>): void;\n}\n\nexport interface ExceptionProperties {\n type: string;\n message: string;\n stack: string | undefined;\n handled: boolean;\n mechanism: string;\n source: \"browser\";\n filename?: string;\n lineno?: number;\n colno?: number;\n /**\n * True for cross-origin \"Script error.\" events where the browser\n * withholds all details — lets the server group/filter them.\n */\n opaque?: boolean;\n /** Caller-supplied extra data (kept nested so it can't collide with the fields above). */\n extra?: Record<string, unknown>;\n}\n\ntype BreadcrumbGetter = () => Array<{\n type: string;\n category: string;\n message: string;\n timestamp: number;\n data?: Record<string, unknown>;\n}>;\n\nconst DEDUP_COOLDOWN_MS = 60_000;\nconst MAX_DEDUP_ENTRIES = 200;\nconst MAX_MESSAGE_LENGTH = 2_048;\nconst MAX_STACK_LENGTH = 16_384;\nconst MAX_REASON_JSON_LENGTH = 1_024;\n\nconst dedupMap = new Map<string, number>();\n\nfunction truncate(str: string, max: number): string {\n return str.length > max ? str.slice(0, max) + \"…\" : str;\n}\n\nfunction fingerprint(\n type: string,\n message: string,\n stack: string | undefined,\n): string {\n const firstFrame = stack?.split(\"\\n\")[1]?.trim() ?? \"\";\n // Include the (truncated) message so distinct errors thrown from the\n // same line — or stackless errors — don't collapse into one entry.\n return `${type}:${truncate(message, 100)}:${firstFrame}`;\n}\n\nfunction isDuplicate(fp: string): boolean {\n const now = Date.now();\n const lastSent = dedupMap.get(fp);\n if (lastSent && now - lastSent < DEDUP_COOLDOWN_MS) return true;\n dedupMap.set(fp, now);\n pruneDedupMap(now);\n return false;\n}\n\n/** Keep the dedup map bounded in long-lived SPAs. */\nfunction pruneDedupMap(now: number): void {\n if (dedupMap.size <= MAX_DEDUP_ENTRIES) return;\n for (const [key, ts] of dedupMap) {\n if (now - ts >= DEDUP_COOLDOWN_MS) dedupMap.delete(key);\n }\n // Still over the cap (all entries fresh): evict oldest-inserted first.\n for (const key of dedupMap.keys()) {\n if (dedupMap.size <= MAX_DEDUP_ENTRIES) break;\n dedupMap.delete(key);\n }\n}\n\n/**\n * Shared exception delivery path for both global handlers and manual\n * `captureException` calls: dedup by fingerprint, cap payload sizes,\n * attach breadcrumbs, and enqueue as a `$exception` event.\n */\nexport function sendException(\n tracker: TrackerLike,\n props: ExceptionProperties,\n getBreadcrumbs: BreadcrumbGetter,\n): void {\n const fp = fingerprint(props.type, props.message, props.stack);\n if (isDuplicate(fp)) return;\n\n tracker.track(\"$exception\", {\n ...props,\n message: truncate(props.message, MAX_MESSAGE_LENGTH),\n stack: props.stack ? truncate(props.stack, MAX_STACK_LENGTH) : undefined,\n context: { breadcrumbs: getBreadcrumbs() as unknown[] },\n });\n}\n\n/**\n * Turn an arbitrary promise rejection reason into type/message/stack.\n * Object reasons are JSON-stringified (size-capped) rather than the\n * useless `String(reason)` → \"[object Object]\".\n */\nfunction describeRejectionReason(reason: unknown): {\n type: string;\n message: string;\n stack: string | undefined;\n} {\n if (reason instanceof Error) {\n return {\n type: reason.name || \"UnhandledRejection\",\n message: reason.message,\n stack: reason.stack,\n };\n }\n\n if (typeof reason === \"object\" && reason !== null) {\n let json: string;\n try {\n json = JSON.stringify(reason) ?? Object.prototype.toString.call(reason);\n } catch {\n json = Object.prototype.toString.call(reason);\n }\n return {\n type: \"UnhandledRejection\",\n message: `Non-Error promise rejection: ${truncate(json, MAX_REASON_JSON_LENGTH)}`,\n stack: undefined,\n };\n }\n\n return {\n type: \"UnhandledRejection\",\n message: String(reason),\n stack: undefined,\n };\n}\n\nlet uninstallHandlers: (() => void) | null = null;\n\n/**\n * Install global error / unhandledrejection listeners.\n *\n * Uses `addEventListener` (never assigns `window.onerror`) so host-page\n * handlers and other SDKs are left intact. Idempotent — calling twice\n * returns the existing uninstaller. The returned function removes the\n * listeners (used by `shutdown()`).\n */\nexport function installGlobalHandlers(\n tracker: TrackerLike,\n getBreadcrumbs: BreadcrumbGetter,\n): () => void {\n if (typeof window === \"undefined\") return () => {};\n if (uninstallHandlers) return uninstallHandlers;\n\n const onError = (event: ErrorEvent) => {\n const error = event.error as Error | undefined;\n const message =\n error?.message ||\n (typeof event.message === \"string\" && event.message\n ? event.message\n : \"Unknown error\");\n // Cross-origin scripts without CORS surface only \"Script error.\"\n // with no stack, filename, or error object.\n const opaque = !error && /^Script error\\.?$/.test(message);\n\n sendException(\n tracker,\n {\n type: error?.name || \"Error\",\n message,\n stack: error?.stack,\n handled: false,\n mechanism: \"onerror\",\n source: \"browser\",\n filename: event.filename || undefined,\n lineno: event.lineno || undefined,\n colno: event.colno || undefined,\n ...(opaque ? { opaque: true } : {}),\n },\n getBreadcrumbs,\n );\n };\n\n const onRejection = (event: PromiseRejectionEvent) => {\n const { type, message, stack } = describeRejectionReason(event.reason);\n sendException(\n tracker,\n {\n type,\n message,\n stack,\n handled: false,\n mechanism: \"onunhandledrejection\",\n source: \"browser\",\n },\n getBreadcrumbs,\n );\n };\n\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n\n uninstallHandlers = () => {\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n uninstallHandlers = null;\n };\n return uninstallHandlers;\n}\n","import type { EvaluatedFlag } from \"./types.js\";\n\nconst DEFAULT_FLAG_POLL_INTERVAL = 60_000;\n\nexport interface FlagClientConfig {\n endpoint: string;\n writeKey: string;\n timeout?: number;\n environment?: string;\n pollInterval?: number;\n getIdentity: () => {\n userId?: string;\n anonymousId?: string;\n orgId?: string;\n };\n /** Called when a flag is first evaluated (for impression tracking). */\n onEvaluation?: (key: string, flag: EvaluatedFlag) => void;\n}\n\nexport class FlagClient {\n private endpoint: string;\n private writeKey: string;\n private timeout: number;\n private environment: string | undefined;\n private getIdentity: FlagClientConfig[\"getIdentity\"];\n private onEvaluation: FlagClientConfig[\"onEvaluation\"];\n\n private cache = new Map<string, EvaluatedFlag>();\n private ready = false;\n private readyCallbacks: Array<() => void> = [];\n private changeListeners = new Set<() => void>();\n private evalTracked = new Set<string>();\n private timer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config: FlagClientConfig) {\n this.endpoint = config.endpoint;\n this.writeKey = config.writeKey;\n this.timeout = config.timeout ?? 10_000;\n this.environment = config.environment;\n this.getIdentity = config.getIdentity;\n this.onEvaluation = config.onEvaluation;\n\n const interval = config.pollInterval ?? DEFAULT_FLAG_POLL_INTERVAL;\n this.fetch();\n this.timer = setInterval(() => this.fetch(), interval);\n }\n\n getFlag<T = unknown>(key: string, defaultValue: T): T {\n const flag = this.cache.get(key);\n if (!flag) return defaultValue;\n\n if (!this.evalTracked.has(key)) {\n this.evalTracked.add(key);\n this.onEvaluation?.(key, flag);\n }\n\n return flag.value as T;\n }\n\n onFlagsReady(callback: () => void): void {\n if (this.ready) {\n callback();\n } else {\n this.readyCallbacks.push(callback);\n }\n }\n\n onFlagChange(listener: () => void): () => void {\n this.changeListeners.add(listener);\n return () => this.changeListeners.delete(listener);\n }\n\n async refresh(): Promise<void> {\n await this.fetch();\n }\n\n destroy(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n private async fetch(): Promise<void> {\n if (!this.writeKey || !this.endpoint) return;\n\n const params = new URLSearchParams();\n if (this.environment) params.set(\"environment\", this.environment);\n const identity = this.getIdentity();\n const identifier = identity.userId ?? identity.anonymousId;\n if (identifier) params.set(\"identifier\", identifier);\n if (identity.orgId) params.set(\"orgId\", identity.orgId);\n\n const qs = params.toString();\n const url = `${this.endpoint}/api/flags${qs ? `?${qs}` : \"\"}`;\n\n try {\n const res = await globalThis.fetch(url, {\n headers: { Authorization: `Bearer ${this.writeKey}` },\n signal: AbortSignal.timeout(this.timeout),\n });\n if (!res.ok) return;\n\n const json = (await res.json()) as {\n flags: Record<string, EvaluatedFlag>;\n };\n\n this.cache.clear();\n for (const [key, flag] of Object.entries(json.flags)) {\n this.cache.set(key, flag);\n }\n\n if (!this.ready) {\n this.ready = true;\n for (const cb of this.readyCallbacks) {\n try {\n cb();\n } catch {\n // ignore callback errors\n }\n }\n this.readyCallbacks = [];\n }\n\n for (const listener of this.changeListeners) {\n try {\n listener();\n } catch {\n // ignore listener errors\n }\n }\n } catch {\n // Silently fail — flags use cached values or defaults\n }\n }\n}\n","import {\n addBreadcrumb,\n getBreadcrumbs,\n startBreadcrumbCollection,\n} from \"./breadcrumbs.js\";\nimport { getBrowserContext } from \"./context.js\";\nimport { installGlobalHandlers, sendException } from \"./errors.js\";\nimport { FlagClient } from \"./flag-client.js\";\nimport type {\n FlushResult,\n IdentifyOptions,\n IdentifyTraits,\n InitOptions,\n TrackEvent,\n} from \"./types.js\";\n\nconst DEFAULT_FLUSH_AT = 10;\nconst DEFAULT_FLUSH_INTERVAL = 3_000;\nconst DEFAULT_TIMEOUT = 10_000;\nconst DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1_000; // 30 minutes\nconst DEFAULT_MAX_QUEUE_SIZE = 1_000;\nconst MAX_BATCH_SIZE = 100;\nconst MAX_RETRY_BACKOFF = 60_000;\nconst ANON_ID_KEY = \"__scoperat_anon_id\";\nconst SESSION_ID_KEY = \"__scoperat_session_id\";\nconst LAST_ACTIVITY_KEY = \"__scoperat_last_activity\";\n/**\n * Last identified user id, persisted so we can tell a genuine login /\n * account switch (→ start a fresh session) apart from a re-identify of\n * the same user across a page reload (→ keep the current session).\n */\nconst USER_ID_KEY = \"__scoperat_user_id\";\n\n/**\n * Lifecycle events emitted as a session begins and ends, mirroring the\n * `session_start` / `session_end` convention used by analytics platforms\n * like GA4 and Amplitude. A session ends on logout, on an explicit\n * `startSession`, or after `sessionTimeout` of inactivity (detected lazily\n * on the next activity, backdated to the last recorded activity).\n */\nconst SESSION_START_EVENT = \"$session_start\";\nconst SESSION_END_EVENT = \"$session_end\";\n\n/** Why a session ended / began, attached as the event's `reason` property. */\nexport type SessionReason = \"initial\" | \"login\" | \"logout\" | \"timeout\";\n/**\n * Storage key for the anonymous support-ticket session token minted\n * by the server via `POST /ticket/session`. Stored as\n * `{ anonymousId, token, expiresAt }` JSON. The widget replays this\n * token on every ticket call so the server never has to trust a\n * client-supplied anonymous id (see `lib/ticket-session.ts` in the\n * API).\n */\nconst TICKET_SESSION_KEY = \"__scoperat_ticket_session\";\n\ninterface StoredTicketSession {\n anonymousId: string;\n token: string;\n expiresAt: number;\n}\n\nfunction generateAnonId(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\nfunction getOrCreateAnonId(): string | undefined {\n if (typeof window === \"undefined\" || typeof localStorage === \"undefined\") {\n return undefined;\n }\n\n try {\n let id = localStorage.getItem(ANON_ID_KEY);\n if (!id) {\n id = generateAnonId();\n localStorage.setItem(ANON_ID_KEY, id);\n }\n return id;\n } catch {\n return generateAnonId();\n }\n}\n\nclass TrackerImpl {\n private writeKey: string | null = null;\n private endpoint: string = \"\";\n private flushAt: number = DEFAULT_FLUSH_AT;\n private flushInterval: number = DEFAULT_FLUSH_INTERVAL;\n private timeout: number = DEFAULT_TIMEOUT;\n private onError?: (error: Error, events: TrackEvent[]) => void;\n\n private buffer: TrackEvent[] = [];\n private maxQueueSize: number = DEFAULT_MAX_QUEUE_SIZE;\n private timer: ReturnType<typeof setInterval> | null = null;\n private visibilityHandler: (() => void) | null = null;\n private pageHideHandler: (() => void) | null = null;\n private uninstallErrorHandlers: (() => void) | null = null;\n /** Consecutive flush failures, drives exponential retry backoff. */\n private flushFailures = 0;\n /** Timestamp before which automatic flushes are suppressed (backoff). */\n private nextFlushAllowedAt = 0;\n private exceptionFlushScheduled = false;\n private userId: string | undefined;\n private orgId: string | undefined;\n private traits: IdentifyTraits = {};\n private userHash: string | undefined;\n private anonymousId: string | undefined;\n private sessionId: string | undefined;\n private sessionTimeout: number = DEFAULT_SESSION_TIMEOUT;\n private ticketSessionFetch: Promise<StoredTicketSession | null> | null = null;\n\n private _flagClient: FlagClient | null = null;\n\n init(writeKey: string, options: InitOptions = {}): void {\n this.writeKey = writeKey;\n this.endpoint = (\n options.endpoint ??\n globalThis.location?.origin ??\n \"\"\n ).replace(/\\/+$/, \"\");\n this.flushAt = options.flushAt ?? DEFAULT_FLUSH_AT;\n this.flushInterval = options.flushInterval ?? DEFAULT_FLUSH_INTERVAL;\n this.timeout = options.timeout ?? DEFAULT_TIMEOUT;\n this.onError = options.onError;\n this.sessionTimeout = options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;\n this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;\n\n this.anonymousId = getOrCreateAnonId();\n\n if (this.timer) clearInterval(this.timer);\n this.timer = setInterval(() => {\n if (this.buffer.length > 0) {\n this.maybeFlush();\n }\n }, this.flushInterval);\n\n if (typeof window !== \"undefined\") {\n if (this.visibilityHandler) {\n window.removeEventListener(\"visibilitychange\", this.visibilityHandler);\n }\n this.visibilityHandler = () => {\n if (document.visibilityState === \"hidden\" && this.buffer.length > 0) {\n this.flushSync();\n }\n };\n window.addEventListener(\"visibilitychange\", this.visibilityHandler);\n\n if (this.pageHideHandler) {\n window.removeEventListener(\"pagehide\", this.pageHideHandler);\n }\n this.pageHideHandler = () => {\n if (this.buffer.length > 0) {\n this.flushSync();\n }\n };\n window.addEventListener(\"pagehide\", this.pageHideHandler);\n }\n\n if (options.captureErrors) {\n this.uninstallErrorHandlers = installGlobalHandlers(this, getBreadcrumbs);\n startBreadcrumbCollection();\n }\n\n this._flagClient?.destroy();\n this._flagClient = new FlagClient({\n endpoint: this.endpoint,\n writeKey,\n timeout: this.timeout,\n environment: options.environment,\n pollInterval: options.flagPollInterval,\n getIdentity: () => ({\n userId: this.userId,\n anonymousId: this.anonymousId,\n orgId: this.orgId,\n }),\n // Flag evaluations are an internal system signal, not a product\n // event — we deliberately don't emit `$flag_evaluation` into the\n // user's event stream (the server also refuses to persist it).\n });\n }\n\n /** The underlying flag evaluation client. Use for direct SDK integration. */\n get flags(): FlagClient {\n if (!this._flagClient) {\n throw new Error(\"Tracker not initialized. Call init() first.\");\n }\n return this._flagClient;\n }\n\n identify(\n userId: string,\n traits?: IdentifyTraits,\n options?: IdentifyOptions,\n ): void {\n // Compare against the last identified user (in-memory, falling back to\n // the persisted id so a fresh page load doesn't look like a new login).\n const previousUserId = this.userId ?? this.readStoredUserId();\n\n this.userId = userId;\n if (traits) {\n this.traits = { ...traits };\n if (traits.orgId) this.orgId = traits.orgId;\n }\n // Switching from one identified user to another invalidates the\n // prior userHash — callers must either pass a new one or accept\n // that ticket calls degrade to the anonymous session channel.\n this.userHash = options?.userHash;\n\n for (const evt of this.buffer) {\n if (!evt.user_id) evt.user_id = userId;\n if (!evt.org_id && this.orgId) evt.org_id = this.orgId;\n }\n\n // A genuine login or account switch starts a fresh analytics session.\n // Re-identifying the same user (e.g. after a reload) keeps the\n // existing session so reloads don't fragment a single visit.\n if (userId !== previousUserId) {\n this.writeStoredUserId(userId);\n this.startSession(\"login\");\n }\n\n this._flagClient?.refresh();\n }\n\n reset(): void {\n // End the active session (logout) and best-effort deliver it before\n // the caller tears down / navigates away.\n this.endSession(\"logout\", true);\n this.userId = undefined;\n this.orgId = undefined;\n this.traits = {};\n this.userHash = undefined;\n this.anonymousId = getOrCreateAnonId();\n this.clearStoredUserId();\n this.clearTicketSession();\n }\n\n /**\n * Begin a new analytics session, ending the current one first if it\n * exists. Emits `$session_end` (for the old session) and\n * `$session_start` (for the new). Called automatically on login via\n * {@link identify}; also exposed for explicit control.\n */\n startSession(reason: SessionReason = \"login\"): string | undefined {\n if (typeof window === \"undefined\") return this.sessionId;\n\n const now = Date.now();\n const existingId = this.readStoredSession();\n if (existingId) {\n this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);\n }\n\n const newId = generateAnonId();\n this.persistSession(newId, now);\n this.emitSessionEvent(SESSION_START_EVENT, newId, reason);\n return newId;\n }\n\n /**\n * End the current analytics session (if any), emitting `$session_end`,\n * and clear the stored session. The next tracked event will lazily\n * start a fresh session. Called automatically on logout via\n * {@link reset}; also exposed for explicit control.\n *\n * @param immediate - flush buffered events synchronously (via\n * `sendBeacon`) so the end event isn't lost if the page unloads.\n */\n endSession(reason: SessionReason = \"logout\", immediate = false): void {\n if (typeof window === \"undefined\") return;\n\n const existingId = this.readStoredSession();\n if (existingId) {\n this.emitSessionEvent(SESSION_END_EVENT, existingId, reason);\n }\n\n this.sessionId = undefined;\n this.clearSession();\n\n if (immediate) {\n this.flushSync();\n }\n }\n\n private readStoredUserId(): string | undefined {\n if (typeof localStorage === \"undefined\") return undefined;\n try {\n return localStorage.getItem(USER_ID_KEY) ?? undefined;\n } catch {\n return undefined;\n }\n }\n\n private writeStoredUserId(userId: string): void {\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.setItem(USER_ID_KEY, userId);\n } catch {\n // ignore storage errors\n }\n }\n\n private clearStoredUserId(): void {\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.removeItem(USER_ID_KEY);\n } catch {\n // ignore storage errors\n }\n }\n\n private clearTicketSession(): void {\n this.ticketSessionFetch = null;\n if (typeof localStorage === \"undefined\") return;\n try {\n localStorage.removeItem(TICKET_SESSION_KEY);\n } catch {\n // ignore storage errors\n }\n }\n\n private readStoredTicketSession(): StoredTicketSession | null {\n if (typeof localStorage === \"undefined\") return null;\n try {\n const raw = localStorage.getItem(TICKET_SESSION_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as Partial<StoredTicketSession>;\n if (\n typeof parsed.anonymousId === \"string\" &&\n typeof parsed.token === \"string\" &&\n typeof parsed.expiresAt === \"number\" &&\n parsed.expiresAt > Date.now() + 60_000\n ) {\n return parsed as StoredTicketSession;\n }\n localStorage.removeItem(TICKET_SESSION_KEY);\n } catch {\n // fall through to network fetch\n }\n return null;\n }\n\n /**\n * @internal Used by the widget API helpers. Returns a cached or\n * newly-minted anonymous session token, or `null` if the server\n * has anonymous sessions disabled. Coalesces concurrent callers\n * onto a single in-flight network request so mount-time parallel\n * API calls don't each trigger a `POST /ticket/session`.\n */\n _ensureTicketSession(): Promise<StoredTicketSession | null> {\n const cached = this.readStoredTicketSession();\n if (cached) return Promise.resolve(cached);\n\n if (this.ticketSessionFetch) return this.ticketSessionFetch;\n if (!this.writeKey) return Promise.resolve(null);\n\n this.ticketSessionFetch = (async () => {\n try {\n const res = await fetch(`${this.endpoint}/api/ingest/ticket/session`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.writeKey}`,\n },\n body: \"{}\",\n signal: AbortSignal.timeout(this.timeout),\n });\n if (!res.ok) return null;\n const json = (await res.json()) as StoredTicketSession;\n if (\n typeof json?.anonymousId === \"string\" &&\n typeof json?.token === \"string\" &&\n typeof json?.expiresAt === \"number\"\n ) {\n try {\n localStorage?.setItem(TICKET_SESSION_KEY, JSON.stringify(json));\n } catch {\n // ignore storage errors\n }\n // Keep our in-memory anonymousId in sync with the\n // server-issued one so any code that reads `_getIdentity`\n // outside the ticket flow (e.g. analytics) sees a\n // consistent value.\n this.anonymousId = json.anonymousId;\n return json;\n }\n return null;\n } catch {\n return null;\n } finally {\n this.ticketSessionFetch = null;\n }\n })();\n\n return this.ticketSessionFetch;\n }\n\n /**\n * Return the id of the current session, rotating it when the previous\n * session has expired through inactivity. On rotation it emits a\n * `$session_end` for the timed-out session (backdated to the last\n * recorded activity) and a `$session_start` for the new one. Called on\n * every tracked event so sessions advance with real user activity.\n */\n private ensureSession(now: number): string | undefined {\n if (typeof window === \"undefined\") return this.sessionId;\n\n try {\n const lastActivity = this.readLastActivity();\n const existingId = this.readStoredSession();\n const expired =\n lastActivity === undefined || now - lastActivity > this.sessionTimeout;\n\n if (existingId && !expired) {\n this.sessionId = existingId;\n try {\n localStorage.setItem(LAST_ACTIVITY_KEY, String(now));\n } catch {\n // ignore storage errors\n }\n return existingId;\n }\n\n if (existingId) {\n // Previous session lapsed — close it, backdated to the moment of\n // last activity so the session duration reflects reality rather\n // than the gap until the user returned.\n this.emitSessionEvent(\n SESSION_END_EVENT,\n existingId,\n \"timeout\",\n lastActivity !== undefined\n ? new Date(lastActivity).toISOString()\n : undefined,\n );\n }\n\n const newId = generateAnonId();\n this.persistSession(newId, now);\n this.emitSessionEvent(\n SESSION_START_EVENT,\n newId,\n existingId ? \"timeout\" : \"initial\",\n );\n return newId;\n } catch {\n return this.sessionId ?? generateAnonId();\n }\n }\n\n private readStoredSession(): string | undefined {\n if (this.sessionId) return this.sessionId;\n if (typeof sessionStorage === \"undefined\") return undefined;\n try {\n return sessionStorage.getItem(SESSION_ID_KEY) ?? undefined;\n } catch {\n return undefined;\n }\n }\n\n private readLastActivity(): number | undefined {\n if (typeof localStorage === \"undefined\") return undefined;\n try {\n const raw = localStorage.getItem(LAST_ACTIVITY_KEY);\n return raw ? Number(raw) : undefined;\n } catch {\n return undefined;\n }\n }\n\n private persistSession(id: string, now: number): void {\n this.sessionId = id;\n if (typeof window === \"undefined\") return;\n try {\n sessionStorage.setItem(SESSION_ID_KEY, id);\n localStorage.setItem(LAST_ACTIVITY_KEY, String(now));\n } catch {\n // ignore storage errors\n }\n }\n\n /**\n * Enqueue a session lifecycle event already stamped with its session id\n * (so it never re-enters {@link ensureSession} and risks recursion).\n */\n private emitSessionEvent(\n event: string,\n sessionId: string,\n reason: SessionReason,\n occurredAt?: string,\n ): void {\n if (!this.writeKey) return;\n\n const traitsContext =\n Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};\n\n this.enqueue({\n event,\n properties: { reason },\n occurred_at: occurredAt ?? new Date().toISOString(),\n source: \"browser\",\n org_id: this.orgId,\n user_id: this.userId,\n anonymous_id: this.anonymousId,\n session_id: sessionId,\n context: {\n ...getBrowserContext(),\n ...traitsContext,\n },\n });\n }\n\n private clearSession(): void {\n if (typeof window === \"undefined\") return;\n try {\n sessionStorage.removeItem(SESSION_ID_KEY);\n localStorage.removeItem(LAST_ACTIVITY_KEY);\n } catch {\n // Ignore storage errors\n }\n }\n\n getSessionId(): string | undefined {\n return this.ensureSession(Date.now());\n }\n\n /** @internal Used by the widget to read tracker config. */\n _getEndpoint(): string {\n return this.endpoint;\n }\n\n /** @internal Used by the widget to read the write key. */\n _getWriteKey(): string | null {\n return this.writeKey;\n }\n\n /** @internal Used by the widget to read the current user identity. */\n _getIdentity(): {\n userId?: string;\n anonymousId?: string;\n email?: string;\n name?: string;\n userHash?: string;\n } {\n return {\n userId: this.userId,\n anonymousId: this.anonymousId,\n email: this.traits.email as string | undefined,\n name: this.traits.name as string | undefined,\n userHash: this.userHash,\n };\n }\n\n captureException(error: Error, context?: Record<string, unknown>): void {\n if (!this.writeKey) return;\n\n // `mechanism` is a first-class field; everything else the caller\n // supplies is kept under `extra` so it can't collide with the\n // event's own type/message/stack/context keys.\n const { mechanism, ...extra } = context ?? {};\n sendException(\n this,\n {\n type: error.name || \"Error\",\n message: error.message,\n stack: error.stack,\n handled: true,\n mechanism: typeof mechanism === \"string\" ? mechanism : \"manual\",\n source: \"browser\",\n extra: Object.keys(extra).length > 0 ? extra : undefined,\n },\n getBreadcrumbs,\n );\n }\n\n addBreadcrumb(\n category: string,\n message: string,\n data?: Record<string, unknown>,\n ): void {\n addBreadcrumb({\n type: \"custom\",\n category,\n message,\n timestamp: Date.now(),\n data,\n });\n }\n\n track(event: string, properties?: Record<string, unknown>): void;\n track(event: TrackEvent): void;\n track(\n eventOrName: string | TrackEvent,\n properties?: Record<string, unknown>,\n ): void {\n if (!this.writeKey) return;\n\n const evt: TrackEvent =\n typeof eventOrName === \"string\"\n ? { event: eventOrName, properties }\n : eventOrName;\n\n const traitsContext =\n Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};\n\n const enriched: TrackEvent = {\n ...evt,\n occurred_at: evt.occurred_at ?? new Date().toISOString(),\n source: evt.source ?? \"browser\",\n org_id: evt.org_id ?? this.orgId,\n user_id: evt.user_id ?? this.userId,\n anonymous_id: evt.anonymous_id ?? this.anonymousId,\n session_id: evt.session_id ?? this.ensureSession(Date.now()),\n context: {\n ...getBrowserContext(),\n ...traitsContext,\n ...evt.context,\n },\n };\n\n this.enqueue(enriched);\n\n if (enriched.event === \"$exception\") {\n // Errors are the events most worth not losing — flush eagerly\n // (micro-debounced so a burst in one tick sends a single batch).\n this.scheduleExceptionFlush();\n } else if (this.buffer.length >= this.flushAt) {\n this.maybeFlush();\n }\n }\n\n /**\n * Push an event onto the buffer, evicting the oldest event when the\n * queue is full. `$exception` events are preferentially retained.\n */\n private enqueue(evt: TrackEvent): void {\n if (this.buffer.length >= this.maxQueueSize) {\n const idx = this.buffer.findIndex((e) => e.event !== \"$exception\");\n const dropped = this.buffer.splice(idx === -1 ? 0 : idx, 1);\n if (dropped.length > 0) {\n this.onError?.(\n new Error(\"Event buffer full — dropped oldest event\"),\n dropped,\n );\n }\n }\n this.buffer.push(evt);\n }\n\n /** Flush unless we're inside a retry-backoff window. */\n private maybeFlush(): void {\n if (Date.now() < this.nextFlushAllowedAt) return;\n this.flush().catch(() => {});\n }\n\n private scheduleExceptionFlush(): void {\n if (this.exceptionFlushScheduled) return;\n this.exceptionFlushScheduled = true;\n setTimeout(() => {\n this.exceptionFlushScheduled = false;\n this.maybeFlush();\n }, 0);\n }\n\n async flush(): Promise<FlushResult> {\n if (!this.writeKey || this.buffer.length === 0) {\n return { success: true, ingested: 0 };\n }\n\n const batch = this.buffer.splice(0, MAX_BATCH_SIZE);\n\n try {\n const res = await fetch(`${this.endpoint}/api/ingest/track`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.writeKey}`,\n },\n body: JSON.stringify({ events: batch }),\n signal: AbortSignal.timeout(this.timeout),\n });\n\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n const err = new Error(`API returned ${res.status}: ${body}`);\n // Transient failures (rate limit, timeout, server error) are\n // re-queued for retry with backoff. Other 4xx responses\n // (revoked key, malformed payload) will never succeed —\n // retrying them forever just wastes requests and wedges the\n // buffer, so the batch is dropped.\n const retryable =\n res.status === 408 || res.status === 429 || res.status >= 500;\n if (retryable) {\n this.buffer.unshift(...batch);\n }\n this.recordFlushFailure();\n this.onError?.(err, batch);\n return { success: false, ingested: 0 };\n }\n\n this.flushFailures = 0;\n this.nextFlushAllowedAt = 0;\n const json = (await res.json()) as { ingested: number };\n return { success: true, ingested: json.ingested };\n } catch (error) {\n // Network error or timeout — retryable.\n this.buffer.unshift(...batch);\n this.recordFlushFailure();\n const err = error instanceof Error ? error : new Error(String(error));\n this.onError?.(err, batch);\n return { success: false, ingested: 0 };\n }\n }\n\n /**\n * Record a failed flush and push out the next automatic flush with\n * exponential backoff (capped) so a down ingest endpoint isn't\n * hammered on every `flushInterval` tick. Explicit `flush()` calls\n * are unaffected.\n */\n private recordFlushFailure(): void {\n this.flushFailures += 1;\n const delay = Math.min(\n this.flushInterval * 2 ** this.flushFailures,\n MAX_RETRY_BACKOFF,\n );\n this.nextFlushAllowedAt = Date.now() + delay;\n }\n\n /**\n * Fire-and-forget flush that survives page unload.\n *\n * Uses `fetch` with `keepalive` rather than `navigator.sendBeacon`:\n * beacons are always dispatched with credentials, and a JSON beacon is\n * preflighted — a credentialed preflighted request fails the CORS check\n * against the wildcard (`Access-Control-Allow-Origin: *`) ingest policy,\n * so the browser silently drops the beacon before it is ever sent. A\n * keepalive fetch is non-credentialed by default and carries the normal\n * Bearer header, giving it the exact CORS profile of the regular\n * `flush()` path while still outliving the page.\n */\n private flushSync(): void {\n if (!this.writeKey) return;\n if (typeof fetch === \"undefined\") return;\n if (this.buffer.length === 0) return;\n\n const batch = this.buffer.splice(0, MAX_BATCH_SIZE);\n\n try {\n void fetch(`${this.endpoint}/api/ingest/track`, {\n method: \"POST\",\n keepalive: true,\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.writeKey}`,\n },\n body: JSON.stringify({ events: batch }),\n }).catch(() => {\n // Best effort on unload\n });\n } catch {\n // Best effort on unload\n }\n }\n\n getFlag<T = unknown>(key: string, defaultValue: T): T {\n return this._flagClient?.getFlag(key, defaultValue) ?? defaultValue;\n }\n\n onFlagsReady(callback: () => void): void {\n this._flagClient?.onFlagsReady(callback);\n }\n\n onFlagChange(listener: () => void): () => void {\n return this._flagClient?.onFlagChange(listener) ?? (() => {});\n }\n\n async refreshFlags(): Promise<void> {\n await this._flagClient?.refresh();\n }\n\n async shutdown(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n\n this._flagClient?.destroy();\n this._flagClient = null;\n\n if (typeof window !== \"undefined\") {\n if (this.visibilityHandler) {\n window.removeEventListener(\"visibilitychange\", this.visibilityHandler);\n this.visibilityHandler = null;\n }\n if (this.pageHideHandler) {\n window.removeEventListener(\"pagehide\", this.pageHideHandler);\n this.pageHideHandler = null;\n }\n }\n\n this.uninstallErrorHandlers?.();\n this.uninstallErrorHandlers = null;\n\n while (this.buffer.length > 0) {\n const result = await this.flush();\n // A failed flush either re-queued the batch (retryable) or\n // dropped it (fatal) — either way, looping again can't make\n // progress during teardown.\n if (!result.success) break;\n }\n }\n}\n\n/** Exported for testing — application code should use the default singleton. */\nexport { TrackerImpl };\n\nconst scoperat = new TrackerImpl();\nexport default scoperat;\n","import scoperat from \"./client.js\";\n\nexport default scoperat;\nexport { FlagClient } from \"./flag-client.js\";\nexport type {\n EvaluatedFlag,\n EventSource,\n FlushResult,\n IdentifyOptions,\n IdentifyTraits,\n InitOptions,\n TrackEvent,\n} from \"./types.js\";\n"],"mappings":"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","names":["truncate","getBreadcrumbs"]}
|
package/dist/widget/index.js
CHANGED
|
@@ -1041,54 +1041,107 @@ function getBrowserContext() {
|
|
|
1041
1041
|
|
|
1042
1042
|
// src/errors.ts
|
|
1043
1043
|
var DEDUP_COOLDOWN_MS = 6e4;
|
|
1044
|
+
var MAX_DEDUP_ENTRIES = 200;
|
|
1045
|
+
var MAX_MESSAGE_LENGTH = 2048;
|
|
1046
|
+
var MAX_STACK_LENGTH = 16384;
|
|
1047
|
+
var MAX_REASON_JSON_LENGTH = 1024;
|
|
1044
1048
|
var dedupMap = /* @__PURE__ */ new Map();
|
|
1045
|
-
function
|
|
1049
|
+
function truncate2(str, max) {
|
|
1050
|
+
return str.length > max ? str.slice(0, max) + "\u2026" : str;
|
|
1051
|
+
}
|
|
1052
|
+
function fingerprint(type, message, stack) {
|
|
1046
1053
|
const firstFrame = stack?.split("\n")[1]?.trim() ?? "";
|
|
1047
|
-
return `${type}:${firstFrame}`;
|
|
1054
|
+
return `${type}:${truncate2(message, 100)}:${firstFrame}`;
|
|
1048
1055
|
}
|
|
1049
|
-
function isDuplicate(
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1056
|
+
function isDuplicate(fp) {
|
|
1057
|
+
const now = Date.now();
|
|
1058
|
+
const lastSent = dedupMap.get(fp);
|
|
1059
|
+
if (lastSent && now - lastSent < DEDUP_COOLDOWN_MS) return true;
|
|
1060
|
+
dedupMap.set(fp, now);
|
|
1061
|
+
pruneDedupMap(now);
|
|
1053
1062
|
return false;
|
|
1054
1063
|
}
|
|
1064
|
+
function pruneDedupMap(now) {
|
|
1065
|
+
if (dedupMap.size <= MAX_DEDUP_ENTRIES) return;
|
|
1066
|
+
for (const [key, ts] of dedupMap) {
|
|
1067
|
+
if (now - ts >= DEDUP_COOLDOWN_MS) dedupMap.delete(key);
|
|
1068
|
+
}
|
|
1069
|
+
for (const key of dedupMap.keys()) {
|
|
1070
|
+
if (dedupMap.size <= MAX_DEDUP_ENTRIES) break;
|
|
1071
|
+
dedupMap.delete(key);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1055
1074
|
function sendException(tracker, props, getBreadcrumbs2) {
|
|
1056
|
-
const fp =
|
|
1075
|
+
const fp = fingerprint(props.type, props.message, props.stack);
|
|
1057
1076
|
if (isDuplicate(fp)) return;
|
|
1058
1077
|
tracker.track("$exception", {
|
|
1059
1078
|
...props,
|
|
1079
|
+
message: truncate2(props.message, MAX_MESSAGE_LENGTH),
|
|
1080
|
+
stack: props.stack ? truncate2(props.stack, MAX_STACK_LENGTH) : void 0,
|
|
1060
1081
|
context: { breadcrumbs: getBreadcrumbs2() }
|
|
1061
1082
|
});
|
|
1062
1083
|
}
|
|
1084
|
+
function describeRejectionReason(reason) {
|
|
1085
|
+
if (reason instanceof Error) {
|
|
1086
|
+
return {
|
|
1087
|
+
type: reason.name || "UnhandledRejection",
|
|
1088
|
+
message: reason.message,
|
|
1089
|
+
stack: reason.stack
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
if (typeof reason === "object" && reason !== null) {
|
|
1093
|
+
let json;
|
|
1094
|
+
try {
|
|
1095
|
+
json = JSON.stringify(reason) ?? Object.prototype.toString.call(reason);
|
|
1096
|
+
} catch {
|
|
1097
|
+
json = Object.prototype.toString.call(reason);
|
|
1098
|
+
}
|
|
1099
|
+
return {
|
|
1100
|
+
type: "UnhandledRejection",
|
|
1101
|
+
message: `Non-Error promise rejection: ${truncate2(json, MAX_REASON_JSON_LENGTH)}`,
|
|
1102
|
+
stack: void 0
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
type: "UnhandledRejection",
|
|
1107
|
+
message: String(reason),
|
|
1108
|
+
stack: void 0
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
var uninstallHandlers = null;
|
|
1063
1112
|
function installGlobalHandlers(tracker, getBreadcrumbs2) {
|
|
1064
|
-
if (typeof window === "undefined") return
|
|
1065
|
-
|
|
1066
|
-
|
|
1113
|
+
if (typeof window === "undefined") return () => {
|
|
1114
|
+
};
|
|
1115
|
+
if (uninstallHandlers) return uninstallHandlers;
|
|
1116
|
+
const onError = (event) => {
|
|
1117
|
+
const error = event.error;
|
|
1118
|
+
const message = error?.message || (typeof event.message === "string" && event.message ? event.message : "Unknown error");
|
|
1119
|
+
const opaque = !error && /^Script error\.?$/.test(message);
|
|
1067
1120
|
sendException(
|
|
1068
1121
|
tracker,
|
|
1069
1122
|
{
|
|
1070
|
-
type:
|
|
1071
|
-
message
|
|
1072
|
-
stack:
|
|
1123
|
+
type: error?.name || "Error",
|
|
1124
|
+
message,
|
|
1125
|
+
stack: error?.stack,
|
|
1073
1126
|
handled: false,
|
|
1074
1127
|
mechanism: "onerror",
|
|
1075
1128
|
source: "browser",
|
|
1076
|
-
filename,
|
|
1077
|
-
lineno,
|
|
1078
|
-
colno
|
|
1129
|
+
filename: event.filename || void 0,
|
|
1130
|
+
lineno: event.lineno || void 0,
|
|
1131
|
+
colno: event.colno || void 0,
|
|
1132
|
+
...opaque ? { opaque: true } : {}
|
|
1079
1133
|
},
|
|
1080
1134
|
getBreadcrumbs2
|
|
1081
1135
|
);
|
|
1082
1136
|
};
|
|
1083
|
-
|
|
1084
|
-
const
|
|
1085
|
-
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
1137
|
+
const onRejection = (event) => {
|
|
1138
|
+
const { type, message, stack } = describeRejectionReason(event.reason);
|
|
1086
1139
|
sendException(
|
|
1087
1140
|
tracker,
|
|
1088
1141
|
{
|
|
1089
|
-
type
|
|
1090
|
-
message
|
|
1091
|
-
stack
|
|
1142
|
+
type,
|
|
1143
|
+
message,
|
|
1144
|
+
stack,
|
|
1092
1145
|
handled: false,
|
|
1093
1146
|
mechanism: "onunhandledrejection",
|
|
1094
1147
|
source: "browser"
|
|
@@ -1096,6 +1149,14 @@ function installGlobalHandlers(tracker, getBreadcrumbs2) {
|
|
|
1096
1149
|
getBreadcrumbs2
|
|
1097
1150
|
);
|
|
1098
1151
|
};
|
|
1152
|
+
window.addEventListener("error", onError);
|
|
1153
|
+
window.addEventListener("unhandledrejection", onRejection);
|
|
1154
|
+
uninstallHandlers = () => {
|
|
1155
|
+
window.removeEventListener("error", onError);
|
|
1156
|
+
window.removeEventListener("unhandledrejection", onRejection);
|
|
1157
|
+
uninstallHandlers = null;
|
|
1158
|
+
};
|
|
1159
|
+
return uninstallHandlers;
|
|
1099
1160
|
}
|
|
1100
1161
|
|
|
1101
1162
|
// src/flag-client.ts
|
|
@@ -1200,7 +1261,9 @@ var DEFAULT_FLUSH_AT = 10;
|
|
|
1200
1261
|
var DEFAULT_FLUSH_INTERVAL = 3e3;
|
|
1201
1262
|
var DEFAULT_TIMEOUT = 1e4;
|
|
1202
1263
|
var DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1e3;
|
|
1264
|
+
var DEFAULT_MAX_QUEUE_SIZE = 1e3;
|
|
1203
1265
|
var MAX_BATCH_SIZE = 100;
|
|
1266
|
+
var MAX_RETRY_BACKOFF = 6e4;
|
|
1204
1267
|
var ANON_ID_KEY = "__scoperat_anon_id";
|
|
1205
1268
|
var SESSION_ID_KEY = "__scoperat_session_id";
|
|
1206
1269
|
var LAST_ACTIVITY_KEY = "__scoperat_last_activity";
|
|
@@ -1241,8 +1304,16 @@ var TrackerImpl = class {
|
|
|
1241
1304
|
timeout = DEFAULT_TIMEOUT;
|
|
1242
1305
|
onError;
|
|
1243
1306
|
buffer = [];
|
|
1307
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
|
|
1244
1308
|
timer = null;
|
|
1245
1309
|
visibilityHandler = null;
|
|
1310
|
+
pageHideHandler = null;
|
|
1311
|
+
uninstallErrorHandlers = null;
|
|
1312
|
+
/** Consecutive flush failures, drives exponential retry backoff. */
|
|
1313
|
+
flushFailures = 0;
|
|
1314
|
+
/** Timestamp before which automatic flushes are suppressed (backoff). */
|
|
1315
|
+
nextFlushAllowedAt = 0;
|
|
1316
|
+
exceptionFlushScheduled = false;
|
|
1246
1317
|
userId;
|
|
1247
1318
|
orgId;
|
|
1248
1319
|
traits = {};
|
|
@@ -1260,12 +1331,12 @@ var TrackerImpl = class {
|
|
|
1260
1331
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
1261
1332
|
this.onError = options.onError;
|
|
1262
1333
|
this.sessionTimeout = options.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
|
|
1334
|
+
this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
1263
1335
|
this.anonymousId = getOrCreateAnonId();
|
|
1264
1336
|
if (this.timer) clearInterval(this.timer);
|
|
1265
1337
|
this.timer = setInterval(() => {
|
|
1266
1338
|
if (this.buffer.length > 0) {
|
|
1267
|
-
this.
|
|
1268
|
-
});
|
|
1339
|
+
this.maybeFlush();
|
|
1269
1340
|
}
|
|
1270
1341
|
}, this.flushInterval);
|
|
1271
1342
|
if (typeof window !== "undefined") {
|
|
@@ -1278,9 +1349,18 @@ var TrackerImpl = class {
|
|
|
1278
1349
|
}
|
|
1279
1350
|
};
|
|
1280
1351
|
window.addEventListener("visibilitychange", this.visibilityHandler);
|
|
1352
|
+
if (this.pageHideHandler) {
|
|
1353
|
+
window.removeEventListener("pagehide", this.pageHideHandler);
|
|
1354
|
+
}
|
|
1355
|
+
this.pageHideHandler = () => {
|
|
1356
|
+
if (this.buffer.length > 0) {
|
|
1357
|
+
this.flushSync();
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
window.addEventListener("pagehide", this.pageHideHandler);
|
|
1281
1361
|
}
|
|
1282
1362
|
if (options.captureErrors) {
|
|
1283
|
-
installGlobalHandlers(this, getBreadcrumbs);
|
|
1363
|
+
this.uninstallErrorHandlers = installGlobalHandlers(this, getBreadcrumbs);
|
|
1284
1364
|
startBreadcrumbCollection();
|
|
1285
1365
|
}
|
|
1286
1366
|
this._flagClient?.destroy();
|
|
@@ -1535,7 +1615,7 @@ var TrackerImpl = class {
|
|
|
1535
1615
|
emitSessionEvent(event, sessionId, reason, occurredAt) {
|
|
1536
1616
|
if (!this.writeKey) return;
|
|
1537
1617
|
const traitsContext = Object.keys(this.traits).length > 0 ? { traits: this.traits } : {};
|
|
1538
|
-
this.
|
|
1618
|
+
this.enqueue({
|
|
1539
1619
|
event,
|
|
1540
1620
|
properties: { reason },
|
|
1541
1621
|
occurred_at: occurredAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1581,17 +1661,20 @@ var TrackerImpl = class {
|
|
|
1581
1661
|
}
|
|
1582
1662
|
captureException(error, context) {
|
|
1583
1663
|
if (!this.writeKey) return;
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1664
|
+
const { mechanism, ...extra } = context ?? {};
|
|
1665
|
+
sendException(
|
|
1666
|
+
this,
|
|
1667
|
+
{
|
|
1668
|
+
type: error.name || "Error",
|
|
1669
|
+
message: error.message,
|
|
1670
|
+
stack: error.stack,
|
|
1671
|
+
handled: true,
|
|
1672
|
+
mechanism: typeof mechanism === "string" ? mechanism : "manual",
|
|
1673
|
+
source: "browser",
|
|
1674
|
+
extra: Object.keys(extra).length > 0 ? extra : void 0
|
|
1675
|
+
},
|
|
1676
|
+
getBreadcrumbs
|
|
1677
|
+
);
|
|
1595
1678
|
}
|
|
1596
1679
|
addBreadcrumb(category, message, data) {
|
|
1597
1680
|
addBreadcrumb({
|
|
@@ -1620,12 +1703,44 @@ var TrackerImpl = class {
|
|
|
1620
1703
|
...evt.context
|
|
1621
1704
|
}
|
|
1622
1705
|
};
|
|
1623
|
-
this.
|
|
1624
|
-
if (
|
|
1625
|
-
this.
|
|
1626
|
-
|
|
1706
|
+
this.enqueue(enriched);
|
|
1707
|
+
if (enriched.event === "$exception") {
|
|
1708
|
+
this.scheduleExceptionFlush();
|
|
1709
|
+
} else if (this.buffer.length >= this.flushAt) {
|
|
1710
|
+
this.maybeFlush();
|
|
1627
1711
|
}
|
|
1628
1712
|
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Push an event onto the buffer, evicting the oldest event when the
|
|
1715
|
+
* queue is full. `$exception` events are preferentially retained.
|
|
1716
|
+
*/
|
|
1717
|
+
enqueue(evt) {
|
|
1718
|
+
if (this.buffer.length >= this.maxQueueSize) {
|
|
1719
|
+
const idx = this.buffer.findIndex((e) => e.event !== "$exception");
|
|
1720
|
+
const dropped = this.buffer.splice(idx === -1 ? 0 : idx, 1);
|
|
1721
|
+
if (dropped.length > 0) {
|
|
1722
|
+
this.onError?.(
|
|
1723
|
+
new Error("Event buffer full \u2014 dropped oldest event"),
|
|
1724
|
+
dropped
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
this.buffer.push(evt);
|
|
1729
|
+
}
|
|
1730
|
+
/** Flush unless we're inside a retry-backoff window. */
|
|
1731
|
+
maybeFlush() {
|
|
1732
|
+
if (Date.now() < this.nextFlushAllowedAt) return;
|
|
1733
|
+
this.flush().catch(() => {
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
scheduleExceptionFlush() {
|
|
1737
|
+
if (this.exceptionFlushScheduled) return;
|
|
1738
|
+
this.exceptionFlushScheduled = true;
|
|
1739
|
+
setTimeout(() => {
|
|
1740
|
+
this.exceptionFlushScheduled = false;
|
|
1741
|
+
this.maybeFlush();
|
|
1742
|
+
}, 0);
|
|
1743
|
+
}
|
|
1629
1744
|
async flush() {
|
|
1630
1745
|
if (!this.writeKey || this.buffer.length === 0) {
|
|
1631
1746
|
return { success: true, ingested: 0 };
|
|
@@ -1643,17 +1758,41 @@ var TrackerImpl = class {
|
|
|
1643
1758
|
});
|
|
1644
1759
|
if (!res.ok) {
|
|
1645
1760
|
const body = await res.text().catch(() => "");
|
|
1646
|
-
|
|
1761
|
+
const err = new Error(`API returned ${res.status}: ${body}`);
|
|
1762
|
+
const retryable = res.status === 408 || res.status === 429 || res.status >= 500;
|
|
1763
|
+
if (retryable) {
|
|
1764
|
+
this.buffer.unshift(...batch);
|
|
1765
|
+
}
|
|
1766
|
+
this.recordFlushFailure();
|
|
1767
|
+
this.onError?.(err, batch);
|
|
1768
|
+
return { success: false, ingested: 0 };
|
|
1647
1769
|
}
|
|
1770
|
+
this.flushFailures = 0;
|
|
1771
|
+
this.nextFlushAllowedAt = 0;
|
|
1648
1772
|
const json = await res.json();
|
|
1649
1773
|
return { success: true, ingested: json.ingested };
|
|
1650
1774
|
} catch (error) {
|
|
1651
1775
|
this.buffer.unshift(...batch);
|
|
1776
|
+
this.recordFlushFailure();
|
|
1652
1777
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1653
1778
|
this.onError?.(err, batch);
|
|
1654
1779
|
return { success: false, ingested: 0 };
|
|
1655
1780
|
}
|
|
1656
1781
|
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Record a failed flush and push out the next automatic flush with
|
|
1784
|
+
* exponential backoff (capped) so a down ingest endpoint isn't
|
|
1785
|
+
* hammered on every `flushInterval` tick. Explicit `flush()` calls
|
|
1786
|
+
* are unaffected.
|
|
1787
|
+
*/
|
|
1788
|
+
recordFlushFailure() {
|
|
1789
|
+
this.flushFailures += 1;
|
|
1790
|
+
const delay = Math.min(
|
|
1791
|
+
this.flushInterval * 2 ** this.flushFailures,
|
|
1792
|
+
MAX_RETRY_BACKOFF
|
|
1793
|
+
);
|
|
1794
|
+
this.nextFlushAllowedAt = Date.now() + delay;
|
|
1795
|
+
}
|
|
1657
1796
|
/**
|
|
1658
1797
|
* Fire-and-forget flush that survives page unload.
|
|
1659
1798
|
*
|
|
@@ -1705,12 +1844,21 @@ var TrackerImpl = class {
|
|
|
1705
1844
|
}
|
|
1706
1845
|
this._flagClient?.destroy();
|
|
1707
1846
|
this._flagClient = null;
|
|
1708
|
-
if (typeof window !== "undefined"
|
|
1709
|
-
|
|
1710
|
-
|
|
1847
|
+
if (typeof window !== "undefined") {
|
|
1848
|
+
if (this.visibilityHandler) {
|
|
1849
|
+
window.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
1850
|
+
this.visibilityHandler = null;
|
|
1851
|
+
}
|
|
1852
|
+
if (this.pageHideHandler) {
|
|
1853
|
+
window.removeEventListener("pagehide", this.pageHideHandler);
|
|
1854
|
+
this.pageHideHandler = null;
|
|
1855
|
+
}
|
|
1711
1856
|
}
|
|
1857
|
+
this.uninstallErrorHandlers?.();
|
|
1858
|
+
this.uninstallErrorHandlers = null;
|
|
1712
1859
|
while (this.buffer.length > 0) {
|
|
1713
|
-
await this.flush();
|
|
1860
|
+
const result = await this.flush();
|
|
1861
|
+
if (!result.success) break;
|
|
1714
1862
|
}
|
|
1715
1863
|
}
|
|
1716
1864
|
};
|