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