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

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, браузер, breadcrumbs

Триггерные сниппеты (page_open, form_submit, button_click) отправляют POST на /script/T<token> с application/json-телом {title, message}, console_errors — батч ошибок (формат ниже). Это публичный эндпоинт, не требующий авторизации (аутентификацией служит сам T<token> в URL).

Каждое созданное уведомление — одно событие дневной квоты. Это обычное сообщение канала: на него действуют эскалация, тихие часы и окна обслуживания канала.

  1. Откройте app.notifly.ru → Интеграции → Скрипты для сайта.
  2. Нажмите «Создать скрипт», заполните:
    • Название — для отображения, например «Лендинг — клик “Купить”».
    • Канал — куда слать уведомления (только ваш собственный канал).
    • Тип скрипта — «Открытие страницы», «Отправка формы», «Клик на кнопку» или «Ошибки консоли». Тип нельзя изменить после создания.
    • Заголовок уведомления — title сообщения по умолчанию.
    • Приоритет (0-10) — 0 = взять defaultPriority канала.
    • Для «Ошибок консоли» — ещё Порог уведомления и Окружения (см. ниже).
  3. На карточке скрипта — готовый код во вкладках: HTML вставка, SDK в коде (только для ошибок консоли), Google Tag Manager и Для LLM. HTML-вставку можно скопировать в <head> сайта или в любое место <body>.
Окно терминала
curl -X POST "$NOTIFLY_URL/web-script" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <api-token>" \
-d '{
"name": "Лендинг — клик «Купить»",
"appId": 12345,
"scriptType": "button_click",
"title": "Клик «Купить» на лендинге",
"priority": 3
}'

Для console_errors:

Окно терминала
curl -X POST "$NOTIFLY_URL/web-script" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <api-token>" \
-d '{
"name": "Production — ошибки фронтенда",
"appId": 12345,
"scriptType": "console_errors",
"title": "JS Error",
"priority": 8,
"notifyThreshold": "error",
"environments": "production"
}'

В ответе придёт объект скрипта с публичным token (префикс T):

{
"id": 8,
"token": "T7c2a8f3b1e0d4a6c8e9f",
"appId": 12345,
"appName": "Marketing",
"name": "Лендинг — клик «Купить»",
"scriptType": "button_click",
"title": "Клик «Купить» на лендинге",
"priority": 3,
"notifyThreshold": "",
"environments": "",
"created": "2026-04-30T10:11:12Z",
"lastUsed": null
}

Полный URL для триггера — ${NOTIFLY_URL}/script/T7c2a8f3b1e0d4a6c8e9f (в примерах токен укорочен; настоящий — T и 40 hex-символов).

Обязательные поля — name (до 200 символов), appId и scriptType; priority — 0–10. PUT /web-script/:id принимает то же тело, scriptType в нём должен совпадать с текущим. На аккаунт — до 500 скриптов.

Все три триггерных сниппета — вариации одного и того же fetch(url, {method: "POST", ...}). Они никогда не ломают страницу (.catch(function(){})) и не требуют CORS-настроек: /script/:token отвечает с разрешающими CORS-заголовками для любого домена.

