Tiago Fortunato
ProjectsOdysObservability

Observability

Observability overview: Sentry 3-runtime + PostHog

Observability

In any application, understanding what's happening under the hood is paramount. The odys application employs a dual-pronged approach to observability, leveraging Sentry for robust error tracking and performance monitoring across its various runtimes, and PostHog for detailed product analytics and user behavior insights. This combination provides a comprehensive view of both the technical health and user engagement within the system.

Overview

The observability strategy in odys is designed to capture critical information from different layers of the application stack. Sentry is configured to monitor errors and performance across three distinct Next.js runtimes: Node.js (server-side), Edge (middleware/API routes), and the client-side browser environment. This ensures that issues are caught regardless of where they originate. Complementing this, PostHog is integrated on the client-side to track user interactions, page views, and other behavioral metrics, providing valuable data for product development and understanding user journeys.

Sentry Integration

Sentry is integrated into odys to provide comprehensive error tracking and performance monitoring. The setup is tailored for Next.js, which operates in multiple environments, necessitating a multi-runtime configuration.

The core of Sentry's initialization for server-side and Edge environments resides in src/instrumentation.ts. This file dynamically imports the appropriate Sentry configuration based on the NEXT_RUNTIME environment variable. If the application is running in a nodejs environment, it loads sentry.server.config. Similarly, for the edge runtime, it loads sentry.edge.config. This conditional loading ensures that Sentry is correctly initialized for each server-side context, capturing errors and performance data from API routes, server components, and middleware. The onRequestError export from this file is a direct hook into Sentry's request error capturing mechanism, ensuring that any unhandled errors during request processing are reported.

