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/APNs 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 when using the default push implementation. 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: Zixflow.instance.trackMetric(deliveryID: ..., deviceToken: ..., event: MetricEvent.delivered) Implementation:
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:zixflow/zixflow.dart';

// Listen for foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  final deliveryId    = message.data['Zixflow-Delivery-ID'] ?? '';
  final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? cachedFcmToken ?? '';

  if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
    Zixflow.instance.trackMetric(
      deliveryID:  deliveryId,
      deviceToken: deliveryToken,
      event:       MetricEvent.delivered,
    );
  }
});
Important: The Zixflow SDK automatically tracks delivery. Manual tracking is only needed for custom implementations.

2. Notification Opened

When to track: When the user taps the notification banner (body or action button) SDK method: Zixflow.instance.trackMetric(deliveryID: ..., deviceToken: ..., event: MetricEvent.opened) Implementation:
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:zixflow/zixflow.dart';

// Background tap — app was backgrounded
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  _trackOpened(message.data, cachedFcmToken);
  _handleDeeplink(message.data['deeplink_url']);
});

// Terminated tap — app was closed when notification was tapped
final initial = await FirebaseMessaging.instance.getInitialMessage();
if (initial != null) {
  _trackOpened(initial.data, cachedFcmToken);
  _handleDeeplink(initial.data['deeplink_url']);
}

void _trackOpened(Map<String, String> data, String? fallbackToken) {
  final deliveryId    = data['Zixflow-Delivery-ID'] ?? '';
  final deliveryToken = data['Zixflow-Delivery-Token'] ?? fallbackToken ?? '';

  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.

3. Action Button Clicked

When to track: When the user taps a named action button SDK method: Zixflow.instance.track(name: "Push Notification Action Clicked", properties: {...}) Implementation (using flutter_local_notifications):
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:zixflow/zixflow.dart';
import 'dart:convert';

// In FlutterLocalNotificationsPlugin.initialize()
onDidReceiveNotificationResponse: (NotificationResponse response) {
  final payload = jsonDecode(response.payload ?? '{}') as Map<String, dynamic>;

  if (response.actionId == null) {
    // Body tap → opened
    _trackOpened(payload, null);
  } else {
    // Action button tap → opened + clicked
    _trackOpened(payload, null);

    final actionIndex = int.tryParse(
        response.actionId!.replaceAll(RegExp(r'\D'), '')) ?? -1;
    final buttons = _parseButtons(payload['action_buttons']);
    final buttonName = (actionIndex >= 0 && actionIndex < buttons.length)
        ? buttons[actionIndex]['name'] ?? 'Action ${actionIndex + 1}'
        : 'Action ${actionIndex + 1}';

    Zixflow.instance.track(
      name: 'Push Notification Action Clicked',
      properties: {
        'Zixflow-Delivery-ID':    payload['Zixflow-Delivery-ID'] ?? '',
        'Zixflow-Delivery-Token': payload['Zixflow-Delivery-Token'] ?? '',
        'notification_id':        payload['Zixflow-Delivery-ID'] ?? '',
        'title':                  payload['title'] ?? '',
        'action_id':              response.actionId!,
        'action_index':           actionIndex,
        'action_name':            buttonName,
        'action_deeplink':        _resolveButtonDeeplink(buttons, actionIndex),
        'source':                 'local_notification',
      },
    );
  }
},

List<Map<String, dynamic>> _parseButtons(dynamic raw) {
  if (raw == null) return [];
  try {
    final decoded = raw is String ? jsonDecode(raw) : raw;
    return List<Map<String, dynamic>>.from(decoded as List);
  } catch (_) {
    return [];
  }
}
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/APNs 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 'dart:convert';

List<Map<String, dynamic>> parseActionButtons(dynamic raw) {
  if (raw == null) return [];

  try {
    final decoded = raw is String ? jsonDecode(raw) : raw;
    return List<Map<String, dynamic>>.from(decoded as List);
  } catch (e) {
    print('Error parsing action buttons: $e');
    return [];
  }
}

// Usage
final buttons = parseActionButtons(message.data['action_buttons']);
for (var i = 0; i < buttons.length; i++) {
  print('Button ${i}: ${buttons[i]['name']} -> ${buttons[i]['deeplink']}');
}
Rules:
  • Maximum 2 buttons per notification (recommended for consistency across platforms)
  • deeplink may be empty — handle gracefully
  • Button index is 0-based (first button = index 0)

Complete Push Tracking Implementation

Here’s a complete example integrating all tracking events:
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:zixflow/zixflow.dart';
import 'dart:convert';

// Background message handler (top-level function)
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();

  // Track delivery
  final deliveryId = message.data['Zixflow-Delivery-ID'] ?? '';
  final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? '';

  if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
    Zixflow.instance.trackMetric(
      deliveryID: deliveryId,
      deviceToken: deliveryToken,
      event: MetricEvent.delivered,
    );
  }
}

