docs: RFC — first-class bring-your-own decision engine support - #64
Closed
ramboz wants to merge 3 commits into
Closed
docs: RFC — first-class bring-your-own decision engine support#64ramboz wants to merge 3 commits into
ramboz wants to merge 3 commits into
Conversation
Captures what a real BYO decision-engine integration ran into (two bugs plus four design gaps), proposes optional extension-point hooks (batched audience resolver with context, external experiment assignment, tracking override, pluggable decision renderer — all no-ops by default), and suggests a reference proxy-worker + client helper + contract tests. Discussion only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ramboz
commented
Aug 12, 2026
Comment on lines
+84
to
+87
| 5. **No exposure/tracking override.** The plugin fires its own RUM per type with | ||
| no opt-out. A BYO engine already fires exposure server-side, so you get | ||
| double counting, and the inline reporting doesn't apply. Proposed: | ||
| `tracking: 'off' | (event) => void`. |
Contributor
Author
There was a problem hiding this comment.
Let's make this explicitly rumTracking. also we have events that can be used to hook in custom tracking if needed.
Comment on lines
+75
to
+82
| 4. **No external experiment assignment — the biggest gap.** Experiments | ||
| self-bucket client-side (`ued.evaluateDecisionPolicy`); the only override is a | ||
| `?experiment=id/variant` query param. There is no clean way to say *"the arm | ||
| comes from my engine — don't randomize."* We could not use the experiment | ||
| primitive at all and had to model BYO experiments as audiences/campaigns. | ||
| Proposed: a pluggable assignment provider the engine owns, e.g. | ||
| `getAssignment(experimentId, context) → variant`, with the plugin still | ||
| rendering variants and reporting. |
Comment on lines
+64
to
+73
| 3. **No batched decision resolver with a shared context.** Each audience is an | ||
| independent, argument-less `options.audiences[key]()`, run in `Promise.all`. | ||
| A remote engine wants **one** call per page with a shared context | ||
| (`{ visitorId, url, consent, geo, … }`), then answers all audiences from it. | ||
| We hand-rolled a memoized fetch to avoid N round trips. Proposed: | ||
| ```js | ||
| loadEager(document, { | ||
| resolveAudiences: async (names, context) => { /* one call */ }, | ||
| }); | ||
| ``` |
Comment on lines
+89
to
+92
| 6. **No pluggable decision renderer.** Application is fetch-URL-then-`innerHTML` | ||
| with a fixed `main > div` fallback. Engines return JSON, content refs, or | ||
| external-CMS ids. Proposed: `renderDecision(el, decision)` (e.g. json2html, | ||
| external fetch) so every integrator doesn't re-implement the seam. |
Contributor
Author
There was a problem hiding this comment.
I can see this being useful indeed
Comment on lines
+125
to
+126
| - Should audiences / campaigns / experiments converge behind one **decision | ||
| provider** abstraction, rather than three parallel mechanisms? |
| - API surface + backward compatibility for the hooks. | ||
| - Should audiences / campaigns / experiments converge behind one **decision | ||
| provider** abstraction, rather than three parallel mechanisms? | ||
| - Where should the reference worker live (this repo `examples/`, or a companion)? |
Contributor
Author
There was a problem hiding this comment.
Let's go with examples/ for now
Comment on lines
+49
to
+60
| 2. **Multi-word audience/campaign names don't resolve at page level.** | ||
| `getAllMetadata` camelCases metadata keys, so `audience-returning-visitor` | ||
| becomes `returningVisitor`. Downstream, names are matched/looked up in | ||
| class-name form, and `toClassName('returningVisitor')` → `returningvisitor` | ||
| (the word boundary is lost) — which matches neither the `returning-visitor` | ||
| project config key nor the camelCase one. Because the camelCasing is lossy, | ||
| this **cannot be fixed downstream** — it needs the metadata reader to | ||
| preserve the audience/campaign *name* (section- and fragment-level already | ||
| keep class-name keys and work). Flagged here rather than patched blindly | ||
| because the right fix touches shared `getAllMetadata` behavior and deserves a | ||
| design decision. *(This is why BYO integrations must currently use | ||
| hyphen-free tokens like `ixptreatment`.)* |
Contributor
Author
There was a problem hiding this comment.
we should fix this cleanly across all 3 experiment, campaigns and audiences
ramboz
added a commit
that referenced
this pull request
Aug 12, 2026
## Problem
A **page-level** audience or campaign with a **multi-word name** (e.g.
`returning-visitor`, `black-friday`) resolves its *membership* but never
serves the variant — the experience is silently dropped.
Root cause: page-level metadata is read with `getAllMetadata`, which
**camelCases** keys (`audience-returning-visitor` → `returningVisitor`).
Names are then matched + looked up in **class-name** form, and
`toClassName('returningVisitor')` → `returningvisitor` (the word
boundary is lost), so it matches neither the project config key
(`returning-visitor`) nor the camelCase one. Section- and fragment-level
already key names by class-name, so they work — only page-level is
affected.
## Fix
- `getAllMetadata(scope, keyFn = toCamelCase)` — an optional key
transform. The default is unchanged, so **experiments keep their
camelCased config props** (`startDate`, `requiresConsent`, …).
- `applyAllModifications` passes `toClassName` for **audience/campaign**
page metadata (experiments keep `toCamelCase`), matching how section-
and fragment-level already read names.
- Config readers (`getAudienceConfig` / `getCampaignConfig`) are
**unchanged** — they already handle class-name keys.
## Tests
Adds page-level fixtures + tests for a multi-word audience and a
multi-word campaign name. They fail red before this change (the variant
isn't served) and pass after. **Full suite green — 101 tests, including
experiments — no regression.**
---
Found while building a bring-your-own-engine integration (see #64) —
this is bug #2 from that RFC. It removes the need for hyphen-free tokens
at page level.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- rename tracking hook to rumTracking; note the existing aem:experimentation DOM events for custom tracking - resolve two open questions into Decisions: one decision provider; reference worker in examples/ - reframe bug #2 as a clean cross-cutting fix (audiences/campaigns class-name at every level; experiments keep camelCase, with the why) and credit #65 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
|
Converting this RFC into a tracking epic + sub-issues so each enabler ships as its own PR, per the review discussion:
The two bugs this RFC flagged already landed as #63 and #65. Closing in favor of #66. |
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.
Draft RFC (discussion only — not merge-ready) proposing first-class support for "bring your own decision engine" integrations, where a customer's own personalization/experimentation engine drives the plugin client-side (via an edge worker that hides the key), while the engine keeps ownership of segmentation, bucketing, and exposure tracking.
Adds
docs/rfcs/byo-decision-engine.md, grounded in a real integration. It covers:getAllMetadatacamelCases names lossily).uedbucketing); a tracking/exposure override; a pluggable decision renderer. All opt-in, no-ops by default.Opening as a draft to gather direction on the API surface and on whether audiences / campaigns / experiments should converge behind a single "decision provider" before any implementation.
🤖 Generated with Claude Code