Skip to main content

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:
PropertySourceExample
storeIduseStoreInfo()eaf782f0-…
sessionIduseCheckoutSession()VTEX OrderForm id
userIduseCheckoutSession().session.user?.idVTEX userProfileId (when identified)
platformuseStoreInfo()vtex
templateuseStoreInfo()default / grocery / sales
envprocess.env.NODE_ENVproduction
Logged-in vs guest is inferrable from userId — it maps to the OrderForm userProfileId via mapOrderFormUser (packages/commerce-clients/src/clients/platforms/vtex/mappers/user.ts), so userId IS NOT NULL means the shopper has a VTEX account.
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.
EventWhen it fires
view_cartCart page mounts with items
begin_checkoutUser enters the first details step
add_to_cartQuantity increase or ADD_ITEMS server action succeeded
remove_from_cartQuantity decrease, item removal, or REMOVE_ITEMS action
add_shipping_infoShipping addresses/packages updated; also fires once when email becomes present and shipping is already valid
add_payment_infoPayment method change committed (UPDATE_PAYMENT_METHODS)
purchaseOrder successfully created — carries transaction_id
page_viewExplicit page-view tracker — fires on initial page load and on every checkout-step query-param change (separate from PostHog’s own $pageview auto-capture)
All carry the ecommerce payload described above (currency, items, value).
Top-level (flat) properties on ecommerce events. PostHog filters and breakdowns operate on flat keys — ecommerce.value is hard to query against. To keep PostHog queries ergonomic, every ecommerce event additionally carries these at the top level (parsed in parseTopLevelEcommerceProperties):
  • value — order total in currency units (e.g. 129.90), same number as ecommerce.value. Use this in PostHog filters and as the funnel breakdown property.
  • cart_total — alias of value, kept for parity with saved_card_skipped_for_new which already used this name.
  • currency — ISO currency code (e.g. BRL).
  • items_count — sum of quantity across all cart items.
Event-specific additions:
  • add_shipping_infopackages_count (length of session.shipping.packages), shipping_type (delivery / pick_up / mixed based on the package mix), shipping_value (shipping subtotal in currency units).
  • purchaseshipping_value, payment_type (human-readable method name).

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 step — outcome events

These fire from useCheckoutOrderAction in packages/react/src/providers/checkout-action/index.tsx when submitPayment resolves or throws a specific class. Each represents an expected business outcome (not a bug).
Outcome events sourced from useCheckoutOrderAction (payment_validation_failed, payment_unauthorized, payment_precondition_required, and purchase) all carry:
  • The payment-method blockpayment_method, payment_method_id, payment_method_type, is_saved_card, installments read from session.payment.selectedPayments[0], plus installments_count_available (count of installment options offered by the selected method, taken from method.installments.length), has_installments_option (boolean — true when installments_count_available > 1, i.e. the installments dropdown was shown), and saved_cards_count (count of non-expired saved cards on file at outcome time).
  • duration_ms — milliseconds from the submitPayment call to the outcome firing. Lets you separate “failed in 200ms = local validation” from “failed in 30s = gateway timeout disguised as decline”.
Failure events (payment_validation_failed, payment_unauthorized) additionally carry the connection blockconnection_type, network_downlink_mbps, network_rtt_ms, network_save_data read from navigator.connection.payment_timeout is the exception — it fires from the secure-iframe timeout handler one level above useCheckoutOrderAction, so it stays minimal. Property names are flat (no nested payment object) so PostHog breakdowns work out of the box.Installments-visibility analysis — funnel payment_method_changed → purchase (or add_payment_info → purchase) broken down by has_installments_option answers “does showing the installments dropdown move conversion?”. installments_count_available lets you slice further (e.g. methods offering 12x vs 6x vs 1x). For saved cards specifically the dropdown is fed by a separate /installments fetch keyed by brand, so installments_count_available on the static method object can undershoot the actual dropdown count; treat it as a “lower bound” indicator of “dropdown shown” rather than the exact option count for saved-card flows.

payment_validation_failed

User-side data problem caught by PaymentValidationError: bad CVV, expired card, malformed number.
PropertyDescription
stageAlways processing_payment
error_messageValidation failure message
payment-method blockSee note above
connection blockSee note above
duration_msSee note above

payment_unauthorized

PaymentUnauthorizedError: bank declined the charge.
PropertyDescription
stageAlways processing_payment
error_messageDecline reason from the gateway
payment-method blockSee note above
connection blockSee note above
duration_msSee note above

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.
PropertyDescription
stageAlways processing_payment
condition_ids[]Array of precondition identifiers (e.g. ["3ds", "redirect"])
payment-method blockSee note above
duration_msSee note above

