Skip to content

Commit

Permalink
feat: Heatmaps instrumentation (#1131)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite authored Apr 18, 2024
1 parent cb5791a commit ba1b431
Show file tree
Hide file tree
Showing 15 changed files with 403 additions and 178 deletions.
6 changes: 5 additions & 1 deletion playground/nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ export default function Home() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<main>
<h1>PostHog React</h1>
<div className="sticky top-0 bg-white border-b mb-4">
<h1 className="m-0">
<b>PostHog</b> React
</h1>
</div>

<p>The current time is {time}</p>

Expand Down
3 changes: 2 additions & 1 deletion playground/nextjs/src/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ if (typeof window !== 'undefined') {
debug: true,
scroll_root_selector: ['#scroll_element', 'html'],
// persistence: cookieConsentGiven() ? 'localStorage+cookie' : 'memory',
__preview_process_person: 'identified_only',
person_profiles: 'identified_only',
__preview_heatmaps: true,
...configForConsent(),
})
;(window as any).posthog = posthog
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/extensions/replay/sessionrecording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ describe('SessionRecording', () => {
{
_url: 'https://test.com/s/',
_noTruncate: true,
_noHeatmaps: true,
_batchKey: 'recordings',
}
)
Expand Down Expand Up @@ -606,6 +607,7 @@ describe('SessionRecording', () => {
{
_url: 'https://test.com/s/',
_noTruncate: true,
_noHeatmaps: true,
_batchKey: 'recordings',
}
)
Expand Down Expand Up @@ -688,6 +690,7 @@ describe('SessionRecording', () => {
{
_url: 'https://test.com/s/',
_noTruncate: true,
_noHeatmaps: true,
_batchKey: 'recordings',
}
)
Expand Down Expand Up @@ -1322,6 +1325,7 @@ describe('SessionRecording', () => {
{
_batchKey: 'recordings',
_noTruncate: true,
_noHeatmaps: true,
_url: 'https://test.com/s/',
}
)
Expand Down
91 changes: 91 additions & 0 deletions src/__tests__/heatmaps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { createPosthogInstance } from './helpers/posthog-instance'
import { uuidv7 } from '../uuidv7'
import { PostHog } from '../posthog-core'
jest.mock('../utils/logger')

describe('heatmaps', () => {
let posthog: PostHog
let onCapture = jest.fn()

const mockClickEvent = {
target: document.body,
clientX: 10,
clientY: 20,
} as unknown as MouseEvent

const createMockMouseEvent = (props: Partial<MouseEvent> = {}) =>
({
target: document.body,
clientX: 10,
clientY: 10,
...props,
} as unknown as MouseEvent)

beforeEach(async () => {
onCapture = jest.fn()
posthog = await createPosthogInstance(uuidv7(), { _onCapture: onCapture })
})

it('should include generated heatmap data', async () => {
posthog.heatmaps?.['_onClick']?.(mockClickEvent as MouseEvent)
posthog.capture('test event')

expect(onCapture).toBeCalledTimes(1)
expect(onCapture.mock.lastCall).toMatchObject([
'test event',
{
event: 'test event',
properties: {
$heatmap_data: {
'http://localhost/': [
{
target_fixed: false,
type: 'click',
x: 10,
y: 20,
},
],
},
},
},
])
})

it('should add rageclick events in the same area', async () => {
posthog.heatmaps?.['_onClick']?.(createMockMouseEvent())
posthog.heatmaps?.['_onClick']?.(createMockMouseEvent())
posthog.heatmaps?.['_onClick']?.(createMockMouseEvent())

posthog.capture('test event')

expect(onCapture).toBeCalledTimes(1)
expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://localhost/']).toHaveLength(4)
expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://localhost/'].map((x) => x.type)).toEqual([
'click',
'click',
'rageclick',
'click',
])
})

it('should clear the buffer after each call', async () => {
posthog.heatmaps?.['_onClick']?.(createMockMouseEvent())
posthog.heatmaps?.['_onClick']?.(createMockMouseEvent())
posthog.capture('test event')
expect(onCapture).toBeCalledTimes(1)
expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://localhost/']).toHaveLength(2)

posthog.capture('test event 2')
expect(onCapture).toBeCalledTimes(2)
expect(onCapture.mock.lastCall[1].properties.$heatmap_data).toBeUndefined()
})

it('should not include generated heatmap data for $snapshot events with _noHeatmaps', async () => {
posthog.heatmaps?.['_onClick']?.(createMockMouseEvent())
posthog.capture('$snapshot', undefined, { _noHeatmaps: true })

expect(onCapture).toBeCalledTimes(1)
expect(onCapture.mock.lastCall).toMatchObject(['$snapshot', {}])
expect(onCapture.mock.lastCall[1].properties).not.toHaveProperty('$heatmap_data')
})
})
25 changes: 13 additions & 12 deletions src/__tests__/page-view.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PageViewManager } from '../page-view'
import { PostHog } from '../posthog-core'
import { ScrollManager } from '../scroll-manager'

const mockWindowGetter = jest.fn()
jest.mock('../utils/globals', () => ({
Expand All @@ -11,10 +12,15 @@ jest.mock('../utils/globals', () => ({

describe('PageView ID manager', () => {
describe('doPageView', () => {
const instance: PostHog = {
config: {},
} as any
let instance: PostHog
let pageViewIdManager: PageViewManager

beforeEach(() => {
instance = {
config: {},
} as any
instance.scrollManager = new ScrollManager(instance)
pageViewIdManager = new PageViewManager(instance)
mockWindowGetter.mockReturnValue({
location: {
pathname: '/pathname',
Expand Down Expand Up @@ -45,11 +51,10 @@ describe('PageView ID manager', () => {
},
})

const pageViewIdManager = new PageViewManager(instance)
pageViewIdManager.doPageView()

// force the manager to update the scroll data by calling an internal method
pageViewIdManager._updateScrollData()
instance.scrollManager['_updateScrollData']()

const secondPageView = pageViewIdManager.doPageView()
expect(secondPageView.$prev_pageview_last_scroll).toEqual(2000)
Expand All @@ -76,11 +81,10 @@ describe('PageView ID manager', () => {
},
})

const pageViewIdManager = new PageViewManager(instance)
pageViewIdManager.doPageView()

// force the manager to update the scroll data by calling an internal method
pageViewIdManager._updateScrollData()
instance.scrollManager['_updateScrollData']()

const secondPageView = pageViewIdManager.doPageView()
expect(secondPageView.$prev_pageview_last_scroll).toEqual(0)
Expand All @@ -94,9 +98,7 @@ describe('PageView ID manager', () => {
})

it('can handle scroll updates before doPageView is called', () => {
const pageViewIdManager = new PageViewManager(instance)

pageViewIdManager._updateScrollData()
instance.scrollManager['_updateScrollData']()
const firstPageView = pageViewIdManager.doPageView()
expect(firstPageView.$prev_pageview_last_scroll).toBeUndefined()

Expand All @@ -105,8 +107,7 @@ describe('PageView ID manager', () => {
})

it('should include the pathname', () => {
const pageViewIdManager = new PageViewManager(instance)

instance.scrollManager['_updateScrollData']()
const firstPageView = pageViewIdManager.doPageView()
expect(firstPageView.$prev_pageview_pathname).toBeUndefined()
const secondPageView = pageViewIdManager.doPageView()
Expand Down
12 changes: 0 additions & 12 deletions src/__tests__/posthog-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,18 +451,6 @@ describe('posthog core', () => {
expect(given.overrides.sessionManager.checkAndGetSessionAndWindowId).not.toHaveBeenCalled()
})

it('only adds a few propertes if event is $performance_event', () => {
given('event_name', () => '$performance_event')
expect(given.subject).toEqual({
distinct_id: 'abc',
event: 'prop', // from actual mock event properties
$current_url: undefined,
$session_id: 'sessionId',
$window_id: 'windowId',
token: 'testtoken',
})
})

it('calls sanitize_properties', () => {
given('sanitize_properties', () => (props, event_name) => ({ token: props.token, event_name }))

Expand Down
2 changes: 1 addition & 1 deletion src/autocapture-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ function checkIfElementTreePassesCSSSelectorAllowList(
return false
}

function getParentElement(curEl: Element): Element | false {
export function getParentElement(curEl: Element): Element | false {
const parentNode = curEl.parentNode
if (!parentNode || !isElementNode(parentNode)) return false
return parentNode
Expand Down
1 change: 0 additions & 1 deletion src/autocapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export class Autocapture {
_isDisabledServerSide: boolean | null = null
rageclicks = new RageClick()
_elementsChainAsString = false
_decideResponse?: DecideResponse

constructor(instance: PostHog) {
this.instance = instance
Expand Down
1 change: 1 addition & 0 deletions src/extensions/exception-autocapture/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export class ExceptionObserver {
this.instance.capture('$exception', properties, {
_noTruncate: true,
_batchKey: 'exceptionEvent',
_noHeatmaps: true,
})
}
}
1 change: 1 addition & 0 deletions src/extensions/replay/sessionrecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,7 @@ export class SessionRecording {
_url: this.instance.requestRouter.endpointFor('api', this._endpoint),
_noTruncate: true,
_batchKey: SESSION_RECORDING_BATCH_KEY,
_noHeatmaps: true, // Session Replay ingestion can't handle heatamap data
})
}
}
Loading

0 comments on commit ba1b431

Please sign in to comment.