<!-- Notifly — уведомление при открытии страницы -->
<script>
(function () {
fetch('https://api.notifly.ru/script/T7c2a8f3b1e0d4a6c8e9f', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: 'Лендинг — клик «Купить»',
message: 'Страница: ' + document.title + '\n\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://api.notifly.ru/script/T...', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: 'Заявка с сайта',
message: lines.join('\n\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://api.notifly.ru/script/T...', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: 'Клик «Купить»',
message: 'Клик: ' + label + '\n\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();
  • console.error(...) — превращается в полноценное событие, даже без throw;
  • CSP-нарушения (securitypolicyviolation) — событием уровня warning;
  • ручные вызовы — ErrorSDK.captureException(err, ctx) и ErrorSDK.captureMessage(msg, level, ctx).

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

  • Breadcrumbs — SDK буферизует вызовы console.error/warn/log/info, fetch/XHR, клики и SPA-навигацию; последние крошки прикладываются к ошибке («что было перед ней»).
  • Батчинг — ошибки копятся в очередь и отправляются пачкой (до 10 шт или каждые 3 сек).
  • Дедупликация на клиенте — одна и та же ошибка не отправляется повторно в окне ~10 сек.
  • Дедупликация на сервере — по одной проблеме уведомление приходит не чаще раза в 2 минуты; повторы в этом окне только увеличивают счётчик.
  • Sampling — sampleRate (1 = всё) прореживает отправку на клиенте; сервер экстраполирует счётчики.
  • Лимит запросов — максимум 100 POST-ов за загрузку страницы, чтобы не попасть в бесконечный цикл.
  • sendBeacon — используется navigator.sendBeacon при уходе со страницы (не блокирует навигацию).
  • Финальный flush — при pagehide / скрытии вкладки скрипт отправляет оставшуюся очередь.
  • Санитизация — секреты (password/token/cookie/…) и чувствительные query-параметры вырезаются и на клиенте, и повторно на сервере.
  • 1 батч = не больше одного уведомления и одного события квоты. Если уведомлять не о чем (всё — повторы в окне дедупликации, ниже порога или из чужого окружения), сервер отвечает {"ok": true, "deduped": true} и квоту не тратит.
<!-- 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://api.notifly.ru/script/T...',
app: 'My App',
release: window.NOTIFLY_RELEASE || '',
environment: 'production',
sampleRate: 1,
maxBreadcrumbs: 20,
});
</script>

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

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

Отфильтрованные на клиенте события не уходят на сервер вовсе — не тратят квоту и не попадают в проблемы и графики.

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, с более чем 50 ошибками или 30 breadcrumbs на ошибку отклоняется с кодом 400; тело больше 64 KiB — 413. Сервер повторно санитизирует всё присланное: чувствительные ключи и query-параметры, JWT/Bearer-токены, email, телефоны, номера карт и публичные IP заменяются на [Filtered].

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

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

У скрипта console_errors есть две серверные настройки (в админке — поля «Порог уведомления» и «Окружения», в API — notifyThreshold и environments):

  • notifyThreshold — info, warning, error или fatal: уведомлять только об ошибках этого уровня и выше. Пусто — любые уровни. Ошибки ниже порога всё равно группируются в проблемы и видны на графике, но без push.
  • environments — allowlist окружений через запятую, например production,staging. Пусто — любое окружение. Батч из окружения не из списка отбрасывается целиком: не попадает ни в проблемы, ни в графики.

Задайте 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.config → app.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: если карта не найдена или фрейм не совпал, в уведомление попадает оригинальный (минифицированный) стек.

Окно терминала
# API-токен с правом записи
curl -H "X-Notifly-Key: <api-token>" \
-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"

Вместо токена эта ручка — единственная в API — принимает и логин с паролем аккаунта (HTTP Basic, curl -u "$NOTIFLY_USER:$NOTIFLY_PASSWORD"): так проще в старых CI-скриптах. Source maps принимаются только для скриптов типа console_errors.

Тело запроса — 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. Файл проверяется при загрузке: не Source Map v3 — 400, больше 10 MB — 413; анти-XSSI префикс )]}' срезается автоматически.

Если сборка уже ходит в Notifly из кода, загрузка — один вызов SDK с API-токеном (право записи), без basic-аутентификации:

f, _ := os.Open("dist/static/js/main.abc123.js.map")
defer f.Close()
_, err := admin.UploadSourceMap(ctx, scriptID, notifly.SourceMapUpload{
Release: os.Getenv("GITHUB_SHA"), // должен совпасть с window.NOTIFLY_RELEASE
URLPrefix: "https://app.example.com/static/js/",
FileName: "main.abc123.js",
Map: f,
})
with open("dist/static/js/main.abc123.js.map", "rb") as f:
admin.upload_source_map(script_id, os.environ["GITHUB_SHA"],
"https://app.example.com/static/js/", "main.abc123.js", f)

Повторная загрузка той же пары (release, fileName) заменяет карту, а не плодит дубли, поэтому перезапуск сборки безопасен. Размер и имя файла SDK проверяет до отправки.

Чтобы не звать curl в цикле, используйте готовый скрипт scripts/upload-sourcemaps.py:

Окно терминала
python3 scripts/upload-sourcemaps.py \
--api https://api.notifly.ru \
--token "$NOTIFLY_TOKEN" \
--script-id 12345 \
--release "$GITHUB_SHA" \
--url-prefix "https://app.example.com/static/js/" \
--dir ./dist/static/js

Скрипт берёт API-токен из --token / NOTIFLY_TOKEN, либо логин и пароль из --user/--password (NOTIFLY_USER/NOTIFLY_PASSWORD). Пример GitHub Actions — выгрузка карт после билда:

- name: Build
run: npm run build
- name: Upload sourcemaps to Notifly
env:
NOTIFLY_TOKEN: ${{ secrets.NOTIFLY_TOKEN }}
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 — повторная заливка одного и того же файла не создаёт лишних копий.
  • Максимальный размер одного .map — 10 MB, не больше 500 карт на скрипт.
  • При удалении 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..."}'

Ответы: 200 {"ok": true} — уведомление создано; 400 — неизвестный токен или невалидное тело; 413 — тело больше 64 KiB; 429 — исчерпана дневная квота или слишком частые запросы; 503 — временный сбой, запрос можно повторить.

