Git-Deploy per Tag, Betriebsüberwachung und Release-Seeder für den Livegang
- dev/betrieb/deploy.sh: Deploy nur per Tag, mit --seed (Seeder im Wartungsmodus) und --initial (Vorschau/Live einmalig an Ort und Stelle auf Git umstellen) - dev/betrieb/vorschau-auffrischen.sh: Live-DB und Live-Medien in die Vorschau (Generalprobe) - InteriorReleaseSeeder: alle Inhaltsschritte des Relaunchs in einem Lauf, idempotent; Displays, Playlists und Mediathek bleiben unberührt - DisplayInteriorBrandingSeeder überschreibt keinen inzwischen gepflegten Claim mehr - Monitoring: GlitchTip (Server + Browser), /health mit Token, Scheduler-Herzschlag, Alarme per Mail und ntfy, Version in about und /health - public/build versioniert; .gitignore: macOS-Muster Icon traf resources/views/flux/icon/ - Runbook: dev/betrieb/README.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fcb10be120
commit
b5ed756d67
98 changed files with 3947 additions and 82 deletions
128
resources/js/error-reporting.js
Normal file
128
resources/js/error-reporting.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* Browserfehler an GlitchTip.
|
||||
*
|
||||
* Fehlerfreie Seiten laden nur diese paar Zeilen: Das Sentry-SDK wird erst
|
||||
* beim ersten Fehler nachgeladen (eigener Chunk) und meldet dann genau diesen
|
||||
* Fehler. Konfiguration kommt aus <meta name="pk-error-reporting">, das die
|
||||
* Blade-Komponente x-monitoring.browser-errors setzt.
|
||||
*
|
||||
* Fehlgeschlagene Livewire-Requests (5xx, 419) meldet bereits der Server.
|
||||
*/
|
||||
const config = (() => {
|
||||
const meta = document.querySelector('meta[name="pk-error-reporting"]');
|
||||
|
||||
try {
|
||||
return meta ? JSON.parse(meta.content) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const ALLOWED_QUERY = ['page', 'q', 'tab', 'sort']; // wie ErrorReportScrubber::AllowedQueryParameters
|
||||
const TOKEN_SEGMENT = /(?<=\/)[A-Za-z0-9_-]{32,}(?=\/|$)/g;
|
||||
const EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
|
||||
|
||||
/** Gleiche Regeln wie ErrorReportScrubber auf dem Server. */
|
||||
function scrubUrl(value) {
|
||||
try {
|
||||
const url = new URL(value, window.location.origin);
|
||||
url.pathname = url.pathname.replace(TOKEN_SEGMENT, '[Filtered]');
|
||||
for (const key of [...url.searchParams.keys()]) {
|
||||
if (!ALLOWED_QUERY.includes(key) && !key.startsWith('utm_')) {
|
||||
url.searchParams.set(key, '[Filtered]');
|
||||
}
|
||||
}
|
||||
url.hash = '';
|
||||
|
||||
return url.toString().replace(EMAIL, '[email]');
|
||||
} catch {
|
||||
return '[Filtered]';
|
||||
}
|
||||
}
|
||||
|
||||
let sdk = null;
|
||||
|
||||
function loadSdk() {
|
||||
sdk ??= import('./error-reporting-sdk.js').then((Sentry) => {
|
||||
Sentry.init({
|
||||
dsn: config.dsn,
|
||||
release: config.release || undefined,
|
||||
environment: config.environment,
|
||||
sampleRate: config.sampleRate ?? 1,
|
||||
sendDefaultPii: false,
|
||||
// Eigene Listener unten statt GlobalHandlers (sonst doppelt);
|
||||
// keine Breadcrumbs (Klicktexte, URLs) und keine Sessions.
|
||||
integrations: (defaults) =>
|
||||
defaults.filter(
|
||||
(integration) =>
|
||||
!['GlobalHandlers', 'Breadcrumbs', 'BrowserSession', 'BrowserApiErrors'].includes(integration.name),
|
||||
),
|
||||
ignoreErrors: ['ResizeObserver loop', 'Script error.', 'AbortError'],
|
||||
denyUrls: [/^chrome-extension:\/\//, /^moz-extension:\/\//, /^safari-(web-)?extension:\/\//],
|
||||
beforeSend(event) {
|
||||
if (event.request?.url) {
|
||||
event.request.url = scrubUrl(event.request.url);
|
||||
}
|
||||
if (event.request?.headers?.Referer) {
|
||||
event.request.headers.Referer = scrubUrl(event.request.headers.Referer);
|
||||
}
|
||||
for (const exception of event.exception?.values ?? []) {
|
||||
if (exception.value) {
|
||||
exception.value = exception.value.replace(EMAIL, '[email]');
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
});
|
||||
|
||||
if (config.userId) {
|
||||
Sentry.setUser({ id: String(config.userId) });
|
||||
}
|
||||
Sentry.setTag('area', config.area);
|
||||
|
||||
return Sentry;
|
||||
});
|
||||
|
||||
return sdk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise-Ablehnungen mit Objekt statt Error kamen als „Error: [object Object]“
|
||||
* an — Typ und Inhalt mitschicken,
|
||||
* damit die Quelle erkennbar ist.
|
||||
*/
|
||||
function rejectionToError(reason) {
|
||||
if (reason instanceof Error) {
|
||||
return reason;
|
||||
}
|
||||
|
||||
const kind = reason?.constructor?.name ?? typeof reason;
|
||||
let text;
|
||||
try {
|
||||
text = typeof reason === 'string' ? reason : JSON.stringify(reason);
|
||||
} catch {
|
||||
text = String(reason);
|
||||
}
|
||||
|
||||
return new Error(`Unbehandelte Ablehnung (${kind}): ${String(text ?? '').slice(0, 300)}`);
|
||||
}
|
||||
|
||||
function capture(error) {
|
||||
loadSdk()
|
||||
.then((Sentry) => Sentry.captureException(error))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
if (config?.dsn) {
|
||||
window.addEventListener('error', (event) => {
|
||||
// Ressourcenfehler (Bild, Skript) haben kein `error` — nur echte Skriptfehler.
|
||||
if (event.error) {
|
||||
capture(event.error);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
capture(rejectionToError(event.reason));
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue