Overview
Sightscreen’s observability stack covers three layers:- Structured logging — pino on the backend,
os.Logger+AppLoggeron iOS - Error tracking — Sentry on both platforms, with a pino-to-Sentry bridge on the backend
- Distributed tracing — Sentry transactions on iOS (
LATracer) andcomponenttags on the backend for CloudWatch filtering
Pino structured logging
The root logger is created insrc/utils/logger.ts and exported as a singleton. Every other module imports it.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
LOG_LEVEL | info | Minimum pino log level (trace, debug, info, warn, error, fatal) |
NODE_ENV | — | When development, pino-pretty is used for human-readable output |
Output modes
pino-pretty is an optional dev dependency. If it is not installed, the logger silently falls back to JSON output even in development.Child logger conventions
Child loggers carry contextual fields that appear in every log line from that scope.Error logging pattern
Always pass errors under theerr key so pino serializes the full stack trace:
Request logging middleware
TherequestLogger middleware in src/middleware/logging.ts runs on every request:
- Generates a
requestId(UUIDv4) and attaches it toreq.requestId - Creates a child logger at
req.logscoped to that request - On response finish, logs the request with method, path, status code, and response time
- Routes the log to the appropriate level based on status code:
>= 500—error>= 400—warn- otherwise —
info
errorLogger middleware catches unhandled errors and logs them with { err } before passing to the next error handler.
Sentry (backend)
Sentry is opt-in — it only initializes whenSENTRY_DSN is set.
Initialization
src/instrument.ts is imported before anything else in the entry point. It calls Sentry.init() with the DSN and a 100% trace sample rate.
Express error handler
After all routes are registered,Sentry.setupExpressErrorHandler(app) is called. This captures unhandled Express errors as Sentry exceptions automatically.
Pino-to-Sentry stream
The logger usespino.multistream to tee output to both stdout and a custom Sentry writable stream:
- The Sentry stream only processes logs at level >= 50 (error and fatal)
- If the log contains an
errobject withmessageandstack, it callsSentry.captureException - Otherwise it calls
Sentry.captureMessagewith the appropriate severity - The stream is wrapped in a try/catch so Sentry failures never break logging
Graceful shutdown
OnSIGTERM or SIGINT, the shutdown handler calls Sentry.close(2000) with a 2-second timeout to flush any pending events before the process exits.
Sentry (iOS)
The iOS app uses sentry-cocoa SDK, configured inSightscreenApp.configureSentry().
Configuration
| Setting | Value | Notes |
|---|---|---|
tracesSampleRate | 1.0 | 100% of transactions are sampled |
tracePropagationTargets | ["api.sightscreen.app", "localhost"] | Auto-injects sentry-trace header on requests to these hosts |
attachScreenshot | true | Captures a screenshot on every error event |
enableAppHangTracking | true | Detects and reports main-thread hangs |
enableMetricKit | true | Forwards MetricKit diagnostics to Sentry |
AppLogger
AppLogger mirrors pino’s child logger pattern on iOS. It is the single entry point for all logging.
- os.Logger — always active. Visible in Xcode console and Console.app
- SentryBackend — device builds only (disabled on simulator). Routes errors to
SentrySDK.captureand all other levels to breadcrumbs - DebugLogService — admin overlay, enabled via
DebugLogSettings
SentryBackend
SentryLogger.swift isolates the Sentry import behind a SentryBackend enum:
captureError— callsSentrySDK.capture(error:)orSentrySDK.capture(message:), attaching the logger category as a tag and any extrasaddBreadcrumb— logs non-error events as Sentry breadcrumbs with category and level mappingsetGlobalContext— attaches device context (OS version, device model, app version, build number, vendor device ID, locale) under the"app"scope keysetLiveActivityStatus— records LA authorization and frequent-push capability under the"live_activity"scope key
SentryBackend.isAvailable returns false on the simulator via #if targetEnvironment(simulator). This avoids noisy local development events.Distributed tracing
LATracer (iOS)
LATracer creates Sentry transactions for the follow-to-Live-Activity lifecycle:
| Constant | Value | Description |
|---|---|---|
opAPICall | la.api.follow | Follow API request |
opAPIUnfollow | la.api.unfollow | Unfollow API request |
opLAStart | la.activity.start | ActivityKit start request |
opLAAdopt | la.activity.adopt | Adopt existing activity |
opTokenRefresh | la.token.refresh | Push token refresh cycle |
Trace propagation
The iOS SDK auto-injectssentry-trace and baggage headers on outgoing requests to hosts matching tracePropagationTargets:
Backend component tags
The backend tags Live Activity-related logs withcomponent: 'live-activity' for CloudWatch filtering:
CloudWatch integration
App Runner captures all stdout output and forwards it to CloudWatch Logs. Since pino outputs JSON in production, you can use CloudWatch Insights filter patterns:No custom CloudWatch agent or log driver is required. App Runner’s built-in log capture handles everything as long as logs go to stdout.
Known issues
sendDefaultPii:true without beforeSend scrubber
sendDefaultPii:true without beforeSend scrubber
instrument.ts sets sendDefaultPii: true which tells Sentry to capture request headers, cookies, and IP addresses. There is currently no beforeSend hook to strip sensitive fields.Risk: User IP addresses and session cookies may appear in Sentry events.Fix: Add a beforeSend callback that strips or redacts sensitive headers, or disable sendDefaultPii entirely.Raw emails in CognitoAuthService logs
Raw emails in CognitoAuthService logs
The Cognito auth middleware logs the decoded JWT claims at
debug level, which can include the user’s email address. If LOG_LEVEL is set to debug in production, these will appear in CloudWatch and potentially in Sentry via the pino-to-Sentry stream.Risk: PII in log storage.Fix: Redact email from claims before logging, or only log the sub claim.Duplicate error reporting in AppConfigService
Duplicate error reporting in AppConfigService
Some error paths in the iOS
AppConfigService call both AppLogger.error() (which routes to Sentry) and directly call SentrySDK.capture. This results in duplicate Sentry events for the same error.Fix: Remove direct SentrySDK.capture calls and rely solely on AppLogger.error() for Sentry routing.