Add Observability

This tutorial adds analytics, error tracking, session replay, and Web Vitals monitoring to an application deployed on Temps in about 10 minutes: one React provider (or script tag) for analytics, replay and Web Vitals, and a Sentry-compatible DSN for errors. Together they cover what Plausible, Sentry and FullStory would otherwise do, and the data stays on your own server. For the product overview, see Temps Observability.


What you will build

By the end of this tutorial, your application will:

  • Track page views and unique visitors automatically
  • Record custom events (button clicks, form submissions, etc.)
  • Capture server-side errors with full stack traces through a Sentry-compatible DSN (browser errors work too, through a Sentry browser SDK pointed at the same DSN)
  • Record user sessions for visual replay
  • Collect Web Vitals: the Core Web Vitals (LCP, INP, CLS) plus FCP, TTFB and the deprecated FID

All data stays on your server. There are no third-party services and no per-event or per-seat fees; capacity is set by the server you run it on.

Time required: approximately 10 minutes.


Prerequisites

You need:

  • A deployed application on Temps (if you have not deployed yet, complete Your First Deployment first)
  • A React, Next.js, or static HTML application — use the React SDK for React apps, or the script tag below for plain HTML and non-React frontends.
  • Access to your codebase — you will add a package and modify a few files

Install the React SDK

In your project directory, install the Temps analytics SDK:

npm install @temps-sdk/react-analytics

This package provides the TempsAnalyticsProvider component and hooks for tracking custom events, page views, engagement and Web Vitals.

Reference: every provider prop and hook is documented in the React Analytics SDK reference.


Enable automatic page views

Wrap your application with TempsAnalyticsProvider. This is the only setup required for automatic page view tracking.

Enable page view analytics

  1. 1

    Wrap your application with TempsAnalyticsProvider (basePath "/api/_temps" when hosted on Temps, or the domain prop when running elsewhere) - this is the only setup required for automatic page view tracking.

  2. 2

    Commit your changes, push to your repository, and let Temps deploy.

  3. 3

    After the deployment completes, visit your application and navigate through a few pages.

  4. 4

    Open your project in the dashboard and click Analytics in the sidebar, then select the Overview tab.

  5. 5

    Watch the Overview tab for incoming traffic.

    Checkpoint: Confirm your page views appear within seconds - the Visitors, Pages, Live, Globe, and Journey views should start populating.

Add the provider

import { TempsAnalyticsProvider } from '@temps-sdk/react-analytics';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <TempsAnalyticsProvider basePath="/api/_temps">
          {children}
        </TempsAnalyticsProvider>
      </body>
    </html>
  );
}

How the basePath works: When your app is deployed on Temps, the proxy automatically handles analytics requests at /api/_temps. This means analytics data is sent through your own domain — no third-party requests, no CORS issues, no ad-blocker interference.

For apps not hosted on Temps (or running locally), use the domain prop instead to point at your Temps instance directly.

Plain HTML or non-React apps

For plain HTML, Astro, Alpine.js, htmx, or another non-React frontend, add the browser analytics script before </head>:

<script
  defer
  src="https://unpkg.com/@temps-sdk/analytics-browser@0.0.6/dist/temps.min.js"
></script>

This uses the same default /api/_temps ingest path as the React SDK. On apps deployed with Temps, no siteId, baseUrl, or extra route handler is required.

Session replay is off by default. To enable it from the same script tag:

<script
  defer
  src="https://unpkg.com/@temps-sdk/analytics-browser@0.0.6/dist/temps.min.js"
  data-session-recording="true"
></script>

Deploy and verify

Commit your changes, push to your repository, and let Temps deploy:

git add .
git commit -m "feat: add Temps analytics"
git push

After the deployment completes, visit your application and navigate through a few pages. Then open the Temps dashboard:

  1. Go to your project
  2. Click Analytics in the sidebar
  3. Select the Overview tab

You should see your page views appearing within seconds. The Analytics section also includes:

  • Visitors — unique visitor details with session history
  • Pages — top pages ranked by views
  • Live — real-time visitor activity
  • Globe — geographic distribution of visitors
  • Journey — page-to-page navigation flow

Track a custom event

Page views are tracked automatically. To track specific user actions like button clicks, form submissions, or feature usage, use the useTrackEvent hook. It works in any component rendered inside TempsAnalyticsProvider.

Add event tracking to any component:

components/SignupButton.tsx

'use client';

import { useTrackEvent } from '@temps-sdk/react-analytics';

export function SignupButton() {
  const trackEvent = useTrackEvent();

  const handleClick = () => {
    trackEvent('signup_started', {
      source: 'hero',
      plan: 'pro',
    });

    // Your signup logic here
  };

  return <button onClick={handleClick}>Start Free Trial</button>;
}

The first argument is the event name (a string you define). The second argument is an optional properties object — use it to attach metadata like which button was clicked, what page the user was on, or which plan they selected.

Declarative tracking (no code required)

For simple click tracking, you can use HTML attributes instead of writing JavaScript:

<button temps-event-name="cta_click" temps-data-location="hero">
  Get Started
</button>

<a href="/pricing" temps-event-name="link_click" temps-data-destination="pricing">
  View Pricing
</a>

Any element with a temps-event-name attribute fires that event when clicked. Every temps-data-* attribute is sent as an event property, with the temps-data- prefix stripped (so the button above sends location: "hero").

Deploy and verify

Push your changes and deploy. After clicking the tracked element in your app, go to the Analytics > Overview tab in the dashboard. Your custom events will appear in the events breakdown.

Reference: useTrackEvent, useTempsAnalytics and declarative tracking in the React Analytics SDK reference.


Enable error tracking

Temps includes a Sentry-compatible error tracking system: each project gets a DSN, and any SDK that speaks the Sentry protocol can send errors to it. Errors are grouped by type with full stack traces.

This tutorial sets up server-side error tracking with @temps-sdk/node-sdk, which runs in Node.js and Bun processes (your Next.js server, an Express API, a worker). It does not run in the browser. To capture errors thrown in client-side code, add @sentry/nextjs or @sentry/react with the same DSN, as shown in Error Tracking setup.

Step 1: Get your DSN

Each project has a unique DSN (Data Source Name) that identifies where to send errors:

Create the error-tracking DSN

  1. 1

    Open your project in the Temps dashboard and click Error Tracking in the sidebar.

  2. 2

    Click the DSN & Setup tab.

  3. 3

    Copy the DSN string (it looks like https://key@your-temps-server/project-id).

    Checkpoint: Confirm a DSN string is shown on the DSN & Setup tab before wiring it into your SDK.

--project-id takes the numeric project ID, not the slug. Run bunx @temps-sdk/cli projects list to see the ID column next to each project name, then substitute that number for <id> in the commands above.

  1. Open your project in the Temps dashboard
  2. Click Error Tracking in the sidebar
  3. Click the DSN & Setup tab
  4. Copy the DSN string (it looks like https://key@your-temps-server/project-id)

Step 2: Install the Node SDK

The error tracking client is included in the Node SDK:

npm install @temps-sdk/node-sdk

Step 3: Initialize error tracking

Create an instrumentation file that runs before your application starts:

Set up error tracking

// instrumentation.ts (in your project root)
import { ErrorTracking } from '@temps-sdk/node-sdk';

export function register() {
  // The Node SDK runs in the Node.js runtime only, not the Edge runtime
  if (process.env.NEXT_RUNTIME !== 'nodejs') return;

  ErrorTracking.init({
    dsn: process.env.SENTRY_DSN!,
    tracesSampleRate: 1.0,
  });
}

Once initialised, the SDK reports uncaught exceptions and unhandled promise rejections in that process, plus anything you pass to ErrorTracking.captureException(). Next.js catches errors thrown inside route handlers and server components itself, so they never become uncaught exceptions; capture them explicitly with captureException() (as in the test below), or use @sentry/nextjs, which hooks into Next.js's own error handling.

Step 4: Add the DSN as an environment variable

Set the DSN environment variable

  1. 1

    Open your project in the dashboard and click Environment Variables in the sidebar.

  2. 2

    Add a new variable with Key SENTRY_DSN and Value set to the DSN you copied earlier.

  3. 3

    Select the environments to apply it to (e.g. production) and save.

    Checkpoint: Confirm SENTRY_DSN is listed for the production environment so the next deployment picks it up.

In the Temps dashboard:

  1. Go to your project
  2. Click Environment Variables in the sidebar
  3. Add a new variable:
    • Key: SENTRY_DSN
    • Value: the DSN you copied in Step 1
  4. Select the environments to apply it to (e.g. production)

Alternatively, use the CLI:

bunx @temps-sdk/cli environments vars set SENTRY_DSN "your-dsn-here" \
  --project my-app --environments production

Step 5: Test it

Because the Node SDK runs on the server, test it with a server route rather than a browser button. Add a temporary route that throws and reports the error:

app/api/test-error/route.ts

import { ErrorTracking } from '@temps-sdk/node-sdk';

export async function GET() {
  try {
    throw new Error('Test error from Temps tutorial');
  } catch (error) {
    ErrorTracking.captureException(error);
    await ErrorTracking.flush(2000); // send before the response ends
    return Response.json({ captured: true }, { status: 500 });
  }
}

Deploy your changes and open https://<your-app>/api/test-error. Then go to Error Tracking in the sidebar. Within a few seconds, you should see the error appear with:

  • The error message and type
  • A full stack trace
  • The environment and server context the SDK attaches
  • A timestamp and occurrence count

Remove the test route after verifying. If you also added a browser SDK (@sentry/nextjs or @sentry/react), errors from client components show up in the same list, with the browser and OS that triggered them.

Already using Sentry? Keep your existing @sentry/node, @sentry/nextjs or @sentry/react setup and change only the DSN to the one from Temps; your Sentry.captureException() calls keep working. @temps-sdk/node-sdk offers a similar API under the ErrorTracking namespace (ErrorTracking.init, ErrorTracking.captureException), documented in the Node SDK reference.

No more "Error: Unknown error" titles. Groups created from Sentry SDK captureMessage() calls omit exception.values[].value, so older versions stored them under the literal title Error: Unknown error. Ingestion now derives a real title by probing the payload (in order) at logentry.formatted, logentry.message, top-level message, exception.values[0].value, the first breadcrumb message, and extra.message. Legacy groups are not rewritten in place, but the Observe view and error detail page re-derive a human-readable message at read time, so they display real text regardless.

Reference: Error Tracking covers the DSN, browser and framework SDK setup, grouping and resolution.


Correlate requests, errors, and revenue

Once you have requests, errors, and revenue events flowing in, the project-level Observe page (/projects/:slug/observe) merges them into a single time-ordered timeline. To jump from an event to its peers, such as the trace behind an error, Temps stores a shared trace ID at write time. Error correlation works today; request-to-trace correlation is still pending on the default storage path (see the note below), and revenue events aren't linked to traces yet.

How trace IDs are captured

SurfaceColumnHow it is populated
Requestsproxy_logs.trace_idPending: the proxy can read the inbound W3C traceparent header, but the default batched request-log writer does not store it yet.
Errorserror_events.trace_id_indexedIngestion promotes an OTel/Sentry trace ID from the event payload to an indexed column.
Revenuerevenue_events.trace_idNot populated yet: revenue ingestion and Stripe import don't set it, so revenue events aren't linked to traces.

The proxy's traceparent parser only accepts a trace-id field that is exactly 32 hexadecimal characters. It rejects the all-zero trace ID that the W3C spec reserves as invalid, and lowercases the value.

Request-to-trace correlation is pending on the default storage path. The live proxy writes request rows through a batched multi-row INSERT whose column list does not yet include trace_id, so the extracted value is dropped before the proxy_logs write. The schema column and indexes are in place, and error and revenue events are already correlated by trace ID, but do not rely on proxy_logs.trace_id being populated for live requests yet. Details: Error Tracking: trace correlation.

For errors, ingestion probes three payload layouts in order and promotes the first value that passes the same 32-hex, non-all-zero validation:

  1. data.sentry.contexts.trace.trace_id
  2. data.contexts.trace.trace_id
  3. data.trace.trace_id

If none match, the column stays NULL and the error simply renders without correlation links.

Nothing to configure, and nothing breaks for old data. There is no flag to enable correlation. The correlation columns (added by migration m20260502_000001_add_observe_correlation) are all nullable, so rows ingested before you upgraded render normally; they just have no trace links. To populate trace IDs going forward, include trace context in the error payloads your SDK sends, and send a standard traceparent header on inbound requests (most OpenTelemetry-instrumented clients do this by default) so requests correlate once the request-log path stores it.

Reference: Observe documents the unified timeline, its event kinds and the correlation columns.


Enable session replay

Session replay records the page as rrweb DOM snapshots plus user input, so you can watch what a user did: clicks, scrolls, typing (masked) and navigation. It does not capture console logs, network requests or FPS/memory metrics, and there are no heatmaps. It covers the session replay part of tools like FullStory or LogRocket.

Session replay is off by default. Turn it on by adding enableSessionRecording to your analytics provider:

Enable session replay

  1. 1

    Add enableSessionRecording={true} (and a sessionRecordingConfig such as sessionSampleRate 0.1) to your TempsAnalyticsProvider.

  2. 2

    Push your changes and deploy.

  3. 3

    Browse your application for a minute or two.

  4. 4

    Open your project in the dashboard, click Analytics in the sidebar, and select the Replays tab.

  5. 5

    Open a recorded session in the replay player.

    Checkpoint: Confirm sessions appear in the Replays tab with duration, page count, and start time, and that clicking one opens the player with playback controls and the timeline scrubber.

app/layout.tsx (updated)

import { TempsAnalyticsProvider } from '@temps-sdk/react-analytics';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <TempsAnalyticsProvider
          basePath="/api/_temps"
          enableSessionRecording={true}
          sessionRecordingConfig={{
            sessionSampleRate: 0.1, // Record 10% of sessions (default: 1.0, all)
            blockClass: 'sensitive-data', // hide these elements entirely
          }}
        >
          {children}
        </TempsAnalyticsProvider>
      </body>
    </html>
  );
}

