Лидия Анисимова

Старший разработчик

Всем привет! Меня зовут Лида Анисимова, я backend разработчик продукта — платформы мотивации сотрудников Тил Эйчар. В этой статье хочу рассказать о нашем опыте реализации push-уведомлений. Если вы, как и я полгода назад, ничего не знаете о том, как работают push-уведомления — эта статья для вас. Расскажу о том, с какими трудностями мы столкнулись, покажу, как отправить первый push и какие данные вам могут понадобиться.

Статью можно условно поделить на 3 части:

  1. Подготовка к реализации, основываясь на специфике нашего проекта, и сбор данных;

  2. Описание архитектуры будущего решения;

  3. Собственно код и другие шаги для отправки первого push-уведомления.

Если вас интересует только код, необходимый для отправки первого push-уведомления, первые две части можно пропустить.

Часть первая (лирическая). Подготовка и сбор данных.

Начнём с небольших вводных, Тил Эйчар — это web-приложение, но на волне веяний моды было решено также делать мобильные Android и iOS приложения. В связи с этим встал вопрос о необходимости привлекать пользователей, мотивировать их пользоваться новым приложением, а также сообщать им о событиях, происходящих в системе. Конечно, выбор пал на push-уведомления как на очевидный инструмент. Думаю, с push-уведомлениями в жизни сталкивается каждый человек, и пояснять, как они выглядят, не надо. Кто же не ждёт push о зачислении зарплаты или о том, что ваш заказ доставлен в ПВЗ?

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

Как видно из этого фрагмента, для начала мы определились со следующими деталями уведомлений:

  • Какое событие в системе триггерит отправку уведомления;

  • Какую информацию уведомление должно содержать;

  • Куда перенаправить пользователя при нажатии на уведомление;

  • Будем ли мы хранить информацию об уведомлении в системе;

  • Кто является получателем уведомления (в нашем случае варианты были — все или конкретный пользователь. Мы не делили уведомления на iOS и Android, но предполагали, что будем в будущем, так что настоятельно рекомендую об этом тоже задуматься).

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

  1. Ни у кого в команде разработки не было опыта реализации push-уведомлений. Более того, ни у кого не было представления даже, как примерно это должно работать;

  2. Не было понимания, какой сервис для отправки использовать, и из чего вообще выбирать.

Страшно,очень страшно,мы не знаем,что это такое by Mirandy - Tuna
Страшно,очень страшно,мы не знаем,что это такое by Mirandy - Tuna

Очевидно, что прыгнуть с места в карьер и начать писать код вслепую было бы ошибкой, поэтому мы начали с небольшого исследования. Цель — выбрать сервис отправки и на коленке накидать архитектурную схему отправки уведомления. Источником знаний я выбрала коллег из других проектов: архитекторов, мобильных разработчиков, более опытных .Net разработчиков и, конечно же, интернет. 

Подсвечу, что для меня как .Net разработчика был неочевидным факт, что push-уведомления триггерит web-приложение. А не мобильное приложение подписывается (каким-то неизвестным мне образом) на события. Ещё у меня в голове была какая-то неосознанная ассоциация «push - firebase», то есть какие-то фантомные знания у меня были.

Итак, погуглив, я узнала о великом множестве сервисов для отправки уведомлений. Проблема была в том, что только о некоторых из них знали коллеги, особенно часто в разговоре мелькали:

  • Push Service RuStore;

  • Firebase Cloud Messaging.

Нашими критериями выбора были:

  • Желательно, но необязательно, бесплатный доступ;

  • Наличие документации;

  • Наличие опыта интеграции с сервисом у кого-то из коллег не из команды, для консультаций;

  • Отсутствие критических ограничений в работе сервиса (например, скорый уход из РФ). Цели выбрать что-то российское не ставилось, но этот риск мы тоже обдумывали.

Таким образом, выбор пал на Firebase Cloud Messaging. Он подходил по всем критериям и был на слуху. Конечно, в рамках ресерча мы выявили ограничения сервиса, коих оказалось немного, а именно (возможно их больше):

  • Ограничение количества токенов (поясню ниже), на которые отправляются уведомления, если использовать, назовем это «массовое сообщение», а не подписку на топики. Данные об ограничении были разрозненные и противоречивые, где-то 500, где-то 1000 токенов, примем просто тот факт, что некие ограничения есть;

  • Максимальный размер сообщения — 4 Кб.

Указанные ограничения были не критичными для нас, поэтому мы сделали выбор и приступили к детальной проработке решения.

Часть вторая. Архитектура

Прочитав справку Firebase и другие источники, посмотрев пару уроков на YouTube, я составила план действий, список вопросов для обсуждения с архитектором и примерную диаграмму отправки уведомления.

Прежде чем приступить к разработке, мне надо было ответить на некоторые вопросы и проработать технические детали вопросов из первой части (помните таблицу?). Вы спросите: «Да сколько можно планировать?» На это я отвечу: во-первых, это мой первый опыт работы с push-уведомлениями, а во-вторых, я сторонник тщательного планирования, которое скажу, забегая вперед, окупилось. Когда в недалеком будущем мы решили сделать уведомления для web-приложения — код оказался расширяемым и продуманным (по словам другого разработчика, конечно).

Итак, что надо было продумать:

Пункт из аналитики

Вопрос

Ответ/решение

Какое событие в системе триггерит отправку уведомления.

 

Для каждого события нужен свой тип уведомления?

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

В коде каждого события, триггерящего уведомления, надо будет добавить собственно отправку уведомления.

Какую информацию уведомление должно содержать.

Кто является получателем уведомления.

Где хранить метаинформацию по уведомлению (тип, текст, заголовок, получателей и тд)?

Новая таблица в бд со всей метаинформацией, в том числе целевой группой.

Куда перенаправить пользователя при нажатии на уведомление.

 

Где и в каком виде хранить и передавать информацию о перенаправлении?

Поскольку редирект — это работа мобильного разработчика, в обсуждении с ним было решено передавать в теле уведомления ссылку на целевую страницу. Как именно её хранить в теле уведомления, ещё предстояло выяснить.

Будем ли мы хранить информацию об уведомлении в системе.

 

Какую именно информацию? Будет ли у пользователя возможность просматривать старые уведомления? Не будет ли разрастаться таблица?

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

