@kanca-app/ikas 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @kanca-app/ikas
2
2
 
3
3
  Observability for [ikas](https://ikas.com) apps: webhook deliveries, Admin API
4
- calls, installs/uninstalls and admin UI web vitals — shown in the
4
+ calls, installs/uninstalls, admin UI web vitals and your storefront script's
5
+ errors and load cost — shown in the
5
6
  [Kanca](https://kanca.onurhan.dev) dashboard.
6
7
 
7
8
  - **Metadata only.** Never payloads, variables, responses, headers or tokens.
@@ -18,7 +19,7 @@ pnpm add @kanca-app/ikas
18
19
  | Variable | Where | Value |
19
20
  | ------------------------------ | ------------- | ------------------------------------ |
20
21
  | `KANCA_KEY` | backend only | server key `kk_...` |
21
- | `NEXT_PUBLIC_KANCA_PUBLIC_KEY` | browser | public key `kp_...` (vitals only) |
22
+ | `NEXT_PUBLIC_KANCA_PUBLIC_KEY` | browser | public key `kp_...` (vitals and storefront events only) |
22
23
 
23
24
  Without `KANCA_KEY` the SDK is a silent no-op, so local dev and CI need no setup.
24
25
 
@@ -34,8 +35,14 @@ export const kanca = createKanca({ key: process.env.KANCA_KEY });
34
35
  ### 1. Webhooks — `src/app/api/webhooks/ikas/route.ts`
35
36
 
36
37
  Wrap your route handler. Each delivery records scope, delivery id, merchant id,
37
- your response status and duration. Subscribe to `store/app/deleted` to get
38
- uninstalls for free.
38
+ your response status and duration.
39
+
40
+ Uninstalls: ikas does not document an app-uninstall webhook scope. If a
41
+ `store/app/deleted`, `store/app/uninstalled` or `store/authorizedApp/deleted`
42
+ delivery ever arrives, the SDK records it as an uninstall too. Kanca
43
+ additionally infers **access revoked** for a store when its Admin API calls
44
+ keep failing with an authorization error (see step 3 and
45
+ [`docs/PROTOCOL.md`](../../docs/PROTOCOL.md)).
39
46
 
40
47
  ```ts
41
48
  import { kanca } from '@/lib/kanca';
@@ -83,6 +90,14 @@ named after the operation (`getMerchant`, `listOrder`, …). A `200` response wi
83
90
  an `errors` array counts as a failure, with `errors[0].extensions.code` as the
84
91
  error code.
85
92
 
93
+ Because each call carries the store's merchant id, Kanca can also show a store
94
+ as **Access revoked** when its calls keep failing with an authorization error
95
+ (HTTP 401, or an auth error code such as `UNAUTHORIZED`; not 403, which
96
+ usually means a missing permission) and none succeed after: at least 2 such
97
+ failures since the last success or install. It is
98
+ inferred (most likely an uninstall, or a token whose refresh failed), not an
99
+ uninstall event. `kanca.fetch` calls carry no merchant id, so they don't count.
100
+
86
101
  `instrument()` works with `@ikas/admin-api-client` 2.x (any
87
102
  `BaseGraphQLAPIClient` subclass) and survives token refreshes. For anything else
88
103
  — your own GraphQL client, `graphql-request`, REST calls — use the drop-in
@@ -114,6 +129,76 @@ pathname and rating, sent with `fetch` + `keepalive` when the page is hidden
114
129
  (`sendBeacon` only as a fallback — content blockers drop beacons silently).
115
130
  Without React: `import { reportVitals } from '@kanca-app/ikas/browser'`.
116
131
 
132
+ ## Storefront script
133
+
134
+ If your app injects a script into merchants' ikas storefronts, add
135
+ `@kanca-app/ikas/storefront` to it: errors thrown by **your** script and what
136
+ it costs visitors (download time, transfer size, init → ready). Zero
137
+ dependencies, under 3 KB min+gzip (`pnpm size`), never throws, never blocks,
138
+ never retries.
139
+
140
+ ```ts
141
+ // First lines of your script's entry — synchronous, before anything else runs.
142
+ import { initKancaStorefront } from '@kanca-app/ikas/storefront';
143
+
144
+ const kanca = initKancaStorefront({
145
+ publicKey: 'kp_...', // public key, safe to ship
146
+ scriptName: 'my-app', // label in the dashboard
147
+ release: '1.4.0', // optional: your script's version
148
+ // sampleRate: 0.1, // share of page views that send load/timing data
149
+ // scriptUrlMatch: /my-app\.js/, // default: document.currentScript.src
150
+ // merchantId: '...', // if the script knows it (or kanca.setMerchantId later)
151
+ });
152
+
153
+ // Callbacks the browser invokes later: errors are recorded, then rethrown.
154
+ window.requestIdleCallback(kanca.wrap(() => {
155
+ render();
156
+ kanca.ready(); // marks init → ready
157
+ }, 'boot'));
158
+
159
+ // fetch with timing: same Response and same errors as fetch.
160
+ const res = await kanca.fetch('config', `${origin}/api/config?key=...`);
161
+
162
+ kanca.timing('render', 12.5); // any duration, ms
163
+ kanca.captureError(err, { where: 'cart' }); // errors you catch yourself
164
+ ```
165
+
166
+ | Method | What it does |
167
+ | --------------------------------- | ------------------------------------------------------------------------------- |
168
+ | `captureError(err, { where })` | Records an error. Deduped per page view, at most 10 per page view. |
169
+ | `wrap(fn, where)` | Returns `fn` wrapped. A throw or a rejected returned promise is recorded, then **rethrown / left rejected**: the page sees exactly what it would without Kanca, and the error still reaches `window.onerror` (it is not counted twice). |
170
+ | `timing(name, ms, { ok, status })`| Records a duration, sampled page views only. `name`: `[A-Za-z0-9_.-]{1,40}`. |
171
+ | `fetch(name, input, init)` | `fetch` + `timing(name)` with the HTTP status (or `ok: false` on network errors). |
172
+ | `ready()` | Marks your script ready. First call counts. |
173
+ | `setMerchantId(id)` | Attaches the ikas merchant id to every event sent from now on. |
174
+ | `flush()` | Sends queued events now. Rarely needed: events are sent when the page is hidden. |
175
+
176
+ How it works:
177
+
178
+ - **Only your errors.** Global `error` / `unhandledrejection` events are kept
179
+ only when the topmost stack frame with a URL is your script
180
+ (`scriptUrlMatch`, default `document.currentScript.src` without query).
181
+ Theme and other apps' errors are ignored, and `preventDefault` is never
182
+ called. A cross-origin script without a `crossorigin` attribute gets muted
183
+ `"Script error."` events that can't be attributed: wrap your entry points
184
+ with `wrap()` so those errors are still captured.
185
+ - **Sampling.** Errors are always sent. `load` and `timing:*` events come from
186
+ `sampleRate` of page views (default 10%), and each carries the rate, so
187
+ Kanca weights counts and percentiles (a row sampled at 0.1 counts as 10
188
+ page views).
189
+ - **Load cost.** Once per sampled page view: your script's download time from
190
+ resource timing, transfer and body size, cache hit, init → `ready()`, and
191
+ how many errors the page view had. Browsers hide sizes of cross-origin files
192
+ unless you serve the script with `Timing-Allow-Origin: *`.
193
+ - **Sending.** Once when the page is hidden (plus early at 10 queued events),
194
+ `fetch` + `keepalive` with a `text/plain` body (no CORS preflight),
195
+ `sendBeacon` as a fallback. No retries, no cookies (`credentials: 'omit'`).
196
+ - **Privacy.** Sent per event: host name, a path pattern (ids, numbers,
197
+ e-mails and tokens replaced, no query), script name, release, a random
198
+ page-view id, the sample rate; for errors the error name, a scrubbed
199
+ message (≤ 300 chars) and the top stack frame of your file. Never cookies,
200
+ storage, customer, cart or order data.
201
+
117
202
  ## Serverless
118
203
 
119
204
  Events are batched in memory and sent every 2 s (or at 100 events), and on
@@ -151,7 +236,8 @@ attempts). Every event has a UUID, so retries never double count.
151
236
  What is sent: event type, name (webhook scope / GraphQL operation name /
152
237
  `METHOD host/path`), HTTP status, ok, duration, error code, error message
153
238
  (≤ 500 chars), merchant id, webhook delivery id, and for installs the store
154
- name. Vitals send metric name, value, rating and pathname.
239
+ name. Vitals send metric name, value, rating and pathname. For storefront
240
+ events see [Storefront script](#storefront-script).
155
241
 
156
242
  What is never sent: webhook bodies, the webhook signature, GraphQL queries,
157
243
  variables or responses, request/response headers, access tokens, query strings,
@@ -165,6 +251,9 @@ customer or order data. Kanca never asks for your ikas client secret.
165
251
  - For `instrument()`, `@ikas/admin-api-client` does not expose the HTTP status
166
252
  of successful calls, so those events carry `ok: true` without a status. Use
167
253
  `kanca.fetch` if you need exact statuses.
254
+ - ikas does not document an app-uninstall webhook, so uninstall events may
255
+ never arrive. "Access revoked" is inferred from Admin API authorization
256
+ failures and only appears once your app calls the Admin API for that store.
168
257
  - Events buffered in memory are lost if the process is killed hard (SIGKILL,
169
258
  OOM) before the next send.
170
259
 
package/dist/browser.cjs CHANGED
@@ -28,7 +28,7 @@ var import_web_vitals = require("web-vitals");
28
28
 
29
29
  // src/shared.ts
30
30
  var SDK_NAME = "@kanca-app/ikas";
31
- var SDK_VERSION = "0.1.1";
31
+ var SDK_VERSION = "0.2.0";
32
32
  var DEFAULT_ENDPOINT = "https://kanca.onurhan.dev";
33
33
  var MAX_NAME = 200;
34
34
  var MAX_ERROR_MESSAGE = 500;
@@ -104,7 +104,6 @@ function sanitizeEvent(e) {
104
104
  function joinUrl(endpoint, path) {
105
105
  return endpoint.replace(/\/+$/, "") + path;
106
106
  }
107
- var encoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
108
107
 
109
108
  // src/browser.ts
110
109
  var VITALS = ["LCP", "INP", "CLS", "FCP", "TTFB"];
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/browser.ts","../src/shared.ts"],"sourcesContent":["// @kanca-app/ikas/browser — Core Web Vitals for the ikas admin iframe.\n// Sends only { name, value, rating, pathname } per metric. Uses a public key (kp_...).\n\nimport { onCLS, onFCP, onINP, onLCP, onTTFB, type Metric } from \"web-vitals\";\nimport { DEFAULT_ENDPOINT, SDK_NAME, SDK_VERSION, joinUrl, sanitizeEvent, uuid, type KancaEvent } from \"./shared\";\n\nexport interface ReportVitalsOptions {\n /** Public key (`kp_...`). Safe to expose in the browser; only accepts `vital` events. */\n publicKey: string | undefined;\n /** Kanca base URL. Default `https://kanca.onurhan.dev`. */\n endpoint?: string;\n debug?: boolean;\n}\n\nconst VITALS = [\"LCP\", \"INP\", \"CLS\", \"FCP\", \"TTFB\"] as const;\n\n// Global (not module) flag so duplicate copies (ESM + CJS, browser + react bundles) still start once.\nconst FLAG = \"__kancaVitalsStarted\";\nconst g = globalThis as { [FLAG]?: boolean };\n\n/**\n * Starts collecting LCP, INP, CLS, FCP and TTFB (final values, once per page\n * load per metric) and sends them when the page is hidden or unloaded.\n * Safe to call more than once — only the first call has an effect.\n */\nexport function reportVitals(options: ReportVitalsOptions): void {\n try {\n if (g[FLAG] || typeof window === \"undefined\" || typeof document === \"undefined\") return;\n const key = typeof options?.publicKey === \"string\" ? options.publicKey.trim() : \"\";\n const debug = !!options?.debug;\n if (!key.startsWith(\"kp_\")) {\n if (debug || key) console.warn(\"[kanca] reportVitals(): a public key (kp_...) is required; vitals disabled.\");\n return;\n }\n g[FLAG] = true;\n\n const url = `${joinUrl(options.endpoint || DEFAULT_ENDPOINT, \"/api/ingest/browser\")}?key=${encodeURIComponent(key)}`;\n const path = location.pathname;\n const ts =\n typeof performance !== \"undefined\" && typeof performance.timeOrigin === \"number\" && performance.timeOrigin > 0\n ? Math.round(performance.timeOrigin)\n : Date.now();\n const queue: KancaEvent[] = [];\n const seen = new Set<string>();\n\n const send = () => {\n if (!queue.length) return;\n const events = queue.splice(0, queue.length);\n const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });\n if (debug) console.debug(\"[kanca] vitals\", events);\n // fetch+keepalive first: content blockers drop sendBeacon (\"ping\") requests\n // while sendBeacon still returns true, so vitals would vanish silently.\n // A text/plain body keeps it a simple request (no CORS preflight).\n try {\n if (typeof fetch === \"function\") {\n void fetch(url, { method: \"POST\", body, keepalive: true, mode: \"cors\", credentials: \"omit\", headers: { \"content-type\": \"text/plain\" } }).catch(() => undefined);\n return;\n }\n } catch {\n /* fall through */\n }\n try {\n if (typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\") navigator.sendBeacon(url, body);\n } catch {\n /* never throw */\n }\n };\n\n const onMetric = (m: Metric) => {\n try {\n if (!(VITALS as readonly string[]).includes(m.name) || seen.has(m.name)) return;\n seen.add(m.name);\n queue.push(sanitizeEvent({ id: uuid(), type: \"vital\", ts, name: m.name, value: m.value, attrs: { path, rating: m.rating } }));\n } catch {\n /* ignore */\n }\n };\n\n // Without reportAllChanges, web-vitals reports each metric's final value.\n onLCP(onMetric);\n onINP(onMetric);\n onCLS(onMetric);\n onFCP(onMetric);\n onTTFB(onMetric);\n\n // Registered after web-vitals' own listeners, so final values are queued before we send.\n addEventListener(\n \"visibilitychange\",\n () => {\n // Microtask: lets any report web-vitals triggers on this same event land first.\n if (document.visibilityState === \"hidden\") queueMicrotask(send);\n },\n true,\n );\n addEventListener(\"pagehide\", send, true);\n } catch {\n /* never break the host page */\n }\n}\n\n/** @internal for tests */\nexport function __resetVitalsForTests(): void {\n g[FLAG] = false;\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.1.1\";\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\";\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\" ? 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/browser.ts","../src/shared.ts"],"sourcesContent":["// @kanca-app/ikas/browser — Core Web Vitals for the ikas admin iframe.\n// Sends only { name, value, rating, pathname } per metric. Uses a public key (kp_...).\n\nimport { onCLS, onFCP, onINP, onLCP, onTTFB, type Metric } from \"web-vitals\";\nimport { DEFAULT_ENDPOINT, SDK_NAME, SDK_VERSION, joinUrl, sanitizeEvent, uuid, type KancaEvent } from \"./shared\";\n\nexport interface ReportVitalsOptions {\n /** Public key (`kp_...`). Safe to expose in the browser; only accepts `vital` events. */\n publicKey: string | undefined;\n /** Kanca base URL. Default `https://kanca.onurhan.dev`. */\n endpoint?: string;\n debug?: boolean;\n}\n\nconst VITALS = [\"LCP\", \"INP\", \"CLS\", \"FCP\", \"TTFB\"] as const;\n\n// Global (not module) flag so duplicate copies (ESM + CJS, browser + react bundles) still start once.\nconst FLAG = \"__kancaVitalsStarted\";\nconst g = globalThis as { [FLAG]?: boolean };\n\n/**\n * Starts collecting LCP, INP, CLS, FCP and TTFB (final values, once per page\n * load per metric) and sends them when the page is hidden or unloaded.\n * Safe to call more than once — only the first call has an effect.\n */\nexport function reportVitals(options: ReportVitalsOptions): void {\n try {\n if (g[FLAG] || typeof window === \"undefined\" || typeof document === \"undefined\") return;\n const key = typeof options?.publicKey === \"string\" ? options.publicKey.trim() : \"\";\n const debug = !!options?.debug;\n if (!key.startsWith(\"kp_\")) {\n if (debug || key) console.warn(\"[kanca] reportVitals(): a public key (kp_...) is required; vitals disabled.\");\n return;\n }\n g[FLAG] = true;\n\n const url = `${joinUrl(options.endpoint || DEFAULT_ENDPOINT, \"/api/ingest/browser\")}?key=${encodeURIComponent(key)}`;\n const path = location.pathname;\n const ts =\n typeof performance !== \"undefined\" && typeof performance.timeOrigin === \"number\" && performance.timeOrigin > 0\n ? Math.round(performance.timeOrigin)\n : Date.now();\n const queue: KancaEvent[] = [];\n const seen = new Set<string>();\n\n const send = () => {\n if (!queue.length) return;\n const events = queue.splice(0, queue.length);\n const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });\n if (debug) console.debug(\"[kanca] vitals\", events);\n // fetch+keepalive first: content blockers drop sendBeacon (\"ping\") requests\n // while sendBeacon still returns true, so vitals would vanish silently.\n // A text/plain body keeps it a simple request (no CORS preflight).\n try {\n if (typeof fetch === \"function\") {\n void fetch(url, { method: \"POST\", body, keepalive: true, mode: \"cors\", credentials: \"omit\", headers: { \"content-type\": \"text/plain\" } }).catch(() => undefined);\n return;\n }\n } catch {\n /* fall through */\n }\n try {\n if (typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\") navigator.sendBeacon(url, body);\n } catch {\n /* never throw */\n }\n };\n\n const onMetric = (m: Metric) => {\n try {\n if (!(VITALS as readonly string[]).includes(m.name) || seen.has(m.name)) return;\n seen.add(m.name);\n queue.push(sanitizeEvent({ id: uuid(), type: \"vital\", ts, name: m.name, value: m.value, attrs: { path, rating: m.rating } }));\n } catch {\n /* ignore */\n }\n };\n\n // Without reportAllChanges, web-vitals reports each metric's final value.\n onLCP(onMetric);\n onINP(onMetric);\n onCLS(onMetric);\n onFCP(onMetric);\n onTTFB(onMetric);\n\n // Registered after web-vitals' own listeners, so final values are queued before we send.\n addEventListener(\n \"visibilitychange\",\n () => {\n // Microtask: lets any report web-vitals triggers on this same event land first.\n if (document.visibilityState === \"hidden\") queueMicrotask(send);\n },\n true,\n );\n addEventListener(\"pagehide\", send, true);\n } catch {\n /* never break the host page */\n }\n}\n\n/** @internal for tests */\nexport function __resetVitalsForTests(): void {\n g[FLAG] = false;\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":[]}
package/dist/browser.js CHANGED
@@ -3,7 +3,7 @@ import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals";
3
3
 
4
4
  // src/shared.ts
5
5
  var SDK_NAME = "@kanca-app/ikas";
6
- var SDK_VERSION = "0.1.1";
6
+ var SDK_VERSION = "0.2.0";
7
7
  var DEFAULT_ENDPOINT = "https://kanca.onurhan.dev";
8
8
  var MAX_NAME = 200;
9
9
  var MAX_ERROR_MESSAGE = 500;
@@ -79,7 +79,6 @@ function sanitizeEvent(e) {
79
79
  function joinUrl(endpoint, path) {
80
80
  return endpoint.replace(/\/+$/, "") + path;
81
81
  }
82
- var encoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
83
82
 
84
83
  // src/browser.ts
85
84
  var VITALS = ["LCP", "INP", "CLS", "FCP", "TTFB"];
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/browser.ts","../src/shared.ts"],"sourcesContent":["// @kanca-app/ikas/browser — Core Web Vitals for the ikas admin iframe.\n// Sends only { name, value, rating, pathname } per metric. Uses a public key (kp_...).\n\nimport { onCLS, onFCP, onINP, onLCP, onTTFB, type Metric } from \"web-vitals\";\nimport { DEFAULT_ENDPOINT, SDK_NAME, SDK_VERSION, joinUrl, sanitizeEvent, uuid, type KancaEvent } from \"./shared\";\n\nexport interface ReportVitalsOptions {\n /** Public key (`kp_...`). Safe to expose in the browser; only accepts `vital` events. */\n publicKey: string | undefined;\n /** Kanca base URL. Default `https://kanca.onurhan.dev`. */\n endpoint?: string;\n debug?: boolean;\n}\n\nconst VITALS = [\"LCP\", \"INP\", \"CLS\", \"FCP\", \"TTFB\"] as const;\n\n// Global (not module) flag so duplicate copies (ESM + CJS, browser + react bundles) still start once.\nconst FLAG = \"__kancaVitalsStarted\";\nconst g = globalThis as { [FLAG]?: boolean };\n\n/**\n * Starts collecting LCP, INP, CLS, FCP and TTFB (final values, once per page\n * load per metric) and sends them when the page is hidden or unloaded.\n * Safe to call more than once — only the first call has an effect.\n */\nexport function reportVitals(options: ReportVitalsOptions): void {\n try {\n if (g[FLAG] || typeof window === \"undefined\" || typeof document === \"undefined\") return;\n const key = typeof options?.publicKey === \"string\" ? options.publicKey.trim() : \"\";\n const debug = !!options?.debug;\n if (!key.startsWith(\"kp_\")) {\n if (debug || key) console.warn(\"[kanca] reportVitals(): a public key (kp_...) is required; vitals disabled.\");\n return;\n }\n g[FLAG] = true;\n\n const url = `${joinUrl(options.endpoint || DEFAULT_ENDPOINT, \"/api/ingest/browser\")}?key=${encodeURIComponent(key)}`;\n const path = location.pathname;\n const ts =\n typeof performance !== \"undefined\" && typeof performance.timeOrigin === \"number\" && performance.timeOrigin > 0\n ? Math.round(performance.timeOrigin)\n : Date.now();\n const queue: KancaEvent[] = [];\n const seen = new Set<string>();\n\n const send = () => {\n if (!queue.length) return;\n const events = queue.splice(0, queue.length);\n const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });\n if (debug) console.debug(\"[kanca] vitals\", events);\n // fetch+keepalive first: content blockers drop sendBeacon (\"ping\") requests\n // while sendBeacon still returns true, so vitals would vanish silently.\n // A text/plain body keeps it a simple request (no CORS preflight).\n try {\n if (typeof fetch === \"function\") {\n void fetch(url, { method: \"POST\", body, keepalive: true, mode: \"cors\", credentials: \"omit\", headers: { \"content-type\": \"text/plain\" } }).catch(() => undefined);\n return;\n }\n } catch {\n /* fall through */\n }\n try {\n if (typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\") navigator.sendBeacon(url, body);\n } catch {\n /* never throw */\n }\n };\n\n const onMetric = (m: Metric) => {\n try {\n if (!(VITALS as readonly string[]).includes(m.name) || seen.has(m.name)) return;\n seen.add(m.name);\n queue.push(sanitizeEvent({ id: uuid(), type: \"vital\", ts, name: m.name, value: m.value, attrs: { path, rating: m.rating } }));\n } catch {\n /* ignore */\n }\n };\n\n // Without reportAllChanges, web-vitals reports each metric's final value.\n onLCP(onMetric);\n onINP(onMetric);\n onCLS(onMetric);\n onFCP(onMetric);\n onTTFB(onMetric);\n\n // Registered after web-vitals' own listeners, so final values are queued before we send.\n addEventListener(\n \"visibilitychange\",\n () => {\n // Microtask: lets any report web-vitals triggers on this same event land first.\n if (document.visibilityState === \"hidden\") queueMicrotask(send);\n },\n true,\n );\n addEventListener(\"pagehide\", send, true);\n } catch {\n /* never break the host page */\n }\n}\n\n/** @internal for tests */\nexport function __resetVitalsForTests(): void {\n g[FLAG] = false;\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.1.1\";\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\";\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\" ? 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/browser.ts","../src/shared.ts"],"sourcesContent":["// @kanca-app/ikas/browser — Core Web Vitals for the ikas admin iframe.\n// Sends only { name, value, rating, pathname } per metric. Uses a public key (kp_...).\n\nimport { onCLS, onFCP, onINP, onLCP, onTTFB, type Metric } from \"web-vitals\";\nimport { DEFAULT_ENDPOINT, SDK_NAME, SDK_VERSION, joinUrl, sanitizeEvent, uuid, type KancaEvent } from \"./shared\";\n\nexport interface ReportVitalsOptions {\n /** Public key (`kp_...`). Safe to expose in the browser; only accepts `vital` events. */\n publicKey: string | undefined;\n /** Kanca base URL. Default `https://kanca.onurhan.dev`. */\n endpoint?: string;\n debug?: boolean;\n}\n\nconst VITALS = [\"LCP\", \"INP\", \"CLS\", \"FCP\", \"TTFB\"] as const;\n\n// Global (not module) flag so duplicate copies (ESM + CJS, browser + react bundles) still start once.\nconst FLAG = \"__kancaVitalsStarted\";\nconst g = globalThis as { [FLAG]?: boolean };\n\n/**\n * Starts collecting LCP, INP, CLS, FCP and TTFB (final values, once per page\n * load per metric) and sends them when the page is hidden or unloaded.\n * Safe to call more than once — only the first call has an effect.\n */\nexport function reportVitals(options: ReportVitalsOptions): void {\n try {\n if (g[FLAG] || typeof window === \"undefined\" || typeof document === \"undefined\") return;\n const key = typeof options?.publicKey === \"string\" ? options.publicKey.trim() : \"\";\n const debug = !!options?.debug;\n if (!key.startsWith(\"kp_\")) {\n if (debug || key) console.warn(\"[kanca] reportVitals(): a public key (kp_...) is required; vitals disabled.\");\n return;\n }\n g[FLAG] = true;\n\n const url = `${joinUrl(options.endpoint || DEFAULT_ENDPOINT, \"/api/ingest/browser\")}?key=${encodeURIComponent(key)}`;\n const path = location.pathname;\n const ts =\n typeof performance !== \"undefined\" && typeof performance.timeOrigin === \"number\" && performance.timeOrigin > 0\n ? Math.round(performance.timeOrigin)\n : Date.now();\n const queue: KancaEvent[] = [];\n const seen = new Set<string>();\n\n const send = () => {\n if (!queue.length) return;\n const events = queue.splice(0, queue.length);\n const body = JSON.stringify({ sdk: { name: SDK_NAME, version: SDK_VERSION }, events });\n if (debug) console.debug(\"[kanca] vitals\", events);\n // fetch+keepalive first: content blockers drop sendBeacon (\"ping\") requests\n // while sendBeacon still returns true, so vitals would vanish silently.\n // A text/plain body keeps it a simple request (no CORS preflight).\n try {\n if (typeof fetch === \"function\") {\n void fetch(url, { method: \"POST\", body, keepalive: true, mode: \"cors\", credentials: \"omit\", headers: { \"content-type\": \"text/plain\" } }).catch(() => undefined);\n return;\n }\n } catch {\n /* fall through */\n }\n try {\n if (typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\") navigator.sendBeacon(url, body);\n } catch {\n /* never throw */\n }\n };\n\n const onMetric = (m: Metric) => {\n try {\n if (!(VITALS as readonly string[]).includes(m.name) || seen.has(m.name)) return;\n seen.add(m.name);\n queue.push(sanitizeEvent({ id: uuid(), type: \"vital\", ts, name: m.name, value: m.value, attrs: { path, rating: m.rating } }));\n } catch {\n /* ignore */\n }\n };\n\n // Without reportAllChanges, web-vitals reports each metric's final value.\n onLCP(onMetric);\n onINP(onMetric);\n onCLS(onMetric);\n onFCP(onMetric);\n onTTFB(onMetric);\n\n // Registered after web-vitals' own listeners, so final values are queued before we send.\n addEventListener(\n \"visibilitychange\",\n () => {\n // Microtask: lets any report web-vitals triggers on this same event land first.\n if (document.visibilityState === \"hidden\") queueMicrotask(send);\n },\n true,\n );\n addEventListener(\"pagehide\", send, true);\n } catch {\n /* never break the host page */\n }\n}\n\n/** @internal for tests */\nexport function __resetVitalsForTests(): void {\n g[FLAG] = false;\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":[]}
package/dist/index.cjs CHANGED
@@ -175,7 +175,7 @@ function classifyGraphql(status, json) {
175
175
 
176
176
  // src/shared.ts
177
177
  var SDK_NAME = "@kanca-app/ikas";
178
- var SDK_VERSION = "0.1.1";
178
+ var SDK_VERSION = "0.2.0";
179
179
  var DEFAULT_ENDPOINT = "https://kanca.onurhan.dev";
180
180
  var MAX_NAME = 200;
181
181
  var MAX_ERROR_MESSAGE = 500;
@@ -260,7 +260,7 @@ function errorMessage(err) {
260
260
  function joinUrl(endpoint, path) {
261
261
  return endpoint.replace(/\/+$/, "") + path;
262
262
  }
263
- var encoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
263
+ var encoder = typeof TextEncoder !== "undefined" ? /* @__PURE__ */ new TextEncoder() : void 0;
264
264
  function byteLength(s) {
265
265
  return encoder ? encoder.encode(s).length : s.length * 3;
266
266
  }