Skip to content

Commit

Permalink
chore(flags): Add new debugging property `$feature_flag_bootstrapped_…
Browse files Browse the repository at this point in the history
…response` and `$feature_flag_bootstrapped_payload` to `$feature_flag_called` event (#320)

* chore(flags): Add new debugging property `$feature_flag_bootstrapped_response` and `$feature_flag_bootstrapped_payload` to `$feature_flag_called` event

* Add $used_bootstrap_value evt property, add+update some tests

* Update changelog

* tweak

* address comments

* tweak

* tweak

* update changelog date

* tweak

---------

Co-authored-by: Manoel Aranda Neto <[email protected]>
  • Loading branch information
havenbarnes and marandaneto authored Dec 13, 2024
1 parent 06973dd commit 2baa794
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 17 deletions.
14 changes: 14 additions & 0 deletions posthog-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,8 @@ export abstract class PostHogCore extends PostHogCoreStateless {
)

if (Object.keys(bootstrapFlags).length) {
this.setPersistedProperty(PostHogPersistedProperty.BootstrapFeatureFlags, bootstrapFlags)

const currentFlags =
this.getPersistedProperty<PostHogDecideResponse['featureFlags']>(PostHogPersistedProperty.FeatureFlags) || {}
const newFeatureFlags = { ...bootstrapFlags, ...currentFlags }
Expand All @@ -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<PostHogDecideResponse['featureFlagPayloads']>(
PostHogPersistedProperty.FeatureFlagPayloads
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<PostHogDecideResponse['featureFlags']>(
PostHogPersistedProperty.BootstrapFeatureFlags
)?.[key],
$feature_flag_bootstrapped_payload: this.getPersistedProperty<PostHogDecideResponse['featureFlagPayloads']>(
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),
})
}

Expand Down
3 changes: 3 additions & 0 deletions posthog-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 = {
Expand Down
134 changes: 120 additions & 14 deletions posthog-core/test/posthog.featureflags.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand All @@ -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',
},
Expand All @@ -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',
},
Expand Down Expand Up @@ -501,35 +504,35 @@ describe('PostHog Core', () => {
})
})

describe('bootstapped feature flags', () => {
describe('bootstrapped feature flags', () => {
beforeEach(() => {
;[posthog, mocks] = createTestClient(
'TEST_API_KEY',
{
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,
},
},
},
(_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({
Expand All @@ -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)
Expand All @@ -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()
})

Expand Down Expand Up @@ -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',
},
],
})
})
})
})

Expand Down
2 changes: 1 addition & 1 deletion posthog-core/test/posthog.shutdown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 4 additions & 0 deletions posthog-react-native/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion posthog-react-native/package.json
Original file line number Diff line number Diff line change
@@ -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/"
Expand Down
7 changes: 7 additions & 0 deletions posthog-web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion posthog-web/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down

0 comments on commit 2baa794

Please sign in to comment.