Skip to content

Render {% pixels %} in the base layout, recover from a consumed cart id; release 1.3.0 - #55

Merged
next-devin merged 4 commits into
mainfrom
pixels
Sep 3, 2026
Merged

Render {% pixels %} in the base layout, recover from a consumed cart id; release 1.3.0#55
next-devin merged 4 commits into
mainfrom
pixels

Conversation

@next-devin

@next-devin next-devin commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Two Spark bugs found on aptest.29next.store on 2026-09-03, plus the 1.3.0 release bump. Three commits, one per concern.

1. App event trackers never load

Spark-based stores send no storefront events to any installed app. The platform injects each app's event tracker (Google Analytics 4, Google Tag Manager, Klaviyo, Taboola) as a hidden iframe through the builtin {% pixels %} template tag. In Intro Bootstrap that tag is rendered indirectly by {% core_js %} at the end of layouts/base.html. Spark deliberately dropped core_js (jQuery-free design, replaced by assets/js/spark-platform.js) and never re-added {% pixels %}.

Verification. Checked in a browser on aptest.29next.store:

  • no iframe[name=customer_event_iframe] elements in the DOM
  • no window.customerEventManager
  • no app receives page_viewed, product_viewed, product_added_to_cart, or checkout_*

The developer docs (content/docs/storefront/themes/templates/tags.md in developer-docs) say core_js is required in every base layout; this is the part of it Spark still needs.

