From 482ab607e035bd982cef55ff0ed1860178029f75 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 17:17:29 +0100 Subject: [PATCH 01/29] step --- src/__tests__/decide.js | 179 +++++++++++++++++++++++----------------- 1 file changed, 103 insertions(+), 76 deletions(-) diff --git a/src/__tests__/decide.js b/src/__tests__/decide.js index 1c0bff886..6b249178b 100644 --- a/src/__tests__/decide.js +++ b/src/__tests__/decide.js @@ -3,14 +3,14 @@ import { PostHogPersistence } from '../posthog-persistence' import { RequestRouter } from '../utils/request-router' import { expectScriptToExist, expectScriptToNotExist } from './helpers/script-utils' -const expectDecodedSendRequest = (send_request, data, noCompression) => { +const expectDecodedSendRequest = (send_request, data, noCompression, posthog) => { const lastCall = send_request.mock.calls[send_request.mock.calls.length - 1] const decoded = lastCall[0].data // Helper to give us more accurate error messages expect(decoded).toEqual(data) - expect(given.posthog._send_request).toHaveBeenCalledWith({ + expect(posthog._send_request).toHaveBeenCalledWith({ url: 'https://test.com/decide/?v=3', data, method: 'POST', @@ -21,36 +21,39 @@ const expectDecodedSendRequest = (send_request, data, noCompression) => { } describe('Decide', () => { - given('decide', () => new Decide(given.posthog)) - given('posthog', () => ({ - config: given.config, - persistence: new PostHogPersistence(given.config), - register: (props) => given.posthog.persistence.register(props), - unregister: (key) => given.posthog.persistence.unregister(key), - get_property: (key) => given.posthog.persistence.props[key], - capture: jest.fn(), - _addCaptureHook: jest.fn(), - _afterDecideResponse: jest.fn(), - get_distinct_id: jest.fn().mockImplementation(() => 'distinctid'), - _send_request: jest.fn().mockImplementation(({ callback }) => callback?.({ config: given.decideResponse })), - featureFlags: { - receivedFeatureFlags: jest.fn(), - setReloadingPaused: jest.fn(), - _startReloadTimer: jest.fn(), - }, - requestRouter: new RequestRouter({ config: given.config }), - _hasBootstrappedFeatureFlags: jest.fn(), - getGroups: () => ({ organization: '5' }), - })) + let posthog + + given('decide', () => new Decide(posthog)) given('decideResponse', () => ({})) - given('config', () => ({ api_host: 'https://test.com', persistence: 'memory' })) + given('config', () => ({ token: 'testtoken', api_host: 'https://test.com', persistence: 'memory' })) beforeEach(() => { // clean the JSDOM to prevent interdependencies between tests document.body.innerHTML = '' document.head.innerHTML = '' + + posthog = { + config: given.config, + persistence: new PostHogPersistence(given.config), + register: (props) => posthog.persistence.register(props), + unregister: (key) => posthog.persistence.unregister(key), + get_property: (key) => posthog.persistence.props[key], + capture: jest.fn(), + _addCaptureHook: jest.fn(), + _afterDecideResponse: jest.fn(), + get_distinct_id: jest.fn().mockImplementation(() => 'distinctid'), + _send_request: jest.fn().mockImplementation(({ callback }) => callback?.({ config: given.decideResponse })), + featureFlags: { + receivedFeatureFlags: jest.fn(), + setReloadingPaused: jest.fn(), + _startReloadTimer: jest.fn(), + }, + requestRouter: new RequestRouter({ config: given.config }), + _hasBootstrappedFeatureFlags: jest.fn(), + getGroups: () => ({ organization: '5' }), + } }) describe('constructor', () => { @@ -65,60 +68,77 @@ describe('Decide', () => { it('should call instance._send_request on constructor', () => { given.subject() - expectDecodedSendRequest(given.posthog._send_request, { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - }) + expectDecodedSendRequest( + posthog._send_request, + { + token: 'testtoken', + distinct_id: 'distinctid', + groups: { organization: '5' }, + }, + false, + posthog + ) }) it('should send all stored properties with decide request', () => { - given.posthog.register({ + posthog.register({ $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) given.subject() - expectDecodedSendRequest(given.posthog._send_request, { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - person_properties: { key: 'value' }, - group_properties: { organization: { orgName: 'orgValue' } }, - }) + expectDecodedSendRequest( + posthog._send_request, + { + token: 'testtoken', + distinct_id: 'distinctid', + groups: { organization: '5' }, + person_properties: { key: 'value' }, + group_properties: { organization: { orgName: 'orgValue' } }, + }, + false, + posthog + ) }) it('should send disable flags with decide request when config is set', () => { - given('config', () => ({ + posthog.config = { api_host: 'https://test.com', token: 'testtoken', persistence: 'memory', advanced_disable_feature_flags: true, - })) - given.posthog.register({ + } + + posthog.register({ $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) given.subject() - expectDecodedSendRequest(given.posthog._send_request, { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - person_properties: { key: 'value' }, - group_properties: { organization: { orgName: 'orgValue' } }, - disable_flags: true, - }) + expectDecodedSendRequest( + posthog._send_request, + { + token: 'testtoken', + distinct_id: 'distinctid', + groups: { organization: '5' }, + person_properties: { key: 'value' }, + group_properties: { organization: { orgName: 'orgValue' } }, + disable_flags: true, + }, + false, + posthog + ) }) it('should disable compression when config is set', () => { - given('config', () => ({ + posthog.config = { api_host: 'https://test.com', token: 'testtoken', persistence: 'memory', disable_compression: true, - })) - given.posthog.register({ + } + + posthog.register({ $stored_person_properties: {}, $stored_group_properties: {}, }) @@ -126,7 +146,7 @@ describe('Decide', () => { // noCompression is true expectDecodedSendRequest( - given.posthog._send_request, + posthog._send_request, { token: 'testtoken', distinct_id: 'distinctid', @@ -134,31 +154,38 @@ describe('Decide', () => { person_properties: {}, group_properties: {}, }, - true + true, + posthog ) }) it('should send disable flags with decide request when config for advanced_disable_feature_flags_on_first_load is set', () => { - given('config', () => ({ + posthog.config = { api_host: 'https://test.com', token: 'testtoken', persistence: 'memory', advanced_disable_feature_flags_on_first_load: true, - })) - given.posthog.register({ + } + + posthog.register({ $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) given.subject() - expectDecodedSendRequest(given.posthog._send_request, { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - person_properties: { key: 'value' }, - group_properties: { organization: { orgName: 'orgValue' } }, - disable_flags: true, - }) + expectDecodedSendRequest( + posthog._send_request, + { + token: 'testtoken', + distinct_id: 'distinctid', + groups: { organization: '5' }, + person_properties: { key: 'value' }, + group_properties: { organization: { orgName: 'orgValue' } }, + disable_flags: true, + }, + false, + posthog + ) }) }) @@ -169,8 +196,8 @@ describe('Decide', () => { given('decideResponse', () => ({})) given.subject() - expect(given.posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith(given.decideResponse, false) - expect(given.posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) + expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith(given.decideResponse, false) + expect(posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) }) it('Make sure receivedFeatureFlags is called with errors if the decide response fails', () => { @@ -180,7 +207,7 @@ describe('Decide', () => { given.subject() - expect(given.posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, true) + expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, true) expect(console.error).toHaveBeenCalledWith('[PostHog.js]', 'Failed to fetch feature flags from PostHog.') }) @@ -188,38 +215,38 @@ describe('Decide', () => { given('decideResponse', () => ({ featureFlags: { 'test-flag': true }, })) - given('config', () => ({ + posthog.config = { api_host: 'https://test.com', token: 'testtoken', persistence: 'memory', advanced_disable_feature_flags_on_first_load: true, - })) + } given.subject() - expect(given.posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) - expect(given.posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() + expect(posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) + expect(posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() }) it('Make sure receivedFeatureFlags is not called if advanced_disable_feature_flags is set', () => { given('decideResponse', () => ({ featureFlags: { 'test-flag': true }, })) - given('config', () => ({ + posthog.config = { api_host: 'https://test.com', token: 'testtoken', persistence: 'memory', advanced_disable_feature_flags: true, - })) + } given.subject() - expect(given.posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) - expect(given.posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() + expect(posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) + expect(posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() }) it('runs site apps if opted in', () => { - given('config', () => ({ api_host: 'https://test.com', opt_in_site_apps: true, persistence: 'memory' })) + posthog.config = { api_host: 'https://test.com', opt_in_site_apps: true, persistence: 'memory' } given('decideResponse', () => ({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] })) given.subject() expectScriptToExist('https://test.com/site_app/1/tokentoken/hash/') From 49f5b7dc362081a1b81a30948a4baf615a127833 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 17:22:30 +0100 Subject: [PATCH 02/29] Step --- src/__tests__/decide.js | 43 ++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/__tests__/decide.js b/src/__tests__/decide.js index 6b249178b..9eaf9e451 100644 --- a/src/__tests__/decide.js +++ b/src/__tests__/decide.js @@ -25,8 +25,6 @@ describe('Decide', () => { given('decide', () => new Decide(posthog)) - given('decideResponse', () => ({})) - given('config', () => ({ token: 'testtoken', api_host: 'https://test.com', persistence: 'memory' })) beforeEach(() => { @@ -44,7 +42,7 @@ describe('Decide', () => { _addCaptureHook: jest.fn(), _afterDecideResponse: jest.fn(), get_distinct_id: jest.fn().mockImplementation(() => 'distinctid'), - _send_request: jest.fn().mockImplementation(({ callback }) => callback?.({ config: given.decideResponse })), + _send_request: jest.fn().mockImplementation(({ callback }) => callback?.({ config: {} })), featureFlags: { receivedFeatureFlags: jest.fn(), setReloadingPaused: jest.fn(), @@ -190,31 +188,26 @@ describe('Decide', () => { }) describe('parseDecideResponse', () => { - given('subject', () => () => given.decide.parseDecideResponse(given.decideResponse)) + const subject = (decideResponse) => given.decide.parseDecideResponse(decideResponse) it('properly parses decide response', () => { - given('decideResponse', () => ({})) - given.subject() + subject({}) - expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith(given.decideResponse, false) - expect(posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) + expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, false) + expect(posthog._afterDecideResponse).toHaveBeenCalledWith({}) }) it('Make sure receivedFeatureFlags is called with errors if the decide response fails', () => { - given('decideResponse', () => undefined) window.POSTHOG_DEBUG = true console.error = jest.fn() - given.subject() + subject(undefined) expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, true) expect(console.error).toHaveBeenCalledWith('[PostHog.js]', 'Failed to fetch feature flags from PostHog.') }) it('Make sure receivedFeatureFlags is not called if advanced_disable_feature_flags_on_first_load is set', () => { - given('decideResponse', () => ({ - featureFlags: { 'test-flag': true }, - })) posthog.config = { api_host: 'https://test.com', token: 'testtoken', @@ -222,16 +215,16 @@ describe('Decide', () => { advanced_disable_feature_flags_on_first_load: true, } - given.subject() + const decideResponse = { + featureFlags: { 'test-flag': true }, + } + subject(decideResponse) - expect(posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) + expect(posthog._afterDecideResponse).toHaveBeenCalledWith(decideResponse) expect(posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() }) it('Make sure receivedFeatureFlags is not called if advanced_disable_feature_flags is set', () => { - given('decideResponse', () => ({ - featureFlags: { 'test-flag': true }, - })) posthog.config = { api_host: 'https://test.com', token: 'testtoken', @@ -239,25 +232,27 @@ describe('Decide', () => { advanced_disable_feature_flags: true, } - given.subject() + const decideResponse = { + featureFlags: { 'test-flag': true }, + } + subject(decideResponse) - expect(posthog._afterDecideResponse).toHaveBeenCalledWith(given.decideResponse) + expect(posthog._afterDecideResponse).toHaveBeenCalledWith(decideResponse) expect(posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() }) it('runs site apps if opted in', () => { posthog.config = { api_host: 'https://test.com', opt_in_site_apps: true, persistence: 'memory' } - given('decideResponse', () => ({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] })) - given.subject() + subject({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] }) expectScriptToExist('https://test.com/site_app/1/tokentoken/hash/') }) it('does not run site apps code if not opted in', () => { window.POSTHOG_DEBUG = true given('config', () => ({ api_host: 'https://test.com', opt_in_site_apps: false, persistence: 'memory' })) - given('decideResponse', () => ({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] })) + expect(() => { - given.subject() + subject({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] }) }).toThrow( // throwing only in tests, just an error in production 'Unexpected console.error: [PostHog.js],PostHog site apps are disabled. Enable the "opt_in_site_apps" config to proceed.' From 834d419359acb5139c267505bfe7760e8b39046c Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 17:23:28 +0100 Subject: [PATCH 03/29] Step --- src/__tests__/decide.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/__tests__/decide.js b/src/__tests__/decide.js index 9eaf9e451..5ba864111 100644 --- a/src/__tests__/decide.js +++ b/src/__tests__/decide.js @@ -25,7 +25,7 @@ describe('Decide', () => { given('decide', () => new Decide(posthog)) - given('config', () => ({ token: 'testtoken', api_host: 'https://test.com', persistence: 'memory' })) + const defaultConfig = { token: 'testtoken', api_host: 'https://test.com', persistence: 'memory' } beforeEach(() => { // clean the JSDOM to prevent interdependencies between tests @@ -33,8 +33,8 @@ describe('Decide', () => { document.head.innerHTML = '' posthog = { - config: given.config, - persistence: new PostHogPersistence(given.config), + config: defaultConfig, + persistence: new PostHogPersistence(defaultConfig), register: (props) => posthog.persistence.register(props), unregister: (key) => posthog.persistence.unregister(key), get_property: (key) => posthog.persistence.props[key], @@ -48,7 +48,7 @@ describe('Decide', () => { setReloadingPaused: jest.fn(), _startReloadTimer: jest.fn(), }, - requestRouter: new RequestRouter({ config: given.config }), + requestRouter: new RequestRouter({ config: defaultConfig }), _hasBootstrappedFeatureFlags: jest.fn(), getGroups: () => ({ organization: '5' }), } From 26e262ae8552f5a34aa8ec47090ebd0809990b46 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 17:25:36 +0100 Subject: [PATCH 04/29] step --- src/__tests__/decide.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/__tests__/decide.js b/src/__tests__/decide.js index 5ba864111..39676f842 100644 --- a/src/__tests__/decide.js +++ b/src/__tests__/decide.js @@ -57,12 +57,6 @@ describe('Decide', () => { describe('constructor', () => { given('subject', () => () => given.decide.call()) - given('config', () => ({ - api_host: 'https://test.com', - token: 'testtoken', - persistence: 'memory', - })) - it('should call instance._send_request on constructor', () => { given.subject() @@ -249,7 +243,8 @@ describe('Decide', () => { it('does not run site apps code if not opted in', () => { window.POSTHOG_DEBUG = true - given('config', () => ({ api_host: 'https://test.com', opt_in_site_apps: false, persistence: 'memory' })) + // don't technically need to run this but this test assumes opt_in_site_apps is false, let's make that explicit + posthog.config = { api_host: 'https://test.com', opt_in_site_apps: false, persistence: 'memory' } expect(() => { subject({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] }) From 4657789c1252990097418dde9aafcd82e44e65ae Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 17:28:52 +0100 Subject: [PATCH 05/29] step --- src/__tests__/decide.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/__tests__/decide.js b/src/__tests__/decide.js index 39676f842..d166550f7 100644 --- a/src/__tests__/decide.js +++ b/src/__tests__/decide.js @@ -23,7 +23,7 @@ const expectDecodedSendRequest = (send_request, data, noCompression, posthog) => describe('Decide', () => { let posthog - given('decide', () => new Decide(posthog)) + const decide = () => new Decide(posthog) const defaultConfig = { token: 'testtoken', api_host: 'https://test.com', persistence: 'memory' } @@ -55,10 +55,10 @@ describe('Decide', () => { }) describe('constructor', () => { - given('subject', () => () => given.decide.call()) + const subject = () => decide().call() it('should call instance._send_request on constructor', () => { - given.subject() + subject() expectDecodedSendRequest( posthog._send_request, @@ -77,7 +77,7 @@ describe('Decide', () => { $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) - given.subject() + subject() expectDecodedSendRequest( posthog._send_request, @@ -105,7 +105,7 @@ describe('Decide', () => { $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) - given.subject() + subject() expectDecodedSendRequest( posthog._send_request, @@ -134,7 +134,7 @@ describe('Decide', () => { $stored_person_properties: {}, $stored_group_properties: {}, }) - given.subject() + subject() // noCompression is true expectDecodedSendRequest( @@ -163,7 +163,7 @@ describe('Decide', () => { $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) - given.subject() + subject() expectDecodedSendRequest( posthog._send_request, @@ -182,7 +182,7 @@ describe('Decide', () => { }) describe('parseDecideResponse', () => { - const subject = (decideResponse) => given.decide.parseDecideResponse(decideResponse) + const subject = (decideResponse) => decide().parseDecideResponse(decideResponse) it('properly parses decide response', () => { subject({}) From ac4ea685cd8f3ba532b2b5d4c38ec26f262ce265 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 17:44:00 +0100 Subject: [PATCH 06/29] step --- src/__tests__/featureflags.js | 372 +++++++++++++++++----------------- 1 file changed, 187 insertions(+), 185 deletions(-) diff --git a/src/__tests__/featureflags.js b/src/__tests__/featureflags.js index bba3575fa..a2c20111f 100644 --- a/src/__tests__/featureflags.js +++ b/src/__tests__/featureflags.js @@ -8,29 +8,14 @@ jest.useFakeTimers() jest.spyOn(global, 'setTimeout') describe('featureflags', () => { - given('decideEndpointWasHit', () => false) + let instance + let featureFlags const config = { token: 'random fake token', persistence: 'memory', api_host: 'https://app.posthog.com', } - given('instance', () => ({ - config, - get_distinct_id: () => 'blah id', - getGroups: () => {}, - 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], - capture: () => {}, - decideEndpointWasHit: given.decideEndpointWasHit, - _send_request: jest.fn().mockImplementation(({ callback }) => callback(given.decideResponsePayload)), - reloadFeatureFlags: () => given.featureFlags.reloadFeatureFlags(), - })) - - given('featureFlags', () => new PostHogFeatureFlags(given.instance)) given('decideResponsePayload', () => ({ statusCode: 200, @@ -38,10 +23,27 @@ describe('featureflags', () => { })) beforeEach(() => { - jest.spyOn(given.instance, 'capture').mockReturnValue() + instance = { + config, + get_distinct_id: () => 'blah id', + getGroups: () => {}, + persistence: new PostHogPersistence(config), + requestRouter: new RequestRouter({ config }), + register: (props) => instance.persistence.register(props), + unregister: (key) => instance.persistence.unregister(key), + get_property: (key) => instance.persistence.props[key], + capture: () => {}, + decideEndpointWasHit: false, + _send_request: jest.fn().mockImplementation(({ callback }) => callback(given.decideResponsePayload)), + reloadFeatureFlags: () => featureFlags.reloadFeatureFlags(), + } + + featureFlags = new PostHogFeatureFlags(instance) + + jest.spyOn(instance, 'capture').mockReturnValue() jest.spyOn(window.console, 'warn').mockImplementation() - given.instance.persistence.register({ + instance.persistence.register({ $feature_flag_payloads: { 'beta-feature': { some: 'payload', @@ -58,29 +60,29 @@ describe('featureflags', () => { $override_feature_flags: false, }) - given.instance.persistence.unregister('$flag_call_reported') + instance.persistence.unregister('$flag_call_reported') }) it('should return flags from persistence even if decide endpoint was not hit', () => { - given.featureFlags.instance.decideEndpointWasHit = false + featureFlags.instance.decideEndpointWasHit = false - expect(given.featureFlags.getFlags()).toEqual([ + expect(featureFlags.getFlags()).toEqual([ 'beta-feature', 'alpha-feature-2', 'multivariate-flag', 'disabled-flag', ]) - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) }) it('should warn if decide endpoint was not hit and no flags exist', () => { window.POSTHOG_DEBUG = true - given.featureFlags.instance.decideEndpointWasHit = false - given.instance.persistence.unregister('$enabled_feature_flags') - given.instance.persistence.unregister('$active_feature_flags') + featureFlags.instance.decideEndpointWasHit = false + instance.persistence.unregister('$enabled_feature_flags') + instance.persistence.unregister('$active_feature_flags') - expect(given.featureFlags.getFlags()).toEqual([]) - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(undefined) + expect(featureFlags.getFlags()).toEqual([]) + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(undefined) expect(window.console.warn).toHaveBeenCalledWith( '[PostHog.js]', 'isFeatureEnabled for key "beta-feature" failed. Feature flags didn\'t load in time.' @@ -88,7 +90,7 @@ describe('featureflags', () => { window.console.warn.mockClear() - expect(given.featureFlags.getFeatureFlag('beta-feature')).toEqual(undefined) + expect(featureFlags.getFeatureFlag('beta-feature')).toEqual(undefined) expect(window.console.warn).toHaveBeenCalledWith( '[PostHog.js]', 'getFeatureFlag for key "beta-feature" failed. Feature flags didn\'t load in time.' @@ -96,30 +98,30 @@ describe('featureflags', () => { }) it('should return the right feature flag and call capture', () => { - given.featureFlags.instance.decideEndpointWasHit = false + featureFlags.instance.decideEndpointWasHit = false - expect(given.featureFlags.getFlags()).toEqual([ + expect(featureFlags.getFlags()).toEqual([ 'beta-feature', 'alpha-feature-2', 'multivariate-flag', 'disabled-flag', ]) - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'alpha-feature-2': true, 'beta-feature': true, 'multivariate-flag': 'variant-1', 'disabled-flag': false, }) - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) - expect(given.featureFlags.isFeatureEnabled('random')).toEqual(false) - expect(given.featureFlags.isFeatureEnabled('multivariate-flag')).toEqual(true) + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) + expect(featureFlags.isFeatureEnabled('random')).toEqual(false) + expect(featureFlags.isFeatureEnabled('multivariate-flag')).toEqual(true) - expect(given.instance.capture).toHaveBeenCalledTimes(3) + expect(instance.capture).toHaveBeenCalledTimes(3) // It should not call `capture` on subsequent calls - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) - expect(given.instance.capture).toHaveBeenCalledTimes(3) - expect(given.instance.get_property('$flag_call_reported')).toEqual({ + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) + expect(instance.capture).toHaveBeenCalledTimes(3) + expect(instance.get_property('$flag_call_reported')).toEqual({ 'beta-feature': ['true'], 'multivariate-flag': ['variant-1'], random: ['undefined'], @@ -127,76 +129,76 @@ describe('featureflags', () => { }) it('should call capture for every different flag response', () => { - given.featureFlags.instance.decideEndpointWasHit = true + featureFlags.instance.decideEndpointWasHit = true - given.instance.persistence.register({ + instance.persistence.register({ $enabled_feature_flags: { 'beta-feature': true, }, }) - expect(given.featureFlags.getFlags()).toEqual(['beta-feature']) - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlags()).toEqual(['beta-feature']) + expect(featureFlags.getFlagVariants()).toEqual({ 'beta-feature': true, }) - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) - expect(given.instance.get_property('$flag_call_reported')).toEqual({ 'beta-feature': ['true'] }) + expect(instance.get_property('$flag_call_reported')).toEqual({ 'beta-feature': ['true'] }) - expect(given.instance.capture).toHaveBeenCalledTimes(1) + expect(instance.capture).toHaveBeenCalledTimes(1) // It should not call `capture` on subsequent calls - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) - expect(given.instance.capture).toHaveBeenCalledTimes(1) + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) + expect(instance.capture).toHaveBeenCalledTimes(1) - given.instance.persistence.register({ + instance.persistence.register({ $enabled_feature_flags: {}, }) - given.featureFlags.instance.decideEndpointWasHit = false - expect(given.featureFlags.getFlagVariants()).toEqual({}) - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(undefined) + featureFlags.instance.decideEndpointWasHit = false + expect(featureFlags.getFlagVariants()).toEqual({}) + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(undefined) // no extra capture call because flags haven't loaded yet. - expect(given.instance.capture).toHaveBeenCalledTimes(1) + expect(instance.capture).toHaveBeenCalledTimes(1) - given.featureFlags.instance.decideEndpointWasHit = true - given.instance.persistence.register({ + featureFlags.instance.decideEndpointWasHit = true + instance.persistence.register({ $enabled_feature_flags: { x: 'y' }, }) - expect(given.featureFlags.getFlagVariants()).toEqual({ x: 'y' }) - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(false) - expect(given.instance.capture).toHaveBeenCalledTimes(2) + expect(featureFlags.getFlagVariants()).toEqual({ x: 'y' }) + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(false) + expect(instance.capture).toHaveBeenCalledTimes(2) - given.instance.persistence.register({ + instance.persistence.register({ $enabled_feature_flags: { 'beta-feature': 'variant-1', }, }) - expect(given.featureFlags.getFlagVariants()).toEqual({ 'beta-feature': 'variant-1' }) - expect(given.featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) - expect(given.instance.capture).toHaveBeenCalledTimes(3) + expect(featureFlags.getFlagVariants()).toEqual({ 'beta-feature': 'variant-1' }) + expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(true) + expect(instance.capture).toHaveBeenCalledTimes(3) - expect(given.instance.get_property('$flag_call_reported')).toEqual({ + expect(instance.get_property('$flag_call_reported')).toEqual({ 'beta-feature': ['true', 'undefined', 'variant-1'], }) }) it('should return the right feature flag and not call capture', () => { - given.featureFlags.instance.decideEndpointWasHit = true + featureFlags.instance.decideEndpointWasHit = true - expect(given.featureFlags.isFeatureEnabled('beta-feature', { send_event: false })).toEqual(true) - expect(given.instance.capture).not.toHaveBeenCalled() + expect(featureFlags.isFeatureEnabled('beta-feature', { send_event: false })).toEqual(true) + expect(instance.capture).not.toHaveBeenCalled() }) it('should return the right payload', () => { - expect(given.featureFlags.getFeatureFlagPayload('beta-feature')).toEqual({ + expect(featureFlags.getFeatureFlagPayload('beta-feature')).toEqual({ some: 'payload', }) - expect(given.featureFlags.getFeatureFlagPayload('alpha-feature-2')).toEqual(200) - expect(given.featureFlags.getFeatureFlagPayload('multivariate-flag')).toEqual(undefined) - expect(given.instance.capture).not.toHaveBeenCalled() + expect(featureFlags.getFeatureFlagPayload('alpha-feature-2')).toEqual(200) + expect(featureFlags.getFeatureFlagPayload('multivariate-flag')).toEqual(undefined) + expect(instance.capture).not.toHaveBeenCalled() }) it('supports overrides', () => { - given.instance.persistence.props = { + instance.persistence.props = { $active_feature_flags: ['beta-feature', 'alpha-feature-2', 'multivariate-flag'], $enabled_feature_flags: { 'beta-feature': true, @@ -210,8 +212,8 @@ describe('featureflags', () => { } // should return both true and false flags - expect(given.featureFlags.getFlags()).toEqual(['beta-feature', 'alpha-feature-2', 'multivariate-flag']) - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlags()).toEqual(['beta-feature', 'alpha-feature-2', 'multivariate-flag']) + expect(featureFlags.getFlagVariants()).toEqual({ 'alpha-feature-2': 'as-a-variant', 'multivariate-flag': 'variant-1', 'beta-feature': false, @@ -233,7 +235,7 @@ describe('featureflags', () => { let _variants = {} let _error = undefined - given.featureFlags.onFeatureFlags((flags, variants, errors) => { + featureFlags.onFeatureFlags((flags, variants, errors) => { called = true _flags = flags _variants = variants @@ -241,8 +243,8 @@ describe('featureflags', () => { }) expect(called).toEqual(false) - given.featureFlags.setAnonymousDistinctId('rando_id') - given.featureFlags.reloadFeatureFlags() + featureFlags.setAnonymousDistinctId('rando_id') + featureFlags.reloadFeatureFlags() jest.runAllTimers() expect(called).toEqual(true) @@ -255,19 +257,19 @@ describe('featureflags', () => { }) it('onFeatureFlags callback should be called immediately if feature flags were loaded', () => { - given.featureFlags.instance.decideEndpointWasHit = true + featureFlags.instance.decideEndpointWasHit = true var called = false - given.featureFlags.onFeatureFlags(() => (called = true)) + featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(true) called = false }) it('onFeatureFlags should not return flags that are off', () => { - given.featureFlags.instance.decideEndpointWasHit = true + featureFlags.instance.decideEndpointWasHit = true let _flags = [] let _variants = {} - given.featureFlags.onFeatureFlags((flags, variants) => { + featureFlags.onFeatureFlags((flags, variants) => { _flags = flags _variants = variants }) @@ -283,12 +285,12 @@ describe('featureflags', () => { it('onFeatureFlags should return function to unsubscribe the function from onFeatureFlags', () => { let called = false - const unsubscribe = given.featureFlags.onFeatureFlags(() => { + const unsubscribe = featureFlags.onFeatureFlags(() => { called = true }) - given.featureFlags.setAnonymousDistinctId('rando_id') - given.featureFlags.reloadFeatureFlags() + featureFlags.setAnonymousDistinctId('rando_id') + featureFlags.reloadFeatureFlags() jest.runAllTimers() expect(called).toEqual(true) @@ -297,8 +299,8 @@ describe('featureflags', () => { unsubscribe() - given.featureFlags.setAnonymousDistinctId('rando_id') - given.featureFlags.reloadFeatureFlags() + featureFlags.setAnonymousDistinctId('rando_id') + featureFlags.reloadFeatureFlags() jest.runAllTimers() expect(called).toEqual(false) @@ -307,7 +309,7 @@ describe('featureflags', () => { describe('earlyAccessFeatures', () => { afterEach(() => { - given.instance.persistence.clear() + instance.persistence.clear() }) // actually early access feature response const EARLY_ACCESS_FEATURE_FIRST = { @@ -333,45 +335,45 @@ describe('featureflags', () => { })) it('getEarlyAccessFeatures requests early access features if not present', () => { - given.featureFlags.getEarlyAccessFeatures((data) => { + featureFlags.getEarlyAccessFeatures((data) => { expect(data).toEqual([EARLY_ACCESS_FEATURE_FIRST]) }) - expect(given.instance._send_request).toHaveBeenCalledWith({ + expect(instance._send_request).toHaveBeenCalledWith({ url: 'https://us.i.posthog.com/api/early_access_features/?token=random fake token', method: 'GET', transport: 'XHR', callback: expect.any(Function), }) - expect(given.instance._send_request).toHaveBeenCalledTimes(1) + expect(instance._send_request).toHaveBeenCalledTimes(1) - expect(given.instance.persistence.props.$early_access_features).toEqual([EARLY_ACCESS_FEATURE_FIRST]) + expect(instance.persistence.props.$early_access_features).toEqual([EARLY_ACCESS_FEATURE_FIRST]) given('decideResponse', () => ({ earlyAccessFeatures: [EARLY_ACCESS_FEATURE_SECOND], })) // request again, shouldn't call _send_request again - given.featureFlags.getEarlyAccessFeatures((data) => { + featureFlags.getEarlyAccessFeatures((data) => { expect(data).toEqual([EARLY_ACCESS_FEATURE_FIRST]) }) - expect(given.instance._send_request).toHaveBeenCalledTimes(1) + expect(instance._send_request).toHaveBeenCalledTimes(1) }) it('getEarlyAccessFeatures force reloads early access features when asked to', () => { - given.featureFlags.getEarlyAccessFeatures((data) => { + featureFlags.getEarlyAccessFeatures((data) => { expect(data).toEqual([EARLY_ACCESS_FEATURE_FIRST]) }) - expect(given.instance._send_request).toHaveBeenCalledWith({ + expect(instance._send_request).toHaveBeenCalledWith({ url: 'https://us.i.posthog.com/api/early_access_features/?token=random fake token', method: 'GET', callback: expect.any(Function), transport: 'XHR', }) - expect(given.instance._send_request).toHaveBeenCalledTimes(1) + expect(instance._send_request).toHaveBeenCalledTimes(1) - expect(given.instance.persistence.props.$early_access_features).toEqual([EARLY_ACCESS_FEATURE_FIRST]) + expect(instance.persistence.props.$early_access_features).toEqual([EARLY_ACCESS_FEATURE_FIRST]) given('decideResponsePayload', () => ({ statusCode: 200, @@ -381,17 +383,17 @@ describe('featureflags', () => { })) // request again, should call _send_request because we're forcing a reload - given.featureFlags.getEarlyAccessFeatures((data) => { + featureFlags.getEarlyAccessFeatures((data) => { expect(data).toEqual([EARLY_ACCESS_FEATURE_SECOND]) }, true) - expect(given.instance._send_request).toHaveBeenCalledTimes(2) + expect(instance._send_request).toHaveBeenCalledTimes(2) }) it('update enrollment should update the early access feature enrollment', () => { - given.featureFlags.updateEarlyAccessFeatureEnrollment('first-flag', true) + featureFlags.updateEarlyAccessFeatureEnrollment('first-flag', true) - expect(given.instance.capture).toHaveBeenCalledTimes(1) - expect(given.instance.capture).toHaveBeenCalledWith('$feature_enrollment_update', { + expect(instance.capture).toHaveBeenCalledTimes(1) + expect(instance.capture).toHaveBeenCalledWith('$feature_enrollment_update', { $feature_enrollment: true, $feature_flag: 'first-flag', $set: { @@ -399,7 +401,7 @@ describe('featureflags', () => { }, }) - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'alpha-feature-2': true, 'beta-feature': true, 'disabled-flag': false, @@ -409,10 +411,10 @@ describe('featureflags', () => { }) // now enrollment is turned off - given.featureFlags.updateEarlyAccessFeatureEnrollment('first-flag', false) + featureFlags.updateEarlyAccessFeatureEnrollment('first-flag', false) - expect(given.instance.capture).toHaveBeenCalledTimes(2) - expect(given.instance.capture).toHaveBeenCalledWith('$feature_enrollment_update', { + expect(instance.capture).toHaveBeenCalledTimes(2) + expect(instance.capture).toHaveBeenCalledWith('$feature_enrollment_update', { $feature_enrollment: false, $feature_flag: 'first-flag', $set: { @@ -420,7 +422,7 @@ describe('featureflags', () => { }, }) - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'alpha-feature-2': true, 'beta-feature': true, 'disabled-flag': false, @@ -431,10 +433,10 @@ describe('featureflags', () => { }) it('reloading flags after update enrollment should send properties', () => { - given.featureFlags.updateEarlyAccessFeatureEnrollment('x-flag', true) + featureFlags.updateEarlyAccessFeatureEnrollment('x-flag', true) - expect(given.instance.capture).toHaveBeenCalledTimes(1) - expect(given.instance.capture).toHaveBeenCalledWith('$feature_enrollment_update', { + expect(instance.capture).toHaveBeenCalledTimes(1) + expect(instance.capture).toHaveBeenCalledWith('$feature_enrollment_update', { $feature_enrollment: true, $feature_flag: 'x-flag', $set: { @@ -442,7 +444,7 @@ describe('featureflags', () => { }, }) - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'alpha-feature-2': true, 'beta-feature': true, 'disabled-flag': false, @@ -451,10 +453,10 @@ describe('featureflags', () => { 'x-flag': true, }) - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() // check the request sent person properties - expect(given.instance._send_request.mock.calls[0][0].data).toEqual({ + expect(instance._send_request.mock.calls[0][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', person_properties: { @@ -473,18 +475,18 @@ describe('featureflags', () => { })) it('on providing anonDistinctId', () => { - given.featureFlags.setAnonymousDistinctId('rando_id') - given.featureFlags.reloadFeatureFlags() + featureFlags.setAnonymousDistinctId('rando_id') + featureFlags.reloadFeatureFlags() jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ first: 'variant-1', second: true, }) // check the request sent $anon_distinct_id - expect(given.instance._send_request.mock.calls[0][0].data).toEqual({ + expect(instance._send_request.mock.calls[0][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', $anon_distinct_id: 'rando_id', @@ -492,40 +494,40 @@ describe('featureflags', () => { }) it('on providing anonDistinctId and calling reload multiple times', () => { - given.featureFlags.setAnonymousDistinctId('rando_id') - given.featureFlags.reloadFeatureFlags() - given.featureFlags.reloadFeatureFlags() + featureFlags.setAnonymousDistinctId('rando_id') + featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ first: 'variant-1', second: true, }) // check the request sent $anon_distinct_id - expect(given.instance._send_request.mock.calls[0][0].data).toEqual({ + expect(instance._send_request.mock.calls[0][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', $anon_distinct_id: 'rando_id', }) - given.featureFlags.reloadFeatureFlags() - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() // check the request didn't send $anon_distinct_id the second time around - expect(given.instance._send_request.mock.calls[1][0].data).toEqual({ + expect(instance._send_request.mock.calls[1][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', // $anon_distinct_id: "rando_id" }) - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() // check the request didn't send $anon_distinct_id the second time around - expect(given.instance._send_request.mock.calls[2][0].data).toEqual({ + expect(instance._send_request.mock.calls[2][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', // $anon_distinct_id: "rando_id" @@ -533,20 +535,20 @@ describe('featureflags', () => { }) it('on providing personProperties runs reload automatically', () => { - given.featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }) + featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }) jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ first: 'variant-1', second: true, }) // check right compression is sent - expect(given.instance._send_request.mock.calls[0][0].compression).toEqual('base64') + expect(instance._send_request.mock.calls[0][0].compression).toEqual('base64') // check the request sent person properties - expect(given.instance._send_request.mock.calls[0][0].data).toEqual({ + expect(instance._send_request.mock.calls[0][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', person_properties: { a: 'b', c: 'd' }, @@ -554,53 +556,53 @@ describe('featureflags', () => { }) it('on providing config advanced_disable_feature_flags', () => { - given.instance.config = { - ...given.instance.config, + instance.config = { + ...instance.config, advanced_disable_feature_flags: true, } - given.instance.persistence.register({ + instance.persistence.register({ $enabled_feature_flags: { 'beta-feature': true, 'random-feature': 'xatu', }, }) - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'beta-feature': true, 'random-feature': 'xatu', }) // check reload request was not sent - expect(given.instance._send_request).not.toHaveBeenCalled() + expect(instance._send_request).not.toHaveBeenCalled() // check the same for other ways to call reload flags - given.featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }) + featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }) jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'beta-feature': true, 'random-feature': 'xatu', }) // check reload request was not sent - expect(given.instance._send_request).not.toHaveBeenCalled() + expect(instance._send_request).not.toHaveBeenCalled() }) it('on providing config disable_compression', () => { - given.instance.config = { - ...given.instance.config, + instance.config = { + ...instance.config, disable_compression: true, } - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() - expect(given.instance._send_request.mock.calls[0][0].compression).toEqual(undefined) + expect(instance._send_request.mock.calls[0][0].compression).toEqual(undefined) }) }) @@ -613,18 +615,18 @@ describe('featureflags', () => { })) it('on providing personProperties updates properties successively', () => { - given.featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }) - given.featureFlags.setPersonPropertiesForFlags({ x: 'y', c: 'e' }) + featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }) + featureFlags.setPersonPropertiesForFlags({ x: 'y', c: 'e' }) jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ first: 'variant-1', second: true, }) // check the request sent person properties - expect(given.instance._send_request.mock.calls[0][0].data).toEqual({ + expect(instance._send_request.mock.calls[0][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', person_properties: { a: 'b', c: 'e', x: 'y' }, @@ -632,56 +634,56 @@ describe('featureflags', () => { }) it('doesnt reload flags if explicitly asked not to', () => { - given.featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }, false) + featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }, false) jest.runAllTimers() // still old flags - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'alpha-feature-2': true, 'beta-feature': true, 'disabled-flag': false, 'multivariate-flag': 'variant-1', }) - expect(given.instance._send_request).not.toHaveBeenCalled() + expect(instance._send_request).not.toHaveBeenCalled() }) it('resetPersonProperties resets all properties', () => { - given.featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }, false) - given.featureFlags.setPersonPropertiesForFlags({ x: 'y', c: 'e' }, false) + featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }, false) + featureFlags.setPersonPropertiesForFlags({ x: 'y', c: 'e' }, false) jest.runAllTimers() - expect(given.instance.persistence.props.$stored_person_properties).toEqual({ a: 'b', c: 'e', x: 'y' }) + expect(instance.persistence.props.$stored_person_properties).toEqual({ a: 'b', c: 'e', x: 'y' }) - given.featureFlags.resetPersonPropertiesForFlags() - given.featureFlags.reloadFeatureFlags() + featureFlags.resetPersonPropertiesForFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() // check the request did not send person properties - expect(given.instance._send_request.mock.calls[0][0].data).toEqual({ + expect(instance._send_request.mock.calls[0][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', }) }) it('on providing groupProperties updates properties successively', () => { - given.featureFlags.setGroupPropertiesForFlags({ orgs: { a: 'b', c: 'd' }, projects: { x: 'y', c: 'e' } }) + featureFlags.setGroupPropertiesForFlags({ orgs: { a: 'b', c: 'd' }, projects: { x: 'y', c: 'e' } }) - expect(given.instance.persistence.props.$stored_group_properties).toEqual({ + expect(instance.persistence.props.$stored_group_properties).toEqual({ orgs: { a: 'b', c: 'd' }, projects: { x: 'y', c: 'e' }, }) jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ first: 'variant-1', second: true, }) // check the request sent person properties - expect(given.instance._send_request.mock.calls[0][0].data).toEqual({ + expect(instance._send_request.mock.calls[0][0].data).toEqual({ token: 'random fake token', distinct_id: 'blah id', group_properties: { orgs: { a: 'b', c: 'd' }, projects: { x: 'y', c: 'e' } }, @@ -689,50 +691,50 @@ describe('featureflags', () => { }) it('handles groupProperties updates', () => { - given.featureFlags.setGroupPropertiesForFlags({ orgs: { a: 'b', c: 'd' }, projects: { x: 'y', c: 'e' } }) + featureFlags.setGroupPropertiesForFlags({ orgs: { a: 'b', c: 'd' }, projects: { x: 'y', c: 'e' } }) - expect(given.instance.persistence.props.$stored_group_properties).toEqual({ + expect(instance.persistence.props.$stored_group_properties).toEqual({ orgs: { a: 'b', c: 'd' }, projects: { x: 'y', c: 'e' }, }) - given.featureFlags.setGroupPropertiesForFlags({ orgs: { w: '1' }, other: { z: '2' } }) + featureFlags.setGroupPropertiesForFlags({ orgs: { w: '1' }, other: { z: '2' } }) - expect(given.instance.persistence.props.$stored_group_properties).toEqual({ + expect(instance.persistence.props.$stored_group_properties).toEqual({ orgs: { a: 'b', c: 'd', w: '1' }, projects: { x: 'y', c: 'e' }, other: { z: '2' }, }) - given.featureFlags.resetGroupPropertiesForFlags('orgs') + featureFlags.resetGroupPropertiesForFlags('orgs') - expect(given.instance.persistence.props.$stored_group_properties).toEqual({ + expect(instance.persistence.props.$stored_group_properties).toEqual({ orgs: {}, projects: { x: 'y', c: 'e' }, other: { z: '2' }, }) - given.featureFlags.resetGroupPropertiesForFlags() + featureFlags.resetGroupPropertiesForFlags() - expect(given.instance.persistence.props.$stored_group_properties).toEqual(undefined) + expect(instance.persistence.props.$stored_group_properties).toEqual(undefined) jest.runAllTimers() }) it('doesnt reload group flags if explicitly asked not to', () => { - given.featureFlags.setGroupPropertiesForFlags({ orgs: { a: 'b', c: 'd' } }, false) + featureFlags.setGroupPropertiesForFlags({ orgs: { a: 'b', c: 'd' } }, false) jest.runAllTimers() // still old flags - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'alpha-feature-2': true, 'beta-feature': true, 'disabled-flag': false, 'multivariate-flag': 'variant-1', }) - expect(given.instance._send_request).not.toHaveBeenCalled() + expect(instance._send_request).not.toHaveBeenCalled() }) }) @@ -743,11 +745,11 @@ describe('featureflags', () => { })) it('should return combined results', () => { - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'alpha-feature-2': true, 'beta-feature': true, 'multivariate-flag': 'variant-1', @@ -765,11 +767,11 @@ describe('featureflags', () => { })) it('should return combined results', () => { - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'x-flag': 'x-value', 'feature-1': false, }) @@ -783,17 +785,17 @@ describe('featureflags', () => { })) it('should not change the existing flags', () => { - given.instance.persistence.register({ + instance.persistence.register({ $enabled_feature_flags: { 'beta-feature': true, 'random-feature': 'xatu', }, }) - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() - expect(given.featureFlags.getFlagVariants()).toEqual({ + expect(featureFlags.getFlagVariants()).toEqual({ 'beta-feature': true, 'random-feature': 'xatu', }) @@ -805,11 +807,11 @@ describe('featureflags', () => { let _variants = {} let _errors = undefined - given.instance.persistence.register({ + instance.persistence.register({ $enabled_feature_flags: {}, }) - given.featureFlags.onFeatureFlags((flags, variants, errors) => { + featureFlags.onFeatureFlags((flags, variants, errors) => { called = true _flags = flags _variants = variants @@ -817,7 +819,7 @@ describe('featureflags', () => { }) expect(called).toEqual(false) - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() expect(called).toEqual(true) @@ -832,7 +834,7 @@ describe('featureflags', () => { let _variants = {} let _errors = undefined - given.featureFlags.onFeatureFlags((flags, variants, errors) => { + featureFlags.onFeatureFlags((flags, variants, errors) => { called = true _flags = flags _variants = variants @@ -840,7 +842,7 @@ describe('featureflags', () => { }) expect(called).toEqual(false) - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() expect(called).toEqual(true) @@ -864,7 +866,7 @@ describe('featureflags', () => { let _variants = {} let _errors = undefined - given.featureFlags.onFeatureFlags((flags, variants, errors) => { + featureFlags.onFeatureFlags((flags, variants, errors) => { called = true _flags = flags _variants = variants @@ -872,7 +874,7 @@ describe('featureflags', () => { }) expect(called).toEqual(false) - given.featureFlags.reloadFeatureFlags() + featureFlags.reloadFeatureFlags() jest.runAllTimers() expect(called).toEqual(true) From 9dc3dcacbc70b99a2b81312dd5bd5c171f3c3f75 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 18:17:47 +0100 Subject: [PATCH 07/29] Step --- package.json | 1 - pnpm-lock.yaml | 7 -- src/__tests__/featureflags.js | 197 ++++++++++++++++++++++------------ 3 files changed, 127 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index 63194e6a4..de7d99cdf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "eslint-plugin-react-hooks": "^4.6.0", "express": "^4.18.2", "fast-check": "^2.17.0", - "given2": "^2.1.7", "husky": "^8.0.1", "jest": "^27.5.1", "jsdom": "16.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0878bc90d..41803cdb6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,9 +144,6 @@ devDependencies: fast-check: specifier: ^2.17.0 version: 2.17.0 - given2: - specifier: ^2.1.7 - version: 2.1.7 husky: specifier: ^8.0.1 version: 8.0.1 @@ -5848,10 +5845,6 @@ packages: omggif: 1.0.10 dev: true - /given2@2.1.7: - resolution: {integrity: sha512-fI3VamsjN2euNVguGpSt2uExyDSMfJoK+SwDxbmV+Thf3v4oF6KKZAFE3LHHuT+PYyMwCsJYXO01TW3euFdPGA==} - dev: true - /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} diff --git a/src/__tests__/featureflags.js b/src/__tests__/featureflags.js index a2c20111f..32365ef8e 100644 --- a/src/__tests__/featureflags.js +++ b/src/__tests__/featureflags.js @@ -1,6 +1,6 @@ /*eslint @typescript-eslint/no-empty-function: "off" */ -import { PostHogFeatureFlags, parseFeatureFlagDecideResponse, filterActiveFeatureFlags } from '../posthog-featureflags' +import { filterActiveFeatureFlags, parseFeatureFlagDecideResponse, PostHogFeatureFlags } from '../posthog-featureflags' import { PostHogPersistence } from '../posthog-persistence' import { RequestRouter } from '../utils/request-router' @@ -17,11 +17,6 @@ describe('featureflags', () => { api_host: 'https://app.posthog.com', } - given('decideResponsePayload', () => ({ - statusCode: 200, - json: given.decideResponse, - })) - beforeEach(() => { instance = { config, @@ -34,7 +29,12 @@ describe('featureflags', () => { get_property: (key) => instance.persistence.props[key], capture: () => {}, decideEndpointWasHit: false, - _send_request: jest.fn().mockImplementation(({ callback }) => callback(given.decideResponsePayload)), + _send_request: jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: {}, + }) + ), reloadFeatureFlags: () => featureFlags.reloadFeatureFlags(), } @@ -221,13 +221,20 @@ describe('featureflags', () => { }) describe('onFeatureFlags', () => { - given('decideResponse', () => ({ - featureFlags: { - first: 'variant-1', - second: true, - third: false, - }, - })) + beforeEach(() => { + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: { + featureFlags: { + first: 'variant-1', + second: true, + third: false, + }, + }, + }) + ) + }) it('onFeatureFlags should not be called immediately if feature flags not loaded', () => { var called = false @@ -330,9 +337,16 @@ describe('featureflags', () => { flagKey: 'second-flag', } - given('decideResponse', () => ({ - earlyAccessFeatures: [EARLY_ACCESS_FEATURE_FIRST], - })) + beforeEach(() => { + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: { + earlyAccessFeatures: [EARLY_ACCESS_FEATURE_FIRST], + }, + }) + ) + }) it('getEarlyAccessFeatures requests early access features if not present', () => { featureFlags.getEarlyAccessFeatures((data) => { @@ -349,15 +363,20 @@ describe('featureflags', () => { expect(instance.persistence.props.$early_access_features).toEqual([EARLY_ACCESS_FEATURE_FIRST]) - given('decideResponse', () => ({ - earlyAccessFeatures: [EARLY_ACCESS_FEATURE_SECOND], - })) + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: { + earlyAccessFeatures: [EARLY_ACCESS_FEATURE_SECOND], + }, + }) + ) // request again, shouldn't call _send_request again featureFlags.getEarlyAccessFeatures((data) => { expect(data).toEqual([EARLY_ACCESS_FEATURE_FIRST]) }) - expect(instance._send_request).toHaveBeenCalledTimes(1) + expect(instance._send_request).toHaveBeenCalledTimes(0) }) it('getEarlyAccessFeatures force reloads early access features when asked to', () => { @@ -375,18 +394,20 @@ describe('featureflags', () => { expect(instance.persistence.props.$early_access_features).toEqual([EARLY_ACCESS_FEATURE_FIRST]) - given('decideResponsePayload', () => ({ - statusCode: 200, - json: { - earlyAccessFeatures: [EARLY_ACCESS_FEATURE_SECOND], - }, - })) + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: { + earlyAccessFeatures: [EARLY_ACCESS_FEATURE_SECOND], + }, + }) + ) // request again, should call _send_request because we're forcing a reload featureFlags.getEarlyAccessFeatures((data) => { expect(data).toEqual([EARLY_ACCESS_FEATURE_SECOND]) }, true) - expect(instance._send_request).toHaveBeenCalledTimes(2) + expect(instance._send_request).toHaveBeenCalledTimes(1) }) it('update enrollment should update the early access feature enrollment', () => { @@ -467,12 +488,19 @@ describe('featureflags', () => { }) describe('reloadFeatureFlags', () => { - given('decideResponse', () => ({ - featureFlags: { - first: 'variant-1', - second: true, - }, - })) + beforeEach(() => { + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: { + featureFlags: { + first: 'variant-1', + second: true, + }, + }, + }) + ) + }) it('on providing anonDistinctId', () => { featureFlags.setAnonymousDistinctId('rando_id') @@ -607,12 +635,19 @@ describe('featureflags', () => { }) describe('override person and group properties', () => { - given('decideResponse', () => ({ - featureFlags: { - first: 'variant-1', - second: true, - }, - })) + beforeEach(() => { + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: { + featureFlags: { + first: 'variant-1', + second: true, + }, + }, + }) + ) + }) it('on providing personProperties updates properties successively', () => { featureFlags.setPersonPropertiesForFlags({ a: 'b', c: 'd' }) @@ -739,10 +774,17 @@ describe('featureflags', () => { }) describe('when subsequent decide calls return partial results', () => { - given('decideResponse', () => ({ - featureFlags: { 'x-flag': 'x-value', 'feature-1': false }, - errorsWhileComputingFlags: true, - })) + beforeEach(() => { + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: { + featureFlags: { 'x-flag': 'x-value', 'feature-1': false }, + errorsWhileComputingFlags: true, + }, + }) + ) + }) it('should return combined results', () => { featureFlags.reloadFeatureFlags() @@ -761,10 +803,17 @@ describe('featureflags', () => { }) describe('when subsequent decide calls return results without errors', () => { - given('decideResponse', () => ({ - featureFlags: { 'x-flag': 'x-value', 'feature-1': false }, - errorsWhileComputingFlags: false, - })) + beforeEach(() => { + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 200, + json: { + featureFlags: { 'x-flag': 'x-value', 'feature-1': false }, + errorsWhileComputingFlags: false, + }, + }) + ) + }) it('should return combined results', () => { featureFlags.reloadFeatureFlags() @@ -779,10 +828,14 @@ describe('featureflags', () => { }) describe('when decide times out or errors out', () => { - given('decideResponsePayload', () => ({ - statusCode: 500, - text: 'Internal Server Error', - })) + beforeEach(() => { + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 500, + text: 'Internal Server Error', + }) + ) + }) it('should not change the existing flags', () => { instance.persistence.register({ @@ -856,10 +909,12 @@ describe('featureflags', () => { }) it('should call onFeatureFlags with existing flags on timeouts', () => { - given('decideResponsePayload', () => ({ - statusCode: 0, - text: '', - })) + instance._send_request = jest.fn().mockImplementation(({ callback }) => + callback({ + statusCode: 0, + text: '', + }) + ) var called = false let _flags = [] @@ -890,12 +945,14 @@ describe('featureflags', () => { }) describe('parseFeatureFlagDecideResponse', () => { - given('decideResponse', () => {}) - given('persistence', () => ({ register: jest.fn(), unregister: jest.fn() })) - given('subject', () => () => parseFeatureFlagDecideResponse(given.decideResponse, given.persistence)) + let persistence + + beforeEach(() => { + persistence = { register: jest.fn(), unregister: jest.fn() } + }) it('enables multivariate feature flags from decide v2^ response', () => { - given('decideResponse', () => ({ + const decideResponse = { featureFlags: { 'beta-feature': true, 'alpha-feature-2': true, @@ -905,10 +962,10 @@ describe('parseFeatureFlagDecideResponse', () => { 'beta-feature': 300, 'alpha-feature-2': 'fake-payload', }, - })) - given.subject() + } + parseFeatureFlagDecideResponse(decideResponse, persistence) - expect(given.persistence.register).toHaveBeenCalledWith({ + expect(persistence.register).toHaveBeenCalledWith({ $active_feature_flags: ['beta-feature', 'alpha-feature-2', 'multivariate-flag'], $enabled_feature_flags: { 'beta-feature': true, @@ -924,21 +981,21 @@ describe('parseFeatureFlagDecideResponse', () => { it('enables feature flags from decide response (v1 backwards compatibility)', () => { // checks that nothing fails when asking for ?v=2 and getting a ?v=1 response - given('decideResponse', () => ({ featureFlags: ['beta-feature', 'alpha-feature-2'] })) - given.subject() + const decideResponse = { featureFlags: ['beta-feature', 'alpha-feature-2'] } + + parseFeatureFlagDecideResponse(decideResponse, persistence) - expect(given.persistence.register).toHaveBeenLastCalledWith({ + expect(persistence.register).toHaveBeenLastCalledWith({ $active_feature_flags: ['beta-feature', 'alpha-feature-2'], $enabled_feature_flags: { 'beta-feature': true, 'alpha-feature-2': true }, }) }) it('doesnt remove existing feature flags when no flags are returned', () => { - given('decideResponse', () => ({})) - given.subject() + parseFeatureFlagDecideResponse({}, persistence) - expect(given.persistence.register).not.toHaveBeenCalled() - expect(given.persistence.unregister).not.toHaveBeenCalled() + expect(persistence.register).not.toHaveBeenCalled() + expect(persistence.unregister).not.toHaveBeenCalled() }) }) From 84a30250786fa8e4b37150e090bc7ec729012fc3 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 21:22:00 +0100 Subject: [PATCH 08/29] step --- package.json | 1 + pnpm-lock.yaml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index de7d99cdf..63194e6a4 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "express": "^4.18.2", "fast-check": "^2.17.0", + "given2": "^2.1.7", "husky": "^8.0.1", "jest": "^27.5.1", "jsdom": "16.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41803cdb6..0878bc90d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,6 +144,9 @@ devDependencies: fast-check: specifier: ^2.17.0 version: 2.17.0 + given2: + specifier: ^2.1.7 + version: 2.1.7 husky: specifier: ^8.0.1 version: 8.0.1 @@ -5845,6 +5848,10 @@ packages: omggif: 1.0.10 dev: true + /given2@2.1.7: + resolution: {integrity: sha512-fI3VamsjN2euNVguGpSt2uExyDSMfJoK+SwDxbmV+Thf3v4oF6KKZAFE3LHHuT+PYyMwCsJYXO01TW3euFdPGA==} + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} From 9c87925be4b2c92a0150b3c108e8cde463a452ac Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 21:24:20 +0100 Subject: [PATCH 09/29] step --- src/__tests__/posthog-core.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 97b3802eb..1d3cc16bd 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -6,7 +6,7 @@ import { document, window } from '../utils/globals' import { uuidv7 } from '../uuidv7' import * as globals from '../utils/globals' import { USER_STATE } from '../constants' -import { defaultPostHog } from './helpers/posthog-instance' +import { createPosthogInstance, defaultPostHog } from './helpers/posthog-instance' jest.mock('../decide') @@ -28,6 +28,7 @@ describe('posthog core', () => { // Make sure there's no cached persistence given.lib.persistence?.clear?.() }) + describe('capture()', () => { given('eventName', () => '$event') @@ -1114,6 +1115,7 @@ describe('posthog core', () => { }) }) }) + describe('session_id', () => { given('overrides', () => ({ sessionManager: { @@ -1145,8 +1147,9 @@ describe('posthog core', () => { }) }) - test('deprecated web performance observer still exposes _forceAllowLocalhost', () => { - expect(given.lib.webPerformance._forceAllowLocalhost).toBe(false) - expect(() => given.lib.webPerformance._forceAllowLocalhost).not.toThrow() + it('deprecated web performance observer still exposes _forceAllowLocalhost', async () => { + const posthog = await createPosthogInstance(uuidv7()) + expect(posthog.webPerformance._forceAllowLocalhost).toBe(false) + expect(() => posthog.webPerformance._forceAllowLocalhost).not.toThrow() }) }) From dc0b88d39e5f677cf102342aec6491e592f82c23 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 21:27:45 +0100 Subject: [PATCH 10/29] step --- src/__tests__/posthog-core.js | 38 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 1d3cc16bd..68ecc0775 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -1117,32 +1117,38 @@ describe('posthog core', () => { }) describe('session_id', () => { - given('overrides', () => ({ - sessionManager: { - checkAndGetSessionAndWindowId: jest.fn().mockReturnValue({ - windowId: 'windowId', - sessionId: 'sessionId', - sessionStartTimestamp: new Date().getTime() - 30000, - }), - }, - })) + let instance + let token + + beforeEach(async () => { + token = uuidv7() + instance = await createPosthogInstance(token, { + api_host: 'https://us.posthog.com', + }) + instance.sessionManager.checkAndGetSessionAndWindowId = jest.fn().mockReturnValue({ + windowId: 'windowId', + sessionId: 'sessionId', + sessionStartTimestamp: new Date().getTime() - 30000, + }) + }) + it('returns the session_id', () => { - expect(given.lib.get_session_id()).toEqual('sessionId') + expect(instance.get_session_id()).toEqual('sessionId') }) it('returns the replay URL', () => { - expect(given.lib.get_session_replay_url()).toEqual( - 'https://us.posthog.com/project/testtoken/replay/sessionId' + expect(instance.get_session_replay_url()).toEqual( + `https://us.posthog.com/project/${token}/replay/sessionId` ) }) it('returns the replay URL including timestamp', () => { - expect(given.lib.get_session_replay_url({ withTimestamp: true })).toEqual( - 'https://us.posthog.com/project/testtoken/replay/sessionId?t=20' // default lookback is 10 seconds + expect(instance.get_session_replay_url({ withTimestamp: true })).toEqual( + `https://us.posthog.com/project/${token}/replay/sessionId?t=20` // default lookback is 10 seconds ) - expect(given.lib.get_session_replay_url({ withTimestamp: true, timestampLookBack: 0 })).toEqual( - 'https://us.posthog.com/project/testtoken/replay/sessionId?t=30' + expect(instance.get_session_replay_url({ withTimestamp: true, timestampLookBack: 0 })).toEqual( + `https://us.posthog.com/project/${token}/replay/sessionId?t=30` ) }) }) From 976316248a19adcf69678d195247f62d3847f45b Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 21:57:25 +0100 Subject: [PATCH 11/29] step --- src/__tests__/posthog-core.js | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 68ecc0775..c99d30eaf 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -1086,28 +1086,41 @@ describe('posthog core', () => { describe('capturing pageviews', () => { it('captures not capture pageview if disabled', async () => { - given('config', () => ({ + jest.useFakeTimers() + + const instance = await createPosthogInstance(uuidv7(), { capture_pageview: false, - loaded: jest.fn(), - })) + }) + instance.capture = jest.fn() - given.subject() + // TODO you shouldn't need to emit an event to get the pending timer to emit the pageview + // but you do :shrug: + instance.capture('not a pageview', {}) - expect(given.overrides.capture).not.toHaveBeenCalled() + jest.runOnlyPendingTimers() + + expect(instance.capture).not.toHaveBeenLastCalledWith( + '$pageview', + { title: 'test' }, + { send_instantly: true } + ) }) it('captures pageview if enabled', async () => { jest.useFakeTimers() - given('config', () => ({ + + const instance = await createPosthogInstance(uuidv7(), { capture_pageview: true, - loaded: jest.fn(), - })) + }) + instance.capture = jest.fn() - given.subject() + // TODO you shouldn't need to emit an event to get the pending timer to emit the pageview + // but you do :shrug: + instance.capture('not a pageview', {}) jest.runOnlyPendingTimers() - expect(given.overrides.capture).toHaveBeenCalledWith( + expect(instance.capture).toHaveBeenLastCalledWith( '$pageview', { title: 'test' }, { send_instantly: true } From 34b91ab309e2e22637711e17e340e6d900e7acdb Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 25 Jul 2024 22:26:25 +0100 Subject: [PATCH 12/29] step --- src/__tests__/posthog-core.js | 83 +++++++++++++++++------------------ 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index c99d30eaf..e2334b6f7 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -1064,68 +1064,65 @@ describe('posthog core', () => { Decide.mockReset() }) - it('is called by default', () => { - given.subject() + it('is called by default', async () => { + const instance = await createPosthogInstance(uuidv7()) + instance.featureFlags.setReloadingPaused = jest.fn() + instance._loaded() expect(new Decide().call).toHaveBeenCalled() - expect(given.overrides.featureFlags.setReloadingPaused).toHaveBeenCalledWith(true) + expect(instance.featureFlags.setReloadingPaused).toHaveBeenCalledWith(true) }) - it('does not call decide if disabled', () => { - given('config', () => ({ + it('does not call decide if disabled', async () => { + const instance = await createPosthogInstance(uuidv7(), { advanced_disable_decide: true, - loaded: jest.fn(), - })) - - given.subject() + }) + instance.featureFlags.setReloadingPaused = jest.fn() + instance._loaded() expect(new Decide().call).not.toHaveBeenCalled() - expect(given.overrides.featureFlags.setReloadingPaused).not.toHaveBeenCalled() + expect(instance.featureFlags.setReloadingPaused).not.toHaveBeenCalled() }) }) + }) - describe('capturing pageviews', () => { - it('captures not capture pageview if disabled', async () => { - jest.useFakeTimers() + describe('capturing pageviews', () => { + it('captures not capture pageview if disabled', async () => { + jest.useFakeTimers() - const instance = await createPosthogInstance(uuidv7(), { - capture_pageview: false, - }) - instance.capture = jest.fn() + const instance = await createPosthogInstance(uuidv7(), { + capture_pageview: false, + }) + instance.capture = jest.fn() - // TODO you shouldn't need to emit an event to get the pending timer to emit the pageview - // but you do :shrug: - instance.capture('not a pageview', {}) + // TODO you shouldn't need to emit an event to get the pending timer to emit the pageview + // but you do :shrug: + instance.capture('not a pageview', {}) - jest.runOnlyPendingTimers() + jest.runOnlyPendingTimers() - expect(instance.capture).not.toHaveBeenLastCalledWith( - '$pageview', - { title: 'test' }, - { send_instantly: true } - ) - }) + expect(instance.capture).not.toHaveBeenLastCalledWith( + '$pageview', + { title: 'test' }, + { send_instantly: true } + ) + }) - it('captures pageview if enabled', async () => { - jest.useFakeTimers() + it('captures pageview if enabled', async () => { + jest.useFakeTimers() - const instance = await createPosthogInstance(uuidv7(), { - capture_pageview: true, - }) - instance.capture = jest.fn() + const instance = await createPosthogInstance(uuidv7(), { + capture_pageview: true, + }) + instance.capture = jest.fn() - // TODO you shouldn't need to emit an event to get the pending timer to emit the pageview - // but you do :shrug: - instance.capture('not a pageview', {}) + // TODO you shouldn't need to emit an event to get the pending timer to emit the pageview + // but you do :shrug: + instance.capture('not a pageview', {}) - jest.runOnlyPendingTimers() + jest.runOnlyPendingTimers() - expect(instance.capture).toHaveBeenLastCalledWith( - '$pageview', - { title: 'test' }, - { send_instantly: true } - ) - }) + expect(instance.capture).toHaveBeenLastCalledWith('$pageview', { title: 'test' }, { send_instantly: true }) }) }) From 74da9c32d115b7fabb48f79ab749cf4dc99888a7 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 26 Jul 2024 22:21:04 +0100 Subject: [PATCH 13/29] step --- src/__tests__/posthog-core.js | 164 ++++++++++++++++------------------ 1 file changed, 79 insertions(+), 85 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index e2334b6f7..561b2b86e 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -1,4 +1,3 @@ -import { PostHogPersistence } from '../posthog-persistence' import { Decide } from '../decide' import { Info } from '../utils/event-utils' @@ -838,100 +837,94 @@ describe('posthog core', () => { }) describe('group()', () => { - given('captureQueue', () => jest.fn()) - given('overrides', () => ({ - persistence: new PostHogPersistence(given.config), - capture: jest.fn(), - _captureMetrics: { - incr: jest.fn(), - }, - reloadFeatureFlags: jest.fn(), - })) - given('config', () => ({ - request_batching: true, - persistence: 'memory', - property_denylist: [], - property_blacklist: [], - _onCapture: jest.fn(), - })) + let posthog beforeEach(() => { - given.overrides.persistence.clear() + posthog = defaultPostHog().init( + 'testtoken', + { + persistence: 'memory', + }, + uuidv7() + ) + posthog.persistence.clear() + posthog.reloadFeatureFlags = jest.fn() + posthog.capture = jest.fn() }) it('records info on groups', () => { - given.lib.group('organization', 'org::5') - expect(given.lib.getGroups()).toEqual({ organization: 'org::5' }) + posthog.group('organization', 'org::5') + expect(posthog.getGroups()).toEqual({ organization: 'org::5' }) - given.lib.group('organization', 'org::6') - expect(given.lib.getGroups()).toEqual({ organization: 'org::6' }) + posthog.group('organization', 'org::6') + expect(posthog.getGroups()).toEqual({ organization: 'org::6' }) - given.lib.group('instance', 'app.posthog.com') - expect(given.lib.getGroups()).toEqual({ organization: 'org::6', instance: 'app.posthog.com' }) + posthog.group('instance', 'app.posthog.com') + expect(posthog.getGroups()).toEqual({ organization: 'org::6', instance: 'app.posthog.com' }) }) it('records info on groupProperties for groups', () => { - given.lib.group('organization', 'org::5', { name: 'PostHog' }) - expect(given.lib.getGroups()).toEqual({ organization: 'org::5' }) + posthog.group('organization', 'org::5', { name: 'PostHog' }) + expect(posthog.getGroups()).toEqual({ organization: 'org::5' }) - expect(given.lib.persistence.props['$stored_group_properties']).toEqual({ + expect(posthog.persistence.props['$stored_group_properties']).toEqual({ organization: { name: 'PostHog' }, }) - given.lib.group('organization', 'org::6') - expect(given.lib.getGroups()).toEqual({ organization: 'org::6' }) - expect(given.lib.persistence.props['$stored_group_properties']).toEqual({ organization: {} }) + posthog.group('organization', 'org::6') + expect(posthog.getGroups()).toEqual({ organization: 'org::6' }) + expect(posthog.persistence.props['$stored_group_properties']).toEqual({ organization: {} }) - given.lib.group('instance', 'app.posthog.com') - expect(given.lib.getGroups()).toEqual({ organization: 'org::6', instance: 'app.posthog.com' }) - expect(given.lib.persistence.props['$stored_group_properties']).toEqual({ organization: {}, instance: {} }) + posthog.group('instance', 'app.posthog.com') + expect(posthog.getGroups()).toEqual({ organization: 'org::6', instance: 'app.posthog.com' }) + expect(posthog.persistence.props['$stored_group_properties']).toEqual({ organization: {}, instance: {} }) // now add properties to the group - given.lib.group('organization', 'org::7', { name: 'PostHog2' }) - expect(given.lib.getGroups()).toEqual({ organization: 'org::7', instance: 'app.posthog.com' }) - expect(given.lib.persistence.props['$stored_group_properties']).toEqual({ + posthog.group('organization', 'org::7', { name: 'PostHog2' }) + expect(posthog.getGroups()).toEqual({ organization: 'org::7', instance: 'app.posthog.com' }) + expect(posthog.persistence.props['$stored_group_properties']).toEqual({ organization: { name: 'PostHog2' }, instance: {}, }) - given.lib.group('instance', 'app.posthog.com', { a: 'b' }) - expect(given.lib.getGroups()).toEqual({ organization: 'org::7', instance: 'app.posthog.com' }) - expect(given.lib.persistence.props['$stored_group_properties']).toEqual({ + posthog.group('instance', 'app.posthog.com', { a: 'b' }) + expect(posthog.getGroups()).toEqual({ organization: 'org::7', instance: 'app.posthog.com' }) + expect(posthog.persistence.props['$stored_group_properties']).toEqual({ organization: { name: 'PostHog2' }, instance: { a: 'b' }, }) - given.lib.resetGroupPropertiesForFlags() - expect(given.lib.persistence.props['$stored_group_properties']).toEqual(undefined) + posthog.resetGroupPropertiesForFlags() + expect(posthog.persistence.props['$stored_group_properties']).toEqual(undefined) }) it('does not result in a capture call', () => { - given.lib.group('organization', 'org::5') + posthog.group('organization', 'org::5') - expect(given.overrides.capture).not.toHaveBeenCalled() + expect(posthog.capture).not.toHaveBeenCalled() }) it('results in a reloadFeatureFlags call if group changes', () => { - given.lib.group('organization', 'org::5', { name: 'PostHog' }) - given.lib.group('instance', 'app.posthog.com') - given.lib.group('organization', 'org::5') + posthog.group('organization', 'org::5', { name: 'PostHog' }) + posthog.group('instance', 'app.posthog.com') + posthog.group('organization', 'org::5') - expect(given.overrides.reloadFeatureFlags).toHaveBeenCalledTimes(2) + expect(posthog.reloadFeatureFlags).toHaveBeenCalledTimes(2) }) it('results in a reloadFeatureFlags call if group properties change', () => { - given.lib.group('organization', 'org::5') - given.lib.group('instance', 'app.posthog.com') - given.lib.group('organization', 'org::5', { name: 'PostHog' }) - given.lib.group('instance', 'app.posthog.com') + posthog.group('organization', 'org::5') + posthog.group('instance', 'app.posthog.com') + posthog.group('organization', 'org::5', { name: 'PostHog' }) + posthog.group('instance', 'app.posthog.com') - expect(given.overrides.reloadFeatureFlags).toHaveBeenCalledTimes(3) + expect(posthog.reloadFeatureFlags).toHaveBeenCalledTimes(3) }) it('captures $groupidentify event', () => { - given.lib.group('organization', 'org::5', { group: 'property', foo: 5 }) + posthog.group('organization', 'org::5', { group: 'property', foo: 5 }) - expect(given.overrides.capture).toHaveBeenCalledWith('$groupidentify', { + expect(posthog.capture).toHaveBeenCalledWith('$groupidentify', { $group_type: 'organization', $group_key: 'org::5', $group_set: { @@ -942,29 +935,30 @@ describe('posthog core', () => { }) describe('subsequent capture calls', () => { - given('overrides', () => ({ - __loaded: true, - config: { - api_host: 'https://app.posthog.com', - ...given.config, - }, - persistence: new PostHogPersistence(given.config), - sessionPersistence: new PostHogPersistence(given.config), - _requestQueue: { - enqueue: given.captureQueue, - }, - reloadFeatureFlags: jest.fn(), - })) + beforeEach(() => { + posthog = defaultPostHog().init( + 'testtoken', + { + persistence: 'memory', + }, + uuidv7() + ) + posthog.persistence.clear() + // mock this internal queue - not capture + posthog._requestQueue = { + enqueue: jest.fn(), + } + }) it('sends group information in event properties', () => { - given.lib.group('organization', 'org::5') - given.lib.group('instance', 'app.posthog.com') + posthog.group('organization', 'org::5') + posthog.group('instance', 'app.posthog.com') - given.lib.capture('some_event', { prop: 5 }) + posthog.capture('some_event', { prop: 5 }) - expect(given.captureQueue).toHaveBeenCalledTimes(1) + expect(posthog._requestQueue.enqueue).toHaveBeenCalledTimes(1) - const eventPayload = given.captureQueue.mock.calls[0][0] + const eventPayload = posthog._requestQueue.enqueue.mock.calls[0][0] expect(eventPayload.data.event).toEqual('some_event') expect(eventPayload.data.properties.$groups).toEqual({ organization: 'org::5', @@ -982,11 +976,11 @@ describe('posthog core', () => { window.console.error = jest.fn() window.console.warn = jest.fn() - given.lib.group(null, 'foo') - given.lib.group('organization', null) - given.lib.group('organization', undefined) - given.lib.group('organization', '') - given.lib.group('', 'foo') + posthog.group(null, 'foo') + posthog.group('organization', null) + posthog.group('organization', undefined) + posthog.group('organization', '') + posthog.group('', 'foo') expect(given.overrides.register).not.toHaveBeenCalled() }) @@ -994,15 +988,15 @@ describe('posthog core', () => { describe('reset group', () => { it('groups property is empty and reloads feature flags', () => { - given.lib.group('organization', 'org::5') - given.lib.group('instance', 'app.posthog.com', { group: 'property', foo: 5 }) + posthog.group('organization', 'org::5') + posthog.group('instance', 'app.posthog.com', { group: 'property', foo: 5 }) - expect(given.lib.persistence.props['$groups']).toEqual({ + expect(posthog.persistence.props['$groups']).toEqual({ organization: 'org::5', instance: 'app.posthog.com', }) - expect(given.lib.persistence.props['$stored_group_properties']).toEqual({ + expect(posthog.persistence.props['$stored_group_properties']).toEqual({ organization: {}, instance: { group: 'property', @@ -1010,12 +1004,12 @@ describe('posthog core', () => { }, }) - given.lib.resetGroups() + posthog.resetGroups() - expect(given.lib.persistence.props['$groups']).toEqual({}) - expect(given.lib.persistence.props['$stored_group_properties']).toEqual(undefined) + expect(posthog.persistence.props['$groups']).toEqual({}) + expect(posthog.persistence.props['$stored_group_properties']).toEqual(undefined) - expect(given.overrides.reloadFeatureFlags).toHaveBeenCalledTimes(3) + expect(posthog.reloadFeatureFlags).toHaveBeenCalledTimes(3) }) }) }) From 28c816f25009e3b77c63e92ded8512aa5b064e78 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 26 Jul 2024 22:40:37 +0100 Subject: [PATCH 14/29] step --- src/__tests__/posthog-core.js | 76 ++++++++++++++--------------------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 561b2b86e..cfaa30ecf 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -731,68 +731,47 @@ describe('posthog core', () => { describe('init()', () => { jest.spyOn(window, 'window', 'get') - given('overrides', () => ({ - get_distinct_id: () => given.distinct_id, - advanced_disable_decide: given.advanced_disable_decide, - _send_request: jest.fn(), - capture: jest.fn(), - register_once: jest.fn(), - })) - beforeEach(() => { jest.spyOn(window.console, 'warn').mockImplementation() jest.spyOn(window.console, 'error').mockImplementation() }) - given('advanced_disable_decide', () => true) - it('can set an xhr error handler', () => { const fakeOnXHRError = 'configured error' - given('subject', () => - given.lib.init( - 'a-token', - { - on_xhr_error: fakeOnXHRError, - }, - 'a-name' - ) + const posthog = defaultPostHog().init( + 'a-token', + { + on_xhr_error: fakeOnXHRError, + }, + 'a-name' ) - expect(given.subject.config.on_xhr_error).toBe(fakeOnXHRError) - }) - - it('does not load decide endpoint on advanced_disable_decide', () => { - expect(given.decide).toBe(undefined) - expect(given.overrides._send_request.mock.calls.length).toBe(0) // No outgoing requests + expect(posthog.config.on_xhr_error).toBe(fakeOnXHRError) }) it('does not load feature flags, toolbar, session recording', () => { - given('overrides', () => ({ - sessionRecording: { - afterDecideResponse: jest.fn(), - startIfEnabledOrStop: jest.fn(), - }, - toolbar: { - maybeLoadToolbar: jest.fn(), - afterDecideResponse: jest.fn(), - }, - persistence: { - register: jest.fn(), - update_config: jest.fn(), - }, - })) - - jest.spyOn(given.lib.toolbar, 'afterDecideResponse').mockImplementation() - jest.spyOn(given.lib.sessionRecording, 'afterDecideResponse').mockImplementation() - jest.spyOn(given.lib.persistence, 'register').mockImplementation() + const posthog = defaultPostHog().init('testtoken', given.config, uuidv7()) + + posthog.toolbar = { + maybeLoadToolbar: jest.fn(), + afterDecideResponse: jest.fn(), + } + posthog.sessionRecording = { + afterDecideResponse: jest.fn(), + startIfEnabledOrStop: jest.fn(), + } + posthog.persistence = { + register: jest.fn(), + update_config: jest.fn(), + } // Feature flags - expect(given.lib.persistence.register).not.toHaveBeenCalled() // FFs are saved this way + expect(posthog.persistence.register).not.toHaveBeenCalled() // FFs are saved this way // Toolbar - expect(given.lib.toolbar.afterDecideResponse).not.toHaveBeenCalled() + expect(posthog.toolbar.afterDecideResponse).not.toHaveBeenCalled() // Session recording - expect(given.lib.sessionRecording.afterDecideResponse).not.toHaveBeenCalled() + expect(posthog.sessionRecording.afterDecideResponse).not.toHaveBeenCalled() }) describe('device id behavior', () => { @@ -832,7 +811,12 @@ describe('posthog core', () => { describe('skipped init()', () => { it('capture() does not throw', () => { - expect(() => given.lib.capture('$pageview')).not.toThrow() + console.error = jest.fn() + expect(() => defaultPostHog().capture('$pageview')).not.toThrow() + expect(console.error).toHaveBeenCalledWith( + '[PostHog.js]', + 'You must initialize PostHog before calling posthog.capture' + ) }) }) From 6caf88ef21ef45c598672c36b4ece36423173829 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 26 Jul 2024 22:50:14 +0100 Subject: [PATCH 15/29] step --- src/__tests__/posthog-core.js | 46 +++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index cfaa30ecf..53a94b11c 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -775,33 +775,53 @@ describe('posthog core', () => { }) describe('device id behavior', () => { + let uninitialisedPostHog + beforeEach(() => { + uninitialisedPostHog = defaultPostHog() + }) + it('sets a random UUID as distinct_id/$device_id if distinct_id is unset', () => { - given('distinct_id', () => undefined) - given('config', () => ({ - get_device_id: (uuid) => uuid, - })) + uninitialisedPostHog.persistence = { props: { distinct_id: undefined } } + const posthog = uninitialisedPostHog.init( + 'testtoken', + { + get_device_id: (uuid) => uuid, + }, + uuidv7() + ) - expect(given.lib.persistence.props).toMatchObject({ + expect(posthog.persistence.props).toMatchObject({ $device_id: expect.stringMatching(/^[0-9a-f-]+$/), distinct_id: expect.stringMatching(/^[0-9a-f-]+$/), }) - expect(given.lib.persistence.props.$device_id).toEqual(given.lib.persistence.props.distinct_id) + expect(posthog.persistence.props.$device_id).toEqual(posthog.persistence.props.distinct_id) }) it('does not set distinct_id/$device_id if distinct_id is unset', () => { - given('distinct_id', () => 'existing-id') + uninitialisedPostHog.persistence = { props: { distinct_id: 'existing-id' } } + const posthog = uninitialisedPostHog.init( + 'testtoken', + { + get_device_id: (uuid) => uuid, + }, + uuidv7() + ) - expect(given.lib.persistence.props.distinct_id).not.toEqual('existing-id') + expect(posthog.persistence.props.distinct_id).not.toEqual('existing-id') }) it('uses config.get_device_id for uuid generation if passed', () => { - given('distinct_id', () => undefined) - given('config', () => ({ - get_device_id: (uuid) => 'custom-' + uuid.slice(0, 8), - })) + uninitialisedPostHog.persistence = { props: { distinct_id: undefined } } + const posthog = uninitialisedPostHog.init( + 'testtoken', + { + get_device_id: (uuid) => 'custom-' + uuid.slice(0, 8), + }, + uuidv7() + ) - expect(given.lib.persistence.props).toMatchObject({ + expect(posthog.persistence.props).toMatchObject({ $device_id: expect.stringMatching(/^custom-[0-9a-f-]+$/), distinct_id: expect.stringMatching(/^custom-[0-9a-f-]+$/), }) From da6472b02bdfda90342731099c4409a39092a789 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 14:14:21 +0100 Subject: [PATCH 16/29] step --- src/__tests__/posthog-core.js | 114 +++++++++++++++++----------------- 1 file changed, 58 insertions(+), 56 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 53a94b11c..6ed0a7b4b 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -576,29 +576,27 @@ describe('posthog core', () => { }) describe('bootstrapping feature flags', () => { - given('overrides', () => ({ - _send_request: jest.fn(), - capture: jest.fn(), - })) - - afterEach(() => { - given.lib.reset() - }) + const posthogWith = (config) => { + const posthog = defaultPostHog().init('testtoken', config, uuidv7()) + posthog._send_request = jest.fn() + posthog.capture = jest.fn() + return posthog + } it('sets the right distinctID', () => { - given('config', () => ({ + const posthog = posthogWith({ bootstrap: { distinctID: 'abcd', }, - })) + }) - expect(given.lib.get_distinct_id()).toBe('abcd') - expect(given.lib.get_property('$device_id')).toBe('abcd') - expect(given.lib.persistence.get_property(USER_STATE)).toBe('anonymous') + expect(posthog.get_distinct_id()).toBe('abcd') + expect(posthog.get_property('$device_id')).toBe('abcd') + expect(posthog.persistence.get_property(USER_STATE)).toBe('anonymous') - given.lib.identify('efgh') + posthog.identify('efgh') - expect(given.overrides.capture).toHaveBeenCalledWith( + expect(posthog.capture).toHaveBeenCalledWith( '$identify', { distinct_id: 'efgh', @@ -609,39 +607,44 @@ describe('posthog core', () => { }) it('treats identified distinctIDs appropriately', () => { - given('config', () => ({ + const posthog = posthogWith({ bootstrap: { distinctID: 'abcd', isIdentifiedID: true, }, get_device_id: () => 'og-device-id', - })) + }) - expect(given.lib.get_distinct_id()).toBe('abcd') - expect(given.lib.get_property('$device_id')).toBe('og-device-id') - expect(given.lib.persistence.get_property(USER_STATE)).toBe('identified') + expect(posthog.get_distinct_id()).toBe('abcd') + expect(posthog.get_property('$device_id')).toBe('og-device-id') + expect(posthog.persistence.get_property(USER_STATE)).toBe('identified') - given.lib.identify('efgh') - expect(given.overrides.capture).not.toHaveBeenCalled() + posthog.identify('efgh') + expect(posthog.capture).not.toHaveBeenCalled() }) it('sets the right feature flags', () => { - given('config', () => ({ + const posthog = posthogWith({ bootstrap: { - featureFlags: { multivariant: 'variant-1', enabled: true, disabled: false, undef: undefined }, + featureFlags: { + multivariant: 'variant-1', + enabled: true, + disabled: false, + undef: undefined, + }, }, - })) + }) - expect(given.lib.get_distinct_id()).not.toBe('abcd') - expect(given.lib.get_distinct_id()).not.toEqual(undefined) - expect(given.lib.getFeatureFlag('multivariant')).toBe('variant-1') - expect(given.lib.getFeatureFlag('disabled')).toBe(undefined) - expect(given.lib.getFeatureFlag('undef')).toBe(undefined) - expect(given.lib.featureFlags.getFlagVariants()).toEqual({ multivariant: 'variant-1', enabled: true }) + expect(posthog.get_distinct_id()).not.toBe('abcd') + expect(posthog.get_distinct_id()).not.toEqual(undefined) + expect(posthog.getFeatureFlag('multivariant')).toBe('variant-1') + expect(posthog.getFeatureFlag('disabled')).toBe(undefined) + expect(posthog.getFeatureFlag('undef')).toBe(undefined) + expect(posthog.featureFlags.getFlagVariants()).toEqual({ multivariant: 'variant-1', enabled: true }) }) it('sets the right feature flag payloads', () => { - given('config', () => ({ + const posthog = posthogWith({ bootstrap: { featureFlags: { multivariant: 'variant-1', @@ -660,70 +663,69 @@ describe('posthog core', () => { jsonString: '{"a":"payload"}', }, }, - })) + }) - expect(given.lib.getFeatureFlagPayload('multivariant')).toBe('some-payload') - expect(given.lib.getFeatureFlagPayload('enabled')).toEqual({ another: 'value' }) - expect(given.lib.getFeatureFlagPayload('jsonString')).toEqual({ a: 'payload' }) - expect(given.lib.getFeatureFlagPayload('disabled')).toBe(undefined) - expect(given.lib.getFeatureFlagPayload('undef')).toBe(undefined) + expect(posthog.getFeatureFlagPayload('multivariant')).toBe('some-payload') + expect(posthog.getFeatureFlagPayload('enabled')).toEqual({ another: 'value' }) + expect(posthog.getFeatureFlagPayload('jsonString')).toEqual({ a: 'payload' }) + expect(posthog.getFeatureFlagPayload('disabled')).toBe(undefined) + expect(posthog.getFeatureFlagPayload('undef')).toBe(undefined) }) it('does nothing when empty', () => { jest.spyOn(console, 'warn').mockImplementation() - given('config', () => ({ + const posthog = posthogWith({ bootstrap: {}, - })) + }) - expect(given.lib.get_distinct_id()).not.toBe('abcd') - expect(given.lib.get_distinct_id()).not.toEqual(undefined) - expect(given.lib.getFeatureFlag('multivariant')).toBe(undefined) + expect(posthog.get_distinct_id()).not.toBe('abcd') + expect(posthog.get_distinct_id()).not.toEqual(undefined) + expect(posthog.getFeatureFlag('multivariant')).toBe(undefined) expect(console.warn).toHaveBeenCalledWith( '[PostHog.js]', expect.stringContaining('getFeatureFlag for key "multivariant" failed') ) - expect(given.lib.getFeatureFlag('disabled')).toBe(undefined) - expect(given.lib.getFeatureFlag('undef')).toBe(undefined) - expect(given.lib.featureFlags.getFlagVariants()).toEqual({}) + expect(posthog.getFeatureFlag('disabled')).toBe(undefined) + expect(posthog.getFeatureFlag('undef')).toBe(undefined) + expect(posthog.featureFlags.getFlagVariants()).toEqual({}) }) it('onFeatureFlags should be called immediately if feature flags are bootstrapped', () => { let called = false - - given('config', () => ({ + const posthog = posthogWith({ bootstrap: { featureFlags: { multivariant: 'variant-1' }, }, - })) + }) - given.lib.featureFlags.onFeatureFlags(() => (called = true)) + posthog.featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(true) }) it('onFeatureFlags should not be called immediately if feature flags bootstrap is empty', () => { let called = false - given('config', () => ({ + const posthog = posthogWith({ bootstrap: { featureFlags: {}, }, - })) + }) - given.lib.featureFlags.onFeatureFlags(() => (called = true)) + posthog.featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(false) }) it('onFeatureFlags should not be called immediately if feature flags bootstrap is undefined', () => { let called = false - given('config', () => ({ + const posthog = posthogWith({ bootstrap: { featureFlags: undefined, }, - })) + }) - given.lib.featureFlags.onFeatureFlags(() => (called = true)) + posthog.featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(false) }) }) From ea0feae12e99cdd9a7ba28357b0a4b2af9e3653d Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 14:26:38 +0100 Subject: [PATCH 17/29] step --- src/__tests__/posthog-core.js | 81 +++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 6ed0a7b4b..6cab305df 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -18,6 +18,16 @@ describe('posthog core', () => { return Object.assign(posthog, given.overrides) }) + const posthogWith = (config) => { + const posthog = defaultPostHog().init('testtoken', config, uuidv7()) + posthog._send_request = jest.fn() + posthog.capture = jest.fn() + posthog._requestQueue = { + unload: jest.fn(), + } + return posthog + } + beforeEach(() => { jest.useFakeTimers().setSystemTime(baseUTCDateTime) }) @@ -518,59 +528,76 @@ describe('posthog core', () => { given('batching', () => true) it('captures $pageleave', () => { - given.subject() + const posthog = posthogWith({ + capture_pageview: true, + capture_pageleave: 'if_capture_pageview', + batching: true, + }) - expect(given.overrides.capture).toHaveBeenCalledWith('$pageleave') + posthog._handle_unload() + + expect(posthog.capture).toHaveBeenCalledWith('$pageleave') }) it('does not capture $pageleave when capture_pageview=false and capture_pageleave=if_capture_pageview', () => { - given('capturePageleave', () => 'if_capture_pageview') - given('capturePageview', () => false) - - given.subject() - - expect(given.overrides.capture).not.toHaveBeenCalled() - }) + const posthog = posthogWith({ + capture_pageview: false, + capture_pageleave: 'if_capture_pageview', + batching: true, + }) - it('does capture $pageleave when capture_pageview=true and capture_pageleave=if_capture_pageview', () => { - given('capturePageleave', () => 'if_capture_pageview') - given('capturePageview', () => true) + posthog._handle_unload() - given.subject() - - expect(given.overrides.capture).toHaveBeenCalled() + expect(posthog.capture).not.toHaveBeenCalled() }) it('does capture $pageleave when capture_pageview=false and capture_pageleave=true', () => { - given('capturePageleave', () => true) - given('capturePageview', () => false) + const posthog = posthogWith({ + capture_pageview: false, + capture_pageleave: true, + batching: true, + }) - given.subject() + posthog._handle_unload() - expect(given.overrides.capture).toHaveBeenCalled() + expect(posthog.capture).toHaveBeenCalledWith('$pageleave') }) it('calls requestQueue unload', () => { - given.subject() + const posthog = posthogWith({ + capture_pageview: true, + capture_pageleave: 'if_capture_pageview', + batching: true, + }) + + posthog._handle_unload() - expect(given.overrides._requestQueue.unload).toHaveBeenCalledTimes(1) + expect(posthog._requestQueue.unload).toHaveBeenCalledTimes(1) }) describe('without batching', () => { given('batching', () => false) it('captures $pageleave', () => { - given.subject() + const posthog = posthogWith({ + capture_pageview: true, + capture_pageleave: 'if_capture_pageview', + request_batching: false, + }) + posthog._handle_unload() - expect(given.overrides.capture).toHaveBeenCalledWith('$pageleave', null, { transport: 'sendBeacon' }) + expect(posthog.capture).toHaveBeenCalledWith('$pageleave', null, { transport: 'sendBeacon' }) }) it('does not capture $pageleave when capture_pageview=false', () => { - given('capturePageview', () => false) - - given.subject() + const posthog = posthogWith({ + capture_pageview: false, + capture_pageleave: 'if_capture_pageview', + request_batching: false, + }) + posthog._handle_unload() - expect(given.overrides.capture).not.toHaveBeenCalled() + expect(posthog.capture).not.toHaveBeenCalled() }) }) }) From 2a0e6422fb48ffa66b5263954de8941167af30e0 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 14:41:31 +0100 Subject: [PATCH 18/29] step --- src/__tests__/posthog-core.js | 140 ++++++++++++++++------------------ 1 file changed, 64 insertions(+), 76 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 6cab305df..8670290c5 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -18,8 +18,12 @@ describe('posthog core', () => { return Object.assign(posthog, given.overrides) }) - const posthogWith = (config) => { + const posthogWith = (config, overrides) => { const posthog = defaultPostHog().init('testtoken', config, uuidv7()) + if (overrides) { + return Object.assign(posthog, overrides) + } + posthog._send_request = jest.fn() posthog.capture = jest.fn() posthog._requestQueue = { @@ -365,22 +369,9 @@ describe('posthog core', () => { }) describe('_calculate_event_properties()', () => { - given('subject', () => - given.lib._calculate_event_properties( - given.event_name, - given.properties, - given.start_timestamp, - given.options - ) - ) - - given('event_name', () => 'custom_event') - given('properties', () => ({ event: 'prop' })) - - given('options', () => ({})) + let posthog - given('overrides', () => ({ - config: given.config, + const overrides = { persistence: { properties: () => ({ distinct_id: 'abc', persistent: 'prop', $is_identified: false }), remove_event_timer: jest.fn(), @@ -396,24 +387,25 @@ describe('posthog core', () => { sessionId: 'sessionId', }), }, - })) - - given('config', () => ({ - api_host: 'https://app.posthog.com', - token: 'testtoken', - property_denylist: given.property_denylist, - property_blacklist: given.property_blacklist, - sanitize_properties: given.sanitize_properties, - })) - given('property_denylist', () => []) - given('property_blacklist', () => []) + } beforeEach(() => { jest.spyOn(Info, 'properties').mockReturnValue({ $lib: 'web' }) + + posthog = posthogWith( + { + api_host: 'https://app.posthog.com', + token: 'testtoken', + property_denylist: [], + property_blacklist: [], + sanitize_properties: undefined, + }, + overrides + ) }) it('returns calculated properties', () => { - expect(given.subject).toEqual({ + expect(posthog._calculate_event_properties('custom_event', { event: 'prop' })).toEqual({ token: 'testtoken', event: 'prop', $lib: 'web', @@ -427,14 +419,14 @@ describe('posthog core', () => { }) it('sets $lib_custom_api_host if api_host is not the default', () => { - given('config', () => ({ - api_host: 'https://custom.posthog.com', - token: 'testtoken', - property_denylist: given.property_denylist, - property_blacklist: given.property_blacklist, - sanitize_properties: given.sanitize_properties, - })) - expect(given.subject).toEqual({ + posthog = posthogWith( + { + api_host: 'https://custom.posthog.com', + }, + overrides + ) + + expect(posthog._calculate_event_properties('custom_event', { event: 'prop' })).toEqual({ token: 'testtoken', event: 'prop', $lib: 'web', @@ -449,37 +441,56 @@ describe('posthog core', () => { }) it("can't deny or blacklist $process_person_profile", () => { - given('property_denylist', () => ['$process_person_profile']) - given('property_blacklist', () => ['$process_person_profile']) + posthog = posthogWith( + { + api_host: 'https://custom.posthog.com', + property_denylist: ['$process_person_profile'], + property_blacklist: ['$process_person_profile'], + }, + overrides + ) - expect(given.subject['$process_person_profile']).toEqual(true) + expect( + posthog._calculate_event_properties('custom_event', { event: 'prop' })['$process_person_profile'] + ).toEqual(true) }) it('only adds token and distinct_id if event_name is $snapshot', () => { - given('event_name', () => '$snapshot') - expect(given.subject).toEqual({ + posthog = posthogWith( + { + api_host: 'https://custom.posthog.com', + }, + overrides + ) + + expect(posthog._calculate_event_properties('$snapshot', { event: 'prop' })).toEqual({ token: 'testtoken', event: 'prop', distinct_id: 'abc', }) - expect(given.overrides.sessionManager.checkAndGetSessionAndWindowId).not.toHaveBeenCalled() + expect(posthog.sessionManager.checkAndGetSessionAndWindowId).not.toHaveBeenCalled() }) it('calls sanitize_properties', () => { - given('sanitize_properties', () => (props, event_name) => ({ token: props.token, event_name })) + posthog = posthogWith( + { + api_host: 'https://custom.posthog.com', + sanitize_properties: (props, event_name) => ({ token: props.token, event_name }), + }, + overrides + ) - expect(given.subject).toEqual({ - event_name: given.event_name, + expect(posthog._calculate_event_properties('custom_event', { event: 'prop' })).toEqual({ + event_name: 'custom_event', token: 'testtoken', $process_person_profile: true, }) }) it('saves $snapshot data and token for $snapshot events', () => { - given('event_name', () => '$snapshot') - given('properties', () => ({ $snapshot_data: {} })) + posthog = posthogWith({}, overrides) - expect(given.subject).toEqual({ + expect(posthog._calculate_event_properties('$snapshot', { $snapshot_data: {} })).toEqual({ token: 'testtoken', $snapshot_data: {}, distinct_id: 'abc', @@ -488,7 +499,8 @@ describe('posthog core', () => { it("doesn't modify properties passed into it", () => { const properties = { prop1: 'val1', prop2: 'val2' } - given.lib._calculate_event_properties(given.event_name, properties, given.start_timestamp, given.options) + + posthog._calculate_event_properties('custom_event', properties) expect(Object.keys(properties)).toEqual(['prop1', 'prop2']) }) @@ -496,37 +508,13 @@ describe('posthog core', () => { it('adds page title to $pageview', () => { document.title = 'test' - given('event_name', () => '$pageview') - - expect(given.subject).toEqual(expect.objectContaining({ title: 'test' })) + expect(posthog._calculate_event_properties('$pageview', {})).toEqual( + expect.objectContaining({ title: 'test' }) + ) }) }) describe('_handle_unload()', () => { - given('subject', () => () => given.lib._handle_unload()) - - given('overrides', () => ({ - config: given.config, - capture: jest.fn(), - compression: {}, - _requestQueue: { - unload: jest.fn(), - }, - _retryQueue: { - unload: jest.fn(), - }, - })) - - given('config', () => ({ - capture_pageview: given.capturePageview, - capture_pageleave: given.capturePageleave, - request_batching: given.batching, - })) - - given('capturePageview', () => true) - given('capturePageleave', () => 'if_capture_pageview') - given('batching', () => true) - it('captures $pageleave', () => { const posthog = posthogWith({ capture_pageview: true, From e8b882fa26e70303c65bdaa2c4aaa8c917ecf675 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 14:43:49 +0100 Subject: [PATCH 19/29] step --- src/__tests__/posthog-core.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 8670290c5..a8da964dc 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -591,13 +591,6 @@ describe('posthog core', () => { }) describe('bootstrapping feature flags', () => { - const posthogWith = (config) => { - const posthog = defaultPostHog().init('testtoken', config, uuidv7()) - posthog._send_request = jest.fn() - posthog.capture = jest.fn() - return posthog - } - it('sets the right distinctID', () => { const posthog = posthogWith({ bootstrap: { From 7a7b0acb94ed5c8c9263b6da18f0f0f91dd1adb0 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 14:47:47 +0100 Subject: [PATCH 20/29] step --- src/__tests__/posthog-core.js | 38 ++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index a8da964dc..39802da32 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -329,42 +329,44 @@ describe('posthog core', () => { }) describe('_afterDecideResponse', () => { - given('subject', () => () => given.lib._afterDecideResponse(given.decideResponse)) - it('enables compression from decide response', () => { - given('decideResponse', () => ({ supportedCompression: ['gzip-js', 'base64'] })) - given.subject() + const posthog = posthogWith({}) + + posthog._afterDecideResponse({ supportedCompression: ['gzip-js', 'base64'] }) - expect(given.lib.compression).toEqual('gzip-js') + expect(posthog.compression).toEqual('gzip-js') }) it('enables compression from decide response when only one received', () => { - given('decideResponse', () => ({ supportedCompression: ['base64'] })) - given.subject() + const posthog = posthogWith({}) + + posthog._afterDecideResponse({ supportedCompression: ['base64'] }) - expect(given.lib.compression).toEqual('base64') + expect(posthog.compression).toEqual('base64') }) it('does not enable compression from decide response if compression is disabled', () => { - given('config', () => ({ disable_compression: true, persistence: 'memory' })) - given('decideResponse', () => ({ supportedCompression: ['gzip-js', 'base64'] })) - given.subject() + const posthog = posthogWith({ disable_compression: true, persistence: 'memory' }) - expect(given.lib.compression).toEqual(undefined) + posthog._afterDecideResponse({ supportedCompression: ['gzip-js', 'base64'] }) + + expect(posthog.compression).toEqual(undefined) }) it('defaults to /e if no endpoint is given', () => { - given('decideResponse', () => ({})) - given.subject() + const posthog = posthogWith({}) + + posthog._afterDecideResponse({}) - expect(given.lib.analyticsDefaultEndpoint).toEqual('/e/') + expect(posthog.analyticsDefaultEndpoint).toEqual('/e/') }) it('uses the specified analytics endpoint if given', () => { - given('decideResponse', () => ({ analytics: { endpoint: '/i/v0/e/' } })) - given.subject() + const posthog = posthogWith({}) + + posthog._afterDecideResponse({ analytics: { endpoint: '/i/v0/e/' } }) - expect(given.lib.analyticsDefaultEndpoint).toEqual('/i/v0/e/') + expect(posthog.analyticsDefaultEndpoint).toEqual('/i/v0/e/') }) }) From 385aa63bf9726e0c4cecfcf7aa4adcd9bd09a179 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 17:21:44 +0100 Subject: [PATCH 21/29] step --- src/__tests__/posthog-core.js | 249 +++++++++++++++++++--------------- 1 file changed, 141 insertions(+), 108 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 39802da32..207d967c3 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -6,17 +6,13 @@ import { uuidv7 } from '../uuidv7' import * as globals from '../utils/globals' import { USER_STATE } from '../constants' import { createPosthogInstance, defaultPostHog } from './helpers/posthog-instance' +import { logger } from '../utils/logger' jest.mock('../decide') describe('posthog core', () => { const baseUTCDateTime = new Date(Date.UTC(2020, 0, 1, 0, 0, 0)) - - given('lib', () => { - const posthog = defaultPostHog().init('testtoken', given.config, uuidv7()) - posthog.debug() - return Object.assign(posthog, given.overrides) - }) + const eventName = '$event' const posthogWith = (config, overrides) => { const posthog = defaultPostHog().init('testtoken', config, uuidv7()) @@ -39,28 +35,20 @@ describe('posthog core', () => { afterEach(() => { jest.useRealTimers() // Make sure there's no cached persistence - given.lib.persistence?.clear?.() + // given.lib.persistence?.clear?.() }) describe('capture()', () => { - given('eventName', () => '$event') - - given('subject', () => () => given.lib.capture(given.eventName, given.eventProperties, given.options)) - - given('config', () => ({ + const config = { api_host: 'https://app.posthog.com', property_denylist: [], property_blacklist: [], - _onCapture: jest.fn(), + // _onCapture: jest.fn(), get_device_id: jest.fn().mockReturnValue('device-id'), - })) + } - given('overrides', () => ({ + const overrides = { __loaded: true, - config: { - api_host: 'https://app.posthog.com', - ...given.config, - }, persistence: { remove_event_timer: jest.fn(), properties: jest.fn(), @@ -89,24 +77,27 @@ describe('posthog core', () => { isServerRateLimited: () => false, clientRateLimitContext: () => false, }, - })) + } it('adds a UUID to each message', () => { - const captureData = given.subject() + const captureData = posthogWith(config, overrides).capture(eventName, {}, {}) expect(captureData).toHaveProperty('uuid') }) it('adds system time to events', () => { - const captureData = given.subject() - console.log(captureData) + const captureData = posthogWith(config, overrides).capture(eventName, {}, {}) + expect(captureData).toHaveProperty('timestamp') // timer is fixed at 2020-01-01 expect(captureData.timestamp).toEqual(baseUTCDateTime) }) it('captures when time is overriden by caller', () => { - given.options = { timestamp: new Date(2020, 0, 2, 12, 34) } - const captureData = given.subject() + const captureData = posthogWith(config, overrides).capture( + eventName, + {}, + { timestamp: new Date(2020, 0, 2, 12, 34) } + ) expect(captureData).toHaveProperty('timestamp') expect(captureData.timestamp).toEqual(new Date(2020, 0, 2, 12, 34)) expect(captureData.properties['$event_time_override_provided']).toEqual(true) @@ -114,21 +105,20 @@ describe('posthog core', () => { }) it('handles recursive objects', () => { - given('eventProperties', () => { - const props = {} - props.recurse = props - return props - }) + const props = {} + props.recurse = props - expect(() => given.subject()).not.toThrow() + expect(() => + posthogWith(config, overrides).capture(eventName, props, { timestamp: new Date(2020, 0, 2, 12, 34) }) + ).not.toThrow() }) it('calls callbacks added via _addCaptureHook', () => { const hook = jest.fn() + const posthog = posthogWith(config, overrides) + posthog._addCaptureHook(hook) - given.lib._addCaptureHook(hook) - - given.subject() + posthog.capture(eventName, {}, {}) expect(hook).toHaveBeenCalledWith( '$event', expect.objectContaining({ @@ -138,42 +128,45 @@ describe('posthog core', () => { }) it('calls update_campaign_params and update_referrer_info on sessionPersistence', () => { - given('config', () => ({ - property_denylist: [], - property_blacklist: [], - _onCapture: jest.fn(), - store_google: true, - save_referrer: true, - })) + const posthog = posthogWith( + { + property_denylist: [], + property_blacklist: [], + _onCapture: jest.fn(), + store_google: true, + save_referrer: true, + }, + overrides + ) - given.subject() + posthog.capture(eventName, {}, {}) - expect(given.lib.sessionPersistence.update_campaign_params).toHaveBeenCalled() - expect(given.lib.sessionPersistence.update_referrer_info).toHaveBeenCalled() + expect(posthog.sessionPersistence.update_campaign_params).toHaveBeenCalled() + expect(posthog.sessionPersistence.update_referrer_info).toHaveBeenCalled() }) it('errors with undefined event name', () => { - given('eventName', () => undefined) - console.error = jest.fn() - const hook = jest.fn() - given.lib._addCaptureHook(hook) - expect(() => given.subject()).not.toThrow() + const posthog = posthogWith(config, overrides) + posthog._addCaptureHook(hook) + jest.spyOn(logger, 'error').mockImplementation() + + expect(() => posthog.capture(undefined)).not.toThrow() expect(hook).not.toHaveBeenCalled() - expect(console.error).toHaveBeenCalledWith('[PostHog.js]', 'No event name provided to posthog.capture') + expect(logger.error).toHaveBeenCalledWith('No event name provided to posthog.capture') }) it('errors with object event name', () => { - given('eventName', () => ({ event: 'object as name' })) - console.error = jest.fn() - const hook = jest.fn() - given.lib._addCaptureHook(hook) + jest.spyOn(logger, 'error').mockImplementation() - expect(() => given.subject()).not.toThrow() + const posthog = posthogWith(config, overrides) + posthog._addCaptureHook(hook) + + expect(() => posthog.capture({ event: 'object as name' })).not.toThrow() expect(hook).not.toHaveBeenCalled() - expect(console.error).toHaveBeenCalledWith('[PostHog.js]', 'No event name provided to posthog.capture') + expect(logger.error).toHaveBeenCalledWith('No event name provided to posthog.capture') }) it('respects opt_out_useragent_filter (default: false)', () => { @@ -183,8 +176,10 @@ describe('posthog core', () => { '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' const hook = jest.fn() - given.lib._addCaptureHook(hook) - given.subject() + const posthog = posthogWith(config, overrides) + posthog._addCaptureHook(hook) + + posthog.capture(eventName, {}, {}) expect(hook).not.toHaveBeenCalledWith('$event') // eslint-disable-next-line no-import-assign @@ -194,20 +189,24 @@ describe('posthog core', () => { it('respects opt_out_useragent_filter', () => { const originalUseragent = globals.userAgent - given('config', () => ({ - opt_out_useragent_filter: true, - property_denylist: [], - property_blacklist: [], - _onCapture: jest.fn(), - })) - // eslint-disable-next-line no-import-assign globals['userAgent'] = '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' const hook = jest.fn() - given.lib._addCaptureHook(hook) - const event = given.subject() + const posthog = posthogWith( + { + opt_out_useragent_filter: true, + property_denylist: [], + property_blacklist: [], + _onCapture: jest.fn(), + }, + overrides + ) + posthog._addCaptureHook(hook) + + const event = posthog.capture(eventName, {}, {}) + expect(hook).toHaveBeenCalledWith( '$event', expect.objectContaining({ @@ -221,30 +220,46 @@ describe('posthog core', () => { }) it('truncates long properties', () => { - given('config', () => ({ - properties_string_max_length: 1000, - property_denylist: [], - property_blacklist: [], - _onCapture: jest.fn(), - })) - given('eventProperties', () => ({ - key: 'value'.repeat(10000), - })) - const event = given.subject() + const posthog = posthogWith( + { + properties_string_max_length: 1000, + property_denylist: [], + property_blacklist: [], + _onCapture: jest.fn(), + }, + overrides + ) + + const event = posthog.capture( + eventName, + { + key: 'value'.repeat(10000), + }, + {} + ) + expect(event.properties.key.length).toBe(1000) }) it('keeps long properties if null', () => { - given('config', () => ({ - properties_string_max_length: null, - property_denylist: [], - property_blacklist: [], - _onCapture: jest.fn(), - })) - given('eventProperties', () => ({ - key: 'value'.repeat(10000), - })) - const event = given.subject() + const posthog = posthogWith( + { + properties_string_max_length: null, + property_denylist: [], + property_blacklist: [], + _onCapture: jest.fn(), + }, + overrides + ) + + const event = posthog.capture( + eventName, + { + key: 'value'.repeat(10000), + }, + {} + ) + expect(event.properties.key.length).toBe(50000) }) @@ -257,7 +272,10 @@ describe('posthog core', () => { // We check that if identify is called with user $set and $set_once // properties, we also want to ensure capture does the expected thing // with them. - const captureResult = given.lib.capture( + + const posthog = posthogWith(config, overrides) + + const captureResult = posthog.capture( '$identify', { distinct_id: 'some-distinct-id' }, { $set: { email: 'john@example.com' }, $set_once: { howOftenAmISet: 'once!' } } @@ -271,26 +289,33 @@ describe('posthog core', () => { }) it('updates persisted person properties for feature flags if $set is present', () => { - given('config', () => ({ - property_denylist: [], - property_blacklist: [], - _onCapture: jest.fn(), - })) - given('eventProperties', () => ({ + const posthog = posthogWith( + { + property_denylist: [], + property_blacklist: [], + _onCapture: jest.fn(), + }, + overrides + ) + + posthog.capture(eventName, { $set: { foo: 'bar' }, - })) - given.subject() - expect(given.overrides.persistence.props.$stored_person_properties).toMatchObject({ foo: 'bar' }) + }) + expect(posthog.persistence.props.$stored_person_properties).toMatchObject({ foo: 'bar' }) }) it('correctly handles the "length" property', () => { - const captureResult = given.lib.capture('event-name', { foo: 'bar', length: 0 }) + const posthog = posthogWith(config, overrides) + const captureResult = posthog.capture('event-name', { foo: 'bar', length: 0 }) expect(captureResult.properties).toEqual(expect.objectContaining({ foo: 'bar', length: 0 })) }) it('sends payloads to /e/ by default', () => { - given.lib.capture('event-name', { foo: 'bar', length: 0 }) - expect(given.lib._send_request).toHaveBeenCalledWith( + const posthog = posthogWith({ ...config, request_batching: false }, overrides) + + posthog.capture('event-name', { foo: 'bar', length: 0 }) + + expect(posthog._send_request).toHaveBeenCalledWith( expect.objectContaining({ url: 'https://us.i.posthog.com/e/', }) @@ -298,10 +323,12 @@ describe('posthog core', () => { }) it('sends payloads to alternative endpoint if given', () => { - given.lib._afterDecideResponse({ analytics: { endpoint: '/i/v0/e/' } }) - given.lib.capture('event-name', { foo: 'bar', length: 0 }) + const posthog = posthogWith({ ...config, request_batching: false }, overrides) + posthog._afterDecideResponse({ analytics: { endpoint: '/i/v0/e/' } }) + + posthog.capture('event-name', { foo: 'bar', length: 0 }) - expect(given.lib._send_request).toHaveBeenCalledWith( + expect(posthog._send_request).toHaveBeenCalledWith( expect.objectContaining({ url: 'https://us.i.posthog.com/i/v0/e/', }) @@ -309,8 +336,11 @@ describe('posthog core', () => { }) it('sends payloads to overriden endpoint if given', () => { - given.lib.capture('event-name', { foo: 'bar', length: 0 }, { _url: 'https://app.posthog.com/s/' }) - expect(given.lib._send_request).toHaveBeenCalledWith( + const posthog = posthogWith({ ...config, request_batching: false }, overrides) + + posthog.capture('event-name', { foo: 'bar', length: 0 }, { _url: 'https://app.posthog.com/s/' }) + + expect(posthog._send_request).toHaveBeenCalledWith( expect.objectContaining({ url: 'https://app.posthog.com/s/', }) @@ -318,9 +348,12 @@ describe('posthog core', () => { }) 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 }, { _url: 'https://app.posthog.com/s/' }) - expect(given.lib._send_request).toHaveBeenCalledWith( + const posthog = posthogWith({ ...config, request_batching: false }, overrides) + posthog._afterDecideResponse({ analytics: { endpoint: '/i/v0/e/' } }) + + posthog.capture('event-name', { foo: 'bar', length: 0 }, { _url: 'https://app.posthog.com/s/' }) + + expect(posthog._send_request).toHaveBeenCalledWith( expect.objectContaining({ url: 'https://app.posthog.com/s/', }) From 8ce543792bcfee32124117a619d752032fe7791f Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 17:26:43 +0100 Subject: [PATCH 22/29] step --- src/__tests__/posthog-core.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 207d967c3..34d6ce85d 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -721,6 +721,7 @@ describe('posthog core', () => { const posthog = posthogWith({ bootstrap: {}, }) + posthog.persistence.clear() expect(posthog.get_distinct_id()).not.toBe('abcd') expect(posthog.get_distinct_id()).not.toEqual(undefined) @@ -783,13 +784,9 @@ describe('posthog core', () => { it('can set an xhr error handler', () => { const fakeOnXHRError = 'configured error' - const posthog = defaultPostHog().init( - 'a-token', - { - on_xhr_error: fakeOnXHRError, - }, - 'a-name' - ) + const posthog = posthogWith({ + on_xhr_error: fakeOnXHRError, + }) expect(posthog.config.on_xhr_error).toBe(fakeOnXHRError) }) @@ -862,6 +859,7 @@ describe('posthog core', () => { 'testtoken', { get_device_id: (uuid) => 'custom-' + uuid.slice(0, 8), + persistence: 'memory', }, uuidv7() ) From 89655fe17c2bc0fc2d85941147decc0253e39ebb Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 17:43:55 +0100 Subject: [PATCH 23/29] step --- src/__tests__/posthog-core.js | 81 +++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 34d6ce85d..75aee00bf 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -716,18 +716,17 @@ describe('posthog core', () => { }) it('does nothing when empty', () => { - jest.spyOn(console, 'warn').mockImplementation() + jest.spyOn(logger, 'warn').mockImplementation() const posthog = posthogWith({ bootstrap: {}, + persistence: 'memory', }) - posthog.persistence.clear() expect(posthog.get_distinct_id()).not.toBe('abcd') expect(posthog.get_distinct_id()).not.toEqual(undefined) expect(posthog.getFeatureFlag('multivariant')).toBe(undefined) - expect(console.warn).toHaveBeenCalledWith( - '[PostHog.js]', + expect(logger.warn).toHaveBeenCalledWith( expect.stringContaining('getFeatureFlag for key "multivariant" failed') ) expect(posthog.getFeatureFlag('disabled')).toBe(undefined) @@ -825,7 +824,7 @@ describe('posthog core', () => { it('sets a random UUID as distinct_id/$device_id if distinct_id is unset', () => { uninitialisedPostHog.persistence = { props: { distinct_id: undefined } } const posthog = uninitialisedPostHog.init( - 'testtoken', + uuidv7(), { get_device_id: (uuid) => uuid, }, @@ -843,7 +842,7 @@ describe('posthog core', () => { it('does not set distinct_id/$device_id if distinct_id is unset', () => { uninitialisedPostHog.persistence = { props: { distinct_id: 'existing-id' } } const posthog = uninitialisedPostHog.init( - 'testtoken', + uuidv7(), { get_device_id: (uuid) => uuid, }, @@ -854,9 +853,9 @@ describe('posthog core', () => { }) it('uses config.get_device_id for uuid generation if passed', () => { - uninitialisedPostHog.persistence = { props: { distinct_id: undefined } } + // uninitialisedPostHog.persistence = { props: { distinct_id: undefined } } const posthog = uninitialisedPostHog.init( - 'testtoken', + uuidv7(), { get_device_id: (uuid) => 'custom-' + uuid.slice(0, 8), persistence: 'memory', @@ -874,12 +873,11 @@ describe('posthog core', () => { describe('skipped init()', () => { it('capture() does not throw', () => { - console.error = jest.fn() + jest.spyOn(logger, 'error').mockImplementation() + expect(() => defaultPostHog().capture('$pageview')).not.toThrow() - expect(console.error).toHaveBeenCalledWith( - '[PostHog.js]', - 'You must initialize PostHog before calling posthog.capture' - ) + + expect(logger.error).toHaveBeenCalledWith('You must initialize PostHog before calling posthog.capture') }) }) @@ -1062,37 +1060,48 @@ describe('posthog core', () => { }) describe('_loaded()', () => { - given('subject', () => () => given.lib._loaded()) - - given('overrides', () => ({ - config: given.config, - capture: jest.fn(), - featureFlags: { - setReloadingPaused: jest.fn(), - resetRequestQueue: jest.fn(), - _startReloadTimer: jest.fn(), - }, - _start_queue_if_opted_in: jest.fn(), - })) - given('config', () => ({ loaded: jest.fn() })) - it('calls loaded config option', () => { - given.subject() + const posthog = posthogWith( + { loaded: jest.fn() }, + { + capture: jest.fn(), + featureFlags: { + setReloadingPaused: jest.fn(), + resetRequestQueue: jest.fn(), + _startReloadTimer: jest.fn(), + }, + _start_queue_if_opted_in: jest.fn(), + } + ) + + posthog._loaded() - expect(given.config.loaded).toHaveBeenCalledWith(given.lib) + expect(posthog.config.loaded).toHaveBeenCalledWith(posthog) }) it('handles loaded config option throwing gracefully', () => { - given('config', () => ({ - loaded: () => { - throw Error() + jest.spyOn(logger, 'critical').mockImplementation() + + const posthog = posthogWith( + { + loaded: () => { + throw Error() + }, }, - })) - console.error = jest.fn() + { + capture: jest.fn(), + featureFlags: { + setReloadingPaused: jest.fn(), + resetRequestQueue: jest.fn(), + _startReloadTimer: jest.fn(), + }, + _start_queue_if_opted_in: jest.fn(), + } + ) - given.subject() + posthog._loaded() - expect(console.error).toHaveBeenCalledWith('[PostHog.js]', '`loaded` function failed', expect.anything()) + expect(logger.critical).toHaveBeenCalledWith('`loaded` function failed', expect.anything()) }) describe('/decide', () => { From f76ae990b510ded4a7dbfdea8cfef2b6cdbc9913 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 17:48:04 +0100 Subject: [PATCH 24/29] finally remove it --- jest.config.js | 3 +- package.json | 1 - pnpm-lock.yaml | 7 --- src/__tests__/posthog-core.js | 92 +++++++++++++++++------------------ 4 files changed, 46 insertions(+), 57 deletions(-) diff --git a/jest.config.js b/jest.config.js index 0cfcfda00..4f36d0842 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,8 @@ +// eslint-disable-next-line no-undef module.exports = { testPathIgnorePatterns: ['/node_modules/', '/cypress/', '/react/', '/test_data/', '/testcafe/'], moduleFileExtensions: ['js', 'json', 'ts', 'tsx'], - setupFilesAfterEnv: ['given2/setup', './src/__tests__/setup.js'], + setupFilesAfterEnv: ['./src/__tests__/setup.js'], modulePathIgnorePatterns: ['src/__tests__/setup.js', 'src/__tests__/helpers/'], clearMocks: true, testEnvironment: 'jsdom', diff --git a/package.json b/package.json index 63194e6a4..de7d99cdf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "eslint-plugin-react-hooks": "^4.6.0", "express": "^4.18.2", "fast-check": "^2.17.0", - "given2": "^2.1.7", "husky": "^8.0.1", "jest": "^27.5.1", "jsdom": "16.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0878bc90d..41803cdb6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -144,9 +144,6 @@ devDependencies: fast-check: specifier: ^2.17.0 version: 2.17.0 - given2: - specifier: ^2.1.7 - version: 2.1.7 husky: specifier: ^8.0.1 version: 8.0.1 @@ -5848,10 +5845,6 @@ packages: omggif: 1.0.10 dev: true - /given2@2.1.7: - resolution: {integrity: sha512-fI3VamsjN2euNVguGpSt2uExyDSMfJoK+SwDxbmV+Thf3v4oF6KKZAFE3LHHuT+PYyMwCsJYXO01TW3euFdPGA==} - dev: true - /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 75aee00bf..d2bf13744 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -14,6 +14,46 @@ describe('posthog core', () => { const baseUTCDateTime = new Date(Date.UTC(2020, 0, 1, 0, 0, 0)) const eventName = '$event' + const config = { + api_host: 'https://app.posthog.com', + property_denylist: [], + property_blacklist: [], + // _onCapture: jest.fn(), + get_device_id: jest.fn().mockReturnValue('device-id'), + } + + const overrides = { + __loaded: true, + persistence: { + remove_event_timer: jest.fn(), + properties: jest.fn(), + update_config: jest.fn(), + register(properties) { + // Simplified version of the real thing + Object.assign(this.props, properties) + }, + props: {}, + get_property: () => 'anonymous', + set_initial_person_info: jest.fn(), + get_initial_props: () => ({}), + }, + sessionPersistence: { + update_search_keyword: jest.fn(), + update_campaign_params: jest.fn(), + update_referrer_info: jest.fn(), + update_config: jest.fn(), + properties: jest.fn(), + get_property: () => 'anonymous', + }, + _send_request: jest.fn(), + compression: {}, + __captureHooks: [], + rateLimiter: { + isServerRateLimited: () => false, + clientRateLimitContext: () => false, + }, + } + const posthogWith = (config, overrides) => { const posthog = defaultPostHog().init('testtoken', config, uuidv7()) if (overrides) { @@ -39,46 +79,6 @@ describe('posthog core', () => { }) describe('capture()', () => { - const config = { - api_host: 'https://app.posthog.com', - property_denylist: [], - property_blacklist: [], - // _onCapture: jest.fn(), - get_device_id: jest.fn().mockReturnValue('device-id'), - } - - const overrides = { - __loaded: true, - persistence: { - remove_event_timer: jest.fn(), - properties: jest.fn(), - update_config: jest.fn(), - register(properties) { - // Simplified version of the real thing - Object.assign(this.props, properties) - }, - props: {}, - get_property: () => 'anonymous', - set_initial_person_info: jest.fn(), - get_initial_props: () => ({}), - }, - sessionPersistence: { - update_search_keyword: jest.fn(), - update_campaign_params: jest.fn(), - update_referrer_info: jest.fn(), - update_config: jest.fn(), - properties: jest.fn(), - get_property: () => 'anonymous', - }, - _send_request: jest.fn(), - compression: {}, - __captureHooks: [], - rateLimiter: { - isServerRateLimited: () => false, - clientRateLimitContext: () => false, - }, - } - it('adds a UUID to each message', () => { const captureData = posthogWith(config, overrides).capture(eventName, {}, {}) expect(captureData).toHaveProperty('uuid') @@ -599,8 +599,6 @@ describe('posthog core', () => { }) describe('without batching', () => { - given('batching', () => false) - it('captures $pageleave', () => { const posthog = posthogWith({ capture_pageview: true, @@ -790,7 +788,7 @@ describe('posthog core', () => { }) it('does not load feature flags, toolbar, session recording', () => { - const posthog = defaultPostHog().init('testtoken', given.config, uuidv7()) + const posthog = defaultPostHog().init('testtoken', config, uuidv7()) posthog.toolbar = { maybeLoadToolbar: jest.fn(), @@ -1013,21 +1011,19 @@ describe('posthog core', () => { }) describe('error handling', () => { - given('overrides', () => ({ - register: jest.fn(), - })) - it('handles blank keys being passed', () => { window.console.error = jest.fn() window.console.warn = jest.fn() + posthog.register = jest.fn() + posthog.group(null, 'foo') posthog.group('organization', null) posthog.group('organization', undefined) posthog.group('organization', '') posthog.group('', 'foo') - expect(given.overrides.register).not.toHaveBeenCalled() + expect(posthog.register).not.toHaveBeenCalled() }) }) From 3b90d5b00263a95378d6fce32c85433b345170c7 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 18:07:29 +0100 Subject: [PATCH 25/29] refactor --- src/__tests__/posthog-core.js | 210 +++++++++++++++++----------------- 1 file changed, 103 insertions(+), 107 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index d2bf13744..3f5a41fc5 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -14,58 +14,15 @@ describe('posthog core', () => { const baseUTCDateTime = new Date(Date.UTC(2020, 0, 1, 0, 0, 0)) const eventName = '$event' - const config = { - api_host: 'https://app.posthog.com', - property_denylist: [], - property_blacklist: [], - // _onCapture: jest.fn(), - get_device_id: jest.fn().mockReturnValue('device-id'), - } + const defaultConfig = {} - const overrides = { - __loaded: true, - persistence: { - remove_event_timer: jest.fn(), - properties: jest.fn(), - update_config: jest.fn(), - register(properties) { - // Simplified version of the real thing - Object.assign(this.props, properties) - }, - props: {}, - get_property: () => 'anonymous', - set_initial_person_info: jest.fn(), - get_initial_props: () => ({}), - }, - sessionPersistence: { - update_search_keyword: jest.fn(), - update_campaign_params: jest.fn(), - update_referrer_info: jest.fn(), - update_config: jest.fn(), - properties: jest.fn(), - get_property: () => 'anonymous', - }, + const defaultOverrides = { _send_request: jest.fn(), - compression: {}, - __captureHooks: [], - rateLimiter: { - isServerRateLimited: () => false, - clientRateLimitContext: () => false, - }, } const posthogWith = (config, overrides) => { const posthog = defaultPostHog().init('testtoken', config, uuidv7()) - if (overrides) { - return Object.assign(posthog, overrides) - } - - posthog._send_request = jest.fn() - posthog.capture = jest.fn() - posthog._requestQueue = { - unload: jest.fn(), - } - return posthog + return Object.assign(posthog, overrides) } beforeEach(() => { @@ -80,12 +37,12 @@ describe('posthog core', () => { describe('capture()', () => { it('adds a UUID to each message', () => { - const captureData = posthogWith(config, overrides).capture(eventName, {}, {}) + const captureData = posthogWith(defaultConfig, defaultOverrides).capture(eventName, {}, {}) expect(captureData).toHaveProperty('uuid') }) it('adds system time to events', () => { - const captureData = posthogWith(config, overrides).capture(eventName, {}, {}) + const captureData = posthogWith(defaultConfig, defaultOverrides).capture(eventName, {}, {}) expect(captureData).toHaveProperty('timestamp') // timer is fixed at 2020-01-01 @@ -93,7 +50,7 @@ describe('posthog core', () => { }) it('captures when time is overriden by caller', () => { - const captureData = posthogWith(config, overrides).capture( + const captureData = posthogWith(defaultConfig, defaultOverrides).capture( eventName, {}, { timestamp: new Date(2020, 0, 2, 12, 34) } @@ -109,13 +66,15 @@ describe('posthog core', () => { props.recurse = props expect(() => - posthogWith(config, overrides).capture(eventName, props, { timestamp: new Date(2020, 0, 2, 12, 34) }) + posthogWith(defaultConfig, defaultOverrides).capture(eventName, props, { + timestamp: new Date(2020, 0, 2, 12, 34), + }) ).not.toThrow() }) it('calls callbacks added via _addCaptureHook', () => { const hook = jest.fn() - const posthog = posthogWith(config, overrides) + const posthog = posthogWith(defaultConfig, defaultOverrides) posthog._addCaptureHook(hook) posthog.capture(eventName, {}, {}) @@ -136,7 +95,17 @@ describe('posthog core', () => { store_google: true, save_referrer: true, }, - overrides + { + ...defaultOverrides, + sessionPersistence: { + update_search_keyword: jest.fn(), + update_campaign_params: jest.fn(), + update_referrer_info: jest.fn(), + update_config: jest.fn(), + properties: jest.fn(), + get_property: () => 'anonymous', + }, + } ) posthog.capture(eventName, {}, {}) @@ -148,7 +117,7 @@ describe('posthog core', () => { it('errors with undefined event name', () => { const hook = jest.fn() - const posthog = posthogWith(config, overrides) + const posthog = posthogWith(defaultConfig, defaultOverrides) posthog._addCaptureHook(hook) jest.spyOn(logger, 'error').mockImplementation() @@ -161,7 +130,7 @@ describe('posthog core', () => { const hook = jest.fn() jest.spyOn(logger, 'error').mockImplementation() - const posthog = posthogWith(config, overrides) + const posthog = posthogWith(defaultConfig, defaultOverrides) posthog._addCaptureHook(hook) expect(() => posthog.capture({ event: 'object as name' })).not.toThrow() @@ -176,7 +145,7 @@ describe('posthog core', () => { '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' const hook = jest.fn() - const posthog = posthogWith(config, overrides) + const posthog = posthogWith(defaultConfig, defaultOverrides) posthog._addCaptureHook(hook) posthog.capture(eventName, {}, {}) @@ -201,7 +170,7 @@ describe('posthog core', () => { property_blacklist: [], _onCapture: jest.fn(), }, - overrides + defaultOverrides ) posthog._addCaptureHook(hook) @@ -227,7 +196,7 @@ describe('posthog core', () => { property_blacklist: [], _onCapture: jest.fn(), }, - overrides + defaultOverrides ) const event = posthog.capture( @@ -249,7 +218,7 @@ describe('posthog core', () => { property_blacklist: [], _onCapture: jest.fn(), }, - overrides + defaultOverrides ) const event = posthog.capture( @@ -273,7 +242,7 @@ describe('posthog core', () => { // properties, we also want to ensure capture does the expected thing // with them. - const posthog = posthogWith(config, overrides) + const posthog = posthogWith(defaultConfig, defaultOverrides) const captureResult = posthog.capture( '$identify', @@ -284,7 +253,10 @@ describe('posthog core', () => { // We assume that the returned result is the object we would send to the // server. expect(captureResult).toEqual( - expect.objectContaining({ $set: { email: 'john@example.com' }, $set_once: { howOftenAmISet: 'once!' } }) + expect.objectContaining({ + $set: { email: 'john@example.com' }, + $set_once: expect.objectContaining({ howOftenAmISet: 'once!' }), + }) ) }) @@ -295,7 +267,7 @@ describe('posthog core', () => { property_blacklist: [], _onCapture: jest.fn(), }, - overrides + defaultOverrides ) posthog.capture(eventName, { @@ -305,13 +277,13 @@ describe('posthog core', () => { }) it('correctly handles the "length" property', () => { - const posthog = posthogWith(config, overrides) + const posthog = posthogWith(defaultConfig, defaultOverrides) const captureResult = posthog.capture('event-name', { foo: 'bar', length: 0 }) expect(captureResult.properties).toEqual(expect.objectContaining({ foo: 'bar', length: 0 })) }) it('sends payloads to /e/ by default', () => { - const posthog = posthogWith({ ...config, request_batching: false }, overrides) + const posthog = posthogWith({ ...defaultConfig, request_batching: false }, defaultOverrides) posthog.capture('event-name', { foo: 'bar', length: 0 }) @@ -323,7 +295,7 @@ describe('posthog core', () => { }) it('sends payloads to alternative endpoint if given', () => { - const posthog = posthogWith({ ...config, request_batching: false }, overrides) + const posthog = posthogWith({ ...defaultConfig, request_batching: false }, defaultOverrides) posthog._afterDecideResponse({ analytics: { endpoint: '/i/v0/e/' } }) posthog.capture('event-name', { foo: 'bar', length: 0 }) @@ -336,7 +308,7 @@ describe('posthog core', () => { }) it('sends payloads to overriden endpoint if given', () => { - const posthog = posthogWith({ ...config, request_batching: false }, overrides) + const posthog = posthogWith({ ...defaultConfig, request_batching: false }, defaultOverrides) posthog.capture('event-name', { foo: 'bar', length: 0 }, { _url: 'https://app.posthog.com/s/' }) @@ -348,7 +320,7 @@ describe('posthog core', () => { }) it('sends payloads to overriden _url, even if alternative endpoint is set', () => { - const posthog = posthogWith({ ...config, request_batching: false }, overrides) + const posthog = posthogWith({ ...defaultConfig, request_batching: false }, defaultOverrides) posthog._afterDecideResponse({ analytics: { endpoint: '/i/v0/e/' } }) posthog.capture('event-name', { foo: 'bar', length: 0 }, { _url: 'https://app.posthog.com/s/' }) @@ -551,11 +523,14 @@ describe('posthog core', () => { describe('_handle_unload()', () => { it('captures $pageleave', () => { - const posthog = posthogWith({ - capture_pageview: true, - capture_pageleave: 'if_capture_pageview', - batching: true, - }) + const posthog = posthogWith( + { + capture_pageview: true, + capture_pageleave: 'if_capture_pageview', + batching: true, + }, + { capture: jest.fn() } + ) posthog._handle_unload() @@ -563,11 +538,14 @@ describe('posthog core', () => { }) it('does not capture $pageleave when capture_pageview=false and capture_pageleave=if_capture_pageview', () => { - const posthog = posthogWith({ - capture_pageview: false, - capture_pageleave: 'if_capture_pageview', - batching: true, - }) + const posthog = posthogWith( + { + capture_pageview: false, + capture_pageleave: 'if_capture_pageview', + batching: true, + }, + { capture: jest.fn() } + ) posthog._handle_unload() @@ -575,11 +553,14 @@ describe('posthog core', () => { }) it('does capture $pageleave when capture_pageview=false and capture_pageleave=true', () => { - const posthog = posthogWith({ - capture_pageview: false, - capture_pageleave: true, - batching: true, - }) + const posthog = posthogWith( + { + capture_pageview: false, + capture_pageleave: true, + batching: true, + }, + { capture: jest.fn() } + ) posthog._handle_unload() @@ -587,11 +568,14 @@ describe('posthog core', () => { }) it('calls requestQueue unload', () => { - const posthog = posthogWith({ - capture_pageview: true, - capture_pageleave: 'if_capture_pageview', - batching: true, - }) + const posthog = posthogWith( + { + capture_pageview: true, + capture_pageleave: 'if_capture_pageview', + batching: true, + }, + { _requestQueue: { enqueue: jest.fn(), unload: jest.fn() } } + ) posthog._handle_unload() @@ -600,22 +584,28 @@ describe('posthog core', () => { describe('without batching', () => { it('captures $pageleave', () => { - const posthog = posthogWith({ - capture_pageview: true, - capture_pageleave: 'if_capture_pageview', - request_batching: false, - }) + const posthog = posthogWith( + { + capture_pageview: true, + capture_pageleave: 'if_capture_pageview', + request_batching: false, + }, + { capture: jest.fn() } + ) posthog._handle_unload() expect(posthog.capture).toHaveBeenCalledWith('$pageleave', null, { transport: 'sendBeacon' }) }) it('does not capture $pageleave when capture_pageview=false', () => { - const posthog = posthogWith({ - capture_pageview: false, - capture_pageleave: 'if_capture_pageview', - request_batching: false, - }) + const posthog = posthogWith( + { + capture_pageview: false, + capture_pageleave: 'if_capture_pageview', + request_batching: false, + }, + { capture: jest.fn() } + ) posthog._handle_unload() expect(posthog.capture).not.toHaveBeenCalled() @@ -625,11 +615,14 @@ describe('posthog core', () => { describe('bootstrapping feature flags', () => { it('sets the right distinctID', () => { - const posthog = posthogWith({ - bootstrap: { - distinctID: 'abcd', + const posthog = posthogWith( + { + bootstrap: { + distinctID: 'abcd', + }, }, - }) + { capture: jest.fn() } + ) expect(posthog.get_distinct_id()).toBe('abcd') expect(posthog.get_property('$device_id')).toBe('abcd') @@ -648,13 +641,16 @@ describe('posthog core', () => { }) it('treats identified distinctIDs appropriately', () => { - const posthog = posthogWith({ - bootstrap: { - distinctID: 'abcd', - isIdentifiedID: true, + const posthog = posthogWith( + { + bootstrap: { + distinctID: 'abcd', + isIdentifiedID: true, + }, + get_device_id: () => 'og-device-id', }, - get_device_id: () => 'og-device-id', - }) + { capture: jest.fn() } + ) expect(posthog.get_distinct_id()).toBe('abcd') expect(posthog.get_property('$device_id')).toBe('og-device-id') @@ -788,7 +784,7 @@ describe('posthog core', () => { }) it('does not load feature flags, toolbar, session recording', () => { - const posthog = defaultPostHog().init('testtoken', config, uuidv7()) + const posthog = defaultPostHog().init('testtoken', defaultConfig, uuidv7()) posthog.toolbar = { maybeLoadToolbar: jest.fn(), From 346b22c0fbebd2d9d74732cd97d44ded174d4e35 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 18:26:04 +0100 Subject: [PATCH 26/29] fix --- src/__tests__/posthog-core.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 3f5a41fc5..a84749e9c 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -31,8 +31,6 @@ describe('posthog core', () => { afterEach(() => { jest.useRealTimers() - // Make sure there's no cached persistence - // given.lib.persistence?.clear?.() }) describe('capture()', () => { @@ -847,7 +845,6 @@ describe('posthog core', () => { }) it('uses config.get_device_id for uuid generation if passed', () => { - // uninitialisedPostHog.persistence = { props: { distinct_id: undefined } } const posthog = uninitialisedPostHog.init( uuidv7(), { From bbf30fc4eb52811f77918c61ada03b0d888e2588 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 20:59:17 +0100 Subject: [PATCH 27/29] convert one to typescript --- src/__tests__/{decide.js => decide.ts} | 97 ++++++++++++++++---------- 1 file changed, 59 insertions(+), 38 deletions(-) rename src/__tests__/{decide.js => decide.ts} (76%) diff --git a/src/__tests__/decide.js b/src/__tests__/decide.ts similarity index 76% rename from src/__tests__/decide.js rename to src/__tests__/decide.ts index d166550f7..37225a1a4 100644 --- a/src/__tests__/decide.js +++ b/src/__tests__/decide.ts @@ -2,9 +2,16 @@ import { Decide } from '../decide' import { PostHogPersistence } from '../posthog-persistence' import { RequestRouter } from '../utils/request-router' import { expectScriptToExist, expectScriptToNotExist } from './helpers/script-utils' +import { PostHog } from '../posthog-core' +import { DecideResponse, PostHogConfig, Properties } from '../types' -const expectDecodedSendRequest = (send_request, data, noCompression, posthog) => { - const lastCall = send_request.mock.calls[send_request.mock.calls.length - 1] +const expectDecodedSendRequest = ( + send_request: PostHog['_send_request'], + data: Record, + noCompression: boolean, + posthog: PostHog +) => { + const lastCall = jest.mocked(send_request).mock.calls[jest.mocked(send_request).mock.calls.length - 1] const decoded = lastCall[0].data // Helper to give us more accurate error messages @@ -21,23 +28,28 @@ const expectDecodedSendRequest = (send_request, data, noCompression, posthog) => } describe('Decide', () => { - let posthog + let posthog: PostHog const decide = () => new Decide(posthog) - const defaultConfig = { token: 'testtoken', api_host: 'https://test.com', persistence: 'memory' } + const defaultConfig: Partial = { + token: 'testtoken', + api_host: 'https://test.com', + persistence: 'memory', + } beforeEach(() => { // clean the JSDOM to prevent interdependencies between tests document.body.innerHTML = '' document.head.innerHTML = '' + jest.spyOn(window.console, 'error').mockImplementation() posthog = { config: defaultConfig, - persistence: new PostHogPersistence(defaultConfig), - register: (props) => posthog.persistence.register(props), - unregister: (key) => posthog.persistence.unregister(key), - get_property: (key) => posthog.persistence.props[key], + persistence: new PostHogPersistence(defaultConfig as PostHogConfig), + register: (props: Properties) => posthog.persistence!.register(props), + unregister: (key: string) => posthog.persistence!.unregister(key), + get_property: (key: string) => posthog.persistence!.props[key], capture: jest.fn(), _addCaptureHook: jest.fn(), _afterDecideResponse: jest.fn(), @@ -48,17 +60,15 @@ describe('Decide', () => { setReloadingPaused: jest.fn(), _startReloadTimer: jest.fn(), }, - requestRouter: new RequestRouter({ config: defaultConfig }), + requestRouter: new RequestRouter({ config: defaultConfig } as unknown as PostHog), _hasBootstrappedFeatureFlags: jest.fn(), getGroups: () => ({ organization: '5' }), - } + } as unknown as PostHog }) describe('constructor', () => { - const subject = () => decide().call() - it('should call instance._send_request on constructor', () => { - subject() + decide().call() expectDecodedSendRequest( posthog._send_request, @@ -77,7 +87,8 @@ describe('Decide', () => { $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) - subject() + + decide().call() expectDecodedSendRequest( posthog._send_request, @@ -99,13 +110,13 @@ describe('Decide', () => { token: 'testtoken', persistence: 'memory', advanced_disable_feature_flags: true, - } + } as PostHogConfig posthog.register({ $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) - subject() + decide().call() expectDecodedSendRequest( posthog._send_request, @@ -128,13 +139,13 @@ describe('Decide', () => { token: 'testtoken', persistence: 'memory', disable_compression: true, - } + } as PostHogConfig posthog.register({ $stored_person_properties: {}, $stored_group_properties: {}, }) - subject() + decide().call() // noCompression is true expectDecodedSendRequest( @@ -157,13 +168,14 @@ describe('Decide', () => { token: 'testtoken', persistence: 'memory', advanced_disable_feature_flags_on_first_load: true, - } + } as PostHogConfig posthog.register({ $stored_person_properties: { key: 'value' }, $stored_group_properties: { organization: { orgName: 'orgValue' } }, }) - subject() + + decide().call() expectDecodedSendRequest( posthog._send_request, @@ -182,20 +194,19 @@ describe('Decide', () => { }) describe('parseDecideResponse', () => { - const subject = (decideResponse) => decide().parseDecideResponse(decideResponse) + const subject = (decideResponse: DecideResponse) => decide().parseDecideResponse(decideResponse) it('properly parses decide response', () => { - subject({}) + subject({} as DecideResponse) expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, false) expect(posthog._afterDecideResponse).toHaveBeenCalledWith({}) }) it('Make sure receivedFeatureFlags is called with errors if the decide response fails', () => { - window.POSTHOG_DEBUG = true - console.error = jest.fn() + ;(window as any).POSTHOG_DEBUG = true - subject(undefined) + subject(undefined as unknown as DecideResponse) expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, true) expect(console.error).toHaveBeenCalledWith('[PostHog.js]', 'Failed to fetch feature flags from PostHog.') @@ -207,11 +218,11 @@ describe('Decide', () => { token: 'testtoken', persistence: 'memory', advanced_disable_feature_flags_on_first_load: true, - } + } as PostHogConfig const decideResponse = { featureFlags: { 'test-flag': true }, - } + } as unknown as DecideResponse subject(decideResponse) expect(posthog._afterDecideResponse).toHaveBeenCalledWith(decideResponse) @@ -224,11 +235,11 @@ describe('Decide', () => { token: 'testtoken', persistence: 'memory', advanced_disable_feature_flags: true, - } + } as PostHogConfig const decideResponse = { featureFlags: { 'test-flag': true }, - } + } as unknown as DecideResponse subject(decideResponse) expect(posthog._afterDecideResponse).toHaveBeenCalledWith(decideResponse) @@ -236,21 +247,31 @@ describe('Decide', () => { }) it('runs site apps if opted in', () => { - posthog.config = { api_host: 'https://test.com', opt_in_site_apps: true, persistence: 'memory' } - subject({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] }) + posthog.config = { + api_host: 'https://test.com', + opt_in_site_apps: true, + persistence: 'memory', + } as PostHogConfig + + subject({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] } as DecideResponse) + expectScriptToExist('https://test.com/site_app/1/tokentoken/hash/') }) it('does not run site apps code if not opted in', () => { - window.POSTHOG_DEBUG = true + ;(window as any).POSTHOG_DEBUG = true // don't technically need to run this but this test assumes opt_in_site_apps is false, let's make that explicit - posthog.config = { api_host: 'https://test.com', opt_in_site_apps: false, persistence: 'memory' } + posthog.config = { + api_host: 'https://test.com', + opt_in_site_apps: false, + persistence: 'memory', + } as unknown as PostHogConfig + + subject({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] } as DecideResponse) - expect(() => { - subject({ siteApps: [{ id: 1, url: '/site_app/1/tokentoken/hash/' }] }) - }).toThrow( - // throwing only in tests, just an error in production - 'Unexpected console.error: [PostHog.js],PostHog site apps are disabled. Enable the "opt_in_site_apps" config to proceed.' + expect(console.error).toHaveBeenCalledWith( + '[PostHog.js]', + 'PostHog site apps are disabled. Enable the "opt_in_site_apps" config to proceed.' ) expectScriptToNotExist('https://test.com/site_app/1/tokentoken/hash/') }) From f93ea8e6413c6dcfb85b3ef0ff8eeafed0249358 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 21:21:26 +0100 Subject: [PATCH 28/29] another to typescript --- functional_tests/feature-flags.test.ts | 8 ++++---- src/__tests__/{featureflags.js => featureflags.ts} | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) rename src/__tests__/{featureflags.js => featureflags.ts} (99%) diff --git a/functional_tests/feature-flags.test.ts b/functional_tests/feature-flags.test.ts index b8a38768c..25c37f0f4 100644 --- a/functional_tests/feature-flags.test.ts +++ b/functional_tests/feature-flags.test.ts @@ -25,14 +25,14 @@ describe('FunctionalTests / Feature Flags', () => { resetRequests(token) // wait for decide callback - await new Promise((res) => setTimeout(res, 500)) + await new Promise((resolve: () => void) => setTimeout(resolve, 500)) // Person properties set here should also be sent to the decide endpoint. posthog.identify('test-id', { email: 'test@email.com', }) - await new Promise((res) => setTimeout(res, 500)) + await new Promise((resolve: () => void) => setTimeout(resolve, 500)) await waitFor(() => { expect(getRequests(token)['/decide/']).toEqual([ @@ -72,7 +72,7 @@ describe('FunctionalTests / Feature Flags', () => { // wait for decide callback // eslint-disable-next-line compat/compat - await new Promise((res) => setTimeout(res, 500)) + await new Promise((resolve: () => void) => setTimeout(resolve, 500)) // First we identify with a new distinct_id but with no properties set posthog.identify('test-id') @@ -140,7 +140,7 @@ describe('FunctionalTests / Feature Flags', () => { // wait for decide callback // eslint-disable-next-line compat/compat - await new Promise((res) => setTimeout(res, 500)) + await new Promise((resolve: () => void) => setTimeout(resolve, 500)) // now second call should've fired await waitFor(() => { diff --git a/src/__tests__/featureflags.js b/src/__tests__/featureflags.ts similarity index 99% rename from src/__tests__/featureflags.js rename to src/__tests__/featureflags.ts index 32365ef8e..1f5b66014 100644 --- a/src/__tests__/featureflags.js +++ b/src/__tests__/featureflags.ts @@ -237,7 +237,7 @@ describe('featureflags', () => { }) it('onFeatureFlags should not be called immediately if feature flags not loaded', () => { - var called = false + let called = false let _flags = [] let _variants = {} let _error = undefined @@ -265,7 +265,7 @@ describe('featureflags', () => { it('onFeatureFlags callback should be called immediately if feature flags were loaded', () => { featureFlags.instance.decideEndpointWasHit = true - var called = false + let called = false featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(true) @@ -855,7 +855,7 @@ describe('featureflags', () => { }) it('should call onFeatureFlags even when decide errors out', () => { - var called = false + let called = false let _flags = [] let _variants = {} let _errors = undefined @@ -882,7 +882,7 @@ describe('featureflags', () => { }) it('should call onFeatureFlags with existing flags', () => { - var called = false + let called = false let _flags = [] let _variants = {} let _errors = undefined @@ -916,7 +916,7 @@ describe('featureflags', () => { }) ) - var called = false + let called = false let _flags = [] let _variants = {} let _errors = undefined From 41047155106d2b632976a8039b2cf5980245e42a Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 27 Jul 2024 22:30:57 +0100 Subject: [PATCH 29/29] and another to typescript --- .../{posthog-core.js => posthog-core.ts} | 183 ++++++++++-------- 1 file changed, 97 insertions(+), 86 deletions(-) rename src/__tests__/{posthog-core.js => posthog-core.ts} (87%) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.ts similarity index 87% rename from src/__tests__/posthog-core.js rename to src/__tests__/posthog-core.ts index a84749e9c..2ce103037 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.ts @@ -7,6 +7,13 @@ import * as globals from '../utils/globals' import { USER_STATE } from '../constants' import { createPosthogInstance, defaultPostHog } from './helpers/posthog-instance' import { logger } from '../utils/logger' +import { PostHogConfig } from '../types' +import { PostHog } from '../posthog-core' +import { PostHogPersistence } from '../posthog-persistence' +import { SessionIdManager } from '../sessionid' +import { RequestQueue } from '../request-queue' +import { SessionRecording } from '../extensions/replay/sessionrecording' +import { PostHogFeatureFlags } from '../posthog-featureflags' jest.mock('../decide') @@ -20,9 +27,9 @@ describe('posthog core', () => { _send_request: jest.fn(), } - const posthogWith = (config, overrides) => { + const posthogWith = (config: Partial, overrides?: Partial) => { const posthog = defaultPostHog().init('testtoken', config, uuidv7()) - return Object.assign(posthog, overrides) + return Object.assign(posthog, overrides || {}) } beforeEach(() => { @@ -60,7 +67,7 @@ describe('posthog core', () => { }) it('handles recursive objects', () => { - const props = {} + const props: Record = {} props.recurse = props expect(() => @@ -102,7 +109,7 @@ describe('posthog core', () => { update_config: jest.fn(), properties: jest.fn(), get_property: () => 'anonymous', - }, + } as unknown as PostHogPersistence, } ) @@ -138,8 +145,7 @@ describe('posthog core', () => { it('respects opt_out_useragent_filter (default: false)', () => { const originalUseragent = globals.userAgent - // eslint-disable-next-line no-import-assign - globals['userAgent'] = + ;(globals as any)['userAgent'] = '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' const hook = jest.fn() @@ -148,16 +154,13 @@ describe('posthog core', () => { posthog.capture(eventName, {}, {}) expect(hook).not.toHaveBeenCalledWith('$event') - - // eslint-disable-next-line no-import-assign - globals['userAgent'] = originalUseragent + ;(globals as any)['userAgent'] = originalUseragent }) it('respects opt_out_useragent_filter', () => { const originalUseragent = globals.userAgent - // eslint-disable-next-line no-import-assign - globals['userAgent'] = + ;(globals as any)['userAgent'] = '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' const hook = jest.fn() @@ -181,9 +184,7 @@ describe('posthog core', () => { }) ) expect(event.properties['$browser_type']).toEqual('bot') - - // eslint-disable-next-line no-import-assign - globals['userAgent'] = originalUseragent + ;(globals as any)['userAgent'] = originalUseragent }) it('truncates long properties', () => { @@ -208,10 +209,10 @@ describe('posthog core', () => { expect(event.properties.key.length).toBe(1000) }) - it('keeps long properties if null', () => { + it('keeps long properties if undefined', () => { const posthog = posthogWith( { - properties_string_max_length: null, + properties_string_max_length: undefined, property_denylist: [], property_blacklist: [], _onCapture: jest.fn(), @@ -374,24 +375,24 @@ describe('posthog core', () => { }) describe('_calculate_event_properties()', () => { - let posthog + let posthog: PostHog - const overrides = { + const overrides: Partial = { persistence: { properties: () => ({ distinct_id: 'abc', persistent: 'prop', $is_identified: false }), remove_event_timer: jest.fn(), get_property: () => 'anonymous', - }, + } as unknown as PostHogPersistence, sessionPersistence: { properties: () => ({ distinct_id: 'abc', persistent: 'prop' }), get_property: () => 'anonymous', - }, + } as unknown as PostHogPersistence, sessionManager: { checkAndGetSessionAndWindowId: jest.fn().mockReturnValue({ windowId: 'windowId', sessionId: 'sessionId', }), - }, + } as unknown as SessionIdManager, } beforeEach(() => { @@ -473,7 +474,7 @@ describe('posthog core', () => { event: 'prop', distinct_id: 'abc', }) - expect(posthog.sessionManager.checkAndGetSessionAndWindowId).not.toHaveBeenCalled() + expect(posthog.sessionManager!.checkAndGetSessionAndWindowId).not.toHaveBeenCalled() }) it('calls sanitize_properties', () => { @@ -511,7 +512,7 @@ describe('posthog core', () => { }) it('adds page title to $pageview', () => { - document.title = 'test' + document!.title = 'test' expect(posthog._calculate_event_properties('$pageview', {})).toEqual( expect.objectContaining({ title: 'test' }) @@ -525,7 +526,7 @@ describe('posthog core', () => { { capture_pageview: true, capture_pageleave: 'if_capture_pageview', - batching: true, + request_batching: true, }, { capture: jest.fn() } ) @@ -540,7 +541,7 @@ describe('posthog core', () => { { capture_pageview: false, capture_pageleave: 'if_capture_pageview', - batching: true, + request_batching: true, }, { capture: jest.fn() } ) @@ -555,7 +556,7 @@ describe('posthog core', () => { { capture_pageview: false, capture_pageleave: true, - batching: true, + request_batching: true, }, { capture: jest.fn() } ) @@ -570,9 +571,9 @@ describe('posthog core', () => { { capture_pageview: true, capture_pageleave: 'if_capture_pageview', - batching: true, + request_batching: true, }, - { _requestQueue: { enqueue: jest.fn(), unload: jest.fn() } } + { _requestQueue: { enqueue: jest.fn(), unload: jest.fn() } as unknown as RequestQueue } ) posthog._handle_unload() @@ -665,7 +666,8 @@ describe('posthog core', () => { multivariant: 'variant-1', enabled: true, disabled: false, - undef: undefined, + // TODO why are we testing that undefined is passed through? + undef: undefined as unknown as string | boolean, }, }, }) @@ -686,7 +688,8 @@ describe('posthog core', () => { enabled: true, jsonString: true, disabled: false, - undef: undefined, + // TODO why are we testing that undefined is passed through? + undef: undefined as unknown as string | boolean, }, featureFlagPayloads: { multivariant: 'some-payload', @@ -766,82 +769,85 @@ describe('posthog core', () => { }) describe('init()', () => { + // @ts-expect-error - it's fine to spy on window jest.spyOn(window, 'window', 'get') beforeEach(() => { + // @ts-expect-error - it's fine to spy on window jest.spyOn(window.console, 'warn').mockImplementation() + // @ts-expect-error - it's fine to spy on window jest.spyOn(window.console, 'error').mockImplementation() }) it('can set an xhr error handler', () => { - const fakeOnXHRError = 'configured error' + const fakeOnXHRError = jest.fn() const posthog = posthogWith({ on_xhr_error: fakeOnXHRError, }) expect(posthog.config.on_xhr_error).toBe(fakeOnXHRError) }) - it('does not load feature flags, toolbar, session recording', () => { - const posthog = defaultPostHog().init('testtoken', defaultConfig, uuidv7()) + it.skip('does not load feature flags, session recording', () => { + // TODO this didn't make a tonne of sense in the given form + // it makes no sense now + // of course mocks added _after_ init will not be called + const posthog = defaultPostHog().init('testtoken', defaultConfig, uuidv7())! - posthog.toolbar = { - maybeLoadToolbar: jest.fn(), - afterDecideResponse: jest.fn(), - } posthog.sessionRecording = { afterDecideResponse: jest.fn(), startIfEnabledOrStop: jest.fn(), - } + } as unknown as SessionRecording posthog.persistence = { register: jest.fn(), update_config: jest.fn(), - } + } as unknown as PostHogPersistence // Feature flags expect(posthog.persistence.register).not.toHaveBeenCalled() // FFs are saved this way - // Toolbar - expect(posthog.toolbar.afterDecideResponse).not.toHaveBeenCalled() - // Session recording expect(posthog.sessionRecording.afterDecideResponse).not.toHaveBeenCalled() }) describe('device id behavior', () => { - let uninitialisedPostHog + let uninitialisedPostHog: PostHog beforeEach(() => { uninitialisedPostHog = defaultPostHog() }) it('sets a random UUID as distinct_id/$device_id if distinct_id is unset', () => { - uninitialisedPostHog.persistence = { props: { distinct_id: undefined } } + uninitialisedPostHog.persistence = { + props: { distinct_id: undefined }, + } as unknown as PostHogPersistence const posthog = uninitialisedPostHog.init( uuidv7(), { get_device_id: (uuid) => uuid, }, uuidv7() - ) + )! - expect(posthog.persistence.props).toMatchObject({ + expect(posthog.persistence!.props).toMatchObject({ $device_id: expect.stringMatching(/^[0-9a-f-]+$/), distinct_id: expect.stringMatching(/^[0-9a-f-]+$/), }) - expect(posthog.persistence.props.$device_id).toEqual(posthog.persistence.props.distinct_id) + expect(posthog.persistence!.props.$device_id).toEqual(posthog.persistence!.props.distinct_id) }) it('does not set distinct_id/$device_id if distinct_id is unset', () => { - uninitialisedPostHog.persistence = { props: { distinct_id: 'existing-id' } } + uninitialisedPostHog.persistence = { + props: { distinct_id: 'existing-id' }, + } as unknown as PostHogPersistence const posthog = uninitialisedPostHog.init( uuidv7(), { get_device_id: (uuid) => uuid, }, uuidv7() - ) + )! - expect(posthog.persistence.props.distinct_id).not.toEqual('existing-id') + expect(posthog.persistence!.props.distinct_id).not.toEqual('existing-id') }) it('uses config.get_device_id for uuid generation if passed', () => { @@ -852,9 +858,9 @@ describe('posthog core', () => { persistence: 'memory', }, uuidv7() - ) + )! - expect(posthog.persistence.props).toMatchObject({ + expect(posthog.persistence!.props).toMatchObject({ $device_id: expect.stringMatching(/^custom-[0-9a-f-]+$/), distinct_id: expect.stringMatching(/^custom-[0-9a-f-]+$/), }) @@ -873,7 +879,7 @@ describe('posthog core', () => { }) describe('group()', () => { - let posthog + let posthog: PostHog beforeEach(() => { posthog = defaultPostHog().init( @@ -882,8 +888,8 @@ describe('posthog core', () => { persistence: 'memory', }, uuidv7() - ) - posthog.persistence.clear() + )! + posthog.persistence!.clear() posthog.reloadFeatureFlags = jest.fn() posthog.capture = jest.fn() }) @@ -903,35 +909,35 @@ describe('posthog core', () => { posthog.group('organization', 'org::5', { name: 'PostHog' }) expect(posthog.getGroups()).toEqual({ organization: 'org::5' }) - expect(posthog.persistence.props['$stored_group_properties']).toEqual({ + expect(posthog.persistence!.props['$stored_group_properties']).toEqual({ organization: { name: 'PostHog' }, }) posthog.group('organization', 'org::6') expect(posthog.getGroups()).toEqual({ organization: 'org::6' }) - expect(posthog.persistence.props['$stored_group_properties']).toEqual({ organization: {} }) + expect(posthog.persistence!.props['$stored_group_properties']).toEqual({ organization: {} }) posthog.group('instance', 'app.posthog.com') expect(posthog.getGroups()).toEqual({ organization: 'org::6', instance: 'app.posthog.com' }) - expect(posthog.persistence.props['$stored_group_properties']).toEqual({ organization: {}, instance: {} }) + expect(posthog.persistence!.props['$stored_group_properties']).toEqual({ organization: {}, instance: {} }) // now add properties to the group posthog.group('organization', 'org::7', { name: 'PostHog2' }) expect(posthog.getGroups()).toEqual({ organization: 'org::7', instance: 'app.posthog.com' }) - expect(posthog.persistence.props['$stored_group_properties']).toEqual({ + expect(posthog.persistence!.props['$stored_group_properties']).toEqual({ organization: { name: 'PostHog2' }, instance: {}, }) posthog.group('instance', 'app.posthog.com', { a: 'b' }) expect(posthog.getGroups()).toEqual({ organization: 'org::7', instance: 'app.posthog.com' }) - expect(posthog.persistence.props['$stored_group_properties']).toEqual({ + expect(posthog.persistence!.props['$stored_group_properties']).toEqual({ organization: { name: 'PostHog2' }, instance: { a: 'b' }, }) posthog.resetGroupPropertiesForFlags() - expect(posthog.persistence.props['$stored_group_properties']).toEqual(undefined) + expect(posthog.persistence!.props['$stored_group_properties']).toEqual(undefined) }) it('does not result in a capture call', () => { @@ -978,12 +984,12 @@ describe('posthog core', () => { persistence: 'memory', }, uuidv7() - ) - posthog.persistence.clear() + )! + posthog.persistence!.clear() // mock this internal queue - not capture posthog._requestQueue = { enqueue: jest.fn(), - } + } as unknown as RequestQueue }) it('sends group information in event properties', () => { @@ -992,11 +998,16 @@ describe('posthog core', () => { posthog.capture('some_event', { prop: 5 }) - expect(posthog._requestQueue.enqueue).toHaveBeenCalledTimes(1) + expect(posthog._requestQueue!.enqueue).toHaveBeenCalledTimes(1) - const eventPayload = posthog._requestQueue.enqueue.mock.calls[0][0] - expect(eventPayload.data.event).toEqual('some_event') - expect(eventPayload.data.properties.$groups).toEqual({ + const eventPayload = jest.mocked(posthog._requestQueue!.enqueue).mock.calls[0][0] + // need to help TS know event payload data is not an array + // eslint-disable-next-line posthog-js/no-direct-array-check + if (Array.isArray(eventPayload.data!)) { + throw new Error('') + } + expect(eventPayload.data!.event).toEqual('some_event') + expect(eventPayload.data!.properties.$groups).toEqual({ organization: 'org::5', instance: 'app.posthog.com', }) @@ -1005,14 +1016,14 @@ describe('posthog core', () => { describe('error handling', () => { it('handles blank keys being passed', () => { - window.console.error = jest.fn() - window.console.warn = jest.fn() + ;(window as any).console.error = jest.fn() + ;(window as any).console.warn = jest.fn() posthog.register = jest.fn() - posthog.group(null, 'foo') - posthog.group('organization', null) - posthog.group('organization', undefined) + posthog.group(null as unknown as string, 'foo') + posthog.group('organization', null as unknown as string) + posthog.group('organization', undefined as unknown as string) posthog.group('organization', '') posthog.group('', 'foo') @@ -1025,12 +1036,12 @@ describe('posthog core', () => { posthog.group('organization', 'org::5') posthog.group('instance', 'app.posthog.com', { group: 'property', foo: 5 }) - expect(posthog.persistence.props['$groups']).toEqual({ + expect(posthog.persistence!.props['$groups']).toEqual({ organization: 'org::5', instance: 'app.posthog.com', }) - expect(posthog.persistence.props['$stored_group_properties']).toEqual({ + expect(posthog.persistence!.props['$stored_group_properties']).toEqual({ organization: {}, instance: { group: 'property', @@ -1040,8 +1051,8 @@ describe('posthog core', () => { posthog.resetGroups() - expect(posthog.persistence.props['$groups']).toEqual({}) - expect(posthog.persistence.props['$stored_group_properties']).toEqual(undefined) + expect(posthog.persistence!.props['$groups']).toEqual({}) + expect(posthog.persistence!.props['$stored_group_properties']).toEqual(undefined) expect(posthog.reloadFeatureFlags).toHaveBeenCalledTimes(3) }) @@ -1058,7 +1069,7 @@ describe('posthog core', () => { setReloadingPaused: jest.fn(), resetRequestQueue: jest.fn(), _startReloadTimer: jest.fn(), - }, + } as unknown as PostHogFeatureFlags, _start_queue_if_opted_in: jest.fn(), } ) @@ -1083,7 +1094,7 @@ describe('posthog core', () => { setReloadingPaused: jest.fn(), resetRequestQueue: jest.fn(), _startReloadTimer: jest.fn(), - }, + } as unknown as PostHogFeatureFlags, _start_queue_if_opted_in: jest.fn(), } ) @@ -1096,11 +1107,11 @@ describe('posthog core', () => { describe('/decide', () => { beforeEach(() => { const call = jest.fn() - Decide.mockImplementation(() => ({ call })) + ;(Decide as any).mockImplementation(() => ({ call })) }) afterEach(() => { - Decide.mockReset() + ;(Decide as any).mockReset() }) it('is called by default', async () => { @@ -1108,7 +1119,7 @@ describe('posthog core', () => { instance.featureFlags.setReloadingPaused = jest.fn() instance._loaded() - expect(new Decide().call).toHaveBeenCalled() + expect(new Decide(instance).call).toHaveBeenCalled() expect(instance.featureFlags.setReloadingPaused).toHaveBeenCalledWith(true) }) @@ -1119,7 +1130,7 @@ describe('posthog core', () => { instance.featureFlags.setReloadingPaused = jest.fn() instance._loaded() - expect(new Decide().call).not.toHaveBeenCalled() + expect(new Decide(instance).call).not.toHaveBeenCalled() expect(instance.featureFlags.setReloadingPaused).not.toHaveBeenCalled() }) }) @@ -1166,15 +1177,15 @@ describe('posthog core', () => { }) describe('session_id', () => { - let instance - let token + let instance: PostHog + let token: string beforeEach(async () => { token = uuidv7() instance = await createPosthogInstance(token, { api_host: 'https://us.posthog.com', }) - instance.sessionManager.checkAndGetSessionAndWindowId = jest.fn().mockReturnValue({ + instance.sessionManager!.checkAndGetSessionAndWindowId = jest.fn().mockReturnValue({ windowId: 'windowId', sessionId: 'sessionId', sessionStartTimestamp: new Date().getTime() - 30000,