payment_timeout

The secure iframe didn’t respond to the submit postMessage within SECURE_PAYMENT_TIMEOUT. createSubmitPayment rejects with PaymentTimeoutError, which useCheckoutOrderAction’s catch block treats as an expected payment outcome and tracks here. Fires from packages/react/src/providers/checkout-action/index.tsx in the submitPayment catch when error instanceof PaymentTimeoutError.
PropertyDescription
stageAlways processing_payment
error_messageError message from PaymentTimeoutError
payment-method blockSee note above
connection blockSee note above
duration_msSee note above

payment_not_ready

The secure iframe didn’t reply to the pre-submit submit:ping handshake within SECURE_PAYMENT_PING_TIMEOUT. Distinct from payment_timeout: we know the gateway was never called, so the failure is safe to retry immediately. Typically fires when the user clicks Pay against an iframe that re-rendered mid-flow or whose container:ready was lost. Fires from packages/react/src/providers/checkout-action/index.tsx in the submitPayment catch when error instanceof PaymentNotReadyError.
PropertyDescription
stageAlways processing_payment
error_messageError message from PaymentNotReadyError
payment-method blockSee note above
connection blockSee note above
duration_msSee note above (close to SECURE_PAYMENT_PING_TIMEOUT)

Payment step — choice events

payment_method_changed

Fires on every payment-method click in the Payment step (templates/default/src/components/steps/Payment/Payment.tsxhandlePaymentMethodChange).
PropertyDescription
from_method_idVTEX method id of the previously selected method, if any
from_method_nameHuman-readable previous method name (brand: Visa, Mastercard, Pagaleve Transparente, …)
from_method_typeCategory of the previous method (credit_card, pix, apple_pay, …) — use this when you don’t care about the brand
to_method_idVTEX method id of the newly selected method
to_method_nameHuman-readable new method name (saved_card if the click was a saved card)
to_method_typeCategory of the new method (credit_card, pix, apple_pay, …) — credit_card even for saved_card clicks; use is_saved_card to disambiguate
is_saved_cardtrue when the click was on a stored card option
installments_count_availableNumber of installment options offered by the newly selected method — selectedMethod.installments.length. null when the method has no static installment options (e.g. Pix, boleto, express methods). For saved-card clicks this reflects the brand’s static method options; the actual dropdown count is fetched dynamically and may differ.
has_installments_optiontrue when installments_count_available > 1, i.e. an installments dropdown is shown for this method. Use as the primary slice for “does showing parcelas move conversion?” funnels.
saved_cards_countCount of non-expired saved cards the user has on file at the moment of the click. Lets you correlate selection behavior with the user’s saved-card inventory size (0, 1, >1).
change_count_in_sessionMonotonic counter per mount of the Payment step
is_initial_defaulttrue when this is the first click in the session and a method was already pre-selected by VTEX/the server — the from_* properties describe that pre-selection, not a user choice. Filter is_initial_default = false when analyzing real user-driven transitions.

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.
PropertyDescription
saved_cards_countHow many saved cards were offered
cart_totalTotal in minor units (cents)
to_method_idVTEX method id of the new credit-card brand the user clicked
to_method_nameBrand of the new credit-card method (Visa, Mastercard, American Express, …)
is_initial_defaulttrue when the skip happened on the user’s first click and a non-saved credit-card method was already pre-selected by VTEX. The user didn’t actively reject the saved card — VTEX’s default forced the skip. Filter is_initial_default = false when measuring genuine saved-card distrust.

Card entry — field interaction

Low-volume milestone events fired from templates/default/.../CreditCard/platforms/vtex/InputCreditCard.tsx as the shopper fills the credit-card form. The card fields live in per-field secure iframes (@ollie-shop/secure’s SecureInput); these events are derived only from the isValid/isEmpty metadata the iframe already posts back — the card number, BIN, CVV, and any field value never leave the iframe and are never attached here (PCI-safe). To stay cheap, each fires at most once per field per mount (guarded by refs), not per keystroke. Expect ≤ ~10 of these per card-entry attempt, vs. dozens if they were per-change.
field is the secure-field identifier: number, expiry, document, verification-value, or name.

card_field_touched

The first time the shopper changes a given card field — i.e. they started entering it.
PropertyDescription
fieldWhich card field was first touched

card_field_completed

