Skip to main content
Technology & EngineeringMonitoring Services353 lines

Highlight.io

Highlight.io: open-source monitoring, session replay, error tracking, logging, tracing, Next.js SDK, self-hosted option

Quick Summary27 lines
Highlight.io is an open-source, full-stack monitoring platform that unifies session replay, error tracking, logging, and tracing. Its principles are:

## Key Points

- **Open source means ownership** — you can inspect the code, self-host, and extend the platform without vendor lock-in. Transparency in monitoring tools is a feature, not a nice-to-have.
- **Full-stack correlation is the default** — a frontend error should link to its backend trace, which links to its log lines. Highlight connects these automatically via session and trace IDs.
- **Session replay is not just for bugs** — replay reveals UX confusion, rage clicks, and dead clicks that error tracking alone cannot surface.
- **Logging and tracing belong together** — structured logs emitted within a trace span are automatically correlated, so you never need to manually grep for request IDs.
- **Start managed, self-host when ready** — use the cloud offering to evaluate, then move to self-hosted when compliance, data residency, or cost requires it.
1. **Initialize on both client and server** — Highlight requires separate initialization for frontend replay and backend tracing/logging to get full-stack correlation.
2. **Set `tracingOrigins` to your API domains** — this propagates the Highlight session ID to backend requests, linking frontend sessions to backend traces.
3. **Use `networkRecording` with URL blocklists** — record request/response bodies for debugging, but block auth endpoints to avoid capturing tokens.
4. **Identify users after authentication** — `H.identify()` ties sessions to user accounts, enabling search by email and impact analysis.
5. **Wrap API routes with error handling** — use `H.consumeError()` to ensure backend errors include full context and are linked to the originating session.
6. **Use semantic span names** — `create_subscription` is better than `handler`; descriptive names make trace waterfalls readable at a glance.
7. **Deploy the feedback widget** — session feedback attaches user context directly to the replay, reducing back-and-forth in bug reports.

## Quick Example

```typescript
// lib/constants.ts
export const CONSTANTS = {
  HIGHLIGHT_PROJECT_ID: process.env.NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID!,
} as const;
```
skilldb get monitoring-services-skills/highlightFull skill: 353 lines
Paste into your CLAUDE.md or agent config

Highlight.io Monitoring Skill

Core Philosophy

Highlight.io is an open-source, full-stack monitoring platform that unifies session replay, error tracking, logging, and tracing. Its principles are:

  • Open source means ownership — you can inspect the code, self-host, and extend the platform without vendor lock-in. Transparency in monitoring tools is a feature, not a nice-to-have.
  • Full-stack correlation is the default — a frontend error should link to its backend trace, which links to its log lines. Highlight connects these automatically via session and trace IDs.
  • Session replay is not just for bugs — replay reveals UX confusion, rage clicks, and dead clicks that error tracking alone cannot surface.
  • Logging and tracing belong together — structured logs emitted within a trace span are automatically correlated, so you never need to manually grep for request IDs.
  • Start managed, self-host when ready — use the cloud offering to evaluate, then move to self-hosted when compliance, data residency, or cost requires it.

Setup

Next.js Full-Stack Integration

// instrumentation.ts (Next.js instrumentation hook)
import { CONSTANTS } from "@/lib/constants";

export async function register() {
  const { H } = await import("@highlight-run/next/server");

  H.init({
    projectID: CONSTANTS.HIGHLIGHT_PROJECT_ID,
    serviceName: "web-api",
    serviceVersion: process.env.APP_VERSION ?? "0.0.0",
    environment: process.env.NODE_ENV,
  });
}
// lib/constants.ts
export const CONSTANTS = {
  HIGHLIGHT_PROJECT_ID: process.env.NEXT_PUBLIC_HIGHLIGHT_PROJECT_ID!,
} as const;

Client-Side Initialization

// app/providers.tsx
"use client";

import { H } from "highlight.run";
import { HighlightInit } from "@highlight-run/next/client";
import { ErrorBoundary } from "@highlight-run/react";
import { CONSTANTS } from "@/lib/constants";

