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

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-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 всегда.

<!-- 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. Скрипт ловит:

  • 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 / 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 (write)список source map
POST /web-script/:id/sourcemapsclient-token (write)загрузить .map
DELETE /web-script/:id/sourcemaps/:smidclient-token (write)удалить .map
GET/POST /script/:tokenпубличныйтриггер из браузера или скрипта
  • T<token> подбирается криптостойким случайным генератором (160 бит) — в URL его указывать безопасно ровно настолько, насколько вы сами не выкладываете токен в публично-доступный репозиторий.
  • Если вы по ошибке закоммитили T<token> в публичный репозиторий — удалите скрипт через админку или DELETE /web-script/:id. Старый URL начнёт «глотать» запросы (тихий 200 OK) без создания уведомлений.
  • На каждом успешном триггере поле lastUsed обновляется — в админке видно, если скрипт «висит» неиспользуемым.