@kharon-labs/browser 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/dist/index.cjs +653 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +630 -0
- package/dist/tracker.cjs +228 -0
- package/dist/tracker.d.ts +130 -0
- package/dist/tracker.js +205 -0
- package/dist/v1.js +1 -0
- package/package.json +27 -4
package/dist/tracker.cjs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/browser/src/tracker.ts
|
|
21
|
+
var tracker_exports = {};
|
|
22
|
+
__export(tracker_exports, {
|
|
23
|
+
COLLECT_PATH: () => COLLECT_PATH,
|
|
24
|
+
PIN_KEY: () => PIN_KEY,
|
|
25
|
+
SESSION_KEY: () => SESSION_KEY,
|
|
26
|
+
startTracker: () => startTracker
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(tracker_exports);
|
|
29
|
+
|
|
30
|
+
// src/engine/analytics/touch.ts
|
|
31
|
+
var MAX_TOUCH_VALUE = 100;
|
|
32
|
+
var CLICK_ID_PARAMS = {
|
|
33
|
+
gclid: "google",
|
|
34
|
+
gbraid: "google",
|
|
35
|
+
wbraid: "google",
|
|
36
|
+
msclkid: "microsoft",
|
|
37
|
+
fbclid: "meta",
|
|
38
|
+
li_fat_id: "linkedin",
|
|
39
|
+
ttclid: "tiktok",
|
|
40
|
+
twclid: "x"
|
|
41
|
+
};
|
|
42
|
+
var CLICK_ID_KINDS = new Set(Object.values(CLICK_ID_PARAMS));
|
|
43
|
+
var UTM_FIELDS = ["source", "medium", "campaign", "term", "content"];
|
|
44
|
+
var KHARON_LINK_PARAM = "ge_co";
|
|
45
|
+
function touchFromUrl(url, referrer) {
|
|
46
|
+
const touch = {};
|
|
47
|
+
let page;
|
|
48
|
+
try {
|
|
49
|
+
page = new URL(url);
|
|
50
|
+
} catch {
|
|
51
|
+
return touch;
|
|
52
|
+
}
|
|
53
|
+
for (const field of UTM_FIELDS) {
|
|
54
|
+
const value = clean(page.searchParams.get(`utm_${field}`));
|
|
55
|
+
if (value) touch[field] = value;
|
|
56
|
+
}
|
|
57
|
+
for (const [param, kind] of Object.entries(CLICK_ID_PARAMS)) {
|
|
58
|
+
if (page.searchParams.get(param)) {
|
|
59
|
+
touch.clickId = kind;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (page.searchParams.get(KHARON_LINK_PARAM)) touch.kharonLink = true;
|
|
64
|
+
const from = referrerHost(referrer);
|
|
65
|
+
if (from && from !== bareHost(page.hostname)) touch.referrer = from;
|
|
66
|
+
return touch;
|
|
67
|
+
}
|
|
68
|
+
function clean(value) {
|
|
69
|
+
const trimmed = value?.replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
70
|
+
return trimmed ? trimmed.slice(0, MAX_TOUCH_VALUE) : null;
|
|
71
|
+
}
|
|
72
|
+
function referrerHost(referrer) {
|
|
73
|
+
if (!referrer) return null;
|
|
74
|
+
try {
|
|
75
|
+
const { protocol, hostname } = new URL(referrer);
|
|
76
|
+
return protocol === "http:" || protocol === "https:" ? bareHost(hostname) : null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function bareHost(hostname) {
|
|
82
|
+
return hostname.toLowerCase().replace(/^www\./, "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/engine/analytics/beacon.ts
|
|
86
|
+
var BEACON_VERSION = 1;
|
|
87
|
+
var MAX_BEACON_EVENTS = 50;
|
|
88
|
+
var SLOT_ATTRIBUTES = {
|
|
89
|
+
slot: "data-ge-slot",
|
|
90
|
+
variant: "data-ge-variant",
|
|
91
|
+
counterfactual: "data-ge-counterfactual",
|
|
92
|
+
/** Why the slot shows what it shows (matched, default, holdout). For debugging; not sent. */
|
|
93
|
+
reason: "data-ge-reason",
|
|
94
|
+
/** Which step of the fallback ladder chose the content (exact, near, segment, default). Sent on impressions. */
|
|
95
|
+
rung: "data-ge-rung"
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// packages/browser/src/tracker.ts
|
|
99
|
+
var COLLECT_PATH = "/api/collect";
|
|
100
|
+
var SESSION_KEY = "ge_sid";
|
|
101
|
+
var PIN_KEY = "ge_pin";
|
|
102
|
+
var VISIBLE_RATIO = 0.5;
|
|
103
|
+
var FLUSH_INTERVAL_MS = 2e3;
|
|
104
|
+
function startTracker({
|
|
105
|
+
pageId,
|
|
106
|
+
configVersion,
|
|
107
|
+
endpoint = COLLECT_PATH,
|
|
108
|
+
root = document,
|
|
109
|
+
match,
|
|
110
|
+
envelope = (events) => ({ v: BEACON_VERSION, events, ...match ? { m: match } : {} }),
|
|
111
|
+
crossOrigin = false
|
|
112
|
+
}) {
|
|
113
|
+
const win = root.defaultView ?? window;
|
|
114
|
+
const { id: sessionId, started } = sessionFor(win);
|
|
115
|
+
const queue = [];
|
|
116
|
+
const seenSlots = /* @__PURE__ */ new Set();
|
|
117
|
+
const push = (type, body) => {
|
|
118
|
+
queue.push({ type, sessionId, ...body });
|
|
119
|
+
if (queue.length >= MAX_BEACON_EVENTS) flush();
|
|
120
|
+
};
|
|
121
|
+
const flush = () => {
|
|
122
|
+
while (queue.length) send(win, endpoint, envelope(queue.splice(0, MAX_BEACON_EVENTS)), crossOrigin);
|
|
123
|
+
};
|
|
124
|
+
push("page_view", started ? { pageId, configVersion, touch: touchFromUrl(win.location.href, root.referrer) } : { pageId, configVersion });
|
|
125
|
+
const slots = Array.from(root.querySelectorAll(`[${SLOT_ATTRIBUTES.slot}]`));
|
|
126
|
+
const observer = "IntersectionObserver" in win ? new win.IntersectionObserver(
|
|
127
|
+
(entries) => {
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
if (!entry.isIntersecting || entry.intersectionRatio < VISIBLE_RATIO) continue;
|
|
130
|
+
const slot = readSlot(entry.target);
|
|
131
|
+
observer?.unobserve(entry.target);
|
|
132
|
+
if (!slot || seenSlots.has(slot.slotId)) continue;
|
|
133
|
+
seenSlots.add(slot.slotId);
|
|
134
|
+
push("slot_impression", slot);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{ threshold: VISIBLE_RATIO }
|
|
138
|
+
) : null;
|
|
139
|
+
for (const slot of slots) observer?.observe(slot);
|
|
140
|
+
const interaction = (action) => (event) => {
|
|
141
|
+
const slot = closestSlot(event.target);
|
|
142
|
+
if (slot) push("interaction", { slotId: slot.slotId, variantId: slot.variantId, action });
|
|
143
|
+
};
|
|
144
|
+
const onClick = interaction("click");
|
|
145
|
+
const onCopy = interaction("copy");
|
|
146
|
+
const onToggle = (event) => {
|
|
147
|
+
if (event.target.open) interaction("expand")(event);
|
|
148
|
+
};
|
|
149
|
+
const onSubmit = (event) => {
|
|
150
|
+
const goal = event.target.closest?.("form")?.getAttribute("data-ge-goal");
|
|
151
|
+
if (goal) conversion(goal);
|
|
152
|
+
};
|
|
153
|
+
const onHide = () => {
|
|
154
|
+
if (root.visibilityState === "hidden") flush();
|
|
155
|
+
};
|
|
156
|
+
const conversion = (goal, value) => {
|
|
157
|
+
push("conversion", value === void 0 ? { goal } : { goal, value });
|
|
158
|
+
flush();
|
|
159
|
+
};
|
|
160
|
+
root.addEventListener("click", onClick, true);
|
|
161
|
+
root.addEventListener("copy", onCopy, true);
|
|
162
|
+
root.addEventListener("toggle", onToggle, true);
|
|
163
|
+
root.addEventListener("submit", onSubmit, true);
|
|
164
|
+
root.addEventListener("visibilitychange", onHide);
|
|
165
|
+
win.addEventListener("pagehide", flush);
|
|
166
|
+
const timer = win.setInterval(flush, FLUSH_INTERVAL_MS);
|
|
167
|
+
return {
|
|
168
|
+
conversion,
|
|
169
|
+
flush,
|
|
170
|
+
stop({ discard = false } = {}) {
|
|
171
|
+
if (discard) queue.length = 0;
|
|
172
|
+
flush();
|
|
173
|
+
observer?.disconnect();
|
|
174
|
+
win.clearInterval(timer);
|
|
175
|
+
root.removeEventListener("click", onClick, true);
|
|
176
|
+
root.removeEventListener("copy", onCopy, true);
|
|
177
|
+
root.removeEventListener("toggle", onToggle, true);
|
|
178
|
+
root.removeEventListener("submit", onSubmit, true);
|
|
179
|
+
root.removeEventListener("visibilitychange", onHide);
|
|
180
|
+
win.removeEventListener("pagehide", flush);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function readSlot(el) {
|
|
185
|
+
const slotId = el.getAttribute(SLOT_ATTRIBUTES.slot);
|
|
186
|
+
if (!slotId) return null;
|
|
187
|
+
return {
|
|
188
|
+
slotId,
|
|
189
|
+
variantId: el.getAttribute(SLOT_ATTRIBUTES.variant) || null,
|
|
190
|
+
counterfactualVariantId: el.getAttribute(SLOT_ATTRIBUTES.counterfactual) || null,
|
|
191
|
+
...el.getAttribute(SLOT_ATTRIBUTES.rung) ? { rung: el.getAttribute(SLOT_ATTRIBUTES.rung) } : {}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function closestSlot(target) {
|
|
195
|
+
const el = target instanceof Element ? target.closest(`[${SLOT_ATTRIBUTES.slot}]`) : null;
|
|
196
|
+
return el ? readSlot(el) : null;
|
|
197
|
+
}
|
|
198
|
+
function sessionFor(win) {
|
|
199
|
+
try {
|
|
200
|
+
const existing = win.sessionStorage.getItem(SESSION_KEY);
|
|
201
|
+
if (existing) return { id: existing, started: false };
|
|
202
|
+
const id = uuid(win);
|
|
203
|
+
win.sessionStorage.setItem(SESSION_KEY, id);
|
|
204
|
+
return { id, started: true };
|
|
205
|
+
} catch {
|
|
206
|
+
return { id: uuid(win), started: true };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function uuid(win) {
|
|
210
|
+
if (typeof win.crypto.randomUUID === "function") return win.crypto.randomUUID();
|
|
211
|
+
const b = win.crypto.getRandomValues(new Uint8Array(16));
|
|
212
|
+
b[6] = b[6] & 15 | 64;
|
|
213
|
+
b[8] = b[8] & 63 | 128;
|
|
214
|
+
const hex = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
215
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
216
|
+
}
|
|
217
|
+
function send(win, endpoint, payload, crossOrigin) {
|
|
218
|
+
const body = JSON.stringify(payload);
|
|
219
|
+
const type = crossOrigin ? "text/plain" : "application/json";
|
|
220
|
+
if (win.navigator.sendBeacon?.(endpoint, new Blob([body], { type }))) return;
|
|
221
|
+
void win.fetch(endpoint, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
body,
|
|
224
|
+
keepalive: true,
|
|
225
|
+
credentials: crossOrigin ? "omit" : "same-origin",
|
|
226
|
+
headers: { "content-type": type }
|
|
227
|
+
}).catch((error) => console.warn("[ge] analytics beacon failed", error));
|
|
228
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
type SignalSource = "campaign_link" | "form_email" | "known_visitor" | "reverse_ip" | "referrer";
|
|
2
|
+
type DecisionRung = "exact" | "near" | "segment" | "default";
|
|
3
|
+
interface Touch$1 {
|
|
4
|
+
/** `utm_source`, `utm_medium`, … as tagged, trimmed to `MAX_TOUCH_VALUE` characters. */
|
|
5
|
+
source?: string;
|
|
6
|
+
medium?: string;
|
|
7
|
+
campaign?: string;
|
|
8
|
+
term?: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
/** Which ad network's click id the URL carried (`gclid` → `google`). The id itself is dropped. */
|
|
11
|
+
clickId?: ClickIdKind;
|
|
12
|
+
/** The landing URL was one of our campaign links (`?ge_co=`). */
|
|
13
|
+
kharonLink?: true;
|
|
14
|
+
/** Host of an external referring page, lowercased and without `www.`. */
|
|
15
|
+
referrer?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const CLICK_ID_PARAMS: {
|
|
18
|
+
readonly gclid: "google";
|
|
19
|
+
readonly gbraid: "google";
|
|
20
|
+
readonly wbraid: "google";
|
|
21
|
+
readonly msclkid: "microsoft";
|
|
22
|
+
readonly fbclid: "meta";
|
|
23
|
+
readonly li_fat_id: "linkedin";
|
|
24
|
+
readonly ttclid: "tiktok";
|
|
25
|
+
readonly twclid: "x";
|
|
26
|
+
};
|
|
27
|
+
type ClickIdKind = (typeof CLICK_ID_PARAMS)[keyof typeof CLICK_ID_PARAMS];
|
|
28
|
+
interface BaseEvent {
|
|
29
|
+
siteId: string;
|
|
30
|
+
visitorId: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
/** Epoch milliseconds, stamped by the collector (not the browser) to avoid clock skew. */
|
|
33
|
+
ts: number;
|
|
34
|
+
/**
|
|
35
|
+
* The company identity resolution matched the visitor to, by domain, stamped by the collector from
|
|
36
|
+
* the server-signed match token. Null when resolve matched no company; absent when unknown (no valid
|
|
37
|
+
* token, e.g. an older SDK, or stored before stamping).
|
|
38
|
+
* Firmographics are joined from the company source at report time.
|
|
39
|
+
*/
|
|
40
|
+
companyDomain?: string | null;
|
|
41
|
+
/** The evidence behind that match (a campaign link, reverse IP…), so reports can weigh or exclude it. */
|
|
42
|
+
companySource?: SignalSource | null;
|
|
43
|
+
}
|
|
44
|
+
interface PageViewEvent extends BaseEvent {
|
|
45
|
+
type: "page_view";
|
|
46
|
+
pageId: string;
|
|
47
|
+
/** Site config version that was resolved, so results are attributable to what was live. */
|
|
48
|
+
configVersion: number;
|
|
49
|
+
/**
|
|
50
|
+
* How the session arrived, on its first page view only (`{}` for a direct visit). Absent on later
|
|
51
|
+
* page views and from trackers older than signup attribution.
|
|
52
|
+
*/
|
|
53
|
+
touch?: Touch$1;
|
|
54
|
+
}
|
|
55
|
+
interface SlotImpressionEvent extends BaseEvent {
|
|
56
|
+
type: "slot_impression";
|
|
57
|
+
slotId: string;
|
|
58
|
+
variantId: string | null;
|
|
59
|
+
/** Set for holdout visitors: the variant withheld from them. See Decision.counterfactualVariantId. */
|
|
60
|
+
counterfactualVariantId: string | null;
|
|
61
|
+
/** The fallback-ladder step behind the decision (Decision.rung). Absent from older scripts and events. */
|
|
62
|
+
rung?: DecisionRung;
|
|
63
|
+
}
|
|
64
|
+
interface InteractionEvent extends BaseEvent {
|
|
65
|
+
type: "interaction";
|
|
66
|
+
slotId: string;
|
|
67
|
+
variantId: string | null;
|
|
68
|
+
action: "click" | "hover" | "copy" | "expand";
|
|
69
|
+
}
|
|
70
|
+
interface ConversionEvent extends BaseEvent {
|
|
71
|
+
type: "conversion";
|
|
72
|
+
goal: string;
|
|
73
|
+
value?: number;
|
|
74
|
+
/** `server`: confirmed by the site's backend (`POST /api/v1/signups`), not reported by the page. */
|
|
75
|
+
source?: "server";
|
|
76
|
+
}
|
|
77
|
+
type AnalyticsEvent = PageViewEvent | SlotImpressionEvent | InteractionEvent | ConversionEvent;
|
|
78
|
+
type ServerStamped = "siteId" | "visitorId" | "ts" | "companyDomain" | "companySource" | "source";
|
|
79
|
+
type BeaconEvent = {
|
|
80
|
+
[K in AnalyticsEvent["type"]]: Omit<Extract<AnalyticsEvent, {
|
|
81
|
+
type: K;
|
|
82
|
+
}>, ServerStamped>;
|
|
83
|
+
}[AnalyticsEvent["type"]];
|
|
84
|
+
export declare const COLLECT_PATH = "/api/collect";
|
|
85
|
+
/** sessionStorage key for the tab's session id. Removed when a visitor withdraws consent. */
|
|
86
|
+
export declare const SESSION_KEY = "ge_sid";
|
|
87
|
+
/**
|
|
88
|
+
* sessionStorage key for the config pin (set by the client, not the tracker): the tab keeps the copy
|
|
89
|
+
* it started with when someone publishes, until a rollback. Stored and removed with the visitor id.
|
|
90
|
+
*/
|
|
91
|
+
export declare const PIN_KEY = "ge_pin";
|
|
92
|
+
export interface TrackerOptions {
|
|
93
|
+
pageId: string;
|
|
94
|
+
configVersion: number;
|
|
95
|
+
endpoint?: string;
|
|
96
|
+
root?: Document;
|
|
97
|
+
/**
|
|
98
|
+
* The server-signed match token for this page view (hosted: rendered by the page). Sent with every
|
|
99
|
+
* batch of the default envelope so the collector can stamp the matched company. Opaque.
|
|
100
|
+
*/
|
|
101
|
+
match?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Wraps a batch of events into the request body. Defaults to beacon v1, which the same-origin
|
|
104
|
+
* collector stamps with the visitor from its own cookie.
|
|
105
|
+
*/
|
|
106
|
+
envelope?: (events: BeaconEvent[]) => object;
|
|
107
|
+
/**
|
|
108
|
+
* The collector is on another origin (snippet mode). Bodies go as `text/plain` without cookies,
|
|
109
|
+
* so the browser sends them without a CORS preflight.
|
|
110
|
+
*/
|
|
111
|
+
crossOrigin?: boolean;
|
|
112
|
+
}
|
|
113
|
+
export interface Tracker {
|
|
114
|
+
/** Record that a workspace goal fired. Flushes at once, since a conversion often precedes navigation. */
|
|
115
|
+
conversion(goal: string, value?: number): void;
|
|
116
|
+
flush(): void;
|
|
117
|
+
/** Stops measuring. Queued events are sent, unless `discard` (consent was withdrawn). */
|
|
118
|
+
stop(options?: {
|
|
119
|
+
discard?: boolean;
|
|
120
|
+
}): void;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The client half of the measurement pipeline. Sends one page view, one impression per slot once at
|
|
124
|
+
* least half of it is on screen, interactions inside slots, and conversions (forms marked
|
|
125
|
+
* `data-ge-goal`, or `conversion()`). Events are batched and sent with `sendBeacon` so a closing tab
|
|
126
|
+
* still delivers them.
|
|
127
|
+
*/
|
|
128
|
+
export declare function startTracker({ pageId, configVersion, endpoint, root, match, envelope, crossOrigin, }: TrackerOptions): Tracker;
|
|
129
|
+
|
|
130
|
+
export {};
|
package/dist/tracker.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/engine/analytics/touch.ts
|
|
2
|
+
var MAX_TOUCH_VALUE = 100;
|
|
3
|
+
var CLICK_ID_PARAMS = {
|
|
4
|
+
gclid: "google",
|
|
5
|
+
gbraid: "google",
|
|
6
|
+
wbraid: "google",
|
|
7
|
+
msclkid: "microsoft",
|
|
8
|
+
fbclid: "meta",
|
|
9
|
+
li_fat_id: "linkedin",
|
|
10
|
+
ttclid: "tiktok",
|
|
11
|
+
twclid: "x"
|
|
12
|
+
};
|
|
13
|
+
var CLICK_ID_KINDS = new Set(Object.values(CLICK_ID_PARAMS));
|
|
14
|
+
var UTM_FIELDS = ["source", "medium", "campaign", "term", "content"];
|
|
15
|
+
var KHARON_LINK_PARAM = "ge_co";
|
|
16
|
+
function touchFromUrl(url, referrer) {
|
|
17
|
+
const touch = {};
|
|
18
|
+
let page;
|
|
19
|
+
try {
|
|
20
|
+
page = new URL(url);
|
|
21
|
+
} catch {
|
|
22
|
+
return touch;
|
|
23
|
+
}
|
|
24
|
+
for (const field of UTM_FIELDS) {
|
|
25
|
+
const value = clean(page.searchParams.get(`utm_${field}`));
|
|
26
|
+
if (value) touch[field] = value;
|
|
27
|
+
}
|
|
28
|
+
for (const [param, kind] of Object.entries(CLICK_ID_PARAMS)) {
|
|
29
|
+
if (page.searchParams.get(param)) {
|
|
30
|
+
touch.clickId = kind;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (page.searchParams.get(KHARON_LINK_PARAM)) touch.kharonLink = true;
|
|
35
|
+
const from = referrerHost(referrer);
|
|
36
|
+
if (from && from !== bareHost(page.hostname)) touch.referrer = from;
|
|
37
|
+
return touch;
|
|
38
|
+
}
|
|
39
|
+
function clean(value) {
|
|
40
|
+
const trimmed = value?.replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
41
|
+
return trimmed ? trimmed.slice(0, MAX_TOUCH_VALUE) : null;
|
|
42
|
+
}
|
|
43
|
+
function referrerHost(referrer) {
|
|
44
|
+
if (!referrer) return null;
|
|
45
|
+
try {
|
|
46
|
+
const { protocol, hostname } = new URL(referrer);
|
|
47
|
+
return protocol === "http:" || protocol === "https:" ? bareHost(hostname) : null;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function bareHost(hostname) {
|
|
53
|
+
return hostname.toLowerCase().replace(/^www\./, "");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/engine/analytics/beacon.ts
|
|
57
|
+
var BEACON_VERSION = 1;
|
|
58
|
+
var MAX_BEACON_EVENTS = 50;
|
|
59
|
+
var SLOT_ATTRIBUTES = {
|
|
60
|
+
slot: "data-ge-slot",
|
|
61
|
+
variant: "data-ge-variant",
|
|
62
|
+
counterfactual: "data-ge-counterfactual",
|
|
63
|
+
/** Why the slot shows what it shows (matched, default, holdout). For debugging; not sent. */
|
|
64
|
+
reason: "data-ge-reason",
|
|
65
|
+
/** Which step of the fallback ladder chose the content (exact, near, segment, default). Sent on impressions. */
|
|
66
|
+
rung: "data-ge-rung"
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// packages/browser/src/tracker.ts
|
|
70
|
+
var COLLECT_PATH = "/api/collect";
|
|
71
|
+
var SESSION_KEY = "ge_sid";
|
|
72
|
+
var PIN_KEY = "ge_pin";
|
|
73
|
+
var VISIBLE_RATIO = 0.5;
|
|
74
|
+
var FLUSH_INTERVAL_MS = 2e3;
|
|
75
|
+
function startTracker({
|
|
76
|
+
pageId,
|
|
77
|
+
configVersion,
|
|
78
|
+
endpoint = COLLECT_PATH,
|
|
79
|
+
root = document,
|
|
80
|
+
match,
|
|
81
|
+
envelope = (events) => ({ v: BEACON_VERSION, events, ...match ? { m: match } : {} }),
|
|
82
|
+
crossOrigin = false
|
|
83
|
+
}) {
|
|
84
|
+
const win = root.defaultView ?? window;
|
|
85
|
+
const { id: sessionId, started } = sessionFor(win);
|
|
86
|
+
const queue = [];
|
|
87
|
+
const seenSlots = /* @__PURE__ */ new Set();
|
|
88
|
+
const push = (type, body) => {
|
|
89
|
+
queue.push({ type, sessionId, ...body });
|
|
90
|
+
if (queue.length >= MAX_BEACON_EVENTS) flush();
|
|
91
|
+
};
|
|
92
|
+
const flush = () => {
|
|
93
|
+
while (queue.length) send(win, endpoint, envelope(queue.splice(0, MAX_BEACON_EVENTS)), crossOrigin);
|
|
94
|
+
};
|
|
95
|
+
push("page_view", started ? { pageId, configVersion, touch: touchFromUrl(win.location.href, root.referrer) } : { pageId, configVersion });
|
|
96
|
+
const slots = Array.from(root.querySelectorAll(`[${SLOT_ATTRIBUTES.slot}]`));
|
|
97
|
+
const observer = "IntersectionObserver" in win ? new win.IntersectionObserver(
|
|
98
|
+
(entries) => {
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!entry.isIntersecting || entry.intersectionRatio < VISIBLE_RATIO) continue;
|
|
101
|
+
const slot = readSlot(entry.target);
|
|
102
|
+
observer?.unobserve(entry.target);
|
|
103
|
+
if (!slot || seenSlots.has(slot.slotId)) continue;
|
|
104
|
+
seenSlots.add(slot.slotId);
|
|
105
|
+
push("slot_impression", slot);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{ threshold: VISIBLE_RATIO }
|
|
109
|
+
) : null;
|
|
110
|
+
for (const slot of slots) observer?.observe(slot);
|
|
111
|
+
const interaction = (action) => (event) => {
|
|
112
|
+
const slot = closestSlot(event.target);
|
|
113
|
+
if (slot) push("interaction", { slotId: slot.slotId, variantId: slot.variantId, action });
|
|
114
|
+
};
|
|
115
|
+
const onClick = interaction("click");
|
|
116
|
+
const onCopy = interaction("copy");
|
|
117
|
+
const onToggle = (event) => {
|
|
118
|
+
if (event.target.open) interaction("expand")(event);
|
|
119
|
+
};
|
|
120
|
+
const onSubmit = (event) => {
|
|
121
|
+
const goal = event.target.closest?.("form")?.getAttribute("data-ge-goal");
|
|
122
|
+
if (goal) conversion(goal);
|
|
123
|
+
};
|
|
124
|
+
const onHide = () => {
|
|
125
|
+
if (root.visibilityState === "hidden") flush();
|
|
126
|
+
};
|
|
127
|
+
const conversion = (goal, value) => {
|
|
128
|
+
push("conversion", value === void 0 ? { goal } : { goal, value });
|
|
129
|
+
flush();
|
|
130
|
+
};
|
|
131
|
+
root.addEventListener("click", onClick, true);
|
|
132
|
+
root.addEventListener("copy", onCopy, true);
|
|
133
|
+
root.addEventListener("toggle", onToggle, true);
|
|
134
|
+
root.addEventListener("submit", onSubmit, true);
|
|
135
|
+
root.addEventListener("visibilitychange", onHide);
|
|
136
|
+
win.addEventListener("pagehide", flush);
|
|
137
|
+
const timer = win.setInterval(flush, FLUSH_INTERVAL_MS);
|
|
138
|
+
return {
|
|
139
|
+
conversion,
|
|
140
|
+
flush,
|
|
141
|
+
stop({ discard = false } = {}) {
|
|
142
|
+
if (discard) queue.length = 0;
|
|
143
|
+
flush();
|
|
144
|
+
observer?.disconnect();
|
|
145
|
+
win.clearInterval(timer);
|
|
146
|
+
root.removeEventListener("click", onClick, true);
|
|
147
|
+
root.removeEventListener("copy", onCopy, true);
|
|
148
|
+
root.removeEventListener("toggle", onToggle, true);
|
|
149
|
+
root.removeEventListener("submit", onSubmit, true);
|
|
150
|
+
root.removeEventListener("visibilitychange", onHide);
|
|
151
|
+
win.removeEventListener("pagehide", flush);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function readSlot(el) {
|
|
156
|
+
const slotId = el.getAttribute(SLOT_ATTRIBUTES.slot);
|
|
157
|
+
if (!slotId) return null;
|
|
158
|
+
return {
|
|
159
|
+
slotId,
|
|
160
|
+
variantId: el.getAttribute(SLOT_ATTRIBUTES.variant) || null,
|
|
161
|
+
counterfactualVariantId: el.getAttribute(SLOT_ATTRIBUTES.counterfactual) || null,
|
|
162
|
+
...el.getAttribute(SLOT_ATTRIBUTES.rung) ? { rung: el.getAttribute(SLOT_ATTRIBUTES.rung) } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function closestSlot(target) {
|
|
166
|
+
const el = target instanceof Element ? target.closest(`[${SLOT_ATTRIBUTES.slot}]`) : null;
|
|
167
|
+
return el ? readSlot(el) : null;
|
|
168
|
+
}
|
|
169
|
+
function sessionFor(win) {
|
|
170
|
+
try {
|
|
171
|
+
const existing = win.sessionStorage.getItem(SESSION_KEY);
|
|
172
|
+
if (existing) return { id: existing, started: false };
|
|
173
|
+
const id = uuid(win);
|
|
174
|
+
win.sessionStorage.setItem(SESSION_KEY, id);
|
|
175
|
+
return { id, started: true };
|
|
176
|
+
} catch {
|
|
177
|
+
return { id: uuid(win), started: true };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function uuid(win) {
|
|
181
|
+
if (typeof win.crypto.randomUUID === "function") return win.crypto.randomUUID();
|
|
182
|
+
const b = win.crypto.getRandomValues(new Uint8Array(16));
|
|
183
|
+
b[6] = b[6] & 15 | 64;
|
|
184
|
+
b[8] = b[8] & 63 | 128;
|
|
185
|
+
const hex = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
186
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
187
|
+
}
|
|
188
|
+
function send(win, endpoint, payload, crossOrigin) {
|
|
189
|
+
const body = JSON.stringify(payload);
|
|
190
|
+
const type = crossOrigin ? "text/plain" : "application/json";
|
|
191
|
+
if (win.navigator.sendBeacon?.(endpoint, new Blob([body], { type }))) return;
|
|
192
|
+
void win.fetch(endpoint, {
|
|
193
|
+
method: "POST",
|
|
194
|
+
body,
|
|
195
|
+
keepalive: true,
|
|
196
|
+
credentials: crossOrigin ? "omit" : "same-origin",
|
|
197
|
+
headers: { "content-type": type }
|
|
198
|
+
}).catch((error) => console.warn("[ge] analytics beacon failed", error));
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
COLLECT_PATH,
|
|
202
|
+
PIN_KEY,
|
|
203
|
+
SESSION_KEY,
|
|
204
|
+
startTracker
|
|
205
|
+
};
|
package/dist/v1.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(()=>{var be=Object.defineProperty;var Oe=(e,t)=>{for(var n in t)be(e,n,{get:t[n],enumerable:!0})};var ie={};Oe(ie,{ANTI_FLICKER_SNIPPET:()=>Ee,DEFAULT_API_BASE:()=>Q,DEFAULT_TIMEOUT_MS:()=>F,VISITOR_COOKIE:()=>E,antiFlickerStart:()=>j,applyDecision:()=>ne,conversion:()=>et,createClient:()=>q,hashSlotDefaults:()=>T,init:()=>re,isWritable:()=>oe,pageIdFor:()=>ee,parseResolveResponse:()=>$,refresh:()=>nt,reveal:()=>D,scanSlots:()=>W,setConsent:()=>Qe,signup:()=>tt});var G="signup";var X={gclid:"google",gbraid:"google",wbraid:"google",msclkid:"microsoft",fbclid:"meta",li_fat_id:"linkedin",ttclid:"tiktok",twclid:"x"},st=new Set(Object.values(X)),Ve=["source","medium","campaign","term","content"],Le="ge_co";function Y(e,t){let n={},o;try{o=new URL(e)}catch{return n}for(let i of Ve){let a=we(o.searchParams.get(`utm_${i}`));a&&(n[i]=a)}for(let[i,a]of Object.entries(X))if(o.searchParams.get(i)){n.clickId=a;break}o.searchParams.get(Le)&&(n.kharonLink=!0);let r=De(t);return r&&r!==se(o.hostname)&&(n.referrer=r),n}function we(e){let t=e?.replace(/[\u0000-\u001f\u007f]/g,"").trim();return t?t.slice(0,100):null}function De(e){if(!e)return null;try{let{protocol:t,hostname:n}=new URL(e);return t==="http:"||t==="https:"?se(n):null}catch{return null}}function se(e){return e.toLowerCase().replace(/^www\./,"")}var Z=1,B=50;var p={slot:"data-ge-slot",variant:"data-ge-variant",counterfactual:"data-ge-counterfactual",reason:"data-ge-reason",rung:"data-ge-rung"};var Me="/api/collect",U="ge_sid",K="ge_pin",ae=.5,Be=2e3;function le({pageId:e,configVersion:t,endpoint:n=Me,root:o=document,match:r,envelope:i=c=>({v:Z,events:c,...r?{m:r}:{}}),crossOrigin:a=!1}){let c=o.defaultView??window,{id:f,started:d}=Ke(c),m=[],b=new Set,R=(l,u)=>{m.push({type:l,sessionId:f,...u}),m.length>=B&&s()},s=()=>{for(;m.length;)$e(c,n,i(m.splice(0,B)),a)};R("page_view",d?{pageId:e,configVersion:t,touch:Y(c.location.href,o.referrer)}:{pageId:e,configVersion:t});let g=Array.from(o.querySelectorAll(`[${p.slot}]`)),h="IntersectionObserver"in c?new c.IntersectionObserver(l=>{for(let u of l){if(!u.isIntersecting||u.intersectionRatio<ae)continue;let v=ue(u.target);h?.unobserve(u.target),!(!v||b.has(v.slotId))&&(b.add(v.slotId),R("slot_impression",v))}},{threshold:ae}):null;for(let l of g)h?.observe(l);let x=l=>u=>{let v=Ue(u.target);v&&R("interaction",{slotId:v.slotId,variantId:v.variantId,action:l})},_=x("click"),N=x("copy"),O=l=>{l.target.open&&x("expand")(l)},C=l=>{let u=l.target.closest?.("form")?.getAttribute("data-ge-goal");u&&S(u)},V=()=>{o.visibilityState==="hidden"&&s()},S=(l,u)=>{R("conversion",u===void 0?{goal:l}:{goal:l,value:u}),s()};o.addEventListener("click",_,!0),o.addEventListener("copy",N,!0),o.addEventListener("toggle",O,!0),o.addEventListener("submit",C,!0),o.addEventListener("visibilitychange",V),c.addEventListener("pagehide",s);let M=c.setInterval(s,Be);return{conversion:S,flush:s,stop({discard:l=!1}={}){l&&(m.length=0),s(),h?.disconnect(),c.clearInterval(M),o.removeEventListener("click",_,!0),o.removeEventListener("copy",N,!0),o.removeEventListener("toggle",O,!0),o.removeEventListener("submit",C,!0),o.removeEventListener("visibilitychange",V),c.removeEventListener("pagehide",s)}}}function ue(e){let t=e.getAttribute(p.slot);return t?{slotId:t,variantId:e.getAttribute(p.variant)||null,counterfactualVariantId:e.getAttribute(p.counterfactual)||null,...e.getAttribute(p.rung)?{rung:e.getAttribute(p.rung)}:{}}:null}function Ue(e){let t=e instanceof Element?e.closest(`[${p.slot}]`):null;return t?ue(t):null}function Ke(e){try{let t=e.sessionStorage.getItem(U);if(t)return{id:t,started:!1};let n=ce(e);return e.sessionStorage.setItem(U,n),{id:n,started:!0}}catch{return{id:ce(e),started:!0}}}function ce(e){if(typeof e.crypto.randomUUID=="function")return e.crypto.randomUUID();let t=e.crypto.getRandomValues(new Uint8Array(16));t[6]=t[6]&15|64,t[8]=t[8]&63|128;let n=Array.from(t,o=>o.toString(16).padStart(2,"0")).join("");return`${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`}function $e(e,t,n,o){let r=JSON.stringify(n),i=o?"text/plain":"application/json";e.navigator.sendBeacon?.(t,new Blob([r],{type:i}))||e.fetch(t,{method:"POST",body:r,keepalive:!0,credentials:o?"omit":"same-origin",headers:{"content-type":i}}).catch(a=>console.warn("[ge] analytics beacon failed",a))}var y="ge_preview";var pe=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;function A(e){return typeof e=="string"&&pe.test(e)}function T(e){let t=JSON.stringify(Object.keys(e).sort().map(o=>[o,e[o]])),n=2166136261;for(let o=0;o<t.length;o++)n^=t.charCodeAt(o),n=Math.imul(n,16777619);return(n>>>0).toString(16).padStart(8,"0")}var de="/api/v1/resolve",ge="/api/v1/collect",He=["matched","default","holdout"],Fe=["exact","near","segment","default"];function $(e){if(!L(e)||e.v!==1||!L(e.decisions)||!Number.isSafeInteger(e.configVersion)||typeof e.persist!="boolean")return null;let t=A(e.vid)?e.vid:void 0,n=e.persist&&t!==void 0,o=n&&typeof e.pin=="string"&&/^\d{1,9}\.\d{1,9}$/.test(e.pin)?e.pin:void 0,r={};for(let[a,c]of Object.entries(e.decisions)){let f=qe(c);f&&(r[a]=f)}let i=n&&typeof e.m=="string"&&e.m.length<=1024?e.m:void 0;if(e.preview===!0){let a=L(e.explain)?e.explain:void 0;return{v:1,configVersion:e.configVersion,persist:!1,preview:!0,decisions:r,...a?{explain:a}:{}}}return{v:1,configVersion:e.configVersion,persist:n,vid:t,...o?{pin:o}:{},decisions:r,...i?{m:i}:{}}}function qe(e){if(!L(e)||!L(e.content)||!fe(e.variantId)||!fe(e.counterfactualVariantId)||!He.includes(e.reason))return null;let t={};for(let[o,r]of Object.entries(e.content))typeof r=="string"&&(t[o]=r);let n=Fe.includes(e.rung)?e.rung:void 0;return{variantId:e.variantId,counterfactualVariantId:e.counterfactualVariantId,reason:e.reason,...n&&{rung:n},content:t}}function L(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function fe(e){return e===null||typeof e=="string"}var Q="https://kharonlabs.com",F=500,ze=5e3,E="ge_vid",We=365*24*60*60;function q(e){let t=e.root??document,n=t.defaultView??window,o=(e.apiBase??Q).replace(/\/+$/,""),r=new Set,i=e.consent??"unknown",a=!1,c,f=null,d=!1,m=(s=!1)=>{f?.stop({discard:s}),f=null},b=()=>{a=!1,c=void 0,m(!0),Ye(t,E,e.cookieDomain);try{n.sessionStorage.removeItem(U),n.sessionStorage.removeItem(K)}catch{}},R=s=>{if(s.preview){d=!0,m(!0),s.explain&&n.console?.info("[kharon] debug link: why this page shows what it does",s.explain);return}s.persist&&s.vid&&i!=="denied"?(a=!0,c=s.m,H(t,E)!==s.vid&&Xe(t,E,s.vid,e.cookieDomain),s.pin&&me(n,K,s.pin)):(a||H(t,E))&&b()};return{get consent(){return i},async resolve(s,g=Date.now()+ze){let h=H(t,E),x=ve(n,K),_=je(n),N={v:1,key:e.key,...A(h)?{vid:h}:{},...x?{pin:x}:{},url:_.url,referrer:t.referrer,slots:s.map(S=>({id:S.id,h:T(S.defaults)})),consent:i,gpc:n.navigator.globalPrivacyControl===!0},O=g-Date.now();if(O<=0||typeof n.fetch!="function")return null;let C=typeof AbortController=="function"?new AbortController:null,V;try{let S=n.fetch(o+de,{method:"POST",body:JSON.stringify(N),credentials:"omit",signal:C?.signal}),M=new Promise(v=>{V=setTimeout(()=>{C?.abort(),v(null)},O)}),l=await Promise.race([S,M]);if(!l)return null;if(!l.ok)return console.warn(`[kharon] resolve answered ${l.status}; keeping defaults`),null;let u=$(await Promise.race([l.json(),M]));return!u||Date.now()>g?null:(u.preview&&_.token?me(n,y,_.token):!u.preview&&_.token&&Ge(n,y),R(u),u)}catch(S){return C?.signal.aborted||console.warn("[kharon] resolve failed; keeping defaults",S),null}finally{clearTimeout(V)}},adopt:R,track(s){m();let g=H(t,E);d||!a||!A(g)||(f=le({pageId:e.pageId??ee(n.location.pathname),configVersion:s,endpoint:o+ge,root:t,crossOrigin:!0,envelope:h=>({v:2,key:e.key,vid:g,consent:i,events:h,...c?{m:c}:{}})}))},setConsent(s){if(s!==i){i=s,s==="denied"&&b();for(let g of r)g(s)}},onConsentChange(s){return r.add(s),()=>r.delete(s)},conversion(s,g){f?.conversion(s,g)},stop:()=>m()}}function ee(e){return e.split("/").map(n=>n.replace(/[^A-Za-z0-9_:-]/g,"")).filter(Boolean).join(".").slice(0,128)||"home"}function H(e,t){for(let n of e.cookie.split(";")){let[o,...r]=n.trim().split("=");if(o===t)try{return decodeURIComponent(r.join("="))}catch{return}}}function ve(e,t){try{return e.sessionStorage.getItem(t)}catch{return null}}function je(e){let t=e.location.href,n;try{n=new URL(t)}catch{return{url:t,token:null}}let o=n.searchParams.get(y);if(o)return{url:t,token:o};let r=ve(e,y);return r?(n.searchParams.set(y,r),{url:n.toString(),token:r}):{url:t,token:null}}function Ge(e,t){try{e.sessionStorage.removeItem(t)}catch{}}function me(e,t,n){try{e.sessionStorage.setItem(t,n)}catch{}}function Ie(e,t){let n=e.location?.protocol==="https:"?"; Secure":"";return`; Path=/; SameSite=Lax${t?`; Domain=${t}`:""}${n}`}function Xe(e,t,n,o){e.cookie=`${t}=${encodeURIComponent(n)}; Max-Age=${We}${Ie(e,o)}`}function Ye(e,t,n){e.cookie=`${t}=; Max-Age=0${Ie(e,n)}`}var Se="data-ge-field",he="data-ge-attr",te="kharon-af",Ee=`<style id="${te}">[${p.slot}]{visibility:hidden!important}</style><script>window.__kharonAf=Date.now();setTimeout(function(){var s=document.getElementById("${te}");s&&s.parentNode.removeChild(s)},500)<\/script>`,Ze=new Set(["href","src","action","formaction","poster","cite","xlink:href"]),Je=/^(on|style$|srcdoc$|srcset$)/,z=new WeakMap,w="#text";function Re(e,t){return t===null?(e.textContent??"").trim():e.getAttribute(t)}function _e(e,t){let n=z.get(e);return n?.has(t??w)?n.get(t??w):Re(e,t)}function ye(e,t,n){let o=z.get(e);o||z.set(e,o=new Map),o.has(t??w)||o.set(t??w,Re(e,t)),t===null?e.textContent=n??"":n===null?e.removeAttribute(t):e.setAttribute(t,n)}function W(e){let t=new Map;for(let n of ke(e)){let o=n.getAttribute(p.slot);if(t.has(o))continue;let r={};for(let i of xe(n))i.field in r||(r[i.field]=_e(i.el,i.attr)??"");t.set(o,{id:o,defaults:r})}return[...t.values()]}function Ae(e,t){for(let n of ke(e)){let o=t.decisions[n.getAttribute(p.slot)];o&&ne(n,o,e.baseURI)}}function ne(e,t,n){for(let{el:o,field:r,attr:i}of xe(e)){let a=t.content[r];typeof a=="string"&&(i===null||oe(i,a,n))?ye(o,i,a):z.get(o)?.has(i??w)&&ye(o,i,_e(o,i))}e.setAttribute(p.variant,t.variantId??""),e.setAttribute(p.counterfactual,t.counterfactualVariantId??""),e.setAttribute(p.reason,t.reason),t.rung?e.setAttribute(p.rung,t.rung):e.removeAttribute(p.rung)}function D(e){e.getElementById(te)?.remove()}function j(e){let t=e.defaultView?.__kharonAf;return typeof t=="number"?t:void 0}function oe(e,t,n){let o=e.toLowerCase();if(Je.test(o))return!1;if(!Ze.has(o))return!0;try{let{protocol:r}=new URL(t,n);return r==="http:"||r==="https:"}catch{return!1}}function ke(e){return Array.from(e.querySelectorAll(`[${p.slot}]`))}function xe(e){let t=[],n=[e,...Array.from(e.querySelectorAll(`[${Se}],[${he}]`))];for(let o of n){if(o!==e&&o.closest(`[${p.slot}]`)!==e)continue;let r=o.getAttribute(Se);r&&t.push({el:o,field:r,attr:null});for(let i of(o.getAttribute(he)??"").split(",")){let a=i.lastIndexOf(":"),c=i.slice(0,a).trim(),f=i.slice(a+1).trim();a>0&&c&&f&&t.push({el:o,field:f,attr:c})}}return t}var I=null,Ce;function re(e){if(I)return console.warn("[kharon] init called twice; keeping the first instance"),I;let t=e.root??document,n=e.timeoutMs??F,o=q({...e,consent:Ce??e.consent}),r=async f=>{try{let d=await o.resolve(W(t),f);d&&Ae(t,d),D(t),d&&o.track(d.configVersion)}catch(d){D(t),console.warn("[kharon] personalization failed; keeping defaults",d)}},a=new Promise(f=>{t.readyState!=="loading"?f():t.addEventListener("DOMContentLoaded",()=>f(),{once:!0})}).then(()=>r((j(t)??Date.now())+n));o.onConsentChange(f=>{f==="granted"&&(a=a.then(()=>r()))});let c={client:o,get ready(){return a},setConsent:o.setConsent,conversion:o.conversion,refresh(){return a=a.then(()=>r(Date.now()+n)),a},stop(){o.stop(),I===c&&(I=null)}};return I=c,c}function Qe(e){I?I.setConsent(e):Ce=e}function et(e,t){I?.conversion(e,t)}function tt(){I?.conversion(G)}function nt(){return I?.refresh()??Promise.resolve()}var ot=["granted","denied","unknown"],P=document.currentScript;window.kharon=ie;var Te=P?.getAttribute("data-key");P&&Te&&re({key:Te,apiBase:P.getAttribute("data-api")??new URL(P.src).origin,consent:ot.find(e=>e===P.getAttribute("data-consent"))});})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kharon-labs/browser",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Personalize slots on any website with Kharon Labs: swaps in the variant for each visitor and measures lift against a holdout.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://kharonlabs.com",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./tracker": {
|
|
19
|
+
"types": "./dist/tracker.d.ts",
|
|
20
|
+
"import": "./dist/tracker.js",
|
|
21
|
+
"require": "./dist/tracker.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": ["dist"],
|
|
25
|
+
"browserslist": ["defaults"],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|