Add Sentry error tracking to both apps
Planner (client + custom server) and Journal (client + entry.server): - Client: @sentry/react with browser tracing + replay on error - Server: @sentry/node for unhandled exceptions - ErrorBoundary captures React errors via Sentry.captureException - Disabled in dev, 10% trace sample rate in production Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1819a17036
commit
ef07915f38
9 changed files with 982 additions and 278 deletions
22
apps/journal/app/entry.client.tsx
Normal file
22
apps/journal/app/entry.client.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { startTransition, StrictMode } from "react";
|
||||||
|
import { hydrateRoot } from "react-dom/client";
|
||||||
|
import { HydratedRouter } from "react-router/dom";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728",
|
||||||
|
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
replaysSessionSampleRate: 0,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
enabled: import.meta.env.PROD,
|
||||||
|
});
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
|
hydrateRoot(
|
||||||
|
document,
|
||||||
|
<StrictMode>
|
||||||
|
<HydratedRouter />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
});
|
||||||
84
apps/journal/app/entry.server.tsx
Normal file
84
apps/journal/app/entry.server.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
|
import { PassThrough } from "node:stream";
|
||||||
|
import type { AppLoadContext, EntryContext } from "react-router";
|
||||||
|
import { createReadableStreamFromReadable } from "@react-router/node";
|
||||||
|
import { ServerRouter } from "react-router";
|
||||||
|
import { isbot } from "isbot";
|
||||||
|
import type { RenderToPipeableStreamOptions } from "react-dom/server";
|
||||||
|
import { renderToPipeableStream } from "react-dom/server";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728",
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
enabled: process.env.NODE_ENV === "production",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const streamTimeout = 5_000;
|
||||||
|
|
||||||
|
export default function handleRequest(
|
||||||
|
request: Request,
|
||||||
|
responseStatusCode: number,
|
||||||
|
responseHeaders: Headers,
|
||||||
|
routerContext: EntryContext,
|
||||||
|
_loadContext: AppLoadContext,
|
||||||
|
) {
|
||||||
|
if (request.method.toUpperCase() === "HEAD") {
|
||||||
|
return new Response(null, {
|
||||||
|
status: responseStatusCode,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let shellRendered = false;
|
||||||
|
const userAgent = request.headers.get("user-agent");
|
||||||
|
|
||||||
|
const readyOption: keyof RenderToPipeableStreamOptions =
|
||||||
|
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
|
||||||
|
? "onAllReady"
|
||||||
|
: "onShellReady";
|
||||||
|
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(
|
||||||
|
() => abort(),
|
||||||
|
streamTimeout + 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { pipe, abort } = renderToPipeableStream(
|
||||||
|
<ServerRouter context={routerContext} url={request.url} />,
|
||||||
|
{
|
||||||
|
[readyOption]() {
|
||||||
|
shellRendered = true;
|
||||||
|
const body = new PassThrough({
|
||||||
|
final(callback) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = undefined;
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const stream = createReadableStreamFromReadable(body);
|
||||||
|
|
||||||
|
responseHeaders.set("Content-Type", "text/html");
|
||||||
|
|
||||||
|
pipe(body);
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
new Response(stream, {
|
||||||
|
headers: responseHeaders,
|
||||||
|
status: responseStatusCode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onShellError(error: unknown) {
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
onError(error: unknown) {
|
||||||
|
responseStatusCode = 500;
|
||||||
|
if (shellRendered) {
|
||||||
|
console.error(error);
|
||||||
|
Sentry.captureException(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from "react-router";
|
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from "react-router";
|
||||||
import type { LinksFunction } from "react-router";
|
import type { LinksFunction } from "react-router";
|
||||||
import type { Route } from "./+types/root";
|
import type { Route } from "./+types/root";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
import stylesheet from "@trails-cool/ui/styles.css?url";
|
import stylesheet from "@trails-cool/ui/styles.css?url";
|
||||||
|
|
||||||
export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }];
|
export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }];
|
||||||
|
|
@ -28,6 +29,8 @@ export default function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
|
||||||
if (isRouteErrorResponse(error)) {
|
if (isRouteErrorResponse(error)) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-md px-4 py-16 text-center">
|
<div className="mx-auto max-w-md px-4 py-16 text-center">
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-router/node": "catalog:",
|
"@react-router/node": "catalog:",
|
||||||
"@react-router/serve": "catalog:",
|
"@react-router/serve": "catalog:",
|
||||||
|
"@sentry/node": "^10.45.0",
|
||||||
|
"@sentry/react": "^10.45.0",
|
||||||
"@simplewebauthn/browser": "^13.3.0",
|
"@simplewebauthn/browser": "^13.3.0",
|
||||||
"@simplewebauthn/server": "^13.3.0",
|
"@simplewebauthn/server": "^13.3.0",
|
||||||
"@trails-cool/db": "workspace:*",
|
"@trails-cool/db": "workspace:*",
|
||||||
|
|
|
||||||
22
apps/planner/app/entry.client.tsx
Normal file
22
apps/planner/app/entry.client.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { startTransition, StrictMode } from "react";
|
||||||
|
import { hydrateRoot } from "react-dom/client";
|
||||||
|
import { HydratedRouter } from "react-router/dom";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208",
|
||||||
|
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
replaysSessionSampleRate: 0,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
enabled: import.meta.env.PROD,
|
||||||
|
});
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
|
hydrateRoot(
|
||||||
|
document,
|
||||||
|
<StrictMode>
|
||||||
|
<HydratedRouter />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from "react-router";
|
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from "react-router";
|
||||||
import type { LinksFunction } from "react-router";
|
import type { LinksFunction } from "react-router";
|
||||||
import type { Route } from "./+types/root";
|
import type { Route } from "./+types/root";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
import stylesheet from "@trails-cool/ui/styles.css?url";
|
import stylesheet from "@trails-cool/ui/styles.css?url";
|
||||||
|
|
||||||
export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }];
|
export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }];
|
||||||
|
|
@ -28,6 +29,8 @@ export default function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
|
||||||
if (isRouteErrorResponse(error)) {
|
if (isRouteErrorResponse(error)) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-router/node": "catalog:",
|
"@react-router/node": "catalog:",
|
||||||
"@react-router/serve": "catalog:",
|
"@react-router/serve": "catalog:",
|
||||||
|
"@sentry/node": "^10.45.0",
|
||||||
|
"@sentry/react": "^10.45.0",
|
||||||
"@trails-cool/db": "workspace:*",
|
"@trails-cool/db": "workspace:*",
|
||||||
"@trails-cool/gpx": "workspace:*",
|
"@trails-cool/gpx": "workspace:*",
|
||||||
"@trails-cool/i18n": "workspace:*",
|
"@trails-cool/i18n": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
|
import * as Sentry from "@sentry/node";
|
||||||
import { createRequestListener } from "@react-router/node";
|
import { createRequestListener } from "@react-router/node";
|
||||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||||
import { createReadStream, statSync } from "node:fs";
|
import { createReadStream, statSync } from "node:fs";
|
||||||
import { join, extname, resolve } from "node:path";
|
import { join, extname, resolve } from "node:path";
|
||||||
import { setupYjsWebSocket } from "./app/lib/yjs-server.ts";
|
import { setupYjsWebSocket } from "./app/lib/yjs-server.ts";
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208",
|
||||||
|
tracesSampleRate: 0.1,
|
||||||
|
enabled: process.env.NODE_ENV === "production",
|
||||||
|
});
|
||||||
|
|
||||||
const port = Number(process.env.PORT ?? 3001);
|
const port = Number(process.env.PORT ?? 3001);
|
||||||
const CLIENT_DIR = resolve(import.meta.dirname, "build", "client");
|
const CLIENT_DIR = resolve(import.meta.dirname, "build", "client");
|
||||||
|
|
||||||
|
|
|
||||||
1115
pnpm-lock.yaml
generated
1115
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue