@kanca-app/ikas 0.2.0 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/storefront.ts","../src/shared.ts"],"sourcesContent":["// @kanca-app/ikas/storefront — errors and load cost of the app's own script on\n// merchants' ikas storefronts. Zero dependencies, runs on third-party pages:\n// it never throws, never blocks, never retries, never calls preventDefault and\n// never reads cookies, storage, customer or cart data.\n// Wire format: docs/PROTOCOL.md → `storefront`.\n\nimport { DEFAULT_ENDPOINT, SDK_NAME, SDK_VERSION, uuid, type KancaEvent } from \"./shared\";\n\nexport interface StorefrontOptions {\n /** Public key (`kp_...`). Safe to ship in the script. */\n publicKey: string | undefined;\n /** Kanca base URL. Default `https://kanca.onurhan.dev`. */\n endpoint?: string;\n /** Label for this script in the dashboard, e.g. `\"rush\"`. Default `\"script\"`. */\n scriptName?: string;\n /**\n * Identifies YOUR script in error stacks and resource timing. A string is\n * matched as a substring, a RegExp with `search`. Default: the URL (without\n * query) of `document.currentScript`, so call `initKancaStorefront`\n * synchronously while your script first runs. Without either, global\n * errors are not captured (manual `captureError` and `wrap` still work).\n */\n scriptUrlMatch?: string | RegExp;\n /** The ikas merchant id, if the script knows it. Can be set later with `setMerchantId`. */\n merchantId?: string;\n /**\n * Share of page views that report `load` and `timing:*` events, 0..1.\n * Default 0.1. Errors are always reported. Values below 0.001 (other than 0) are raised to 0.001.\n */\n sampleRate?: number;\n /** Your script's version, e.g. a git sha or package version. */\n release?: string;\n debug?: boolean;\n}\n\nexport interface TimingOptions {\n /** Outcome, when there is no HTTP status. With a status, ok = status < 400. */\n ok?: boolean;\n status?: number;\n}\n\nexport interface KancaStorefront {\n /** Records an error thrown by your script. Deduped and capped per page view. */\n captureError(error: unknown, context?: { where?: string }): void;\n /**\n * Returns `fn` wrapped: an error it throws (or a promise it returns that\n * rejects) is recorded and then rethrown / left rejected, so the page sees\n * exactly what it would have seen without Kanca.\n */\n wrap<F extends (...args: never[]) => unknown>(fn: F, where?: string): F;\n /** Records a duration (sampled page views only). `name`: 1–40 chars of A–Z a–z 0–9 _ . - */\n timing(name: string, durationMs: number, options?: TimingOptions): void;\n /** `fetch` that records `timing:<name>` with the status. Same result and errors as `fetch`. */\n fetch(name: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;\n /** Marks your script as ready (e.g. first render done). Only the first call counts. */\n ready(): void;\n setMerchantId(merchantId: string | undefined): void;\n /** Sends what is queued now. Normally not needed: events go out when the page is hidden. */\n flush(): void;\n}\n\nexport const MAX_ERRORS_PER_VIEW = 10;\nexport const MAX_TIMINGS_PER_VIEW = 20;\n/** Queue size that triggers an early send. */\nexport const FLUSH_AT = 10;\nconst MAX_MESSAGE = 300;\n\nconst clip = (s: string, n: number) => (s.length > n ? s.slice(0, n) : s);\nconst round1 = (n: number) => Math.round(n * 10) / 10;\nconst now = () => (typeof performance !== \"undefined\" && performance.now ? performance.now() : Date.now());\n\nconst UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;\n\n/**\n * URL path → low-cardinality pattern without identifiers: uuids → `:id`,\n * numeric segments → `:n`, e-mails → `:email`, long opaque tokens → `:token`,\n * digit runs of 5+ inside a slug → `:n`. Query and hash are dropped, at most\n * 8 segments are kept.\n */\nexport function pathPattern(path: string): string {\n const segs = path.split(/[?#]/)[0]!.split(\"/\").filter(Boolean).slice(0, 8);\n return clip(\n \"/\" +\n segs\n .map((s) =>\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)\n ? \":id\"\n : /^\\d+$/.test(s)\n ? \":n\"\n : /@|%40/.test(s)\n ? \":email\"\n : s.length > 24 && /\\d/.test(s) && !/-.*-/.test(s)\n ? \":token\"\n : clip(s.replace(/\\d{5,}/g, \":n\"), 60),\n )\n .join(\"/\"),\n 200,\n );\n}\n\n/** Strips likely PII / identifiers from an error message and caps its length. */\nexport function scrubMessage(message: string): string {\n return clip(\n message\n .replace(/(https?:\\/\\/[^\\s?#\"'<>()]*)[?#][^\\s\"'<>()]*/g, \"$1\")\n .replace(/[\\w.+-]+@[\\w-]+\\.[\\w.-]+/g, \"<email>\")\n .replace(UUID, \"<id>\")\n .replace(/[\\w-]{32,}/g, (m) => (/\\d/.test(m) ? \"<token>\" : m))\n .replace(/\\d{6,}/g, \"<n>\"),\n MAX_MESSAGE,\n );\n}\n\n/**\n * The topmost stack frame that has a URL, as `file:line:col` (last path\n * segment, no query), if that frame is ours. Frames without a URL (native\n * code such as `JSON.parse`) are skipped, so an error thrown by a built-in\n * called from our code is ours, while an error thrown by theme code that our\n * code called is not. Returns undefined when the top URL frame is not ours.\n */\nexport function ownFrame(stack: unknown, isOurs: (s: string) => boolean): string | undefined {\n if (typeof stack !== \"string\") return undefined;\n for (const line of stack.split(\"\\n\")) {\n const m = /(https?:\\/\\/[^\\s)]+?):(\\d+):(\\d+)/.exec(line);\n if (!m) continue;\n if (!isOurs(line)) return undefined;\n const file = m[1]!.split(/[?#]/)[0]!.split(\"/\").pop() || \"script\";\n return clip(`${file}:${m[2]}:${m[3]}`, 200);\n }\n return undefined;\n}\n\n/** Name + scrubbed message of anything thrown. */\nexport function describeError(err: unknown): { code: string; message?: string } {\n const e = err as { name?: unknown; message?: unknown } | null;\n if (e && typeof e === \"object\" && typeof e.message === \"string\") {\n const name = typeof e.name === \"string\" && e.name ? e.name : \"Error\";\n return { code: clip(name, 100), message: scrubMessage(e.message) || undefined };\n }\n if (typeof err === \"string\") return { code: \"Error\", message: scrubMessage(err) || undefined };\n return { code: \"NonError\", message: typeof err };\n}\n\nconst noop = () => undefined;\n\nfunction disabled(): KancaStorefront {\n return {\n captureError: noop,\n wrap: (fn) => fn,\n timing: noop,\n fetch: (_name, input, init) => fetch(input, init),\n ready: noop,\n setMerchantId: noop,\n flush: noop,\n };\n}\n\n// Global (not module) registry so two copies of the script on one page share one instance per scriptName.\nconst REG = \"__kancaStorefront\";\nconst g = globalThis as { [REG]?: Record<string, KancaStorefront> };\n\n/**\n * Starts storefront monitoring for one script. Call it synchronously at the\n * top of your script. Returns an instance whose methods never throw. Safe to\n * call more than once: the same `scriptName` returns the same instance.\n */\nexport function initKancaStorefront(options: StorefrontOptions): KancaStorefront {\n try {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return disabled();\n const o = options || ({} as StorefrontOptions);\n const script = clip(o.scriptName || \"script\", 64);\n const reg = (g[REG] ||= {});\n if (reg[script]) return reg[script]!;\n const key = typeof o.publicKey === \"string\" ? o.publicKey.trim() : \"\";\n const debug = !!o.debug;\n if (!key.startsWith(\"kp_\")) {\n if (debug || key) console.warn(\"[kanca] storefront: kp_ key required\");\n return disabled();\n }\n\n const url = `${(o.endpoint || DEFAULT_ENDPOINT).replace(/\\/+$/, \"\")}/api/ingest/browser?key=${encodeURIComponent(key)}`;\n let match = o.scriptUrlMatch;\n if (!match) {\n const src = (document.currentScript as HTMLScriptElement | null)?.src;\n if (src && /^https?:/.test(src)) match = src.split(/[?#]/)[0];\n }\n const isOurs = (s: string) =>\n !!match && !!s && (typeof match === \"string\" ? s.indexOf(match) >= 0 : s.search(match) >= 0);\n\n let rate = typeof o.sampleRate === \"number\" && o.sampleRate >= 0 ? Math.min(1, o.sampleRate) : 0.1;\n if (rate > 0 && rate < 0.001) rate = 0.001;\n const sampled = rate > 0 && Math.random() < rate;\n const path0 = pathPattern(location.pathname || \"/\");\n const pv = uuid().replace(/-/g, \"\").slice(0, 16);\n const t0 = now();\n const ts0 = Date.now();\n let merchantId = o.merchantId;\n let readyMs: number | undefined;\n let loadDone = false;\n let errors = 0;\n let timings = 0;\n const seen = new Set<string>();\n const captured = new WeakSet<object>();\n let queue: KancaEvent[] = [];\n\n const event = (name: string, fields: Partial<KancaEvent>, attrs?: Record<string, string | number | undefined>): KancaEvent => {\n const a: Record<string, string | number> = {\n host: clip(location.hostname || \"unknown\", 200),\n path: pathPattern(location.pathname || \"/\"),\n script,\n pv,\n sample: name === \"error\" ? 1 : rate,\n };\n if (o.release) a.release = clip(String(o.release), 200);\n for (const k in attrs) if (attrs[k] !== undefined) a[k] = attrs[k]!;\n return { id: uuid(), type: \"storefront\", ts: Date.now(), name, ...fields, attrs: a };\n };\n\n const send = (final: boolean) => {\n try {\n if (final && sampled && !loadDone) {\n loadDone = true;\n queue.push(loadEvent());\n }\n if (!queue.length) return;\n const events = queue;\n queue = [];\n if (merchantId) for (const e of events) e.merchantId = clip(merchantId, 200);\n const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });\n if (debug) console.debug(\"[kanca] storefront\", events);\n // fetch+keepalive first: content blockers drop sendBeacon (\"ping\")\n // requests while sendBeacon still returns true. text/plain keeps it a\n // simple request (no CORS preflight). Never retried.\n try {\n if (typeof fetch === \"function\") {\n fetch(url, { method: \"POST\", body, keepalive: true, mode: \"cors\", credentials: \"omit\", headers: { \"content-type\": \"text/plain\" } }).catch(noop);\n return;\n }\n } catch {\n /* fall through */\n }\n navigator.sendBeacon?.(url, body);\n } catch {\n /* never throw */\n }\n };\n\n const push = (e: KancaEvent) => {\n queue.push(e);\n if (queue.length >= FLUSH_AT) send(false);\n };\n\n // One per sampled page view: the page-view record behind every storefront metric.\n const loadEvent = (): KancaEvent => {\n const attrs: Record<string, string | number | undefined> = { path: path0, errors, ready_ms: readyMs };\n const fields: Partial<KancaEvent> = {};\n try {\n const entries = performance.getEntriesByType(\"resource\") as PerformanceResourceTiming[];\n for (let i = entries.length - 1; i >= 0; i--) {\n const r = entries[i]!;\n if (!isOurs(r.name)) continue;\n if (r.responseEnd > 0) fields.durationMs = round1(r.responseEnd - r.startTime);\n // Sizes are only exposed with Timing-Allow-Origin; 0/0 means \"unknown\".\n if (r.encodedBodySize > 0) {\n fields.value = r.transferSize;\n attrs.encoded = r.encodedBodySize;\n attrs.cache = r.transferSize === 0 ? \"hit\" : r.transferSize < r.encodedBodySize ? \"revalidated\" : \"network\";\n }\n break;\n }\n } catch {\n /* no resource timing */\n }\n const e = event(\"load\", fields, attrs);\n e.ts = ts0;\n return e;\n };\n\n const captureError = (err: unknown, where?: string) => {\n try {\n if (err && typeof err === \"object\") {\n if (captured.has(err)) return;\n captured.add(err);\n }\n if (errors >= MAX_ERRORS_PER_VIEW) return;\n const d = describeError(err);\n const frame = ownFrame((err as { stack?: unknown } | null)?.stack, isOurs);\n const k = `${d.code}|${d.message}|${frame}`;\n if (seen.has(k)) return;\n seen.add(k);\n errors++;\n push(event(\"error\", { ok: false, error: d }, { where: where ? clip(String(where), 100) : undefined, frame }));\n } catch {\n /* ignore */\n }\n };\n\n const timing = (name: string, ms: number, t?: TimingOptions) => {\n try {\n if (!sampled || timings >= MAX_TIMINGS_PER_VIEW) return;\n if (!/^[A-Za-z0-9_.-]{1,40}$/.test(name) || !(ms >= 0 && ms < 864e5)) {\n if (debug) console.warn(\"[kanca] bad timing\", name, ms);\n return;\n }\n timings++;\n const f: Partial<KancaEvent> = { durationMs: round1(ms) };\n if (t && typeof t.status === \"number\" && t.status >= 100 && t.status <= 599) f.status = Math.trunc(t.status);\n if (t && typeof t.ok === \"boolean\") f.ok = t.ok;\n push(event(`timing:${name}`, f));\n } catch {\n /* ignore */\n }\n };\n\n if (match) {\n // Only errors from our script: the page's theme and other apps are never recorded.\n // Attribution: the top URL frame of the stack, else the event's filename.\n const ours = (err: unknown, filename?: string) => {\n const stack = (err as { stack?: unknown } | null)?.stack;\n return typeof stack === \"string\" && /https?:/.test(stack) ? !!ownFrame(stack, isOurs) : !!filename && isOurs(filename);\n };\n addEventListener(\"error\", (ev: ErrorEvent) => {\n try {\n if (ours(ev.error, ev.filename)) captureError(ev.error ?? ev.message, \"onerror\");\n } catch {\n /* ignore */\n }\n });\n addEventListener(\"unhandledrejection\", (ev: PromiseRejectionEvent) => {\n try {\n if (ours(ev.reason)) captureError(ev.reason, \"unhandledrejection\");\n } catch {\n /* ignore */\n }\n });\n } else if (debug) {\n console.warn(\"[kanca] storefront: no script URL, global errors off\");\n }\n\n addEventListener(\n \"visibilitychange\",\n () => {\n if (document.visibilityState === \"hidden\") send(true);\n },\n true,\n );\n addEventListener(\"pagehide\", () => send(true), true);\n\n const instance: KancaStorefront = {\n captureError: (err, ctx) => captureError(err, ctx?.where),\n wrap(fn, where) {\n if (typeof fn !== \"function\") return fn;\n return function (this: unknown, ...args: never[]) {\n let r: unknown;\n try {\n r = fn.apply(this, args);\n } catch (e) {\n captureError(e, where);\n throw e;\n }\n try {\n if (r && typeof (r as Promise<unknown>).then === \"function\") {\n // A side branch: the returned promise is untouched and still rejects for the caller.\n (r as Promise<unknown>).then(undefined, (e) => captureError(e, where));\n }\n } catch {\n /* ignore */\n }\n return r;\n } as unknown as typeof fn;\n },\n timing,\n async fetch(name, input, init) {\n const t = now();\n let res: Response;\n try {\n res = await fetch(input, init);\n } catch (e) {\n timing(name, now() - t, { ok: false });\n throw e;\n }\n timing(name, now() - t, { status: res.status });\n return res;\n },\n ready() {\n if (readyMs === undefined) readyMs = Math.round(now() - t0);\n },\n setMerchantId(id) {\n merchantId = typeof id === \"string\" && id ? id : undefined;\n },\n flush: () => send(false),\n };\n reg[script] = instance;\n return instance;\n } catch {\n return disabled();\n }\n}\n\n/** @internal for tests */\nexport function __resetStorefrontForTests(): void {\n delete g[REG];\n}\n","// Shared, runtime-agnostic helpers (used by both the Node and browser entries).\n// Wire format: see docs/PROTOCOL.md (v1).\n\nexport const SDK_NAME = \"@kanca-app/ikas\";\nexport const SDK_VERSION = \"0.2.0\";\nexport const DEFAULT_ENDPOINT = \"https://kanca.onurhan.dev\";\n\nexport const MAX_NAME = 200;\nexport const MAX_ERROR_MESSAGE = 500;\nexport const MAX_ERROR_CODE = 100;\nexport const MAX_ATTR_KEYS = 20;\nexport const MAX_ATTR_STRING = 500;\nexport const MAX_ATTR_KEY = 64;\nexport const MAX_EVENTS_PER_BATCH = 500;\n/** Protocol limit is 512 KB; keep headroom for the envelope. */\nexport const MAX_BATCH_BYTES = 480 * 1024;\n\nexport type EventType = \"webhook\" | \"graphql\" | \"rest\" | \"install\" | \"uninstall\" | \"vital\" | \"storefront\";\n\nexport type AttrValue = string | number | boolean;\n\nexport interface KancaError {\n code: string;\n message?: string;\n}\n\n/** A single wire event (PROTOCOL.md → Event). */\nexport interface KancaEvent {\n id: string;\n type: EventType;\n ts: number;\n name: string;\n status?: number;\n ok?: boolean;\n durationMs?: number;\n value?: number;\n error?: KancaError;\n merchantId?: string;\n deliveryId?: string;\n attrs?: Record<string, AttrValue>;\n}\n\nexport interface Batch {\n sdk: { name: string; version: string };\n events: KancaEvent[];\n}\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max) : s;\n}\n\nfunction hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\n/** UUID v4 via crypto.randomUUID, with a getRandomValues fallback (non-secure browser contexts). */\nexport function uuid(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === \"function\") return c.randomUUID();\n const b = new Uint8Array(16);\n if (c && typeof c.getRandomValues === \"function\") c.getRandomValues(b);\n else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);\n b[6] = (b[6]! & 0x0f) | 0x40;\n b[8] = (b[8]! & 0x3f) | 0x80;\n const h = hex(b);\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === \"number\" && Number.isFinite(v);\n}\n\nfunction cleanAttrs(attrs: unknown): Record<string, AttrValue> | undefined {\n if (!attrs || typeof attrs !== \"object\") return undefined;\n const out: Record<string, AttrValue> = {};\n let n = 0;\n for (const [k, v] of Object.entries(attrs as Record<string, unknown>)) {\n if (n >= MAX_ATTR_KEYS) break;\n if (v === undefined || v === null || !k) continue;\n let val: AttrValue;\n if (typeof v === \"boolean\") val = v;\n else if (typeof v === \"number\") {\n if (!Number.isFinite(v)) continue;\n val = v;\n } else if (typeof v === \"string\") val = truncate(v, MAX_ATTR_STRING);\n else continue; // attrs are flat: nested values are dropped, never stringified\n out[truncate(k, MAX_ATTR_KEY)] = val;\n n++;\n }\n return n > 0 ? out : undefined;\n}\n\n/**\n * Normalises an event so it always satisfies the protocol limits. Unknown\n * fields are dropped — only the whitelisted metadata below ever leaves the process.\n */\nexport function sanitizeEvent(e: KancaEvent): KancaEvent {\n const out: KancaEvent = {\n id: typeof e.id === \"string\" && e.id ? e.id : uuid(),\n type: e.type,\n ts: isFiniteNumber(e.ts) ? Math.round(e.ts) : Date.now(),\n name: truncate(typeof e.name === \"string\" && e.name ? e.name : \"unknown\", MAX_NAME),\n };\n if (isFiniteNumber(e.status) && e.status >= 100 && e.status <= 599) out.status = Math.trunc(e.status);\n if (typeof e.ok === \"boolean\") out.ok = e.ok;\n if (isFiniteNumber(e.durationMs)) out.durationMs = Math.min(86_400_000, Math.max(0, Math.round(e.durationMs * 10) / 10));\n if (isFiniteNumber(e.value)) out.value = e.value;\n if (e.error && typeof e.error.code === \"string\") {\n out.error = { code: truncate(e.error.code, MAX_ERROR_CODE) };\n if (typeof e.error.message === \"string\" && e.error.message) {\n out.error.message = truncate(e.error.message, MAX_ERROR_MESSAGE);\n }\n }\n if (typeof e.merchantId === \"string\" && e.merchantId) out.merchantId = truncate(e.merchantId, 200);\n if (typeof e.deliveryId === \"string\" && e.deliveryId) out.deliveryId = truncate(e.deliveryId, 200);\n const attrs = cleanAttrs(e.attrs);\n if (attrs) out.attrs = attrs;\n return out;\n}\n\nexport function errorMessage(err: unknown): string | undefined {\n try {\n if (err instanceof Error) return truncate(err.message || err.name, MAX_ERROR_MESSAGE);\n if (typeof err === \"string\") return truncate(err, MAX_ERROR_MESSAGE);\n } catch {\n /* ignore */\n }\n return undefined;\n}\n\nexport function joinUrl(endpoint: string, path: string): string {\n return endpoint.replace(/\\/+$/, \"\") + path;\n}\n\nconst encoder = typeof TextEncoder !== \"undefined\" ? /* @__PURE__ */ new TextEncoder() : undefined;\nexport function byteLength(s: string): number {\n return encoder ? encoder.encode(s).length : s.length * 3;\n}\n\n/**\n * Splits events into protocol-sized batches (<= maxEvents events and\n * <= MAX_BATCH_BYTES serialized). Returns serialized bodies.\n */\nexport function buildBodies(events: KancaEvent[], maxEvents: number): { body: string; events: KancaEvent[] }[] {\n const sdk = { name: SDK_NAME, version: SDK_VERSION };\n const envelope = byteLength(JSON.stringify({ sdk, events: [] }));\n const out: { body: string; events: KancaEvent[] }[] = [];\n let cur: KancaEvent[] = [];\n let size = envelope;\n const push = () => {\n if (cur.length) out.push({ body: JSON.stringify({ sdk, events: cur } satisfies Batch), events: cur });\n cur = [];\n size = envelope;\n };\n for (const ev of events) {\n const len = byteLength(JSON.stringify(ev)) + 1;\n if (cur.length && (cur.length >= maxEvents || size + len > MAX_BATCH_BYTES)) push();\n cur.push(ev);\n size += len;\n }\n push();\n return out;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/storefront.ts","../src/shared.ts"],"sourcesContent":["// @kanca-app/ikas/storefront — errors and load cost of the app's own script on\n// merchants' ikas storefronts. Zero dependencies, runs on third-party pages:\n// it never throws, never blocks, never retries, never calls preventDefault and\n// never reads cookies, storage, customer or cart data.\n// Wire format: docs/PROTOCOL.md → `storefront`.\n\nimport { DEFAULT_ENDPOINT, SDK_NAME, SDK_VERSION, uuid, type KancaEvent } from \"./shared\";\n\nexport interface StorefrontOptions {\n /** Public key (`kp_...`). Safe to ship in the script. */\n publicKey: string | undefined;\n /** Kanca base URL. Default `https://kanca.onurhan.dev`. */\n endpoint?: string;\n /** Label for this script in the dashboard, e.g. `\"rush\"`. Default `\"script\"`. */\n scriptName?: string;\n /**\n * Identifies YOUR script in error stacks and resource timing. A string is\n * matched as a substring, a RegExp with `search`. Default: the URL (without\n * query) of `document.currentScript`, so call `initKancaStorefront`\n * synchronously while your script first runs. Without either, global\n * errors are not captured (manual `captureError` and `wrap` still work).\n */\n scriptUrlMatch?: string | RegExp;\n /** The ikas merchant id, if the script knows it. Can be set later with `setMerchantId`. */\n merchantId?: string;\n /**\n * Share of page views that report `load` and `timing:*` events, 0..1.\n * Default 0.1. Errors are always reported. Values below 0.001 (other than 0) are raised to 0.001.\n */\n sampleRate?: number;\n /** Your script's version, e.g. a git sha or package version. */\n release?: string;\n debug?: boolean;\n}\n\nexport interface TimingOptions {\n /** Outcome, when there is no HTTP status. With a status, ok = status < 400. */\n ok?: boolean;\n status?: number;\n}\n\nexport interface KancaStorefront {\n /** Records an error thrown by your script. Deduped and capped per page view. */\n captureError(error: unknown, context?: { where?: string }): void;\n /**\n * Returns `fn` wrapped: an error it throws (or a promise it returns that\n * rejects) is recorded and then rethrown / left rejected, so the page sees\n * exactly what it would have seen without Kanca.\n */\n wrap<F extends (...args: never[]) => unknown>(fn: F, where?: string): F;\n /** Records a duration (sampled page views only). `name`: 1–40 chars of A–Z a–z 0–9 _ . - */\n timing(name: string, durationMs: number, options?: TimingOptions): void;\n /** `fetch` that records `timing:<name>` with the status. Same result and errors as `fetch`. */\n fetch(name: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;\n /** Marks your script as ready (e.g. first render done). Only the first call counts. */\n ready(): void;\n setMerchantId(merchantId: string | undefined): void;\n /** Sends what is queued now. Normally not needed: events go out when the page is hidden. */\n flush(): void;\n}\n\nexport const MAX_ERRORS_PER_VIEW = 10;\nexport const MAX_TIMINGS_PER_VIEW = 20;\n/** Queue size that triggers an early send. */\nexport const FLUSH_AT = 10;\nconst MAX_MESSAGE = 300;\n\nconst clip = (s: string, n: number) => (s.length > n ? s.slice(0, n) : s);\nconst round1 = (n: number) => Math.round(n * 10) / 10;\nconst now = () => (typeof performance !== \"undefined\" && performance.now ? performance.now() : Date.now());\n\nconst UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;\n\n/**\n * URL path → low-cardinality pattern without identifiers: uuids → `:id`,\n * numeric segments → `:n`, e-mails → `:email`, long opaque tokens → `:token`,\n * digit runs of 5+ inside a slug → `:n`. Query and hash are dropped, at most\n * 8 segments are kept.\n */\nexport function pathPattern(path: string): string {\n const segs = path.split(/[?#]/)[0]!.split(\"/\").filter(Boolean).slice(0, 8);\n return clip(\n \"/\" +\n segs\n .map((s) =>\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)\n ? \":id\"\n : /^\\d+$/.test(s)\n ? \":n\"\n : /@|%40/.test(s)\n ? \":email\"\n : s.length > 24 && /\\d/.test(s) && !/-.*-/.test(s)\n ? \":token\"\n : clip(s.replace(/\\d{5,}/g, \":n\"), 60),\n )\n .join(\"/\"),\n 200,\n );\n}\n\n/** Strips likely PII / identifiers from an error message and caps its length. */\nexport function scrubMessage(message: string): string {\n return clip(\n message\n .replace(/(https?:\\/\\/[^\\s?#\"'<>()]*)[?#][^\\s\"'<>()]*/g, \"$1\")\n .replace(/[\\w.+-]+@[\\w-]+\\.[\\w.-]+/g, \"<email>\")\n .replace(UUID, \"<id>\")\n .replace(/[\\w-]{32,}/g, (m) => (/\\d/.test(m) ? \"<token>\" : m))\n .replace(/\\d{6,}/g, \"<n>\"),\n MAX_MESSAGE,\n );\n}\n\n/**\n * The topmost stack frame that has a URL, as `file:line:col` (last path\n * segment, no query), if that frame is ours. Frames without a URL (native\n * code such as `JSON.parse`) are skipped, so an error thrown by a built-in\n * called from our code is ours, while an error thrown by theme code that our\n * code called is not. Returns undefined when the top URL frame is not ours.\n */\nexport function ownFrame(stack: unknown, isOurs: (s: string) => boolean): string | undefined {\n if (typeof stack !== \"string\") return undefined;\n for (const line of stack.split(\"\\n\")) {\n const m = /(https?:\\/\\/[^\\s)]+?):(\\d+):(\\d+)/.exec(line);\n if (!m) continue;\n if (!isOurs(line)) return undefined;\n const file = m[1]!.split(/[?#]/)[0]!.split(\"/\").pop() || \"script\";\n return clip(`${file}:${m[2]}:${m[3]}`, 200);\n }\n return undefined;\n}\n\n/** Name + scrubbed message of anything thrown. */\nexport function describeError(err: unknown): { code: string; message?: string } {\n const e = err as { name?: unknown; message?: unknown } | null;\n if (e && typeof e === \"object\" && typeof e.message === \"string\") {\n const name = typeof e.name === \"string\" && e.name ? e.name : \"Error\";\n return { code: clip(name, 100), message: scrubMessage(e.message) || undefined };\n }\n if (typeof err === \"string\") return { code: \"Error\", message: scrubMessage(err) || undefined };\n return { code: \"NonError\", message: typeof err };\n}\n\nconst noop = () => undefined;\n\nfunction disabled(): KancaStorefront {\n return {\n captureError: noop,\n wrap: (fn) => fn,\n timing: noop,\n fetch: (_name, input, init) => fetch(input, init),\n ready: noop,\n setMerchantId: noop,\n flush: noop,\n };\n}\n\n// Global (not module) registry so two copies of the script on one page share one instance per scriptName.\nconst REG = \"__kancaStorefront\";\nconst g = globalThis as { [REG]?: Record<string, KancaStorefront> };\n\n/**\n * Starts storefront monitoring for one script. Call it synchronously at the\n * top of your script. Returns an instance whose methods never throw. Safe to\n * call more than once: the same `scriptName` returns the same instance.\n */\nexport function initKancaStorefront(options: StorefrontOptions): KancaStorefront {\n try {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return disabled();\n const o = options || ({} as StorefrontOptions);\n const script = clip(o.scriptName || \"script\", 64);\n const reg = (g[REG] ||= {});\n if (reg[script]) return reg[script]!;\n const key = typeof o.publicKey === \"string\" ? o.publicKey.trim() : \"\";\n const debug = !!o.debug;\n if (!key.startsWith(\"kp_\")) {\n if (debug || key) console.warn(\"[kanca] storefront: kp_ key required\");\n return disabled();\n }\n\n const url = `${(o.endpoint || DEFAULT_ENDPOINT).replace(/\\/+$/, \"\")}/api/ingest/browser?key=${encodeURIComponent(key)}`;\n let match = o.scriptUrlMatch;\n if (!match) {\n const src = (document.currentScript as HTMLScriptElement | null)?.src;\n if (src && /^https?:/.test(src)) match = src.split(/[?#]/)[0];\n }\n const isOurs = (s: string) =>\n !!match && !!s && (typeof match === \"string\" ? s.indexOf(match) >= 0 : s.search(match) >= 0);\n\n let rate = typeof o.sampleRate === \"number\" && o.sampleRate >= 0 ? Math.min(1, o.sampleRate) : 0.1;\n if (rate > 0 && rate < 0.001) rate = 0.001;\n const sampled = rate > 0 && Math.random() < rate;\n const path0 = pathPattern(location.pathname || \"/\");\n const pv = uuid().replace(/-/g, \"\").slice(0, 16);\n const t0 = now();\n const ts0 = Date.now();\n let merchantId = o.merchantId;\n let readyMs: number | undefined;\n let loadDone = false;\n let errors = 0;\n let timings = 0;\n const seen = new Set<string>();\n const captured = new WeakSet<object>();\n let queue: KancaEvent[] = [];\n\n const event = (name: string, fields: Partial<KancaEvent>, attrs?: Record<string, string | number | undefined>): KancaEvent => {\n const a: Record<string, string | number> = {\n host: clip(location.hostname || \"unknown\", 200),\n path: pathPattern(location.pathname || \"/\"),\n script,\n pv,\n sample: name === \"error\" ? 1 : rate,\n };\n if (o.release) a.release = clip(String(o.release), 200);\n for (const k in attrs) if (attrs[k] !== undefined) a[k] = attrs[k]!;\n return { id: uuid(), type: \"storefront\", ts: Date.now(), name, ...fields, attrs: a };\n };\n\n const send = (final: boolean) => {\n try {\n if (final && sampled && !loadDone) {\n loadDone = true;\n queue.push(loadEvent());\n }\n if (!queue.length) return;\n const events = queue;\n queue = [];\n if (merchantId) for (const e of events) e.merchantId = clip(merchantId, 200);\n const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });\n if (debug) console.debug(\"[kanca] storefront\", events);\n // fetch+keepalive first: content blockers drop sendBeacon (\"ping\")\n // requests while sendBeacon still returns true. text/plain keeps it a\n // simple request (no CORS preflight). Never retried.\n try {\n if (typeof fetch === \"function\") {\n fetch(url, { method: \"POST\", body, keepalive: true, mode: \"cors\", credentials: \"omit\", headers: { \"content-type\": \"text/plain\" } }).catch(noop);\n return;\n }\n } catch {\n /* fall through */\n }\n navigator.sendBeacon?.(url, body);\n } catch {\n /* never throw */\n }\n };\n\n const push = (e: KancaEvent) => {\n queue.push(e);\n if (queue.length >= FLUSH_AT) send(false);\n };\n\n // One per sampled page view: the page-view record behind every storefront metric.\n const loadEvent = (): KancaEvent => {\n const attrs: Record<string, string | number | undefined> = { path: path0, errors, ready_ms: readyMs };\n const fields: Partial<KancaEvent> = {};\n try {\n const entries = performance.getEntriesByType(\"resource\") as PerformanceResourceTiming[];\n for (let i = entries.length - 1; i >= 0; i--) {\n const r = entries[i]!;\n if (!isOurs(r.name)) continue;\n if (r.responseEnd > 0) fields.durationMs = round1(r.responseEnd - r.startTime);\n // Sizes are only exposed with Timing-Allow-Origin; 0/0 means \"unknown\".\n if (r.encodedBodySize > 0) {\n fields.value = r.transferSize;\n attrs.encoded = r.encodedBodySize;\n attrs.cache = r.transferSize === 0 ? \"hit\" : r.transferSize < r.encodedBodySize ? \"revalidated\" : \"network\";\n }\n break;\n }\n } catch {\n /* no resource timing */\n }\n const e = event(\"load\", fields, attrs);\n e.ts = ts0;\n return e;\n };\n\n const captureError = (err: unknown, where?: string) => {\n try {\n if (err && typeof err === \"object\") {\n if (captured.has(err)) return;\n captured.add(err);\n }\n if (errors >= MAX_ERRORS_PER_VIEW) return;\n const d = describeError(err);\n const frame = ownFrame((err as { stack?: unknown } | null)?.stack, isOurs);\n const k = `${d.code}|${d.message}|${frame}`;\n if (seen.has(k)) return;\n seen.add(k);\n errors++;\n push(event(\"error\", { ok: false, error: d }, { where: where ? clip(String(where), 100) : undefined, frame }));\n } catch {\n /* ignore */\n }\n };\n\n const timing = (name: string, ms: number, t?: TimingOptions) => {\n try {\n if (!sampled || timings >= MAX_TIMINGS_PER_VIEW) return;\n if (!/^[A-Za-z0-9_.-]{1,40}$/.test(name) || !(ms >= 0 && ms < 864e5)) {\n if (debug) console.warn(\"[kanca] bad timing\", name, ms);\n return;\n }\n timings++;\n const f: Partial<KancaEvent> = { durationMs: round1(ms) };\n if (t && typeof t.status === \"number\" && t.status >= 100 && t.status <= 599) f.status = Math.trunc(t.status);\n if (t && typeof t.ok === \"boolean\") f.ok = t.ok;\n push(event(`timing:${name}`, f));\n } catch {\n /* ignore */\n }\n };\n\n if (match) {\n // Only errors from our script: the page's theme and other apps are never recorded.\n // Attribution: the top URL frame of the stack, else the event's filename.\n const ours = (err: unknown, filename?: string) => {\n const stack = (err as { stack?: unknown } | null)?.stack;\n return typeof stack === \"string\" && /https?:/.test(stack) ? !!ownFrame(stack, isOurs) : !!filename && isOurs(filename);\n };\n addEventListener(\"error\", (ev: ErrorEvent) => {\n try {\n if (ours(ev.error, ev.filename)) captureError(ev.error ?? ev.message, \"onerror\");\n } catch {\n /* ignore */\n }\n });\n addEventListener(\"unhandledrejection\", (ev: PromiseRejectionEvent) => {\n try {\n if (ours(ev.reason)) captureError(ev.reason, \"unhandledrejection\");\n } catch {\n /* ignore */\n }\n });\n } else if (debug) {\n console.warn(\"[kanca] storefront: no script URL, global errors off\");\n }\n\n addEventListener(\n \"visibilitychange\",\n () => {\n if (document.visibilityState === \"hidden\") send(true);\n },\n true,\n );\n addEventListener(\"pagehide\", () => send(true), true);\n\n const instance: KancaStorefront = {\n captureError: (err, ctx) => captureError(err, ctx?.where),\n wrap(fn, where) {\n if (typeof fn !== \"function\") return fn;\n return function (this: unknown, ...args: never[]) {\n let r: unknown;\n try {\n r = fn.apply(this, args);\n } catch (e) {\n captureError(e, where);\n throw e;\n }\n try {\n if (r && typeof (r as Promise<unknown>).then === \"function\") {\n // A side branch: the returned promise is untouched and still rejects for the caller.\n (r as Promise<unknown>).then(undefined, (e) => captureError(e, where));\n }\n } catch {\n /* ignore */\n }\n return r;\n } as unknown as typeof fn;\n },\n timing,\n async fetch(name, input, init) {\n const t = now();\n let res: Response;\n try {\n res = await fetch(input, init);\n } catch (e) {\n timing(name, now() - t, { ok: false });\n throw e;\n }\n timing(name, now() - t, { status: res.status });\n return res;\n },\n ready() {\n if (readyMs === undefined) readyMs = Math.round(now() - t0);\n },\n setMerchantId(id) {\n merchantId = typeof id === \"string\" && id ? id : undefined;\n },\n flush: () => send(false),\n };\n reg[script] = instance;\n return instance;\n } catch {\n return disabled();\n }\n}\n\n/** @internal for tests */\nexport function __resetStorefrontForTests(): void {\n delete g[REG];\n}\n","// Shared, runtime-agnostic helpers (used by both the Node and browser entries).\n// Wire format: see docs/PROTOCOL.md (v1).\n\nexport const SDK_NAME = \"@kanca-app/ikas\";\nexport const SDK_VERSION = \"0.3.0\";\nexport const DEFAULT_ENDPOINT = \"https://kanca.onurhan.dev\";\n\nexport const MAX_NAME = 200;\nexport const MAX_ERROR_MESSAGE = 500;\nexport const MAX_ERROR_CODE = 100;\nexport const MAX_ATTR_KEYS = 20;\nexport const MAX_ATTR_STRING = 500;\nexport const MAX_ATTR_KEY = 64;\nexport const MAX_EVENTS_PER_BATCH = 500;\n/** Protocol limit is 512 KB; keep headroom for the envelope. */\nexport const MAX_BATCH_BYTES = 480 * 1024;\n\nexport type EventType = \"webhook\" | \"graphql\" | \"rest\" | \"install\" | \"uninstall\" | \"vital\" | \"storefront\";\n\nexport type AttrValue = string | number | boolean;\n\nexport interface KancaError {\n code: string;\n message?: string;\n}\n\n/** A single wire event (PROTOCOL.md → Event). */\nexport interface KancaEvent {\n id: string;\n type: EventType;\n ts: number;\n name: string;\n status?: number;\n ok?: boolean;\n durationMs?: number;\n value?: number;\n error?: KancaError;\n merchantId?: string;\n deliveryId?: string;\n attrs?: Record<string, AttrValue>;\n}\n\nexport interface Batch {\n sdk: { name: string; version: string };\n events: KancaEvent[];\n}\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max) : s;\n}\n\nfunction hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\n/** UUID v4 via crypto.randomUUID, with a getRandomValues fallback (non-secure browser contexts). */\nexport function uuid(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === \"function\") return c.randomUUID();\n const b = new Uint8Array(16);\n if (c && typeof c.getRandomValues === \"function\") c.getRandomValues(b);\n else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);\n b[6] = (b[6]! & 0x0f) | 0x40;\n b[8] = (b[8]! & 0x3f) | 0x80;\n const h = hex(b);\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === \"number\" && Number.isFinite(v);\n}\n\nfunction cleanAttrs(attrs: unknown): Record<string, AttrValue> | undefined {\n if (!attrs || typeof attrs !== \"object\") return undefined;\n const out: Record<string, AttrValue> = {};\n let n = 0;\n for (const [k, v] of Object.entries(attrs as Record<string, unknown>)) {\n if (n >= MAX_ATTR_KEYS) break;\n if (v === undefined || v === null || !k) continue;\n let val: AttrValue;\n if (typeof v === \"boolean\") val = v;\n else if (typeof v === \"number\") {\n if (!Number.isFinite(v)) continue;\n val = v;\n } else if (typeof v === \"string\") val = truncate(v, MAX_ATTR_STRING);\n else continue; // attrs are flat: nested values are dropped, never stringified\n out[truncate(k, MAX_ATTR_KEY)] = val;\n n++;\n }\n return n > 0 ? out : undefined;\n}\n\n/**\n * Normalises an event so it always satisfies the protocol limits. Unknown\n * fields are dropped — only the whitelisted metadata below ever leaves the process.\n */\nexport function sanitizeEvent(e: KancaEvent): KancaEvent {\n const out: KancaEvent = {\n id: typeof e.id === \"string\" && e.id ? e.id : uuid(),\n type: e.type,\n ts: isFiniteNumber(e.ts) ? Math.round(e.ts) : Date.now(),\n name: truncate(typeof e.name === \"string\" && e.name ? e.name : \"unknown\", MAX_NAME),\n };\n if (isFiniteNumber(e.status) && e.status >= 100 && e.status <= 599) out.status = Math.trunc(e.status);\n if (typeof e.ok === \"boolean\") out.ok = e.ok;\n if (isFiniteNumber(e.durationMs)) out.durationMs = Math.min(86_400_000, Math.max(0, Math.round(e.durationMs * 10) / 10));\n if (isFiniteNumber(e.value)) out.value = e.value;\n if (e.error && typeof e.error.code === \"string\") {\n out.error = { code: truncate(e.error.code, MAX_ERROR_CODE) };\n if (typeof e.error.message === \"string\" && e.error.message) {\n out.error.message = truncate(e.error.message, MAX_ERROR_MESSAGE);\n }\n }\n if (typeof e.merchantId === \"string\" && e.merchantId) out.merchantId = truncate(e.merchantId, 200);\n if (typeof e.deliveryId === \"string\" && e.deliveryId) out.deliveryId = truncate(e.deliveryId, 200);\n const attrs = cleanAttrs(e.attrs);\n if (attrs) out.attrs = attrs;\n return out;\n}\n\nexport function errorMessage(err: unknown): string | undefined {\n try {\n if (err instanceof Error) return truncate(err.message || err.name, MAX_ERROR_MESSAGE);\n if (typeof err === \"string\") return truncate(err, MAX_ERROR_MESSAGE);\n } catch {\n /* ignore */\n }\n return undefined;\n}\n\nexport function joinUrl(endpoint: string, path: string): string {\n return endpoint.replace(/\\/+$/, \"\") + path;\n}\n\nconst encoder = typeof TextEncoder !== \"undefined\" ? /* @__PURE__ */ new TextEncoder() : undefined;\nexport function byteLength(s: string): number {\n return encoder ? encoder.encode(s).length : s.length * 3;\n}\n\n/**\n * Splits events into protocol-sized batches (<= maxEvents events and\n * <= MAX_BATCH_BYTES serialized). Returns serialized bodies.\n */\nexport function buildBodies(events: KancaEvent[], maxEvents: number): { body: string; events: KancaEvent[] }[] {\n const sdk = { name: SDK_NAME, version: SDK_VERSION };\n const envelope = byteLength(JSON.stringify({ sdk, events: [] }));\n const out: { body: string; events: KancaEvent[] }[] = [];\n let cur: KancaEvent[] = [];\n let size = envelope;\n const push = () => {\n if (cur.length) out.push({ body: JSON.stringify({ sdk, events: cur } satisfies Batch), events: cur });\n cur = [];\n size = envelope;\n };\n for (const ev of events) {\n const len = byteLength(JSON.stringify(ev)) + 1;\n if (cur.length && (cur.length >= maxEvents || size + len > MAX_BATCH_BYTES)) push();\n cur.push(ev);\n size += len;\n }\n push();\n return out;\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  // src/shared.ts
2
2
  var SDK_NAME = "@kanca-app/ikas";
