Skip to content

Commit

Permalink
feat: Support custom scroll selector (#961)
Browse files Browse the repository at this point in the history
* Support custom scroll selector

* Add a comment for scroll_root_selector
  • Loading branch information
robbie-c authored Jan 15, 2024
1 parent 89f96cc commit ffc3d4a
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 22 deletions.
2 changes: 2 additions & 0 deletions playground/nextjs/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ if (typeof window !== 'undefined') {
},
debug: true,
__preview_send_client_session_params: true,
__preview_measure_pageview_stats: true,
scroll_root_selector: ['#scroll_element', 'html'],
})
;(window as any).posthog = posthog
}
Expand Down
19 changes: 10 additions & 9 deletions playground/nextjs/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Html, Head, Main, NextScript } from 'next/document'
import React from 'react'

export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
1 change: 1 addition & 0 deletions playground/nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default function Home() {
<Link href="/iframe">Iframe</Link>
<Link href="/media">Media</Link>
<Link href="/long">Long</Link>
<Link href="/longmain">Long Main</Link>
</div>

<p>Feature flag response: {JSON.stringify(result)}</p>
Expand Down
70 changes: 70 additions & 0 deletions playground/nextjs/pages/longmain.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Head from 'next/head'
import Link from 'next/link'
import React, { useEffect } from 'react'

export default function Home() {
useEffect(() => {
const html = document.querySelector('html')
const body = document.querySelector('body')
const nextRoot = document.querySelector<HTMLDivElement>('div#__next')
if (!html || !body || !nextRoot) return
html.style.height = '100%'
html.style.overflow = 'hidden'
body.style.height = '100%'
nextRoot.style.height = '100%'
return () => {
html.style.height = ''
html.style.overflow = ''
body.style.height = ''
nextRoot.style.height = ''
}
}, [])

return (
<>
<Head>
<title>PostHog</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<main
id="scroll_element"
style={{
height: '100%',
overflow: 'scroll',
margin: 0,
padding: 0,
}}
>
<div
style={{
height: '4000px',
overflow: 'hidden',
}}
>
<h1>A long page</h1>
<p>
The window itself does not scroll, the <code>main</code> element does. The content is exactly
4000px tall.
</p>
<div className="buttons">
<Link href="/">Home</Link>
</div>

{Array.from({ length: 100 }, (_, i) => (
<p key={i}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
))}

<div className="buttons">
<Link href="/">Home</Link>
</div>
</div>
</main>
</>
)
}
4 changes: 4 additions & 0 deletions playground/nextjs/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ main {
font-family: helvetica, arial, sans-serif;
}

html, body {
margin: 0
}

.buttons {
display: flex;
gap: 0.5rem;
Expand Down
12 changes: 8 additions & 4 deletions src/__tests__/page-view.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PageViewManager } from '../page-view'
import { PostHog } from '../posthog-core'

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

describe('PageView ID manager', () => {
describe('doPageView', () => {
const instance: PostHog = {
config: {},
} as any
beforeEach(() => {
mockWindowGetter.mockReturnValue({
location: {
Expand Down Expand Up @@ -41,7 +45,7 @@ describe('PageView ID manager', () => {
},
})

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

// force the manager to update the scroll data by calling an internal method
Expand Down Expand Up @@ -72,7 +76,7 @@ describe('PageView ID manager', () => {
},
})

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

// force the manager to update the scroll data by calling an internal method
Expand All @@ -90,7 +94,7 @@ describe('PageView ID manager', () => {
})

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

pageViewIdManager._updateScrollData()
const firstPageView = pageViewIdManager.doPageView()
Expand All @@ -101,7 +105,7 @@ describe('PageView ID manager', () => {
})

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

const firstPageView = pageViewIdManager.doPageView()
expect(firstPageView.$prev_pageview_pathname).toBeUndefined()
Expand Down
49 changes: 41 additions & 8 deletions src/page-view.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { window } from './utils/globals'
import { PostHog } from './posthog-core'
import { _isArray } from './utils/type-utils'

interface PageViewData {
pathname: string
Expand Down Expand Up @@ -32,6 +34,11 @@ interface PageViewEventProperties extends ScrollProperties {
export class PageViewManager {
_pageViewData: PageViewData | undefined
_hasSeenPageView = false
_instance: PostHog

constructor(instance: PostHog) {
this._instance = instance
}

_createPageViewData(): PageViewData {
return {
Expand Down Expand Up @@ -134,8 +141,11 @@ export class PageViewManager {
}

startMeasuringScrollPosition() {
window?.addEventListener('scroll', this._updateScrollData)
window?.addEventListener('scrollend', this._updateScrollData)
// setting the third argument to `true` means that we will receive scroll events for other scrollable elements
// on the page, not just the window
// see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture
window?.addEventListener('scroll', this._updateScrollData, true)
window?.addEventListener('scrollend', this._updateScrollData, true)
window?.addEventListener('resize', this._updateScrollData)
}

Expand All @@ -145,22 +155,45 @@ export class PageViewManager {
window?.removeEventListener('resize', this._updateScrollData)
}

_scrollElement(): Element | null | undefined {
if (this._instance.config.scroll_root_selector) {
const selectors = _isArray(this._instance.config.scroll_root_selector)
? this._instance.config.scroll_root_selector
: [this._instance.config.scroll_root_selector]
for (const selector of selectors) {
const element = window?.document.querySelector(selector)
if (element) {
return element
}
}
return undefined
} else {
return window?.document.documentElement
}
}

_scrollHeight(): number {
return window
? Math.max(0, window.document.documentElement.scrollHeight - window.document.documentElement.clientHeight)
: 0
const element = this._scrollElement()
return element ? Math.max(0, element.scrollHeight - element.clientHeight) : 0
}

_scrollY(): number {
return window ? window.scrollY || window.pageYOffset || window.document.documentElement.scrollTop || 0 : 0
if (this._instance.config.scroll_root_selector) {
const element = this._scrollElement()
return (element && element.scrollTop) || 0
} else {
return window ? window.scrollY || window.pageYOffset || window.document.documentElement.scrollTop || 0 : 0
}
}

_contentHeight(): number {
return window?.document.documentElement.scrollHeight || 0
const element = this._scrollElement()
return element?.scrollHeight || 0
}

_contentY(): number {
const clientHeight = window?.document.documentElement.clientHeight || 0
const element = this._scrollElement()
const clientHeight = element?.clientHeight || 0
return this._scrollY() + clientHeight
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export class PostHog {

this.featureFlags = new PostHogFeatureFlags(this)
this.toolbar = new Toolbar(this)
this.pageViewManager = new PageViewManager()
this.pageViewManager = new PageViewManager(this)
this.surveys = new PostHogSurveys(this)
this.rateLimiter = new RateLimiter()

Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export interface PostHogConfig {
segment?: any
__preview_measure_pageview_stats?: boolean
__preview_send_client_session_params?: boolean
// Let the pageview scroll stats use a custom css selector for the root element, e.g. `main`
scroll_root_selector?: string | string[]
}

export interface OptInOutCapturingOptions {
Expand Down

0 comments on commit ffc3d4a

Please sign in to comment.