diff --git a/.changeset/history-change-captures-search-and-hash.md b/.changeset/history-change-captures-search-and-hash.md new file mode 100644 index 0000000000..259318d2f3 --- /dev/null +++ b/.changeset/history-change-captures-search-and-hash.md @@ -0,0 +1,6 @@ +--- +'posthog-js': patch +'@posthog/types': patch +--- + +`capture_pageview: 'history_change'` now captures a `$pageview` when a single-page-app navigation changes the query string or the hash, not only the path. Before this change, navigation to search results, filters, pagination, and tab state captured no pageview, so these apps undercounted pageviews. `disable_capture_url_hashes` still suppresses hash-only changes. To keep the old behaviour, set `capture_pageview: { path: true }`. diff --git a/packages/browser/references/posthog-js-references-latest.json b/packages/browser/references/posthog-js-references-latest.json index c3d175d4e1..e38897c368 100644 --- a/packages/browser/references/posthog-js-references-latest.json +++ b/packages/browser/references/posthog-js-references-latest.json @@ -3070,7 +3070,7 @@ "releaseTag": "deprecated" }, { - "description": "Determines whether PostHog should capture pageview events automatically.\nCan be:\n- `true`: Capture the initial pageview\n- `false`: Don't capture any pageviews\n- `'history_change'`: Capture the initial pageview and pageviews when the pathname changes\n- An object: Capture the initial pageview and pageviews when any selected URL component changes", + "description": "Determines whether PostHog should capture pageview events automatically.\nCan be:\n- `true`: Capture the initial pageview\n- `false`: Don't capture any pageviews\n- `'history_change'`: Capture the initial pageview and pageviews when the path, query string, or hash changes.\n `disable_capture_url_hashes` still suppresses hash-only changes.\n- An object: Capture the initial pageview and pageviews when any selected URL component changes", "type": "boolean | \"history_change\" | CapturePageviewOptions", "name": "capture_pageview" }, diff --git a/packages/browser/src/__tests__/extensions/history-autocapture.test.ts b/packages/browser/src/__tests__/extensions/history-autocapture.test.ts index 74aa491c49..ee336e871f 100644 --- a/packages/browser/src/__tests__/extensions/history-autocapture.test.ts +++ b/packages/browser/src/__tests__/extensions/history-autocapture.test.ts @@ -171,13 +171,14 @@ describe('HistoryAutocapture', () => { expect(capture).toHaveBeenCalledWith('$pageview', { navigation_type: 'pushState' }) }) - it('should not capture when only the query string changes with history_change', () => { + it('should capture when only the query string changes with history_change', () => { capture.mockClear() mockLocation.search = '?param=value' window.history.pushState({ page: 1 }, 'Test Page', '/initial?param=value') - expect(capture).not.toHaveBeenCalled() + expect(capture).toHaveBeenCalledTimes(1) + expect(capture).toHaveBeenCalledWith('$pageview', { navigation_type: 'pushState' }) }) it('should not capture pageview when capture_pageview is disabled', () => { @@ -201,12 +202,23 @@ describe('HistoryAutocapture', () => { expect(capture).toHaveBeenCalledWith('$pageview', { navigation_type: 'replaceState' }) }) - it('should not capture when only the hash changes with history_change', () => { + it('should capture when only the hash changes with history_change', () => { capture.mockClear() mockLocation.hash = '#section' window.history.replaceState({ page: 2 }, 'Test Page 2', '/initial#section') + expect(capture).toHaveBeenCalledTimes(1) + expect(capture).toHaveBeenCalledWith('$pageview', { navigation_type: 'replaceState' }) + }) + + it('should not capture hash-only changes with history_change when disable_capture_url_hashes is set', () => { + posthog.config.disable_capture_url_hashes = true + restartWithCapturePageview('history_change') + + mockLocation.hash = '#section' + window.history.replaceState({ page: 2 }, 'Test Page 2', '/initial#section') + expect(capture).not.toHaveBeenCalled() }) }) @@ -309,13 +321,14 @@ describe('HistoryAutocapture', () => { }) describe('hashchange events', () => { - it('should not capture direct hash changes with history_change', () => { + it('should capture direct hash changes with history_change', () => { capture.mockClear() mockLocation.hash = '#section' window.dispatchEvent(new Event('hashchange')) - expect(capture).not.toHaveBeenCalled() + expect(capture).toHaveBeenCalledTimes(1) + expect(capture).toHaveBeenCalledWith('$pageview', { navigation_type: 'hashchange' }) }) it('should capture direct hash changes when hash is enabled', () => { @@ -398,14 +411,15 @@ describe('HistoryAutocapture', () => { expect(capture).toHaveBeenCalledWith('$pageview', { navigation_type: 'pushState' }) }) - it('should not capture with history_change when only query and hash change together', () => { + it('should capture once with history_change when query and hash change together', () => { capture.mockClear() mockLocation.search = '?filter=new' mockLocation.hash = '#results' window.history.pushState({ page: 1 }, 'Filter Results', '/initial?filter=new#results') - expect(capture).not.toHaveBeenCalled() + expect(capture).toHaveBeenCalledTimes(1) + expect(capture).toHaveBeenCalledWith('$pageview', { navigation_type: 'pushState' }) }) }) diff --git a/packages/browser/src/extensions/history-autocapture.ts b/packages/browser/src/extensions/history-autocapture.ts index bf65cee6fe..d474666ec8 100644 --- a/packages/browser/src/extensions/history-autocapture.ts +++ b/packages/browser/src/extensions/history-autocapture.ts @@ -118,7 +118,7 @@ export class HistoryAutocapture implements Extension { const capturePageview = this._instance.config.capture_pageview if (capturePageview === 'history_change') { - return { path: true } + return { path: true, search: true, hash: true } } return isObject(capturePageview) ? capturePageview : {} diff --git a/packages/types/src/posthog-config.ts b/packages/types/src/posthog-config.ts index 1114b08d61..5d69ac6887 100644 --- a/packages/types/src/posthog-config.ts +++ b/packages/types/src/posthog-config.ts @@ -1368,7 +1368,8 @@ export interface PostHogConfig { * Can be: * - `true`: Capture the initial pageview * - `false`: Don't capture any pageviews - * - `'history_change'`: Capture the initial pageview and pageviews when the pathname changes + * - `'history_change'`: Capture the initial pageview and pageviews when the path, query string, or hash changes. + * `disable_capture_url_hashes` still suppresses hash-only changes. * - An object: Capture the initial pageview and pageviews when any selected URL component changes * * @default true (or `'history_change'` when `defaults` is `'2025-05-24'` or later)