Pad vykreslovani uz nesmi shodit stranku a sam zaklada incident
Ukazka tela webhooku shazovala builder pri psani cesty parametru. Chyba sama byla na jednom radku, ale to podstatne je, ze jedna vyjimka pri vykreslovani odstranila celou stranku. Uzivateli zustala bila plocha a rozdelana prace byla pryc. Aplikace na to nemela zadnou pojistku, takze totez mohlo prijit odkudkoli. Pojistka: - nova web/src/components/ErrorBoundary.tsx obaluje obsah portalu. Mistni chyba ted shodi svoji cast obrazovky, ne aplikaci: navigace, prepinac firmy i odhlaseni zustanou funkcni. Odchod jinam pojistku srovna zpatky - je to jedina trida v celem klientovi, React to jinak zachytit neumi Incident: - POST /api/dashboard/client-crash zaklada incident z padu. Bez toho je jedina stopa v konzoli prohlizece uzivatele, kam se nikdo nedostane - title a impact cte zakaznik, detail cte spravce platformy: hlaska, misto v kodu, strom komponent, adresa stranky, ucet, prohlizec a verze buildu - verze buildu je tam schvalne. U tohohle padu se ukazalo, ze bez ni se neda poznat, jestli uzivatel vidi chybu, ktera uz je opravena, nebo novou - tentyz pad na tomtez miste zalozi incident nejvys jednou za deset minut. Pad pri vykreslovani se opakuje pri kazdem prekresleni Sama ukazka: - parametr, kterym vede cesta jineho parametru, uz nedostane zastupnou hodnotu podle typu. Kdyz je jeden parametr `data` a druhy `data.result`, `data` musi byt objekt a zastupna hodnota by ho prepsala - cela ukazka je v try. Je to napoveda a nesmi shodit ani ten kus obrazovky Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
00d9d2d4b1
commit
35d1e43307
@@ -1785,6 +1785,57 @@ export function buildOpenApiDocument() {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/client-crash': {
|
||||
post: {
|
||||
tags: ['Portal'],
|
||||
summary: 'Nahlasit pad vykreslovani',
|
||||
description:
|
||||
'Zaklada incident z padu portalu v prohlizeci. Bez toho je jedina stopa v konzoli ' +
|
||||
'uzivatele, kam se nikdo nedostane, takze bychom o padu vedeli jen tehdy, kdyby ho ' +
|
||||
'nekdo nahlasil. Incident nese title a impact pro zakaznika a detail pro spravce ' +
|
||||
'platformy: hlaska, misto v kodu, strom komponent, adresa stranky a verze buildu. ' +
|
||||
'Tentyz pad na tomtez miste zalozi incident nejvys jednou za deset minut - pad pri ' +
|
||||
'vykreslovani se opakuje pri kazdem prekresleni a jinak by z jedne chyby vzniklo ' +
|
||||
'padesat incidentu. Volá to pojistka v klientovi, ne clovek.',
|
||||
security: [{ bearerAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['message'],
|
||||
properties: {
|
||||
message: { type: 'string' },
|
||||
stack: { type: 'string' },
|
||||
componentStack: { type: 'string' },
|
||||
path: { type: 'string', description: 'Kde v portalu se to stalo.' },
|
||||
build: { type: 'string', description: 'Verze nasazeneho klienta.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
'201': {
|
||||
description: 'Incident zalozen',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
created: { type: 'boolean' },
|
||||
incidentId: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'202': { description: 'Stejny pad uz je hlaseny, incident se nezaklada' },
|
||||
'400': { description: 'Neplatny vstup' },
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/dashboard/connectors/{id}/mcp/tools': {
|
||||
post: {
|
||||
tags: ['Konektory'],
|
||||
|
||||
+102
-1
@@ -41,7 +41,7 @@ import {
|
||||
} from '../data/dashboardLayouts.js';
|
||||
import { collectScopes } from '../data/flowScope.js';
|
||||
import { widgetCatalog } from '../data/widgets.js';
|
||||
import { listIncidents } from '../data/incidentStore.js';
|
||||
import { createIncident, listIncidents } from '../data/incidentStore.js';
|
||||
import { getSummary } from '../data/mock.js';
|
||||
import { findPersonByEmail, groupsOfPerson, listGroups, listPeople } from '../data/people.js';
|
||||
import { hasPermission } from '../data/permissions.js';
|
||||
@@ -139,6 +139,107 @@ dashboardRouter.get('/incidents', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Hlaseni padu portalu.
|
||||
*
|
||||
* Kdyz v prohlizeci spadne vykreslovani, uzivatel to vidi, ale my ne. Bez
|
||||
* tohohle endpointu je jedina stopa v jeho konzoli, kam se nikdo nedostane,
|
||||
* takze o padu vime jen tehdy, kdyz ho nekdo nahlasi. To znamena, ze o vetsine
|
||||
* padu nevime vubec.
|
||||
*
|
||||
* Incident nese dve casti a je to zamerne:
|
||||
* - `title` a `impact` cte zakaznik, takze zadne stack trace,
|
||||
* - `detail` cte spravce platformy a je v nem vsechno: hlaska, misto v kodu,
|
||||
* strom komponent, adresa stranky a verze buildu.
|
||||
*
|
||||
* Cely `detail` je to, **jak se to stalo**. Bez adresy a stromu komponent je
|
||||
* hlaska "Cannot create property" k nepouziti.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Kolik nejvys znaku se z jednoho pole prevezme.
|
||||
*
|
||||
* Strom komponent umi byt velmi dlouhy a do incidentu patri jeho zacatek,
|
||||
* protoze prave nahore je komponenta, ktera spadla.
|
||||
*/
|
||||
const FIELD_LIMIT = 4_000;
|
||||
|
||||
/**
|
||||
* Jak dlouho se tentyz pad povazuje za jeden.
|
||||
*
|
||||
* Pad pri vykreslovani se opakuje pri kazdem stisku klavesy. Bez tohohle by
|
||||
* z jedne chyby vzniklo padesat incidentu a ten pravy by v nich zapadl.
|
||||
*/
|
||||
const DEDUPE_MS = 600_000;
|
||||
|
||||
/** Otisk padu a kdy naposled zalozil incident. */
|
||||
const recent = new Map<string, number>();
|
||||
|
||||
const crashSchema = z.object({
|
||||
message: z.string().trim().min(1).max(FIELD_LIMIT),
|
||||
stack: z.string().max(FIELD_LIMIT).optional(),
|
||||
componentStack: z.string().max(FIELD_LIMIT).optional(),
|
||||
/** Kde v portalu se to stalo. Bez toho se to nema kde hledat. */
|
||||
path: z.string().max(500).optional(),
|
||||
/** Verze nasazeneho klienta. Rika, jestli uz je v tom oprava. */
|
||||
build: z.string().max(200).optional(),
|
||||
});
|
||||
|
||||
dashboardRouter.post('/client-crash', (req, res) => {
|
||||
const access = accessFor(req.user!);
|
||||
const requested = typeof req.query.tenantId === 'string' ? req.query.tenantId : undefined;
|
||||
const tenantId = requested ?? access.defaultTenantId;
|
||||
|
||||
const parsed = crashSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({
|
||||
error: 'validation_error',
|
||||
message: parsed.error.issues[0]?.message ?? 'Neplatný vstup.',
|
||||
});
|
||||
}
|
||||
|
||||
const crash = parsed.data;
|
||||
const where = crash.path ?? 'neznámá stránka';
|
||||
|
||||
/*
|
||||
* Otisk je hlaska a misto, ne cas. Tatáz chyba na tomtez miste je porad
|
||||
* jeden problem, i kdyz na ni clovek narazi desetkrat za sebou.
|
||||
*/
|
||||
const fingerprint = `${crash.message}|${where}`;
|
||||
const last = recent.get(fingerprint) ?? 0;
|
||||
const now = Date.now();
|
||||
if (now - last < DEDUPE_MS) {
|
||||
return res.status(202).json({ created: false, reason: 'stejný pád už je hlášený' });
|
||||
}
|
||||
recent.set(fingerprint, now);
|
||||
|
||||
const detail = [
|
||||
`Stránka: ${where}`,
|
||||
`Uživatel: ${req.user!.email}`,
|
||||
`Verze klienta: ${crash.build ?? 'neznámá'}`,
|
||||
`Prohlížeč: ${String(req.headers['user-agent'] ?? 'neznámý')}`,
|
||||
'',
|
||||
`Hláška: ${crash.message}`,
|
||||
...(crash.componentStack ? ['', 'Strom komponent:', crash.componentStack] : []),
|
||||
...(crash.stack ? ['', 'Zásobník volání:', crash.stack] : []),
|
||||
].join('\n');
|
||||
|
||||
const incident = createIncident({
|
||||
tenantId,
|
||||
title: 'Část portálu se nepodařilo vykreslit',
|
||||
service: 'Portál',
|
||||
// Pad vykreslovani neni vypadek sluzby, ale uzivatel u toho nemuze
|
||||
// pokracovat v praci. Prostredni zavaznost, ne nejvyssi.
|
||||
severity: 'sev2',
|
||||
impact: `Stránka ${where} se části uživatelů nezobrazila správně. Ostatní části portálu fungují.`,
|
||||
detail,
|
||||
source: 'portál',
|
||||
});
|
||||
|
||||
console.error(`[ui] pad portalu na ${where}: ${crash.message} (incident ${incident.id})`);
|
||||
return res.status(201).json({ created: true, incidentId: incident.id });
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- rozlozeni dashboardu
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user