К этому моменту я также узнала, что мобильные устройства, подписываясь на рассылку, получают некий токен, по которому firebase затем будет отправлять уведомления. Назовем его device token. В связи с этим появилось множество вопросов для разных случаев использования мобильных устройств (Что делать, если на устройстве приложение разлогинено? Что делать, если несколько человек используют один мобильный телефон? (да, и такое бывает). Не буду приводить тут все вопросы, которые мы обсуждали, так как в контексте статьи они не очень важны. Скажу только, что device token, и об этом пишут в справке firebase, надо как-то получить из мобильного устройства и хранить в системе для отправки на него уведомления. Мы для этого доработали публичное api, чтобы мобильное приложение могло нам сообщить свой device token, и сделали ещё одну таблицу в бд, где мы храним и токены, и информацию об устройствах.

Долго ли, коротко ли, но ответив на все вопросы (кстати, тут уместно будет поблагодарить за терпение нашего замечательного аналитика и архитектора мобильного приложения) и проработав структуру новых объектов базы данных, я нарисовала примерную схему. На тот момент выглядела она вот так (не ругайте за несоответствие стандартам, главное — наглядность):

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

Сам flow, думаю, очевиден, но я всё же поясню:

  1. Пользователь инициирует какое-то действие, например, подтверждает доставку заказа;

  2. Получаем из бд метаинформацию по типу пуша (OrderStatusChanged в данном случае);

  3. Получаем из бд информацию по устройству (device token);

  4. Получаем из сервиса локализованное тело и заголовок уведомления по ключам, которые хранятся в метаинформации;

  5. Формируем push и отправляем;

  6. Фиксируем в истории.

Итак, мы всё хорошо спланировали и, наконец, можем приступать к реализации.

Часть третья. Реализация

Чтобы отправить свой первый push, нам необходимо сделать следующее:

  1. Зайти в консоль firebase и создать новый проект. Рекомендую сразу озадачиться вопросом разных проектов для прод среды и среды разработки (если, конечно, не хотите видеть тестовые уведомления на проде вроде таких, что ниже).

Проект можно зарегистрировать на любую почту, но лучше заведите Google-аккаунт для вашего проекта и сделайте его владельцем через вкладку Users and permissions.

  1. Получить приватный ключ. Для этого зайти в настройки проекта и на вкладке Service account и нажать кнопку generate private key.

Этот ключ вам позже понадобится для подключения к firebase из кода. Пару слов про хранение этого ключа. Ключ нужен для аутентификации учетной записи службы и предоставления ей доступа к службам firebase, поэтому никогда (никогда) не храните этот ключ в репозитории вместе с кодом, как не стали бы хранить пароль. Если вы просто хотите разобраться, как работать с firebase, ключ может храниться локально, но на реальных проектах ключ необходим сервису, который, как правило, лежит где-то в контейнере, и есть большой соблазн хранить его рядом с исходниками — не надо так! Положите его в переменную CI/CD или в защищенную смонтированную папку для контейнера.

  1. Подключаем к проекту пакет FireBaseAdmin.

  2. Создаем инстанс FirebaseApp (тут нам и понадобится приватный ключ), мы это делаем в классе startup. Код выглядит примерно так:

    private static void StartFireBaseApp(PushNotificationSettings settings)
    
    {
    
    	if (!settings.IsEnabled)
    
    		return;
    
    	if (FirebaseApp.DefaultInstance != null)
    
    		return;
    
    	if (!string.IsNullOrWhiteSpace(settings.PrivateKeyPath))
    
    		FirebaseApp.Create(new AppOptions { Credential = GoogleCredential.FromFile(settings.PrivateKeyPath) });
    
    }

    Обратите внимание на GoogleCredential, у них много методов расширения. Технически ключ можно положить прямо в код и вызвать метод .FromJson, но делать так можно исключительно в тестовых целях.

  3. Готово! Вы великолепны! Можно отправлять уведомления. Всё, что осталось сделать — в любом месте вашего приложения написать следующий код. Для него вам понадобится токен (его вам может выдать Android-разработчик, когда подключит firebase на стороне мобильного приложения). Если токена у вас нет, можно отправить сообщение в топик (он будет создан при первой подписке на него, либо можно его создать через api, но мы не пользовались такой возможностью).

    private static async Task<string> SendPush(CancellationToken cancellationToken = default)
    
    {
    
    	var message = new Message()
    
    	{
    
    		Data = new Dictionary<string, string>()
    
    		{
    
    			{ "ключ", "любые данные" }
    
    		},
    
    		Notification = new Notification
    
    		{ Title = "я родился!", Body = "это мой первый пуш" },
    
    		Token =  "тут нужен тестовый токен",
    
    		//Topic = "название топика"
    
    	};
    
    	var response = await FirebaseMessaging.DefaultInstance.SendAsync(message, cancellationToken);
    
    	return response;
    
    }
  1. Можно отследить, что уведомления уходят, посмотрев на вкладку messaging/reports. Количество отправок (sends) должно меняться, количество получений (received) меняться не будет, если к вам никто не подключен (у нас сейчас там 0, потому что мы использовали метод SendMulticastAsync, который летом перестал поддерживаться, но мы скоро всё починим, а вы просто обратите на это внимание). Метод SendAsync возвращает результат, из которого можно получить текст ошибки, если что-то пошло не так.

На этом ваша работа закончена и начинается работа мобильного разработчика. Во 2й части я упоминала, что нам где-то нужно было хранить ссылку на целевую страницу (куда ведет push), в этой связи обратите внимание на поле Data в теле уведомления. Это поле как раз предназначено для разнородных данных, главное — согласовать контракт с мобильным разработчиком, туда мы и поместили ссылку, а также тип отправляемого уведомления.

Глава последняя. Заключение

На написание статьи меня натолкнуло воспоминание о том, как я по крупицам собирала информацию и часами и днями планировала эту большую задачу, после чего, наконец, получила сообщение от тестировщика со словами:

Это был очень радостный момент, но оглядываясь назад, я бы хотела в своё время прочитать похожую статью. 

Хотела бы дать ещё пару советов и резюмировать сказанное:

  • При реализации push-уведомлений надо помнить, что они не гарантируют доставку. Если вам важно, чтобы пользователь обратил внимание на сообщение, рассмотрите другие варианты информирования;

  • Необходимо предусмотреть возможность отключить уведомления (у нас это сделано аж на 3х уровнях: на уровне всего приложения, конкретного пользователя и конкретного устройства);

  • Нужно хорошо обдумать триггеры и частоту отправки уведомлений, чтобы приложение не стало слишком навязчивым;

  • Перед началом реализации определитесь, что именно будут показывать ваши уведомления, какие данные и как вы будете хранить в системе.

Я надеюсь, что статья получилась интересной и наш опыт кому-то пригодится. Если вы только начинаете свой путь в реализации push-уведомлений, надеюсь, информация окажется полезной и у вас всё получится. Удачи!

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