Skip to content

Commit 83e223a

Browse files
authored
feat: sdks and docs (#239)
* init * fix * update docs * bump: all sdks * rename types test
1 parent 790801b commit 83e223a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+802
-146
lines changed

apps/public/content/articles/cookieless-analytics.mdx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,7 @@ We built OpenPanel from the ground up with privacy at its heart—and with featu
8888

8989
```html
9090
<script>
91-
window.op = window.op || function(...args) {
92-
(window.op.q = window.op.q || []).push(args);
93-
};
91+
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}();
9492
window.op('init', {
9593
clientId: 'YOUR_CLIENT_ID',
9694
trackScreenViews: true,
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
title: Avoid adblockers with proxy
3+
description: Learn why adblockers block analytics and how to avoid it by proxying events.
4+
---
5+
6+
In this article we need to talk about adblockers, why they exist, how they work, and how to avoid them.
7+
8+
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.
9+
10+
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.
11+
12+
## Built-in Support
13+
14+
Today, our Next.js SDK and WordPress plugin have built-in support for proxying:
15+
- **WordPress**: Does it automatically.
16+
- **Next.js**: Easy to setup with a route handler.
17+
18+
## Implementing Proxying for Any Framework
19+
20+
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.
21+
22+
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.
23+
24+
> 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).
25+
26+
### Hono Example
27+
28+
```typescript
29+
import { Hono } from 'hono'
30+
31+
const app = new Hono()
32+
33+
// 1. Proxy the script file
34+
app.get('/op1.js', async (c) => {
35+
const scriptUrl = 'https://openpanel.dev/op1.js'
36+
try {
37+
const res = await fetch(scriptUrl)
38+
const text = await res.text()
39+
40+
c.header('Content-Type', 'text/javascript')
41+
// Optional caching for 24 hours
42+
c.header('Cache-Control', 'public, max-age=86400, stale-while-revalidate=86400')
43+
return c.body(text)
44+
} catch (e) {
45+
return c.json({ error: 'Failed to fetch script' }, 500)
46+
}
47+
})
48+
49+
// 2. Proxy the track event
50+
app.post('/track', async (c) => {
51+
const body = await c.req.json()
52+
53+
// Forward the client's IP address (be sure to pick correct IP based on your infra)
54+
const ip = c.req.header('cf-connecting-ip') ??
55+
c.req.header('x-forwarded-for')?.split(',')[0]
56+
57+
const headers = new Headers()
58+
headers.set('Content-Type', 'application/json')
59+
headers.set('Origin', c.req.header('origin') ?? '')
60+
headers.set('User-Agent', c.req.header('user-agent') ?? '')
61+
headers.set('openpanel-client-id', c.req.header('openpanel-client-id') ?? '')
62+
63+
if (ip) {
64+
headers.set('openpanel-client-ip', ip)
65+
}
66+
67+
try {
68+
const res = await fetch('https://api.openpanel.dev/track', {
69+
method: 'POST',
70+
headers,
71+
body: JSON.stringify(body),
72+
})
73+
return c.json(await res.text(), res.status)
74+
} catch (e) {
75+
return c.json(e, 500)
76+
}
77+
})
78+
79+
export default app
80+
```
81+
82+
This script sets up two endpoints:
83+
1. `GET /op1.js`: Fetches the OpenPanel script and serves it from your domain.
84+
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.
85+
86+
## Frontend Configuration
87+
88+
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.
89+
90+
```html
91+
<script>
92+
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}();
93+
window.op('init', {
94+
apiUrl: 'https://api.domain.com'
95+
clientId: 'YOUR_CLIENT_ID',
96+
trackScreenViews: true,
97+
trackOutgoingLinks: true,
98+
trackAttributes: true,
99+
});
100+
</script>
101+
<script src="https://api.domain.com/op1.js" defer async></script>
102+
```
103+
104+
By doing this, all requests are sent to your domain first, bypassing adblockers that look for third-party tracking domains.
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
---
2+
title: How it works
3+
description: Understanding device IDs, session IDs, profile IDs, and event tracking
4+
---
5+
6+
## Device ID
7+
8+
A **device ID** is a unique identifier generated for each device/browser combination. It's calculated using a hash function that combines:
9+
10+
- **User Agent** (browser/client information)
11+
- **IP Address**
12+
- **Origin** (project ID)
13+
- **Salt** (a rotating secret key)
14+
15+
```typescript:packages/common/server/profileId.ts
16+
export function generateDeviceId({
17+
salt,
18+
ua,
19+
ip,
20+
origin,
21+
}: GenerateDeviceIdOptions) {
22+
return createHash(`${ua}:${ip}:${origin}:${salt}`, 16);
23+
}
24+
```
25+
26+
### Salt Rotation
27+
28+
The salt used for device ID generation rotates **daily at midnight** (UTC). This means:
29+
30+
- Device IDs remain consistent throughout a single day
31+
- Device IDs reset each day for privacy purposes
32+
- The system maintains both the current and previous day's salt to handle events that may arrive slightly after midnight
33+
34+
```typescript:apps/worker/src/jobs/cron.salt.ts
35+
// Salt rotation happens daily at midnight (pattern: '0 0 * * *')
36+
```
37+
38+
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.
39+
40+
## Session ID
41+
42+
A **session** represents a continuous period of user activity. Sessions are used to group related events together and understand user behavior patterns.
43+
44+
### Session Duration
45+
46+
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.
47+
48+
```typescript:apps/worker/src/utils/session-handler.ts
49+
export const SESSION_TIMEOUT = 1000 * 60 * 30; // 30 minutes
50+
```
51+
52+
### Session Creation Rules
53+
54+
Sessions are **only created for client events**, not server events. This means:
55+
56+
- Events sent from browsers, mobile apps, or client-side SDKs will create sessions
57+
- Events sent from backend servers, scripts, or server-side SDKs will **not** create sessions
58+
- If you only track events from your backend, no sessions will be created
59+
60+
Additionally, sessions are **not created for events older than 15 minutes**. This prevents historical data imports from creating artificial sessions.
61+
62+
```typescript:apps/worker/src/jobs/events.incoming-event.ts
63+
// Sessions are not created if:
64+
// 1. The event is from a server (uaInfo.isServer === true)
65+
// 2. The timestamp is from the past (isTimestampFromThePast === true)
66+
if (uaInfo.isServer || isTimestampFromThePast) {
67+
// Event is attached to existing session or no session
68+
}
69+
```
70+
71+
## Profile ID
72+
73+
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.
74+
75+
### Profile ID Assignment
76+
77+
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`**.
78+
79+
This means:
80+
- Anonymous users (without a profile ID) are tracked by their device ID
81+
- Once you identify a user (by providing a profile ID), all their events will be associated with that profile
82+
- The same user can be tracked across multiple devices by using the same profile ID
83+
84+
```typescript:packages/db/src/services/event.service.ts
85+
// If no profileId is provided, it defaults to deviceId
86+
if (!payload.profileId && payload.deviceId) {
87+
payload.profileId = payload.deviceId;
88+
}
89+
```
90+
91+
## Client Events vs Server Events
92+
93+
OpenPanel distinguishes between **client events** and **server events** based on the User-Agent header.
94+
95+
### Client Events
96+
97+
Client events are sent from:
98+
- Web browsers (Chrome, Firefox, Safari, etc.)
99+
- Mobile apps using client-side SDKs
100+
- Any client that sends a browser-like User-Agent
101+
102+
Client events:
103+
- Create sessions
104+
- Generate device IDs
105+
- Support full session tracking
106+
107+
### Server Events
108+
109+
Server events are detected when the User-Agent matches server patterns, such as:
110+
- `Go-http-client/1.0`
111+
- `node-fetch/1.0`
112+
- Other single-name/version patterns (e.g., `LibraryName/1.0`)
113+
114+
Server events:
115+
- Do **not** create sessions
116+
- Are attached to existing sessions if available
117+
- Are useful for backend tracking without session management
118+
119+
```typescript:packages/common/server/parser-user-agent.ts
120+
// Server events are detected by patterns like "Go-http-client/1.0"
121+
function isServer(res: UAParser.IResult) {
122+
if (SINGLE_NAME_VERSION_REGEX.test(res.ua)) {
123+
return true;
124+
}
125+
// ... additional checks
126+
}
127+
```
128+
129+
The distinction is made in the event processing pipeline:
130+
131+
```typescript:apps/worker/src/jobs/events.incoming-event.ts
132+
const uaInfo = parseUserAgent(userAgent, properties);
133+
134+
// Only client events create sessions
135+
if (uaInfo.isServer || isTimestampFromThePast) {
136+
// Server events or old events don't create new sessions
137+
}
138+
```
139+
140+
## Timestamps
141+
142+
Events can include custom timestamps to track when events actually occurred, rather than when they were received by the server.
143+
144+
### Setting Custom Timestamps
145+
146+
You can provide a custom timestamp using the `__timestamp` property in your event properties:
147+
148+
```javascript
149+
track('page_view', {
150+
__timestamp: '2024-01-15T10:30:00Z'
151+
});
152+
```
153+
154+
### Timestamp Validation
155+
156+
The system validates timestamps to prevent abuse and ensure data quality:
157+
158+
1. **Future timestamps**: If a timestamp is more than **1 minute in the future**, the server timestamp is used instead
159+
2. **Past timestamps**: If a timestamp is older than **15 minutes**, it's marked as `isTimestampFromThePast: true`
160+
161+
```typescript:apps/api/src/controllers/track.controller.ts
162+
// Timestamp validation logic
163+
const ONE_MINUTE_MS = 60 * 1000;
164+
const FIFTEEN_MINUTES_MS = 15 * ONE_MINUTE_MS;
165+
166+
// Future check: more than 1 minute ahead
167+
if (clientTimestampNumber > safeTimestamp + ONE_MINUTE_MS) {
168+
return { timestamp: safeTimestamp, isTimestampFromThePast: false };
169+
}
170+
171+
// Past check: older than 15 minutes
172+
const isTimestampFromThePast =
173+
clientTimestampNumber < safeTimestamp - FIFTEEN_MINUTES_MS;
174+
```
175+
176+
### Timestamp Impact on Sessions
177+
178+
**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.
179+
180+
```typescript:apps/worker/src/jobs/events.incoming-event.ts
181+
// Events from the past don't create sessions
182+
if (uaInfo.isServer || isTimestampFromThePast) {
183+
// Attach to existing session or track without session
184+
}
185+
```
186+
187+
This ensures that:
188+
- Real-time tracking creates proper sessions
189+
- Historical data imports don't interfere with session analytics
190+
- Backdated events are still tracked but don't affect session metrics
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"pages": ["sdks", "how-it-works", "..."]
3+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)