Highlight.io
"Highlight.io: open-source monitoring, session replay, error tracking, logging, tracing, Next.js SDK, self-hosted option"
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/Highlight.ioFull skill: 354 linesHighlight.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
- Initialize on both client and server — Highlight requires separate initialization for frontend replay and backend tracing/logging to get full-stack correlation.
- Set
tracingOriginsto your API domains — this propagates the Highlight session ID to backend requests, linking frontend sessions to backend traces. - Use
networkRecordingwith URL blocklists — record request/response bodies for debugging, but block auth endpoints to avoid capturing tokens. - Identify users after authentication —
H.identify()ties sessions to user accounts, enabling search by email and impact analysis. - Wrap API routes with error handling — use
H.consumeError()to ensure backend errors include full context and are linked to the originating session. - Use semantic span names —
create_subscriptionis better thanhandler; descriptive names make trace waterfalls readable at a glance. - Deploy the feedback widget — session feedback attaches user context directly to the replay, reducing back-and-forth in bug reports.
- Evaluate self-hosting for compliance — if data residency or HIPAA compliance is required, self-hosted Highlight keeps all data on your infrastructure.
Anti-Patterns
- Client-only initialization — without server-side setup, backend errors and logs are invisible; you only see half the picture.
- Recording everything without privacy settings — default privacy mode masks user input; disabling it without review risks capturing sensitive data in replays.
- Ignoring
tracingOrigins— without it, frontend requests and backend traces are disconnected, losing the full-stack correlation that is Highlight's main advantage. - Not blocking auth URLs in network recording — tokens and credentials in recorded network payloads are a security risk if session data is accessed broadly.
- Self-hosting without resource planning — Highlight stores session replay data, which is large; plan for significant storage growth and set retention policies.
- Using Highlight for infrastructure metrics — Highlight excels at application-level observability; for host/container metrics, pair it with Prometheus or a dedicated infrastructure tool.
- 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
Related Skills
Baselime
Baselime is a serverless-native observability platform designed for AWS, unifying logs, traces, and metrics. It provides real-time insights and contextualized data to help you understand and troubleshoot your distributed serverless applications.
BetterStack
"BetterStack (formerly Better Uptime + Logtail): uptime monitoring, log management, status pages, incident management, alerting"
Checkly
"Checkly: synthetic monitoring, API checks, browser checks, Playwright-based E2E monitoring, monitoring-as-code CLI"
Cronitor
Cronitor is a robust monitoring service designed to ensure your background jobs (cron jobs, scheduled tasks, async workers) and APIs run reliably. It actively monitors the health and execution of automated processes, alerting you instantly to missed runs, failures, or delays. Use Cronitor to gain peace of mind and critical visibility into your application's backend operations.
Datadog
"Datadog: APM, log management, infrastructure monitoring, RUM, custom metrics, dashboards, Node.js tracing"
Grafana Cloud
Grafana Cloud is a fully managed observability platform that unifies metrics (Prometheus/Graphite), logs (Loki), and traces (Tempo) within a single Grafana interface. Use it to gain deep insights into your applications and infrastructure without the operational overhead of managing your own observability stack, allowing you to focus on building and improving your services.