The first time a given card field becomes valid (isValid flips true).
PropertyDescription
fieldWhich card field reached a valid state
Use these for a card-entry funnel (card_field_touched{number}card_field_completed{number} → … → add_payment_infopurchase) to see which field shoppers get stuck on (touched but never completed), and to isolate card-entry abandonment that the cross-origin payment iframe otherwise hides.

Shipping step

delivery_method_changed

Fires from templates/default/src/components/steps/Shipping/DeliveryMethodSelector.tsx when the user picks delivery / pickup / custom.
PropertyDescription
from_methoddelivery | pickup | custom | null (first selection)
to_methodSame enum
available_methods_countHow 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.
PropertyDescription
coupon_code_hashNon-reversible char-sum hash of the attempted code (c_<base36>). Same code in the same session produces the same hash, enabling retry analysis.
server_errorServer-side error message
error_codeError code from the action response
attempts_in_sessionMonotonic counter — resets on success

Checkout step navigation

page_view (with checkout step properties)

Fires from templates/default/src/components/tracking/PageViewTracker.tsx. On the /details page the tracker stays mounted across step changes, so it re-fires every time the step query param changes — making each step entry observable as its own page_view. A per-step entry counter survives the navigations.
PropertyDescription
page_title, page_location, page_referrerStandard page-view fields, populated on every fire
is_initial_viewtrue on the very first page_view of this PageViewTracker mount (preserves the legacy once-per-page semantics for queries that pre-date the step-change refire), false on subsequent step-change fires
checkout_stepStep id the user just entered (details, address, shipping, payment, …). Omitted when there’s no step query param (cart, order pages)
entry_countMonotonic count of how many times the user has now entered this step within the checkout visit — 1 on first entry, 2+ on re-entry. Only set when checkout_step is present
is_revisittrue when entry_count > 1. Convenience boolean for funnels / breakdowns
previous_stepStep the user came from. null on the first step-bearing page_view
time_on_previous_step_msMilliseconds spent on the previous step before this navigation. null on the first step-bearing page_view
Use this to detect when shoppers re-visit a step (e.g. “went back from shipping to address and forward again”) — filter event = "page_view" AND is_revisit = true AND checkout_step = "address".
Compatibility with pre-existing page_view analyses. Before this change page_view only fired once per page load. Queries that counted page_view as a proxy for “checkout entries” will now over-count, since each step navigation also fires page_view. To restore the legacy behavior add is_initial_view = true to those filters; to opt into the new step-revisit slice, filter is_initial_view = false or just use is_revisit.

checkout_step_back_clicked

Fires from the prev-step link in templates/default/src/components/step-navigation/StepNavigation.tsx. Complements checkout_step_viewed by isolating the explicit back-click signal (vs. any step entry).
PropertyDescription
from_stepCurrent step (details, shipping, payment, …) — read from the step query param
to_stepprev.id or cart
time_on_step_msMilliseconds 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).
PropertyDescription
slot_idThe Slot id of this wrapper instance

express_checkout_succeeded

Express flow completed and the order was created.
PropertyDescription
slot_idSame as above
order_idThe created order id
duration_msFrom initiation to success

express_checkout_failed

Express flow threw an error (excluding the muted "ApplePaySDK failed to load" noise filter).
PropertyDescription
slot_idSame as above
error_messageOriginal error message
duration_msFrom 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.).
PropertyDescription
origin"ollie"
action_typeAction name (UPDATE_COUPONS, UPDATE_PAYMENT_METHODS, …)
server_errorMessage from server
error_codeServer error code
platform_codeVTEX platform error code
error_groupVTEX 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.
PropertyDescription
origin"ollie"
action_type"CREATE_ORDER"
stage"creating_order"
error_code, error_group, platform_codeServer-side error metadata
server_errorServer message
has_validation_errorsBoolean — 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.
PropertyDescription
origin"ollie"
action_type"CREATE_ORDER"
stage"processing_payment"
error_messageOriginal 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).
PropertyDescription
origin"ollie"
source"secure_container_iframe"
iframe_urlFull URL of the payments iframe that failed
session_idCheckout 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".
PropertyDescription
status_codeHTTP status
urlFailing URL
methodHTTP method
response_bodyFirst 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:
LayerWhereWhat it adds
recordHeaders: truesession_recording in apps/web/src/instrumentation-client.tsRequest and response headers (subject to the mask function below)
recordBody: truesameRequest and response bodies (truncated to 1 MB per request, mask applied)
capture_performance.network_timing: trueposthog.init(...) top-levelPerformance Timing data per request — DNS, TCP, TTFB, transfer size. Server-side feature toggle on the PostHog project must also be enabled.
maskCapturedNetworkRequestFnapps/web/src/providers/posthog-provider/index.tsxStrips 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:
  1. Sessions where express_checkout_initiated fired (Apple Pay slot) and the order page (page_view with url contains /order/) never followed.
  2. 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.

