From 9c0ce5245fd1ac1988ff30e6b96c8532839ca851 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:14:02 +0000 Subject: [PATCH 1/2] fix(browser): capture SPA pageviews on query string and hash changes `capture_pageview: 'history_change'` expanded to `{ path: true }`, so a pushState, replaceState, or popstate that changed only the query string or only the hash captured no `$pageview`. Single-page apps that navigate for search, filters, pagination, and tab state lost those pageviews. This is the default for any SDK initialized with `defaults` of '2025-05-24' or later. `'history_change'` now expands to `{ path: true, search: true, hash: true }`. `disable_capture_url_hashes` still suppresses hash-only changes. Generated-By: PostHog Desktop Task-Id: ca7d1e51-64ae-42fc-9c7c-20c2eb623cf9 --- ...history-change-captures-search-and-hash.md | 6 ++++ .../extensions/history-autocapture.test.ts | 28 ++++++++++++++----- .../src/extensions/history-autocapture.ts | 2 +- packages/types/src/posthog-config.ts | 3 +- 4 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 .changeset/history-change-captures-search-and-hash.md 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..4edf087ab1 --- /dev/null +++ b/.changeset/history-change-captures-search-and-hash.md @@ -0,0 +1,6 @@ +--- +'posthog-js': minor +'@posthog/types': minor +--- + +`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/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) From d87fcd0707d7db983bfdbdb56876c1f678e24226 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:35:47 +0000 Subject: [PATCH 2/2] chore: use a patch changeset and regenerate public API references The versioning check requires a patch bump for a `fix:` title, and this is a bug fix, so the changeset drops from minor to patch. The public API reference file picks up the reworded `capture_pageview` doc comment. Generated-By: PostHog Desktop Task-Id: ca7d1e51-64ae-42fc-9c7c-20c2eb623cf9 --- .changeset/history-change-captures-search-and-hash.md | 4 ++-- packages/browser/references/posthog-js-references-latest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/history-change-captures-search-and-hash.md b/.changeset/history-change-captures-search-and-hash.md index 4edf087ab1..259318d2f3 100644 --- a/.changeset/history-change-captures-search-and-hash.md +++ b/.changeset/history-change-captures-search-and-hash.md @@ -1,6 +1,6 @@ --- -'posthog-js': minor -'@posthog/types': minor +'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" },