Skip to content
trakoo
Esc
↑↓navigate↵open⌘Jpreview
On this page

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:

PropType
clientIdstring

OpenPanel project client ID.

Typestring
apiUrl?string

API URL for OpenPanel Cloud or a self-hosted instance.

Typestring
trackScreenViews?boolean

Automatically track browser navigation.

Typeboolean
Defaultfalse
trackOutgoingLinks?boolean

Automatically track external link clicks.

Typeboolean
Defaultfalse
trackAttributes?boolean

Enable OpenPanel data-attribute tracking.

Typeboolean
Defaultfalse
sessionReplay?object

Configure optional OpenPanel session replay.

Typeobject
filter?function

Filter OpenPanel payloads before sending.

Typefunction
debug?boolean

Enable provider and SDK debug logging.

Typeboolean
Defaultfalse
onDeliveryFailure?function

Called when OpenPanel rejects an event. Logged when omitted.

Typefunction
enabled?boolean

Set to false to disable the provider without removing it.

Typeboolean
Defaulttrue

Server options:

PropType
clientIdstring

OpenPanel project client ID.

Typestring
clientSecretstring

Secret for authenticated server events.

Typestring
apiUrl?string

API URL for OpenPanel Cloud or a self-hosted instance.

Typestring
filter?function

Filter OpenPanel payloads before sending.

Typefunction
debug?boolean

Enable provider and SDK debug logging.

Typeboolean
Defaultfalse
onDeliveryFailure?function

Called when OpenPanel rejects an event. Logged when omitted.

Typefunction
enabled?boolean

Set to false to disable the provider without removing it.

Typeboolean
Defaulttrue

The 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:

  • profileId is the profile the event belongs to. The provider sets it from the event’s user ID when there is one.
  • groups is 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, null or 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

Resources

Was this page helpful?