Skip to content

Commit

Permalink
feat(feature-flags): update to v3, json payload, bootstrap (#511)
Browse files Browse the repository at this point in the history
* update to v3, json payload, bootstrap

* format

* lint

* constants

* update comment

* change to v3

* parse json strings

* lint

* cast

* trigger test
  • Loading branch information
EDsCODE authored Jan 24, 2023
1 parent 8891ba3 commit 932a2e6
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 19 deletions.
2 changes: 1 addition & 1 deletion src/__tests__/compression.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('Payload Compression', () => {
debug: true,
_prepare_callback: sandbox.spy((callback) => callback),
_send_request: sandbox.spy((url, params, options, callback) => {
if (url === 'https://test.com/decide/?v=2') {
if (url === 'https://test.com/decide/?v=3') {
callback({ config: { enable_collect_everything: true }, supportedCompression: ['lz64'] })
} else {
throw new Error('Should not get here')
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/decide.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('Decide', () => {
given.subject()

expect(given.posthog._send_request).toHaveBeenCalledWith(
'https://test.com/decide/?v=2',
'https://test.com/decide/?v=3',
{
data: _base64Encode(
JSON.stringify({
Expand Down
25 changes: 24 additions & 1 deletion src/__tests__/featureflags.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ describe('featureflags', () => {
_prepare_callback: (callback) => callback,
persistence: {
props: {
$feature_flag_payloads: {
'beta-feature': {
some: 'payload',
},
'alpha-feature-2': 200,
},
$active_feature_flags: ['beta-feature', 'alpha-feature-2', 'multivariate-flag'],
$enabled_feature_flags: {
'beta-feature': true,
Expand Down Expand Up @@ -59,6 +65,15 @@ describe('featureflags', () => {
expect(given.instance.capture).not.toHaveBeenCalled()
})

it('should return the right payload', () => {
expect(given.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()
})

it('supports overrides', () => {
given.instance.persistence.props = {
$active_feature_flags: ['beta-feature', 'alpha-feature-2', 'multivariate-flag'],
Expand Down Expand Up @@ -183,13 +198,17 @@ describe('parseFeatureFlagDecideResponse', () => {
given('persistence', () => ({ register: jest.fn(), unregister: jest.fn() }))
given('subject', () => () => parseFeatureFlagDecideResponse(given.decideResponse, given.persistence))

it('enables multivariate feature flags from decide v2 response', () => {
it('enables multivariate feature flags from decide v2^ response', () => {
given('decideResponse', () => ({
featureFlags: {
'beta-feature': true,
'alpha-feature-2': true,
'multivariate-flag': 'variant-1',
},
featureFlagPayloads: {
'beta-feature': 300,
'alpha-feature-2': 'fake-payload',
},
}))
given.subject()

Expand All @@ -200,6 +219,10 @@ describe('parseFeatureFlagDecideResponse', () => {
'alpha-feature-2': true,
'multivariate-flag': 'variant-1',
},
$feature_flag_payloads: {
'beta-feature': 300,
'alpha-feature-2': 'fake-payload',
},
})
})

Expand Down
30 changes: 30 additions & 0 deletions src/__tests__/posthog-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,36 @@ describe('bootstrapping feature flags', () => {
expect(given.lib.featureFlags.getFlagVariants()).toEqual({ multivariant: 'variant-1', enabled: true })
})

it('sets the right feature flag payloads', () => {
given('config', () => ({
bootstrap: {
featureFlags: {
multivariant: 'variant-1',
enabled: true,
jsonString: true,
disabled: false,
undef: undefined,
},
featureFlagPayloads: {
multivariant: 'some-payload',
enabled: {
another: 'value',
},
disabled: 'should not show',
undef: 200,
jsonString: '{"a":"payload"}',
},
},
}))

given.subject()
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)
})

it('does nothing when empty', () => {
given('config', () => ({
bootstrap: {},
Expand Down
2 changes: 1 addition & 1 deletion src/decide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class Decide {

const encoded_data = _base64Encode(json_data)
this.instance._send_request(
`${this.instance.get_config('api_host')}/decide/?v=2`,
`${this.instance.get_config('api_host')}/decide/?v=3`,
{ data: encoded_data, verbose: true },
{ method: 'POST' },
(response) => this.parseDecideResponse(response as DecideResponse)
Expand Down
32 changes: 31 additions & 1 deletion src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
SnippetArrayItem,
XHROptions,
AutocaptureConfig,
JsonType,
} from './types'
import { SentryIntegration } from './extensions/sentry-integration'
import { createSegmentIntegration } from './extensions/segment-integration'
Expand Down Expand Up @@ -394,8 +395,16 @@ export class PostHog {
),
{}
)
const featureFlagPayloads = Object.keys(config.bootstrap?.featureFlagPayloads || {})
.filter((key) => activeFlags[key])
.reduce((res: Record<string, JsonType>, key) => {
if (config.bootstrap?.featureFlagPayloads?.[key]) {
res[key] = config.bootstrap?.featureFlagPayloads?.[key]
}
return res
}, {})

this.featureFlags.receivedFeatureFlags({ featureFlags: activeFlags })
this.featureFlags.receivedFeatureFlags({ featureFlags: activeFlags, featureFlagPayloads })
}

if (!this.get_distinct_id()) {
Expand Down Expand Up @@ -938,6 +947,27 @@ export class PostHog {
return this.featureFlags.getFeatureFlag(key, options)
}

/*
* Get feature flag payload value matching key for user (supports multivariate flags).
*
* ### Usage:
*
* if(posthog.getFeatureFlag('beta-feature') === 'some-value') {
* const someValue = posthog.getFeatureFlagPayload('beta-feature')
* // do something
* }
*
* @param {Object|String} prop Key of the feature flag.
*/
getFeatureFlagPayload(key: string): JsonType {
const payload = this.featureFlags.getFeatureFlagPayload(key)
try {
return JSON.parse(payload as any)
} catch {
return payload
}
}

/*
* See if feature flag is enabled for user.
*
Expand Down
46 changes: 32 additions & 14 deletions src/posthog-featureflags.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { _base64Encode, _extend } from './utils'
import { PostHog } from './posthog-core'
import { DecideResponse, FeatureFlagsCallback, RequestCallback } from './types'
import { DecideResponse, FeatureFlagsCallback, JsonType, RequestCallback } from './types'
import { PostHogPersistence } from './posthog-persistence'

const PERSISTENCE_ACTIVE_FEATURE_FLAGS = '$active_feature_flags'
const PERSISTENCE_ENABLED_FEATURE_FLAGS = '$enabled_feature_flags'
const PERSISTENCE_OVERRIDE_FEATURE_FLAGS = '$override_feature_flags'
const PERSISTENCE_FEATURE_FLAG_PAYLOADS = '$feature_flag_payloads'

export const parseFeatureFlagDecideResponse = (response: Partial<DecideResponse>, persistence: PostHogPersistence) => {
const flags = response['featureFlags']
const flagPayloads = response['featureFlagPayloads']
if (flags) {
// using the v1 api
if (Array.isArray(flags)) {
Expand All @@ -16,21 +22,23 @@ export const parseFeatureFlagDecideResponse = (response: Partial<DecideResponse>
}
persistence &&
persistence.register({
$active_feature_flags: flags,
$enabled_feature_flags,
[PERSISTENCE_ACTIVE_FEATURE_FLAGS]: flags,
[PERSISTENCE_ENABLED_FEATURE_FLAGS]: $enabled_feature_flags,
})
} else {
// using the v2 api
// using the v2+ api
persistence &&
persistence.register({
$active_feature_flags: Object.keys(flags || {}),
$enabled_feature_flags: flags || {},
[PERSISTENCE_ACTIVE_FEATURE_FLAGS]: Object.keys(flags || {}),
[PERSISTENCE_ENABLED_FEATURE_FLAGS]: flags || {},
[PERSISTENCE_FEATURE_FLAG_PAYLOADS]: flagPayloads || {},
})
}
} else {
if (persistence) {
persistence.unregister('$active_feature_flags')
persistence.unregister('$enabled_feature_flags')
persistence.unregister(PERSISTENCE_ACTIVE_FEATURE_FLAGS)
persistence.unregister(PERSISTENCE_ENABLED_FEATURE_FLAGS)
persistence.unregister(PERSISTENCE_FEATURE_FLAG_PAYLOADS)
}
}
}
Expand Down Expand Up @@ -59,8 +67,8 @@ export class PostHogFeatureFlags {
}

getFlagVariants(): Record<string, string | boolean> {
const enabledFlags = this.instance.get_property('$enabled_feature_flags')
const overriddenFlags = this.instance.get_property('$override_feature_flags')
const enabledFlags = this.instance.get_property(PERSISTENCE_ENABLED_FEATURE_FLAGS)
const overriddenFlags = this.instance.get_property(PERSISTENCE_OVERRIDE_FEATURE_FLAGS)
if (!overriddenFlags) {
return enabledFlags || {}
}
Expand All @@ -85,6 +93,11 @@ export class PostHogFeatureFlags {
return finalFlags
}

getFlagPayloads(): Record<string, JsonType> {
const flagPayloads = this.instance.get_property(PERSISTENCE_FEATURE_FLAG_PAYLOADS)
return flagPayloads || {}
}

/**
* Reloads feature flags asynchronously.
*
Expand Down Expand Up @@ -136,7 +149,7 @@ export class PostHogFeatureFlags {

const encoded_data = _base64Encode(json_data)
this.instance._send_request(
this.instance.get_config('api_host') + '/decide/?v=2',
this.instance.get_config('api_host') + '/decide/?v=3',
{ data: encoded_data },
{ method: 'POST' },
this.instance._prepare_callback((response) => {
Expand Down Expand Up @@ -176,6 +189,11 @@ export class PostHogFeatureFlags {
return flagValue
}

getFeatureFlagPayload(key: string): JsonType {
const payloads = this.getFlagPayloads()
return payloads[key]
}

/*
* See if feature flag is enabled for user.
*
Expand Down Expand Up @@ -221,15 +239,15 @@ export class PostHogFeatureFlags {
this._override_warning = false

if (flags === false) {
this.instance.persistence.unregister('$override_feature_flags')
this.instance.persistence.unregister(PERSISTENCE_OVERRIDE_FEATURE_FLAGS)
} else if (Array.isArray(flags)) {
const flagsObj: Record<string, string | boolean> = {}
for (let i = 0; i < flags.length; i++) {
flagsObj[flags[i]] = true
}
this.instance.persistence.register({ $override_feature_flags: flagsObj })
this.instance.persistence.register({ [PERSISTENCE_OVERRIDE_FEATURE_FLAGS]: flagsObj })
} else {
this.instance.persistence.register({ $override_feature_flags: flags })
this.instance.persistence.register({ [PERSISTENCE_OVERRIDE_FEATURE_FLAGS]: flags })
}
}
/*
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export interface PostHogConfig {
distinctID?: string
isIdentifiedID?: boolean
featureFlags?: Record<string, boolean | string>
featureFlagPayloads?: Record<string, JsonType>
}
segment?: any
}
Expand Down Expand Up @@ -191,6 +192,7 @@ export interface DecideResponse {
}
custom_properties: AutoCaptureCustomProperty[] // TODO: delete, not sent
featureFlags: Record<string, string | boolean>
featureFlagPayloads: Record<string, JsonType>
capturePerformance?: boolean
sessionRecording?: {
endpoint?: string
Expand Down Expand Up @@ -282,3 +284,5 @@ export interface JSC {
}

export type SnippetArrayItem = [method: string, ...args: any[]]

export type JsonType = string | number | boolean | null | { [key: string]: JsonType } | Array<JsonType>

0 comments on commit 932a2e6

Please sign in to comment.