Platform chain (traced in the platform source). core_jspixelscustomer_event_manager (loads pixels/js/dist/pixels.min.js, creates window.customerEventManager, hooks window.fetch and htmx:afterRequest to read the Analytic-Events response header) + customer_event_iframes (one hidden iframe per active tracker, plus a script that publishes the page's server-side events to each frame on load). Guarded by {% if not disable_pixel %}. All three tags are platform builtins, so no {% load %} is needed, and none of it depends on jQuery.

Points checked against Intro Bootstrap and the platform templates:

  • core_js is the only place Intro Bootstrap touches tracking; the rest of that tag is jQuery plumbing Spark already replaced.
  • The iframes tag reads the view's object / object_list for product_viewed and category events, not template variables, so Spark templates need nothing extra.
  • The GraphQL view attaches Analytic-Events to every response and the manager wraps window.fetch. spark-cart.js calls bare fetch() at call time, so it picks up the patched global; the wrapper preserves the abort-signal init.
  • Rendering the block before the theme script stack (Intro renders it after) is deliberate: the fetch hook must exist before the first cart mutation. The iframes still follow the manager, which is the only hard ordering constraint, and footer app hooks still come after.
  • Checkout is unaffected: the platform's checkout pages are standalone templates that render {% pixels %} themselves and do not extend the theme layout, so there is no double render.

Change. layouts/base.html gets {% block pixels %}{% pixels %}{% endblock %} immediately before {% block scripts %}. docs/performance-load-order.md and CLAUDE.md document the block.

2. Add-to-cart is dead after a purchase until browser storage is cleared

Reproduced right after an order completed on aptest.29next.store: every add afterwards returned HTTP 200 with

{"addCartLines":{"success":false,"errors":{"nonFieldErrors":[[{"message":"Cart not found.","code":"cart_not_found"}]]},"cart":null}}

Cause. Checkout consumes the cart, but its id stays in sessionStorage and the storefront_cart_id cookie: the order-confirmation page is rendered by the platform, not the theme, so nothing theme-side runs to forget it. SparkCartClient.addToCart only recovered when the request rejected and the message matched isCartExpiredError. This payload resolves, so the .then branch handed back success: false and the create-and-retry path never ran.

Change (assets/js/spark-cart.js).

  • isCartNotFoundResult() recognises the resolved shape (code cart_not_found, or a message matching the existing expired-cart heuristic, at any depth of errors).
  • addToCart settles the request into an outcome first, then recovers exactly once for either a rejection or a resolved cart_not_found: clear the stored id, createCart, retry with recovery off. A failure inside the retry cannot re-enter the recovery branch.
  • getCart clears the stored id when the platform returns cart: null or a not-found error. Badge hydration calls this on the first storefront page after confirmation, so the stale id is gone before the shopper's next add.
  • updateCartLines, removeCartLines, addVoucher, removeVoucher clear the id on cart_not_found without recreating (the caller is editing a cart it believes exists), so a stale id cannot leak into the next add.

Tests (tests/js/spark-cart-client.test.js, 14 → 17). The exact aptest payload on the first add asserts createCart and a second addCartLines follow with the new id stored; a second miss returns the platform answer without looping; getCart null/reject and each explicit-id mutation clear the stored id and dispatch nothing.

3. Release 1.3.0

Bumps manifest.json, README.md, and CLAUDE.md to 1.3.0 and promotes the Unreleased changelog section to a dated 1.3.0 heading, with entries for both fixes above and for the changes merged since 1.2.0 that had none (#52, #53, #54). Tag / GitHub release to follow after merge.

Checks

  • python3 -m unittest discover -s tests: 61 tests OK (the Tailwind source-guard tests need the tailwindcss binary in the checkout).
  • for f in tests/js/*.test.js; do node "$f"; done: all pass.
  • scripts/check-templates.py: template integrity gate passed (62 templates).
  • make css-check: rebuilt assets/main.css is byte-identical to the committed file, Sass-compat OK.

Related

Velin (Sellmore-Co/velin-theme, a Spark derivative) has the same gaps and received the same fixes separately.

🤖 Generated with Claude Code

The platform injects every app's storefront event tracker (GA4, GTM,
Klaviyo, Taboola) as hidden iframes through the builtin {% pixels %} tag.
Intro Bootstrap reaches it indirectly through {% core_js %}; Spark replaced
core_js with spark-platform.js and never re-added pixels, so no tracker
iframe rendered, window.customerEventManager never existed, and no app
received any storefront event on Spark-based stores.

Add a `pixels` block before the theme script stack so the tag's fetch hook
is installed before spark-cart and friends make requests. Document the
block in the load-order contract, CLAUDE.md, and the changelog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@next-devin
next-devin marked this pull request as ready for review September 3, 2026 04:10
Bump the version markers in manifest.json, README.md, and CLAUDE.md, and
promote the Unreleased changelog section to a dated 1.3.0 heading. Adds
entries for the merged-but-unlogged changes since 1.2.0: the filter-argument
escape check (#52), the scoped Tailwind content scan (#53), and the
__pycache__ ignore (#54).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@next-devin next-devin changed the title Render {% pixels %} in the base layout so app event trackers load Render {% pixels %} in the base layout so app event trackers load; release 1.3.0 Sep 3, 2026
Reproduced on aptest.29next.store on 2026-09-03 right after an order
completed: every addCartLines afterwards returned HTTP 200 with

  {"addCartLines":{"success":false,"errors":{"nonFieldErrors":
    [[{"message":"Cart not found.","code":"cart_not_found"}]]},"cart":null}}

and the shopper could not add anything until browser storage was cleared.

Two things combined. The consumed cart id stays in sessionStorage and the
storefront_cart_id cookie because the order-confirmation page is rendered by
the platform, not the theme, so nothing on the theme side ever ran to forget
it. And addToCart only recovered when the request *rejected* and the message
matched isCartExpiredError; this payload resolves, so the .then branch handed
back success:false and the create-and-retry path never ran.

- isCartNotFoundResult() recognises the resolved shape (code cart_not_found,
  or a message matching the existing expired-cart heuristic, at any depth of
  the errors object).
- addToCart settles the request into an outcome first, then recovers exactly
  once for either a rejection or a resolved cart_not_found: clear the stored
  id, createCart, retry with recover=false. A failure inside the retry cannot
  re-enter the recovery branch.
- getCart clears the stored id when the platform returns cart:null or a
  not-found error. Badge hydration calls this on the first storefront page
  after confirmation, so the stale id is gone before the shopper's next add.
- updateCartLines, removeCartLines, addVoucher, removeVoucher clear the id on
  cart_not_found without recreating (the caller is editing a cart it believes
  exists), so a stale id cannot leak into the next add.
- clearCartId() removes the sessionStorage key and expires the cookie.

Tests: the exact aptest payload on the first add asserts createCart and a
second addCartLines follow with the new id stored; a second miss returns the
platform answer without looping; getCart null/reject and each explicit-id
mutation clear the stored id and dispatch nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@next-devin next-devin changed the title Render {% pixels %} in the base layout so app event trackers load; release 1.3.0 Render {% pixels %} in the base layout, recover from a consumed cart id; release 1.3.0 Sep 3, 2026
Comment thread assets/js/spark-cart.js Outdated
@kilo-code-bot

kilo-code-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
assets/js/spark-cart.js 254 Resolved-result cart-not-found heuristic falls back to isCartExpiredError, whose substring match on 'invalid' will falsely flag unrelated validation errors ("Invalid quantity", "Invalid voucher code") as a missing cart and wipe the stored id.
Files Reviewed (8 files)
  • CHANGELOG.md - 0 issues
  • CLAUDE.md - 0 issues
  • README.md - 0 issues
  • manifest.json - 0 issues
  • docs/performance-load-order.md - 0 issues
  • layouts/base.html - 0 issues
  • assets/js/spark-cart.js - 1 issue
  • tests/js/spark-cart-client.test.js - 0 issues

Fix these issues in Kilo Cloud

Previous Review Summary (commit 1df9121)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 1df9121)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
assets/js/spark-cart.js 254 Resolved-result cart-not-found heuristic falls back to isCartExpiredError, whose substring match on 'invalid' will falsely flag unrelated validation errors ("Invalid quantity", "Invalid voucher code") as a missing cart and wipe the stored id.
Files Reviewed (8 files)
  • CHANGELOG.md - 0 issues
  • CLAUDE.md - 0 issues
  • README.md - 0 issues
  • manifest.json - 0 issues
  • docs/performance-load-order.md - 0 issues
  • layouts/base.html - 0 issues
  • assets/js/spark-cart.js - 1 issue
  • tests/js/spark-cart-client.test.js - 0 issues

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 23.2K · Output: 1.9K · Cached: 239.4K

…resolved result

Review finding on #55: isCartNotFoundResult fell back to isCartExpiredError,
whose substring match on "invalid" would have flagged ordinary resolved
validation errors ("Invalid quantity", "Invalid voucher code") as a missing
cart and wiped a live cart id. Resolved results now count only the
cart_not_found code or a message naming the cart as not found; the broad
matcher stays on the rejection path where it started. Regression test covers
an add, a voucher, and a line update that each fail validation with "invalid"
in the message and assert no recreate and the id kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@next-devin
next-devin merged commit 7d6cf2c into main Sep 3, 2026
2 checks passed
@next-devin
next-devin deleted the pixels branch September 3, 2026 05:47
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