Skip to main content

Overview

Zixflow automatically tracks push notification delivery and engagement metrics to help you measure campaign performance. The SDK tracks three key lifecycle events:
  1. Delivery Confirmed - When the notification arrives on the device
  2. Notification Opened - When the user taps the notification
  3. Action Button Clicked - When the user taps an action button

How Push Tracking Works

When Zixflow sends a push notification, it includes special tracking fields in the data payload:
FieldPurpose
Zixflow-Delivery-IDUnique ID for this delivery (links to campaign record)
Zixflow-Delivery-TokenThe FCM token the notification was sent to
Example push payload:
{
  "Zixflow-Delivery-ID": "626533406292836846",
  "Zixflow-Delivery-Token": "dcFRlDhiRbehM1vg...",
  "title": "Flash Sale! 70% OFF",
  "body": "Limited time only — grab your deal now!",
  "deeplink_url": "https://yourapp.com/sale",
  "image_url": "https://cdn.yourapp.com/banner.png",
  "action_buttons": "[{\"name\":\"Shop Now\",\"deeplink\":\"https://yourapp.com/sale\"},{\"name\":\"Remind Me\",\"deeplink\":\"\"}]"
}
Note: The SDK automatically handles most tracking for you. This section covers advanced customization scenarios.

Tracking Events

1. Delivery Confirmed

When to track: The moment the push payload arrives on the device SDK method: trackMetric(deliveryID, deviceToken, event = MetricEvent.DELIVERED) Basic implementation (in FirebaseMessagingService):
override fun onMessageReceived(message: RemoteMessage) {
    val deliveryId    = message.data["Zixflow-Delivery-ID"] ?: ""
    val deliveryToken = message.data["Zixflow-Delivery-Token"] ?: cachedToken ?: ""

    if (deliveryId.isNotEmpty() && deliveryToken.isNotEmpty()) {
        Zixflow.instance.trackMetric(
            deliveryID  = deliveryId,
            deviceToken = deliveryToken,
            event       = MetricEvent.DELIVERED
        )
    }
}
Important: The Zixflow SDK automatically tracks delivery when using ModuleMessagingPushFCM. Manual tracking is only needed if you have a custom FirebaseMessagingService implementation.

2. Notification Opened

When to track: When the user taps the notification banner (body or action button) SDK method: trackMetric(deliveryID, deviceToken, event = MetricEvent.OPENED) Implementation (in launcher Activity):
// In your launcher Activity.onCreate() or onNewIntent()
intent.extras?.let { extras ->
    val deliveryId    = extras.getString("Zixflow-Delivery-ID", "")
    val deliveryToken = extras.getString("Zixflow-Delivery-Token") ?: cachedToken ?: ""

    if (deliveryId.isNotEmpty() && deliveryToken.isNotEmpty()) {
        Zixflow.instance.trackMetric(
            deliveryID  = deliveryId,
            deviceToken = deliveryToken,
            event       = MetricEvent.OPENED
        )
    }
}
Also handle in onNewIntent():
override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    intent?.extras?.let { extras ->
        val deliveryId    = extras.getString("Zixflow-Delivery-ID", "")
        val deliveryToken = extras.getString("Zixflow-Delivery-Token") ?: cachedToken ?: ""

        if (deliveryId.isNotEmpty() && deliveryToken.isNotEmpty()) {
            Zixflow.instance.trackMetric(
                deliveryID  = deliveryId,
                deviceToken = deliveryToken,
                event       = MetricEvent.OPENED
            )
        }
    }
}
Note: The SDK automatically tracks opens when using the default push implementation. This is only needed for custom implementations.

3. Action Button Clicked

When to track: When the user taps a named action button SDK method: track("Push Notification Action Clicked", properties) Implementation (in NotificationActionReceiver):
// In your BroadcastReceiver or Activity that handles action taps
val deliveryId    = intent.getStringExtra("Zixflow-Delivery-ID") ?: ""
val deliveryToken = intent.getStringExtra("Zixflow-Delivery-Token") ?: cachedToken ?: ""
val actionIndex   = intent.getIntExtra("action_index", -1)
val actionName    = intent.getStringExtra("action_name") ?: "Action ${actionIndex + 1}"
val actionDeeplink = intent.getStringExtra("action_deeplink") ?: ""

// Always fire opened first
Zixflow.instance.trackMetric(
    deliveryID  = deliveryId,
    deviceToken = deliveryToken,
    event       = MetricEvent.OPENED
)

