Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 14 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
})
}

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-05

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-05

## 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
Loading