Skip to main content

Overview

Zixflow automatically tracks web 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 push subscription endpoint
Example push payload:
{
  "Zixflow-Delivery-ID": "626533406292836846",
  "Zixflow-Delivery-Token": "https://fcm.googleapis.com/...",
  "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: When using the Zixflow service worker implementation, tracking is handled automatically. This section covers custom implementations.

Tracking Events

1. Delivery Confirmed

When to track: The moment the push event fires in the service worker Implementation (service-worker.js):
// service-worker.js
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  const deliveryId = data['Zixflow-Delivery-ID'] ?? '';
  const token = data['Zixflow-Delivery-Token'] ?? '';

  if (deliveryId && token) {
    // Track delivery using fetch API
    fetch('https://events.zixflow.in/v1/track', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Basic ${btoa(WRITE_KEY + ':')}`
      },
      body: JSON.stringify({
        event: 'Push Notification Delivered',
        userId: self.__ZIXFLOW_USER_ID__,
        properties: {
          'Zixflow-Delivery-ID': deliveryId,
          'Zixflow-Delivery-Token': token,
        },
      }),
    }).catch(() => {});
  }

  // Show notification
  event.waitUntil(
    self.registration.showNotification(data.title ?? 'Notification', {
      body: data.body ?? '',
      icon: data.image_url,
      data: data,
      actions: parseButtons(data.action_buttons),
    })
  );
});

function parseButtons(raw) {
  if (!raw) return [];
  try {
    const decoded = typeof raw === 'string' ? JSON.parse(raw) : raw;
    return decoded.slice(0, 2).map((btn, i) => ({
      action: `ACTION_${i}`,
      title: btn.name || `Action ${i + 1}`,
    }));
  } catch {
    return [];
  }
}

2. Notification Opened

When to track: When the user clicks the notification Implementation (service-worker.js):
// service-worker.js
self.addEventListener('notificationclick', (event) => {
  const data = event.notification.data ?? {};
  const deliveryId = data['Zixflow-Delivery-ID'] ?? '';
  const token = data['Zixflow-Delivery-Token'] ?? '';

  event.notification.close();

  // Track opened
  if (deliveryId && token) {
    fetch('https://events.zixflow.in/v1/track', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Basic ${btoa(WRITE_KEY + ':')}`
      },
      body: JSON.stringify({
        event: 'Push Notification Opened',
        userId: self.__ZIXFLOW_USER_ID__,
        properties: {
          'Zixflow-Delivery-ID': deliveryId,
          'Zixflow-Delivery-Token': token,
        },
      }),
    }).catch(() => {});
  }

  // Handle action button click
  if (event.action) {
    const actionIndex = parseInt(event.action.replace(/\D/g, ''), 10);
    const buttons = parseButtons(data.action_buttons);
    const actionName = buttons[actionIndex]?.name ?? '';
    const deeplink = buttons[actionIndex]?.deeplink ?? '';

    // Track action click
    fetch('https://events.zixflow.in/v1/track', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Basic ${btoa(WRITE_KEY + ':')}`
      },
      body: JSON.stringify({
        event: 'Push Notification Action Clicked',
        userId: self.__ZIXFLOW_USER_ID__,
        properties: {
          'Zixflow-Delivery-ID': deliveryId,
          'Zixflow-Delivery-Token': token,
          'action_index': actionIndex,
          'action_name': actionName,
          'action_deeplink': deeplink,
          'source': 'web_push',
        },
      }),
    }).catch(() => {});

    if (deeplink) {
      event.waitUntil(clients.openWindow(deeplink));
      return;
    }
  }

  // Default: open deeplink or focus existing tab
  const deeplink = data['deeplink_url'] ?? '';
  event.waitUntil(
    deeplink ? clients.openWindow(deeplink) : clients.openWindow('/')
  );
});

3. Action Button Clicked

Action button clicks are tracked within the notificationclick event handler shown above when event.action is present. 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-TokenstringRecommendedPush subscription endpoint
action_deeplinkstringRecommendedButton’s URL
sourcestringOptionalSet to “web_push”

Complete Service Worker Implementation

Here’s a complete service worker with all tracking:
// public/zixflow-sw.js
const WRITE_KEY = 'YOUR_API_KEY';
const ZIXFLOW_API = 'https://events.zixflow.in/v1/track';

// Store user ID (set from main app)
self.__ZIXFLOW_USER_ID__ = null;

// Listen for messages from main app
self.addEventListener('message', (event) => {
  if (event.data?.type === 'SET_USER_ID') {
    self.__ZIXFLOW_USER_ID__ = event.data.userId;
  }
});

// Track delivery
self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? {};
  const deliveryId = data['Zixflow-Delivery-ID'] ?? '';
  const token = data['Zixflow-Delivery-Token'] ?? '';

  // Track delivery
  if (deliveryId && token) {
    trackEvent('Push Notification Delivered', {
      'Zixflow-Delivery-ID': deliveryId,
      'Zixflow-Delivery-Token': token,
    });
  }

  // Parse action buttons
  const buttons = parseActionButtons(data.action_buttons);

  // Show notification
  event.waitUntil(
    self.registration.showNotification(data.title ?? 'Notification', {
      body: data.body ?? '',
      icon: data.image_url,
      badge: data.badge_url,
      data: data,
      actions: buttons,
    })
  );
});

// Track opens and action clicks
self.addEventListener('notificationclick', (event) => {
  const data = event.notification.data ?? {};
  const deliveryId = data['Zixflow-Delivery-ID'] ?? '';
  const token = data['Zixflow-Delivery-Token'] ?? '';

  event.notification.close();

  // Always track opened
  if (deliveryId && token) {
    trackEvent('Push Notification Opened', {
      'Zixflow-Delivery-ID': deliveryId,
      'Zixflow-Delivery-Token': token,
    });
  }

  // Handle action button click
  if (event.action) {
    const actionIndex = parseInt(event.action.replace(/\D/g, ''), 10);
    const rawButtons = parseActionButtonsRaw(data.action_buttons);
    const actionName = rawButtons[actionIndex]?.name ?? '';
    const actionDeeplink = rawButtons[actionIndex]?.deeplink ?? '';

    trackEvent('Push Notification Action Clicked', {
      'Zixflow-Delivery-ID': deliveryId,
      'Zixflow-Delivery-Token': token,
      'action_index': actionIndex,
      'action_name': actionName,
      'action_deeplink': actionDeeplink,
      'source': 'web_push',
    });

    if (actionDeeplink) {
      event.waitUntil(clients.openWindow(actionDeeplink));
      return;
    }
  }

  // Default navigation
  const deeplink = data['deeplink_url'] ?? '';
  event.waitUntil(
    deeplink ? clients.openWindow(deeplink) : clients.openWindow('/')
  );
});

// Helper: Track event
function trackEvent(eventName, properties) {
  fetch(ZIXFLOW_API, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Basic ${btoa(WRITE_KEY + ':')}`,
    },
    body: JSON.stringify({
      event: eventName,
      userId: self.__ZIXFLOW_USER_ID__,
      properties: properties,
    }),
  }).catch((err) => {
    console.error('Failed to track event:', err);
  });
}