// Then fire action clicked
Zixflow.instance.track(
    name = "Push Notification Action Clicked",
    properties = mapOf(
        "Zixflow-Delivery-ID"    to deliveryId,
        "Zixflow-Delivery-Token" to deliveryToken,
        "action_index"           to actionIndex,
        "action_name"            to actionName,
        "action_deeplink"        to actionDeeplink
    )
)
Action button properties:
PropertyTypeRequiredDescription
Zixflow-Delivery-IDstringLinks event to campaign delivery
action_indexinteger0-based index of button tapped
action_namestringButton label (e.g., “Shop Now”)
Zixflow-Delivery-TokenstringRecommendedFCM token
action_deeplinkstringRecommendedButton’s URL

Action Buttons Format

The action_buttons field is a JSON-encoded string containing button definitions: Raw payload value:
"[{\"name\":\"Shop Now\",\"deeplink\":\"https://yourapp.com/sale\"},{\"name\":\"Remind Me\",\"deeplink\":\"\"}]"
Parsed structure:
[
  { "name": "Shop Now",  "deeplink": "https://yourapp.com/sale" },
  { "name": "Remind Me", "deeplink": "" }
]
Parsing example:
import org.json.JSONArray

fun parseActionButtons(buttonsJson: String?): List<ActionButton> {
    if (buttonsJson.isNullOrEmpty()) return emptyList()

    return try {
        val jsonArray = JSONArray(buttonsJson)
        List(jsonArray.length()) { index ->
            val button = jsonArray.getJSONObject(index)
            ActionButton(
                name = button.getString("name"),
                deeplink = button.optString("deeplink", "")
            )
        }
    } catch (e: Exception) {
        emptyList()
    }
}

data class ActionButton(
    val name: String,
    val deeplink: String
)
Rules:
  • Maximum 2 buttons per notification (recommended for consistency)
  • deeplink may be empty — handle gracefully
  • Button index is 0-based (first button = index 0)

Handling Existing FirebaseMessagingService

If your app already has a custom FirebaseMessagingService, delegate Zixflow push handling:
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.zixflow.messagingpush.ZixflowFirebaseMessagingService

class MyFirebaseMessagingService : FirebaseMessagingService() {

    override fun onMessageReceived(message: RemoteMessage) {
        // Let Zixflow SDK handle push notifications
        val handled = ZixflowFirebaseMessagingService.onMessageReceived(
            context = this,
            remoteMessage = message
        )

        if (!handled) {
            // Handle non-Zixflow push notifications
            handleCustomPush(message)
        }
    }

    override fun onNewToken(token: String) {
        super.onNewToken(token)

        // Register new token with Zixflow
        ZixflowFirebaseMessagingService.onNewToken(
            context = this,
            token = token
        )

        // Handle token for other services
        handleCustomToken(token)
    }

    private fun handleCustomPush(message: RemoteMessage) {
        // Your custom push handling logic
    }

    private fun handleCustomToken(token: String) {
        // Your custom token handling logic
    }
}
Register your service in AndroidManifest.xml:
<service
    android:name=".MyFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

Tracking Decision Flowchart

Notification received?
├── YES → trackMetric(delivered)        [automatic with SDK]

User interacted?
├── Tapped notification body
│   └── trackMetric(opened)             [automatic with SDK]
│       └── Navigate to deeplink_url

└── Tapped action button
    ├── trackMetric(opened)             [automatic with SDK]
    └── track("Push Notification Action Clicked")
        └── Navigate to button's deeplink

Fallback for Non-Zixflow Push

If a push notification doesn’t contain Zixflow-Delivery-ID (sent from another source), the SDK automatically skips tracking. For custom analytics on non-Zixflow pushes, use a different event name:
// Don't use reserved push event names for non-Zixflow pushes
Zixflow.instance.track(
    name = "External Push Received",
    properties = mapOf(
        "notification_id" to (message.messageId ?: ""),
        "title" to (message.data["title"] ?: ""),
        "body" to (message.data["body"] ?: ""),
        "source" to "external"
    )
)
Important: Push notification event names (Push Notification Delivered, Push Notification Opened, Push Notification Action Clicked) are reserved for Zixflow’s delivery pipeline.

Testing Push Tracking

Verify Tracking Events

Enable debug logging to see tracking events:
val config = ZixflowConfigBuilder(this, "YOUR_API_KEY")
    .logLevel(ZixflowLogLevel.DEBUG)
    .build()
Look for these log messages:
  • [Zixflow] Push Notification Delivered tracked
  • [Zixflow] Push Notification Opened tracked
  • [Zixflow] Push Notification Action Clicked tracked

Check Campaign Analytics

  1. Log into Zixflow dashboard
  2. Navigate to MessagingPush Notifications
  3. View campaign metrics:
    • Sent - Total notifications sent
    • Delivered - Arrived on device
    • Opened - User tapped notification
    • Clicked - User tapped action button