Mobile Analytics: Measuring What Users Actually Do

Implement mobile analytics that reveal real user behavior, not vanity metrics. Covers event design, user journey tracking, funnel analysis, session recording, privacy compliance, and building an analytics architecture that scales from prototype to millions of users.

Mobile analytics measures the actual behaviors of users across real devices, real networks, and real time. It captures the full journey from app launch to feature completion, revealing not just *what* users do, but *how* they do it. It matters when onboarding fails, when users abandon a screen, when a new feature doesn’t stick, or when a performance improvement doesn’t translate to engagement. This is the engineering discipline of instrumenting mobile apps to record, process, and analyse user actions with precision and scale.

Instrumenting the User Journey

To measure what users actually do, engineers must define and collect events that represent meaningful interactions. An event is a timestamped record of a user action: tapping a button, viewing a screen, completing a purchase, or failing a login. Events are recorded at the client, transmitted to a backend, and stored for analysis.

Events are structured as key-value dictionaries with a required event_name and optional properties. Each property captures context: screen_name, device_model, network_type, user_id, session_id, and custom metadata like product_id or cart_size.

{
  "event_name": "purchase_completed",
  "timestamp": "2024-03-15T12:34:56.789Z",
  "properties": {
    "user_id": "u_12345",
    "session_id": "s_98765",
    "product_id": "p_456",
    "total_amount": 29.99,
    "payment_method": "credit_card",
    "shipping_address": "new_york",
    "app_version": "1.2.3"
  }
}

The most common failure is recording events without ensuring they are reliably delivered. Events are often lost during network transitions, especially on mobile. A common mistake is sending events via a single HTTP POST per event, with no batching or retry logic.

To improve reliability, clients should batch events into groups of 10–50 and send them in a single POST request. Each batch is wrapped in a events array and includes metadata:

{
  "batch_id": "b_20240315_123456",
  "timestamp": "2024-03-15T12:34:56.789Z",
  "device_id": "d_abc123",
  "app_version": "1.2.3",
  "events": [
    {
      "event_name": "screen_viewed",
      "timestamp": "2024-03-15T12:34:56.789Z",
      "properties": { "screen_name": "checkout_summary" }
    },
    {
      "event_name": "button_tapped",
      "timestamp": "2024-03-15T12:34:56.812Z",
      "properties": { "button_name": "confirm_order" }
    }
  ]
}

This batch is sent to an endpoint like /api/v1/events with a Content-Type: application/json header. The server responds with a 200 OK on success, or 429 Too Many Requests if the queue is full.

Clients must persist events locally when offline. A common pattern uses a SQLite database with a table events:

CREATE TABLE events (
  id INTEGER PRIMARY KEY,
  batch_id TEXT,
  event_name TEXT NOT NULL,
  timestamp DATETIME NOT NULL,
  properties JSON NOT NULL,
  sent BOOLEAN DEFAULT FALSE,
  failure_count INTEGER DEFAULT 0,
  last_attempt DATETIME
);

On app start, the client checks for unsent events and attempts to send them in a loop, with exponential backoff. If a send fails, the event is retried up to 5 times with increasing delays: 1s, 2s, 4s, 8s, 16s. After the final failure, the event is marked failed and remains in the database for later reprocessing.

Tracking User Sessions

A session is a contiguous period of user activity. It starts on app launch or resume and ends after a period of inactivity—typically 30 seconds. Sessions allow engineers to understand how users engage with the app over time.

Sessions are tracked via session_start and session_end events. The session ID is generated on launch and stored in the app’s shared preferences or keychain. On every event, the client includes the current session_id.

A session begins when the app enters the foreground. The client listens for applicationDidBecomeActive (iOS) or onResume (Android). On this event, it checks if the current session is still valid:

{
  "event_name": "session_end",
  "timestamp": "2024-03-15T12:45:23.456Z",
  "properties": {
    "session_id": "s_98765",
    "duration": 723,
    "event_count": 14,
    "app_version": "1.2.3",
    "network_type": "wifi",
    "battery_level": 87
  }
}

The session is then closed and a new one starts with the next app_launch or app_resume.

A silent failure occurs when the app is backgrounded but never sends session_end. This happens when the device goes to sleep during a session and the app is suspended without a final applicationDidEnterBackground call. To fix this, engineers should schedule a periodic task (e.g., every 60 seconds) that checks the last event timestamp and sends a session end if the gap exceeds 45 seconds.

Measuring Feature Adoption

Feature adoption is measured by tracking sequences of events that form a user journey. A common case is onboarding, where users must complete a series of steps.

Engineers define a funnel as a list of events that must occur in order. For example, a signup funnel:

  1. app_opened
  2. screen_viewed → onboarding_welcome
  3. button_tapped → start_signup
  4. screen_viewed → signup_form
  5. form_submitted → signup_complete
  6. user_created

Each event in the funnel is tracked with a funnel_step property:

{
  "event_name": "form_submitted",
  "properties": {
    "funnel_step": "signup_form",
    "funnel_name": "signup_flow",
    "user_id": "u_12345"
  }
}

The funnel is computed by aggregating the number of users who completed each step. The funnel is visualized as a table or graph showing drop-off between steps.

