@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/index.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
type SignalSource = "campaign_link" | "form_email" | "known_visitor" | "reverse_ip" | "referrer";
|
|
2
|
+
type AttributeKey = "company.domain" | "company.name" | "company.legalName" | "company.industry"
|
|
3
|
+
/** NAICS sector id ("finance", "health-care"), rolled up from `company.naics` by `naicsSector`. */
|
|
4
|
+
| "company.sector" | "company.naics" | "company.sic" | "company.employeeCount" | "company.employeeBand" | "company.revenueBand" | "company.country" | "company.region" | "company.city" | "company.founded" | "company.ownership" | "company.ticker" | "crm.lists" | "crm.stage" | "crm.owner" | "crm.isCustomer" | "visitor.role" | "visitor.networkType" | "visitor.identified" | "visitor.country" | "intent.campaign" | "intent.utmSource" | "intent.utmMedium" | "intent.utmTerm" | "intent.referrerHost";
|
|
5
|
+
type AttributeValue = string | number | boolean;
|
|
6
|
+
type ListAttributeKey = "crm.lists";
|
|
7
|
+
type ScalarAttributeKey = Exclude<AttributeKey, ListAttributeKey>;
|
|
8
|
+
type IdentityContext = Partial<Record<ScalarAttributeKey, AttributeValue>> & Partial<Record<ListAttributeKey, string[]>>;
|
|
9
|
+
type Condition = ({
|
|
10
|
+
attr: ScalarAttributeKey;
|
|
11
|
+
op: "eq";
|
|
12
|
+
value: AttributeValue;
|
|
13
|
+
} | {
|
|
14
|
+
attr: ScalarAttributeKey;
|
|
15
|
+
op: "in";
|
|
16
|
+
value: AttributeValue[];
|
|
17
|
+
}
|
|
18
|
+
/** `startsWith` is a prefix match, for hierarchical codes: NAICS "51" covers "5112". */
|
|
19
|
+
| {
|
|
20
|
+
attr: ScalarAttributeKey;
|
|
21
|
+
op: "contains" | "startsWith";
|
|
22
|
+
value: string;
|
|
23
|
+
} | {
|
|
24
|
+
attr: ScalarAttributeKey;
|
|
25
|
+
op: "gte" | "lte";
|
|
26
|
+
value: number;
|
|
27
|
+
}
|
|
28
|
+
/** The list attribute holds this id, e.g. the account is on CRM list "hs-123". */
|
|
29
|
+
| {
|
|
30
|
+
attr: ListAttributeKey;
|
|
31
|
+
op: "has";
|
|
32
|
+
value: string;
|
|
33
|
+
} | {
|
|
34
|
+
attr: AttributeKey;
|
|
35
|
+
op: "exists";
|
|
36
|
+
}) & {
|
|
37
|
+
/**
|
|
38
|
+
* For near matching: a visitor who fails this condition never gets a near match. Exact matching
|
|
39
|
+
* ignores it. Conditions under `not` are always required.
|
|
40
|
+
*/
|
|
41
|
+
required?: boolean;
|
|
42
|
+
};
|
|
43
|
+
type SlotContent = Record<string, string>;
|
|
44
|
+
type VariantStatus = "draft" | "live" | "paused" | "retired";
|
|
45
|
+
type DecisionReason = "matched" | "default" | "holdout";
|
|
46
|
+
type DecisionRung = "exact" | "near" | "segment" | "default";
|
|
47
|
+
interface Decision {
|
|
48
|
+
slotId: string;
|
|
49
|
+
/** The variant or segment row (`seg:…`) shown, or null when default content was shown. */
|
|
50
|
+
variantId: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* For holdout visitors: the variant they *would* have seen. This is the control group for that
|
|
53
|
+
* variant's lift. Null for visitors no variant matched.
|
|
54
|
+
*/
|
|
55
|
+
counterfactualVariantId: string | null;
|
|
56
|
+
reason: DecisionReason;
|
|
57
|
+
rung: DecisionRung;
|
|
58
|
+
/** Content with all tokens rendered. */
|
|
59
|
+
content: SlotContent;
|
|
60
|
+
}
|
|
61
|
+
type ConsentChoice = "granted" | "denied" | "unknown";
|
|
62
|
+
type CandidateOutcome = "wins" | "outranked" | "skipped" | "held-out";
|
|
63
|
+
type ExplainOutcome = Exclude<CandidateOutcome, "held-out"> | "no-match" | "not-live";
|
|
64
|
+
interface ConditionExplanation {
|
|
65
|
+
attr: AttributeKey;
|
|
66
|
+
op: Condition["op"];
|
|
67
|
+
/** The rule's value; absent for `exists`. */
|
|
68
|
+
value?: Extract<Condition, {
|
|
69
|
+
value: unknown;
|
|
70
|
+
}>["value"];
|
|
71
|
+
/** The visitor's value for `attr`, or null when they have none. */
|
|
72
|
+
actual: AttributeValue | string[] | null;
|
|
73
|
+
/** Whether this condition alone holds. Null when the attribute can't be known here (see `unavailable`). */
|
|
74
|
+
matched: boolean | null;
|
|
75
|
+
}
|
|
76
|
+
interface AudienceExplanation {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
matched: boolean;
|
|
80
|
+
/** Some condition reads an attribute this explanation can't see, so `matched` may be wrong for real visitors. */
|
|
81
|
+
unknown: boolean;
|
|
82
|
+
/** Every leaf condition of the rule, in order. `not`, `any` and `all` are folded into `matched`. */
|
|
83
|
+
conditions: ConditionExplanation[];
|
|
84
|
+
}
|
|
85
|
+
interface VariantExplanation {
|
|
86
|
+
variantId: string;
|
|
87
|
+
audienceId: string;
|
|
88
|
+
status: VariantStatus;
|
|
89
|
+
/** Whether the variant's audience matches this visitor, whatever its status. */
|
|
90
|
+
audienceMatched: boolean;
|
|
91
|
+
outcome: ExplainOutcome;
|
|
92
|
+
/** For `skipped`: the attributes with no value and no fallback that stopped it rendering. */
|
|
93
|
+
missing?: string[];
|
|
94
|
+
}
|
|
95
|
+
interface SlotExplanation {
|
|
96
|
+
/** The variant or segment row (`seg:…`) shown, or null for default content. */
|
|
97
|
+
variantId: string | null;
|
|
98
|
+
reason: DecisionReason;
|
|
99
|
+
/** The step of the fallback ladder that chose the content. */
|
|
100
|
+
rung: DecisionRung;
|
|
101
|
+
/** The text each field shows. */
|
|
102
|
+
content: Record<string, string>;
|
|
103
|
+
variants: VariantExplanation[];
|
|
104
|
+
}
|
|
105
|
+
interface SiteExplanation {
|
|
106
|
+
company: {
|
|
107
|
+
domain: string;
|
|
108
|
+
source: SignalSource;
|
|
109
|
+
confidence: number;
|
|
110
|
+
} | null;
|
|
111
|
+
/** The flat attributes audiences and templates see. */
|
|
112
|
+
context: IdentityContext;
|
|
113
|
+
audiences: AudienceExplanation[];
|
|
114
|
+
slots: Record<string, SlotExplanation>;
|
|
115
|
+
}
|
|
116
|
+
declare const RESOLVE_API_VERSION = 1;
|
|
117
|
+
interface DeclaredSlot {
|
|
118
|
+
id: string;
|
|
119
|
+
h: string;
|
|
120
|
+
}
|
|
121
|
+
/** `POST /api/v1/resolve`. Browsers send it as `text/plain` so no CORS preflight is needed. */
|
|
122
|
+
export interface ResolveRequest {
|
|
123
|
+
v: typeof RESOLVE_API_VERSION;
|
|
124
|
+
/** Publishable key. A server call sends its secret key as `Authorization: Bearer` instead. */
|
|
125
|
+
key?: string;
|
|
126
|
+
/** The visitor id the package stored after an earlier `persist: true`. */
|
|
127
|
+
vid?: string;
|
|
128
|
+
/** Page URL; campaign links and UTM parameters are read from it. */
|
|
129
|
+
url: string;
|
|
130
|
+
referrer?: string | null;
|
|
131
|
+
slots: DeclaredSlot[];
|
|
132
|
+
/** What the customer's CMP reported through `setConsent`. */
|
|
133
|
+
consent?: ConsentChoice;
|
|
134
|
+
/** Global Privacy Control as the page or the customer's server saw it. */
|
|
135
|
+
gpc?: boolean;
|
|
136
|
+
/** Secret-key calls only: the visitor's IP and country from the customer's own edge. Ignored otherwise. */
|
|
137
|
+
ip?: string;
|
|
138
|
+
country?: string;
|
|
139
|
+
/** The session's config pin from an earlier response (`"<version>.<epoch>"`), to keep its copy stable. */
|
|
140
|
+
pin?: string;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* What one slot shows. The same fields `<Slot>` writes as `data-ge-*` attributes. `rung` is absent in
|
|
144
|
+
* answers from servers that predate the fallback ladder.
|
|
145
|
+
*/
|
|
146
|
+
export type ResolvedSlot = Pick<Decision, "variantId" | "counterfactualVariantId" | "reason" | "content"> & Partial<Pick<Decision, "rung">>;
|
|
147
|
+
export interface ResolveResponse {
|
|
148
|
+
v: typeof RESOLVE_API_VERSION;
|
|
149
|
+
/** Published config version the decisions came from; 0 when the site has never published. */
|
|
150
|
+
configVersion: number;
|
|
151
|
+
/** Whether consent lets the package store `vid` and start the tracker. */
|
|
152
|
+
persist: boolean;
|
|
153
|
+
/** The visitor id to store, present only when `persist` is true. */
|
|
154
|
+
vid?: string;
|
|
155
|
+
/**
|
|
156
|
+
* The pin to keep for the rest of the session and send back as `pin`, present only when `persist`
|
|
157
|
+
* is true. The session then sees `configVersion` until a rollback breaks the pin.
|
|
158
|
+
*/
|
|
159
|
+
pin?: string;
|
|
160
|
+
/** Requested slots that exist in the live config. Any other slot keeps its code defaults. */
|
|
161
|
+
decisions: Record<string, ResolvedSlot>;
|
|
162
|
+
/** Match token for the tracker to echo in beacon v2 (`m`), present only when `persist` is true. Opaque. */
|
|
163
|
+
m?: string;
|
|
164
|
+
/**
|
|
165
|
+
* A live-page preview from the console (the page URL carried a valid `ge_preview` token): drafts
|
|
166
|
+
* for a synthetic visitor. The package stores nothing, forgets nothing and doesn't measure.
|
|
167
|
+
*/
|
|
168
|
+
preview?: true;
|
|
169
|
+
/**
|
|
170
|
+
* A debug link (docs/design/resolve-debug): why each requested slot shows what it does to this
|
|
171
|
+
* request, resolved from what our CDN saw. Only with `preview`; the browser SDK logs it to the console.
|
|
172
|
+
*/
|
|
173
|
+
explain?: SiteExplanation;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A short, stable fingerprint of a slot's defaults, so resolve can tell when the code and the
|
|
177
|
+
* console disagree. FNV-1a over the fields sorted by name; 8 hex characters.
|
|
178
|
+
*/
|
|
179
|
+
export declare function hashSlotDefaults(content: SlotContent): string;
|
|
180
|
+
/** What the visitor chose in the site's consent tool. `unknown` fails closed where opt-in applies. */
|
|
181
|
+
export type Consent = ConsentChoice;
|
|
182
|
+
/**
|
|
183
|
+
* Validates a resolve response. Returns null when the envelope is unusable, so the page keeps its
|
|
184
|
+
* defaults; a single malformed decision is dropped and its slot keeps its defaults.
|
|
185
|
+
*/
|
|
186
|
+
export declare function parseResolveResponse(body: unknown): ResolveResponse | null;
|
|
187
|
+
export declare const DEFAULT_API_BASE = "https://kharonlabs.com";
|
|
188
|
+
/** After this, a response is dropped and the page keeps its defaults (design: hard 500 ms timeout). */
|
|
189
|
+
export declare const DEFAULT_TIMEOUT_MS = 500;
|
|
190
|
+
export declare const VISITOR_COOKIE = "ge_vid";
|
|
191
|
+
export interface KharonOptions {
|
|
192
|
+
/** Publishable key (`ge_pk_…`). Bound to the site's allowed origins. */
|
|
193
|
+
key: string;
|
|
194
|
+
/** Where the API lives. Defaults to Kharon Labs production. */
|
|
195
|
+
apiBase?: string;
|
|
196
|
+
/** The visitor's consent, if the page already knows it. Update later with `setConsent`. */
|
|
197
|
+
consent?: Consent;
|
|
198
|
+
/** Hard limit for the first resolve; a later answer is dropped. Default 500 ms. */
|
|
199
|
+
timeoutMs?: number;
|
|
200
|
+
/** Page id for page views. Defaults to one derived from the URL path. */
|
|
201
|
+
pageId?: string;
|
|
202
|
+
/** Cookie domain for the visitor id, e.g. `example.com` to share it across subdomains. */
|
|
203
|
+
cookieDomain?: string;
|
|
204
|
+
/** The document to work in. Tests pass their own. */
|
|
205
|
+
root?: Document;
|
|
206
|
+
}
|
|
207
|
+
export interface SlotDeclaration {
|
|
208
|
+
id: string;
|
|
209
|
+
defaults: Record<string, string>;
|
|
210
|
+
}
|
|
211
|
+
export interface KharonClient {
|
|
212
|
+
/**
|
|
213
|
+
* Asks the server what to show. Resolves to null on any failure or after `deadline` (epoch ms),
|
|
214
|
+
* meaning: keep the defaults. Never rejects.
|
|
215
|
+
*/
|
|
216
|
+
resolve(slots: SlotDeclaration[], deadline?: number): Promise<ResolveResponse | null>;
|
|
217
|
+
/** Adopts a decision made elsewhere (server mode): keeps or drops the visitor id it carries. */
|
|
218
|
+
adopt(response: ResolveResponse): void;
|
|
219
|
+
/** Starts measuring the page's slots, only if the last response allowed storing the visitor. */
|
|
220
|
+
track(configVersion: number): void;
|
|
221
|
+
setConsent(consent: Consent): void;
|
|
222
|
+
/** Called after a consent change; a grant needs a fresh resolve to get an id. */
|
|
223
|
+
onConsentChange(listener: (consent: Consent) => void): () => void;
|
|
224
|
+
conversion(goal: string, value?: number): void;
|
|
225
|
+
stop(): void;
|
|
226
|
+
readonly consent: Consent;
|
|
227
|
+
}
|
|
228
|
+
export declare function createClient(options: KharonOptions): KharonClient;
|
|
229
|
+
/** `/pricing/team` → `pricing.team`; `/` → `home`. Page ids allow `[A-Za-z0-9_.:-]`. */
|
|
230
|
+
export declare function pageIdFor(pathname: string): string;
|
|
231
|
+
/**
|
|
232
|
+
* Paste into `<head>` before any stylesheet to hide slots (never the page) until the swap. It
|
|
233
|
+
* reveals them after 500 ms on its own, so a slow or missing script can't keep them hidden.
|
|
234
|
+
*/
|
|
235
|
+
export declare const ANTI_FLICKER_SNIPPET: string;
|
|
236
|
+
/** Every slot on the page with its code defaults, read from the markup. A repeated id is one slot. */
|
|
237
|
+
export declare function scanSlots(root: Document): SlotDeclaration[];
|
|
238
|
+
export declare function applyDecision(el: Element, decision: ResolvedSlot, baseURI: string): void;
|
|
239
|
+
/** Removes the anti-flicker style, if the page has one. */
|
|
240
|
+
export declare function reveal(root: Document): void;
|
|
241
|
+
/** When the anti-flicker snippet started hiding slots, so the swap shares its deadline. */
|
|
242
|
+
export declare function antiFlickerStart(root: Document): number | undefined;
|
|
243
|
+
export declare function isWritable(attr: string, value: string, baseURI: string): boolean;
|
|
244
|
+
export interface Kharon {
|
|
245
|
+
/** Forward your consent tool's answer. A grant re-resolves and starts measuring; a denial stops it. */
|
|
246
|
+
setConsent(consent: Consent): void;
|
|
247
|
+
/** Records a goal. Not recorded for visitors we may not store. */
|
|
248
|
+
conversion(goal: string, value?: number): void;
|
|
249
|
+
/** Re-scans and re-resolves the page, e.g. after a client-side route change. */
|
|
250
|
+
refresh(): Promise<void>;
|
|
251
|
+
stop(): void;
|
|
252
|
+
/** Settles once the latest resolve has been applied, or given up on. */
|
|
253
|
+
readonly ready: Promise<void>;
|
|
254
|
+
readonly client: KharonClient;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Personalizes the slots in the page's HTML. The page shows its own defaults until the decision
|
|
258
|
+
* arrives; after `timeoutMs` (500 ms) the answer is dropped and the defaults stay.
|
|
259
|
+
*/
|
|
260
|
+
export declare function init(options: KharonOptions): Kharon;
|
|
261
|
+
/** Same as `instance.setConsent`. Safe to call before `init`; the choice is used by the first resolve. */
|
|
262
|
+
export declare function setConsent(consent: Consent): void;
|
|
263
|
+
export declare function conversion(goal: string, value?: number): void;
|
|
264
|
+
/**
|
|
265
|
+
* Records a signup from the page. Prefer `recordSignup` in `@kharon-labs/server` from your signup
|
|
266
|
+
* handler: it counts only signups that succeeded, and ones whose browser blocks scripts.
|
|
267
|
+
*/
|
|
268
|
+
export declare function signup(): void;
|
|
269
|
+
export declare function refresh(): Promise<void>;
|
|
270
|
+
|
|
271
|
+
export {};
|