feat: Sentry monitoring a kompletnější sledování stavů Bolt Food
- integrace Sentry na serveru (@sentry/node) i klientu (@sentry/react): chyby 5xx, pády a console logy se hlásí do self-hosted Sentry - nový veřejný endpoint GET /api/config — klient si z něj za běhu vyzvedne Sentry DSN (SENTRY_DSN / SENTRY_CLIENT_DSN v env) - ErrorBoundary na klientu a sourcemapy pro čitelné stack traces - oprava mapování stavu waiting_acceptance (zobrazoval se jako Příprava) - mapování stavů kurýra matched/accepted, logování přechodů stavů Bolt objednávek pro postupné zmapování nedokumentovaného API - simulátor Bolt ve vývoji nově prochází celou reálnou sekvenci stavů - vypnuté logování connect/disconnect websocket klientů
This commit is contained in:
@@ -44,8 +44,10 @@ interface Simulation {
|
||||
* (Přijato → Příprava → Vyzvedávání → Na cestě → Doručeno).
|
||||
*/
|
||||
export const DEFAULT_SCENARIO: SimStep[] = [
|
||||
{ order_state: 'waiting_acceptance', etaSeconds: 2100 },
|
||||
{ order_state: 'accepted', etaSeconds: 1800 },
|
||||
{ order_state: 'preparing', etaSeconds: 1500 },
|
||||
{ order_state: 'waiting_preparation', courier_state: 'accepted', etaSeconds: 1800 },
|
||||
{ order_state: 'preparing', courier_state: 'heading_to_provider', etaSeconds: 1500 },
|
||||
{ order_state: 'waiting_delivery', courier_state: 'arrived_to_provider', etaSeconds: 900 },
|
||||
{ order_state: 'in_delivery', courier_state: 'heading_to_client', etaSeconds: 300 },
|
||||
{ order_state: 'delivered', courier_state: 'delivered', etaSeconds: 0 },
|
||||
|
||||
@@ -66,14 +66,14 @@ export async function pollBoltOrder(token: string): Promise<BoltOrder | null> {
|
||||
}
|
||||
const res = await axios.post(BOLT_POLLING_URL, { token }, {
|
||||
params: {
|
||||
version: 'FW.1.111',
|
||||
version: 'FW.1.113',
|
||||
language: 'cs-CZ',
|
||||
country: 'cz',
|
||||
device_name: 'web',
|
||||
device_os_version: 'web',
|
||||
deviceType: 'web',
|
||||
session_id: DEVICE_ID,
|
||||
distinct_id: DEVICE_ID,
|
||||
distinct_id: `$device:${DEVICE_ID}`,
|
||||
deviceId: DEVICE_ID,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -143,6 +143,15 @@ export async function checkBoltTracking(): Promise<void> {
|
||||
const courierChanged = courierState !== group.boltCourierState && !clearToken;
|
||||
if (!clearToken && !timeChanged && !stateChanged && !courierChanged) continue;
|
||||
|
||||
// Log každého přechodu stavu — Bolt API není dokumentované, takže si takhle
|
||||
// průběžně mapujeme jeho stavový automat (viz mapování v BoltOrderProgress.tsx).
|
||||
if (stateChanged || courierChanged) {
|
||||
console.log(
|
||||
`Bolt tracking: skupina "${group.name}" stav ${group.boltOrderState ?? '(žádný)'} → ${orderState ?? '(žádný)'}` +
|
||||
`, kurýr ${group.boltCourierState ?? '(žádný)'} → ${courierState ?? '(žádný)'}`
|
||||
);
|
||||
}
|
||||
|
||||
updated = await storage.updateData<ClientData>(key, current => {
|
||||
const d = current ?? data!;
|
||||
const g = d.groups?.find(x => x.id === group.id);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './instrument'; // Sentry — musí být první import (auto-instrumentace)
|
||||
import * as Sentry from '@sentry/node';
|
||||
import express from "express";
|
||||
import bodyParser from "body-parser";
|
||||
import cors from 'cors';
|
||||
@@ -98,6 +100,9 @@ async function shutdown(signal: string) {
|
||||
await shutdownRedisStorage();
|
||||
}
|
||||
|
||||
// Flush zbývajících Sentry eventů/logů (no-op, když Sentry není inicializované)
|
||||
await Sentry.close(2000);
|
||||
|
||||
console.log('Graceful shutdown complete');
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -122,6 +127,16 @@ app.get("/api/health/ready", async (_req, res) => {
|
||||
res.status(200).json({ ok: true });
|
||||
});
|
||||
|
||||
/** Veřejná runtime konfigurace pro klienta (Sentry DSN apod.). */
|
||||
app.get("/api/config", (_req, res) => {
|
||||
res.status(200).json({
|
||||
sentry: {
|
||||
dsn: process.env.SENTRY_CLIENT_DSN ?? process.env.SENTRY_DSN ?? null,
|
||||
environment: ENVIRONMENT,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/whoami", (req, res) => {
|
||||
if (!HTTP_REMOTE_USER_ENABLED) {
|
||||
res.status(403).json({ error: 'Není zapnuté přihlášení z hlaviček' });
|
||||
@@ -264,6 +279,10 @@ app.get('*splat', (_req, res) => {
|
||||
res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
|
||||
});
|
||||
|
||||
// Sentry error handler — hlásí jen 5xx (výchozí shouldHandleError), očekávané
|
||||
// chyby (403/409) do Sentry nepadají. Musí být před vlastním error middlewarem.
|
||||
Sentry.setupExpressErrorHandler(app);
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err: any, req: any, res: any, next: any) => {
|
||||
if (err instanceof InsufficientPermissions) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
// Sentry se musí inicializovat před načtením ostatních modulů (auto-instrumentace),
|
||||
// proto si tento soubor načítá .env sám — index.ts na to dojde až po importech.
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${process.env.NODE_ENV ?? 'production'}`) });
|
||||
|
||||
// Bez SENTRY_DSN (dev, Jest, E2E) zůstává Sentry úplně vypnuté.
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
environment: process.env.NODE_ENV ?? 'production',
|
||||
enableLogs: true,
|
||||
// Přeposílá console.log/warn/error jako Sentry Logs — mj. přechody stavů Bolt trackingu
|
||||
integrations: [Sentry.consoleLoggingIntegration({ levels: ['log', 'warn', 'error'] })],
|
||||
});
|
||||
console.log('Sentry: inicializováno');
|
||||
}
|
||||
@@ -291,18 +291,18 @@ describe('DEV simulace (boltSimulator + checkBoltTracking)', () => {
|
||||
expect(mockedAxios.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('první poll nastaví stav accepted a ETA', async () => {
|
||||
test('první poll nastaví stav waiting_acceptance a ETA', async () => {
|
||||
await checkBoltTracking();
|
||||
const g = await getGroup();
|
||||
expect(g.boltOrderState).toBe('accepted');
|
||||
expect(g.deliveryAt).toBe(computeDeliveryHHMM(1800));
|
||||
expect(g.boltOrderState).toBe('waiting_acceptance');
|
||||
expect(g.deliveryAt).toBe(computeDeliveryHHMM(2100));
|
||||
});
|
||||
|
||||
test('advance posune sekvenci na preparing', async () => {
|
||||
test('advance posune sekvenci na accepted', async () => {
|
||||
await checkBoltTracking();
|
||||
advanceBoltSimulation(groupId);
|
||||
await checkBoltTracking();
|
||||
expect((await getGroup()).boltOrderState).toBe('preparing');
|
||||
expect((await getGroup()).boltOrderState).toBe('accepted');
|
||||
});
|
||||
|
||||
test('ruční nastavení stavu (override) se projeví při pollu', async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const initWebsocket = (server: any) => {
|
||||
transports: ["websocket"],
|
||||
});
|
||||
io.on("connection", (socket) => {
|
||||
console.log(`New client connected: ${socket.id}`);
|
||||
// console.log(`New client connected: ${socket.id}`);
|
||||
|
||||
socket.on("join", (login: string) => {
|
||||
if (login && typeof login === "string") {
|
||||
@@ -25,7 +25,7 @@ export const initWebsocket = (server: any) => {
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
console.log(`Client disconnected: ${socket.id}`);
|
||||
// console.log(`Client disconnected: ${socket.id}`);
|
||||
});
|
||||
});
|
||||
return io;
|
||||
|
||||
Reference in New Issue
Block a user