В этой статье поделюсь тем, как настроить трекинг событий о получении и об открытии нотификаций на iOS устройствах. Рассматривается вариант, когда приложение уже интегрировано с Firebase Cloud Messaging для получения FCM токенов и отправки нотификаций через собственный сервер, а для трекинга событий подключен сервис Amplitude. Также предполагается, что приложение уже обрабатывает запрос на разрешение получения нотификаций, включая вариант с provisional опцией. Даже если в вашем приложении используется другой набор сервисов для работы с нотификациями и аналитикой, решения, описанные здесь, могут подойти.

Задача заключается в том, чтобы затрекать два события аналитики: событие получения нотификации (нотификация пришла на устройство пользователя, но пользователь еще не нажимал на нее) и событие открытия приложения по клику на нотификацию.

За трекинг событий отвечает пара методов в классе AnalyticsTracker.
class AnalyticsTracker {

  // Событие получения нотификации
  func didReceiveNotification() {
    let eventName = "Notification Received"
    Amplitude.instance().logEvent(eventName)
  }

  // Событие открытия нотификации
  func didOpenNotification() {
    let eventName = "Notification Opened"
    Amplitude.instance().logEvent(eventName)
  }
}

Работа с нотификациями происходит через UNUserNotificationCenter. Протокол UNUserNotificationCenterDelegate содержит методы, которые кажутся подходящими для трекинга нужных событий:

1) userNotificationCenter(:didReceive:withCompletionHandler:) - из названия можно предположить, что метод вызывается, когда получена нотификация и здесь можно трекать событие didReceiveNotification()

2) userNotificationCenter(:willPresent:withCompletionHandler:) - раз у UNUserNotificationCenterDelegate всего 2 метода и один метод уже предположительно вызывается при получении нотификации, то, похоже, этот метод мог бы использоваться для трекинга didOpenNotification() события.

На самом деле оба предположения выше некорректны и приведут к неправильному трекингу событий.

Где и как тогда трекать получение и открытие нотификаций?

Трекинг открытия нотификации

В методах application(:willFinishLaunchingWithOptions:) и  application(:didFinishLaunchingWithOptions:) из словаря launchOptions можно получить информацию о том, было ли приложение запущено через нотификацию. Для этого необходимо проверить значения для ключа remoteNotification. Если пользователь запустил приложение через локальную нотификацию, то словарь launchOptions не будет содержать информации об этом, так как ключ localNotification существовал только до iOS 10. 

Трекинг события открытия нотификации может происходить внутри application(:didFinishLaunchingWithOptions:).
func application(
  application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
  …
  if let launchOptions = launchOptions,
    let remoteNotification = launchOptions[.remoteNotification] {
      analyticsTracker.didOpenNotification()
    }

  return true
}

Кажется, что если в приложении используются только remote нотификации, то этот вариант мог бы подойти для трекинга события didOpenNotification(). Но application(:willFinishLaunchingWithOptions:) и  application(:didFinishLaunchingWithOptions:) сработают только при инициализации приложения. Если приложение уже было запущено и находится в бэкграунде, то событие открытия приложения по клику на нотификации не будет затрекано.

Зато метод userNotificationCenter(:didReceive:withCompletionHandler:), из названия которого в начале статьи было сделано предположение, что речь именно про получение нотификации на устройство, будет вызван как раз во всех случаях:

  1. При инициализации приложения (если приложение не было запущено на устройстве пользователя)

  2. При запуске приложения из бэкграунда

  3. При получении local и remote нотификаций 

Для того, чтобы этот метод был вызван при открытии нотификации, важно задать делегат для UNUserNotificationCenter перед окончанием запуска приложения - в методах application(:willFinishLaunchingWithOptions:) или application(:didFinishLaunchingWithOptions:). Если делегат задан в какой-то момент позднее, то приложение не сможет обрабатывать нотификации. 

Настройка делегата для UNUserNotificationCenter
func application(
  application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
  UNUserNotificationCenter.current().delegate = self
  
  return true
}

С трекингом открытия приложения по клику на нотификацию разобрались, и это была самая простая часть.

Трекинг получения нотификации на устройство

Как уже упоминалось, приложение использует Firebase Cloud Messaging и собственный сервер для отправки нотификаций. После отправки нотификации, сервер получает ответ от FCM API, который содержит информацию об успешности отправки нотификации. FCM отправляет скомпонованное сообщение в Apple Push Notification сервис (APNS), а не сразу на девайс пользователя, поэтому успешная отправка нотификации означает лишь то, что APNS был вызван успешно, но нотификация могла так и не прийти на устройство пользователя. 

Пример ответа FCM API для ситуации, когда пользователь сначала дал права на получение нотификаций, а затем забрал их через Settings приложение:
{
    "multicast_id": 1314429988295507285,
    "success": 1,
    "failure": 0,
    "canonical_ids": 0,
    "results": [
        {
            "message_id": "1666543512501125"
        }
    ]
}

Видно, что в респонсе FCM API нет ошибок, message_id сформирован, но сообщение так и не доставлено на устройство пользователя.

В приложении используется Firebase Analytics как дополнительный трекер событий. Firebase Analytics трекает ряд событий автоматически, их список доступен в документации. Возникает идея посмотреть, как Firebase трекает событие получения нотификации, чтобы использовать похожий способ.

Документация Firebase про Автоматический трекинг события notification_receive
Документация Firebase про Автоматический трекинг события notification_receive

К сожалению, оказывается, что событие notification_receive трекается только для Android устройств и только если приложение запущено в бэкграунде. Это наталкивает на мысль, что с трекингом события получения нотификаций на iOS устройстве есть какие-то сложности. Ищем дальше.

С iOS 10 появился класс UNNotificationServiceExtension, который отвечает за подготовку данных нотификации к отображению. Этот класс позволяет обработать и при необходимости изменить контент, который увидит пользователь в нотификации. Один из примеров такой обработки - это загрузка изображения или видео, которое надо показать в нотификации. И это расширение - отличное место для того, чтобы затрекать событие получения нотификации. 

Но и тут есть свои ограничения. UNNotificationServiceExtension будет использован только в следующих случаях:

  1. Приложение настроено для получения remote нотификаций

  2. Приложению даны права показывать alert при получении remote нотификации. Включая вариант, когда такое разрешение было выдано приложению с provisional опцией.

  3. Remote нотификация содержит в пейлоаде значение "1" для ключа mutable-content 

Это значит, что следующие нотификации не будут обработаны через расширение UNNotificationServiceExtension:

  1. local нотификации

  2. silent нотификации, которые не приводят к появлению алерта (такие нотификации могут использоваться, например, для того чтобы приложение, запущенное в бэкграунде, выполнило загрузку новых данных без команды пользователя).

  3. Нотификации, которые только воспроизводят звук или обновляют бейджик иконки приложения.

При получении remote нотификации система загружает расширение UNNotificationServiceExtension и вызывает метод didReceive(:withContentHandler:), в котором можно трекать событие получения нотификации. Поскольку это расширение, то важно учитывать два важных момента:

  1. Расширение загружается отдельно от приложения, а потому сконфигурировать сервис аналитики надо не только в приложении, но и в данном расширении. 

  2. Время жизни расширения - короткое. Как только будет выполнена задача подготовки нотификации к отображению, расширение будет уничтожено. Обычно системы аналитики накапливают события за определенный промежуток времени и отправляют их пачкой. У Amplitude, например, есть параметр eventUploadPeriodSeconds, который по умолчанию имеет значение "30 секунд". Чтобы успеть отправить события, пока расширение не уничтожено, можно уменьшить значение такой настройки, а можно использовать метод для принудительной отправки событий - uploadEvents().

Использование NotificationService для трекинга события получения нотификации
class NotificationService {
  func didReceive(
    _ request: UNNotificationRequest,
    withContentHandler contentHandler: (UNNotificationContent) -> Void
  ) {
    trackDidReceiveNotification()
    // ...
    // Подготовка тела нотификации notificationContent
    // ...
    contentHandler(notificationContent);
  }

  private func trackDidReceiveNotification() {
    let apiKey = "NNNNNNN"
    let amplitudeTracker = Amplitude.instance()
    amplitudeTracker.initializeApiKey(apiKey)
    analyticsTracker.didReceiveNotification()
    amplitudeTracker.uploadEvents() 
  }
  
}

