Fehler passieren immer. Vor allem durch das komplexe Zusammenspiel von Komponenten. Man kann vorab viel an einer Software testen, aber es ist eine Illusion, dass Programme fehlerfrei seien. Daher ist es wichtig, diese Fehler, wenn sie auftreten, zu protokollieren und dem Entwickler in geeigneter Weise zugänglich zu machen.

Bei Webseiten ist dies nicht viel anders. Bei mir ist jede Seite eine eigene Datei mit dem Kern der Seite und der Rumpf liegt in einer anderen Datei. Bei der Veröffentlichung werden alle Teile zusammengefügt und als eine HTML-Seite ausgegeben; mit Javascript-Dateien verhält es sich ähnlich. Zwar sollten die Daten gut gekapselt sein, aber schon durch HTML ist dies nicht leicht möglich, da gewisse Einträge nur im <head>-Bereich stehen dürfen, der Kern aber im <body> eingefügt wird. Strikte Separation ist wünschens- und erstrebenswert, aber auch eine Illusion wie die Fehlerfreiheit. Damit steigt leider auch die Wahrscheinlichkeit für Fehler, weil an einer Stelle etwas verändert wird, das zu einer anderen Stelle nicht passt.

Andere Probleme sind fehlerhafte Verweise auf Bilder oder eben gar Programmierfehler im Javascript, die sich erst unter bestimmten Bedingungen zeigen. Gründe gibt es also genug, weshalb Fehler auftreten können und daher habe ich in fast alle größeren Webprojekte, die ich angefasst habe, eine Fehlerprotokollierung in folgender Form eingebaut:

<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="utf-8" />
<script>
!function() {
    var dnt = window.navigator.doNotTrack;
    if (window.location.protocol === 'file:' || (dnt != null && dnt !== 'unspecified'))
        return;

    window.addEventListener('error', function(ev) {
        var data = typeof ev === 'object' ? {
            target: ev.target && {
                type: '' + ev.target,
                baseURI: ev.target.baseURI,
                html: ev.target.outerHTML,
                tag: ev.target.nodeName,
            },
            error: ev.error && {
                columnNumber: ev.error.columnNumber,
                fileName: ev.error.fileName,
                lineNumber: ev.error.lineNumber,
                message: ev.error.message,
                stack: ev.error.stack,
            } || /* IE 11 (Win 10) */ {
                columnNumber: ev.colno,
                fileName: ev.filename,
                lineNumber: ev.lineno,
                message: ev.message,
            },
        } : { arguments: arguments };

        data.level = 'error';
        data.location = '' + window.location;
        data.userAgent = '' + window.navigator.userAgent;

        var xhr = new XMLHttpRequest();
        xhr.open("POST", "api/client-error", true);
        xhr.setRequestHeader("Content-type", "application/json");
        xhr.send(JSON.stringify(data));
    }, {
        capture: true,
        passive: true,
    });
}();
</script>
  <title>Hauptseite • Am Interneteingang 8</title>

Diese Funktion binde ich so früh wie möglich an das Ereignis error und versuche im Fall eines Fehlers so viele Informationen wie möglich an den Server zu senden, um im Nachhinein den Fehler reproduzieren und beheben zu können. Dies funktioniert natürlich nur, wenn Javascript auch unterstützt wird. Wenn der Code kleiner ist, kann man ihn auch direkt vor <html> platzieren, aber mit der obigen Größe würde <meta charset="utf-8" /> erst nach 1024 Bytes kommen, was nicht erlaubt ist.

Empfänger auf Serverseite

Bisher hatte ich bei den Projekten auf dem Server immer einen Dienst laufen, der die Daten entgegennehmen konnte. Bei meiner statischen Webseite habe ich jedoch keinen solchen Server. Aber ich habe einen kreative Trick entdeckt, mit der Nginx die Daten in eine Datei schreibt:

log_format postdata escape=json '{"time":"$time_iso8601","referer":"$http_referer","userAgent":"$http_user_agent","body":"$request_body"}';

server {
    …
    location = /api/client-error {
        if ($request_method = POST) {
                access_log /var/log/nginx/client-errors.log postdata;
        }
        echo_status 204;
        echo_read_request_body;
    }

Ich definiere erst ein spezielles Logformat, das einem JSON-Objekt mit den Informationen entspricht und dann filtere ich für den Pfad auf POST-Anfragen und definiere den access_log entsprechend für eine eigene Datei. Über die Einstellung client_max_body_size ließe sich noch steuern, die groß die Meldungen seien dürfen. Von Haus aus liegt die Begrenzung bei 1 Megabyte.

Mit dem Nginx-Modul http_echo (im Debian-Paket libnginx-mod-http-echo) gibt es zusätzlich die Anweisungen echo_status, um den Rückgabewert auf 204 zu setzen und dem Client zu signalisieren, dass die Daten angekommen sind, und echo_read_request_body, um die kompletten Daten vom Client zu lesen, da sonst Nginx nach den Kopfzeilen abbricht.

Somit entsteht eine Datei in der zeilenweise die Meldungen stehen. Auslesen kann man sie dann mit jq:

jq -C '.body = (.body |fromjson)' $L/nginx/client-errors.log

Moralische Bedenken

An dieser Stelle möchte ich aber nicht meine Bedenken verschweigen, denn da der Fehler im Browser beim Benutzer passiert, hat es immer einen fahlen Beigeschmack, wenn der Browser selbständig »nach Hause telefoniert«. Das automatische Übermitteln von Fehlerberichten hat schon bei Windows und Ubuntu zu heftigen Diskussionen geführt. Dort zwar immer auch unter dem Aspekt, dass die Programme komplett auf dem Gerät des Benutzers liefen und es keine Server-Client-Interaktion gab, aber es ist schwierig die Grenze zwischen privaten Daten und Diagnosedaten zu ziehen. Fakt ist: Es passieren sehr viele Fehler während des Betriebs und nur sehr wenige Benutzer machen sich den Aufwand und melden ihre Beobachtungen (geschweige denn, dass sie eine qualifizierte Beschreibung abgeben).

Als Entwickler sage ich, diese Daten sind sehr hilfreich, um das Produkt zu verbessern, und Benutzern, denen die Verbesserung helfen würde, geben diese Informationen nicht weiter – zumal es auch manuell zu aufwendig und nervig ist. Deshalb ist die automatische Übermittlung ideal. Aber im Sinne des Datenschutzes ist diese Übermittlung schon bedenklich, da sie eben auch immer verrät, wo der Nutzer sich gerade befindet – gut, das weiß man auf dem Server durch den Seitenabruf eh.

Daher habe ich so als Kompromiss gewählt, dass die Fehlermeldung nicht aktiviert wird, wenn der Benutzer Do-Not-Track aktiviert hat.