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