feat: sdks and docs (#239)
* init * fix * update docs * bump: all sdks * rename types test
This commit is contained in:
committed by
GitHub
parent
790801b728
commit
83e223a496
@@ -88,9 +88,7 @@ We built OpenPanel from the ground up with privacy at its heart—and with featu
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.op = window.op || function(...args) {
|
||||
(window.op.q = window.op.q || []).push(args);
|
||||
};
|
||||
window.op=window.op||function(){var n=[],o=new Proxy((function(){arguments.length>0&&n.push(Array.prototype.slice.call(arguments))}),{get:function(o,t){return"q"===t?n:function(){n.push([t].concat(Array.prototype.slice.call(arguments)))}}});return o}();
|
||||
window.op('init', {
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
|
||||
104
apps/public/content/docs/(tracking)/adblockers.mdx
Normal file
104
apps/public/content/docs/(tracking)/adblockers.mdx
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Avoid adblockers with proxy
|
||||
description: Learn why adblockers block analytics and how to avoid it by proxying events.
|
||||
---
|
||||
|
||||
In this article we need to talk about adblockers, why they exist, how they work, and how to avoid them.
|
||||
|
||||
Adblockers' main purpose was initially to block ads, but they have since started to block tracking scripts as well. This is primarily for privacy reasons, and while we respect that, there are legitimate use cases for understanding your visitors. OpenPanel is designed to be a privacy-friendly, cookieless analytics tool that doesn't track users across sites, but generic blocklists often catch all analytics tools indiscriminately.
|
||||
|
||||
The best way to avoid adblockers is to proxy events via your own domain name. Adblockers generally cannot block requests to your own domain (first-party requests) without breaking the functionality of the site itself.
|
||||
|
||||
## Built-in Support
|
||||
|
||||
Today, our Next.js SDK and WordPress plugin have built-in support for proxying:
|
||||
- **WordPress**: Does it automatically.
|
||||
- **Next.js**: Easy to setup with a route handler.
|
||||
|
||||
## Implementing Proxying for Any Framework
|
||||
|
||||
If you are not using Next.js or WordPress, you can implement proxying in any backend framework. The key is to set up an API endpoint on your domain (e.g., `api.domain.com` or `domain.com/api`) that forwards requests to OpenPanel.
|
||||
|
||||
Below is an example of how to set up a proxy using a [Hono](https://hono.dev/) server. This implementation mimics the logic used in our Next.js SDK.
|
||||
|
||||
> You can always see how our Next.js implementation looks like in our [repository](https://github.com/Openpanel-dev/openpanel/blob/main/packages/sdks/nextjs/createNextRouteHandler.ts).
|
||||
|
||||
### Hono Example
|
||||
|
||||
```typescript
|
||||
import { Hono } from 'hono'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
// 1. Proxy the script file
|
||||
app.get('/op1.js', async (c) => {
|
||||
const scriptUrl = 'https://openpanel.dev/op1.js'
|
||||
try {
|
||||
const res = await fetch(scriptUrl)
|
||||
const text = await res.text()
|
||||
|
||||
c.header('Content-Type', 'text/javascript')
|
||||
// Optional caching for 24 hours
|
||||
c.header('Cache-Control', 'public, max-age=86400, stale-while-revalidate=86400')
|
||||
return c.body(text)
|
||||
} catch (e) {
|
||||
return c.json({ error: 'Failed to fetch script' }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Proxy the track event
|
||||
app.post('/track', async (c) => {
|
||||
const body = await c.req.json()
|
||||
|
||||
// Forward the client's IP address (be sure to pick correct IP based on your infra)
|
||||
const ip = c.req.header('cf-connecting-ip') ??
|
||||
c.req.header('x-forwarded-for')?.split(',')[0]
|
||||
|
||||
const headers = new Headers()
|
||||
headers.set('Content-Type', 'application/json')
|
||||
headers.set('Origin', c.req.header('origin') ?? '')
|
||||
headers.set('User-Agent', c.req.header('user-agent') ?? '')
|
||||
headers.set('openpanel-client-id', c.req.header('openpanel-client-id') ?? '')
|
||||
|
||||
if (ip) {
|
||||
headers.set('openpanel-client-ip', ip)
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('https://api.openpanel.dev/track', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
return c.json(await res.text(), res.status)
|
||||
} catch (e) {
|
||||
return c.json(e, 500)
|
||||
}
|
||||
})
|
||||
|
||||
export default app
|
||||
```
|
||||
|
||||
This script sets up two endpoints:
|
||||
1. `GET /op1.js`: Fetches the OpenPanel script and serves it from your domain.
|
||||
2. `POST /track`: Receives events from the frontend, adds necessary headers (User-Agent, Origin, Content-Type, openpanel-client-id, openpanel-client-ip), and forwards them to OpenPanel's API.
|
||||
|
||||
## Frontend Configuration
|
||||
|
||||
Once your proxy is running, you need to configure the OpenPanel script on your frontend to use your proxy endpoints instead of the default ones.
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.op=window.op||function(){var n=[],o=new Proxy((function(){arguments.length>0&&n.push(Array.prototype.slice.call(arguments))}),{get:function(o,t){return"q"===t?n:function(){n.push([t].concat(Array.prototype.slice.call(arguments)))}}});return o}();
|
||||
window.op('init', {
|
||||
apiUrl: 'https://api.domain.com'
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
</script>
|
||||
<script src="https://api.domain.com/op1.js" defer async></script>
|
||||
```
|
||||
|
||||
By doing this, all requests are sent to your domain first, bypassing adblockers that look for third-party tracking domains.
|
||||
190
apps/public/content/docs/(tracking)/how-it-works.mdx
Normal file
190
apps/public/content/docs/(tracking)/how-it-works.mdx
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
title: How it works
|
||||
description: Understanding device IDs, session IDs, profile IDs, and event tracking
|
||||
---
|
||||
|
||||
## Device ID
|
||||
|
||||
A **device ID** is a unique identifier generated for each device/browser combination. It's calculated using a hash function that combines:
|
||||
|
||||
- **User Agent** (browser/client information)
|
||||
- **IP Address**
|
||||
- **Origin** (project ID)
|
||||
- **Salt** (a rotating secret key)
|
||||
|
||||
```typescript:packages/common/server/profileId.ts
|
||||
export function generateDeviceId({
|
||||
salt,
|
||||
ua,
|
||||
ip,
|
||||
origin,
|
||||
}: GenerateDeviceIdOptions) {
|
||||
return createHash(`${ua}:${ip}:${origin}:${salt}`, 16);
|
||||
}
|
||||
```
|
||||
|
||||
### Salt Rotation
|
||||
|
||||
The salt used for device ID generation rotates **daily at midnight** (UTC). This means:
|
||||
|
||||
- Device IDs remain consistent throughout a single day
|
||||
- Device IDs reset each day for privacy purposes
|
||||
- The system maintains both the current and previous day's salt to handle events that may arrive slightly after midnight
|
||||
|
||||
```typescript:apps/worker/src/jobs/cron.salt.ts
|
||||
// Salt rotation happens daily at midnight (pattern: '0 0 * * *')
|
||||
```
|
||||
|
||||
When the salt rotates, all device IDs change, effectively anonymizing tracking data on a daily basis while still allowing session continuity within a 24-hour period.
|
||||
|
||||
## Session ID
|
||||
|
||||
A **session** represents a continuous period of user activity. Sessions are used to group related events together and understand user behavior patterns.
|
||||
|
||||
### Session Duration
|
||||
|
||||
Sessions have a **30-minute timeout**. If no events are received for 30 minutes, the session automatically ends. Each new event resets this 30-minute timer.
|
||||
|
||||
```typescript:apps/worker/src/utils/session-handler.ts
|
||||
export const SESSION_TIMEOUT = 1000 * 60 * 30; // 30 minutes
|
||||
```
|
||||
|
||||
### Session Creation Rules
|
||||
|
||||
Sessions are **only created for client events**, not server events. This means:
|
||||
|
||||
- Events sent from browsers, mobile apps, or client-side SDKs will create sessions
|
||||
- Events sent from backend servers, scripts, or server-side SDKs will **not** create sessions
|
||||
- If you only track events from your backend, no sessions will be created
|
||||
|
||||
Additionally, sessions are **not created for events older than 15 minutes**. This prevents historical data imports from creating artificial sessions.
|
||||
|
||||
```typescript:apps/worker/src/jobs/events.incoming-event.ts
|
||||
// Sessions are not created if:
|
||||
// 1. The event is from a server (uaInfo.isServer === true)
|
||||
// 2. The timestamp is from the past (isTimestampFromThePast === true)
|
||||
if (uaInfo.isServer || isTimestampFromThePast) {
|
||||
// Event is attached to existing session or no session
|
||||
}
|
||||
```
|
||||
|
||||
## Profile ID
|
||||
|
||||
A **profile ID** is a persistent identifier for a user across multiple devices and sessions. It allows you to track the same user across different browsers, devices, and time periods.
|
||||
|
||||
### Profile ID Assignment
|
||||
|
||||
If a `profileId` is provided when tracking an event, it will be used to identify the user. However, **if no `profileId` is provided, it defaults to the `deviceId`**.
|
||||
|
||||
This means:
|
||||
- Anonymous users (without a profile ID) are tracked by their device ID
|
||||
- Once you identify a user (by providing a profile ID), all their events will be associated with that profile
|
||||
- The same user can be tracked across multiple devices by using the same profile ID
|
||||
|
||||
```typescript:packages/db/src/services/event.service.ts
|
||||
// If no profileId is provided, it defaults to deviceId
|
||||
if (!payload.profileId && payload.deviceId) {
|
||||
payload.profileId = payload.deviceId;
|
||||
}
|
||||
```
|
||||
|
||||
## Client Events vs Server Events
|
||||
|
||||
OpenPanel distinguishes between **client events** and **server events** based on the User-Agent header.
|
||||
|
||||
### Client Events
|
||||
|
||||
Client events are sent from:
|
||||
- Web browsers (Chrome, Firefox, Safari, etc.)
|
||||
- Mobile apps using client-side SDKs
|
||||
- Any client that sends a browser-like User-Agent
|
||||
|
||||
Client events:
|
||||
- Create sessions
|
||||
- Generate device IDs
|
||||
- Support full session tracking
|
||||
|
||||
### Server Events
|
||||
|
||||
Server events are detected when the User-Agent matches server patterns, such as:
|
||||
- `Go-http-client/1.0`
|
||||
- `node-fetch/1.0`
|
||||
- Other single-name/version patterns (e.g., `LibraryName/1.0`)
|
||||
|
||||
Server events:
|
||||
- Do **not** create sessions
|
||||
- Are attached to existing sessions if available
|
||||
- Are useful for backend tracking without session management
|
||||
|
||||
```typescript:packages/common/server/parser-user-agent.ts
|
||||
// Server events are detected by patterns like "Go-http-client/1.0"
|
||||
function isServer(res: UAParser.IResult) {
|
||||
if (SINGLE_NAME_VERSION_REGEX.test(res.ua)) {
|
||||
return true;
|
||||
}
|
||||
// ... additional checks
|
||||
}
|
||||
```
|
||||
|
||||
The distinction is made in the event processing pipeline:
|
||||
|
||||
```typescript:apps/worker/src/jobs/events.incoming-event.ts
|
||||
const uaInfo = parseUserAgent(userAgent, properties);
|
||||
|
||||
// Only client events create sessions
|
||||
if (uaInfo.isServer || isTimestampFromThePast) {
|
||||
// Server events or old events don't create new sessions
|
||||
}
|
||||
```
|
||||
|
||||
## Timestamps
|
||||
|
||||
Events can include custom timestamps to track when events actually occurred, rather than when they were received by the server.
|
||||
|
||||
### Setting Custom Timestamps
|
||||
|
||||
You can provide a custom timestamp using the `__timestamp` property in your event properties:
|
||||
|
||||
```javascript
|
||||
track('page_view', {
|
||||
__timestamp: '2024-01-15T10:30:00Z'
|
||||
});
|
||||
```
|
||||
|
||||
### Timestamp Validation
|
||||
|
||||
The system validates timestamps to prevent abuse and ensure data quality:
|
||||
|
||||
1. **Future timestamps**: If a timestamp is more than **1 minute in the future**, the server timestamp is used instead
|
||||
2. **Past timestamps**: If a timestamp is older than **15 minutes**, it's marked as `isTimestampFromThePast: true`
|
||||
|
||||
```typescript:apps/api/src/controllers/track.controller.ts
|
||||
// Timestamp validation logic
|
||||
const ONE_MINUTE_MS = 60 * 1000;
|
||||
const FIFTEEN_MINUTES_MS = 15 * ONE_MINUTE_MS;
|
||||
|
||||
// Future check: more than 1 minute ahead
|
||||
if (clientTimestampNumber > safeTimestamp + ONE_MINUTE_MS) {
|
||||
return { timestamp: safeTimestamp, isTimestampFromThePast: false };
|
||||
}
|
||||
|
||||
// Past check: older than 15 minutes
|
||||
const isTimestampFromThePast =
|
||||
clientTimestampNumber < safeTimestamp - FIFTEEN_MINUTES_MS;
|
||||
```
|
||||
|
||||
### Timestamp Impact on Sessions
|
||||
|
||||
**Important**: Events with timestamps older than 15 minutes (`isTimestampFromThePast: true`) will **not create new sessions**. This prevents historical data imports from creating artificial sessions in your analytics.
|
||||
|
||||
```typescript:apps/worker/src/jobs/events.incoming-event.ts
|
||||
// Events from the past don't create sessions
|
||||
if (uaInfo.isServer || isTimestampFromThePast) {
|
||||
// Attach to existing session or track without session
|
||||
}
|
||||
```
|
||||
|
||||
This ensures that:
|
||||
- Real-time tracking creates proper sessions
|
||||
- Historical data imports don't interfere with session analytics
|
||||
- Backdated events are still tracked but don't affect session metrics
|
||||
3
apps/public/content/docs/(tracking)/meta.json
Normal file
3
apps/public/content/docs/(tracking)/meta.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"pages": ["sdks", "how-it-works", "..."]
|
||||
}
|
||||
20
apps/public/content/docs/(tracking)/sdks/meta.json
Normal file
20
apps/public/content/docs/(tracking)/sdks/meta.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"title": "SDKs",
|
||||
"pages": [
|
||||
"script",
|
||||
"web",
|
||||
"javascript",
|
||||
"nextjs",
|
||||
"react",
|
||||
"vue",
|
||||
"astro",
|
||||
"remix",
|
||||
"express",
|
||||
"python",
|
||||
"react-native",
|
||||
"swift",
|
||||
"kotlin",
|
||||
"..."
|
||||
],
|
||||
"defaultOpen": false
|
||||
}
|
||||
@@ -15,7 +15,7 @@ Just insert this snippet and replace `YOUR_CLIENT_ID` with your client id.
|
||||
|
||||
```html title="index.html" /clientId: 'YOUR_CLIENT_ID'/
|
||||
<script>
|
||||
window.op = window.op||function(...args){(window.op.q=window.op.q||[]).push(args);};
|
||||
window.op=window.op||function(){var n=[],o=new Proxy((function(){arguments.length>0&&n.push(Array.prototype.slice.call(arguments))}),{get:function(o,t){return"q"===t?n:function(){n.push([t].concat(Array.prototype.slice.call(arguments)))}}});return o}();
|
||||
window.op('init', {
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
47
apps/public/content/docs/get-started/identify-users.mdx
Normal file
47
apps/public/content/docs/get-started/identify-users.mdx
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Identify Users
|
||||
description: Connect anonymous events to specific users.
|
||||
---
|
||||
|
||||
By default, OpenPanel tracks visitors anonymously. To connect these events to a specific user in your database, you need to identify them.
|
||||
|
||||
## How it works
|
||||
|
||||
When a user logs in or signs up, you should call the `identify` method. This associates their current session and all future events with their unique ID from your system.
|
||||
|
||||
```javascript
|
||||
op.identify({
|
||||
profileId: 'user_123'
|
||||
});
|
||||
```
|
||||
|
||||
## Adding user traits
|
||||
|
||||
You can also pass user traits (like name, email, or plan type) when you identify them. These traits will appear in the user's profile in your dashboard.
|
||||
|
||||
```javascript
|
||||
op.identify({
|
||||
profileId: 'user_123',
|
||||
firstName: 'Jane',
|
||||
lastName: 'Doe',
|
||||
email: 'jane@example.com',
|
||||
company: 'Acme Inc'
|
||||
});
|
||||
```
|
||||
|
||||
### Standard traits
|
||||
|
||||
We recommend using these standard keys for common user information so they display correctly in the OpenPanel dashboard:
|
||||
|
||||
- `firstName`
|
||||
- `lastName`
|
||||
- `email`
|
||||
- `phone`
|
||||
- `avatar`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Call on login**: Always identify the user immediately after they log in.
|
||||
2. **Call on update**: If a user updates their profile, call identify again with the new information.
|
||||
3. **Unique IDs**: Use a stable, unique ID from your database (like a UUID) rather than an email address or username that might change.
|
||||
|
||||
81
apps/public/content/docs/get-started/install-openpanel.mdx
Normal file
81
apps/public/content/docs/get-started/install-openpanel.mdx
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: Install OpenPanel
|
||||
description: Get started with OpenPanel in less than 2 minutes.
|
||||
---
|
||||
|
||||
import { Cards, Card } from 'fumadocs-ui/components/card';
|
||||
import { Code, Globe, Layout, Smartphone, FileJson } from 'lucide-react';
|
||||
|
||||
The quickest way to get started with OpenPanel is to use our Web SDK. It works with any website.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Simply add this script tag to your website's `<head>` section.
|
||||
|
||||
```html title="index.html"
|
||||
<script>
|
||||
window.op=window.op||function(){var n=[],o=new Proxy((function(){arguments.length>0&&n.push(Array.prototype.slice.call(arguments))}),{get:function(o,t){return"q"===t?n:function(){n.push([t].concat(Array.prototype.slice.call(arguments)))}}});return o}();
|
||||
window.op('init', {
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
</script>
|
||||
<script src="https://openpanel.dev/op1.js" defer async></script>
|
||||
```
|
||||
|
||||
That's it! OpenPanel will now automatically track:
|
||||
- Page views
|
||||
- Visit duration
|
||||
- Referrers
|
||||
- Device and browser information
|
||||
- Location
|
||||
|
||||
## Using a Framework?
|
||||
|
||||
If you are using a specific framework or platform, we have dedicated SDKs that provide a better developer experience.
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
href="/docs/sdks/nextjs"
|
||||
title="Next.js"
|
||||
icon={<Globe />}
|
||||
description="Optimized for App Router and Server Components"
|
||||
/>
|
||||
<Card
|
||||
href="/docs/sdks/react"
|
||||
title="React"
|
||||
icon={<Layout />}
|
||||
description="Components and hooks for React applications"
|
||||
/>
|
||||
<Card
|
||||
href="/docs/sdks/vue"
|
||||
title="Vue"
|
||||
icon={<Layout />}
|
||||
description="Integration for Vue.js applications"
|
||||
/>
|
||||
<Card
|
||||
href="/docs/sdks/javascript"
|
||||
title="JavaScript"
|
||||
icon={<FileJson />}
|
||||
description="Universal JavaScript/TypeScript SDK"
|
||||
/>
|
||||
<Card
|
||||
href="/docs/sdks/react-native"
|
||||
title="React Native"
|
||||
icon={<Smartphone />}
|
||||
description="Track mobile apps with React Native"
|
||||
/>
|
||||
<Card
|
||||
href="/docs/sdks/python"
|
||||
title="Python"
|
||||
icon={<Code />}
|
||||
description="Server-side tracking for Python"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
## Explore all SDKs
|
||||
|
||||
We support many more platforms. Check out our [SDKs Overview](/docs/sdks) for the full list.
|
||||
|
||||
8
apps/public/content/docs/get-started/meta.json
Normal file
8
apps/public/content/docs/get-started/meta.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"pages": [
|
||||
"install-openpanel",
|
||||
"track-events",
|
||||
"identify-users",
|
||||
"revenue-tracking"
|
||||
]
|
||||
}
|
||||
48
apps/public/content/docs/get-started/track-events.mdx
Normal file
48
apps/public/content/docs/get-started/track-events.mdx
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Track Events
|
||||
description: Learn how to track custom events to measure user actions.
|
||||
---
|
||||
|
||||
Events are the core of OpenPanel. They allow you to measure specific actions users take on your site, like clicking a button, submitting a form, or completing a purchase.
|
||||
|
||||
## Tracking an event
|
||||
|
||||
To track an event, simply call the `track` method with an event name.
|
||||
|
||||
```javascript
|
||||
op.track('button_clicked');
|
||||
```
|
||||
|
||||
## Adding properties
|
||||
|
||||
You can add additional context to your events by passing a properties object. This helps you understand the details of the interaction.
|
||||
|
||||
```javascript
|
||||
op.track('signup_button_clicked', {
|
||||
location: 'header',
|
||||
color: 'blue',
|
||||
variant: 'primary'
|
||||
});
|
||||
```
|
||||
|
||||
### Common property types
|
||||
|
||||
- **Strings**: Text values like names, categories, or IDs.
|
||||
- **Numbers**: Numeric values like price, quantity, or score.
|
||||
- **Booleans**: True or false values.
|
||||
|
||||
## Using Data Attributes
|
||||
|
||||
If you prefer not to write JavaScript, you can use data attributes to track clicks automatically.
|
||||
|
||||
```html
|
||||
<button
|
||||
data-track="signup_clicked"
|
||||
data-location="header"
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
```
|
||||
|
||||
When a user clicks this button, OpenPanel will automatically track a `signup_clicked` event with the property `location: 'header'`.
|
||||
|
||||
@@ -1,111 +1,83 @@
|
||||
---
|
||||
title: Introduction to OpenPanel
|
||||
description: Get started with OpenPanel's powerful analytics platform that combines the best of product and web analytics in one simple solution.
|
||||
title: What is OpenPanel?
|
||||
description: OpenPanel is an open-source web and product analytics platform that combines the power of Mixpanel with the ease of Plausible. Whether you're tracking website visitors or analyzing user behavior in your app, OpenPanel provides the insights you need without the complexity.
|
||||
---
|
||||
|
||||
## What is OpenPanel?
|
||||
import { UserIcon,HardDriveIcon } from 'lucide-react'
|
||||
|
||||
OpenPanel is an open-source analytics platform that combines product analytics (like Mixpanel) with web analytics (like Plausible) into one simple solution. Whether you're tracking website visitors or analyzing user behavior in your app, OpenPanel provides the insights you need without the complexity.
|
||||
## ✨ Key Features
|
||||
|
||||
## Key Features
|
||||
- **🔍 Advanced Analytics**: Funnels, cohorts, user profiles, and session history
|
||||
- **📊 Real-time Dashboards**: Live data updates and interactive charts
|
||||
- **🎯 A/B Testing**: Built-in variant testing with detailed breakdowns
|
||||
- **🔔 Smart Notifications**: Event and funnel-based alerts
|
||||
- **🌍 Privacy-First**: Cookieless tracking and GDPR compliance
|
||||
- **🚀 Developer-Friendly**: Comprehensive SDKs and API access
|
||||
- **📦 Self-Hosted**: Full control over your data and infrastructure
|
||||
- **💸 Transparent Pricing**: No hidden costs
|
||||
- **🛠️ Custom Dashboards**: Flexible chart creation and data visualization
|
||||
- **📱 Multi-Platform**: Web, mobile (iOS/Android), and server-side tracking
|
||||
|
||||
### Web Analytics
|
||||
- **Real-time data**: See visitor activity as it happens
|
||||
- **Traffic sources**: Understand where your visitors come from
|
||||
- **Geographic insights**: Track visitor locations and trends
|
||||
- **Device analytics**: Monitor usage across different devices
|
||||
- **Page performance**: Analyze your most visited pages
|
||||
## 📊 Analytics Platform Comparison
|
||||
|
||||
### Product Analytics
|
||||
- **Event tracking**: Monitor user actions and interactions
|
||||
- **User profiles**: Build detailed user journey insights
|
||||
- **Funnels**: Analyze conversion paths
|
||||
- **Retention**: Track user engagement over time
|
||||
- **Custom properties**: Add context to your events
|
||||
| Feature | OpenPanel | Mixpanel | GA4 | Plausible |
|
||||
|----------------------------------------|-----------|----------|-----------|-----------|
|
||||
| ✅ Open-source | ✅ | ❌ | ❌ | ✅ |
|
||||
| 🧩 Self-hosting supported | ✅ | ❌ | ❌ | ✅ |
|
||||
| 🔒 Cookieless by default | ✅ | ❌ | ❌ | ✅ |
|
||||
| 🔁 Real-time dashboards | ✅ | ✅ | ❌ | ✅ |
|
||||
| 🔍 Funnels & cohort analysis | ✅ | ✅ | ✅* | ✅*** |
|
||||
| 👤 User profiles & session history | ✅ | ✅ | ❌ | ❌ |
|
||||
| 📈 Custom dashboards & charts | ✅ | ✅ | ✅ | ❌ |
|
||||
| 💬 Event & funnel notifications | ✅ | ✅ | ❌ | ❌ |
|
||||
| 🌍 GDPR-compliant tracking | ✅ | ✅ | ❌** | ✅ |
|
||||
| 📦 SDKs (Web, Swift, Kotlin, ReactNative) | ✅ | ✅ | ✅ | ❌ |
|
||||
| 💸 Transparent pricing | ✅ | ❌ | ✅* | ✅ |
|
||||
| 🚀 Built for developers | ✅ | ✅ | ❌ | ✅ |
|
||||
| 🔧 A/B testing & variant breakdowns | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
## Getting Started
|
||||
✅* GA4 has a free tier but often requires BigQuery (paid) for raw data access.
|
||||
❌** GA4 has faced GDPR bans in several EU countries due to data transfers to US-based servers.
|
||||
✅*** Plausible has simple goals
|
||||
|
||||
1. **Installation**: Choose your preferred method:
|
||||
- [Script tag](/docs/sdks/script) - Quickest way to get started
|
||||
- [Web SDK](/docs/sdks/web) - For more control and TypeScript support
|
||||
- [React](/docs/sdks/react) - Native React integration
|
||||
- [Next.js](/docs/sdks/nextjs) - Optimized for Next.js apps
|
||||
## 🚀 Quick Start
|
||||
|
||||
2. **Core Methods**:
|
||||
```js
|
||||
// Track an event
|
||||
track('button_clicked', {
|
||||
buttonId: 'signup',
|
||||
location: 'header'
|
||||
});
|
||||
Before you can start tracking your events you'll need to create an account or spin up your own instance of OpenPanel.
|
||||
|
||||
// Identify a user
|
||||
identify({
|
||||
profileId: 'user123',
|
||||
email: 'user@example.com',
|
||||
firstName: 'John'
|
||||
});
|
||||
```
|
||||
<Cards>
|
||||
<Card
|
||||
href="https://dashboard.openpanel.dev/onboarding"
|
||||
title="Create an account"
|
||||
icon={<UserIcon />}
|
||||
description="Create your account and workspace"
|
||||
/>
|
||||
<Card
|
||||
href="/docs/self-hosting/self-hosting"
|
||||
title="Self-hosted OpenPanel"
|
||||
icon={<HardDriveIcon />}
|
||||
description="Get full control and start self-host"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
## Privacy First
|
||||
1. **[Install OpenPanel](/docs/get-started/install-openpanel)** - Add the script tag or use one of our SDKs
|
||||
2. **[Track Events](/docs/get-started/track-events)** - Start measuring user actions
|
||||
3. **[Identify Users](/docs/get-started/identify-users)** - Connect events to specific users
|
||||
4. **[Track Revenue](/docs/get-started/revenue-tracking)** - Monitor purchases and subscriptions
|
||||
|
||||
## 🔒 Privacy First
|
||||
|
||||
OpenPanel is built with privacy in mind:
|
||||
- No cookies required
|
||||
- GDPR and CCPA compliant
|
||||
- Self-hosting option available
|
||||
- Full control over your data
|
||||
- **No cookies required** - Cookieless tracking by default
|
||||
- **GDPR and CCPA compliant** - Built for privacy regulations
|
||||
- **Self-hosting option** - Full control over your data
|
||||
- **Transparent data handling** - You own your data
|
||||
|
||||
## Open Source
|
||||
## 🌐 Open Source
|
||||
|
||||
OpenPanel is fully open-source and available on [GitHub](https://github.com/Openpanel-dev/openpanel). We believe in transparency and community-driven development.
|
||||
|
||||
## Need Help?
|
||||
## 💬 Need Help?
|
||||
|
||||
- Join our [Discord community](https://go.openpanel.dev/discord)
|
||||
- Check our [GitHub issues](https://github.com/Openpanel-dev/openpanel/issues)
|
||||
- Email us at [hello@openpanel.dev](mailto:hello@openpanel.dev)
|
||||
|
||||
## Core Methods
|
||||
|
||||
### Set global properties
|
||||
|
||||
Sets global properties that will be included with every subsequent event.
|
||||
|
||||
### Track
|
||||
|
||||
Tracks a custom event with the given name and optional properties.
|
||||
|
||||
#### Tips
|
||||
|
||||
You can identify the user directly with this method.
|
||||
|
||||
```js title="Example shown in JavaScript"
|
||||
track('your_event_name', {
|
||||
foo: 'bar',
|
||||
baz: 'qux',
|
||||
// reserved property name
|
||||
__identify: {
|
||||
profileId: 'your_user_id', // required
|
||||
email: 'your_user_email',
|
||||
firstName: 'your_user_name',
|
||||
lastName: 'your_user_name',
|
||||
avatar: 'your_user_avatar',
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Identify
|
||||
|
||||
Associates the current user with a unique identifier and optional traits.
|
||||
|
||||
### Increment
|
||||
|
||||
Increments a numeric property for a user.
|
||||
|
||||
### Decrement
|
||||
|
||||
Decrements a numeric property for a user.
|
||||
|
||||
### Clear
|
||||
|
||||
Clears the current user identifier and ends the session.
|
||||
|
||||
16
apps/public/content/docs/meta.json
Normal file
16
apps/public/content/docs/meta.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"pages": [
|
||||
"---Introduction---",
|
||||
"index",
|
||||
"---Get started---",
|
||||
"...get-started",
|
||||
"---Tracking---",
|
||||
"...(tracking)",
|
||||
"---API---",
|
||||
"...api",
|
||||
"---Self-hosting---",
|
||||
"...self-hosting",
|
||||
"---Migration---",
|
||||
"...migration"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "SDKs",
|
||||
"pages": ["script", "web", "javascript", "nextjs", "react", "vue", "astro", "remix", "express", "python", "react-native", "swift", "kotlin"],
|
||||
"defaultOpen": true
|
||||
}
|
||||
@@ -76,7 +76,7 @@ The path should be `/api` and the domain should be your domain.
|
||||
|
||||
```html title="index.html"
|
||||
<script>
|
||||
window.op = window.op||function(...args){(window.op.q=window.op.q||[]).push(args);};
|
||||
window.op=window.op||function(){var n=[],o=new Proxy((function(){arguments.length>0&&n.push(Array.prototype.slice.call(arguments))}),{get:function(o,t){return"q"===t?n:function(){n.push([t].concat(Array.prototype.slice.call(arguments)))}}});return o}();
|
||||
window.op('init', {
|
||||
apiUrl: 'https://your-domain.com/api', // [!code highlight]
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@ const ConnectWeb = ({ client }: Props) => {
|
||||
<Syntax
|
||||
className="border"
|
||||
code={`<script>
|
||||
window.op = window.op||function(...args){(window.op.q=window.op.q||[]).push(args);};
|
||||
window.op=window.op||function(){var n=[],o=new Proxy((function(){arguments.length>0&&n.push(Array.prototype.slice.call(arguments))}),{get:function(o,t){return"q"===t?n:function(){n.push([t].concat(Array.prototype.slice.call(arguments)))}}});return o}();
|
||||
window.op('init', {
|
||||
clientId: '${client?.id ?? 'YOUR_CLIENT_ID'}',
|
||||
trackScreenViews: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openpanel/astro",
|
||||
"version": "1.0.2-local",
|
||||
"version": "1.0.3-local",
|
||||
"config": {
|
||||
"transformPackageJson": false,
|
||||
"transformEnvs": true
|
||||
@@ -14,7 +14,7 @@
|
||||
"files": ["src", "index.ts"],
|
||||
"keywords": ["astro-component"],
|
||||
"dependencies": {
|
||||
"@openpanel/web": "workspace:1.0.2-local"
|
||||
"@openpanel/web": "workspace:1.0.3-local"
|
||||
},
|
||||
"devDependencies": {
|
||||
"astro": "^5.7.7"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import type { OpenPanelMethodNames, OpenPanelOptions } from '@openpanel/web';
|
||||
import { getInitSnippet } from '@openpanel/web';
|
||||
|
||||
type Props = Omit<OpenPanelOptions, 'filter'> & {
|
||||
profileId?: string;
|
||||
@@ -32,7 +33,7 @@ const methods: { name: OpenPanelMethodNames; value: unknown }[] = [
|
||||
value: {
|
||||
...options,
|
||||
sdk: 'astro',
|
||||
sdkVersion: '1.0.2',
|
||||
sdkVersion: '1.0.3',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -51,7 +52,7 @@ if (globalProperties) {
|
||||
});
|
||||
}
|
||||
|
||||
const scriptContent = `window.op = window.op || function(...args) {(window.op.q = window.op.q || []).push(args)};
|
||||
const scriptContent = `${getInitSnippet()}
|
||||
${methods
|
||||
.map((method) => {
|
||||
return `window.op('${method.name}', ${stringify(method.value)});`;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@openpanel/express",
|
||||
"version": "1.0.1-local",
|
||||
"version": "1.0.2-local",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openpanel/sdk": "workspace:1.0.0-local",
|
||||
"@openpanel/sdk": "workspace:1.0.1-local",
|
||||
"@openpanel/common": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@openpanel/tsconfig/base.json",
|
||||
"extends": "@openpanel/tsconfig/sdk.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"outDir": "dist"
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
OpenPanelOptions,
|
||||
TrackProperties,
|
||||
} from '@openpanel/web';
|
||||
import { getInitSnippet } from '@openpanel/web';
|
||||
|
||||
export * from '@openpanel/web';
|
||||
|
||||
@@ -73,7 +74,7 @@ export function OpenPanelComponent({
|
||||
<Script
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.op = window.op || function(...args) {(window.op.q = window.op.q || []).push(args)};
|
||||
__html: `${getInitSnippet()}
|
||||
${methods
|
||||
.map((method) => {
|
||||
return `window.op('${method.name}', ${stringify(method.value)});`;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@openpanel/nextjs",
|
||||
"version": "1.0.15-local",
|
||||
"version": "1.0.16-local",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openpanel/web": "workspace:1.0.2-local"
|
||||
"@openpanel/web": "workspace:1.0.3-local"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@openpanel/tsconfig/base.json",
|
||||
"extends": "@openpanel/tsconfig/sdk.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"outDir": "dist"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@openpanel/react-native",
|
||||
"version": "1.0.1-local",
|
||||
"version": "1.0.2-local",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openpanel/sdk": "workspace:1.0.0-local"
|
||||
"@openpanel/sdk": "workspace:1.0.1-local"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@openpanel/tsconfig/base.json",
|
||||
"extends": "@openpanel/tsconfig/sdk.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"outDir": "dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openpanel/sdk",
|
||||
"version": "1.0.0-local",
|
||||
"version": "1.0.1-local",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@openpanel/tsconfig/base.json",
|
||||
"extends": "@openpanel/tsconfig/sdk.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"outDir": "dist"
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './src/index';
|
||||
export * from './src/types.d';
|
||||
export { getInitSnippet } from './src/init-snippet';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@openpanel/web",
|
||||
"version": "1.0.2-local",
|
||||
"version": "1.0.3-local",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsup",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@openpanel/sdk": "workspace:1.0.0-local"
|
||||
"@openpanel/sdk": "workspace:1.0.1-local"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
|
||||
23
packages/sdks/web/src/init-snippet.ts
Normal file
23
packages/sdks/web/src/init-snippet.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Source:
|
||||
// window.op = window.op || (function() {
|
||||
// var q = [];
|
||||
// var op = new Proxy(function() {
|
||||
// if (arguments.length > 0) {
|
||||
// q.push(Array.prototype.slice.call(arguments));
|
||||
// }
|
||||
// }, {
|
||||
// get: function(_, prop) {
|
||||
// if (prop === 'q') {
|
||||
// return q;
|
||||
// }
|
||||
// return function() {
|
||||
// q.push([prop].concat(Array.prototype.slice.call(arguments)));
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
// return op;
|
||||
// })();
|
||||
|
||||
export function getInitSnippet(): string {
|
||||
return `window.op=window.op||function(){var n=[],o=new Proxy((function(){arguments.length>0&&n.push(Array.prototype.slice.call(arguments))}),{get:function(o,t){return"q"===t?n:function(){n.push([t].concat(Array.prototype.slice.call(arguments)))}}});return o}();`;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OpenPanel } from './index';
|
||||
|
||||
((window) => {
|
||||
if (window.op && 'q' in window.op) {
|
||||
if (window.op) {
|
||||
const queue = window.op.q || [];
|
||||
// @ts-expect-error
|
||||
const op = new OpenPanel(queue.shift()[1]);
|
||||
@@ -12,16 +12,36 @@ import { OpenPanel } from './index';
|
||||
}
|
||||
});
|
||||
|
||||
window.op = (t, ...args) => {
|
||||
const fn = op[t] ? op[t].bind(op) : undefined;
|
||||
if (typeof fn === 'function') {
|
||||
// @ts-expect-error
|
||||
fn(...args);
|
||||
} else {
|
||||
console.warn(`OpenPanel: ${t} is not a function`);
|
||||
}
|
||||
};
|
||||
// Create a Proxy that supports both window.op('track', ...) and window.op.track(...)
|
||||
const opCallable = new Proxy(
|
||||
((method: string, ...args: any[]) => {
|
||||
const fn = (op as any)[method]
|
||||
? (op as any)[method].bind(op)
|
||||
: undefined;
|
||||
if (typeof fn === 'function') {
|
||||
fn(...args);
|
||||
} else {
|
||||
console.warn(`OpenPanel: ${method} is not a function`);
|
||||
}
|
||||
}) as typeof op & ((method: string, ...args: any[]) => void),
|
||||
{
|
||||
get(target, prop) {
|
||||
// Handle special properties
|
||||
if (prop === 'q') {
|
||||
return undefined; // q doesn't exist after SDK loads
|
||||
}
|
||||
// If accessing a method on op, return the bound method
|
||||
const value = (op as any)[prop];
|
||||
if (typeof value === 'function') {
|
||||
return value.bind(op);
|
||||
}
|
||||
// Otherwise return the property from op (for things like options, etc.)
|
||||
return value;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
window.op = opCallable;
|
||||
window.openpanel = op;
|
||||
}
|
||||
})(window);
|
||||
|
||||
28
packages/sdks/web/src/types.d.ts
vendored
28
packages/sdks/web/src/types.d.ts
vendored
@@ -12,7 +12,9 @@ type ExposedMethodsNames =
|
||||
| 'revenue'
|
||||
| 'flushRevenue'
|
||||
| 'clearRevenue'
|
||||
| 'pendingRevenue';
|
||||
| 'pendingRevenue'
|
||||
| 'screenView'
|
||||
| 'fetchDeviceId';
|
||||
|
||||
export type ExposedMethods = {
|
||||
[K in ExposedMethodsNames]: OpenPanel[K] extends (...args: any[]) => any
|
||||
@@ -20,7 +22,7 @@ export type ExposedMethods = {
|
||||
: never;
|
||||
}[ExposedMethodsNames];
|
||||
|
||||
export type OpenPanelMethodNames = ExposedMethodsNames | 'init' | 'screenView';
|
||||
export type OpenPanelMethodNames = ExposedMethodsNames | 'init';
|
||||
export type OpenPanelMethods =
|
||||
| ExposedMethods
|
||||
| ['init', OpenPanelOptions]
|
||||
@@ -30,12 +32,26 @@ export type OpenPanelMethods =
|
||||
TrackProperties | undefined,
|
||||
];
|
||||
|
||||
// Extract method signatures from OpenPanel for direct method calls
|
||||
type OpenPanelMethodSignatures = {
|
||||
[K in ExposedMethodsNames]: OpenPanel[K];
|
||||
} & {
|
||||
screenView(
|
||||
pathOrProperties?: string | TrackProperties,
|
||||
properties?: TrackProperties,
|
||||
): void;
|
||||
};
|
||||
|
||||
// Create a type that supports both callable and direct method access
|
||||
type OpenPanelAPI = OpenPanelMethodSignatures & {
|
||||
q?: OpenPanelMethods[];
|
||||
// Callable function API: window.op('track', 'event', {...})
|
||||
(...args: OpenPanelMethods): void;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
openpanel?: OpenPanel;
|
||||
op: {
|
||||
q?: OpenPanelMethods[];
|
||||
(...args: OpenPanelMethods): void;
|
||||
};
|
||||
op: OpenPanelAPI;
|
||||
}
|
||||
}
|
||||
|
||||
89
packages/sdks/web/src/types.debug.ts
Normal file
89
packages/sdks/web/src/types.debug.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
// Test callable function API
|
||||
function testCallableAPI() {
|
||||
// ✅ Should work - correct callable syntax
|
||||
window.op('track', 'button_clicked', { location: 'header' });
|
||||
window.op('identify', { profileId: 'user123', email: 'test@example.com' });
|
||||
window.op('init', { clientId: 'test-client-id' });
|
||||
window.op('screenView', '/page', { title: 'Test Page' });
|
||||
window.op('setGlobalProperties', { version: '1.0.0' });
|
||||
|
||||
// ❌ Should error - wrong method name
|
||||
// @ts-expect-error - 'invalidMethod' is not a valid method
|
||||
window.op('invalidMethod', 'test');
|
||||
|
||||
// ❌ Should error - wrong arguments for track
|
||||
// @ts-expect-error - track expects (name: string, properties?: TrackProperties)
|
||||
window.op('track', 123);
|
||||
}
|
||||
|
||||
// Test direct method API
|
||||
function testDirectMethodAPI() {
|
||||
// ✅ Should work - correct direct method syntax
|
||||
window.op.track('button_clicked', { location: 'header' });
|
||||
window.op.identify({ profileId: 'user123', email: 'test@example.com' });
|
||||
window.op.screenView('/page', { title: 'Test Page' });
|
||||
window.op.screenView({ title: 'Test Page' }); // Overload with just properties
|
||||
window.op.setGlobalProperties({ version: '1.0.0' });
|
||||
window.op.revenue(1000, { currency: 'USD' });
|
||||
window.op.pendingRevenue(500, { productId: '123' });
|
||||
window.op.flushRevenue();
|
||||
window.op.clearRevenue();
|
||||
window.op.fetchDeviceId();
|
||||
|
||||
// ❌ Should error - wrong arguments for track
|
||||
// @ts-expect-error - track expects (name: string, properties?: TrackProperties)
|
||||
window.op.track(123);
|
||||
|
||||
// ❌ Should error - wrong arguments for identify
|
||||
// @ts-expect-error - identify expects IdentifyPayload
|
||||
window.op.identify('user123');
|
||||
}
|
||||
|
||||
// Test queue property
|
||||
function testQueueProperty() {
|
||||
// ✅ Should work - q is optional and can be accessed
|
||||
const queue = window.op.q;
|
||||
if (queue) {
|
||||
queue.forEach((item) => {
|
||||
// Queue items should be properly typed
|
||||
if (item[0] === 'track') {
|
||||
const eventName = item[1]; // Should be string
|
||||
const properties = item[2]; // Should be TrackProperties | undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Test that both APIs work together
|
||||
function testBothAPIs() {
|
||||
// Mix and match - both should work
|
||||
window.op('track', 'event1', { prop: 'value' });
|
||||
window.op.track('event2', { prop: 'value' });
|
||||
window.op('identify', { profileId: '123' });
|
||||
window.op.identify({ profileId: '456' });
|
||||
}
|
||||
|
||||
// Test autocomplete and type inference
|
||||
function testTypeInference() {
|
||||
// TypeScript should infer the correct types
|
||||
const trackCall = window.op.track;
|
||||
// trackCall should be: (name: string, properties?: TrackProperties) => Promise<void>
|
||||
|
||||
const identifyCall = window.op.identify;
|
||||
// identifyCall should be: (payload: IdentifyPayload) => Promise<void>
|
||||
|
||||
// Callable function should accept OpenPanelMethods
|
||||
const callable = window.op;
|
||||
// callable should be callable with OpenPanelMethods
|
||||
}
|
||||
|
||||
function testExpectedErrors() {
|
||||
// @ts-expect-error - 'invalidMethod' is not a valid method
|
||||
window.op('invalidMethod', 'test');
|
||||
// @ts-expect-error - track expects (name: string, properties?: TrackProperties)
|
||||
window.op.track(123);
|
||||
// @ts-expect-error - identify expects IdentifyPayload
|
||||
window.op.identify('user123');
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@openpanel/tsconfig/base.json",
|
||||
"extends": "@openpanel/tsconfig/sdk.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"outDir": "dist"
|
||||
|
||||
@@ -144,7 +144,7 @@ const updatePackageJsonForRelease = (
|
||||
newPkgJson = {
|
||||
...newPkgJson,
|
||||
main: './dist/index.js',
|
||||
module: './dist/index.mjs',
|
||||
module: './dist/index.js',
|
||||
types: './dist/index.d.ts',
|
||||
files: ['dist'],
|
||||
exports: restPkgJson.exports ?? {
|
||||
|
||||
23
tooling/typescript/sdk.json
Normal file
23
tooling/typescript/sdk.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"allowUnreachableCode": true
|
||||
},
|
||||
"exclude": ["node_modules", "build", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user