Configuration options

  • Name
    sessionSampleRate
    Description

    A number between 0 and 1. 0.1 means 10% of sessions are recorded. Default: 1.0 (every session). Lower it on high-traffic production apps to reduce storage.

  • Name
    maskAllInputs
    Description

    When true, all form input values are replaced with asterisks in the recording. Default: true. Password and email fields are masked even if you turn this off.

  • Name
    blockClass
    Description

    Any element with this CSS class is replaced by a placeholder in recordings. Use it for sensitive content like account numbers or personal data. Default: rr-block.

The full option list (text masking selectors, excluded paths, batching) is in the Session Replay configuration reference.

Deploy and verify

Push your changes and deploy. Browse your application for a minute or two. Then:

  1. Go to your project in the dashboard
  2. Click Analytics in the sidebar
  3. Select the Replays tab

You will see recorded sessions listed with their duration, page count, and start time. Click on a session to open the replay player, which provides:

  • Play, pause, and variable-speed playback
  • A timeline scrubber with event markers (clicks, navigation, input)
  • Session metadata panel (browser, OS, device, screen size, duration)
  • Per-session visitor history (all sessions by the same visitor ID)

Privacy: All input values are masked by default, and password and email fields are always masked. Session recordings are stored on your server and are never sent to third parties. Use blockClass, maskTextSelector and excludedPaths to control what is captured, and see Session Replay privacy controls for consent-based recording.

Reference: Session Replay covers setup for React, Svelte and the script tag, privacy controls, the CLI and data retention.


Check your Web Vitals

Web Vitals measure your application's real-world performance as experienced by users. Temps collects six metrics: the three Core Web Vitals (LCP, INP and CLS), two additional loading metrics (FCP and TTFB), and FID, which INP replaced as a Core Web Vital in March 2024 and which is still collected for comparison with older data.

MetricTypeMeasuresTarget
LCP (Largest Contentful Paint)Core Web VitalLoading performance< 2.5s
INP (Interaction to Next Paint)Core Web VitalResponsiveness< 200ms
CLS (Cumulative Layout Shift)Core Web VitalVisual stability< 0.1
FCP (First Contentful Paint)AdditionalFirst render< 1.8s
TTFB (Time to First Byte)AdditionalServer response< 800ms
FID (First Input Delay)Deprecated, still collectedFirst interaction delay< 100ms

Web Vitals collection is on by default (autoTrackSpeedAnalytics defaults to true). The final provider below sets it explicitly alongside session replay:

app/layout.tsx (final version)

import { TempsAnalyticsProvider } from '@temps-sdk/react-analytics';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <TempsAnalyticsProvider
          basePath="/api/_temps"
          autoTrackSpeedAnalytics={true}
          enableSessionRecording={true}
          sessionRecordingConfig={{
            sessionSampleRate: 0.1,
          }}
        >
          {children}
        </TempsAnalyticsProvider>
      </body>
    </html>
  );
}

After deploying, open Speed Insights in the project sidebar to see your performance data. The dashboard shows:

  • Percentiles (P75, P90, P95, P99) for each metric
  • Historical trends over time
  • Breakdown by page, device type, and browser
  • Color-coded scoring (green = good, orange = needs improvement, red = poor)

Reference: useSpeedAnalytics lists how each metric is collected and sent.


What you have accomplished

Your application now has a complete observability stack:

FeatureReplacesStatus
Page view analyticsPlausible / Google AnalyticsActive
Custom event trackingMixpanel / AmplitudeActive
Error tracking with stack tracesSentryActive
Session replayFullStory / LogRocketActive
Web Vitals monitoring (real users)PageSpeed Insights field dataActive

All of this runs on your own infrastructure, with no per-event or per-seat fees. To compare each piece with the hosted tool it replaces, see Temps vs Plausible, Temps vs Sentry, Temps vs FullStory and Temps vs LogRocket.


What to explore next

Last updated

Was this page helpful?