Observability - это способность понять снаружи, что делает работающее приложение, не влезая в него отладчиком. В обычной SPA телеметрия грузится вместе с приложением, и этого почти всегда достаточно. В PWA появляется новый слой: воркер стоит перед приложением и решает, что вообще загрузится. Значит, он может сломать приложение раньше, чем ваша телеметрия успеет подняться, - и тогда классический подход слепнет ровно в тот момент, когда он нужнее всего.
Две вещи должны быть видимы прежде остального: какая версия приложения сейчас работает и какой воркер, точнее controller, обслужил страницу. Причина в статистике инцидентов: застрявший старый воркер, который продолжает отдавать прежний код после деплоя, - самая частая авария offline-first. Без пары версия-controller вы не отличите свежий баг в новом релизе от пользователя, застрявшего на позапрошлой сборке, и будете чинить не то.
Наивное решение - слать телеметрию тем же хрупким сетевым путём, что и само приложение. Оно ломается по очевидной причине: если сеть лежит - а это ровно то состояние, которое вам интересно, - телеметрия тонет вместе со всем остальным, и вы не узнаёте об офлайн-поведении именно тогда, когда офлайн и есть проблема. Отчёт о сбое не должен зависеть от того же канала, который сбоит.
Поэтому начинают с диагностического снимка - небольшого набора полей, который можно собрать локально в любой момент, не обращаясь к сети. Он описывает не бизнес-события, а состояние платформы под приложением: версия, контроллер, режим отображения, признак сети, глубина outbox и оценка хранилища.
Каждое поле снимка отвечает на конкретный вопрос. Поле controller равно null значит, что страницу не контролирует ни один воркер - приложение uncontrolled, и офлайна у него сейчас нет. displayMode отличает установленное приложение (standalone) от вкладки браузера - поведение и ожидания пользователя в этих режимах разные. outboxDepth - это метрика здоровья синхронизации: растущая глубина означает, что отправка застряла и данные копятся на клиенте. storage.estimate даёт quota и usage; приближение usage к quota предсказывает будущий eviction раньше, чем он случится.
Собирать снимок - половина дела; вторая половина в том, как его доставить. Не проталкивайте телеметрию тем же хрупким путём без буфера. Минимальные события складывают в ограниченную очередь и отправляют позже, когда сеть вернётся. Слово ограниченную здесь ключевое: неограниченная очередь логов сама начинает конкурировать за ту самую quota, которую вы этим логом и мониторите, и ускоряет eviction полезных данных.
И жёсткая граница: privacy важнее полноты. Никогда не логируйте bodies приватных запросов. Метрики офлайна - счётчики, глубины, версии, тайминги - безопасны; содержимое запросов и заметок пользователя - нет. Весь смысл офлайн-архива был в том, чтобы держать приватные данные локально; телеметрия не должна стать тем самым каналом, по которому они утекут наружу.
Что из этого складывается в постоянные метрики: доля загрузок, отданных воркером, распределение глубины outbox, доля успешных и неуспешных синхронизаций, запас по quota и лаг адаптации обновления - сколько пользователи сидят на старой версии. Конкретный провал без этого: приложение заметок тихо перестаёт синхронизировать outbox, глубина очереди растёт, но её нет в телеметрии - и вы узнаёте о поломке из тикетов поддержки, а не с дашборда, спустя дни потерянных мутаций.
const diagnostic = {
appVersion: APP_VERSION,
controller: navigator.serviceWorker.controller?.scriptURL ?? null,
displayMode: matchMedia('(display-mode: standalone)').matches
? 'standalone' : 'browser',
onlineHint: navigator.onLine,
outboxDepth: await outbox.count(),
storage: await navigator.storage.estimate()
};