2 Commits

Author SHA1 Message Date
Carl-Gerhard Lindesvärd
ba86316218 fix: improve api route for nuxt 2025-12-15 22:26:18 +01:00
Carl-Gerhard Lindesvärd
684cba9c84 wip 2025-12-15 15:38:51 +01:00
13 changed files with 4990 additions and 215 deletions

View File

@@ -0,0 +1,256 @@
---
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,4 +2,244 @@
title: React
---
Use [script tag](/docs/sdks/script) or [Web SDK](/docs/sdks/web) for now. We'll add a dedicated react sdk soon.
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>;
}
```

View File

@@ -2,4 +2,219 @@
title: Vue
---
Use [script tag](/docs/sdks/script) or [Web SDK](/docs/sdks/web) for now. We'll add a dedicated vue sdk soon.
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>
```

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
{
"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

@@ -0,0 +1,56 @@
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

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

View File

@@ -0,0 +1,30 @@
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

@@ -0,0 +1,90 @@
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);
});

20
packages/sdks/nuxt/src/types.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,17 @@
{
"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"]
}

4224
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff