1 Commits

Author SHA1 Message Date
Carl-Gerhard Lindesvärd
bc3d7b7ea8 docs: add guides 2025-12-15 10:14:40 +01:00
31 changed files with 4491 additions and 11 deletions

View File

@@ -3,9 +3,14 @@ title: Astro
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
import CommonSdkConfig from '@/components/common-sdk-config.mdx';
import WebSdkConfig from '@/components/web-sdk-config.mdx';
<Callout>
Looking for a step-by-step tutorial? Check out the [Astro analytics guide](/guides/astro-analytics).
</Callout>
## Installation
<Steps>

View File

@@ -4,11 +4,16 @@ description: The Express middleware is a basic wrapper around Javascript SDK. It
---
import Link from 'next/link';
import { Callout } from 'fumadocs-ui/components/callout';
import { DeviceIdWarning } from '@/components/device-id-warning';
import { PersonalDataWarning } from '@/components/personal-data-warning';
import CommonSdkConfig from '@/components/common-sdk-config.mdx';
<Callout>
Looking for a step-by-step tutorial? Check out the [Express analytics guide](/guides/express-analytics).
</Callout>
## Installation
```bash

View File

@@ -13,7 +13,7 @@ For most web projects, we recommend starting with one of these:
- **[Script Tag](/docs/sdks/script)** - The quickest way to get started, no build step required
- **[Web SDK](/docs/sdks/web)** - For TypeScript support and more control
- **[Next.js](/docs/sdks/nextjs)** - Optimized for Next.js applications
- **[Next.js](/docs/sdks/nextjs)** - Optimized for Next.js applications | [Setup guide](/guides/nextjs-analytics)
## Web & Browser SDKs
@@ -38,12 +38,12 @@ For most web projects, we recommend starting with one of these:
### Frontend Frameworks
- **[Next.js](/docs/sdks/nextjs)** - Optimized for Next.js
- **[Next.js](/docs/sdks/nextjs)** - Optimized for Next.js | [Setup guide](/guides/nextjs-analytics)
- ✅ Server and client side tracking
- ✅ App Router support
- ✅ Automatic route tracking
- **[Astro](/docs/sdks/astro)** - Astro framework integration
- **[Astro](/docs/sdks/astro)** - Astro framework integration | [Setup guide](/guides/astro-analytics)
- ✅ Static and SSR support
- ✅ Component-based tracking
- ✅ Island architecture compatible
@@ -51,10 +51,10 @@ For most web projects, we recommend starting with one of these:
- **[React](/docs/sdks/react)** - React integration
- 📝 Use Script Tag or Web SDK for now
- **[Vue](/docs/sdks/vue)** - Vue.js integration
- **[Vue](/docs/sdks/vue)** - Vue.js integration | [Setup guide](/guides/vue-analytics)
- 📝 Use Script Tag or Web SDK for now
- **[Remix](/docs/sdks/remix)** - Remix framework integration
- **[Remix](/docs/sdks/remix)** - Remix framework integration | [Setup guide](/guides/remix-analytics)
- 📝 Use Script Tag or Web SDK for now
- **Svelte** - Svelte integration
@@ -71,12 +71,12 @@ For most web projects, we recommend starting with one of these:
## Server-Side SDKs
- **[Node.js (Express)](/docs/sdks/express)** - Express.js middleware
- **[Node.js (Express)](/docs/sdks/express)** - Express.js middleware | [Setup guide](/guides/express-analytics)
- ✅ Request tracking
- ✅ Error tracking
- ✅ Custom middleware support
- **[Python](/docs/sdks/python)** - Python SDK for server-side tracking
- **[Python](/docs/sdks/python)** - Python SDK for server-side tracking | [Setup guide](/guides/python-analytics)
- ✅ Thread-safe
- ✅ Async support
- ✅ Django and Flask compatible
@@ -93,18 +93,18 @@ For most web projects, we recommend starting with one of these:
## Mobile SDKs
- **[React Native](/docs/sdks/react-native)** - Cross-platform mobile analytics
- **[React Native](/docs/sdks/react-native)** - Cross-platform mobile analytics | [Setup guide](/guides/react-native-analytics)
- ✅ iOS and Android support
- ✅ Native performance
- ✅ Offline support
- **[Swift](/docs/sdks/swift)** - Native iOS, macOS, tvOS, and watchOS SDK
- **[Swift](/docs/sdks/swift)** - Native iOS, macOS, tvOS, and watchOS SDK | [Setup guide](/guides/swift-analytics)
- ✅ Apple platform support
- ✅ Swift Package Manager
- ✅ Thread-safe API
- ✅ Automatic lifecycle tracking
- **[Kotlin / Android](/docs/sdks/kotlin)** - Native Android SDK
- **[Kotlin / Android](/docs/sdks/kotlin)** - Native Android SDK | [Setup guide](/guides/kotlin-analytics)
- ✅ Android support
- ✅ System information collection
- ✅ Thread-safe API

View File

@@ -9,6 +9,10 @@ import CommonSdkConfig from '@/components/common-sdk-config.mdx';
The OpenPanel Kotlin SDK allows you to track user behavior in your Kotlin applications. This guide provides instructions for installing and using the Kotlin SDK in your project.
<Callout>
Looking for a step-by-step tutorial? Check out the [Kotlin analytics guide](/guides/kotlin-analytics).
</Callout>
## Installation
<Callout type="warn">

View File

@@ -7,9 +7,14 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
import { DeviceIdWarning } from '@/components/device-id-warning';
import { PersonalDataWarning } from '@/components/personal-data-warning';
import { Callout } from 'fumadocs-ui/components/callout';
import CommonSdkConfig from '@/components/common-sdk-config.mdx';
import WebSdkConfig from '@/components/web-sdk-config.mdx';
<Callout>
Looking for a step-by-step tutorial? Check out the [Next.js analytics guide](/guides/nextjs-analytics).
</Callout>
## Good to know
Keep in mind that all tracking here happens on the client!

View File

@@ -8,6 +8,10 @@ import CommonSdkConfig from '@/components/common-sdk-config.mdx';
The OpenPanel Python SDK allows you to track user behavior in your Python applications. This guide provides instructions for installing and using the Python SDK in your project.
<Callout>
Looking for a step-by-step tutorial? Check out the [Python analytics guide](/guides/python-analytics).
</Callout>
## Installation
<Steps>

View File

@@ -8,8 +8,13 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
import { DeviceIdWarning } from '@/components/device-id-warning';
import { PersonalDataWarning } from '@/components/personal-data-warning';
import { Callout } from 'fumadocs-ui/components/callout';
import CommonSdkConfig from '@/components/common-sdk-config.mdx';
<Callout>
Looking for a step-by-step tutorial? Check out the [React Native analytics guide](/guides/react-native-analytics).
</Callout>
## Installation
<Steps>

View File

@@ -9,6 +9,10 @@ import CommonSdkConfig from '@/components/common-sdk-config.mdx';
The OpenPanel Swift SDK allows you to integrate OpenPanel analytics into your iOS, macOS, tvOS, and watchOS applications.
<Callout>
Looking for a step-by-step tutorial? Check out the [Swift analytics guide](/guides/swift-analytics).
</Callout>
## Features
- Easy-to-use API for tracking events and user properties

View File

@@ -0,0 +1,171 @@
---
title: "How to add analytics to Astro"
description: "Add privacy-first analytics to your Astro site with OpenPanel. Track page views, custom events, and user behavior without cookies."
difficulty: beginner
timeToComplete: 5
date: 2025-12-14
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Add the component to your layout"
anchor: "setup"
- name: "Track custom events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Verify your setup"
anchor: "verify"
---
# How to add analytics to Astro
Adding analytics to your Astro site helps you understand how visitors interact with your content. This guide walks you through setting up OpenPanel to track page views, custom events, and user behavior in about five minutes.
OpenPanel works well with Astro because it's a lightweight script that loads asynchronously and doesn't block your site's rendering. It tracks page views automatically across both static and server-rendered pages, and the component-based API fits naturally into Astro's architecture.
## Prerequisites
- An Astro project
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Your Client ID from the OpenPanel dashboard
## Install the SDK [#install]
Start by adding the OpenPanel Astro package to your project. This package provides Astro components that handle initialization and tracking.
```bash
npm install @openpanel/astro
```
You can also use pnpm or yarn if that's your preference.
## Add the component to your layout [#setup]
The `OpenPanelComponent` initializes tracking and should be placed in your root layout so it loads on every page. Add it inside the `<head>` tag to ensure it initializes before any user interactions.
```astro
---
import { OpenPanelComponent } from '@openpanel/astro';
---
<html>
<head>
<OpenPanelComponent
clientId="your-client-id"
trackScreenViews={true}
trackOutgoingLinks={true}
/>
</head>
<body>
<slot />
</body>
</html>
```
The `trackScreenViews` option automatically records a page view event whenever someone navigates to a new page. The `trackOutgoingLinks` option tracks when visitors click links that take them to external sites. Both are optional but recommended for most sites.
You can also pass a `profileId` prop if you already know the user's identity at render time, and `globalProperties` to attach metadata to every event.
## Track custom events [#events]
Beyond automatic page views, you'll want to track specific interactions that matter to your business. OpenPanel exposes a global `op` function that you can call from any event handler.
```astro
<button onclick="window.op('track', 'button_clicked', {button_name: 'signup'})">
Sign up
</button>
```
The first argument is always `'track'`, the second is your event name, and the third is an optional object of properties you want to attach to the event. Keep event names consistent across your codebase, using snake_case is a good convention.
For elements where you'd rather not write JavaScript, you can use data attributes instead. Any element with a `data-track` attribute will automatically fire an event when clicked.
```astro
<button data-track="button_clicked" data-button-name="signup">
Sign up
</button>
```
Properties are pulled from any `data-*` attributes on the element. The `data-track` value becomes the event name, and other data attributes become event properties.
### Tracking form submissions
Forms are a common tracking target. You can fire an event in the `onsubmit` handler while still allowing the form to submit normally.
```astro
<form onsubmit="window.op('track', 'form_submitted', {form_name: 'contact'}); return true;">
<input type="email" name="email" placeholder="Your email" required />
<button type="submit">Submit</button>
</form>
```
The `return true` ensures the form submission continues after the tracking call.
## Identify users [#identify]
When a user logs in or you otherwise learn their identity, you can associate their activity with a profile. The `IdentifyComponent` handles this declaratively.
```astro
---
import { IdentifyComponent } from '@openpanel/astro';
const user = await getCurrentUser();
---
<IdentifyComponent
profileId={user.id}
firstName={user.firstName}
lastName={user.lastName}
email={user.email}
properties={{
plan: user.plan,
}}
/>
```
Place this component on pages where the user is authenticated. Once identified, all subsequent events from that browser session will be linked to this profile until they clear their browser data or you explicitly clear the identity.
### Setting global properties
Sometimes you want to attach the same properties to every event, like an app version or environment. The `SetGlobalPropertiesComponent` lets you do this once rather than repeating it in every tracking call.
```astro
---
import { SetGlobalPropertiesComponent } from '@openpanel/astro';
---
<SetGlobalPropertiesComponent
properties={{
app_version: '1.0.2',
environment: import.meta.env.MODE,
}}
/>
```
These properties merge with any event-specific properties you pass to individual tracking calls.
## Verify your setup [#verify]
Open your Astro site in the browser and navigate between a few pages. Then open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the Real-time view. You should see page view events appearing within seconds.
If events aren't showing up, open your browser's developer console and look for errors. The most common issues are an incorrect Client ID or an ad blocker preventing the tracking script from loading. You can also check the Network tab to confirm requests are being sent to OpenPanel's servers.
## Next steps
The [Astro SDK reference](/docs/sdks/astro) covers additional configuration options like filtering events and customizing the CDN URL. If you're interested in running OpenPanel on your own infrastructure, the [self-hosting guide](/articles/how-to-self-host-openpanel) walks through the setup process.
<Faqs>
<FaqItem question="Does OpenPanel work with Astro Islands?">
Yes. The OpenPanelComponent is a client-side script that hydrates independently of your island components. It will track interactions across your entire page regardless of which parts are hydrated.
</FaqItem>
<FaqItem question="Can I use OpenPanel with Astro's SSR mode?">
Yes. OpenPanel works with both static and server-rendered Astro sites. The tracking script runs in the browser regardless of how the page was rendered.
</FaqItem>
<FaqItem question="Does OpenPanel use cookies?">
No. OpenPanel uses cookieless tracking by default, which means you don't need cookie consent banners for basic analytics under most privacy regulations including GDPR. Read more about [cookieless analytics](/articles/cookieless-analytics).
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,200 @@
---
title: "How to track e-commerce events and revenue"
description: "Track product views, cart activity, and purchases with OpenPanel to understand your e-commerce funnel and revenue."
difficulty: intermediate
timeToComplete: 10
date: 2025-12-15
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Track product views"
anchor: "track-product-views"
- name: "Track cart activity"
anchor: "track-cart-activity"
- name: "Track purchases and revenue"
anchor: "track-purchases-and-revenue"
- name: "Verify your setup"
anchor: "verify"
---
# How to track e-commerce events and revenue
E-commerce tracking gives you visibility into how users interact with your products and what drives purchases. By the end of this guide, you'll have product views, cart events, and revenue tracking working in your store.
OpenPanel tracks revenue using a dedicated `revenue()` method that links payments to visitor sessions. This lets you see which traffic sources, campaigns, and pages generate the most revenue. You can track from your frontend for quick setup, or from your backend via webhooks for more accurate data.
## Prerequisites
- An OpenPanel account
- Your Client ID from the [dashboard](https://dashboard.openpanel.dev)
- The OpenPanel SDK installed in your project
## Track product views [#track-product-views]
Product view tracking helps you understand which items attract attention. When a user lands on a product page, fire an event with the product details so you can later analyze which products get viewed but not purchased.
```tsx
function ProductPage({ product }) {
const op = useOpenPanel();
useEffect(() => {
op.track('product_viewed', {
product_id: product.id,
product_name: product.name,
product_category: product.category,
product_price: product.price,
currency: 'USD',
});
}, [product.id]);
return <div>{product.name}</div>;
}
```
Include consistent properties across all your product events. Using the same `product_id` format everywhere makes it easy to build reports that connect views to purchases.
## Track cart activity [#track-cart-activity]
Cart events reveal where users drop off in the purchase process. Track both additions and removals to understand cart abandonment patterns.
When a user adds an item, capture the product details along with the current cart value. This gives you context about order sizes at different stages of the funnel.
```tsx
function ProductCard({ product, cart }) {
const op = useOpenPanel();
const handleAddToCart = () => {
op.track('product_added_to_cart', {
product_id: product.id,
product_name: product.name,
product_price: product.price,
quantity: 1,
cart_value: cart.total + product.price,
currency: 'USD',
});
addToCart(product);
};
return (
<button onClick={handleAddToCart}>Add to Cart</button>
);
}
```
Track removals the same way. The symmetry between add and remove events makes it straightforward to calculate net cart changes.
```tsx
const handleRemoveFromCart = (item) => {
op.track('product_removed_from_cart', {
product_id: item.id,
product_name: item.name,
product_price: item.price,
quantity: item.quantity,
currency: 'USD',
});
removeFromCart(item);
};
```
## Track purchases and revenue [#track-purchases-and-revenue]
Revenue tracking is where e-commerce analytics becomes actionable. OpenPanel provides dedicated methods for revenue that link payments back to visitor sessions, so you can see which traffic sources generate the most value.
For frontend tracking, use `pendingRevenue()` before redirecting to checkout, then `flushRevenue()` on your success page. This approach works well when you want to get started quickly without backend changes.
```tsx
async function handleCheckout(cart) {
const op = useOpenPanel();
op.pendingRevenue(cart.total, {
order_items: cart.items.length,
currency: 'USD',
});
window.location.href = await createCheckoutUrl(cart);
}
```
On your success page, flush the pending revenue to send it to OpenPanel.
```tsx
function SuccessPage() {
const op = useOpenPanel();
useEffect(() => {
op.flushRevenue();
}, []);
return <div>Thank you for your purchase!</div>;
}
```
For more accurate tracking, handle revenue in your backend webhook. This ensures you only record completed payments. Pass the visitor's `deviceId` when creating the checkout so you can link the payment back to their session.
```tsx
// Frontend: include deviceId when starting checkout
const deviceId = await op.fetchDeviceId();
const response = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({
deviceId,
items: cart.items,
}),
});
```
In your webhook handler, use that `deviceId` to attribute the revenue.
```javascript
// Backend: webhook handler
export async function POST(req) {
const event = await req.json();
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
op.revenue(session.amount_total, {
deviceId: session.metadata.deviceId,
});
}
return Response.json({ received: true });
}
```
If your users are logged in, you can use `profileId` instead of `deviceId`. This simplifies the flow since you don't need to capture the device ID during checkout.
## Verify your setup [#verify]
Open your OpenPanel dashboard and trigger a few events manually. Add a product to your cart, then check the live events view to confirm the events are arriving with the correct properties.
For revenue tracking, you can test with a small transaction or use your payment provider's test mode. Look for your revenue events in the dashboard and verify the amounts match what you expect.
If events aren't appearing, check that your Client ID is correct and that ad blockers aren't interfering. The browser's network tab can help you confirm requests are being sent.
## Next steps
Once you have basic e-commerce tracking working, you can build purchase funnels to visualize conversion rates at each step. The [revenue tracking documentation](/docs/revenue-tracking) covers advanced patterns like subscription tracking and refunds. For a deeper understanding of attribution, read about how OpenPanel's [cookieless tracking](/articles/cookieless-analytics) works.
To learn more about tracking custom events in general, check out the [track custom events guide](/guides/track-custom-events) which covers event structure, properties, and common patterns.
<Faqs>
<FaqItem question="Does OpenPanel automatically calculate revenue metrics?">
Yes. Once you track revenue using the `revenue()` or `flushRevenue()` methods, OpenPanel calculates totals, averages, and breakdowns by source automatically. You can view these in the revenue section of your dashboard.
</FaqItem>
<FaqItem question="Should I track revenue from the frontend or backend?">
Backend tracking via webhooks is more accurate since it only records completed payments. Frontend tracking is faster to implement but may count abandoned checkouts. For production stores, backend tracking is recommended.
</FaqItem>
<FaqItem question="Can I track subscription revenue?">
Yes. Track subscription events like any other revenue event, including properties for plan name and billing period. The revenue tracking documentation covers subscription-specific patterns in detail.
</FaqItem>
<FaqItem question="How do I handle refunds?">
Track refunds as separate events with the refund amount. This lets you calculate net revenue by subtracting refunds from gross revenue in your reports.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,219 @@
---
title: "How to add analytics to Express"
description: "Add server-side analytics to your Express application with OpenPanel middleware. Track API requests, user actions, and custom events."
difficulty: beginner
timeToComplete: 8
date: 2025-12-15
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Add the middleware"
anchor: "middleware"
- name: "Track events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Verify your setup"
anchor: "verify"
---
# How to add analytics to Express
Server-side analytics gives you reliable event tracking that cannot be blocked by ad blockers or browser extensions. The OpenPanel Express middleware wraps the JavaScript SDK and attaches it to every request, making it simple to track events throughout your application.
OpenPanel is an open-source alternative to Mixpanel and Amplitude. You get powerful analytics with full control over your data, and you can [self-host](/articles/how-to-self-host-openpanel) if privacy requirements demand it.
## Prerequisites
- An Express application
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Your Client ID and Client Secret from the OpenPanel dashboard
Server-side tracking requires a `clientSecret` for authentication since the server cannot rely on browser CORS headers to verify the request origin.
## Install the SDK [#install]
The Express SDK is a lightweight middleware that creates an OpenPanel instance for each request. Install it with npm (pnpm and yarn work too).
```bash
npm install @openpanel/express
```
## Add the middleware [#middleware]
The middleware attaches the OpenPanel SDK to every request as `req.op`. Add it early in your middleware chain so it is available in all your route handlers.
```ts
import express from 'express';
import createOpenpanelMiddleware from '@openpanel/express';
const app = express();
app.use(express.json());
app.use(
createOpenpanelMiddleware({
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
})
);
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
```
You should store your credentials in environment variables rather than hardcoding them. This keeps secrets out of version control and makes it easy to use different credentials in development and production.
```ts
app.use(
createOpenpanelMiddleware({
clientId: process.env.OPENPANEL_CLIENT_ID!,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET!,
})
);
```
The middleware also forwards the client IP address and user-agent from incoming requests, so geographic and device data will be accurate even though events originate from your server.
## Track events [#events]
Once the middleware is in place, you can track events in any route handler by calling `req.op.track()`. The first argument is the event name and the second is an object of properties you want to attach.
```ts
app.post('/signup', async (req, res) => {
const { email, name } = req.body;
req.op.track('user_signed_up', {
email,
name,
source: 'website',
});
const user = await createUser({ email, name });
res.json({ success: true, user });
});
```
You can track any event that matters to your business. Common examples include form submissions, purchases, feature usage, and API errors.
```ts
app.post('/contact', async (req, res) => {
const { email, message } = req.body;
req.op.track('contact_form_submitted', {
email,
message_length: message.length,
});
await sendContactEmail(email, message);
res.json({ success: true });
});
```
### Automatic request tracking
The middleware can automatically track every request if you provide a `trackRequest` function. This is useful for monitoring API usage without manually adding tracking calls to each route.
```ts
app.use(
createOpenpanelMiddleware({
clientId: process.env.OPENPANEL_CLIENT_ID!,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET!,
trackRequest: (url) => url.startsWith('/api/'),
})
);
```
When `trackRequest` returns true, the middleware sends a `request` event with the URL, method, and query parameters.
## Identify users [#identify]
To associate events with specific users, use the `getProfileId` option in the middleware configuration. This function receives the request object and should return the user's ID.
```ts
app.use(
createOpenpanelMiddleware({
clientId: process.env.OPENPANEL_CLIENT_ID!,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET!,
getProfileId: (req) => req.user?.id,
})
);
```
You can also send user profile data with `req.op.identify()`. This updates the user's profile in OpenPanel with properties like name, email, and any custom attributes.
```ts
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
req.op.identify({
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: {
plan: user.plan,
signupDate: user.createdAt,
},
});
req.op.track('user_logged_in', { method: 'email' });
res.json({ success: true, user });
});
```
### Increment profile properties
If you want to track cumulative values on a user profile, like login count or total purchases, use the `increment` method.
```ts
req.op.increment({
profileId: user.id,
property: 'login_count',
value: 1,
});
```
## Verify your setup [#verify]
Start your Express server and trigger a few events by making requests to your endpoints. Open the [OpenPanel dashboard](https://dashboard.openpanel.dev) and navigate to the Real-time view to see events as they arrive.
If events are not appearing, check your server logs for error responses from OpenPanel. Verify that both `clientId` and `clientSecret` are correct and that the middleware is added before your routes.
## TypeScript support
The Express SDK automatically extends the `Request` interface to include `req.op`. If your TypeScript configuration does not pick this up, you can extend the interface manually in a declaration file.
```ts
import { OpenPanel } from '@openpanel/express';
declare global {
namespace Express {
export interface Request {
op: OpenPanel;
}
}
}
```
## Next steps
The [Express SDK reference](/docs/sdks/express) covers all available options and methods. If you are using a different Node.js framework, the [Node.js tracking guide](/guides/nodejs-analytics) shows how to use the base SDK directly. For comparing OpenPanel to other analytics tools, see the [Mixpanel alternative](/compare/mixpanel-alternative) page.
<Faqs>
<FaqItem question="Why do I need a clientSecret for server-side tracking?">
Server-side tracking requires authentication because requests come from your server, not a browser with CORS restrictions. The clientSecret ensures events are properly authenticated and prevents unauthorized tracking from other sources.
</FaqItem>
<FaqItem question="Can I track events asynchronously?">
Yes. The OpenPanel SDK sends events asynchronously by default. Events are queued and dispatched in the background, so tracking calls will not block your route handlers or slow down response times.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with cookieless tracking and data minimization. Server-side tracking gives you full control over what data you collect. With self-hosting, you eliminate international data transfer concerns entirely.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,266 @@
---
title: "How to add analytics to Android apps"
description: "Add privacy-first analytics to Android applications using OpenPanel's Kotlin SDK. Track events, identify users, and analyze behavior."
type: guide
difficulty: intermediate
timeToComplete: 10
date: 2025-12-15
lastUpdated: 2025-12-15
steps:
- name: "Add the dependency"
anchor: "install"
- name: "Initialize OpenPanel"
anchor: "setup"
- name: "Track events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Track screen views"
anchor: "screenviews"
- name: "Verify your setup"
anchor: "verify"
---
# How to add analytics to Android apps
This guide walks you through adding OpenPanel analytics to an Android application using the Kotlin SDK. You'll learn how to track events, identify users, and monitor screen views across your app.
OpenPanel works well for Android apps because it provides a lightweight, privacy-focused SDK that handles offline queuing and automatic system information collection. Unlike web SDKs, native apps require a client secret for authentication since CORS headers aren't available.
## Prerequisites
- An Android project (minSdkVersion 21+)
- An OpenPanel account
- Your Client ID and Client Secret from the [dashboard](https://dashboard.openpanel.dev)
## Add the dependency [#install]
Start by adding the OpenPanel SDK to your app's `build.gradle.kts` file. The SDK is available through standard Gradle dependency management.
```kotlin
dependencies {
implementation("dev.openpanel:openpanel:0.0.1")
}
```
The Kotlin SDK is currently in development, so check the [GitHub repository](https://github.com/Openpanel-dev/kotlin-sdk) for the latest version number before adding it to your project.
## Initialize OpenPanel [#setup]
Before you can track events, you need to initialize OpenPanel in your Application class. This ensures the SDK is available throughout your app and can properly manage its lifecycle.
```kotlin
import android.app.Application
import dev.openpanel.OpenPanel
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
OpenPanel.create(
context = this,
options = OpenPanel.Options(
clientId = "YOUR_CLIENT_ID",
clientSecret = "YOUR_CLIENT_SECRET"
)
)
}
}
```
You also need to register your Application class in the Android manifest so the system knows to use it.
```xml
<application
android:name=".MyApplication"
...>
</application>
```
If you're using dependency injection with Hilt or Dagger, you can provide OpenPanel as a singleton instead. This approach integrates better with modern Android architecture patterns.
```kotlin
import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dev.openpanel.OpenPanel
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideOpenPanel(@ApplicationContext context: Context): OpenPanel {
return OpenPanel.create(
context,
OpenPanel.Options(
clientId = "YOUR_CLIENT_ID",
clientSecret = "YOUR_CLIENT_SECRET"
)
)
}
}
```
## Track events [#events]
Once OpenPanel is initialized, you can track events anywhere in your app by getting the SDK instance and calling the `track` method. Each event has a name and an optional map of properties.
```kotlin
import dev.openpanel.OpenPanel
class MainActivity : AppCompatActivity() {
private lateinit var op: OpenPanel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
op = OpenPanel.getInstance(this)
findViewById<Button>(R.id.signupButton).setOnClickListener {
op.track(
"button_clicked",
mapOf(
"button_name" to "signup",
"button_location" to "hero"
)
)
}
}
}
```
The SDK is thread-safe, so you can call `track` from any thread without additional synchronization. This is particularly useful when tracking events from background operations or coroutines.
If you want to attach properties to every event automatically, use `setGlobalProperties`. This is helpful for including app version, build number, or environment information.
```kotlin
op.setGlobalProperties(
mapOf(
"app_version" to BuildConfig.VERSION_NAME,
"build_number" to BuildConfig.VERSION_CODE.toString(),
"platform" to "Android"
)
)
```
## Identify users [#identify]
When a user logs in or you have information about who they are, call `identify` to associate their profile with tracked events. This enables user-level analytics and cohort analysis in your dashboard.
```kotlin
op.identify(
user.id,
mapOf(
"firstName" to user.firstName,
"lastName" to user.lastName,
"email" to user.email,
"plan" to user.plan
)
)
```
When a user logs out, clear their data so subsequent events aren't attributed to them.
```kotlin
fun logout() {
op.clear()
}
```
You can also increment numeric properties on user profiles. This is useful for tracking things like login counts or credits without needing to know the current value.
```kotlin
op.increment(user.id, "login_count", 1)
```
## Track screen views [#screenviews]
Screen view tracking helps you understand navigation patterns and which parts of your app get the most attention. The simplest approach is to create a base activity that tracks screen views automatically.
```kotlin
import androidx.appcompat.app.AppCompatActivity
import dev.openpanel.OpenPanel
abstract class BaseActivity : AppCompatActivity() {
protected lateinit var op: OpenPanel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
op = OpenPanel.getInstance(this)
}
override fun onResume() {
super.onResume()
op.track(
"screen_view",
mapOf("screen_name" to this::class.simpleName ?: "Unknown")
)
}
}
```
For Jetpack Compose, use a `LaunchedEffect` to track when a composable screen appears.
```kotlin
import androidx.compose.runtime.LaunchedEffect
import dev.openpanel.OpenPanel
@Composable
fun MainScreen(op: OpenPanel) {
LaunchedEffect(Unit) {
op.track(
"screen_view",
mapOf("screen_name" to "MainScreen")
)
}
// Your composable content
}
```
If you want the SDK to automatically track app lifecycle events like `app_opened` and `app_closed`, enable automatic tracking during initialization.
```kotlin
OpenPanel.create(
context,
OpenPanel.Options(
clientId = "YOUR_CLIENT_ID",
clientSecret = "YOUR_CLIENT_SECRET",
automaticTracking = true
)
)
```
## Verify your setup [#verify]
Run your Android app in an emulator or on a physical device and interact with a few screens and buttons. Open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the real-time view to see events arriving.
If events aren't appearing, check Logcat for error messages from the SDK. The most common issues are incorrect credentials or missing the `clientSecret` parameter. You can also use Android Studio's Network Profiler to verify that requests are being sent to OpenPanel's servers.
## Next steps
The [full Kotlin SDK reference](/docs/sdks/kotlin) covers additional options like event filtering and verbose logging. If you're building for multiple platforms, the [React Native guide](/guides/react-native-analytics) shows how to share analytics code across iOS and Android.
<Faqs>
<FaqItem question="Why do I need a client secret for native apps?">
Native apps can't use CORS headers for authentication like web apps do. The client secret provides server-side authentication to ensure your events are properly validated. Keep this secret secure and never expose it in client-side web code.
</FaqItem>
<FaqItem question="Does OpenPanel work with Jetpack Compose?">
Yes. OpenPanel works with both traditional Views and Jetpack Compose. For Compose, use LaunchedEffect to track screen views when a composable appears, as shown in the screen tracking section above.
</FaqItem>
<FaqItem question="Can I track events when the device is offline?">
Yes. The SDK queues events locally when there's no network connection and sends them automatically when connectivity is restored. You won't lose events if users go through tunnels or airplane mode.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with data minimization and full support for data subject rights. With self-hosting, you also eliminate international data transfer concerns entirely. See the [cookieless analytics guide](/articles/cookieless-analytics) for more details.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,232 @@
---
title: "How to migrate from Google Analytics to OpenPanel"
description: "Switch from Google Analytics to OpenPanel in under an hour. Learn how to map GA4 events, set up parallel tracking, and gain privacy-first analytics."
difficulty: intermediate
timeToComplete: 45
date: 2025-12-15
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install OpenPanel"
anchor: "install"
- name: "Map your GA4 events"
anchor: "map-events"
- name: "Run parallel tracking"
anchor: "parallel"
- name: "Verify and compare data"
anchor: "verify"
- name: "Remove Google Analytics"
anchor: "remove"
---
# How to migrate from Google Analytics to OpenPanel
Migrating from Google Analytics to OpenPanel takes about 45 minutes for most websites. You'll end up with privacy-first analytics that doesn't require cookie consent banners, a simpler interface than GA4, and full ownership of your data.
OpenPanel uses a similar event-based tracking model to GA4, which makes the migration straightforward. The biggest difference is that OpenPanel is designed for privacy by default, using cookieless tracking that doesn't require consent under most privacy regulations.
## Prerequisites
- Existing Google Analytics 4 (GA4) setup
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Access to your website's code or Google Tag Manager
## Install OpenPanel [#install]
The first step is adding OpenPanel to your website alongside your existing Google Analytics installation. You'll run both in parallel during the migration to ensure nothing is lost.
Add the OpenPanel script to your website's HTML, replacing `YOUR_CLIENT_ID` with the client ID from your OpenPanel dashboard.
```html
<script>
window.op=window.op||function(){var n=[];return new Proxy(function(){arguments.length&&n.push([].slice.call(arguments))},{get:function(t,r){return"q"===r?n:function(){n.push([r].concat([].slice.call(arguments)))}} ,has:function(t,r){return"q"===r}}) }();
window.op('init', {
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
</script>
<script src="https://openpanel.dev/op1.js" defer async></script>
```
The `trackScreenViews` option automatically tracks page views, similar to GA4's automatic page view tracking. The `trackOutgoingLinks` option tracks clicks to external domains, and `trackAttributes` enables declarative tracking via `data-track` attributes on HTML elements.
If you're using Google Tag Manager, create a new Custom HTML tag, paste the script above, set the trigger to All Pages, and publish your container.
For projects using a build system, you can also install the npm package with `npm install @openpanel/web`. See the full [Script Tag SDK documentation](/docs/sdks/script) for detailed configuration options.
## Map your GA4 events [#map-events]
OpenPanel uses a `track()` API that closely mirrors GA4's event structure. Most of your existing event tracking can be migrated with minimal changes.
### Page views
GA4 tracks page views automatically, and so does OpenPanel when you set `trackScreenViews: true`. If you need to track page views manually for single-page applications or specific scenarios, use the `screen_view` event.
```js
window.op('track', 'screen_view', {
path: window.location.pathname,
title: document.title,
});
```
### Custom events
The syntax for custom events is nearly identical between GA4 and OpenPanel. Your GA4 event that looks like this:
```js
gtag('event', 'button_click', {
button_name: 'signup',
button_location: 'hero'
});
```
Becomes this in OpenPanel:
```js
window.op('track', 'button_click', {
button_name: 'signup',
button_location: 'hero'
});
```
The event name and properties carry over directly. OpenPanel doesn't have the reserved event restrictions that GA4 has, so you can use any event name that makes sense for your application.
### User identification
GA4 uses `set user_properties` for user identification, while OpenPanel uses a dedicated `identify` method. This gives you richer user profiles and session history.
```js
window.op('identify', {
profileId: 'user_123', // Required
firstName: 'Joe',
lastName: 'Doe',
email: 'joe@doe.com',
properties: {
tier: 'premium',
},
});
```
The `profileId` is required and should be a unique identifier for the user, typically their user ID from your database.
### E-commerce events
GA4's e-commerce events like `purchase` and `add_to_cart` map directly to OpenPanel. The property structure stays the same.
```js
window.op('track', 'purchase', {
transaction_id: 'T12345',
value: 99.99,
currency: 'USD',
items: [{
item_id: 'SKU123',
item_name: 'Widget',
price: 99.99,
quantity: 1
}]
});
```
You can use the same event names and property structure you're already using in GA4, or take the opportunity to simplify your naming conventions.
## Run parallel tracking [#parallel]
Running both analytics tools simultaneously for one to two weeks lets you compare data and catch any gaps before fully migrating. Create a wrapper function that sends events to both platforms.
```js
function trackEvent(eventName, properties) {
// Send to GA4
if (typeof gtag !== 'undefined') {
gtag('event', eventName, properties);
}
// Send to OpenPanel
if (window.op) {
window.op('track', eventName, properties);
}
}
// Use this wrapper for all event tracking
trackEvent('button_click', { button_name: 'signup' });
```
Replace your existing `gtag()` calls with this wrapper function. This approach ensures you're capturing the same events in both systems and makes the final migration a simple change to the wrapper.
## Verify and compare data [#verify]
After a week of parallel tracking, compare the data between GA4 and OpenPanel to ensure everything is being captured correctly. Check both systems' real-time views to confirm events are flowing in.
In OpenPanel, navigate to the real-time view to see events as they happen. Compare the event counts and property values against GA4's Realtime report. Small discrepancies (within 5-10%) are normal due to differences in how each platform handles bot filtering and session attribution.
Pay particular attention to custom events and user identification. Open a few user profiles in OpenPanel to verify that the `identify` calls are linking events correctly across sessions.
If you're seeing significant discrepancies, check that your wrapper function is being called everywhere, that there are no JavaScript errors preventing tracking, and that both scripts are loading on all pages.
## Remove Google Analytics [#remove]
Once you've verified that OpenPanel is tracking correctly and you're comfortable with the data, remove Google Analytics from your site.
If you added the GA4 script directly to your HTML, remove the gtag.js script and initialization code.
```html
<!-- Remove this entire block -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
```
If you're using Google Tag Manager, disable or delete the GA4 Configuration tag and publish your container. You can keep GTM for other purposes if needed.
Update your wrapper function to only send to OpenPanel.
```js
function trackEvent(eventName, properties) {
if (window.op) {
window.op('track', eventName, properties);
}
}
```
Since OpenPanel doesn't use cookies by default, you may be able to remove your cookie consent banner entirely. This depends on whether you have other cookies or tracking scripts that require consent. Check the [cookieless analytics guide](/articles/cookieless-analytics) for details on GDPR compliance without cookie banners.
## What changes after migration
Moving from GA4 to OpenPanel means gaining some things and losing others. You'll get privacy-first tracking without cookies, full ownership of your data (especially if you [self-host](/articles/how-to-self-host-openpanel)), and a simpler interface for day-to-day analytics.
On the other hand, you won't be able to import historical data from GA4 since Google doesn't provide an easy export mechanism. If you rely heavily on Google Ads conversion tracking, you'll need to either keep GA4 running for that specific use case or use Google Ads' standalone conversion tracking pixel.
GA4's attribution modeling is more sophisticated than OpenPanel's, so if you depend on multi-touch attribution for ad spend optimization, consider your requirements carefully before fully migrating.
## Next steps
Once you're up and running with OpenPanel, explore the [funnel analysis](/articles/how-to-create-a-funnel) feature to track user journeys through your conversion paths. If you're interested in maximum data privacy and ownership, the self-hosting guide walks through running OpenPanel on your own infrastructure.
For framework-specific setup instructions, check out our guides:
- [Next.js analytics guide](/guides/nextjs-analytics) for Next.js applications
- [React analytics guide](/guides/react-analytics) for React applications
- [Node.js analytics guide](/guides/nodejs-analytics) for server-side tracking
- [Python analytics guide](/guides/python-analytics) for Python applications
<Faqs>
<FaqItem question="Can I import historical data from Google Analytics?">
Google Analytics doesn't provide an easy way to export historical event data. Most teams start fresh with OpenPanel, which gives you cleaner, privacy-compliant data going forward. Your GA4 data remains accessible in Google Analytics for the retention period you've configured.
</FaqItem>
<FaqItem question="How do I handle GA4 custom dimensions?">
OpenPanel uses properties instead of dimensions. Map your GA4 custom dimensions directly to OpenPanel event properties. For example, if you had a `user_type` custom dimension in GA4, include it as a property in your track calls: `window.op('track', 'event_name', { user_type: 'premium' })`.
</FaqItem>
<FaqItem question="Does OpenPanel use cookies?">
No. OpenPanel uses cookieless tracking by default. This means you don't need cookie consent banners for basic analytics under most privacy regulations, including GDPR and PECR.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with cookieless tracking, data minimization, and full support for data subject rights. With self-hosting, you also eliminate international data transfer concerns entirely.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,251 @@
---
title: "How to migrate from Mixpanel to OpenPanel"
description: "Switch from Mixpanel to OpenPanel in under 2 hours with this step-by-step migration guide."
difficulty: intermediate
timeToComplete: 90
date: 2025-12-15
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Export Mixpanel data"
anchor: "export"
- name: "Install OpenPanel SDK"
anchor: "install"
- name: "Map your events"
anchor: "map-events"
- name: "Import historical data"
anchor: "import"
- name: "Run in parallel"
anchor: "parallel"
- name: "Remove Mixpanel"
anchor: "remove"
---
# How to migrate from Mixpanel to OpenPanel
Migrating analytics tools sounds painful, but OpenPanel's API closely mirrors Mixpanel's patterns. Most implementations translate with minimal code changes. You'll map your existing events, optionally import historical data, and run both tools in parallel before cutting over.
OpenPanel gives you better privacy controls with cookieless tracking by default, transparent pricing without surprise overages, and the option to self-host. The migration typically takes 1-2 hours for code changes, plus a week or two of parallel tracking to verify data consistency.
## Prerequisites
- An existing Mixpanel implementation
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Access to your Mixpanel project settings for data export
- Access to your application codebase
## Export Mixpanel data [#export]
Before migrating, you'll want to export your historical data from Mixpanel. This step is optional if you're okay starting fresh, but most teams prefer to preserve their historical events for comparison.
Mixpanel provides an export API that returns events in JSON format. You'll need your API credentials from Project Settings in Mixpanel.
```bash
curl https://mixpanel.com/api/2.0/export \
-u YOUR_API_SECRET: \
-d 'from_date=2024-01-01' \
-d 'to_date=2024-12-15' \
-d 'event=["event1","event2"]' \
> mixpanel_export.json
```
Alternatively, you can export through Mixpanel's dashboard by navigating to Data Management, then Export. Select your date range and events, then download the file. Keep this export handy for the import step later.
## Install OpenPanel SDK [#install]
Install the OpenPanel SDK alongside Mixpanel. You'll remove Mixpanel later after verifying the migration.
For web applications, install the web SDK. This handles browser-specific features like automatic page view tracking and outgoing link tracking.
```bash
npm install @openpanel/web
```
For server-side applications or Node.js, use the core SDK instead. This version requires a client secret and is meant for trusted environments.
```bash
npm install @openpanel/sdk
```
If you're using Next.js, there's a dedicated package that handles both client and server tracking with proper component integration. Check the [SDK documentation](/docs/sdks/nextjs) for framework-specific setup.
## Map your events [#map-events]
OpenPanel's API follows similar patterns to Mixpanel, with a few naming convention differences. Mixpanel uses Title Case for events and properties, while OpenPanel uses snake_case.
Here's how basic event tracking translates. In Mixpanel, you might track a button click like this:
```js
mixpanel.track('Button Clicked', {
'Button Name': 'Sign Up',
'Button Location': 'Hero'
});
```
The equivalent in OpenPanel uses snake_case naming:
```js
op.track('button_clicked', {
button_name: 'Sign Up',
button_location: 'Hero'
});
```
User identification works similarly, but OpenPanel uses a structured object instead of separate method calls. Mixpanel's pattern splits identify and people.set:
```js
mixpanel.identify('user_123');
mixpanel.people.set({
'$email': 'user@example.com',
'$name': 'John Doe',
'Plan': 'Premium'
});
```
OpenPanel combines this into a single identify call with built-in fields for common properties:
```js
op.identify({
profileId: 'user_123',
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe',
properties: {
plan: 'Premium'
}
});
```
For super properties (global properties that attach to every event), Mixpanel uses `register` or `registerSuperProperties`. OpenPanel calls this `setGlobalProperties`, but the concept is identical:
```js
op.setGlobalProperties({
app_version: '1.0.0',
environment: 'production'
});
```
Incrementing user properties works the same way, just with different method names:
```js
op.increment({
profileId: user.id,
property: 'login_count',
value: 1
});
```
## Import historical data [#import]
OpenPanel can import your Mixpanel export using the SDK's server-side client. You'll need to transform the Mixpanel format slightly since Mixpanel uses Unix timestamps and different property names.
```js
import { OpenPanel } from '@openpanel/sdk';
const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
});
const mixpanelEvents = await loadMixpanelExport();
for (const event of mixpanelEvents) {
op.track(event.event, event.properties, {
profileId: event.distinct_id,
timestamp: new Date(event.time * 1000),
});
}
```
For user profiles, map Mixpanel's dollar-prefixed properties to OpenPanel's built-in fields:
```js
const mixpanelUsers = await loadMixpanelUsers();
for (const user of mixpanelUsers) {
op.identify({
profileId: user.distinct_id,
email: user.$email,
firstName: user.$first_name,
lastName: user.$last_name,
properties: {
plan: user.Plan,
signupDate: user['Signup Date'],
},
});
}
```
For large migrations, contact support. OpenPanel has tooling to help with bulk imports.
## Run in parallel [#parallel]
Before removing Mixpanel, run both tools simultaneously for 1-2 weeks. This lets you verify that event counts match and user identification is working correctly.
Create a simple wrapper function that sends events to both platforms:
```js
function trackEvent(eventName, properties) {
if (typeof mixpanel !== 'undefined') {
mixpanel.track(eventName, properties);
}
if (op) {
op.track(eventName, properties);
}
}
trackEvent('button_clicked', { button_name: 'Sign Up' });
```
After a week, compare Mixpanel's Live View with OpenPanel's real-time dashboard. Check that event counts are close (they won't be exact due to timing differences), user profiles are being created correctly, and any funnels you've set up show similar conversion rates.
## Remove Mixpanel [#remove]
Once you've verified data consistency, remove the Mixpanel SDK from your project. For npm packages, uninstall the dependency:
```bash
npm uninstall mixpanel-browser
```
Then search your codebase for any remaining `mixpanel.*` calls and replace them with the OpenPanel equivalents. If you were using the wrapper function from the parallel tracking step, you can now remove it and call OpenPanel directly.
```js
op.track('button_clicked', { button_name: 'Sign Up' });
```
Don't forget to remove any Mixpanel script tags if you were loading their SDK via CDN.
## Verify your setup [#verify]
After removing Mixpanel, monitor OpenPanel for a few days to ensure everything is tracking correctly. Check the real-time view to confirm events are flowing, verify that user profiles show the expected properties, and test any funnels or reports you've configured.
If events aren't appearing, double-check that you're using the correct client ID and that the SDK is initialized before any tracking calls. The browser console will show network requests to `api.openpanel.dev` if tracking is working.
## Next steps
The [SDK documentation](/docs) covers advanced features like custom event properties, server-side tracking, and framework-specific integrations. If you're coming from Mixpanel because of pricing concerns, you might also want to explore [self-hosting](/articles/how-to-self-host-openpanel) for complete control over your data and costs.
For framework-specific setup instructions, check out our guides:
- [Next.js analytics guide](/guides/nextjs-analytics) for Next.js applications
- [React analytics guide](/guides/react-analytics) for React applications
- [Express analytics guide](/guides/express-analytics) for Express.js applications
- [Python analytics guide](/guides/python-analytics) for Python applications
<Faqs>
<FaqItem question="Can I import my Mixpanel historical data?">
Yes. OpenPanel can import Mixpanel exports using the SDK's server-side client. For large migrations with millions of events, contact support for assistance with bulk imports.
</FaqItem>
<FaqItem question="Will my Mixpanel funnels work in OpenPanel?">
You'll need to recreate funnels in OpenPanel's dashboard, but the concepts map directly. Select the same events in sequence and configure your date range and filters. Conversion logic works the same way.
</FaqItem>
<FaqItem question="How long does the full migration take?">
Most migrations take 1-2 hours for code changes. Plan for an additional 1-2 weeks of parallel tracking to verify data consistency before fully switching over.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel uses cookieless tracking by default, which means you don't need cookie consent banners for basic analytics under most privacy regulations. With self-hosting, you also eliminate international data transfer concerns entirely.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,255 @@
---
title: "How to add analytics to Next.js"
description: "Add privacy-first analytics to your Next.js app in under 5 minutes with OpenPanel."
difficulty: beginner
timeToComplete: 8
date: 2025-12-15
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Add OpenPanel to your layout"
anchor: "setup"
- name: "Track custom events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Verify your setup"
anchor: "verify"
---
# How to add analytics to Next.js
This guide walks you through adding OpenPanel to a Next.js application. By the end, you'll have automatic page view tracking, custom event tracking, and user identification working in your app.
OpenPanel works with both the App Router and Pages Router. It uses cookieless tracking by default, so you won't need cookie consent banners for basic analytics. If you're looking for a privacy-focused alternative to Mixpanel or Google Analytics, this is a straightforward setup.
## Prerequisites
- A Next.js project (App Router or Pages Router)
- An OpenPanel account
- Your Client ID from the [OpenPanel dashboard](https://dashboard.openpanel.dev/onboarding)
## Install the SDK [#install]
Start by installing the OpenPanel Next.js package. This SDK is a thin wrapper around the core OpenPanel library with Next.js-specific components for easier integration.
```bash
npm install @openpanel/nextjs
```
If you prefer pnpm or yarn, those work too.
## Add OpenPanel to your layout [#setup]
Before tracking anything, you need to add the OpenPanelComponent to your app's root. This loads the tracking script and makes the SDK available throughout your application.
For App Router projects, add the component to your root layout file. The component should be placed inside the body tag, and setting `trackScreenViews` to true enables automatic page view tracking as users navigate your app.
```tsx
// app/layout.tsx
import { OpenPanelComponent } from '@openpanel/nextjs';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<OpenPanelComponent
clientId="your-client-id"
trackScreenViews={true}
/>
{children}
</body>
</html>
);
}
```
For Pages Router projects, the setup is nearly identical. Add the component to your `_app.tsx` file instead.
```tsx
// pages/_app.tsx
import { OpenPanelComponent } from '@openpanel/nextjs';
import type { AppProps } from 'next/app';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<OpenPanelComponent
clientId="your-client-id"
trackScreenViews={true}
/>
<Component {...pageProps} />
</>
);
}
```
You can also enable `trackOutgoingLinks` to automatically track when users click external links, or `trackAttributes` to track elements with data attributes.
## Track custom events [#events]
Page views only tell part of the story. To understand how users interact with your product, you'll want to track custom events like button clicks, form submissions, or feature usage.
In client components, use the `useOpenPanel` hook. This gives you access to the tracking methods anywhere in your component tree.
```tsx
'use client';
import { useOpenPanel } from '@openpanel/nextjs';
export function SignupButton() {
const op = useOpenPanel();
return (
<button
type="button"
onClick={() => op.track('signup_clicked', { location: 'header' })}
>
Sign up
</button>
);
}
```
For server-side tracking, you'll need to create an SDK instance with your client secret. This is useful for tracking events in API routes, webhooks, or server actions.
```tsx
// lib/op.ts
import { OpenPanel } from '@openpanel/nextjs';
export const op = new OpenPanel({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
});
```
Never expose your client secret on the client side. Keep it in server-only code.
With the instance created, you can track events from anywhere on the server.
```tsx
// app/api/webhook/route.ts
import { op } from '@/lib/op';
export async function POST(request: Request) {
const data = await request.json();
op.track('webhook_received', {
source: data.source,
event_type: data.type,
});
return Response.json({ success: true });
}
```
If you're running on Vercel or another serverless platform, wrap your tracking calls with `waitUntil` to ensure events are sent before the function terminates.
## Identify users [#identify]
Anonymous tracking is useful, but identifying users unlocks more valuable insights. You can track behavior across sessions, segment users by properties, and build cohort analyses.
In client components, call `identify` after a user logs in or when you have their information available.
```tsx
'use client';
import { useOpenPanel } from '@openpanel/nextjs';
import { useEffect } from 'react';
export function UserProfile({ user }) {
const op = useOpenPanel();
useEffect(() => {
op.identify({
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: {
plan: user.plan,
},
});
}, [user]);
return <div>Welcome, {user.firstName}!</div>;
}
```
If you already have user data on the server, the `IdentifyComponent` is a cleaner approach. It renders nothing visible but handles identification when the component mounts.
```tsx
// app/dashboard/layout.tsx
import { IdentifyComponent } from '@openpanel/nextjs';
export default async function DashboardLayout({ children }) {
const user = await getCurrentUser();
return (
<>
<IdentifyComponent
profileId={user.id}
firstName={user.firstName}
lastName={user.lastName}
email={user.email}
properties={{
plan: user.plan,
}}
/>
{children}
</>
);
}
```
## Verify your setup [#verify]
Open your Next.js app in the browser and navigate between a few pages. Then open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the real-time view. You should see page view events appearing within seconds.
If events aren't showing up, check the browser console for errors. The most common issues are an incorrect client ID or ad blockers intercepting requests. You can solve the ad blocker problem by proxying events through your own server.
To set up a proxy, create a catch-all API route that forwards requests to OpenPanel.
```tsx
// app/api/[...op]/route.ts
import { createRouteHandler } from '@openpanel/nextjs/server';
export const { GET, POST } = createRouteHandler();
```
Then update your OpenPanelComponent to use the proxy endpoint.
```tsx
<OpenPanelComponent
apiUrl="/api/op"
cdnUrl="/api/op/op1.js"
clientId="your-client-id"
trackScreenViews={true}
/>
```
This routes all tracking requests through your domain, making them invisible to browser extensions that block third-party analytics.
## Next steps
The [Next.js SDK reference](/docs/sdks/nextjs) covers additional features like global properties, event filtering, and incrementing user properties. If you're interested in understanding how OpenPanel handles privacy, read our article on [cookieless analytics](/articles/cookieless-analytics).
<Faqs>
<FaqItem question="Does OpenPanel work with both App Router and Pages Router?">
Yes. The setup is nearly identical for both. Add OpenPanelComponent to your root layout for App Router or to _app.tsx for Pages Router. All tracking features work the same way regardless of which router you use.
</FaqItem>
<FaqItem question="Does OpenPanel use cookies?">
No. OpenPanel uses cookieless tracking by default. This means you don't need cookie consent banners for basic analytics under most privacy regulations, including GDPR and PECR.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with cookieless tracking, data minimization, and full support for data subject rights. With self-hosting, you also eliminate international data transfer concerns entirely.
</FaqItem>
<FaqItem question="How do I avoid ad blockers?">
Set up the proxy route handler as shown in the verification section. This routes tracking requests through your own domain, which ad blockers don't typically block.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,324 @@
---
title: "How to Track Events with Node.js"
description: "Add server-side analytics to your Node.js application. Track events, identify users, and analyze behavior with OpenPanel's JavaScript SDK."
difficulty: beginner
timeToComplete: 7
date: 2025-12-14
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Initialize OpenPanel"
anchor: "setup"
- name: "Track events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Verify your setup"
anchor: "verify"
---
## Introduction
Server-side analytics gives you complete control over what data you track and ensures events are never blocked by ad blockers. OpenPanel's JavaScript SDK works perfectly in Node.js environments, allowing you to track events from your backend, API routes, and background jobs.
OpenPanel is an open-source alternative to Mixpanel and Amplitude, giving you powerful server-side analytics without compromising user privacy.
## Prerequisites
- Node.js project set up
- OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Your Client ID and Client Secret from the OpenPanel dashboard
> **Important:** Server-side tracking requires a `clientSecret` for authentication.
## Step 1: Install the SDK
Install the OpenPanel JavaScript SDK:
```bash
npm install @openpanel/sdk
```
Or with pnpm:
```bash
pnpm install @openpanel/sdk
```
## Step 2: Initialize OpenPanel
Create an OpenPanel instance with your credentials:
```js title="lib/op.js"
import { OpenPanel } from '@openpanel/sdk';
export const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET', // Required for server-side
});
```
> **Security Note:** Never expose your `clientSecret` in client-side code. Use environment variables in production.
### Using environment variables
```js title="lib/op.js"
import { OpenPanel } from '@openpanel/sdk';
export const op = new OpenPanel({
clientId: process.env.OPENPANEL_CLIENT_ID,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET,
});
```
```bash title=".env"
OPENPANEL_CLIENT_ID=your-client-id
OPENPANEL_CLIENT_SECRET=your-client-secret
```
## Step 3: Track events
Track events throughout your Node.js application:
```js title="routes/api/signup.js"
import { op } from '@/lib/op';
export async function POST(request) {
const { email, name } = await request.json();
// Track signup event
op.track('user_signed_up', {
email,
name,
source: 'website',
});
// Your signup logic
const user = await createUser({ email, name });
return Response.json({ success: true, user });
}
```
### Track API requests
```js title="middleware/analytics.js"
import { op } from '@/lib/op';
export function analyticsMiddleware(req, res, next) {
// Track API request
op.track('api_request', {
method: req.method,
path: req.path,
status_code: res.statusCode,
});
next();
}
```
### Track background jobs
```js title="jobs/send-email.js"
import { op } from '@/lib/op';
export async function sendEmailJob(userId, emailData) {
try {
await sendEmail(emailData);
op.track('email_sent', {
user_id: userId,
email_type: emailData.type,
success: true,
});
} catch (error) {
op.track('email_failed', {
user_id: userId,
email_type: emailData.type,
error: error.message,
});
}
}
```
## Step 4: Identify users
To identify users and track their behavior:
```js title="routes/api/login.js"
import { op } from '@/lib/op';
export async function POST(request) {
const { email, password } = await request.json();
const user = await authenticateUser(email, password);
// Identify the user
op.identify({
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: {
plan: user.plan,
signupDate: user.createdAt,
},
});
// Track login event
op.track('user_logged_in', {
user_id: user.id,
method: 'email',
});
return Response.json({ success: true, user });
}
```
### Track user actions
```js title="routes/api/purchase.js"
import { op } from '@/lib/op';
export async function POST(request) {
const { userId, productId, amount } = await request.json();
// Track purchase event
op.track('purchase_completed', {
user_id: userId,
product_id: productId,
amount,
currency: 'USD',
}, {
profileId: userId, // Associate event with user
});
// Your purchase logic
await processPurchase(userId, productId, amount);
return Response.json({ success: true });
}
```
## Verify your setup
1. Make requests to your API endpoints that trigger events
2. Run background jobs that track events
3. Open your [OpenPanel dashboard](https://dashboard.openpanel.dev)
4. Check the Real-time view to see events coming in
**Not seeing events?**
- Check server logs for errors
- Verify your Client ID and Client Secret are correct
- Ensure `clientSecret` is provided (required for server-side)
- Check network requests in your server logs
## Common Patterns
### Track webhook events
```js title="routes/api/webhook.js"
import { op } from '@/lib/op';
export async function POST(request) {
const payload = await request.json();
op.track('webhook_received', {
source: payload.source,
event_type: payload.type,
timestamp: new Date().toISOString(),
});
// Process webhook
await processWebhook(payload);
return Response.json({ success: true });
}
```
### Set global properties
```js title="lib/op.js"
import { OpenPanel } from '@openpanel/sdk';
export const op = new OpenPanel({
clientId: process.env.OPENPANEL_CLIENT_ID,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET,
globalProperties: {
app_version: process.env.APP_VERSION,
environment: process.env.NODE_ENV,
server_region: process.env.SERVER_REGION,
},
});
```
### Track errors
```js title="middleware/error-handler.js"
import { op } from '@/lib/op';
export function errorHandler(err, req, res, next) {
// Track error event
op.track('error_occurred', {
error_message: err.message,
error_stack: err.stack,
path: req.path,
method: req.method,
});
// Your error handling logic
res.status(500).json({ error: 'Internal server error' });
}
```
### Increment user properties
```js
import { op } from '@/lib/op';
// Increment login count
op.increment({
profileId: user.id,
property: 'login_count',
value: 1,
});
```
## Serverless & Vercel
If you're using serverless functions (like Vercel), use `waitUntil` to ensure events are tracked before the function completes:
```js title="api/track.js"
import { waitUntil } from '@vercel/functions';
import { op } from '@/lib/op';
export default async function handler(req, res) {
// Returns response immediately while keeping function alive
waitUntil(op.track('important_event', { foo: 'bar' }));
return res.status(200).json({ success: true });
}
```
## Next Steps
- [Full JavaScript SDK reference](/docs/sdks/javascript)
- [How to Add Analytics to Express](/guides/express-analytics)
- [Compare OpenPanel to Mixpanel](/compare/mixpanel-alternative)
## FAQ
### Why do I need a clientSecret for server-side tracking?
Server-side tracking requires authentication since we can't use CORS headers. The `clientSecret` ensures your events are properly authenticated and prevents unauthorized tracking.
### Can I track events asynchronously?
Yes! The OpenPanel SDK tracks events asynchronously by default. Events are queued and sent in the background, so they won't block your application.
### Is OpenPanel GDPR compliant?
Yes! OpenPanel is designed with privacy in mind. Server-side tracking gives you complete control over what data you collect. Check out our [cookieless analytics guide](/articles/cookieless-analytics) for more information.

View File

@@ -0,0 +1,208 @@
---
title: "How to add analytics to Python"
description: "Add server-side analytics to your Python application with OpenPanel's Python SDK."
type: guide
difficulty: beginner
timeToComplete: 7
date: 2025-12-15
lastUpdated: 2025-12-15
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Initialize OpenPanel"
anchor: "initialize"
- name: "Track events"
anchor: "track-events"
- name: "Identify users"
anchor: "identify-users"
- name: "Verify your setup"
anchor: "verify"
---
# How to add analytics to Python
This guide walks you through adding server-side analytics to any Python application. You'll install the OpenPanel SDK, configure it with your credentials, track custom events, and identify users.
Server-side tracking gives you complete control over what data you collect and ensures events are never blocked by browser extensions or ad blockers. The Python SDK works with Django, Flask, FastAPI, and any other Python framework or script.
## Prerequisites
- A Python project
- An OpenPanel account
- Your Client ID and Client Secret from the dashboard
## Install the SDK [#install]
Start by installing the OpenPanel package from PyPI.
```bash
pip install openpanel
```
If you're using Poetry, you can run `poetry add openpanel` instead.
## Initialize OpenPanel [#initialize]
Create a shared module for your OpenPanel instance. This approach lets you import the same configured instance throughout your application.
```python
# lib/op.py
import os
from openpanel import OpenPanel
op = OpenPanel(
client_id=os.getenv("OPENPANEL_CLIENT_ID"),
client_secret=os.getenv("OPENPANEL_CLIENT_SECRET")
)
```
Server-side tracking requires both a client ID and client secret for authentication. Add these to your environment variables.
```bash
# .env
OPENPANEL_CLIENT_ID=your-client-id
OPENPANEL_CLIENT_SECRET=your-client-secret
```
You can also pass global properties during initialization. These properties are included with every event automatically.
```python
op = OpenPanel(
client_id=os.getenv("OPENPANEL_CLIENT_ID"),
client_secret=os.getenv("OPENPANEL_CLIENT_SECRET"),
global_properties={
"app_version": "1.0.0",
"environment": os.getenv("ENVIRONMENT", "production")
}
)
```
## Track events [#track-events]
Use the `track` method to record events. The first argument is the event name, and the second is an optional dictionary of properties.
```python
from lib.op import op
# Track a simple event
op.track("button_clicked")
# Track with properties
op.track("purchase_completed", {
"product_id": "123",
"price": 99.99,
"currency": "USD"
})
```
When tracking events in request handlers, you'll typically pull data from the request and track it alongside your business logic. Here's an example in a Django view.
```python
from lib.op import op
def signup_view(request):
if request.method == 'POST':
email = request.POST.get('email')
name = request.POST.get('name')
user = create_user(email, name)
op.track("user_signed_up", {
"email": email,
"source": "website"
})
return JsonResponse({"success": True})
```
The same pattern works in Flask and FastAPI. Import your OpenPanel instance and call `track` wherever you need to record an event.
You can also track events from background tasks. This is useful for monitoring async jobs, email delivery, and scheduled tasks.
```python
from celery import shared_task
from lib.op import op
@shared_task
def send_email_task(user_id, email_type):
try:
send_email(user_id, email_type)
op.track("email_sent", {
"user_id": user_id,
"email_type": email_type
})
except Exception as e:
op.track("email_failed", {
"user_id": user_id,
"error": str(e)
})
```
## Identify users [#identify-users]
The `identify` method associates a user profile with their ID. Call this after authentication to link subsequent events to that user.
```python
from lib.op import op
def login_view(request):
user = authenticate_user(request)
op.identify(user.id, {
"firstName": user.first_name,
"lastName": user.last_name,
"email": user.email,
"tier": user.plan
})
op.track("user_logged_in", {"method": "email"})
return JsonResponse({"success": True})
```
To track an event for a specific user without calling identify first, pass the `profile_id` parameter to the track method.
```python
op.track("purchase_completed", {
"product_id": "123",
"amount": 99.99
}, profile_id="user_456")
```
You can increment numeric properties on user profiles. This is useful for counters like login count or total purchases.
```python
op.increment({
"profile_id": "user_456",
"property": "login_count",
"value": 1
})
```
## Verify your setup [#verify]
Run your Python application and trigger a few events. Open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and navigate to the real-time view. You should see your events appearing within seconds.
If events aren't showing up, check that your client ID and client secret are correct. Server-side tracking won't work without the client secret. Review your application logs for any error messages from the SDK.
## Next steps
The [Python SDK reference](/docs/sdks/python) covers additional configuration options like event filtering and disabling tracking. If you're also tracking client-side events, you might want to read about [cookieless analytics](/articles/cookieless-analytics) to understand how OpenPanel handles privacy without cookies.
<Faqs>
<FaqItem question="Why do I need a client secret for server-side tracking?">
Server-side tracking can't rely on browser-based authentication like CORS headers. The client secret authenticates your requests and prevents unauthorized parties from sending events to your project.
</FaqItem>
<FaqItem question="Is the SDK thread-safe?">
Yes. The OpenPanel Python SDK is thread-safe and you can use a single instance across multiple threads. This makes it safe to use in threaded web servers and background task workers.
</FaqItem>
<FaqItem question="Does OpenPanel work with async Python?">
Yes. The SDK tracks events asynchronously by default, so it won't block your async request handlers. Events are queued and sent in the background.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with cookieless tracking, data minimization, and support for data subject rights. Server-side tracking gives you complete control over what data you collect.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,295 @@
---
title: "How to add analytics to React"
description: "Add privacy-first analytics to your React application with OpenPanel's Web SDK. Track page views, custom events, and user behavior."
difficulty: beginner
timeToComplete: 8
date: 2025-12-14
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Create an OpenPanel instance"
anchor: "setup"
- name: "Track page views"
anchor: "pageviews"
- name: "Track custom events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Verify your setup"
anchor: "verify"
---
Adding analytics to your React application helps you understand how users interact with your app. OpenPanel's Web SDK works seamlessly with React and React Router, giving you page view tracking, custom events, and user identification without the complexity of dedicated React bindings.
OpenPanel is an open-source alternative to Mixpanel and Google Analytics. It provides powerful insights while respecting user privacy through cookieless tracking by default.
## Prerequisites
- A React project (Create React App, Vite, or similar)
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Your Client ID from the OpenPanel dashboard
## Install the SDK [#install]
The OpenPanel Web SDK is a lightweight package that works in any JavaScript environment, including React. Install it using npm, and pnpm or yarn work the same way.
```bash
npm install @openpanel/web
```
## Create an OpenPanel instance [#setup]
Create a dedicated file for your OpenPanel instance. This keeps your analytics configuration centralized and makes the instance easy to import throughout your application.
```ts title="src/lib/op.ts"
import { OpenPanel } from '@openpanel/web';
export const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```
The `trackScreenViews` option automatically tracks page views when the URL changes. This works with React Router, TanStack Router, and other client-side routing solutions. The `trackAttributes` option enables declarative tracking using `data-track` attributes on HTML elements.
### Using environment variables
For production applications, store your Client ID in environment variables. Vite and Create React App handle these differently.
```ts title="src/lib/op.ts (Vite)"
import { OpenPanel } from '@openpanel/web';
export const op = new OpenPanel({
clientId: import.meta.env.VITE_OPENPANEL_CLIENT_ID,
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```
```ts title="src/lib/op.ts (Create React App)"
import { OpenPanel } from '@openpanel/web';
export const op = new OpenPanel({
clientId: process.env.REACT_APP_OPENPANEL_CLIENT_ID,
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```
### Initialize on app load
Import the OpenPanel instance in your app's entry point to ensure it initializes when your application loads. This is all you need to start tracking page views automatically.
```tsx title="src/main.tsx"
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './lib/op'; // Initialize OpenPanel
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
```
## Track page views [#pageviews]
With `trackScreenViews: true`, OpenPanel automatically tracks page views when the browser's URL changes. This works out of the box with React Router and other routing libraries that use the History API.
If you need to track page views manually or want more control over the data sent with each view, you can create a component that listens to route changes.
```tsx title="src/components/PageTracker.tsx"
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { op } from '../lib/op';
export function PageTracker() {
const location = useLocation();
useEffect(() => {
op.track('screen_view', {
path: location.pathname,
search: location.search,
});
}, [location.pathname, location.search]);
return null;
}
```
Add this component near the root of your application, inside your router context. Note that if you use this approach, you should set `trackScreenViews: false` in your OpenPanel configuration to avoid duplicate tracking.
## Track custom events [#events]
Import the OpenPanel instance wherever you need to track events. The `track` method accepts an event name and an optional properties object.
```tsx title="src/components/SignupButton.tsx"
import { op } from '../lib/op';
export function SignupButton() {
const handleClick = () => {
op.track('button_clicked', {
button_name: 'signup',
button_location: 'hero',
});
};
return (
<button type="button" onClick={handleClick}>
Sign Up
</button>
);
}
```
### Track form submissions
Form tracking helps you understand conversion rates and identify where users drop off.
```tsx title="src/components/ContactForm.tsx"
import { useState } from 'react';
import { op } from '../lib/op';
export function ContactForm() {
const [email, setEmail] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
op.track('form_submitted', {
form_name: 'contact',
form_location: 'homepage',
});
// Your form submission logic
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
/>
<button type="submit">Submit</button>
</form>
);
}
```
### Use data attributes for declarative tracking
The Web SDK supports declarative tracking using `data-track` attributes. This is useful for simple click tracking without writing JavaScript.
```tsx
<button
data-track="button_clicked"
data-track-button_name="signup"
data-track-button_location="hero"
>
Sign Up
</button>
```
When a user clicks this button, OpenPanel automatically sends a `button_clicked` event with the specified properties. This requires `trackAttributes: true` in your configuration.
## Identify users [#identify]
Once a user logs in or provides identifying information, call `identify` to associate their activity with a profile. This enables user-level analytics and cohort analysis.
```tsx title="src/hooks/useAuth.ts"
import { useEffect } from 'react';
import { op } from '../lib/op';
export function useAuth(user: User | null) {
useEffect(() => {
if (user) {
op.identify({
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: {
plan: user.plan,
signupDate: user.createdAt,
},
});
}
}, [user]);
}
```
### Clear user data on logout
When users log out, clear the stored profile data to ensure subsequent events aren't associated with the previous user.
```tsx title="src/components/LogoutButton.tsx"
import { op } from '../lib/op';
export function LogoutButton({ onLogout }: { onLogout: () => void }) {
const handleLogout = () => {
op.clear();
onLogout();
};
return <button onClick={handleLogout}>Logout</button>;
}
```
### Set global properties
Properties set with `setGlobalProperties` are included with every event. This is useful for app version tracking, feature flags, or A/B test variants.
```tsx title="src/App.tsx"
import { useEffect } from 'react';
import { op } from './lib/op';
export default function App() {
useEffect(() => {
op.setGlobalProperties({
app_version: '1.0.0',
environment: import.meta.env.MODE,
});
}, []);
return <div>{/* Your app */}</div>;
}
```
## Verify your setup [#verify]
Open your React app in the browser and navigate between a few pages. Interact with elements that trigger custom events. Then open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the Real-time view to see events appearing.
If events aren't appearing, check the browser console for errors. Verify your Client ID is correct and ensure ad blockers aren't blocking requests to the OpenPanel API. The Network tab in your browser's developer tools can help you confirm that requests are being sent.
## Next steps
The Web SDK has additional features like property incrementing and event filtering. Read the full [Web SDK documentation](/docs/sdks/web) for the complete API reference.
For server-side tracking in your React application's API routes, see the [Node.js analytics guide](/guides/nodejs-analytics) which covers the `@openpanel/sdk` package.
<Faqs>
<FaqItem question="Does OpenPanel work with React Router?">
Yes. OpenPanel automatically tracks page views when the URL changes, which works with React Router and other routing libraries that use the History API. Set `trackScreenViews: true` in your configuration.
</FaqItem>
<FaqItem question="Can I use OpenPanel with class components?">
Yes. Import the OpenPanel instance directly and call its methods. The instance is framework-agnostic and works in any JavaScript context.
</FaqItem>
<FaqItem question="Does OpenPanel use cookies?">
No. OpenPanel uses cookieless tracking by default. This means you don't need cookie consent banners for basic analytics under most privacy regulations, including GDPR and PECR.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with cookieless tracking, data minimization, and full support for data subject rights. With self-hosting, you also eliminate international data transfer concerns entirely.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,216 @@
---
title: "How to add analytics to React Native"
description: "Add privacy-first analytics to your React Native app with OpenPanel. Track screen views, custom events, and user behavior across iOS and Android."
difficulty: intermediate
timeToComplete: 10
date: 2025-12-14
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Initialize OpenPanel"
anchor: "setup"
- name: "Track screen views"
anchor: "screenviews"
- name: "Track custom events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Verify your setup"
anchor: "verify"
---
# How to add analytics to React Native
Adding analytics to your React Native app helps you understand how users interact with your product across both iOS and Android. This guide walks you through setting up OpenPanel to track screen views, custom events, and user behavior in about ten minutes.
OpenPanel works well with React Native because it handles the complexities of native environments for you. The SDK automatically captures app version, build number, and install referrer on Android. It also queues events when the device is offline and sends them when connectivity returns.
## Prerequisites
- A React Native project (Expo or bare React Native)
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Your Client ID and Client Secret from the OpenPanel dashboard
## Install the SDK [#install]
Start by adding the OpenPanel React Native package and its Expo dependencies. The SDK relies on `expo-application` for version information and `expo-constants` for user-agent data.
```bash
npm install @openpanel/react-native
npx expo install expo-application expo-constants
```
You can also use pnpm or yarn if that's your preference. The Expo packages work in both Expo and bare React Native projects.
## Initialize OpenPanel [#setup]
Create an OpenPanel instance that you'll use throughout your app. React Native requires a `clientSecret` for authentication since native apps can't use CORS headers like web browsers do.
```typescript
import { OpenPanel } from '@openpanel/react-native';
export const op = new OpenPanel({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
});
```
Put this in a shared file like `lib/op.ts` so you can import it from anywhere in your app. The SDK automatically sets up listeners for app state changes and configures default properties like version and build number.
## Track screen views [#screenviews]
Screen view tracking requires hooking into your navigation library. The approach differs slightly depending on whether you use Expo Router or React Navigation.
If you're using Expo Router, add the tracking call to your root layout component. The `usePathname` hook gives you the current route, and `useSegments` provides the route segments which can be useful for grouping dynamic routes together.
```typescript
import { usePathname, useSegments } from 'expo-router';
import { useEffect } from 'react';
import { op } from '@/lib/op';
export default function RootLayout() {
const pathname = usePathname();
const segments = useSegments();
useEffect(() => {
op.screenView(pathname, {
segments: segments.join('/'),
});
}, [pathname, segments]);
return (
// Your layout JSX
);
}
```
For React Navigation, track screen changes using the navigation container's state change callbacks. Create a navigation ref and pass handlers to `onReady` and `onStateChange`.
```tsx
import { createNavigationContainerRef, NavigationContainer } from '@react-navigation/native';
import { op } from '@/lib/op';
const navigationRef = createNavigationContainerRef();
export function App() {
const handleNavigationStateChange = () => {
const current = navigationRef.getCurrentRoute();
if (current) {
op.screenView(current.name, {
params: current.params,
});
}
};
return (
<NavigationContainer
ref={navigationRef}
onReady={handleNavigationStateChange}
onStateChange={handleNavigationStateChange}
>
{/* Your navigators */}
</NavigationContainer>
);
}
```
The `onReady` callback fires on initial load, and `onStateChange` fires on subsequent navigations. This ensures you capture every screen view including the first one.
## Track custom events [#events]
Beyond screen views, you'll want to track specific interactions that matter to your business. Call `op.track` with an event name and optional properties wherever you need to record user actions.
```tsx
import { op } from '@/lib/op';
function SignupButton() {
const handlePress = () => {
op.track('button_clicked', {
button_name: 'signup',
screen: 'home',
});
// Continue with signup logic
};
return <Button onPress={handlePress} title="Sign Up" />;
}
```
Keep event names consistent across your codebase. Using snake_case and a verb-noun pattern like `button_clicked` or `form_submitted` makes your analytics easier to query later.
You can also set global properties that attach to every event. This is useful for metadata like app version or user plan that you want on all events without passing them manually each time.
```tsx
op.setGlobalProperties({
app_version: '1.0.0',
platform: Platform.OS,
});
```
## Identify users [#identify]
When a user logs in, associate their activity with a profile so you can track their behavior across sessions and devices. Call `op.identify` with the user's ID and any profile properties you want to store.
```tsx
import { useEffect } from 'react';
import { op } from '@/lib/op';
function UserProfile({ user }) {
useEffect(() => {
if (user) {
op.identify({
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: {
plan: user.plan,
},
});
}
}, [user]);
return <Text>Welcome, {user?.firstName}!</Text>;
}
```
When the user logs out, call `op.clear()` to reset the identity. This ensures subsequent events aren't incorrectly attributed to the previous user.
```tsx
function handleLogout() {
op.clear();
// Continue with logout logic
}
```
## Verify your setup [#verify]
Run your app and navigate between a few screens to trigger screen view events. Interact with any buttons or forms you've added custom tracking to. Then open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the Real-time view. You should see events appearing within seconds.
If events aren't showing up, open React Native Debugger or your browser's developer tools (if using Expo web) and check the console for errors. The most common issue is an incorrect Client ID or Client Secret. Make sure both values match what's shown in your OpenPanel dashboard, and remember that React Native requires the client secret whereas web SDKs do not.
## Next steps
The [React Native SDK reference](/docs/sdks/react-native) covers additional configuration options. If you're also building for web, the [JavaScript SDK](/docs/sdks/javascript) shares the same tracking API so your event naming can stay consistent across platforms.
If you're building native iOS or Android apps without React Native, check out the [Swift analytics guide](/guides/swift-analytics) for iOS apps or the [Kotlin analytics guide](/guides/kotlin-analytics) for Android apps.
<Faqs>
<FaqItem question="Why does React Native require a client secret?">
React Native apps can't use CORS headers for authentication like web browsers do. The client secret provides server-side authentication to ensure your events come from a legitimate source. Keep it bundled in your app binary rather than fetching it from an API.
</FaqItem>
<FaqItem question="Does OpenPanel work with Expo?">
Yes. The SDK uses Expo packages for version information and user-agent data, but these work in bare React Native projects too. No special configuration is needed for Expo-managed workflows.
</FaqItem>
<FaqItem question="What happens when the device is offline?">
OpenPanel queues events locally and sends them when the app comes back online. Events won't be lost if the user temporarily loses connectivity.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with data minimization and full support for data subject rights. With self-hosting, you also eliminate international data transfer concerns. Read more about [cookieless analytics](/articles/cookieless-analytics).
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,387 @@
---
title: "How to add analytics to Remix"
description: "Add privacy-first analytics to your Remix application with OpenPanel's Web SDK. Track page views, custom events, and user behavior."
difficulty: beginner
timeToComplete: 8
date: 2025-12-14
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Create an OpenPanel instance"
anchor: "setup"
- name: "Track page views"
anchor: "pageviews"
- name: "Track custom events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Verify your setup"
anchor: "verify"
---
Adding analytics to your Remix application helps you understand how users interact with your app. OpenPanel's Web SDK works seamlessly with Remix's client-side navigation, providing automatic page view tracking, custom events, and user identification.
OpenPanel is an open-source alternative to Mixpanel and Google Analytics. It delivers powerful insights while respecting user privacy through cookieless tracking by default.
## Prerequisites
- A Remix project
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Your Client ID from the OpenPanel dashboard
## Install the SDK [#install]
The OpenPanel Web SDK is a lightweight package that works in any JavaScript environment. Install it using npm, and pnpm or yarn work the same way.
```bash
npm install @openpanel/web
```
## Create an OpenPanel instance [#setup]
Create a dedicated file for your OpenPanel instance. Since this runs in the browser, place it in your app directory and ensure it only executes on the client.
```ts title="app/lib/op.client.ts"
import { OpenPanel } from '@openpanel/web';
export const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```
The `.client.ts` suffix tells Remix this module should only run in the browser. The `trackScreenViews` option automatically tracks page views when the URL changes, which works with Remix's client-side navigation. The `trackAttributes` option enables declarative tracking using `data-track` attributes.
### Using environment variables
For production applications, pass your Client ID from the server to the client using Remix's loader pattern.
```ts title="app/root.tsx"
import { json } from '@remix-run/node';
import type { LoaderFunctionArgs } from '@remix-run/node';
export async function loader({ request }: LoaderFunctionArgs) {
return json({
ENV: {
OPENPANEL_CLIENT_ID: process.env.OPENPANEL_CLIENT_ID,
},
});
}
```
Then initialize OpenPanel with the environment variable in a client component.
### Initialize in root.tsx
Import and initialize OpenPanel in your root component using a `useEffect` hook to ensure it only runs on the client.
```tsx title="app/root.tsx"
import { useEffect } from 'react';
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
useLoaderData,
} from '@remix-run/react';
export default function App() {
const { ENV } = useLoaderData<typeof loader>();
useEffect(() => {
// Dynamic import ensures this only runs on the client
import('./lib/op.client').then(({ op }) => {
// OpenPanel is now initialized and tracking
});
}, []);
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
```
## Track page views [#pageviews]
With `trackScreenViews: true`, OpenPanel automatically tracks page views when the browser's URL changes. This works with Remix's client-side navigation using `<Link>` components.
If you need more control over page view tracking or want to include additional route metadata, you can create a component that uses Remix's `useLocation` hook.
```tsx title="app/components/PageTracker.tsx"
import { useEffect } from 'react';
import { useLocation } from '@remix-run/react';
export function PageTracker() {
const location = useLocation();
useEffect(() => {
import('../lib/op.client').then(({ op }) => {
op.track('screen_view', {
path: location.pathname,
search: location.search,
});
});
}, [location.pathname, location.search]);
return null;
}
```
Add this component to your root layout. If you use this approach, set `trackScreenViews: false` in your OpenPanel configuration to avoid duplicate tracking.
## Track custom events [#events]
Import the OpenPanel instance in your components to track events. Since the SDK only works in the browser, use dynamic imports or ensure your tracking code runs in `useEffect` hooks.
```tsx title="app/components/SignupButton.tsx"
export function SignupButton() {
const handleClick = () => {
import('../lib/op.client').then(({ op }) => {
op.track('button_clicked', {
button_name: 'signup',
button_location: 'hero',
});
});
};
return (
<button type="button" onClick={handleClick}>
Sign Up
</button>
);
}
```
### Create a tracking hook
For cleaner code, create a custom hook that handles the dynamic import.
```tsx title="app/hooks/useOpenPanel.ts"
import { useCallback } from 'react';
import type { OpenPanel } from '@openpanel/web';
type TrackFn = OpenPanel['track'];
type IdentifyFn = OpenPanel['identify'];
export function useTrack() {
return useCallback<TrackFn>((name, properties) => {
import('../lib/op.client').then(({ op }) => {
op.track(name, properties);
});
}, []);
}
export function useIdentify() {
return useCallback<IdentifyFn>((payload) => {
import('../lib/op.client').then(({ op }) => {
op.identify(payload);
});
}, []);
}
```
Now your components become cleaner.
```tsx title="app/components/SignupButton.tsx"
import { useTrack } from '../hooks/useOpenPanel';
export function SignupButton() {
const track = useTrack();
const handleClick = () => {
track('button_clicked', {
button_name: 'signup',
button_location: 'hero',
});
};
return (
<button type="button" onClick={handleClick}>
Sign Up
</button>
);
}
```
### Track form submissions
Remix encourages using form actions for data mutations. You can track form submissions in your action handlers or on the client.
```tsx title="app/routes/contact.tsx"
import { Form } from '@remix-run/react';
import { useTrack } from '../hooks/useOpenPanel';
export default function Contact() {
const track = useTrack();
const handleSubmit = () => {
track('form_submitted', {
form_name: 'contact',
form_location: 'contact-page',
});
};
return (
<Form method="post" onSubmit={handleSubmit}>
<input type="email" name="email" placeholder="Your email" required />
<button type="submit">Submit</button>
</Form>
);
}
```
### Use data attributes for declarative tracking
The Web SDK supports declarative tracking using `data-track` attributes. This is useful for simple click tracking without writing JavaScript.
```tsx
<button
data-track="button_clicked"
data-track-button_name="signup"
data-track-button_location="hero"
>
Sign Up
</button>
```
When a user clicks this button, OpenPanel automatically sends a `button_clicked` event with the specified properties. This requires `trackAttributes: true` in your configuration.
## Identify users [#identify]
Once a user logs in, call `identify` to associate their activity with a profile. In Remix, you typically have user data available from a loader.
```tsx title="app/routes/dashboard.tsx"
import { useEffect } from 'react';
import { useLoaderData } from '@remix-run/react';
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { getUser } from '../lib/auth.server';
export async function loader({ request }: LoaderFunctionArgs) {
const user = await getUser(request);
return json({ user });
}
export default function Dashboard() {
const { user } = useLoaderData<typeof loader>();
useEffect(() => {
if (user) {
import('../lib/op.client').then(({ op }) => {
op.identify({
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: {
plan: user.plan,
},
});
});
}
}, [user]);
return <div>Welcome, {user?.firstName}!</div>;
}
```
### Clear user data on logout
When users log out, clear the stored profile data to ensure subsequent events aren't associated with the previous user.
```tsx title="app/components/LogoutButton.tsx"
import { Form } from '@remix-run/react';
export function LogoutButton() {
const handleClick = () => {
import('../lib/op.client').then(({ op }) => {
op.clear();
});
};
return (
<Form method="post" action="/logout">
<button type="submit" onClick={handleClick}>
Logout
</button>
</Form>
);
}
```
## Server-side tracking [#server-side]
For tracking events in loaders, actions, or API routes, use the `@openpanel/sdk` package instead of the web SDK. Server-side tracking requires a client secret.
```ts title="app/lib/op.server.ts"
import { OpenPanel } from '@openpanel/sdk';
export const op = new OpenPanel({
clientId: process.env.OPENPANEL_CLIENT_ID!,
clientSecret: process.env.OPENPANEL_CLIENT_SECRET!,
});
```
```ts title="app/routes/api.webhook.ts"
import type { ActionFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { op } from '../lib/op.server';
export async function action({ request }: ActionFunctionArgs) {
const payload = await request.json();
op.track('webhook_received', {
source: payload.source,
event_type: payload.type,
});
return json({ success: true });
}
```
## Verify your setup [#verify]
Open your Remix app in the browser and navigate between a few pages. Interact with elements that trigger custom events. Then open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the Real-time view to see events appearing.
If events aren't appearing, check the browser console for errors. Verify your Client ID is correct and ensure ad blockers aren't blocking requests to the OpenPanel API. The Network tab in your browser's developer tools can help you confirm that requests are being sent.
## Next steps
The Web SDK has additional features like property incrementing and event filtering. Read the full [Web SDK documentation](/docs/sdks/web) for the complete API reference.
For comprehensive server-side tracking, see the [Node.js analytics guide](/guides/nodejs-analytics) which covers the `@openpanel/sdk` package in detail.
<Faqs>
<FaqItem question="Does OpenPanel work with Remix's SSR?">
Yes. OpenPanel's client-side SDK tracks events in the browser after hydration. For server-side events in loaders and actions, use the `@openpanel/sdk` package with your client secret.
</FaqItem>
<FaqItem question="Why do I need a client secret for server-side tracking?">
Server-side tracking requires authentication since we can't use CORS headers. The client secret ensures your events are properly authenticated and prevents unauthorized tracking.
</FaqItem>
<FaqItem question="Does OpenPanel use cookies?">
No. OpenPanel uses cookieless tracking by default. This means you don't need cookie consent banners for basic analytics under most privacy regulations, including GDPR and PECR.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with cookieless tracking, data minimization, and full support for data subject rights. With self-hosting, you also eliminate international data transfer concerns entirely.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,263 @@
---
title: "How to Add Analytics to Swift Apps"
description: "Add privacy-first analytics to your iOS, macOS, tvOS, and watchOS apps with OpenPanel's Swift SDK."
difficulty: beginner
timeToComplete: 10
date: 2025-12-15
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Add Swift package"
anchor: "install"
- name: "Initialize OpenPanel"
anchor: "setup"
- name: "Track events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Track screen views"
anchor: "screenviews"
- name: "Verify your setup"
anchor: "verify"
---
## Introduction
Understanding how users interact with your native Apple apps requires solid analytics. OpenPanel's Swift SDK gives you event tracking, user identification, and screen view analytics across iOS, macOS, tvOS, and watchOS platforms.
Since native apps can't rely on CORS headers for authentication like web apps do, the Swift SDK uses a client secret for secure server-side authentication. This makes it suitable for production apps where you need reliable, privacy-respecting analytics. OpenPanel is an open-source alternative to Mixpanel and Amplitude that you can self-host for complete data ownership.
## Prerequisites
- Xcode project set up (iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+)
- OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Your Client ID and Client Secret from the OpenPanel dashboard
> **Important:** Native app tracking requires a `clientSecret` for authentication.
## Step 1: Add Swift package
The OpenPanel Swift SDK is distributed through Swift Package Manager. Open your project in Xcode, then go to File and select Add Packages. Enter the repository URL and click Add Package.
```
https://github.com/Openpanel-dev/swift-sdk
```
If you're working with a Package.swift file directly, add OpenPanel as a dependency instead.
```swift
dependencies: [
.package(url: "https://github.com/Openpanel-dev/swift-sdk")
]
```
## Step 2: Initialize OpenPanel
Before tracking any events, you need to initialize the SDK when your app launches. This should happen early in your app's lifecycle so the SDK is available throughout your application.
For UIKit apps, add the initialization to your AppDelegate's `application(_:didFinishLaunchingWithOptions:)` method.
```swift title="AppDelegate.swift"
import UIKit
import OpenPanel
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
OpenPanel.initialize(options: .init(
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET"
))
return true
}
}
```
For SwiftUI apps, initialize the SDK in your App struct's initializer.
```swift title="MyApp.swift"
import SwiftUI
import OpenPanel
@main
struct MyApp: App {
init() {
OpenPanel.initialize(options: .init(
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET"
))
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
### Automatic lifecycle tracking
You can enable automatic lifecycle tracking by setting `automaticTracking: true`. This will track `app_opened` and `app_closed` events for you.
```swift
OpenPanel.initialize(options: .init(
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
automaticTracking: true
))
```
## Step 3: Track events
Once initialized, you can track events anywhere in your app by calling `OpenPanel.track`. Each event has a name and optional properties that provide additional context.
```swift title="SignupButton.swift"
import SwiftUI
import OpenPanel
struct SignupButton: View {
var body: some View {
Button("Sign Up") {
OpenPanel.track(
name: "button_clicked",
properties: [
"button_name": "signup",
"button_location": "hero"
]
)
}
}
}
```
Properties can be any key-value pairs relevant to the event. Common patterns include tracking form submissions, purchases, and feature usage. Keep event names consistent across your app by using snake_case and being descriptive about what action occurred.
### Set global properties
If you have properties that should be sent with every event, set them once using `setGlobalProperties`. This is useful for app version, build number, or device information.
```swift
OpenPanel.setGlobalProperties([
"app_version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "",
"platform": UIDevice.current.systemName
])
```
## Step 4: Identify users
When a user signs in, call `identify` to associate their events with their profile. This enables you to track user journeys across sessions and understand individual user behavior.
```swift title="AuthService.swift"
OpenPanel.identify(payload: IdentifyPayload(
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: [
"plan": user.plan,
"signup_date": user.createdAt.ISO8601Format()
]
))
```
The `profileId` should be a unique identifier for the user, typically from your authentication system. Additional properties like `firstName`, `lastName`, and `email` help you recognize users in your dashboard.
### Clear user data on logout
When a user logs out, call `clear` to reset the local state. This ensures subsequent events aren't incorrectly attributed to the previous user.
```swift
func logout() {
OpenPanel.clear()
}
```
### Increment user properties
You can also increment numeric properties on user profiles. This is useful for tracking counts like logins or purchases without needing to fetch and update the current value.
```swift
OpenPanel.increment(payload: IncrementPayload(
profileId: user.id,
property: "login_count"
))
```
## Step 5: Track screen views
Tracking screen views helps you understand how users navigate through your app. In SwiftUI, use the `onAppear` modifier to track when a view becomes visible.
```swift title="HomeView.swift"
struct HomeView: View {
var body: some View {
VStack {
Text("Home")
}
.onAppear {
OpenPanel.track(
name: "screen_view",
properties: ["screen_name": "HomeScreen"]
)
}
}
}
```
In UIKit, override `viewDidAppear` in your view controllers.
```swift title="HomeViewController.swift"
class HomeViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
OpenPanel.track(
name: "screen_view",
properties: [
"screen_name": "HomeScreen",
"screen_class": String(describing: type(of: self))
]
)
}
}
```
The OpenPanel SDK is designed to be thread-safe. You can call its methods from any thread without additional synchronization.
## Verify your setup
Run your app in the simulator or on a physical device and perform some actions. Navigate between screens and tap a few buttons to generate events. Then open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the real-time view.
**Not seeing events?**
- Check the Xcode console for any error messages
- Verify that your Client ID and Client Secret are correct
- Confirm that you included the `clientSecret` parameter (required for native apps)
- Test with a stable network connection first
## Next steps
The [Swift SDK reference](/docs/sdks/swift) covers additional configuration options like event filtering and disabling tracking. If you're building a cross-platform mobile app, the [React Native analytics guide](/guides/react-native-analytics) shows how to set up OpenPanel in that environment.
## FAQ
### Why do I need a clientSecret for native apps?
Native apps can't use CORS headers for authentication like web applications can. The clientSecret provides secure server-side authentication for your events, ensuring they're properly validated before being recorded.
### Does OpenPanel work with both SwiftUI and UIKit?
Yes. OpenPanel works with both UIKit and SwiftUI. Use the `.onAppear` modifier in SwiftUI views or lifecycle methods like `viewDidAppear` in UIKit view controllers to track screen views and other events.
### Can I track events when the user is offline?
OpenPanel queues events locally and sends them when network connectivity is restored. Events won't be lost if the user temporarily goes offline.
### Is OpenPanel GDPR compliant?
Yes. OpenPanel is designed for GDPR compliance with data minimization and support for data subject rights. With self-hosting, you also eliminate international data transfer concerns entirely. See the [cookieless analytics article](/articles/cookieless-analytics) for more details.

View File

@@ -0,0 +1,226 @@
---
title: "How to track custom events with OpenPanel"
description: "Learn how to track custom events like button clicks, form submissions, and user interactions in OpenPanel."
difficulty: beginner
timeToComplete: 5
date: 2025-12-15
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Understand event structure"
anchor: "event-structure"
- name: "Track simple events"
anchor: "track-simple-events"
- name: "Track events with properties"
anchor: "track-events-with-properties"
- name: "Use data attributes"
anchor: "data-attributes"
- name: "Verify your setup"
anchor: "verify"
---
# How to track custom events with OpenPanel
Custom events are the foundation of product analytics. They let you track specific user actions like button clicks, form submissions, video plays, and purchases. This guide walks you through tracking custom events in OpenPanel across different platforms.
OpenPanel provides a consistent API for event tracking across all SDKs. Once you understand the pattern, you can apply it to any integration.
## Prerequisites
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- OpenPanel SDK installed in your project
- Your Client ID from the dashboard
## Understand event structure [#event-structure]
Every event in OpenPanel consists of two parts: a name and optional properties. The name describes what happened, while properties add context about how, where, and when it happened.
A well-named event reads like a sentence in past tense. Instead of naming an event "click" or "button", use descriptive names like `signup_button_clicked` or `purchase_completed`. This makes your analytics dashboard immediately understandable.
For property names, use snake_case and keep them consistent across your application. If you track `button_name` on one event, use the same property name on similar events rather than switching to `buttonName` or `name`.
## Track simple events [#track-simple-events]
The simplest event is just a name with no properties. This works well for actions where the context is obvious, like clicking a specific button that only appears in one place.
If you're using the Web SDK with npm, import your OpenPanel instance and call the track method.
```typescript
import { op } from './op';
op.track('signup_button_clicked');
```
For the script tag integration, use the global `window.op` function with 'track' as the first argument.
```html
<button onclick="window.op('track', 'signup_button_clicked')">
Sign Up
</button>
```
If you're tracking events server-side with the JavaScript SDK, the pattern is identical.
```typescript
import { op } from './op';
op.track('order_placed');
```
The track method queues events and sends them asynchronously, so it won't block your application logic.
## Track events with properties [#track-events-with-properties]
Properties transform raw events into actionable data. Instead of knowing that someone clicked a button, you can know which button, where it was located, and what the user was doing at the time.
Pass properties as the second argument to the track method. Include any context that will help you segment and analyze the data later.
```typescript
import { op } from './op';
op.track('button_clicked', {
button_name: 'signup',
button_location: 'hero_section',
page: 'homepage',
});
```
With the script tag, pass the properties object as the third argument.
```html
<button onclick="window.op('track', 'button_clicked', {button_name: 'signup', button_location: 'hero_section'})">
Sign Up
</button>
```
Think carefully about which properties to include. Track data that helps you answer questions about user behavior, like understanding which signup button performs best or which page drives the most conversions.
Here's an example of tracking a purchase with meaningful properties.
```typescript
op.track('purchase_completed', {
product_id: 'prod_123',
product_name: 'Premium Plan',
amount: 99.99,
currency: 'USD',
payment_method: 'credit_card',
previous_plan: 'free',
});
```
Never include sensitive data in event properties. Passwords, credit card numbers, and personally identifiable information should never appear in your analytics.
## Use data attributes [#data-attributes]
For HTML elements, OpenPanel supports declarative tracking with `data-track` attributes. This approach works well when you want to add tracking without writing JavaScript handlers.
Add `data-track` with the event name, then use additional `data-*` attributes for properties.
```html
<button
data-track="button_clicked"
data-button-name="signup"
data-button-location="hero_section"
>
Sign Up
</button>
```
The SDK automatically converts kebab-case attribute names to snake_case properties. So `data-button-name` becomes `button_name` in your event data.
For complex or nested properties, use `data-track-properties` with a JSON string.
```html
<button
data-track="feature_used"
data-track-properties='{"feature_name": "export", "export_format": "csv", "row_count": 500}'
>
Export Data
</button>
```
Data attributes require the `trackAttributes: true` option in your SDK initialization. If you're not seeing events from data attributes, check that this option is enabled.
## Common event patterns
Form submissions benefit from tracking both the submission and key context about what was submitted.
```typescript
function handleSubmit(event) {
event.preventDefault();
op.track('form_submitted', {
form_name: 'contact',
form_location: 'footer',
fields_completed: 3,
});
// Submit the form
}
```
For video interactions, track play, pause, and completion events with timing information.
```typescript
function handleVideoPlay(video) {
op.track('video_played', {
video_id: video.id,
video_title: video.title,
video_duration: video.duration,
});
}
function handleVideoComplete(video) {
op.track('video_completed', {
video_id: video.id,
watch_time: video.currentTime,
});
}
```
Search events should include the query and results count to help you understand what users are looking for.
```typescript
function handleSearch(query, results) {
op.track('search_performed', {
query: query,
query_length: query.length,
results_count: results.length,
has_results: results.length > 0,
});
}
```
## Verify your setup [#verify]
After implementing event tracking, open your OpenPanel dashboard and navigate to the Real-time view. Trigger the events in your application and confirm they appear within a few seconds.
If events aren't showing up, check that your Client ID is correct and that the SDK initialized without errors. Browser developer tools can help you verify that tracking requests are being sent. Look for network requests to `openpanel.dev` or your self-hosted endpoint.
## Next steps
Now that you're tracking custom events, you can identify users to connect events to specific people. For analyzing your event data, the [funnel analysis guide](/articles/how-to-create-a-funnel) shows you how to measure conversion rates across multi-step flows. You can also explore the [SDK documentation](/docs/sdks/web) for advanced features like global properties and event filtering.
For framework-specific examples and setup instructions, check out:
- [Next.js analytics guide](/guides/nextjs-analytics) for Next.js applications
- [React analytics guide](/guides/react-analytics) for React applications
- [Node.js analytics guide](/guides/nodejs-analytics) for server-side tracking
- [Python analytics guide](/guides/python-analytics) for Python applications
<Faqs>
<FaqItem question="How many events can I track?">
OpenPanel doesn't limit the number of events you can track. Focus on tracking meaningful events that help you understand user behavior and make product decisions.
</FaqItem>
<FaqItem question="Should I track every click?">
No. Track events that help you make decisions. Tracking every click creates noise and makes it harder to find insights. Focus on actions that indicate user intent or progress toward a goal.
</FaqItem>
<FaqItem question="Can I track events server-side?">
Yes. The [Node.js SDK](/docs/sdks/javascript) supports server-side tracking with the same API. Server-side tracking is useful for events that happen outside the browser, like webhook callbacks or background jobs.
</FaqItem>
<FaqItem question="Does OpenPanel use cookies for event tracking?">
No. OpenPanel uses cookieless tracking by default, so you don't need cookie consent banners for basic analytics under most privacy regulations.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,348 @@
---
title: "How to add analytics to Vue"
description: "Add privacy-first analytics to your Vue application with OpenPanel's Web SDK. Track page views, custom events, and user behavior."
difficulty: beginner
timeToComplete: 8
date: 2025-12-14
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"
- name: "Create an OpenPanel instance"
anchor: "setup"
- name: "Track page views"
anchor: "pageviews"
- name: "Track custom events"
anchor: "events"
- name: "Identify users"
anchor: "identify"
- name: "Verify your setup"
anchor: "verify"
---
Adding analytics to your Vue application helps you understand how users interact with your app. OpenPanel's Web SDK integrates smoothly with Vue 3 and Vue Router, providing automatic page view tracking, custom events, and user identification without requiring Vue-specific bindings.
OpenPanel is an open-source alternative to Mixpanel and Google Analytics. It delivers powerful insights while respecting user privacy through cookieless tracking by default.
## Prerequisites
- A Vue 3 project (Vite or Vue CLI)
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Your Client ID from the OpenPanel dashboard
## Install the SDK [#install]
The OpenPanel Web SDK is a lightweight package that works in any JavaScript environment, including Vue. Install it using npm, and pnpm or yarn work the same way.
```bash
npm install @openpanel/web
```
## Create an OpenPanel instance [#setup]
Create a dedicated file for your OpenPanel instance. This centralizes your analytics configuration and makes the instance easy to import throughout your application.
```ts title="src/lib/op.ts"
import { OpenPanel } from '@openpanel/web';
export const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```
The `trackScreenViews` option automatically tracks page views when the URL changes. This works with Vue Router's client-side navigation. The `trackAttributes` option enables declarative tracking using `data-track` attributes on HTML elements.
### Using environment variables
For production applications, store your Client ID in environment variables.
```ts title="src/lib/op.ts"
import { OpenPanel } from '@openpanel/web';
export const op = new OpenPanel({
clientId: import.meta.env.VITE_OPENPANEL_CLIENT_ID,
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```
```bash title=".env"
VITE_OPENPANEL_CLIENT_ID=your-client-id
```
### Initialize on app load
Import the OpenPanel instance in your app's entry point to ensure it initializes when your application loads. This is all you need to start tracking page views automatically.
```ts title="src/main.ts"
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import './lib/op'; // Initialize OpenPanel
const app = createApp(App);
app.use(router);
app.mount('#app');
```
### Make OpenPanel available globally (optional)
If you prefer accessing OpenPanel through the Vue instance rather than importing it in each component, you can add it as a global property.
```ts title="src/main.ts"
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { op } from './lib/op';
const app = createApp(App);
app.config.globalProperties.$op = op;
app.use(router);
app.mount('#app');
```
This makes `this.$op` available in Options API components and allows you to inject it in Composition API components.
## Track page views [#pageviews]
With `trackScreenViews: true`, OpenPanel automatically tracks page views when the browser's URL changes. This works out of the box with Vue Router.
If you need to track page views manually or want to include additional route metadata, you can use a Vue Router navigation guard.
```ts title="src/router/index.ts"
import { createRouter, createWebHistory } from 'vue-router';
import { op } from '../lib/op';
import routes from './routes';
const router = createRouter({
history: createWebHistory(),
routes,
});
router.afterEach((to) => {
op.track('screen_view', {
path: to.path,
name: String(to.name),
});
});
export default router;
```
If you use this approach, set `trackScreenViews: false` in your OpenPanel configuration to avoid duplicate tracking.
## Track custom events [#events]
Import the OpenPanel instance in your components to track events. The `track` method accepts an event name and an optional properties object.
### Using Composition API
```vue title="src/components/SignupButton.vue"
<template>
<button type="button" @click="handleClick">Sign Up</button>
</template>
<script setup lang="ts">
import { op } from '../lib/op';
function handleClick() {
op.track('button_clicked', {
button_name: 'signup',
button_location: 'hero',
});
}
</script>
```
### Using Options API
```vue title="src/components/SignupButton.vue"
<template>
<button type="button" @click="handleClick">Sign Up</button>
</template>
<script>
import { op } from '../lib/op';
export default {
methods: {
handleClick() {
op.track('button_clicked', {
button_name: 'signup',
button_location: 'hero',
});
},
},
};
</script>
```
### Track form submissions
Form tracking helps you understand conversion rates and identify where users drop off.
```vue title="src/components/ContactForm.vue"
<template>
<form @submit.prevent="handleSubmit">
<input
v-model="email"
type="email"
placeholder="Enter your email"
/>
<button type="submit">Submit</button>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { op } from '../lib/op';
const email = ref('');
function handleSubmit() {
op.track('form_submitted', {
form_name: 'contact',
form_location: 'homepage',
});
// Your form submission logic
}
</script>
```
### Use data attributes for declarative tracking
The Web SDK supports declarative tracking using `data-track` attributes. This is useful for simple click tracking without writing JavaScript.
```vue
<template>
<button
data-track="button_clicked"
data-track-button_name="signup"
data-track-button_location="hero"
>
Sign Up
</button>
</template>
```
When a user clicks this button, OpenPanel automatically sends a `button_clicked` event with the specified properties. This requires `trackAttributes: true` in your configuration.
## Identify users [#identify]
Once a user logs in or provides identifying information, call `identify` to associate their activity with a profile. This enables user-level analytics and cohort analysis.
```vue title="src/components/UserProfile.vue"
<template>
<div>Welcome, {{ user?.firstName }}!</div>
</template>
<script setup lang="ts">
import { watch } from 'vue';
import { op } from '../lib/op';
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
plan: string;
createdAt: string;
}
const props = defineProps<{ user: User | null }>();
watch(
() => props.user,
(user) => {
if (user) {
op.identify({
profileId: user.id,
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: {
plan: user.plan,
signupDate: user.createdAt,
},
});
}
},
{ immediate: true }
);
</script>
```
### Clear user data on logout
When users log out, clear the stored profile data to ensure subsequent events aren't associated with the previous user.
```vue title="src/components/LogoutButton.vue"
<template>
<button @click="handleLogout">Logout</button>
</template>
<script setup lang="ts">
import { op } from '../lib/op';
const emit = defineEmits<{ logout: [] }>();
function handleLogout() {
op.clear();
emit('logout');
}
</script>
```
### Set global properties
Properties set with `setGlobalProperties` are included with every event. This is useful for app version tracking, feature flags, or A/B test variants.
```vue title="src/App.vue"
<script setup lang="ts">
import { onMounted } from 'vue';
import { op } from './lib/op';
onMounted(() => {
op.setGlobalProperties({
app_version: '1.0.0',
environment: import.meta.env.MODE,
});
});
</script>
```
## Verify your setup [#verify]
Open your Vue app in the browser and navigate between a few pages. Interact with elements that trigger custom events. Then open your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the Real-time view to see events appearing.
If events aren't appearing, check the browser console for errors. Verify your Client ID is correct and ensure ad blockers aren't blocking requests to the OpenPanel API. The Network tab in your browser's developer tools can help you confirm that requests are being sent.
## Next steps
The Web SDK has additional features like property incrementing and event filtering. Read the full [Web SDK documentation](/docs/sdks/web) for the complete API reference.
For Nuxt.js applications, the setup is similar. Initialize OpenPanel in a Nuxt plugin and the automatic page view tracking will work with Nuxt's router.
<Faqs>
<FaqItem question="Does OpenPanel work with Vue Router?">
Yes. OpenPanel automatically tracks page views when the URL changes, which works with Vue Router. Set `trackScreenViews: true` in your configuration. You can also use router navigation guards for manual tracking.
</FaqItem>
<FaqItem question="Can I use OpenPanel with Vue 2?">
Yes. The OpenPanel Web SDK is framework-agnostic and works with Vue 2. Import the instance directly in your components and call its methods.
</FaqItem>
<FaqItem question="Does OpenPanel use cookies?">
No. OpenPanel uses cookieless tracking by default. This means you don't need cookie consent banners for basic analytics under most privacy regulations, including GDPR and PECR.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with cookieless tracking, data minimization, and full support for data subject rights. With self-hosting, you also eliminate international data transfer concerns entirely.
</FaqItem>
</Faqs>

View File

@@ -0,0 +1,178 @@
---
title: "How to add analytics to any website"
description: "Add privacy-first analytics to any website in minutes using a simple script tag. Works with WordPress, Webflow, Squarespace, and plain HTML."
difficulty: beginner
timeToComplete: 5
date: 2025-12-14
cover: /content/cover-default.jpg
team: OpenPanel Team
steps:
- name: "Get your Client ID"
anchor: "get-id"
- name: "Add the script tag"
anchor: "script"
- name: "Track custom events"
anchor: "events"
- name: "Verify your setup"
anchor: "verify"
---
# How to add analytics to any website
Adding analytics to your website does not require developer expertise. OpenPanel's script tag works with any website, whether you're using WordPress, Webflow, Squarespace, or plain HTML. In about five minutes, you'll have privacy-first analytics running on your site.
OpenPanel is an open-source analytics platform that works without cookies by default. This means you can track meaningful user behavior while respecting privacy and avoiding cookie consent banners in most cases.
## Prerequisites
- A website on any platform
- An OpenPanel account ([sign up free](https://dashboard.openpanel.dev/onboarding))
- Access to your website's HTML header or footer
## Get your Client ID [#get-id]
Start by creating an OpenPanel account at [dashboard.openpanel.dev](https://dashboard.openpanel.dev/onboarding). Once logged in, create a new project and copy your Client ID from the project settings.
Your Client ID will look something like `cl_xxxxxxxxxxxxxxxx`. Keep this handy, as you'll need it in the next step.
## Add the script tag [#script]
The OpenPanel script needs to be added to every page of your website. The best approach depends on your platform, but the core snippet remains the same.
Here's the script tag you'll be adding. Replace `YOUR_CLIENT_ID` with the Client ID you copied earlier.
```html
<script>
window.op=window.op||function(){var n=[];return new Proxy(function(){arguments.length&&n.push([].slice.call(arguments))},{get:function(t,r){return"q"===r?n:function(){n.push([r].concat([].slice.call(arguments)))}} ,has:function(t,r){return"q"===r}}) }();
window.op('init', {
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
</script>
<script src="https://openpanel.dev/op1.js" defer async></script>
```
The `trackScreenViews` option enables automatic page view tracking, so you don't need to manually track each page. The `trackOutgoingLinks` option captures clicks on external links, which is useful for understanding how users leave your site.
### Platform-specific instructions
For WordPress sites, the easiest approach is to use a plugin like "Insert Headers and Footers" or "Code Snippets". Install the plugin, then paste the script tag into the header section. If you prefer working with code, you can add the script directly to your theme's `header.php` file before the closing `</head>` tag.
WordPress users who want more control can add the script via `functions.php` instead.
```php
function add_openpanel_script() {
?>
<script>
window.op=window.op||function(){var n=[];return new Proxy(function(){arguments.length&&n.push([].slice.call(arguments))},{get:function(t,r){return"q"===r?n:function(){n.push([r].concat([].slice.call(arguments)))}} ,has:function(t,r){return"q"===r}}) }();
window.op('init', {
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
</script>
<script src="https://openpanel.dev/op1.js" defer async></script>
<?php
}
add_action('wp_head', 'add_openpanel_script');
```
For Webflow, go to your project settings and navigate to Custom Code. Paste the script tag into the Head Code section and publish your site. The script will now load on every page.
Squarespace users can find the code injection settings under Settings > Advanced > Code Injection. Add the script to the Header section and save your changes.
If you're using Google Tag Manager, create a new Custom HTML tag and paste the script tag code. Set the trigger to All Pages and publish your container.
## Track custom events [#events]
Page views are tracked automatically, but you'll likely want to track specific user actions like button clicks, form submissions, or video plays. OpenPanel provides two ways to do this.
The simplest approach uses `data-track` attributes directly in your HTML. When a user clicks an element with this attribute, OpenPanel automatically sends an event.
```html
<button data-track="button_clicked" data-button_name="signup">
Sign Up
</button>
```
Any `data-` attributes on the element (except `data-track` itself) are included as event properties. This is useful when you want to track additional context without writing JavaScript.
For more complex tracking, you can call the `window.op` function directly. This gives you full control over when and what to track.
```html
<script>
document.querySelector('.signup-button').addEventListener('click', function() {
window.op('track', 'button_clicked', {
button_name: 'signup',
button_location: 'hero'
});
});
</script>
```
Form submissions are a common tracking use case. You can track them inline using the `onsubmit` attribute.
```html
<form onsubmit="window.op('track', 'form_submitted', {form_name: 'contact'}); return true;">
<input type="email" name="email" placeholder="Your email">
<button type="submit">Submit</button>
</form>
```
### Identifying users
When users log in or provide their email, you can associate their activity with a profile. This is done using the `identify` method.
```html
<script>
window.op('identify', {
profileId: 'user_123',
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe'
});
</script>
```
The `profileId` is required and should be a unique identifier from your system. Once identified, all subsequent events are associated with this user, enabling cross-session analysis.
## Verify your setup [#verify]
Open your website in a browser and navigate through a few pages. Click some buttons or links that you've set up tracking for. Then head to your [OpenPanel dashboard](https://dashboard.openpanel.dev) and check the Real-time view.
Events should appear within a few seconds. If you're not seeing any data, open your browser's developer console (F12) and look for JavaScript errors. The most common issues are an incorrect Client ID or ad blockers preventing the script from loading.
Ad blockers can interfere with analytics scripts. If this is a concern for your audience, you can proxy the OpenPanel script through your own domain. The [ad blocker documentation](/docs/adblockers) explains how to set this up.
## Next steps
The script tag covers most tracking needs for traditional websites. For more advanced configuration options, check out the [Script Tag SDK reference](/docs/sdks/script). If you want to understand user journeys better, the article on [how to create a funnel](/articles/how-to-create-a-funnel) walks through setting up conversion funnels.
For sites with backend logic or server-side rendering, you might want to combine client-side tracking with server-side events. The [Node.js guide](/guides/nodejs-analytics) and [Python guide](/guides/python-analytics) cover those use cases.
If you're using a specific framework, check out our framework-specific guides for more advanced setups:
- [Next.js analytics guide](/guides/nextjs-analytics) for Next.js applications
- [React analytics guide](/guides/react-analytics) for React applications
- [Astro analytics guide](/guides/astro-analytics) for Astro sites
- [Vue analytics guide](/guides/vue-analytics) for Vue.js applications
<Faqs>
<FaqItem question="Does OpenPanel use cookies?">
No. OpenPanel uses cookieless tracking by default. This means you typically don't need cookie consent banners for basic analytics under most privacy regulations, including GDPR and PECR.
</FaqItem>
<FaqItem question="Will ad blockers prevent tracking?">
Some ad blockers may block requests to openpanel.dev. You can mitigate this by proxying the script through your own domain or by self-hosting OpenPanel. The documentation covers both approaches.
</FaqItem>
<FaqItem question="Can I use OpenPanel with Google Tag Manager?">
Yes. Add the OpenPanel script as a Custom HTML tag in Google Tag Manager with an All Pages trigger. This lets you manage OpenPanel alongside your other tracking scripts.
</FaqItem>
<FaqItem question="Is OpenPanel GDPR compliant?">
Yes. OpenPanel is designed for GDPR compliance with cookieless tracking, data minimization, and support for data subject rights. Self-hosting eliminates international data transfer concerns entirely. Read more about [cookieless analytics](/articles/cookieless-analytics) for details.
</FaqItem>
</Faqs>

View File

@@ -36,6 +36,23 @@ const zPage = z.object({
description: z.string(),
});
const zGuide = z.object({
title: z.string().min(1),
description: z.string(),
difficulty: z.enum(['beginner', 'intermediate', 'advanced']),
timeToComplete: z.number(), // minutes
date: z.date(),
updated: z.date().optional(),
cover: z.string().default('/content/cover-default.jpg'),
team: z.string().optional(),
steps: z.array(
z.object({
name: z.string(),
anchor: z.string(),
}),
),
});
export const articleCollection = defineCollections({
type: 'doc',
dir: './content/articles',
@@ -60,6 +77,18 @@ export const pageMeta = defineCollections({
schema: zPage,
});
export const guideCollection = defineCollections({
type: 'doc',
dir: './content/guides',
schema: zGuide,
});
export const guideMeta = defineCollections({
type: 'meta',
dir: './content/guides',
schema: zGuide,
});
export default defineConfig({
mdxOptions: {
// MDX options

View File

@@ -0,0 +1,212 @@
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
import { HeroContainer } from '@/app/(home)/_sections/hero';
import { Testimonials } from '@/app/(home)/_sections/testimonials';
import { FeatureCardContainer } from '@/components/feature-card';
import { GetStartedButton } from '@/components/get-started-button';
import { GuideCard } from '@/components/guide-card';
import { Logo } from '@/components/logo';
import { SectionHeader } from '@/components/section';
import { Toc } from '@/components/toc';
import { url, getAuthor } from '@/lib/layout.shared';
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
import { guideSource } from '@/lib/source';
import { getMDXComponents } from '@/mdx-components';
import { ArrowLeftIcon, ClockIcon } from 'lucide-react';
import type { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import Script from 'next/script';
const difficultyColors = {
beginner: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
intermediate:
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
advanced: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
};
const difficultyLabels = {
beginner: 'Beginner',
intermediate: 'Intermediate',
advanced: 'Advanced',
};
export async function generateStaticParams() {
const guides = await guideSource.getPages();
return guides.map((guide) => {
// Extract slug from URL (e.g., '/guides/my-guide' -> 'my-guide')
const slug = guide.url.replace(/^\/guides\//, '').replace(/\/$/, '');
return { guideSlug: slug };
});
}
export async function generateMetadata({
params,
}: {
params: Promise<{ guideSlug: string }>;
}): Promise<Metadata> {
const { guideSlug } = await params;
const guide = await guideSource.getPage([guideSlug]);
if (!guide) {
return {
title: 'Guide Not Found',
};
}
return getPageMetadata({
title: guide.data.title,
description: guide.data.description,
url: url(guide.url),
image: getOgImageUrl(guide.url),
});
}
export default async function Page({
params,
}: {
params: Promise<{ guideSlug: string }>;
}) {
const { guideSlug } = await params;
const guide = await guideSource.getPage([guideSlug]);
const Body = guide?.data.body;
const author = getAuthor(guide?.data.team);
const goBackUrl = '/guides';
const relatedGuides = (await guideSource.getPages())
.filter(
(item) =>
item.data.difficulty === guide?.data.difficulty &&
item.url !== guide?.url,
)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
.slice(0, 3);
if (!Body) {
return notFound();
}
const slug = guide.url.replace(/^\/guides\//, '').replace(/\/$/, '');
// Create the HowTo JSON-LD schema
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'HowTo',
name: guide?.data.title,
description: guide?.data.description,
totalTime: `PT${guide?.data.timeToComplete}M`,
step: guide?.data.steps.map((step, i) => ({
'@type': 'HowToStep',
position: i + 1,
name: step.name,
url: url(`/guides/${slug}#${step.anchor}`),
})),
};
return (
<div>
<HeroContainer>
<div className="col">
<Link
href={goBackUrl}
className="flex items-center gap-2 mb-4 text-muted-foreground"
>
<ArrowLeftIcon className="w-4 h-4" />
<span>Back to all guides</span>
</Link>
<SectionHeader
as="h1"
title={guide?.data.title}
description={guide?.data.description}
/>
<div className="row gap-4 items-center mt-8">
<div className="size-10 center-center bg-black rounded-full">
{author.image ? (
<Image
className="size-10 object-cover rounded-full"
src={author.image}
alt={author.name}
width={48}
height={48}
/>
) : (
<Logo className="w-6 h-6 fill-white" />
)}
</div>
<div className="col flex-1">
<p className="font-medium">{author.name}</p>
<div className="row gap-4 items-center">
<p className="text-muted-foreground text-sm">
{guide?.data.date.toLocaleDateString()}
</p>
{guide?.data.updated && (
<p className="text-muted-foreground text-sm italic">
Updated on {guide?.data.updated.toLocaleDateString()}
</p>
)}
</div>
</div>
<div className="row gap-3 items-center">
<span
className={`font-mono text-xs px-3 py-1 rounded ${difficultyColors[guide?.data.difficulty || 'beginner']}`}
>
{difficultyLabels[guide?.data.difficulty || 'beginner']}
</span>
<div className="row gap-1 items-center text-muted-foreground text-sm">
<ClockIcon className="w-4 h-4" />
<span>{guide?.data.timeToComplete} min</span>
</div>
</div>
</div>
</div>
</HeroContainer>
<Script
strategy="beforeInteractive"
id="guide-howto-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<article className="container max-w-5xl col">
<div className="grid grid-cols-1 md:grid-cols-[1fr_300px] gap-0">
<div className="min-w-0">
<div className="prose [&_table]:w-auto [&_img]:max-w-full [&_img]:h-auto">
<Body components={getMDXComponents()} />
</div>
</div>
<aside className="pl-12 pb-12 gap-8 col">
<Toc toc={guide?.data.toc} />
<FeatureCardContainer className="gap-2">
<span className="text-lg font-semibold">Try OpenPanel</span>
<p className="text-muted-foreground text-sm mb-4">
Give it a spin for free. No credit card required.
</p>
<GetStartedButton />
</FeatureCardContainer>
</aside>
</div>
{relatedGuides.length > 0 && (
<div className="my-16">
<h3 className="text-2xl font-bold mb-8">Related guides</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{relatedGuides.map((item) => (
<GuideCard
key={item.url}
url={item.url}
title={item.data.title}
difficulty={item.data.difficulty}
timeToComplete={item.data.timeToComplete}
cover={item.data.cover}
team={item.data.team}
date={item.data.date}
/>
))}
</div>
</div>
)}
</article>
<Testimonials />
<CtaBanner />
</div>
);
}

View File

@@ -0,0 +1,79 @@
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
import { HeroContainer } from '@/app/(home)/_sections/hero';
import { Testimonials } from '@/app/(home)/_sections/testimonials';
import { GuideCard } from '@/components/guide-card';
import { Section, SectionHeader } from '@/components/section';
import { url } from '@/lib/layout.shared';
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
import { guideSource } from '@/lib/source';
import type { Metadata } from 'next';
import Script from 'next/script';
export const metadata: Metadata = getPageMetadata({
title: 'Implementation Guides',
description:
'Step-by-step tutorials for adding privacy-first analytics to your app with OpenPanel.',
url: url('/guides'),
image: getOgImageUrl('/guides'),
});
export default async function Page() {
const guides = (await guideSource.getPages()).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
// Create ItemList schema for SEO
const itemListSchema = {
'@context': 'https://schema.org',
'@type': 'ItemList',
name: 'OpenPanel Implementation Guides',
description: 'Step-by-step tutorials for adding analytics to your app',
itemListElement: guides.map((guide, index) => {
const slug = guide.url.replace(/^\/guides\//, '').replace(/\/$/, '');
return {
'@type': 'ListItem',
position: index + 1,
name: guide.data.title,
url: url(guide.url),
};
}),
};
return (
<div>
<HeroContainer className="-mb-32">
<SectionHeader
as="h1"
align="center"
className="flex-1"
title="Implementation Guides"
description="Step-by-step tutorials for adding privacy-first analytics to your app with OpenPanel."
/>
</HeroContainer>
<Script
strategy="beforeInteractive"
id="guides-itemlist-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(itemListSchema) }}
/>
<Section className="container grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8">
{guides.map((item) => (
<GuideCard
key={item.url}
url={item.url}
title={item.data.title}
difficulty={item.data.difficulty}
timeToComplete={item.data.timeToComplete}
cover={item.data.cover}
team={item.data.team}
date={item.data.date}
/>
))}
</Section>
<Testimonials />
<CtaBanner />
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { getAllCompareSlugs, getCompareData } from '@/lib/compare';
import { url as baseUrl } from '@/lib/layout.shared';
import { articleSource, pageSource, source } from '@/lib/source';
import { articleSource, guideSource, pageSource, source } from '@/lib/source';
import { ImageResponse } from 'next/og';
import type { NextRequest } from 'next/server';
@@ -62,6 +62,20 @@ async function getOgData(
description: data?.seo.description || data?.hero.subheading,
};
}
case 'guides': {
if (segments.length > 1) {
const data = await guideSource.getPage(segments.slice(1));
return {
title: data?.data.title ?? 'Guide Not Found',
description:
data?.data.description || 'Whooops, could not find this guide',
};
}
return {
title: 'Implementation Guides',
description: 'Step-by-step tutorials for adding analytics to your app',
};
}
case 'docs': {
const data = await source.getPage(segments.slice(1));
return {

View File

@@ -36,6 +36,7 @@ export async function Footer() {
{ title: 'Pricing', url: '/pricing' },
{ title: 'Documentation', url: '/docs' },
{ title: 'SDKs', url: '/docs/sdks' },
{ title: 'Guides', url: '/guides' },
{ title: 'Articles', url: '/articles' },
{ title: 'Compare', url: '/compare' },
]}

View File

@@ -0,0 +1,67 @@
import Image from 'next/image';
import Link from 'next/link';
const difficultyColors = {
beginner: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
intermediate:
'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
advanced: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
};
const difficultyLabels = {
beginner: 'Beginner',
intermediate: 'Intermediate',
advanced: 'Advanced',
};
export function GuideCard({
url,
title,
difficulty,
timeToComplete,
cover,
team,
date,
}: {
url: string;
title: string;
difficulty: 'beginner' | 'intermediate' | 'advanced';
timeToComplete: number;
cover: string;
team?: string;
date: Date;
}) {
return (
<Link
href={url}
key={url}
className="border rounded-lg overflow-hidden bg-background-light col hover:scale-105 transition-all duration-300 hover:shadow-lg hover:shadow-background-dark"
>
<Image
src={cover}
alt={title}
width={323}
height={181}
className="w-full"
/>
<span className="p-4 col flex-1">
<div className="flex items-center gap-2 mb-2">
<span
className={`font-mono text-xs px-2 py-1 rounded ${difficultyColors[difficulty]}`}
>
{difficultyLabels[difficulty]}
</span>
<span className="text-xs text-muted-foreground">
{timeToComplete} min
</span>
</div>
<span className="flex-1 mb-6">
<h2 className="text-xl font-semibold">{title}</h2>
</span>
<p className="text-sm text-muted-foreground">
{[team, date.toLocaleDateString()].filter(Boolean).join(' · ')}
</p>
</span>
</Link>
);
}

View File

@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
import {
articleCollection,
docs,
guideCollection,
pageCollection,
} from 'fumadocs-mdx:collections/server';
import { type InferPageType, loader } from 'fumadocs-core/source';
@@ -29,6 +30,12 @@ export const pageSource = loader({
source: toFumadocsSource(pageCollection, []),
});
export const guideSource = loader({
baseUrl: '/guides',
source: toFumadocsSource(guideCollection, []),
plugins: [lucideIconsPlugin()],
});
export function getPageImage(page: InferPageType<typeof source>) {
const segments = [...page.slugs, 'image.png'];