Перейти к содержимому

Уведомления об ошибках фронтенда

Notifly прекрасно подходит как «дешёвая Sentry» для маленьких сайтов и pet-проектов: ловите window.onerror, unhandledrejection, React error boundary — и шлите краткое сообщение со стектрейсом и URL-ом.

Что делает скрипт:

  • Перехватывает window.onerror, unhandledrejection, console.error.
  • Дедупликация в рамках сессии — повторные ошибки не шлются.
  • Батчинг: до 10 ошибок в одном запросе, flush каждые 3 секунды.
  • Использует navigator.sendBeacon — не блокирует закрытие страницы.
  • Лимит 100 запросов за сессию — защита от бесконечных циклов.
  • Весь батч = 1 событие квоты = 1 push-уведомление.
СценарийПочему Notifly вместо Sentry
Pet-проект на продеБесплатно, без лимитов на events, нет привязки к платформе
Лендинг на статикеОдна строка в <head> — никакого бэкенда не нужно
Внутренний инструментДостаточно знать «что упало» — без replay/tracing
Вайбкодинг с ИИИИ-ассистенту легко вставить скрипт в любой фреймворк (см. LLM-инструкцию в админке)
Стейджинг / QAPush сразу на телефон тестировщика при падении
E-commerce checkoutУзнать что юзеры видят ошибку на оплате раньше, чем они напишут в поддержку
Мобильный WebViewОдин и тот же скрипт работает в Capacitor/Cordova/React Native WebView
Electron-приложениеВ renderer HTML — без отдельного Sentry SDK
  1. Откройте app.notifly.ru/web-scripts.
  2. Нажмите Создать скрипт → тип Ошибки консоли.
  3. Скопируйте сниппет из вкладки «HTML вставка».
  4. Вставьте в <head> вашего сайта до основных бандлов.
  5. Готово — ошибки начнут приходить в push.

Альтернативный способ: собственный бэкенд-прокси

Заголовок раздела «Альтернативный способ: собственный бэкенд-прокси»

Если вам нужен полный контроль (rate limiting, обогащение данными, source maps) — используйте тонкий прокси.

::: caution[Не светите токен в браузере] Никогда не вставляйте app-токен прямо в JavaScript на странице. Вместо этого сделайте тонкий прокси на бэкенде (POST /api/error-report → Notifly). Иначе любой посетитель сможет спамить вашим Notifly. :::

Минимальный пример на Express (~15 строк):

server.js
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.use(express.json({limit: '20kb'}));
const RATE = new Map(); // ip → timestamp
const COOLDOWN = 30_000;
app.post('/api/error-report', async (req, res) => {
const ip = req.ip;
if (Date.now() - (RATE.get(ip) || 0) < COOLDOWN) {
return res.status(204).end(); // молча глотаем спам
}
RATE.set(ip, Date.now());
const {title, message} = req.body || {};
if (!title) return res.status(400).end();
await fetch(`${process.env.NOTIFLY_URL}/message?token=${process.env.NOTIFLY_TOKEN}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: `🌐 [front] ${title}`.slice(0, 200),
message: String(message || '').slice(0, 1500),
priority: 6,
}),
});
res.status(204).end();
});
app.listen(3000);

Подключите на любую страницу:

<script>
(function () {
var ENDPOINT = '/api/error-report';
var lastSent = 0;
function send(payload) {
if (Date.now() - lastSent < 5000) return; // не чаще 1 раза в 5с с одного клиента
lastSent = Date.now();
try {
fetch(ENDPOINT, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
keepalive: true,
});
} catch (e) {}
}
window.addEventListener('error', function (e) {
send({
title: (e.message || 'unknown error').slice(0, 100),
message: [
'URL: ' + location.href,
'UA: ' + navigator.userAgent,
'At: ' + (e.filename || '?') + ':' + (e.lineno || '?') + ':' + (e.colno || '?'),
'',
(e.error && e.error.stack) ? e.error.stack : ''
].join('\n'),
});
});
window.addEventListener('unhandledrejection', function (e) {
var reason = e.reason || {};
send({
title: 'unhandledrejection: ' + (reason.message || String(reason)).slice(0, 80),
message: 'URL: ' + location.href + '\n\n' + (reason.stack || ''),
});
});
})();
</script>
ErrorBoundary.jsx
import React from 'react';
export class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
componentDidCatch(error, info) {
fetch('/api/error-report', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: `React: ${error.name}: ${error.message}`.slice(0, 100),
message:
`URL: ${location.href}\n` +
`UA: ${navigator.userAgent}\n\n` +
(error.stack || '') + '\n\nComponent stack:' +
(info?.componentStack || ''),
}),
keepalive: true,
}).catch(() => {});
}
render() {
if (this.state.error) {
return <div className="err">Что-то пошло не так. Мы уже знаем.</div>;
}
return this.props.children;
}
}

Используем:

import {ErrorBoundary} from './ErrorBoundary';
createRoot(document.getElementById('root')).render(
<ErrorBoundary>
<App />
</ErrorBoundary>
);
  • Слишком долгая загрузка: PerformanceObserver для largest-contentful-paint, если LCP > 5с — отправлять предупреждение.
  • Сломанные изображения: img.onerror → массовый репорт пачкой.
  • Пользовательские «жалобы». Кнопка «Сообщить о проблеме» в футере → отправляет URL и localStorage.userId в Notifly.
  • Дешёвая «Sentry для своих» — никаких лимитов и подписок.
  • Реалити-чек после каждого деплоя: пара ошибок — значит зацепили старую часть фронта.
  • Контекст по жалобе пользователя — он пишет «у меня не работает», вы открываете Notifly и видите точный URL и стектрейс.
  • Source maps — загружайте .map-файлы через POST /web-script/:id/sourcemaps, и стектрейсы в уведомлении автоматически раскроются обратно в src/file.ts:LINE:COL (подробности — в документации). В CI используйте готовый scripts/upload-sourcemaps.py.
  • Хеш стека → дедупликация на стороне Notifly: одинаковые ошибки укрупнять в одно сообщение с счётчиком.
  • window.NOTIFLY_RELEASE — пропишите git-хэш или семвер, чтобы при деплое сразу видеть, новый ли релиз сломался.
КритерийWebScript console_errorsБэкенд-прокси
Время интеграции2 минуты15–30 минут
Нужен бэкендНетДа
SourcemapsДа (загрузка через REST)Можно добавить
Rate limitingВстроен (100 req/session)Свой
Подходит дляPet-проекты, лендинги, MVPПродакшн с кастомной обработкой