Skip to content

Feat/popups v2 rules - #84

Open
anyelopetit wants to merge 33 commits into
mainfrom
feat/popups-v2-rules
Open

anyelopetit wants to merge 33 commits into
mainfrom
feat/popups-v2-rules

Conversation

@anyelopetit

@anyelopetit anyelopetit commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the browser-runtime half of Popup Rules V2 for the paired Rails change in hellotext#5833.

The SDK evaluates current page, visit, and activity conditions immediately in the browser, follows modern SPA navigation correctly, and coordinates late identification with the new Rails receipt protocol before re-evaluating popup eligibility. Profile, audience, and signal conditions remain trusted server-side checks and are never evaluated from browser-visible contact data.

This PR releases the runtime contract as version 2.6.1.

Runtime contract with Rails

  1. Rails validates and stores the merchant rule document.
  2. On each popup request, Rails evaluates visitor-dependent conditions: profile, audience, signals, and country resolution.
  3. Rails sends the SDK only the residual page, session, and activity lanes that are safe to evaluate in the browser.
  4. The SDK evaluates the residual document against the current visit. A popup is displayed only if the server decision and the local decision both pass.

The client evaluator expects the canonical { lanes: [...] } document:

  • alternatives are ORed;
  • conditions within one alternative are ANDed;
  • multiple positive text values are alternatives;
  • excluded text values are required negatives;
  • invalid input, unknown fields, and unknown operators fail closed.

The composer restricts the merchant to three alternatives, but the runtime stays defensive when evaluating persisted data.

Supported client-side context

Rule Runtime source Important behavior
Page URL Canonical current route Rails stores this as page.path even though the authoring label is “Page URL”. It matches a storefront path, not an arbitrary full href.
Page title document.title The runtime observes title changes when title rules exist, including SPA title updates.
Referrer Browser referrer May be empty for direct traffic, strict referrer policies, and privacy tools.
Scroll depth Current document scroll position Evaluated for the current page.
Time on page Time since canonical route entry Resets only on a canonical route change, not on a title-only update.
Pages viewed Visit route count Increments only when the canonical route changes.
Browser language navigator.language Browser language only; it is not a contact profile language.
Visitor type and browser Runtime visit/device context Browser family is derived locally. Device targeting is fixed when the popup controller connects rather than changing unexpectedly on resize.
UTM source, medium, campaign Current tab and visit URL state Parsed once with URLSearchParams; values are case-insensitive; URL encoding, +, and hash-route query parameters are handled.
Product viewed, cart added, purchase completed, form completed Activity emitted for the current business and visit Events must come from a real successful integration action. Backdated, cross-business, and prior-visit activity does not satisfy a current-visit rule.

Profile fields, audience membership, signal values, and country fallback are deliberately absent from this table because Rails evaluates them before a public popup response is returned.

Navigation and route normalization

The popup runtime watches the navigation mechanisms that matter on modern storefronts:

  • History API pushState and replaceState;
  • browser popstate and hashchange;
  • Turbo load and render events;
  • relevant document.title mutations.

It canonicalizes routes so a route change is meaningful rather than an incidental URL mutation:

  • ordinary query strings and ordinary hash anchors do not create a new page view;
  • hash-router paths such as #/collections/sale and #!/collections/sale are treated as routes;
  • the Page URL normalizer has explicit behavior for trailing slashes and index.html;
  • only a canonical route change resets time-on-page and increments pages viewed.

This keeps popup decisions stable for SPAs while allowing a merchant to target actual client-side routes.

Page URL authoring compatibility

The Rails editor intentionally names the path field “Page URL” for non-technical merchants. The SDK aligns with that decision:

  • a merchant may paste a full storefront URL or type a path;
  • Rails normalizes the stored value to a path;
  • runtime matching uses the same canonical route form;
  • observed path suggestions are a Rails authoring feature and remain manual-entry-safe when no suggestion exists.

The normalizer must not silently broaden a rule. Query strings, fragments, trailing slashes, index.html, and hash routes have explicit documented behavior so the displayed input is not misleading.

