JavaScript-Error-Tracking für kleine Web-Apps: was du wirklich brauchst

Entwickler-Tools

Für eine kleine Web-App läuft JavaScript-Error-Tracking auf vier Dinge hinaus: jeden nicht abgefangenen Fehler und jede unbehandelte Promise-Rejection fangen, Wiederholungen gruppieren, damit hundert Abstürze als ein Bug erscheinen, minifizierte Stack-Traces in deinen eigenen Code zurückübersetzen und die Console- und Netzwerkaktivität der Minute davor aufbewahren. Dashboards, Alert-Regeln und Performance-Tracing können warten, bis es jemandes Job ist, sie im Blick zu behalten.

Dieser Beitrag geht die vier Punkte durch, die Browser-Details hinter jedem und das Rauschen, das du wegwerfen kannst. Ich habe gerade die Fehlererfassung in BugDump veröffentlicht, deshalb stammen die Beispiele daher, aber die Mechanik ist dieselbe, egal welches Tool du nimmst.

Niemand sagt dir Bescheid

Wenn ein Checkout-Button nichts tut, versuchen es die meisten noch einmal, seufzen und gehen. Ein paar melden sich, meist Tage später, mit einer Beschreibung wie „auf meinem Handy geht's nicht“. Der Fehler war die ganze Zeit da, in einer Console, auf die niemand geschaut hat.

Das ist das Argument für Error-Tracking in einem Satz: Es macht aus den Bugs, die niemand meldet, eine Liste, die du abarbeiten kannst. Für eine kleine App zählt diese Liste mehr als jedes Diagramm, das man darüber legt.

1. Fehler fangen: zwei Events

Browser melden Fehler über zwei Events, und du brauchst beide:

js
window.addEventListener('error', (event) => {
  report(event.error ?? new Error(event.message));
});

window.addEventListener('unhandledrejection', (event) => {
  report(event.reason);
});

error feuert bei einer Exception, die nichts abgefangen hat: ein Tippfehler, ein Property-Zugriff auf undefined, der Aufruf einer Funktion, die es nicht gibt. unhandledrejection feuert bei einem Promise, das fehlgeschlagen ist, ohne dass jemand zuhört. In Code, der überwiegend aus async und await besteht, landen die meisten Fehler bei diesem zweiten Event. Ein fetch, das eine HTML-Fehlerseite bekommt und darauf response.json() aufruft, berührt window.onerror nie.

Drei Dinge liefern dir diese Events nicht:

  • Fehler, die du selbst abfängst. Ein try/catch, das eine freundliche Meldung zeigt, versteckt den Fehler auch vor beiden Events. Wenn du trotzdem davon wissen willst, melde ihn aus dem catch-Block.

  • Die Details von Fehlern aus anderen Ursprüngen. Ein Script von einer anderen Domain, ohne CORS geladen, meldet seine Fehler nur als Script error., ohne Datei, Zeile oder Stack. Der Browser versteckt das mit Absicht. Lade das Script mit crossorigin="anonymous" und liefere es mit einem Access-Control-Allow-Origin-Header aus, oder akzeptiere, dass du nicht hineinsehen kannst.

  • Fehler, bevor dein Tracker geladen ist. Wenn das Tracking-Script zuletzt lädt, sind die Abstürze beim Start verloren. Lade es früh.

Die Listener sind der einfache Teil. Der Rest dieses Beitrags dreht sich darum, was nach report() passiert.

2. Gruppieren: tausend Ereignisse, ein Bug

Ein Fehler, der bei jedem Besuch einer Seite auftritt, kommt tausendfach an. Du willst eine Zeile mit einem Zähler, also entscheidet die Gruppierung, ob die Liste überhaupt brauchbar ist.

Der übliche Ansatz, und der, den ich nutze, baut einen Fingerprint aus drei Teilen:

  • dem Fehlertyp, etwa TypeError;

  • der Meldung ohne ihre variablen Teile: Order 58213 not found und Order 61007 not found sind derselbe Fehler, also werden Zahlen, URLs, E-Mail-Adressen und IDs zu Platzhaltern;

  • den obersten paar Frames deines eigenen Codes, nach Datei und Funktion statt nach Zeile, damit eine unabhängige Änderung weiter oben in der Datei die Gruppe nicht aufspaltet.

Die Seite „Fehler“ des Projekts Acme Store: 29 Ereignisse in den letzten 14 Tagen und drei neue Fehler, jeweils mit Anzahl der Ereignisse, Release und per Source Map aufgelöster Stelle

Das funktioniert gut, aber nicht perfekt. Browser formulieren denselben Fehler unterschiedlich: Chrome sagt Cannot read properties of undefined (reading 'total'), Safari sagt undefined is not an object (evaluating 'cart.summary.total'). Auch Stacks bauen sie unterschiedlich, Firefox fügt einen Frame für die Stelle hinzu, an der ein Callback registriert wurde. Ab und zu kommt ein Bug also als zwei Fehler an, einer pro Browser. Ein Tool sollte dich beide an dieselbe Aufgabe hängen und weitermachen lassen, statt so zu tun, als passiere das nie.

