diff --git a/playground/nextjs/package.json b/playground/nextjs/package.json
index 9f2d195b3..f40a3ad69 100644
--- a/playground/nextjs/package.json
+++ b/playground/nextjs/package.json
@@ -22,7 +22,7 @@
"eslint-config-next": "13.1.6",
"hls.js": "^1.5.15",
"next": "13.5.6",
- "posthog-js": "1.166.0",
+ "posthog-js": "1.194.6",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
diff --git a/playground/nextjs/pages/_app.tsx b/playground/nextjs/pages/_app.tsx
index 3d74c2fe6..8c16f2c3b 100644
--- a/playground/nextjs/pages/_app.tsx
+++ b/playground/nextjs/pages/_app.tsx
@@ -42,7 +42,7 @@ export default function App({ Component, pageProps }: AppProps) {
{/* CSP - useful for testing our documented recommendations. NOTE: Unsafe is only needed for nextjs pre-loading */}
= 0.6'}
dev: false
+ /core-js@3.39.0:
+ resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==}
+ requiresBuild: true
+ dev: false
+
/cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@@ -2312,9 +2317,10 @@ packages:
source-map-js: 1.2.1
dev: true
- /posthog-js@1.166.0:
- resolution: {integrity: sha512-om7rhbgP3OzJDJ3wdrp6cuKVWC+7iiFeVv+g8uaZPLI6zvzuaNdq4a3s0ltnfBpROlZrWw4oJknoVwj7I3hVTQ==}
+ /posthog-js@1.194.6:
+ resolution: {integrity: sha512-5g5n7FjWLha/QWVTeWeMErGff21v4/V3wYCZ2z8vAbHaCyHkaDBEbuM756jMFBQMsq3HJcDX9mlxi2HhAHxq2A==}
dependencies:
+ core-js: 3.39.0
fflate: 0.4.8
preact: 10.24.1
web-vitals: 4.2.3
diff --git a/src/__tests__/cookieless.test.ts b/src/__tests__/cookieless.test.ts
new file mode 100644
index 000000000..da1f55ba2
--- /dev/null
+++ b/src/__tests__/cookieless.test.ts
@@ -0,0 +1,88 @@
+import { defaultPostHog } from './helpers/posthog-instance'
+import type { PostHogConfig } from '../types'
+import { uuidv7 } from '../uuidv7'
+
+describe('cookieless', () => {
+ const eventName = 'custom_event'
+ const eventProperties = {
+ event: 'prop',
+ }
+ const identifiedDistinctId = 'user-1'
+ const setup = (config: Partial = {}, token: string = uuidv7()) => {
+ const beforeSendMock = jest.fn().mockImplementation((e) => e)
+ const posthog = defaultPostHog().init(token, { ...config, before_send: beforeSendMock }, token)!
+ posthog.debug()
+ return { posthog, beforeSendMock }
+ }
+
+ it('should send events with the sentinel distinct id', () => {
+ const { posthog, beforeSendMock } = setup({
+ persistence: 'memory',
+ __preview_experimental_cookieless_mode: true,
+ })
+
+ posthog.capture(eventName, eventProperties)
+ expect(beforeSendMock).toBeCalledTimes(1)
+ let event = beforeSendMock.mock.calls[0][0]
+ expect(event.properties.distinct_id).toBe('$posthog_cklsh')
+ expect(event.properties.$anon_distinct_id).toBe(undefined)
+ expect(event.properties.$device_id).toBe(null)
+ expect(event.properties.$session_id).toBe(undefined)
+ expect(event.properties.$window_id).toBe(undefined)
+ expect(event.properties.$cklsh_mode).toEqual(true)
+ expect(document.cookie).toBe('')
+
+ // simulate user giving cookie consent
+ posthog.set_config({ persistence: 'localStorage+cookie' })
+
+ // send an event after consent
+ posthog.capture(eventName, eventProperties)
+ expect(beforeSendMock).toBeCalledTimes(2)
+ event = beforeSendMock.mock.calls[1][0]
+ expect(event.properties.distinct_id).toBe('$posthog_cklsh')
+ expect(event.properties.$anon_distinct_id).toBe(undefined)
+ expect(event.properties.$device_id).toBe(null)
+ expect(event.properties.$session_id).toBe(undefined)
+ expect(event.properties.$window_id).toBe(undefined)
+ expect(event.properties.$cklsh_mode).toEqual(true)
+ expect(document.cookie).not.toBe('')
+
+ // a user identifying
+ posthog.identify(identifiedDistinctId)
+ expect(beforeSendMock).toBeCalledTimes(3)
+ event = beforeSendMock.mock.calls[2][0]
+ expect(event.properties.distinct_id).toBe(identifiedDistinctId)
+ expect(event.properties.$anon_distinct_id).toBe('$posthog_cklsh')
+ expect(event.properties.$device_id).toBe(null)
+ expect(event.properties.$session_id).toBe(undefined)
+ expect(event.properties.$window_id).toBe(undefined)
+ expect(event.properties.$cklsh_mode).toEqual(true)
+
+ // an event after identifying
+ posthog.capture(eventName, eventProperties)
+ expect(beforeSendMock).toBeCalledTimes(4)
+ event = beforeSendMock.mock.calls[3][0]
+ expect(event.properties.distinct_id).toBe(identifiedDistinctId)
+ expect(event.properties.$anon_distinct_id).toBe(undefined)
+ expect(event.properties.$device_id).toBe(null)
+ expect(event.properties.$session_id).toBe(undefined)
+ expect(event.properties.$window_id).toBe(undefined)
+ expect(event.properties.$cklsh_mode).toEqual(true)
+
+ // reset
+ posthog.reset()
+ posthog.set_config({ persistence: 'memory' })
+
+ // an event after reset
+ posthog.capture(eventName, eventProperties)
+ expect(beforeSendMock).toBeCalledTimes(5)
+ event = beforeSendMock.mock.calls[4][0]
+ expect(event.properties.distinct_id).toBe('$posthog_cklsh')
+ expect(event.properties.$anon_distinct_id).toBe(undefined)
+ expect(event.properties.$device_id).toBe(null)
+ expect(event.properties.$session_id).toBe(undefined)
+ expect(event.properties.$window_id).toBe(undefined)
+ expect(event.properties.$cklsh_mode).toEqual(true)
+ expect(document.cookie).toBe('')
+ })
+})
diff --git a/src/constants.ts b/src/constants.ts
index 89cda8f3b..34b7a8bad 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -49,6 +49,13 @@ export const ENABLE_PERSON_PROCESSING = '$epp'
export const TOOLBAR_ID = '__POSTHOG_TOOLBAR__'
export const TOOLBAR_CONTAINER_CLASS = 'toolbar-global-fade-container'
+/**
+ * PREVIEW - MAY CHANGE WITHOUT WARNING - DO NOT USE IN PRODUCTION
+ * Sentinel value for distinct id, device id, session id. Signals that the server should generate the value
+ * */
+export const COOKIELESS_SENTINEL_VALUE = '$posthog_cklsh'
+export const COOKIELESS_MODE_FLAG_PROPERTY = '$cklsh_mode'
+
export const WEB_EXPERIMENTS = '$web_experiments'
// These are properties that are reserved and will not be automatically included in events
diff --git a/src/entrypoints/tracing-headers.ts b/src/entrypoints/tracing-headers.ts
index 06abc4e01..18acbe3a0 100644
--- a/src/entrypoints/tracing-headers.ts
+++ b/src/entrypoints/tracing-headers.ts
@@ -2,13 +2,15 @@ import { SessionIdManager } from '../sessionid'
import { patch } from '../extensions/replay/rrweb-plugins/patch'
import { assignableWindow, window } from '../utils/globals'
-const addTracingHeaders = (sessionManager: SessionIdManager, req: Request) => {
- const { sessionId, windowId } = sessionManager.checkAndGetSessionAndWindowId(true)
- req.headers.set('X-POSTHOG-SESSION-ID', sessionId)
- req.headers.set('X-POSTHOG-WINDOW-ID', windowId)
+const addTracingHeaders = (sessionManager: SessionIdManager | undefined, req: Request) => {
+ if (sessionManager) {
+ const { sessionId, windowId } = sessionManager.checkAndGetSessionAndWindowId(true)
+ req.headers.set('X-POSTHOG-SESSION-ID', sessionId)
+ req.headers.set('X-POSTHOG-WINDOW-ID', windowId)
+ }
}
-const patchFetch = (sessionManager: SessionIdManager): (() => void) => {
+const patchFetch = (sessionManager?: SessionIdManager): (() => void) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return patch(window, 'fetch', (originalFetch: typeof fetch) => {
@@ -24,7 +26,7 @@ const patchFetch = (sessionManager: SessionIdManager): (() => void) => {
})
}
-const patchXHR = (sessionManager: SessionIdManager): (() => void) => {
+const patchXHR = (sessionManager?: SessionIdManager): (() => void) => {
return patch(
// we can assert this is present because we've checked previously
window!.XMLHttpRequest.prototype,
diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts
index 030501aab..822fdb413 100644
--- a/src/extensions/replay/sessionrecording.ts
+++ b/src/extensions/replay/sessionrecording.ts
@@ -449,6 +449,9 @@ export class SessionRecording {
logger.error('started without valid sessionManager')
throw new Error(LOGGER_PREFIX + ' started without valid sessionManager. This is a bug.')
}
+ if (this.instance.config.__preview_experimental_cookieless_mode) {
+ throw new Error(LOGGER_PREFIX + ' cannot be used with __preview_experimental_cookieless_mode.')
+ }
// we know there's a sessionManager, so don't need to start without a session id
const { sessionId, windowId } = this.sessionManager.checkAndGetSessionAndWindowId()
diff --git a/src/extensions/tracing-headers.ts b/src/extensions/tracing-headers.ts
index b8867af5c..d3706dd65 100644
--- a/src/extensions/tracing-headers.ts
+++ b/src/extensions/tracing-headers.ts
@@ -37,12 +37,11 @@ export class TracingHeaders {
}
private _startCapturing = () => {
- // NB: we can assert sessionManager is present only because we've checked previously
if (isUndefined(this._restoreXHRPatch)) {
- assignableWindow.__PosthogExtensions__?.tracingHeadersPatchFns?._patchXHR(this.instance.sessionManager!)
+ assignableWindow.__PosthogExtensions__?.tracingHeadersPatchFns?._patchXHR(this.instance.sessionManager)
}
if (isUndefined(this._restoreFetchPatch)) {
- assignableWindow.__PosthogExtensions__?.tracingHeadersPatchFns?._patchFetch(this.instance.sessionManager!)
+ assignableWindow.__PosthogExtensions__?.tracingHeadersPatchFns?._patchFetch(this.instance.sessionManager)
}
}
}
diff --git a/src/posthog-core.ts b/src/posthog-core.ts
index c2bced9f5..ddede2ea3 100644
--- a/src/posthog-core.ts
+++ b/src/posthog-core.ts
@@ -19,6 +19,8 @@ import {
PEOPLE_DISTINCT_ID_KEY,
USER_STATE,
ENABLE_PERSON_PROCESSING,
+ COOKIELESS_SENTINEL_VALUE,
+ COOKIELESS_MODE_FLAG_PROPERTY,
} from './constants'
import { SessionRecording } from './extensions/replay/sessionrecording'
import { Decide } from './decide'
@@ -429,16 +431,20 @@ export class PostHog {
this._retryQueue = new RetryQueue(this)
this.__request_queue = []
- this.sessionManager = new SessionIdManager(this)
- this.sessionPropsManager = new SessionPropsManager(this.sessionManager, this.persistence)
+ if (!this.config.__preview_experimental_cookieless_mode) {
+ this.sessionManager = new SessionIdManager(this)
+ this.sessionPropsManager = new SessionPropsManager(this.sessionManager, this.persistence)
+ }
new TracingHeaders(this).startIfEnabledOrStop()
this.siteApps = new SiteApps(this)
this.siteApps?.init()
- this.sessionRecording = new SessionRecording(this)
- this.sessionRecording.startIfEnabledOrStop()
+ if (!this.config.__preview_experimental_cookieless_mode) {
+ this.sessionRecording = new SessionRecording(this)
+ this.sessionRecording.startIfEnabledOrStop()
+ }
if (!this.config.disable_scroll_properties) {
this.scrollManager.startMeasuringScrollPosition()
@@ -507,10 +513,18 @@ export class PostHog {
this.featureFlags.receivedFeatureFlags({ featureFlags: activeFlags, featureFlagPayloads })
}
- if (!this.get_distinct_id()) {
+ if (this.config.__preview_experimental_cookieless_mode) {
+ this.register_once(
+ {
+ distinct_id: COOKIELESS_SENTINEL_VALUE,
+ $device_id: null,
+ },
+ ''
+ )
+ } else if (!this.get_distinct_id()) {
// There is no need to set the distinct id
// or the device id if something was already stored
- // in the persitence
+ // in the persistence
const uuid = this.config.get_device_id(uuidv7())
this.register_once(
@@ -915,6 +929,11 @@ export class PostHog {
let properties = { ...event_properties }
properties['token'] = this.config.token
+ if (this.config.__preview_experimental_cookieless_mode) {
+ // Set a flag to tell the plugin server to use cookieless server hash mode
+ properties[COOKIELESS_MODE_FLAG_PROPERTY] = true
+ }
+
if (event_name === '$snapshot') {
const persistenceProps = { ...this.persistence.properties(), ...this.sessionPersistence.properties() }
properties['distinct_id'] = persistenceProps.distinct_id
@@ -1533,14 +1552,24 @@ export class PostHog {
this.surveys?.reset()
this.persistence?.set_property(USER_STATE, 'anonymous')
this.sessionManager?.resetSessionId()
- const uuid = this.config.get_device_id(uuidv7())
- this.register_once(
- {
- distinct_id: uuid,
- $device_id: reset_device_id ? uuid : device_id,
- },
- ''
- )
+ if (this.config.__preview_experimental_cookieless_mode) {
+ this.register_once(
+ {
+ distinct_id: COOKIELESS_SENTINEL_VALUE,
+ $device_id: null,
+ },
+ ''
+ )
+ } else {
+ const uuid = this.config.get_device_id(uuidv7())
+ this.register_once(
+ {
+ distinct_id: uuid,
+ $device_id: reset_device_id ? uuid : device_id,
+ },
+ ''
+ )
+ }
}
/**
diff --git a/src/sessionid.ts b/src/sessionid.ts
index b9b7c63c0..bab3e88d5 100644
--- a/src/sessionid.ts
+++ b/src/sessionid.ts
@@ -37,6 +37,9 @@ export class SessionIdManager {
if (!instance.persistence) {
throw new Error('SessionIdManager requires a PostHogPersistence instance')
}
+ if (instance.config.__preview_experimental_cookieless_mode) {
+ throw new Error('SessionIdManager cannot be used with __preview_experimental_cookieless_mode')
+ }
this.config = instance.config
this.persistence = instance.persistence
@@ -215,6 +218,11 @@ export class SessionIdManager {
* @param {Number} timestamp (optional) Defaults to the current time. The timestamp to be stored with the sessionId (used when determining if a new sessionId should be generated)
*/
checkAndGetSessionAndWindowId(readOnly = false, _timestamp: number | null = null) {
+ if (this.config.__preview_experimental_cookieless_mode) {
+ throw new Error(
+ 'checkAndGetSessionAndWindowId should not be called in __preview_experimental_cookieless_mode'
+ )
+ }
const timestamp = _timestamp || new Date().getTime()
// eslint-disable-next-line prefer-const
diff --git a/src/types.ts b/src/types.ts
index 303b501b1..02d2d709b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -344,6 +344,12 @@ export interface PostHogConfig {
* enables the new RemoteConfig approach to loading config instead of decide
* */
__preview_remote_config?: boolean
+
+ /**
+ * PREVIEW - MAY CHANGE WITHOUT WARNING - DO NOT USE IN PRODUCTION
+ * whether to send a sentinel value for distinct id, device id, and session id, which will be replaced server-side by a cookieless hash
+ * */
+ __preview_experimental_cookieless_mode?: boolean
}
export interface OptInOutCapturingOptions {
diff --git a/src/utils/globals.ts b/src/utils/globals.ts
index a2e2479fa..487d78193 100644
--- a/src/utils/globals.ts
+++ b/src/utils/globals.ts
@@ -79,8 +79,8 @@ interface PostHogExtensions {
onINP: (metric: any) => void
}
tracingHeadersPatchFns?: {
- _patchFetch: (sessionManager: SessionIdManager) => () => void
- _patchXHR: (sessionManager: any) => () => void
+ _patchFetch: (sessionManager?: SessionIdManager) => () => void
+ _patchXHR: (sessionManager?: SessionIdManager) => () => void
}
initDeadClicksAutocapture?: (
ph: PostHog,