@kywi-software/js 0.24.0 → 0.26.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/README.md +10 -5
- package/dist/audience/adapter-runner.d.ts.map +1 -1
- package/dist/audience/index.d.ts.map +1 -1
- package/dist/audience/preview.d.ts +20 -0
- package/dist/audience/preview.d.ts.map +1 -1
- package/dist/audience/selfid-api.d.ts.map +1 -1
- package/dist/audience/selfid-collector.d.ts +9 -1
- package/dist/audience/selfid-collector.d.ts.map +1 -1
- package/dist/audience/selfid-submit.d.ts +13 -1
- package/dist/audience/selfid-submit.d.ts.map +1 -1
- package/dist/audience/selfid-widget.d.ts +20 -0
- package/dist/audience/selfid-widget.d.ts.map +1 -1
- package/dist/audience/transparency.d.ts.map +1 -1
- package/dist/consent.d.ts +24 -0
- package/dist/consent.d.ts.map +1 -1
- package/dist/cookies.d.ts +1 -0
- package/dist/cookies.d.ts.map +1 -1
- package/dist/entity.d.ts.map +1 -1
- package/dist/experiments/index.d.ts.map +1 -1
- package/dist/experiments/pending.d.ts +54 -0
- package/dist/experiments/pending.d.ts.map +1 -0
- package/dist/feed.d.ts.map +1 -1
- package/dist/form-hooks.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/kywi.esm.js +340 -135
- package/dist/kywi.esm.js.map +4 -4
- package/dist/kywi.js +340 -135
- package/dist/kywi.js.map +4 -4
- package/dist/site-scope.d.ts +18 -0
- package/dist/site-scope.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/kywi.esm.js
CHANGED
|
@@ -15,6 +15,26 @@ function readContext() {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// src/site-scope.ts
|
|
19
|
+
function currentSiteId() {
|
|
20
|
+
if (typeof document === "undefined") return "";
|
|
21
|
+
return document.querySelector('meta[name="kywi:site-id"]')?.getAttribute("content") || "";
|
|
22
|
+
}
|
|
23
|
+
function withSiteId(url) {
|
|
24
|
+
if (!isSameOrigin(url)) return url;
|
|
25
|
+
const siteId = currentSiteId();
|
|
26
|
+
if (!siteId || /[?&]siteId=/.test(url)) return url;
|
|
27
|
+
const [path, hash] = url.split("#", 2);
|
|
28
|
+
const joined = `${path}${path.includes("?") ? "&" : "?"}siteId=${encodeURIComponent(siteId)}`;
|
|
29
|
+
return hash === void 0 ? joined : `${joined}#${hash}`;
|
|
30
|
+
}
|
|
31
|
+
function isSameOrigin(url) {
|
|
32
|
+
if (url.startsWith("/")) return !url.startsWith("//");
|
|
33
|
+
const origin = typeof window !== "undefined" ? window.location?.origin : void 0;
|
|
34
|
+
if (!origin || origin === "null") return false;
|
|
35
|
+
return url === origin || url.startsWith(`${origin}/`);
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
// src/entity.ts
|
|
19
39
|
function createEntity(type, basePath, csrfToken, data) {
|
|
20
40
|
const fields = { ...data };
|
|
@@ -34,7 +54,7 @@ function createEntity(type, basePath, csrfToken, data) {
|
|
|
34
54
|
async save() {
|
|
35
55
|
const id = fields["id"];
|
|
36
56
|
const url = id ? `${basePath}/content/${type}/${id}` : `${basePath}/content/${type}`;
|
|
37
|
-
const res = await fetch(url, { method: id ? "PUT" : "POST", headers, body: JSON.stringify(Object.fromEntries(dirty)) });
|
|
57
|
+
const res = await fetch(withSiteId(url), { method: id ? "PUT" : "POST", headers, body: JSON.stringify(Object.fromEntries(dirty)) });
|
|
38
58
|
const json = await res.json();
|
|
39
59
|
if (!res.ok || json.error) {
|
|
40
60
|
errors.push(json.error?.message ?? "Save failed");
|
|
@@ -46,14 +66,14 @@ function createEntity(type, basePath, csrfToken, data) {
|
|
|
46
66
|
},
|
|
47
67
|
async delete() {
|
|
48
68
|
const id = fields["id"];
|
|
49
|
-
await fetch(`${basePath}/content/${type}/${id}
|
|
69
|
+
await fetch(withSiteId(`${basePath}/content/${type}/${id}`), { method: "DELETE", headers });
|
|
50
70
|
},
|
|
51
71
|
getErrors() {
|
|
52
72
|
return [...errors];
|
|
53
73
|
},
|
|
54
74
|
async loadBy(field, value) {
|
|
55
75
|
const url = field === "id" ? `${basePath}/content/${type}/${value}` : `${basePath}/content/${type}?${field}=${encodeURIComponent(value)}`;
|
|
56
|
-
const res = await fetch(url, { headers });
|
|
76
|
+
const res = await fetch(withSiteId(url), { headers });
|
|
57
77
|
const json = await res.json();
|
|
58
78
|
if (!res.ok || json.error) {
|
|
59
79
|
errors.push(json.error?.message ?? "Load failed");
|
|
@@ -172,7 +192,7 @@ var FeedBuilder = class {
|
|
|
172
192
|
const agg = { function: fn };
|
|
173
193
|
if (field) agg["field"] = field;
|
|
174
194
|
body["aggregate"] = agg;
|
|
175
|
-
const res = await fetch(`${this.basePath}/feeds/aggregate
|
|
195
|
+
const res = await fetch(withSiteId(`${this.basePath}/feeds/aggregate`), {
|
|
176
196
|
method: "POST",
|
|
177
197
|
headers: this.buildHeaders(),
|
|
178
198
|
body: JSON.stringify(body)
|
|
@@ -182,7 +202,7 @@ var FeedBuilder = class {
|
|
|
182
202
|
}
|
|
183
203
|
async getQuery() {
|
|
184
204
|
const body = this.buildBody();
|
|
185
|
-
const res = await fetch(`${this.basePath}/feeds/query
|
|
205
|
+
const res = await fetch(withSiteId(`${this.basePath}/feeds/query`), {
|
|
186
206
|
method: "POST",
|
|
187
207
|
headers: this.buildHeaders(),
|
|
188
208
|
body: JSON.stringify(body)
|
|
@@ -471,6 +491,248 @@ function initNavMenus(root = document) {
|
|
|
471
491
|
};
|
|
472
492
|
}
|
|
473
493
|
|
|
494
|
+
// src/cookies.ts
|
|
495
|
+
var COOKIE_NAMES = {
|
|
496
|
+
SIGNALS: "kywi_signals",
|
|
497
|
+
VISITOR: "kywi_visitor",
|
|
498
|
+
UTM: "kywi_utm",
|
|
499
|
+
AUDIENCE: "kywi_audience",
|
|
500
|
+
OPTOUT: "kywi_optout",
|
|
501
|
+
KNOWN: "kywi_known",
|
|
502
|
+
PREVIEW_INIT: "kywi_preview_init",
|
|
503
|
+
BUCKET: "kywi_bucket"
|
|
504
|
+
};
|
|
505
|
+
function readCookies() {
|
|
506
|
+
const map = /* @__PURE__ */ new Map();
|
|
507
|
+
if (typeof document === "undefined") return map;
|
|
508
|
+
for (const pair of document.cookie.split(";")) {
|
|
509
|
+
const trimmed = pair.trim();
|
|
510
|
+
const eqIdx = trimmed.indexOf("=");
|
|
511
|
+
if (eqIdx < 0) continue;
|
|
512
|
+
map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
|
|
513
|
+
}
|
|
514
|
+
return map;
|
|
515
|
+
}
|
|
516
|
+
function setCookie(name, value, maxAge) {
|
|
517
|
+
let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
518
|
+
if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
|
|
519
|
+
document.cookie = str;
|
|
520
|
+
}
|
|
521
|
+
function deleteCookie(name) {
|
|
522
|
+
document.cookie = `${name}=; Path=/; Max-Age=0`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/consent.ts
|
|
526
|
+
var CONSENT_COOKIE = "kywi_consent";
|
|
527
|
+
var CONSENT_EVENT = "kywi:consent";
|
|
528
|
+
var CONSENT_REOPEN_EVENT = "kywi:consent-reopen";
|
|
529
|
+
function reopenConsent() {
|
|
530
|
+
if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
|
|
531
|
+
window.dispatchEvent(new CustomEvent(CONSENT_REOPEN_EVENT));
|
|
532
|
+
}
|
|
533
|
+
var AUDIENCE_PIN_COOKIE = "kywi_audience";
|
|
534
|
+
var SWITCHER_PIN_PREFIX = "switcher:";
|
|
535
|
+
var COOKIE_CONSENT_CATEGORY = {
|
|
536
|
+
kywi_consent: "essential",
|
|
537
|
+
kywi_optout: "essential",
|
|
538
|
+
kywi_preview_init: "essential",
|
|
539
|
+
kywi_bucket: "essential",
|
|
540
|
+
kywi_entry: "essential",
|
|
541
|
+
kywi_utm_visit: "essential",
|
|
542
|
+
kywi_selfid: "essential",
|
|
543
|
+
kywi_signals: "personalization",
|
|
544
|
+
kywi_visitor: "personalization",
|
|
545
|
+
kywi_utm: "personalization",
|
|
546
|
+
kywi_audience: "personalization",
|
|
547
|
+
kywi_known: "personalization"
|
|
548
|
+
};
|
|
549
|
+
function getConsentState() {
|
|
550
|
+
const cookies = readCookies();
|
|
551
|
+
const raw = cookies.get(CONSENT_COOKIE);
|
|
552
|
+
if (!raw) return null;
|
|
553
|
+
let parsed;
|
|
554
|
+
try {
|
|
555
|
+
parsed = JSON.parse(raw);
|
|
556
|
+
} catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
560
|
+
const obj = parsed;
|
|
561
|
+
return {
|
|
562
|
+
essential: true,
|
|
563
|
+
analytics: obj.analytics === true,
|
|
564
|
+
marketing: obj.marketing === true,
|
|
565
|
+
personalization: obj.personalization === true
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function setConsent(prefs) {
|
|
569
|
+
const state = {
|
|
570
|
+
essential: true,
|
|
571
|
+
analytics: prefs.analytics ?? false,
|
|
572
|
+
marketing: prefs.marketing ?? false,
|
|
573
|
+
personalization: prefs.personalization ?? false
|
|
574
|
+
};
|
|
575
|
+
setCookie(CONSENT_COOKIE, JSON.stringify(state), 31536e3);
|
|
576
|
+
clearDeniedCookies(state);
|
|
577
|
+
announceConsent(state);
|
|
578
|
+
}
|
|
579
|
+
function announceConsent(state) {
|
|
580
|
+
if (typeof window === "undefined" || typeof CustomEvent === "undefined") return;
|
|
581
|
+
try {
|
|
582
|
+
window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: state }));
|
|
583
|
+
} catch {
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function clearDeniedCookies(state) {
|
|
587
|
+
if (typeof document === "undefined") return [];
|
|
588
|
+
const cookies = readCookies();
|
|
589
|
+
const deleted = [];
|
|
590
|
+
for (const [name, category] of Object.entries(COOKIE_CONSENT_CATEGORY)) {
|
|
591
|
+
if (category === "essential") continue;
|
|
592
|
+
if (state && state[category] === true) continue;
|
|
593
|
+
if (isDeliberatePreference(name, cookies.get(name))) continue;
|
|
594
|
+
deleteCookie(name);
|
|
595
|
+
deleted.push(name);
|
|
596
|
+
}
|
|
597
|
+
return deleted;
|
|
598
|
+
}
|
|
599
|
+
function isDeliberatePreference(name, value) {
|
|
600
|
+
if (name !== AUDIENCE_PIN_COOKIE || value === void 0) return false;
|
|
601
|
+
return value.startsWith(SWITCHER_PIN_PREFIX) && value.length > SWITCHER_PIN_PREFIX.length;
|
|
602
|
+
}
|
|
603
|
+
function hasConsent(category, options = {}) {
|
|
604
|
+
if (options.requireConsent === false) return true;
|
|
605
|
+
if (category === "essential") return true;
|
|
606
|
+
const state = getConsentState();
|
|
607
|
+
if (!state) return false;
|
|
608
|
+
return state[category] === true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/experiments/pending.ts
|
|
612
|
+
var PENDING_EVENTS_META = "kywi:pending-events";
|
|
613
|
+
var PENDING_STORAGE_KEY = "kywi:pending-events";
|
|
614
|
+
var EMPTY = () => ({ exposures: [], matched: [] });
|
|
615
|
+
var memory = EMPTY();
|
|
616
|
+
var consentListener = null;
|
|
617
|
+
function hasValidSiteId(o) {
|
|
618
|
+
return o.siteId === void 0 || typeof o.siteId === "string" && o.siteId.length > 0;
|
|
619
|
+
}
|
|
620
|
+
function isExposure(v) {
|
|
621
|
+
if (typeof v !== "object" || v === null) return false;
|
|
622
|
+
const o = v;
|
|
623
|
+
return typeof o.experimentId === "string" && o.experimentId.length > 0 && (typeof o.variantKey === "string" || o.variantKey === null) && typeof o.at === "number" && hasValidSiteId(o);
|
|
624
|
+
}
|
|
625
|
+
function isMatch(v) {
|
|
626
|
+
if (typeof v !== "object" || v === null) return false;
|
|
627
|
+
const o = v;
|
|
628
|
+
return typeof o.audienceId === "string" && o.audienceId.length > 0 && typeof o.at === "number" && typeof o.sig === "string" && o.sig.length > 0 && hasValidSiteId(o);
|
|
629
|
+
}
|
|
630
|
+
function parsePending(raw) {
|
|
631
|
+
if (!raw) return EMPTY();
|
|
632
|
+
try {
|
|
633
|
+
const parsed = JSON.parse(raw);
|
|
634
|
+
const exposures = Array.isArray(parsed?.exposures) ? parsed.exposures.filter(isExposure) : [];
|
|
635
|
+
const matched = Array.isArray(parsed?.matched) ? parsed.matched.filter(isMatch) : [];
|
|
636
|
+
return { exposures, matched };
|
|
637
|
+
} catch {
|
|
638
|
+
return EMPTY();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function readStorage() {
|
|
642
|
+
try {
|
|
643
|
+
return parsePending(sessionStorage.getItem(PENDING_STORAGE_KEY));
|
|
644
|
+
} catch {
|
|
645
|
+
return EMPTY();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
function writeStorage(events) {
|
|
649
|
+
try {
|
|
650
|
+
if (isEmpty(events)) sessionStorage.removeItem(PENDING_STORAGE_KEY);
|
|
651
|
+
else sessionStorage.setItem(PENDING_STORAGE_KEY, JSON.stringify(events));
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function isEmpty(events) {
|
|
656
|
+
return events.exposures.length === 0 && events.matched.length === 0;
|
|
657
|
+
}
|
|
658
|
+
function merge(into, from) {
|
|
659
|
+
const expKey = (e) => `${e.siteId ?? ""}|${e.experimentId}`;
|
|
660
|
+
const audKey = (m) => `${m.siteId ?? ""}|${m.audienceId}`;
|
|
661
|
+
const byExperiment = new Map(into.exposures.map((e) => [expKey(e), e]));
|
|
662
|
+
for (const e of from.exposures) if (!byExperiment.has(expKey(e))) byExperiment.set(expKey(e), e);
|
|
663
|
+
const byAudience = new Map(into.matched.map((m) => [audKey(m), m]));
|
|
664
|
+
for (const m of from.matched) if (!byAudience.has(audKey(m))) byAudience.set(audKey(m), m);
|
|
665
|
+
return { exposures: [...byExperiment.values()], matched: [...byAudience.values()] };
|
|
666
|
+
}
|
|
667
|
+
function stampSite(events) {
|
|
668
|
+
const siteId = currentSiteId();
|
|
669
|
+
if (!siteId) return events;
|
|
670
|
+
return {
|
|
671
|
+
exposures: events.exposures.map((e) => e.siteId ? e : { ...e, siteId }),
|
|
672
|
+
matched: events.matched.map((m) => m.siteId ? m : { ...m, siteId })
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
function pendingEvents() {
|
|
676
|
+
return merge(memory, readStorage());
|
|
677
|
+
}
|
|
678
|
+
function queuePendingEvents(events) {
|
|
679
|
+
memory = merge(pendingEvents(), stampSite(events));
|
|
680
|
+
writeStorage(memory);
|
|
681
|
+
}
|
|
682
|
+
function clearQueue() {
|
|
683
|
+
memory = EMPTY();
|
|
684
|
+
writeStorage(memory);
|
|
685
|
+
}
|
|
686
|
+
function urlForSite(url, siteId) {
|
|
687
|
+
if (!siteId) return withSiteId(url);
|
|
688
|
+
const origin = typeof window !== "undefined" ? window.location?.origin : void 0;
|
|
689
|
+
const sameOrigin = url.startsWith("/") && !url.startsWith("//") || !!origin && origin !== "null" && (url === origin || url.startsWith(`${origin}/`));
|
|
690
|
+
if (!sameOrigin || /[?&]siteId=/.test(url)) return url;
|
|
691
|
+
return `${url}${url.includes("?") ? "&" : "?"}siteId=${encodeURIComponent(siteId)}`;
|
|
692
|
+
}
|
|
693
|
+
function post(url, siteId, body, csrfToken) {
|
|
694
|
+
try {
|
|
695
|
+
void fetch(urlForSite(url, siteId), {
|
|
696
|
+
method: "POST",
|
|
697
|
+
headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
|
|
698
|
+
body: JSON.stringify(body),
|
|
699
|
+
keepalive: true
|
|
700
|
+
}).catch(() => {
|
|
701
|
+
});
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function flushPendingEvents(config) {
|
|
706
|
+
const gate = config.requireConsent === void 0 ? {} : { requireConsent: config.requireConsent };
|
|
707
|
+
const queue = pendingEvents();
|
|
708
|
+
if (isEmpty(queue)) return 0;
|
|
709
|
+
if (!hasConsent("personalization", gate) || !hasConsent("analytics", gate)) {
|
|
710
|
+
if (getConsentState() !== null) clearQueue();
|
|
711
|
+
return 0;
|
|
712
|
+
}
|
|
713
|
+
for (const e of queue.exposures) {
|
|
714
|
+
post(`${config.apiBase}/kywi/experiments/exposure`, e.siteId, { experimentId: e.experimentId, at: e.at }, config.csrfToken);
|
|
715
|
+
}
|
|
716
|
+
for (const m of queue.matched) {
|
|
717
|
+
post(`${config.apiBase}/kywi/analytics/matched`, m.siteId, { audienceId: m.audienceId, at: m.at, sig: m.sig }, config.csrfToken);
|
|
718
|
+
}
|
|
719
|
+
clearQueue();
|
|
720
|
+
return queue.exposures.length + queue.matched.length;
|
|
721
|
+
}
|
|
722
|
+
function bootPendingEvents(config) {
|
|
723
|
+
if (typeof document !== "undefined") {
|
|
724
|
+
const raw = document.querySelector(`meta[name="${PENDING_EVENTS_META}"]`)?.getAttribute("content");
|
|
725
|
+
queuePendingEvents(parsePending(raw));
|
|
726
|
+
}
|
|
727
|
+
flushPendingEvents(config);
|
|
728
|
+
if (typeof window === "undefined") return;
|
|
729
|
+
if (consentListener) window.removeEventListener(CONSENT_EVENT, consentListener);
|
|
730
|
+
consentListener = () => {
|
|
731
|
+
flushPendingEvents(config);
|
|
732
|
+
};
|
|
733
|
+
window.addEventListener(CONSENT_EVENT, consentListener);
|
|
734
|
+
}
|
|
735
|
+
|
|
474
736
|
// src/experiments/index.ts
|
|
475
737
|
function apiBaseFrom(ctx) {
|
|
476
738
|
return ctx.basePath && ctx.basePath !== "/" ? `${ctx.basePath}/api/v1` : "/api/v1";
|
|
@@ -497,7 +759,7 @@ function installFormListener() {
|
|
|
497
759
|
}
|
|
498
760
|
function beacon(url, body, csrfToken) {
|
|
499
761
|
try {
|
|
500
|
-
void fetch(url, {
|
|
762
|
+
void fetch(withSiteId(url), {
|
|
501
763
|
method: "POST",
|
|
502
764
|
headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
|
|
503
765
|
body: JSON.stringify(body),
|
|
@@ -523,6 +785,7 @@ function bootExperiments(config = {}) {
|
|
|
523
785
|
if (experimentId) {
|
|
524
786
|
beacon(`${apiBase}/kywi/experiments/exposure`, { experimentId, visitorId }, csrf);
|
|
525
787
|
}
|
|
788
|
+
bootPendingEvents({ apiBase, ...csrf ? { csrfToken: csrf } : {} });
|
|
526
789
|
if (config.autoPageview !== false) {
|
|
527
790
|
trackConversion({ type: "pageview", url: currentPath });
|
|
528
791
|
}
|
|
@@ -539,7 +802,7 @@ function bootExperiments(config = {}) {
|
|
|
539
802
|
// src/audience/selfid-collector.ts
|
|
540
803
|
var SELFID_KEY = "kywi_selfid";
|
|
541
804
|
function collectSelfId() {
|
|
542
|
-
const raw =
|
|
805
|
+
const raw = readStorage2(SELFID_KEY);
|
|
543
806
|
if (!raw) return {};
|
|
544
807
|
try {
|
|
545
808
|
const parsed = JSON.parse(raw);
|
|
@@ -549,7 +812,9 @@ function collectSelfId() {
|
|
|
549
812
|
return {};
|
|
550
813
|
}
|
|
551
814
|
}
|
|
552
|
-
function storeSelfId(responses) {
|
|
815
|
+
function storeSelfId(responses, gate = {}) {
|
|
816
|
+
const category = COOKIE_CONSENT_CATEGORY[SELFID_KEY] ?? "personalization";
|
|
817
|
+
if (!hasConsent(category, gate)) return;
|
|
553
818
|
try {
|
|
554
819
|
localStorage.setItem(SELFID_KEY, JSON.stringify(responses));
|
|
555
820
|
} catch {
|
|
@@ -561,7 +826,7 @@ function clearStoredSelfId() {
|
|
|
561
826
|
} catch {
|
|
562
827
|
}
|
|
563
828
|
}
|
|
564
|
-
function
|
|
829
|
+
function readStorage2(key) {
|
|
565
830
|
try {
|
|
566
831
|
return localStorage.getItem(key);
|
|
567
832
|
} catch {
|
|
@@ -608,7 +873,7 @@ async function clearSelfId(options = {}) {
|
|
|
608
873
|
async function submit(fields, options) {
|
|
609
874
|
const csrfToken = resolveCsrfToken(options.csrfToken);
|
|
610
875
|
try {
|
|
611
|
-
const res = await fetch(`${resolveApiBase(options.apiBase)}/kywi/self-id
|
|
876
|
+
const res = await fetch(withSiteId(`${resolveApiBase(options.apiBase)}/kywi/self-id`), {
|
|
612
877
|
method: "POST",
|
|
613
878
|
headers: { "Content-Type": "application/json", ...csrfToken ? { "X-CSRF-Token": csrfToken } : {} },
|
|
614
879
|
body: JSON.stringify({
|
|
@@ -882,106 +1147,6 @@ function entryPageFor(currentPath) {
|
|
|
882
1147
|
return search ? `${currentPath}${search}` : currentPath;
|
|
883
1148
|
}
|
|
884
1149
|
|
|
885
|
-
// src/cookies.ts
|
|
886
|
-
var COOKIE_NAMES = {
|
|
887
|
-
SIGNALS: "kywi_signals",
|
|
888
|
-
VISITOR: "kywi_visitor",
|
|
889
|
-
UTM: "kywi_utm",
|
|
890
|
-
AUDIENCE: "kywi_audience",
|
|
891
|
-
OPTOUT: "kywi_optout",
|
|
892
|
-
KNOWN: "kywi_known",
|
|
893
|
-
PREVIEW_INIT: "kywi_preview_init"
|
|
894
|
-
};
|
|
895
|
-
function readCookies() {
|
|
896
|
-
const map = /* @__PURE__ */ new Map();
|
|
897
|
-
if (typeof document === "undefined") return map;
|
|
898
|
-
for (const pair of document.cookie.split(";")) {
|
|
899
|
-
const trimmed = pair.trim();
|
|
900
|
-
const eqIdx = trimmed.indexOf("=");
|
|
901
|
-
if (eqIdx < 0) continue;
|
|
902
|
-
map.set(trimmed.slice(0, eqIdx), decodeURIComponent(trimmed.slice(eqIdx + 1)));
|
|
903
|
-
}
|
|
904
|
-
return map;
|
|
905
|
-
}
|
|
906
|
-
function setCookie(name, value, maxAge) {
|
|
907
|
-
let str = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
908
|
-
if (maxAge !== void 0 && maxAge >= 0) str += `; Max-Age=${maxAge}`;
|
|
909
|
-
document.cookie = str;
|
|
910
|
-
}
|
|
911
|
-
function deleteCookie(name) {
|
|
912
|
-
document.cookie = `${name}=; Path=/; Max-Age=0`;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// src/consent.ts
|
|
916
|
-
var CONSENT_COOKIE = "kywi_consent";
|
|
917
|
-
var AUDIENCE_PIN_COOKIE = "kywi_audience";
|
|
918
|
-
var SWITCHER_PIN_PREFIX = "switcher:";
|
|
919
|
-
var COOKIE_CONSENT_CATEGORY = {
|
|
920
|
-
kywi_consent: "essential",
|
|
921
|
-
kywi_optout: "essential",
|
|
922
|
-
kywi_preview_init: "essential",
|
|
923
|
-
kywi_signals: "personalization",
|
|
924
|
-
kywi_visitor: "personalization",
|
|
925
|
-
kywi_utm: "personalization",
|
|
926
|
-
kywi_audience: "personalization",
|
|
927
|
-
kywi_known: "personalization",
|
|
928
|
-
kywi_entry: "personalization",
|
|
929
|
-
kywi_selfid: "personalization"
|
|
930
|
-
};
|
|
931
|
-
function getConsentState() {
|
|
932
|
-
const cookies = readCookies();
|
|
933
|
-
const raw = cookies.get(CONSENT_COOKIE);
|
|
934
|
-
if (!raw) return null;
|
|
935
|
-
let parsed;
|
|
936
|
-
try {
|
|
937
|
-
parsed = JSON.parse(raw);
|
|
938
|
-
} catch {
|
|
939
|
-
return null;
|
|
940
|
-
}
|
|
941
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
942
|
-
const obj = parsed;
|
|
943
|
-
return {
|
|
944
|
-
essential: true,
|
|
945
|
-
analytics: obj.analytics === true,
|
|
946
|
-
marketing: obj.marketing === true,
|
|
947
|
-
personalization: obj.personalization === true
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
function setConsent(prefs) {
|
|
951
|
-
const state = {
|
|
952
|
-
essential: true,
|
|
953
|
-
analytics: prefs.analytics ?? false,
|
|
954
|
-
marketing: prefs.marketing ?? false,
|
|
955
|
-
personalization: prefs.personalization ?? false
|
|
956
|
-
};
|
|
957
|
-
setCookie(CONSENT_COOKIE, JSON.stringify(state), 31536e3);
|
|
958
|
-
clearDeniedCookies(state);
|
|
959
|
-
}
|
|
960
|
-
function clearDeniedCookies(state) {
|
|
961
|
-
if (typeof document === "undefined") return [];
|
|
962
|
-
const cookies = readCookies();
|
|
963
|
-
const deleted = [];
|
|
964
|
-
for (const [name, category] of Object.entries(COOKIE_CONSENT_CATEGORY)) {
|
|
965
|
-
if (category === "essential") continue;
|
|
966
|
-
if (state && state[category] === true) continue;
|
|
967
|
-
if (isDeliberatePreference(name, cookies.get(name))) continue;
|
|
968
|
-
deleteCookie(name);
|
|
969
|
-
deleted.push(name);
|
|
970
|
-
}
|
|
971
|
-
return deleted;
|
|
972
|
-
}
|
|
973
|
-
function isDeliberatePreference(name, value) {
|
|
974
|
-
if (name !== AUDIENCE_PIN_COOKIE || value === void 0) return false;
|
|
975
|
-
return value.startsWith(SWITCHER_PIN_PREFIX) && value.length > SWITCHER_PIN_PREFIX.length;
|
|
976
|
-
}
|
|
977
|
-
function hasConsent(category, options = {}) {
|
|
978
|
-
if (options.requireConsent === false) return true;
|
|
979
|
-
if (category === "essential") return true;
|
|
980
|
-
const state = getConsentState();
|
|
981
|
-
if (!state) return false;
|
|
982
|
-
return state[category] === true;
|
|
983
|
-
}
|
|
984
|
-
|
|
985
1150
|
// src/audience/behavioral-collector.ts
|
|
986
1151
|
var SESSION_KEY = "kywi_session";
|
|
987
1152
|
var TOTAL_VIEWS_KEY = "kywi_total_views";
|
|
@@ -1036,7 +1201,7 @@ function collectBehavioral(currentPath, category, gate = {}) {
|
|
|
1036
1201
|
}
|
|
1037
1202
|
|
|
1038
1203
|
// src/audience/evaluate.ts
|
|
1039
|
-
function
|
|
1204
|
+
function merge2(base, partial) {
|
|
1040
1205
|
return {
|
|
1041
1206
|
utm: partial.utm ?? base.utm,
|
|
1042
1207
|
referrer: partial.referrer ?? base.referrer,
|
|
@@ -1050,10 +1215,10 @@ function merge(base, partial) {
|
|
|
1050
1215
|
};
|
|
1051
1216
|
}
|
|
1052
1217
|
function evaluateClientSide(audiences, serverSignals, currentPath, pageCategory, gate = {}) {
|
|
1053
|
-
let signals =
|
|
1218
|
+
let signals = merge2(createEmptySignals(), serverSignals);
|
|
1054
1219
|
const client = collectBehavioral(currentPath, pageCategory, gate);
|
|
1055
1220
|
const selfId = collectSelfId();
|
|
1056
|
-
signals =
|
|
1221
|
+
signals = merge2(signals, { session: client.session, behavioral: client.behavioral, query: collectQuery(), selfId, meta: { ...signals.meta, resolvedAt: "client" } });
|
|
1057
1222
|
const { winningAudienceId, allMatchedIds } = resolveAudiences(audiences, signals);
|
|
1058
1223
|
const isOptedOut2 = signals.meta.isOptedOut;
|
|
1059
1224
|
return { winningAudienceId: isOptedOut2 ? null : winningAudienceId, allMatchedIds, signals, isOptedOut: isOptedOut2 };
|
|
@@ -1091,14 +1256,14 @@ var SWITCHER_LABEL_ID = "kywi-transparency-switcher-label";
|
|
|
1091
1256
|
var DEFAULT_API_BASE2 = "/api/v1";
|
|
1092
1257
|
var lastState = null;
|
|
1093
1258
|
var forcedOpen = false;
|
|
1094
|
-
function
|
|
1259
|
+
function readStorage3(key) {
|
|
1095
1260
|
try {
|
|
1096
1261
|
return localStorage.getItem(key);
|
|
1097
1262
|
} catch {
|
|
1098
1263
|
return null;
|
|
1099
1264
|
}
|
|
1100
1265
|
}
|
|
1101
|
-
function
|
|
1266
|
+
function writeStorage2(key, value) {
|
|
1102
1267
|
try {
|
|
1103
1268
|
localStorage.setItem(key, value);
|
|
1104
1269
|
} catch {
|
|
@@ -1111,15 +1276,15 @@ function removeStorage(key) {
|
|
|
1111
1276
|
}
|
|
1112
1277
|
}
|
|
1113
1278
|
function isCollapsed() {
|
|
1114
|
-
if (
|
|
1279
|
+
if (readStorage3(LEGACY_DISMISSED_KEY) === "1") {
|
|
1115
1280
|
removeStorage(LEGACY_DISMISSED_KEY);
|
|
1116
|
-
|
|
1281
|
+
writeStorage2(COLLAPSED_KEY, "1");
|
|
1117
1282
|
return true;
|
|
1118
1283
|
}
|
|
1119
|
-
return
|
|
1284
|
+
return readStorage3(COLLAPSED_KEY) !== "0";
|
|
1120
1285
|
}
|
|
1121
1286
|
function setCollapsed(collapsed) {
|
|
1122
|
-
|
|
1287
|
+
writeStorage2(COLLAPSED_KEY, collapsed ? "1" : "0");
|
|
1123
1288
|
}
|
|
1124
1289
|
function optOut() {
|
|
1125
1290
|
setCookie(COOKIE_NAMES.OPTOUT, "1", 365 * 24 * 3600);
|
|
@@ -1137,7 +1302,7 @@ function hasPin() {
|
|
|
1137
1302
|
async function pinAudience(audienceId, opts = {}) {
|
|
1138
1303
|
const apiBase = opts.apiBase ?? DEFAULT_API_BASE2;
|
|
1139
1304
|
try {
|
|
1140
|
-
const res = await fetch(`${apiBase}/kywi/audience
|
|
1305
|
+
const res = await fetch(withSiteId(`${apiBase}/kywi/audience`), {
|
|
1141
1306
|
method: "POST",
|
|
1142
1307
|
headers: { "Content-Type": "application/json", ...opts.csrfToken ? { "X-CSRF-Token": opts.csrfToken } : {} },
|
|
1143
1308
|
body: JSON.stringify({ audienceId, source: "switcher" })
|
|
@@ -1350,14 +1515,18 @@ function setPreviewAudienceId(id) {
|
|
|
1350
1515
|
}
|
|
1351
1516
|
function clearPreview() {
|
|
1352
1517
|
sessionStorage.removeItem(PREVIEW_KEY);
|
|
1518
|
+
deleteCookie(COOKIE_NAMES.PREVIEW_INIT);
|
|
1519
|
+
}
|
|
1520
|
+
function previewAudienceFromCookie(value) {
|
|
1521
|
+
if (!value) return null;
|
|
1522
|
+
const parts = value.split(".");
|
|
1523
|
+
if (parts.length < 3) return null;
|
|
1524
|
+
const id = parts[0];
|
|
1525
|
+
return id.length > 0 ? id : null;
|
|
1353
1526
|
}
|
|
1354
1527
|
function checkPreviewInit() {
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
if (initId) {
|
|
1358
|
-
setPreviewAudienceId(initId);
|
|
1359
|
-
deleteCookie(COOKIE_NAMES.PREVIEW_INIT);
|
|
1360
|
-
}
|
|
1528
|
+
const initId = previewAudienceFromCookie(readCookies().get(COOKIE_NAMES.PREVIEW_INIT));
|
|
1529
|
+
if (initId) setPreviewAudienceId(initId);
|
|
1361
1530
|
}
|
|
1362
1531
|
function renderPreviewBanner(audienceName2) {
|
|
1363
1532
|
document.querySelector(".kywi-preview-banner")?.remove();
|
|
@@ -1418,7 +1587,7 @@ async function runClientAdapters(adapters, currentSignals, apiBase = "/api/v1")
|
|
|
1418
1587
|
if (cached) return cached;
|
|
1419
1588
|
let result;
|
|
1420
1589
|
if (adapter.side === "server-proxy") {
|
|
1421
|
-
const res = await fetch(`${apiBase}/kywi/adapters/${adapter.id}
|
|
1590
|
+
const res = await fetch(withSiteId(`${apiBase}/kywi/adapters/${adapter.id}`), {
|
|
1422
1591
|
method: "POST",
|
|
1423
1592
|
headers: { "Content-Type": "application/json" },
|
|
1424
1593
|
body: JSON.stringify({ signals: currentSignals })
|
|
@@ -1437,16 +1606,18 @@ async function runClientAdapters(adapters, currentSignals, apiBase = "/api/v1")
|
|
|
1437
1606
|
}
|
|
1438
1607
|
|
|
1439
1608
|
// src/audience/selfid-submit.ts
|
|
1440
|
-
function handleSelfIdSubmit(responses, audiences, currentPath) {
|
|
1441
|
-
storeSelfId(responses);
|
|
1609
|
+
function handleSelfIdSubmit(responses, audiences, currentPath, gate = {}) {
|
|
1610
|
+
storeSelfId(responses, gate);
|
|
1442
1611
|
if (audiences && currentPath) {
|
|
1443
1612
|
const cookies = readCookies();
|
|
1444
1613
|
const serverMeta = readServerMeta();
|
|
1614
|
+
const mayPersonalize = hasConsent("personalization", gate);
|
|
1615
|
+
const storedVisitorId = mayPersonalize ? cookies.get(COOKIE_NAMES.VISITOR) : void 0;
|
|
1445
1616
|
const serverSignals = {
|
|
1446
|
-
identity: { visitorId: serverMeta.visitorId ||
|
|
1617
|
+
identity: { visitorId: serverMeta.visitorId || storedVisitorId || "", isKnown: mayPersonalize && cookies.has(COOKIE_NAMES.KNOWN), maLeadId: null },
|
|
1447
1618
|
meta: { isOptedOut: cookies.has(COOKIE_NAMES.OPTOUT), isPreview: false, previewAudienceId: null, resolvedAt: "server" }
|
|
1448
1619
|
};
|
|
1449
|
-
const result = evaluateClientSide(audiences, serverSignals, currentPath);
|
|
1620
|
+
const result = evaluateClientSide(audiences, serverSignals, currentPath, void 0, gate);
|
|
1450
1621
|
patchVariantContainers(result.winningAudienceId);
|
|
1451
1622
|
populateDataLayer({
|
|
1452
1623
|
visitorId: result.signals.identity.visitorId,
|
|
@@ -1518,10 +1689,28 @@ function createSelfIdWidget(config) {
|
|
|
1518
1689
|
function markShown() {
|
|
1519
1690
|
localStorage.setItem(SHOWN_KEY, String(Date.now()));
|
|
1520
1691
|
}
|
|
1692
|
+
function consentGate() {
|
|
1693
|
+
return config.requireConsent === void 0 ? {} : { requireConsent: config.requireConsent };
|
|
1694
|
+
}
|
|
1695
|
+
function patchClientSide(responses) {
|
|
1696
|
+
handleSelfIdSubmit(responses, config.audiences ?? [], config.currentPath ?? "/", consentGate());
|
|
1697
|
+
}
|
|
1521
1698
|
function handleSubmit(responses) {
|
|
1522
|
-
|
|
1699
|
+
if (config.serverSubmit !== false) {
|
|
1700
|
+
markShown();
|
|
1701
|
+
const options = {
|
|
1702
|
+
...config.apiBase !== void 0 ? { apiBase: config.apiBase } : {},
|
|
1703
|
+
...config.csrfToken !== void 0 ? { csrfToken: config.csrfToken } : {}
|
|
1704
|
+
};
|
|
1705
|
+
void setSelfId(responses, options).then((result) => {
|
|
1706
|
+
if (!result || !result.persisted) patchClientSide(responses);
|
|
1707
|
+
}).catch(() => {
|
|
1708
|
+
});
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
storeSelfId(responses, consentGate());
|
|
1523
1712
|
try {
|
|
1524
|
-
|
|
1713
|
+
patchClientSide(responses);
|
|
1525
1714
|
} finally {
|
|
1526
1715
|
markShown();
|
|
1527
1716
|
}
|
|
@@ -1774,7 +1963,7 @@ async function bootAudienceEngine(config) {
|
|
|
1774
1963
|
populateDataLayer({ visitorId: result.signals.identity.visitorId, audienceId: effectiveAudienceId, experimentId: serverMeta.experimentId, variantId: serverMeta.variantId, isPreview: false, isOptedOut: result.isOptedOut });
|
|
1775
1964
|
fireKywiReady();
|
|
1776
1965
|
if (!isPinned && mayPersonalize && result.winningAudienceId && result.winningAudienceId !== serverMeta.audienceId) {
|
|
1777
|
-
fetch(`${apiBase}/kywi/audience
|
|
1966
|
+
fetch(withSiteId(`${apiBase}/kywi/audience`), {
|
|
1778
1967
|
method: "POST",
|
|
1779
1968
|
headers: { "Content-Type": "application/json", ...Kywi.context?.csrfToken ? { "X-CSRF-Token": Kywi.context.csrfToken } : {} },
|
|
1780
1969
|
// Explicitly the AUTOMATIC write, so the server applies the
|
|
@@ -1785,7 +1974,15 @@ async function bootAudienceEngine(config) {
|
|
|
1785
1974
|
});
|
|
1786
1975
|
}
|
|
1787
1976
|
if (config.selfIdWidget) {
|
|
1788
|
-
const widget = createSelfIdWidget(
|
|
1977
|
+
const widget = createSelfIdWidget({
|
|
1978
|
+
// The API base and CSRF token this boot resolved, so the widget's server
|
|
1979
|
+
// submit posts where the rest of the runtime does (review fix 4); a
|
|
1980
|
+
// value the widget config sets itself still wins.
|
|
1981
|
+
apiBase,
|
|
1982
|
+
csrfToken: Kywi.context?.csrfToken ?? null,
|
|
1983
|
+
...config.selfIdWidget,
|
|
1984
|
+
...config.requireConsent === void 0 ? {} : { requireConsent: config.requireConsent }
|
|
1985
|
+
});
|
|
1789
1986
|
if (widget.shouldShow()) {
|
|
1790
1987
|
const doRender = () => {
|
|
1791
1988
|
const mount = resolveSelfIdMount(config.selfIdWidget.displayMode);
|
|
@@ -1871,6 +2068,11 @@ var Kywi = {
|
|
|
1871
2068
|
setSelfId,
|
|
1872
2069
|
/** Forget the visitor's self-identification and un-pin their audience. */
|
|
1873
2070
|
clearSelfId,
|
|
2071
|
+
/**
|
|
2072
|
+
* Re-open the cookie-consent banner so the visitor can change an earlier
|
|
2073
|
+
* decision (kywi-cms#191) — for a site's own "Cookie settings" link.
|
|
2074
|
+
*/
|
|
2075
|
+
reopenConsent,
|
|
1874
2076
|
getEntity: null,
|
|
1875
2077
|
getFeed: null,
|
|
1876
2078
|
renderFeed: null,
|
|
@@ -1904,6 +2106,8 @@ if (typeof document !== "undefined") {
|
|
|
1904
2106
|
export {
|
|
1905
2107
|
AUDIENCE_PIN_COOKIE,
|
|
1906
2108
|
CONSENT_COOKIE,
|
|
2109
|
+
CONSENT_EVENT,
|
|
2110
|
+
CONSENT_REOPEN_EVENT,
|
|
1907
2111
|
COOKIE_CONSENT_CATEGORY,
|
|
1908
2112
|
Kywi,
|
|
1909
2113
|
NAV_BREAKPOINT,
|
|
@@ -1918,6 +2122,7 @@ export {
|
|
|
1918
2122
|
initNavMenus,
|
|
1919
2123
|
openTransparencyPanel,
|
|
1920
2124
|
renderTransparencyPanel,
|
|
2125
|
+
reopenConsent,
|
|
1921
2126
|
setConsent,
|
|
1922
2127
|
setSelfId
|
|
1923
2128
|
};
|