A common mistake is assuming that a user who completed signup_form also completed signup_complete. But if form_submitted is fired immediately after signup_complete, the funnel is misaligned. To correct this, engineers must ensure that each step is tracked only once per session, and that the funnel is computed over the session timeline.

Another issue arises when users revisit a screen. A screen_viewed event is recorded on every visit, but a funnel step may require the first visit only. To handle this, engineers use first_visit flags:

{
  "event_name": "screen_viewed",
  "properties": {
    "screen_name": "checkout_summary",
    "first_visit": true,
    "session_id": "s_98765"
  }
}

The funnel logic then checks for the earliest screen_viewed event per session where first_visit is true.

Analyzing User Paths

User paths are sequences of screens or actions that users follow. They reveal how users navigate the app, which features are linked, and where they get stuck.

To generate paths, the system collects all screen_viewed events, ordered by timestamp, and groups them by session_id. The path is a list of screen names:

[
  "home",
  "product_list",
  "product_detail",
  "add_to_cart",
  "checkout_summary",
  "payment_methods"
]

Paths are analyzed by computing the frequency of each transition: from home to product_list occurs 12,432 times, while add_to_cart to checkout_summary occurs 8,911 times.

A critical edge case is path truncation. When a user opens the app and navigates through three screens, but then the app is backgrounded before the next screen is viewed, the final screen is not marked as “visited” in the path. This leads to undercounting of transitions.

To fix this, engineers should record screen_viewed events with a duration and exit_reason:

{
  "event_name": "screen_viewed",
  "properties": {
    "screen_name": "product_detail",
    "duration": 18.4,
    "exit_reason": "user_swipe",
    "session_id": "s_98765"
  }
}

The exit_reason can be user_swipe, app_backgrounded, button_tapped, or timeout. This allows analysis of which screens are most engaged and which are quickly exited.

Handling Data Quality and Failures

Data quality is often overlooked. The most common failure mode is the missing user_id in events. Without user_id, all analysis is at the device level, not the user level. This leads to inaccurate cohort analysis and funnel conversion.

To ensure user_id is present, engineers must validate it on every event. The client checks for a non-empty user_id in the app’s storage. If missing, it sends a user_identified event:

{
  "event_name": "user_identified",
  "properties": {
    "user_id": "u_12345",
    "previous_id": "anon_67890",
    "method": "login",
    "timestamp": "2024-03-15T12:34:56.789Z"
  }
}

This event links an anonymous user to a known user, enabling cross-session analysis.

Another failure mode is event deduplication. When a user taps a button, the client may send the event multiple times due to network delays or UI updates. To deduplicate, the client uses a deduplication_id per event, derived from the event’s event_name, properties, and timestamp.

The server applies a deduplication window: events with identical deduplication_id are merged, with the latest timestamp and the properties from the latest event.

A final edge case is event schema evolution. As features grow, new properties are added. The client sends events with a schema_version field:

{
  "event_name": "purchase_completed",
  "schema_version": "1.2",
  "properties": {
    "user_id": "u_12345",
    "total_amount": 29.99,
    "payment_method": "credit_card",
    "shipping_address": "new_york",
    "app_version": "1.2.3",
    "promo_code": "SUMMER2024"
  }
}

The server validates the schema and logs any schema_mismatch errors. When a new schema is introduced, the system can detect when a client sends an event using a newer schema than expected.

Operationalizing Analytics

Analytics data is consumed by dashboards, reports, and downstream systems. The client must support configuration-driven analytics.

The app reads a analytics.json file at startup:

{
  "enabled": true,
  "batch_size": 20,
  "batch_timeout": 5000,
  "max_queue_size": 1000,
  "send_on_background": true,
  "funnels": {
    "signup_flow": {
      "steps": [
        "app_opened",
        "screen_viewed",
        "button_tapped",
        "form_submitted",
        "user_created"
      ],
      "step_names": {
        "screen_viewed": "onboarding_welcome",
        "button_tapped": "start_signup",
        "form_submitted": "signup_complete"
      }
    }
  },
  "events": {
    "screen_viewed": {
      "include_duration": true,
      "include_exit_reason": true
    },
    "button_tapped": {
      "include_button_name": true
    }
  }
}

This configuration controls when and what events are sent. The client reads this file from a remote endpoint (/config/analytics.json) and reloads it on app restart or network change.

A common mistake is forgetting to include schema_version in the configuration. When a new event property is added, the analytics pipeline may fail silently because the server expects a schema_version field that is not sent.

Engineers monitor the analytics pipeline using logs and metrics. Key metrics include:

A critical alert is events_sent_rate < 500 per minute over 10 minutes, indicating a network or client issue.

Finally, engineers write SQL queries to analyze data. A simple query computes daily funnel conversion:

SELECT
  DATE(e.timestamp) AS date,
  COUNT(*) AS total_users,
  SUM(CASE WHEN e.event_name = 'signup_complete' THEN 1 ELSE 0 END) AS completed,
  (SUM(CASE WHEN e.event_name = 'signup_complete' THEN 1 ELSE 0 END) * 1.0 / COUNT(*)) AS conversion_rate
FROM events e
WHERE e.event_name IN ('app_opened', 'signup_form', 'signup_complete')
  AND e.timestamp >= '2024-03-15'
GROUP BY DATE(e.timestamp)
ORDER BY date;

This query reveals trends, drop-offs, and the impact of changes.

This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.