@kanca-app/ikas 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/browser.cjs +172 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +17 -0
- package/dist/browser.d.ts +17 -0
- package/dist/browser.js +151 -0
- package/dist/browser.js.map +1 -0
- package/dist/index.cjs +803 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +780 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +181 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +22 -0
- package/dist/react.d.ts +22 -0
- package/dist/react.js +161 -0
- package/dist/react.js.map +1 -0
- package/package.json +97 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
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/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
SDK_NAME: () => SDK_NAME,
|
|
24
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
25
|
+
UNINSTALL_SCOPES: () => UNINSTALL_SCOPES,
|
|
26
|
+
createKanca: () => createKanca,
|
|
27
|
+
operationName: () => operationName
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(src_exports);
|
|
30
|
+
|
|
31
|
+
// src/graphql.ts
|
|
32
|
+
function lex(src) {
|
|
33
|
+
const out = [];
|
|
34
|
+
let i = 0;
|
|
35
|
+
const n = src.length;
|
|
36
|
+
while (i < n) {
|
|
37
|
+
const c = src[i];
|
|
38
|
+
if (c === "#") {
|
|
39
|
+
while (i < n && src[i] !== "\n" && src[i] !== "\r") i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (c === '"') {
|
|
43
|
+
if (src.startsWith('"""', i)) {
|
|
44
|
+
const end = src.indexOf('"""', i + 3);
|
|
45
|
+
i = end === -1 ? n : end + 3;
|
|
46
|
+
} else {
|
|
47
|
+
i++;
|
|
48
|
+
while (i < n && src[i] !== '"' && src[i] !== "\n") i += src[i] === "\\" ? 2 : 1;
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
54
|
+
let j = i + 1;
|
|
55
|
+
while (j < n && /[A-Za-z0-9_]/.test(src[j])) j++;
|
|
56
|
+
out.push({ t: "name", v: src.slice(i, j) });
|
|
57
|
+
i = j;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (src.startsWith("...", i)) {
|
|
61
|
+
out.push({ t: "punct", v: "..." });
|
|
62
|
+
i += 3;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if ("{}()[]:@$!=|&".includes(c)) out.push({ t: "punct", v: c });
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function skipBlock(toks, start) {
|
|
71
|
+
const open = toks[start].v;
|
|
72
|
+
const close = open === "{" ? "}" : ")";
|
|
73
|
+
let depth = 0;
|
|
74
|
+
for (let i = start; i < toks.length; i++) {
|
|
75
|
+
const v = toks[i];
|
|
76
|
+
if (v.t !== "punct") continue;
|
|
77
|
+
if (v.v === open) depth++;
|
|
78
|
+
else if (v.v === close && --depth === 0) return i + 1;
|
|
79
|
+
}
|
|
80
|
+
return toks.length;
|
|
81
|
+
}
|
|
82
|
+
function firstRootField(toks, start) {
|
|
83
|
+
for (let i = start + 1; i < toks.length; i++) {
|
|
84
|
+
const tk = toks[i];
|
|
85
|
+
if (tk.t === "punct") {
|
|
86
|
+
if (tk.v === "}") return void 0;
|
|
87
|
+
if (tk.v === "...") return void 0;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const next = toks[i + 1];
|
|
91
|
+
if (next && next.t === "punct" && next.v === ":") {
|
|
92
|
+
const real = toks[i + 2];
|
|
93
|
+
return real && real.t === "name" ? real.v : void 0;
|
|
94
|
+
}
|
|
95
|
+
return tk.v;
|
|
96
|
+
}
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
function operationName(query, requestedName) {
|
|
100
|
+
if (typeof query !== "string" || !query.trim()) return "anonymous";
|
|
101
|
+
let toks;
|
|
102
|
+
try {
|
|
103
|
+
toks = lex(query);
|
|
104
|
+
} catch {
|
|
105
|
+
return "anonymous";
|
|
106
|
+
}
|
|
107
|
+
const ops = [];
|
|
108
|
+
let i = 0;
|
|
109
|
+
while (i < toks.length) {
|
|
110
|
+
const tk = toks[i];
|
|
111
|
+
if (tk.t === "punct" && tk.v === "{") {
|
|
112
|
+
ops.push({ field: firstRootField(toks, i) });
|
|
113
|
+
i = skipBlock(toks, i);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (tk.t === "name" && (tk.v === "query" || tk.v === "mutation" || tk.v === "subscription")) {
|
|
117
|
+
let j = i + 1;
|
|
118
|
+
let name;
|
|
119
|
+
if (toks[j]?.t === "name") name = toks[j++].v;
|
|
120
|
+
while (j < toks.length && !(toks[j].t === "punct" && toks[j].v === "{")) {
|
|
121
|
+
if (toks[j].t === "punct" && toks[j].v === "(") j = skipBlock(toks, j);
|
|
122
|
+
else j++;
|
|
123
|
+
}
|
|
124
|
+
if (j >= toks.length) {
|
|
125
|
+
ops.push({ name });
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
ops.push({ name, field: firstRootField(toks, j) });
|
|
129
|
+
i = skipBlock(toks, j);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (tk.t === "name" && tk.v === "fragment") {
|
|
133
|
+
let j = i + 1;
|
|
134
|
+
while (j < toks.length && !(toks[j].t === "punct" && toks[j].v === "{")) j++;
|
|
135
|
+
i = j < toks.length ? skipBlock(toks, j) : j;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
i++;
|
|
139
|
+
}
|
|
140
|
+
if (!ops.length) return "anonymous";
|
|
141
|
+
let op = ops[0];
|
|
142
|
+
if (typeof requestedName === "string" && requestedName) {
|
|
143
|
+
op = ops.find((o) => o.name === requestedName) ?? op;
|
|
144
|
+
}
|
|
145
|
+
return op.name ?? op.field ?? "anonymous";
|
|
146
|
+
}
|
|
147
|
+
function isIkasGraphqlUrl(url) {
|
|
148
|
+
const host = url.hostname.toLowerCase();
|
|
149
|
+
if (host !== "myikas.com" && !host.endsWith(".myikas.com")) return false;
|
|
150
|
+
return url.pathname.toLowerCase().includes("graphql");
|
|
151
|
+
}
|
|
152
|
+
function operationNameFromBody(body) {
|
|
153
|
+
if (typeof body !== "string") return "anonymous";
|
|
154
|
+
try {
|
|
155
|
+
let parsed = JSON.parse(body);
|
|
156
|
+
if (Array.isArray(parsed)) parsed = parsed[0];
|
|
157
|
+
if (!parsed || typeof parsed !== "object") return "anonymous";
|
|
158
|
+
const p = parsed;
|
|
159
|
+
return operationName(p.query, p.operationName);
|
|
160
|
+
} catch {
|
|
161
|
+
return "anonymous";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function classifyGraphql(status, json) {
|
|
165
|
+
const payload = Array.isArray(json) ? json.find((j) => j && typeof j === "object" && Array.isArray(j.errors) && j.errors.length) ?? json[0] : json;
|
|
166
|
+
const errors = payload && typeof payload === "object" && Array.isArray(payload.errors) ? payload.errors : [];
|
|
167
|
+
const is2xx = status >= 200 && status < 300;
|
|
168
|
+
if (is2xx && errors.length === 0) return { ok: true };
|
|
169
|
+
const first = errors[0];
|
|
170
|
+
const extCode = first?.extensions?.code;
|
|
171
|
+
const code = typeof extCode === "string" && extCode ? extCode : `HTTP_${status}`;
|
|
172
|
+
const message = typeof first?.message === "string" ? first.message : void 0;
|
|
173
|
+
return { ok: false, code, message };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/shared.ts
|
|
177
|
+
var SDK_NAME = "@kanca-app/ikas";
|
|
178
|
+
var SDK_VERSION = "0.1.1";
|
|
179
|
+
var DEFAULT_ENDPOINT = "https://kanca.onurhan.dev";
|
|
180
|
+
var MAX_NAME = 200;
|
|
181
|
+
var MAX_ERROR_MESSAGE = 500;
|
|
182
|
+
var MAX_ERROR_CODE = 100;
|
|
183
|
+
var MAX_ATTR_KEYS = 20;
|
|
184
|
+
var MAX_ATTR_STRING = 500;
|
|
185
|
+
var MAX_ATTR_KEY = 64;
|
|
186
|
+
var MAX_EVENTS_PER_BATCH = 500;
|
|
187
|
+
var MAX_BATCH_BYTES = 480 * 1024;
|
|
188
|
+
function truncate(s, max) {
|
|
189
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
190
|
+
}
|
|
191
|
+
function hex(bytes) {
|
|
192
|
+
let out = "";
|
|
193
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
function uuid() {
|
|
197
|
+
const c = globalThis.crypto;
|
|
198
|
+
if (c && typeof c.randomUUID === "function") return c.randomUUID();
|
|
199
|
+
const b = new Uint8Array(16);
|
|
200
|
+
if (c && typeof c.getRandomValues === "function") c.getRandomValues(b);
|
|
201
|
+
else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
|
|
202
|
+
b[6] = b[6] & 15 | 64;
|
|
203
|
+
b[8] = b[8] & 63 | 128;
|
|
204
|
+
const h = hex(b);
|
|
205
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
206
|
+
}
|
|
207
|
+
function isFiniteNumber(v) {
|
|
208
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
209
|
+
}
|
|
210
|
+
function cleanAttrs(attrs) {
|
|
211
|
+
if (!attrs || typeof attrs !== "object") return void 0;
|
|
212
|
+
const out = {};
|
|
213
|
+
let n = 0;
|
|
214
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
215
|
+
if (n >= MAX_ATTR_KEYS) break;
|
|
216
|
+
if (v === void 0 || v === null || !k) continue;
|
|
217
|
+
let val;
|
|
218
|
+
if (typeof v === "boolean") val = v;
|
|
219
|
+
else if (typeof v === "number") {
|
|
220
|
+
if (!Number.isFinite(v)) continue;
|
|
221
|
+
val = v;
|
|
222
|
+
} else if (typeof v === "string") val = truncate(v, MAX_ATTR_STRING);
|
|
223
|
+
else continue;
|
|
224
|
+
out[truncate(k, MAX_ATTR_KEY)] = val;
|
|
225
|
+
n++;
|
|
226
|
+
}
|
|
227
|
+
return n > 0 ? out : void 0;
|
|
228
|
+
}
|
|
229
|
+
function sanitizeEvent(e) {
|
|
230
|
+
const out = {
|
|
231
|
+
id: typeof e.id === "string" && e.id ? e.id : uuid(),
|
|
232
|
+
type: e.type,
|
|
233
|
+
ts: isFiniteNumber(e.ts) ? Math.round(e.ts) : Date.now(),
|
|
234
|
+
name: truncate(typeof e.name === "string" && e.name ? e.name : "unknown", MAX_NAME)
|
|
235
|
+
};
|
|
236
|
+
if (isFiniteNumber(e.status) && e.status >= 100 && e.status <= 599) out.status = Math.trunc(e.status);
|
|
237
|
+
if (typeof e.ok === "boolean") out.ok = e.ok;
|
|
238
|
+
if (isFiniteNumber(e.durationMs)) out.durationMs = Math.min(864e5, Math.max(0, Math.round(e.durationMs * 10) / 10));
|
|
239
|
+
if (isFiniteNumber(e.value)) out.value = e.value;
|
|
240
|
+
if (e.error && typeof e.error.code === "string") {
|
|
241
|
+
out.error = { code: truncate(e.error.code, MAX_ERROR_CODE) };
|
|
242
|
+
if (typeof e.error.message === "string" && e.error.message) {
|
|
243
|
+
out.error.message = truncate(e.error.message, MAX_ERROR_MESSAGE);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (typeof e.merchantId === "string" && e.merchantId) out.merchantId = truncate(e.merchantId, 200);
|
|
247
|
+
if (typeof e.deliveryId === "string" && e.deliveryId) out.deliveryId = truncate(e.deliveryId, 200);
|
|
248
|
+
const attrs = cleanAttrs(e.attrs);
|
|
249
|
+
if (attrs) out.attrs = attrs;
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
function errorMessage(err) {
|
|
253
|
+
try {
|
|
254
|
+
if (err instanceof Error) return truncate(err.message || err.name, MAX_ERROR_MESSAGE);
|
|
255
|
+
if (typeof err === "string") return truncate(err, MAX_ERROR_MESSAGE);
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
function joinUrl(endpoint, path) {
|
|
261
|
+
return endpoint.replace(/\/+$/, "") + path;
|
|
262
|
+
}
|
|
263
|
+
var encoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
|
|
264
|
+
function byteLength(s) {
|
|
265
|
+
return encoder ? encoder.encode(s).length : s.length * 3;
|
|
266
|
+
}
|
|
267
|
+
function buildBodies(events, maxEvents) {
|
|
268
|
+
const sdk = { name: SDK_NAME, version: SDK_VERSION };
|
|
269
|
+
const envelope = byteLength(JSON.stringify({ sdk, events: [] }));
|
|
270
|
+
const out = [];
|
|
271
|
+
let cur = [];
|
|
272
|
+
let size = envelope;
|
|
273
|
+
const push = () => {
|
|
274
|
+
if (cur.length) out.push({ body: JSON.stringify({ sdk, events: cur }), events: cur });
|
|
275
|
+
cur = [];
|
|
276
|
+
size = envelope;
|
|
277
|
+
};
|
|
278
|
+
for (const ev of events) {
|
|
279
|
+
const len = byteLength(JSON.stringify(ev)) + 1;
|
|
280
|
+
if (cur.length && (cur.length >= maxEvents || size + len > MAX_BATCH_BYTES)) push();
|
|
281
|
+
cur.push(ev);
|
|
282
|
+
size += len;
|
|
283
|
+
}
|
|
284
|
+
push();
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/transport.ts
|
|
289
|
+
function unref(t) {
|
|
290
|
+
t.unref?.();
|
|
291
|
+
return t;
|
|
292
|
+
}
|
|
293
|
+
var sleep = (ms) => new Promise((r) => unref(setTimeout(r, ms)));
|
|
294
|
+
var Transport = class {
|
|
295
|
+
constructor(o) {
|
|
296
|
+
this.o = o;
|
|
297
|
+
}
|
|
298
|
+
o;
|
|
299
|
+
queue = [];
|
|
300
|
+
timer;
|
|
301
|
+
immediate;
|
|
302
|
+
chain = Promise.resolve();
|
|
303
|
+
warned401 = false;
|
|
304
|
+
dropped = 0;
|
|
305
|
+
sent = 0;
|
|
306
|
+
get size() {
|
|
307
|
+
return this.queue.length;
|
|
308
|
+
}
|
|
309
|
+
enqueue(ev) {
|
|
310
|
+
this.queue.push(ev);
|
|
311
|
+
if (this.queue.length > this.o.maxQueueSize) {
|
|
312
|
+
const over = this.queue.length - this.o.maxQueueSize;
|
|
313
|
+
this.queue.splice(0, over);
|
|
314
|
+
this.dropped += over;
|
|
315
|
+
this.o.log(`queue full, dropped ${over} oldest event(s) (total dropped: ${this.dropped})`);
|
|
316
|
+
}
|
|
317
|
+
if (this.queue.length >= this.o.maxBatchSize) this.scheduleSoon();
|
|
318
|
+
else this.arm();
|
|
319
|
+
}
|
|
320
|
+
/** Arms the interval flush if not armed. */
|
|
321
|
+
arm() {
|
|
322
|
+
if (this.timer) return;
|
|
323
|
+
this.timer = unref(
|
|
324
|
+
setTimeout(() => {
|
|
325
|
+
this.timer = void 0;
|
|
326
|
+
void this.flush();
|
|
327
|
+
}, this.o.flushIntervalMs)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
/** Coalesced flush on the next macrotask (does not block the caller). */
|
|
331
|
+
scheduleSoon() {
|
|
332
|
+
if (this.immediate) return;
|
|
333
|
+
this.immediate = unref(
|
|
334
|
+
setTimeout(() => {
|
|
335
|
+
this.immediate = void 0;
|
|
336
|
+
void this.flush();
|
|
337
|
+
}, 0)
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Sends everything queued at call time. Calls are serialized, so awaiting
|
|
342
|
+
* flush() always covers events enqueued before it. Always resolves.
|
|
343
|
+
*/
|
|
344
|
+
flush() {
|
|
345
|
+
this.chain = this.chain.then(() => this.drain()).catch((e) => this.o.log("flush failed", e));
|
|
346
|
+
return this.chain;
|
|
347
|
+
}
|
|
348
|
+
async drain() {
|
|
349
|
+
if (this.timer) {
|
|
350
|
+
clearTimeout(this.timer);
|
|
351
|
+
this.timer = void 0;
|
|
352
|
+
}
|
|
353
|
+
if (!this.queue.length) return;
|
|
354
|
+
const events = this.queue.splice(0, this.queue.length);
|
|
355
|
+
await this.sendAll(events);
|
|
356
|
+
}
|
|
357
|
+
async sendAll(events) {
|
|
358
|
+
for (const { body, events: evs } of buildBodies(events, this.o.maxBatchSize)) {
|
|
359
|
+
const ok = await this.send(body);
|
|
360
|
+
if (ok) this.sent += evs.length;
|
|
361
|
+
else {
|
|
362
|
+
this.dropped += evs.length;
|
|
363
|
+
this.o.log(`dropped batch of ${evs.length} event(s) (total dropped: ${this.dropped})`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async send(body) {
|
|
368
|
+
const f = this.o.getFetch();
|
|
369
|
+
if (!f) {
|
|
370
|
+
this.o.log("no fetch available; cannot send");
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const url = joinUrl(this.o.endpoint, "/api/ingest");
|
|
374
|
+
for (let attempt = 1; attempt <= this.o.maxAttempts; attempt++) {
|
|
375
|
+
let retryAfterMs;
|
|
376
|
+
try {
|
|
377
|
+
const res = await f(url, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: { authorization: `Bearer ${this.o.key}`, "content-type": "application/json" },
|
|
380
|
+
body
|
|
381
|
+
});
|
|
382
|
+
res.text().catch(() => void 0);
|
|
383
|
+
if (res.status >= 200 && res.status < 300) {
|
|
384
|
+
this.o.log(`sent batch (${res.status})`);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (res.status === 401 && !this.warned401) {
|
|
388
|
+
this.warned401 = true;
|
|
389
|
+
this.o.warn("[kanca] ingest rejected the key (401). Check KANCA_KEY \u2014 events are being dropped.");
|
|
390
|
+
}
|
|
391
|
+
if (res.status !== 429 && res.status < 500) {
|
|
392
|
+
this.o.log(`ingest responded ${res.status}; not retrying`);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
const ra = Number(res.headers.get("retry-after"));
|
|
396
|
+
if (res.status === 429 && Number.isFinite(ra) && ra >= 0) retryAfterMs = Math.min(ra * 1e3, 3e4);
|
|
397
|
+
this.o.log(`ingest responded ${res.status} (attempt ${attempt}/${this.o.maxAttempts})`);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
this.o.log(`network error (attempt ${attempt}/${this.o.maxAttempts})`, err);
|
|
400
|
+
}
|
|
401
|
+
if (attempt < this.o.maxAttempts) {
|
|
402
|
+
const backoff = this.o.retryBaseMs * 2 ** (attempt - 1);
|
|
403
|
+
const jitter = backoff * 0.2 * Math.random();
|
|
404
|
+
await sleep(retryAfterMs ?? backoff + jitter);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/index.ts
|
|
412
|
+
var exitFlushers = /* @__PURE__ */ new Set();
|
|
413
|
+
var exitHookInstalled = false;
|
|
414
|
+
function registerExitFlush(fn) {
|
|
415
|
+
exitFlushers.add(fn);
|
|
416
|
+
if (exitHookInstalled) return;
|
|
417
|
+
try {
|
|
418
|
+
const proc = globalThis.process;
|
|
419
|
+
if (proc && typeof proc.on === "function") {
|
|
420
|
+
proc.on("beforeExit", () => {
|
|
421
|
+
for (const f of [...exitFlushers]) void f();
|
|
422
|
+
});
|
|
423
|
+
exitHookInstalled = true;
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
var UNINSTALL_SCOPES = [
|
|
429
|
+
"store/app/deleted",
|
|
430
|
+
"store/app/uninstalled",
|
|
431
|
+
"store/authorizedApp/deleted"
|
|
432
|
+
];
|
|
433
|
+
var MARK = /* @__PURE__ */ Symbol.for("kanca.instrumented");
|
|
434
|
+
var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
435
|
+
function clampInt(v, def, min, max) {
|
|
436
|
+
const n = typeof v === "number" && Number.isFinite(v) ? Math.floor(v) : def;
|
|
437
|
+
return Math.min(max, Math.max(min, n));
|
|
438
|
+
}
|
|
439
|
+
function isRequest(v) {
|
|
440
|
+
return typeof Request !== "undefined" && v instanceof Request;
|
|
441
|
+
}
|
|
442
|
+
function toUrl(input) {
|
|
443
|
+
try {
|
|
444
|
+
if (typeof input === "string") return new URL(input);
|
|
445
|
+
if (input instanceof URL) return input;
|
|
446
|
+
if (input && typeof input.url === "string") return new URL(input.url);
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
return void 0;
|
|
450
|
+
}
|
|
451
|
+
function bodyToString(body) {
|
|
452
|
+
if (typeof body === "string") return body;
|
|
453
|
+
try {
|
|
454
|
+
if (body instanceof Uint8Array) return new TextDecoder().decode(body);
|
|
455
|
+
if (body instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(body));
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
return void 0;
|
|
459
|
+
}
|
|
460
|
+
function parseWebhookMeta(text) {
|
|
461
|
+
try {
|
|
462
|
+
const j = JSON.parse(text);
|
|
463
|
+
if (!j || typeof j !== "object" || Array.isArray(j)) return null;
|
|
464
|
+
const o = j;
|
|
465
|
+
return {
|
|
466
|
+
scope: typeof o.scope === "string" && o.scope ? o.scope : void 0,
|
|
467
|
+
id: typeof o.id === "string" && o.id ? o.id : void 0,
|
|
468
|
+
merchantId: typeof o.merchantId === "string" && o.merchantId ? o.merchantId : void 0
|
|
469
|
+
};
|
|
470
|
+
} catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function noopKanca() {
|
|
475
|
+
return {
|
|
476
|
+
webhook: (handler) => async (req, ...rest) => handler(req, ...rest),
|
|
477
|
+
fetch: ((input, init) => globalThis.fetch(input, init)),
|
|
478
|
+
instrument: (client) => client,
|
|
479
|
+
track: () => void 0,
|
|
480
|
+
flush: () => Promise.resolve(),
|
|
481
|
+
enabled: false
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function createKanca(options = {}) {
|
|
485
|
+
const debug = !!options.debug;
|
|
486
|
+
const log = (msg, ...args) => {
|
|
487
|
+
if (debug) console.debug(`[kanca] ${msg}`, ...args);
|
|
488
|
+
};
|
|
489
|
+
const warn = (msg) => console.warn(msg);
|
|
490
|
+
if (options.disabled) {
|
|
491
|
+
log("disabled; SDK is a no-op");
|
|
492
|
+
return noopKanca();
|
|
493
|
+
}
|
|
494
|
+
const key = typeof options.key === "string" ? options.key.trim() : "";
|
|
495
|
+
if (!key) {
|
|
496
|
+
log("no key (KANCA_KEY) configured; SDK is a no-op");
|
|
497
|
+
return noopKanca();
|
|
498
|
+
}
|
|
499
|
+
if (!key.startsWith("kk_")) {
|
|
500
|
+
warn(
|
|
501
|
+
key.startsWith("kp_") ? "[kanca] createKanca() needs a server key (kk_...), got a public key (kp_...). Public keys are for reportVitals()/<KancaVitals/> only. SDK disabled." : "[kanca] KANCA_KEY must start with kk_. SDK disabled."
|
|
502
|
+
);
|
|
503
|
+
return noopKanca();
|
|
504
|
+
}
|
|
505
|
+
const maxBatchSize = clampInt(options.maxBatchSize, 100, 1, MAX_EVENTS_PER_BATCH);
|
|
506
|
+
const transport = new Transport({
|
|
507
|
+
key,
|
|
508
|
+
endpoint: options.endpoint || DEFAULT_ENDPOINT,
|
|
509
|
+
flushIntervalMs: clampInt(options.flushIntervalMs, 2e3, 0, 60 * 60 * 1e3),
|
|
510
|
+
maxBatchSize,
|
|
511
|
+
maxQueueSize: clampInt(options.maxQueueSize, 5e3, maxBatchSize, 1e6),
|
|
512
|
+
maxAttempts: 3,
|
|
513
|
+
retryBaseMs: clampInt(options.retryBaseMs, 500, 0, 6e4),
|
|
514
|
+
log,
|
|
515
|
+
warn,
|
|
516
|
+
getFetch: () => typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0
|
|
517
|
+
});
|
|
518
|
+
const pending = /* @__PURE__ */ new Set();
|
|
519
|
+
const observe = (p) => {
|
|
520
|
+
const tracked = p.catch((e) => log("observation failed", e)).finally(() => pending.delete(tracked));
|
|
521
|
+
pending.add(tracked);
|
|
522
|
+
};
|
|
523
|
+
let keepAlive = false;
|
|
524
|
+
const holdUntilSent = () => {
|
|
525
|
+
if (!options.waitUntil || keepAlive) return;
|
|
526
|
+
keepAlive = true;
|
|
527
|
+
try {
|
|
528
|
+
options.waitUntil(
|
|
529
|
+
new Promise((r) => setTimeout(r, 0)).then(flush).finally(() => {
|
|
530
|
+
keepAlive = false;
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
} catch (e) {
|
|
534
|
+
keepAlive = false;
|
|
535
|
+
log("waitUntil threw", e);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const emit = (ev) => {
|
|
539
|
+
try {
|
|
540
|
+
const clean = sanitizeEvent({ ...ev, id: ev.id ?? uuid() });
|
|
541
|
+
transport.enqueue(clean);
|
|
542
|
+
registerExitFlush(flush);
|
|
543
|
+
holdUntilSent();
|
|
544
|
+
log(`${clean.type} ${clean.name}`, { status: clean.status, ok: clean.ok, durationMs: clean.durationMs });
|
|
545
|
+
} catch (e) {
|
|
546
|
+
log("failed to record event", e);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const flush = async () => {
|
|
550
|
+
try {
|
|
551
|
+
while (pending.size) await Promise.allSettled([...pending]);
|
|
552
|
+
await transport.flush();
|
|
553
|
+
} catch (e) {
|
|
554
|
+
log("flush error", e);
|
|
555
|
+
}
|
|
556
|
+
if (!transport.size && !pending.size) exitFlushers.delete(flush);
|
|
557
|
+
};
|
|
558
|
+
const afterDelivery = () => {
|
|
559
|
+
transport.scheduleSoon();
|
|
560
|
+
if (options.waitUntil) {
|
|
561
|
+
try {
|
|
562
|
+
options.waitUntil(flush());
|
|
563
|
+
} catch (e) {
|
|
564
|
+
log("waitUntil threw", e);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
const webhook = (handler) => async function kancaWebhook(req, ...rest) {
|
|
569
|
+
const ts = Date.now();
|
|
570
|
+
const t0 = now();
|
|
571
|
+
let meta;
|
|
572
|
+
try {
|
|
573
|
+
meta = req.clone().text().then(parseWebhookMeta, () => null);
|
|
574
|
+
} catch {
|
|
575
|
+
meta = Promise.resolve(null);
|
|
576
|
+
}
|
|
577
|
+
const record = (status, error, durationMs) => {
|
|
578
|
+
observe(
|
|
579
|
+
meta.then((m) => {
|
|
580
|
+
const base = { ts, merchantId: m?.merchantId, deliveryId: m?.id };
|
|
581
|
+
emit({
|
|
582
|
+
...base,
|
|
583
|
+
type: "webhook",
|
|
584
|
+
name: m?.scope ?? "unknown",
|
|
585
|
+
status,
|
|
586
|
+
ok: status === 200,
|
|
587
|
+
durationMs,
|
|
588
|
+
error
|
|
589
|
+
});
|
|
590
|
+
if (m?.scope && UNINSTALL_SCOPES.includes(m.scope)) emit({ ...base, type: "uninstall", name: "uninstall" });
|
|
591
|
+
})
|
|
592
|
+
);
|
|
593
|
+
afterDelivery();
|
|
594
|
+
};
|
|
595
|
+
let res;
|
|
596
|
+
try {
|
|
597
|
+
res = await handler.call(this, req, ...rest);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
try {
|
|
600
|
+
record(500, { code: "HANDLER_THREW", message: errorMessage(err) }, now() - t0);
|
|
601
|
+
} catch {
|
|
602
|
+
}
|
|
603
|
+
throw err;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const status = res && typeof res.status === "number" ? res.status : void 0;
|
|
607
|
+
record(status, status === void 0 ? { code: "NO_RESPONSE", message: "handler did not return a Response" } : status === 200 ? void 0 : { code: `HTTP_${status}` }, now() - t0);
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
return res;
|
|
611
|
+
};
|
|
612
|
+
const kfetch = async function kancaFetch(input, init) {
|
|
613
|
+
const base = globalThis.fetch;
|
|
614
|
+
let url;
|
|
615
|
+
let method = "GET";
|
|
616
|
+
let gql = false;
|
|
617
|
+
let opName;
|
|
618
|
+
try {
|
|
619
|
+
url = toUrl(input);
|
|
620
|
+
method = (init?.method ?? (isRequest(input) ? input.method : "GET")).toUpperCase();
|
|
621
|
+
gql = !!url && isIkasGraphqlUrl(url);
|
|
622
|
+
if (gql) {
|
|
623
|
+
if (init && init.body != null) opName = Promise.resolve(operationNameFromBody(bodyToString(init.body)));
|
|
624
|
+
else if (isRequest(input) && input.body) opName = input.clone().text().then(operationNameFromBody, () => "anonymous");
|
|
625
|
+
else opName = Promise.resolve("anonymous");
|
|
626
|
+
}
|
|
627
|
+
} catch {
|
|
628
|
+
url = void 0;
|
|
629
|
+
}
|
|
630
|
+
if (!url) return base(input, init);
|
|
631
|
+
const ts = Date.now();
|
|
632
|
+
const t0 = now();
|
|
633
|
+
let res;
|
|
634
|
+
try {
|
|
635
|
+
res = await base(input, init);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
const durationMs2 = now() - t0;
|
|
638
|
+
try {
|
|
639
|
+
const error = { code: "NETWORK", message: errorMessage(err) };
|
|
640
|
+
if (gql) observe((opName ?? Promise.resolve("anonymous")).then((name) => emit({ type: "graphql", name, ts, ok: false, durationMs: durationMs2, error })));
|
|
641
|
+
else emit({ type: "rest", name: `${method} ${url.host}${url.pathname}`, ts, ok: false, durationMs: durationMs2, error });
|
|
642
|
+
} catch {
|
|
643
|
+
}
|
|
644
|
+
throw err;
|
|
645
|
+
}
|
|
646
|
+
const durationMs = now() - t0;
|
|
647
|
+
try {
|
|
648
|
+
const status = res.status;
|
|
649
|
+
if (gql) {
|
|
650
|
+
let clone;
|
|
651
|
+
try {
|
|
652
|
+
clone = res.clone();
|
|
653
|
+
} catch {
|
|
654
|
+
clone = void 0;
|
|
655
|
+
}
|
|
656
|
+
observe(
|
|
657
|
+
(async () => {
|
|
658
|
+
const name = await (opName ?? Promise.resolve("anonymous"));
|
|
659
|
+
let json;
|
|
660
|
+
try {
|
|
661
|
+
json = clone ? JSON.parse(await clone.text()) : void 0;
|
|
662
|
+
} catch {
|
|
663
|
+
json = void 0;
|
|
664
|
+
}
|
|
665
|
+
const out = classifyGraphql(status, json);
|
|
666
|
+
emit({
|
|
667
|
+
type: "graphql",
|
|
668
|
+
name,
|
|
669
|
+
ts,
|
|
670
|
+
status,
|
|
671
|
+
ok: out.ok,
|
|
672
|
+
durationMs,
|
|
673
|
+
error: out.ok ? void 0 : { code: out.code ?? `HTTP_${status}`, message: out.message }
|
|
674
|
+
});
|
|
675
|
+
})()
|
|
676
|
+
);
|
|
677
|
+
} else {
|
|
678
|
+
const ok = status < 400;
|
|
679
|
+
emit({ type: "rest", name: `${method} ${url.host}${url.pathname}`, ts, status, ok, durationMs, error: ok ? void 0 : { code: `HTTP_${status}` } });
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
return res;
|
|
684
|
+
};
|
|
685
|
+
let warnedInstrument = false;
|
|
686
|
+
const instrument = (client, opts) => {
|
|
687
|
+
const c = client;
|
|
688
|
+
try {
|
|
689
|
+
if (!c || typeof c !== "object" || typeof c.createGraphQLClient !== "function" || !c._client || typeof c._client.request !== "function") {
|
|
690
|
+
if (!warnedInstrument) {
|
|
691
|
+
warnedInstrument = true;
|
|
692
|
+
warn(
|
|
693
|
+
"[kanca] instrument(): not an @ikas/admin-api-client client (expected createGraphQLClient() and _client.request()). Nothing will be recorded for it \u2014 use kanca.fetch instead."
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return client;
|
|
697
|
+
}
|
|
698
|
+
if (c[MARK]) return client;
|
|
699
|
+
const merchantIdFor = () => {
|
|
700
|
+
if (opts?.merchantId) return opts.merchantId;
|
|
701
|
+
const m = c.tokenData?.merchantId;
|
|
702
|
+
return typeof m === "string" && m ? m : void 0;
|
|
703
|
+
};
|
|
704
|
+
const wrap = (gc) => {
|
|
705
|
+
const g = gc;
|
|
706
|
+
if (!g || typeof g.request !== "function" || g[MARK]) return gc;
|
|
707
|
+
const orig = g.request;
|
|
708
|
+
g.request = async function(...args) {
|
|
709
|
+
let name = "anonymous";
|
|
710
|
+
try {
|
|
711
|
+
const a0 = args[0];
|
|
712
|
+
const doc = typeof a0 === "string" ? a0 : a0 && typeof a0 === "object" && "document" in a0 ? a0.document : a0;
|
|
713
|
+
const text = typeof doc === "string" ? doc : doc?.loc?.source?.body;
|
|
714
|
+
name = operationName(text, typeof a0 === "object" ? a0?.operationName : void 0);
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
717
|
+
const ts = Date.now();
|
|
718
|
+
const t0 = now();
|
|
719
|
+
try {
|
|
720
|
+
const data = await orig.apply(this, args);
|
|
721
|
+
emit({ type: "graphql", name, ts, ok: true, durationMs: now() - t0, merchantId: merchantIdFor() });
|
|
722
|
+
return data;
|
|
723
|
+
} catch (err) {
|
|
724
|
+
try {
|
|
725
|
+
const durationMs = now() - t0;
|
|
726
|
+
const resp = err?.response;
|
|
727
|
+
if (resp && typeof resp.status === "number") {
|
|
728
|
+
const out = classifyGraphql(resp.status, { errors: Array.isArray(resp.errors) ? resp.errors : [] });
|
|
729
|
+
emit({
|
|
730
|
+
type: "graphql",
|
|
731
|
+
name,
|
|
732
|
+
ts,
|
|
733
|
+
status: resp.status,
|
|
734
|
+
ok: false,
|
|
735
|
+
durationMs,
|
|
736
|
+
merchantId: merchantIdFor(),
|
|
737
|
+
error: { code: out.code ?? `HTTP_${resp.status}`, message: out.message }
|
|
738
|
+
});
|
|
739
|
+
} else {
|
|
740
|
+
emit({ type: "graphql", name, ts, ok: false, durationMs, merchantId: merchantIdFor(), error: { code: "NETWORK", message: errorMessage(err) } });
|
|
741
|
+
}
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
throw err;
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
g[MARK] = true;
|
|
748
|
+
return gc;
|
|
749
|
+
};
|
|
750
|
+
const origCreate = c.createGraphQLClient;
|
|
751
|
+
c.createGraphQLClient = function(...a) {
|
|
752
|
+
return wrap(origCreate.apply(this, a));
|
|
753
|
+
};
|
|
754
|
+
c._client = wrap(c._client);
|
|
755
|
+
c[MARK] = true;
|
|
756
|
+
log("instrumented @ikas/admin-api-client");
|
|
757
|
+
} catch (e) {
|
|
758
|
+
log("instrument failed", e);
|
|
759
|
+
}
|
|
760
|
+
return client;
|
|
761
|
+
};
|
|
762
|
+
const track = (type, fields = {}) => {
|
|
763
|
+
try {
|
|
764
|
+
const f = fields ?? {};
|
|
765
|
+
let name = f.name;
|
|
766
|
+
if (type === "install" || type === "uninstall") name = type;
|
|
767
|
+
let ok = f.ok;
|
|
768
|
+
if (ok === void 0 && typeof f.status === "number") {
|
|
769
|
+
if (type === "webhook") ok = f.status === 200;
|
|
770
|
+
else if (type === "graphql") ok = f.status >= 200 && f.status < 300;
|
|
771
|
+
else if (type === "rest") ok = f.status < 400;
|
|
772
|
+
}
|
|
773
|
+
const attrs = { ...f.attrs ?? {} };
|
|
774
|
+
if (typeof f.storeName === "string" && f.storeName) attrs.storeName = f.storeName;
|
|
775
|
+
emit({
|
|
776
|
+
type,
|
|
777
|
+
name: name ?? "unknown",
|
|
778
|
+
ts: f.ts ?? Date.now(),
|
|
779
|
+
status: f.status,
|
|
780
|
+
ok,
|
|
781
|
+
durationMs: f.durationMs,
|
|
782
|
+
value: f.value,
|
|
783
|
+
error: f.error,
|
|
784
|
+
merchantId: f.merchantId,
|
|
785
|
+
deliveryId: f.deliveryId,
|
|
786
|
+
attrs
|
|
787
|
+
});
|
|
788
|
+
if (type === "install") transport.scheduleSoon();
|
|
789
|
+
} catch (e) {
|
|
790
|
+
log("track failed", e);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
log(`enabled \u2192 ${options.endpoint || DEFAULT_ENDPOINT}`);
|
|
794
|
+
return {
|
|
795
|
+
webhook,
|
|
796
|
+
fetch: kfetch,
|
|
797
|
+
instrument,
|
|
798
|
+
track,
|
|
799
|
+
flush,
|
|
800
|
+
enabled: true
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
//# sourceMappingURL=index.cjs.map
|