diff --git a/posthog-core/src/index.ts b/posthog-core/src/index.ts index 44171f8f..ff470a72 100644 --- a/posthog-core/src/index.ts +++ b/posthog-core/src/index.ts @@ -7,6 +7,7 @@ import { PosthogCoreOptions, PostHogEventProperties, PostHogPersistedProperty, + JsonType, } from './types' import { assert, @@ -434,15 +435,22 @@ export abstract class PostHogCore { .then((res) => { if (res.featureFlags) { let newFeatureFlags = res.featureFlags + let newFeatureFlagPayloads = res.featureFlagPayloads if (res.errorsWhileComputingFlags) { // if not all flags were computed, we upsert flags instead of replacing them const currentFlags = this.getPersistedProperty( PostHogPersistedProperty.FeatureFlags ) + const currentFlagPayloads = this.getPersistedProperty( + PostHogPersistedProperty.FeatureFlagPayloads + ) newFeatureFlags = { ...currentFlags, ...res.featureFlags } + newFeatureFlagPayloads = { ...currentFlagPayloads, ...res.featureFlagPayloads } } this.setKnownFeatureFlags(newFeatureFlags) + this.setKnownFeatureFlagPayloads(newFeatureFlagPayloads) } + return res }) .finally(() => { @@ -459,6 +467,13 @@ export abstract class PostHogCore { this._events.emit('featureflags', featureFlags) } + private setKnownFeatureFlagPayloads(featureFlagPayloads: PostHogDecideResponse['featureFlagPayloads']): void { + this.setPersistedProperty( + PostHogPersistedProperty.FeatureFlagPayloads, + featureFlagPayloads + ) + } + getFeatureFlag(key: string): boolean | string | undefined { const featureFlags = this.getFeatureFlags() @@ -487,6 +502,30 @@ export abstract class PostHogCore { return response } + getFeatureFlagPayload(key: string): JsonType | undefined { + const payloads = this.getFeatureFlagPayloads() + + if (!payloads) { + return undefined + } + + let response = payloads[key] + + // Undefined means a loading or missing data issue. Null means evaluation happened and there was no match + if (response === undefined) { + return null + } + + return response + } + + getFeatureFlagPayloads(): PostHogDecideResponse['featureFlagPayloads'] | undefined { + let payloads = this.getPersistedProperty( + PostHogPersistedProperty.FeatureFlagPayloads + ) + return payloads + } + getFeatureFlags(): PostHogDecideResponse['featureFlags'] | undefined { let flags = this.getPersistedProperty(PostHogPersistedProperty.FeatureFlags) const overriddenFlags = this.getPersistedProperty( diff --git a/posthog-core/src/types.ts b/posthog-core/src/types.ts index 47f14c29..252fd417 100644 --- a/posthog-core/src/types.ts +++ b/posthog-core/src/types.ts @@ -34,6 +34,7 @@ export enum PostHogPersistedProperty { DistinctId = 'distinct_id', Props = 'props', FeatureFlags = 'feature_flags', + FeatureFlagPayloads = 'feature_flag_payloads', OverrideFeatureFlags = 'override_feature_flags', Queue = 'queue', OptedOut = 'opted_out', @@ -86,6 +87,11 @@ export type PostHogDecideResponse = { featureFlags: { [key: string]: string | boolean } + featureFlagPayloads: { + [key: string]: JsonType + } errorsWhileComputingFlags: boolean sessionRecording: boolean } + +export type JsonType = string | number | boolean | null | { [key: string]: JsonType } | Array diff --git a/posthog-core/test/posthog.featureflags.spec.ts b/posthog-core/test/posthog.featureflags.spec.ts index 9122647b..b4b83fb6 100644 --- a/posthog-core/test/posthog.featureflags.spec.ts +++ b/posthog-core/test/posthog.featureflags.spec.ts @@ -15,6 +15,13 @@ describe('PostHog Core', () => { 'feature-variant': 'variant', }) + const createMockFeatureFlagPayloads = (): any => ({ + 'feature-1': { + color: 'blue', + }, + 'feature-variant': 5, + }) + const errorAPIResponse = Promise.resolve({ status: 400, text: () => Promise.resolve('error'), @@ -34,6 +41,7 @@ describe('PostHog Core', () => { json: () => Promise.resolve({ featureFlags: createMockFeatureFlags(), + featureFlagPayloads: createMockFeatureFlagPayloads(), }), }) } @@ -55,11 +63,19 @@ describe('PostHog Core', () => { expect(posthog.getFeatureFlags()).toEqual(undefined) }) + it('getFeatureFlagPayloads should return undefined if not loaded', () => { + expect(posthog.getFeatureFlagPayloads()).toEqual(undefined) + }) + it('getFeatureFlag should return undefined if not loaded', () => { expect(posthog.getFeatureFlag('my-flag')).toEqual(undefined) expect(posthog.getFeatureFlag('feature-1')).toEqual(undefined) }) + it('getFeatureFlagPayload should return undefined if not loaded', () => { + expect(posthog.getFeatureFlagPayload('my-flag')).toEqual(undefined) + }) + it('isFeatureEnabled should return undefined if not loaded', () => { expect(posthog.isFeatureEnabled('my-flag')).toEqual(undefined) expect(posthog.isFeatureEnabled('feature-1')).toEqual(undefined) @@ -106,6 +122,13 @@ describe('PostHog Core', () => { 'feature-2': true, 'feature-variant': 'variant', }) + + expect(posthog.getFeatureFlagPayloads()).toEqual({ + 'feature-1': { + color: 'blue', + }, + 'feature-variant': 5, + }) }) it('should return the value of a flag', async () => { @@ -114,6 +137,15 @@ describe('PostHog Core', () => { expect(posthog.getFeatureFlag('feature-missing')).toEqual(false) }) + it('should return payload of matched flags only', async () => { + expect(posthog.getFeatureFlagPayload('feature-variant')).toEqual(5) + expect(posthog.getFeatureFlagPayload('feature-1')).toEqual({ + color: 'blue', + }) + + expect(posthog.getFeatureFlagPayload('feature-2')).toEqual(null) + }) + describe('when errored out', () => { beforeEach(() => { ;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 1 }, (_mocks) => { @@ -160,6 +192,9 @@ describe('PostHog Core', () => { expect(posthog.isFeatureEnabled('feature-1')).toEqual(undefined) expect(posthog.isFeatureEnabled('feature-variant')).toEqual(undefined) expect(posthog.isFeatureEnabled('feature-missing')).toEqual(undefined) + + expect(posthog.getFeatureFlagPayloads()).toEqual(undefined) + expect(posthog.getFeatureFlagPayload('feature-1')).toEqual(undefined) }) })