@preprio/toolkit 0.6.0-alpha.7 → 0.6.0-alpha.8
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 +5 -5
- package/dist/astro/{chunk-3EXK2LHI.js → chunk-R2YPGDVU.js} +1 -1
- package/dist/astro/graphql.js +1 -1
- package/dist/astro/index.cjs +1 -1
- package/dist/astro/index.js +1 -1
- package/dist/{chunk-U7TRFVK7.js → chunk-2QGUP7VQ.js} +1 -1
- package/dist/{chunk-VQIPTUN4.js → chunk-ATPB7GEC.js} +1 -1
- package/dist/{chunk-PFJO4TYL.js → chunk-OY7OKO6Q.js} +1 -1
- package/dist/{chunk-3QGWCYTL.js → chunk-T7ESBVBI.js} +1 -1
- package/dist/graphql/codegen.js +3 -3
- package/dist/graphql.js +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/nextjs/{chunk-CZK7SMBD.js → chunk-O2SBSBDG.js} +1 -1
- package/dist/nextjs/components.cjs +101 -65
- package/dist/nextjs/components.d.cts +14 -13
- package/dist/nextjs/components.d.ts +14 -13
- package/dist/nextjs/components.js +102 -66
- package/dist/nextjs/graphql.cjs +71 -24
- package/dist/nextjs/graphql.d.cts +5 -5
- package/dist/nextjs/graphql.d.ts +5 -5
- package/dist/nextjs/graphql.js +71 -24
- package/dist/nextjs/index.cjs +1 -1
- package/dist/nextjs/index.d.cts +9 -7
- package/dist/nextjs/index.d.ts +9 -7
- package/dist/nextjs/index.js +1 -1
- package/dist/nuxt/{chunk-JL33ENCM.js → chunk-SX3MSVPK.js} +1 -1
- package/dist/nuxt/graphql.js +1 -1
- package/dist/nuxt/index.cjs +1 -1
- package/dist/nuxt/index.js +1 -1
- package/dist/react/components.cjs +1 -1
- package/dist/react/components.js +1 -1
- package/dist/sveltekit/{chunk-S7S276V7.js → chunk-UOASSHZO.js} +1 -1
- package/dist/sveltekit/graphql.js +1 -1
- package/dist/sveltekit/index.cjs +1 -1
- package/dist/sveltekit/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -384,14 +384,14 @@ A/B tests go through the same decision as segment groups: both variants are in t
|
|
|
384
384
|
|
|
385
385
|
##### How each visitor gets their variant
|
|
386
386
|
|
|
387
|
-
Every page view is decided. Nothing is reused from a prefetch, the router cache or another page's view; the only
|
|
387
|
+
Every page view is decided. Nothing is reused from a prefetch, the router cache or another page's view; the browser only reuses a decision this tab already got for the same URL (see **Reuse** below).
|
|
388
388
|
|
|
389
389
|
- **Page loads.** The page is the same for every visitor and is sent at once, from the cache. An inline script at the start of each `PreprStack` asks the proxy for this view's decision as soon as it is parsed, so Prepr answers while the rest of the page, its styles and scripts (the tracking pixel included) still load. The personalized sections show the visitor's variant when the answer arrives; everything else never waits for it.
|
|
390
|
-
- **Client-side navigations.** When the visitor
|
|
390
|
+
- **Client-side navigations.** When the visitor hovers over a link (or presses it, on touch devices), `PreprPersonalizationSwitch` asks the proxy for the target page's decision. Clicks are never held: when the answer is in by the click, which hovering mostly ensures, the new page paints with the visitor's variants; otherwise its personalized sections show when the answer arrives. Prefetches still bring the page itself; only the decision is fetched.
|
|
391
391
|
- **Never the default first.** Until a view's decision is in, its personalized sections are not shown: the default variant keeps its space, invisible, so the page does not move when the visitor's variant appears (only by any difference in height). The default variant only shows when it is the decision: for visitors in none of the segments, for requests `onlyIf` skips, and when Prepr does not answer within `timeoutMs` (default 3000). Navigations started from code ask for a decision when the page opens; the personalized sections appear when it arrives.
|
|
392
|
-
- **
|
|
392
|
+
- **Reuse.** Back and Forward show a page as this tab last showed it, at once, without asking Prepr. Any other view of a URL this tab got a decision for (kept in `sessionStorage`) shows it at once too: under 10 seconds old without asking again, since Prepr needs a few seconds to process a visitor's activity anyway; up to 30 minutes old while a new decision is asked for in the background, which replaces the variant only when it differs. A page without personalization answers with no groups, which is trusted for an hour with no background check, so links to it cost nothing; when that page renders personalization by then (published since), it asks at once. A URL with other UTM parameters is another view; a failed answer is kept for 10 seconds and never replaces a real one.
|
|
393
393
|
- **"Right" means what Prepr knows when the visitor clicks.** Prepr assigns segments from the visitor's tracked activity. If it has not yet processed the pixel event of the previous page view, the decision cannot include it, and the visitor sees that segment from a later page view.
|
|
394
|
-
- **Costs.** The page carries the HTML of every variant, and an item with groups nested inside it once per combination of their variants. `render` gets `defaultVariant`: give `priority` images only to the default variant and `loading="lazy"` to the others, or the browser downloads every variant's hero image. Every page
|
|
394
|
+
- **Costs.** The page carries the HTML of every variant, and an item with groups nested inside it once per combination of their variants. `render` gets `defaultVariant`: give `priority` images only to the default variant and `loading="lazy"` to the others, or the browser downloads every variant's hero image. Every page load with personalization, and every link hovered or pressed (also a press that turns into a scroll), is at most one decision request when this tab has no fresh decision for that URL, answered from Prepr's cache when other visitors had the same answer.
|
|
395
395
|
- Without JavaScript, visitors see the default variants.
|
|
396
396
|
|
|
397
397
|
##### Deploying
|
|
@@ -1054,7 +1054,7 @@ The toolbar. Takes the props returned by `getToolbarProps`, and renders nothing
|
|
|
1054
1054
|
|
|
1055
1055
|
#### `PreprPersonalizationSwitch`
|
|
1056
1056
|
|
|
1057
|
-
From `@preprio/toolkit/nextjs`.
|
|
1057
|
+
From `@preprio/toolkit/nextjs`. Gets each client-side navigation's decision in before the click: when the visitor hovers over a same-site link (or presses it, on touch devices), it asks `createPreprProxy` for the target page's decision. Clicks are never held (see [How each visitor gets their variant](#how-each-visitor-gets-their-variant)). Mount it once in the root layout, so links on pages without a `PreprStack` are covered too. Links to other sites and links that open a new tab or download are left alone. The decision requests are answered by `createPreprProxy` only. Takes no props and renders nothing.
|
|
1058
1058
|
|
|
1059
1059
|
#### `PreprPreview`
|
|
1060
1060
|
|
package/dist/astro/graphql.js
CHANGED
package/dist/astro/index.cjs
CHANGED
package/dist/astro/index.js
CHANGED
package/dist/graphql/codegen.js
CHANGED
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
toDecisionQuery,
|
|
3
3
|
toSchemaInfo,
|
|
4
4
|
toVariantsQuery
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-
|
|
7
|
-
import "../chunk-
|
|
5
|
+
} from "../chunk-OY7OKO6Q.js";
|
|
6
|
+
import "../chunk-T7ESBVBI.js";
|
|
7
|
+
import "../chunk-ATPB7GEC.js";
|
|
8
8
|
|
|
9
9
|
// src/codegen/index.ts
|
|
10
10
|
function typeRef(type) {
|
package/dist/graphql.js
CHANGED
|
@@ -21,12 +21,12 @@ import {
|
|
|
21
21
|
toSchemaInfo,
|
|
22
22
|
toVariantsDocument,
|
|
23
23
|
toVariantsQuery
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-OY7OKO6Q.js";
|
|
25
25
|
import {
|
|
26
26
|
PreprError,
|
|
27
27
|
validatePreprToken
|
|
28
|
-
} from "./chunk-
|
|
29
|
-
import "./chunk-
|
|
28
|
+
} from "./chunk-T7ESBVBI.js";
|
|
29
|
+
import "./chunk-ATPB7GEC.js";
|
|
30
30
|
|
|
31
31
|
// src/core/graphql/cache-tags.ts
|
|
32
32
|
var MAX_TAGS = 128;
|
package/dist/index.cjs
CHANGED
package/dist/index.d.cts
CHANGED
|
@@ -7,7 +7,7 @@ export { P as PreprPixelConfig, l as loadTrackingPixel, s as setTrackingParam, t
|
|
|
7
7
|
* Package version; must match package.json. Lives in its own leaf module so
|
|
8
8
|
* core can import it without cycling through the barrel.
|
|
9
9
|
*/
|
|
10
|
-
declare const VERSION = "0.6.0-alpha.
|
|
10
|
+
declare const VERSION = "0.6.0-alpha.8";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { P as PreprPixelConfig, l as loadTrackingPixel, s as setTrackingParam, t
|
|
|
7
7
|
* Package version; must match package.json. Lives in its own leaf module so
|
|
8
8
|
* core can import it without cycling through the barrel.
|
|
9
9
|
*/
|
|
10
|
-
declare const VERSION = "0.6.0-alpha.
|
|
10
|
+
declare const VERSION = "0.6.0-alpha.8";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Resolve a `PreprFeatures` bag to plain booleans, defaulting every omitted key
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
loadTrackingPixel,
|
|
16
16
|
setTrackingParam,
|
|
17
17
|
trackEvent
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-2QGUP7VQ.js";
|
|
19
19
|
import {
|
|
20
20
|
isPersonalizedResponse
|
|
21
21
|
} from "./chunk-3NCEVJJ3.js";
|
|
@@ -32,11 +32,11 @@ import {
|
|
|
32
32
|
getPreprUUIDFromHeaders,
|
|
33
33
|
getToolbarPropsFromHeaders,
|
|
34
34
|
validatePreprToken
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-T7ESBVBI.js";
|
|
36
36
|
import {
|
|
37
37
|
VERSION,
|
|
38
38
|
resolveFeatures
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-ATPB7GEC.js";
|
|
40
40
|
|
|
41
41
|
// src/core/middleware.ts
|
|
42
42
|
var UTM_PARAMS = [
|
|
@@ -2346,12 +2346,76 @@ function applyPreprDecisions(decision) {
|
|
|
2346
2346
|
});
|
|
2347
2347
|
return true;
|
|
2348
2348
|
}
|
|
2349
|
+
function requestPreprDecision(key, options = {}) {
|
|
2350
|
+
const fresh = 1e4;
|
|
2351
|
+
const keep = 18e5;
|
|
2352
|
+
const keepStatic = 36e5;
|
|
2353
|
+
const state = window;
|
|
2354
|
+
const asking = state.__preprAsking = state.__preprAsking || {};
|
|
2355
|
+
const read = () => {
|
|
2356
|
+
try {
|
|
2357
|
+
const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
|
|
2358
|
+
return all && typeof all === "object" ? all : {};
|
|
2359
|
+
} catch {
|
|
2360
|
+
return {};
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
const ask = () => {
|
|
2364
|
+
if (!asking[key]) {
|
|
2365
|
+
asking[key] = fetch(key, {
|
|
2366
|
+
headers: { "Prepr-Decide": "1" },
|
|
2367
|
+
cache: "no-store",
|
|
2368
|
+
credentials: "same-origin",
|
|
2369
|
+
// The proxy gives up on Prepr after its own `timeoutMs`; this only
|
|
2370
|
+
// covers a proxy that never answers.
|
|
2371
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
|
|
2372
|
+
}).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
|
|
2373
|
+
delete asking[key];
|
|
2374
|
+
try {
|
|
2375
|
+
const all = read();
|
|
2376
|
+
const at = Date.now();
|
|
2377
|
+
Object.keys(all).forEach((stored) => {
|
|
2378
|
+
const entry = all[stored];
|
|
2379
|
+
if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
|
|
2380
|
+
delete all[stored];
|
|
2381
|
+
});
|
|
2382
|
+
const kept = all[key];
|
|
2383
|
+
if (answer !== "*" || !kept || kept.value === "*")
|
|
2384
|
+
all[key] = { value: answer, at };
|
|
2385
|
+
sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
|
|
2386
|
+
} catch {
|
|
2387
|
+
}
|
|
2388
|
+
return answer;
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
return asking[key];
|
|
2392
|
+
};
|
|
2393
|
+
const last = read()[key];
|
|
2394
|
+
const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
|
|
2395
|
+
const value = last && typeof last.value === "string" ? last.value : null;
|
|
2396
|
+
if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
|
|
2397
|
+
if (value !== "" && age >= fresh) {
|
|
2398
|
+
ask().then((answer) => {
|
|
2399
|
+
if (answer !== "*" && answer !== value && options.onChange)
|
|
2400
|
+
options.onChange(answer);
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
return { result: value, value: Promise.resolve(value) };
|
|
2404
|
+
}
|
|
2405
|
+
const pending = {
|
|
2406
|
+
value: ask()
|
|
2407
|
+
};
|
|
2408
|
+
pending.value.then((answer) => {
|
|
2409
|
+
pending.result = answer;
|
|
2410
|
+
});
|
|
2411
|
+
return pending;
|
|
2412
|
+
}
|
|
2349
2413
|
|
|
2350
2414
|
// src/react/components.ts
|
|
2351
2415
|
var import_react = require("react");
|
|
2352
2416
|
|
|
2353
2417
|
// src/version.ts
|
|
2354
|
-
var VERSION = "0.6.0-alpha.
|
|
2418
|
+
var VERSION = "0.6.0-alpha.8";
|
|
2355
2419
|
|
|
2356
2420
|
// src/core/pixel.ts
|
|
2357
2421
|
var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
|
|
@@ -2426,35 +2490,21 @@ function PreprToolbar({
|
|
|
2426
2490
|
}, []);
|
|
2427
2491
|
return null;
|
|
2428
2492
|
}
|
|
2429
|
-
var DECIDE_HEADER = "Prepr-Decide";
|
|
2430
|
-
var DECIDE_TIMEOUT_MS = 6e3;
|
|
2431
|
-
function requestDecision(key) {
|
|
2432
|
-
const pending = { key, value: null };
|
|
2433
|
-
pending.value = fetch(key, {
|
|
2434
|
-
headers: { [DECIDE_HEADER]: "1" },
|
|
2435
|
-
cache: "no-store",
|
|
2436
|
-
credentials: "same-origin",
|
|
2437
|
-
signal: AbortSignal.timeout(DECIDE_TIMEOUT_MS)
|
|
2438
|
-
}).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((value) => {
|
|
2439
|
-
pending.result = value;
|
|
2440
|
-
try {
|
|
2441
|
-
sessionStorage.setItem(
|
|
2442
|
-
"prepr_decision",
|
|
2443
|
-
JSON.stringify({ key, value, at: Date.now() })
|
|
2444
|
-
);
|
|
2445
|
-
} catch {
|
|
2446
|
-
}
|
|
2447
|
-
return value;
|
|
2448
|
-
});
|
|
2449
|
-
return pending;
|
|
2450
|
-
}
|
|
2451
|
-
var pressed = null;
|
|
2452
|
-
var replaying = null;
|
|
2453
2493
|
var shown = /* @__PURE__ */ new Map();
|
|
2454
2494
|
var historyStep = false;
|
|
2455
2495
|
var onPopState = () => {
|
|
2456
2496
|
historyStep = true;
|
|
2457
2497
|
};
|
|
2498
|
+
function requestDecision(key, personalized = false) {
|
|
2499
|
+
return requestPreprDecision(key, {
|
|
2500
|
+
personalized,
|
|
2501
|
+
onChange: (value) => {
|
|
2502
|
+
if (location.pathname + location.search !== key) return;
|
|
2503
|
+
applyPreprDecisions(value);
|
|
2504
|
+
shown.set(key, value);
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2458
2508
|
function linkTarget(event) {
|
|
2459
2509
|
const anchor = event.target?.closest?.("a[href]");
|
|
2460
2510
|
if (!(anchor instanceof HTMLAnchorElement)) return null;
|
|
@@ -2466,55 +2516,37 @@ function linkTarget(event) {
|
|
|
2466
2516
|
if (url.origin !== location.origin || key === location.pathname + location.search) {
|
|
2467
2517
|
return null;
|
|
2468
2518
|
}
|
|
2469
|
-
return
|
|
2519
|
+
return key;
|
|
2520
|
+
}
|
|
2521
|
+
var HOVER_DELAY_MS = 65;
|
|
2522
|
+
var hovering = null;
|
|
2523
|
+
function onPointerOver(event) {
|
|
2524
|
+
if (event.pointerType !== "mouse") return;
|
|
2525
|
+
const key = linkTarget(event);
|
|
2526
|
+
if (hovering?.key === key) return;
|
|
2527
|
+
if (hovering) clearTimeout(hovering.timer);
|
|
2528
|
+
hovering = key ? { key, timer: setTimeout(() => requestDecision(key), HOVER_DELAY_MS) } : null;
|
|
2470
2529
|
}
|
|
2471
2530
|
function onPointerDown(event) {
|
|
2472
2531
|
if (event.button !== 0) return;
|
|
2473
|
-
const
|
|
2474
|
-
if (
|
|
2475
|
-
return;
|
|
2476
|
-
}
|
|
2477
|
-
pressed = requestDecision(target.key);
|
|
2478
|
-
}
|
|
2479
|
-
function onClick(event) {
|
|
2480
|
-
const target = linkTarget(event);
|
|
2481
|
-
if (!target) return;
|
|
2482
|
-
if (replaying === target.anchor) {
|
|
2483
|
-
replaying = null;
|
|
2484
|
-
return;
|
|
2485
|
-
}
|
|
2486
|
-
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
2487
|
-
return;
|
|
2488
|
-
}
|
|
2489
|
-
const pending = pressed && pressed.key === target.key ? pressed : requestDecision(target.key);
|
|
2490
|
-
pressed = null;
|
|
2491
|
-
const record = (value) => {
|
|
2492
|
-
window.__preprDecision = { key: target.key, value };
|
|
2493
|
-
};
|
|
2494
|
-
if (pending.result !== void 0) {
|
|
2495
|
-
record(pending.result);
|
|
2496
|
-
return;
|
|
2497
|
-
}
|
|
2498
|
-
event.preventDefault();
|
|
2499
|
-
event.stopImmediatePropagation();
|
|
2500
|
-
void pending.value.then((value) => {
|
|
2501
|
-
record(value);
|
|
2502
|
-
replaying = target.anchor;
|
|
2503
|
-
target.anchor.click();
|
|
2504
|
-
});
|
|
2532
|
+
const key = linkTarget(event);
|
|
2533
|
+
if (key) requestDecision(key);
|
|
2505
2534
|
}
|
|
2506
2535
|
var listeners = 0;
|
|
2507
2536
|
function useDecisionRequests() {
|
|
2508
2537
|
(0, import_react2.useEffect)(() => {
|
|
2509
2538
|
if (listeners++ === 0) {
|
|
2510
|
-
window.addEventListener("
|
|
2511
|
-
window.addEventListener("
|
|
2539
|
+
window.addEventListener("pointerover", onPointerOver, { passive: true });
|
|
2540
|
+
window.addEventListener("pointerdown", onPointerDown, {
|
|
2541
|
+
capture: true,
|
|
2542
|
+
passive: true
|
|
2543
|
+
});
|
|
2512
2544
|
window.addEventListener("popstate", onPopState);
|
|
2513
2545
|
}
|
|
2514
2546
|
return () => {
|
|
2515
2547
|
if (--listeners === 0) {
|
|
2548
|
+
window.removeEventListener("pointerover", onPointerOver);
|
|
2516
2549
|
window.removeEventListener("pointerdown", onPointerDown, true);
|
|
2517
|
-
window.removeEventListener("click", onClick, true);
|
|
2518
2550
|
window.removeEventListener("popstate", onPopState);
|
|
2519
2551
|
}
|
|
2520
2552
|
};
|
|
@@ -2538,12 +2570,16 @@ function PreprDecisionApplier() {
|
|
|
2538
2570
|
remember();
|
|
2539
2571
|
return;
|
|
2540
2572
|
}
|
|
2573
|
+
const pending = requestDecision(key, true);
|
|
2574
|
+
if (pending.result !== void 0) {
|
|
2575
|
+
applyPreprDecisions(pending.result);
|
|
2576
|
+
remember();
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2541
2579
|
let live = true;
|
|
2542
|
-
|
|
2543
|
-
const value = started?.key === key ? started.value : requestDecision(key).value;
|
|
2544
|
-
void value.then((value2) => {
|
|
2580
|
+
void pending.value.then((value) => {
|
|
2545
2581
|
if (live && location.pathname + location.search === key) {
|
|
2546
|
-
applyPreprDecisions(
|
|
2582
|
+
applyPreprDecisions(value);
|
|
2547
2583
|
remember();
|
|
2548
2584
|
}
|
|
2549
2585
|
});
|
|
@@ -23,15 +23,17 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
|
|
|
23
23
|
*/
|
|
24
24
|
declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Gets each client-side navigation's decision in before the click: when the
|
|
27
|
+
* visitor hovers over a same-site link (or presses it, on touch devices), it
|
|
28
|
+
* asks `createPreprProxy` which variants they get on that page. Clicks are
|
|
29
|
+
* never held: the new page paints with those variants when the answer is in,
|
|
30
|
+
* and keeps its personalized sections hidden until then otherwise. Pages
|
|
31
|
+
* without personalization answer with no groups and are not asked again for
|
|
32
|
+
* an hour.
|
|
31
33
|
*
|
|
32
34
|
* Mount it once in the root layout, so links on pages without a
|
|
33
|
-
* `PreprStack` are covered too.
|
|
34
|
-
*
|
|
35
|
+
* `PreprStack` are covered too. Links to other sites and links that open a
|
|
36
|
+
* new tab or download are left alone.
|
|
35
37
|
*
|
|
36
38
|
* @example
|
|
37
39
|
* <PreprPersonalizationSwitch />
|
|
@@ -39,12 +41,11 @@ declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps)
|
|
|
39
41
|
declare function PreprPersonalizationSwitch(): null;
|
|
40
42
|
/**
|
|
41
43
|
* Applies this page view's decision to the variant wrappers of the
|
|
42
|
-
* `PreprStack` it is rendered by
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `PreprStack`; you do not need it yourself.
|
|
44
|
+
* `PreprStack` it is rendered by, before the page paints when the decision is
|
|
45
|
+
* already in: asked for on hover, or kept from an earlier view of the page.
|
|
46
|
+
* Back and Forward show the page as this tab last showed it. Any other view
|
|
47
|
+
* keeps its personalized sections hidden until the decision request answers.
|
|
48
|
+
* Rendered by `PreprStack`; you do not need it yourself.
|
|
48
49
|
*/
|
|
49
50
|
declare function PreprDecisionApplier(): null;
|
|
50
51
|
/**
|
|
@@ -23,15 +23,17 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
|
|
|
23
23
|
*/
|
|
24
24
|
declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Gets each client-side navigation's decision in before the click: when the
|
|
27
|
+
* visitor hovers over a same-site link (or presses it, on touch devices), it
|
|
28
|
+
* asks `createPreprProxy` which variants they get on that page. Clicks are
|
|
29
|
+
* never held: the new page paints with those variants when the answer is in,
|
|
30
|
+
* and keeps its personalized sections hidden until then otherwise. Pages
|
|
31
|
+
* without personalization answer with no groups and are not asked again for
|
|
32
|
+
* an hour.
|
|
31
33
|
*
|
|
32
34
|
* Mount it once in the root layout, so links on pages without a
|
|
33
|
-
* `PreprStack` are covered too.
|
|
34
|
-
*
|
|
35
|
+
* `PreprStack` are covered too. Links to other sites and links that open a
|
|
36
|
+
* new tab or download are left alone.
|
|
35
37
|
*
|
|
36
38
|
* @example
|
|
37
39
|
* <PreprPersonalizationSwitch />
|
|
@@ -39,12 +41,11 @@ declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps)
|
|
|
39
41
|
declare function PreprPersonalizationSwitch(): null;
|
|
40
42
|
/**
|
|
41
43
|
* Applies this page view's decision to the variant wrappers of the
|
|
42
|
-
* `PreprStack` it is rendered by
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `PreprStack`; you do not need it yourself.
|
|
44
|
+
* `PreprStack` it is rendered by, before the page paints when the decision is
|
|
45
|
+
* already in: asked for on hover, or kept from an earlier view of the page.
|
|
46
|
+
* Back and Forward show the page as this tab last showed it. Any other view
|
|
47
|
+
* keeps its personalized sections hidden until the decision request answers.
|
|
48
|
+
* Rendered by `PreprStack`; you do not need it yourself.
|
|
48
49
|
*/
|
|
49
50
|
declare function PreprDecisionApplier(): null;
|
|
50
51
|
/**
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createPreprPreview,
|
|
4
4
|
loadTrackingPixel
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-2QGUP7VQ.js";
|
|
6
6
|
import "../chunk-S5RWVMDJ.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-ATPB7GEC.js";
|
|
8
8
|
|
|
9
9
|
// src/nextjs/components.ts
|
|
10
10
|
import { usePathname, useRouter } from "next/navigation";
|
|
@@ -64,6 +64,70 @@ function applyPreprDecisions(decision) {
|
|
|
64
64
|
});
|
|
65
65
|
return true;
|
|
66
66
|
}
|
|
67
|
+
function requestPreprDecision(key, options = {}) {
|
|
68
|
+
const fresh = 1e4;
|
|
69
|
+
const keep = 18e5;
|
|
70
|
+
const keepStatic = 36e5;
|
|
71
|
+
const state = window;
|
|
72
|
+
const asking = state.__preprAsking = state.__preprAsking || {};
|
|
73
|
+
const read = () => {
|
|
74
|
+
try {
|
|
75
|
+
const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
|
|
76
|
+
return all && typeof all === "object" ? all : {};
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const ask = () => {
|
|
82
|
+
if (!asking[key]) {
|
|
83
|
+
asking[key] = fetch(key, {
|
|
84
|
+
headers: { "Prepr-Decide": "1" },
|
|
85
|
+
cache: "no-store",
|
|
86
|
+
credentials: "same-origin",
|
|
87
|
+
// The proxy gives up on Prepr after its own `timeoutMs`; this only
|
|
88
|
+
// covers a proxy that never answers.
|
|
89
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
|
|
90
|
+
}).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
|
|
91
|
+
delete asking[key];
|
|
92
|
+
try {
|
|
93
|
+
const all = read();
|
|
94
|
+
const at = Date.now();
|
|
95
|
+
Object.keys(all).forEach((stored) => {
|
|
96
|
+
const entry = all[stored];
|
|
97
|
+
if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
|
|
98
|
+
delete all[stored];
|
|
99
|
+
});
|
|
100
|
+
const kept = all[key];
|
|
101
|
+
if (answer !== "*" || !kept || kept.value === "*")
|
|
102
|
+
all[key] = { value: answer, at };
|
|
103
|
+
sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
return answer;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return asking[key];
|
|
110
|
+
};
|
|
111
|
+
const last = read()[key];
|
|
112
|
+
const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
|
|
113
|
+
const value = last && typeof last.value === "string" ? last.value : null;
|
|
114
|
+
if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
|
|
115
|
+
if (value !== "" && age >= fresh) {
|
|
116
|
+
ask().then((answer) => {
|
|
117
|
+
if (answer !== "*" && answer !== value && options.onChange)
|
|
118
|
+
options.onChange(answer);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return { result: value, value: Promise.resolve(value) };
|
|
122
|
+
}
|
|
123
|
+
const pending = {
|
|
124
|
+
value: ask()
|
|
125
|
+
};
|
|
126
|
+
pending.value.then((answer) => {
|
|
127
|
+
pending.result = answer;
|
|
128
|
+
});
|
|
129
|
+
return pending;
|
|
130
|
+
}
|
|
67
131
|
|
|
68
132
|
// src/react/components.ts
|
|
69
133
|
import { useEffect } from "react";
|
|
@@ -100,35 +164,21 @@ function PreprToolbar({
|
|
|
100
164
|
}, []);
|
|
101
165
|
return null;
|
|
102
166
|
}
|
|
103
|
-
var DECIDE_HEADER = "Prepr-Decide";
|
|
104
|
-
var DECIDE_TIMEOUT_MS = 6e3;
|
|
105
|
-
function requestDecision(key) {
|
|
106
|
-
const pending = { key, value: null };
|
|
107
|
-
pending.value = fetch(key, {
|
|
108
|
-
headers: { [DECIDE_HEADER]: "1" },
|
|
109
|
-
cache: "no-store",
|
|
110
|
-
credentials: "same-origin",
|
|
111
|
-
signal: AbortSignal.timeout(DECIDE_TIMEOUT_MS)
|
|
112
|
-
}).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((value) => {
|
|
113
|
-
pending.result = value;
|
|
114
|
-
try {
|
|
115
|
-
sessionStorage.setItem(
|
|
116
|
-
"prepr_decision",
|
|
117
|
-
JSON.stringify({ key, value, at: Date.now() })
|
|
118
|
-
);
|
|
119
|
-
} catch {
|
|
120
|
-
}
|
|
121
|
-
return value;
|
|
122
|
-
});
|
|
123
|
-
return pending;
|
|
124
|
-
}
|
|
125
|
-
var pressed = null;
|
|
126
|
-
var replaying = null;
|
|
127
167
|
var shown = /* @__PURE__ */ new Map();
|
|
128
168
|
var historyStep = false;
|
|
129
169
|
var onPopState = () => {
|
|
130
170
|
historyStep = true;
|
|
131
171
|
};
|
|
172
|
+
function requestDecision(key, personalized = false) {
|
|
173
|
+
return requestPreprDecision(key, {
|
|
174
|
+
personalized,
|
|
175
|
+
onChange: (value) => {
|
|
176
|
+
if (location.pathname + location.search !== key) return;
|
|
177
|
+
applyPreprDecisions(value);
|
|
178
|
+
shown.set(key, value);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
132
182
|
function linkTarget(event) {
|
|
133
183
|
const anchor = event.target?.closest?.("a[href]");
|
|
134
184
|
if (!(anchor instanceof HTMLAnchorElement)) return null;
|
|
@@ -140,55 +190,37 @@ function linkTarget(event) {
|
|
|
140
190
|
if (url.origin !== location.origin || key === location.pathname + location.search) {
|
|
141
191
|
return null;
|
|
142
192
|
}
|
|
143
|
-
return
|
|
193
|
+
return key;
|
|
194
|
+
}
|
|
195
|
+
var HOVER_DELAY_MS = 65;
|
|
196
|
+
var hovering = null;
|
|
197
|
+
function onPointerOver(event) {
|
|
198
|
+
if (event.pointerType !== "mouse") return;
|
|
199
|
+
const key = linkTarget(event);
|
|
200
|
+
if (hovering?.key === key) return;
|
|
201
|
+
if (hovering) clearTimeout(hovering.timer);
|
|
202
|
+
hovering = key ? { key, timer: setTimeout(() => requestDecision(key), HOVER_DELAY_MS) } : null;
|
|
144
203
|
}
|
|
145
204
|
function onPointerDown(event) {
|
|
146
205
|
if (event.button !== 0) return;
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
pressed = requestDecision(target.key);
|
|
152
|
-
}
|
|
153
|
-
function onClick(event) {
|
|
154
|
-
const target = linkTarget(event);
|
|
155
|
-
if (!target) return;
|
|
156
|
-
if (replaying === target.anchor) {
|
|
157
|
-
replaying = null;
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const pending = pressed && pressed.key === target.key ? pressed : requestDecision(target.key);
|
|
164
|
-
pressed = null;
|
|
165
|
-
const record = (value) => {
|
|
166
|
-
window.__preprDecision = { key: target.key, value };
|
|
167
|
-
};
|
|
168
|
-
if (pending.result !== void 0) {
|
|
169
|
-
record(pending.result);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
event.preventDefault();
|
|
173
|
-
event.stopImmediatePropagation();
|
|
174
|
-
void pending.value.then((value) => {
|
|
175
|
-
record(value);
|
|
176
|
-
replaying = target.anchor;
|
|
177
|
-
target.anchor.click();
|
|
178
|
-
});
|
|
206
|
+
const key = linkTarget(event);
|
|
207
|
+
if (key) requestDecision(key);
|
|
179
208
|
}
|
|
180
209
|
var listeners = 0;
|
|
181
210
|
function useDecisionRequests() {
|
|
182
211
|
useEffect2(() => {
|
|
183
212
|
if (listeners++ === 0) {
|
|
184
|
-
window.addEventListener("
|
|
185
|
-
window.addEventListener("
|
|
213
|
+
window.addEventListener("pointerover", onPointerOver, { passive: true });
|
|
214
|
+
window.addEventListener("pointerdown", onPointerDown, {
|
|
215
|
+
capture: true,
|
|
216
|
+
passive: true
|
|
217
|
+
});
|
|
186
218
|
window.addEventListener("popstate", onPopState);
|
|
187
219
|
}
|
|
188
220
|
return () => {
|
|
189
221
|
if (--listeners === 0) {
|
|
222
|
+
window.removeEventListener("pointerover", onPointerOver);
|
|
190
223
|
window.removeEventListener("pointerdown", onPointerDown, true);
|
|
191
|
-
window.removeEventListener("click", onClick, true);
|
|
192
224
|
window.removeEventListener("popstate", onPopState);
|
|
193
225
|
}
|
|
194
226
|
};
|
|
@@ -212,12 +244,16 @@ function PreprDecisionApplier() {
|
|
|
212
244
|
remember();
|
|
213
245
|
return;
|
|
214
246
|
}
|
|
247
|
+
const pending = requestDecision(key, true);
|
|
248
|
+
if (pending.result !== void 0) {
|
|
249
|
+
applyPreprDecisions(pending.result);
|
|
250
|
+
remember();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
215
253
|
let live = true;
|
|
216
|
-
|
|
217
|
-
const value = started?.key === key ? started.value : requestDecision(key).value;
|
|
218
|
-
void value.then((value2) => {
|
|
254
|
+
void pending.value.then((value) => {
|
|
219
255
|
if (live && location.pathname + location.search === key) {
|
|
220
|
-
applyPreprDecisions(
|
|
256
|
+
applyPreprDecisions(value);
|
|
221
257
|
remember();
|
|
222
258
|
}
|
|
223
259
|
});
|
package/dist/nextjs/graphql.cjs
CHANGED
|
@@ -101,7 +101,7 @@ function preprCacheTags(data) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// src/version.ts
|
|
104
|
-
var VERSION = "0.6.0-alpha.
|
|
104
|
+
var VERSION = "0.6.0-alpha.8";
|
|
105
105
|
|
|
106
106
|
// src/core/features.ts
|
|
107
107
|
function isEnabled(config) {
|
|
@@ -1480,35 +1480,82 @@ function applyPreprDecisions(decision) {
|
|
|
1480
1480
|
});
|
|
1481
1481
|
return true;
|
|
1482
1482
|
}
|
|
1483
|
-
function
|
|
1483
|
+
function requestPreprDecision(key, options = {}) {
|
|
1484
|
+
const fresh = 1e4;
|
|
1485
|
+
const keep = 18e5;
|
|
1486
|
+
const keepStatic = 36e5;
|
|
1484
1487
|
const state = window;
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
let last;
|
|
1488
|
-
try {
|
|
1489
|
-
last = JSON.parse(sessionStorage.getItem("prepr_decision") || "{}");
|
|
1490
|
-
} catch {
|
|
1491
|
-
last = {};
|
|
1492
|
-
}
|
|
1493
|
-
const age = Date.now() - (last.at || 0);
|
|
1494
|
-
const value = last.key === key && typeof last.value === "string" && age >= 0 && age < 1e4 ? Promise.resolve(last.value) : fetch(key, {
|
|
1495
|
-
headers: { "Prepr-Decide": "1" },
|
|
1496
|
-
cache: "no-store",
|
|
1497
|
-
credentials: "same-origin"
|
|
1498
|
-
}).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
|
|
1488
|
+
const asking = state.__preprAsking = state.__preprAsking || {};
|
|
1489
|
+
const read = () => {
|
|
1499
1490
|
try {
|
|
1500
|
-
sessionStorage.
|
|
1501
|
-
|
|
1502
|
-
JSON.stringify({ key, value: answer, at: Date.now() })
|
|
1503
|
-
);
|
|
1491
|
+
const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
|
|
1492
|
+
return all && typeof all === "object" ? all : {};
|
|
1504
1493
|
} catch {
|
|
1494
|
+
return {};
|
|
1505
1495
|
}
|
|
1506
|
-
|
|
1496
|
+
};
|
|
1497
|
+
const ask = () => {
|
|
1498
|
+
if (!asking[key]) {
|
|
1499
|
+
asking[key] = fetch(key, {
|
|
1500
|
+
headers: { "Prepr-Decide": "1" },
|
|
1501
|
+
cache: "no-store",
|
|
1502
|
+
credentials: "same-origin",
|
|
1503
|
+
// The proxy gives up on Prepr after its own `timeoutMs`; this only
|
|
1504
|
+
// covers a proxy that never answers.
|
|
1505
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
|
|
1506
|
+
}).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
|
|
1507
|
+
delete asking[key];
|
|
1508
|
+
try {
|
|
1509
|
+
const all = read();
|
|
1510
|
+
const at = Date.now();
|
|
1511
|
+
Object.keys(all).forEach((stored) => {
|
|
1512
|
+
const entry = all[stored];
|
|
1513
|
+
if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
|
|
1514
|
+
delete all[stored];
|
|
1515
|
+
});
|
|
1516
|
+
const kept = all[key];
|
|
1517
|
+
if (answer !== "*" || !kept || kept.value === "*")
|
|
1518
|
+
all[key] = { value: answer, at };
|
|
1519
|
+
sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
|
|
1520
|
+
} catch {
|
|
1521
|
+
}
|
|
1522
|
+
return answer;
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
return asking[key];
|
|
1526
|
+
};
|
|
1527
|
+
const last = read()[key];
|
|
1528
|
+
const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
|
|
1529
|
+
const value = last && typeof last.value === "string" ? last.value : null;
|
|
1530
|
+
if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
|
|
1531
|
+
if (value !== "" && age >= fresh) {
|
|
1532
|
+
ask().then((answer) => {
|
|
1533
|
+
if (answer !== "*" && answer !== value && options.onChange)
|
|
1534
|
+
options.onChange(answer);
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
return { result: value, value: Promise.resolve(value) };
|
|
1538
|
+
}
|
|
1539
|
+
const pending = {
|
|
1540
|
+
value: ask()
|
|
1541
|
+
};
|
|
1542
|
+
pending.value.then((answer) => {
|
|
1543
|
+
pending.result = answer;
|
|
1507
1544
|
});
|
|
1508
|
-
|
|
1509
|
-
|
|
1545
|
+
return pending;
|
|
1546
|
+
}
|
|
1547
|
+
function startPreprDecision() {
|
|
1548
|
+
const state = window;
|
|
1549
|
+
const key = location.pathname + location.search;
|
|
1550
|
+
const apply = (answer) => {
|
|
1510
1551
|
if (location.pathname + location.search === key) state.__preprApply(answer);
|
|
1552
|
+
};
|
|
1553
|
+
const pending = state.__preprRequest(key, {
|
|
1554
|
+
onChange: apply,
|
|
1555
|
+
personalized: true
|
|
1511
1556
|
});
|
|
1557
|
+
if (pending.result !== void 0) apply(pending.result);
|
|
1558
|
+
else pending.value.then(apply);
|
|
1512
1559
|
}
|
|
1513
1560
|
|
|
1514
1561
|
// src/nextjs/stack.ts
|
|
@@ -1518,7 +1565,7 @@ var DEFAULT_ATTRIBUTE = "data-prepr-default";
|
|
|
1518
1565
|
var NESTED_ATTRIBUTE = "data-prepr-nested";
|
|
1519
1566
|
var GROUP_BASE_CSS = `[${VARIANT_ATTRIBUTE}]{display:none}[${VARIANT_ATTRIBUTE}][data-prepr-show]{display:contents}[data-prepr-decision]:not([data-prepr-decided])~[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents;visibility:hidden}`;
|
|
1520
1567
|
var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
|
|
1521
|
-
var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};(${startPreprDecision.toString()})();`;
|
|
1568
|
+
var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
|
|
1522
1569
|
var APPLY_SCRIPT = "__preprApply()";
|
|
1523
1570
|
function variantKeysOf(items) {
|
|
1524
1571
|
return items.flatMap((item) => {
|
|
@@ -326,7 +326,7 @@ interface PreprProxyOptions {
|
|
|
326
326
|
*
|
|
327
327
|
* Pages themselves never wait for Prepr: they are the same for every visitor
|
|
328
328
|
* and pass straight through. The browser sends a decision request for each
|
|
329
|
-
* page view, from the page load and when a visitor
|
|
329
|
+
* page view, from the page load and when a visitor points at a link; the proxy
|
|
330
330
|
* answers it with the visitor's variant of every A/B test and personalization
|
|
331
331
|
* group on that page, asked from Prepr in one request, as text and no page.
|
|
332
332
|
* Add your own cookies or headers to the response it returns.
|
|
@@ -398,10 +398,10 @@ interface PreprStackProps<T> {
|
|
|
398
398
|
* stack or nested inside an item, renders every variant, hidden. The page
|
|
399
399
|
* is the same for every visitor and never waits for Prepr: an inline script
|
|
400
400
|
* asks `createPreprProxy` for this page view's decision while the page
|
|
401
|
-
* loads, and shows the visitor's variant when it answers.
|
|
402
|
-
*
|
|
403
|
-
* next page paints. Nothing streams, and the
|
|
404
|
-
* first to be replaced.
|
|
401
|
+
* loads, and shows the visitor's variant when it answers.
|
|
402
|
+
* `PreprPersonalizationSwitch` asks for a link's decision on hover, so it
|
|
403
|
+
* is mostly in before the next page paints. Nothing streams, and the
|
|
404
|
+
* default variant never shows first to be replaced.
|
|
405
405
|
*
|
|
406
406
|
* The page carries the HTML of every variant: an item with nested groups
|
|
407
407
|
* repeats once per combination of their variants.
|
package/dist/nextjs/graphql.d.ts
CHANGED
|
@@ -326,7 +326,7 @@ interface PreprProxyOptions {
|
|
|
326
326
|
*
|
|
327
327
|
* Pages themselves never wait for Prepr: they are the same for every visitor
|
|
328
328
|
* and pass straight through. The browser sends a decision request for each
|
|
329
|
-
* page view, from the page load and when a visitor
|
|
329
|
+
* page view, from the page load and when a visitor points at a link; the proxy
|
|
330
330
|
* answers it with the visitor's variant of every A/B test and personalization
|
|
331
331
|
* group on that page, asked from Prepr in one request, as text and no page.
|
|
332
332
|
* Add your own cookies or headers to the response it returns.
|
|
@@ -398,10 +398,10 @@ interface PreprStackProps<T> {
|
|
|
398
398
|
* stack or nested inside an item, renders every variant, hidden. The page
|
|
399
399
|
* is the same for every visitor and never waits for Prepr: an inline script
|
|
400
400
|
* asks `createPreprProxy` for this page view's decision while the page
|
|
401
|
-
* loads, and shows the visitor's variant when it answers.
|
|
402
|
-
*
|
|
403
|
-
* next page paints. Nothing streams, and the
|
|
404
|
-
* first to be replaced.
|
|
401
|
+
* loads, and shows the visitor's variant when it answers.
|
|
402
|
+
* `PreprPersonalizationSwitch` asks for a link's decision on hover, so it
|
|
403
|
+
* is mostly in before the next page paints. Nothing streams, and the
|
|
404
|
+
* default variant never shows first to be replaced.
|
|
405
405
|
*
|
|
406
406
|
* The page carries the HTML of every variant: an item with nested groups
|
|
407
407
|
* repeats once per combination of their variants.
|
package/dist/nextjs/graphql.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
preprModelTag,
|
|
12
12
|
preprModelTagsFor,
|
|
13
13
|
validatePreprToken
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-O2SBSBDG.js";
|
|
15
15
|
|
|
16
16
|
// src/nextjs/graphql.ts
|
|
17
17
|
import * as nextCache from "next/cache";
|
|
@@ -1067,35 +1067,82 @@ function applyPreprDecisions(decision) {
|
|
|
1067
1067
|
});
|
|
1068
1068
|
return true;
|
|
1069
1069
|
}
|
|
1070
|
-
function
|
|
1070
|
+
function requestPreprDecision(key, options = {}) {
|
|
1071
|
+
const fresh = 1e4;
|
|
1072
|
+
const keep = 18e5;
|
|
1073
|
+
const keepStatic = 36e5;
|
|
1071
1074
|
const state = window;
|
|
1072
|
-
const
|
|
1073
|
-
|
|
1074
|
-
let last;
|
|
1075
|
-
try {
|
|
1076
|
-
last = JSON.parse(sessionStorage.getItem("prepr_decision") || "{}");
|
|
1077
|
-
} catch {
|
|
1078
|
-
last = {};
|
|
1079
|
-
}
|
|
1080
|
-
const age = Date.now() - (last.at || 0);
|
|
1081
|
-
const value = last.key === key && typeof last.value === "string" && age >= 0 && age < 1e4 ? Promise.resolve(last.value) : fetch(key, {
|
|
1082
|
-
headers: { "Prepr-Decide": "1" },
|
|
1083
|
-
cache: "no-store",
|
|
1084
|
-
credentials: "same-origin"
|
|
1085
|
-
}).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
|
|
1075
|
+
const asking = state.__preprAsking = state.__preprAsking || {};
|
|
1076
|
+
const read = () => {
|
|
1086
1077
|
try {
|
|
1087
|
-
sessionStorage.
|
|
1088
|
-
|
|
1089
|
-
JSON.stringify({ key, value: answer, at: Date.now() })
|
|
1090
|
-
);
|
|
1078
|
+
const all = JSON.parse(sessionStorage.getItem("prepr_decisions") || "{}");
|
|
1079
|
+
return all && typeof all === "object" ? all : {};
|
|
1091
1080
|
} catch {
|
|
1081
|
+
return {};
|
|
1092
1082
|
}
|
|
1093
|
-
|
|
1083
|
+
};
|
|
1084
|
+
const ask = () => {
|
|
1085
|
+
if (!asking[key]) {
|
|
1086
|
+
asking[key] = fetch(key, {
|
|
1087
|
+
headers: { "Prepr-Decide": "1" },
|
|
1088
|
+
cache: "no-store",
|
|
1089
|
+
credentials: "same-origin",
|
|
1090
|
+
// The proxy gives up on Prepr after its own `timeoutMs`; this only
|
|
1091
|
+
// covers a proxy that never answers.
|
|
1092
|
+
signal: AbortSignal.timeout ? AbortSignal.timeout(6e3) : void 0
|
|
1093
|
+
}).then((response) => response.ok ? response.text() : "*").catch(() => "*").then((answer) => {
|
|
1094
|
+
delete asking[key];
|
|
1095
|
+
try {
|
|
1096
|
+
const all = read();
|
|
1097
|
+
const at = Date.now();
|
|
1098
|
+
Object.keys(all).forEach((stored) => {
|
|
1099
|
+
const entry = all[stored];
|
|
1100
|
+
if (!(entry && typeof entry.at === "number" && at - entry.at < keepStatic))
|
|
1101
|
+
delete all[stored];
|
|
1102
|
+
});
|
|
1103
|
+
const kept = all[key];
|
|
1104
|
+
if (answer !== "*" || !kept || kept.value === "*")
|
|
1105
|
+
all[key] = { value: answer, at };
|
|
1106
|
+
sessionStorage.setItem("prepr_decisions", JSON.stringify(all));
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
return answer;
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
return asking[key];
|
|
1113
|
+
};
|
|
1114
|
+
const last = read()[key];
|
|
1115
|
+
const age = last && typeof last.at === "number" ? Date.now() - last.at : -1;
|
|
1116
|
+
const value = last && typeof last.value === "string" ? last.value : null;
|
|
1117
|
+
if (value !== null && !(value === "" && options.personalized) && age >= 0 && age < (value === "*" ? fresh : value === "" ? keepStatic : keep)) {
|
|
1118
|
+
if (value !== "" && age >= fresh) {
|
|
1119
|
+
ask().then((answer) => {
|
|
1120
|
+
if (answer !== "*" && answer !== value && options.onChange)
|
|
1121
|
+
options.onChange(answer);
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
return { result: value, value: Promise.resolve(value) };
|
|
1125
|
+
}
|
|
1126
|
+
const pending = {
|
|
1127
|
+
value: ask()
|
|
1128
|
+
};
|
|
1129
|
+
pending.value.then((answer) => {
|
|
1130
|
+
pending.result = answer;
|
|
1094
1131
|
});
|
|
1095
|
-
|
|
1096
|
-
|
|
1132
|
+
return pending;
|
|
1133
|
+
}
|
|
1134
|
+
function startPreprDecision() {
|
|
1135
|
+
const state = window;
|
|
1136
|
+
const key = location.pathname + location.search;
|
|
1137
|
+
const apply = (answer) => {
|
|
1097
1138
|
if (location.pathname + location.search === key) state.__preprApply(answer);
|
|
1139
|
+
};
|
|
1140
|
+
const pending = state.__preprRequest(key, {
|
|
1141
|
+
onChange: apply,
|
|
1142
|
+
personalized: true
|
|
1098
1143
|
});
|
|
1144
|
+
if (pending.result !== void 0) apply(pending.result);
|
|
1145
|
+
else pending.value.then(apply);
|
|
1099
1146
|
}
|
|
1100
1147
|
|
|
1101
1148
|
// src/nextjs/stack.ts
|
|
@@ -1105,7 +1152,7 @@ var DEFAULT_ATTRIBUTE = "data-prepr-default";
|
|
|
1105
1152
|
var NESTED_ATTRIBUTE = "data-prepr-nested";
|
|
1106
1153
|
var GROUP_BASE_CSS = `[${VARIANT_ATTRIBUTE}]{display:none}[${VARIANT_ATTRIBUTE}][data-prepr-show]{display:contents}[data-prepr-decision]:not([data-prepr-decided])~[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents;visibility:hidden}`;
|
|
1107
1154
|
var NOSCRIPT_HTML = `<style>[${VARIANT_ATTRIBUTE}][${DEFAULT_ATTRIBUTE}]{display:contents!important;visibility:visible!important}</style>`;
|
|
1108
|
-
var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};(${startPreprDecision.toString()})();`;
|
|
1155
|
+
var DECISION_SCRIPT = `window.__preprApply=${applyPreprDecisions.toString()};window.__preprRequest=${requestPreprDecision.toString()};(${startPreprDecision.toString()})();`;
|
|
1109
1156
|
var APPLY_SCRIPT = "__preprApply()";
|
|
1110
1157
|
function variantKeysOf(items) {
|
|
1111
1158
|
return items.flatMap((item) => {
|
package/dist/nextjs/index.cjs
CHANGED
|
@@ -43,7 +43,7 @@ module.exports = __toCommonJS(nextjs_exports);
|
|
|
43
43
|
var import_server = require("next/server");
|
|
44
44
|
|
|
45
45
|
// src/version.ts
|
|
46
|
-
var VERSION = "0.6.0-alpha.
|
|
46
|
+
var VERSION = "0.6.0-alpha.8";
|
|
47
47
|
|
|
48
48
|
// src/core/constants.ts
|
|
49
49
|
var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
|
package/dist/nextjs/index.d.cts
CHANGED
|
@@ -175,15 +175,17 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
|
|
|
175
175
|
*/
|
|
176
176
|
declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
|
|
177
177
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
178
|
+
* Gets each client-side navigation's decision in before the click: when the
|
|
179
|
+
* visitor hovers over a same-site link (or presses it, on touch devices), it
|
|
180
|
+
* asks `createPreprProxy` which variants they get on that page. Clicks are
|
|
181
|
+
* never held: the new page paints with those variants when the answer is in,
|
|
182
|
+
* and keeps its personalized sections hidden until then otherwise. Pages
|
|
183
|
+
* without personalization answer with no groups and are not asked again for
|
|
184
|
+
* an hour.
|
|
183
185
|
*
|
|
184
186
|
* Mount it once in the root layout, so links on pages without a
|
|
185
|
-
* `PreprStack` are covered too.
|
|
186
|
-
*
|
|
187
|
+
* `PreprStack` are covered too. Links to other sites and links that open a
|
|
188
|
+
* new tab or download are left alone.
|
|
187
189
|
*
|
|
188
190
|
* @example
|
|
189
191
|
* <PreprPersonalizationSwitch />
|
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -175,15 +175,17 @@ declare function PreprTrackingPixel({ id, config, }: PreprTrackingPixelProps): n
|
|
|
175
175
|
*/
|
|
176
176
|
declare function PreprToolbar({ options, ...props }: PreprToolbarComponentProps): null;
|
|
177
177
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
178
|
+
* Gets each client-side navigation's decision in before the click: when the
|
|
179
|
+
* visitor hovers over a same-site link (or presses it, on touch devices), it
|
|
180
|
+
* asks `createPreprProxy` which variants they get on that page. Clicks are
|
|
181
|
+
* never held: the new page paints with those variants when the answer is in,
|
|
182
|
+
* and keeps its personalized sections hidden until then otherwise. Pages
|
|
183
|
+
* without personalization answer with no groups and are not asked again for
|
|
184
|
+
* an hour.
|
|
183
185
|
*
|
|
184
186
|
* Mount it once in the root layout, so links on pages without a
|
|
185
|
-
* `PreprStack` are covered too.
|
|
186
|
-
*
|
|
187
|
+
* `PreprStack` are covered too. Links to other sites and links that open a
|
|
188
|
+
* new tab or download are left alone.
|
|
187
189
|
*
|
|
188
190
|
* @example
|
|
189
191
|
* <PreprPersonalizationSwitch />
|
package/dist/nextjs/index.js
CHANGED
package/dist/nuxt/graphql.js
CHANGED
package/dist/nuxt/index.cjs
CHANGED
package/dist/nuxt/index.js
CHANGED
|
@@ -2313,7 +2313,7 @@ function isReactHydrated(text) {
|
|
|
2313
2313
|
}
|
|
2314
2314
|
|
|
2315
2315
|
// src/version.ts
|
|
2316
|
-
var VERSION = "0.6.0-alpha.
|
|
2316
|
+
var VERSION = "0.6.0-alpha.8";
|
|
2317
2317
|
|
|
2318
2318
|
// src/core/pixel.ts
|
|
2319
2319
|
var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
|
package/dist/react/components.js
CHANGED
|
@@ -2289,7 +2289,7 @@ function isReactHydrated(text) {
|
|
|
2289
2289
|
}
|
|
2290
2290
|
|
|
2291
2291
|
// src/version.ts
|
|
2292
|
-
var VERSION = "0.6.0-alpha.
|
|
2292
|
+
var VERSION = "0.6.0-alpha.8";
|
|
2293
2293
|
|
|
2294
2294
|
// src/core/pixel.ts
|
|
2295
2295
|
var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
|
package/dist/sveltekit/index.cjs
CHANGED
package/dist/sveltekit/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preprio/toolkit",
|
|
3
|
-
"version": "0.6.0-alpha.
|
|
3
|
+
"version": "0.6.0-alpha.8",
|
|
4
4
|
"description": "Prepr personalization, A/B testing and preview toolkit for React, Next.js, Nuxt, Astro, SvelteKit and Express",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Prepr B.V. (https://prepr.io)",
|