// Helper: Parse action buttons for notification API
function parseActionButtons(raw) {
  const buttons = parseActionButtonsRaw(raw);
  return buttons.slice(0, 2).map((btn, i) => ({
    action: `ACTION_${i}`,
    title: btn.name || `Action ${i + 1}`,
  }));
}

// Helper: Parse raw action buttons data
function parseActionButtonsRaw(raw) {
  if (!raw) return [];
  try {
    return typeof raw === 'string' ? JSON.parse(raw) : raw;
  } catch {
    return [];
  }
}

Setting User ID in Service Worker

To associate tracking events with the correct user, pass the user ID from your main application to the service worker:
// main app (after analytics.identify())
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
  navigator.serviceWorker.controller.postMessage({
    type: 'SET_USER_ID',
    userId: 'user@example.com',
  });
}

Action Buttons Format

The action_buttons field 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": "" }
]
Rules:
  • Maximum 2 buttons (browser limitation)
  • deeplink may be empty
  • Button index is 0-based

Tracking Decision Flowchart

Notification received (push event)?
├── YES → Track "Push Notification Delivered"
│         Show notification

User clicked notification?
├── Clicked body
│   └── Track "Push Notification Opened"
│       Navigate to deeplink_url

└── Clicked action button
    ├── Track "Push Notification Opened"
    └── Track "Push Notification Action Clicked"
        Navigate to button's deeplink

Fallback for Non-Zixflow Push

If a push doesn’t contain Zixflow-Delivery-ID, skip tracking or use custom event names:
// Don't use reserved push event names for non-Zixflow pushes
if (!data['Zixflow-Delivery-ID']) {
  trackEvent('External Push Received', {
    'title': data.title ?? '',
    'body': data.body ?? '',
    'source': 'external',
  });
}
Important: Event names Push Notification Delivered, Push Notification Opened, and Push Notification Action Clicked are reserved for Zixflow’s delivery pipeline.

Testing Push Tracking

Enable Debug Logging

Add logging to your service worker:
function trackEvent(eventName, properties) {
  console.log('[Zixflow] Tracking:', eventName, properties);

  fetch(ZIXFLOW_API, {
    // ... existing implementation
  }).then(response => {
    console.log('[Zixflow] Track response:', response.status);
  }).catch((err) => {
    console.error('[Zixflow] Track failed:', err);
  });
}
View logs in browser DevTools → Application → Service Workers → Console

Check Campaign Analytics

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