Лидия Анисимова
Старший разработчик
Всем привет! Меня зовут Лида Анисимова, я backend разработчик продукта — платформы мотивации сотрудников Тил Эйчар. В этой статье хочу рассказать о нашем опыте реализации push-уведомлений. Если вы, как и я полгода назад, ничего не знаете о том, как работают push-уведомления — эта статья для вас. Расскажу о том, с какими трудностями мы столкнулись, покажу, как отправить первый push и какие данные вам могут понадобиться.
Статью можно условно поделить на 3 части:
Подготовка к реализации, основываясь на специфике нашего проекта, и сбор данных;
Описание архитектуры будущего решения;
Собственно код и другие шаги для отправки первого push-уведомления.
Если вас интересует только код, необходимый для отправки первого push-уведомления, первые две части можно пропустить.
Часть первая (лирическая). Подготовка и сбор данных.
Начнём с небольших вводных, Тил Эйчар — это web-приложение, но на волне веяний моды было решено также делать мобильные Android и iOS приложения. В связи с этим встал вопрос о необходимости привлекать пользователей, мотивировать их пользоваться новым приложением, а также сообщать им о событиях, происходящих в системе. Конечно, выбор пал на push-уведомления как на очевидный инструмент. Думаю, с push-уведомлениями в жизни сталкивается каждый человек, и пояснять, как они выглядят, не надо. Кто же не ждёт push о зачислении зарплаты или о том, что ваш заказ доставлен в ПВЗ?
На этапе создания MVP бизнес-аналитиком и владельцем продукта был собран некий список событий системы, о которых необходимо было информировать пользователя. Ниже приведу фрагмент первоначального документа с требованиями:
Как видно из этого фрагмента, для начала мы определились со следующими деталями уведомлений:
Какое событие в системе триггерит отправку уведомления;
Какую информацию уведомление должно содержать;
Куда перенаправить пользователя при нажатии на уведомление;
Будем ли мы хранить информацию об уведомлении в системе;
Кто является получателем уведомления (в нашем случае варианты были — все или конкретный пользователь. Мы не делили уведомления на iOS и Android, но предполагали, что будем в будущем, так что настоятельно рекомендую об этом тоже задуматься).
Итак, задача поставлена, время приступать к реализации. И тут мы столкнулись с рядом проблем:
Ни у кого в команде разработки не было опыта реализации push-уведомлений. Более того, ни у кого не было представления даже, как примерно это должно работать;
Не было понимания, какой сервис для отправки использовать, и из чего вообще выбирать.
Очевидно, что прыгнуть с места в карьер и начать писать код вслепую было бы ошибкой, поэтому мы начали с небольшого исследования. Цель — выбрать сервис отправки и на коленке накидать архитектурную схему отправки уведомления. Источником знаний я выбрала коллег из других проектов: архитекторов, мобильных разработчиков, более опытных .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, думаю, очевиден, но я всё же поясню:
Пользователь инициирует какое-то действие, например, подтверждает доставку заказа;
Получаем из бд метаинформацию по типу пуша (OrderStatusChanged в данном случае);
Получаем из бд информацию по устройству (device token);
Получаем из сервиса локализованное тело и заголовок уведомления по ключам, которые хранятся в метаинформации;
Формируем push и отправляем;
Фиксируем в истории.
Итак, мы всё хорошо спланировали и, наконец, можем приступать к реализации.
Часть третья. Реализация
Чтобы отправить свой первый push, нам необходимо сделать следующее:
Зайти в консоль firebase и создать новый проект. Рекомендую сразу озадачиться вопросом разных проектов для прод среды и среды разработки (если, конечно, не хотите видеть тестовые уведомления на проде вроде таких, что ниже).
Проект можно зарегистрировать на любую почту, но лучше заведите Google-аккаунт для вашего проекта и сделайте его владельцем через вкладку Users and permissions.
Получить приватный ключ. Для этого зайти в настройки проекта и на вкладке Service account и нажать кнопку generate private key.
Этот ключ вам позже понадобится для подключения к firebase из кода. Пару слов про хранение этого ключа. Ключ нужен для аутентификации учетной записи службы и предоставления ей доступа к службам firebase, поэтому никогда (никогда) не храните этот ключ в репозитории вместе с кодом, как не стали бы хранить пароль. Если вы просто хотите разобраться, как работать с firebase, ключ может храниться локально, но на реальных проектах ключ необходим сервису, который, как правило, лежит где-то в контейнере, и есть большой соблазн хранить его рядом с исходниками — не надо так! Положите его в переменную CI/CD или в защищенную смонтированную папку для контейнера.
Подключаем к проекту пакет FireBaseAdmin.
-
Создаем инстанс 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, но делать так можно исключительно в тестовых целях.
-
Готово! Вы великолепны! Можно отправлять уведомления. Всё, что осталось сделать — в любом месте вашего приложения написать следующий код. Для него вам понадобится токен (его вам может выдать 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; }
Можно отследить, что уведомления уходят, посмотрев на вкладку messaging/reports. Количество отправок (sends) должно меняться, количество получений (received) меняться не будет, если к вам никто не подключен (у нас сейчас там 0, потому что мы использовали метод SendMulticastAsync, который летом перестал поддерживаться, но мы скоро всё починим, а вы просто обратите на это внимание). Метод SendAsync возвращает результат, из которого можно получить текст ошибки, если что-то пошло не так.
На этом ваша работа закончена и начинается работа мобильного разработчика. Во 2й части я упоминала, что нам где-то нужно было хранить ссылку на целевую страницу (куда ведет push), в этой связи обратите внимание на поле Data в теле уведомления. Это поле как раз предназначено для разнородных данных, главное — согласовать контракт с мобильным разработчиком, туда мы и поместили ссылку, а также тип отправляемого уведомления.
Глава последняя. Заключение
На написание статьи меня натолкнуло воспоминание о том, как я по крупицам собирала информацию и часами и днями планировала эту большую задачу, после чего, наконец, получила сообщение от тестировщика со словами:
Это был очень радостный момент, но оглядываясь назад, я бы хотела в своё время прочитать похожую статью.
Хотела бы дать ещё пару советов и резюмировать сказанное:
При реализации push-уведомлений надо помнить, что они не гарантируют доставку. Если вам важно, чтобы пользователь обратил внимание на сообщение, рассмотрите другие варианты информирования;
Необходимо предусмотреть возможность отключить уведомления (у нас это сделано аж на 3х уровнях: на уровне всего приложения, конкретного пользователя и конкретного устройства);
Нужно хорошо обдумать триггеры и частоту отправки уведомлений, чтобы приложение не стало слишком навязчивым;
Перед началом реализации определитесь, что именно будут показывать ваши уведомления, какие данные и как вы будете хранить в системе.
Я надеюсь, что статья получилась интересной и наш опыт кому-то пригодится. Если вы только начинаете свой путь в реализации push-уведомлений, надеюсь, информация окажется полезной и у вас всё получится. Удачи!