3. Rauschen: was du zuerst wegwirfst

Manche Fehler sind nicht deine, und manche sind eigentlich gar keine Fehler. Verwirf sie, bevor sie in deiner Liste landen, sonst lohnt es sich nicht mehr, die Liste zu lesen.

Was du siehst

Warum es passiert

Was zu tun ist

Script error.

ein Script aus einem anderen Ursprung, vom Browser verborgen

verwerfen, oder crossorigin und CORS-Header ergänzen, um es zu sehen

ein Frame in chrome-extension:// oder moz-extension://

eine Browser-Erweiterung hat Code in deine Seite eingeschleust

verwerfen

ResizeObserver loop completed with undelivered notifications

eine harmlose Layout-Schleife, vor der der Browser warnt

verwerfen

gtag is not defined

ein Adblocker hat ein Analytics-Script entfernt, das dein Code noch aufruft

ignorieren oder den Aufruf absichern

AbortError

ein Request, den dein eigener Code abgebrochen hat

ignorieren

Zwei weitere Regeln halten die Menge im Rahmen, ohne dass etwas verloren geht:

  • Jeden einzelnen Fehler einmal pro Session senden. Ein Absturz in einer Render-Schleife kann in einem Tab hunderte Male pro Sekunde feuern. Der erste sagt dir alles.

  • Die Rate pro Seite begrenzen. Zehn pro Minute sind für einen Tab reichlich. Alles darüber ist eine Schleife, keine neue Information.

Sampling, also nur einen Teil der Fehler zu senden, hilft erst, wenn der Traffic so groß ist, dass 10 % davon immer noch jeden Bug finden. In einer kleinen App versteckt es vor allem den seltenen Fehler, den du am dringendsten hättest sehen müssen. Lass es auf 100 %, bis die monatliche Zahl etwas anderes sagt.

4. Lesbare Stack-Traces, ohne deinen Quellcode zu veröffentlichen

Produktionscode ist minifiziert, ein roher Stack-Trace sieht also so aus:

