Merge pull request #53 from trails-cool/implement-sentry-improvements
This commit is contained in:
commit
2c47f2a705
18 changed files with 297 additions and 6 deletions
|
|
@ -1,14 +1,25 @@
|
|||
import * as Sentry from "@sentry/react";
|
||||
import { useEffect } from "react";
|
||||
import { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { HydratedRouter } from "react-router/dom";
|
||||
import { useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from "react-router";
|
||||
|
||||
const sentryEnvironment = import.meta.env.VITE_SENTRY_ENVIRONMENT ??
|
||||
(import.meta.env.PROD ? "production" : "development");
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728",
|
||||
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||
integrations: [
|
||||
Sentry.reactRouterV7BrowserTracingIntegration({
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
}),
|
||||
Sentry.replayIntegration(),
|
||||
],
|
||||
environment: sentryEnvironment,
|
||||
tracesSampleRate: sentryEnvironment === "ci" ? 0 : 1.0,
|
||||
replaysSessionSampleRate: sentryEnvironment === "ci" ? 0 : 1.0,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useEffect } from "react";
|
||||
import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from "react-router";
|
||||
import type { LinksFunction } from "react-router";
|
||||
import type { Route } from "./+types/root";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { initI18n } from "@trails-cool/i18n";
|
||||
import { getSessionUser } from "~/lib/auth.server";
|
||||
import stylesheet from "@trails-cool/ui/styles.css?url";
|
||||
|
||||
initI18n();
|
||||
|
|
@ -28,7 +30,21 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const user = await getSessionUser(request);
|
||||
return { user: user ? { id: user.id, username: user.username } : null };
|
||||
}
|
||||
|
||||
export default function App({ loaderData }: Route.ComponentProps) {
|
||||
const user = loaderData?.user;
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
Sentry.setUser({ id: user.id, username: user.username });
|
||||
} else {
|
||||
Sentry.setUser(null);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ export default [
|
|||
route("activities/new", "routes/activities.new.tsx"),
|
||||
route("activities/:id", "routes/activities.$id.tsx"),
|
||||
route("users/:username", "routes/users.$username.tsx"),
|
||||
route("privacy", "routes/privacy.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
|
|
|
|||
|
|
@ -125,6 +125,12 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="mt-16 border-t border-gray-200 pt-6">
|
||||
<a href="/privacy" className="text-sm text-gray-400 hover:text-gray-600">
|
||||
Privacy
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
98
apps/journal/app/routes/privacy.tsx
Normal file
98
apps/journal/app/routes/privacy.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import type { Route } from "./+types/privacy";
|
||||
|
||||
export function meta(_args: Route.MetaArgs) {
|
||||
return [{ title: "Privacy — trails.cool" }];
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl px-4 py-12">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Privacy Manifest</h1>
|
||||
<p className="mt-4 text-gray-600">
|
||||
trails.cool is committed to privacy by design. This manifest documents
|
||||
everything we collect, why, and how you can control it.
|
||||
</p>
|
||||
|
||||
<section className="mt-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Planner (planner.trails.cool)</h2>
|
||||
<p className="mt-2 text-gray-600">
|
||||
The Planner collects <strong>no personal data</strong>. Sessions are anonymous —
|
||||
there are no user accounts, no tracking, and no analytics on your routes.
|
||||
</p>
|
||||
<ul className="mt-3 list-disc pl-6 text-gray-600 space-y-1">
|
||||
<li>No cookies (except ephemeral session state)</li>
|
||||
<li>No user accounts or login</li>
|
||||
<li>No route data is stored permanently without your action</li>
|
||||
<li>Session data is automatically deleted after 7 days of inactivity</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Journal (trails.cool)</h2>
|
||||
<p className="mt-2 text-gray-600">
|
||||
The Journal stores the data you explicitly provide:
|
||||
</p>
|
||||
<ul className="mt-3 list-disc pl-6 text-gray-600 space-y-1">
|
||||
<li><strong>Account data</strong>: email, username, display name, passkey credentials</li>
|
||||
<li><strong>Routes</strong>: name, description, GPX data, geometry</li>
|
||||
<li><strong>Activities</strong>: title, description, date, linked routes</li>
|
||||
</ul>
|
||||
<p className="mt-3 text-gray-600">
|
||||
All your data is exportable at any time in open formats (GPX, JSON).
|
||||
You can self-host your own instance and migrate your data completely.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mt-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Error Tracking (Sentry)</h2>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Both apps use <a href="https://sentry.io" className="text-blue-600 hover:underline">Sentry</a> for
|
||||
error monitoring. This helps us find and fix bugs quickly.
|
||||
</p>
|
||||
<h3 className="mt-4 font-medium text-gray-800">What Sentry collects:</h3>
|
||||
<ul className="mt-2 list-disc pl-6 text-gray-600 space-y-1">
|
||||
<li><strong>Error details</strong>: stack traces, error messages, browser/OS info</li>
|
||||
<li><strong>Performance traces</strong>: page load times, route navigation timing</li>
|
||||
<li><strong>Session replays on error</strong>: a recording of the session leading up to an error (DOM snapshots, not video)</li>
|
||||
<li><strong>User context</strong> (Journal only): user ID and username are attached to errors for debugging</li>
|
||||
<li><strong>Session ID</strong> (Planner only): the anonymous session ID is attached to errors</li>
|
||||
</ul>
|
||||
<h3 className="mt-4 font-medium text-gray-800">What Sentry does NOT collect:</h3>
|
||||
<ul className="mt-2 list-disc pl-6 text-gray-600 space-y-1">
|
||||
<li>Route or GPX data</li>
|
||||
<li>Passwords or passkey credentials</li>
|
||||
<li>Form input contents (masked in replays)</li>
|
||||
</ul>
|
||||
<h3 className="mt-4 font-medium text-gray-800">Data retention:</h3>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Sentry data is retained for 90 days and then automatically deleted.
|
||||
Sentry's servers are hosted in the EU (Frankfurt).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mt-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900">Third Parties</h2>
|
||||
<ul className="mt-3 list-disc pl-6 text-gray-600 space-y-1">
|
||||
<li><strong>Sentry</strong> (Functional Software Inc.) — error tracking, as described above</li>
|
||||
<li><strong>OpenStreetMap</strong> — map tiles are loaded from OSM tile servers. OSM's <a href="https://wiki.osmfoundation.org/wiki/Privacy_Policy" className="text-blue-600 hover:underline">privacy policy</a> applies to tile requests.</li>
|
||||
<li><strong>BRouter</strong> — routing requests are processed by our self-hosted BRouter instance. No data is sent to third parties for routing.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mt-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900">What We Don't Do</h2>
|
||||
<ul className="mt-3 list-disc pl-6 text-gray-600 space-y-1">
|
||||
<li>We don't sell data</li>
|
||||
<li>We don't show ads</li>
|
||||
<li>We don't build user profiles</li>
|
||||
<li>We don't use tracking pixels or analytics</li>
|
||||
<li>We don't share data with anyone except as listed above</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<p className="mt-10 text-sm text-gray-500">
|
||||
Last updated: March 2026. If this manifest changes, we'll note it here.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
2
apps/journal/public/robots.txt
Normal file
2
apps/journal/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
|
|
@ -6,7 +6,7 @@ import path from "node:path";
|
|||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
sourcemap: true,
|
||||
sourcemap: "hidden",
|
||||
},
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
|
|
@ -15,6 +15,7 @@ export default defineConfig({
|
|||
org: "trails-qq",
|
||||
project: "journal",
|
||||
release: { name: process.env.SENTRY_RELEASE },
|
||||
sourcemaps: { filesToDeleteAfterUpload: ["./build/**/*.map"] },
|
||||
disable: !process.env.SENTRY_AUTH_TOKEN,
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Suspense, lazy, useState, useCallback } from "react";
|
||||
import { Suspense, lazy, useState, useCallback, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { useYjs } from "~/lib/use-yjs";
|
||||
import { useRouting } from "~/lib/use-routing";
|
||||
import { ProfileSelector } from "~/components/ProfileSelector";
|
||||
|
|
@ -27,6 +28,7 @@ interface SessionViewProps {
|
|||
|
||||
export function SessionView({ sessionId, callbackUrl, callbackToken, returnUrl, initialWaypoints }: SessionViewProps) {
|
||||
const { t } = useTranslation("planner");
|
||||
useEffect(() => { Sentry.setTag("session_id", sessionId); }, [sessionId]);
|
||||
const yjs = useYjs(sessionId, initialWaypoints);
|
||||
const { isHost, computing, routeStats, requestRoute } = useRouting(yjs);
|
||||
const [highlightPosition, setHighlightPosition] = useState<[number, number] | null>(null);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
import * as Sentry from "@sentry/react";
|
||||
import { useEffect } from "react";
|
||||
import { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { HydratedRouter } from "react-router/dom";
|
||||
import { useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from "react-router";
|
||||
|
||||
const sentryEnvironment = import.meta.env.VITE_SENTRY_ENVIRONMENT ??
|
||||
(import.meta.env.PROD ? "production" : "development");
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208",
|
||||
integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()],
|
||||
integrations: [
|
||||
Sentry.reactRouterV7BrowserTracingIntegration({
|
||||
useEffect,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
}),
|
||||
Sentry.replayIntegration(),
|
||||
],
|
||||
environment: sentryEnvironment,
|
||||
tracesSampleRate: sentryEnvironment === "ci" ? 0 : 1.0,
|
||||
replaysSessionSampleRate: sentryEnvironment === "ci" ? 0 : 1.0,
|
||||
|
|
|
|||
2
apps/planner/public/robots.txt
Normal file
2
apps/planner/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
|
|
@ -7,7 +7,7 @@ import { yjsDevPlugin } from "./app/lib/vite-yjs-plugin.ts";
|
|||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
sourcemap: true,
|
||||
sourcemap: "hidden",
|
||||
},
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
|
|
@ -17,6 +17,7 @@ export default defineConfig({
|
|||
org: "trails-qq",
|
||||
project: "planner",
|
||||
release: { name: process.env.SENTRY_RELEASE },
|
||||
sourcemaps: { filesToDeleteAfterUpload: ["./build/**/*.map"] },
|
||||
disable: !process.env.SENTRY_AUTH_TOKEN,
|
||||
}),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-25
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
## Context
|
||||
|
||||
Both apps have basic Sentry (`@sentry/react` + `@sentry/node`) with source map uploads, release tracking, and environment tagging. Errors appear in Sentry but lack user/session context, making triage slow. Source maps are also served publicly.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Identify which user or session caused an error without guessing
|
||||
- Route-level performance traces (not just page loads)
|
||||
- Stop serving source maps to browsers
|
||||
|
||||
**Non-Goals:**
|
||||
- Custom Sentry dashboards or alert rules (configure in Sentry UI)
|
||||
- Server-side performance instrumentation beyond request handling
|
||||
- Profiling integration
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: Set user context via Sentry.setUser in root loader
|
||||
|
||||
Journal's root loader already fetches the session user. Call `Sentry.setUser({ id, username })` on the client when authenticated, `Sentry.setUser(null)` when not. Done in root.tsx since it runs on every navigation.
|
||||
|
||||
### D2: Tag planner errors with session ID via Sentry.setTag
|
||||
|
||||
In `SessionView`, call `Sentry.setTag("session_id", sessionId)` on mount. This tags all subsequent errors with the active session.
|
||||
|
||||
### D3: Use reactRouterV7BrowserTracingIntegration
|
||||
|
||||
Sentry provides `Sentry.reactRouterV7BrowserTracingIntegration` which hooks into React Router's navigation to create route-aware spans. Replace the generic `browserTracingIntegration()` in both `entry.client.tsx` files. Requires passing `useEffect` and `useLocation` from react-router.
|
||||
|
||||
### D4: Hidden source maps via Vite config
|
||||
|
||||
Change `build.sourcemap` from `true` to `"hidden"`. This generates `.map` files for the Sentry plugin to upload but doesn't add `//# sourceMappingURL` comments to the bundles. Browsers won't request the map files. The Sentry plugin's `sourcemaps.filesToDeleteAfterUpload` option can also clean them from the build output.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **D3 couples Sentry to React Router version** → Acceptable; we already depend on both. If we upgrade React Router, we upgrade the Sentry integration too.
|
||||
- **D4 makes browser debugging harder in production** → Acceptable; errors go to Sentry with full source context. DevTools debugging in production was never a supported workflow.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
## Why
|
||||
|
||||
Sentry is deployed but errors lack context — we don't know which user or session triggered them, route-level performance is invisible, and source maps are served to clients unnecessarily. These gaps make debugging harder and leak internal details.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Set Sentry user context when a Journal user is authenticated (user ID, username)
|
||||
- Tag Planner errors with the session ID for debugging
|
||||
- Replace generic `browserTracingIntegration` with `reactRouterV7BrowserTracingIntegration` for route-aware performance traces
|
||||
- Configure Vite to produce source maps as `hidden` (uploaded to Sentry but not referenced in the bundle, so browsers don't fetch them)
|
||||
- Wrap server-side request handlers with Sentry error capturing for unhandled exceptions
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
(None — this enhances the existing Sentry integration, no new behavioral capabilities.)
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `infrastructure`: Adding Sentry context enrichment and source map handling to the deployment pipeline
|
||||
|
||||
## Impact
|
||||
|
||||
- **Files**: `entry.client.tsx` (both apps), `entry.server.tsx` (journal), `server.ts` (planner), `vite.config.ts` (both apps), Journal root loader or layout
|
||||
- **Dependencies**: No new packages — `@sentry/react` and `@sentry/node` already support all features
|
||||
- **APIs**: No API changes
|
||||
- **Security**: Source maps no longer served to clients (currently `.map` files are publicly accessible)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Sentry error tracking
|
||||
The system SHALL enrich Sentry events with user and session context, use route-aware tracing, and prevent source maps from being served to clients.
|
||||
|
||||
#### Scenario: Journal error includes user context
|
||||
- **WHEN** an authenticated Journal user triggers an error
|
||||
- **THEN** the Sentry event SHALL include the user's ID and username
|
||||
|
||||
#### Scenario: Journal error without user context
|
||||
- **WHEN** an unauthenticated visitor triggers an error
|
||||
- **THEN** the Sentry event SHALL have no user context (Sentry.setUser(null))
|
||||
|
||||
#### Scenario: Planner error includes session ID
|
||||
- **WHEN** an error occurs during a Planner session
|
||||
- **THEN** the Sentry event SHALL include a `session_id` tag with the active session ID
|
||||
|
||||
#### Scenario: Route-level performance traces
|
||||
- **WHEN** a user navigates between routes in either app
|
||||
- **THEN** Sentry SHALL create a transaction span named after the route pattern (e.g., `/routes/:id`)
|
||||
|
||||
#### Scenario: Source maps not served to clients
|
||||
- **WHEN** a client requests a `.map` file from the production server
|
||||
- **THEN** the server SHALL return 404 (source maps are uploaded to Sentry during build, not shipped in the bundle)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
## 1. User & Session Context
|
||||
|
||||
- [x] 1.1 Set Sentry user context (id, username) in Journal root loader/component when authenticated, clear when not
|
||||
- [x] 1.2 Set Sentry `session_id` tag in Planner's SessionView on mount
|
||||
|
||||
## 2. Route-Aware Tracing
|
||||
|
||||
- [x] 2.1 Replace `browserTracingIntegration()` with `reactRouterV7BrowserTracingIntegration` in planner entry.client.tsx
|
||||
- [x] 2.2 Replace `browserTracingIntegration()` with `reactRouterV7BrowserTracingIntegration` in journal entry.client.tsx
|
||||
|
||||
## 3. Source Map Hardening
|
||||
|
||||
- [x] 3.1 Change `build.sourcemap` from `true` to `"hidden"` in both vite.config.ts files
|
||||
- [x] 3.2 Add `sourcemaps.filesToDeleteAfterUpload` to the Sentry Vite plugin config to clean up .map files after upload
|
||||
|
||||
## 4. Privacy Manifest
|
||||
|
||||
- [x] 4.1 Create `/privacy` route in Journal with a user-visible privacy manifest documenting: what Sentry collects (errors, traces, session replays), what the Planner does NOT collect, data retention, and third-party disclosure (Sentry)
|
||||
- [x] 4.2 Add link to privacy manifest in Journal footer/home page
|
||||
|
||||
## 5. Verify
|
||||
|
||||
- [x] 5.1 Test locally: build, check no `.map` files remain in build output (or no sourceMappingURL in bundles)
|
||||
- [x] 5.2 Test locally: trigger error in Journal while logged in, verify Sentry.setUser was called (check devtools)
|
||||
|
|
@ -63,3 +63,26 @@ The infrastructure SHALL configure DNS and TLS for trails.cool and planner.trail
|
|||
#### Scenario: HTTPS access
|
||||
- **WHEN** a user navigates to https://trails.cool
|
||||
- **THEN** the connection is secured with a valid TLS certificate
|
||||
|
||||
### Requirement: Sentry error tracking
|
||||
The system SHALL enrich Sentry events with user and session context, use route-aware tracing, and prevent source maps from being served to clients.
|
||||
|
||||
#### Scenario: Journal error includes user context
|
||||
- **WHEN** an authenticated Journal user triggers an error
|
||||
- **THEN** the Sentry event SHALL include the user's ID and username
|
||||
|
||||
#### Scenario: Journal error without user context
|
||||
- **WHEN** an unauthenticated visitor triggers an error
|
||||
- **THEN** the Sentry event SHALL have no user context (Sentry.setUser(null))
|
||||
|
||||
#### Scenario: Planner error includes session ID
|
||||
- **WHEN** an error occurs during a Planner session
|
||||
- **THEN** the Sentry event SHALL include a `session_id` tag with the active session ID
|
||||
|
||||
#### Scenario: Route-level performance traces
|
||||
- **WHEN** a user navigates between routes in either app
|
||||
- **THEN** Sentry SHALL create a transaction span named after the route pattern (e.g., `/routes/:id`)
|
||||
|
||||
#### Scenario: Source maps not served to clients
|
||||
- **WHEN** a client requests a `.map` file from the production server
|
||||
- **THEN** the server SHALL return 404 (source maps are uploaded to Sentry during build, not shipped in the bundle)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export function initI18n() {
|
|||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
showSupportNotice: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue