feat(personalization): wire Intuit pzn + ixp onto aem-experimentation BYO hooks (frozen-API content model) - #756
feat(personalization): wire Intuit pzn + ixp onto aem-experimentation BYO hooks (frozen-API content model)#756ramboz wants to merge 17 commits into
Conversation
Decisioning stays in Intuit's engine (scripts/pzn.js + scripts/exp.js); the opt-in ?martech=adobe path no longer applies AJO/Target propositions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51da897 perf: run listAudiences catalog in lazy phase, not eager (no LCP impact) b710d58 test: cover BYO hook error/edge branches for codecov 69381f3 test: fix flaky "RUM fired before redirect" experiment test 5590131 docs: document the bring-your-own decision engine hooks 6685ea0 feat: add reference auth-proxy worker under examples/ (#72) b3f2c58 feat: add versioned client ⇄ engine decision contract + tests (#71) f7043aa feat: add listAudiences catalog seam for the simulation panel 6b15918 feat: add getAssignment hook for external experiment assignment (#68) d542487 feat: add resolveAudiences hook + remote resolver helper (#67) d085fa4 feat: add renderDecision hook for pluggable decision application (#70) 8e9a1a1 feat: add rumTracking hook to disable or delegate built-in RUM (#69) 7245090 chore(release): 1.2.2 [skip ci] 3fa2579 fix: resolve multi-word audience/campaign names at page level (#65) 6d50ecd chore(release): 1.2.1 [skip ci] 6f7b3e8 fix: support a single audience/campaign per selector in fragment manifests (#63) git-subtree-dir: plugins/experimentation git-subtree-split: 51da897116b5f3bf86b7eace7c3c52a143678ebf
…entation's BYO manifest hooks Adds scripts/personalization/byo.js implementing the plugin's bring-your-own decision-engine contract (resolveAudiences/renderDecision/rumTracking) for the manifest-driven, cell-resolution model: resolveAudiences delegates to a remote engine via the vendored plugin's createRemoteAudienceResolver when a `pzn-cell-endpoint` page metadata is set, otherwise falls back to a sticky demo mock; renderDecision reuses decision.js's applyFragment so fragment swaps stay consistent with the existing pzn.js/exp.js paths; rumTracking records exposure onto window.appVars. experiment-loader.js now dynamically imports byo.js and spreads its hooks onto the config passed to the plugin's loadEager/loadLazy, opt-in and fail open (falls back to the plugin's default no-BYO behavior if the hooks module fails to load). This does not touch the existing data-pzn/data-exp/ personalization-id path. The disable comments on the import/no-cycle lines in scripts.js and experiment-loader.js are pre-existing, unavoidable cycle edges (same as pzn.js/exp.js/decision.js/fragment.js already carry) that newly surface because byo.js reuses the fragment-loading machinery; no behavior changed on either line.
Minimal blocks (unwrap the single authored cell, no other decoration) that give the audience-manifest cell-resolution mechanism a stable, targetable selector (.pzn-hero / .pzn-offer) with real default content. Without a matching blocks/<name>/<name>.js, the boilerplate's block loader 404s trying to fetch one for any div-shaped block, which is avoidable console noise on the demo page.
…fest wiring drafts/pzn-cell-demo.html + drafts/pzn-manifest.json + drafts/fragments/pzn/* exercise scripts/personalization/byo.js end to end via the plugin's own audience-manifest mechanism only (no data-pzn/data-exp/personalization-id), so it never touches the existing Intuit path. Manifest rows cover .pzn-hero and .pzn-offer across the retail/hospitality/default cells. Force-added: drafts/ and *.html are gitignored by default (see .gitignore, "gitignore drafts/ and untrack sample pzn/exp fixtures"), same as the existing drafts/parity-probe.html test fixture. Verified locally with `aem up --html-folder drafts`: content is served under a /drafts/ prefix (not site root), so the meta/manifest/fragment URLs are authored with that prefix baked in. The `audience-manifest` value is authored directly as a <head> meta tag rather than a Metadata block, because --html-folder serves static files verbatim (no pipeline step lifts a Metadata block into <head>, and .plain.html is not auto-derived from .html — confirmed empirically, not assumed).
|
Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch and validate page speed.
|
…xperimentation's getAssignment hook Adds the IXP lane alongside the existing cell-resolution lane in byo.js: the engine now owns an experiment's arm via getAssignment (remote endpoint or a sticky demo mock), while the plugin's native Experiment/Experiment Variants authoring owns arm->content. renderDecision now branches on decision.scope so page/section-scope swaps (which land before the page's own decoration pass) splice in raw markup instead of pre-decorating it, avoiding a double-wrapped .section that decorateBlocks was misreading as a "section" block.
…e BYO getAssignment wiring Section-level native experiment (Section Metadata Experiment/Experiment Variants, not a manifest sheet) with one challenger fragment, mirroring the cell-resolution demo's structure. Section-level was chosen over page-level because a hand-authored page Metadata block isn't converted into <head> <meta> tags when served locally via --html-folder, whereas .section-metadata is read straight off the DOM.
… model Intuit's DE/PZN and IXP endpoints return CONTENT (a fragment ref, or a variation asset/redirect path), not a cell/arm name for authoring to map. Flip byo.js accordingly: resolveAudiences/getAssignment now make the real batched call and cache what the engine hands back; renderDecision applies that cached content instead of decision.url verbatim (which is now only a placement-id key or a placeholder). The manifest sheet and Experiment Variants authoring keep owning discovery, not content. Demo mocks (pzn-mock/ixp-mock) stand in for the real endpoint only when it's absent, and are shaped exactly like the real payloads so the demos exercise the actual pzn-response.js/ixp-response.js parse path. Also closes a gap where the plugin's own ?audience=/?experiment=<id>/<arm> QA overrides bypass resolveAudiences/getAssignment entirely: renderDecision now lazily resolves in that case too, so a forced arm still shows real engine content. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… profile resolvePznPlacementCache now awaits getMarketingProfile() and passes the ZoomInfo firmographics into buildBatchBody, matching scripts/pzn.js so the engine decides on the same attributes on both the plugin and bespoke paths. Memoized + fail-open upstream, so no extra network call and a miss sends the batch unenriched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… bypass A BYO decision engine that returns content per slot had no first-class lane in the vendored plugin, and the ?audience=/?experiment= QA overrides bypassed the configured resolveAudiences/getAssignment hooks entirely, so a content-returning engine had nothing to render under them. Adds an opt-in, no-op-by-default resolveDecisions/decisions-manifest lane, fixes the overrides to still invoke the configured hook for its resolution side-effect while still winning the plugin's own selection, and collapses the project's PZN audience-manifest scaffolding in byo.js onto the new hook (dropping the now-unnecessary lazy IXP-content fallback). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ration The vendored aem-experimentation plugin's BYO decision-engine hooks (scripts/personalization/byo.js) now own section/block discovery + apply for both PZN (decisions-manifest -> resolveDecisions) and IXP (native Experiment blocks -> getAssignment), making the old bespoke path redundant: - scripts/scripts.js: remove runExperienceLayer and its loadEager/loadLazy call sites. - scripts/pzn.js: remove collectSlots, runPersonalization, sameTargetAsExp. - scripts/exp.js: remove collectExperiments, runBlockExperiments. Page-level pzn/exp (personalization-id / experiment-id metadata -> runPersonalizationPage / runExperiment) is not covered by the plugin yet, so it's unchanged. Extract getMarketingProfile (+ its cookie/cache helpers) out of pzn.js into a new scripts/personalization/marketing-profile.js so both the kept page-level path and byo.js's resolveDecisions share it without pzn.js<->byo.js coupling. Section/block click attribution (data-pzn/data-experiment stamping, window.appVars records) is an intentional, documented gap on this path for now - it will be covered by the upcoming Option B region-keyed tracking registry rather than DOM data-attributes at rest. Page-level attribution is unaffected. Tests updated/moved to match: marketing-profile tests moved to their own file; tests for the removed section/block functions are dropped (no replacement, per the above); page-level test coverage is untouched.
…ntext registry Two independent cleanups to the BYO decision-engine hooks: - Move the PZN/IXP demo mocks (candidates, cookie helpers, forced-arm QA override reader) out of byo.js into a new byo-mock.js. byo.js now reaches it via a dynamic import() only after a real fetchDecision() call fails AND the page opts in via pzn-mock/ixp-mock metadata, so the mock module never even loads on a production page (verified via the network waterfall: the fetch for byo-mock.js only fires after the /api/pzn or /api/ixp miss). - Replace the section/block click-attribution "KNOWN GAP" with a region- context registry (scripts/personalization/tracking-context.js): registerRegionContext/resolveRegionContext over a window-backed WeakMap (window.__pznTrackingContext), not DOM data-attributes. resolveDecisions and getAssignment now capture each region's PZN/IXP identity, and renderDecision publishes it onto the applied element for the click- tracking runtime (Option B, clicktrack-optionb branch) to resolve at interaction time. Verified: npm run lint clean; vitest unchanged at 15 failed/414 passed (pre-existing, unrelated testimonial-video failures); both drafts demos render via the lazy-loaded mocks with zero data-pzn/data-experiment attributes at rest, and window.__pznTrackingContext resolves the expected context for both the PZN hero/offer slots and the IXP section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…data-exp Intuit's real authoring writes data-pzn / data-exp (+ -block) attributes directly on a section (see experience-workspace/skills/ add-personalization-experimentation.md), not the demo's decisions-manifest sheet — so the plugin's own serveDecisions never sees them and those pages never personalize. Add scripts/personalization/discover.js, a thin discovery+dispatch layer that finds these attributes and feeds them to byo.js's existing resolveDecisions/getAssignment/renderDecision hooks unchanged (they already register the served identity into window.__pznTrackingContext for click attribution — no DOM stamping, no analytics.js call). The decisions-manifest lane keeps working as-is; this is a second, independent discovery source into the same hooks. PZN and IXP need opposite timing relative to scripts.js's decorateMain, because byo.js applies them differently: a PZN decision uses applyFragment (self-decorating, so it's only safe to splice in AFTER decorateMain — the plugin's own decisions-manifest lane relies on this same ordering), while an IXP section/page decision uses applyRawFragment (a deliberately undecorated swap, documented in byo.js as relying on the page's own decorateMain running afterward — the same contract the plugin's native Experiment-block dispatch and the page-level exp.js/pzn.js swaps already rely on). So: - runAuthoredPersonalization (PZN) keeps the deleted runExperienceLayer's eager-first-section / lazy-rest phase split, called after decorateMain. - runAuthoredExperiments (IXP) runs once, before decorateMain, for the whole page — decorateMain never runs a second time, so there's no later pre-decoration moment to defer a below-the-fold data-exp swap into. Verified locally (aem up --html-folder drafts) against a new drafts/pzn-authored-demo.html carrying real data-pzn/data-pzn-block/data-exp sections: whole-section and block-scoped PZN swaps land correctly (sibling prose outside the block scope is left untouched), the IXP section swap renders the engine's challenger content, and a section carrying both tags correctly drops PZN in favor of IXP. Confirmed no new per-visitor data-* attributes appear anywhere on the page, and that resolveRegionContext(cta)/window.__pznTrackingContext resolve the served identity for both lanes. The existing decisions-manifest and native-IXP demos (pzn-cell-demo.html, ixp-demo.html) still render unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n/data-exp The aem.live pipeline serves Section Metadata as a raw `.section-metadata` block, not as attributes; this project runs a trimmed decorateSections that (unlike stock aem.js) never converted it. So section-level data-pzn/data-exp were never produced and the authored pzn/exp discovery lane found nothing on real content — only the literal-attribute demo appeared to work. Add scripts/personalization/section-metadata.js (decorateSectionMetadata): lifts the pzn/exp Section Metadata rows to data-* on the owning section and removes the consumed block, scoped to the pzn/exp keys so section backgrounds and the plugin's native Experiment blocks are left untouched. scripts.js runs it in loadEager before the IXP lane and decorateMain, so discover.js and the existing lane timing work unchanged. Demo now authors real .section-metadata blocks (converted client-side, so it works under `aem up` with no pipeline step). Corrects the authoring skill's "pipeline emits data-*" claim. 10 unit tests; browser-verified all four cases (whole-section pzn, block-scoped pzn, section exp, IXP-wins). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tures, trim comments - Inline decorateSectionMetadata into scripts.js (was its own module). A static import on the critical path is a needless LCP waterfall, and it's scripts.js-only, not a shared lib — so it doesn't earn a file. - Untrack the pzn/exp demo fixtures (drafts/* pzn|ixp demos, blocks/pzn-hero, blocks/pzn-offer); kept on disk via .gitignore for local dev. They were never the PR's preview URL anyway (drafts aren't served by the .aem.page preview). - Trim the verbose comments across byo.js / discover.js / tracking-context.js / marketing-profile.js / experiment-loader.js / scripts.js. These ship unminified, so comment bytes are wire bytes; load-bearing "why" notes kept, condensed. - Drop the section-metadata unit test: inlined, it can't be imported without triggering scripts.js's top-level loadPage(); the 10-line fn stays browser-verified. Lint clean; suite 15 fail/440 pass = baseline. Browser re-verified all four authored cases render + the region-context registry populated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@ramboz question about this ->
currently, the way we will know whether something needs to be replaced is based on page metadata or section metadata (or additional prop in section meta about block selector). Not from a sheet. So would want to make sure we can support this approach while rolling the changes into the OOTB plugin. |
|
Also, there's not worker doing anything right now. calls are made to /api/zoominfo, /api/ixp or /api/pzn (and in the future it will be consolidated into one) and wired via Akamai instead of the edge worker. |
|
@amol-anand The plugin supports 3 layers of pzn/xp: page, section and "block" (but essentially any CSS selector). The worker you see if just cleanup I still need to do in the plugin itself. I had a dummy proxy worker as documentation to show how to route to an internal service, but it would not be used here. Also since you have already a working version, I'd probably park the cleanup and changes to the experimentation pluging as post go-live. |
What
Wires Intuit's personalization + experimentation onto the vendored aem-experimentation plugin's bring-your-own decision-engine hooks, in the frozen-API content model — Intuit's APIs return content (
pznblock/ IXP variation), so the hooks apply the engine's content rather than mapping a cell/arm to authored content.It also folds two generic additions into the plugin itself (opt-in, no-op by default — these go upstream to adobe/aem-experimentation#73):
resolveDecisionshook +serveDecisionslane — consumes the contract's already-defineddecisionsfacet ("engine returns content per slot"), which nothing in the plugin used before.?audience=/?experiment=now still callresolveAudiences/getAssignmentfor their resolution side-effect (the override still wins the plugin's own selection).How it works
decisions-manifestlane (pluginserveDecisions). A sheet declares{ selector, placement }. The plugin reads it and callsresolveDecisions(entries, context)once; byo.js's hook batches a singlePOST /api/pzn(with the marketing-profile / ZoomInfo enrichment) and returns{ selector → { url: pznblock } }. The plugin applies each viarenderDecision(→applyFragment). No cache/indirection — the engine's fragment is returned directly.getAssignment(frozen). NativeExperiment/Experiment Variantsauthoring gates the experiment (the variant href is a placeholder).getAssignmentcallsGET /api/ixp, reports the arm, and stashes the engine's variation (assetLocation);renderDecisionapplies the stash. The QA override now invokesgetAssignmenttoo, so content resolves under a forced arm (no lazy workaround needed).decision.js/attributes.js/pzn-response.js/ixp-response.js+pzn.js'sgetMarketingProfile, unchanged.scripts/pzn.js/exp.jsare untouched (for the perf A/B).Contents
plugins/experimentation/, → media-text cards: migration-path cards drift from original in color, type scale, and spacing #73 upstream):resolveDecisions/serveDecisions/watchMutationsAndApplyDecisions+decisions-manifestmetadata (additive, no-op by default);getResolvedAudiences/getExperimentConfiginvoke the configured BYO hook under a QA override;getManifestEntriesForCurrentPagegains{ requireUrl }(default preserves existing behavior); byo doc + README updated.scripts/personalization/byo.js— the hooks (PZN collapsed ontoresolveDecisions, net −62 lines);scripts/experiment-loader.js(spreads the hooks;isExperimentationEnabledrecognizesdecisions-manifest);blocks/pzn-hero/pzn-offer; aem-martech transport-only (personalization: false); demos +pzn-manifest.json(now{ selector, placement }).Test it (local)
/drafts/pzn-cell-demo→ hero + offer render the engine'spznblock(sticky per placement viapzn-mock-*cookies)./drafts/ixp-demo?experiment=ixp-hero-test/challenger-1→ the engine's variation;/control→ control; no param → sticky mock.Demos are local-only (drafts aren't served by the
.aem.pagepreview; a shareable demo needs DA authoring).Measure perf
PZN = one batched
/api/pzn(viaresolveDecisions) +renderDecisionswaps; IXP = one/api/ixp+ a variant swap. Drop thepzn-mock/ixp-mockmetas to hit the real engine (the hooks call/api/pzn,/api/ixpvia the existing Akamai transport) on the same content asscripts/pzn.js/exp.jsfor the head-to-head.Verification
npm run lintclean; projectnpm test15 failed / 450 passed =mainbaseline (pre-existingtestimonial-video*/lead-form-config).pznblockvia the newserveDecisionslane; IXP challenger/control render (challenger under the override confirmsgetAssignmentruns); no console errors beyond the Intuit martech CORS baseline.Caveats / follow-ups
#73pre-merge. The plugin changes here also need pushing up to media-text cards: migration-path cards drift from original in color, type scale, and spacing #73 (git subtree push, or a direct commit to the media-text cards: migration-path cards drift from original in color, type scale, and spacing #73 branch) so the subtree stays in sync when media-text cards: migration-path cards drift from original in color, type scale, and spacing #73 merges. Everything added to the plugin is generic + no-op by default.listAudiencesenumeration.collectSlots/runExperienceLayer/runExperimentinscripts/pzn.js/exp.js; the plugin owns that orchestration now. That's the real code reduction.🤖 Generated with Claude Code