@decideflow/sdk
v1.3.1
Published
Lightweight browser SDK for the **pricing-page hesitation** MVP: one detected state (`hesitation`), **one random group per session** after detection (4-arm: `control` / `help_choose` / `reduce_friction` / `clarify_value`), **at most one intervention** per
Downloads
940
Readme
@decideflow/sdk
Lightweight browser SDK for the pricing-page hesitation MVP: one detected state (hesitation), one random group per session after detection (4-arm: control / help_choose / reduce_friction / clarify_value), at most one intervention per session (host-rendered), and batched analytics to POST /v1/events/batch.
Pilot install: copy-paste snippets and DOM contract live in the repo root PILOT_INSTALL.md and docs/PILOT_SDK_USAGE.md.
Install
npm install @decideflow/sdkWorkspace / monorepo: depend on "@decideflow/sdk": "*" like apps/demo-site. Published installs resolve @decideflow/shared for shared types.
Quick start (ESM)
import { initDecisionEngine } from "@decideflow/sdk";
const engine = initDecisionEngine({
siteKey: "demo",
apiBaseUrl: "https://api.example.com",
pricingSectionSelector: "[data-clp-pricing]",
planCardSelector: "[data-clp-plan]",
ctaSelector: "[data-clp-pricing-cta]",
thresholds: {
minTimeOnPageMs: 12_000,
minTimeBeforeAnyTriggerMs: 8_000,
minIdleMs: 4_000,
minScrollStallMs: 5_000,
pricingSeenPercent: 30,
exitIntentTopPx: 24,
},
weights: { control: 24, help_choose: 0, reduce_friction: 38, clarify_value: 38 },
debug: false,
/* Optional: fixed top-left overlay with session id, storage flags, gates (host usually gates on env). */
// debugOverlay: process.env.NEXT_PUBLIC_CLP_SDK_DEBUG_OVERLAY === "1",
});
engine.on("hesitation", ({ reason }) => {
/* analytics hook */
});
engine.on("assigned_group", ({ group }) => {});
engine.on("show_help_choose", ({ group, copy }) => {
/* render your modal */
});
engine.on("show_reduce_friction", ({ group, copy }) => {});
engine.on("show_clarify_value", ({ group, copy }) => {
/* render a short value-anchor (1 line, single CTA) */
});
engine.on("no_action", ({ group }) => {});
/* After mounting a fixed/docked intervention (not inline in the pricing column), scroll pricing into view if needed: */
queueMicrotask(() => engine.scrollPricingSectionIntoView());
// later
engine.destroy();Script tag (global IIFE)
After npm run build in this package, load dist/clp.global.js:
<script src="./clp.global.js"></script>
<script>
const engine = CLP.initDecisionEngine({
siteKey: "demo",
apiBaseUrl: "http://localhost:8080",
pricingSectionSelector: "[data-clp-pricing]",
planCardSelector: "[data-clp-plan]",
ctaSelector: "[data-clp-pricing-cta]",
});
engine.on("show_help_choose", function (d) {
console.log(d.copy.title);
});
</script>See also examples/plain.html in this package.
React
import { useEffect, useMemo } from "react";
import { usePricingHesitation } from "@decideflow/sdk/react";
export function Pricing() {
const config = useMemo(
() => ({
siteKey: "demo",
apiBaseUrl: process.env.NEXT_PUBLIC_SDK_API_URL!,
pricingSectionSelector: "[data-clp-pricing]",
planCardSelector: "[data-clp-plan]",
ctaSelector: "[data-clp-pricing-cta]",
}),
[],
);
const engine = usePricingHesitation(config);
useEffect(() => {
if (!engine) return;
return engine.on("show_help_choose", (d) => {
/* set state, open modal */
});
}, [engine]);
}Memoize config so the hook does not tear down the engine every render. See examples/react-example.tsx.
Public API
| Export / method | Purpose |
| --- | --- |
| initDecisionEngine(config) | Create engine; loads remote config from GET /v1/config, then starts if enabled. |
| engine.destroy() | Tear down listeners and timers; stop the engine. |
| engine.getSession() | { sessionId } for this visitor. |
| engine.getAssignedGroup() | control | help_choose | reduce_friction | clarify_value | null. |
| engine.on(event, handler) | Subscribe; returns off(). |
| engine.ready | Promise resolved when config fetch finishes (engine may still be inactive). |
| engine.getPricingSectionElement() | The pricing section element from pricingSectionSelector, if present. |
| engine.scrollPricingSectionIntoView(options?) | Scrolls the pricing section into view when it is mostly off-screen; respects prefers-reduced-motion. Use when the intervention UI is a fixed dock (bottom/side) rather than inline. |
| scrollPricingSectionIntoView(element, options?) (named export) | Same scroll helper without an engine instance. |
Intervention layout (host)
The SDK does not render intervention UI. For easier embedding, mount a single container (e.g. at the end of <body>) and use CSS so that on narrow viewports it is fixed to the bottom (sheet/bar) and on wide viewports fixed to the side (viewport edge or content column—inset as you prefer). Call scrollPricingSectionIntoView when the intervention appears so users still see the pricing block. See docs/PILOT_SDK_USAGE.md for the production contract and accessibility notes (aria-live, focus, safe-area insets).
Remote config
On init, the SDK loads GET /v1/config (server-validated JSON). Local options override merged fields:
- Weights:
config.weightsoverrides remoteweights. - Copy:
config.copyOverridesmerges resolved intervention copy (helpChoose/reduceFrictionobjects). - Thresholds:
config.thresholdsoverrides remotethresholds(milliseconds / percent).
If enabled === false or killSwitch === true, the SDK does nothing. With debug: true, optional debug logging may appear in the console.
Debug overlay (debugOverlay: true)
A fixed top-left panel that streams the live SDK state (sessionId, visitorId, gates, last eligibility result, queue length, …) plus two reload buttons:
- Reload (keep session) — normal
window.location.reload(). Same visitor, same session, same server-side cap. - New visitor + reload (debug) — clears the SDK's
sessionStoragekeys and the persistedclp_visitor_idso the next page load mints a fresh visitor. Without this, the server-side 24h per-visitor cap (keyed onclp_visitor_id) blocks every retest past the first show — the assigned arm re-rolls butinterventionShownnever flips. Available only whendebugOverlay: true(the underlyingengine.clearVisitorForDebug()is a no-op otherwise so production code that imports the SDK can't accidentally invoke it).
The overlay also sends X-CLP-Debug: 1 on the /v1/visitor/intervention-eligibility request. A backend with CLP_DEBUG_VISITOR_BYPASS=1 will honor it, skip the 24h cap, and return { eligible: true, bypassed: true, reason: "debug_bypass" }. This is the no-state-mutation path: leave clp_visitor_id alone and just retest. The bypass is double-gated (SDK build flag + server env) so production cap behavior is unchanged when either gate is off.
Host events
| Event | When |
| --- | --- |
| hesitation | User behavior matches configured hesitation criteria. |
| assigned_group | Randomized group for this session ({ group }). |
| show_help_choose | Show help-choose intervention ({ group, copy }). |
| show_reduce_friction | Show reduce-friction intervention ({ group, copy }). |
| show_clarify_value | Show clarify-value intervention — short value-anchor ({ group, copy }). |
| no_action | Control arm; do not show an intervention. |
Examples in this repo
examples/plain.html— static page + global bundle.examples/react-example.tsx— minimal hook usage.
