feat(rokt): fire early selectPlacements preselect call on configured pageviews - #1398
feat(rokt): fire early selectPlacements preselect call on configured pageviews#1398alexs-mparticle wants to merge 12 commits into
Conversation
69d3d49 to
3b7865b
Compare
a85fb46 to
c8d08e5
Compare
cfef828 to
c293c69
Compare
PR SummaryMedium Risk Overview Matching is driven by a new hardcoded
Reviewed by Cursor Bugbot for commit ce2ca98. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Reviewed against our follow-ups, The sticky-attribute fix you found is the right catch and was the thing I would most have expected to bite.
Three below. The first two mean that as written a fired preselect would pre-select the page it fired from and go out untagged, which together is the February shape.
🤖 Generated with Claude Code
…pageviews Adds a static, hardcoded config (kits/rokt/src/preselectionConfig.ts) keyed by Rokt account ID + pathname, naming which attributes to collect and the target page identifier being warmed. When a matching pageview fires and the current user has at least one valid identifier, Rokt-Kit dispatches an early selectPlacements call carrying the preselect/identifier/omitUrl options, independent of RoktManager, to warm Rokt's backend preselect/ fingerprint-matching path. The call is tagged via the preselect option (not folded into attributes) so it flows through as speculative traffic rather than a real, billable placement request. Calls made before the launcher is ready are queued (capturing the pathname and pageview event at enqueue time) and re-evaluated once the launcher attaches, so identity/attribute state that wasn't available yet at first pageview is retried rather than dropped, and a queued call still resolves against the page it was fired from even if the user has since navigated away. Firing requires every configured attribute to be present; missing attributes are each logged individually for diagnosability. Hit/miss/queued/ fired diagnostics are logged throughout, and preselect dispatches are excluded from the customer-facing selectPlacements event log. A dedicated #preselect test suite covers attribute precedence, gating, queueing/flush re-evaluation, diagnostics, and the preselect contract.
67876fe to
9ac778b
Compare
buildPreselectDiagnosticLogEntry took a fired/missed boolean, so the not-ready enqueue path logged PRESELECT_FIRED even though nothing was dispatched yet. A later flush-time miss then double-reported the same attempt as both fired and missed. Adds a third queued outcome with its own PRESELECT_QUEUED code.
Persists a short-lived (1 minute) "active preselection" record keyed by account + pathname after a preselect fires, storing the attributes that were sent. A repeat firing for the same page is skipped while that window is still active and the attributes are unchanged, and fires again once the window expires or the collected attributes differ. Guards against redundant preselect calls on rapid re-renders or repeat navigation to the same page. Also extracts all preselect dispatch/queueing logic (previously several private RoktKit methods) into a standalone kits/rokt/src/preselection.ts module operating on an explicit state object and a small host interface, mirroring the existing launcherAttachState.ts pattern. Rokt-Kit.ts now just builds the host and state, and calls into the module.
The Rokt launcher now sets enablePreselection on the object it returns from createLauncher/createLocalLauncher, reflecting its own PRESELECT_SAMPLING_PERCENTAGE rollout gate (ROKT/sdk-web#1798). Treat a missing or non-true value the same as false so the kit never attempts a preselect the launcher isn't ready to serve.
pageViewStorage.ts and activePreselectStorage.ts each redefined the same 'mp-rokt-kit' localStorage namespace key independently; export it once from storage.ts, which both already depend on.
A disabled/missing enablePreselection flag is an expected, routine outcome, not something worth a diagnostic log entry.
A real (non-preselect) selectPlacements call always supersedes an in-flight one, but a preselect call never does. Firing preselect before attachKit let a live partner call (e.g. Cinemark's Pay+ selection on checkout) cancel our own warm-up prefetch for a different page. Firing it after attachKit puts the real call safely in flight first, since nothing later cancels it, while our own late-starting preselect isn't at risk either. Addresses PR #1398 review thread on Rokt-Kit.ts:1156.
The kit never named which attribute keys the WSDK should match a preselect cache entry on. Pass the same attributeKeys already required to fire preselect, so cache matching is scoped explicitly rather than relying on the WSDK's default behavior.
Rather than naming all attribute keys in cacheMatchKeys, compute one hash over the collected key/value pairs and point cacheMatchKeys at a single synthetic attribute carrying it. Any change to a tracked attribute value changes the hash, so the WSDK treats it as a new selection rather than serving a stale cache hit.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7823837. Configure here.
Move cacheMatchKeys off the preselect-only path and into _dispatchPlacements, the single choke point both preselect and live selectPlacements calls share. It is now a top-level string (never inside attributes, so it can't leak into persisted userAttributes) recomputed fresh for every dispatch by looking up the matching preselection config via identifier and hashing its attributeKeys, gated on isPreselectionEnabled() to skip the work entirely when preselection isn't active. Addresses both Cursor Bugbot findings on the prior commit: the hash no longer leaks into later placements, and the live call now recomputes and sends it so WSDK can check it against the cached preselect. Also consolidates the djb2 hash and buildCacheMatchHash helper into utils.ts (previously duplicated between Rokt-Kit.ts and a since-removed cacheMatchHash.ts).
The hash removed the one signal that would let a mismatch be debugged (which attribute actually changed), and required this kit's serialization to stay bit-for-bit consistent with a separate WSDK repo it can't verify against. WSDK's cacheMatchKeys is being changed in parallel (ROKT/sdk-web#1798) to only accept an array, for the same reasons. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGJZDPXqhcudxi94RDKfGu
|
| } | ||
|
|
||
| private buildCacheMatchKeys(identifier: unknown, attributes: Record<string, unknown>): string | undefined { | ||
| private buildCacheMatchKeys(identifier: unknown): string[] | undefined { |




Summary
Adds an early, speculative
selectPlacementscall ("preselect") that fires on a configured pageview before the customer reaches the page a placement is actually needed on, so Rokt's backend can warm its fingerprint-matching cache ahead of time.kits/rokt/src/preselectionConfig.ts): a hardcoded list (to move to a server-delivered kit setting later) of entries keyed byaccountId+ triggerpathname, each naming thetargetPageIdentifierto pre-select for and theattributeKeysrequired to fire.pathname(where the call fires) andtargetPageIdentifier(the page being pre-selected for) are deliberately separate fields — conflating them was the root cause of a prior production incident where a speculative call resolved against the wrong page.kits/rokt/src/preselection.ts, wired intoRokt-Kit.ts#process): on every pageview,maybeFirePreselectlooks up a matching config entry, then requires a valid user identifier, and all of the entry'sattributeKeyspresent on the pageview event or inuserAttributes. Any missing key logs aPRESELECT_MISSEDdiagnostic and the call does not fire — partial attribute sets are never sent.activePreselectStorage.ts) for 60s keyed by account + pathname; a repeat pageview with unchanged attributes within that window is skipped (PRESELECT_SKIPPED) rather than re-fired.PRESELECT_QUEUED) instead of dropped, and flushed once the launcher attaches.initRoktLaunchercallsattachKitbefore flushing the queue, not after, so a same-page liveselectPlacementscall (e.g. a partner's own placement request) always gets its call started first and can't be pre-empted by our own warm-up request.preselect: true,identifier: <targetPageIdentifier>, andomitUrl: trueas top-levelselectPlacementsoptions (not insideattributes) — this is what the launcher reads to emit therokt-is-pre-selectionheader that keeps speculative traffic out of real placement counts.cacheMatchKeysis passed as the entry'sattributeKeysarray on both the preselect call and the later live call from the same config entry, so the live call can match against the warmed cache.RoktManager: the preselect path callsselectPlacementsdirectly — no passback ID capture, no identify-on-mismatch, and no customer-facingSelectPlacementsanalytics event is logged for it, so it's invisible to the rest of the SDK and to the customer's own event stream.PRESELECTION_CONFIGcurrently has one entry (a pilot account, triggering on/checkout, pre-selecting aprod.rokt.confplacement) to validate the mechanism end-to-end; more entries get added as additional partners onboard.Test plan
npx vitest run— full suite passing, including dedicated preselect coverage inkits/rokt/test/src/tests.spec.ts(queueing/flush, identity gating, missing-attribute handling, dedup TTL, diagnostic codes,cacheMatchKeysshape on both preselect and live calls)npm run build— builds cleanly