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
@@ -8,14 +8,19 @@ import { computeCaptureSize } from '@/lib/bug-report-api';
/**
* bug-report-button.test — NEU (quick-260914-m97, Fehler-melden-Knopf).
*
* Elf Tests. Die Kernanforderung (Test 1): das Bild wird VOR dem Dialog
* aufgenommen — der `toPng`-Mock prueft waehrend seines Aufrufs, dass noch
* KEIN `role="dialog"` im DOM steht. `html-to-image` selbst laeuft in jsdom
* nicht (gemessen zur Planungszeit: `HTMLVideoElement is not defined`, kein
* Canvas-Backend) — deshalb der Mock; der Bildbeweis kommt aus dem Browser.
* Dreizehn Tests. Die Kernanforderung (Test 1): das Bild wird VOR dem
* Dialog aufgenommen — der `toPng`-Mock prueft waehrend seines Aufrufs,
* dass noch KEIN `role="dialog"` im DOM steht. `html-to-image` selbst
* laeuft in jsdom nicht (gemessen zur Planungszeit:
* `HTMLVideoElement is not defined`, kein Canvas-Backend) — deshalb der
* Mock; der Bildbeweis kommt aus dem Browser.
*
* Der next-intl-Mock liest die Texte aus der ECHTEN `de.json` (Muster
* `tessera-logo.test.tsx`), Erwartungen zitieren `de.bugReport.<key>`.
*
* Test 12/13 (quick-260918-gza): die vier Herkunftsfelder in der Nutzlast,
* abhaengig vom Cookie `tessera_desktop_client` — jsdom hat ohne gesetztes
* Cookie keinen Desktop-Client, darum bleibt Test 2 beim Browser-Fall.
*/
const { mockToPng, mockFetch, mockUser } = vi.hoisted(() => ({
mockToPng: vi.fn(),
@@ -86,9 +91,15 @@ beforeEach(() => {
vi.stubGlobal('fetch', mockFetch);
});
function clearDesktopCookies() {
document.cookie = 'tessera_desktop=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
document.cookie = 'tessera_desktop_client=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
}
afterEach(() => {
cleanup();
clearErrorBuffer();
clearDesktopCookies();
vi.unstubAllGlobals();
});
@@ -142,6 +153,10 @@ describe('BugReportButton (quick-260914-m97)', () => {
expect(body.get('userAgent')).toBeTruthy();
expect(body.get('viewport')).toMatch(/^\d+x\d+$/);
expect(body.get('clientTime')).toMatch(/^\d{4}-\d{2}-\d{2}T/);
expect(body.get('clientKind')).toBe('browser');
expect(body.get('clientOs')).toBe('');
expect(body.get('clientVersion')).toBe('');
expect(body.get('clientCommit')).toBe('');
expect(body.getAll('errors').some((e) => String(e).includes('GET /modules -> 500'))).toBe(true);
const shot = body.get('screenshot');
expect(shot).toBeInstanceOf(Blob);
@@ -271,6 +286,41 @@ describe('BugReportButton (quick-260914-m97)', () => {
expect(screen.queryByText(specific)).toBeNull();
}
});
it('Test 12 (quick-260918-gza): Desktop-Cookies gesetzt -> clientKind desktop mit Os/Version/Commit aus dem Cookie', async () => {
document.cookie = 'tessera_desktop=1; path=/';
document.cookie = 'tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows; path=/';
mockFetch.mockResolvedValue(jsonResponse(200, { sent: true }));
const user = userEvent.setup();
await renderButton();
await openDialog(user);
await user.click(screen.getByRole('button', { name: T.send }));
await screen.findByText(T.sent);
const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].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');
});
it('Test 13 (quick-260918-gza, alter Client): nur tessera_desktop ohne tessera_desktop_client -> desktop ohne Details', async () => {
document.cookie = 'tessera_desktop=1; path=/';
mockFetch.mockResolvedValue(jsonResponse(200, { sent: true }));
const user = userEvent.setup();
await renderButton();
await openDialog(user);
await user.click(screen.getByRole('button', { name: T.send }));
await screen.findByText(T.sent);
const body = (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as FormData;
expect(body.get('clientKind')).toBe('desktop');
expect(body.get('clientOs')).toBe('');
expect(body.get('clientVersion')).toBe('');
expect(body.get('clientCommit')).toBe('');
});
});
describe('computeCaptureSize (quick-260914-m97, reine Funktion)', () => {
@@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl';
import { useEffect, useRef, useState } from 'react';
import { appVersion } from '@/lib/app-version';
import { dataUrlToBlob, sendBugReport } from '@/lib/bug-report-api';
import { getDesktopClientInfo, isDesktopClient } from '@/lib/desktop-client';
import { formatErrorsForReport } from '@/lib/error-buffer';
/**
@@ -54,6 +55,11 @@ export function BugReportDialog({ open, screenshot, isAdmin, onClose }: BugRepor
const handleSend = async () => {
setStatus('sending');
// Herkunft (quick-260918-gza): tessera_desktop entscheidet Desktop/
// Browser, tessera_desktop_client liefert die Details dazu; fehlt es
// (alter Client), bleibt es bei Desktop ohne Details.
const desktop = isDesktopClient();
const info = desktop ? getDesktopClientInfo() : null;
const result = await sendBugReport({
description: description.trim(),
page: window.location.pathname + window.location.search,
@@ -65,6 +71,10 @@ export function BugReportDialog({ open, screenshot, isAdmin, onClose }: BugRepor
clientTime: new Date().toISOString(),
errors: formatErrorsForReport(),
screenshot: attach && screenshot ? dataUrlToBlob(screenshot) : null,
clientKind: desktop ? 'desktop' : 'browser',
clientOs: info?.os ?? '',
clientVersion: info?.version ?? '',
clientCommit: info?.commit ?? '',
});
if (result.ok) {
setStatus('sent');