@gurulu/web 0.1.0 → 1.0.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/dist/index.js CHANGED
@@ -1,83 +1,1425 @@
1
- "use strict";
2
- /**
3
- * @gurulu/web — Gurulu Web SDK
4
- *
5
- * Programmatic interface for Gurulu analytics.
6
- * For React/Next.js, use `@gurulu/web/react` instead.
7
- */
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.config = void 0;
10
- exports.init = init;
11
- exports.getInstance = getInstance;
12
- exports.track = track;
13
- exports.identify = identify;
14
- exports.purchase = purchase;
15
- exports.subscribe = subscribe;
16
- exports.setConsent = setConsent;
17
- let _config = null;
18
- exports.config = _config;
19
- let _scriptLoaded = false;
20
- /**
21
- * Initialize the Gurulu tracker by injecting the script tag.
22
- * Call once at app startup.
23
- */
24
- function init(config) {
25
- if (typeof window === 'undefined')
26
- return; // SSR guard
27
- if (_scriptLoaded)
28
- return; // idempotent
29
- exports.config = _config = config;
30
- const endpoint = config.endpoint || 'https://gurulu.io';
31
- const features = config.features?.join(',') || '';
32
- const script = document.createElement('script');
33
- script.src = `${endpoint}/t.js`;
34
- script.defer = true;
35
- script.setAttribute('data-site-id', config.siteId);
36
- script.setAttribute('data-token', config.token);
37
- if (features)
38
- script.setAttribute('data-features', features);
39
- if (config.consent)
40
- script.setAttribute('data-consent', config.consent);
41
- if (config.debug)
42
- script.setAttribute('data-debug', 'true');
43
- document.head.appendChild(script);
44
- _scriptLoaded = true;
45
- }
46
- /**
47
- * Get the global gurulu instance (available after tracker loads).
48
- */
49
- function getInstance() {
50
- if (typeof window === 'undefined')
1
+ // src/consent.ts
2
+ var DEFAULT_API_URL = "https://api.gurulu.io";
3
+ var STORAGE_PREFIX = "gurulu_consent_";
4
+ var ANON_STORAGE_KEY = "gurulu_anon_id";
5
+
6
+ class GuruluConsent {
7
+ workspaceId;
8
+ apiUrl;
9
+ fetchImpl;
10
+ storage;
11
+ autoBanner;
12
+ locale;
13
+ anonymousIdValue;
14
+ bannerConfigCache = null;
15
+ bannerEl = null;
16
+ constructor(opts) {
17
+ this.workspaceId = opts.workspaceId;
18
+ this.apiUrl = (opts.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
19
+ this.fetchImpl = opts.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : noFetch);
20
+ this.storage = opts.storage ?? (typeof globalThis !== "undefined" && "localStorage" in globalThis ? globalThis.localStorage : null);
21
+ this.autoBanner = opts.autoBanner ?? true;
22
+ this.locale = opts.locale ?? detectLocale();
23
+ this.anonymousIdValue = opts.anonymousId ?? this.loadOrCreateAnonId();
24
+ }
25
+ async init() {
26
+ if (typeof window === "undefined")
27
+ return;
28
+ try {
29
+ const res = await this.fetchImpl(`${this.apiUrl}/v1/consent/banner-config?workspace_id=${encodeURIComponent(this.workspaceId)}`, { method: "GET", credentials: "omit" });
30
+ if (!res.ok)
31
+ return;
32
+ this.bannerConfigCache = await res.json();
33
+ } catch {
34
+ return;
35
+ }
36
+ const local = this.getState();
37
+ if (!local && this.autoBanner && this.bannerConfigCache?.mode === "banner_required") {
38
+ this.showBanner();
39
+ }
40
+ }
41
+ getState() {
42
+ if (!this.storage)
43
+ return null;
44
+ try {
45
+ const raw = this.storage.getItem(this.storageKey());
46
+ if (!raw)
51
47
  return null;
52
- return window.gurulu || null;
53
- }
54
- /**
55
- * Track a custom event.
56
- */
57
- function track(eventName, properties) {
58
- getInstance()?.track(eventName, properties);
59
- }
60
- /**
61
- * Identify a user with traits.
62
- */
63
- function identify(userId, traits) {
64
- getInstance()?.identify(userId, traits);
65
- }
66
- /**
67
- * Track a purchase event.
68
- */
69
- function purchase(params) {
70
- getInstance()?.purchase(params);
71
- }
72
- /**
73
- * Track a subscription event.
74
- */
75
- function subscribe(params) {
76
- getInstance()?.subscribe(params);
77
- }
78
- /**
79
- * Set consent level.
80
- */
81
- function setConsent(level) {
82
- getInstance()?.setConsent(level);
48
+ const parsed = JSON.parse(raw);
49
+ if (parsed.expiresAt && new Date(parsed.expiresAt).getTime() < Date.now()) {
50
+ this.storage.removeItem(this.storageKey());
51
+ return null;
52
+ }
53
+ return parsed;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ async setState(categories) {
59
+ const current = this.getState()?.categories;
60
+ const next = {
61
+ necessary: true,
62
+ analytics: categories.analytics ?? current?.analytics ?? false,
63
+ marketing: categories.marketing ?? current?.marketing ?? false,
64
+ functional: categories.functional ?? current?.functional ?? false,
65
+ personalization: categories.personalization ?? current?.personalization ?? false
66
+ };
67
+ const grantedAt = new Date;
68
+ const renewalMonths = this.bannerConfigCache?.renewal_months ?? 13;
69
+ const expiresAt = new Date(grantedAt);
70
+ expiresAt.setMonth(expiresAt.getMonth() + renewalMonths);
71
+ const snapshot = {
72
+ workspaceId: this.workspaceId,
73
+ anonymousId: this.anonymousIdValue,
74
+ categories: next,
75
+ grantedAt: grantedAt.toISOString(),
76
+ expiresAt: expiresAt.toISOString(),
77
+ source: "sdk_api"
78
+ };
79
+ if (this.storage) {
80
+ try {
81
+ this.storage.setItem(this.storageKey(), JSON.stringify(snapshot));
82
+ } catch {}
83
+ }
84
+ try {
85
+ await this.fetchImpl(`${this.apiUrl}/v1/consent/state`, {
86
+ method: "POST",
87
+ credentials: "omit",
88
+ headers: { "content-type": "application/json" },
89
+ body: JSON.stringify({
90
+ workspace_id: this.workspaceId,
91
+ anonymous_id: this.anonymousIdValue,
92
+ necessary: next.necessary,
93
+ analytics: next.analytics,
94
+ marketing: next.marketing,
95
+ functional: next.functional,
96
+ personalization: next.personalization,
97
+ source: "sdk_api"
98
+ })
99
+ });
100
+ } catch {}
101
+ }
102
+ showBanner() {
103
+ if (typeof document === "undefined")
104
+ return;
105
+ if (this.bannerEl)
106
+ return;
107
+ const cfg = this.bannerConfigCache?.banner_config ?? {};
108
+ const text = this.bannerText();
109
+ const accentColor = cfg.brand?.primary_color ?? "#fafafa";
110
+ const position = cfg.position ?? "bottom";
111
+ const el = document.createElement("div");
112
+ el.setAttribute("data-gurulu-consent-banner", "");
113
+ el.setAttribute("role", "dialog");
114
+ el.setAttribute("aria-label", text.heading);
115
+ el.style.cssText = bannerCss(position);
116
+ el.innerHTML = bannerHtml(text, accentColor);
117
+ const acceptBtn = el.querySelector("[data-gurulu-accept]");
118
+ const rejectBtn = el.querySelector("[data-gurulu-reject]");
119
+ acceptBtn?.addEventListener("click", () => {
120
+ this.setState({
121
+ analytics: true,
122
+ marketing: true,
123
+ functional: true,
124
+ personalization: true
125
+ });
126
+ this.hideBanner();
127
+ });
128
+ rejectBtn?.addEventListener("click", () => {
129
+ this.setState({
130
+ analytics: false,
131
+ marketing: false,
132
+ functional: false,
133
+ personalization: false
134
+ });
135
+ this.hideBanner();
136
+ });
137
+ document.body.appendChild(el);
138
+ this.bannerEl = el;
139
+ }
140
+ hideBanner() {
141
+ if (this.bannerEl?.parentNode) {
142
+ this.bannerEl.parentNode.removeChild(this.bannerEl);
143
+ }
144
+ this.bannerEl = null;
145
+ }
146
+ buildIngestHeader() {
147
+ const snap = this.getState();
148
+ if (!snap)
149
+ return JSON.stringify({ necessary: true });
150
+ return JSON.stringify(snap.categories);
151
+ }
152
+ getAnonymousId() {
153
+ return this.anonymousIdValue;
154
+ }
155
+ storageKey() {
156
+ return `${STORAGE_PREFIX}${this.workspaceId}`;
157
+ }
158
+ loadOrCreateAnonId() {
159
+ if (!this.storage)
160
+ return generateAnonId();
161
+ try {
162
+ const existing = this.storage.getItem(ANON_STORAGE_KEY);
163
+ if (existing)
164
+ return existing;
165
+ const fresh = generateAnonId();
166
+ this.storage.setItem(ANON_STORAGE_KEY, fresh);
167
+ return fresh;
168
+ } catch {
169
+ return generateAnonId();
170
+ }
171
+ }
172
+ bannerText() {
173
+ const cfg = this.bannerConfigCache?.banner_config;
174
+ const localeOverride = cfg?.text_overrides?.[this.locale];
175
+ if (this.locale === "tr") {
176
+ return {
177
+ heading: localeOverride?.heading ?? "Çerezleri tercih et",
178
+ body: localeOverride?.body ?? "Deneyimini iyileştirmek için analytics + pazarlama çerezleri kullanıyoruz.",
179
+ accept: localeOverride?.accept ?? "Tümünü kabul et",
180
+ reject: localeOverride?.reject ?? "Sadece gerekli olanlar"
181
+ };
182
+ }
183
+ return {
184
+ heading: localeOverride?.heading ?? "Manage cookies",
185
+ body: localeOverride?.body ?? "We use analytics and marketing cookies to improve your experience.",
186
+ accept: localeOverride?.accept ?? "Accept all",
187
+ reject: localeOverride?.reject ?? "Necessary only"
188
+ };
189
+ }
190
+ }
191
+ function bannerCss(position) {
192
+ const base = "position:fixed;z-index:2147483647;background:#141414;color:#fafafa;border:1px solid #262626;border-radius:8px;padding:16px;font-family:-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.5;box-shadow:0 4px 24px rgba(0,0,0,0.4);max-width:480px;";
193
+ switch (position) {
194
+ case "top":
195
+ return `${base}top:16px;left:50%;transform:translateX(-50%);`;
196
+ case "bottom-left":
197
+ return `${base}bottom:16px;left:16px;`;
198
+ case "bottom-right":
199
+ return `${base}bottom:16px;right:16px;`;
200
+ case "modal":
201
+ return `${base}top:50%;left:50%;transform:translate(-50%,-50%);`;
202
+ default:
203
+ return `${base}bottom:16px;left:50%;transform:translateX(-50%);`;
204
+ }
205
+ }
206
+ function bannerHtml(text, accent) {
207
+ return `
208
+ <div style="margin-bottom:12px;font-weight:600;">${escapeHtml(text.heading)}</div>
209
+ <div style="margin-bottom:16px;color:#a3a3a3;">${escapeHtml(text.body)}</div>
210
+ <div style="display:flex;gap:8px;flex-wrap:wrap;">
211
+ <button data-gurulu-accept type="button" style="background:${escapeAttr(accent)};color:#0a0a0a;border:none;padding:8px 16px;border-radius:6px;font-weight:600;cursor:pointer;">${escapeHtml(text.accept)}</button>
212
+ <button data-gurulu-reject type="button" style="background:transparent;color:#fafafa;border:1px solid #404040;padding:8px 16px;border-radius:6px;cursor:pointer;">${escapeHtml(text.reject)}</button>
213
+ </div>
214
+ `;
215
+ }
216
+ function escapeHtml(s) {
217
+ return s.replace(/[&<>"']/g, (c) => {
218
+ switch (c) {
219
+ case "&":
220
+ return "&amp;";
221
+ case "<":
222
+ return "&lt;";
223
+ case ">":
224
+ return "&gt;";
225
+ case '"':
226
+ return "&quot;";
227
+ case "'":
228
+ return "&#39;";
229
+ default:
230
+ return c;
231
+ }
232
+ });
233
+ }
234
+ function escapeAttr(s) {
235
+ return s.replace(/[^a-zA-Z0-9#()_\-., ]/g, "");
236
+ }
237
+ function detectLocale() {
238
+ if (typeof navigator === "undefined")
239
+ return "en";
240
+ const lang = (navigator.language ?? "").toLowerCase();
241
+ if (lang.startsWith("tr"))
242
+ return "tr";
243
+ if (lang.startsWith("zh"))
244
+ return "zh";
245
+ if (lang.startsWith("ar"))
246
+ return "ar";
247
+ return "en";
248
+ }
249
+ function generateAnonId() {
250
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
251
+ return `anon_${crypto.randomUUID()}`;
252
+ }
253
+ return `anon_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
254
+ }
255
+ async function noFetch() {
256
+ throw new Error("fetch not available — provide opts.fetchImpl");
257
+ }
258
+ // src/autocapture/click.ts
259
+ var CLICKABLE_TAGS = new Set(["A", "BUTTON"]);
260
+ var DOWNLOAD_EXT = /\.(pdf|zip|dmg|exe|tar|gz|rar|7z|mp3|mp4|csv|xlsx?|docx?|pptx?)(\?|$)/i;
261
+ function isTrackable(el) {
262
+ let node = el;
263
+ while (node) {
264
+ if (node.hasAttribute?.("data-gurulu-no-track"))
265
+ return false;
266
+ node = node.parentElement;
267
+ }
268
+ return true;
269
+ }
270
+ function findClickable(target) {
271
+ let node = target;
272
+ while (node) {
273
+ if (!node.tagName) {
274
+ node = node.parentElement;
275
+ continue;
276
+ }
277
+ if (CLICKABLE_TAGS.has(node.tagName))
278
+ return node;
279
+ if (node.getAttribute?.("role") === "button")
280
+ return node;
281
+ if (node.tagName === "INPUT") {
282
+ const t = node.type?.toLowerCase();
283
+ if (t === "submit" || t === "button")
284
+ return node;
285
+ }
286
+ node = node.parentElement;
287
+ }
288
+ return null;
289
+ }
290
+ function readCustomProps(el) {
291
+ const out = {};
292
+ if (!el.attributes)
293
+ return out;
294
+ for (let i = 0;i < el.attributes.length; i += 1) {
295
+ const attr = el.attributes.item(i);
296
+ if (!attr)
297
+ continue;
298
+ if (attr.name.startsWith("data-gurulu-prop-")) {
299
+ const k = attr.name.slice("data-gurulu-prop-".length);
300
+ out[k] = attr.value;
301
+ }
302
+ }
303
+ return out;
304
+ }
305
+ function startClickAutocapture(track) {
306
+ if (typeof document === "undefined")
307
+ return { stop: () => {
308
+ return;
309
+ } };
310
+ const handler = (ev) => {
311
+ const found = findClickable(ev.target);
312
+ if (!found || !isTrackable(found))
313
+ return;
314
+ const payload = {
315
+ element_tag: found.tagName.toLowerCase()
316
+ };
317
+ if (found.id)
318
+ payload.element_id = found.id;
319
+ if (found.className && typeof found.className === "string") {
320
+ payload.element_class = found.className.slice(0, 256);
321
+ }
322
+ const text = (found.textContent ?? "").trim().slice(0, 200);
323
+ if (text)
324
+ payload.element_text = text;
325
+ if (found.tagName === "A") {
326
+ const href = found.href;
327
+ if (href) {
328
+ payload.href = href;
329
+ try {
330
+ const host = new URL(href).hostname;
331
+ if (typeof window !== "undefined" && host !== window.location.hostname) {
332
+ payload.is_outbound = true;
333
+ }
334
+ } catch {}
335
+ if (DOWNLOAD_EXT.test(href))
336
+ payload.is_download = true;
337
+ }
338
+ }
339
+ const customEvent = found.getAttribute?.("data-gurulu-event");
340
+ if (customEvent) {
341
+ payload.custom_event = customEvent;
342
+ payload.custom_props = readCustomProps(found);
343
+ }
344
+ track(payload);
345
+ };
346
+ document.addEventListener("click", handler, true);
347
+ return {
348
+ stop() {
349
+ document.removeEventListener("click", handler, true);
350
+ }
351
+ };
352
+ }
353
+
354
+ // src/autocapture/form.ts
355
+ var SENSITIVE_INPUT_TYPES = new Set(["password", "tel"]);
356
+ function isSensitiveField(el) {
357
+ if (el.tagName !== "INPUT")
358
+ return false;
359
+ const input = el;
360
+ const type = (input.type ?? "").toLowerCase();
361
+ if (SENSITIVE_INPUT_TYPES.has(type))
362
+ return true;
363
+ const autocomplete = (input.autocomplete ?? "").toLowerCase();
364
+ if (autocomplete.startsWith("cc-"))
365
+ return true;
366
+ return false;
367
+ }
368
+ function isTrackable2(el) {
369
+ let node = el;
370
+ while (node) {
371
+ if (node.hasAttribute?.("data-gurulu-no-track"))
372
+ return false;
373
+ node = node.parentElement;
374
+ }
375
+ return true;
376
+ }
377
+ function findForm(target) {
378
+ let node = target;
379
+ while (node) {
380
+ if (node.tagName === "FORM")
381
+ return node;
382
+ node = node.parentElement;
383
+ }
384
+ return null;
385
+ }
386
+ function formMeta(form) {
387
+ const out = {
388
+ count: form.elements?.length ?? 0
389
+ };
390
+ if (form.id)
391
+ out.id = form.id;
392
+ if (form.name)
393
+ out.name = form.name;
394
+ return out;
395
+ }
396
+ function startFormAutocapture(track) {
397
+ if (typeof document === "undefined")
398
+ return { stop: () => {
399
+ return;
400
+ } };
401
+ const startedForms = new WeakSet;
402
+ const onFocus = (ev) => {
403
+ const t = ev.target;
404
+ if (!t)
405
+ return;
406
+ if (isSensitiveField(t))
407
+ return;
408
+ if (!isTrackable2(t))
409
+ return;
410
+ const form = findForm(t);
411
+ if (!form || startedForms.has(form))
412
+ return;
413
+ startedForms.add(form);
414
+ const meta = formMeta(form);
415
+ const p = { field_count: meta.count };
416
+ if (meta.id)
417
+ p.form_id = meta.id;
418
+ if (meta.name)
419
+ p.form_name = meta.name;
420
+ track.onStart(p);
421
+ };
422
+ const onSubmit = (ev) => {
423
+ const form = ev.target;
424
+ if (!form || form.tagName !== "FORM")
425
+ return;
426
+ if (!isTrackable2(form))
427
+ return;
428
+ const meta = formMeta(form);
429
+ const filled = [];
430
+ const els = form.elements;
431
+ for (let i = 0;i < els.length; i += 1) {
432
+ const el = els[i];
433
+ if (!el)
434
+ continue;
435
+ if (isSensitiveField(el))
436
+ continue;
437
+ if (!("value" in el))
438
+ continue;
439
+ if (typeof el.value === "string" && el.value.length > 0 && el.name) {
440
+ filled.push(el.name);
441
+ }
442
+ }
443
+ const p = { field_count: meta.count };
444
+ if (meta.id)
445
+ p.form_id = meta.id;
446
+ if (meta.name)
447
+ p.form_name = meta.name;
448
+ if (filled.length > 0)
449
+ p.filled_fields = filled;
450
+ track.onSubmit(p);
451
+ };
452
+ document.addEventListener("focus", onFocus, true);
453
+ document.addEventListener("submit", onSubmit, true);
454
+ return {
455
+ stop() {
456
+ document.removeEventListener("focus", onFocus, true);
457
+ document.removeEventListener("submit", onSubmit, true);
458
+ }
459
+ };
460
+ }
461
+
462
+ // src/autocapture/page.ts
463
+ function startPageAutocapture(track) {
464
+ if (typeof window === "undefined" || typeof document === "undefined") {
465
+ return { stop: () => {
466
+ return;
467
+ } };
468
+ }
469
+ let lastPath = window.location.pathname + window.location.search;
470
+ function emit() {
471
+ const current = window.location.pathname + window.location.search;
472
+ if (current === lastPath)
473
+ return;
474
+ lastPath = current;
475
+ track(window.location.href, document.title, document.referrer);
476
+ }
477
+ track(window.location.href, document.title, document.referrer);
478
+ const onPop = () => emit();
479
+ window.addEventListener("popstate", onPop);
480
+ const origPush = history.pushState.bind(history);
481
+ const origReplace = history.replaceState.bind(history);
482
+ history.pushState = function patchedPush(...args) {
483
+ const ret = origPush(...args);
484
+ queueMicrotask(emit);
485
+ return ret;
486
+ };
487
+ history.replaceState = function patchedReplace(...args) {
488
+ const ret = origReplace(...args);
489
+ queueMicrotask(emit);
490
+ return ret;
491
+ };
492
+ return {
493
+ stop() {
494
+ window.removeEventListener("popstate", onPop);
495
+ history.pushState = origPush;
496
+ history.replaceState = origReplace;
497
+ }
498
+ };
499
+ }
500
+
501
+ // src/autocapture/scroll.ts
502
+ var THRESHOLDS = [25, 50, 75, 90];
503
+ function startScrollAutocapture(track) {
504
+ if (typeof window === "undefined" || typeof document === "undefined") {
505
+ return { stop: () => {
506
+ return;
507
+ } };
508
+ }
509
+ const fired = new Set;
510
+ let raf = 0;
511
+ const compute = () => {
512
+ raf = 0;
513
+ const doc = document.documentElement;
514
+ const body = document.body;
515
+ if (!doc || !body)
516
+ return;
517
+ const scrollTop = window.scrollY ?? doc.scrollTop ?? 0;
518
+ const viewport = window.innerHeight ?? doc.clientHeight ?? 0;
519
+ const docHeight = Math.max(body.scrollHeight ?? 0, doc.scrollHeight ?? 0);
520
+ if (docHeight <= viewport)
521
+ return;
522
+ const percent = (scrollTop + viewport) / docHeight * 100;
523
+ for (const t of THRESHOLDS) {
524
+ if (percent >= t && !fired.has(t)) {
525
+ fired.add(t);
526
+ track({ depth_percent: t });
527
+ }
528
+ }
529
+ };
530
+ const onScroll = () => {
531
+ if (raf !== 0)
532
+ return;
533
+ raf = requestAnimationFrame(compute);
534
+ };
535
+ window.addEventListener("scroll", onScroll, { passive: true });
536
+ return {
537
+ stop() {
538
+ window.removeEventListener("scroll", onScroll);
539
+ if (raf !== 0)
540
+ cancelAnimationFrame(raf);
541
+ }
542
+ };
543
+ }
544
+
545
+ // src/autocapture/web-vitals.ts
546
+ var RATINGS = {
547
+ LCP: [2500, 4000],
548
+ FID: [100, 300],
549
+ INP: [200, 500],
550
+ CLS: [0.1, 0.25],
551
+ TTFB: [800, 1800],
552
+ FCP: [1800, 3000]
553
+ };
554
+ function rate(metric, value) {
555
+ const [good, poor] = RATINGS[metric];
556
+ if (value <= good)
557
+ return "good";
558
+ if (value <= poor)
559
+ return "needs-improvement";
560
+ return "poor";
561
+ }
562
+ function safeObserve(entryType, buffered, cb) {
563
+ if (typeof PerformanceObserver === "undefined")
564
+ return null;
565
+ try {
566
+ const po = new PerformanceObserver((list) => cb(list.getEntries()));
567
+ po.observe({ type: entryType, buffered });
568
+ return po;
569
+ } catch {
570
+ return null;
571
+ }
572
+ }
573
+ function startWebVitalsAutocapture(track) {
574
+ if (typeof window === "undefined")
575
+ return { stop: () => {
576
+ return;
577
+ } };
578
+ const observers = [];
579
+ try {
580
+ const nav = performance.getEntriesByType("navigation")[0];
581
+ if (nav && nav.responseStart > 0) {
582
+ track({ metric: "TTFB", value: nav.responseStart, rating: rate("TTFB", nav.responseStart) });
583
+ }
584
+ const fcp = performance.getEntriesByName("first-contentful-paint")[0];
585
+ if (fcp) {
586
+ track({ metric: "FCP", value: fcp.startTime, rating: rate("FCP", fcp.startTime) });
587
+ }
588
+ } catch {}
589
+ let lastLcp = 0;
590
+ const lcpPo = safeObserve("largest-contentful-paint", true, (entries) => {
591
+ const last = entries[entries.length - 1];
592
+ if (last)
593
+ lastLcp = last.startTime;
594
+ });
595
+ if (lcpPo)
596
+ observers.push(lcpPo);
597
+ const fidPo = safeObserve("first-input", true, (entries) => {
598
+ const first = entries[0];
599
+ if (first) {
600
+ const value = first.processingStart - first.startTime;
601
+ track({ metric: "FID", value, rating: rate("FID", value) });
602
+ }
603
+ });
604
+ if (fidPo)
605
+ observers.push(fidPo);
606
+ let worstInp = 0;
607
+ const eventPo = safeObserve("event", true, (entries) => {
608
+ for (const e of entries) {
609
+ const dur = e.duration;
610
+ if (dur > worstInp)
611
+ worstInp = dur;
612
+ }
613
+ });
614
+ if (eventPo)
615
+ observers.push(eventPo);
616
+ let cls = 0;
617
+ const clsPo = safeObserve("layout-shift", true, (entries) => {
618
+ for (const e of entries) {
619
+ const ls = e;
620
+ if (!ls.hadRecentInput)
621
+ cls += ls.value;
622
+ }
623
+ });
624
+ if (clsPo)
625
+ observers.push(clsPo);
626
+ const flush = () => {
627
+ if (lastLcp > 0)
628
+ track({ metric: "LCP", value: lastLcp, rating: rate("LCP", lastLcp) });
629
+ if (worstInp > 0)
630
+ track({ metric: "INP", value: worstInp, rating: rate("INP", worstInp) });
631
+ if (cls > 0)
632
+ track({ metric: "CLS", value: cls, rating: rate("CLS", cls) });
633
+ lastLcp = 0;
634
+ worstInp = 0;
635
+ cls = 0;
636
+ };
637
+ let onHidden = null;
638
+ let onPagehide = null;
639
+ if (typeof document !== "undefined") {
640
+ onHidden = () => {
641
+ if (document.visibilityState === "hidden")
642
+ flush();
643
+ };
644
+ onPagehide = flush;
645
+ document.addEventListener("visibilitychange", onHidden);
646
+ window.addEventListener("pagehide", onPagehide);
647
+ }
648
+ return {
649
+ stop() {
650
+ for (const po of observers) {
651
+ try {
652
+ po.disconnect();
653
+ } catch {}
654
+ }
655
+ if (typeof document !== "undefined" && onHidden) {
656
+ document.removeEventListener("visibilitychange", onHidden);
657
+ }
658
+ if (typeof window !== "undefined" && onPagehide) {
659
+ window.removeEventListener("pagehide", onPagehide);
660
+ }
661
+ }
662
+ };
663
+ }
664
+
665
+ // src/autocapture/index.ts
666
+ var DEFAULTS = {
667
+ page_view: true,
668
+ click: true,
669
+ form_started: true,
670
+ form_submitted: true,
671
+ scroll_depth: true,
672
+ web_vitals: true,
673
+ outbound_link_click: true,
674
+ download_click: true,
675
+ js_error: false,
676
+ console_error: false,
677
+ network_error: false
678
+ };
679
+ function startAutocapture(cfg, sinks) {
680
+ if (cfg === false)
681
+ return { stopAll: () => {
682
+ return;
683
+ } };
684
+ const merged = { ...DEFAULTS, ...cfg ?? {} };
685
+ const handles = [];
686
+ if (merged.page_view)
687
+ handles.push(startPageAutocapture(sinks.pageView));
688
+ if (merged.click)
689
+ handles.push(startClickAutocapture(sinks.click));
690
+ if (merged.form_started || merged.form_submitted) {
691
+ handles.push(startFormAutocapture({
692
+ onStart: merged.form_started ? sinks.formStarted : () => {
693
+ return;
694
+ },
695
+ onSubmit: merged.form_submitted ? sinks.formSubmitted : () => {
696
+ return;
697
+ }
698
+ }));
699
+ }
700
+ if (merged.scroll_depth)
701
+ handles.push(startScrollAutocapture(sinks.scrollDepth));
702
+ if (merged.web_vitals)
703
+ handles.push(startWebVitalsAutocapture(sinks.webVital));
704
+ return {
705
+ stopAll() {
706
+ for (const h of handles) {
707
+ try {
708
+ h.stop();
709
+ } catch {}
710
+ }
711
+ }
712
+ };
713
+ }
714
+
715
+ // src/identity.ts
716
+ var ANON_KEY = "gurulu_aid";
717
+ var ANON_COOKIE = "gurulu_aid_mirror";
718
+ var PERSON_KEY = "gurulu_pid";
719
+ function hasWindow() {
720
+ return typeof window !== "undefined" && typeof document !== "undefined";
721
+ }
722
+ function uuid() {
723
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
724
+ return crypto.randomUUID();
725
+ }
726
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
727
+ }
728
+ function readLocalStorage(key) {
729
+ if (!hasWindow())
730
+ return null;
731
+ try {
732
+ return window.localStorage.getItem(key);
733
+ } catch {
734
+ return null;
735
+ }
736
+ }
737
+ function writeLocalStorage(key, value) {
738
+ if (!hasWindow())
739
+ return;
740
+ try {
741
+ window.localStorage.setItem(key, value);
742
+ } catch {}
743
+ }
744
+ function deleteLocalStorage(key) {
745
+ if (!hasWindow())
746
+ return;
747
+ try {
748
+ window.localStorage.removeItem(key);
749
+ } catch {}
750
+ }
751
+ function readCookie(name) {
752
+ if (!hasWindow())
753
+ return null;
754
+ const cookieStr = document.cookie ?? "";
755
+ for (const part of cookieStr.split(";")) {
756
+ const [k, v] = part.trim().split("=");
757
+ if (k === name && v !== undefined)
758
+ return decodeURIComponent(v);
759
+ }
760
+ return null;
761
+ }
762
+ function writeCookie(name, value, days = 365) {
763
+ if (!hasWindow())
764
+ return;
765
+ const expires = new Date(Date.now() + days * 86400000).toUTCString();
766
+ const host = window.location.hostname;
767
+ const parts = host.split(".");
768
+ const domain = parts.length >= 2 ? `.${parts.slice(-2).join(".")}` : host;
769
+ try {
770
+ document.cookie = `${name}=${encodeURIComponent(value)}; path=/; domain=${domain}; expires=${expires}; SameSite=Lax`;
771
+ } catch {}
772
+ }
773
+ function getOrCreateAnonymousId() {
774
+ const existing = readLocalStorage(ANON_KEY) ?? readCookie(ANON_COOKIE);
775
+ if (existing) {
776
+ writeLocalStorage(ANON_KEY, existing);
777
+ writeCookie(ANON_COOKIE, existing);
778
+ return existing;
779
+ }
780
+ const fresh = uuid();
781
+ writeLocalStorage(ANON_KEY, fresh);
782
+ writeCookie(ANON_COOKIE, fresh);
783
+ return fresh;
784
+ }
785
+ function setPersonId(personId) {
786
+ writeLocalStorage(PERSON_KEY, personId);
787
+ }
788
+ function getPersonId() {
789
+ return readLocalStorage(PERSON_KEY);
790
+ }
791
+ function clearIdentity() {
792
+ deleteLocalStorage(PERSON_KEY);
793
+ deleteLocalStorage(ANON_KEY);
794
+ const fresh = uuid();
795
+ writeLocalStorage(ANON_KEY, fresh);
796
+ writeCookie(ANON_COOKIE, fresh);
797
+ return fresh;
798
+ }
799
+ function newEventId() {
800
+ return uuid();
801
+ }
802
+
803
+ // src/errors.ts
804
+ class SDKError extends Error {
805
+ code;
806
+ constructor(code, message) {
807
+ super(message);
808
+ this.code = code;
809
+ this.name = "SDKError";
810
+ }
811
+ }
812
+
813
+ class InitError extends SDKError {
814
+ constructor(message) {
815
+ super("SDK_INIT", message);
816
+ this.name = "InitError";
817
+ }
818
+ }
819
+
820
+ class NetworkError extends SDKError {
821
+ status;
822
+ constructor(message, status) {
823
+ super("SDK_NETWORK", message);
824
+ this.name = "NetworkError";
825
+ if (status !== undefined)
826
+ this.status = status;
827
+ }
828
+ }
829
+
830
+ class QueueFullError extends SDKError {
831
+ constructor(message) {
832
+ super("SDK_QUEUE_FULL", message);
833
+ this.name = "QueueFullError";
834
+ }
835
+ }
836
+
837
+ class OptedOutError extends SDKError {
838
+ constructor() {
839
+ super("SDK_OPTED_OUT", "tracking disabled — gurulu.optOut() in effect");
840
+ this.name = "OptedOutError";
841
+ }
842
+ }
843
+
844
+ class ConsentBlockedError extends SDKError {
845
+ constructor() {
846
+ super("SDK_CONSENT_BLOCKED", "event queued — awaiting consent grant");
847
+ this.name = "ConsentBlockedError";
848
+ }
849
+ }
850
+
851
+ // src/transport.ts
852
+ var BEACON_SIZE_LIMIT = 65536;
853
+ function buildHeaders(cfg, extra) {
854
+ const h = new Headers({
855
+ "content-type": "application/json",
856
+ authorization: `Bearer ${cfg.workspaceKey}`,
857
+ "x-gurulu-sdk": `@gurulu/web@${cfg.sdkVersion}`
858
+ });
859
+ if (extra) {
860
+ for (const [k, v] of Object.entries(extra))
861
+ h.set(k, v);
862
+ }
863
+ return h;
864
+ }
865
+ async function sendBatch(cfg, body, extraHeaders) {
866
+ const f = cfg.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
867
+ if (!f)
868
+ throw new NetworkError("fetch unavailable");
869
+ const url = `${cfg.endpoint}/v1/ingest/batch`;
870
+ const res = await f(url, {
871
+ method: "POST",
872
+ keepalive: true,
873
+ credentials: "omit",
874
+ headers: buildHeaders(cfg, extraHeaders),
875
+ body: JSON.stringify(body)
876
+ });
877
+ if (!res.ok)
878
+ throw new NetworkError(`ingest ${res.status}`, res.status);
879
+ }
880
+ function sendBeaconBatch(cfg, body) {
881
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function")
882
+ return false;
883
+ const url = `${cfg.endpoint}/v1/ingest/batch?wk=${encodeURIComponent(cfg.workspaceKey)}&sdk=${encodeURIComponent(cfg.sdkVersion)}`;
884
+ try {
885
+ const payload = JSON.stringify(body);
886
+ if (payload.length >= BEACON_SIZE_LIMIT)
887
+ return false;
888
+ const blob = new Blob([payload], { type: "application/json" });
889
+ return navigator.sendBeacon(url, blob);
890
+ } catch {
891
+ return false;
892
+ }
893
+ }
894
+ async function sendIdentify(cfg, body) {
895
+ const f = cfg.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
896
+ if (!f)
897
+ throw new NetworkError("fetch unavailable");
898
+ const res = await f(`${cfg.endpoint}/v1/ingest/identify`, {
899
+ method: "POST",
900
+ keepalive: true,
901
+ credentials: "omit",
902
+ headers: buildHeaders(cfg),
903
+ body: JSON.stringify(body)
904
+ });
905
+ if (!res.ok)
906
+ throw new NetworkError(`identify ${res.status}`, res.status);
907
+ }
908
+
909
+ // src/queue.ts
910
+ var QUEUE_KEY = "gurulu_queue";
911
+ function hasWindow2() {
912
+ return typeof window !== "undefined";
913
+ }
914
+ function loadPersisted() {
915
+ if (!hasWindow2())
916
+ return [];
917
+ try {
918
+ const raw = window.localStorage.getItem(QUEUE_KEY);
919
+ if (!raw)
920
+ return [];
921
+ const arr = JSON.parse(raw);
922
+ return Array.isArray(arr) ? arr : [];
923
+ } catch {
924
+ return [];
925
+ }
926
+ }
927
+ function persist(events) {
928
+ if (!hasWindow2())
929
+ return;
930
+ try {
931
+ if (events.length === 0)
932
+ window.localStorage.removeItem(QUEUE_KEY);
933
+ else
934
+ window.localStorage.setItem(QUEUE_KEY, JSON.stringify(events));
935
+ } catch {}
936
+ }
937
+
938
+ class EventQueue {
939
+ opts;
940
+ buffer;
941
+ timer = null;
942
+ flushing = false;
943
+ constructor(opts) {
944
+ this.opts = opts;
945
+ this.buffer = loadPersisted();
946
+ this.scheduleNext();
947
+ this.installUnloadHooks();
948
+ if (this.buffer.length > 0)
949
+ this.scheduleImmediate();
950
+ }
951
+ enqueue(event) {
952
+ this.buffer.push(event);
953
+ persist(this.buffer);
954
+ if (this.buffer.length >= this.opts.maxQueueSize) {
955
+ this.flush();
956
+ } else {
957
+ this.scheduleNext();
958
+ }
959
+ }
960
+ size() {
961
+ return this.buffer.length;
962
+ }
963
+ async flush() {
964
+ if (this.flushing || this.buffer.length === 0)
965
+ return;
966
+ this.flushing = true;
967
+ const batch = this.buffer.slice(0);
968
+ try {
969
+ await this.sendWithRetry(batch);
970
+ this.buffer = this.buffer.slice(batch.length);
971
+ persist(this.buffer);
972
+ } catch (err) {
973
+ if (this.opts.debug && typeof console !== "undefined") {
974
+ console.warn("[gurulu] flush failed, retain queue", err);
975
+ }
976
+ } finally {
977
+ this.flushing = false;
978
+ }
979
+ }
980
+ flushBeacon() {
981
+ if (this.buffer.length === 0)
982
+ return;
983
+ const sent = sendBeaconBatch(this.opts.transport, { events: this.buffer });
984
+ if (sent) {
985
+ this.buffer = [];
986
+ persist(this.buffer);
987
+ }
988
+ }
989
+ scheduleNext() {
990
+ if (this.timer !== null)
991
+ return;
992
+ this.timer = setTimeout(() => {
993
+ this.timer = null;
994
+ this.flush();
995
+ }, this.opts.flushIntervalMs);
996
+ }
997
+ scheduleImmediate() {
998
+ if (this.timer !== null) {
999
+ clearTimeout(this.timer);
1000
+ this.timer = null;
1001
+ }
1002
+ this.timer = setTimeout(() => {
1003
+ this.timer = null;
1004
+ this.flush();
1005
+ }, 50);
1006
+ }
1007
+ async sendWithRetry(batch) {
1008
+ let attempt = 0;
1009
+ const max = 3;
1010
+ while (attempt < max) {
1011
+ try {
1012
+ await sendBatch(this.opts.transport, { events: batch });
1013
+ return;
1014
+ } catch (err) {
1015
+ attempt += 1;
1016
+ if (attempt >= max)
1017
+ throw err;
1018
+ const backoff = 2 ** attempt * 1000;
1019
+ await new Promise((r) => setTimeout(r, backoff));
1020
+ }
1021
+ }
1022
+ }
1023
+ installUnloadHooks() {
1024
+ if (!hasWindow2() || typeof document === "undefined")
1025
+ return;
1026
+ const onHidden = () => {
1027
+ if (document.visibilityState === "hidden")
1028
+ this.flushBeacon();
1029
+ };
1030
+ try {
1031
+ document.addEventListener("visibilitychange", onHidden);
1032
+ window.addEventListener("pagehide", () => this.flushBeacon());
1033
+ } catch {}
1034
+ }
1035
+ }
1036
+
1037
+ // src/session.ts
1038
+ var SESSION_KEY = "gurulu_sid";
1039
+ var SESSION_STARTED_KEY = "gurulu_session_started_at";
1040
+ var LAST_EVENT_KEY = "gurulu_last_event_at";
1041
+ var FIRST_SOURCE_KEY = "gurulu_first_source";
1042
+ var SESSION_TIMEOUT_MS = 30 * 60 * 1000;
1043
+ function hasWindow3() {
1044
+ return typeof window !== "undefined";
1045
+ }
1046
+ function uuid2() {
1047
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1048
+ return crypto.randomUUID();
1049
+ }
1050
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
1051
+ }
1052
+ function readLs(key) {
1053
+ if (!hasWindow3())
1054
+ return null;
1055
+ try {
1056
+ return window.localStorage.getItem(key);
1057
+ } catch {
1058
+ return null;
1059
+ }
1060
+ }
1061
+ function writeLs(key, value) {
1062
+ if (!hasWindow3())
1063
+ return;
1064
+ try {
1065
+ window.localStorage.setItem(key, value);
1066
+ } catch {}
1067
+ }
1068
+ function resolveSession(now = Date.now()) {
1069
+ const existing = readLs(SESSION_KEY);
1070
+ const startedAt = Number(readLs(SESSION_STARTED_KEY) ?? "0");
1071
+ const lastEvent = Number(readLs(LAST_EVENT_KEY) ?? "0");
1072
+ if (existing && startedAt && now - lastEvent < SESSION_TIMEOUT_MS) {
1073
+ writeLs(LAST_EVENT_KEY, String(now));
1074
+ return { session_id: existing, session_started_at: startedAt, is_new: false };
1075
+ }
1076
+ const fresh = uuid2();
1077
+ writeLs(SESSION_KEY, fresh);
1078
+ writeLs(SESSION_STARTED_KEY, String(now));
1079
+ writeLs(LAST_EVENT_KEY, String(now));
1080
+ return { session_id: fresh, session_started_at: now, is_new: true };
1081
+ }
1082
+ function parseUrlContext(href, referrer) {
1083
+ const utm = {};
1084
+ const clickId = {};
1085
+ if (!href)
1086
+ return { utm, click_id: clickId };
1087
+ let url;
1088
+ try {
1089
+ url = new URL(href);
1090
+ } catch {
1091
+ return { utm, click_id: clickId };
1092
+ }
1093
+ const p = url.searchParams;
1094
+ const s = p.get("utm_source");
1095
+ if (s)
1096
+ utm.source = s;
1097
+ const m = p.get("utm_medium");
1098
+ if (m)
1099
+ utm.medium = m;
1100
+ const c = p.get("utm_campaign");
1101
+ if (c)
1102
+ utm.campaign = c;
1103
+ const t = p.get("utm_term");
1104
+ if (t)
1105
+ utm.term = t;
1106
+ const ct = p.get("utm_content");
1107
+ if (ct)
1108
+ utm.content = ct;
1109
+ const gclid = p.get("gclid");
1110
+ if (gclid)
1111
+ clickId.gclid = gclid;
1112
+ const fbclid = p.get("fbclid");
1113
+ if (fbclid)
1114
+ clickId.fbclid = fbclid;
1115
+ const ttclid = p.get("ttclid");
1116
+ if (ttclid)
1117
+ clickId.ttclid = ttclid;
1118
+ const li = p.get("li_fat_id");
1119
+ if (li)
1120
+ clickId.li_fat_id = li;
1121
+ return { utm, click_id: clickId };
1122
+ }
1123
+ function preserveFirstSource(touch) {
1124
+ if (readLs(FIRST_SOURCE_KEY))
1125
+ return;
1126
+ writeLs(FIRST_SOURCE_KEY, JSON.stringify(touch));
1127
+ }
1128
+ function getFirstSource() {
1129
+ const raw = readLs(FIRST_SOURCE_KEY);
1130
+ if (!raw)
1131
+ return null;
1132
+ try {
1133
+ return JSON.parse(raw);
1134
+ } catch {
1135
+ return null;
1136
+ }
1137
+ }
1138
+
1139
+ // src/core.ts
1140
+ var SDK_VERSION = "0.1.0";
1141
+ var DEFAULT_ENDPOINT = "https://ingest.gurulu.io";
1142
+ var DEFAULT_API_URL2 = "https://api.gurulu.io";
1143
+ var OPT_OUT_KEY = "gurulu_opt_out";
1144
+ function isOptedOut() {
1145
+ if (typeof window === "undefined")
1146
+ return false;
1147
+ try {
1148
+ return window.localStorage.getItem(OPT_OUT_KEY) === "1";
1149
+ } catch {
1150
+ return false;
1151
+ }
1152
+ }
1153
+ function setOptOut(flag) {
1154
+ if (typeof window === "undefined")
1155
+ return;
1156
+ try {
1157
+ if (flag)
1158
+ window.localStorage.setItem(OPT_OUT_KEY, "1");
1159
+ else
1160
+ window.localStorage.removeItem(OPT_OUT_KEY);
1161
+ } catch {}
1162
+ }
1163
+ function nowIso() {
1164
+ return new Date().toISOString();
1165
+ }
1166
+
1167
+ class Gurulu {
1168
+ initialized = false;
1169
+ opts = null;
1170
+ queue = null;
1171
+ session = null;
1172
+ anonymousId = null;
1173
+ autocaptureHandle = null;
1174
+ consent;
1175
+ init(opts) {
1176
+ if (this.initialized) {
1177
+ if (opts.debug && typeof console !== "undefined") {
1178
+ console.warn("[gurulu] init() already called — ignoring");
1179
+ }
1180
+ return;
1181
+ }
1182
+ if (typeof window === "undefined")
1183
+ return;
1184
+ this.opts = opts;
1185
+ this.anonymousId = getOrCreateAnonymousId();
1186
+ this.session = resolveSession();
1187
+ this.consent = new GuruluConsent({
1188
+ workspaceId: opts.workspaceKey,
1189
+ apiUrl: opts.apiUrl ?? DEFAULT_API_URL2,
1190
+ anonymousId: this.anonymousId,
1191
+ autoBanner: opts.consent_mode === "banner_required"
1192
+ });
1193
+ this.consent.init();
1194
+ const transport = {
1195
+ endpoint: opts.endpoint ?? DEFAULT_ENDPOINT,
1196
+ workspaceKey: opts.workspaceKey,
1197
+ sdkVersion: SDK_VERSION
1198
+ };
1199
+ this.queue = new EventQueue({
1200
+ transport,
1201
+ flushIntervalMs: opts.flush_interval_ms ?? 5000,
1202
+ maxQueueSize: opts.max_queue_size ?? 50,
1203
+ ...opts.debug ? { debug: true } : {}
1204
+ });
1205
+ const ctx = parseUrlContext(window.location.href, document.referrer);
1206
+ if (Object.keys(ctx.utm).length > 0 || Object.keys(ctx.click_id).length > 0) {
1207
+ const touch = {
1208
+ utm: ctx.utm,
1209
+ click_id: ctx.click_id,
1210
+ referrer: document.referrer,
1211
+ landing_url: window.location.href,
1212
+ captured_at: Date.now()
1213
+ };
1214
+ preserveFirstSource(touch);
1215
+ }
1216
+ this.autocaptureHandle = startAutocapture(opts.autocapture, {
1217
+ pageView: (url, title, referrer) => this.queueEvent("page_view", "interaction", { url, title, referrer }),
1218
+ click: (payload) => {
1219
+ const ev = payload.custom_event ?? "element_clicked";
1220
+ const props = {
1221
+ element_tag: payload.element_tag,
1222
+ ...payload.element_id ? { element_id: payload.element_id } : {},
1223
+ ...payload.element_class ? { element_class: payload.element_class } : {},
1224
+ ...payload.element_text ? { element_text: payload.element_text } : {},
1225
+ ...payload.href ? { href: payload.href } : {},
1226
+ ...payload.is_outbound ? { is_outbound: true } : {},
1227
+ ...payload.is_download ? { is_download: true } : {},
1228
+ ...payload.custom_props ?? {}
1229
+ };
1230
+ this.queueEvent(ev, "interaction", props);
1231
+ },
1232
+ formStarted: (p) => this.queueEvent("form_started", "interaction", p),
1233
+ formSubmitted: (p) => this.queueEvent("form_submitted", "interaction", p),
1234
+ scrollDepth: (p) => this.queueEvent("scroll_depth", "interaction", p),
1235
+ webVital: (p) => this.queueEvent("web_vital", "interaction", p)
1236
+ });
1237
+ this.initialized = true;
1238
+ }
1239
+ track(eventKey, properties) {
1240
+ this.queueEvent(eventKey, "interaction", properties);
1241
+ }
1242
+ page(override) {
1243
+ if (!this.initialized || typeof window === "undefined")
1244
+ return;
1245
+ const url = override?.url ?? window.location.href;
1246
+ const title = override?.title ?? document.title;
1247
+ const referrer = override?.referrer ?? document.referrer;
1248
+ this.queueEvent("page_view", "interaction", { url, title, referrer });
1249
+ }
1250
+ async identify(personId, traits) {
1251
+ if (!this.initialized || isOptedOut())
1252
+ return;
1253
+ setPersonId(personId);
1254
+ if (!this.opts)
1255
+ return;
1256
+ try {
1257
+ await sendIdentify({
1258
+ endpoint: this.opts.endpoint ?? DEFAULT_ENDPOINT,
1259
+ workspaceKey: this.opts.workspaceKey,
1260
+ sdkVersion: SDK_VERSION
1261
+ }, {
1262
+ anonymous_id: this.anonymousId,
1263
+ external_user_id: personId,
1264
+ ...traits?.email ? { email: traits.email } : {},
1265
+ ...traits?.phone ? { phone: traits.phone } : {},
1266
+ ...traits ? { traits } : {}
1267
+ });
1268
+ } catch (err) {
1269
+ if (this.opts?.debug && typeof console !== "undefined") {
1270
+ console.warn("[gurulu] identify failed", err);
1271
+ }
1272
+ }
1273
+ }
1274
+ reset() {
1275
+ this.anonymousId = clearIdentity();
1276
+ if (typeof window !== "undefined") {
1277
+ try {
1278
+ window.localStorage.removeItem("gurulu_sid");
1279
+ window.localStorage.removeItem("gurulu_session_started_at");
1280
+ window.localStorage.removeItem("gurulu_last_event_at");
1281
+ window.localStorage.removeItem("gurulu_first_source");
1282
+ } catch {}
1283
+ }
1284
+ this.session = resolveSession();
1285
+ }
1286
+ async flush() {
1287
+ await this.queue?.flush();
1288
+ }
1289
+ optOut() {
1290
+ setOptOut(true);
1291
+ }
1292
+ optIn() {
1293
+ setOptOut(false);
1294
+ }
1295
+ queueEvent(eventKey, eventType, properties) {
1296
+ if (!this.initialized || !this.queue || isOptedOut())
1297
+ return;
1298
+ if (!this.consentAllowsAnalytics() && this.opts?.consent_mode === "banner_required") {
1299
+ return;
1300
+ }
1301
+ this.session = resolveSession();
1302
+ const event = {
1303
+ anonymous_id: this.anonymousId ?? "anon_unknown",
1304
+ event_id: newEventId(),
1305
+ event_key: eventKey,
1306
+ event_type: eventType,
1307
+ occurred_at: nowIso(),
1308
+ producer: "script",
1309
+ producer_version: SDK_VERSION,
1310
+ ...this.session ? { session_id: this.session.session_id } : {}
1311
+ };
1312
+ const pid = getPersonId();
1313
+ if (pid)
1314
+ event.person_id = pid;
1315
+ if (properties && Object.keys(properties).length > 0)
1316
+ event.properties = properties;
1317
+ const ctx = this.buildContext();
1318
+ if (ctx)
1319
+ event.context = ctx;
1320
+ const consent = this.snapshotConsent();
1321
+ if (consent)
1322
+ event.consent_state = consent;
1323
+ this.queue.enqueue(event);
1324
+ }
1325
+ snapshotConsent() {
1326
+ const snap = this.consent?.getState();
1327
+ if (!snap)
1328
+ return;
1329
+ return {
1330
+ necessary: snap.categories.necessary,
1331
+ analytics: snap.categories.analytics,
1332
+ marketing: snap.categories.marketing,
1333
+ functional: snap.categories.functional,
1334
+ personalization: snap.categories.personalization
1335
+ };
1336
+ }
1337
+ consentAllowsAnalytics() {
1338
+ const snap = this.consent?.getState();
1339
+ if (!snap)
1340
+ return false;
1341
+ return snap.categories.analytics === true;
1342
+ }
1343
+ buildContext() {
1344
+ if (typeof window === "undefined")
1345
+ return;
1346
+ const ctx = {
1347
+ url: window.location.href,
1348
+ referrer: document.referrer,
1349
+ page_title: document.title,
1350
+ user_agent: navigator.userAgent,
1351
+ domain: window.location.hostname
1352
+ };
1353
+ const first = getFirstSource();
1354
+ if (first?.utm && Object.keys(first.utm).length > 0)
1355
+ ctx.utm = first.utm;
1356
+ if (first?.click_id && Object.keys(first.click_id).length > 0)
1357
+ ctx.click_id = first.click_id;
1358
+ return ctx;
1359
+ }
1360
+ }
1361
+ // src/index.ts
1362
+ var VERSION = "0.1.0";
1363
+ var singleton = new Gurulu;
1364
+ function createGurulu() {
1365
+ return new Gurulu;
1366
+ }
1367
+ var publicSdk = {
1368
+ init: (opts) => singleton.init(opts),
1369
+ track: (eventKey, properties) => singleton.track(eventKey, properties),
1370
+ identify: (personId, traits) => singleton.identify(personId, traits),
1371
+ page: (override) => singleton.page(override),
1372
+ reset: () => singleton.reset(),
1373
+ flush: () => singleton.flush(),
1374
+ optOut: () => singleton.optOut(),
1375
+ optIn: () => singleton.optIn(),
1376
+ get consent() {
1377
+ return singleton.consent;
1378
+ },
1379
+ VERSION
1380
+ };
1381
+ function autoBootstrap() {
1382
+ if (typeof document === "undefined")
1383
+ return;
1384
+ const script = document.currentScript;
1385
+ const fallback = !script ? document.querySelector("script[data-workspace]") : script;
1386
+ if (!fallback)
1387
+ return;
1388
+ const workspaceKey = fallback.getAttribute("data-workspace");
1389
+ if (!workspaceKey)
1390
+ return;
1391
+ const opts = { workspaceKey };
1392
+ const endpoint = fallback.getAttribute("data-endpoint");
1393
+ if (endpoint)
1394
+ opts.endpoint = endpoint;
1395
+ const apiUrl = fallback.getAttribute("data-api-url");
1396
+ if (apiUrl)
1397
+ opts.apiUrl = apiUrl;
1398
+ const consentMode = fallback.getAttribute("data-consent");
1399
+ if (consentMode === "banner_required" || consentMode === "allow_by_default") {
1400
+ opts.consent_mode = consentMode;
1401
+ }
1402
+ const allowlist = fallback.getAttribute("data-allowlist");
1403
+ if (allowlist)
1404
+ opts.cross_domain_allowlist = allowlist.split(",").map((s) => s.trim());
1405
+ publicSdk.init(opts);
1406
+ if (typeof window !== "undefined") {
1407
+ window.gurulu = publicSdk;
1408
+ }
83
1409
  }
1410
+ autoBootstrap();
1411
+ var src_default = publicSdk;
1412
+ export {
1413
+ src_default as default,
1414
+ createGurulu,
1415
+ autoBootstrap,
1416
+ VERSION,
1417
+ SDKError,
1418
+ QueueFullError,
1419
+ OptedOutError,
1420
+ NetworkError,
1421
+ InitError,
1422
+ GuruluConsent,
1423
+ Gurulu,
1424
+ ConsentBlockedError
1425
+ };