Mobile Push is currently in beta. The schema and implementation details described in this document are provided as examples and may change. You should design your own table schema based on your specific tracking requirements and business needs.
When you implement Mobile Push notifications with Engage Studio, your mobile app tracks user interactions and sends event data to Treasure Data. This data is stored in a dedicated table (recommended name: push_events) within your Treasure Data database.
This document describes a sample schema, event types, and best practices for working with push notification event data. Note: The schema provided here is an example. You should customize it based on your application's requirements and data governance policies.
The table structure shown below is a sample implementation. Design your own schema based on:
- Your tracking requirements
- Data governance and privacy policies
- Analytics and reporting needs
- Compliance requirements (GDPR, CCPA, etc.)
| Column Name | Data Type | Required | Description |
|---|---|---|---|
time | INTEGER | Yes | Unix timestamp (seconds) when the event occurred |
type | STRING | Yes | Event type: delivery, open, dismiss, link_open, deeplink_open, token_register |
td_campaign_id | STRING | Yes | Campaign identifier from Engage Studio (passed from notification payload) |
platform | STRING | Yes | Mobile platform: android or ios |
fcm_token | STRING | No | Firebase Cloud Messaging device token (required for token_register events) |
user_id | STRING | No | User identifier (e.g., customer ID, email hash) if user is logged in |
value | STRING | No | Additional data (e.g., URL for link_open or deeplink_open) |
device_model | STRING | No | Device model (optional enhancement) |
os_version | STRING | No | Operating system version (optional enhancement) |
app_version | STRING | No | Application version (optional enhancement) |
-- Sample push_events table creation (customize for your needs)
CREATE TABLE IF NOT EXISTS mobile.push_events (
time INTEGER,
type STRING,
td_campaign_id STRING,
platform STRING,
fcm_token STRING,
user_id STRING,
value STRING,
device_model STRING,
os_version STRING,
app_version STRING
)Logged when a push notification is successfully delivered and displayed to the user.
Event Type: delivery
Sample Event:
{
"time": 1734134400,
"type": "delivery",
"td_campaign_id": "cmp_20251214_promo",
"platform": "android",
"user_id": "user_12345"
}When Tracked:
- Android: When
onMessageReceived()is called inMyFirebaseMessagingService - iOS: When
willPresent notificationis called inUNUserNotificationCenterDelegate
Logged when a user taps on the notification to open the app.
Event Type: open
Sample Event:
{
"time": 1734134410,
"type": "open",
"td_campaign_id": "cmp_20251214_promo",
"platform": "ios",
"user_id": "user_12345"
}When Tracked:
- Android: When notification is tapped and
MainActivityreceivesPushAction.OPENintent - iOS: When
didReceive responseis called withUNNotificationDefaultActionIdentifier
Logged when a user dismisses (swipes away) the notification without opening it.
Event Type: dismiss
Sample Event:
{
"time": 1734134420,
"type": "dismiss",
"td_campaign_id": "cmp_20251214_promo",
"platform": "android",
"user_id": "user_12345"
}When Tracked:
- Android: When
deleteIntentis triggered in notification - iOS: When
didReceive responseis called withUNNotificationDismissActionIdentifier
Logged when a user taps on a web URL link from the notification.
Event Type: link_open
Sample Event:
{
"time": 1734134430,
"type": "link_open",
"td_campaign_id": "cmp_20251214_promo",
"platform": "android",
"user_id": "user_12345",
"value": "https://example.com/promo?utm_campaign=promo"
}When Tracked:
- Android: When action button with
PushAction.ACTION_LINKis tapped - iOS: When action button with identifier
ACTION_OPEN_LINKis tapped
Note: The value field contains the full URL that was opened.
Logged when a user taps on an app deeplink from the notification.
Event Type: deeplink_open
Sample Event:
{
"time": 1734134440,
"type": "deeplink_open",
"td_campaign_id": "cmp_20251214_promo",
"platform": "ios",
"user_id": "user_12345",
"value": "myapp://product/12345"
}When Tracked:
- Android: When action button with
PushAction.ACTION_DEEPLINKis tapped - iOS: When action button with identifier
ACTION_OPEN_DEEPLINKis tapped
Note: The value field contains the deeplink URI.
Logged when the app registers or updates the FCM device token with Treasure Data.
Event Type: token_register
Sample Event:
{
"time": 1734134400,
"type": "token_register",
"fcm_token": "dQw4w9WgXcQ:APA91bF...",
"platform": "android",
"user_id": "user_12345",
"app_version": "1.2.3",
"os_version": "14",
"device_model": "Pixel 7"
}When Tracked:
- Android: When
onNewToken()is called inMyFirebaseMessagingService - iOS: When
didReceiveRegistrationTokenis called inMessagingDelegate - Also triggered on app launch to ensure token is up-to-date
Note:
- For logged-in users,
user_idshould be included - For anonymous users,
user_idcan benull— it will be associated later when the user logs in
The token_register event is crucial for linking device tokens to user identities. Here's how the association works:
When a user installs the app and is already logged in (or logs in immediately):
{
"type": "token_register",
"fcm_token": "ABC123...",
"user_id": "user_12345",
"platform": "android"
}The token is immediately associated with the user.
When a user installs the app but doesn't log in yet:
{
"type": "token_register",
"fcm_token": "ABC123...",
"user_id": null,
"platform": "android"
}The token is registered but not yet linked to a user.
When the previously anonymous user logs in, send a new token_register event:
{
"type": "token_register",
"fcm_token": "ABC123...",
"user_id": "user_12345",
"platform": "android"
}Treasure Data can now link all previous events with this fcm_token to user_12345.
To resolve anonymous tokens to users, use this query:
WITH latest_tokens AS (
SELECT
fcm_token,
user_id,
time,
ROW_NUMBER() OVER (PARTITION BY fcm_token ORDER BY time DESC) AS rn
FROM mobile.push_events
WHERE type = 'token_register'
AND user_id IS NOT NULL
)
SELECT
fcm_token,
user_id
FROM latest_tokens
WHERE rn = 1Get delivery, open, and click rates for all campaigns:
SELECT
td_campaign_id,
COUNT(CASE WHEN type = 'delivery' THEN 1 END) AS deliveries,
COUNT(CASE WHEN type = 'open' THEN 1 END) AS opens,
COUNT(CASE WHEN type = 'link_open' OR type = 'deeplink_open' THEN 1 END) AS clicks,
COUNT(CASE WHEN type = 'dismiss' THEN 1 END) AS dismissals,
ROUND(100.0 * COUNT(CASE WHEN type = 'open' THEN 1 END) / NULLIF(COUNT(CASE WHEN type = 'delivery' THEN 1 END), 0), 2) AS open_rate_pct,
ROUND(100.0 * COUNT(CASE WHEN type = 'link_open' OR type = 'deeplink_open' THEN 1 END) / NULLIF(COUNT(CASE WHEN type = 'delivery' THEN 1 END), 0), 2) AS click_rate_pct
FROM mobile.push_events
WHERE time >= CAST(strftime('%s', 'now', '-30 days') AS INTEGER)
GROUP BY td_campaign_id
ORDER BY deliveries DESCCompare performance between Android and iOS:
SELECT
platform,
COUNT(CASE WHEN type = 'delivery' THEN 1 END) AS deliveries,
COUNT(CASE WHEN type = 'open' THEN 1 END) AS opens,
ROUND(100.0 * COUNT(CASE WHEN type = 'open' THEN 1 END) / NULLIF(COUNT(CASE WHEN type = 'delivery' THEN 1 END), 0), 2) AS open_rate_pct
FROM mobile.push_events
WHERE td_campaign_id = 'cmp_20251214_promo'
GROUP BY platformAnalyze how quickly users open notifications:
WITH delivery_times AS (
SELECT
td_campaign_id,
user_id,
fcm_token,
time AS delivery_time
FROM mobile.push_events
WHERE type = 'delivery'
),
open_times AS (
SELECT
td_campaign_id,
user_id,
fcm_token,
time AS open_time
FROM mobile.push_events
WHERE type = 'open'
)
SELECT
CASE
WHEN (o.open_time - d.delivery_time) < 60 THEN '< 1 minute'
WHEN (o.open_time - d.delivery_time) < 300 THEN '1-5 minutes'
WHEN (o.open_time - d.delivery_time) < 3600 THEN '5-60 minutes'
WHEN (o.open_time - d.delivery_time) < 86400 THEN '1-24 hours'
ELSE '> 24 hours'
END AS time_bucket,
COUNT(*) AS count
FROM delivery_times d
JOIN open_times o
ON d.td_campaign_id = o.td_campaign_id
AND d.user_id = o.user_id
AND d.fcm_token = o.fcm_token
GROUP BY time_bucket
ORDER BY MIN(o.open_time - d.delivery_time)Identify which links receive the most clicks:
SELECT
td_campaign_id,
value AS clicked_url,
COUNT(*) AS click_count,
COUNT(DISTINCT user_id) AS unique_users
FROM mobile.push_events
WHERE type IN ('link_open', 'deeplink_open')
AND value IS NOT NULL
GROUP BY td_campaign_id, value
ORDER BY click_count DESC
LIMIT 20Track daily active device tokens:
SELECT
DATE(time, 'unixepoch') AS event_date,
platform,
COUNT(DISTINCT fcm_token) AS active_tokens
FROM mobile.push_events
WHERE time >= CAST(strftime('%s', 'now', '-30 days') AS INTEGER)
AND type IN ('delivery', 'open', 'link_open', 'deeplink_open')
GROUP BY event_date, platform
ORDER BY event_date DESC, platformAnalyze the engagement funnel for a specific campaign:
WITH funnel_data AS (
SELECT
user_id,
MAX(CASE WHEN type = 'delivery' THEN 1 ELSE 0 END) AS delivered,
MAX(CASE WHEN type = 'open' THEN 1 ELSE 0 END) AS opened,
MAX(CASE WHEN type IN ('link_open', 'deeplink_open') THEN 1 ELSE 0 END) AS clicked
FROM mobile.push_events
WHERE td_campaign_id = 'cmp_20251214_promo'
GROUP BY user_id
)
SELECT
SUM(delivered) AS total_delivered,
SUM(opened) AS total_opened,
SUM(clicked) AS total_clicked,
ROUND(100.0 * SUM(opened) / NULLIF(SUM(delivered), 0), 2) AS open_rate_pct,
ROUND(100.0 * SUM(clicked) / NULLIF(SUM(opened), 0), 2) AS click_through_rate_pct
FROM funnel_dataFor large-scale deployments, consider partitioning the table by date:
-- Create monthly partitioned tables
CREATE TABLE mobile.push_events_202512 AS
SELECT * FROM mobile.push_events
WHERE time >= 1733011200 AND time < 1735689600Archive events older than 90 days to a separate table:
CREATE TABLE mobile.push_events_archive AS
SELECT * FROM mobile.push_events
WHERE time < CAST(strftime('%s', 'now', '-90 days') AS INTEGER)Then delete from the main table:
DELETE FROM mobile.push_events
WHERE time < CAST(strftime('%s', 'now', '-90 days') AS INTEGER)Recommended: Store push notification events in a separate push_events table.
Reason: Push events have a different schema from general app events (e.g., event_app), making it easier to query and analyze.
Always include user_id in events when the user is logged in. This enables:
- User-level analytics
- Cross-device tracking
- Personalized campaign optimization
Upload events in batches (up to 500 events per request) to:
- Reduce network overhead
- Improve app performance
- Ensure reliable delivery even with poor network conditions
If event upload fails:
- Keep events in local queue
- Retry with exponential backoff
- Limit maximum retry attempts (e.g., 3-5 times)
Regularly check for:
- Missing required fields (
td_campaign_id,platform,type) - Duplicate events (same
td_campaign_id+user_id+fcm_token+type+time) - Abnormal event patterns (e.g., opens without deliveries)
Enhance the schema with additional fields relevant to your business:
device_model: Track performance by device typeos_version: Identify OS-specific issuesapp_version: Correlate events with app releaseslocation: Geo-based analytics (ensure privacy compliance)
- Do not log PII: Avoid storing email addresses, phone numbers, or other personally identifiable information in events
- Hash user IDs: Consider hashing user IDs before sending to Treasure Data
- Use write-only keys: Mobile apps should use write-only API keys, never master keys
- Encrypt in transit: All data uploads use HTTPS
If your app serves users in privacy-regulated regions:
- Obtain user consent before tracking events
- Provide opt-out mechanisms
- Implement data deletion workflows
- Document data retention policies
- Mobile Push Setup - Configure Firebase and Treasure Data integration
- Android Developer Guide - Implement push notifications on Android
- iOS Developer Guide - Implement push notifications on iOS
- Campaign Creation - Create and send Mobile Push campaigns