1 Commits

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

View File

@@ -1,256 +0,0 @@
---
title: Nuxt
---
import Link from 'next/link';
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 [Nuxt analytics guide](/guides/nuxt-analytics).
</Callout>
## Good to know
Keep in mind that all tracking here happens on the client!
Read more about server side tracking in the [Server Side Tracking](#track-server-events) section.
## Installation
<Steps>
### Install dependencies
```bash
pnpm install @openpanel/nuxt
```
### Initialize
Add the module to your `nuxt.config.ts`:
```typescript
export default defineNuxtConfig({
modules: ['@openpanel/nuxt'],
openpanel: {
clientId: 'your-client-id',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
},
});
```
#### Options
<CommonSdkConfig />
<WebSdkConfig />
##### Nuxt options
- `clientId` - Your OpenPanel client ID (required)
- `apiUrl` - The API URL to send events to (default: `https://api.openpanel.dev`)
- `trackScreenViews` - Automatically track screen views (default: `true`)
- `trackOutgoingLinks` - Automatically track outgoing links (default: `true`)
- `trackAttributes` - Automatically track elements with `data-track` attributes (default: `true`)
- `trackHashChanges` - Track hash changes in URL (default: `false`)
- `disabled` - Disable tracking (default: `false`)
- `proxy` - Enable server-side proxy to avoid adblockers (default: `false`)
</Steps>
## Usage
### Using the composable
The `useOpenPanel` composable is auto-imported, so you can use it directly in any component:
```vue
<script setup>
const op = useOpenPanel(); // Auto-imported!
function handleClick() {
op.track('button_click', { button: 'signup' });
}
</script>
<template>
<button @click="handleClick">Trigger event</button>
</template>
```
### Accessing via useNuxtApp
You can also access the OpenPanel instance directly via `useNuxtApp()`:
```vue
<script setup>
const { $openpanel } = useNuxtApp();
$openpanel.track('my_event', { foo: 'bar' });
</script>
```
### Tracking Events
You can track events with two different methods: by calling the `op.track()` method directly or by adding `data-track` attributes to your HTML elements.
```vue
<script setup>
const op = useOpenPanel();
op.track('my_event', { foo: 'bar' });
</script>
```
### Identifying Users
To identify a user, call the `op.identify()` method with a unique identifier.
```vue
<script setup>
const op = useOpenPanel();
op.identify({
profileId: '123', // Required
firstName: 'Joe',
lastName: 'Doe',
email: 'joe@doe.com',
properties: {
tier: 'premium',
},
});
</script>
```
### Setting Global Properties
To set properties that will be sent with every event:
```vue
<script setup>
const op = useOpenPanel();
op.setGlobalProperties({
app_version: '1.0.2',
environment: 'production',
});
</script>
```
### Incrementing Properties
To increment a numeric property on a user profile.
- `value` is the amount to increment the property by. If not provided, the property will be incremented by 1.
```vue
<script setup>
const op = useOpenPanel();
op.increment({
profileId: '1',
property: 'visits',
value: 1, // optional
});
</script>
```
### Decrementing Properties
To decrement a numeric property on a user profile.
- `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1.
```vue
<script setup>
const op = useOpenPanel();
op.decrement({
profileId: '1',
property: 'visits',
value: 1, // optional
});
</script>
```
### Clearing User Data
To clear the current user's data:
```vue
<script setup>
const op = useOpenPanel();
op.clear();
</script>
```
## Server side
If you want to track server-side events, you should create an instance of our Javascript SDK. Import `OpenPanel` from `@openpanel/sdk`
<Callout>
When using server events it's important that you use a secret to authenticate the request. This is to prevent unauthorized requests since we cannot use cors headers.
You can use the same clientId but you should pass the associated client secret to the SDK.
</Callout>
```typescript
import { OpenPanel } from '@openpanel/sdk';
const opServer = new OpenPanel({
clientId: '{YOUR_CLIENT_ID}',
clientSecret: '{YOUR_CLIENT_SECRET}',
});
opServer.track('my_server_event', { ok: '✅' });
// Pass `profileId` to track events for a specific user
opServer.track('my_server_event', { profileId: '123', ok: '✅' });
```
### Serverless & Edge Functions
If you log events in a serverless environment, make sure to await the event call to ensure it completes before the function terminates.
```typescript
import { OpenPanel } from '@openpanel/sdk';
const opServer = new OpenPanel({
clientId: '{YOUR_CLIENT_ID}',
clientSecret: '{YOUR_CLIENT_SECRET}',
});
export default defineEventHandler(async (event) => {
// Await to ensure event is logged before function completes
await opServer.track('my_server_event', { foo: 'bar' });
return { message: 'Event logged!' };
});
```
### Proxy events
With the `proxy` option enabled, you can proxy your events through your server, which ensures all events are tracked since many adblockers block requests to third-party domains.
```typescript title="nuxt.config.ts"
export default defineNuxtConfig({
modules: ['@openpanel/nuxt'],
openpanel: {
clientId: 'your-client-id',
proxy: true, // Enables proxy at /api/openpanel/*
},
});
```
When `proxy: true` is set:
- The module automatically sets `apiUrl` to `/api/openpanel`
- A server handler is registered at `/api/openpanel/**`
- All tracking requests route through your server
This helps bypass adblockers that might block requests to `api.openpanel.dev`.

View File

@@ -2,244 +2,4 @@
title: React
---
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { PersonalDataWarning } from '@/components/personal-data-warning';
import CommonSdkConfig from '@/components/common-sdk-config.mdx';
import WebSdkConfig from '@/components/web-sdk-config.mdx';
## Good to know
Keep in mind that all tracking here happens on the client!
For React SPAs, you can use `@openpanel/web` directly - no need for a separate React SDK. Simply create an OpenPanel instance and use it throughout your application.
## Installation
<Steps>
### Step 1: Install
```bash
npm install @openpanel/web
```
### Step 2: Initialize
Create a shared OpenPanel instance in your project:
```ts title="src/openpanel.ts"
import { OpenPanel } from '@openpanel/web';
export const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```
#### Options
<CommonSdkConfig />
<WebSdkConfig />
- `clientId` - Your OpenPanel client ID (required)
- `apiUrl` - The API URL to send events to (default: `https://api.openpanel.dev`)
- `trackScreenViews` - Automatically track screen views (default: `true`)
- `trackOutgoingLinks` - Automatically track outgoing links (default: `true`)
- `trackAttributes` - Automatically track elements with `data-track` attributes (default: `true`)
- `trackHashChanges` - Track hash changes in URL (default: `false`)
- `disabled` - Disable tracking (default: `false`)
### Step 3: Usage
Import and use the instance in your React components:
```tsx
import { op } from '@/openpanel';
function MyComponent() {
const handleClick = () => {
op.track('button_click', { button: 'signup' });
};
return <button onClick={handleClick}>Trigger event</button>;
}
```
</Steps>
## Usage
### Tracking Events
You can track events with two different methods: by calling the `op.track()` method directly or by adding `data-track` attributes to your HTML elements.
```tsx
import { op } from '@/openpanel';
function MyComponent() {
useEffect(() => {
op.track('my_event', { foo: 'bar' });
}, []);
return <div>My Component</div>;
}
```
### Identifying Users
To identify a user, call the `op.identify()` method with a unique identifier.
```tsx
import { op } from '@/openpanel';
function LoginComponent() {
const handleLogin = (user: User) => {
op.identify({
profileId: user.id, // Required
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
properties: {
tier: 'premium',
},
});
};
return <button onClick={() => handleLogin(user)}>Login</button>;
}
```
### Setting Global Properties
To set properties that will be sent with every event:
```tsx
import { op } from '@/openpanel';
function App() {
useEffect(() => {
op.setGlobalProperties({
app_version: '1.0.2',
environment: 'production',
});
}, []);
return <div>App</div>;
}
```
### Incrementing Properties
To increment a numeric property on a user profile.
- `value` is the amount to increment the property by. If not provided, the property will be incremented by 1.
```tsx
import { op } from '@/openpanel';
function MyComponent() {
const handleAction = () => {
op.increment({
profileId: '1',
property: 'visits',
value: 1, // optional
});
};
return <button onClick={handleAction}>Increment</button>;
}
```
### Decrementing Properties
To decrement a numeric property on a user profile.
- `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1.
```tsx
import { op } from '@/openpanel';
function MyComponent() {
const handleAction = () => {
op.decrement({
profileId: '1',
property: 'visits',
value: 1, // optional
});
};
return <button onClick={handleAction}>Decrement</button>;
}
```
### Clearing User Data
To clear the current user's data:
```tsx
import { op } from '@/openpanel';
function LogoutComponent() {
const handleLogout = () => {
op.clear();
// ... logout logic
};
return <button onClick={handleLogout}>Logout</button>;
}
```
### Revenue Tracking
Track revenue events:
```tsx
import { op } from '@/openpanel';
function CheckoutComponent() {
const handlePurchase = async () => {
// Track revenue immediately
await op.revenue(29.99, { currency: 'USD' });
// Or accumulate revenue and flush later
op.pendingRevenue(29.99, { currency: 'USD' });
op.pendingRevenue(19.99, { currency: 'USD' });
await op.flushRevenue(); // Sends both revenue events
// Clear pending revenue
op.clearRevenue();
};
return <button onClick={handlePurchase}>Purchase</button>;
}
```
### Optional: Create a Hook
If you prefer using a React hook pattern, you can create your own wrapper:
```ts title="src/hooks/useOpenPanel.ts"
import { op } from '@/openpanel';
export function useOpenPanel() {
return op;
}
```
Then use it in your components:
```tsx
import { useOpenPanel } from '@/hooks/useOpenPanel';
function MyComponent() {
const op = useOpenPanel();
useEffect(() => {
op.track('my_event', { foo: 'bar' });
}, []);
return <div>My Component</div>;
}
```
Use [script tag](/docs/sdks/script) or [Web SDK](/docs/sdks/web) for now. We'll add a dedicated react sdk soon.

View File

@@ -216,4 +216,3 @@ tracker = OpenPanel::SDK::Tracker.new(
)
```

View File

@@ -2,219 +2,4 @@
title: Vue
---
import Link from 'next/link';
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 [Vue analytics guide](/guides/vue-analytics).
</Callout>
## Good to know
Keep in mind that all tracking here happens on the client!
For Vue SPAs, you can use `@openpanel/web` directly - no need for a separate Vue SDK. Simply create an OpenPanel instance and use it throughout your application.
## Installation
<Steps>
### Step 1: Install
```bash
pnpm install @openpanel/web
```
### Step 2: Initialize
Create a shared OpenPanel instance in your project:
```ts title="src/openpanel.ts"
import { OpenPanel } from '@openpanel/web';
export const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
});
```
#### Options
<CommonSdkConfig />
<WebSdkConfig />
- `clientId` - Your OpenPanel client ID (required)
- `apiUrl` - The API URL to send events to (default: `https://api.openpanel.dev`)
- `trackScreenViews` - Automatically track screen views (default: `true`)
- `trackOutgoingLinks` - Automatically track outgoing links (default: `true`)
- `trackAttributes` - Automatically track elements with `data-track` attributes (default: `true`)
- `trackHashChanges` - Track hash changes in URL (default: `false`)
- `disabled` - Disable tracking (default: `false`)
### Step 3: Usage
Import and use the instance in your Vue components:
```vue
<script setup>
import { op } from '@/openpanel';
function handleClick() {
op.track('button_click', { button: 'signup' });
}
</script>
<template>
<button @click="handleClick">Trigger event</button>
</template>
```
</Steps>
## Usage
### Tracking Events
You can track events with two different methods: by calling the `op.track()` method directly or by adding `data-track` attributes to your HTML elements.
```vue
<script setup>
import { op } from '@/openpanel';
op.track('my_event', { foo: 'bar' });
</script>
```
### Identifying Users
To identify a user, call the `op.identify()` method with a unique identifier.
```vue
<script setup>
import { op } from '@/openpanel';
op.identify({
profileId: '123', // Required
firstName: 'Joe',
lastName: 'Doe',
email: 'joe@doe.com',
properties: {
tier: 'premium',
},
});
</script>
```
### Setting Global Properties
To set properties that will be sent with every event:
```vue
<script setup>
import { op } from '@/openpanel';
op.setGlobalProperties({
app_version: '1.0.2',
environment: 'production',
});
</script>
```
### Incrementing Properties
To increment a numeric property on a user profile.
- `value` is the amount to increment the property by. If not provided, the property will be incremented by 1.
```vue
<script setup>
import { op } from '@/openpanel';
op.increment({
profileId: '1',
property: 'visits',
value: 1, // optional
});
</script>
```
### Decrementing Properties
To decrement a numeric property on a user profile.
- `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1.
```vue
<script setup>
import { op } from '@/openpanel';
op.decrement({
profileId: '1',
property: 'visits',
value: 1, // optional
});
</script>
```
### Clearing User Data
To clear the current user's data:
```vue
<script setup>
import { op } from '@/openpanel';
op.clear();
</script>
```
### Revenue Tracking
Track revenue events:
```vue
<script setup>
import { op } from '@/openpanel';
// Track revenue immediately
await op.revenue(29.99, { currency: 'USD' });
// Or accumulate revenue and flush later
op.pendingRevenue(29.99, { currency: 'USD' });
op.pendingRevenue(19.99, { currency: 'USD' });
await op.flushRevenue(); // Sends both revenue events
// Clear pending revenue
op.clearRevenue();
</script>
```
### Optional: Create a Composable
If you prefer using a composable pattern, you can create your own wrapper:
```ts title="src/composables/useOpenPanel.ts"
import { op } from '@/openpanel';
export function useOpenPanel() {
return op;
}
```
Then use it in your components:
```vue
<script setup>
import { useOpenPanel } from '@/composables/useOpenPanel';
const op = useOpenPanel();
op.track('my_event', { foo: 'bar' });
</script>
```
Use [script tag](/docs/sdks/script) or [Web SDK](/docs/sdks/web) for now. We'll add a dedicated vue sdk soon.

View File

@@ -6,7 +6,6 @@ difficulty: intermediate
timeToComplete: 10
date: 2025-12-15
lastUpdated: 2025-12-15
team: OpenPanel Team
steps:
- name: "Add the dependency"
anchor: "install"

View File

@@ -6,7 +6,6 @@ difficulty: beginner
timeToComplete: 7
date: 2025-12-15
lastUpdated: 2025-12-15
team: OpenPanel Team
steps:
- name: "Install the SDK"
anchor: "install"

File diff suppressed because one or more lines are too long

View File

@@ -121,7 +121,7 @@ export default async function Page({
/>
<div className="row gap-4 items-center mt-8">
<div className="size-10 center-center bg-black rounded-full">
{author?.image ? (
{author.image ? (
<Image
className="size-10 object-cover rounded-full"
src={author.image}
@@ -134,7 +134,7 @@ export default async function Page({
)}
</div>
<div className="col flex-1">
<p className="font-medium">{author?.name || 'OpenPanel Team'}</p>
<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()}

View File

@@ -12,11 +12,7 @@ import {
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
type PageProps = {
params: Promise<{ slug: string[] }>;
};
export default async function Page(props: PageProps) {
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
@@ -43,7 +39,9 @@ export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: PageProps): Promise<Metadata> {
export async function generateMetadata(
props: PageProps<'/docs/[[...slug]]'>,
): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();

View File

@@ -1,8 +1,8 @@
import { baseOptions } from '@/lib/layout.shared';
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: { children: React.ReactNode }) {
export default function Layout({ children }: LayoutProps<'/docs'>) {
return (
<DocsLayout tree={source.pageTree} {...baseOptions()}>
{children}

View File

@@ -28,7 +28,7 @@ export const viewport: Viewport = {
export const metadata: Metadata = getRootMetadata();
export default function Layout({ children }: { children: React.ReactNode }) {
export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html
lang="en"

View File

@@ -84,34 +84,6 @@ async function getOgData(
data?.data.description || 'Whooops, could not find this page',
};
}
case 'tools': {
if (segments.length > 1) {
const tool = segments[1];
switch (tool) {
case 'ip-lookup':
return {
title: 'IP Lookup Tool',
description:
'Find detailed information about any IP address including geolocation, ISP, and network details.',
};
case 'url-checker':
return {
title: 'URL Checker',
description:
'Analyze any website for SEO, social media, technical, and security information. Get comprehensive insights about any URL.',
};
default:
return {
title: 'Tools',
description: 'Free web tools for developers and website owners',
};
}
}
return {
title: 'Tools',
description: 'Free web tools for developers and website owners',
};
}
default: {
const data = await pageSource.getPage(segments);
return {

10
packages/auth/nextjs.ts Normal file
View File

@@ -0,0 +1,10 @@
import { unstable_cache } from 'next/cache';
import { cookies } from 'next/headers';
import { validateSessionToken } from './src/session';
export const auth = async () => {
const token = (await cookies().get('session')?.value) ?? null;
return cachedAuth(token);
};
export const cachedAuth = unstable_cache(validateSessionToken);

View File

@@ -22,6 +22,7 @@
"typescript": "catalog:"
},
"peerDependencies": {
"next": "14.2.1",
"react": "catalog:"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openpanel/astro",
"version": "1.0.6-local",
"version": "1.0.5-local",
"config": {
"transformPackageJson": false,
"transformEnvs": true
@@ -14,7 +14,7 @@
"files": ["src", "index.ts"],
"keywords": ["astro-component"],
"dependencies": {
"@openpanel/web": "workspace:1.0.6-local"
"@openpanel/web": "workspace:1.0.5-local"
},
"devDependencies": {
"astro": "^5.7.7"

View File

@@ -33,7 +33,7 @@ const methods: { name: OpenPanelMethodNames; value: unknown }[] = [
value: {
...options,
sdk: 'astro',
sdkVersion: '1.0.6',
sdkVersion: '1.0.5',
},
},
];

View File

@@ -1,13 +1,13 @@
{
"name": "@openpanel/express",
"version": "1.0.4-local",
"version": "1.0.3-local",
"module": "index.ts",
"scripts": {
"build": "rm -rf dist && tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@openpanel/sdk": "workspace:1.0.3-local",
"@openpanel/sdk": "workspace:1.0.2-local",
"@openpanel/common": "workspace:*"
},
"peerDependencies": {

View File

@@ -1,13 +1,13 @@
{
"name": "@openpanel/nextjs",
"version": "1.1.2-local",
"version": "1.1.1-local",
"module": "index.ts",
"scripts": {
"build": "rm -rf dist && tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@openpanel/web": "workspace:1.0.6-local"
"@openpanel/web": "workspace:1.0.5-local"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",

View File

@@ -1,5 +0,0 @@
import { defineBuildConfig } from 'unbuild';
export default defineBuildConfig({
failOnWarn: false,
});

View File

@@ -1,2 +0,0 @@
// This file is for development - the built version uses src/module.ts
export { default, type ModuleOptions } from './src/module';

View File

@@ -1,40 +0,0 @@
{
"name": "@openpanel/nuxt",
"version": "0.0.2-local",
"type": "module",
"main": "./dist/module.mjs",
"exports": {
".": {
"types": "./dist/module.d.mts",
"import": "./dist/module.mjs"
}
},
"files": ["dist"],
"config": {
"transformPackageJson": false,
"transformEnvs": false
},
"scripts": {
"build": "npx nuxt-module-build build",
"dev:prepare": "npx nuxt-module-build build --stub",
"prepack": "npx nuxt-module-build build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@openpanel/web": "workspace:1.0.6-local"
},
"peerDependencies": {
"h3": "^1.0.0",
"nuxt": "^3.0.0 || ^4.0.0"
},
"devDependencies": {
"@nuxt/kit": "^3.0.0",
"@nuxt/module-builder": "^1.0.2",
"@nuxt/types": "^2.18.1",
"@openpanel/tsconfig": "workspace:*",
"@types/node": "catalog:",
"@vue/runtime-core": "^3.5.25",
"typescript": "catalog:",
"unbuild": "^3.6.1"
}
}

View File

@@ -1,56 +0,0 @@
import {
addImports,
addPlugin,
addServerHandler,
createResolver,
defineNuxtModule,
} from '@nuxt/kit';
import type { ModuleOptions } from './types';
export type { ModuleOptions };
export default defineNuxtModule<ModuleOptions>({
meta: {
name: '@openpanel/nuxt',
configKey: 'openpanel',
},
defaults: {
trackScreenViews: true,
trackOutgoingLinks: true,
trackAttributes: true,
trackHashChanges: false,
disabled: false,
proxy: false, // Disabled by default
},
setup(options, nuxt) {
const resolver = createResolver(import.meta.url);
// If proxy is enabled, override apiUrl to use the proxy route
if (options.proxy) {
options.apiUrl = '/api/openpanel';
}
// Expose options to runtime config
nuxt.options.runtimeConfig.public.openpanel = options;
// Add client plugin (creates OpenPanel instance)
addPlugin({
src: resolver.resolve('./runtime/plugin.client'),
mode: 'client',
});
// Only register server proxy handler if proxy is enabled
if (options.proxy) {
addServerHandler({
route: '/api/openpanel/**',
handler: resolver.resolve('./runtime/server/api/[...openpanel]'),
});
}
// Auto-import the useOpenPanel composable
addImports({
name: 'useOpenPanel',
from: resolver.resolve('./runtime/composables/useOpenPanel'),
});
},
});

View File

@@ -1,6 +0,0 @@
import { useNuxtApp } from '#app';
export function useOpenPanel() {
const { $openpanel } = useNuxtApp();
return $openpanel;
}

View File

@@ -1,30 +0,0 @@
import { OpenPanel } from '@openpanel/web';
import { defineNuxtPlugin, useRuntimeConfig } from '#app';
import type { ModuleOptions } from '../types';
declare module '#app' {
interface NuxtApp {
$openpanel: OpenPanel;
}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$openpanel: OpenPanel;
}
}
export default defineNuxtPlugin({
name: 'openpanel',
parallel: true,
setup() {
const config = useRuntimeConfig().public.openpanel as ModuleOptions;
const op = new OpenPanel(config);
return {
provide: {
openpanel: op,
},
};
},
});

View File

@@ -1,90 +0,0 @@
import {
type EventHandlerRequest,
type H3Event,
createError,
defineEventHandler,
getHeader,
getRequestIP,
getRequestURL,
readBody,
setResponseStatus,
} from 'h3';
const API_URL = 'https://api.openpanel.dev';
function getClientHeaders(event: H3Event<EventHandlerRequest>): Headers {
const headers = new Headers();
// Get IP from multiple possible headers (like Next.js does)
const ip =
getHeader(event, 'cf-connecting-ip') ||
getHeader(event, 'x-forwarded-for')?.split(',')[0] ||
getRequestIP(event);
headers.set('Content-Type', 'application/json');
headers.set(
'openpanel-client-id',
getHeader(event, 'openpanel-client-id') || '',
);
// Construct origin: browsers send Origin header for POST requests and cross-origin requests,
// but not for same-origin GET requests. Fallback to constructing from request URL.
const origin =
getHeader(event, 'origin') ||
(() => {
const url = getRequestURL(event);
return `${url.protocol}//${url.host}`;
})();
headers.set('origin', origin);
headers.set('User-Agent', getHeader(event, 'user-agent') || '');
if (ip) {
headers.set('openpanel-client-ip', ip);
}
return headers;
}
async function handleApiRoute(
event: H3Event<EventHandlerRequest>,
apiPath: string,
) {
try {
const res = await fetch(`${API_URL}${apiPath}`, {
method: event.method,
headers: getClientHeaders(event),
body:
event.method === 'POST'
? JSON.stringify(await readBody(event))
: undefined,
});
setResponseStatus(event, res.status);
if (res.headers.get('content-type')?.includes('application/json')) {
return res.json();
}
return res.text();
} catch (e) {
throw createError({
statusCode: 500,
message: 'Failed to proxy request',
data: e instanceof Error ? e.message : String(e),
});
}
}
export default defineEventHandler(async (event) => {
const url = getRequestURL(event);
const pathname = url.pathname;
// Handle API routes: /track/*
const apiPathMatch = pathname.indexOf('/track');
if (apiPathMatch === -1) {
throw createError({ statusCode: 404, message: 'Not found' });
}
const apiPath = pathname.substring(apiPathMatch);
return handleApiRoute(event, apiPath);
});

View File

@@ -1,20 +0,0 @@
import type { OpenPanel, OpenPanelOptions } from '@openpanel/web';
export interface ModuleOptions extends OpenPanelOptions {
proxy?: boolean;
}
declare module '#app' {
interface NuxtApp {
$openpanel: OpenPanel;
}
}
declare module 'vue' {
interface ComponentCustomProperties {
$openpanel: OpenPanel;
}
}
// biome-ignore lint/complexity/noUselessEmptyExport: we need to export an empty object to satisfy the type checker
export {};

View File

@@ -1,17 +0,0 @@
{
"extends": "@openpanel/tsconfig/sdk.json",
"compilerOptions": {
"incremental": false,
"outDir": "dist",
"paths": {
"#app": [
"./node_modules/nuxt/dist/app/index"
]
},
"types": [
"@types/node",
"@nuxt/types"
]
},
"exclude": ["dist"]
}

View File

@@ -1,13 +1,13 @@
{
"name": "@openpanel/react-native",
"version": "1.0.4-local",
"version": "1.0.3-local",
"module": "index.ts",
"scripts": {
"build": "rm -rf dist && tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@openpanel/sdk": "workspace:1.0.3-local"
"@openpanel/sdk": "workspace:1.0.2-local"
},
"devDependencies": {
"@openpanel/tsconfig": "workspace:*",

View File

@@ -1,6 +1,6 @@
{
"name": "@openpanel/sdk",
"version": "1.0.3-local",
"version": "1.0.2-local",
"module": "index.ts",
"scripts": {
"build": "rm -rf dist && tsup",

View File

@@ -68,7 +68,6 @@ export type OpenPanelOptions = {
waitForProfile?: boolean;
filter?: (payload: TrackHandlerPayload) => boolean;
disabled?: boolean;
debug?: boolean;
};
export class OpenPanel {
@@ -130,7 +129,6 @@ export class OpenPanel {
}
async track(name: string, properties?: TrackProperties) {
this.log('track event', name, properties);
return this.send({
type: 'track',
payload: {
@@ -145,7 +143,6 @@ export class OpenPanel {
}
async identify(payload: IdentifyPayload) {
this.log('identify user', payload);
if (payload.profileId) {
this.profileId = payload.profileId;
this.flush();
@@ -165,10 +162,12 @@ export class OpenPanel {
}
}
/**
* @deprecated This method is deprecated and will be removed in a future version.
*/
async alias(payload: AliasPayload) {}
async alias(payload: AliasPayload) {
return this.send({
type: 'alias',
payload,
});
}
async increment(payload: IncrementPayload) {
return this.send({
@@ -225,10 +224,4 @@ export class OpenPanel {
});
this.queue = [];
}
log(...args: any[]) {
if (this.options.debug) {
console.log('[OpenPanel.dev]', ...args);
}
}
}

View File

@@ -1,13 +1,13 @@
{
"name": "@openpanel/web",
"version": "1.0.6-local",
"version": "1.0.5-local",
"module": "index.ts",
"scripts": {
"build": "rm -rf dist && tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@openpanel/sdk": "workspace:1.0.3-local"
"@openpanel/sdk": "workspace:1.0.2-local"
},
"devDependencies": {
"@openpanel/tsconfig": "workspace:*",

View File

@@ -38,6 +38,8 @@ export class OpenPanel extends OpenPanelBase {
});
if (!this.isServer()) {
console.log('OpenPanel.dev - Initialized', this.options);
try {
const pending = sessionStorage.getItem('openpanel-pending-revenues');
if (pending) {
@@ -200,6 +202,7 @@ export class OpenPanel extends OpenPanelBase {
}
this.lastPath = path;
console.log('OpenPanel.dev - Track page view');
super.track('screen_view', {
...(properties ?? {}),
__path: path,

6833
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,10 @@ packages:
# Define a catalog of version ranges.
catalog:
zod: ^3.24.2
react: ^19.2.3
"@types/react": ^19.2.3
"react-dom": ^19.2.3
"@types/react-dom": ^19.2.3
react: ^19.0.0
"@types/react": ^19.0.0
"react-dom": ^19.0.0
"@types/react-dom": ^19.0.0
"@types/node": ^24.7.1
typescript: ^5.9.3
groupmq: 1.1.1-next.2