For the client-side, Sentry is initialized in src/instrumentation-client.ts. This file is responsible for configuring Sentry for the browser environment. The Sentry.init call includes a specific dsn (https://4c49acd323505a3be3603c5c4c7245ef@o4511124807417856.ingest.us.sentry.io/4511124816658432), which directs error reports to the correct Sentry project. A key integration here is Sentry.replayIntegration(), which allows for session replays, providing visual context to errors.

To manage noise and focus on actionable errors, a beforeSend hook is implemented. This hook filters out specific messages, such as those containing "Object Not Found Matching" or "Non-Error promise rejection captured", which might be benign or less critical for immediate debugging. Performance tracing is enabled with tracesSampleRate set to 1, meaning all transactions are sampled. This is often a good starting point for development or smaller applications to ensure full visibility. Additionally, enableLogs is set to true to send console logs to Sentry, enriching error context.

For session replays, replaysSessionSampleRate is set to 0.1, capturing 10% of all user sessions. However, replaysOnErrorSampleRate is set to 1.0, ensuring that 100% of sessions that encounter an error are recorded, providing maximum context when something goes wrong. The sendDefaultPii option is also enabled, which means Sentry will include personally identifiable information by default, offering richer debugging context at the cost of increased data privacy considerations. The onRouterTransitionStart export hooks into Next.js router events, allowing Sentry to capture performance metrics related to page navigations.

PostHog Integration

PostHog is integrated into odys to provide product analytics, focusing on understanding user behavior on the client-side. The integration is managed through the src/components/posthog-provider.tsx component, which wraps the application's children and conditionally initializes PostHog based on user consent.

The initPostHog function is responsible for the actual PostHog initialization. It takes a key (from process.env.NEXT_PUBLIC_POSTHOG_KEY) and configures PostHog with an api_host (defaulting to https://us.i.posthog.com). Notably, capture_pageview is set to false, giving the application explicit control over when pageview events are sent. Conversely, capture_pageleave is set to true to track when users exit a page. The persistence option is set to "localStorage+cookie", ensuring robust user identification across sessions. A initialized flag prevents multiple initializations.

A crucial aspect of the PostHog integration is the cookie consent mechanism. PostHog is only initialized if the user has previously accepted cookies, indicated by the odys_cookie_consent key in localStorage being set to "accepted". Furthermore, an event listener is set up for a custom browser event, odys:cookie-accepted. If a user accepts cookies during their current session, this event triggers the initPostHog function, enabling analytics tracking. If no PostHog key is provided, the PostHogProvider simply renders its children without any analytics functionality.

The PageViewTracker component, nested within the PostHogProvider, is responsible for manually capturing pageview events. It leverages Next.js's usePathname and useSearchParams hooks. Because useSearchParams requires a Suspense boundary, the PageViewTracker is wrapped in <Suspense fallback={null}>. An useEffect hook within PageViewTracker captures a $pageview event with the $current_url property whenever the pathname or searchParams change, providing precise control over pageview tracking.

Design decisions

The observability setup in odys reflects several deliberate design choices:

  • Sentry's 3-Runtime Configuration: Next.js applications operate in distinct environments (Node.js server, Edge runtime, browser client). By separating Sentry configurations into sentry.server.config, sentry.edge.config, and src/instrumentation-client.ts, the application ensures that errors and performance metrics are captured appropriately for each context. This prevents issues specific to one runtime from being missed and allows for tailored configurations (e.g., different integrations or data scrubbing rules) if needed. The src/instrumentation.ts file acts as the orchestrator, dynamically loading the correct server-side configuration based on the NEXT_RUNTIME environment variable.
  • Sentry beforeSend Filtering: The decision to filter out specific error messages like "Object Not Found Matching" and "Non-Error promise rejection captured" in src/instrumentation-client.ts is a practical choice to reduce noise in Sentry. These messages can often be benign or represent expected client-side behaviors that don't require immediate developer attention, allowing the team to focus on more critical issues.
  • Sentry sendDefaultPii: true: Enabling sendDefaultPii provides richer context for debugging. While it introduces data privacy considerations, it means that by default, Sentry will include user-related information (like IP addresses, user IDs if set) with error reports. This can significantly speed up the debugging process by providing immediate insights into which users were affected and under what circumstances.
  • PostHog Client-Side Only with Consent: Integrating PostHog exclusively on the client-side and gating its initialization behind explicit user consent (odys_cookie_consent and odys:cookie-accepted event) is a strong commitment to user privacy and compliance with regulations. It ensures that no user behavior data is collected until the user has given permission, building trust and adhering to best practices.
  • Manual PostHog Pageview Tracking: Setting capture_pageview: false in initPostHog and implementing a custom PageViewTracker component allows for fine-grained control over pageview events. In single-page applications or Next.js apps with client-side routing, the browser's native pageview events might not accurately reflect user navigation. Manual tracking ensures that pageviews are recorded precisely when the URL changes, providing more accurate analytics data.

Potential improvements

  1. Refine Sentry tracesSampleRate for Production: The current tracesSampleRate is set to 1 in src/instrumentation-client.ts, meaning 100% of transactions are sampled. While excellent for development, this can lead to high data volume and costs in a production environment with significant traffic. Consider implementing a dynamic tracesSampler function to adjust the sampling rate based on factors like user type (e.g., higher rate for paying users, lower for anonymous), error presence, or specific routes, to balance observability with cost efficiency.
  2. Implement Server-Side PostHog Event Capture: Currently, PostHog is exclusively client-side, initialized only after cookie consent in src/components/posthog-provider.tsx. This means critical backend actions or events occurring before client-side rendering or without user interaction (e.g., cron jobs, API calls from other services, or even initial page loads before consent) are not tracked. Introducing a server-side PostHog integration for key backend events would provide a more complete picture of the user journey and system health, independent of client-side limitations or ad-blockers.
  3. Centralize PostHog Event Tracking Logic: The PageViewTracker component in src/components/posthog-provider.tsx manually captures $pageview events. As the application grows, tracking various custom events (ph.capture(...)) scattered throughout the codebase can become difficult to manage and audit. Establishing a dedicated analytics service or utility function that centralizes all PostHog event calls, potentially with a structured event naming convention and property validation, would improve consistency, maintainability, and the overall quality of analytics data.

References

  • src/instrumentation.ts
  • src/instrumentation-client.ts
  • src/components/posthog-provider.tsx

On this page