Skip to content

fix: keep the cart and the product rating alive when a dependency fails - #14

Merged
marcoferreiradev merged 5 commits into
main-altfrom
fix/cart-orderform-fallback
Sep 23, 2026
Merged

marcoferreiradev merged 5 commits into
main-altfrom
fix/cart-orderform-fallback

Conversation

@marcoferreiradev

@marcoferreiradev marcoferreiradev commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Duas correções de carrinho no fork, todas de erro medido em produção. Sobe junto com deco-sites/oficina-reserva#784, que já pina esta branch.

Rebaseada em main-alt hoje: antes disso o pin do site revertia o deco-cx#790 (o content-type das avaliações), porque dd0cafb2 não descendia de cdfe1bd6.

1. orderFormId do cliente quando o cookie atrasa — vtex/actions/cart/updateAttachment.ts

updateAttachment resolvia o carrinho só pelo cookie checkout.vtex.com, que é HttpOnly e chega atrasado: na sessão fria ele só é setado pelo getCart que acabou de rodar, e as escritas que vêm logo atrás saem antes. 3.354 erros em 6 dias, o maior grupo da loja. A VTEX endereça o orderForm pelo path param, então o cookie é opcional para a escrita — ele continua vencendo quando existe, e o props.orderFormId só entra quando falta. O throw fica: id genuinamente ausente ainda precisa falhar, senão a fila grava um carrinho vazio por cima do real.

2. CHK0038 não derruba mais o carrinho inteiro — vtex/loaders/cart.ts

O getCart re-posta marketingData para ecoar atribuição que a VTEX já guardou. Quando uma audiência de campanha injeta marketingTag nula (bug conhecido deles), o eco volta 400 CHK0038 e o erro subia, derrubando o getCart inteiro — 143 erros em 6 dias, em todo pageview dos carrinhos afetados. O eco é no-op no sucesso, então falhar nele não perde nada: serve o carrinho já buscado e avisa.

⚠️ Nunca sanear marketingTags para contornar — a PR deco-cx#352 fez isso, apagou o gatilho da promoção e machucou vendas.

Retirado da PR após revisão

  • verified-reviews/ volta ao contrato do main-alt: ratings e fullReview devolvem o resultado tratado em vez de lançar. Devolver o vazio numa falha grava esse vazio no cache do loader, mas por pouco tempo (CACHE_MAX_AGE_S=180, e todo deploy zera o cache). Lançar quebrava o contrato 200 de quem consome, inclusive o app mobile no /live/invoke do fullReview. O retry de corpo vazio também sai: nunca foi provado que a segunda tentativa resolve.
  • stripNulls no Seo.tsx sai: no site da Oficina não fazia nada (o JSON-LD de produto não passa por este componente), e o erro do iOS que ele mirava parou em 11/09, antes dele existir.

deno fmt, deno lint e deno check **/mod.ts verdes (hook de pre-commit).

🤖 Generated with Claude Code

@marcoferreiradev

Copy link
Copy Markdown
Collaborator Author

Terceiro commit: Seo.tsx — tira os nulls do JSON-LD antes de serializar

O scanner de schema do Apple Wallet, no iOS Safari, percorre o JSON-LD da página chamando hasOwnProperty.call(node, "telephone") em todo nó, e estoura quando o nó é null. As PDPs publicam seo.description: null — JSON.stringify mantém null e só descarta undefined — então o scanner quebrava em ~104 erros de client por semana, todos iOS, todos em página de produto.

schema.org não atribui significado a propriedade com valor nulo, então removê-las é sem perda para consumidores e é schema melhor para o Google.

Verificado em isolado antes de commitar:

seo.description: null      removido
null dentro de array       removido (itemListElement cai de 3 para 2)
0, "" e false              PRESERVADOS  ← a armadilha do falsy

Encadeado antes do escape de </script> que já existia, não no lugar dele: as duas proteções valem para o mesmo payload.

O pin do site já aponta para dd0cafb2 (deco-sites/oficina-reserva#784).

marcoferreiradev and others added 3 commits September 21, 2026 12:11
…okie is absent

updateAttachment resolved the orderForm only from the checkout.vtex.com cookie
and threw when it was missing — 3,354 errors in 6 days, the storefront's largest
error group.

Reproduced locally on a cold session: getCart creates the orderForm and returns
the Set-Cookie, but the writes that follow it in the same page load leave before
that cookie is in the jar, while the client already holds a valid orderFormId in
its own signal. Second load, with the cookie present: zero errors.

VTEX addresses the orderForm by the `:orderFormId` path param, so the cookie is
optional for the write — it governs PII masking, not addressing. The cookie still
wins when present; props.orderFormId is only consulted when it is not.

The throw stays. A genuinely missing id must still error: the cart queue
overwrites the local cart signal with whatever the action returns, so a no-op
here would wipe a real cart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LQPsjnQxLYzAg6qx5aC2a
…down

getCart re-POSTs marketingData to echo attribution VTEX has already stored on the
orderForm. When a matched campaign audience injects a null marketingTag (VTEX
known issue marketingtags-with-null-value-when-campaign-audience-is-matched),
that echo returns 400 CHK0038 and the error bubbled out of the loader, failing
the whole getCart — 143 errors in 6 days, on every page view for the affected
carts, which are concentrated in a handful of poisoned orderForms.

The echo is a no-op on success, so failing it loses nothing: serve the cart that
was already fetched and warn.

Never sanitize or strip marketingTags to "fix" this. Overwriting VTEX's
marketingData drops the promotion trigger; it hurt sales and was reverted
(PR deco-cx#352). The campaign tags stay intact on the server cart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LQPsjnQxLYzAg6qx5aC2a
iOS Safari's Apple Wallet schema scanner walks the page's JSON-LD calling
hasOwnProperty.call(node, "telephone") on every node, and throws when a node is
null. PDPs ship `seo.description: null` — JSON.stringify keeps null and only
drops undefined — so the scanner crashed on ~104 client errors a week, all iOS,
all on product pages.

schema.org assigns no meaning to a null-valued property, so removing them is
lossless for consumers and better schema for Google. Stripped deeply: object
properties and array elements alike. Falsy-but-meaningful values survive — 0, ""
and false are kept, only null goes.

Chained before the existing "</script>" escaping rather than replacing it: both
guards apply to the same payload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LQPsjnQxLYzAg6qx5aC2a
@marcoferreiradev
marcoferreiradev force-pushed the fix/cart-orderform-fallback branch from dd0cafb to 14d49d0 Compare September 21, 2026 15:11
…ews"

The ratings endpoint answers 200 with an empty body on a small share of the
calls made from our pods — about 30 per thousand product pages, flat across the
day, spread over 46 pods, and not reproducible from outside (800 ids in one
payload, concurrent pairs, idle keep-alive: all answer JSON). Its sibling
`reviews` call, same host and same Promise.all, sees it about once a week.

`JSON.parse("")` threw, the catch returned undefined, and undefined reads as
"this product has no reviews" — which the stale-while-revalidate loader then
stored as the product's answer. So one failed call blanked the rating on the
page and the aggregateRating in the JSON-LD for the whole TTL, and kept serving
that blank as stale afterwards. It was also the storefront's largest error
group, ~4,000 lines a day.

Now the ratings call reads the body as text, retries once when it comes back
empty, and throws with what the response actually carried (status,
content-length, content-type, duration) if it repeats. An empty body is not
silence: an unknown product answers `{}`, and that still returns undefined
without a retry.

Throwing is the point. A loader handler that resolves is what gets written to
the cache, so failing leaves the last good rating in place. The PDP extension
catches it and returns the page without a rating, which is the degradation the
components already render: the reviews button and sheet hide themselves and
withRating drops the block from the JSON-LD.

Verified with a stubbed fetch: empty body always -> throws after 2 calls; empty
then valid -> returns the rating on the retry; `{}` -> undefined in 1 call;
valid -> data in 1 call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@marcoferreiradev marcoferreiradev changed the title fix(vtex): keep the cart alive when the checkout cookie is late and when attribution is rejected fix(vtex,verified-reviews,website): manter carrinho, JSON-LD e nota do produto de pé quando a dependência falha Sep 21, 2026
@marcoferreiradev marcoferreiradev changed the title fix(vtex,verified-reviews,website): manter carrinho, JSON-LD e nota do produto de pé quando a dependência falha fix: keep the cart and the product rating alive when a dependency fails Sep 22, 2026
ratings and fullReview return the handled empty result again instead of
throwing; the retry and the extension try/catch go with it. The loader
cache TTL is 180s and every deploy clears it, so a blank rating after a
failed call is short-lived, and callers (the mobile app included) keep
their 200 contract.

stripNulls leaves too: the Oficina PDP JSON-LD does not go through this
component, and the iOS telephone error stopped before it existed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkCbBdPTvcEiJmvG3U7MfL
@marcoferreiradev
marcoferreiradev merged commit 247da68 into main-alt Sep 23, 2026
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