3
- var SDK_VERSION = "0.2.0";
3
+ var SDK_VERSION = "0.3.0";
4
4
  var DEFAULT_ENDPOINT = "https://kanca.onurhan.dev";
5
5
  var MAX_BATCH_BYTES = 480 * 1024;
6
6
  function hex(bytes) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shared.ts","../src/storefront.ts"],"sourcesContent":["// Shared, runtime-agnostic helpers (used by both the Node and browser entries).\n// Wire format: see docs/PROTOCOL.md (v1).\n\nexport const SDK_NAME = \"@kanca-app/ikas\";\nexport const SDK_VERSION = \"0.2.0\";\nexport const DEFAULT_ENDPOINT = \"https://kanca.onurhan.dev\";\n\nexport const MAX_NAME = 200;\nexport const MAX_ERROR_MESSAGE = 500;\nexport const MAX_ERROR_CODE = 100;\nexport const MAX_ATTR_KEYS = 20;\nexport const MAX_ATTR_STRING = 500;\nexport const MAX_ATTR_KEY = 64;\nexport const MAX_EVENTS_PER_BATCH = 500;\n/** Protocol limit is 512 KB; keep headroom for the envelope. */\nexport const MAX_BATCH_BYTES = 480 * 1024;\n\nexport type EventType = \"webhook\" | \"graphql\" | \"rest\" | \"install\" | \"uninstall\" | \"vital\" | \"storefront\";\n\nexport type AttrValue = string | number | boolean;\n\nexport interface KancaError {\n code: string;\n message?: string;\n}\n\n/** A single wire event (PROTOCOL.md → Event). */\nexport interface KancaEvent {\n id: string;\n type: EventType;\n ts: number;\n name: string;\n status?: number;\n ok?: boolean;\n durationMs?: number;\n value?: number;\n error?: KancaError;\n merchantId?: string;\n deliveryId?: string;\n attrs?: Record<string, AttrValue>;\n}\n\nexport interface Batch {\n sdk: { name: string; version: string };\n events: KancaEvent[];\n}\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max) : s;\n}\n\nfunction hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\n/** UUID v4 via crypto.randomUUID, with a getRandomValues fallback (non-secure browser contexts). */\nexport function uuid(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === \"function\") return c.randomUUID();\n const b = new Uint8Array(16);\n if (c && typeof c.getRandomValues === \"function\") c.getRandomValues(b);\n else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);\n b[6] = (b[6]! & 0x0f) | 0x40;\n b[8] = (b[8]! & 0x3f) | 0x80;\n const h = hex(b);\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === \"number\" && Number.isFinite(v);\n}\n\nfunction cleanAttrs(attrs: unknown): Record<string, AttrValue> | undefined {\n if (!attrs || typeof attrs !== \"object\") return undefined;\n const out: Record<string, AttrValue> = {};\n let n = 0;\n for (const [k, v] of Object.entries(attrs as Record<string, unknown>)) {\n if (n >= MAX_ATTR_KEYS) break;\n if (v === undefined || v === null || !k) continue;\n let val: AttrValue;\n if (typeof v === \"boolean\") val = v;\n else if (typeof v === \"number\") {\n if (!Number.isFinite(v)) continue;\n val = v;\n } else if (typeof v === \"string\") val = truncate(v, MAX_ATTR_STRING);\n else continue; // attrs are flat: nested values are dropped, never stringified\n out[truncate(k, MAX_ATTR_KEY)] = val;\n n++;\n }\n return n > 0 ? out : undefined;\n}\n\n/**\n * Normalises an event so it always satisfies the protocol limits. Unknown\n * fields are dropped — only the whitelisted metadata below ever leaves the process.\n */\nexport function sanitizeEvent(e: KancaEvent): KancaEvent {\n const out: KancaEvent = {\n id: typeof e.id === \"string\" && e.id ? e.id : uuid(),\n type: e.type,\n ts: isFiniteNumber(e.ts) ? Math.round(e.ts) : Date.now(),\n name: truncate(typeof e.name === \"string\" && e.name ? e.name : \"unknown\", MAX_NAME),\n };\n if (isFiniteNumber(e.status) && e.status >= 100 && e.status <= 599) out.status = Math.trunc(e.status);\n if (typeof e.ok === \"boolean\") out.ok = e.ok;\n if (isFiniteNumber(e.durationMs)) out.durationMs = Math.min(86_400_000, Math.max(0, Math.round(e.durationMs * 10) / 10));\n if (isFiniteNumber(e.value)) out.value = e.value;\n if (e.error && typeof e.error.code === \"string\") {\n out.error = { code: truncate(e.error.code, MAX_ERROR_CODE) };\n if (typeof e.error.message === \"string\" && e.error.message) {\n out.error.message = truncate(e.error.message, MAX_ERROR_MESSAGE);\n }\n }\n if (typeof e.merchantId === \"string\" && e.merchantId) out.merchantId = truncate(e.merchantId, 200);\n if (typeof e.deliveryId === \"string\" && e.deliveryId) out.deliveryId = truncate(e.deliveryId, 200);\n const attrs = cleanAttrs(e.attrs);\n if (attrs) out.attrs = attrs;\n return out;\n}\n\nexport function errorMessage(err: unknown): string | undefined {\n try {\n if (err instanceof Error) return truncate(err.message || err.name, MAX_ERROR_MESSAGE);\n if (typeof err === \"string\") return truncate(err, MAX_ERROR_MESSAGE);\n } catch {\n /* ignore */\n }\n return undefined;\n}\n\nexport function joinUrl(endpoint: string, path: string): string {\n return endpoint.replace(/\\/+$/, \"\") + path;\n}\n\nconst encoder = typeof TextEncoder !== \"undefined\" ? /* @__PURE__ */ new TextEncoder() : undefined;\nexport function byteLength(s: string): number {\n return encoder ? encoder.encode(s).length : s.length * 3;\n}\n\n/**\n * Splits events into protocol-sized batches (<= maxEvents events and\n * <= MAX_BATCH_BYTES serialized). Returns serialized bodies.\n */\nexport function buildBodies(events: KancaEvent[], maxEvents: number): { body: string; events: KancaEvent[] }[] {\n const sdk = { name: SDK_NAME, version: SDK_VERSION };\n const envelope = byteLength(JSON.stringify({ sdk, events: [] }));\n const out: { body: string; events: KancaEvent[] }[] = [];\n let cur: KancaEvent[] = [];\n let size = envelope;\n const push = () => {\n if (cur.length) out.push({ body: JSON.stringify({ sdk, events: cur } satisfies Batch), events: cur });\n cur = [];\n size = envelope;\n };\n for (const ev of events) {\n const len = byteLength(JSON.stringify(ev)) + 1;\n if (cur.length && (cur.length >= maxEvents || size + len > MAX_BATCH_BYTES)) push();\n cur.push(ev);\n size += len;\n }\n push();\n return out;\n}\n","// @kanca-app/ikas/storefront — errors and load cost of the app's own script on\n// merchants' ikas storefronts. Zero dependencies, runs on third-party pages:\n// it never throws, never blocks, never retries, never calls preventDefault and\n// never reads cookies, storage, customer or cart data.\n// Wire format: docs/PROTOCOL.md → `storefront`.\n\nimport { DEFAULT_ENDPOINT, SDK_NAME, SDK_VERSION, uuid, type KancaEvent } from \"./shared\";\n\nexport interface StorefrontOptions {\n /** Public key (`kp_...`). Safe to ship in the script. */\n publicKey: string | undefined;\n /** Kanca base URL. Default `https://kanca.onurhan.dev`. */\n endpoint?: string;\n /** Label for this script in the dashboard, e.g. `\"rush\"`. Default `\"script\"`. */\n scriptName?: string;\n /**\n * Identifies YOUR script in error stacks and resource timing. A string is\n * matched as a substring, a RegExp with `search`. Default: the URL (without\n * query) of `document.currentScript`, so call `initKancaStorefront`\n * synchronously while your script first runs. Without either, global\n * errors are not captured (manual `captureError` and `wrap` still work).\n */\n scriptUrlMatch?: string | RegExp;\n /** The ikas merchant id, if the script knows it. Can be set later with `setMerchantId`. */\n merchantId?: string;\n /**\n * Share of page views that report `load` and `timing:*` events, 0..1.\n * Default 0.1. Errors are always reported. Values below 0.001 (other than 0) are raised to 0.001.\n */\n sampleRate?: number;\n /** Your script's version, e.g. a git sha or package version. */\n release?: string;\n debug?: boolean;\n}\n\nexport interface TimingOptions {\n /** Outcome, when there is no HTTP status. With a status, ok = status < 400. */\n ok?: boolean;\n status?: number;\n}\n\nexport interface KancaStorefront {\n /** Records an error thrown by your script. Deduped and capped per page view. */\n captureError(error: unknown, context?: { where?: string }): void;\n /**\n * Returns `fn` wrapped: an error it throws (or a promise it returns that\n * rejects) is recorded and then rethrown / left rejected, so the page sees\n * exactly what it would have seen without Kanca.\n */\n wrap<F extends (...args: never[]) => unknown>(fn: F, where?: string): F;\n /** Records a duration (sampled page views only). `name`: 1–40 chars of A–Z a–z 0–9 _ . - */\n timing(name: string, durationMs: number, options?: TimingOptions): void;\n /** `fetch` that records `timing:<name>` with the status. Same result and errors as `fetch`. */\n fetch(name: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;\n /** Marks your script as ready (e.g. first render done). Only the first call counts. */\n ready(): void;\n setMerchantId(merchantId: string | undefined): void;\n /** Sends what is queued now. Normally not needed: events go out when the page is hidden. */\n flush(): void;\n}\n\nexport const MAX_ERRORS_PER_VIEW = 10;\nexport const MAX_TIMINGS_PER_VIEW = 20;\n/** Queue size that triggers an early send. */\nexport const FLUSH_AT = 10;\nconst MAX_MESSAGE = 300;\n\nconst clip = (s: string, n: number) => (s.length > n ? s.slice(0, n) : s);\nconst round1 = (n: number) => Math.round(n * 10) / 10;\nconst now = () => (typeof performance !== \"undefined\" && performance.now ? performance.now() : Date.now());\n\nconst UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;\n\n/**\n * URL path → low-cardinality pattern without identifiers: uuids → `:id`,\n * numeric segments → `:n`, e-mails → `:email`, long opaque tokens → `:token`,\n * digit runs of 5+ inside a slug → `:n`. Query and hash are dropped, at most\n * 8 segments are kept.\n */\nexport function pathPattern(path: string): string {\n const segs = path.split(/[?#]/)[0]!.split(\"/\").filter(Boolean).slice(0, 8);\n return clip(\n \"/\" +\n segs\n .map((s) =>\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)\n ? \":id\"\n : /^\\d+$/.test(s)\n ? \":n\"\n : /@|%40/.test(s)\n ? \":email\"\n : s.length > 24 && /\\d/.test(s) && !/-.*-/.test(s)\n ? \":token\"\n : clip(s.replace(/\\d{5,}/g, \":n\"), 60),\n )\n .join(\"/\"),\n 200,\n );\n}\n\n/** Strips likely PII / identifiers from an error message and caps its length. */\nexport function scrubMessage(message: string): string {\n return clip(\n message\n .replace(/(https?:\\/\\/[^\\s?#\"'<>()]*)[?#][^\\s\"'<>()]*/g, \"$1\")\n .replace(/[\\w.+-]+@[\\w-]+\\.[\\w.-]+/g, \"<email>\")\n .replace(UUID, \"<id>\")\n .replace(/[\\w-]{32,}/g, (m) => (/\\d/.test(m) ? \"<token>\" : m))\n .replace(/\\d{6,}/g, \"<n>\"),\n MAX_MESSAGE,\n );\n}\n\n/**\n * The topmost stack frame that has a URL, as `file:line:col` (last path\n * segment, no query), if that frame is ours. Frames without a URL (native\n * code such as `JSON.parse`) are skipped, so an error thrown by a built-in\n * called from our code is ours, while an error thrown by theme code that our\n * code called is not. Returns undefined when the top URL frame is not ours.\n */\nexport function ownFrame(stack: unknown, isOurs: (s: string) => boolean): string | undefined {\n if (typeof stack !== \"string\") return undefined;\n for (const line of stack.split(\"\\n\")) {\n const m = /(https?:\\/\\/[^\\s)]+?):(\\d+):(\\d+)/.exec(line);\n if (!m) continue;\n if (!isOurs(line)) return undefined;\n const file = m[1]!.split(/[?#]/)[0]!.split(\"/\").pop() || \"script\";\n return clip(`${file}:${m[2]}:${m[3]}`, 200);\n }\n return undefined;\n}\n\n/** Name + scrubbed message of anything thrown. */\nexport function describeError(err: unknown): { code: string; message?: string } {\n const e = err as { name?: unknown; message?: unknown } | null;\n if (e && typeof e === \"object\" && typeof e.message === \"string\") {\n const name = typeof e.name === \"string\" && e.name ? e.name : \"Error\";\n return { code: clip(name, 100), message: scrubMessage(e.message) || undefined };\n }\n if (typeof err === \"string\") return { code: \"Error\", message: scrubMessage(err) || undefined };\n return { code: \"NonError\", message: typeof err };\n}\n\nconst noop = () => undefined;\n\nfunction disabled(): KancaStorefront {\n return {\n captureError: noop,\n wrap: (fn) => fn,\n timing: noop,\n fetch: (_name, input, init) => fetch(input, init),\n ready: noop,\n setMerchantId: noop,\n flush: noop,\n };\n}\n\n// Global (not module) registry so two copies of the script on one page share one instance per scriptName.\nconst REG = \"__kancaStorefront\";\nconst g = globalThis as { [REG]?: Record<string, KancaStorefront> };\n\n/**\n * Starts storefront monitoring for one script. Call it synchronously at the\n * top of your script. Returns an instance whose methods never throw. Safe to\n * call more than once: the same `scriptName` returns the same instance.\n */\nexport function initKancaStorefront(options: StorefrontOptions): KancaStorefront {\n try {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return disabled();\n const o = options || ({} as StorefrontOptions);\n const script = clip(o.scriptName || \"script\", 64);\n const reg = (g[REG] ||= {});\n if (reg[script]) return reg[script]!;\n const key = typeof o.publicKey === \"string\" ? o.publicKey.trim() : \"\";\n const debug = !!o.debug;\n if (!key.startsWith(\"kp_\")) {\n if (debug || key) console.warn(\"[kanca] storefront: kp_ key required\");\n return disabled();\n }\n\n const url = `${(o.endpoint || DEFAULT_ENDPOINT).replace(/\\/+$/, \"\")}/api/ingest/browser?key=${encodeURIComponent(key)}`;\n let match = o.scriptUrlMatch;\n if (!match) {\n const src = (document.currentScript as HTMLScriptElement | null)?.src;\n if (src && /^https?:/.test(src)) match = src.split(/[?#]/)[0];\n }\n const isOurs = (s: string) =>\n !!match && !!s && (typeof match === \"string\" ? s.indexOf(match) >= 0 : s.search(match) >= 0);\n\n let rate = typeof o.sampleRate === \"number\" && o.sampleRate >= 0 ? Math.min(1, o.sampleRate) : 0.1;\n if (rate > 0 && rate < 0.001) rate = 0.001;\n const sampled = rate > 0 && Math.random() < rate;\n const path0 = pathPattern(location.pathname || \"/\");\n const pv = uuid().replace(/-/g, \"\").slice(0, 16);\n const t0 = now();\n const ts0 = Date.now();\n let merchantId = o.merchantId;\n let readyMs: number | undefined;\n let loadDone = false;\n let errors = 0;\n let timings = 0;\n const seen = new Set<string>();\n const captured = new WeakSet<object>();\n let queue: KancaEvent[] = [];\n\n const event = (name: string, fields: Partial<KancaEvent>, attrs?: Record<string, string | number | undefined>): KancaEvent => {\n const a: Record<string, string | number> = {\n host: clip(location.hostname || \"unknown\", 200),\n path: pathPattern(location.pathname || \"/\"),\n script,\n pv,\n sample: name === \"error\" ? 1 : rate,\n };\n if (o.release) a.release = clip(String(o.release), 200);\n for (const k in attrs) if (attrs[k] !== undefined) a[k] = attrs[k]!;\n return { id: uuid(), type: \"storefront\", ts: Date.now(), name, ...fields, attrs: a };\n };\n\n const send = (final: boolean) => {\n try {\n if (final && sampled && !loadDone) {\n loadDone = true;\n queue.push(loadEvent());\n }\n if (!queue.length) return;\n const events = queue;\n queue = [];\n if (merchantId) for (const e of events) e.merchantId = clip(merchantId, 200);\n const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });\n if (debug) console.debug(\"[kanca] storefront\", events);\n // fetch+keepalive first: content blockers drop sendBeacon (\"ping\")\n // requests while sendBeacon still returns true. text/plain keeps it a\n // simple request (no CORS preflight). Never retried.\n try {\n if (typeof fetch === \"function\") {\n fetch(url, { method: \"POST\", body, keepalive: true, mode: \"cors\", credentials: \"omit\", headers: { \"content-type\": \"text/plain\" } }).catch(noop);\n return;\n }\n } catch {\n /* fall through */\n }\n navigator.sendBeacon?.(url, body);\n } catch {\n /* never throw */\n }\n };\n\n const push = (e: KancaEvent) => {\n queue.push(e);\n if (queue.length >= FLUSH_AT) send(false);\n };\n\n // One per sampled page view: the page-view record behind every storefront metric.\n const loadEvent = (): KancaEvent => {\n const attrs: Record<string, string | number | undefined> = { path: path0, errors, ready_ms: readyMs };\n const fields: Partial<KancaEvent> = {};\n try {\n const entries = performance.getEntriesByType(\"resource\") as PerformanceResourceTiming[];\n for (let i = entries.length - 1; i >= 0; i--) {\n const r = entries[i]!;\n if (!isOurs(r.name)) continue;\n if (r.responseEnd > 0) fields.durationMs = round1(r.responseEnd - r.startTime);\n // Sizes are only exposed with Timing-Allow-Origin; 0/0 means \"unknown\".\n if (r.encodedBodySize > 0) {\n fields.value = r.transferSize;\n attrs.encoded = r.encodedBodySize;\n attrs.cache = r.transferSize === 0 ? \"hit\" : r.transferSize < r.encodedBodySize ? \"revalidated\" : \"network\";\n }\n break;\n }\n } catch {\n /* no resource timing */\n }\n const e = event(\"load\", fields, attrs);\n e.ts = ts0;\n return e;\n };\n\n const captureError = (err: unknown, where?: string) => {\n try {\n if (err && typeof err === \"object\") {\n if (captured.has(err)) return;\n captured.add(err);\n }\n if (errors >= MAX_ERRORS_PER_VIEW) return;\n const d = describeError(err);\n const frame = ownFrame((err as { stack?: unknown } | null)?.stack, isOurs);\n const k = `${d.code}|${d.message}|${frame}`;\n if (seen.has(k)) return;\n seen.add(k);\n errors++;\n push(event(\"error\", { ok: false, error: d }, { where: where ? clip(String(where), 100) : undefined, frame }));\n } catch {\n /* ignore */\n }\n };\n\n const timing = (name: string, ms: number, t?: TimingOptions) => {\n try {\n if (!sampled || timings >= MAX_TIMINGS_PER_VIEW) return;\n if (!/^[A-Za-z0-9_.-]{1,40}$/.test(name) || !(ms >= 0 && ms < 864e5)) {\n if (debug) console.warn(\"[kanca] bad timing\", name, ms);\n return;\n }\n timings++;\n const f: Partial<KancaEvent> = { durationMs: round1(ms) };\n if (t && typeof t.status === \"number\" && t.status >= 100 && t.status <= 599) f.status = Math.trunc(t.status);\n if (t && typeof t.ok === \"boolean\") f.ok = t.ok;\n push(event(`timing:${name}`, f));\n } catch {\n /* ignore */\n }\n };\n\n if (match) {\n // Only errors from our script: the page's theme and other apps are never recorded.\n // Attribution: the top URL frame of the stack, else the event's filename.\n const ours = (err: unknown, filename?: string) => {\n const stack = (err as { stack?: unknown } | null)?.stack;\n return typeof stack === \"string\" && /https?:/.test(stack) ? !!ownFrame(stack, isOurs) : !!filename && isOurs(filename);\n };\n addEventListener(\"error\", (ev: ErrorEvent) => {\n try {\n if (ours(ev.error, ev.filename)) captureError(ev.error ?? ev.message, \"onerror\");\n } catch {\n /* ignore */\n }\n });\n addEventListener(\"unhandledrejection\", (ev: PromiseRejectionEvent) => {\n try {\n if (ours(ev.reason)) captureError(ev.reason, \"unhandledrejection\");\n } catch {\n /* ignore */\n }\n });\n } else if (debug) {\n console.warn(\"[kanca] storefront: no script URL, global errors off\");\n }\n\n addEventListener(\n \"visibilitychange\",\n () => {\n if (document.visibilityState === \"hidden\") send(true);\n },\n true,\n );\n addEventListener(\"pagehide\", () => send(true), true);\n\n const instance: KancaStorefront = {\n captureError: (err, ctx) => captureError(err, ctx?.where),\n wrap(fn, where) {\n if (typeof fn !== \"function\") return fn;\n return function (this: unknown, ...args: never[]) {\n let r: unknown;\n try {\n r = fn.apply(this, args);\n } catch (e) {\n captureError(e, where);\n throw e;\n }\n try {\n if (r && typeof (r as Promise<unknown>).then === \"function\") {\n // A side branch: the returned promise is untouched and still rejects for the caller.\n (r as Promise<unknown>).then(undefined, (e) => captureError(e, where));\n }\n } catch {\n /* ignore */\n }\n return r;\n } as unknown as typeof fn;\n },\n timing,\n async fetch(name, input, init) {\n const t = now();\n let res: Response;\n try {\n res = await fetch(input, init);\n } catch (e) {\n timing(name, now() - t, { ok: false });\n throw e;\n }\n timing(name, now() - t, { status: res.status });\n return res;\n },\n ready() {\n if (readyMs === undefined) readyMs = Math.round(now() - t0);\n },\n setMerchantId(id) {\n merchantId = typeof id === \"string\" && id ? id : undefined;\n },\n flush: () => send(false),\n };\n reg[script] = instance;\n return instance;\n } catch {\n return disabled();\n }\n}\n\n/** @internal for tests */\nexport function __resetStorefrontForTests(): void {\n delete g[REG];\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/shared.ts","../src/storefront.ts"],"sourcesContent":["// Shared, runtime-agnostic helpers (used by both the Node and browser entries).\n// Wire format: see docs/PROTOCOL.md (v1).\n\nexport const SDK_NAME = \"@kanca-app/ikas\";\nexport const SDK_VERSION = \"0.3.0\";\nexport const DEFAULT_ENDPOINT = \"https://kanca.onurhan.dev\";\n\nexport const MAX_NAME = 200;\nexport const MAX_ERROR_MESSAGE = 500;\nexport const MAX_ERROR_CODE = 100;\nexport const MAX_ATTR_KEYS = 20;\nexport const MAX_ATTR_STRING = 500;\nexport const MAX_ATTR_KEY = 64;\nexport const MAX_EVENTS_PER_BATCH = 500;\n/** Protocol limit is 512 KB; keep headroom for the envelope. */\nexport const MAX_BATCH_BYTES = 480 * 1024;\n\nexport type EventType = \"webhook\" | \"graphql\" | \"rest\" | \"install\" | \"uninstall\" | \"vital\" | \"storefront\";\n\nexport type AttrValue = string | number | boolean;\n\nexport interface KancaError {\n code: string;\n message?: string;\n}\n\n/** A single wire event (PROTOCOL.md → Event). */\nexport interface KancaEvent {\n id: string;\n type: EventType;\n ts: number;\n name: string;\n status?: number;\n ok?: boolean;\n durationMs?: number;\n value?: number;\n error?: KancaError;\n merchantId?: string;\n deliveryId?: string;\n attrs?: Record<string, AttrValue>;\n}\n\nexport interface Batch {\n sdk: { name: string; version: string };\n events: KancaEvent[];\n}\n\nexport function truncate(s: string, max: number): string {\n return s.length > max ? s.slice(0, max) : s;\n}\n\nfunction hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\n/** UUID v4 via crypto.randomUUID, with a getRandomValues fallback (non-secure browser contexts). */\nexport function uuid(): string {\n const c = (globalThis as { crypto?: Crypto }).crypto;\n if (c && typeof c.randomUUID === \"function\") return c.randomUUID();\n const b = new Uint8Array(16);\n if (c && typeof c.getRandomValues === \"function\") c.getRandomValues(b);\n else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);\n b[6] = (b[6]! & 0x0f) | 0x40;\n b[8] = (b[8]! & 0x3f) | 0x80;\n const h = hex(b);\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;\n}\n\nfunction isFiniteNumber(v: unknown): v is number {\n return typeof v === \"number\" && Number.isFinite(v);\n}\n\nfunction cleanAttrs(attrs: unknown): Record<string, AttrValue> | undefined {\n if (!attrs || typeof attrs !== \"object\") return undefined;\n const out: Record<string, AttrValue> = {};\n let n = 0;\n for (const [k, v] of Object.entries(attrs as Record<string, unknown>)) {\n if (n >= MAX_ATTR_KEYS) break;\n if (v === undefined || v === null || !k) continue;\n let val: AttrValue;\n if (typeof v === \"boolean\") val = v;\n else if (typeof v === \"number\") {\n if (!Number.isFinite(v)) continue;\n val = v;\n } else if (typeof v === \"string\") val = truncate(v, MAX_ATTR_STRING);\n else continue; // attrs are flat: nested values are dropped, never stringified\n out[truncate(k, MAX_ATTR_KEY)] = val;\n n++;\n }\n return n > 0 ? out : undefined;\n}\n\n/**\n * Normalises an event so it always satisfies the protocol limits. Unknown\n * fields are dropped — only the whitelisted metadata below ever leaves the process.\n */\nexport function sanitizeEvent(e: KancaEvent): KancaEvent {\n const out: KancaEvent = {\n id: typeof e.id === \"string\" && e.id ? e.id : uuid(),\n type: e.type,\n ts: isFiniteNumber(e.ts) ? Math.round(e.ts) : Date.now(),\n name: truncate(typeof e.name === \"string\" && e.name ? e.name : \"unknown\", MAX_NAME),\n };\n if (isFiniteNumber(e.status) && e.status >= 100 && e.status <= 599) out.status = Math.trunc(e.status);\n if (typeof e.ok === \"boolean\") out.ok = e.ok;\n if (isFiniteNumber(e.durationMs)) out.durationMs = Math.min(86_400_000, Math.max(0, Math.round(e.durationMs * 10) / 10));\n if (isFiniteNumber(e.value)) out.value = e.value;\n if (e.error && typeof e.error.code === \"string\") {\n out.error = { code: truncate(e.error.code, MAX_ERROR_CODE) };\n if (typeof e.error.message === \"string\" && e.error.message) {\n out.error.message = truncate(e.error.message, MAX_ERROR_MESSAGE);\n }\n }\n if (typeof e.merchantId === \"string\" && e.merchantId) out.merchantId = truncate(e.merchantId, 200);\n if (typeof e.deliveryId === \"string\" && e.deliveryId) out.deliveryId = truncate(e.deliveryId, 200);\n const attrs = cleanAttrs(e.attrs);\n if (attrs) out.attrs = attrs;\n return out;\n}\n\nexport function errorMessage(err: unknown): string | undefined {\n try {\n if (err instanceof Error) return truncate(err.message || err.name, MAX_ERROR_MESSAGE);\n if (typeof err === \"string\") return truncate(err, MAX_ERROR_MESSAGE);\n } catch {\n /* ignore */\n }\n return undefined;\n}\n\nexport function joinUrl(endpoint: string, path: string): string {\n return endpoint.replace(/\\/+$/, \"\") + path;\n}\n\nconst encoder = typeof TextEncoder !== \"undefined\" ? /* @__PURE__ */ new TextEncoder() : undefined;\nexport function byteLength(s: string): number {\n return encoder ? encoder.encode(s).length : s.length * 3;\n}\n\n/**\n * Splits events into protocol-sized batches (<= maxEvents events and\n * <= MAX_BATCH_BYTES serialized). Returns serialized bodies.\n */\nexport function buildBodies(events: KancaEvent[], maxEvents: number): { body: string; events: KancaEvent[] }[] {\n const sdk = { name: SDK_NAME, version: SDK_VERSION };\n const envelope = byteLength(JSON.stringify({ sdk, events: [] }));\n const out: { body: string; events: KancaEvent[] }[] = [];\n let cur: KancaEvent[] = [];\n let size = envelope;\n const push = () => {\n if (cur.length) out.push({ body: JSON.stringify({ sdk, events: cur } satisfies Batch), events: cur });\n cur = [];\n size = envelope;\n };\n for (const ev of events) {\n const len = byteLength(JSON.stringify(ev)) + 1;\n if (cur.length && (cur.length >= maxEvents || size + len > MAX_BATCH_BYTES)) push();\n cur.push(ev);\n size += len;\n }\n push();\n return out;\n}\n","// @kanca-app/ikas/storefront — errors and load cost of the app's own script on\n// merchants' ikas storefronts. Zero dependencies, runs on third-party pages:\n// it never throws, never blocks, never retries, never calls preventDefault and\n// never reads cookies, storage, customer or cart data.\n// Wire format: docs/PROTOCOL.md → `storefront`.\n\nimport { DEFAULT_ENDPOINT, SDK_NAME, SDK_VERSION, uuid, type KancaEvent } from \"./shared\";\n\nexport interface StorefrontOptions {\n /** Public key (`kp_...`). Safe to ship in the script. */\n publicKey: string | undefined;\n /** Kanca base URL. Default `https://kanca.onurhan.dev`. */\n endpoint?: string;\n /** Label for this script in the dashboard, e.g. `\"rush\"`. Default `\"script\"`. */\n scriptName?: string;\n /**\n * Identifies YOUR script in error stacks and resource timing. A string is\n * matched as a substring, a RegExp with `search`. Default: the URL (without\n * query) of `document.currentScript`, so call `initKancaStorefront`\n * synchronously while your script first runs. Without either, global\n * errors are not captured (manual `captureError` and `wrap` still work).\n */\n scriptUrlMatch?: string | RegExp;\n /** The ikas merchant id, if the script knows it. Can be set later with `setMerchantId`. */\n merchantId?: string;\n /**\n * Share of page views that report `load` and `timing:*` events, 0..1.\n * Default 0.1. Errors are always reported. Values below 0.001 (other than 0) are raised to 0.001.\n */\n sampleRate?: number;\n /** Your script's version, e.g. a git sha or package version. */\n release?: string;\n debug?: boolean;\n}\n\nexport interface TimingOptions {\n /** Outcome, when there is no HTTP status. With a status, ok = status < 400. */\n ok?: boolean;\n status?: number;\n}\n\nexport interface KancaStorefront {\n /** Records an error thrown by your script. Deduped and capped per page view. */\n captureError(error: unknown, context?: { where?: string }): void;\n /**\n * Returns `fn` wrapped: an error it throws (or a promise it returns that\n * rejects) is recorded and then rethrown / left rejected, so the page sees\n * exactly what it would have seen without Kanca.\n */\n wrap<F extends (...args: never[]) => unknown>(fn: F, where?: string): F;\n /** Records a duration (sampled page views only). `name`: 1–40 chars of A–Z a–z 0–9 _ . - */\n timing(name: string, durationMs: number, options?: TimingOptions): void;\n /** `fetch` that records `timing:<name>` with the status. Same result and errors as `fetch`. */\n fetch(name: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;\n /** Marks your script as ready (e.g. first render done). Only the first call counts. */\n ready(): void;\n setMerchantId(merchantId: string | undefined): void;\n /** Sends what is queued now. Normally not needed: events go out when the page is hidden. */\n flush(): void;\n}\n\nexport const MAX_ERRORS_PER_VIEW = 10;\nexport const MAX_TIMINGS_PER_VIEW = 20;\n/** Queue size that triggers an early send. */\nexport const FLUSH_AT = 10;\nconst MAX_MESSAGE = 300;\n\nconst clip = (s: string, n: number) => (s.length > n ? s.slice(0, n) : s);\nconst round1 = (n: number) => Math.round(n * 10) / 10;\nconst now = () => (typeof performance !== \"undefined\" && performance.now ? performance.now() : Date.now());\n\nconst UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;\n\n/**\n * URL path → low-cardinality pattern without identifiers: uuids → `:id`,\n * numeric segments → `:n`, e-mails → `:email`, long opaque tokens → `:token`,\n * digit runs of 5+ inside a slug → `:n`. Query and hash are dropped, at most\n * 8 segments are kept.\n */\nexport function pathPattern(path: string): string {\n const segs = path.split(/[?#]/)[0]!.split(\"/\").filter(Boolean).slice(0, 8);\n return clip(\n \"/\" +\n segs\n .map((s) =>\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)\n ? \":id\"\n : /^\\d+$/.test(s)\n ? \":n\"\n : /@|%40/.test(s)\n ? \":email\"\n : s.length > 24 && /\\d/.test(s) && !/-.*-/.test(s)\n ? \":token\"\n : clip(s.replace(/\\d{5,}/g, \":n\"), 60),\n )\n .join(\"/\"),\n 200,\n );\n}\n\n/** Strips likely PII / identifiers from an error message and caps its length. */\nexport function scrubMessage(message: string): string {\n return clip(\n message\n .replace(/(https?:\\/\\/[^\\s?#\"'<>()]*)[?#][^\\s\"'<>()]*/g, \"$1\")\n .replace(/[\\w.+-]+@[\\w-]+\\.[\\w.-]+/g, \"<email>\")\n .replace(UUID, \"<id>\")\n .replace(/[\\w-]{32,}/g, (m) => (/\\d/.test(m) ? \"<token>\" : m))\n .replace(/\\d{6,}/g, \"<n>\"),\n MAX_MESSAGE,\n );\n}\n\n/**\n * The topmost stack frame that has a URL, as `file:line:col` (last path\n * segment, no query), if that frame is ours. Frames without a URL (native\n * code such as `JSON.parse`) are skipped, so an error thrown by a built-in\n * called from our code is ours, while an error thrown by theme code that our\n * code called is not. Returns undefined when the top URL frame is not ours.\n */\nexport function ownFrame(stack: unknown, isOurs: (s: string) => boolean): string | undefined {\n if (typeof stack !== \"string\") return undefined;\n for (const line of stack.split(\"\\n\")) {\n const m = /(https?:\\/\\/[^\\s)]+?):(\\d+):(\\d+)/.exec(line);\n if (!m) continue;\n if (!isOurs(line)) return undefined;\n const file = m[1]!.split(/[?#]/)[0]!.split(\"/\").pop() || \"script\";\n return clip(`${file}:${m[2]}:${m[3]}`, 200);\n }\n return undefined;\n}\n\n/** Name + scrubbed message of anything thrown. */\nexport function describeError(err: unknown): { code: string; message?: string } {\n const e = err as { name?: unknown; message?: unknown } | null;\n if (e && typeof e === \"object\" && typeof e.message === \"string\") {\n const name = typeof e.name === \"string\" && e.name ? e.name : \"Error\";\n return { code: clip(name, 100), message: scrubMessage(e.message) || undefined };\n }\n if (typeof err === \"string\") return { code: \"Error\", message: scrubMessage(err) || undefined };\n return { code: \"NonError\", message: typeof err };\n}\n\nconst noop = () => undefined;\n\nfunction disabled(): KancaStorefront {\n return {\n captureError: noop,\n wrap: (fn) => fn,\n timing: noop,\n fetch: (_name, input, init) => fetch(input, init),\n ready: noop,\n setMerchantId: noop,\n flush: noop,\n };\n}\n\n// Global (not module) registry so two copies of the script on one page share one instance per scriptName.\nconst REG = \"__kancaStorefront\";\nconst g = globalThis as { [REG]?: Record<string, KancaStorefront> };\n\n/**\n * Starts storefront monitoring for one script. Call it synchronously at the\n * top of your script. Returns an instance whose methods never throw. Safe to\n * call more than once: the same `scriptName` returns the same instance.\n */\nexport function initKancaStorefront(options: StorefrontOptions): KancaStorefront {\n try {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return disabled();\n const o = options || ({} as StorefrontOptions);\n const script = clip(o.scriptName || \"script\", 64);\n const reg = (g[REG] ||= {});\n if (reg[script]) return reg[script]!;\n const key = typeof o.publicKey === \"string\" ? o.publicKey.trim() : \"\";\n const debug = !!o.debug;\n if (!key.startsWith(\"kp_\")) {\n if (debug || key) console.warn(\"[kanca] storefront: kp_ key required\");\n return disabled();\n }\n\n const url = `${(o.endpoint || DEFAULT_ENDPOINT).replace(/\\/+$/, \"\")}/api/ingest/browser?key=${encodeURIComponent(key)}`;\n let match = o.scriptUrlMatch;\n if (!match) {\n const src = (document.currentScript as HTMLScriptElement | null)?.src;\n if (src && /^https?:/.test(src)) match = src.split(/[?#]/)[0];\n }\n const isOurs = (s: string) =>\n !!match && !!s && (typeof match === \"string\" ? s.indexOf(match) >= 0 : s.search(match) >= 0);\n\n let rate = typeof o.sampleRate === \"number\" && o.sampleRate >= 0 ? Math.min(1, o.sampleRate) : 0.1;\n if (rate > 0 && rate < 0.001) rate = 0.001;\n const sampled = rate > 0 && Math.random() < rate;\n const path0 = pathPattern(location.pathname || \"/\");\n const pv = uuid().replace(/-/g, \"\").slice(0, 16);\n const t0 = now();\n const ts0 = Date.now();\n let merchantId = o.merchantId;\n let readyMs: number | undefined;\n let loadDone = false;\n let errors = 0;\n let timings = 0;\n const seen = new Set<string>();\n const captured = new WeakSet<object>();\n let queue: KancaEvent[] = [];\n\n const event = (name: string, fields: Partial<KancaEvent>, attrs?: Record<string, string | number | undefined>): KancaEvent => {\n const a: Record<string, string | number> = {\n host: clip(location.hostname || \"unknown\", 200),\n path: pathPattern(location.pathname || \"/\"),\n script,\n pv,\n sample: name === \"error\" ? 1 : rate,\n };\n if (o.release) a.release = clip(String(o.release), 200);\n for (const k in attrs) if (attrs[k] !== undefined) a[k] = attrs[k]!;\n return { id: uuid(), type: \"storefront\", ts: Date.now(), name, ...fields, attrs: a };\n };\n\n const send = (final: boolean) => {\n try {\n if (final && sampled && !loadDone) {\n loadDone = true;\n queue.push(loadEvent());\n }\n if (!queue.length) return;\n const events = queue;\n queue = [];\n if (merchantId) for (const e of events) e.merchantId = clip(merchantId, 200);\n const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });\n if (debug) console.debug(\"[kanca] storefront\", events);\n // fetch+keepalive first: content blockers drop sendBeacon (\"ping\")\n // requests while sendBeacon still returns true. text/plain keeps it a\n // simple request (no CORS preflight). Never retried.\n try {\n if (typeof fetch === \"function\") {\n fetch(url, { method: \"POST\", body, keepalive: true, mode: \"cors\", credentials: \"omit\", headers: { \"content-type\": \"text/plain\" } }).catch(noop);\n return;\n }\n } catch {\n /* fall through */\n }\n navigator.sendBeacon?.(url, body);\n } catch {\n /* never throw */\n }\n };\n\n const push = (e: KancaEvent) => {\n queue.push(e);\n if (queue.length >= FLUSH_AT) send(false);\n };\n\n // One per sampled page view: the page-view record behind every storefront metric.\n const loadEvent = (): KancaEvent => {\n const attrs: Record<string, string | number | undefined> = { path: path0, errors, ready_ms: readyMs };\n const fields: Partial<KancaEvent> = {};\n try {\n const entries = performance.getEntriesByType(\"resource\") as PerformanceResourceTiming[];\n for (let i = entries.length - 1; i >= 0; i--) {\n const r = entries[i]!;\n if (!isOurs(r.name)) continue;\n if (r.responseEnd > 0) fields.durationMs = round1(r.responseEnd - r.startTime);\n // Sizes are only exposed with Timing-Allow-Origin; 0/0 means \"unknown\".\n if (r.encodedBodySize > 0) {\n fields.value = r.transferSize;\n attrs.encoded = r.encodedBodySize;\n attrs.cache = r.transferSize === 0 ? \"hit\" : r.transferSize < r.encodedBodySize ? \"revalidated\" : \"network\";\n }\n break;\n }\n } catch {\n /* no resource timing */\n }\n const e = event(\"load\", fields, attrs);\n e.ts = ts0;\n return e;\n };\n\n const captureError = (err: unknown, where?: string) => {\n try {\n if (err && typeof err === \"object\") {\n if (captured.has(err)) return;\n captured.add(err);\n }\n if (errors >= MAX_ERRORS_PER_VIEW) return;\n const d = describeError(err);\n const frame = ownFrame((err as { stack?: unknown } | null)?.stack, isOurs);\n const k = `${d.code}|${d.message}|${frame}`;\n if (seen.has(k)) return;\n seen.add(k);\n errors++;\n push(event(\"error\", { ok: false, error: d }, { where: where ? clip(String(where), 100) : undefined, frame }));\n } catch {\n /* ignore */\n }\n };\n\n const timing = (name: string, ms: number, t?: TimingOptions) => {\n try {\n if (!sampled || timings >= MAX_TIMINGS_PER_VIEW) return;\n if (!/^[A-Za-z0-9_.-]{1,40}$/.test(name) || !(ms >= 0 && ms < 864e5)) {\n if (debug) console.warn(\"[kanca] bad timing\", name, ms);\n return;\n }\n timings++;\n const f: Partial<KancaEvent> = { durationMs: round1(ms) };\n if (t && typeof t.status === \"number\" && t.status >= 100 && t.status <= 599) f.status = Math.trunc(t.status);\n if (t && typeof t.ok === \"boolean\") f.ok = t.ok;\n push(event(`timing:${name}`, f));\n } catch {\n /* ignore */\n }\n };\n\n if (match) {\n // Only errors from our script: the page's theme and other apps are never recorded.\n // Attribution: the top URL frame of the stack, else the event's filename.\n const ours = (err: unknown, filename?: string) => {\n const stack = (err as { stack?: unknown } | null)?.stack;\n return typeof stack === \"string\" && /https?:/.test(stack) ? !!ownFrame(stack, isOurs) : !!filename && isOurs(filename);\n };\n addEventListener(\"error\", (ev: ErrorEvent) => {\n try {\n if (ours(ev.error, ev.filename)) captureError(ev.error ?? ev.message, \"onerror\");\n } catch {\n /* ignore */\n }\n });\n addEventListener(\"unhandledrejection\", (ev: PromiseRejectionEvent) => {\n try {\n if (ours(ev.reason)) captureError(ev.reason, \"unhandledrejection\");\n } catch {\n /* ignore */\n }\n });\n } else if (debug) {\n console.warn(\"[kanca] storefront: no script URL, global errors off\");\n }\n\n addEventListener(\n \"visibilitychange\",\n () => {\n if (document.visibilityState === \"hidden\") send(true);\n },\n true,\n );\n addEventListener(\"pagehide\", () => send(true), true);\n\n const instance: KancaStorefront = {\n captureError: (err, ctx) => captureError(err, ctx?.where),\n wrap(fn, where) {\n if (typeof fn !== \"function\") return fn;\n return function (this: unknown, ...args: never[]) {\n let r: unknown;\n try {\n r = fn.apply(this, args);\n } catch (e) {\n captureError(e, where);\n throw e;\n }\n try {\n if (r && typeof (r as Promise<unknown>).then === \"function\") {\n // A side branch: the returned promise is untouched and still rejects for the caller.\n (r as Promise<unknown>).then(undefined, (e) => captureError(e, where));\n }\n } catch {\n /* ignore */\n }\n return r;\n } as unknown as typeof fn;\n },\n timing,\n async fetch(name, input, init) {\n const t = now();\n let res: Response;\n try {\n res = await fetch(input, init);\n } catch (e) {\n timing(name, now() - t, { ok: false });\n throw e;\n }\n timing(name, now() - t, { status: res.status });\n return res;\n },\n ready() {\n if (readyMs === undefined) readyMs = Math.round(now() - t0);\n },\n setMerchantId(id) {\n merchantId = typeof id === \"string\" && id ? id : undefined;\n },\n flush: () => send(false),\n };\n reg[script] = instance;\n return instance;\n } catch {\n return disabled();\n }\n}\n\n/** @internal for tests */\nexport function __resetStorefrontForTests(): void {\n delete g[REG];\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanca-app/ikas",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Observability for ikas apps: webhook deliveries, Admin API calls, installs/uninstalls, admin UI web vitals and storefront script errors.",
5
5
  "license": "MIT",
6
6
  "type": "module",