Frontend Error Notifications
Notifly is great as a “cheap Sentry” for small sites and pet projects: catch window.onerror, unhandledrejection, React error boundary — and send a short message with the stack trace and URL.
Recommended method: WebScript console_errors
Section titled “Recommended method: WebScript console_errors”What the script does:
- Captures
window.onerror,unhandledrejection,console.error. - Deduplication within a session — repeated errors are not sent.
- Batching: up to 10 errors per request, flush every 3 seconds.
- Uses
navigator.sendBeacon— does not block page unload. - Limit of 100 requests per session — protection against infinite loops.
- The whole batch = 1 quota event = 1 push notification.
Use cases
Section titled “Use cases”| Scenario | Why Notifly instead of Sentry |
|---|---|
| Pet project in production | Free, no limits on events, no platform lock-in |
| Static landing page | One line in <head> — no backend required |
| Internal tool | Enough to know “what crashed” — no replay/tracing |
| Vibe-coding with AI | An AI assistant can easily insert the script into any framework (see the LLM instruction in the admin panel) |
| Staging / QA | Push straight to the tester’s phone on failure |
| E-commerce checkout | Find out users see a payment error before they contact support |
| Mobile WebView | The same script works in Capacitor/Cordova/React Native WebView |
| Electron app | In the renderer HTML — without a separate Sentry SDK |
Quick start
Section titled “Quick start”- Open app.notifly.ru/web-scripts.
- Click Create script → type Console Errors.
- Copy the snippet from the “HTML embed” tab.
- Insert into the
<head>of your site before your main bundles. - Done — errors will start arriving as push notifications.
Alternative method: your own backend proxy
Section titled “Alternative method: your own backend proxy”If you need full control (rate limiting, data enrichment, source maps) — use a thin proxy.
Backend proxy
Section titled “Backend proxy”Minimal example in Express (~15 lines):
const express = require('express');const fetch = require('node-fetch');const app = express();app.use(express.json({limit: '20kb'}));
const RATE = new Map(); // ip → timestampconst 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(); // silently ignore spam } 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);Vanilla JS tracker
Section titled “Vanilla JS tracker”Include on any page:
<script>(function () { var ENDPOINT = '/api/error-report'; var lastSent = 0;
function send(payload) { if (Date.now() - lastSent < 5000) return; // no more than once every 5s per client 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>React ErrorBoundary
Section titled “React ErrorBoundary”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; }}Usage:
import {ErrorBoundary} from './ErrorBoundary';
createRoot(document.getElementById('root')).render( <ErrorBoundary> <App /> </ErrorBoundary>);What else you can catch
Section titled “What else you can catch”- Excessively long load times:
PerformanceObserverforlargest-contentful-paint, if LCP > 5s — send a warning. - Broken images:
img.onerror→ batch report. - User “complaints”. A “Report a problem” button in the footer →
sends the URL and
localStorage.userIdto Notifly.
Benefits
Section titled “Benefits”- A cheap “Sentry for your own” — no limits or subscriptions.
- Reality check after every deploy: a couple of errors means you hit an old part of the frontend.
- Context for a user’s complaint — they say “it’s not working”, you open Notifly and see the exact URL and stack trace.
What to improve next
Section titled “What to improve next”- Source maps — upload
.mapfiles viaPOST /web-script/:id/sourcemaps, and stack traces in notifications will automatically be expanded back tosrc/file.ts:LINE:COL(details — in the documentation). Use the providedscripts/upload-sourcemaps.pyin CI. - Stack hash → deduplication on the Notifly side: group identical errors into one message with a counter.
window.NOTIFLY_RELEASE— set a git hash or semver so that after deploy you can immediately see whether the new release broke.
Comparison of approaches
Section titled “Comparison of approaches”| Criterion | WebScript console_errors | Backend proxy |
|---|---|---|
| Integration time | 2 minutes | 15–30 minutes |
| Backend required | No | Yes |
| Sourcemaps | Yes (upload via REST) | Can be added |
| Rate limiting | Built-in (100 req/session) | Custom |
| Suitable for | Pet projects, landing pages, MVP | Production with custom processing |