@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/index.js CHANGED
@@ -1,3 +1,132 @@
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
+ }
1
130
  // src/consent.ts
2
131
  var DEFAULT_API_URL = "https://api.gurulu.io";
3
132
  var STORAGE_PREFIX = "gurulu_consent_";
@@ -351,6 +480,44 @@ function startClickAutocapture(track) {
351
480
  };
352
481
  }
353
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
+
354
521
  // src/autocapture/form.ts
355
522
  var SENSITIVE_INPUT_TYPES = new Set(["password", "tel"]);
356
523
  function isSensitiveField(el) {
@@ -701,6 +868,8 @@ function startAutocapture(cfg, sinks) {
701
868
  handles.push(startScrollAutocapture(sinks.scrollDepth));
702
869
  if (merged.web_vitals)
703
870
  handles.push(startWebVitalsAutocapture(sinks.webVital));
871
+ if (merged.js_error)
872
+ handles.push(startErrorAutocapture(sinks.jsError));
704
873
  return {
705
874
  stopAll() {
706
875
  for (const h of handles) {
@@ -1173,6 +1342,7 @@ class Gurulu {
1173
1342
  autocaptureHandle = null;
1174
1343
  testMode = false;
1175
1344
  consent;
1345
+ activation;
1176
1346
  init(opts) {
1177
1347
  if (this.initialized) {
1178
1348
  if (opts.debug && typeof console !== "undefined") {
@@ -1193,6 +1363,12 @@ class Gurulu {
1193
1363
  autoBanner: opts.consent_mode === "banner_required"
1194
1364
  });
1195
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
+ });
1196
1372
  const transport = {
1197
1373
  endpoint: opts.endpoint ?? DEFAULT_ENDPOINT,
1198
1374
  workspaceKey: opts.workspaceKey,
@@ -1234,7 +1410,8 @@ class Gurulu {
1234
1410
  formStarted: (p) => this.queueEvent("form_started", "interaction", p),
1235
1411
  formSubmitted: (p) => this.queueEvent("form_submitted", "interaction", p),
1236
1412
  scrollDepth: (p) => this.queueEvent("scroll_depth", "interaction", p),
1237
- webVital: (p) => this.queueEvent("web_vital", "interaction", p)
1413
+ webVital: (p) => this.queueEvent("web_vital", "interaction", p),
1414
+ jsError: (p) => this.queueEvent("js_error", "interaction", p)
1238
1415
  });
1239
1416
  this.initialized = true;
1240
1417
  }
@@ -1380,6 +1557,9 @@ var publicSdk = {
1380
1557
  get consent() {
1381
1558
  return singleton.consent;
1382
1559
  },
1560
+ get activation() {
1561
+ return singleton.activation;
1562
+ },
1383
1563
  VERSION
1384
1564
  };
1385
1565
  function autoBootstrap() {
@@ -1424,6 +1604,7 @@ export {
1424
1604
  NetworkError,
1425
1605
  InitError,
1426
1606
  GuruluConsent,
1607
+ GuruluActivation,
1427
1608
  Gurulu,
1428
1609
  ConsentBlockedError
1429
1610
  };
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { GuruluPublicSDK, InitOptions } from './index.ts';
3
+ export type GuruluProviderProps = InitOptions & {
4
+ children: ReactNode;
5
+ };
6
+ /** Uygulama kökünü sar — mount'ta gurulu.init (idempotent). */
7
+ export declare function GuruluProvider({ children, ...opts }: GuruluProviderProps): ReactNode;
8
+ /** Singleton SDK — track/identify/page/... handler'larında kullan. */
9
+ export declare function useGurulu(): GuruluPublicSDK;
10
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,OAAO,CAAC;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE/D,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,CAAC;AAExE,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,mBAAmB,GAAG,SAAS,CAOpF;AAED,sEAAsE;AACtE,wBAAgB,SAAS,IAAI,eAAe,CAE3C"}