Ik bouwde client-side foutregistratie omdat ik problemen van echte gebruikers wilde zien die ik lokaal niet betrouwbaar kon reproduceren. Het meldsysteem deed precies wat ik vroeg: fouten opvangen en naar mij sturen.
Het probleem was dat bijna alles even belangrijk leek.
Een externe analytics-script laadt niet? Rode melding. Een advertentiescript wordt geblokkeerd? Rode melding. Een crawler kan Google Analytics niet laden? Rode melding. Een videovoorbeeld roept play() aan en wordt gepauzeerd voordat de Promise klaar is? Rode melding. Er komt een ondoorzichtige Script error. zonder bruikbare bron of stack? Ook rood.
In dezelfde stroom zaten wél gebeurtenissen die de applicatie echt konden breken: een eigen URL die per ongeluk https://example.comhttps://example.com/... was geworden en een eigen Next.js-bestand onder /_next/static/chunks/... dat een browser niet kon laden.
De verzameling werkte. De monitoring niet.
Dat verschil veranderde mijn kijk op frontend-observability. Een foutgebeurtenis is bewijs dat er iets is gebeurd. Het is nog geen diagnose, prioriteit of incident.
De eerste fout was dat “error” automatisch “urgent” betekende
Mijn vroege model zag er ongeveer zo uit:
browser meldt een fout
↓
stuur CLIENT ERROR
↓
ontwikkelaar moet reageren
Daarmee gooide ik te veel vragen op één hoop. Is het mijn code? Is de actieve pagina stuk? Was het een verwachte annulering? Had de browser genoeg informatie om de bron te identificeren? Herstelde de applicatie zichzelf? Zijn tien berichten tien incidenten of tien symptomen van één probleem?
Zolang die vragen onbeantwoord zijn, hoort een event niet automatisch een alert te worden.
In een vroege analyse keek ik naar ongeveer achttien meldingen. Het grootste deel was externe- of lifecycle-ruis. Twee gevallen sprongen eruit: de fout opgebouwde eigen URL was aantoonbaar verkeerd en de mislukte eigen JavaScript-chunk kon noodzakelijke applicatiecode onbeschikbaar hebben gemaakt. Toch kregen ze bijna dezelfde visuele urgentie als geblokkeerde advertenties.
Toen begreep ik dat “alle browserfouten verzamelen” en “productiemonitoring bouwen” twee verschillende problemen zijn. De verzamellaag bewaart bewijs. Het monitoringsysteem moet dat bewijs omzetten in beslissingen.
De browser heeft niet één universeel foutkanaal
Client-side fouten komen niet met dezelfde betekenis binnen.
Het error-event op window wordt onder andere gebruikt voor synchrone scriptfouten en speelt ook een rol bij resourcefouten. Een afgewezen Promise zonder handler volgt een ander pad via unhandledrejection. Elementen die scripts, afbeeldingen of media laden kunnen hun eigen error afvuren. React en Next.js voegen daar framework error boundaries aan toe.
window.error
→ er kan een synchrone scriptfout zijn ontsnapt
unhandledrejection
→ een afgewezen Promise was op dat moment niet afgehandeld
element error
→ een resource kon niet worden geladen of gebruikt
framework boundary
→ rendering of uitvoering bereikte een foutgrens
Een globale hook ziet een symptoom aan een systeemgrens. Hij kent niet noodzakelijk de volledige oorzaak.
Daarom normaliseer ik niet langer elke gebeurtenis direct naar één generieke Error met dezelfde ernst.
Eigenaarschap is de eerste nuttige filter
De eerste vraag die echt ruis wegneemt is: van wie is de code of resource die faalt?
Een fout in /_next/static/chunks/app/... is iets anders dan een advertentie-SDK op een andere origin. Een verkeerd door mijn eigen code samengestelde URL is niet hetzelfde als een geblokkeerde analytics-request. Een browserextensie is weer een andere categorie.
- eigen applicatie: JavaScript, CSS, API-calls, media en gegenereerde URLs;
- framework/runtime: Next.js of React als onderdeel van het uitvoeringspad;
- externe integraties: analytics, advertenties, widgets en SDKs;
- omgeving: extensies, crawlers, netwerkstatus, privacytools en browserafwijkingen.
Extern betekent niet onbelangrijk. Een betalings- of authenticatieprovider kan bedrijfskritiek zijn. Een advertentiestoring kan economische gevolgen hebben. Maar integratiegezondheid is niet automatisch een applicatiecrash.
Als alles naar hetzelfde urgente kanaal gaat, verliest dat kanaal zijn betekenis.
Twee eigen fouten leerden mij wat “handelbare” betekent
De verkeerde URL was eenvoudig:
https://example.comhttps://example.com/resource
Daar waren geen theorieën over AdBlock, VPN of browserbeleid voor nodig. De URL zelf was ongeldig. Ergens plakte mijn code een origin voor een waarde die al absoluut was.
Het event was handelbare omdat het bewijs specifiek was, de resource van mij was en het probleem naar code wees die ik beheer.
De Next.js-chunk was anders:
Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js
Ook eigen en potentieel pagina-brekend, maar het event bewees de oorzaak niet. Een oude client kon een asset van een eerdere deployment opvragen. Een request kon timeouten. Reverse proxy of CDN konden falen. De verbinding kon wegvallen. Het bestand kon werkelijk ontbreken.
De correcte conclusie was dus niet “oorzaak gevonden”, maar “hoge prioriteit, meer context nodig”.
De ernst kan hoog zijn terwijl de zekerheid over de oorzaak laag blijft.
Script error. is een aanwijzing, geen stack trace
Error: Script error.
filename: unknown
line: 0
column: 0
Het klinkt ernstig en bevat bijna niets bruikbaars.
Browsers beperken details van cross-origin-scriptfouten bewust. MDN legt uit dat window.onerror zonder passende CORS-configuratie beperkte informatie krijgt; het crossorigin-gedrag van <script> bepaalt mede hoeveel informatie beschikbaar is.
Daarom vertaal ik één ondoorzichtige Script error. niet automatisch naar “mijn applicatie is gecrasht”. Het kan eigen code, externe code, geïnjecteerde code of een cross-origin-fout zijn waarvan de browser details niet mag tonen.
Ik bewaar het als telemetrie, correleer het met pagina, build, browser en nabije events, maar één 0:0-melding is geen reden voor een urgente alert. Een cluster rond één release of route verandert de prioriteit.
Onbekend betekent niet onschuldig. Het betekent ook niet kritiek.
Een AbortError kan echt én verwacht zijn
Videopreviews maakten dit heel duidelijk:
AbortError:
The play() request was interrupted by a call to pause()
HTMLMediaElement.play() retourneert een Promise die kan worden afgewezen. Media-lifecyclehandelingen kunnen pending playback bewust afbreken; MDN documenteert ook dat load() openstaande play-Promises met AbortError afbreekt.
In een preview-grid kan dit gebeuren zonder zichtbare productfout: element verschijnt, play() start, gebruiker scrollt, element verdwijnt uit beeld en de code pauzeert of vervangt de media voordat starten klaar is.
De Promise-rejection is echt. Een gebruikersincident hoeft niet te bestaan.
De juiste afhandeling hoort dicht bij de call: verwachte annulering onderscheiden van een echte playbackfout. unhandledrejection is een vangnet, niet de eerste plek waar normaal componentgedrag wordt geïnterpreteerd.
Fouten van externe diensten hebben een eigen gezondheidsmodel nodig
Mijn vroege logs bevatten veel fouten van analytics- en advertentiedomeinen. Sommige kwamen uit privacygerichte browsers, andere van crawlers. Een crawler die Google Analytics niet kon laden was een bijzonder slecht “urgent incident”.
Dat bewijst dat één netwerkrequest faalde. Het zegt vrijwel niets over of een mens de applicatie kon gebruiken.
Het opslaan van zo’n event was niet de fout. Het samenvoegen met een mislukte eigen JavaScript-chunk was de fout.
- Is de applicatie voor de gebruiker stuk?
- Is een externe integratie gezond?
Geblokkeerde advertenties kunnen naar ad-delivery-metrics. Analytics-fouten kunnen analytics-dekking meten. Geen van beide hoeft als “frontend crash” te alarmeren wanneer kernfunctionaliteit niet afhankelijk is van die resource.
Gescheiden worden externe-problemen bovendien beter zichtbaar per provider, browser en regio.
navigator.onLine geeft context, geen bewijs van bereikbaarheid
Ik begon ook te loggen of de browser zichzelf online vond. Dat is nuttig zolang het slechts een hint blijft.
Ik had netwerkgerelateerde events met:
Online: true
Dat is geen tegenspraak. MDN waarschuwt dat navigator.onLine gebaseerd is op heuristieken van browser en besturingssysteem. Een machine kan een lokaal netwerk zien en toch mijn origin niet bereiken. VPN, firewall, DNS en gedeeltelijke storingen maken het nog minder absoluut.
online === false
→ sterke aanwijzing dat de omgeving meespeelt
online === true
→ bewijst NIET dat origin of resource bereikbaar waren
Die nuance voorkomt dat monitoring een hint als diagnose presenteert.
Eén onderliggende storing kan meerdere browsergebeurtenissen opleveren
Na betere verzameling verscheen een nieuw probleem: één incident kon meerdere meldingen produceren.
Een chunk kan eerst resource.error geven, daarna ChunkLoadError, vervolgens een React/Next.js error boundary en ten slotte herstel die een reload plant. Wanneer elke laag apart alert, lijkt één gebruikersactie op meerdere storingen.
Vijf berichten voelen als vijf getroffen gebruikers, terwijl ze uit één sessie en één resource kunnen komen.
Tekst dedupliceren is onvoldoende. Ik wil incidentcorrelatie:
sessie
+ kort tijdvenster
+ genormaliseerde foutklasse
+ eigen resource
+ client build
+ route
Ruwe events blijven bestaan, maar de mens krijgt het rijkste incident. Als een framework boundary al een eigen stack en exacte chunk-URL bevat, hoeft een eerdere generieke resourcefout geen tweede urgente alert te zijn.
Alert op incidenten. Bewaar events.
De velden rond de fout werden waardevoller dan de fouttekst
Latere telemetrie werd veel rijker:
clientBuildId
resource URL
resourceResponseStatus
resourceTransferSize
resourceDurationMs
serviceWorkerVersion
serviceWorkerController
serviceWorkerState
chunkRecoveryScheduled
online
stack / component stack
Voor media verzamelde ik ook media-errorcode, HTTP-status als die onafhankelijk observeerbaar was, de werkelijk ontvangen Content-Type en een indeling als HTTP- of netwerkprobleem.
Daarmee kan ik vragen beantwoorden die één exception string niet kan beantwoorden: begon het op één build? Kwam er een HTTP-response? Bestuurde een Service Worker de pagina? Was herstel al gestart? Verwijzen meerdere events naar dezelfde resource? Was de actieve route echt defect?
De Resource Timing API kan timing, transferinformatie en waar ondersteund en toegestaan response status leveren. Er zijn beperkingen: cross-origin-data zijn beperkt, cached resources kunnen transferSize: 0 hebben en responseStatus is niet overal beschikbaar. Daarom blijven null en 0 eerlijke toestanden in plaats van aanleiding om iets te raden.
ChunkLoadError is een symptoom, geen 404-detector
Eén later incident veranderde mijn interpretatie sterk.
De browser rapporteerde een Next.js ChunkLoadError voor een layout-chunk, terwijl de verrijkte telemetrie ook bevatte:
resourceResponseStatus: 200
resourceDurationMs: 170523
serviceWorkerState: activated
chunkRecoveryScheduled: true
De vastgelegde meting was ongeveer 170 seconden. Wat de precieze hoofdoorzaak ook was, dit was genoeg om een simpele regel af te wijzen:
ChunkLoadError === server gaf 404
Andere chunkfouten hadden geen observeerbare status. Sommige waren timeouts. Sommige hadden transferdata. De error class was dezelfde, het bewijs niet.
Dat is belangrijk bij Next.js, omdat bestanden onder /_next/static/ normaal content-hashed en immutable zijn. De huidige Next.js-documentatie voor self-hosting beschrijft lange cache headers voor zulke assets. Een chunkfout kan dus samenhangen met deployment skew, oude clients, netwerk, reverse proxy, CDN, cache, Service Worker of werkelijk ontbrekende artefacts.
De alertlaag moet de oorzaak niet verzinnen. Hij moet bewijs bewaren waarmee de oorzaak onderzocht kan worden.
Mediafouten leerden hetzelfde vanuit een andere laag
Een media-element rapporteerde in sommige gevallen:
MEDIA_ELEMENT_ERROR: Format error
Dat klinkt als een codecprobleem.
Aanvullende delivery-verificatie liet voor sommige events echter zien:
HTTP status: 410
Content-Type: text/html; charset=utf-8
failure kind: http
De browser vroeg om video en kreeg HTML met een HTTP-fout terug. Het media-element kon HTML niet als video decoderen en rapporteerde daarom een “format error”. De bruikbare diagnose lag bij de levering.
De laag die het symptoom ziet hoeft niet de laag te zijn die het probleem veroorzaakt.
“Codecprobleem”, “netwerkstoring”, “cachebug” en “ontbrekende chunk” zijn conclusies. Telemetrie hoort te beginnen met observaties.
Ik beoordeel browserfouten langs vijf dimensies
1. Eigenaarschap
Eigen applicatie, framework/runtime, externe integratie of omgeving?
2. Gebruikersimpact
Brak de actieve route, rendering, authenticatie, chat, checkout of een andere kernflow? Of faalde alleen een optionele advertentie, analytics-call, preload of preview?
3. Bewijskwaliteit
Heb ik een eigen stack, resource-URL, HTTP-status, build ID en component stack? Of alleen Script error. op 0:0?
4. Herhaling en spreiding
Eén sessie, of dezelfde signature bij verschillende gebruikers, routes en browsers na dezelfde release?
5. Herstel
Herstelde de applicatie? Werd een chunk-reload gepland? Werkte fallback? Is de gebruiker nog geblokkeerd?
duidelijk first-party
+ hoge gebruikersimpact
+ sterk bewijs
+ meerdere sessies
+ geen herstel
= urgent incident
third-party
+ optionele functie
+ zwak bewijs
+ geïsoleerd
+ gebruiker niet geraakt
= metric of lage prioriteit
Onderdrukking moet conservatief blijven
Wanneer ruis irritant wordt, is een enorme regex-blacklist verleidelijk. Dat kan echte problemen verbergen.
Elke AbortError wegfilteren kan echte afgebroken API-requests verbergen. Elk Script error. verwijderen kan een browserspecifiek cluster missen. Alle externe fouten negeren kan een kapotte betalings-, authenticatie- of toestemmingsprovider verbergen.
ALERT
→ sterk incident dat actie vereist
RETAIN / AGGREGATE
→ bewaren en tellen; pas alerten bij een cluster
METRIC / SAMPLE
→ verwachte of lage-impactruis; trends en voorbeelden bewaren
Zo wordt het systeem stiller zonder blind te worden.
Een classifier is vooral beleid in code
Onderstaande code komt niet uit mijn productiecode. Het is een compacte weergave van het beleid waarmee ik liever was begonnen:
function classifyClientEvent(event) {
const owner = classifyOwner(event);
if (isExpectedMediaCancellation(event)) {
return { severity: "metric", reason: "expected-cancellation" };
}
if (owner === "first-party" && breaksActiveRoute(event)) {
return { severity: "alert", reason: "first-party-user-impact" };
}
if (isActiveFirstPartyChunkFailure(event)) {
return { severity: "alert", reason: "application-chunk" };
}
if (owner === "third-party") {
return { severity: "aggregate", reason: "integration-health" };
}
if (isOpaqueScriptError(event)) {
return { severity: "aggregate", reason: "insufficient-evidence" };
}
return { severity: "aggregate", reason: "needs-correlation" };
}
De echte moeilijkheid zit in functies zoals breaksActiveRoute(). Daarvoor zijn routecontext, resource-eigenaarschap, boundary-data en soms productkennis nodig.
Een fingerprint moet het incident volgen, niet de tekst
Letterlijke berichtgelijkheid dedupliceert slecht. Minified offsets, chunk-hashes, dynamische IDs en browsertekst veranderen.
{
errorClass,
normalizedFirstPartyResource,
routeFamily,
clientBuild,
sessionId,
shortTimeBucket
}
Voor een globale crash kan het eerste eigen stackframe belangrijk zijn, voor een chunk de genormaliseerde resource, voor media de delivery-klasse.
resource.error
→ ChunkLoadError
→ framework boundary
→ recovery scheduled
wordt dan één incident met vier observaties in plaats van vier noodmeldingen.
Het urgente kanaal moet een veel smallere taak hebben
Directe alerts reserveer ik voor eigen runtimefouten die de route breken, relevante React/Next.js-boundaries, noodzakelijk eigen JavaScript/CSS, herhaalde ChunkLoadError over sessies, blokkerende core-API-fouten en duidelijke eigen invariants zoals ongeldige gegenereerde URLs.
Een losse Script error., een eigen resourcefout met succesvol herstel, nog onbegrepen media-errors en browserspecifieke anomalieën zou ik bewaren en aggregeren.
Bekende advertentie- of analyticsfouten, verwachte AbortError-annuleringen, externe ruis die alleen bij crawlers voorkomt, sterke offline-signalen en optionele speculatieve resources gaan meestal naar metrics of samples.
Een urgente alert moet gebruikersimpact weergeven waarop je kunt handelen, niet het ruwe volume browserklachten.
Ik meet liever incidenten dan één globaal “aantal fouten”
- incidenten uit de eigen applicatie per 1.000 sessies;
- getroffen sessies per build ID;
- error-boundaries per route;
- chunkfouten per resource en deployment;
- foutpercentage per externe provider;
- volume van verwachte annuleringen om plotselinge wijzigingen te zien;
- succespercentage van herstel;
- unieke incidenten tegenover ruwe eventaantallen.
“Er gebeurde één event” is zelden een bruikbare productiedrempel. “Hetzelfde incident in de eigen applicatie raakt meerdere onafhankelijke sessies op de nieuwe build en herstel faalt” is veel relevanter.
Clientmonitoring bewijst de hoofdoorzaak nog steeds niet
Browsertelemetrie kent harde grenzen.
Een ontbrekende HTTP-status kan komen door ontbrekende browserondersteuning, cross-origin-beperkingen, annulering of een ander observability-gat. Een actieve Service Worker bewijst niet dat hij een stale resource leverde. ChunkLoadError na een deployment bewijst geen deployment skew. online: true bewijst geen bereikbaarheid.
Clienttelemetrie verkleint de set hypotheses. Server- en proxylogs, deploymentmanifesten, cachestatus en reproductie kunnen nog steeds nodig zijn.
Observability hoort ook geen onbeperkte gebruikersdataverzameling te worden. Elk veld moet een diagnostisch doel hebben.
Betere telemetrie is niet per se méér telemetrie, maar telemetrie die verschillende soorten fouten beter van elkaar onderscheidt.
Mijn regel nu: verzamel events, onderzoek incidenten, alert op impact
Aanvankelijk wilde mijn meldsysteem alleen beantwoorden: “ging er iets mis?” In productie is die vraag te breed. Er faalt altijd ergens een crawler, privacyfilter, media-Promise, netwerk of externe SDK.
Is het van ons?
Verloor de gebruiker functionaliteit?
Hoe sterk is het bewijs?
Herhaalt het zich?
Herstelde de app?
Zijn dit meerdere events of één incident?
Toen ik monitoring rond die vragen bouwde, veranderde de rode berichtenstroom in een engineeringinstrument.
Een browserfout is een observatie. Een incident is een gecorreleerde verklaring van gebruikersimpact. Een alert is de beslissing dat een mens moet handelen.
Die drie wil ik niet meer als synoniemen behandelen.