export function HighlightProvider({ children }: { children: React.ReactNode }) {
  return (
    <>
      <HighlightInit
        projectId={CONSTANTS.HIGHLIGHT_PROJECT_ID}
        serviceName="web-client"
        tracingOrigins={["api.example.com", "localhost"]}
        networkRecording={{
          enabled: true,
          recordHeadersAndBody: true,
          urlBlocklist: [/\/api\/auth\/token/],
        }}
        privacySetting="default"
        enableCanvasRecording={false}
        enablePerformanceRecording={true}
      />
      <ErrorBoundary showDialog={false}>
        {children}
      </ErrorBoundary>
    </>
  );
}

export function identifyHighlightUser(user: {
  id: string;
  email: string;
  name: string;
}) {
  H.identify(user.email, {
    id: user.id,
    name: user.name,
  });
}

Backend Logger Setup

// lib/highlight-logger.ts
import { H } from "@highlight-run/next/server";
import winston from "winston";

class HighlightTransport extends winston.Transport {
  log(info: { level: string; message: string; [key: string]: unknown }, callback: () => void) {
    const { level, message, ...meta } = info;

    const severity = this.mapLevel(level);
    H.log(message, severity, {
      ...meta,
      service: process.env.SERVICE_NAME ?? "api",
      timestamp: new Date().toISOString(),
    });

    callback();
  }

  private mapLevel(level: string): "trace" | "debug" | "info" | "warn" | "error" | "fatal" {
    const mapping: Record<string, "trace" | "debug" | "info" | "warn" | "error" | "fatal"> = {
      silly: "trace",
      debug: "debug",
      verbose: "debug",
      info: "info",
      warn: "warn",
      error: "error",
    };
    return mapping[level] ?? "info";
  }
}

export const logger = winston.createLogger({
  level: "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new HighlightTransport(),
    ...(process.env.NODE_ENV !== "production"
      ? [new winston.transports.Console({ format: winston.format.simple() })]
      : []),
  ],
});

Key Techniques

API Route Error Handling with Highlight

// lib/highlight-api.ts
import { H } from "@highlight-run/next/server";
import { type NextRequest, NextResponse } from "next/server";

type ApiHandler = (req: NextRequest) => Promise<NextResponse>;

export function withHighlight(handler: ApiHandler): ApiHandler {
  return async (req: NextRequest) => {
    const requestId = crypto.randomUUID();

    try {
      const response = await handler(req);

      if (response.status >= 400) {
        H.log(`HTTP ${response.status} on ${req.method} ${req.nextUrl.pathname}`, "warn", {
          requestId,
          method: req.method,
          path: req.nextUrl.pathname,
          status: response.status,
        });
      }

      return response;
    } catch (error) {
      H.consumeError(
        error instanceof Error ? error : new Error(String(error)),
        undefined,
        {
          requestId,
          method: req.method,
          path: req.nextUrl.pathname,
        }
      );

      return NextResponse.json(
        { error: "Internal Server Error", requestId },
        { status: 500 }
      );
    }
  };
}

// Usage
export const GET = withHighlight(async (req) => {
  const data = await fetchData();
  return NextResponse.json(data);
});

Custom Trace Spans

// lib/highlight-tracing.ts
import { H } from "@highlight-run/next/server";

export async function withSpan<T>(
  spanName: string,
  attributes: Record<string, string>,
  fn: () => Promise<T>
): Promise<T> {
  return H.startActiveSpan(spanName, async (span) => {
    for (const [key, value] of Object.entries(attributes)) {
      span.setAttribute(key, value);
    }

    try {
      const result = await fn();
      span.setStatus({ code: 1 }); // OK
      return result;
    } catch (error) {
      span.setStatus({ code: 2, message: error instanceof Error ? error.message : "error" });
      span.recordException(error instanceof Error ? error : new Error(String(error)));
      throw error;
    } finally {
      span.end();
    }
  });
}

// Usage in a service
async function createSubscription(userId: string, planId: string) {
  return withSpan("create_subscription", { "user.id": userId, "plan.id": planId }, async () => {
    const user = await withSpan("fetch_user", { "db.operation": "findUnique" }, () =>
      prisma.user.findUniqueOrThrow({ where: { id: userId } })
    );

    const subscription = await withSpan("stripe_create_subscription", { "stripe.plan": planId }, () =>
      stripe.subscriptions.create({
        customer: user.stripeCustomerId,
        items: [{ price: planId }],
      })
    );

    return withSpan("save_subscription", { "db.operation": "create" }, () =>
      prisma.subscription.create({
        data: { userId, stripeId: subscription.id, planId },
      })
    );
  });
}

User Feedback Collection