class PushNotificationHandler {
  final FlutterLocalNotificationsPlugin _localNotifications =
      FlutterLocalNotificationsPlugin();
  String? _fcmToken;

  Future<void> initialize() async {
    // Register background handler
    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

    // Get and cache FCM token
    _fcmToken = await FirebaseMessaging.instance.getToken();
    if (_fcmToken != null) {
      Zixflow.instance.registerDeviceToken(deviceToken: _fcmToken!);
    }

    // Listen for token refresh
    FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
      _fcmToken = newToken;
      Zixflow.instance.registerDeviceToken(deviceToken: newToken);
    });

    // Initialize local notifications
    await _initializeLocalNotifications();

    // Set up message listeners
    _setupMessageListeners();
  }

  Future<void> _initializeLocalNotifications() async {
    const androidSettings = AndroidInitializationSettings('@drawable/ic_notification');
    const iosSettings = DarwinInitializationSettings();

    await _localNotifications.initialize(
      InitializationSettings(android: androidSettings, iOS: iosSettings),
      onDidReceiveNotificationResponse: _handleNotificationResponse,
    );
  }

  void _setupMessageListeners() {
    // Foreground messages
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      _trackDelivery(message);
      _showLocalNotification(message);
    });

    // Background tap
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      _trackOpened(message.data);
      _handleDeeplink(message.data['deeplink_url']);
    });

    // Terminated tap
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message != null) {
        _trackOpened(message.data);
        _handleDeeplink(message.data['deeplink_url']);
      }
    });
  }

  void _trackDelivery(RemoteMessage message) {
    final deliveryId = message.data['Zixflow-Delivery-ID'] ?? '';
    final deliveryToken = message.data['Zixflow-Delivery-Token'] ?? _fcmToken ?? '';

    if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
      Zixflow.instance.trackMetric(
        deliveryID: deliveryId,
        deviceToken: deliveryToken,
        event: MetricEvent.delivered,
      );
    }
  }

  void _trackOpened(Map<String, dynamic> data) {
    final deliveryId = data['Zixflow-Delivery-ID'] ?? '';
    final deliveryToken = data['Zixflow-Delivery-Token'] ?? _fcmToken ?? '';

    if (deliveryId.isNotEmpty && deliveryToken.isNotEmpty) {
      Zixflow.instance.trackMetric(
        deliveryID: deliveryId,
        deviceToken: deliveryToken,
        event: MetricEvent.opened,
      );
    }
  }

  void _handleNotificationResponse(NotificationResponse response) {
    if (response.payload == null) return;

    final payload = jsonDecode(response.payload!) as Map<String, dynamic>;

    // Track opened
    _trackOpened(payload);

    // If action button was tapped
    if (response.actionId != null) {
      _trackActionClick(payload, response.actionId!);
    }

    // Handle navigation
    _handleDeeplink(payload['deeplink_url']);
  }

  void _trackActionClick(Map<String, dynamic> payload, String actionId) {
    final actionIndex = int.tryParse(actionId.replaceAll(RegExp(r'\D'), '')) ?? -1;
    final buttons = _parseButtons(payload['action_buttons']);
    final actionName = (actionIndex >= 0 && actionIndex < buttons.length)
        ? buttons[actionIndex]['name'] ?? 'Action ${actionIndex + 1}'
        : 'Action ${actionIndex + 1}';
    final actionDeeplink = (actionIndex >= 0 && actionIndex < buttons.length)
        ? buttons[actionIndex]['deeplink'] ?? ''
        : '';

    Zixflow.instance.track(
      name: 'Push Notification Action Clicked',
      properties: {
        'Zixflow-Delivery-ID': payload['Zixflow-Delivery-ID'] ?? '',
        'Zixflow-Delivery-Token': payload['Zixflow-Delivery-Token'] ?? '',
        'action_index': actionIndex,
        'action_name': actionName,
        'action_deeplink': actionDeeplink,
      },
    );
  }

  List<Map<String, dynamic>> _parseButtons(dynamic raw) {
    if (raw == null) return [];
    try {
      final decoded = raw is String ? jsonDecode(raw) : raw;
      return List<Map<String, dynamic>>.from(decoded as List);
    } catch (_) {
      return [];
    }
  }

  Future<void> _showLocalNotification(RemoteMessage message) async {
    // Implementation for showing local notification
  }

  void _handleDeeplink(String? deeplink) {
    if (deeplink == null || deeplink.isEmpty) return;
    // Implement your deeplink navigation logic
    print('Navigating to: $deeplink');
  }
}

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: {
    'notification_id': message.messageId ?? '',
    'title': message.notification?.title ?? message.data['title'] ?? '',
    'body': message.notification?.body ?? message.data['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:
final config = ZixflowConfig(
  apiKey: 'YOUR_API_KEY',
  logLevel: LogLevel.debug,
);
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