Conversion Funnel Optimization Questions
Analyzing and improving a bounded, ordered conversion path: mapping the sequence of steps a user takes from acquisition through one terminal conversion or activation event (signup, first purchase, first paid order, trial-to-paid, onboarding to first-success), computing step-to-step and overall conversion rates and drop-off, and diagnosing where and why users fall out. Covers the SQL and query techniques for computing funnel metrics at scale (stage-by-stage conversion tables, time-to-conversion and time-to-first-value, cohort LTV measured within a funnel window, path analysis across non-linear user journeys, event instrumentation and data-quality practices for funnel tracking), attribution modeling for crediting conversions across channels and touchpoints (first-touch, last-touch, linear, time-decay, Markov-chain, and Shapley-value approaches) and customer acquisition cost by channel, and the experiment design and statistics used to validate funnel changes (A/B and multi-armed-bandit test design, sample-size and power calculations, quasi-experimental methods such as difference-in-differences and synthetic control when randomization is not possible, and testing whether a single funnel-stage drop is a real, statistically significant shift rather than noise). Also covers diagnosing UX and flow friction that causes drop-off (checkout, signup, and onboarding friction points) and prioritizing a program of funnel-improvement experiments (impact and effort frameworks such as RICE or ICE, guardrail metrics, roadmap sequencing). Distinct from User Retention and Engagement, which covers what an already-converted or already-activated user does afterward: repeat usage over time, cohort retention curves, DAU/WAU/MAU, churn, and reactivation. A question belongs here if it concerns a user's first, bounded pass toward one conversion or activation event; it belongs to User Retention and Engagement if it concerns recurring behavior after that event. General-purpose rolling-window anomaly and change-point detection techniques (CUSUM, Bayesian change-point, seasonality-aware baselines) for monitoring any metric over time belong to the companion topic Advanced SQL: Metric Monitoring, Anomaly Detection, and Data Correctness at Scale, not here.
Tell me about a time you discovered a tracking bug that materially affected business decisions. Describe the context, how you detected the issue, steps you took to diagnose and fix it, how you communicated the impact to stakeholders, and what preventive measures you implemented afterwards.
Sample Answer
Direct answer
This is scored on whether the candidate can walk an interviewer through a real incident with the discipline of an actual post-mortem, not on how dramatic the bug was. Structure it with Situation, Task, Action, Result (STAR): the context the tracking sat in and why it mattered, how the discrepancy was actually noticed (not "we just knew"), the concrete diagnostic steps that pinned the root cause and the fix that followed, exactly how the business impact was communicated to stakeholders once it was understood, and the specific preventive measure put in place afterward so the same class of bug cannot recur silently again.
Structured elaboration
Situation (context). State what the tracking was being used for and why a bug in it was consequential: a decision (a launch go/no-go, a budget reallocation across channels, a pricing change) was made, or was about to be made, using a metric that turned out to be wrong. Name the specific metric and the specific decision it fed, not "our metrics were affected in general," since the business-impact weight of this story depends entirely on what was actually at stake.
Task. The candidate's specific role in discovering and resolving it. Ideally something owned directly (the person who found the anomaly, or who was assigned to run it down), not a bug someone else diagnosed that the candidate only heard about secondhand.
Action, detection. How the discrepancy was actually noticed matters to the story's credibility: a number that looked implausible against a sanity check (a conversion rate suddenly higher than any comparable historical period, or a channel showing zero volume when spend clearly continued), a cross-check against an independent source (a payment processor's own reporting disagreeing with the internal dashboard), or a stakeholder asking a question the data could not actually answer consistently. Naming the specific trigger, rather than a vague "I noticed something was off," is what separates a credible account from a generic one.
Action, diagnosis and fix. Walk through the actual debugging path: which layer turned out to be at fault (a client-side instrumentation bug, an error in the extract-transform-load or ETL data-processing step, a broken join, a misconfigured filter in the reporting layer), how that was isolated (comparing counts at successive stages of the pipeline, checking a recent deploy or schema change against the timing of when the numbers started drifting), and what the fix specifically was (a corrected event definition, a patched join key, a backfill of affected data). This is the part interviewers probe hardest, so it needs to show real technical reasoning, not just "we found the bug and fixed it."
Action, stakeholder communication. Describe how the impact was actually communicated once understood: what was said, to whom, and how directly the "this changes the decision we were about to make" implication was stated rather than buried in a technical caveat. A strong version of this shows the candidate proactively surfacing a finding that reflected badly on the team's own prior reporting, rather than only fixing it quietly.
Result, preventive measures. What changed afterward so the same failure mode cannot recur unnoticed: a monitoring check or alert on the specific metric or pipeline stage that broke, a reconciliation process comparing two independent sources on a schedule, a code-review or schema-change checklist item, or a broader instrumentation-testing practice. A story that ends at "we fixed the bug" without this part is missing the piece that shows the candidate thinks about systems, not just incidents.
Worked example
An illustrative version of this story, built to show the shape rather than as a claim of a specific real outcome. At a company running a multi-step signup funnel, the marketing team was preparing to reallocate a meaningful share of acquisition spend toward the channel that appeared to be converting best according to the internal dashboard. Before the reallocation was finalized, the metric looked implausible on a routine sanity check: that channel's conversion rate had climbed well above every other channel's, and above its own historical range, right after a client library update had shipped two weeks earlier. Diagnosing it: comparing raw event counts against the BI-layer numbers at each pipeline stage showed the raw counts were consistent, but a join in the transformation layer that attributed conversions to channels was silently defaulting unmatched attribution records to that one channel instead of dropping or flagging them, a bug introduced by the same library update that had changed how a upstream field was formatted. The fix corrected the join's matching logic and reprocessed the affected date range. Communicating the impact meant going directly to the marketing lead before the reallocation was approved, stating plainly that the channel's apparent lead was substantially an artifact of the bug rather than real performance, and providing the corrected comparison so the decision could be re-evaluated on accurate numbers rather than quietly footnoting the correction after the fact. Afterward, the team added an automated check comparing total attributed conversions against total raw conversions on a schedule, alerting if the two diverged beyond a small tolerance, specifically so an attribution-layer bug like this one would surface on its own rather than depending on someone noticing an implausible number before a decision got made on it.
Trade-offs and pitfalls
- Fabricating precise-sounding metrics is the single most common way this answer goes wrong. A suspiciously exact improvement or error figure with no explanation of how it was derived reads as invented to an experienced interviewer; if exact numbers cannot be shared or are not precisely remembered, describe the direction and rough magnitude honestly ("meaningfully higher than any other channel," "roughly a third of the reported conversions") rather than reaching for false precision.
- Skipping the communication step, or describing it too vaguely, undercuts the story. "I let the team know" gives an interviewer nothing to probe; naming who specifically was told, what was actually at stake for them, and how directly the finding was stated is what makes this part credible.
- Ending at the fix, without naming a concrete preventive measure, leaves the story half-finished. The prevention step is what shows systems thinking rather than one-off firefighting, and a vague "we were more careful after that" is a weaker answer than a specific monitoring check, alert, or process change.
- Taking sole credit for a diagnosis that was actually a team effort tends to read as less credible, not more; a version of the story that includes how the candidate collaborated with whoever owned the affected pipeline stage is usually a stronger and more believable account than a solo-hero framing.
You're mapping and instrumenting the signup-to-purchase user journey for a mobile consumer app. For each funnel step (e.g., 'app_open', 'product_view', 'add_to_cart', 'checkout_start', 'purchase') list the event names, required event properties (including data types), which events belong client-side vs server-side, and how you'd ensure idempotency and ordering. Also describe a simple versioning strategy for the event schema to support analytics over time.
Sample Answer
Direct answer
Instrument each step (app_open, product_view, add_to_cart, checkout_start, purchase) as its own named event carrying a typed property schema, fire user-initiated events client-side and money-moving or security-sensitive events server-side (ideally both, with the server-side event as the source of truth), guarantee idempotency with a client-generated event ID the backend deduplicates on, guarantee ordering with a monotonic client-side sequence number rather than trusting wall-clock arrival order, and version the schema explicitly so historical events remain interpretable as the tracking plan evolves.
Structured elaboration
Event names and required properties, with data types. A consistent naming convention (snake_case, object_action ordering) makes the tracking plan self-documenting:
| Event | Event name | Required properties (name: type) |
|---|---|---|
| App opened | app_open | session_id: string, app_version: string, platform: enum('ios','android'), is_cold_start: boolean |
| Product viewed | product_view | product_id: string, category: string, price_cents: integer, currency: string (ISO 4217, the standard 3-letter currency code), referrer_screen: string |
| Added to cart | add_to_cart | product_id: string, quantity: integer, unit_price_cents: integer, cart_id: string |
| Checkout started | checkout_start | cart_id: string, item_count: integer, cart_value_cents: integer, currency: string |
| Purchase completed | purchase | order_id: string, cart_id: string, total_cents: integer, currency: string, payment_method: enum, tax_cents: integer |
Every event, regardless of stage, also carries a common envelope: event_id: string (UUID, a universally unique identifier), user_id: string (nullable pre-login), client_event_ts: timestamp, schema_version: integer. price_cents and total_cents are integers (minor currency units, cents) rather than floating-point dollars, a deliberate choice: floating-point representations of money accumulate rounding error across aggregation and are a well-known source of off-by-a-cent bugs in analytics pipelines.
Client-side versus server-side split. The rule of thumb: fire an event client-side when it represents pure UI interaction the server has no independent way to observe (app_open, product_view, add_to_cart are all things a user does inside the app that the backend only learns about if the client tells it). Fire an event server-side, or duplicate it server-side, when it represents something the backend can verify independently and where correctness matters for revenue or fraud (purchase above all: the backend that actually processes payment and creates the order record is the authoritative source, since a client-only purchase event can be spoofed, dropped by a crash before it sends, or double-fired by a flaky retry). checkout_start is a reasonable middle case: client-fired for responsiveness, but if the backend also creates a "checkout session" record when the checkout API is called, that server record becomes a valuable cross-check against the client event, useful for detecting instrumentation bugs (a mismatch between client-reported and server-reported checkout starts is itself a signal something is broken).
Idempotency. Mobile clients retry on flaky networks, and a user can also double-tap a button before the UI disables it; both can fire the same logical event twice. The fix is a client-generated event_id (a UUID created at the moment the event is captured, not at send time, so a retried send reuses the same ID) that the ingestion backend treats as a deduplication key, typically via an upsert or a short-lived dedup cache keyed on event_id. Server-side events get the same treatment using a server-generated idempotency key tied to the business operation (for purchase, the order_id itself is a natural idempotency key, since the payment provider's own idempotency guarantees usually already prevent a duplicate charge from a retried request).
Ordering. Client-side wall-clock timestamps are not reliable for ordering: device clocks drift, users travel across timezones, and events queued while offline can be sent in a batch after connectivity returns, arriving at the server in a different order than they occurred. Attach a monotonically increasing client-side sequence number per session (a simple incrementing counter, reset at app_open) alongside the timestamp; downstream processing sorts by (session_id, sequence_number) for anything ordering-sensitive, and treats client_event_ts as informational (useful for latency analysis) rather than authoritative for sequencing.
Schema versioning strategy. Three practical pieces: (1) every event payload carries an explicit schema_version: integer field, incremented whenever a property is added, removed, or its type or meaning changes; (2) the ingestion pipeline keeps a versioned schema registry so a downstream consumer can look up "what did schema_version: 3 of purchase mean" rather than guessing from the data; (3) additive changes (a new optional property) do not require a version bump if consumers are built to tolerate unknown fields, but changing an existing property's TYPE or MEANING (renaming price to price_cents and changing units) always requires a version bump, since silently reinterpreting old data under a new meaning corrupts historical analysis.
Worked example
Single-event deep dive: checkout_complete, naming conventions and privacy. Take one event and work through it end to end, since the same discipline scales to all five.
- Naming convention.
checkout_complete, notCheckoutComplete,checkout-complete, ororderPlaced: consistentsnake_case, past-tense verb, and a stable name that survives UI copy changes (the button might say "Place order" today and "Buy now" tomorrow, the event name should not follow the button copy). - Required properties:
event_id: string (UUID),user_id: string,order_id: string,total_cents: integer,currency: string,item_count: integer,payment_method: enum('card','paypal','apple_pay','google_pay'),client_event_ts: timestamp,schema_version: integer. - Privacy and GDPR (General Data Protection Regulation, the EU's data-protection law). This event should NOT carry raw payment instrument data (card numbers, CVV, the card verification value printed on the card) at all, that belongs exclusively to the payment processor's PCI (Payment Card Industry) compliance-scoped systems, never the analytics pipeline. It also should not carry the user's full name, email, or shipping address as flat properties on an analytics event: those are personal data under GDPR, and putting them on a high-fan-out analytics event multiplies the number of systems a deletion request has to reach. The safer pattern is to reference identity via
user_idonly and let a deletion request cascade through a single identity-resolution point, rather than through every event table that happens to have denormalized the user's PII (personally identifiable information) onto it. If a property is genuinely needed for analysis and is itself PII (an email domain for B2B segmentation, for example), document it explicitly in the schema registry as PII-tagged so retention and deletion tooling can find and purge it on request, rather than discovering years later that it was never covered.
Trade-offs and pitfalls
- Client-only tracking of
purchaseis the single highest-risk shortcut. It looks fine in a demo and silently underreports whenever the app crashes between payment success and the event firing, or whenever a user is offline at the moment of purchase and the queued event never flushes; a revenue-critical event needs a server-side source of truth, with the client event used for early UX-level signal (like showing a live purchase counter), not for the number that reconciles against actual revenue. - Common mistake: treating
client_event_tsas authoritative for ordering. A batch of offline-queued events flushed on reconnect can haveclient_event_tsvalues that are minutes or hours apart but arrive at the server within the same second; sorting by arrival time or by client timestamp alone, instead of the session-scoped sequence number, produces a funnel that looks like users completed checkout before adding anything to their cart. - Under-scoping the schema-versioning cost. Adding
schema_versionafter a tracking plan is already live and has a year of un-versioned historical data is far more expensive than starting with it: retrofitting requires either backfilling a version onto old rows (often ambiguous, since you cannot always tell which version an old row was written under) or maintaining permanent special-case logic for "pre-versioning" data. Start every new event with a version field from day one, even a v1, even if it never needs to change. - Over-instrumenting client-side PII "just in case." Denormalizing user attributes onto every event because a future analysis MIGHT need them creates real privacy-compliance surface area (more systems to purge on a deletion request) for speculative value; prefer joining to a slim, well-governed user dimension table at query time over duplicating PII onto high-volume event streams.
You observe a marketing channel shows large conversion increases for a cohort, but you're concerned about confounding factors. Describe analyses and diagnostics you would run to detect selection bias, seasonality, or other confounders, and how you would adjust your estimate (e.g., weighting, matching, placebo tests).
Sample Answer
Direct answer
A channel's raw conversion lift is confounded whenever exposure to it was not randomly assigned, which is almost always true of observational channel data, so before crediting the channel with a causal effect, run diagnostics for the three usual suspects, selection bias (who got exposed), seasonality (when the lift appeared), and other concurrent confounders (what else changed at the same time), and once a specific one is implicated, adjust the estimate with weighting, matching, or a placebo test depending on what exactly needs correcting.
Structured elaboration
Selection bias. The cohort that ended up exposed to the channel is systematically different from the cohort that did not, in ways related to the outcome itself, not just to the channel. A common concrete case: the channel targets warm leads or a lookalike audience built from past converters, so its exposed group was already more likely to convert before the channel ever touched them. Diagnostic: compare the exposed and unexposed cohorts' characteristics and, critically, their PRE-exposure baseline conversion rate; if the exposed group already converted better before the channel launched, that gap is evidence of selection, not of the channel's effect.
Seasonality. Conversion moves on its own calendar rhythm, day-of-week, a holiday season, a promotional calendar unrelated to this specific channel, and that rhythm can coincide with the channel's spend ramp by accident. Diagnostic: compare the exposed cohort's lift against the same period's movement in a comparable unexposed cohort, or against overall site-wide conversion; if everything moved together across cohorts that never saw the channel, a shared calendar effect is implicated instead of the channel.
Other confounders. Any third factor influencing both who got exposed and how likely they were to convert: a concurrent product launch, a competitor's outage, a parallel pricing promotion, a broad demand shift. Diagnostic: cross-reference the lift's timing against a change log or launch calendar for anything else that shipped in the same window, and check whether the "lift" also shows up in metrics the channel could not plausibly influence; a lift appearing broadly across unrelated funnels or products points at a shared external driver, not a channel-specific effect.
Adjustment methods, named as asked, mechanism and when to use each:
| Method | How it works | When to use it | Key risk |
|---|---|---|---|
| Weighting (inverse propensity weighting) | Estimate each unit's propensity to be exposed from its covariates, then reweight the comparison group so its weighted covariate distribution matches the exposed group's | Good covariate data on what drives exposure; want to keep full sample size rather than discard units | Corrects only for confounders actually measured and included; extreme propensity scores near 0 or 1 inflate weight variance |
| Matching | For each exposed unit, find one or more unexposed units with similar covariate (or propensity-score) values and compare outcomes only within matched sets | Want an intuitive, auditable comparison and distrust the functional-form assumptions a regression or weighting approach requires | Same measured-confounders-only limit as weighting; a loose match reintroduces the bias it was meant to remove |
| Placebo test | Re-run the identical lift analysis somewhere it should show no effect: a fake treatment date before the channel launched, or a metric or segment the channel could not plausibly move | As a sanity check on any observational causal claim; cheap, and it can falsify an analysis rather than only support one | A passed placebo test rules out that specific alternative explanation, it does not prove the remaining effect is genuinely causal |
Worked example
Weighting, with hand-computed numbers. Suppose users split into two propensity strata by prior purchase history, "high" and "low." In the exposed group, 70 percent of users are high-propensity (converting at 20 percent) and 30 percent are low-propensity (converting at 4 percent):
0.7×20%+0.3×4%=14%+1.2%=15.2%
In the unexposed group the composition is inverted, only 30 percent high-propensity, 70 percent low, with similar within-stratum rates (18 percent and 3 percent):
0.3×18%+0.7×3%=5.4%+2.1%=7.5%
The naive comparison, 15.2 percent versus 7.5 percent, is a 7.7-point gap that looks like a large channel effect. But most of that gap is composition: the exposed group simply contains far more of the users who convert well regardless of the channel. Reweighting the unexposed group to the exposed group's composition (apply the exposed group's 70/30 mix to the unexposed group's own within-stratum rates):
0.7×18%+0.3×3%=12.6%+0.9%=13.5%
The adjusted comparison is now 15.2 percent versus 13.5 percent, a 1.7-point gap, versus the naive 7.7-point gap: roughly three quarters of the apparent lift was compositional imbalance, not a channel effect.
Placebo test, with hand-computed numbers. Suppose the exposed cohort converts at 8 percent in the post-period against 5 percent for the unexposed cohort, a naive 3-point lift. Before trusting that number, run the identical comparison on the pre-period, before the channel existed: exposed 6 percent, unexposed 4 percent, a 2-point gap that predates the channel entirely and is direct evidence of a pre-existing difference between these two cohorts (selection bias). Netting the pre-period gap out of the post-period gap, the same difference-in-differences arithmetic used for a designed quasi-experiment, applied here as a diagnostic rather than a planned design:
(8%−6%)−(5%−4%)=2%−1%=1%
The genuine, confounder-adjusted lift is closer to 1 point, not the naive 3. The placebo test did its job: it caught that most of the naive lift was a pre-existing gap between the two cohorts, not something the channel produced.
Trade-offs and pitfalls
- All three methods only correct for what you can see or that shows up in the pre-period. Weighting and matching adjust for measured covariates; a genuinely unmeasured confounder (a sales rep manually reaching out to accounts that also happen to fall inside this channel's targeting) is invisible to either. A placebo test can catch a confounder that produces a spurious pre-period gap, but not one that only appears after the channel launched and mimics its timing exactly. Real unmeasured confounding is what an instrumental-variable approach or an actual randomized design exists to solve instead.
- Common mistake: running one diagnostic and declaring the effect confirmed. A clean covariate-balance check does not rule out seasonality, and a clean placebo test at one date does not rule out a different, later confounder. Each diagnostic only rules out one class of failure, so use several, not one, before trusting the estimate.
- Common mistake: treating a passed placebo test as proof, not a failed falsification attempt. A placebo test that shows a spurious effect is strong, direct evidence against the causal claim; a placebo test that shows nothing is necessary support for the claim, not sufficient proof of it.
- Weighting-specific pitfall: propensity scores near 0 or 1 produce very large weights that let a handful of units dominate the estimate; trim or cap extreme weights and check the effective sample size after weighting, not just the raw count before it.
- Matching-specific pitfall: a close match on the wrong covariates (matching on demographics when the real confounder is browsing behavior) creates a false sense of rigor without removing the actual bias; matching only helps to the extent the matched covariates are the ones truly driving both exposure and outcome.
Write a SQL query (standard SQL / BigQuery-compatible) that finds users who completed a funnel in order (visit -> signup -> purchase) where the time between any two consecutive steps is no more than 7 days. Use the events table below and produce counts and conversion rate. Explain how your query handles repeated events and out-of-order timestamps.
Schema:
events(user_id STRING, event_name STRING, occurred_at TIMESTAMP)
Sample Answer
Direct answer
Match each signup to its NEAREST preceding visit within 7 days, then each purchase to its nearest preceding qualifying signup within 7 days, using two correlated lookups rather than each user's globally-first visit and globally-first signup. This single design choice is what correctly handles repeated events, out-of-order timestamps, and users who re-enter the funnel: it always looks for the closest, chronologically valid predecessor for each step rather than assuming there is exactly one clean occurrence of each stage per user.
Structured elaboration
Why "nearest preceding match," not "first occurrence of each stage." This question is a genuinely harder companion to a simpler stage-by-stage funnel query: that simpler query counts distinct users who reached each stage at all, with no ordering or timing constraint between stages. This one requires visit, then signup, then purchase, IN ORDER, with each consecutive pair no more than 7 days apart. Taking the globally-first visit and globally-first signup per user (the natural first instinct) breaks under funnel RE-ENTRY: a user who visited once long ago, came back and visited again more recently, then signed up shortly after the SECOND visit, should qualify, since a valid 7-day-windowed chain genuinely exists using the second visit, even though the gap from the first visit is too large. Anchoring on the globally-first visit would incorrectly reject that user. Matching each signup to its nearest preceding visit within the window (via a correlated MAX(visit_ts) WHERE visit_ts <= signup_ts AND gap <= 7 days), and each purchase to its nearest preceding qualifying signup the same way, finds a valid chain if one exists, regardless of how many times the user re-entered earlier stages.
Repeated events. A user firing visit twice, or purchase twice, needs no special deduplication logic beyond what the nearest-match query already does: signup_matched and purchase_matched each independently produce one row per signup and one row per purchase, and the correlated subquery naturally picks the single closest qualifying predecessor among however many candidate rows exist, so duplicate visits do not double-count or confuse the match.
Out-of-order timestamps. A data-quality bug (client clock skew, or an event arriving out of order through a distributed pipeline) can produce a purchase row whose occurred_at is earlier than the user's signup row, even though the purchase logically happened after. Enforcing the ordering constraint directly on the TIMESTAMP COLUMN itself (signup_ts <= purchase_ts), rather than relying on row insertion order or event-arrival order, is what correctly rejects this case: no valid matched_signup_ts is found for a purchase whose timestamp precedes every candidate signup, so that user is correctly excluded from the completed count rather than incorrectly matched.
Worked example
Schema: events(user_id STRING, event_name STRING, occurred_at TIMESTAMP). Six synthetic users, each engineered to hit exactly one of the complications above:
- u1: clean pass, visit day 0, signup day 3, purchase day 8. Should qualify.
- u2: signup 10 days after visit (exceeds the 7-day window). Should NOT qualify.
- u3: REPEATED visit events (day 0 and day 2); signup day 4 should match the closer visit. Should qualify.
- u4: OUT-OF-ORDER timestamps, purchase recorded at day 4, signup recorded at day 5 (purchase timestamp precedes signup timestamp due to a logging bug). Should NOT qualify.
- u5: FUNNEL RE-ENTRY, a stale visit at day 0, a genuine re-entry visit at day 20, signup day 22 (2 days after the re-entry visit, 22 days after the stale one), purchase day 25. Should qualify only under nearest-match logic, not under a naive first-occurrence anchor.
- u6: visits, never signs up. Counts in the visitor denominator only.
CREATE TABLE events (user_id TEXT, event_name TEXT, occurred_at TIMESTAMP);
INSERT INTO events VALUES
('u1','visit','2026-01-01 00:00:00'),
('u1','signup','2026-01-04 00:00:00'),
('u1','purchase','2026-01-09 00:00:00'),
('u2','visit','2026-01-01 00:00:00'),
('u2','signup','2026-01-11 00:00:00'), -- 10 days after visit, exceeds the 7-day window
('u2','purchase','2026-01-13 00:00:00'),
('u3','visit','2026-01-01 00:00:00'),
('u3','visit','2026-01-03 00:00:00'), -- repeated visit; signup should match this closer one
('u3','signup','2026-01-05 00:00:00'),
('u3','purchase','2026-01-07 00:00:00'),
('u4','visit','2026-01-01 00:00:00'),
('u4','purchase','2026-01-05 00:00:00'), -- purchase timestamp precedes signup: logging/clock-skew bug
('u4','signup','2026-01-06 00:00:00'),
('u5','visit','2026-01-01 00:00:00'), -- stale first visit
('u5','visit','2026-01-21 00:00:00'), -- genuine re-entry visit
('u5','signup','2026-01-23 00:00:00'), -- 2 days after re-entry, 22 days after the stale visit
('u5','purchase','2026-01-26 00:00:00'),
('u6','visit','2026-01-01 00:00:00'); -- visits, never signs up
WITH visits AS (
SELECT user_id, occurred_at AS visit_ts FROM events WHERE event_name = 'visit'
),
signups AS (
SELECT user_id, occurred_at AS signup_ts FROM events WHERE event_name = 'signup'
),
purchases AS (
SELECT user_id, occurred_at AS purchase_ts FROM events WHERE event_name = 'purchase'
),
-- Match each signup to its NEAREST PRECEDING visit within 7 days (not the
-- user's globally-first visit). This correctly handles both repeated visit
-- events (u3) and funnel re-entry (u5): a re-entry visit that is
-- chronologically closer to the signup wins over a stale, too-old first visit.
signup_matched AS (
SELECT
s.user_id,
s.signup_ts,
(SELECT MAX(v.visit_ts) FROM visits v
WHERE v.user_id = s.user_id
AND v.visit_ts <= s.signup_ts
AND julianday(s.signup_ts) - julianday(v.visit_ts) <= 7) AS matched_visit_ts
FROM signups s
),
qualifying_signups AS (
SELECT user_id, signup_ts FROM signup_matched WHERE matched_visit_ts IS NOT NULL
),
-- Same nearest-preceding-match logic for purchase -> signup. Enforcing
-- signup_ts <= purchase_ts on the TIMESTAMP COLUMN (not row order) is what
-- correctly rejects u4's out-of-order purchase.
purchase_matched AS (
SELECT
p.user_id,
p.purchase_ts,
(SELECT MAX(qs.signup_ts) FROM qualifying_signups qs
WHERE qs.user_id = p.user_id
AND qs.signup_ts <= p.purchase_ts
AND julianday(p.purchase_ts) - julianday(qs.signup_ts) <= 7) AS matched_signup_ts
FROM purchases p
),
completed AS (
SELECT user_id, MIN(purchase_ts) AS completed_at
FROM purchase_matched
WHERE matched_signup_ts IS NOT NULL
GROUP BY user_id
),
visitor_base AS (
SELECT DISTINCT user_id FROM visits
)
SELECT
(SELECT COUNT(*) FROM visitor_base) AS total_visitors,
(SELECT COUNT(*) FROM completed) AS completed_users,
ROUND(1.0 * (SELECT COUNT(*) FROM completed) / (SELECT COUNT(*) FROM visitor_base), 4) AS conversion_rate;
Output (actually executed against SQLite as a stand-in for BigQuery-compatible standard SQL; julianday() is SQLite's date-arithmetic function, the equivalent of DATE_DIFF in BigQuery):
total_visitors completed_users conversion_rate
6 3 0.5
Per-user diagnostic confirming exactly the expected 3 completions (u1, u3, u5) and exactly the expected 2 non-matches (u2, u4):
('u1', '2026-01-09 00:00:00', '2026-01-04 00:00:00', 'COMPLETED')
('u2', '2026-01-13 00:00:00', None, 'not matched')
('u3', '2026-01-07 00:00:00', '2026-01-05 00:00:00', 'COMPLETED')
('u4', '2026-01-05 00:00:00', None, 'not matched')
('u5', '2026-01-26 00:00:00', '2026-01-23 00:00:00', 'COMPLETED')
u5's completion is the key result: the query correctly used the re-entry visit (day 20) to qualify the signup, which a first-occurrence-only anchor on the stale day-0 visit would have missed entirely.
Complexity
Each correlated subquery (finding the nearest preceding match) costs, in the naive form shown, O(k) per row where k is the number of candidate predecessor rows for that user, so the overall query is roughly O(V⋅S) where V and S are typical per-user visit and signup counts; at real scale, this is rewritten as a window-function-based nearest-match (using LAST_VALUE with a range-based frame, or a self-join restricted to a bounded date range) so the engine can use sort-based or index-based access instead of a per-row correlated lookup. Space: O(V+S+P) for the intermediate CTEs, where V, S, P are total visit, signup, and purchase row counts.
Edge cases
- A user with a qualifying signup but whose EVENTUAL purchase falls outside the window of every qualifying signup (say, they signed up correctly but bought 3 months later, well past any 7-day link) is correctly excluded, since no
matched_signup_tsexists within range for that purchase. - The retention question this data invites, "what happens to users who reached purchase, do they come back," is deliberately NOT computed here: that is a recurring-behavior question about ALREADY-converted users, a different measurement (repeat visits after conversion) from this funnel-completion query (whether a user reaches purchase at all in the first place), so it falls outside this query's scope.
Trade-offs and pitfalls
- Common mistake: using each user's globally-first occurrence of every stage as if that were always the "cleanest" or safest choice. It is the wrong default specifically when a funnel is order-and-window-constrained (as this one is): the worked example's u5 shows a globally-first anchor produces a false negative for a user who genuinely completed a valid, timely sequence via re-entry.
- This interval and stage set are parameters, not fixed facts about the query. The same nearest-match structure generalizes directly to a 24-hour window, a 30-day window, or a weekly-cohort framing by changing only the
<= 7comparisons and, for a cohort framing, adding aGROUP BYon the visit week; none of the matching LOGIC changes. - At real data volumes, the naive correlated-subquery form above should be rewritten with window functions for performance (as noted under Complexity); shipping the correlated-subquery version to production on a billion-row events table without that rewrite is a common way this pattern becomes a slow, expensive query in practice.
Design an A/B test to reduce checkout abandonment. Define the hypothesis, primary and guardrail metrics, sample size estimation approach, measurement window, segmentation rules, and rollout plan. Explain how you'd handle users who appear in multiple concurrent experiments and how you'd prevent cross-contamination.
Sample Answer
Direct answer
Design the checkout-abandonment test around a single primary metric (checkout completion rate) with named guardrails (refund rate, payment failure rate, revenue per visitor), a sample size sized off the primary metric's baseline rate and minimum detectable effect, a measurement window long enough to cover a full weekly cycle plus any post-purchase guardrail lag, deterministic-hash bucketing so users are consistently assigned and isolated from other concurrent experiments, and a staged rollout (5% then 50% then 100%) gated on both the primary metric and every guardrail staying healthy at each stage.
Structured elaboration
Getting to the hypothesis before designing the test. A test does not start from "let's A/B test the checkout button"; it starts from a diagnosed problem: pull the funnel drop-off data for the checkout step, segment it (device, payment method, new versus returning), and find where drop-off concentrates. If mobile users abandon at 2x the desktop rate specifically at the payment-form step, the hypothesis becomes concrete and falsifiable, not "make checkout better" but a specific, testable causal claim, shown next.
Hypothesis. "Reducing the mobile checkout payment form from 3 required fields to 1 (with autofill) increases mobile checkout completion rate, because the current form's field count is the primary abandonment driver for mobile users at that step."
Primary and guardrail metrics. Primary: checkout completion rate (users who reach checkout_start and reach purchase, the metric the hypothesis is directly about). Guardrails, metrics that must NOT get worse even if the primary improves: refund/chargeback rate within 7 days of purchase, payment failure/decline rate, average order value, and page load time on the checkout step. Guardrails exist because a change can win on the primary metric while quietly damaging something else the primary metric cannot see; the concrete case worth naming explicitly: a simplified, lower-friction checkout can raise completion rate while ALSO raising refund and fraud rates, because some of the friction being removed (a required billing-address confirmation, for example) was doing real fraud-prevention or informed-consent work, not just slowing people down. A test that only looks at the primary metric would ship a change that looks like a clear win and is actually trading real revenue for a fraud or return-rate problem that shows up weeks later. Refund rate needs its own, LONGER measurement window than the primary metric for exactly this reason (see Measurement window below).
Sample size, approach (not full derivation). Sample size for a proportions test like this depends on four inputs: the baseline conversion rate (checkout completion today), the minimum detectable effect (the smallest lift worth being able to detect, decided by business judgment, not statistics, typically informed by the revenue value of that lift versus the cost of running the test longer), the desired significance level (commonly α=0.05), and the desired statistical power (commonly 1−β=0.80). Those four feed a standard two-proportion power calculation to produce the required sample size per arm; this answer names the inputs and the approach at a conceptual level, since the point here is knowing which four factors determine sample size and how they trade off against each other, not re-deriving the closed-form formula step by step. Two practical notes beyond the inputs: (1) the baseline rate should come from RECENT data for the exact segment being tested (mobile checkout specifically, not the blended overall rate), since a mismatched baseline silently under- or over-powers the test; (2) if traffic is limited, a smaller minimum detectable effect requires disproportionately more sample, so the MDE choice is often the practical lever that determines whether the test is even feasible in a reasonable timeframe.
Measurement window. Primary metric: at least one full week (to average out day-of-week effects like weekday-versus-weekend purchase behavior), and typically extended to cover 2 full weekly cycles for a stable read. Guardrails on a LAG, like refund rate, need a window that extends PAST the point where the last user entered the test, since a refund can happen up to the refund policy's window (7, 14, or 30 days) after purchase; declaring the test a winner from primary-metric data alone, before the guardrail's own lag window has closed, is how the conversion-versus-refunds tension above gets missed in practice.
Segmentation rules. Pre-register the segments you will check (mobile versus desktop, new versus returning, geography) BEFORE launching, not after seeing results; this is a bias-prevention rule, since hunting for a significant segment after the fact (the primary metric was flat, but "mobile users in Region X" moved) inflates the false-positive rate through unplanned multiple comparisons. If mobile is the hypothesized driver, mobile can be the PRIMARY analysis population rather than just a post-hoc segment, with desktop as a secondary check that the change did not regress the segment it was not designed for.
Rollout plan. Staged: 5% of eligible traffic for an initial health check (catch a broken build or an obviously bad guardrail signal fast, cheaply), then 50% once nothing looks wrong, then 100% once the test reaches its planned sample size and duration and the primary metric plus every guardrail clears their bar. Each stage gate is a genuine decision point, not a formality: a guardrail regression at 5% should halt the rollout before it reaches more users, not just get noted for later.
Worked example: novelty effects and personalization-decay
A test of a PERSONALIZED checkout flow (recommended payment method pre-selected based on past behavior) surfaces a pitfall generic to any personalization change, worth calling out explicitly rather than folding into the generic guardrail list above: a novelty effect can make week-1 results look better than the change's true steady-state impact, because some of the lift is users reacting to something new and different rather than to the personalization genuinely being more relevant. The concrete failure mode: a personalization model trained on stale behavioral data can also DECAY in relevance over time as user preferences drift, so a test that looks flat-to-positive in week 1 and week 2 can quietly erode by week 6 as the underlying model's recommendations grow less accurate relative to current behavior, an effect a short measurement window would never catch. The mitigation is to extend monitoring PAST the initial test window into a post-launch tracking period specifically watching for a downward trend in the personalized arm's lift over time, not just a single end-of-test readout, and to re-train or re-validate the personalization model on a defined cadence so a stale model is caught before its degraded relevance shows up as a silent conversion-rate regression.
Trade-offs and pitfalls: running many concurrent experiments (the experiment-platform question)
This question's own ask, how to handle users in multiple concurrent experiments and prevent cross-contamination, is a real system-design problem once a company runs more than a handful of tests at once, and deserves concrete technical treatment, not just a mention:
- Deterministic hash-based bucketing. Assign each user to a variant via a hash of
(user_id, experiment_id)(for examplehash(user_id + experiment_id) mod 100), NOT a random draw at request time. This gives two properties that matter: the SAME user always lands in the SAME variant across sessions and devices as long asuser_idis stable (consistency), and, critically, hashinguser_idcombined with the experiment's OWN id means a user's bucket in experiment A is statistically INDEPENDENT of their bucket in experiment B, because the two hashes are effectively unrelated draws even for the same user. This independence is what allows many experiments to run simultaneously on overlapping populations without the assignments correlating with each other. - Overlapping-audience management via layers/namespaces. A common pattern (used by large-scale experimentation platforms) organizes experiments into orthogonal LAYERS: within one layer, experiments are mutually exclusive (a user in the checkout-form-layout experiment cannot simultaneously be in a different checkout-form-layout experiment), while experiments in DIFFERENT layers (checkout layout versus a pricing-page experiment) can run concurrently on the same user, since they touch unrelated parts of the product and their effects are assumed roughly additive/independent. This lets a platform run dozens of tests at once without every pair needing manual conflict analysis.
- Explicit interaction checks for experiments that plausibly interact. Orthogonal hashing prevents ACCIDENTAL correlation, but it does not prevent two experiments from genuinely interacting in effect (a checkout-form simplification test and a pricing-display test both touching the same screen could interact even with independent bucketing). For experiments the team suspects might interact, either force them into the same layer (mutual exclusion) or run an explicit 2x2 factorial analysis on the overlap population to check whether the combined effect differs from the sum of the individual effects.
- Cross-contamination beyond assignment: leakage through shared infrastructure. Bucketing correctly is necessary but not sufficient; a shared cache, a shared recommendation model retrained on mixed-variant data, or a support agent manually "fixing" a treatment-group user's cart back to the control experience are all ways cross-contamination can happen AFTER correct assignment. Auditing for this means checking that variant-specific behavior does not leak into shared state that both arms read from.
Other pitfalls: running the test on a stage-X restatement (payment-entry drop-off, or onboarding/activation instead of checkout) uses this exact same framework unchanged, only the hypothesis, primary metric, and guardrails swap to match the new stage, nothing about the sample-size approach, bucketing, or rollout staging logic changes. Common mistake: treating the 5% rollout stage as a formality rather than a real gate, shipping straight to 50% because the initial numbers "look fine" after only a few hours, well before the guardrails (especially the lagged refund-rate guardrail) have had time to produce a meaningful read.
Unlock Full Question Bank
Get access to all 46 Conversion Funnel Optimization interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.