From 3e9366b33cf3b775d91fd7919b1d24e73f67507e Mon Sep 17 00:00:00 2001 From: Robbie Coomber Date: Mon, 15 Jan 2024 13:55:09 +0000 Subject: [PATCH 1/2] Support custom scroll selector --- playground/nextjs/pages/_app.tsx | 2 + playground/nextjs/pages/_document.tsx | 19 ++++---- playground/nextjs/pages/index.tsx | 1 + playground/nextjs/pages/longmain.tsx | 70 +++++++++++++++++++++++++++ playground/nextjs/styles/globals.css | 4 ++ src/__tests__/page-view.test.ts | 12 +++-- src/page-view.ts | 49 ++++++++++++++++--- src/posthog-core.ts | 2 +- src/types.ts | 1 + 9 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 playground/nextjs/pages/longmain.tsx diff --git a/playground/nextjs/pages/_app.tsx b/playground/nextjs/pages/_app.tsx index 7f0601849..d36154d28 100644 --- a/playground/nextjs/pages/_app.tsx +++ b/playground/nextjs/pages/_app.tsx @@ -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 } diff --git a/playground/nextjs/pages/_document.tsx b/playground/nextjs/pages/_document.tsx index 54e8bf3e2..4984c17fa 100644 --- a/playground/nextjs/pages/_document.tsx +++ b/playground/nextjs/pages/_document.tsx @@ -1,13 +1,14 @@ import { Html, Head, Main, NextScript } from 'next/document' +import React from 'react' export default function Document() { - return ( - - - -
- - - - ) + return ( + + + +
+ + + + ) } diff --git a/playground/nextjs/pages/index.tsx b/playground/nextjs/pages/index.tsx index 72a23909b..1af228127 100644 --- a/playground/nextjs/pages/index.tsx +++ b/playground/nextjs/pages/index.tsx @@ -59,6 +59,7 @@ export default function Home() { Iframe Media Long + Long Main

Feature flag response: {JSON.stringify(result)}

diff --git a/playground/nextjs/pages/longmain.tsx b/playground/nextjs/pages/longmain.tsx new file mode 100644 index 000000000..07408e363 --- /dev/null +++ b/playground/nextjs/pages/longmain.tsx @@ -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('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 ( + <> + + PostHog + + +
+
+

A long page

+

+ The window itself does not scroll, the main element does. The content is exactly + 4000px tall. +

+
+ Home +
+ + {Array.from({ length: 100 }, (_, 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. +

+ ))} + +
+ Home +
+
+
+ + ) +} diff --git a/playground/nextjs/styles/globals.css b/playground/nextjs/styles/globals.css index 759e4997d..e29a0a67a 100644 --- a/playground/nextjs/styles/globals.css +++ b/playground/nextjs/styles/globals.css @@ -5,6 +5,10 @@ main { font-family: helvetica, arial, sans-serif; } +html, body { + margin: 0 +} + .buttons { display: flex; gap: 0.5rem; diff --git a/src/__tests__/page-view.test.ts b/src/__tests__/page-view.test.ts index 2dbce5c11..3dcc30fe2 100644 --- a/src/__tests__/page-view.test.ts +++ b/src/__tests__/page-view.test.ts @@ -1,4 +1,5 @@ import { PageViewManager } from '../page-view' +import { PostHog } from '../posthog-core' const mockWindowGetter = jest.fn() jest.mock('../utils/globals', () => ({ @@ -10,6 +11,9 @@ jest.mock('../utils/globals', () => ({ describe('PageView ID manager', () => { describe('doPageView', () => { + const instance: PostHog = { + config: {}, + } as any beforeEach(() => { mockWindowGetter.mockReturnValue({ location: { @@ -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 @@ -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 @@ -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() @@ -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() diff --git a/src/page-view.ts b/src/page-view.ts index 6abb7f1a2..be9b71b71 100644 --- a/src/page-view.ts +++ b/src/page-view.ts @@ -1,4 +1,6 @@ import { window } from './utils/globals' +import { PostHog } from './posthog-core' +import { _isArray } from './utils/type-utils' interface PageViewData { pathname: string @@ -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 { @@ -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) } @@ -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 } } diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 07efdd15f..948be0c46 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -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() diff --git a/src/types.ts b/src/types.ts index 4f9efc479..8292bf171 100644 --- a/src/types.ts +++ b/src/types.ts @@ -128,6 +128,7 @@ export interface PostHogConfig { segment?: any __preview_measure_pageview_stats?: boolean __preview_send_client_session_params?: boolean + scroll_root_selector?: string | string[] } export interface OptInOutCapturingOptions { From 7b4ab5e73a83210c820fa00cdee84791412b09b7 Mon Sep 17 00:00:00 2001 From: Robbie Coomber Date: Mon, 15 Jan 2024 14:31:07 +0000 Subject: [PATCH 2/2] Add a comment for scroll_root_selector --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index 8292bf171..3dde979d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -128,6 +128,7 @@ 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[] }