--- 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