Plain Text
TypeError: Cannot read properties of undefined (reading 'subtotal')
    at T (https://acme-store.com/assets/app-L5L4OHQM.js:2:1095)
    at x (https://acme-store.com/assets/app-L5L4OHQM.js:2:2081)

Eine Source Map macht daraus wieder renderCartSummary in src/cart/cart-summary.ts, Zeile 6. Der Haken ist, wo die Maps liegen. Neben deinem JavaScript ausgeliefert, funktionieren sie für die DevTools, aber sie veröffentlichen deinen Quellcode auch für alle, die den Netzwerk-Tab öffnen.

Das bessere Muster sind versteckte Source Maps (hidden source maps). Der Bundler schreibt die .map-Dateien, ohne dass dein JavaScript auf sie verweist, deine CI lädt sie zum Error-Tracker hoch, und sie erreichen deinen Server nie.

Der Tracker muss dann für jeden Frame die richtige Map finden. Die Zuordnung über Datei-URL und Release-Namen ist der alte Weg, und sie geht leicht kaputt: Ein CDN ändert die URL, ein Release-Name passt nicht, zwei Builds teilen sich einen Namen. Der zuverlässigere Weg, auf den Sentry umgestiegen ist und den ich auch nutze, ist eine Debug-ID. Ein Schritt nach dem Build berechnet aus dem Inhalt jeder Datei eine ID und schreibt sie in die Datei und in ihre Map. Zur Laufzeit registriert die Datei ihre ID, der Fehler trägt sie mit, und die Map wird allein über die ID gefunden.

In der Praxis sind das drei Befehle:

Bash
vite build --sourcemap hidden
npx @bugdump/cli sourcemaps inject dist
npx @bugdump/cli sourcemaps upload dist --release "$GIT_SHA" --delete-after

Die Seite eines TypeError, der mit der Aufgabe AS-6 verknüpft ist: der Stack-Trace auf src/cart/cart-summary.ts zurückgeführt und die fehlerhafte Zeile im Originalcode hervorgehoben

5. Die Minute vor dem Fehler

Ein Stack-Trace sagt dir, wo der Code aufgegeben hat. Warum, sagt er oft nicht.

Ein Beispiel aus meinem Demo-Shop. Der Fehler ist SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON, geworfen in einem kleinen Helper getJson(). Der Stack zeigt auf JSON.parse, was stimmt und überhaupt nicht hilft. Das Netzwerk-Log desselben Ereignisses erklärt es: GET /api/recommendations kam als 502-Seite vom Load Balancer zurück, und der Code versuchte, das HTML als JSON zu parsen.

Gespeicherte Ereignisse eines SyntaxError aus fünf Besuchen in Chrome und Edge, im Tab „Netzwerk“ der Request GET /api/recommendations, der 502 zurückgab

Deshalb sollte ein Fehlerereignis die letzte Minute an Console-Logs, Netzwerk-Requests und Klicks mitbringen, dieselben Belege, die ein guter Fehlerbericht hat. Ohne sie endet die Hälfte deiner Fehler als „nicht reproduzierbar“.

Die andere Hälfte des Kontexts kommt von den Menschen, die deine App nutzen. Im selben Demo-Shop entstand die Aufgabe AS-1 aus Beschwerden der Kundschaft: „Checkout button does nothing on Safari“. Für sich genommen ist das der klassische Bericht, mit dem niemand etwas anfangen kann. An derselben Aufgabe hängt jetzt der erfasste Fehler, viermal gesehen, nur in Safari und Mobile Safari: window.requestIdleCallback is not a function. Safari kennt kein requestIdleCallback, also wirft der Click-Handler, bevor die Bestellung überhaupt abgeschickt wird. Was die Kundschaft gesehen hat und die Zeile, die es erklärt, stehen auf einer Karte.

Aufgabe AS-1, „Checkout button does nothing on Safari“, mit dem verknüpften Fehler window.requestIdleCallback is not a function, viermal gesehen in Safari und Mobile Safari

Genau das entgeht einem separaten Fehler-Dashboard. Der Bug, den jemand beschreibt, und der Fehler, den der Code wirft, sind oft derselbe Bug, und sie gehören an denselben Ort.

Brauchst du ein vollwertiges Monitoring-Tool?

Nicht immer. Spezialisierte Produkte für Error-Monitoring tun viel mehr, als Browser-Fehler zu fangen, und wenn du brauchst, was sie zusätzlich bieten, nimm eins. Für eine kleine App sieht der ehrliche Vergleich so aus:

Fehlererfassung in deinem Bug-Tracker

Ein spezialisiertes Monitoring-Tool

Was es fängt

JavaScript-Fehler und Rejections im Browser

Browser-, Server- und Mobile-Fehler, dazu Performance

Wohin Fehler gehen

auf dasselbe Board wie die Fehlerberichte, die du bekommst

in einen separaten Posteingang, mit Links zu deinem Tracker

Einrichtung

das Widget, das du schon hast

ein zweites SDK, ein zweites Projekt und eigene Alerts

Alerts

eine Benachrichtigung bei neuen und wiederkehrenden Fehlern

Regeln, Schwellenwerte und On-Call-Routing

Gut geeignet für

kleine Web-Apps, ein kleines Entwicklungsteam

Teams mit On-Call, mehreren Services und SLOs

Wenn deine App aus einem Frontend und einer API besteht und dieselben zwei, drei Leute die Fehlerberichte lesen und die Fehler beheben, heißt ein einziges Board: weniger einzurichten und weniger zu prüfen. Sobald du ein Backend überwachen musst, eine On-Call-Rotation oder ein Performance-Budget hast, verdient sich ein spezialisiertes Tool seinen Platz.

Checkliste für eine kleine App

  1. Höre auf error und unhandledrejection, und lade den Tracker früh.

  2. Melde die Fehler, die du selbst abfängst, wenn sie dir noch wichtig sind.

  3. Setze bei jedem Build ein release, damit du einen neuen Bug von einem alten Tab unterscheiden kannst.

  4. Baue mit versteckten Source Maps und lade sie aus der CI hoch. Deploye die Maps nie.

  5. Verwirf Fehler aus Erweiterungen, Script error. und harmlose Browser-Warnungen direkt an der Quelle.

  6. Sende jeden Fehler einmal pro Session und lass Sampling aus, bis die Menge es verlangt.

  7. Mach aus Fehlern Aufgaben neben deinen Fehlerberichten, nicht einen zweiten Posteingang, den niemand öffnet.

FAQ

Macht Error-Tracking meine Seite langsamer? Nicht messbar. Die beiden Listener tun nichts, bis etwas fehlschlägt, und ein vernünftiger Tracker sendet jeden einzelnen Fehler einmal pro Session, im Hintergrund.

Was bedeutet „Script error.“? In einem Script aus einem anderen Ursprung ist ein Fehler passiert, und der Browser hat die Details verborgen. Setz crossorigin="anonymous" an diesen Script-Tag und lass seinen Server Access-Control-Allow-Origin senden, oder verwirf diese Fehler: Ohne Details gibt es nichts, womit du arbeiten kannst.

Ist es sicher, Source Maps hochzuladen? Sie zu deinem Error-Tracker hochzuladen ist in Ordnung. Offen liegt dein Quellcode erst, wenn du sie öffentlich auslieferst. Baue versteckte Maps, lade sie hoch und lösche sie vor dem Deploy.

Wie viele Fehler sendet eine kleine App? Das hängt viel stärker davon ab, wie viele verschiedene Bugs du hast, als vom Traffic. Wenn jeder Fehler einmal pro Session gesendet wird, sendet eine Seite mit einem Bug ein Ereignis pro betroffenem Besuch, nicht eins pro Absturz.

So probierst du es in BugDump aus

Die Fehlererfassung gehört zum BugDump-Widget, in jedem Plan, und ist standardmäßig eingeschaltet. Der kostenlose Plan enthält 1.000 Fehlerereignisse pro Monat und Projekt. Fang mit JavaScript-Fehler automatisch erfassen an, danach kommen Lesbare Stack-Traces: Source Maps hochladen und Mit erfassten Fehlern arbeiten.

Mehr zum Thema