Метод и путьАвторизацияНазначение
GET /web-scriptAPI-токенсписок скриптов
POST /web-scriptAPI-токен (write)создание
PUT /web-script/:idAPI-токен (write)обновление
DELETE /web-script/:idAPI-токен (write)удалить
GET /web-script/:id/sourcemapsAPI-токенсписок source map
POST /web-script/:id/sourcemapsAPI-токен (write) или Basicзагрузить .map
DELETE /web-script/:id/sourcemaps/:smidAPI-токен (write)удалить .map
GET /web-script/:id/issuesAPI-токенсписок агрегированных ошибок
GET /web-script/:id/issue-countAPI-токенчисло нерешённых проблем ({"unresolved": N})
POST /web-script/:id/issues/:iid/resolveAPI-токен (write)отметить решённой
POST /web-script/:id/issues/:iid/ignoreAPI-токен (write)игнорировать проблему
DELETE /web-script/:id/issues/:iidAPI-токен (write)удалить
GET /web-script/:id/issues/:iid/breadcrumbsAPI-токенпоследние действия пользователя перед ошибкой
POST /web-script/:id/issues/:iid/analyzeAPI-токенAI-разбор причины (тело: lang, force)
GET /web-script/:id/issues/:iid/analysisAPI-токенсохранённый разбор (204, если его ещё нет)
GET /web-script/:id/seriesAPI-токентайм-серия срабатываний скрипта
GET /web-script/:id/error-seriesAPI-токентайм-серия ошибок console_errors
GET /web-script/:id/flush-statsAPI-токенраспределение причин отправки батчей
GET/POST /script/:tokenпубличныйтриггер из браузера или скрипта

Токены канала (в том числе расшаренного) к управляющим ручкам не пускаются — 403.

Раздел «Проблемы» на карточке скрипта console_errors группирует входящие ошибки в дедуплицированные проблемы. Ключ группировки — тип ошибки, до пяти верхних кадров стека (после source maps; номера строк без колонок, URL без query и с обобщёнными id в пути) и release; если распознаваемого стека нет — нормализованное сообщение (числа, UUID, URL заменены плейсхолдерами) и URL страницы. Для каждой проблемы видно число повторений (и оценку с учётом sampleRate — estimatedCount), время первого и последнего появления, образец стека, URL и браузер; «События перед ошибкой» (breadcrumbs) хранятся ~14 дней, сами проблемы удаляются через 90 дней без новых событий.

  • Пометить решённой (/issues/:iid/resolve) — следующее появление снова даст уведомление как регрессия. Тело {"resolved": false} открывает проблему снова.
  • Заглушить уведомления (/issues/:iid/ignore) — события продолжают считаться, но уведомления по проблеме не приходят никогда. {"ignored": false} — включить обратно.
  • Удалить — DELETE /web-script/:id/issues/:iid.

issue-count не учитывает решённые и заглушённые проблемы — это число для бейджа на карточке.

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: <api-token>" \
-d '{"lang": "ru"}'

Ответ:

{
"probableCause": "…",
"evidence": ["…"],
"suggestedSteps": ["…"],
"confidence": "medium",
"model": "…",
"lang": "ru",
"created": "2026-09-27T10:00:00Z",
"stale": false
}

lang — язык разбора (по умолчанию ru), confidence — high, medium или low; в evidence и suggestedSteps — до пяти пунктов.

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

Ошибки: 422 — у проблемы нет ни стека, ни breadcrumbs, разбирать нечего; 429 — исчерпана дневная квота ИИ-запросов; 503 — ИИ не настроен; 502 — модель не ответила или вернула некорректный результат.

Тайм-серии принимают общие параметры ?bucket=1m|1h|1d&from=&to= (RFC3339); по умолчанию — 1h за последние 24 часа, окно не длиннее 35 дней. Минутные данные хранятся около суток, дневные — около месяца, поэтому для длинных окон берите 1d.

  • /series — срабатывания триггерных скриптов (page_open, form_submit, button_click); для console_errors вернётся пусто.
  • /error-series — динамика ошибок console_errors с разбивкой по уровням (info, warning, error, fatal); считаются все принятые события, включая ниже порога уведомлений.
  • /flush-stats — распределение причин отправки батчей (reason → count: batch, timer, pagehide, visibilitychange, manual, …): видно, какая доля батчей уходит при закрытии страницы (через sendBeacon).
  • T<token> подбирается криптостойким случайным генератором (160 бит). Он неизбежно виден в коде страницы, поэтому считайте его «публичным ключом приёма»: с ним можно только слать события в этот скрипт.
  • Сменить токен у существующего скрипта нельзя. Если токеном злоупотребляют — удалите скрипт через админку или DELETE /web-script/:id и создайте новый: запросы на старый URL будут получать 400 без создания уведомлений.
  • На каждом успешном триггере поле lastUsed обновляется — в админке видно, если скрипт «висит» неиспользуемым.