Many clients come to us with a problem: the product funnel looks like a black box. We see that after the add_to_cart step, 40% of users are lost, but the reason is unknown. Money goes into advertising instead of improvements. We solve this by implementing end-to-end tracking from scratch. Over 5 years, we've implemented tracking for 50+ mobile projects — from MVPs to enterprise apps with millions of users.
A conversion funnel without proper tracking is a diagram drawn from memory by a product manager. With tracking, it's numbers showing exactly where and for which user segment money slips through your fingers. A common mistake: track only the start and end of the funnel (checkout_started → payment_succeeded) and then wonder why 60% drop-off is unclear. The right approach is to track each step with enough context to explain the drop-off reason.
Why Standard Funnels Fall Short
Often, funnels are built from overly generic events: screen_viewed instead of checkout_payment_opened. Without granularity, it's impossible to know which screen or action causes churn. For example, if 30% of users drop off at payment_opened, it could be due to slow loading, missing Apple Pay, or a 3DS error. In our practice, after adding 3ds_required to the event properties, the drop-off on payment_initiated halved — simply because we saw that 80% of drop-offs went through 3DS and optimized that process.
How We Solve It: Type-Safe Wrapper Implementation
We design a type-safe wrapper for funnel events, ensuring each step has mandatory context. For example, on a fintech project, we noticed that the checkout step had a 50% drop-off. By adding properties like payment_methods_available and 3ds_required, we identified that users with only one payment method were 3x more likely to abandon. The fix was adding a fallback option, reducing drop-off by 25%.
Designing the Funnel
Before implementation, we define:
- Funnel steps — atomic events that unambiguously correspond to user progress
- Properties for each step — what needs to be known about the user and context at that step
- Drop-off points — what happens when a user doesn't proceed to the next step
Example e-commerce funnel:
| Step | Event | Key Properties |
|---|---|---|
| 1 | product_viewed |
product_id, category, source |
| 2 | add_to_cart |
product_id, quantity, cart_size |
| 3 | checkout_started |
cart_total, item_count, has_promo |
| 4 | checkout_address_completed |
address_type (new/saved) |
| 5 | checkout_payment_opened |
payment_methods_available |
| 6 | payment_method_selected |
method (card/paypal/apple_pay) |
| 7 | payment_initiated |
method, 3ds_required |
| 8 | payment_succeeded |
order_id, total, method |
Step 7 → 8 with 3ds_required = true gives a separate branch in the funnel. Without this property, it's unclear why there is more drop-off at this step.
Implementation Details
// Android — type-safe wrapper for funnel
object CheckoutFunnel {
fun trackStepCompleted(step: CheckoutStep, props: Map<String, Any> = emptyMap()) {
val eventName = when (step) {
CheckoutStep.ADDRESS -> "checkout_address_completed"
CheckoutStep.PAYMENT_OPENED -> "checkout_payment_opened"
CheckoutStep.PAYMENT_METHOD_SELECTED -> "checkout_payment_method_selected"
CheckoutStep.PAYMENT_INITIATED -> "payment_initiated"
CheckoutStep.PAYMENT_SUCCEEDED -> "payment_succeeded"
CheckoutStep.PAYMENT_FAILED -> "payment_failed"
}
val baseProps = mapOf(
"session_id" to sessionManager.currentSessionId,
"cart_id" to cartManager.currentCartId,
"user_id" to authManager.currentUserId,
"timestamp" to System.currentTimeMillis()
)
analyticsClient.track(eventName, baseProps + props)
}
}
// Usage in ViewModel
checkoutViewModel.onAddressConfirmed.observe(this) { address ->
CheckoutFunnel.trackStepCompleted(
CheckoutStep.ADDRESS,
mapOf(
"address_type" to if (address.isNew) "new" else "saved",
"country" to address.country
)
)
}
// iOS — similar approach
enum CheckoutStep {
case addressCompleted(isNew: Bool, country: String)
case paymentOpened(availableMethods: [String])
case paymentMethodSelected(method: String, requires3DS: Bool)
case paymentSucceeded(orderId: String, total: Double, method: String)
case paymentFailed(errorCode: String, method: String)
}
extension AnalyticsService {
func track(checkoutStep: CheckoutStep) {
let (eventName, props) = checkoutStep.analyticsPayload
amplitude.track(eventType: eventName, eventProperties: props)
}
}
extension CheckoutStep {
var analyticsPayload: (String, [String: Any]) {
switch self {
case .paymentMethodSelected(let method, let requires3DS):
return ("checkout_payment_method_selected", [
"payment_method": method,
"requires_3ds": requires3DS,
"cart_id": CartManager.shared.currentCartId
])
// ...
}
}
}
Building Funnels in Amplitude
In Amplitude Funnel Analysis:
// Amplitude Chart — configuration via UI or API
{
"chart_type": "FUNNEL",
"steps": [
{ "event_type": "product_viewed" },
{ "event_type": "add_to_cart" },
{ "event_type": "checkout_started" },
{ "event_type": "payment_succeeded" }
],
"funnel_type": "ordered", // strict order
"conversion_window": 7, // 7 days to complete funnel
"conversion_window_unit": "days",
"segment_definitions": [
{
"name": "iOS users",
"filters": [{"subprop_key": "platform", "subprop_value": ["iOS"]}]
},
{
"name": "Android users",
"filters": [{"subprop_key": "platform", "subprop_value": ["Android"]}]
}
]
}
conversion_window is a critical parameter. For a product purchase, 7 days is reasonable. For a SaaS subscription, it might be 30 days. Too short a window lowers conversion.
Firebase Analytics Funnels
// Firebase funnel via Google Analytics
// Configured in GA4 → Explore → Funnel Exploration
// Via API:
const { BetaAnalyticsDataClient } = require('@google-analytics/data');
const client = new BetaAnalyticsDataClient();
const [response] = await client.runFunnelReport({
property: 'properties/YOUR_PROPERTY_ID',
funnelSteps: [
{
name: 'Product Viewed',
filterExpression: {
filter: {
fieldName: 'eventName',
stringFilter: { value: 'product_viewed' }
}
}
},
{
name: 'Add to Cart',
filterExpression: {
filter: {
fieldName: 'eventName',
stringFilter: { value: 'add_to_cart' }
}
}
},
{
name: 'Purchase',
filterExpression: {
filter: {
fieldName: 'eventName',
stringFilter: { value: 'purchase' }
}
}
}
],
dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }]
});
Comparison of Tracking Tools
| Tool | Free Limit | Segment Flexibility | Session Replays |
|---|---|---|---|
| Amplitude | 10M events/month | High: cohorts, behavioral segments | No |
| Firebase Analytics | Unlimited (GA4) | Medium: only user segments | No |
| Mixpanel | 20M events/month | High: custom reports, projectors | No |
Amplitude enables faster funnel building than Firebase, especially with complex segmentation. Firebase wins on price — it is free for virtually any data volume.
Analyzing Drop-Off Causes
Raw funnel data says "we lose 40% here." It doesn't say why. To understand the causes:
- Session replays on the drop-off step (UXCam/Smartlook): what users did before leaving
- User properties in analytics: who leaves — new or returning, iOS or Android, which plan
- A/B test on the high-drop-off step: test a hypothesis for improvement
// Tracking abandonment reason
class PaymentViewModel : ViewModel() {
override fun onCleared() {
super.onCleared()
if (!paymentCompleted) {
CheckoutFunnel.trackStepCompleted(
CheckoutStep.ABANDONED,
mapOf(
"last_step" to currentStep.name,
"time_on_step_seconds" to stepTimer.elapsed(),
"error_shown" to lastErrorShown
)
)
}
}
}
How to interpret funnel results
After setting up tracking, it's important not only to look at percentages but also to compare segments. For example, conversion on iOS might be 15% while on Android it's 10%. That's a reason to check for differences in UI/UX across platforms. Also look at the time between steps: a sharp increase in time on a step may indicate user frustration.
Funnel description approach is based on Amplitude's Funnel Analysis documentation: event attributes and conversion window determine analysis accuracy.
What's Included in Our Service
- Audit of current events and taxonomy.
- Design of the full funnel map: atomic events with context.
- Implementation of type-safe wrappers in Kotlin/Swift.
- Integration with Amplitude/Firebase, setting up Funnel Report with segmentation.
- Connecting Session Replay on critical steps.
- Taxonomy documentation and team training in analytics.
- Ongoing support — adjusting events when logic changes.
Timeline and Pricing
Taxonomy design and implementation: 2–3 days. Dashboards and primary analysis: another 1–2 days. Pricing is determined individually after an audit. We'll assess your project within 1 day — contact us.
Why Choose Us
We have 5 years of experience in mobile analytics — we've implemented tracking in fintech, e-commerce, and SaaS. We guarantee that every funnel step will come with clear context, not just an event name. After implementation, you get not only numbers but a tool for daily optimization. Order an audit of your current funnel — we'll dissect your taxonomy and propose an action plan.
Get in touch with us for a consultation — we'll analyze your funnel and propose an action plan.