Activity semantics

Activities are evaluated only for the current business and visit. This prevents an event from another tenant, an older visit, or an arbitrary backdated timestamp from unlocking a popup rule.

Commerce fields are visible because they are product decisions, not because every platform is already proven to emit them. Each installed Shopify, Wix, and VTEX integration must be smoke-tested against a real successful action:

  • Product viewed requires the product-view integration event.
  • Added to cart requires a confirmed cart add event. A webchat button click alone does not count.
  • Purchase completed requires a completed purchase event.
  • Form completed remains attributed to the business and visit that originated the form.

The runtime accepts Hellotext.track activity events, but an integration must call it at the right successful storefront boundary.

Late identification receipt protocol

Popup eligibility can change after identify(), because a visitor may gain a profile, audience, or signal condition only after the server has mapped the identification.

The SDK supports the receipt-aware Rails API:

  1. identify() submits the identification and receives a short-lived receipt.
  2. The SDK polls that receipt with the current session header using bounded backoff.
  3. While the receipt is pending, popup loading is held so an anonymous decision is not displayed immediately before a known-contact decision.
  4. On completion, the SDK remembers the contact and reloads popup eligibility.
  5. On terminal failure, it resumes anonymous evaluation rather than waiting forever.

Safety properties:

  • the receipt is bound by Rails to business and session;
  • stale responses from a previous identify call, a changed business, or a changed session are ignored;
  • pending, transient server failure, and rate-limit responses retry;
  • malformed or terminal receipt responses stop retrying cleanly;
  • the same-visit initialization path preserves a valid pending identification instead of accidentally cancelling it;
  • servers that do not return a receipt retain the legacy immediate compatibility path.

The intended rollout order is Rails receipt endpoint first, then this SDK version. The legacy path reduces compatibility risk but is not a substitute for deploying the server contract.

Failure behavior and privacy boundaries

  • Local rule parsing is fail-closed: an invalid condition never makes a popup eligible.
  • The SDK does not receive profile, audience, or signal values to evaluate.
  • Browser language and browser country-like signals are not treated as durable profile facts.
  • Failed or unavailable URL suggestions do not affect runtime evaluation because suggestions are only an authoring aid.
  • A server-side no-op identification is not treated as a completed identity change.
  • Public receipt responses are not cached by the server contract, and the SDK does not use a cross-session receipt.

Build and dependency updates

  • Version bumped to 2.6.1.
  • Regenerated distribution assets are included.
  • DOMPurify was rebuilt at 3.4.15 using the locked dependency graph.

Validation

  • Full local JavaScript suite: 45 suites, 975 tests passing.
  • Production build completed successfully.
  • Formatting check completed successfully.
  • GitHub Actions are green on Node 20, Node 22, and the security scan.

Release checklist

  1. Merge and deploy hellotext#5833 so the receipt endpoints are available.
  2. Publish this PR as hellotext.js@2.6.1.
  3. Update the Rails runtime dependency to the exact published SDK version.
  4. Test an installed popup on actual Shopify, Wix, and VTEX storefronts, including SPA route changes, URL input normalization, UTM values in standard and hash routes, cart and purchase actions, anonymous country fallback, and a late identify().
  5. Keep display frequency out of this release; it was intentionally deferred from the paired Rails work.

Intentionally out of scope

  • Display frequency.
  • Browser-side profile, audience, or signal evaluation.
  • A page-history rule such as “visited a previous page in this visit”.
  • Popularity-ranked URL suggestions.
  • Inferring commerce activity from intent-only UI events.

