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

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).

  1. Откройте app.notifly.ruВеб-скрипты.
  2. Нажмите «Создать скрипт», заполните:
    • Название — для отображения, например «Лендинг — клик “Купить”».
    • Канал — куда слать уведомления.
    • Тип событияОткрытие страницы, Отправка формы или Клик на кнопку.
    • Заголовок уведомленияtitle сообщения по умолчанию.
    • Приоритет — 0 = взять defaultPriority канала, иначе 1–10.
  3. После создания нажмите «Показать сниппет» — там готовый HTML, который можно скопировать в <head> сайта или в любое место <body>.
Окно терминала
curl -X POST "$NOTIFLY_URL/web-script" \
-H "Content-Type: application/json" \
-H "X-Notifly-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-Notifly-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 всегда.

<!-- 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> на странице, собирает все непустые поля и отправляет их в теле уведомления:

<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>

Срабатывает только для элементов, у которых есть атрибут 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. Для него используется hosted SDK (error-sdk.js), который Notifly раздаёт со своего CDN — готовый сниппет с вашим токеном генерируется в админке на карточке скрипта («Примеры кода»). SDK ловит:

  • window.onerror — все необработанные исключения (включая ошибки загрузки ресурсов);
  • unhandledrejection — отклонённые промисы без .catch();
  • ручные вызовыErrorSDK.captureException(err, ctx) и ErrorSDK.captureMessage(msg, level, ctx).

Особенности:

  • Breadcrumbs — SDK буферизует события console.*, fetch/XHR, клики и SPA-навигацию; последние крошки прикладываются к ошибке («что было перед ней»).
  • Батчинг — ошибки копятся в очередь и отправляются пачкой (до 10 шт или каждые 3 сек).
  • Дедупликация — одна и та же ошибка (по fingerprint) не отправляется повторно в окне ~10 сек.
  • SamplingsampleRate (1 = всё) прореживает отправку на клиенте; сервер экстраполирует счётчики.
  • Лимит запросов — максимум 100 POST-ов за загрузку страницы, чтобы не попасть в бесконечный цикл.
  • sendBeacon — используется navigator.sendBeacon при уходе со страницы (не блокирует навигацию).
  • Финальный flush — при pagehide / скрытии вкладки скрипт отправляет оставшуюся очередь.
  • Санитизация — секреты (password/token/cookie/…) и чувствительные query-параметры вырезаются и на клиенте, и повторно на сервере.
  • 1 батч = 1 событие квоты — весь массив ошибок превращается в одно уведомление с одним push.
<!-- Notifly — перехват ошибок браузера (hosted SDK) -->
<script src="https://app.notifly.ru/static/error-sdk.js" crossorigin="anonymous"></script>
<script>
// window.NOTIFLY_RELEASE можно задать ДО этого скрипта, чтобы помечать события релизом.
window.ErrorSDK && window.ErrorSDK.init({
endpoint: "https://your-notifly/script/T...",
app: "My App",
release: (window.NOTIFLY_RELEASE || ""),
environment: "production",
sampleRate: 1,
maxBreadcrumbs: 20
});
</script>

Параметры init():

ПараметрОписание
endpointПубличный URL скрипта POST /script/T<token> (обязателен)
appНазвание приложения — попадает в заголовок уведомления
releaseМетка релиза (git-sha / версия) — нужна для source maps
environmentОкружение (production / staging / …) — сверяется с allowlist скрипта
sampleRateДоля отправляемых ошибок, 1 = все
notifyThresholdКлиентская подсказка порога уведомлений (сервер всё равно решает сам)
maxBreadcrumbsСколько «крошек» прикладывать к ошибке (≤ 30)

SDK формирует батч сам; ниже — схема для кастомных интеграций. Тело запроса POST /script/T<token> (макс. 64 KiB, до 50 ошибок в батче):

{
"title": "My App",
"release": "v1.2.3",
"environment": "production",
"sampleRate": 1,
"flushReason": "batch",
"errors": [
{
"eventId": "3f2a…",
"type": "error",
"level": "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,
"sentAt": "2026-07-14T12:00:00.000Z",
"exception": {"name": "TypeError", "message": "...", "stack": "..."},
"runtime": {
"url": "https://example.com/dashboard",
"path": "/dashboard",
"referrer": "https://example.com/",
"userAgent": "Mozilla/5.0 ...",
"language": "ru-RU",
"viewport": "1920x1080"
},
"context": {"feature": "checkout"},
"breadcrumbs": [
{"category": "ui.click", "level": "info", "message": "", "data": {"target": "button#pay"}, "timestamp": "2026-07-14T11:59:58.000Z"}
]
}
]
}

Обязательные поля каждой ошибки: message, type (error | unhandledrejection | console.error | exception | message), level (info | warning | error | fatal), runtime.url и sentAt (RFC3339). Батч без environment, с неизвестным type/level или с более чем 30 breadcrumbs на ошибку отклоняется с кодом 400.

