Sentry improvements: context, tracing, source maps, privacy manifest

- Set Sentry user context (id, username) in Journal root for all routes
- Tag Planner errors with session_id
- Use reactRouterV7BrowserTracingIntegration for route-aware traces
- Hidden source maps (no sourceMappingURL in bundles, .map deleted after
  upload to Sentry)
- Privacy manifest at /privacy documenting all data collection
- robots.txt blocking all bots on both apps
- Suppress i18next promotional console log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-25 03:11:07 +01:00
parent 6c4d92f7fc
commit 9a904d9f69
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
16 changed files with 273 additions and 6 deletions

View file

@ -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,

View file

@ -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 />;
}

View file

@ -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>
);
}

View 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&apos;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&apos;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&apos;t Do</h2>
<ul className="mt-3 list-disc pl-6 text-gray-600 space-y-1">
<li>We don&apos;t sell data</li>
<li>We don&apos;t show ads</li>
<li>We don&apos;t build user profiles</li>
<li>We don&apos;t use tracking pixels or analytics</li>
<li>We don&apos;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&apos;ll note it here.
</p>
</div>
);
}

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -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,
}),
],