Documentation Index
Fetch the complete documentation index at: https://docs.ollie.shop/llms.txt
Use this file to discover all available pages before exploring further.
This catalog is the source of truth for everything the checkout sends to PostHog. It covers two streams:
- Events (
posthog.capture(...)) — what users did. Show up in Activity → Events and can be filtered, funnelled, and used as Session Replay filters.
- Errors (
posthog.captureException(...)) — what broke. Show up in Error Tracking with stack traces and automatic grouping.
The events and errors documented here only started flowing into PostHog reliably after the provider-ordering fix. Funnel-event volume before that date is not comparable; treat the new floor as the baseline.
Properties attached to every event automatically
The AnalyticsProvider injects these into every event — you don’t need to filter on them at the call site, but they’re available in PostHog for slicing:
| Property | Source | Example |
|---|
storeId | useStoreInfo() | eaf782f0-… |
sessionId | useCheckoutSession() | VTEX OrderForm id |
userId | useCheckoutSession().session.user?.id | VTEX userProfileId (when identified) |
platform | useStoreInfo() | vtex |
template | useStoreInfo() | default / grocery / sales |
env | process.env.NODE_ENV | production |
PostHog also auto-registers store_id, platform, environment, and ollie_session_id as session super properties, so every PostHog-generated event ($pageview, $exception, session recording, etc.) carries them too.
Ecommerce events additionally receive a nested ecommerce payload (currency, items, value, coupon, etc.) parsed from the current session. This is the GA4-compatible structure the AnalyticsProvider emits via parseEcommerceData.
Ecommerce funnel events
Standard GA4-style funnel events. They were broken in PostHog (but working in GTM/GA4) until the provider-ordering fix; now they reach both sinks.
| Event | When it fires |
|---|
view_cart | Cart page mounts with items |
begin_checkout | User enters the first details step |
add_to_cart | Quantity increase or ADD_ITEMS server action succeeded |
remove_from_cart | Quantity decrease, item removal, or REMOVE_ITEMS action |
add_shipping_info | Shipping addresses/packages updated; also fires once when email becomes present and shipping is already valid |
add_payment_info | Payment method change committed (UPDATE_PAYMENT_METHODS) |
purchase | Order successfully created — carries transaction_id |
page_view | Explicit page-view tracker (separate from PostHog’s own $pageview auto-capture) |
All carry the ecommerce payload described above (currency, items, value).
UX behavior events
The events below are the focus of this catalog: signals you can’t derive by filtering the funnel above. Every event carries the automatic context (storeId, sessionId, etc.) plus the documented properties.
Payment processing modal — abandonment lifecycle
payment_abandonment_attempt
Fires when the browser confirm dialog is shown during payment processing (e.g. the user hit Cmd+R, closed the tab, or navigated back). Fires whether or not the user ultimately confirms.
| Property | Description |
|---|
stage | creating_order | processing_payment | generating_order — which sub-stage of payment was active |
time_in_modal_ms | Milliseconds elapsed since the loading modal first mounted — short = “saw something wrong fast”, long = “suspected the request hung” |
Triggered from: templates/default/src/components/steps/Payment/PaymentLoadingModal.tsx — beforeunload listener.
payment_abandoned
Fires only when the user confirmed and the page actually unloads. The “confirmed abandonment” signal. Registered with { capture: true } so it enqueues into PostHog’s batch before the SDK’s own pagehide-flush handler.
| Property | Description |
|---|
stage | Same as above |
time_in_modal_ms | Same as above |
Triggered from: same file — pagehide listener (capture phase).
Comparing the two: payment_abandonment_attempt − payment_abandoned ≈ users who reconsidered. The delta is itself a “users hesitated” metric.
Payment step — outcome events
These fire from useCheckoutOrderAction in packages/react/src/providers/checkout-action/index.tsx when submitPayment throws a specific class. Each represents an expected business outcome (not a bug).
payment_validation_failed
User-side data problem caught by PaymentValidationError: bad CVV, expired card, malformed number.
| Property | Description |
|---|
stage | Always processing_payment |
error_message | Validation failure message |
payment_unauthorized
PaymentUnauthorizedError: bank declined the charge.
| Property | Description |
|---|
stage | Always processing_payment |
error_message | Decline reason from the gateway |
payment_precondition_required
PaymentPreconditionError: the payment method needs extra steps — 3DS challenge, bank redirect, Pix QR display, etc. This isn’t a failure — for some methods the order is even considered placed and purchase fires anyway.
| Property | Description |
|---|
stage | Always processing_payment |
condition_ids[] | Array of precondition identifiers (e.g. ["3ds", "redirect"]) |
payment_timeout
The secure iframe didn’t respond to the submit postMessage within the configured paymentTimeout window (SECURE_PAYMENT_TIMEOUT by default, overridable per store). The checkout self-recovers with a hard window.location.reload() 2 seconds after the event fires — the delay exists so the PostHog beacon can flush before page unload.
Fires from the timeout handler in packages/secure/src/factories/create-submit-payment/index.ts via the onTimeout callback, wired in apps/web/src/providers/checkout-provider/index.tsx.
| Property | Description |
|---|
stage | Always processing_payment |
iframe_origin | Origin of the payments iframe that timed out |
session_id | Checkout session id |
Payment step — choice events
payment_method_changed
Fires on every payment-method click in the Payment step (templates/default/src/components/steps/Payment/Payment.tsx → handlePaymentMethodChange).
| Property | Description |
|---|
from_method_id | VTEX method id of the previously selected method, if any |
from_method_name | Human-readable previous method name |
to_method_id | VTEX method id of the newly selected method |
to_method_name | Human-readable new method name (saved_card if the click was a saved card) |
is_saved_card | true when the click was on a stored card option |
change_count_in_session | Monotonic counter per mount of the Payment step |
saved_card_skipped_for_new
Fires only when the user has saved cards on file but clicks the credit-card method to enter a new one. Trust / discovery friction signal for the saved-card UI.
| Property | Description |
|---|
saved_cards_count | How many saved cards were offered |
cart_total | Total in minor units (cents) |
Shipping step
delivery_method_changed
Fires from templates/default/src/components/steps/Shipping/DeliveryMethodSelector.tsx when the user picks delivery / pickup / custom.
| Property | Description |
|---|
from_method | delivery | pickup | custom | null (first selection) |
to_method | Same enum |
available_methods_count | How many options were shown (1 = no real choice) |
Cart step
coupon_apply_failed
Fires from templates/default/src/components/CartCoupon/CartCoupon.tsx on the UPDATE_COUPONS action error. Success is not captured — it’s already implicit in the cart total change visible in subsequent funnel events.
| Property | Description |
|---|
coupon_code_hash | Non-reversible char-sum hash of the attempted code (c_<base36>). Same code in the same session produces the same hash, enabling retry analysis. |
server_error | Server-side error message |
error_code | Error code from the action response |
attempts_in_session | Monotonic counter — resets on success |
Checkout step navigation
checkout_step_back_clicked
Fires from the prev-step link in templates/default/src/components/step-navigation/StepNavigation.tsx.
| Property | Description |
|---|
from_step | Current step (details, shipping, payment, …) — read from the step query param |
to_step | prev.id or cart |
time_on_step_ms | Milliseconds since the step mounted |
Express checkout (Google Pay / Apple Pay / PayPal)
Lifecycle for the express-checkout buttons. Fired from templates/default/src/components/checkout-express-wrapper/index.tsx. All three share a slot_id so events from the same button instance can be correlated.
express_checkout_initiated
User clicked the express button; the leading edge of onLoading(true).
| Property | Description |
|---|
slot_id | The Slot id of this wrapper instance |
express_checkout_succeeded
Express flow completed and the order was created.
| Property | Description |
|---|
slot_id | Same as above |
order_id | The created order id |
duration_ms | From initiation to success |
express_checkout_failed
Express flow threw an error (excluding the muted "ApplePaySDK failed to load" noise filter).
| Property | Description |
|---|
slot_id | Same as above |
error_message | Original error message |
duration_ms | From initiation to failure |
Engineering errors (Error Tracking)
These go to PostHog Error Tracking via posthog.captureException, not Events. They carry stack traces, get auto-grouped, and should not fire during a healthy flow — every spike is worth investigating.
origin: "ollie", action_type: "<ACTION>" — generic checkout-action failures
Source: useCheckoutAction onError in packages/react/src/providers/checkout-action/context.ts. Fires for every action’s terminal failure (UPDATE_COUPONS, UPDATE_PAYMENT_METHODS, UPDATE_SHIPPING_*, etc.).
| Property | Description |
|---|
origin | "ollie" |
action_type | Action name (UPDATE_COUPONS, UPDATE_PAYMENT_METHODS, …) |
server_error | Message from server |
error_code | Server error code |
platform_code | VTEX platform error code |
error_group | VTEX error group (e.g. paymentAuthorizationErrors) |
origin: "ollie", action_type: "CREATE_ORDER", stage: "creating_order" — Order creation terminal failure
Source: useCheckoutOrderAction in packages/react/src/providers/checkout-action/index.tsx. Fires when the order-creation retry loop exhausts and CREATE_ORDER still returned an error or empty data.
| Property | Description |
|---|
origin | "ollie" |
action_type | "CREATE_ORDER" |
stage | "creating_order" |
error_code, error_group, platform_code | Server-side error metadata |
server_error | Server message |
has_validation_errors | Boolean — were there validation errors in the response? |
origin: "ollie", action_type: "CREATE_ORDER", stage: "processing_payment" — Unknown payment error
Source: same file. Fires inside the catch block when the thrown error is not PaymentValidationError, PaymentUnauthorizedError, or PaymentPreconditionError. The “Unknown error” catch-all — by definition unanticipated.
| Property | Description |
|---|
origin | "ollie" |
action_type | "CREATE_ORDER" |
stage | "processing_payment" |
error_message | Original error message |
source: "secure_container_iframe" — Payments iframe failed to load
Source: apps/web/src/providers/checkout-provider/index.tsx wiring of SecureContainer.onLoadError. Fires for genuine load failures (DNS, refused connection, CSP block) — not for HTTP 4xx/5xx responses (cross-origin policy hides those).
| Property | Description |
|---|
origin | "ollie" |
source | "secure_container_iframe" |
iframe_url | Full URL of the payments iframe that failed |
session_id | Checkout session id |
event: "http_server_error" — Server-side error pings (this one is an Event, not an exception)
Captured by a window.fetch patch in apps/web/src/instrumentation-client.ts for any response with status ≥ 500. Listed here for completeness because it predates this catalog and shows up alongside the rest. PostHog filter target: event = "http_server_error".
| Property | Description |
|---|
status_code | HTTP status |
url | Failing URL |
method | HTTP method |
response_body | First 4 KB of the response body |
Expected business outcomes (bad card, declined card, 3DS) are intentionally not captured as exceptions — they live in the Events stream as payment_validation_failed / payment_unauthorized / payment_precondition_required. This keeps Error Tracking signal-to-noise high.
Session Replay — network request capture
Session Replay records every browser-emitted HTTP request alongside the DOM mutations, so a replay shows what the page looked like and what calls it was making. Three layers govern what’s captured and how it can be filtered:
| Layer | Where | What it adds |
|---|
recordHeaders: true | session_recording in apps/web/src/instrumentation-client.ts | Request and response headers (subject to the mask function below) |
recordBody: true | same | Request and response bodies (truncated to 1 MB per request, mask applied) |
capture_performance.network_timing: true | posthog.init(...) top-level | Performance Timing data per request — DNS, TCP, TTFB, transfer size. Server-side feature toggle on the PostHog project must also be enabled. |
maskCapturedNetworkRequestFn | apps/web/src/providers/posthog-provider/index.tsx | Strips sensitive JSON keys (cardNumber, encryptedSecurityCode, cvv, cpf, etc.) from request and response bodies before they’re shipped. |
With network_timing on, each captured request carries timing breakdown and size in Session Replay’s Network tab, and those fields are queryable from the PostHog MCP just like event properties.
What can be filtered
- URL pattern — substring or regex against the request URL.
- HTTP method — GET / POST / etc.
- Status code — exact match or ranges (
>= 500, 4xx).
- Duration — total request time, useful for “slow request” hunting.
- Headers and body content — limited search across captured values.
Privacy
maskNetworkRequest runs before any data leaves the browser. The current key list:
password, passwd, cardNumber, card_number, encryptedCardNumber, encryptedSecurityCode, encryptedExpiryMonth, encryptedExpiryYear, cvv, cvc, securityCode, security_code, cpf, ssn, secret, private_key, privatekey, credentials. Any JSON value at any nesting depth under one of these keys is replaced with "[REDACTED]" before the body is captured. Extend the list there if a new sensitive field appears in any request body.
Prompt examples for PostHog MCP
These are natural-language questions you can ask the PostHog assistant (or write as HogQL by hand). Each example explains the filter applied so you can adapt it.
Basic — single-event questions
”Show me purchases from the last 24 hours.”
What it does: counts purchase events grouped by hour.
Filter: event = "purchase" AND timestamp >= now() - interval 1 day.
Why useful: sanity check that funnel events are flowing.
”How many users initiated express checkout but didn’t complete?”
What it does: in the last 24h, count sessions where express_checkout_initiated fired but express_checkout_succeeded did not.
Filter: sessions with event = "express_checkout_initiated" AND no later event = "express_checkout_succeeded" in the same session.
Why useful: Apple Pay / Google Pay drop-off rate — invisible in standard funnel events because failed express attempts don’t fire add_payment_info.
”Purchases with Apple Pay as the method but didn’t reach the order page.”
What it does: there are two possible interpretations:
- Sessions where
express_checkout_initiated fired (Apple Pay slot) and the order page (page_view with url contains /order/) never followed.
purchase events where properties.ecommerce.payment_type = "Apple Pay" and no subsequent $pageview on the order page.
Filter (interpretation 1): event in ("express_checkout_initiated", "express_checkout_failed") AND slot_id matches the Apple Pay slot, grouped by session, having no $pageview matching pathname like '%/order/%'.
Why useful: catches the case where the redirect after express success silently fails.
”Coupon codes that fail more than twice in the same session.”
Filter: event = "coupon_apply_failed" AND attempts_in_session > 2.
Why useful: identifies which codes are systematically wrong (typos? expired? not yet enabled?) — group by coupon_code_hash to see repeat offenders without exposing the raw code.
”Which payment methods get switched away from most often?”
Filter: event = "payment_method_changed" grouped by from_method_name, ordered by count desc.
Why useful: methods users select then leave signal UX or trust friction.
Filter: event = "checkout_step_back_clicked" AND from_step = "payment" AND timestamp >= now() - interval 1 hour.
Why useful: highest-value back-button signal. Reading time_on_step_ms tells you whether they bounced immediately or got stuck.
Medium — funnels and cross-event questions
”Of users who saw the express checkout fail, what % retried with a regular payment method and completed the order?”
Funnel: express_checkout_failed → payment_method_changed → purchase.
Filter: same session ordering, all within a 30-minute window.
Why useful: recovery rate after express-checkout failure. Low = users abandon, high = the fallback UI is good enough.
”What % of users with saved cards on file end up entering a new card?”
Filter: sessions where event = "saved_card_skipped_for_new" AND eventually event = "purchase", divided by sessions where saved_cards_count > 0 AND event = "view_cart".
Why useful: trust signal for the saved-card UI. High % = users don’t recognize / trust their saved cards.
”Sessions that abandoned during processing_payment stage, in the last 7 days.”
Filter: event = "payment_abandoned" AND properties.stage = "processing_payment" AND timestamp >= now() - interval 7 day.
Why useful: stage-2 abandonment is your “we lost them while talking to the gateway” metric. Comparing volume across stages reveals which sub-step is the longest / scariest.
”Top error_message in stage-2 unknown payment errors, last 7 days.”
Filter (Error Tracking): $exception events where properties.action_type = "CREATE_ORDER" AND properties.stage = "processing_payment" AND timestamp >= now() - interval 7 day, grouped by properties.error_message.
Why useful: surfaces patterns in the unanticipated-error catch-all. Recurring messages = candidate for promotion into a named expected error class.
”Sessions where the SecureContainer iframe failed to load — by store.”
Filter (Error Tracking): exceptions where properties.source = "secure_container_iframe", grouped by properties.storeId.
Why useful: per-store iframe load reliability. Bursts on one store = CSP / network / DNS issue scoped to that domain.
Advanced — multi-event behavioral analysis
”Sessions in the last hour where the user changed the shipping method more than 3 times and didn’t purchase anything.”
HogQL sketch:
SELECT session_id
FROM events
WHERE timestamp >= now() - interval 1 hour
GROUP BY session_id
HAVING countIf(event = 'delivery_method_changed') > 3
AND countIf(event = 'purchase') = 0
Filter explanation: groups events into sessions, keeps those with >3 delivery_method_changed and zero purchase within the same time window.
Why useful: indecisive shipping behavior strongly correlates with churn. Joining with Session Replay on those session_ids lets you watch why.
HogQL sketch:
SELECT quantile(0.5)(properties.time_on_step_ms) / 1000 AS median_seconds
FROM events
WHERE event = 'checkout_step_back_clicked'
AND properties.from_step = 'payment'
AND properties.to_step = 'shipping'
AND timestamp >= now() - interval 30 day
Why useful: a fast back means “saw something wrong, fled”; a slow back means “tried, failed, gave up”. Different fix.
”Users who hit any coupon_apply_failed and then abandoned during payment processing.”
Funnel + filter: sessions with coupon_apply_failed followed by payment_abandoned within the same session, no purchase in between.
Why useful: does coupon friction cascade into later abandonment? If yes, the coupon UI is leaking checkout completion.
”Top from_method → to_method transitions for payment_method_changed, last 30 days.”
Filter: event = "payment_method_changed", grouped by (from_method_name, to_method_name) tuple, ordered by count desc.
Why useful: reveals “everyone tries credit_card first then switches to pix” patterns — informs which method should be the default.
”Sessions that hit payment_precondition_required with condition_ids containing pix and successfully purchased — average time between events.”
HogQL sketch:
SELECT avg(date_diff('second', precondition_time, purchase_time)) AS avg_seconds
FROM (
SELECT
session_id,
minIf(timestamp, event = 'payment_precondition_required'
AND has(properties.condition_ids, 'pix')) AS precondition_time,
minIf(timestamp, event = 'purchase') AS purchase_time
FROM events
WHERE timestamp >= now() - interval 30 day
GROUP BY session_id
)
WHERE precondition_time > 0 AND purchase_time > precondition_time
Why useful: how long Pix payments actually take end-to-end. Distribution of this metric drives QR-code timeout / polling-interval tuning.
”Sessions that triggered the order-creation terminal failure exception, then retried and succeeded.”
Filter: sessions with both an exception (origin = "ollie", stage = "creating_order") and a later purchase event.
Why useful: tells you how often the retry path actually works for users (vs. how often the engineering error becomes a lost sale). Compare to total exception volume.
Network-request prompts (Session Replay)
These use the captured request data from network_timing. PostHog routes them against the recordings / replay layer instead of the events table — phrase the prompt so the assistant knows you’re querying Session Replay.
”Show me sessions where any request to /checkout/api/orders took longer than 5 seconds.”
Filter: Session Replay → network.url matches /checkout/api/orders AND network.duration_ms > 5000.
Why useful: catches gateway / VTEX slow-path before users complain. Open the replay to see what the page was doing during the wait.
”Sessions in the last hour where any request returned a 5xx status.”
Filter: Session Replay → network.status_code >= 500 AND timestamp >= now() - interval 1 hour.
Why useful: complements the http_server_error event — gives you the visual replay alongside the structured event, so you can see what UI state the user was in when the request failed.
”Sessions that hit payment_abandoned and the last network request before abandonment was a payment-gateway call.”
Filter: events payment_abandoned joined to Session Replay; look at the last network.url matches /payment request occurring within the 30s window before the event.
Why useful: distinguishes “gateway is slow” abandonments from “user just changed their mind”. The latter has no in-flight payment call, the former does.
”What’s the p95 duration of /cart POSTs across stores, last 7 days?”
Filter: Session Replay network panel → network.url matches /cart$ AND network.method = POST, aggregate quantile(0.95)(duration_ms) grouped by storeId.
Why useful: shows which stores have the slowest cart-update path. Pair with coupon_apply_failed rate per store to confirm whether slow ≠ broken.
”Sessions with a network request to /customer-credentials that took longer than 2 seconds.”
Filter: Session Replay → network.url contains /customer-credentials AND network.duration_ms > 2000.
Why useful: long auth roundtrips are a common drop-off cause when the user expects an instant transition; tracking them lets you tie slow auth to abandonment.
Appendix: querying via the PostHog MCP
Once your PostHog MCP integration is configured, prompts like the ones above can be passed directly. Phrasing tips:
- Always anchor in time. PostHog defaults to 7 days; if you want last hour or last 30 days, say so.
- Mention the session boundary if relevant. “In the same session” is interpreted as PostHog’s
session_id grouping.
- Use property names verbatim.
from_step, stage, coupon_code_hash — exact spelling matches reduce ambiguity.
- Distinguish events from exceptions explicitly. “Show exceptions where…” vs “show events where…” — they live in different tables.
- For funnel-style questions, list the steps in order. PostHog’s funnel insight builder expects an ordered event list.