// components/FeedbackWidget.tsx
"use client";

import { useState } from "react";
import { H } from "highlight.run";

export function FeedbackWidget() {
  const [feedback, setFeedback] = useState("");
  const [submitted, setSubmitted] = useState(false);

  function handleSubmit() {
    if (!feedback.trim()) return;

    H.addSessionFeedback({
      verbatim: feedback,
      userName: "current-user",
      userEmail: "user@example.com",
      timestamp: new Date().toISOString(),
    });

    setSubmitted(true);
    setFeedback("");
    setTimeout(() => setSubmitted(false), 3000);
  }

  return (
    <div className="fixed bottom-4 right-4 p-4 bg-white rounded-lg shadow-lg">
      {submitted ? (
        <p className="text-green-600">Thank you for your feedback!</p>
      ) : (
        <>
          <textarea
            value={feedback}
            onChange={(e) => setFeedback(e.target.value)}
            placeholder="Report an issue or share feedback..."
            className="w-64 h-24 p-2 border rounded"
          />
          <button
            onClick={handleSubmit}
            className="mt-2 w-full px-4 py-2 bg-blue-600 text-white rounded"
          >
            Send Feedback
          </button>
        </>
      )}
    </div>
  );
}

Self-Hosted Docker Compose Setup

// scripts/generate-highlight-compose.ts
// Helper to generate docker-compose for self-hosted Highlight

interface HighlightConfig {
  adminEmail: string;
  licenseKey: string;
  domain: string;
  storageBackend: "s3" | "gcs" | "local";
}

function generateComposeEnv(config: HighlightConfig): string {
  return [
    `ADMIN_EMAIL=${config.adminEmail}`,
    `LICENSE_KEY=${config.licenseKey}`,
    `REACT_APP_FRONTEND_URI=https://${config.domain}`,
    `REACT_APP_PUBLIC_GRAPH_URI=https://${config.domain}/public`,
    `REACT_APP_PRIVATE_GRAPH_URI=https://${config.domain}/private`,
    `SSL=true`,
    `OBJECT_STORAGE_FS=${config.storageBackend === "local" ? "/highlight-data" : ""}`,
    `IN_DOCKER=true`,
  ].join("\n");
}

// Self-host deployment:
// 1. git clone https://github.com/highlight/highlight
// 2. cd docker && cp .env.example .env
// 3. Edit .env with the values above
// 4. docker compose up -d

Best Practices

  1. Initialize on both client and server — Highlight requires separate initialization for frontend replay and backend tracing/logging to get full-stack correlation.
  2. Set tracingOrigins to your API domains — this propagates the Highlight session ID to backend requests, linking frontend sessions to backend traces.
  3. Use networkRecording with URL blocklists — record request/response bodies for debugging, but block auth endpoints to avoid capturing tokens.
  4. Identify users after authentication — H.identify() ties sessions to user accounts, enabling search by email and impact analysis.
  5. Wrap API routes with error handling — use H.consumeError() to ensure backend errors include full context and are linked to the originating session.
  6. Use semantic span names — create_subscription is better than handler; descriptive names make trace waterfalls readable at a glance.
  7. Deploy the feedback widget — session feedback attaches user context directly to the replay, reducing back-and-forth in bug reports.
  8. Evaluate self-hosting for compliance — if data residency or HIPAA compliance is required, self-hosted Highlight keeps all data on your infrastructure.

Anti-Patterns

  1. Client-only initialization — without server-side setup, backend errors and logs are invisible; you only see half the picture.
  2. Recording everything without privacy settings — default privacy mode masks user input; disabling it without review risks capturing sensitive data in replays.
  3. Ignoring tracingOrigins — without it, frontend requests and backend traces are disconnected, losing the full-stack correlation that is Highlight's main advantage.
  4. Not blocking auth URLs in network recording — tokens and credentials in recorded network payloads are a security risk if session data is accessed broadly.
  5. Self-hosting without resource planning — Highlight stores session replay data, which is large; plan for significant storage growth and set retention policies.
  6. Using Highlight for infrastructure metrics — Highlight excels at application-level observability; for host/container metrics, pair it with Prometheus or a dedicated infrastructure tool.
  7. Skipping error boundaries — without ErrorBoundary, React crashes unmount the Highlight recorder, and you lose the final moments of the session before the error.

Install this skill directly: skilldb add monitoring-services-skills

Get CLI access →