Skip to content

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.

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.
ScenarioWhy Notifly instead of Sentry
Pet project in productionFree, no limits on events, no platform lock-in
Static landing pageOne line in <head> — no backend required
Internal toolEnough to know “what crashed” — no replay/tracing
Vibe-coding with AIAn AI assistant can easily insert the script into any framework (see the LLM instruction in the admin panel)
Staging / QAPush straight to the tester’s phone on failure
E-commerce checkoutFind out users see a payment error before they contact support
Mobile WebViewThe same script works in Capacitor/Cordova/React Native WebView
Electron appIn the renderer HTML — without a separate Sentry SDK
  1. Open app.notifly.ru/web-scripts.
  2. Click Create script → type Console Errors.
  3. Copy the snippet from the “HTML embed” tab.
  4. Insert into the <head> of your site before your main bundles.
  5. 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.

Minimal example in Express (~15 lines):

server.js
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.use(express.json({limit: '20kb'}));
const RATE = new Map(); // ip → timestamp
const 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);

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>
ErrorBoundary.jsx
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>
);
  • Excessively long load times: PerformanceObserver for largest-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.userId to Notifly.
  • 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.
  • Source maps — upload .map files via POST /web-script/:id/sourcemaps, and stack traces in notifications will automatically be expanded back to src/file.ts:LINE:COL (details — in the documentation). Use the provided scripts/upload-sourcemaps.py in 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.
CriterionWebScript console_errorsBackend proxy
Integration time2 minutes15–30 minutes
Backend requiredNoYes
SourcemapsYes (upload via REST)Can be added
Rate limitingBuilt-in (100 req/session)Custom
Suitable forPet projects, landing pages, MVPProduction with custom processing