Skip to content

Error reporting

Errors nobody expected, in the API, the web app and the mobile app, are reported to Sentry with who was signed in and what they were doing. It is off until you set SENTRY_DSN; until then errors are logged, as always, and go nowhere else.

Create a Sentry project and copy its DSN from Settings > Client Keys:

Terminal window
SENTRY_DSN=https://abc123@o123.ingest.sentry.io/456

That one setting turns it on everywhere: the web app and the mobile app read it from GET /api/config when they start, so no image is rebuilt for it. Sentry suggests a project per platform. To do that, make three, set the API’s as SENTRY_DSN, and the others as SENTRY_WEB_DSN and SENTRY_MOBILE_DSN. On Railway, put them in .env.railway.

From What
API A request that ends in a 500; a job given up on after its last attempt; an error inside Better Auth
Web app An error a page throws, one nothing catches, and one in a promise nobody waited for
Mobile app An error a screen throws, and one nothing catches

An answer the API gives on purpose is not an error: a 400, a 402, a 404, or a 503 that says something is not set up. Nor is a failed call to the API from the web or mobile app: the API reports its own failures.

Each report says who was signed in (their id), the workspace, and for a request its method, route, path and x-request-id, the id on its log line. A job’s report says its type and how many times it ran.

Errors in the web app from before Sentry has loaded, such as the app failing as it starts, are kept and sent once it has. In the mobile app, an error before the API has answered is not reported.

Never a password, a token or a cookie. Whatever Sentry would collect, the API, the web app and the mobile app send:

  • a request’s method and path, never its headers, cookies, query string or body;
  • a person’s id, never their email, name or IP address;
  • the addresses of requests and pages in breadcrumbs, without their query strings.

The rules are in packages/contracts/src/error-reports.ts, shared by the three, with a test. Errors only: Sentry records no performance traces, which are OpenTelemetry’s job, and no session replays.

The real line, in the version that threw it

Section titled “The real line, in the version that threw it”

Each report is filed under a release: the commit on Railway (RAILWAY_GIT_COMMIT_SHA), or SENTRY_RELEASE elsewhere. The mobile app’s release is its version and build number.

The API runs its TypeScript as it is, so its errors point at the right line with nothing to upload.

The web app is minified. With SENTRY_AUTH_TOKEN set when it is built, the build sends its source maps to Sentry and leaves them out of what is served. Make the token under Settings > Auth Tokens with the project:releases scope, and set:

Terminal window
SENTRY_AUTH_TOKEN=sntrys_…
SENTRY_ORG=acme
SENTRY_WEB_PROJECT=web

On Railway, .env.railway passes them to the web app’s build only; the API never sees the token. A build whose upload fails still finishes, and says why in its log.

The mobile app sends its source maps from EAS builds. Add SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_PROJECT (the mobile app’s project) as EAS environment variables, the token as a secret. Without them the build finishes without sending any: eas.json sets SENTRY_ALLOW_FAILURE. Sentry needs native code, so make a new development build once (Mobile builds); Expo Go has it already.

Most errors need nothing: throw, and the API reports what would be a 500. For an error you catch and recover from, but want to know about:

constructor(@Inject(ERROR_REPORTER) private readonly errors: ErrorReporter) {}
this.errors.capture(error, { workspaceId: workspace.id });

In the web app, errors.capture(error) from src/lib/errors.ts; in the mobile app, the same from its src/lib/errors.ts.

Sentry is behind ErrorReporter in apps/api/src/errors/error-reporter.ts, and in one file in each app: apps/web/src/lib/sentry.ts and apps/mobile/src/lib/errors.ts. Replace those, and the variables in config/env.ts and GET /api/config.

createTestApp({ teams, errors: new FakeErrorReporter() }) keeps every error the API would report, with its context. See apps/api/test/errors.e2e.test.ts.