From 339b7df04da8ba283ef89394c50fb55d4ca50c5a Mon Sep 17 00:00:00 2001 From: Robbie Date: Tue, 6 Feb 2024 08:46:54 +0000 Subject: [PATCH 01/26] fix: Add warning and conversion for number distinct_id (#993) * Add warning and conversion for number distinct_id * Move test to unit test --- functional_tests/identify.test.ts | 4 +++- src/__tests__/identify.test.ts | 39 +++++++++++++++++++++++++++++++ src/posthog-core.ts | 15 +++++++++++- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/identify.test.ts diff --git a/functional_tests/identify.test.ts b/functional_tests/identify.test.ts index e9f1c591e..d9f95d587 100644 --- a/functional_tests/identify.test.ts +++ b/functional_tests/identify.test.ts @@ -3,7 +3,8 @@ import { waitFor } from '@testing-library/dom' import { v4 } from 'uuid' import { getRequests } from './mock-server' import { createPosthogInstance } from './posthog-instance' - +import { logger } from '../src/utils/logger' +jest.mock('../src/utils/logger') test('identify sends a identify event', async () => { const token = v4() const posthog = await createPosthogInstance(token) @@ -24,6 +25,7 @@ test('identify sends a identify event', async () => { }) ) ) + expect(jest.mocked(logger).error).toBeCalledTimes(0) }) test('identify sends an engage request if identify called twice with the same distinct id and with $set/$set_once', async () => { diff --git a/src/__tests__/identify.test.ts b/src/__tests__/identify.test.ts new file mode 100644 index 000000000..b164255b4 --- /dev/null +++ b/src/__tests__/identify.test.ts @@ -0,0 +1,39 @@ +import { v4 } from 'uuid' +import { createPosthogInstance } from '../../functional_tests/posthog-instance' +import { logger } from '../utils/logger' +jest.mock('../utils/logger') + +describe('identify', () => { + // Note that there are other tests for identify in posthog-core.identify.js + // These are in the old style of tests, if you are feeling helpful you could + // convert them to the new style in this file. + + it('should persist the distinct_id', async () => { + // arrange + const token = v4() + const posthog = await createPosthogInstance(token) + const distinctId = '123' + + // act + posthog.identify(distinctId) + + // assert + expect(posthog.persistence!.properties()['$user_id']).toEqual(distinctId) + expect(jest.mocked(logger).error).toBeCalledTimes(0) + }) + + it('should convert a numeric distinct_id to a string', async () => { + // arrange + const token = v4() + const posthog = await createPosthogInstance(token) + const distinctIdNum = 123 + const distinctIdString = '123' + + // act + posthog.identify(distinctIdNum as any) + + // assert + expect(posthog.persistence!.properties()['$user_id']).toEqual(distinctIdString) + expect(jest.mocked(logger).error).toBeCalledTimes(1) + }) +}) diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 6b332845f..fa43907db 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -52,7 +52,15 @@ import { PostHogSurveys } from './posthog-surveys' import { RateLimiter } from './rate-limiter' import { uuidv7 } from './uuidv7' import { SurveyCallback } from './posthog-surveys-types' -import { _isArray, _isEmptyObject, _isFunction, _isObject, _isString, _isUndefined } from './utils/type-utils' +import { + _isArray, + _isEmptyObject, + _isFunction, + _isNumber, + _isObject, + _isString, + _isUndefined, +} from './utils/type-utils' import { _info } from './utils/event-utils' import { logger } from './utils/logger' import { document, userAgent } from './utils/globals' @@ -1293,6 +1301,11 @@ export class PostHog { if (!this.__loaded || !this.persistence) { return logger.uninitializedWarning('posthog.identify') } + if (_isNumber(new_distinct_id)) { + logger.error('The first argument to posthog.identify was a number, but it should be a string.') + new_distinct_id = (new_distinct_id as number).toString() + } + //if the new_distinct_id has not been set ignore the identify event if (!new_distinct_id) { logger.error('Unique user id has not been set in posthog.identify') From d555d677b576aaeb65fa20ecabc65bb1b8b45f9f Mon Sep 17 00:00:00 2001 From: robbie-c Date: Tue, 6 Feb 2024 08:47:29 +0000 Subject: [PATCH 02/26] chore: Bump version to 1.105.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6746291d..fd23b14e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.105.0 - 2024-02-06 + +- fix: Add warning and conversion for number distinct_id (#993) +- fix: Remove `baseUrl` from TypeScript compiler options (#996) + ## 1.104.4 - 2024-02-02 - fix: very defensive body redaction (#988) diff --git a/package.json b/package.json index cd138bfaf..8e63ef795 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.104.4", + "version": "1.105.0", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From c989eba95c86e7e64cf534e40adf0163797903f0 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 6 Feb 2024 09:27:51 +0000 Subject: [PATCH 03/26] chore: copy most recent changelog entry when creating a release (#995) --- .github/workflows/cd.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 534a89f83..fd561ccd5 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -68,6 +68,14 @@ jobs: REDIS_URL: 'redis://localhost' NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Set LAST_CHANGELOG_ENTRY + run: | + # read from the first until the second header in the changelog file + # this assumes the formatting of the file + # and that this workflow is always running for the most recent entry in the file + LAST_CHANGELOG_ENTRY=$(awk '/^## /{if (flag) exit; flag=1} flag && /^##$/{exit} flag' CHANGELOG.md) + echo "LAST_CHANGELOG_ENTRY=$LAST_CHANGELOG_ENTRY" >> $GITHUB_ENV + - name: Create GitHub release uses: actions/create-release@v1 env: @@ -75,6 +83,7 @@ jobs: with: tag_name: v${{ env.COMMITTED_VERSION }} release_name: ${{ env.COMMITTED_VERSION }} + body: ${{ env.LAST_CHANGELOG_ENTRY || 'see CHANGELOG.md' }} create-pull-request: name: Create main repo PR with new posthog-js version From 560c63ab966e228efd83c3c7ed99b9afe65b9c01 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 6 Feb 2024 10:01:13 +0000 Subject: [PATCH 04/26] chore: nit pick log message (#997) * chore: nit pick log message * update test * no need to change tests that don't run --- src/__tests__/identify.test.ts | 4 +++- src/posthog-core.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/__tests__/identify.test.ts b/src/__tests__/identify.test.ts index b164255b4..fe18888ca 100644 --- a/src/__tests__/identify.test.ts +++ b/src/__tests__/identify.test.ts @@ -20,6 +20,7 @@ describe('identify', () => { // assert expect(posthog.persistence!.properties()['$user_id']).toEqual(distinctId) expect(jest.mocked(logger).error).toBeCalledTimes(0) + expect(jest.mocked(logger).warn).toBeCalledTimes(0) }) it('should convert a numeric distinct_id to a string', async () => { @@ -34,6 +35,7 @@ describe('identify', () => { // assert expect(posthog.persistence!.properties()['$user_id']).toEqual(distinctIdString) - expect(jest.mocked(logger).error).toBeCalledTimes(1) + expect(jest.mocked(logger).error).toBeCalledTimes(0) + expect(jest.mocked(logger).warn).toBeCalledTimes(1) }) }) diff --git a/src/posthog-core.ts b/src/posthog-core.ts index fa43907db..ce30edf80 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -1302,8 +1302,10 @@ export class PostHog { return logger.uninitializedWarning('posthog.identify') } if (_isNumber(new_distinct_id)) { - logger.error('The first argument to posthog.identify was a number, but it should be a string.') new_distinct_id = (new_distinct_id as number).toString() + logger.warn( + 'The first argument to posthog.identify was a number, but it should be a string. It has been converted to a string.' + ) } //if the new_distinct_id has not been set ignore the identify event From cd08c97f707bb674e5b6ec04e98fd4fc1fa08140 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 6 Feb 2024 10:50:59 +0000 Subject: [PATCH 05/26] chore: move posthog test instance helper (#999) --- .github/workflows/library-ci.yml | 2 +- functional_tests/feature-flags.test.ts | 2 +- functional_tests/identify.test.ts | 2 +- package.json | 3 ++- .../__tests__/helpers}/posthog-instance.ts | 11 ++++++++--- src/__tests__/identify.test.ts | 2 +- 6 files changed, 14 insertions(+), 8 deletions(-) rename {functional_tests => src/__tests__/helpers}/posthog-instance.ts (79%) diff --git a/.github/workflows/library-ci.yml b/.github/workflows/library-ci.yml index 7e91f7e86..2d99c8686 100644 --- a/.github/workflows/library-ci.yml +++ b/.github/workflows/library-ci.yml @@ -69,7 +69,7 @@ jobs: node-version: '18' cache: 'pnpm' - run: pnpm install - - run: pnpm jest functional_tests/ + - run: pnpm run test:functional lint: name: Lint diff --git a/functional_tests/feature-flags.test.ts b/functional_tests/feature-flags.test.ts index 3b2cc6861..f795b6282 100644 --- a/functional_tests/feature-flags.test.ts +++ b/functional_tests/feature-flags.test.ts @@ -1,5 +1,5 @@ import { v4 } from 'uuid' -import { createPosthogInstance } from './posthog-instance' +import { createPosthogInstance } from '../src/__tests__/helpers/posthog-instance' import { waitFor } from '@testing-library/dom' import { getRequests, resetRequests } from './mock-server' diff --git a/functional_tests/identify.test.ts b/functional_tests/identify.test.ts index d9f95d587..c7a83dfff 100644 --- a/functional_tests/identify.test.ts +++ b/functional_tests/identify.test.ts @@ -2,7 +2,7 @@ import 'regenerator-runtime/runtime' import { waitFor } from '@testing-library/dom' import { v4 } from 'uuid' import { getRequests } from './mock-server' -import { createPosthogInstance } from './posthog-instance' +import { createPosthogInstance } from '../src/__tests__/helpers/posthog-instance' import { logger } from '../src/utils/logger' jest.mock('../src/utils/logger') test('identify sends a identify event', async () => { diff --git a/package.json b/package.json index 8e63ef795..7652203d2 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,11 @@ "lint": "eslint src", "prettier": "prettier --write src/ functional_tests/", "prepublishOnly": "pnpm lint && pnpm test && pnpm build && pnpm test:react", - "test": "pnpm test:unit && pnpm test:custom-eslint-rules", + "test": "pnpm test:unit && pnpm test:custom-eslint-rules && pnpm test:functional", "test:unit": "jest src", "test:custom-eslint-rules": "jest eslint-rules", "test:react": "cd react; pnpm test", + "test:functional": "jest functional_tests", "test-watch": "jest --watch src", "cypress": "cypress open", "prepare": "husky install" diff --git a/functional_tests/posthog-instance.ts b/src/__tests__/helpers/posthog-instance.ts similarity index 79% rename from functional_tests/posthog-instance.ts rename to src/__tests__/helpers/posthog-instance.ts index 48767c0b8..860838858 100644 --- a/functional_tests/posthog-instance.ts +++ b/src/__tests__/helpers/posthog-instance.ts @@ -1,19 +1,24 @@ // The library depends on having the module initialized before it can be used. import { v4 } from 'uuid' -import { PostHog, init_as_module } from '../src/posthog-core' +import { PostHog, init_as_module } from '../../posthog-core' import 'regenerator-runtime/runtime' -import { PostHogConfig } from '../src/types' +import { PostHogConfig } from '../../types' // It sets a global variable that is set and used to initialize subsequent libaries. beforeAll(() => init_as_module()) -export const createPosthogInstance = async (token: string = v4(), config: Partial = {}) => { +export const createPosthogInstance = async ( + token: string = v4(), + config: Partial = {} +): Promise => { // We need to create a new instance of the library for each test, to ensure // that they are isolated from each other. The way the library is currently // written, we first create an instance, then call init on it which then // creates another instance. const posthog = new PostHog() + + // eslint-disable-next-line compat/compat return await new Promise((resolve) => posthog.init( // Use a random UUID for the token, such that we don't have to worry diff --git a/src/__tests__/identify.test.ts b/src/__tests__/identify.test.ts index fe18888ca..947a3d700 100644 --- a/src/__tests__/identify.test.ts +++ b/src/__tests__/identify.test.ts @@ -1,5 +1,5 @@ import { v4 } from 'uuid' -import { createPosthogInstance } from '../../functional_tests/posthog-instance' +import { createPosthogInstance } from './helpers/posthog-instance' import { logger } from '../utils/logger' jest.mock('../utils/logger') From 3156346c8b4a42917966cec2332ee0bccf533a7b Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Wed, 7 Feb 2024 11:41:14 +0000 Subject: [PATCH 06/26] fix: autocapture allowlist should consider the tree (#1000) * fix: autocapture allowlist should consider the tree * comment update on types * rename refactor * parameterise tests * Update src/__tests__/autocapture-utils.test.ts Co-authored-by: David Newell * reword type comment --------- Co-authored-by: David Newell --- src/__tests__/autocapture-utils.test.ts | 102 ++++++++++++++++++++++++ src/autocapture-utils.ts | 80 +++++++++++++++---- src/types.ts | 8 ++ 3 files changed, 175 insertions(+), 15 deletions(-) diff --git a/src/__tests__/autocapture-utils.test.ts b/src/__tests__/autocapture-utils.test.ts index 53e490440..f0176196b 100644 --- a/src/__tests__/autocapture-utils.test.ts +++ b/src/__tests__/autocapture-utils.test.ts @@ -158,6 +158,108 @@ describe(`Autocapture utility functions`, () => { expect(shouldCaptureDomEvent(document!.createElement(tagName), makeMouseEvent({}))).toBe(false) }) }) + + describe('css selector allowlist', () => { + function makeSingleBranchOfDomTree(tree: { tag: string; id?: string }[]): Element { + let finalElement: Element | null = null + for (const { tag, id } of tree) { + const el = document!.createElement(tag) + if (id) { + el.id = id + } + if (finalElement) { + finalElement.appendChild(el) + finalElement = el + } else { + finalElement = el + } + } + if (!finalElement) { + throw new Error('No elements in tree') + } + return finalElement + } + + it.each([ + [ + 'when there is no allowlist', + makeSingleBranchOfDomTree([{ tag: 'div' }, { tag: 'button', id: 'in-allowlist' }, { tag: 'svg' }]), + undefined, + true, + ], + [ + 'when there is a parent matching the allow list', + makeSingleBranchOfDomTree([{ tag: 'div' }, { tag: 'button', id: 'in-allowlist' }, { tag: 'svg' }]), + { + css_selector_allowlist: ['[id]'], + }, + true, + ], + [ + 'when the click target is matching in the allow list', + makeSingleBranchOfDomTree([{ tag: 'div' }, { tag: 'button' }, { tag: 'svg', id: 'in-allowlist' }]), + { + css_selector_allowlist: ['[id]'], + }, + true, + ], + [ + 'when the parent does not match the allowlist', + makeSingleBranchOfDomTree([ + { tag: 'div' }, + { tag: 'button', id: '[id=not-the-configured-value]' }, + { tag: 'svg' }, + ]), + { + // the click was detected on the SVG, but the button is not in the allow list, + // so we should detect the click + css_selector_allowlist: ['in-allowlist'], + }, + false, + ], + [ + 'when the click target (or its parents) does not match the allowlist', + makeSingleBranchOfDomTree([ + { tag: 'div' }, + { tag: 'button' }, + { tag: 'svg', id: '[id=not-the-configured-value]' }, + ]), + { + css_selector_allowlist: ['in-allowlist'], + }, + false, + ], + [ + 'when combining allow lists', + makeSingleBranchOfDomTree([{ tag: 'div' }, { tag: 'button', id: 'in-allowlist' }, { tag: 'svg' }]), + { + // the tree for the click does have an id + css_selector_allowlist: ['[id]'], + // but we only detect if there is an img in the tree + element_allowlist: ['img'], + }, + false, + ], + [ + 'combine allow lists - but showing it considers them separately', + makeSingleBranchOfDomTree([ + { tag: 'div' }, + { tag: 'button', id: 'in-allowlist' }, + { tag: 'img' }, + { tag: 'svg' }, + ]), + { + // the tree for the click does have an id + css_selector_allowlist: ['[id]'], + // and the tree for the click does have an img + element_allowlist: ['img'], + }, + true, + ], + ])('correctly respects the allow list: %s', (_, clickTarget, autoCaptureConfig, shouldCapture) => { + expect(shouldCaptureDomEvent(clickTarget, makeMouseEvent({}), autoCaptureConfig)).toBe(shouldCapture) + }) + }) }) describe(`isSensitiveElement`, () => { diff --git a/src/autocapture-utils.ts b/src/autocapture-utils.ts index 0c6948a95..776e7d919 100644 --- a/src/autocapture-utils.ts +++ b/src/autocapture-utils.ts @@ -107,6 +107,62 @@ export function isDocumentFragment(el: Element | ParentNode | undefined | null): } export const autocaptureCompatibleElements = ['a', 'button', 'form', 'input', 'select', 'textarea', 'label'] + +/* + if there is no config, then all elements are allowed + if there is a config, and there is an allow list, then only elements in the allow list are allowed + assumes that some other code is checking this element's parents + */ +function checkIfElementTreePassesElementAllowList( + elements: Element[], + autocaptureConfig: AutocaptureConfig | undefined +): boolean { + const allowlist = autocaptureConfig?.element_allowlist + if (_isUndefined(allowlist)) { + // everything is allowed, when there is no allow list + return true + } + + // check each element in the tree + // if any of the elements are in the allow list, then the tree is allowed + for (const el of elements) { + if (allowlist.some((elementType) => el.tagName.toLowerCase() === elementType)) { + return true + } + } + + // otherwise there is an allow list and this element tree didn't match it + return false +} + +/* + if there is no config, then all elements are allowed + if there is a config, and there is an allow list, then + only elements that match the css selector in the allow list are allowed + assumes that some other code is checking this element's parents + */ +function checkIfElementTreePassesCSSSelectorAllowList( + elements: Element[], + autocaptureConfig: AutocaptureConfig | undefined +): boolean { + const allowlist = autocaptureConfig?.css_selector_allowlist + if (_isUndefined(allowlist)) { + // everything is allowed, when there is no allow list + return true + } + + // check each element in the tree + // if any of the elements are in the allow list, then the tree is allowed + for (const el of elements) { + if (allowlist.some((selector) => el.matches(selector))) { + return true + } + } + + // otherwise there is an allow list and this element tree didn't match it + return false +} + /* * Check whether a DOM event should be "captured" or if it may contain sentitive data * using a variety of heuristics. @@ -139,22 +195,8 @@ export function shouldCaptureDomEvent( } } - if (autocaptureConfig?.element_allowlist) { - const allowlist = autocaptureConfig.element_allowlist - if (allowlist && !allowlist.some((elementType) => el.tagName.toLowerCase() === elementType)) { - return false - } - } - - if (autocaptureConfig?.css_selector_allowlist) { - const allowlist = autocaptureConfig.css_selector_allowlist - if (allowlist && !allowlist.some((selector) => el.matches(selector))) { - return false - } - } - let parentIsUsefulElement = false - const targetElementList: Element[] = [el] // TODO: remove this var, it's never queried + const targetElementList: Element[] = [el] let parentNode: Element | boolean = true let curEl: Element = el while (curEl.parentNode && !isTag(curEl, 'body')) { @@ -179,6 +221,14 @@ export function shouldCaptureDomEvent( curEl = parentNode } + if (!checkIfElementTreePassesElementAllowList(targetElementList, autocaptureConfig)) { + return false + } + + if (!checkIfElementTreePassesCSSSelectorAllowList(targetElementList, autocaptureConfig)) { + return false + } + const compStyles = window.getComputedStyle(el) if (compStyles && compStyles.getPropertyValue('cursor') === 'pointer' && event.type === 'click') { return true diff --git a/src/types.ts b/src/types.ts index 841e7798f..3ef5cd638 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,12 +37,20 @@ export interface AutocaptureConfig { /** * List of DOM elements to allow autocapture on * e.g. ['a', 'button', 'form', 'input', 'select', 'textarea', 'label'] + * we consider the tree of elements from the root to the target element of the click event + * so for the tree div > div > button > svg + * if the allowlist has button then we allow the capture when the button or the svg is the click target + * but not if either of the divs are detected as the click target */ element_allowlist?: AutocaptureCompatibleElement[] /** * List of CSS selectors to allow autocapture on * e.g. ['[ph-capture]'] + * we consider the tree of elements from the root to the target element of the click event + * so for the tree div > div > button > svg + * and allow list config `['[id]']` + * we will capture the click if the click-target or its parents has any id */ css_selector_allowlist?: string[] From 690a9939196d4933793f8cf7b05603a5fb1799c9 Mon Sep 17 00:00:00 2001 From: pauldambra Date: Wed, 7 Feb 2024 11:41:46 +0000 Subject: [PATCH 07/26] chore: Bump version to 1.105.1 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd23b14e6..b8bbe6c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.105.1 - 2024-02-07 + +- fix: autocapture allowlist should consider the tree (#1000) +- chore: move posthog test instance helper (#999) +- chore: nit pick log message (#997) +- chore: copy most recent changelog entry when creating a release (#995) + ## 1.105.0 - 2024-02-06 - fix: Add warning and conversion for number distinct_id (#993) diff --git a/package.json b/package.json index 7652203d2..340361f97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.105.0", + "version": "1.105.1", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 964e783c34af9acd8188df023a33e4a59dfb0ffc Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Wed, 7 Feb 2024 12:00:56 +0000 Subject: [PATCH 08/26] fix: change log update can't be written to env (#1002) --- .github/workflows/cd.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index fd561ccd5..0e2c2170d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -69,12 +69,13 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Set LAST_CHANGELOG_ENTRY + id: set-last-changelog-entry run: | # read from the first until the second header in the changelog file # this assumes the formatting of the file # and that this workflow is always running for the most recent entry in the file - LAST_CHANGELOG_ENTRY=$(awk '/^## /{if (flag) exit; flag=1} flag && /^##$/{exit} flag' CHANGELOG.md) - echo "LAST_CHANGELOG_ENTRY=$LAST_CHANGELOG_ENTRY" >> $GITHUB_ENV + LAST_CHANGELOG_ENTRY=$(awk -v default='see CHANGELOG.md' '/^## /{if (flag) exit; flag=1} flag && /^##$/{exit} flag; END{if (!flag) print default}' CHANGELOG.md) + echo "LAST_CHANGELOG_ENTRY=$LAST_CHANGELOG_ENTRY" >> "$GITHUB_OUTPUT" - name: Create GitHub release uses: actions/create-release@v1 @@ -83,7 +84,7 @@ jobs: with: tag_name: v${{ env.COMMITTED_VERSION }} release_name: ${{ env.COMMITTED_VERSION }} - body: ${{ env.LAST_CHANGELOG_ENTRY || 'see CHANGELOG.md' }} + body: ${{ steps.set-last-changelog-entry.outputs.LAST_CHANGELOG_ENTRY }} create-pull-request: name: Create main repo PR with new posthog-js version From 1b37dd2b3a81b60649a23c1b0cec056e1a746dd8 Mon Sep 17 00:00:00 2001 From: pauldambra Date: Wed, 7 Feb 2024 12:01:29 +0000 Subject: [PATCH 09/26] chore: Bump version to 1.105.2 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8bbe6c27..cb025ddb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.105.2 - 2024-02-07 + + + ## 1.105.1 - 2024-02-07 - fix: autocapture allowlist should consider the tree (#1000) diff --git a/package.json b/package.json index 340361f97..7d985ad17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.105.1", + "version": "1.105.2", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From c05633e6607b91c50c014aa393ff07587765b5cc Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Wed, 7 Feb 2024 12:55:51 +0000 Subject: [PATCH 10/26] fix: mac and github actions don't agree on usable awk variables (#1003) --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 0e2c2170d..dea055bed 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -74,7 +74,7 @@ jobs: # read from the first until the second header in the changelog file # this assumes the formatting of the file # and that this workflow is always running for the most recent entry in the file - LAST_CHANGELOG_ENTRY=$(awk -v default='see CHANGELOG.md' '/^## /{if (flag) exit; flag=1} flag && /^##$/{exit} flag; END{if (!flag) print default}' CHANGELOG.md) + awk -v defText='see CHANGELOG.md' '/^## /{if (flag) exit; flag=1} flag && /^##$/{exit} flag; END{if (!flag) print defText}' CHANGELOG.md echo "LAST_CHANGELOG_ENTRY=$LAST_CHANGELOG_ENTRY" >> "$GITHUB_OUTPUT" - name: Create GitHub release From ca90e4f1ac4226952d318e3fabbe99c319632528 Mon Sep 17 00:00:00 2001 From: pauldambra Date: Wed, 7 Feb 2024 12:56:26 +0000 Subject: [PATCH 11/26] chore: Bump version to 1.105.3 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb025ddb7..171b3bed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.105.3 - 2024-02-07 + + + ## 1.105.2 - 2024-02-07 diff --git a/package.json b/package.json index 7d985ad17..12c92e267 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.105.2", + "version": "1.105.3", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 04fe644730b2fd23e1e34708d28eaaa48535cb2d Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Wed, 7 Feb 2024 13:58:29 +0000 Subject: [PATCH 12/26] Update CHANGELOG.md (#1004) --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 171b3bed3..e16c71f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ ## 1.105.3 - 2024-02-07 - +identical to 1.105.1 - bug in CI scripts ## 1.105.2 - 2024-02-07 - +identical to 1.105.1 - bug in CI scripts ## 1.105.1 - 2024-02-07 From ba57cd029300fbcaebe71f501b29b0d8dd5a613a Mon Sep 17 00:00:00 2001 From: Ben White Date: Wed, 7 Feb 2024 15:11:56 +0000 Subject: [PATCH 13/26] feat: Add dynamic routing of ingestion endpoints (#986) --- src/__tests__/decide.js | 2 + .../exception-observer.test.ts | 2 + .../replay/sessionrecording.test.ts | 10 ++- src/__tests__/extensions/toolbar.test.ts | 3 + src/__tests__/featureflags.js | 14 ++-- src/__tests__/posthog-core.js | 25 +++--- src/__tests__/posthog-core.loaded.js | 6 +- src/__tests__/surveys.test.ts | 3 + src/__tests__/utils/request-router.test.ts | 79 +++++++++++++++++++ src/decide.ts | 12 +-- src/extensions/cloud.ts | 1 - src/extensions/exception-autocapture/index.ts | 5 +- src/extensions/replay/sessionrecording.ts | 17 ++-- src/extensions/sentry-integration.ts | 6 +- src/extensions/toolbar.ts | 12 +-- src/posthog-core.ts | 19 +++-- src/posthog-featureflags.ts | 7 +- src/posthog-surveys.ts | 2 +- src/types.ts | 5 +- src/utils/request-router.ts | 71 +++++++++++++++++ 20 files changed, 230 insertions(+), 71 deletions(-) create mode 100644 src/__tests__/utils/request-router.test.ts delete mode 100644 src/extensions/cloud.ts create mode 100644 src/utils/request-router.ts diff --git a/src/__tests__/decide.js b/src/__tests__/decide.js index 1b04482c7..ad1562765 100644 --- a/src/__tests__/decide.js +++ b/src/__tests__/decide.js @@ -2,6 +2,7 @@ import { autocapture } from '../autocapture' import { Decide } from '../decide' import { _base64Encode } from '../utils' import { PostHogPersistence } from '../posthog-persistence' +import { RequestRouter } from '../utils/request-router' const expectDecodedSendRequest = (send_request, data) => { const lastCall = send_request.mock.calls[send_request.mock.calls.length - 1] @@ -49,6 +50,7 @@ describe('Decide', () => { setReloadingPaused: jest.fn(), _startReloadTimer: jest.fn(), }, + requestRouter: new RequestRouter({ config: given.config }), _hasBootstrappedFeatureFlags: jest.fn(), getGroups: () => ({ organization: '5' }), })) diff --git a/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts index e2bd50be9..53a86766b 100644 --- a/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts +++ b/src/__tests__/extensions/exception-autocapture/exception-observer.test.ts @@ -3,6 +3,7 @@ import { PostHog } from '../../../posthog-core' import { DecideResponse, PostHogConfig } from '../../../types' import { ExceptionObserver } from '../../../extensions/exception-autocapture' import { window } from '../../../utils/globals' +import { RequestRouter } from '../../../utils/request-router' describe('Exception Observer', () => { let exceptionObserver: ExceptionObserver @@ -17,6 +18,7 @@ describe('Exception Observer', () => { config: mockConfig, get_distinct_id: jest.fn(() => 'mock-distinct-id'), capture: mockCapture, + requestRouter: new RequestRouter({ config: mockConfig } as any), } exceptionObserver = new ExceptionObserver(mockPostHogInstance as PostHog) }) diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts index f79796d29..c76fb3fdc 100644 --- a/src/__tests__/extensions/replay/sessionrecording.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording.test.ts @@ -24,6 +24,7 @@ import { SessionRecording, } from '../../../extensions/replay/sessionrecording' import { assignableWindow } from '../../../utils/globals' +import { RequestRouter } from '../../../utils/request-router' // Type and source defined here designate a non-user-generated recording event @@ -117,6 +118,7 @@ describe('SessionRecording', () => { onFeatureFlagsCallback = cb }, sessionManager: sessionManager, + requestRouter: new RequestRouter({ config } as any), _addCaptureHook: jest.fn(), } as unknown as PostHog @@ -537,7 +539,7 @@ describe('SessionRecording', () => { }, { method: 'POST', - endpoint: '/s/', + _url: 'https://test.com/s/', _noTruncate: true, _batchKey: 'recordings', _metrics: expect.anything(), @@ -574,7 +576,7 @@ describe('SessionRecording', () => { }, { method: 'POST', - endpoint: '/s/', + _url: 'https://test.com/s/', _noTruncate: true, _batchKey: 'recordings', _metrics: expect.anything(), @@ -658,7 +660,7 @@ describe('SessionRecording', () => { }, { method: 'POST', - endpoint: '/s/', + _url: 'https://test.com/s/', _noTruncate: true, _batchKey: 'recordings', _metrics: expect.anything(), @@ -1283,7 +1285,7 @@ describe('SessionRecording', () => { _batchKey: 'recordings', _metrics: { rrweb_full_snapshot: false }, _noTruncate: true, - endpoint: '/s/', + _url: 'https://test.com/s/', method: 'POST', } ) diff --git a/src/__tests__/extensions/toolbar.test.ts b/src/__tests__/extensions/toolbar.test.ts index c938cdaaa..c5cd49cfe 100644 --- a/src/__tests__/extensions/toolbar.test.ts +++ b/src/__tests__/extensions/toolbar.test.ts @@ -3,6 +3,7 @@ import { _isString, _isUndefined } from '../../utils/type-utils' import { PostHog } from '../../posthog-core' import { PostHogConfig, ToolbarParams } from '../../types' import { assignableWindow, window } from '../../utils/globals' +import { RequestRouter } from '../../utils/request-router' jest.mock('../../utils', () => ({ ...jest.requireActual('../../utils'), @@ -25,6 +26,8 @@ describe('Toolbar', () => { api_host: 'http://api.example.com', token: 'test_token', } as unknown as PostHogConfig, + requestRouter: new RequestRouter(instance), + set_config: jest.fn(), } as unknown as PostHog toolbar = new Toolbar(instance) diff --git a/src/__tests__/featureflags.js b/src/__tests__/featureflags.js index 061de4b74..825a97b30 100644 --- a/src/__tests__/featureflags.js +++ b/src/__tests__/featureflags.js @@ -2,6 +2,7 @@ import { PostHogFeatureFlags, parseFeatureFlagDecideResponse, filterActiveFeatureFlags } from '../posthog-featureflags' import { PostHogPersistence } from '../posthog-persistence' +import { RequestRouter } from '../utils/request-router' jest.useFakeTimers() jest.spyOn(global, 'setTimeout') @@ -12,6 +13,7 @@ describe('featureflags', () => { const config = { token: 'random fake token', persistence: 'memory', + api_host: 'https://app.posthog.com', } given('instance', () => ({ config, @@ -19,6 +21,7 @@ describe('featureflags', () => { getGroups: () => {}, _prepare_callback: (callback) => callback, persistence: new PostHogPersistence(config), + requestRouter: new RequestRouter({ config }), register: (props) => given.instance.persistence.register(props), unregister: (key) => given.instance.persistence.unregister(key), get_property: (key) => given.instance.persistence.props[key], @@ -320,20 +323,13 @@ describe('featureflags', () => { earlyAccessFeatures: [EARLY_ACCESS_FEATURE_FIRST], })) - beforeEach(() => { - given.instance.config = { - ...given.instance.config, - api_host: 'https://decide.com', - } - }) - it('getEarlyAccessFeatures requests early access features if not present', () => { given.featureFlags.getEarlyAccessFeatures((data) => { expect(data).toEqual([EARLY_ACCESS_FEATURE_FIRST]) }) expect(given.instance._send_request).toHaveBeenCalledWith( - 'https://decide.com/api/early_access_features/?token=random fake token', + 'https://app.posthog.com/api/early_access_features/?token=random fake token', {}, { method: 'GET' }, expect.any(Function) @@ -359,7 +355,7 @@ describe('featureflags', () => { }) expect(given.instance._send_request).toHaveBeenCalledWith( - 'https://decide.com/api/early_access_features/?token=random fake token', + 'https://app.posthog.com/api/early_access_features/?token=random fake token', {}, { method: 'GET' }, expect.any(Function) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 9408874bb..c3bd28abd 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -39,7 +39,10 @@ describe('posthog core', () => { given('overrides', () => ({ __loaded: true, - config: given.config, + config: { + api_host: 'https://app.posthog.com', + ...given.config, + }, persistence: { remove_event_timer: jest.fn(), properties: jest.fn(), @@ -254,7 +257,7 @@ describe('posthog core', () => { }) it('sends payloads to overriden endpoint if given', () => { - given.lib.capture('event-name', { foo: 'bar', length: 0 }, { endpoint: '/s/' }) + given.lib.capture('event-name', { foo: 'bar', length: 0 }, { _url: 'https://app.posthog.com/s/' }) expect(given.lib._send_request).toHaveBeenCalledWith( 'https://app.posthog.com/s/', expect.any(Object), @@ -263,9 +266,9 @@ describe('posthog core', () => { ) }) - it('sends payloads to overriden endpoint, even if alternative endpoint is set', () => { + it('sends payloads to overriden _url, even if alternative endpoint is set', () => { given.lib._afterDecideResponse({ analytics: { endpoint: '/i/v0/e/' } }) - given.lib.capture('event-name', { foo: 'bar', length: 0 }, { endpoint: '/s/' }) + given.lib.capture('event-name', { foo: 'bar', length: 0 }, { _url: 'https://app.posthog.com/s/' }) expect(given.lib._send_request).toHaveBeenCalledWith( 'https://app.posthog.com/s/', expect.any(Object), @@ -741,15 +744,6 @@ describe('posthog core', () => { expect(given.overrides._send_request.mock.calls.length).toBe(0) // No outgoing requests }) - it('sanitizes api_host urls', () => { - given('config', () => ({ - api_host: 'https://example.com/custom/', - })) - given.subject() - - expect(given.lib.config.api_host).toBe('https://example.com/custom') - }) - it('does not set __loaded_recorder_version flag if recording script has not been included', () => { given('overrides', () => ({ __loaded_recorder_version: undefined, @@ -979,7 +973,10 @@ describe('posthog core', () => { describe('subsequent capture calls', () => { given('overrides', () => ({ __loaded: true, - config: given.config, + config: { + api_host: 'https://app.posthog.com', + ...given.config, + }, persistence: new PostHogPersistence(given.config), sessionPersistence: new PostHogPersistence(given.config), _requestQueue: { diff --git a/src/__tests__/posthog-core.loaded.js b/src/__tests__/posthog-core.loaded.js index 99f7da655..e96e8f9e8 100644 --- a/src/__tests__/posthog-core.loaded.js +++ b/src/__tests__/posthog-core.loaded.js @@ -1,5 +1,6 @@ import { PostHog } from '../posthog-core' import { PostHogPersistence } from '../posthog-persistence' +import { RequestRouter } from '../utils/request-router' jest.useFakeTimers() @@ -21,11 +22,12 @@ describe('loaded() with flags', () => { _startReloadTimer: jest.fn(), receivedFeatureFlags: jest.fn(), }, + requestRouter: new RequestRouter({ config: given.config }), _start_queue_if_opted_in: jest.fn(), persistence: new PostHogPersistence(given.config), _send_request: jest.fn((host, data, header, callback) => callback({ status: 200 })), })) - given('config', () => ({ loaded: jest.fn(), persistence: 'memory' })) + given('config', () => ({ loaded: jest.fn(), persistence: 'memory', api_host: 'https://app.posthog.com' })) describe('toggling flag reloading', () => { given('config', () => ({ @@ -36,6 +38,7 @@ describe('loaded() with flags', () => { }, 100) }, persistence: 'memory', + api_host: 'https://app.posthog.com', })) given('overrides', () => ({ @@ -44,6 +47,7 @@ describe('loaded() with flags', () => { _send_request: jest.fn((host, data, header, callback) => setTimeout(() => callback({ status: 200 }), 1000)), _start_queue_if_opted_in: jest.fn(), persistence: new PostHogPersistence(given.config), + requestRouter: new RequestRouter({ config: given.config }), })) beforeEach(() => { diff --git a/src/__tests__/surveys.test.ts b/src/__tests__/surveys.test.ts index 8f0896bb1..484ca17c2 100644 --- a/src/__tests__/surveys.test.ts +++ b/src/__tests__/surveys.test.ts @@ -5,6 +5,8 @@ import { SurveyType, SurveyQuestionType, Survey } from '../posthog-surveys-types import { PostHogPersistence } from '../posthog-persistence' import { PostHog } from '../posthog-core' import { DecideResponse, PostHogConfig, Properties } from '../types' +import { window } from '../utils/globals' +import { RequestRouter } from '../utils/request-router' import { assignableWindow } from '../utils/globals' describe('surveys', () => { @@ -60,6 +62,7 @@ describe('surveys', () => { config: config, _prepare_callback: (callback: any) => callback, persistence: new PostHogPersistence(config), + requestRouter: new RequestRouter({ config } as any), register: (props: Properties) => instance.persistence?.register(props), unregister: (key: string) => instance.persistence?.unregister(key), get_property: (key: string) => instance.persistence?.props[key], diff --git a/src/__tests__/utils/request-router.test.ts b/src/__tests__/utils/request-router.test.ts new file mode 100644 index 000000000..2bff23418 --- /dev/null +++ b/src/__tests__/utils/request-router.test.ts @@ -0,0 +1,79 @@ +import { RequestRouter, RequestRouterTarget } from '../../utils/request-router' + +describe('request-router', () => { + const router = (api_host = 'https://app.posthog.com', ui_host?: string) => { + return new RequestRouter({ + config: { + api_host, + ui_host, + __preview_ingestion_endpoints: true, + }, + } as any) + } + + const testCases: [string, RequestRouterTarget, string][] = [ + // US domain + ['https://app.posthog.com', 'ui', 'https://app.posthog.com'], + ['https://app.posthog.com', 'capture_events', 'https://us-c.i.posthog.com'], + ['https://app.posthog.com', 'capture_recordings', 'https://us-s.i.posthog.com'], + ['https://app.posthog.com', 'decide', 'https://us-d.i.posthog.com'], + ['https://app.posthog.com', 'assets', 'https://us-assets.i.posthog.com'], + ['https://app.posthog.com', 'api', 'https://us-api.i.posthog.com'], + // US domain via app domain + ['https://us.posthog.com', 'ui', 'https://us.posthog.com'], + ['https://us.posthog.com', 'capture_events', 'https://us-c.i.posthog.com'], + ['https://us.posthog.com', 'capture_recordings', 'https://us-s.i.posthog.com'], + ['https://us.posthog.com', 'decide', 'https://us-d.i.posthog.com'], + ['https://us.posthog.com', 'assets', 'https://us-assets.i.posthog.com'], + ['https://us.posthog.com', 'api', 'https://us-api.i.posthog.com'], + + // EU domain + ['https://eu.posthog.com', 'ui', 'https://eu.posthog.com'], + ['https://eu.posthog.com', 'capture_events', 'https://eu-c.i.posthog.com'], + ['https://eu.posthog.com', 'capture_recordings', 'https://eu-s.i.posthog.com'], + ['https://eu.posthog.com', 'decide', 'https://eu-d.i.posthog.com'], + ['https://eu.posthog.com', 'assets', 'https://eu-assets.i.posthog.com'], + ['https://eu.posthog.com', 'api', 'https://eu-api.i.posthog.com'], + + // custom domain + ['https://my-custom-domain.com', 'ui', 'https://my-custom-domain.com'], + ['https://my-custom-domain.com', 'capture_events', 'https://my-custom-domain.com'], + ['https://my-custom-domain.com', 'capture_recordings', 'https://my-custom-domain.com'], + ['https://my-custom-domain.com', 'decide', 'https://my-custom-domain.com'], + ['https://my-custom-domain.com', 'assets', 'https://my-custom-domain.com'], + ['https://my-custom-domain.com', 'api', 'https://my-custom-domain.com'], + ] + + it.each(testCases)( + 'should create the appropriate endpoints for host %s and target %s', + (host, target, expectation) => { + expect(router(host).endpointFor(target)).toEqual(expectation) + } + ) + + it('should sanitize the api_host values', () => { + expect(router('https://app.posthog.com/').endpointFor('decide', '/decide?v=3')).toEqual( + 'https://us-d.i.posthog.com/decide?v=3' + ) + + expect(router('https://example.com/').endpointFor('decide', '/decide?v=3')).toEqual( + 'https://example.com/decide?v=3' + ) + }) + + it('should use the ui_host if provided', () => { + expect(router('https://my.domain.com/', 'https://app.posthog.com/').endpointFor('ui')).toEqual( + 'https://app.posthog.com' + ) + }) + + it('should react to config changes', () => { + const mockPostHog = { config: { api_host: 'https://app.posthog.com', __preview_ingestion_endpoints: true } } + + const router = new RequestRouter(mockPostHog as any) + expect(router.endpointFor('capture_events')).toEqual('https://us-c.i.posthog.com') + + mockPostHog.config.api_host = 'https://eu.posthog.com' + expect(router.endpointFor('capture_events')).toEqual('https://eu-c.i.posthog.com') + }) +}) diff --git a/src/decide.ts b/src/decide.ts index 8da838f00..280b6e449 100644 --- a/src/decide.ts +++ b/src/decide.ts @@ -35,7 +35,7 @@ export class Decide { const encoded_data = _base64Encode(json_data) this.instance._send_request( - `${this.instance.config.api_host}/decide/?v=3`, + this.instance.requestRouter.endpointFor('decide', '/decide/?v=3'), { data: encoded_data, verbose: true }, { method: 'POST' }, (response) => this.parseDecideResponse(response as DecideResponse) @@ -76,7 +76,7 @@ export class Decide { const surveysGenerator = window?.extendPostHogWithSurveys if (response['surveys'] && !surveysGenerator) { - loadScript(this.instance.config.api_host + `/static/surveys.js`, (err) => { + loadScript(this.instance.requestRouter.endpointFor('assets', '/static/surveys.js'), (err) => { if (err) { return logger.error(`Could not load surveys script`, err) } @@ -95,7 +95,7 @@ export class Decide { !!response['autocaptureExceptions'] && _isUndefined(exceptionAutoCaptureAddedToWindow) ) { - loadScript(this.instance.config.api_host + `/static/exception-autocapture.js`, (err) => { + loadScript(this.instance.requestRouter.endpointFor('assets', '/static/exception-autocapture.js'), (err) => { if (err) { return logger.error(`Could not load exception autocapture script`, err) } @@ -108,12 +108,8 @@ export class Decide { if (response['siteApps']) { if (this.instance.config.opt_in_site_apps) { - const apiHost = this.instance.config.api_host for (const { id, url } of response['siteApps']) { - const scriptUrl = [ - apiHost, - apiHost[apiHost.length - 1] === '/' && url[0] === '/' ? url.substring(1) : url, - ].join('') + const scriptUrl = this.instance.requestRouter.endpointFor('assets', url) assignableWindow[`__$$ph_site_app_${id}`] = this.instance diff --git a/src/extensions/cloud.ts b/src/extensions/cloud.ts deleted file mode 100644 index fbaf9b831..000000000 --- a/src/extensions/cloud.ts +++ /dev/null @@ -1 +0,0 @@ -export const POSTHOG_MANAGED_HOSTS = ['https://app.posthog.com', 'https://eu.posthog.com'] diff --git a/src/extensions/exception-autocapture/index.ts b/src/extensions/exception-autocapture/index.ts index 17a97e6ce..f97151ad5 100644 --- a/src/extensions/exception-autocapture/index.ts +++ b/src/extensions/exception-autocapture/index.ts @@ -7,8 +7,6 @@ import { isPrimitive } from './type-checking' import { _isArray, _isObject, _isUndefined } from '../../utils/type-utils' import { logger } from '../../utils/logger' -const EXCEPTION_INGESTION_ENDPOINT = '/e/' - export const extendPostHog = (instance: PostHog, response: DecideResponse) => { const exceptionObserver = new ExceptionObserver(instance) exceptionObserver.afterDecideResponse(response) @@ -129,7 +127,7 @@ export class ExceptionObserver { const propertiesToSend = { ...properties, ...errorProperties } - const posthogHost = this.instance.config.ui_host || this.instance.config.api_host + const posthogHost = this.instance.requestRouter.endpointFor('ui') errorProperties.$exception_personURL = posthogHost + '/person/' + this.instance.get_distinct_id() this.sendExceptionEvent(propertiesToSend) @@ -141,7 +139,6 @@ export class ExceptionObserver { sendExceptionEvent(properties: { [key: string]: any }) { this.instance.capture('$exception', properties, { method: 'POST', - endpoint: EXCEPTION_INGESTION_ENDPOINT, _noTruncate: true, _batchKey: 'exceptionEvent', }) diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index e6be439c2..5310f277b 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -412,13 +412,16 @@ export class SessionRecording { // imported) or matches the requested recorder version, don't load script. Otherwise, remotely import // recorder.js from cdn since it hasn't been loaded. if (this.instance.__loaded_recorder_version !== this.recordingVersion) { - loadScript(this.instance.config.api_host + `/static/${recorderJS}?v=${Config.LIB_VERSION}`, (err) => { - if (err) { - return logger.error(`Could not load ${recorderJS}`, err) - } + loadScript( + this.instance.requestRouter.endpointFor('assets', `/static/${recorderJS}?v=${Config.LIB_VERSION}`), + (err) => { + if (err) { + return logger.error(`Could not load ${recorderJS}`, err) + } - this._onScriptLoaded() - }) + this._onScriptLoaded() + } + ) } else { this._onScriptLoaded() } @@ -835,7 +838,7 @@ export class SessionRecording { // :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings. this.instance.capture('$snapshot', properties, { method: 'POST', - endpoint: this._endpoint, + _url: this.instance.requestRouter.endpointFor('capture_recordings', this._endpoint), _noTruncate: true, _batchKey: SESSION_RECORDING_BATCH_KEY, _metrics: { diff --git a/src/extensions/sentry-integration.ts b/src/extensions/sentry-integration.ts index 914cf7a55..73d160c01 100644 --- a/src/extensions/sentry-integration.ts +++ b/src/extensions/sentry-integration.ts @@ -64,8 +64,8 @@ export class SentryIntegration implements _SentryIntegration { if (event.level !== 'error' || !_posthog.__loaded) return event if (!event.tags) event.tags = {} - const host = _posthog.config.ui_host || _posthog.config.api_host - event.tags['PostHog Person URL'] = host + '/person/' + _posthog.get_distinct_id() + const personUrl = _posthog.requestRouter.endpointFor('ui', '/person/' + _posthog.get_distinct_id()) + event.tags['PostHog Person URL'] = personUrl if (_posthog.sessionRecordingStarted()) { event.tags['PostHog Recording URL'] = _posthog.get_session_replay_url({ withTimestamp: true }) } @@ -82,7 +82,7 @@ export class SentryIntegration implements _SentryIntegration { // PostHog Exception Properties, $exception_message: exceptions[0]?.value, $exception_type: exceptions[0]?.type, - $exception_personURL: host + '/person/' + _posthog.get_distinct_id(), + $exception_personURL: personUrl, // Sentry Exception Properties $sentry_event_id: event.event_id, $sentry_exception: event.exception, diff --git a/src/extensions/toolbar.ts b/src/extensions/toolbar.ts index 4aa6be883..1f9e97358 100644 --- a/src/extensions/toolbar.ts +++ b/src/extensions/toolbar.ts @@ -1,7 +1,6 @@ import { _register_event, _try, loadScript } from '../utils' import { PostHog } from '../posthog-core' import { DecideResponse, ToolbarParams } from '../types' -import { POSTHOG_MANAGED_HOSTS } from './cloud' import { _getHashParam } from '../utils/request-utils' import { logger } from '../utils/logger' import { window, document, assignableWindow } from '../utils/globals' @@ -126,21 +125,22 @@ export class Toolbar { // only load the toolbar once, even if there are multiple instances of PostHogLib assignableWindow['_postHogToolbarLoaded'] = true - const host = this.instance.config.api_host // toolbar.js is served from the PostHog CDN, this has a TTL of 24 hours. // the toolbar asset includes a rotating "token" that is valid for 5 minutes. const fiveMinutesInMillis = 5 * 60 * 1000 // this ensures that we bust the cache periodically const timestampToNearestFiveMinutes = Math.floor(Date.now() / fiveMinutesInMillis) * fiveMinutesInMillis - const toolbarUrl = `${host}${host.endsWith('/') ? '' : '/'}static/toolbar.js?t=${timestampToNearestFiveMinutes}` + const toolbarUrl = this.instance.requestRouter.endpointFor( + 'assets', + `/static/toolbar.js?t=${timestampToNearestFiveMinutes}` + ) const disableToolbarMetrics = - !POSTHOG_MANAGED_HOSTS.includes(this.instance.config.api_host) && - this.instance.config.advanced_disable_toolbar_metrics + this.instance.requestRouter.region === 'custom' && this.instance.config.advanced_disable_toolbar_metrics const toolbarParams = { token: this.instance.config.token, ...params, - apiURL: host, // defaults to api_host from the instance config if nothing else set + apiURL: this.instance.requestRouter.endpointFor('api'), // defaults to api_host from the instance config if nothing else set ...(disableToolbarMetrics ? { instrument: false } : {}), } diff --git a/src/posthog-core.ts b/src/posthog-core.ts index ce30edf80..dcf8d9d95 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -24,6 +24,7 @@ import { compressData, decideCompression } from './compression' import { addParamsToURL, encodePostData, request } from './send-request' import { RetryQueue } from './retry-queue' import { SessionIdManager } from './sessionid' +import { RequestRouter } from './utils/request-router' import { AutocaptureConfig, CaptureOptions, @@ -292,6 +293,7 @@ export class PostHog { sessionPersistence?: PostHogPersistence sessionManager?: SessionIdManager sessionPropsManager?: SessionPropsManager + requestRouter: RequestRouter _requestQueue?: RequestQueue _retryQueue?: RetryQueue @@ -337,6 +339,7 @@ export class PostHog { this.pageViewManager = new PageViewManager(this) this.surveys = new PostHogSurveys(this) this.rateLimiter = new RateLimiter() + this.requestRouter = new RequestRouter(this) // NOTE: See the property definition for deprecation notice this.people = { @@ -564,6 +567,10 @@ export class PostHog { if (response.elementsChainAsString) { this.elementsChainAsString = response.elementsChainAsString } + + if (response.__preview_ingestion_endpoints) { + this.config.__preview_ingestion_endpoints = response.__preview_ingestion_endpoints + } } _loaded(): void { @@ -920,7 +927,7 @@ export class PostHog { logger.info('send', data) const jsonData = JSON.stringify(data) - const url = this.config.api_host + (options.endpoint || this.analyticsDefaultEndpoint) + const url = options._url ?? this.requestRouter.endpointFor('capture_events', this.analyticsDefaultEndpoint) const has_unique_traits = options !== __NOOPTIONS @@ -1545,9 +1552,8 @@ export class PostHog { if (!this.sessionManager) { return '' } - const host = this.config.ui_host || this.config.api_host const { sessionId, sessionStartTimestamp } = this.sessionManager.checkAndGetSessionAndWindowId(true) - let url = host + '/replay/' + sessionId + let url = this.requestRouter.endpointFor('ui', '/replay/' + sessionId) if (options?.withTimestamp && sessionStartTimestamp) { const LOOK_BACK = options.timestampLookBack ?? 10 if (!sessionStartTimestamp) { @@ -1754,13 +1760,6 @@ export class PostHog { this.config.disable_persistence = this.config.disable_cookie } - // We assume the api_host is without a trailing slash in most places throughout the codebase - this.config.api_host = this.config.api_host.replace(/\/$/, '') - - // us.posthog.com is only for the web app, so we don't allow that to be used as a capture endpoint - if (this.config.api_host === 'https://us.posthog.com') { - this.config.api_host = 'https://app.posthog.com' - } this.persistence?.update_config(this.config) this.sessionPersistence?.update_config(this.config) diff --git a/src/posthog-featureflags.ts b/src/posthog-featureflags.ts index 924134252..24418428c 100644 --- a/src/posthog-featureflags.ts +++ b/src/posthog-featureflags.ts @@ -190,7 +190,7 @@ export class PostHogFeatureFlags { const encoded_data = _base64Encode(json_data) this.instance._send_request( - this.instance.config.api_host + '/decide/?v=3', + this.instance.requestRouter.endpointFor('decide', '/decide/?v=3'), { data: encoded_data }, { method: 'POST' }, this.instance._prepare_callback((response) => { @@ -357,7 +357,10 @@ export class PostHogFeatureFlags { if (!existing_early_access_features || force_reload) { this.instance._send_request( - `${this.instance.config.api_host}/api/early_access_features/?token=${this.instance.config.token}`, + this.instance.requestRouter.endpointFor( + 'api', + `/api/early_access_features/?token=${this.instance.config.token}` + ), {}, { method: 'GET' }, (response) => { diff --git a/src/posthog-surveys.ts b/src/posthog-surveys.ts index 3ac9de094..73b5b1f9c 100644 --- a/src/posthog-surveys.ts +++ b/src/posthog-surveys.ts @@ -22,7 +22,7 @@ export class PostHogSurveys { const existingSurveys = this.instance.get_property(SURVEYS) if (!existingSurveys || forceReload) { this.instance._send_request( - `${this.instance.config.api_host}/api/surveys/?token=${this.instance.config.token}`, + this.instance.requestRouter.endpointFor('api', `/api/surveys/?token=${this.instance.config.token}`), {}, { method: 'GET' }, (response) => { diff --git a/src/types.ts b/src/types.ts index 3ef5cd638..f97f22a1a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -142,6 +142,8 @@ export interface PostHogConfig { disable_scroll_properties?: boolean // Let the pageview scroll stats use a custom css selector for the root element, e.g. `main` scroll_root_selector?: string | string[] + /** WARNING: This is an experimental option not meant for public use. */ + __preview_ingestion_endpoints?: boolean } export interface OptInOutCapturingOptions { @@ -204,10 +206,10 @@ export interface XHROptions { export interface CaptureOptions extends XHROptions { $set?: Properties /** used with $identify */ $set_once?: Properties /** used with $identify */ + _url?: string /** Used to override the desired endpoint for the captured event */ _batchKey?: string /** key of queue, e.g. 'sessionRecording' vs 'event' */ _metrics?: Properties _noTruncate?: boolean /** if set, overrides and disables config.properties_string_max_length */ - endpoint?: string /** defaults to '/e/' */ send_instantly?: boolean /** if set skips the batched queue */ timestamp?: Date } @@ -281,6 +283,7 @@ export interface DecideResponse { toolbarVersion: 'toolbar' /** @deprecated, moved to toolbarParams */ isAuthenticated: boolean siteApps: { id: number; url: string }[] + __preview_ingestion_endpoints?: boolean } export type FeatureFlagsCallback = (flags: string[], variants: Record) => void diff --git a/src/utils/request-router.ts b/src/utils/request-router.ts new file mode 100644 index 000000000..1aaf31502 --- /dev/null +++ b/src/utils/request-router.ts @@ -0,0 +1,71 @@ +import { PostHog } from '../posthog-core' + +/** + * The request router helps simplify the logic to determine which endpoints should be called for which things + * The basic idea is that for a given region (US or EU), we have a set of endpoints that we should call depending + * on the type of request (events, replays, decide, etc.) and handle overrides that may come from configs or the decide endpoint + */ + +export enum RequestRouterRegion { + US = 'us', + EU = 'eu', + CUSTOM = 'custom', +} + +export type RequestRouterTarget = 'ui' | 'capture_events' | 'capture_recordings' | 'decide' | 'assets' | 'api' + +export class RequestRouter { + instance: PostHog + + constructor(instance: PostHog) { + this.instance = instance + } + + get apiHost(): string { + return this.instance.config.api_host.replace(/\/$/, '') + } + get uiHost(): string | undefined { + return this.instance.config.ui_host?.replace(/\/$/, '') + } + + get region(): RequestRouterRegion { + switch (this.apiHost) { + case 'https://app.posthog.com': + case 'https://us.posthog.com': + return RequestRouterRegion.US + case 'https://eu.posthog.com': + return RequestRouterRegion.EU + default: + return RequestRouterRegion.CUSTOM + } + } + + endpointFor(target: RequestRouterTarget, path: string = ''): string { + if (path) { + path = path[0] === '/' ? path : `/${path}` + } + + if (target === 'ui') { + return (this.uiHost || this.apiHost) + path + } + + if (!this.instance.config.__preview_ingestion_endpoints || this.region === RequestRouterRegion.CUSTOM) { + return this.apiHost + path + } + + const suffix = 'i.posthog.com' + path + + switch (target) { + case 'capture_events': + return `https://${this.region}-c.${suffix}` + case 'capture_recordings': + return `https://${this.region}-s.${suffix}` + case 'decide': + return `https://${this.region}-d.${suffix}` + case 'assets': + return `https://${this.region}-assets.${suffix}` + case 'api': + return `https://${this.region}-api.${suffix}` + } + } +} From cde7a8a469f5d13f1db555f6979a918adf01a944 Mon Sep 17 00:00:00 2001 From: benjackwhite Date: Wed, 7 Feb 2024 15:12:39 +0000 Subject: [PATCH 14/26] chore: Bump version to 1.105.4 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e16c71f4b..11fdb11ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.105.4 - 2024-02-07 + +- feat: Add dynamic routing of ingestion endpoints (#986) +- Update CHANGELOG.md (#1004) + ## 1.105.3 - 2024-02-07 identical to 1.105.1 - bug in CI scripts diff --git a/package.json b/package.json index 12c92e267..38f87b918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.105.3", + "version": "1.105.4", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 6236d48a1154ea411a71d82e993b35527a3348c3 Mon Sep 17 00:00:00 2001 From: David Newell Date: Thu, 8 Feb 2024 11:21:34 +0000 Subject: [PATCH 15/26] chore: improve template to account for backwards compatibility (#1007) --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9df249338..ba2df9d0b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,3 +5,4 @@ ## Checklist - [ ] Tests for new code (see [advice on the tests we use](https://github.com/PostHog/posthog-js#tiers-of-testing)) - [ ] Accounted for the impact of any changes across different browsers +- [ ] Accounted for backwards compatibility of any changes (no breaking changes in posthog-js!) From 0a48cd2f9ade9036166cd7dfc2101b2c90187860 Mon Sep 17 00:00:00 2001 From: David Newell Date: Thu, 8 Feb 2024 12:51:03 +0000 Subject: [PATCH 16/26] feat: account for persistence for canvas recording (#1006) --- playground/nextjs/package.json | 2 +- playground/nextjs/pages/canvas.tsx | 2 +- playground/nextjs/pnpm-lock.yaml | 13 +++++-- .../replay/sessionrecording.test.ts | 34 +++++++++++----- src/constants.ts | 1 + src/extensions/replay/sessionrecording.ts | 39 ++++++++++--------- 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/playground/nextjs/package.json b/playground/nextjs/package.json index 277cc23c4..248f09b3f 100644 --- a/playground/nextjs/package.json +++ b/playground/nextjs/package.json @@ -16,7 +16,7 @@ "eslint": "8.34.0", "eslint-config-next": "13.1.6", "next": "13.5.6", - "posthog-js": "^1.88.1", + "posthog-js": "^1.103.1", "react": "18.2.0", "react-dom": "18.2.0", "typescript": "4.9.5" diff --git a/playground/nextjs/pages/canvas.tsx b/playground/nextjs/pages/canvas.tsx index f1fdfa586..7c96cd0a8 100644 --- a/playground/nextjs/pages/canvas.tsx +++ b/playground/nextjs/pages/canvas.tsx @@ -2,7 +2,7 @@ import React from 'react' import Head from 'next/head' import { useEffect, useRef } from 'react' -export default function Home() { +export default function Canvas() { const ref = useRef(null) useEffect(() => { diff --git a/playground/nextjs/pnpm-lock.yaml b/playground/nextjs/pnpm-lock.yaml index c4473ad29..7aebcd79b 100644 --- a/playground/nextjs/pnpm-lock.yaml +++ b/playground/nextjs/pnpm-lock.yaml @@ -27,8 +27,8 @@ dependencies: specifier: 13.5.6 version: 13.5.6(react-dom@18.2.0)(react@18.2.0) posthog-js: - specifier: ^1.88.1 - version: 1.100.0 + specifier: ^1.103.1 + version: 1.105.4 react: specifier: 18.2.0 version: 18.2.0 @@ -1880,10 +1880,15 @@ packages: source-map-js: 1.0.2 dev: false - /posthog-js@1.100.0: - resolution: {integrity: sha512-r2XZEiHQ9mBK7D1G9k57I8uYZ2kZTAJ0OCX6K/OOdCWN8jKPhw3h5F9No5weilP6eVAn+hrsy7NvPV7SCX7gMg==} + /posthog-js@1.105.4: + resolution: {integrity: sha512-hazxQYi4nxSqktu0Hh1xCV+sJCpN8mp5E5Ei/cfEa2nsb13xQbzn81Lf3VIDA0xMU1mXxNRStntlY267eQVC/w==} dependencies: fflate: 0.4.8 + preact: 10.19.4 + dev: false + + /preact@10.19.4: + resolution: {integrity: sha512-dwaX5jAh0Ga8uENBX1hSOujmKWgx9RtL80KaKUFLc6jb4vCEAc3EeZ0rnQO/FO4VgjfPMfoLFWnNG8bHuZ9VLw==} dev: false /prelude-ls@1.2.1: diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts index c76fb3fdc..afc18094b 100644 --- a/src/__tests__/extensions/replay/sessionrecording.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording.test.ts @@ -7,6 +7,7 @@ import { SESSION_RECORDING_ENABLED_SERVER_SIDE, SESSION_RECORDING_IS_SAMPLED, SESSION_RECORDING_RECORDER_VERSION_SERVER_SIDE, + SESSION_RECORDING_CANVAS_RECORDING, } from '../../../constants' import { SessionIdManager } from '../../../sessionid' import { @@ -294,6 +295,22 @@ describe('SessionRecording', () => { expect(posthog.get_property(SESSION_RECORDING_ENABLED_SERVER_SIDE)).toBe(true) }) + it('stores true in persistence if canvas is enabled from the server', () => { + posthog.persistence?.register({ [SESSION_RECORDING_CANVAS_RECORDING]: undefined }) + + sessionRecording.afterDecideResponse( + makeDecideResponse({ + sessionRecording: { endpoint: '/s/', recordCanvas: true, canvasFps: 6, canvasQuality: '0.2' }, + }) + ) + + expect(posthog.get_property(SESSION_RECORDING_CANVAS_RECORDING)).toEqual({ + enabled: true, + fps: 6, + quality: '0.2', + }) + }) + it('stores false in persistence if recording is not enabled from the server', () => { posthog.persistence?.register({ [SESSION_RECORDING_ENABLED_SERVER_SIDE]: undefined }) @@ -424,16 +441,15 @@ describe('SessionRecording', () => { describe('canvas', () => { it('passes the remote config to rrweb', () => { - sessionRecording.startRecordingIfEnabled() + posthog.persistence?.register({ + [SESSION_RECORDING_CANVAS_RECORDING]: { + enabled: true, + fps: 6, + quality: 0.2, + }, + }) - sessionRecording.afterDecideResponse( - makeDecideResponse({ - sessionRecording: { endpoint: '/s/', recordCanvas: true, canvasFps: 6, canvasQuality: '0.2' }, - }) - ) - expect(sessionRecording['_recordCanvas']).toStrictEqual(true) - expect(sessionRecording['_canvasFps']).toStrictEqual(6) - expect(sessionRecording['_canvasQuality']).toStrictEqual(0.2) + sessionRecording.startRecordingIfEnabled() sessionRecording['_onScriptLoaded']() expect(assignableWindow.rrwebRecord).toHaveBeenCalledWith( diff --git a/src/constants.ts b/src/constants.ts index 7c0fda8de..8423864c3 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,6 +15,7 @@ export const SESSION_RECORDING_ENABLED_SERVER_SIDE = '$session_recording_enabled export const CONSOLE_LOG_RECORDING_ENABLED_SERVER_SIDE = '$console_log_recording_enabled_server_side' export const SESSION_RECORDING_RECORDER_VERSION_SERVER_SIDE = '$session_recording_recorder_version_server_side' // follows rrweb versioning export const SESSION_RECORDING_NETWORK_PAYLOAD_CAPTURE = '$session_recording_network_payload_capture' +export const SESSION_RECORDING_CANVAS_RECORDING = '$session_recording_canvas_recording' export const SESSION_ID = '$sesid' export const SESSION_RECORDING_IS_SAMPLED = '$session_is_sampled' export const ENABLED_FEATURE_FLAGS = '$enabled_feature_flags' diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index 5310f277b..240d40ccd 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -3,6 +3,7 @@ import { SESSION_RECORDING_ENABLED_SERVER_SIDE, SESSION_RECORDING_IS_SAMPLED, SESSION_RECORDING_NETWORK_PAYLOAD_CAPTURE, + SESSION_RECORDING_CANVAS_RECORDING, SESSION_RECORDING_RECORDER_VERSION_SERVER_SIDE, } from '../../constants' import { @@ -123,9 +124,6 @@ export class SessionRecording { private _linkedFlag: string | null = null private _sampleRate: number | null = null private _minimumDuration: number | null = null - private _recordCanvas: boolean = false - private _canvasFps: number | null = null - private _canvasQuality: number | null = null private _fullSnapshotTimer?: number @@ -172,6 +170,17 @@ export class SessionRecording { return enabled_client_side ?? enabled_server_side } + private get canvasRecording(): { enabled: boolean; fps: number; quality: number } | undefined { + const canvasRecording_server_side = this.instance.get_property(SESSION_RECORDING_CANVAS_RECORDING) + return canvasRecording_server_side && canvasRecording_server_side.fps && canvasRecording_server_side.quality + ? { + enabled: canvasRecording_server_side.enabled, + fps: canvasRecording_server_side.fps, + quality: canvasRecording_server_side.quality, + } + : undefined + } + private get recordingVersion() { const recordingVersion_server_side = this.instance.get_property(SESSION_RECORDING_RECORDER_VERSION_SERVER_SIDE) const recordingVersion_client_side = this.instance.config.session_recording?.recorderVersion @@ -318,6 +327,11 @@ export class SessionRecording { capturePerformance: response.capturePerformance, ...response.sessionRecording?.networkPayloadCapture, }, + [SESSION_RECORDING_CANVAS_RECORDING]: { + enabled: response.sessionRecording?.recordCanvas, + fps: response.sessionRecording?.canvasFps, + quality: response.sessionRecording?.canvasQuality, + }, }) } @@ -328,19 +342,6 @@ export class SessionRecording { const receivedMinimumDuration = response.sessionRecording?.minimumDurationMilliseconds this._minimumDuration = _isUndefined(receivedMinimumDuration) ? null : receivedMinimumDuration - const receivedRecordCanvas = response.sessionRecording?.recordCanvas - this._recordCanvas = - _isUndefined(receivedRecordCanvas) || _isNull(receivedRecordCanvas) ? false : receivedRecordCanvas - - const receivedCanvasFps = response.sessionRecording?.canvasFps - this._canvasFps = _isUndefined(receivedCanvasFps) ? null : receivedCanvasFps - - const receivedCanvasQuality = response.sessionRecording?.canvasQuality - this._canvasQuality = - _isUndefined(receivedCanvasQuality) || _isNull(receivedCanvasQuality) - ? null - : parseFloat(receivedCanvasQuality) - this._linkedFlag = response.sessionRecording?.linkedFlag || null if (response.sessionRecording?.endpoint) { @@ -549,10 +550,10 @@ export class SessionRecording { } } - if (this._recordCanvas && !_isNull(this._canvasFps) && !_isNull(this._canvasQuality)) { + if (this.canvasRecording && this.canvasRecording.enabled) { sessionRecordingOptions.recordCanvas = true - sessionRecordingOptions.sampling = { canvas: this._canvasFps } - sessionRecordingOptions.dataURLOptions = { type: 'image/webp', quality: this._canvasQuality } + sessionRecordingOptions.sampling = { canvas: this.canvasRecording.fps } + sessionRecordingOptions.dataURLOptions = { type: 'image/webp', quality: this.canvasRecording.quality } } if (!this.rrwebRecord) { From 3632db2037db97cda119bdeecdf25972a542b3f7 Mon Sep 17 00:00:00 2001 From: daibhin Date: Thu, 8 Feb 2024 12:51:39 +0000 Subject: [PATCH 17/26] chore: Bump version to 1.105.5 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11fdb11ef..4a0462b1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.105.5 - 2024-02-08 + +- feat: account for persistence for canvas recording (#1006) +- chore: improve template to account for backwards compatibility (#1007) + ## 1.105.4 - 2024-02-07 - feat: Add dynamic routing of ingestion endpoints (#986) diff --git a/package.json b/package.json index 38f87b918..0256edb07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.105.4", + "version": "1.105.5", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 33c65fba91861b6c99904da7dc9a9c59a06f6da0 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 8 Feb 2024 13:26:55 +0000 Subject: [PATCH 18/26] chore: test stopping and starting (#1009) --- cypress/e2e/session-recording.cy.ts | 81 ++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/cypress/e2e/session-recording.cy.ts b/cypress/e2e/session-recording.cy.ts index 6b033503f..e4dfbd2b6 100644 --- a/cypress/e2e/session-recording.cy.ts +++ b/cypress/e2e/session-recording.cy.ts @@ -3,6 +3,38 @@ import { _isNull } from '../../src/utils/type-utils' import { start } from '../support/setup' +function ensureRecordingIsStopped() { + cy.get('[data-cy-input]') + .type('hello posthog!') + .wait(250) + .then(() => { + cy.phCaptures({ full: true }).then((captures) => { + // should be no captured data + expect(captures.map((c) => c.event)).to.deep.equal([]) + }) + }) +} + +function ensureActivitySendsSnapshots() { + cy.get('[data-cy-input]') + .type('hello posthog!') + .wait('@session-recording') + .then(() => { + cy.phCaptures({ full: true }).then((captures) => { + expect(captures.map((c) => c.event)).to.deep.equal(['$snapshot']) + expect(captures[0]['properties']['$snapshot_data']).to.have.length.above(14).and.below(39) + // a meta and then a full snapshot + expect(captures[0]['properties']['$snapshot_data'][0].type).to.equal(4) // meta + expect(captures[0]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot + expect(captures[0]['properties']['$snapshot_data'][2].type).to.equal(5) // custom event with options + // Making a set from the rest should all be 3 - incremental snapshots + expect(new Set(captures[0]['properties']['$snapshot_data'].slice(3).map((s) => s.type))).to.deep.equal( + new Set([3]) + ) + }) + }) +} + describe('Session recording', () => { describe('array.full.js', () => { it('captures session events', () => { @@ -57,26 +89,39 @@ describe('Session recording', () => { }) it('captures session events', () => { + cy.phCaptures({ full: true }).then((captures) => { + // should be a pageview at the beginning + expect(captures.map((c) => c.event)).to.deep.equal(['$pageview']) + }) + cy.resetPhCaptures() + + let startingSessionId: string | null = null + cy.posthog().then((ph) => { + startingSessionId = ph.get_session_id() + }) + cy.get('[data-cy-input]').type('hello world! ') cy.wait(500) - cy.get('[data-cy-input]') - .type('hello posthog!') - .wait('@session-recording') - .then(() => { - cy.phCaptures({ full: true }).then((captures) => { - // should be a pageview and a $snapshot - expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$snapshot']) - expect(captures[1]['properties']['$snapshot_data']).to.have.length.above(33).and.below(38) - // a meta and then a full snapshot - expect(captures[1]['properties']['$snapshot_data'][0].type).to.equal(4) // meta - expect(captures[1]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot - expect(captures[1]['properties']['$snapshot_data'][2].type).to.equal(5) // custom event with options - // Making a set from the rest should all be 3 - incremental snapshots - expect( - new Set(captures[1]['properties']['$snapshot_data'].slice(3).map((s) => s.type)) - ).to.deep.equal(new Set([3])) - }) - }) + ensureActivitySendsSnapshots() + cy.posthog().then((ph) => { + ph.stopSessionRecording() + }) + cy.resetPhCaptures() + ensureRecordingIsStopped() + + // restarting recording + cy.posthog().then((ph) => { + ph.startSessionRecording() + }) + ensureActivitySendsSnapshots() + + // the session id is not rotated by stopping and starting the recording + cy.posthog().then((ph) => { + const secondSessionId = ph.get_session_id() + expect(startingSessionId).not.to.be.null + expect(secondSessionId).not.to.be.null + expect(secondSessionId).to.equal(startingSessionId) + }) }) it('captures snapshots when the mouse moves', () => { From eb49d9b9a32ae6acc47728d0bd4bfa80edf89450 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 8 Feb 2024 18:37:17 +0000 Subject: [PATCH 19/26] feat: save posthog config at start of session recording (#1005) * feat: save posthog config at start of session recording * fix tests --- cypress/e2e/session-recording.cy.ts | 12 +++++++----- src/extensions/replay/sessionrecording.ts | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/session-recording.cy.ts b/cypress/e2e/session-recording.cy.ts index e4dfbd2b6..d791200fb 100644 --- a/cypress/e2e/session-recording.cy.ts +++ b/cypress/e2e/session-recording.cy.ts @@ -27,8 +27,9 @@ function ensureActivitySendsSnapshots() { expect(captures[0]['properties']['$snapshot_data'][0].type).to.equal(4) // meta expect(captures[0]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot expect(captures[0]['properties']['$snapshot_data'][2].type).to.equal(5) // custom event with options + expect(captures[0]['properties']['$snapshot_data'][3].type).to.equal(5) // custom event with posthog config // Making a set from the rest should all be 3 - incremental snapshots - expect(new Set(captures[0]['properties']['$snapshot_data'].slice(3).map((s) => s.type))).to.deep.equal( + expect(new Set(captures[0]['properties']['$snapshot_data'].slice(4).map((s) => s.type))).to.deep.equal( new Set([3]) ) }) @@ -59,13 +60,14 @@ describe('Session recording', () => { // should be a pageview and a $snapshot expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$snapshot']) - expect(captures[1]['properties']['$snapshot_data']).to.have.length.above(33).and.below(38) + expect(captures[1]['properties']['$snapshot_data']).to.have.length.above(33).and.below(39) // a meta and then a full snapshot expect(captures[1]['properties']['$snapshot_data'][0].type).to.equal(4) // meta expect(captures[1]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot expect(captures[1]['properties']['$snapshot_data'][2].type).to.equal(5) // custom event with options + expect(captures[1]['properties']['$snapshot_data'][3].type).to.equal(5) // custom event with posthog config // Making a set from the rest should all be 3 - incremental snapshots - const incrementalSnapshots = captures[1]['properties']['$snapshot_data'].slice(3) + const incrementalSnapshots = captures[1]['properties']['$snapshot_data'].slice(4) expect(new Set(incrementalSnapshots.map((s) => s.type))).to.deep.equal(new Set([3])) }) }) @@ -240,9 +242,9 @@ describe('Session recording', () => { expect(captures[1]['properties']['$snapshot_data'][0].type).to.equal(4) // meta expect(captures[1]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot expect(captures[1]['properties']['$snapshot_data'][2].type).to.equal(5) // custom event with options - + expect(captures[1]['properties']['$snapshot_data'][3].type).to.equal(5) // custom event with posthog config const xPositions = [] - for (let i = 3; i < captures[1]['properties']['$snapshot_data'].length; i++) { + for (let i = 4; i < captures[1]['properties']['$snapshot_data'].length; i++) { expect(captures[1]['properties']['$snapshot_data'][i].type).to.equal(3) expect(captures[1]['properties']['$snapshot_data'][i].data.source).to.equal( 6, diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index 240d40ccd..352c614e4 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -616,6 +616,10 @@ export class SessionRecording { sessionRecordingOptions, activePlugins: activePlugins.map((p) => p?.name), }) + + this._tryAddCustomEvent('$posthog_config', { + config: this.instance.config, + }) } private _scheduleFullSnapshot(): void { From ae79aacf4ddbc7e0dfa41b22bb7544ceccbde9e3 Mon Sep 17 00:00:00 2001 From: pauldambra Date: Thu, 8 Feb 2024 18:38:12 +0000 Subject: [PATCH 20/26] chore: Bump version to 1.105.6 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a0462b1d..eeab9974c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.105.6 - 2024-02-08 + +- feat: save posthog config at start of session recording (#1005) +- chore: test stopping and starting (#1009) + ## 1.105.5 - 2024-02-08 - feat: account for persistence for canvas recording (#1006) diff --git a/package.json b/package.json index 0256edb07..194afb6cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.105.5", + "version": "1.105.6", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From a9f25e0de3e6d5be123ba32869dff953f40876f2 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 8 Feb 2024 22:35:24 +0000 Subject: [PATCH 21/26] chore: really really write changelog to release (#1008) --- .github/workflows/cd.yml | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index dea055bed..a9108b7b7 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -68,23 +68,31 @@ jobs: REDIS_URL: 'redis://localhost' NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Set LAST_CHANGELOG_ENTRY - id: set-last-changelog-entry + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # read from the first until the second header in the changelog file # this assumes the formatting of the file # and that this workflow is always running for the most recent entry in the file - awk -v defText='see CHANGELOG.md' '/^## /{if (flag) exit; flag=1} flag && /^##$/{exit} flag; END{if (!flag) print defText}' CHANGELOG.md - echo "LAST_CHANGELOG_ENTRY=$LAST_CHANGELOG_ENTRY" >> "$GITHUB_OUTPUT" - - - name: Create GitHub release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: v${{ env.COMMITTED_VERSION }} - release_name: ${{ env.COMMITTED_VERSION }} - body: ${{ steps.set-last-changelog-entry.outputs.LAST_CHANGELOG_ENTRY }} + LAST_CHANGELOG_ENTRY=$(awk -v defText="see CHANGELOG.md" '/^## /{if (flag) exit; flag=1} flag && /^##$/{exit} flag; END{if (!flag) print defText}' CHANGELOG.md) + # the action we used to use was archived, and made it really difficult to create a release with a body + # because the LAST_CHANGELOG_ENTRY contains bash special characters so passing it between steps + # was a pain. + # we can use the github cli to create a release with a body + # all as part of one step + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/posthog/posthog-js/releases \ + -f tag_name="${{ env.COMMITTED_VERSION }}" \ + -f target_commitish='main' \ + -f name="${{ env.COMMITTED_VERSION }}" \ + -f body="$LAST_CHANGELOG_ENTRY" \ + -F draft=false \ + -F prerelease=false \ + -F generate_release_notes=false create-pull-request: name: Create main repo PR with new posthog-js version From 30979cd23bf285f4f131c9a03773d2435e5bf2e8 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 9 Feb 2024 08:13:52 +0000 Subject: [PATCH 22/26] chore: add test case for ahrefs bot (#1011) * chore: add bot test case * fix nesting * parameterise tests --- src/__tests__/utils.test.ts | 76 +++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 9648eaf8a..92ab050a7 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -195,51 +195,45 @@ describe('utils', () => { new_script.onerror!('uh-oh') expect(callback).toHaveBeenCalledWith('uh-oh') }) + }) - describe('user agent blocking', () => { - it.each(DEFAULT_BLOCKED_UA_STRS.concat('testington'))( - 'blocks a bot based on the user agent %s', - (botString) => { - const randomisedUserAgent = userAgentFor(botString) - - expect(_isBlockedUA(randomisedUserAgent, ['testington'])).toBe(true) - } - ) - - it('should block googlebot desktop', () => { - expect( - _isBlockedUA( - 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36', - [] - ) - ).toBe(true) - }) + describe('user agent blocking', () => { + it.each(DEFAULT_BLOCKED_UA_STRS.concat('testington'))( + 'blocks a bot based on the user agent %s', + (botString) => { + const randomisedUserAgent = userAgentFor(botString) - it('should block openai bot', () => { - expect( - _isBlockedUA( - 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.0; +https://openai.com/gptbot)', - [] - ) - ).toBe(true) - }) + expect(_isBlockedUA(randomisedUserAgent, ['testington'])).toBe(true) + } + ) + + it.each([ + [ + 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36', + ], + ['AhrefsSiteAudit (Desktop) - Mozilla/5.0 (compatible; AhrefsSiteAudit/6.1; +http://ahrefs.com/robot/)'], + ['Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.0; +https://openai.com/gptbot)'], + ])('blocks based on user agent', (botString) => { + expect(_isBlockedUA(botString, [])).toBe(true) + expect(_isBlockedUA(botString.toLowerCase(), [])).toBe(true) + expect(_isBlockedUA(botString.toUpperCase(), [])).toBe(true) }) + }) - describe('check for cross domain cookies', () => { - it.each([ - [false, 'https://test.herokuapp.com'], - [false, 'test.herokuapp.com'], - [false, 'herokuapp.com'], - [false, undefined], - // ensure it isn't matching herokuapp anywhere in the domain - [true, 'https://test.herokuapp.com.impersonator.io'], - [true, 'mysite-herokuapp.com'], - [true, 'https://bbc.co.uk'], - [true, 'bbc.co.uk'], - [true, 'www.bbc.co.uk'], - ])('should return %s when hostname is %s', (expectedResult, hostname) => { - expect(isCrossDomainCookie({ hostname } as unknown as Location)).toEqual(expectedResult) - }) + describe('check for cross domain cookies', () => { + it.each([ + [false, 'https://test.herokuapp.com'], + [false, 'test.herokuapp.com'], + [false, 'herokuapp.com'], + [false, undefined], + // ensure it isn't matching herokuapp anywhere in the domain + [true, 'https://test.herokuapp.com.impersonator.io'], + [true, 'mysite-herokuapp.com'], + [true, 'https://bbc.co.uk'], + [true, 'bbc.co.uk'], + [true, 'www.bbc.co.uk'], + ])('should return %s when hostname is %s', (expectedResult, hostname) => { + expect(isCrossDomainCookie({ hostname } as unknown as Location)).toEqual(expectedResult) }) }) From 0a5ecc20eca4749c8dbbe5ac84c4e488a955fd09 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 11 Feb 2024 10:17:43 +0000 Subject: [PATCH 23/26] chore: no need to account for performance raw (#1012) --- src/utils/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/utils/index.ts b/src/utils/index.ts index bdcbee003..bd82200a7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -254,16 +254,11 @@ function deepCircularCopy = Record>( return internalDeepCircularCopy(value) } -const LONG_STRINGS_ALLOW_LIST = ['$performance_raw'] - export function _copyAndTruncateStrings = Record>( object: T, maxStringLength: number | null ): T { - return deepCircularCopy(object, (value: any, key) => { - if (key && LONG_STRINGS_ALLOW_LIST.indexOf(key as string) > -1) { - return value - } + return deepCircularCopy(object, (value: any) => { if (_isString(value) && !_isNull(maxStringLength)) { return (value as string).slice(0, maxStringLength) } From efb5bbd8badcaebfc958ea609479dc9e59a0bd39 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 11 Feb 2024 10:18:07 +0000 Subject: [PATCH 24/26] fix: allow custom events when idle (#1013) * fix: allow custom events when idle * fix --- .../replay/sessionrecording.test.ts | 60 ++++++++++++++++++- src/extensions/replay/sessionrecording.ts | 6 +- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts index afc18094b..1b730da91 100644 --- a/src/__tests__/extensions/replay/sessionrecording.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording.test.ts @@ -4,10 +4,10 @@ import { loadScript } from '../../../utils' import { PostHogPersistence } from '../../../posthog-persistence' import { CONSOLE_LOG_RECORDING_ENABLED_SERVER_SIDE, + SESSION_RECORDING_CANVAS_RECORDING, SESSION_RECORDING_ENABLED_SERVER_SIDE, SESSION_RECORDING_IS_SAMPLED, SESSION_RECORDING_RECORDER_VERSION_SERVER_SIDE, - SESSION_RECORDING_CANVAS_RECORDING, } from '../../../constants' import { SessionIdManager } from '../../../sessionid' import { @@ -18,7 +18,6 @@ import { import { PostHog } from '../../../posthog-core' import { DecideResponse, PostHogConfig, Property, SessionIdChangedCallback } from '../../../types' import { uuidv7 } from '../../../uuidv7' -import Mock = jest.Mock import { RECORDING_IDLE_ACTIVITY_TIMEOUT_MS, RECORDING_MAX_EVENT_SIZE, @@ -26,6 +25,8 @@ import { } from '../../../extensions/replay/sessionrecording' import { assignableWindow } from '../../../utils/globals' import { RequestRouter } from '../../../utils/request-router' +import { customEvent, EventType, eventWithTime, pluginEvent } from '@rrweb/types' +import Mock = jest.Mock // Type and source defined here designate a non-user-generated recording event @@ -56,6 +57,24 @@ const createIncrementalSnapshot = (event = {}) => ({ ...event, }) +const createCustomSnapshot = (event = {}): customEvent => ({ + type: EventType.Custom, + data: { + tag: 'custom', + payload: {}, + }, + ...event, +}) + +const createPluginSnapshot = (event = {}): pluginEvent => ({ + type: EventType.Plugin, + data: { + plugin: 'plugin', + payload: {}, + }, + ...event, +}) + function makeDecideResponse(partialResponse: Partial) { return partialResponse as unknown as DecideResponse } @@ -1120,6 +1139,43 @@ describe('SessionRecording', () => { _addCustomEvent.mockClear() }) + it('does not emit when idle', () => { + // force idle state + sessionRecording['isIdle'] = true + // buffer is empty + expect(sessionRecording['buffer']).toEqual(EMPTY_BUFFER) + // a plugin event doesn't count as returning from idle + sessionRecording.onRRwebEmit(createPluginSnapshot({}) as unknown as eventWithTime) + + // buffer is still empty + expect(sessionRecording['buffer']).toEqual(EMPTY_BUFFER) + }) + + it('emits custom events even when idle', () => { + // force idle state + sessionRecording['isIdle'] = true + // buffer is empty + expect(sessionRecording['buffer']).toEqual(EMPTY_BUFFER) + + sessionRecording.onRRwebEmit(createCustomSnapshot({}) as unknown as eventWithTime) + + // custom event is buffered + expect(sessionRecording['buffer']).toEqual({ + data: [ + { + data: { + payload: {}, + tag: 'custom', + }, + type: 5, + }, + ], + sessionId: null, + size: 47, + windowId: null, + }) + }) + it("enters idle state within one session if the activity is non-user generated and there's no activity for (RECORDING_IDLE_ACTIVITY_TIMEOUT_MS) 5 minutes", () => { const firstActivityTimestamp = startingTimestamp + 100 const secondActivityTimestamp = startingTimestamp + 200 diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index 352c614e4..03dab654b 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -1,9 +1,9 @@ import { CONSOLE_LOG_RECORDING_ENABLED_SERVER_SIDE, + SESSION_RECORDING_CANVAS_RECORDING, SESSION_RECORDING_ENABLED_SERVER_SIDE, SESSION_RECORDING_IS_SAMPLED, SESSION_RECORDING_NETWORK_PAYLOAD_CAPTURE, - SESSION_RECORDING_CANVAS_RECORDING, SESSION_RECORDING_RECORDER_VERSION_SERVER_SIDE, } from '../../constants' import { @@ -602,6 +602,7 @@ export class SessionRecording { return } this._tryAddCustomEvent('$pageview', { href }) + this._tryTakeFullSnapshot() } } catch (e) { logger.error('Could not add $pageview to rrweb session', e) @@ -690,7 +691,8 @@ export class SessionRecording { this._updateWindowAndSessionIds(event) - if (this.isIdle) { + // allow custom events even when idle + if (this.isIdle && event.type !== EventType.Custom) { // When in an idle state we keep recording, but don't capture the events return } From 7b3b6ecd1287a4fb6150fbace71fdb88152801b5 Mon Sep 17 00:00:00 2001 From: pauldambra Date: Sun, 11 Feb 2024 10:18:53 +0000 Subject: [PATCH 25/26] chore: Bump version to 1.105.7 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eeab9974c..4fe14ae2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.105.7 - 2024-02-11 + +- fix: allow custom events when idle (#1013) +- chore: no need to account for performance raw (#1012) +- chore: add test case for ahrefs bot (#1011) +- chore: really really write changelog to release (#1008) + ## 1.105.6 - 2024-02-08 - feat: save posthog config at start of session recording (#1005) diff --git a/package.json b/package.json index 194afb6cc..855bc7ac5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.105.6", + "version": "1.105.7", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 40888206a46a2418e944d2ff09664c86ede28ffd Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Mon, 12 Feb 2024 11:11:11 +0000 Subject: [PATCH 26/26] fix: for want of a v the war was lost (#1017) --- .github/workflows/cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a9108b7b7..d53ce17fb 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -86,7 +86,7 @@ jobs: -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ /repos/posthog/posthog-js/releases \ - -f tag_name="${{ env.COMMITTED_VERSION }}" \ + -f tag_name="v${{ env.COMMITTED_VERSION }}" \ -f target_commitish='main' \ -f name="${{ env.COMMITTED_VERSION }}" \ -f body="$LAST_CHANGELOG_ENTRY" \