diff --git a/posthog-core/src/index.ts b/posthog-core/src/index.ts index 4e238993..a672d14f 100644 --- a/posthog-core/src/index.ts +++ b/posthog-core/src/index.ts @@ -777,6 +777,8 @@ export abstract class PostHogCore extends PostHogCoreStateless { ) if (Object.keys(bootstrapFlags).length) { + this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlags, bootstrapFlags) + const currentFlags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags) || {} const newFeatureFlags = { ...bootstrapFlags, ...currentFlags } @@ -785,6 +787,8 @@ export abstract class PostHogCore extends PostHogCoreStateless { const bootstrapFlagPayloads = bootstrap.featureFlagPayloads if (bootstrapFlagPayloads && Object.keys(bootstrapFlagPayloads).length) { + this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlagPayloads, bootstrapFlagPayloads) + const currentFlagPayloads = this.getPersistedProperty( PostHogPersistedProperty.FeatureFlagPayloads @@ -1190,6 +1194,8 @@ export abstract class PostHogCore extends PostHogCoreStateless { Object.entries(newFeatureFlagPayloads || {}).map(([k, v]) => [k, this._parsePayload(v)]) ) ) + // Mark that we hit the /decide endpoint so we can capture this in the $feature_flag_called event + this.setPersistedProperty(PostHogPersistedProperty.DecideEndpointWasHit, true) const sessionReplay = res?.sessionRecording if (sessionReplay) { @@ -1252,6 +1258,14 @@ export abstract class PostHogCore extends PostHogCoreStateless { this.capture('$feature_flag_called', { $feature_flag: key, $feature_flag_response: response, + $feature_flag_bootstrapped_response: this.getPersistedProperty( + PostHogPersistedProperty.BootstrapFeatureFlags + )?.[key], + $feature_flag_bootstrapped_payload: this.getPersistedProperty( + PostHogPersistedProperty.BootstrapFeatureFlagPayloads + )?.[key], + // If we haven't yet received a response from the /decide endpoint, we must have used the bootstrapped value + $used_bootstrap_value: !this.getPersistedProperty(PostHogPersistedProperty.DecideEndpointWasHit), }) } diff --git a/posthog-core/src/types.ts b/posthog-core/src/types.ts index 38e8c65f..53681162 100644 --- a/posthog-core/src/types.ts +++ b/posthog-core/src/types.ts @@ -49,6 +49,8 @@ export enum PostHogPersistedProperty { Props = 'props', FeatureFlags = 'feature_flags', FeatureFlagPayloads = 'feature_flag_payloads', + BootstrapFeatureFlags = 'bootstrap_feature_flags', + BootstrapFeatureFlagPayloads = 'bootstrap_feature_flag_payloads', OverrideFeatureFlags = 'override_feature_flags', Queue = 'queue', OptedOut = 'opted_out', @@ -59,6 +61,7 @@ export enum PostHogPersistedProperty { InstalledAppBuild = 'installed_app_build', // only used by posthog-react-native InstalledAppVersion = 'installed_app_version', // only used by posthog-react-native SessionReplay = 'session_replay', // only used by posthog-react-native + DecideEndpointWasHit = 'decide_endpoint_was_hit', // only used by posthog-react-native } export type PostHogFetchOptions = { diff --git a/posthog-core/test/posthog.featureflags.spec.ts b/posthog-core/test/posthog.featureflags.spec.ts index 5bdbcf10..c9d59287 100644 --- a/posthog-core/test/posthog.featureflags.spec.ts +++ b/posthog-core/test/posthog.featureflags.spec.ts @@ -407,6 +407,7 @@ describe('PostHog Core', () => { $feature_flag: 'feature-1', $feature_flag_response: true, '$feature/feature-1': true, + $used_bootstrap_value: false, }, type: 'capture', }, @@ -432,6 +433,7 @@ describe('PostHog Core', () => { $feature_flag: 'feature-1', $feature_flag_response: true, '$feature/feature-1': true, + $used_bootstrap_value: false, }, type: 'capture', }, @@ -453,6 +455,7 @@ describe('PostHog Core', () => { $feature_flag: 'feature-1', $feature_flag_response: true, '$feature/feature-1': true, + $used_bootstrap_value: false, }, type: 'capture', }, @@ -501,7 +504,7 @@ describe('PostHog Core', () => { }) }) - describe('bootstapped feature flags', () => { + describe('bootstrapped feature flags', () => { beforeEach(() => { ;[posthog, mocks] = createTestClient( 'TEST_API_KEY', @@ -509,11 +512,19 @@ describe('PostHog Core', () => { flushAt: 1, bootstrap: { distinctId: 'tomato', - featureFlags: { 'bootstrap-1': 'variant-1', enabled: true, disabled: false }, + featureFlags: { + 'bootstrap-1': 'variant-1', + 'feature-1': 'feature-1-bootstrap-value', + enabled: true, + disabled: false, + }, featureFlagPayloads: { 'bootstrap-1': { some: 'key', }, + 'feature-1': { + color: 'feature-1-bootstrap-color', + }, enabled: 200, }, }, @@ -521,15 +532,7 @@ describe('PostHog Core', () => { (_mocks) => { _mocks.fetch.mockImplementation((url) => { if (url.includes('/decide/')) { - return Promise.resolve({ - status: 200, - text: () => Promise.resolve('ok'), - json: () => - Promise.resolve({ - featureFlags: createMockFeatureFlags(), - featureFlagPayloads: createMockFeatureFlagPayloads(), - }), - }) + return Promise.reject(new Error('Not responding to emulate use of bootstrapped values')) } return Promise.resolve({ @@ -545,19 +548,48 @@ describe('PostHog Core', () => { ) }) - it('getFeatureFlags should return bootstrapped flags', () => { - expect(posthog.getFeatureFlags()).toEqual({ 'bootstrap-1': 'variant-1', enabled: true }) + it('getFeatureFlags should return bootstrapped flags', async () => { + expect(posthog.getFeatureFlags()).toEqual({ + 'bootstrap-1': 'variant-1', + enabled: true, + 'feature-1': 'feature-1-bootstrap-value', + }) expect(posthog.getDistinctId()).toEqual('tomato') expect(posthog.getAnonymousId()).toEqual('tomato') }) - it('getFeatureFlag should return bootstrapped flags', () => { + it('getFeatureFlag should return bootstrapped flags', async () => { expect(posthog.getFeatureFlag('my-flag')).toEqual(false) expect(posthog.getFeatureFlag('bootstrap-1')).toEqual('variant-1') expect(posthog.getFeatureFlag('enabled')).toEqual(true) expect(posthog.getFeatureFlag('disabled')).toEqual(false) }) + it('getFeatureFlag should capture $feature_flag_called with bootstrapped values', async () => { + expect(posthog.getFeatureFlag('bootstrap-1')).toEqual('variant-1') + + await waitForPromises() + expect(mocks.fetch).toHaveBeenCalledTimes(1) + + expect(parseBody(mocks.fetch.mock.calls[0])).toMatchObject({ + batch: [ + { + event: '$feature_flag_called', + distinct_id: posthog.getDistinctId(), + properties: { + $feature_flag: 'bootstrap-1', + $feature_flag_response: 'variant-1', + '$feature/bootstrap-1': 'variant-1', + $feature_flag_bootstrapped_response: 'variant-1', + $feature_flag_bootstrapped_payload: { some: 'key' }, + $used_bootstrap_value: true, + }, + type: 'capture', + }, + ], + }) + }) + it('isFeatureEnabled should return true/false for bootstrapped flags', () => { expect(posthog.isFeatureEnabled('my-flag')).toEqual(false) expect(posthog.isFeatureEnabled('bootstrap-1')).toEqual(true) @@ -575,6 +607,55 @@ describe('PostHog Core', () => { describe('when loaded', () => { beforeEach(() => { + ;[posthog, mocks] = createTestClient( + 'TEST_API_KEY', + { + flushAt: 1, + bootstrap: { + distinctId: 'tomato', + featureFlags: { + 'bootstrap-1': 'variant-1', + 'feature-1': 'feature-1-bootstrap-value', + enabled: true, + disabled: false, + }, + featureFlagPayloads: { + 'bootstrap-1': { + some: 'key', + }, + 'feature-1': { + color: 'feature-1-bootstrap-color', + }, + enabled: 200, + }, + }, + }, + (_mocks) => { + _mocks.fetch.mockImplementation((url) => { + if (url.includes('/decide/')) { + return Promise.resolve({ + status: 200, + text: () => Promise.resolve('ok'), + json: () => + Promise.resolve({ + featureFlags: createMockFeatureFlags(), + featureFlagPayloads: createMockFeatureFlagPayloads(), + }), + }) + } + + return Promise.resolve({ + status: 200, + text: () => Promise.resolve('ok'), + json: () => + Promise.resolve({ + status: 'ok', + }), + }) + }) + } + ) + posthog.reloadFeatureFlags() }) @@ -626,6 +707,31 @@ describe('PostHog Core', () => { }) expect(posthog.getFeatureFlagPayload('feature-variant')).toEqual([5]) }) + + it('should capture $feature_flag_called with bootstrapped values', async () => { + expect(posthog.getFeatureFlag('feature-1')).toEqual(true) + + await waitForPromises() + expect(mocks.fetch).toHaveBeenCalledTimes(2) + + expect(parseBody(mocks.fetch.mock.calls[1])).toMatchObject({ + batch: [ + { + event: '$feature_flag_called', + distinct_id: posthog.getDistinctId(), + properties: { + $feature_flag: 'feature-1', + $feature_flag_response: true, + '$feature/feature-1': true, + $feature_flag_bootstrapped_response: 'feature-1-bootstrap-value', + $feature_flag_bootstrapped_payload: { color: 'feature-1-bootstrap-color' }, + $used_bootstrap_value: false, + }, + type: 'capture', + }, + ], + }) + }) }) }) diff --git a/posthog-core/test/posthog.shutdown.spec.ts b/posthog-core/test/posthog.shutdown.spec.ts index 3ddf27c6..bbdff9d6 100644 --- a/posthog-core/test/posthog.shutdown.spec.ts +++ b/posthog-core/test/posthog.shutdown.spec.ts @@ -22,7 +22,7 @@ describe('PostHog Core', () => { expect(mocks.fetch).toHaveBeenCalledTimes(1) }) - it.only('respects timeout', async () => { + it('respects timeout', async () => { mocks.fetch.mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 1000)) console.log('FETCH RETURNED') diff --git a/posthog-react-native/CHANGELOG.md b/posthog-react-native/CHANGELOG.md index 100c0b9e..16438175 100644 --- a/posthog-react-native/CHANGELOG.md +++ b/posthog-react-native/CHANGELOG.md @@ -1,5 +1,9 @@ # Next +# 3.6.0 - 2024-12-12 + +1. Add new debugging property `$feature_flag_bootstrapped_response`, `$feature_flag_bootstrapped_payload` and `$used_bootstrap_value` to `$feature_flag_called` event + # 3.5.0 - 2024-12-03 1. fix: deprecate maskPhotoLibraryImages due to unintended masking issues diff --git a/posthog-react-native/package.json b/posthog-react-native/package.json index 9ada5d22..991986d7 100644 --- a/posthog-react-native/package.json +++ b/posthog-react-native/package.json @@ -1,6 +1,6 @@ { "name": "posthog-react-native", - "version": "3.5.0", + "version": "3.6.0", "main": "lib/posthog-react-native/index.js", "files": [ "lib/" diff --git a/posthog-web/CHANGELOG.md b/posthog-web/CHANGELOG.md index 02e39f33..7caa150c 100644 --- a/posthog-web/CHANGELOG.md +++ b/posthog-web/CHANGELOG.md @@ -1,5 +1,12 @@ # Next +# 3.2.0 - 2024-12-12 + +## Changed + +1. Add new debugging property `$feature_flag_bootstrapped_response`, `$feature_flag_bootstrapped_payload` and `$used_bootstrap_value` to `$feature_flag_called` event + + # 3.1.0 - 2024-11-21 ## Changed diff --git a/posthog-web/package.json b/posthog-web/package.json index 4c3d0ef1..73a1765a 100644 --- a/posthog-web/package.json +++ b/posthog-web/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js-lite", - "version": "3.1.0", + "version": "3.2.0", "main": "lib/index.cjs.js", "module": "lib/index.esm.js", "types": "lib/index.d.ts",