”Users who used the back button from the payment step in the last hour.”

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_failedpayment_method_changedpurchase. 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.

”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.

Step-revisit & shipping-shape questions

These rely on properties added by this round of telemetry: per-step entry counters on page_view, packages_count / shipping_type on add_shipping_info, and top-level value on purchase.

”What % of users who re-enter the address step end up abandoning?”

Filter: sessions with event = "page_view" AND checkout_step = "address" AND is_revisit = true, divided into those that later fire purchase and those that don’t. Why useful: directly answers “do users who keep going back to address abandon more?”. time_on_previous_step_ms on the same event tells you how long they stayed on the step they’re coming from.

”Time on shipping step when there’s pickup or multiple packages, broken down by conversion.”

“Conversion” here = user moved from shipping to payment and did not come back to shipping (we infer “did not come back” from max(entry_count) = 1 on shipping page_views in the session). HogQL sketch:
SELECT
  shipping_type,
  packages_count > 1 AS is_multi_package,
  countIf(shipping_converted) AS converted,
  count() AS reached_shipping,
  avg(time_on_shipping_ms) AS avg_ms
FROM (
  SELECT
    session_id,
    anyIf(properties.shipping_type, event = 'add_shipping_info') AS shipping_type,
    anyIf(properties.packages_count, event = 'add_shipping_info') AS packages_count,
    -- time spent on shipping = ms recorded when the user left shipping for any other step
    minIf(properties.time_on_previous_step_ms,
          event = 'page_view'
            AND properties.previous_step = 'shipping') AS time_on_shipping_ms,
    -- shipping visited at least once
    countIf(event = 'page_view' AND properties.checkout_step = 'shipping') > 0
      AS reached_shipping,
    -- and never re-entered (max entry_count stayed 1)
    maxIf(properties.entry_count,
          event = 'page_view' AND properties.checkout_step = 'shipping') = 1
      AS no_return_to_shipping,
    -- and reached payment afterwards
    countIf(event = 'page_view' AND properties.checkout_step = 'payment') > 0
      AS reached_payment,
    (reached_shipping AND no_return_to_shipping AND reached_payment)
      AS shipping_converted
  FROM events
  WHERE timestamp >= now() - interval 30 day
  GROUP BY session_id
  HAVING reached_shipping
)
GROUP BY shipping_type, is_multi_package
If you also want to weed out sessions that converted to payment but never purchased, add countIf(event = 'purchase') > 0 to the inner SELECT and use it as another dimension — the question above stops at “moved forward without backtracking” by design. Why useful: isolates pickup vs delivery vs mixed flows, splits by single-package vs multi-package, and joins shipping-step dwell time with conversion in one shot.

”When a card errors out, which method do users switch to and close the order with?”

Funnel: payment_validation_failed OR payment_unauthorizedpayment_method_changedpurchase. Breakdown: payment_method_changed.to_method_name. Window: same session, 30 min. Why useful: directly answers “after a card error, which method recovers the sale?”. The same query keyed by error_message reveals whether specific failure modes have specific recovery paths (decline → pix, validation → another card, etc).

”Saved-card conversion rate, split by orders above vs below R$100.”

Funnel: add_payment_info (filter is_saved_card = true) → purchase. Breakdown: bucket the funnel by value ≥ 100 vs < 100 (use the top-level value field on purchase/add_payment_info, in BRL). Why useful: the question asked. The top-level value lets you filter and bucket directly in PostHog without joining on begin_checkout.cart_total.

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.

”Median time users spend on the payment step before going back to shipping.”

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.

”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.

”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:
  1. Always anchor in time. PostHog defaults to 7 days; if you want last hour or last 30 days, say so.
  2. Mention the session boundary if relevant. “In the same session” is interpreted as PostHog’s session_id grouping.
  3. Use property names verbatim. from_step, stage, coupon_code_hash — exact spelling matches reduce ambiguity.
  4. Distinguish events from exceptions explicitly. “Show exceptions where…” vs “show events where…” — they live in different tables.
  5. For funnel-style questions, list the steps in order. PostHog’s funnel insight builder expects an ordered event list.