|
| 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 |
0 commit comments