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.trackMetric({ deliveryID, deviceToken, event: MetricEvent.delivered }) Implementation:
import messaging from '@react-native-firebase/messaging';
import { Zixflow, MetricEvent } from 'zixflow-reactnative';

// Listen for foreground messages
useEffect(() => {
  const unsubscribe = messaging().onMessage(async (remoteMessage) => {
    const deliveryId = remoteMessage.data?.['Zixflow-Delivery-ID'] ?? '';
    const deliveryToken = remoteMessage.data?.['Zixflow-Delivery-Token'] ?? cachedToken ?? '';

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

  return unsubscribe;
}, []);
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 SDK method: Zixflow.trackMetric({ deliveryID, deviceToken, event: MetricEvent.opened }) Implementation:
import messaging from '@react-native-firebase/messaging';
import { Zixflow, MetricEvent } from 'zixflow-reactnative';

// Background / quit tap
useEffect(() => {
  const unsubscribe = messaging().onNotificationOpenedApp(async (remoteMessage) => {
    trackOpened(remoteMessage.data);
    handleDeeplink(remoteMessage.data?.deeplink_url);
  });

  return unsubscribe;
}, []);

// Terminated tap
useEffect(() => {
  messaging()
    .getInitialNotification()
    .then((remoteMessage) => {
      if (remoteMessage) {
        trackOpened(remoteMessage.data);
        handleDeeplink(remoteMessage.data?.deeplink_url);
      }
    });
}, []);

const trackOpened = async (data: any) => {
  const deliveryId = data?.['Zixflow-Delivery-ID'] ?? '';
  const deliveryToken = data?.['Zixflow-Delivery-Token'] ?? cachedToken ?? '';

  if (deliveryId && deliveryToken) {
    await Zixflow.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.track({ name: "Push Notification Action Clicked", properties: {...} }) Implementation:
import { Zixflow, MetricEvent } from 'zixflow-reactnative';

// Handle action press from local notification
const handleActionPress = async (data: any, actionId: string) => {
  const deliveryId = data['Zixflow-Delivery-ID'] ?? '';
  const token = data['Zixflow-Delivery-Token'] ?? '';
  const actionIndex = parseInt(actionId.replace(/\D/g, ''), 10);
  const buttons = parseButtons(data['action_buttons']);
  const actionName = buttons[actionIndex]?.name ?? `Action ${actionIndex + 1}`;
  const actionDeeplink = buttons[actionIndex]?.deeplink ?? '';

  // Always fire opened first
  await Zixflow.trackMetric({
    deliveryID: deliveryId,
    deviceToken: token,
    event: MetricEvent.opened,
  });

  // Then fire action clicked
  await Zixflow.track({
    name: 'Push Notification Action Clicked',
    properties: {
      'Zixflow-Delivery-ID': deliveryId,
      'Zixflow-Delivery-Token': token,
      'action_index': actionIndex,
      'action_name': actionName,
      'action_deeplink': actionDeeplink,
    },
  });
};

const parseButtons = (raw: any) => {
  if (!raw) return [];
  try {
    return typeof raw === 'string' ? JSON.parse(raw) : raw;
  } 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:
const parseActionButtons = (raw: any): Array<{ name: string; deeplink: string }> => {
  if (!raw) return [];

  try {
    const decoded = typeof raw === 'string' ? JSON.parse(raw) : raw;
    return Array.isArray(decoded) ? decoded : [];
  } catch (error) {
    console.error('Error parsing action buttons:', error);
    return [];
  }
};

// Usage
const buttons = parseActionButtons(remoteMessage.data?.action_buttons);
buttons.forEach((btn, index) => {
  console.log(`Button ${index}: ${btn.name} -> ${btn.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 React, { useEffect, useState } from 'react';
import messaging, { FirebaseMessagingTypes } from '@react-native-firebase/messaging';
import { Zixflow, MetricEvent } from 'zixflow-reactnative';

const App = () => {
  const [fcmToken, setFcmToken] = useState<string | null>(null);

  useEffect(() => {
    setupPushNotifications();
  }, []);

  const setupPushNotifications = async () => {
    // Request permission
    const authStatus = await messaging().requestPermission();
    const enabled =
      authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
      authStatus === messaging.AuthorizationStatus.PROVISIONAL;

    if (enabled) {
      // Get FCM token
      const token = await messaging().getToken();
      setFcmToken(token);

      // Register with Zixflow
      await Zixflow.registerDeviceToken({ deviceToken: token });

      // Listen for token refresh
      messaging().onTokenRefresh(async (newToken) => {
        setFcmToken(newToken);
        await Zixflow.registerDeviceToken({ deviceToken: newToken });
      });

      // Handle foreground messages
      messaging().onMessage(async (remoteMessage) => {
        trackDelivery(remoteMessage);
        showLocalNotification(remoteMessage);
      });

      // Handle background/quit taps
      messaging().onNotificationOpenedApp((remoteMessage) => {
        trackOpened(remoteMessage.data);
        handleDeeplink(remoteMessage.data?.deeplink_url);
      });

      // Handle terminated tap
      const initialNotification = await messaging().getInitialNotification();
      if (initialNotification) {
        trackOpened(initialNotification.data);
        handleDeeplink(initialNotification.data?.deeplink_url);
      }
    }
  };

  const trackDelivery = async (message: FirebaseMessagingTypes.RemoteMessage) => {
    const deliveryId = message.data?.['Zixflow-Delivery-ID'] ?? '';
    const deliveryToken = message.data?.['Zixflow-Delivery-Token'] ?? fcmToken ?? '';

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

  const trackOpened = async (data: any) => {
    const deliveryId = data?.['Zixflow-Delivery-ID'] ?? '';
    const deliveryToken = data?.['Zixflow-Delivery-Token'] ?? fcmToken ?? '';

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

  const handleDeeplink = (url?: string) => {
    if (!url) return;
    // Implement your deep link navigation logic
    console.log('Navigating to:', url);
  };

  const showLocalNotification = async (message: FirebaseMessagingTypes.RemoteMessage) => {
    // Use local notification library to show notification
  };

  return (
    // Your app UI
    <></>
  );
};

export default App;

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
await Zixflow.track({
  name: 'External Push Received',
  properties: {
    'notification_id': message.messageId ?? '',
    'title': message.notification?.title ?? '',
    'body': message.notification?.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:
const config: ZixflowConfig = {
  apiKey: 'YOUR_API_KEY',
  logLevel: 'debug',
};

await Zixflow.setup(config);
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