Особенность трекинга событий из UNNotificationServiceExtension в том, что в аналитике такое событие получения нотификации отображается в отдельной сессии, а не перед событием открытия нотификации, как ожидается. Но при построении воронки учитывается время события, а поскольку событие получения произошло раньше (в 11:36:29), чем событие открытия нотификации (11:47:04), то конверсия из получения нотификаций в открытие будет посчитана корректно.

Пример событий пользователя из Amplitude: Push Notification Received отображается как будто позже, чем Push Notification Opened. Но если обратить внимание на временную метку, то события затреканы в правильном порядке, хотя и в разных сессиях.
Пример событий пользователя из Amplitude: Push Notification Received отображается как будто позже, чем Push Notification Opened. Но если обратить внимание на временную метку, то события затреканы в правильном порядке, хотя и в разных сессиях.

В документации Amplitude рекомендуется трекать события для нотификаций не из приложения, а через HTTP API или через их партнеров. Поэтому есть еще способ обработки пришедшей нотификации - это в UNNotificationServiceExtension сохранять информацию о том, что пришла нотификация с помощью App Groups и затем трекать событие в основном приложении. В этом случае событие didReceiveNotification() будет трекаться в рамках той же сессии, что и событие didOpenNotification(). Но тут есть риск, что событие получения нотификации не будет затрекано, так как пользователь должен открыть приложение после получения нотификации, чтобы отправка события выполнилась. При этом не важно, кликнул он на нотификацию или открыл приложение другим образом. 

Особенности обновления токенов.

Для того, чтобы приложение регулярно получало нотификации, нужно учитывать пару важных моментов, связанных с токенами. Хотя это напрямую не касается аналитики и трекинга событий, связанных с нотификациями, хочется зафиксировать эту информацию.

Для работы с нотификациями на iOS устройствах и при использовании Firebase Cloud Messaging есть два токена - FCM и APNS токены. 

APNs токен - это "адрес" устройства, уникальный для приложения, который генерируется Apple Push Notification сервисом для доставки нотификаций. Firebase использует свой собственный FCM токен, но он связан с APNS токеном. APNS токен может иногда изменяться, например, если пользователь обновил операционную систему или если восстановил приложение из бэкапа. В этом случае также изменится и FCM токен. Приложение должно учитывать и обрабатывать такое обновление токенов, чтобы Firebase всегда использовал свежие токены для отправки нотификаций.

Регистрация устройства и генерация APNs токена происходят после вызова метода registerForRemoteNotifications(). Apple рекомендует регулярно запрашивать токен, а не кэшировать его, чтобы он всегда был в актуальном состоянии. Поэтому вызов registerForRemoteNotifications() не должен происходить только после запроса разрешений на получение нотификаций, особенно если такой запрос возникает в контексте выполнения какого-то действия и при этом возникает редко. 

Регистрация приложения для получения remote нотификаций
func application(
  application: UIApplication,
  didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
  // ...
  registerForRemoteNotifications()
  
  return true
}

Firebase при старте приложения генерирует новый токен, если токена нет или он невалидный, а также периодически обновляет существующий токен. Если FCM токен изменился, то пользователя надо переподписать на те топики, на которые он был ранее подписан.

Важно сначала получить APNs токен и установить его для FCM, а затем уже можно получать FCM токен и подписывать пользователя на топик. Иначе FCM выдает предупреждение в консоли и пользователь не будет получать нотификации:

APNS device token not set before retrieving FCM Token for Sender ID '1000000000000'. Notifications to this FCM Token will not be delivered over APNS. Be sure to re-retrieve the FCM token once the APNS device token is set.

Выводы

Если подвести итоги, то они окажутся короткими:

  1. Событие открытия нотификации надо трекать в методе userNotificationCenter(:didReceive:withCompletionHandler:)

  2. Событие получения remote нотификации надо трекать в методе didReceive(_:withContentHandler:) расширения UNNotificationServiceExtension.

Но в процессе изучения вопроса выяснилось много деталей, которые влияют на корректность трекинга событий, связанных с нотификациями. Надеюсь, эта информация позволит кому-то сэкономить время для выполнения похожей задачи.

Ресурсы

Registering Your App with APNs

UNNotificationServiceExtension

Testing notification with Firebase Notifications Composer

Firebase Cloud Messaging Architectural Overview

Комментарии (0)