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 APNs device token the notification was sent to
Example push payload:
{
  "Zixflow-Delivery-ID": "626533406292836846",
  "Zixflow-Delivery-Token": "dcFRlDhiRbehM1vg...",
  "aps": {
    "alert": {
      "title": "Flash Sale! 70% OFF",
      "body": "Limited time only — grab your deal now!"
    },
    "mutable-content": 1
  },
  "zixflow": {
    "push": {
      "link": "https://yourapp.com/sale",
      "image": "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 when using the Notification Service Extension. This section covers advanced customization scenarios.

Tracking Events

1. Delivery Confirmed

When to track: In the Notification Service Extension when the push payload arrives SDK method: MessagingPush.shared.trackMetric(deliveryID:, event:, deviceToken:) Implementation (NotificationService.swift):
import UserNotifications
import ZixflowMessagingPushAPN

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        // Initialize SDK for extension
        MessagingPush.initialize(
            withConfig: MessagingPushConfigBuilder(apiKey: "YOUR_API_KEY")
                .build()
        )

        if let bestAttemptContent = bestAttemptContent {
            // Let Zixflow handle rich push and track delivery
            MessagingPush.shared.didReceive(request, withContentHandler: contentHandler)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        MessagingPush.shared.serviceExtensionTimeWillExpire()

        if let contentHandler = contentHandler,
           let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}
Important: The SDK automatically tracks delivery when using the Notification Service Extension.

2. Notification Opened

When to track: When the user taps the notification SDK method: Handled automatically by MessagingPush.shared.didReceive(_:) Implementation (AppDelegate.swift):
import UIKit
import UserNotifications
import ZixflowMessagingPushAPN

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Initialize Zixflow push messaging
        MessagingPush.initialize(
            withConfig: MessagingPushConfigBuilder()
                .showPushAppInForeground(true)
                .build()
        )

        // Set notification delegate
        UNUserNotificationCenter.current().delegate = self

        return true
    }

    // Handle notification tap
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        // Zixflow automatically tracks opens
        MessagingPush.shared.userNotificationCenter(
            center,
            didReceive: response,
            withCompletionHandler: completionHandler
        )
    }

    // Handle foreground presentation
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        completionHandler([.banner, .list, .badge, .sound])
    }
}
Note: The SDK automatically tracks opens when using the default delegate implementation.

3. Action Button Clicked

When to track: When the user taps a named action button SDK method: Automatically tracked in userNotificationCenter(_:didReceive:withCompletionHandler:) 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-TokenstringRecommendedAPNs device token
action_deeplinkstringRecommendedButton’s URL

Action Buttons Format

The action_buttons field in the push payload is a JSON-encoded string: 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 Foundation

struct ActionButton: Codable {
    let name: String
    let deeplink: String
}

func parseActionButtons(from jsonString: String?) -> [ActionButton] {
    guard let jsonString = jsonString,
          let data = jsonString.data(using: .utf8) else {
        return []
    }

    do {
        let buttons = try JSONDecoder().decode([ActionButton].self, from: data)
        return buttons
    } catch {
        print("Error parsing action buttons: \(error)")
        return []
    }
}

// Usage
let buttonsJSON = notification.request.content.userInfo["action_buttons"] as? String
let buttons = parseActionButtons(from: buttonsJSON)

for (index, button) in buttons.enumerated() {
    print("Button \(index): \(button.name) -> \(button.deeplink)")
}
Rules:
  • Maximum 2-4 buttons per notification (iOS limitation)
  • deeplink may be empty — handle gracefully
  • Button index is 0-based (first button = index 0)

Complete Tracking Implementation

Here’s a complete AppDelegate with all tracking integrated:
import UIKit
import UserNotifications
import ZixflowMessagingPushAPN
import ZixflowDataPipelines

@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Initialize Zixflow SDK
        Zixflow.initialize(
            apiKey: "YOUR_API_KEY",
            config: ZixflowConfigBuilder()
                .logLevel(.debug)
                .build()
        )

        // Initialize push messaging
        MessagingPush.initialize(
            withConfig: MessagingPushConfigBuilder()
                .showPushAppInForeground(true)
                .build()
        )

        // Set notification delegate
        UNUserNotificationCenter.current().delegate = self

        // Request push permission
        UNUserNotificationCenter.current().requestAuthorization(
            options: [.alert, .badge, .sound]
        ) { granted, error in
            if granted {
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            }
        }

        return true
    }

    func application(
        _ application: UIApplication,
        didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        // Register device token with Zixflow
        MessagingPush.shared.application(
            application,
            didRegisterForRemoteNotificationsWithDeviceToken: deviceToken
        )
    }

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        // Zixflow automatically tracks:
        // 1. Opened event
        // 2. Action button click (if action tapped)
        MessagingPush.shared.userNotificationCenter(
            center,
            didReceive: response,
            withCompletionHandler: completionHandler
        )
    }

    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        // Show notification in foreground
        completionHandler([.banner, .list, .badge, .sound])
    }
}

Tracking Decision Flowchart

Notification received (in NSE)?
├── YES → trackMetric(delivered)        [automatic with SDK]
│         Download rich media (images)
│         Display notification

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

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

App Groups for Delivery Tracking

To ensure delivery metrics are accurately tracked, configure App Groups: 1. Enable App Groups in Xcode:
  1. Select your main app target
  2. Go to Signing & Capabilities
  3. Click + Capability → Add App Groups
  4. Create a group: group.com.yourcompany.yourapp.zixflow
  5. Repeat for Notification Service Extension target
2. Configure in MessagingPush:
MessagingPush.initialize(
    withConfig: MessagingPushConfigBuilder()
        .appGroup("group.com.yourcompany.yourapp.zixflow")
        .build()
)
This allows the Notification Service Extension to persist delivery metrics that the main app can report when launched.

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: [
        "notification_id": notification.request.identifier,
        "title": notification.request.content.title,
        "body": notification.request.content.body,
        "source": "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:
Zixflow.initialize(
    apiKey: "YOUR_API_KEY",
    config: ZixflowConfigBuilder()
        .logLevel(.debug)
        .build()
)
Look for these log messages in Xcode console:
  • [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