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 |
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.
| 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 — 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_info — packages_count (length of session.shipping.packages), shipping_type (delivery / pick_up / mixed based on the package mix), shipping_value (shipping subtotal in currency units).
purchase — shipping_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 block —
payment_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 block — connection_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.
| Property | Description |
|---|
stage | Always processing_payment |
error_message | Validation failure message |
| payment-method block | See note above |
| connection block | See note above |
duration_ms | See note above |
payment_unauthorized
PaymentUnauthorizedError: bank declined the charge.
| Property | Description |
|---|
stage | Always processing_payment |
error_message | Decline reason from the gateway |
| payment-method block | See note above |
| connection block | See note above |
duration_ms | See 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.
| Property | Description |
|---|
stage | Always processing_payment |
condition_ids[] | Array of precondition identifiers (e.g. ["3ds", "redirect"]) |
| payment-method block | See note above |
duration_ms | See 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.
| Property | Description |
|---|
stage | Always processing_payment |
error_message | Error message from PaymentTimeoutError |
| payment-method block | See note above |
| connection block | See note above |
duration_ms | See 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.
| Property | Description |
|---|
stage | Always processing_payment |
error_message | Error message from PaymentNotReadyError |
| payment-method block | See note above |
| connection block | See note above |
duration_ms | See 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.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 (brand: Visa, Mastercard, Pagaleve Transparente, …) |
from_method_type | Category of the previous method (credit_card, pix, apple_pay, …) — use this when you don’t care about the brand |
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) |
to_method_type | Category of the new method (credit_card, pix, apple_pay, …) — credit_card even for saved_card clicks; use is_saved_card to disambiguate |
is_saved_card | true when the click was on a stored card option |
installments_count_available | Number 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_option | true 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_count | Count 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_session | Monotonic counter per mount of the Payment step |
is_initial_default | true 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.
| Property | Description |
|---|
saved_cards_count | How many saved cards were offered |
cart_total | Total in minor units (cents) |
to_method_id | VTEX method id of the new credit-card brand the user clicked |
to_method_name | Brand of the new credit-card method (Visa, Mastercard, American Express, …) |
is_initial_default | true 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.
| Property | Description |
|---|
field | Which card field was first touched |
card_field_completed
The first time a given card field becomes valid (isValid flips true).
| Property | Description |
|---|
field | Which card field reached a valid state |
Use these for a card-entry funnel (card_field_touched{number} → card_field_completed{number} → … → add_payment_info → purchase) 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.
| 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
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.
| Property | Description |
|---|
page_title, page_location, page_referrer | Standard page-view fields, populated on every fire |
is_initial_view | true 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_step | Step id the user just entered (details, address, shipping, payment, …). Omitted when there’s no step query param (cart, order pages) |
entry_count | Monotonic 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_revisit | true when entry_count > 1. Convenience boolean for funnels / breakdowns |
previous_step | Step the user came from. null on the first step-bearing page_view |
time_on_previous_step_ms | Milliseconds 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).
| 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.
”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_unauthorized → payment_method_changed → purchase.
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.
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:
- 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.