WebScript (уведомления прямо со страницы)
WebScript — это лёгкий способ получать уведомления прямо с чужой/своей
веб-страницы, ничего не трогая в бекенде. Вы создаёте «скрипт» в Notifly,
указываете тип события (открытие страницы / отправка формы / клик по кнопке /
ошибки консоли) — и получаете готовый HTML-сниппет с публичным URL вида
/script/T<token>.
Вставляете его в шаблон сайта, в виджет, в письмо, в README — каждый раз, когда событие срабатывает, в выбранный канал прилетает уведомление.
Удобно для:
- сайтов на «коробочной» CMS, где сложно дотянуться до бекенда;
- лендингов и форм на статике (Tilda, GitHub Pages, Netlify);
- быстрых экспериментов («хочу пинг каждый раз, когда кто-то жмёт Купить»);
- виджетов в админках сторонних SaaS, куда можно вставить только
<script>; - замены Sentry/Bugsnag для pet-проектов и небольших команд.
Типы скриптов
Заголовок раздела «Типы скриптов»Поддерживается четыре предустановленных шаблона. Сниппет с готовым HTML/JS вы скопируете прямо из админки — здесь только описание поведения.
scriptType | Когда срабатывает | Что попадает в message |
|---|---|---|
page_open | При загрузке страницы (<script> исполняется) | Страница: <title> + URL: <location> |
form_submit | При сабмите любой <form> на странице | Все непустые поля формы (name: value) |
button_click | При клике на элемент с атрибутом data-notifly | Текст атрибута data-notifly или текстовое содержимое + URL |
console_errors | При возникновении JS-ошибки в браузере | Стектрейс, URL, User-Agent, lineno/colno |
Все три варианта отправляют POST на /script/T<token> с application/json-
телом {title, message} — это публичный эндпоинт, не требующий авторизации
(аутентификацией служит сам T<token> в URL).
Создание скрипта
Заголовок раздела «Создание скрипта»Через админку
Заголовок раздела «Через админку»- Откройте app.notifly.ru → Веб-скрипты.
- Нажмите «Создать скрипт», заполните:
- Название — для отображения, например «Лендинг — клик “Купить”».
- Канал — куда слать уведомления.
- Тип события —
Открытие страницы,Отправка формыилиКлик на кнопку. - Заголовок уведомления —
titleсообщения по умолчанию. - Приоритет — 0 = взять
defaultPriorityканала, иначе 1–10.
- После создания нажмите «Показать сниппет» — там готовый HTML, который
можно скопировать в
<head>сайта или в любое место<body>.
Через REST API
Заголовок раздела «Через REST API»curl -X POST "$NOTIFLY_URL/web-script" \ -H "Content-Type: application/json" \ -H "X-Gotify-Key: <client-token>" \ -d '{ "name": "Лендинг — клик «Купить»", "appId": 12345, "scriptType": "button_click", "title": "Клик «Купить» на лендинге", "priority": 7 }'Для console_errors:
curl -X POST "$NOTIFLY_URL/web-script" \ -H "Content-Type: application/json" \ -H "X-Gotify-Key: <client-token>" \ -d '{ "name": "Production — ошибки фронтенда", "appId": 12345, "scriptType": "console_errors", "title": "JS Error", "priority": 8 }'В ответе придёт объект скрипта с публичным token (префикс T):
{ "id": 8, "token": "T7c2a8f3b1e0d4a6c8e9f", "appId": 12345, "appName": "Marketing", "name": "Лендинг — клик «Купить»", "scriptType": "button_click", "title": "Клик «Купить» на лендинге", "priority": 7, "created": "2026-04-30T10:11:12Z", "lastUsed": null}Полный URL для триггера — ${NOTIFLY_URL}/script/T7c2a8f3b1e0d4a6c8e9f.
Готовые сниппеты
Заголовок раздела «Готовые сниппеты»Все три сниппета — вариации одного и того же fetch(url, {method: "POST", ...}).
Они никогда не падают на стороне сайта (.catch(function(){})) и не
требуют CORS-настроек, потому что /script/:token отвечает 200 OK всегда.
page_open — пинг при открытии страницы
Заголовок раздела «page_open — пинг при открытии страницы»<!-- Notifly — уведомление при открытии страницы --><script>(function() { fetch("https://your-notifly/script/T7c2a8f3b1e0d4a6c8e9f", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ title: "Лендинг — клик «Купить»", message: "Страница: " + document.title + "\nURL: " + location.href }) }).catch(function(){});})();</script>form_submit — пинг при отправке формы
Заголовок раздела «form_submit — пинг при отправке формы»Скрипт навешивается на все <form> на странице, собирает все непустые
поля и отправляет их в теле уведомления:
<script>document.addEventListener("DOMContentLoaded", function() { document.querySelectorAll("form").forEach(function(form) { form.addEventListener("submit", function() { var fd = new FormData(form); var lines = []; fd.forEach(function(v, k) { if (v) lines.push(k + ": " + v); }); fetch("https://your-notifly/script/T...", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ title: "Заявка с сайта", message: lines.join("\n") || "(пустая форма)" }) }).catch(function(){}); }); });});</script>button_click — пинг при клике на элемент
Заголовок раздела «button_click — пинг при клике на элемент»Срабатывает только для элементов, у которых есть атрибут data-notifly:
<script>document.addEventListener("click", function(e) { var el = e.target.closest("[data-notifly]"); if (!el) return; var label = el.getAttribute("data-notifly") || el.textContent.trim() || "кнопка"; fetch("https://your-notifly/script/T...", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ title: "Клик «Купить»", message: "Клик: " + label + "\nСтраница: " + location.href }) }).catch(function(){});});</script><!-- Пример использования: --><button data-notifly="Заказать">Заказать</button>console_errors — перехват ошибок консоли (Sentry-lite)
Заголовок раздела «console_errors — перехват ошибок консоли (Sentry-lite)»Самый мощный тип скрипта — полноценный перехватчик JS-ошибок, работающий как облегчённый аналог Sentry / Bugsnag / TrackJS. Скрипт ловит:
window.onerror— все необработанные исключения;unhandledrejection— отклонённые промисы без.catch();console.error()— явные вызовыconsole.error(опционально).
Особенности:
- Батчинг — ошибки копятся в очередь и отправляются пачкой (до 10 шт или каждые 3 сек).
- Дедупликация — одна и та же ошибка (по message + верхний фрейм стека) не отправляется повторно в рамках одной сессии.
- Лимит запросов — максимум 100 POST-ов за загрузку страницы, чтобы не попасть в бесконечный цикл.
- sendBeacon — используется
navigator.sendBeacon, когда доступен (не блокирует навигацию / закрытие вкладки). - Финальный flush — при
pagehideскрипт отправляет оставшуюся очередь. - 1 батч = 1 событие квоты — весь массив ошибок превращается в одно уведомление с одним push.
<!-- Notifly — перехват ошибок консоли браузера --><script>(function() { var ENDPOINT = "https://your-notifly/script/T..."; var RELEASE = (window.NOTIFLY_RELEASE || ""); var queue = [], timer = null, seen = {}, SENT_CAP = 100, sent = 0;
function fingerprint(e) { return (e.message || "") + "|" + ((e.stack || "").split("\n")[1] || ""); } function flush() { timer = null; if (!queue.length || sent >= SENT_CAP) return; var batch = queue.splice(0, queue.length); sent++; var payload = JSON.stringify({ title: "Console errors", release: RELEASE, errors: batch }); try { if (navigator.sendBeacon) { navigator.sendBeacon(ENDPOINT, new Blob([payload], {type: "application/json"})); return; } } catch (_) {} fetch(ENDPOINT, { method: "POST", headers: {"Content-Type": "application/json"}, body: payload, keepalive: true }).catch(function(){}); } function enqueue(item) { var fp = fingerprint(item); if (seen[fp]) return; seen[fp] = 1; item.url = location.href; item.userAgent = navigator.userAgent; item.ts = Date.now(); queue.push(item); if (queue.length >= 10) { clearTimeout(timer); flush(); return; } if (!timer) timer = setTimeout(flush, 3000); }
window.addEventListener("error", function(ev) { var err = ev.error || {}; enqueue({ type: "error", message: ev.message || String(err.message || err) || "Unknown error", stack: (err && err.stack) || "", lineno: ev.lineno || 0, colno: ev.colno || 0 }); }); window.addEventListener("unhandledrejection", function(ev) { var r = ev.reason; enqueue({ type: "unhandledrejection", message: String(r && (r.message || r.toString()) || "Unhandled promise rejection"), stack: (r && r.stack) || "" }); }); var _ce = console.error; console.error = function() { try { var parts = []; for (var i = 0; i < arguments.length; i++) { var a = arguments[i]; parts.push(a && a.message ? a.message : (typeof a === "string" ? a : JSON.stringify(a))); } enqueue({type: "console.error", message: parts.join(" ").slice(0, 500), stack: ""}); } catch (_) {} return _ce.apply(console, arguments); }; window.addEventListener("pagehide", flush);})();</script>Формат данных
Заголовок раздела «Формат данных»Тело запроса POST /script/T<token>:
{ "title": "Console errors", "release": "v1.2.3", "errors": [ { "type": "error", "message": "Cannot read properties of undefined (reading 'map')", "stack": "TypeError: Cannot read properties...\n at App.tsx:42:12\n at ...", "url": "https://example.com/dashboard", "lineno": 42, "colno": 12, "userAgent": "Mozilla/5.0 ...", "ts": 1716556800000 } ]}Сервер Notifly формирует из батча одно уведомление:
- Заголовок — первая ошибка (+
(+N more)если ошибок несколько). - Тело — URL, release, User-Agent и стектрейсы (до 5 ошибок отображаются полностью).
Привязка к релизу
Заголовок раздела «Привязка к релизу»Задайте window.NOTIFLY_RELEASE до загрузки скрипта — значение попадёт
в поле release каждого батча:
<script>window.NOTIFLY_RELEASE = "v2.1.0-abc1234";</script><!-- Notifly console_errors snippet here -->Куда вставлять в популярных фреймворках
Заголовок раздела «Куда вставлять в популярных фреймворках»| Фреймворк | Где размещать |
|---|---|
| Next.js (App Router) | app/layout.tsx в <head> через next/script strategy="beforeInteractive" |
| Next.js (Pages) | pages/_document.tsx в <Head> |
| React (CRA / Vite) | public/index.html в <head> перед бандлом |
| Vue / Nuxt | nuxt.config → app.head.script или app.html |
| Angular | src/index.html в <head> перед polyfills |
| Astro | src/layouts/Layout.astro в <head> |
| Electron | В renderer HTML <head> |
Source maps — раскрывание минифицированного стека
Заголовок раздела «Source maps — раскрывание минифицированного стека»Если ваш JS прошёл через минификатор (esbuild, terser, Webpack production), стектрейсы
в браузере выглядят как at f (https://app.example.com/static/js/main.abc123.js:1:12345) —
без имён функций и реальных строк. Notifly умеет на лету разворачивать такие фреймы
обратно к исходникам (TypeScript / .vue / .jsx), используя загруженные source map.
Как это работает
Заголовок раздела «Как это работает»- Соберите фронтенд с включёнными
.map-файлами (sourceMap: true). - Загрузите эти
.mapв Notifly через REST API — по одной карте на каждый.js-файл. - Укажите для каждого
.mapтот жеrelease, что прокидываете вwindow.NOTIFLY_RELEASE. - При получении ошибки Notifly находит
.mapпоrelease+ URL-префиксу + имени файла и подменяетURL:LINE:COLнаsrc/file.ts:LINE:COL— прямо в тексте уведомления.
Резолвинг — best-effort: если карта не найдена или фрейм не совпал, в уведомление попадает оригинальный (минифицированный) стек.
Загрузка через REST
Заголовок раздела «Загрузка через REST»# базовая аутентификация — те же логин/пароль, что для админкиcurl -u admin:admin \ -F "release=v2.1.0-abc1234" \ -F "urlPrefix=https://app.example.com/static/js/" \ -F "fileName=main.abc123.js" \ -F "file=@dist/static/js/main.abc123.js.map" \ "$NOTIFLY_URL/web-script/<scriptId>/sourcemaps"Тело запроса — multipart/form-data с обязательными полями:
| Поле | Описание |
|---|---|
file | Сам .map-файл (JSON Source Map v3, до 10 MB) |
release | Версия релиза — должна совпасть с window.NOTIFLY_RELEASE |
urlPrefix | Префикс URL, по которому браузер грузит этот .js (например https://x/static/) |
fileName | Имя .js-файла без префикса (main.abc123.js), без / и \ |
При совпадении кадра по urlPrefix + fileName Notifly резолвит позицию. Поиск
кадра — по длиннейшему префиксу, так что можно держать одновременно карты
с разных доменов / CDN.
CLI-хелпер
Заголовок раздела «CLI-хелпер»Чтобы не звать curl в цикле, используйте готовый скрипт
scripts/upload-sourcemaps.py:
python3 scripts/upload-sourcemaps.py \ --api https://api.notifly.ru \ --user "$NOTIFLY_USER" --password "$NOTIFLY_PASS" \ --script-id 12345 \ --release "$GITHUB_SHA" \ --url-prefix "https://app.example.com/static/js/" \ --dir ./dist/static/jsПример GitHub Actions — выгрузка карт после билда:
- name: Build run: npm run build
- name: Upload sourcemaps to Notifly env: NOTIFLY_USER: ${{ secrets.NOTIFLY_USER }} NOTIFLY_PASSWORD: ${{ secrets.NOTIFLY_PASSWORD }} run: | python3 scripts/upload-sourcemaps.py \ --script-id ${{ vars.NOTIFLY_SCRIPT_ID }} \ --release "${{ github.sha }}" \ --url-prefix "https://app.example.com/static/js/" \ --dir ./dist/static/jsУправление и REST
Заголовок раздела «Управление и REST»| Метод и путь | Назначение |
|---|---|
GET /web-script/:id/sourcemaps | список загруженных карт |
POST /web-script/:id/sourcemaps | загрузить .map (multipart) |
DELETE /web-script/:id/sourcemaps/:smid | удалить карту (S3 + БД) |
В админке кнопка «Source maps» появляется на карточке скрипта типа
console_errors — там же можно посмотреть список, удалить устаревшие карты
или загрузить новый .map без CLI.
Безопасность и лимиты
Заголовок раздела «Безопасность и лимиты»- Карты хранятся в приватном S3-бакете Yandex Object Storage, прямого URL у них нет.
- Содержимое дедуплицируется по
sha256— повторная заливка одного и того же файла не создаёт лишних копий. - Максимальный размер одного
.map— 10 MB. - При удалении web-скрипта все связанные
.mapудаляются автоматически (cascade). - Резолв ограничен таймаутом 3 секунды на запрос — если карт много, лишние игнорируются, оригинальный стек всё равно попадёт в уведомление.
Триггер вручную
Заголовок раздела «Триггер вручную»Вы можете дёрнуть /script/T<token> чем угодно — этот эндпоинт публичный
и принимает GET и POST. JSON-тело необязательно: если его нет, в push
уйдёт title из настроек скрипта и текст «(web script trigger)».
# минимальноcurl -fsS "$NOTIFLY_URL/script/T7c2a8f3b1e0d4a6c8e9f" -o /dev/null
# с переопределением заголовка и текстаcurl -X POST "$NOTIFLY_URL/script/T7c2a8f3b1e0d4a6c8e9f" \ -H "Content-Type: application/json" \ -d '{"title":"Заявка №42","message":"Имя: Иван\nТелефон: +7..."}'REST API
Заголовок раздела «REST API»| Метод и путь | Авторизация | Назначение |
|---|---|---|
GET /web-script | client-token | список скриптов |
POST /web-script | client-token (write) | создание |
PUT /web-script/:id | client-token (write) | обновление |
DELETE /web-script/:id | client-token (write) | удалить |
GET /web-script/:id/sourcemaps | client-token (write) | список source map |
POST /web-script/:id/sourcemaps | client-token (write) | загрузить .map |
DELETE /web-script/:id/sourcemaps/:smid | client-token (write) | удалить .map |
GET/POST /script/:token | публичный | триггер из браузера или скрипта |
Безопасность
Заголовок раздела «Безопасность»T<token>подбирается криптостойким случайным генератором (160 бит) — в URL его указывать безопасно ровно настолько, насколько вы сами не выкладываете токен в публично-доступный репозиторий.- Если вы по ошибке закоммитили
T<token>в публичный репозиторий — удалите скрипт через админку илиDELETE /web-script/:id. Старый URL начнёт «глотать» запросы (тихий200 OK) без создания уведомлений. - На каждом успешном триггере поле
lastUsedобновляется — в админке видно, если скрипт «висит» неиспользуемым.