Repository navigation
feat(rokt): fire a logged-in shopper's express tap before checkout - #1639
crisryantan wants to merge 34 commits into
Conversation
With event batching on, an auto button click waited in the upload queue until the next batch interval. A logged click now schedules an upload 300 ms later, and each further click restarts the wait, so a double or multi-tap goes out as one request carrying every click. 300 ms is about the gap between the taps of a double tap, and a single click still leaves well within a second. The early upload goes through the SDK's upload(). It only runs when the click is waiting in the upload queue: a click held back until there is an MPID goes out when it is released, and with batching off every event already uploads as it is logged. When the page is hidden or left, the SDK's exit upload already sends the queue, so the pending upload is dropped rather than sent twice. Teardown, reset and opt-out drop it too. Clicks are logged exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shortens the upload wait from 300 ms to 100 ms so a single click reaches the server sooner. Each click still restarts the wait, so rapid taps share one request. The burst test now spaces taps 60 ms apart to stay inside the window, and a new case shows two clicks more than 100 ms apart upload separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… intent is active Removes the per-entry intentTrigger key. While checkout intent is active for the workspace (auto button tracking on, nothing suppressing it, and intent privacy allowed), every pre-selection entry's page view or pathname trigger waits for the shopper's commit or wallet click and fires then. There is no delay fallback, so a shopper who never clicks gets no speculative call, and the call that does go out carries the checkout details the shopper filled in. When checkout intent is not active, an entry keeps its dispatchDelayMs exactly as before. Configured trigger events still dispatch at once. A preselectionConfig setting that still carries intentTrigger, with any value, parses and the key is ignored like any other unknown key. Along with it: - Checkout intent is recorded for every target page of the account, and the intent arrival line is logged while checkout intent applies. - A fired page view or configured event always guards a later click that would resend it with fewer attributes, including one fired before checkout intent turned on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checkout intent matched a control's whole label, so a confirm button whose label carried more than the action fell through. Labels are now read without bracketed asides such as "(2 items)" and without the separators, arrows and symbols left at either end once the price is removed, so "Place order - $43.95", "Place order (2 items)" and "Pay now ($43.95)" read as a commit. Steps and wallet labels get the same treatment. Matching stays anchored to the whole remaining label, so "Place order history" is still not a commit, and an excluded word anywhere in the original label still vetoes the control. A control holding a form field or more than four children now gives the classifier its aria-label, as it already does for button descriptions. Its text is still never read. Focus moving into a hosted wallet button frame now signals a wallet click, which the Rokt kit acts on like a wallet button. That covers the wallet hosts' own frames and the quick-pay button frames a card processor serves beside its card fields, matched by stable frame path segments. Card entry fields and pay-later messages still signal only a frame, which the kit observes without firing. A frame marked data-mp-no-track, or inside a marked element, signals nothing. In the Rokt kit, a checkout intent signal that arrives after consent removes the kit from the active forwarders is no longer recorded or logged, and the intent arrival line is no longer sent. No call went out in that state before either. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prepareAndUpload takes events and batches out of the queue before the request goes out, so a fetch that a navigation cancels loses them: the exit beacon then has nothing left to send. That window is now common, since auto button clicks upload 100 ms after the last tap and a confirm click is usually followed by a navigation. Send each fetch upload with keepalive when its body is within 60,000 bytes, under the browser's 64 KB limit for a page's in-flight keepalive bodies. Larger batches send as before. A keepalive request the browser refuses throws, and the existing catch requeues the batch for the next upload. The beacon path is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A click that navigates can leave the page before a delayed upload starts. The first click now uploads as soon as it is logged. Taps that follow within 100 ms are held and go out together once they stop. A click delivered while the page is being left still goes with the exit upload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checkout intent now ignores pickers and toggles (radio, tab, option and checkbox roles, aria-checked, aria-selected, a button inside a radio group or tab list, aria-expanded or aria-pressed) and anything inside pay-later messaging. Those clicks are still logged as auto button clicks. Vendor wallet buttons are read from their own markup, on the control or the shadow host that owns it: Apple's element and class, Google's button and card button, PayPal's web components, Shopify's buttons, Amazon Pay and the Afterpay entry point. A vendor button counts before any label rule; Apple's set-up and top-up buttons give no intent and PayPal's card button is a checkout step. A merchant wrapper reusing a vendor class does not turn its children into wallet buttons. findControl rules out Rokt elements and data-mp-no-track with one composed walk from the target instead of one per path entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checkout intent is classified in a fixed order: quiet pickers and messaging, a vendor's own wallet button, a confirm label on any source, then the look-alike exclusions, then wallet phrases and steps. - Labels are normalised: '&' and '+' read 'and', separators become spaces, currency codes are dropped, and common wallet spellings are aliased. A label over eight words neither matches nor vetoes. - Confirm labels accept leading 'agree and' or 'review and', trailing 'now', 'securely', 'with <tender>', totals and item counts, and 'transaction'. 'Place another order' and gift-card payments stay out. - Wallet phrases such as 'Pay with <wallet>', 'Buy with <wallet>' and 'Continue to <wallet>' count on button-like controls, as does a wallet's bare name. Account sign-in labels do not. - Steps add 'Let's check out', 'Continue to review', 'Review order' and 'Pay with card'. - On a plain link only placing, submitting or completing an order or purchase commits in English. - English exclusions match whole words; the other languages keep their existing substring lists, and their commit and step lists are unchanged. classifyIntent returns the kind and the rule that recognised it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… buttons Focus into a hosted frame now reads: - wallet for PayPal only on its /smart/buttons and /smart/menu frames; its other frames, such as card fields and pay-later messages, are frames; - frame for card-entry and fingerprint frames on adyen.com, braintreegateway.com, cybersource.com, squarecdn.com and checkout.com, including their subdomains, so every regional Adyen host is covered. Hosts are matched exactly or as a dotted subdomain, over HTTPS only. A frame never fires pre-selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…intent live RoktManager.isCheckoutIntentLive() is true when checkout intent is not suppressed and the browser exposes composedPath. It reads only config, so tracker setup and teardown during init never change it. The kit now waits for the confirm click only when that method exists and returns true; a core released without it keeps the entry's dispatchDelayMs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A button anywhere inside a radio group, tab list or list box, not only as a direct child, is a picker and gives no intent. 'Buy now' and 'Purchase' count as a confirm click only on a button-like control on a checkout route, never on a plain link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s email on a confirm click - Once this tab fired pre-selection for a target page, another fire for it is skipped (skipped/active_preselection) until the shopper arrives or 30 minutes pass. That covers a later step of the same checkout, a repeated confirm click and a retry after a declined card. A page view on the same page with changed attributes still re-fires outside the confirm click. - A fire on the confirm click needs an email the call will carry: an email or emailsha256 attribute, the filtered email identity, the identity mapped to the hashed email, or the entry's form email. Without one it waits, logs missed/no_strict_email once per page, and fires when an email arrives while the shopper is still there. Delay fires are unchanged. - When the shopper moves between two pages of one entry, such as from payment to review under a '/checkout/*' entry, the wait for the confirm click moves with them without a left_trigger_path or second awaiting_intent line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on their own budget - A commit or wallet tap off every entry, such as an express wallet on a cart page, is now logged as skipped/intent_observed instead of dropped silently. Steps and frames are still observed on entries only. - Observed lines carry intent_route (entry, left, before_entry), intent_source when the core sends it, has_email and has_identity. hold_running and on_trigger_path are dropped. 'left' means this tab saw an entry's trigger within 30 minutes. Each route and kind pair is observed once per page visit, still at most four per document. - Fired lines for a confirm click carry intent_route and intent_source. - Fired and arrived lines go through their own transport and rate limiter, so a run of held, missed or observed lines can't suppress them. Only enums and booleans are added; no label, path, class or value is logged or stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One entry's pathname moves from its payment page to a trailing wildcard, so the shopper's confirm click counts on any step of that checkout, the review page included. A single '*' matches exactly one segment, so the cart and the confirmation page stay outside it, and the kit still sends one pre-selection per checkout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…one-fire guard on failure - The email check reads what selectPlacements would send: the kit's cached user attributes merged in and the user attribute filters applied. A cached email outside the entry's keys counts; an email the filters strip does not. - Setting an email-class user attribute flushes waiting fires. - A fire whose selectPlacements call throws, rejects or is dropped no longer counts as the checkout's pre-selection, so it does not hold off the next fire. - The first fire after the 30-minute window starts a fresh window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…signal
The tracker now hands the kit {kind, t, source, pathname}. source names
the rule that recognised the intent: vendor markup, a label phrase, a
wallet's bare name, a hosted frame, or a form submitted right after a
confirm click. pathname is the path the shopper acted on.
The kit uses a carried path in place of its own route history, which
starts when the kit attaches, so a confirm click buffered before then
still fires on its entry. A path that is not a plain path falls back to
the history. Older kits read only kind and t, and nothing new is logged,
stored or sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… final payload - Each fire builds one rollback. A call that throws, rejects or is dropped restores the active record it replaced and the fire time it built on, so an identical retry goes out at once and an earlier successful fire in the window is kept. - The email check reads the payload in the order selectPlacements merges it: identities, then filtered and overridden attributes, then session attributes. An empty cached email that overwrites an identity email no longer passes. - A confirm-click call that reaches dispatch without an email, for example after the only email was removed while it waited, is stopped and logs missed/no_strict_email with cause lost_before_dispatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e one-line email miss Adds specs for a rollback called twice, restoring an unexpired active record, a no-op restore with no fire recorded, a flush after a move between pages of one entry, the missing-email line logged once per page, and the identity fallback when the host cannot build the outgoing attributes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssification Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* cov-detection: test: cover an unparseable src and a missing getRootNode in click classification
…tive record Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* cov-live-signal: test: ignore a submit whose submitter is not a checkout control test: cover an unparseable src and a missing getRootNode in click classification
- A commit or wallet tap off every entry route, such as an express wallet or buy-now button on a product or cart page, now fires a pre-selection when the shopper is logged in. Login is mParticle's own isLoggedIn() on the current user, counted only when that user is the one the kit's call goes out for. A guest's tap is still only observed. - The fire is for the account's one entry. An account with several entries names several target pages and nothing ties the tap to one of them, so its taps are observed instead. - The usual gates apply: checkout intent live, an identity, the entry's required attributes, an email on the outgoing call and one pre-selection per checkout. A tap replayed after the shopper moved on does not fire, and the page form is never read off the entry route. - The fired line carries intent_route=before_entry or left and logged_in=true. Off-entry intent_observed lines carry logged_in, and a logged-in tap that did not fire says why in cause: multiple_entries, path_changed, no_valid_identity, active_preselection, missing_attribute or no_strict_email. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ill logged in - The call can wait on identity search or launcher setup. An off-entry fire now carries the user it was made for, and the kit sends it only if that user is still the kit's user, still the current user and still logged in. - A call that fails the check is dropped, logs missed/logged_in_dropped and rolls back its records, so a later tap can fire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ser change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into feat/checkout-intent-logged-in-express * origin/feat/checkout-intent-fire-gate: test: ignore a submit whose submitter is not a checkout control test(rokt): check the stored namespace when a rollback removes the active record test: cover an unparseable src and a missing getRootNode in click classification test(rokt): cover the fire rollback, the quiet move on a flush and the one-line email miss
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v3-development #1639 +/- ##
=================================================
Coverage ? 58.49%
=================================================
Files ? 75
Lines ? 9093
Branches ? 2456
=================================================
Hits ? 5319
Misses ? 3494
Partials ? 280
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The stack below this branch landed on v3-development as one squashed commit, so the branch now sits on the new base plus its own change: a logged-in shopper's commit or wallet tap off every entry fires for the account's one entry. Storage files and specs this branch never changed take the base as is. In preselection.ts the off-entry fire keeps going through fireDispatch, which now carries the base's digest guard and linked rollback. Its specs expect the digest on the fire mark and on each rollback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The off-entry tap checked for an earlier fire before it collected attributes, so any fire in the last 30 minutes held it off, even when the shopper's attributes or email had changed since. It now collects first and passes the same digest fireDispatch uses, so an identical tap is still skipped and a changed one goes out, as on the entry route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alexs-mparticle
left a comment
There was a problem hiding this comment.
Thanks, this is careful work: the tap-time and dispatch-time user binding, the shared once-per-checkout guard and the rollback paths all hold up, and the tests cover the negative cases well. Kit tests pass at the head (1,277) and on current v3-development (1,294). A few things before merge:
- Merge order with #1644. That PR renames the host hook
isIntentPrivacyAllowedtoisPreselectionPrivacyAllowed. The two merge cleanly as text, but the new "honors the privacy gate" case here still sets the old name, so on the combined tree the tap fires and that test fails (1 of 1,296). Renaming the mock fixes it. Since v3-development has no required checks, whichever lands second should rerun CI after the rename. - Release timing. #1635 is merged but not released yet, and nothing here holds this change back from the same release. Could we hold this until the release with #1635 is out, and say so in the description? Shipped together, the two effects are hard to separate: the off-entry call carries the same
rokt.preselecttriggervalue as a confirm-click call (intent_wallet|<entry path>), and the arrival line only saysfired. If it's cheap, a distinct trigger value for the off-entry fire would let us measure it on its own. - What "logged in" means.
isLoggedIn()is mParticle's identity state, not the site's sign-in. It is persisted across page loads, and in workspaces where email is a login ID it can turn true for a guest once any identify carries their email (including the background identifyselectPlacementsmakes when it's passed an email), and stay true on later visits. Privacy-wise that's fine (same gates and payload as the on-entry call), but "guests stay observe-only" isn't quite true there, andlogged_inwill over-count site logins. Worth a line in the description and the comment, and a check that product is OK with it.
Smaller, inline below.
…ut-intent-logged-in-express * origin/v3-development: fix(rokt): skip every pre-selection call under do-not-share-or-sell or GPC (#1644) feat(rokt): say why a tab reached the target page without seeing the trigger (#1630) fix: send event batch uploads without keepalive (#1642) feat(apv): allow s, oq and colour/size query params on auto page views (#1643)
… to the target page An off-entry tap's call now carries intent_commit_off_entry or intent_wallet_off_entry, keeping the intent_ prefix the dispatch-time email check reads, so it can be measured apart from the confirm click. Its active record is scoped to the entry's target page, as a recovered fire is, rather than to the tapped page. The logged-in comment says it is mParticle's persisted identity state, and the privacy-gate case uses the renamed isPreselectionPrivacyAllowed. A tap while checkout intent is not live sends nothing and logs nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks Alex. Merged v3-development with the rename, split out the off-entry trigger, and the description now says to hold this until the release with #1635 is out. On logged in, it is spelled out in the comment and the description; I will check with product that counting a returning guest is fine before we merge. |
Summary
Builds on #1635, now in v3-development. Hold until the release that carries #1635 is out, so the confirm-click fire and this off-entry fire can be measured separately. Today a commit or wallet tap off every configured entry route is only logged, which misses shoppers who pay with an express wallet or buy-now button on a product or cart page.
This change fires a pre-selection for those taps when mParticle sees the shopper as logged in. Everyone else keeps today's observe-only behaviour.
isLoggedIn()on the current user, counted only when that user is the one the kit's call goes out for. The kit's filtered user does not expose login state. It is mParticle's persisted identity state, not the site's sign-in: in a workspace where email is a login identity, a returning guest whose email was once identified also counts, andlogged_inwill count more than site sign-ins. The gates and payload are the same as an on-entry call.Telemetry
rokt.preselecttriggerisintent_commit_off_entryorintent_wallet_off_entrywith the entry's path, distinct from the confirm-click fire and still under theintent_prefix the dispatch-time email check reads.intent_route=before_entryorleftandlogged_in=true.intent_observedlines carrylogged_in, and a logged-in tap that did not fire addscause:multiple_entries,path_changed,no_valid_identity,active_preselection,missing_attributeorno_strict_email.missedwith reasonlogged_in_dropped.Testing
Kit Vitest passes, 1,314 tests on current v3-development, with new specs for each gate, several entries, logout or a user switch during both waits, rollback, and a changed-attribute tap firing again while an identical one is skipped.
🤖 Generated with Claude Code