Skip to content

Commit

Permalink
feat(web-analytics): Add client-side session params (#869)
Browse files Browse the repository at this point in the history
  • Loading branch information
robbie-c authored Nov 2, 2023
1 parent 96544a8 commit 4213ffa
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 28 deletions.
46 changes: 23 additions & 23 deletions playground/nextjs/package.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
{
"name": "nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@lottiefiles/react-lottie-player": "^3.5.3",
"@next/font": "13.1.6",
"@types/node": "18.13.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.10",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"next": "13.1.6",
"posthog-js": "^1.45.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
}
"name": "nextjs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@lottiefiles/react-lottie-player": "^3.5.3",
"@next/font": "13.1.6",
"@types/node": "18.13.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.10",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"next": "13.1.6",
"posthog-js": "^1.87.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
}
}
1 change: 1 addition & 0 deletions playground/nextjs/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ if (typeof window !== 'undefined') {
recordCrossOriginIframes: true,
},
debug: true,
__preview_send_client_session_params: true,
})
;(window as any).posthog = posthog
}
Expand Down
8 changes: 4 additions & 4 deletions playground/nextjs/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1770,10 +1770,10 @@ [email protected]:
picocolors "^1.0.0"
source-map-js "^1.0.2"

posthog-js@^1.45.1:
version "1.73.1"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.73.1.tgz#8b994e595ab8e847bc9b55707c9cd0b22f655352"
integrity sha512-eN08SkMdOG14TbUWek/7it4nCwcclK4MklCemq4/JK+rdsVqCWJXnYG2EdUOMoLv8wZOZ0yKIdc0vLI/ctrT/w==
posthog-js@^1.87.5:
version "1.87.5"
resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.87.5.tgz#a279fc2016984008f2378a451bbbe33d9c89b857"
integrity sha512-GvSOX9oA1iPPaZSwFkuA333PDDo9yCKj4yqFYjxf/dU2mBIOuIMjdPLTiCvoVmsf2UL/2/9c7AwlnFAG4iRZuQ==
dependencies:
fflate "^0.4.1"

Expand Down
120 changes: 120 additions & 0 deletions src/__tests__/session-props.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { SessionPropsManager } from '../session-props'
import { SessionIdManager } from '../sessionid'
import { PostHogPersistence } from '../posthog-persistence'

describe('Session Props Manager', () => {
const createSessionPropsManager = () => {
const onSessionId = jest.fn()
const generateProps = jest.fn()
const persistenceRegister = jest.fn()
const sessionIdManager = {
onSessionId,
} as unknown as SessionIdManager
const persistence = {
register: persistenceRegister,
props: {},
} as unknown as PostHogPersistence
const sessionPropsManager = new SessionPropsManager(sessionIdManager, persistence, generateProps)

return {
onSessionId,
sessionPropsManager,
persistence,
sessionIdManager,
generateProps,
persistenceRegister,
}
}

it('should register a callback with the session id manager', () => {
const { onSessionId } = createSessionPropsManager()
expect(onSessionId).toHaveBeenCalledTimes(1)
})

it('should update persistence with client session props', () => {
// arrange
const utmSource = 'some-utm-source'
const sessionId = 'session-id'
const { onSessionId, generateProps, persistenceRegister } = createSessionPropsManager()
generateProps.mockReturnValue({ utm_source: utmSource })
const callback = onSessionId.mock.calls[0][0]

// act
callback(sessionId)

//assert
expect(generateProps).toHaveBeenCalledTimes(1)

expect(persistenceRegister).toBeCalledWith({
$client_session_props: {
props: {
utm_source: 'some-utm-source',
},
sessionId: 'session-id',
},
})
})

it('should not update client session props when session id stays the same', () => {
// arrange
const sessionId1 = 'session-id-1'
const { onSessionId, persistence, generateProps, persistenceRegister } = createSessionPropsManager()
persistence.props = {
$client_session_props: {
props: {},
sessionId: sessionId1,
},
}
const callback = onSessionId.mock.calls[0][0]

// act
callback(sessionId1)

//assert
expect(generateProps).toHaveBeenCalledTimes(0)
expect(persistenceRegister).toHaveBeenCalledTimes(0)
})

it('should update client session props when session id changes', () => {
// arrange
const sessionId1 = 'session-id-1'
const sessionId2 = 'session-id-2'

const { onSessionId, persistence, generateProps, persistenceRegister } = createSessionPropsManager()
persistence.props = {
$client_session_props: {
props: {},
sessionId: sessionId1,
},
}
const callback = onSessionId.mock.calls[0][0]

// act
callback(sessionId2)

//assert
expect(generateProps).toHaveBeenCalledTimes(1)
expect(persistenceRegister).toHaveBeenCalledTimes(1)
})

it('should return client session props', () => {
// arrange
const { persistence, sessionPropsManager } = createSessionPropsManager()
persistence.props = {
$client_session_props: {
props: {
utm_source: 'some-utm-source',
},
sessionId: 'session-id',
},
}

// act
const properties = sessionPropsManager.getSessionProps()

//assert
expect(properties).toEqual({
$client_session_utm_source: 'some-utm-source',
})
})
})
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ export const SURVEYS = '$surveys'
export const FLAG_CALL_REPORTED = '$flag_call_reported'
export const USER_STATE = '$user_state'
export const POSTHOG_QUOTA_LIMITED = '$posthog_quota_limited'
export const CLIENT_SESSION_PROPS = '$client_session_props'

