Skip to content

Commit

Permalink
feat: Allow bootstrapping session id (#1251)
Browse files Browse the repository at this point in the history
* Allow bootstrapping session id

* Add test for bootstrapping session id
  • Loading branch information
robbie-c authored Jun 19, 2024
1 parent 73c8d7b commit d1ad80b
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 5 deletions.
21 changes: 19 additions & 2 deletions src/__tests__/sessionid.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { SessionIdManager } from '../sessionid'
import { SESSION_ID } from '../constants'
import { sessionStore } from '../storage'
import { uuidv7 } from '../uuidv7'
import { PostHogConfig, Properties } from '../types'
import { uuidv7, uuid7ToTimestampMs } from '../uuidv7'
import { BootstrapConfig, PostHogConfig, Properties } from '../types'
import { PostHogPersistence } from '../posthog-persistence'
import { assignableWindow } from '../utils/globals'

Expand Down Expand Up @@ -38,6 +38,7 @@ describe('Session ID manager', () => {
;(sessionStore.is_supported as jest.Mock).mockReturnValue(true)
jest.spyOn(global, 'Date').mockImplementation(() => new originalDate(now))
;(uuidv7 as jest.Mock).mockReturnValue('newUUID')
;(uuid7ToTimestampMs as jest.Mock).mockReturnValue(timestamp)
})

describe('new session id manager', () => {
Expand All @@ -64,6 +65,22 @@ describe('Session ID manager', () => {
})
expect(sessionStore.set).toHaveBeenCalledWith('ph_persistance-name_window_id', 'newUUID')
})

it('should allow bootstrapping of the session id', () => {
// arrange
const bootstrapSessionId = 'bootstrap-session-id'
const bootstrap: BootstrapConfig = {
sessionID: bootstrapSessionId,
}
const sessionIdManager = new SessionIdManager({ ...config, bootstrap }, persistence as PostHogPersistence)

// act
const { sessionId, sessionStartTimestamp } = sessionIdManager.checkAndGetSessionAndWindowId(false, now)

// assert
expect(sessionId).toEqual(bootstrapSessionId)
expect(sessionStartTimestamp).toEqual(timestamp)
})
})

describe('stored session data', () => {
Expand Down
20 changes: 18 additions & 2 deletions src/__tests__/test-uuid.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import { uuidv7 } from '../uuidv7'

import { uuid7ToTimestampMs, uuidv7 } from '../uuidv7'
const TEN_SECONDS = 10_000
describe('uuid', () => {
it('should be a uuid when requested', () => {
expect(uuidv7()).toHaveLength(36)
expect(uuidv7()).not.toEqual(uuidv7())
})
describe('uuid7ToTimestampMs', () => {
it('should convert a UUIDv7 generated with uuidv7() to a timestamp', () => {
const uuid = uuidv7()
const timestamp = uuid7ToTimestampMs(uuid)
const now = Date.now()
expect(typeof timestamp).toBe('number')
expect(timestamp).toBeLessThan(now + TEN_SECONDS)
expect(timestamp).toBeGreaterThan(now - TEN_SECONDS)
})
it('should convert a known UUIDv7 to a known timestamp', () => {
const uuid = '01902c33-4925-7f20-818a-4095f9251383'
const timestamp = uuid7ToTimestampMs(uuid)
const expected = new Date('Tue, 18 Jun 2024 16:34:36.965 GMT').getTime()
expect(timestamp).toBe(expected)
})
})
})
11 changes: 10 additions & 1 deletion src/sessionid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PostHogPersistence } from './posthog-persistence'
import { SESSION_ID } from './constants'
import { sessionStore } from './storage'
import { PostHogConfig, SessionIdChangedCallback } from './types'
import { uuidv7 } from './uuidv7'
import { uuid7ToTimestampMs, uuidv7 } from './uuidv7'
import { window } from './utils/globals'

import { isArray, isNumber, isUndefined } from './utils/type-utils'
Expand Down Expand Up @@ -76,6 +76,15 @@ export class SessionIdManager {
sessionStore.set(this._primary_window_exists_storage_key, true)
}

if (this.config.bootstrap?.sessionID) {
try {
const sessionStartTimestamp = uuid7ToTimestampMs(this.config.bootstrap.sessionID)
this._setSessionId(this.config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp)
} catch (e) {
logger.error('Invalid sessionID in bootstrap', e)
}
}

this._listenToReloadWindow()
}

Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export interface BootstrapConfig {
isIdentifiedID?: boolean
featureFlags?: Record<string, boolean | string>
featureFlagPayloads?: Record<string, JsonType>
/**
* Optionally provide a sessionID, this is so that you can provide an existing sessionID here to continue a user's session across a domain or device. It MUST be:
* - unique to this user
* - a valid UUID v7
* - the timestamp part must be <= the timestamp of the first event in the session
* - the timestamp of the last event in the session must be < the timestamp part + 24 hours
* **/
sessionID?: string
}

export interface PostHogConfig {
Expand Down
14 changes: 14 additions & 0 deletions src/uuidv7.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,17 @@ export const uuidv7 = (): string => uuidv7obj().toString()

/** Generates a UUIDv7 object. */
const uuidv7obj = (): UUID => (defaultGenerator || (defaultGenerator = new V7Generator())).generate()

export const uuid7ToTimestampMs = (uuid: string): number => {
// remove hyphens
const hex = uuid.replace(/-/g, '')
// ensure that it's a version 7 UUID
if (hex.length !== 32) {
throw new Error('Not a valid UUID')
}
if (hex[12] !== '7') {
throw new Error('Not a UUIDv7')
}
// the first 6 bytes are the timestamp, which means that we can read only the first 12 hex characters
return parseInt(hex.substring(0, 12), 16)
}

0 comments on commit d1ad80b

Please sign in to comment.