Сервер Notifly формирует из батча одно уведомление в стиле Sentry-карточки:

  • Заголовок — первая ошибка (TypeError: …, + (+N more) если ошибок несколько).
  • Тело — тэги (level · environment · release · браузер · язык), URL:строка:колонка, referrer/viewport, стектрейс первой ошибки, компактный список остальных (до 4), последние breadcrumbs и счётчик повторений проблемы.

Задайте 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 / Nuxtnuxt.configapp.head.script или app.html
Angularsrc/index.html в <head> перед polyfills
Astrosrc/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.

  1. Соберите фронтенд с включёнными .map-файлами (sourceMap: true).
  2. Загрузите эти .map в Notifly через REST API — по одной карте на каждый .js-файл.
  3. Укажите для каждого .map тот же release, что прокидываете в window.NOTIFLY_RELEASE.
  4. При получении ошибки Notifly находит .map по release + URL-префиксу + имени файла и подменяет URL:LINE:COL на src/file.ts:LINE:COL — прямо в тексте уведомления.

Резолвинг — best-effort: если карта не найдена или фрейм не совпал, в уведомление попадает оригинальный (минифицированный) стек.

Окно терминала
# базовая аутентификация — те же логин/пароль, что для админки
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.

Чтобы не звать 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
Метод и путьНазначение
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 — повторная заливка одного и того же файла не создаёт лишних копий.
  • Максимальный размер одного .map10 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..."}'
Метод и путьАвторизацияНазначение
GET /web-scriptclient-tokenсписок скриптов
POST /web-scriptclient-token (write)создание
PUT /web-script/:idclient-token (write)обновление
DELETE /web-script/:idclient-token (write)удалить
GET /web-script/:id/sourcemapsclient-tokenсписок source map
POST /web-script/:id/sourcemapsclient-token (write)загрузить .map
DELETE /web-script/:id/sourcemaps/:smidclient-token (write)удалить .map
GET /web-script/:id/issuesclient-tokenсписок агрегированных ошибок
GET /web-script/:id/issue-countclient-tokenчисло нерешённых проблем ({"unresolved": N})
POST /web-script/:id/issues/:iid/resolveclient-token (write)отметить решённой
POST /web-script/:id/issues/:iid/ignoreclient-token (write)игнорировать проблему
DELETE /web-script/:id/issues/:iidclient-token (write)удалить
GET /web-script/:id/issues/:iid/breadcrumbsclient-tokenпоследние действия пользователя перед ошибкой
POST /web-script/:id/issues/:iid/analyzeclient-token (write)AI-разбор причины (тело: lang, force)
GET /web-script/:id/issues/:iid/analysisclient-tokenсохранённый разбор (204, если его ещё нет)
GET /web-script/:id/seriesclient-tokenтайм-серия срабатываний скрипта
GET /web-script/:id/error-seriesclient-tokenтайм-серия ошибок console_errors
GET /web-script/:id/flush-statsclient-tokenраспределение причин отправки батчей
GET/POST /script/:tokenпубличныйтриггер из браузера или скрипта

Раздел «Issues» на карточке скрипта console_errors группирует входящие ошибки в дедуплицированные проблемы (по message + верхнему фрейму стека + release): для каждой видно число повторений, время первого и последнего появления. Проблему можно отметить решённой (/issues/:iid/resolve) — её следующее появление снова даст уведомление как регрессия, — скрыть (/issues/:iid/ignore) или удалить (DELETE /web-script/:id/issues/:iid).

POST /web-script/:id/issues/:iid/analyze собирает улики (стек, breadcrumbs, контекст страницы), отдаёт их модели и сохраняет разбор — вероятную причину и что проверить в первую очередь:

Окно терминала
curl -X POST "$NOTIFLY_URL/web-script/77/issues/1234/analyze" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <client-token>" \
-d '{"lang": "ru"}'

Разбор кэшируется по отпечатку входных данных: пока проблема не получила новых событий и язык не менялся, повторный вызов отдаёт сохранённый результат — без обращения к модели и без списания AI-квоты. Принудительно пересчитать — {"force": true}. Готовый разбор доступен отдельно через GET /web-script/:id/issues/:iid/analysis (204, если его ещё нет).

Тайм-серии принимают общие параметры ?bucket=1m|1h|1d&from=&to=; окно зажимается до глубины хранения.

  • /series — срабатывания триггерных скриптов (page_open, form_submit, button_click); для console_errors вернётся пусто.
  • /error-series — динамика ошибок console_errors.
  • /flush-stats — распределение причин отправки батчей (reason → count): видно, какая доля событий уходит через beacon при закрытии страницы.
  • T<token> подбирается криптостойким случайным генератором (160 бит) — в URL его указывать безопасно ровно настолько, насколько вы сами не выкладываете токен в публично-доступный репозиторий.
  • Если вы по ошибке закоммитили T<token> в публичный репозиторий — удалите скрипт через админку или DELETE /web-script/:id. Старый URL начнёт «глотать» запросы (тихий 200 OK) без создания уведомлений.
  • На каждом успешном триггере поле lastUsed обновляется — в админке видно, если скрипт «висит» неиспользуемым.