@kanca-app/ikas 0.1.1 → 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.
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/storefront.ts
21
+ var storefront_exports = {};
22
+ __export(storefront_exports, {
23
+ FLUSH_AT: () => FLUSH_AT,
24
+ MAX_ERRORS_PER_VIEW: () => MAX_ERRORS_PER_VIEW,
25
+ MAX_TIMINGS_PER_VIEW: () => MAX_TIMINGS_PER_VIEW,
26
+ __resetStorefrontForTests: () => __resetStorefrontForTests,
27
+ describeError: () => describeError,
28
+ initKancaStorefront: () => initKancaStorefront,
29
+ ownFrame: () => ownFrame,
30
+ pathPattern: () => pathPattern,
31
+ scrubMessage: () => scrubMessage
32
+ });
33
+ module.exports = __toCommonJS(storefront_exports);
34
+
35
+ // src/shared.ts
36
+ var SDK_NAME = "@kanca-app/ikas";
37
+ var SDK_VERSION = "0.3.0";
38
+ var DEFAULT_ENDPOINT = "https://kanca.onurhan.dev";
39
+ var MAX_BATCH_BYTES = 480 * 1024;
40
+ function hex(bytes) {
41
+ let out = "";
42
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
43
+ return out;
44
+ }
45
+ function uuid() {
46
+ const c = globalThis.crypto;
47
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
48
+ const b = new Uint8Array(16);
49
+ if (c && typeof c.getRandomValues === "function") c.getRandomValues(b);
50
+ else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
51
+ b[6] = b[6] & 15 | 64;
52
+ b[8] = b[8] & 63 | 128;
53
+ const h = hex(b);
54
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
55
+ }
56
+
57
+ // src/storefront.ts
58
+ var MAX_ERRORS_PER_VIEW = 10;
59
+ var MAX_TIMINGS_PER_VIEW = 20;
60
+ var FLUSH_AT = 10;
61
+ var MAX_MESSAGE = 300;
62
+ var clip = (s, n) => s.length > n ? s.slice(0, n) : s;
63
+ var round1 = (n) => Math.round(n * 10) / 10;
64
+ var now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
65
+ var UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
66
+ function pathPattern(path) {
67
+ const segs = path.split(/[?#]/)[0].split("/").filter(Boolean).slice(0, 8);
68
+ return clip(
69
+ "/" + segs.map(
70
+ (s) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s) ? ":id" : /^\d+$/.test(s) ? ":n" : /@|%40/.test(s) ? ":email" : s.length > 24 && /\d/.test(s) && !/-.*-/.test(s) ? ":token" : clip(s.replace(/\d{5,}/g, ":n"), 60)
71
+ ).join("/"),
72
+ 200
73
+ );
74
+ }
75
+ function scrubMessage(message) {
76
+ return clip(
77
+ message.replace(/(https?:\/\/[^\s?#"'<>()]*)[?#][^\s"'<>()]*/g, "$1").replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "<email>").replace(UUID, "<id>").replace(/[\w-]{32,}/g, (m) => /\d/.test(m) ? "<token>" : m).replace(/\d{6,}/g, "<n>"),
78
+ MAX_MESSAGE
79
+ );
80
+ }
81
+ function ownFrame(stack, isOurs) {
82
+ if (typeof stack !== "string") return void 0;
83
+ for (const line of stack.split("\n")) {
84
+ const m = /(https?:\/\/[^\s)]+?):(\d+):(\d+)/.exec(line);
85
+ if (!m) continue;
86
+ if (!isOurs(line)) return void 0;
87
+ const file = m[1].split(/[?#]/)[0].split("/").pop() || "script";
88
+ return clip(`${file}:${m[2]}:${m[3]}`, 200);
89
+ }
90
+ return void 0;
91
+ }
92
+ function describeError(err) {
93
+ const e = err;
94
+ if (e && typeof e === "object" && typeof e.message === "string") {
95
+ const name = typeof e.name === "string" && e.name ? e.name : "Error";
96
+ return { code: clip(name, 100), message: scrubMessage(e.message) || void 0 };
97
+ }
98
+ if (typeof err === "string") return { code: "Error", message: scrubMessage(err) || void 0 };
99
+ return { code: "NonError", message: typeof err };
100
+ }
101
+ var noop = () => void 0;
102
+ function disabled() {
103
+ return {
104
+ captureError: noop,
105
+ wrap: (fn) => fn,
106
+ timing: noop,
107
+ fetch: (_name, input, init) => fetch(input, init),
108
+ ready: noop,
109
+ setMerchantId: noop,
110
+ flush: noop
111
+ };
112
+ }
113
+ var REG = "__kancaStorefront";
114
+ var g = globalThis;
115
+ function initKancaStorefront(options) {
116
+ try {
117
+ if (typeof window === "undefined" || typeof document === "undefined") return disabled();
118
+ const o = options || {};
119
+ const script = clip(o.scriptName || "script", 64);
120
+ const reg = g[REG] ||= {};
121
+ if (reg[script]) return reg[script];
122
+ const key = typeof o.publicKey === "string" ? o.publicKey.trim() : "";
123
+ const debug = !!o.debug;
124
+ if (!key.startsWith("kp_")) {
125
+ if (debug || key) console.warn("[kanca] storefront: kp_ key required");
126
+ return disabled();
127
+ }
128
+ const url = `${(o.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "")}/api/ingest/browser?key=${encodeURIComponent(key)}`;
129
+ let match = o.scriptUrlMatch;
130
+ if (!match) {
131
+ const src = document.currentScript?.src;
132
+ if (src && /^https?:/.test(src)) match = src.split(/[?#]/)[0];
133
+ }
134
+ const isOurs = (s) => !!match && !!s && (typeof match === "string" ? s.indexOf(match) >= 0 : s.search(match) >= 0);
135
+ let rate = typeof o.sampleRate === "number" && o.sampleRate >= 0 ? Math.min(1, o.sampleRate) : 0.1;
136
+ if (rate > 0 && rate < 1e-3) rate = 1e-3;
137
+ const sampled = rate > 0 && Math.random() < rate;
138
+ const path0 = pathPattern(location.pathname || "/");
139
+ const pv = uuid().replace(/-/g, "").slice(0, 16);
140
+ const t0 = now();
141
+ const ts0 = Date.now();
142
+ let merchantId = o.merchantId;
143
+ let readyMs;
144
+ let loadDone = false;
145
+ let errors = 0;
146
+ let timings = 0;
147
+ const seen = /* @__PURE__ */ new Set();
148
+ const captured = /* @__PURE__ */ new WeakSet();
149
+ let queue = [];
150
+ const event = (name, fields, attrs) => {
151
+ const a = {
152
+ host: clip(location.hostname || "unknown", 200),
153
+ path: pathPattern(location.pathname || "/"),
154
+ script,
155
+ pv,
156
+ sample: name === "error" ? 1 : rate
157
+ };
158
+ if (o.release) a.release = clip(String(o.release), 200);
159
+ for (const k in attrs) if (attrs[k] !== void 0) a[k] = attrs[k];
160
+ return { id: uuid(), type: "storefront", ts: Date.now(), name, ...fields, attrs: a };
161
+ };
162
+ const send = (final) => {
163
+ try {
164
+ if (final && sampled && !loadDone) {
165
+ loadDone = true;
166
+ queue.push(loadEvent());
167
+ }
168
+ if (!queue.length) return;
169
+ const events = queue;
170
+ queue = [];
171
+ if (merchantId) for (const e of events) e.merchantId = clip(merchantId, 200);
172
+ const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });
173
+ if (debug) console.debug("[kanca] storefront", events);
174
+ try {
175
+ if (typeof fetch === "function") {
176
+ fetch(url, { method: "POST", body, keepalive: true, mode: "cors", credentials: "omit", headers: { "content-type": "text/plain" } }).catch(noop);
177
+ return;
178
+ }
179
+ } catch {
180
+ }
181
+ navigator.sendBeacon?.(url, body);
182
+ } catch {
183
+ }
184
+ };
185
+ const push = (e) => {
186
+ queue.push(e);
187
+ if (queue.length >= FLUSH_AT) send(false);
188
+ };
189
+ const loadEvent = () => {
190
+ const attrs = { path: path0, errors, ready_ms: readyMs };
191
+ const fields = {};
192
+ try {
193
+ const entries = performance.getEntriesByType("resource");
194
+ for (let i = entries.length - 1; i >= 0; i--) {
195
+ const r = entries[i];
196
+ if (!isOurs(r.name)) continue;
197
+ if (r.responseEnd > 0) fields.durationMs = round1(r.responseEnd - r.startTime);
198
+ if (r.encodedBodySize > 0) {
199
+ fields.value = r.transferSize;
200
+ attrs.encoded = r.encodedBodySize;
201
+ attrs.cache = r.transferSize === 0 ? "hit" : r.transferSize < r.encodedBodySize ? "revalidated" : "network";
202
+ }
203
+ break;
204
+ }
205
+ } catch {
206
+ }
207
+ const e = event("load", fields, attrs);
208
+ e.ts = ts0;
209
+ return e;
210
+ };
211
+ const captureError = (err, where) => {
212
+ try {
213
+ if (err && typeof err === "object") {
214
+ if (captured.has(err)) return;
215
+ captured.add(err);
216
+ }
217
+ if (errors >= MAX_ERRORS_PER_VIEW) return;
218
+ const d = describeError(err);
219
+ const frame = ownFrame(err?.stack, isOurs);
220
+ const k = `${d.code}|${d.message}|${frame}`;
221
+ if (seen.has(k)) return;
222
+ seen.add(k);
223
+ errors++;
224
+ push(event("error", { ok: false, error: d }, { where: where ? clip(String(where), 100) : void 0, frame }));
225
+ } catch {
226
+ }
227
+ };
228
+ const timing = (name, ms, t) => {
229
+ try {
230
+ if (!sampled || timings >= MAX_TIMINGS_PER_VIEW) return;
231
+ if (!/^[A-Za-z0-9_.-]{1,40}$/.test(name) || !(ms >= 0 && ms < 864e5)) {
232
+ if (debug) console.warn("[kanca] bad timing", name, ms);
233
+ return;
234
+ }
235
+ timings++;
236
+ const f = { durationMs: round1(ms) };
237
+ if (t && typeof t.status === "number" && t.status >= 100 && t.status <= 599) f.status = Math.trunc(t.status);
238
+ if (t && typeof t.ok === "boolean") f.ok = t.ok;
239
+ push(event(`timing:${name}`, f));
240
+ } catch {
241
+ }
242
+ };
243
+ if (match) {
244
+ const ours = (err, filename) => {
245
+ const stack = err?.stack;
246
+ return typeof stack === "string" && /https?:/.test(stack) ? !!ownFrame(stack, isOurs) : !!filename && isOurs(filename);
247
+ };
248
+ addEventListener("error", (ev) => {
249
+ try {
250
+ if (ours(ev.error, ev.filename)) captureError(ev.error ?? ev.message, "onerror");
251
+ } catch {
252
+ }
253
+ });
254
+ addEventListener("unhandledrejection", (ev) => {
255
+ try {
256
+ if (ours(ev.reason)) captureError(ev.reason, "unhandledrejection");
257
+ } catch {
258
+ }
259
+ });
260
+ } else if (debug) {
261
+ console.warn("[kanca] storefront: no script URL, global errors off");
262
+ }
263
+ addEventListener(
264
+ "visibilitychange",
265
+ () => {
266
+ if (document.visibilityState === "hidden") send(true);
267
+ },
268
+ true
269
+ );
270
+ addEventListener("pagehide", () => send(true), true);
271
+ const instance = {
272
+ captureError: (err, ctx) => captureError(err, ctx?.where),
273
+ wrap(fn, where) {
274
+ if (typeof fn !== "function") return fn;
275
+ return function(...args) {
276
+ let r;
277
+ try {
278
+ r = fn.apply(this, args);
279
+ } catch (e) {
280
+ captureError(e, where);
281
+ throw e;
282
+ }
283
+ try {
284
+ if (r && typeof r.then === "function") {
285
+ r.then(void 0, (e) => captureError(e, where));
286
+ }
287
+ } catch {
288
+ }
289
+ return r;
290
+ };
291
+ },
292
+ timing,
293
+ async fetch(name, input, init) {
294
+ const t = now();
295
+ let res;
296
+ try {
297
+ res = await fetch(input, init);
298
+ } catch (e) {
299
+ timing(name, now() - t, { ok: false });
300
+ throw e;
301
+ }
302
+ timing(name, now() - t, { status: res.status });
303
+ return res;
304
+ },
305
+ ready() {
306
+ if (readyMs === void 0) readyMs = Math.round(now() - t0);
307
+ },
308
+ setMerchantId(id) {
309
+ merchantId = typeof id === "string" && id ? id : void 0;
310
+ },
311
+ flush: () => send(false)
312
+ };
313
+ reg[script] = instance;
314
+ return instance;
315
+ } catch {
316
+ return disabled();
317
+ }
318
+ }
319
+ function __resetStorefrontForTests() {
320
+ delete g[REG];
321
+ }
322
+ //# sourceMappingURL=storefront.cjs.map
@@ -0,0 +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.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":[]}
@@ -0,0 +1,88 @@
1
+ interface StorefrontOptions {
2
+ /** Public key (`kp_...`). Safe to ship in the script. */
3
+ publicKey: string | undefined;
4
+ /** Kanca base URL. Default `https://kanca.onurhan.dev`. */
5
+ endpoint?: string;
6
+ /** Label for this script in the dashboard, e.g. `"rush"`. Default `"script"`. */
7
+ scriptName?: string;
8
+ /**
9
+ * Identifies YOUR script in error stacks and resource timing. A string is
10
+ * matched as a substring, a RegExp with `search`. Default: the URL (without
11
+ * query) of `document.currentScript`, so call `initKancaStorefront`
12
+ * synchronously while your script first runs. Without either, global
13
+ * errors are not captured (manual `captureError` and `wrap` still work).
14
+ */
15
+ scriptUrlMatch?: string | RegExp;
16
+ /** The ikas merchant id, if the script knows it. Can be set later with `setMerchantId`. */
17
+ merchantId?: string;
18
+ /**
19
+ * Share of page views that report `load` and `timing:*` events, 0..1.
20
+ * Default 0.1. Errors are always reported. Values below 0.001 (other than 0) are raised to 0.001.
21
+ */
22
+ sampleRate?: number;
23
+ /** Your script's version, e.g. a git sha or package version. */
24
+ release?: string;
25
+ debug?: boolean;
26
+ }
27
+ interface TimingOptions {
28
+ /** Outcome, when there is no HTTP status. With a status, ok = status < 400. */
29
+ ok?: boolean;
30
+ status?: number;
31
+ }
32
+ interface KancaStorefront {
33
+ /** Records an error thrown by your script. Deduped and capped per page view. */
34
+ captureError(error: unknown, context?: {
35
+ where?: string;
36
+ }): void;
37
+ /**
38
+ * Returns `fn` wrapped: an error it throws (or a promise it returns that
39
+ * rejects) is recorded and then rethrown / left rejected, so the page sees
40
+ * exactly what it would have seen without Kanca.
41
+ */
42
+ wrap<F extends (...args: never[]) => unknown>(fn: F, where?: string): F;
43
+ /** Records a duration (sampled page views only). `name`: 1–40 chars of A–Z a–z 0–9 _ . - */
44
+ timing(name: string, durationMs: number, options?: TimingOptions): void;
45
+ /** `fetch` that records `timing:<name>` with the status. Same result and errors as `fetch`. */
46
+ fetch(name: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
47
+ /** Marks your script as ready (e.g. first render done). Only the first call counts. */
48
+ ready(): void;
49
+ setMerchantId(merchantId: string | undefined): void;
50
+ /** Sends what is queued now. Normally not needed: events go out when the page is hidden. */
51
+ flush(): void;
52
+ }
53
+ declare const MAX_ERRORS_PER_VIEW = 10;
54
+ declare const MAX_TIMINGS_PER_VIEW = 20;
55
+ /** Queue size that triggers an early send. */
56
+ declare const FLUSH_AT = 10;
57
+ /**
58
+ * URL path → low-cardinality pattern without identifiers: uuids → `:id`,
59
+ * numeric segments → `:n`, e-mails → `:email`, long opaque tokens → `:token`,
60
+ * digit runs of 5+ inside a slug → `:n`. Query and hash are dropped, at most
61
+ * 8 segments are kept.
62
+ */
63
+ declare function pathPattern(path: string): string;
64
+ /** Strips likely PII / identifiers from an error message and caps its length. */
65
+ declare function scrubMessage(message: string): string;
66
+ /**
67
+ * The topmost stack frame that has a URL, as `file:line:col` (last path
68
+ * segment, no query), if that frame is ours. Frames without a URL (native
69
+ * code such as `JSON.parse`) are skipped, so an error thrown by a built-in
70
+ * called from our code is ours, while an error thrown by theme code that our
71
+ * code called is not. Returns undefined when the top URL frame is not ours.
72
+ */
73
+ declare function ownFrame(stack: unknown, isOurs: (s: string) => boolean): string | undefined;
74
+ /** Name + scrubbed message of anything thrown. */
75
+ declare function describeError(err: unknown): {
76
+ code: string;
77
+ message?: string;
78
+ };
79
+ /**
80
+ * Starts storefront monitoring for one script. Call it synchronously at the
81
+ * top of your script. Returns an instance whose methods never throw. Safe to
82
+ * call more than once: the same `scriptName` returns the same instance.
83
+ */
84
+ declare function initKancaStorefront(options: StorefrontOptions): KancaStorefront;
85
+ /** @internal for tests */
86
+ declare function __resetStorefrontForTests(): void;
87
+
88
+ export { FLUSH_AT, type KancaStorefront, MAX_ERRORS_PER_VIEW, MAX_TIMINGS_PER_VIEW, type StorefrontOptions, type TimingOptions, __resetStorefrontForTests, describeError, initKancaStorefront, ownFrame, pathPattern, scrubMessage };
@@ -0,0 +1,88 @@
1
+ interface StorefrontOptions {
2
+ /** Public key (`kp_...`). Safe to ship in the script. */
3
+ publicKey: string | undefined;
4
+ /** Kanca base URL. Default `https://kanca.onurhan.dev`. */
5
+ endpoint?: string;
6
+ /** Label for this script in the dashboard, e.g. `"rush"`. Default `"script"`. */
7
+ scriptName?: string;
8
+ /**
9
+ * Identifies YOUR script in error stacks and resource timing. A string is
10
+ * matched as a substring, a RegExp with `search`. Default: the URL (without
11
+ * query) of `document.currentScript`, so call `initKancaStorefront`
12
+ * synchronously while your script first runs. Without either, global
13
+ * errors are not captured (manual `captureError` and `wrap` still work).
14
+ */
15
+ scriptUrlMatch?: string | RegExp;
16
+ /** The ikas merchant id, if the script knows it. Can be set later with `setMerchantId`. */
17
+ merchantId?: string;
18
+ /**
19
+ * Share of page views that report `load` and `timing:*` events, 0..1.
20
+ * Default 0.1. Errors are always reported. Values below 0.001 (other than 0) are raised to 0.001.
21
+ */
22
+ sampleRate?: number;
23
+ /** Your script's version, e.g. a git sha or package version. */
24
+ release?: string;
25
+ debug?: boolean;
26
+ }
27
+ interface TimingOptions {
28
+ /** Outcome, when there is no HTTP status. With a status, ok = status < 400. */
29
+ ok?: boolean;
30
+ status?: number;
31
+ }
32
+ interface KancaStorefront {
33
+ /** Records an error thrown by your script. Deduped and capped per page view. */
34
+ captureError(error: unknown, context?: {
35
+ where?: string;
36
+ }): void;
37
+ /**
38
+ * Returns `fn` wrapped: an error it throws (or a promise it returns that
39
+ * rejects) is recorded and then rethrown / left rejected, so the page sees
40
+ * exactly what it would have seen without Kanca.
41
+ */
42
+ wrap<F extends (...args: never[]) => unknown>(fn: F, where?: string): F;
43
+ /** Records a duration (sampled page views only). `name`: 1–40 chars of A–Z a–z 0–9 _ . - */
44
+ timing(name: string, durationMs: number, options?: TimingOptions): void;
45
+ /** `fetch` that records `timing:<name>` with the status. Same result and errors as `fetch`. */
46
+ fetch(name: string, input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
47
+ /** Marks your script as ready (e.g. first render done). Only the first call counts. */
48
+ ready(): void;
49
+ setMerchantId(merchantId: string | undefined): void;
50
+ /** Sends what is queued now. Normally not needed: events go out when the page is hidden. */
51
+ flush(): void;
52
+ }
53
+ declare const MAX_ERRORS_PER_VIEW = 10;
54
+ declare const MAX_TIMINGS_PER_VIEW = 20;
55
+ /** Queue size that triggers an early send. */
56
+ declare const FLUSH_AT = 10;
57
+ /**
58
+ * URL path → low-cardinality pattern without identifiers: uuids → `:id`,
59
+ * numeric segments → `:n`, e-mails → `:email`, long opaque tokens → `:token`,
60
+ * digit runs of 5+ inside a slug → `:n`. Query and hash are dropped, at most
61
+ * 8 segments are kept.
62
+ */
63
+ declare function pathPattern(path: string): string;
64
+ /** Strips likely PII / identifiers from an error message and caps its length. */
65
+ declare function scrubMessage(message: string): string;
66
+ /**
67
+ * The topmost stack frame that has a URL, as `file:line:col` (last path
68
+ * segment, no query), if that frame is ours. Frames without a URL (native
69
+ * code such as `JSON.parse`) are skipped, so an error thrown by a built-in
70
+ * called from our code is ours, while an error thrown by theme code that our
71
+ * code called is not. Returns undefined when the top URL frame is not ours.
72
+ */
73
+ declare function ownFrame(stack: unknown, isOurs: (s: string) => boolean): string | undefined;
74
+ /** Name + scrubbed message of anything thrown. */
75
+ declare function describeError(err: unknown): {
76
+ code: string;
77
+ message?: string;
78
+ };
79
+ /**
80
+ * Starts storefront monitoring for one script. Call it synchronously at the
81
+ * top of your script. Returns an instance whose methods never throw. Safe to
82
+ * call more than once: the same `scriptName` returns the same instance.
83
+ */
84
+ declare function initKancaStorefront(options: StorefrontOptions): KancaStorefront;
85
+ /** @internal for tests */
86
+ declare function __resetStorefrontForTests(): void;
87
+
88
+ export { FLUSH_AT, type KancaStorefront, MAX_ERRORS_PER_VIEW, MAX_TIMINGS_PER_VIEW, type StorefrontOptions, type TimingOptions, __resetStorefrontForTests, describeError, initKancaStorefront, ownFrame, pathPattern, scrubMessage };