feat(web): Herkunft der Fehlermeldung — Cookie tessera_desktop_client und Client-Felder in der Nutzlast

Die Middleware liest jetzt zusaetzlich dv/dc/dos aus der Anfrage und legt
daraus das Cookie tessera_desktop_client an (bereinigt per Muster, nur
wenn alle drei Werte gueltig sind); desktop-client.ts liest es zurueck.
Der Fehler-melden-Dialog fuellt daraus vier neue Nutzlastfelder
(clientKind/clientOs/clientVersion/clientCommit), damit die API die
Herkunft der Meldung ausweisen kann. Ohne das zweite Cookie (alter
Client) bleibt es bei "Desktop-App (unbekannt)".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
This commit is contained in:
2026-09-18 12:36:19 +02:00
parent b03cb211b3
commit f2457113a2
8 changed files with 376 additions and 10 deletions
+87
View File
@@ -0,0 +1,87 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { sendBugReport } from './bug-report-api';
/**
* bug-report-api.test — NEU (quick-260918-gza). Bisher pruefte nur der
* Komponententest (`bug-report-button.test.tsx`) die FormData von
* `sendBugReport` — die reinen Nutzlastfelder (insbesondere die vier neuen
* Herkunftsfelder) gehoeren aber an die Funktion selbst, unabhaengig vom
* Dialog. Drei Tests: Desktop-Nutzlast, Browser-Nutzlast, Netzwerk-/
* HTTP-Fehler.
*/
const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() }));
const basePayload = {
description: 'x',
page: '/a',
webVersion: 'v1',
webChannel: 'beta',
webCommit: 'c',
userAgent: 'UA',
viewport: '1x1',
clientTime: 't',
errors: ['e1', 'e2'],
screenshot: null,
};
beforeEach(() => {
mockFetch.mockReset();
mockFetch.mockResolvedValue(new Response('{}', { status: 200 }));
vi.stubGlobal('fetch', mockFetch);
});
afterEach(() => {
vi.unstubAllGlobals();
});
describe('sendBugReport (quick-260918-gza)', () => {
it('Test 1: Desktop-Nutzlast — clientKind/clientOs/clientVersion/clientCommit im FormData, errors mehrfach, kein screenshot-Feld', async () => {
const result = await sendBugReport({
...basePayload,
clientKind: 'desktop',
clientOs: 'windows',
clientVersion: '1.2.0',
clientCommit: 'a6d1a64',
});
expect(result).toEqual({ ok: true });
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
expect(String(url).endsWith('/bug-reports')).toBe(true);
expect(init.credentials).toBe('include');
const body = init.body as FormData;
expect(body.get('clientKind')).toBe('desktop');
expect(body.get('clientOs')).toBe('windows');
expect(body.get('clientVersion')).toBe('1.2.0');
expect(body.get('clientCommit')).toBe('a6d1a64');
expect(body.getAll('errors')).toEqual(['e1', 'e2']);
expect(body.has('screenshot')).toBe(false);
});
it('Test 2: Browser-Nutzlast — die drei uebrigen Client-Felder sind vorhanden, aber leer (nicht weggelassen)', async () => {
await sendBugReport({
...basePayload,
clientKind: 'browser',
clientOs: '',
clientVersion: '',
clientCommit: '',
});
const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData;
expect(body.get('clientKind')).toBe('browser');
expect(body.get('clientOs')).toBe('');
expect(body.get('clientVersion')).toBe('');
expect(body.get('clientCommit')).toBe('');
});
it('Test 3: Netzwerkfehler -> { ok: false, status: 0 }; HTTP-Fehler -> { ok: false, status }', async () => {
mockFetch.mockRejectedValueOnce(new Error('offline'));
await expect(
sendBugReport({ ...basePayload, clientKind: 'browser', clientOs: '', clientVersion: '', clientCommit: '' }),
).resolves.toEqual({ ok: false, status: 0 });
mockFetch.mockResolvedValueOnce(new Response('', { status: 429 }));
await expect(
sendBugReport({ ...basePayload, clientKind: 'browser', clientOs: '', clientVersion: '', clientCommit: '' }),
).resolves.toEqual({ ok: false, status: 429 });
});
});