@anyelopetit anyelopetit self-assigned this Sep 14, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-16T22:18:36.781102Z d15985a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce1f90104a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/controllers/popup_controller.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js
Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd3d873ba5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/controllers/popup_controller.js
Comment thread src/controllers/popup_controller.js
Comment thread src/controllers/popup_controller.js Outdated
Comment thread README.md Outdated
Comment thread src/controllers/popup_controller.js Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de17524ce2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hellotext.js Outdated
Comment thread src/controllers/popup_controller.js
Comment thread src/controllers/popup_controller.js
Comment thread src/hellotext.js Outdated
Comment thread src/controllers/popup_controller.js
Comment thread src/controllers/popup_controller.js
Adds the browser half of the split evaluation. The server decides profile conditions and
sends only the page conditions of the lanes that survived, so this treats the payload as
the whole remaining question: lanes are OR'd, conditions inside a lane are AND'd, and no
lanes means the popup may display.

PopupDisplayRules mirrors Popup::DisplayRules::PageEvaluator, including complement
semantics for unset values, so Test a URL and the runtime cannot disagree.

Scroll depth and time on page only grow, so a popup gated on them is re-checked on scroll
and once a second instead of being decided on connect. Watching starts only when a rule
needs a measurement, and stops the moment the popup displays: a popup counts as shown
when it actually appears, never when its rules merely match.

An eligible: false response carries no markup and is treated as nothing to render rather
than a failure.
@anyelopetit
anyelopetit force-pushed the feat/popups-v2-rules branch 2 times, most recently from de17524 to beec397 Compare September 16, 2026 14:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: beec397f97

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/controllers/popup_controller.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/controllers/popup_controller.js Outdated
Comment thread src/controllers/popup_controller.js Outdated
Comment thread src/models/popup_display_rules.js

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0d07955a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/controllers/message_controller.js Outdated
Comment thread src/controllers/popup_controller.js
Comment thread src/hellotext.js
Comment thread src/controllers/popup_controller.js Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38acfaea16

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hellotext.js Outdated
Comment thread src/controllers/popup_controller.js Outdated
Comment thread src/controllers/popup_controller.js Outdated
Comment thread src/controllers/popup_controller.js
Comment thread __tests__/controllers/message_controller_test.js Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f048e59a7a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/hellotext.js Outdated
Comment thread src/controllers/popup_controller.js Outdated
Comment thread src/controllers/popup_controller.js
Comment thread src/models/popup_display_rules.js
Comment thread src/models/form.js

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70df204cba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/controllers/popup_controller.js Outdated
Comment thread dist/hellotext.js.LICENSE.txt Outdated
Comment thread src/controllers/popup_controller.js

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d15985a71c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/hellotext.js
)
return

if (response.data.status === 202) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear identification state when polling is exhausted

If the status endpoint remains at 202 through the eighth attempt—such as when the identification job takes longer than roughly 16 seconds—this continue exits the loop without clearing identificationPending. Because identify() already unmounted the active popup and loadPopup() refuses to run while that flag is set, popup evaluation then remains disabled for the rest of the visit. Clear the pending state and restore anonymous popup evaluation when the retry budget is exhausted.

Useful? React with 👍 / 👎.

Comment thread src/api/popups.js
// `eligible: false` with no markup when this visitor does not qualify. That is a
// deliberate outcome rather than an error, so it is treated the same as "nothing to
// render" instead of surfacing as a failure.
if (data.eligible === false || !data.html) return null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep SPA page tracking active without eligible markup

When the initial response is eligible: false, this early return means no popup controller is mounted, but PopupController.watchNavigation() is the only runtime component that records SPA route changes. For example, an anonymous visitor can navigate through several routes and then identify into an eligible audience; the reloaded popup receives a stale page-view count and a session.page_views rule may stay false. The remaining uncovered case is specifically this no-markup response, so route tracking needs to live outside the rendered popup controller or remain active while eligibility is false.

Useful? React with 👍 / 👎.

Comment thread src/hellotext.js
)
return

User.remember(details.externalId, details.source, details.fingerprint)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel pending identification when forgetting the user

When an app awaits identify() and then logs the user out before receipt polling completes, identify() has already returned but forget() does not invalidate the background identification. A later successful poll reaches this line and recreates the user cookies that logout just cleared, so subsequent activity can be attributed to the previous user. forget() must advance the identification version, cancel polling, and clear the pending state before deleting the identity.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant