feat: bring-your-own decision engine — first-class support (#66) - #73
Open
ramboz wants to merge 12 commits into
Open
feat: bring-your-own decision engine — first-class support (#66)#73ramboz wants to merge 12 commits into
ramboz wants to merge 12 commits into
Conversation
Opt-in `rumTracking` plugin option so a bring-your-own decision engine that
already fires exposure server-side can avoid double counting:
- `'off'` suppresses the built-in RUM for all decision types
- a function receives `{ type, source, target }` instead of the built-in call
- unset keeps today's behavior (no change)
Also wires a `window.PLUGIN_OPTIONS` passthrough into the test harness so
fixtures can exercise the BYO config hooks, and documents the hook set in
DEFAULT_OPTIONS.
Part of #66.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opt-in `renderDecision(el, decision)` plugin option. When provided, the plugin
hands the target element and the normalized decision to the integrator instead
of the built-in fetch-URL → innerHTML, so an engine that returns JSON, a content
reference or an external-CMS id can apply the decision however it is shaped.
The decision is `{ type, scope, url, selector?, config }`. Routed through a new
`applyDecision` helper at both application sites (page/section and fragment); the
default fetch-and-replace behavior is unchanged when the hook is unset.
Part of #66.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opt-in `resolveAudiences(names, context)` plugin option: a batched,
context-aware resolver that answers every configured audience in a single call
(with a shared `{ url, consent }` context) instead of N argument-less
per-audience calls. It short-circuits the per-audience `audiences[key]()` path,
works without a per-audience registry, and a rejection falls back to control.
Also exports `createRemoteAudienceResolver({ endpoint, timeout })` — a memoized
remote resolver (one request per page) with a timeout → control fallback so a
slow or failing engine never blocks render — and a shared `getDecisionContext`.
Part of #66.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opt-in `getAssignment(experimentId, context)` plugin option. When it returns a variant, the plugin serves that arm and skips its own client-side randomization (`ued.evaluateDecisionPolicy`); sticky assignment and exposure stay the engine's responsibility. - a known variant is served as-is (no re-bucketing) - a falsy answer or an error falls back to the plugin's self-bucketing - an unknown variant serves control rather than re-randomizing against intent - the `?experiment=` QA override still wins - unset keeps today's self-bucketing behavior Part of #66. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opt-in async `listAudiences()` that advertises an engine's audience universe to the AEM Sidekick simulation panel at author time. In preview/dev the returned names are merged into `body[data-audiences]` (which the panel enumerates its switcher from), so a BYO project that registers only a generic remote resolver can still surface the engine's segments — enumeration, distinct from the membership resolution in #67. Author-time only; a no-op by default and never fetched in production. Part of #66. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New `src/contract.js` pins down the normalized shapes exchanged between the client hooks and an engine/worker — decision context, audience resolution, experiment assignment, and rendered decision — under a single versioned `DecisionResponse` envelope, with dependency-free validators usable on both ends (browser + edge/Node). Shared JSON fixtures and contract tests validate the shapes and exercise the client resolver against a contract-shaped response. Part of #66. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A reference edge worker for a bring-your-own decision engine integration: - src/core.js — framework-neutral core (plain data in/out; ports to any runtime) - src/cloudflare.js — Cloudflare Worker entry adapting the core - src/engine-stub.js — a deterministic fake engine so it runs standalone - wrangler.toml / package.json — config + wrangler dev/deploy scripts - README.md — setup, wiring to a real engine, and to the client resolver hook It hides the engine API key server-side, reads/mints a first-party HttpOnly visitor-id cookie, and emits decisions conforming to the versioned contract (#71). A Node-side test asserts conformance, sticky membership, and that the key never leaks to the client. Part of #66. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add documentation/byo-decision-engine.md covering all the BYO hooks (resolveAudiences + helper, getAssignment, rumTracking, renderDecision, listAudiences), the decision context, the versioned contract, and the reference worker. Link it from the README and list the hooks in the config reference. Part of #66. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The test raced the redirect two ways: it (a) asserted rumCalls[0] after only waiting for sampleRUM to *exist* (set in addInitScript) rather than for the exposure to actually fire, and (b) read body.innerText mid-navigation, which destroyed the execution context. Wait for the recorded RUM call (expect.poll), derive the expected landing state from the RUM target, and wait for the redirect to settle before asserting content. Pre-existing flake, independent of the BYO work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Add tests exercising the error and edge branches of the new BYO code that the happy-path tests missed (flagged by codecov patch coverage): - renderDecision throwing -> control - getAssignment returning an unknown variant -> control, and throwing -> self-bucket - listAudiences absent-in-preview / empty / throwing - isDecisionResponse rejecting non-objects and invalid assignment/decision facets contract.js is now 100% covered; every new src line in the PR is exercised. Part of #66. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The catalog only feeds the simulation panel's audience switcher, which loads lazily, so awaiting listAudiences() in loadEager needlessly blocked the eager/LCP path. Move it to loadLazy and make it fire-and-forget so a slow catalog can't stall panel setup either. Rewrite the catalog tests to drive the real loadLazy (the simulation.test.js pattern) and drop the now-unused loadEager-based fixtures. Part of #66. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…A overrides
resolveDecisions(entries, context) => { [selector]: { url } } is a first-class 'engine returns content per slot' lane, consuming the decisions facet already defined in src/contract.js. A decisions-manifest page metadata declares the slots (selectors); the plugin reads it, calls resolveDecisions once with all entries, and applies each returned { url } via renderDecision. Opt-in and a no-op unless both resolveDecisions and a decisions-manifest are configured; getManifestEntriesForCurrentPage gains an opt-in { requireUrl } (default keeps existing behavior).
Separately, the ?audience= / ?experiment= QA overrides now still invoke the configured resolveAudiences / getAssignment for their resolution side-effect — the override still wins the plugin's own selection, but a content-returning BYO engine has run and has content to render under a forced selection.
Both additive and no-op by default; existing audience/experiment/campaign lanes are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the "bring your own decision engine" (BYO) epic from #66: a set of opt-in, no-op-by-default hooks that let the plugin apply an external engine's decisions while the engine keeps ownership of segmentation, bucketing and exposure — plus the reference material to wire it up.
Built as one branch per the plan agreed on the issue: independent hooks first (convergence to a single provider left as a later internal refactor), with the contract and worker synthesized from the RFC.
Enablers
rumTracking: 'off' | fn— disable or delegate the built-in RUM exposure trackingrenderDecision(el, decision)— pluggable decision application (JSON / content ref / DOM patch)resolveAudiences(names, context)— one batched, context-aware resolution + bundledcreateRemoteAudienceResolverhelper (memoized, timeout→control)getAssignment(experimentId, context)— external experiment assignment (skip randomization;?experiment=still wins)src/contract.js)examples/auth-proxy-worker/) — Cloudflare entry + framework-neutral core + standalone stublistAudiences()catalog seam (from the #66 comment) — advertises the audience universe to the simulation panel (enumeration, distinct from #67's membership resolution)Docs:
documentation/byo-decision-engine.md, linked from the README with the hooks listed in the config reference.Guarantees
npm run lintclean.Also
test:commit fixes a pre-existing flaky test (experiments.test.js— "RUM fired before redirect", a redirect navigation race). Independent of this work; it failed ~85% under--repeat-eachbefore and passes 40/40 after.Deferred (by design)
The RFC's "converge on one decision provider" is intentionally left as the later internal refactor — the four seams now exist for it to compose.
Closes #67, #68, #69, #70, #71, #72. Advances #66.
🤖 Generated with Claude Code