OpenPanel
Open-source web and product analytics for browser and server events.
OpenPanel combines web analytics and product analytics with support for custom events, user profiles, and optional session replay.
When to use it
- You want open-source product analytics that work in the browser and on the server.
- You need web analytics, custom events, profiles, and optional session replay in one provider.
- You want to self-host OpenPanel or send events to OpenPanel Cloud.
Installation
The OpenPanel providers ship in the @trakoo/openpanel package. Install it alongside trakoo and only the OpenPanel SDK for the side you use: @openpanel/web for the client and @openpanel/sdk for the server.
# Client
pnpm add trakoo @trakoo/openpanel @openpanel/web
# Server
pnpm add trakoo @trakoo/openpanel @openpanel/sdk
Client-side usage
Use OpenPanelClientProvider from @trakoo/openpanel/client. Browser code needs only the public OpenPanel client ID.
import { createClientAnalytics } from 'trakoo/client';
import { OpenPanelClientProvider } from '@trakoo/openpanel/client';
import { appEvents } from '@/lib/events';
export const analytics = createClientAnalytics({
events: appEvents,
providers: [
new OpenPanelClientProvider({
clientId: import.meta.env.VITE_OPENPANEL_CLIENT_ID
})
]
});
await analytics.track('signup_button_clicked', {
location: 'header',
variant: 'primary'
});
analytics.pageView({ section: 'pricing' });
The provider uses explicit trakoo calls by default. OpenPanel’s automatic screen views, outgoing-link tracking, data-attribute tracking, and session replay remain disabled unless you enable their corresponding SDK options:
new OpenPanelClientProvider({
clientId: import.meta.env.VITE_OPENPANEL_CLIENT_ID,
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
sessionReplay: {
enabled: true,
maskAllInputs: true,
maskAllText: true
}
})
Server-side usage
Use OpenPanelServerProvider from @trakoo/openpanel/server. Server analytics is stateless, so pass user context with each attributed event.
import { createServerAnalytics } from 'trakoo/server';
import { OpenPanelServerProvider } from '@trakoo/openpanel/server';
import { appEvents } from '@/lib/events';
export const serverAnalytics = createServerAnalytics({
events: appEvents,
providers: [
new OpenPanelServerProvider({
clientId: process.env.OPENPANEL_CLIENT_ID!,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET!
})
]
});
await serverAnalytics.track('invoice_paid', {
invoiceId: 'inv_123',
amount: 4900
}, {
userId: 'user_123',
user: {
email: 'jane@example.com',
traits: { plan: 'pro' }
}
});
await serverAnalytics.shutdown();
The provider clears OpenPanel’s local identity state after server-side identify() and on shutdown(), so it never reuses the identity from another request.
Geo and device attribution
A server event carries your server’s IP and user agent, not the caller’s, so without request context OpenPanel attributes every one of them to your datacenter. Pass the caller’s address and user agent as context.server and the provider forwards them as the openpanel-client-ip and user-agent headers OpenPanel resolves geo and device from:
await serverAnalytics.track('api_request', { route: '/v1/generations' }, {
userId: 'user_123',
context: {
server: {
ip: request.headers.get('x-forwarded-for')?.split(',')[0]?.trim(),
userAgent: request.headers.get('user-agent') ?? undefined
}
}
});
Each event is attributed to its own caller, so one long-lived provider can serve concurrent requests. context.device.ip and context.device.userAgent are used for whichever of the two context.server does not supply. The IP travels as a header only — it is never stored as an event property, and a device.ip you pass is removed from the properties.
A value that cannot be sent as an HTTP header, such as one containing a line break or a character above U+00FF, is skipped and the next source is tried. The event is still delivered, without that attribute.
Resolve the caller’s address with whatever your platform vouches for (x-forwarded-for behind a trusted proxy, x-real-ip, or a framework helper). Forwarding a spoofable header you do not control attributes events to whatever the caller claims.
Identifying users
Client identification persists until analytics.reset(). OpenPanel’s standard firstName, lastName, email, and avatar traits are sent as profile fields. Other traits are stored as custom profile properties.
analytics.identify('user_123', {
firstName: 'Jane',
lastName: 'Doe',
email: 'jane@example.com',
avatar: 'https://example.com/jane.png',
plan: 'pro'
});
On the server, identify() updates the profile but does not persist that identity for later events. Continue to pass the profile ID on each tracked server event.
await serverAnalytics.identify('user_123', {
email: 'jane@example.com',
plan: 'enterprise'
});
OpenPanel only writes a profile when there is something besides the ID, so identify() without traits sends nothing. In the browser it still attributes later events to that profile; on the server it has no effect.
Configuration
Client options:
clientIdstring
OpenPanel project client ID.
stringapiUrl?string
API URL for OpenPanel Cloud or a self-hosted instance.
stringtrackScreenViews?boolean
Automatically track browser navigation.
booleanfalsetrackOutgoingLinks?boolean
Automatically track external link clicks.
booleanfalsetrackAttributes?boolean
Enable OpenPanel data-attribute tracking.
booleanfalsesessionReplay?object
Configure optional OpenPanel session replay.
objectfilter?function
Filter OpenPanel payloads before sending.
functiondebug?boolean
Enable provider and SDK debug logging.
booleanfalseonDeliveryFailure?function
Called when OpenPanel rejects an event. Logged when omitted.
functionenabled?boolean
Set to false to disable the provider without removing it.
booleantrueServer options:
clientIdstring
OpenPanel project client ID.
stringclientSecretstring
Secret for authenticated server events.
stringapiUrl?string
API URL for OpenPanel Cloud or a self-hosted instance.
stringfilter?function
Filter OpenPanel payloads before sending.
functiondebug?boolean
Enable provider and SDK debug logging.
booleanfalseonDeliveryFailure?function
Called when OpenPanel rejects an event. Logged when omitted.
functionenabled?boolean
Set to false to disable the provider without removing it.
booleantrueThe server provider passes only these options to the SDK. OpenPanel’s queueing options, disabled and waitForProfile, are ignored: one server client is shared by every request, so it would hold anonymous events and release them under whichever request identifies next.
Delivery failures
OpenPanel’s SDKs answer an HTTP 401 by dropping the event: they do not throw, retry, or log. A wrong client ID, a rotated secret, or a browser origin the project does not allow therefore stops analytics without producing a single error. Both providers report those rejections.
new OpenPanelServerProvider({
clientId: process.env.OPENPANEL_CLIENT_ID!,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET!,
onDeliveryFailure: (failure) => {
logger.warn('openpanel_delivery_failed', {
attempts: failure.attempts,
payloadType: failure.payloadType,
reason: failure.reason,
status: failure.status
});
}
});
reason is unauthorized for a rejected key or origin, server_error for a response that was still rejected once the SDK’s retries were exhausted, and network_error when the request never produced a response. The failure carries the OpenPanel envelope type and the ingestion URL, never the event properties.
Without a handler the failure is reported with console.error, so a rejected key is visible by default. Delivery never throws either way, so a rejected event cannot turn into an application error.
Browser events are rejected the same way when the page origin is not on the project’s allowed origins. That is why the same client ID can work in production and be refused from localhost.
Failure reporting, and on the server the caller’s IP and user agent, depend on the transport the OpenPanel SDK exposes. If an installed SDK version changes that transport so the provider no longer recognizes it, the provider warns once at initialization with console.warn. Events are still delivered, but without failure reporting and, on the server, without caller attribution.
Reserved property names
OpenPanel’s SDK reads two event property names itself instead of storing them:
profileIdis the profile the event belongs to. The provider sets it from the event’s user ID when there is one.groupsis a list of OpenPanel group IDs. An array becomes the event’s groups rather than a property, a string is split into one-character group IDs,nullor an empty array is dropped, and a number, boolean or object makes the SDK throw, so the event is not sent.
Give event properties other names unless you mean OpenPanel’s own fields.
Method mapping
| trakoo method | OpenPanel behavior |
|---|---|
track() |
Sends the event name, properties, and trakoo context |
identify() |
Creates or updates an OpenPanel profile |
pageView() |
Sends OpenPanel’s screen_view event |
pageLeave() |
No-op; OpenPanel has no dedicated page-leave method |
reset() |
Clears the browser or provider-local identity |