// These are propertties that are reserved and will not be automatically included in events
// These are properties that are reserved and will not be automatically included in events
export const PERSISTENCE_RESERVED_PROPERTIES = [
PEOPLE_DISTINCT_ID_KEY,
ALIAS_ID_KEY,
Expand All @@ -41,4 +42,5 @@ export const PERSISTENCE_RESERVED_PROPERTIES = [
STORED_PERSON_PROPERTIES_KEY,
SURVEYS,
FLAG_CALL_REPORTED,
CLIENT_SESSION_PROPS,
]
12 changes: 12 additions & 0 deletions src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { _isArray, _isEmptyObject, _isFunction, _isObject, _isString, _isUndefin
import { _info } from './utils/event-utils'
import { logger } from './utils/logger'
import { document, userAgent } from './utils/globals'
import { SessionPropsManager } from './session-props'

/*
SIMPLE STYLE GUIDE:
Expand Down Expand Up @@ -271,6 +272,7 @@ export class PostHog {
persistence?: PostHogPersistence
sessionPersistence?: PostHogPersistence
sessionManager?: SessionIdManager
sessionPropsManager?: SessionPropsManager

_requestQueue?: RequestQueue
_retryQueue?: RetryQueue
Expand Down Expand Up @@ -437,6 +439,7 @@ export class PostHog {
this.__request_queue = []

this.sessionManager = new SessionIdManager(this.config, this.persistence)
this.sessionPropsManager = new SessionPropsManager(this.sessionManager, this.persistence)
this.sessionPersistence =
this.config.persistence === 'sessionStorage'
? this.persistence
Expand Down Expand Up @@ -934,6 +937,15 @@ export class PostHog {
properties['$window_id'] = windowId
}

if (
this.sessionPropsManager &&
this.config.__preview_send_client_session_params &&
(event_name === '$pageview' || event_name === '$pageleave' || event_name === '$autocapture')
) {
const sessionProps = this.sessionPropsManager.getSessionProps()
properties = _extend(properties, sessionProps)
}

if (this.config.__preview_measure_pageview_stats) {
let performanceProperties: Record<string, any> = {}
if (event_name === '$pageview') {
Expand Down
88 changes: 88 additions & 0 deletions src/session-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* Client-side session parameters. These are primarily used by web analytics,
* which relies on these for session analytics without the plugin server being
* available for the person level set-once properties. Obviously not consistent
* between client-side events and server-side events but this is acceptable
* as web analytics only uses client-side.
*
* These have the same lifespan as a session_id
*/
import { window } from './utils/globals'
import { _info } from './utils/event-utils'
import { SessionIdManager } from './sessionid'
import { PostHogPersistence } from './posthog-persistence'
import { CLIENT_SESSION_PROPS } from './constants'

interface SessionSourceProps {
initialPathName: string
referringDomain: string // Is actually host, but named domain for internal consistency. Should contain a port if there is one.
utm_medium?: string
utm_source?: string
utm_campaign?: string
utm_content?: string
utm_term?: string
}

interface StoredSessionSourceProps {
sessionId: string
props: SessionSourceProps
}

export const generateSessionSourceParams = (): SessionSourceProps => {
return {
initialPathName: window.location.pathname,
referringDomain: _info.referringDomain(),
..._info.campaignParams(),
}
}

export class SessionPropsManager {
private readonly _sessionIdManager: SessionIdManager
private readonly _persistence: PostHogPersistence
private readonly _sessionSourceParamGenerator: typeof generateSessionSourceParams

constructor(
sessionIdManager: SessionIdManager,
persistence: PostHogPersistence,
sessionSourceParamGenerator?: typeof generateSessionSourceParams
) {
this._sessionIdManager = sessionIdManager
this._persistence = persistence
this._sessionSourceParamGenerator = sessionSourceParamGenerator || generateSessionSourceParams

this._sessionIdManager.onSessionId(this._onSessionIdCallback)
}

_getStoredProps(): StoredSessionSourceProps | undefined {
return this._persistence.props[CLIENT_SESSION_PROPS]
}

_onSessionIdCallback = (sessionId: string) => {
const stored = this._getStoredProps()
if (stored && stored.sessionId === sessionId) {
return
}

const newProps: StoredSessionSourceProps = {
sessionId,
props: this._sessionSourceParamGenerator(),
}
this._persistence.register({ [CLIENT_SESSION_PROPS]: newProps })
}

getSessionProps() {
const p = this._getStoredProps()?.props
if (!p) {
return {}
}

return {
$client_session_referring_host: p.referringDomain,
$client_session_initial_pathname: p.initialPathName,
$client_session_utm_source: p.utm_source,
$client_session_utm_campaign: p.utm_campaign,
$client_session_utm_medium: p.utm_medium,
$client_session_utm_content: p.utm_content,
$client_session_utm_term: p.utm_term,
}
}
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export interface PostHogConfig {
}
segment?: any
__preview_measure_pageview_stats?: boolean
__preview_send_client_session_params?: boolean
}

export interface OptInOutCapturingOptions {
Expand Down

0 comments on commit 4213ffa

Please sign in to comment.