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

feat(feature-flags): Enable experience continuity #404

Merged
merged 9 commits into from
Jun 28, 2022
118 changes: 105 additions & 13 deletions src/__tests__/featureflags.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { PostHogFeatureFlags, parseFeatureFlagDecideResponse } from '../posthog-featureflags'
jest.useFakeTimers()
jest.spyOn(global, 'setTimeout')

describe('featureflags', () => {
given('decideEndpointWasHit', () => false)
given('instance', () => ({
get_config: jest.fn().mockImplementation((key) => given.config[key]),
get_property: (key) => given.properties[key],
get_distinct_id: () => 'blah id',
getGroups: () => {},
_prepare_callback: (callback) => callback,
persistence: {
props: {
$active_feature_flags: ['beta-feature', 'alpha-feature-2', 'multivariate-flag'],
$enabled_feature_flags: {
'beta-feature': true,
'alpha-feature-2': true,
'multivariate-flag': 'variant-1',
},
$override_feature_flags: false,
},
register: (dict) => {
given.instance.persistence.props = { ...given.instance.persistence.props, ...dict }
},
},
get_property: (key) => given.instance.persistence.props[key],
capture: () => {},
decideEndpointWasHit: given.decideEndpointWasHit,
_send_request: jest.fn().mockImplementation((url, data, headers, callback) => callback(given.decideResponse)),
}))

given('featureFlags', () => new PostHogFeatureFlags(given.instance))
Expand All @@ -16,16 +36,6 @@ describe('featureflags', () => {
jest.spyOn(window.console, 'warn').mockImplementation()
})

given('properties', () => ({
$active_feature_flags: ['beta-feature', 'alpha-feature-2', 'multivariate-flag'],
$enabled_feature_flags: {
'beta-feature': true,
'alpha-feature-2': true,
'multivariate-flag': 'variant-1',
},
$override_feature_flags: false,
}))

it('should return the right feature flag and call capture', () => {
expect(given.featureFlags.getFlags()).toEqual(['beta-feature', 'alpha-feature-2', 'multivariate-flag'])
expect(given.featureFlags.getFlagVariants()).toEqual({
Expand All @@ -50,7 +60,7 @@ describe('featureflags', () => {
})

it('supports overrides', () => {
given('properties', () => ({
given.instance.persistence.props = {
$active_feature_flags: ['beta-feature', 'alpha-feature-2', 'multivariate-flag'],
$enabled_feature_flags: {
'beta-feature': true,
Expand All @@ -61,7 +71,8 @@ describe('featureflags', () => {
'beta-feature': false,
'alpha-feature-2': 'as-a-variant',
},
}))
}

expect(given.featureFlags.getFlags()).toEqual(['alpha-feature-2', 'multivariate-flag'])
expect(given.featureFlags.getFlagVariants()).toEqual({
'alpha-feature-2': 'as-a-variant',
Expand All @@ -84,6 +95,87 @@ describe('featureflags', () => {

called = false
})

describe('reloadFeatureFlags', () => {
given('decideResponse', () => ({
featureFlags: {
first: 'variant-1',
second: true,
},
}))

given('config', () => ({
token: 'random fake token',
}))

it('on providing anonDistinctId', () => {
given.featureFlags.setAnonymousDistinctId('rando_id')
given.featureFlags.reloadFeatureFlags()

jest.runAllTimers()

expect(given.featureFlags.getFlagVariants()).toEqual({
first: 'variant-1',
second: true,
})

// check the request sent $anon_distinct_id
expect(
JSON.parse(Buffer.from(given.instance._send_request.mock.calls[0][1].data, 'base64').toString())
).toEqual({
token: 'random fake token',
distinct_id: 'blah id',
$anon_distinct_id: 'rando_id',
})
})

it('on providing anonDistinctId and calling reload multiple times', () => {
given.featureFlags.setAnonymousDistinctId('rando_id')
given.featureFlags.reloadFeatureFlags()
given.featureFlags.reloadFeatureFlags()

jest.runAllTimers()

expect(given.featureFlags.getFlagVariants()).toEqual({
first: 'variant-1',
second: true,
})

// check the request sent $anon_distinct_id
expect(
JSON.parse(Buffer.from(given.instance._send_request.mock.calls[0][1].data, 'base64').toString())
).toEqual({
token: 'random fake token',
distinct_id: 'blah id',
$anon_distinct_id: 'rando_id',
})

given.featureFlags.reloadFeatureFlags()
given.featureFlags.reloadFeatureFlags()
jest.runAllTimers()

// check the request didn't send $anon_distinct_id the second time around
expect(
JSON.parse(Buffer.from(given.instance._send_request.mock.calls[1][1].data, 'base64').toString())
).toEqual({
token: 'random fake token',
distinct_id: 'blah id',
// $anon_distinct_id: "rando_id"
})

given.featureFlags.reloadFeatureFlags()
jest.runAllTimers()

// check the request didn't send $anon_distinct_id the second time around
expect(
JSON.parse(Buffer.from(given.instance._send_request.mock.calls[2][1].data, 'base64').toString())
).toEqual({
token: 'random fake token',
distinct_id: 'blah id',
// $anon_distinct_id: "rando_id"
})
})
})
})

describe('parseFeatureFlagDecideResponse', () => {
Expand Down
13 changes: 12 additions & 1 deletion src/__tests__/posthog-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ describe('identify()', () => {
_captureMetrics: {
incr: jest.fn(),
},
featureFlags: {
setAnonymousDistinctId: jest.fn(),
},
reloadFeatureFlags: jest.fn(),
}))

Expand Down Expand Up @@ -63,6 +66,7 @@ describe('identify()', () => {
{ $set_once: {} }
)
expect(given.overrides.people.set).not.toHaveBeenCalled()
expect(given.overrides.featureFlags.setAnonymousDistinctId).toHaveBeenCalledWith('oldIdentity')
})

it('calls capture when identity changes and old ID is anonymous', () => {
Expand All @@ -80,6 +84,7 @@ describe('identify()', () => {
{ $set_once: {} }
)
expect(given.overrides.people.set).not.toHaveBeenCalled()
expect(given.overrides.featureFlags.setAnonymousDistinctId).toHaveBeenCalledWith('oldIdentity')
})

it("don't identify if the old id isn't anonymous", () => {
Expand All @@ -89,6 +94,7 @@ describe('identify()', () => {

expect(given.overrides.capture).not.toHaveBeenCalled()
expect(given.overrides.people.set).not.toHaveBeenCalled()
expect(given.overrides.featureFlags.setAnonymousDistinctId).not.toHaveBeenCalled()
})

it('calls capture with user properties if passed', () => {
Expand All @@ -106,6 +112,7 @@ describe('identify()', () => {
{ $set: { email: 'john@example.com' } },
{ $set_once: { howOftenAmISet: 'once!' } }
)
expect(given.overrides.featureFlags.setAnonymousDistinctId).toHaveBeenCalledWith('oldIdentity')
})

describe('identity did not change', () => {
Expand All @@ -116,6 +123,7 @@ describe('identify()', () => {

expect(given.overrides.capture).not.toHaveBeenCalled()
expect(given.overrides.people.set).not.toHaveBeenCalled()
expect(given.overrides.featureFlags.setAnonymousDistinctId).not.toHaveBeenCalled()
})

it('calls people.set when user properties passed', () => {
Expand All @@ -125,6 +133,7 @@ describe('identify()', () => {
given.subject()

expect(given.overrides.capture).not.toHaveBeenCalled()
expect(given.overrides.featureFlags.setAnonymousDistinctId).not.toHaveBeenCalled()
expect(given.overrides.people.set).toHaveBeenCalledWith({ email: 'john@example.com' })
expect(given.overrides.people.set_once).toHaveBeenCalledWith({ howOftenAmISet: 'once!' })
})
Expand All @@ -148,6 +157,7 @@ describe('identify()', () => {
it('reloads when identity changes', () => {
given.subject()

expect(given.overrides.featureFlags.setAnonymousDistinctId).toHaveBeenCalledWith('oldIdentity')
expect(given.overrides.reloadFeatureFlags).toHaveBeenCalled()
})

Expand All @@ -156,6 +166,7 @@ describe('identify()', () => {

given.subject()

expect(given.overrides.featureFlags.setAnonymousDistinctId).not.toHaveBeenCalled()
expect(given.overrides.reloadFeatureFlags).not.toHaveBeenCalled()
})

Expand All @@ -165,7 +176,7 @@ describe('identify()', () => {
given('userPropertiesToSetOnce', () => ({ howOftenAmISet: 'once!' }))

given.subject()

expect(given.overrides.featureFlags.setAnonymousDistinctId).not.toHaveBeenCalled()
expect(given.overrides.reloadFeatureFlags).not.toHaveBeenCalled()
})
})
Expand Down
3 changes: 3 additions & 0 deletions src/posthog-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,9 @@ PostHogLib.prototype.identify = function (new_distinct_id, userPropertiesToSet,
{ $set: userPropertiesToSet || {} },
{ $set_once: userPropertiesToSetOnce || {} }
)
// let the reload feature flag request know to send this previous distinct id
// for flag consistency
this.featureFlags.setAnonymousDistinctId(previous_distinct_id)
} else {
if (userPropertiesToSet) {
this['people'].set(userPropertiesToSet)
Expand Down
10 changes: 10 additions & 0 deletions src/posthog-featureflags.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export class PostHogFeatureFlags {
}
}

setAnonymousDistinctId(anon_distinct_id) {
this.$anon_distinct_id = anon_distinct_id
}

setReloadingPaused(isPaused) {
this.reloadFeatureFlagsInAction = isPaused
}
Expand All @@ -116,13 +120,19 @@ export class PostHogFeatureFlags {
token: token,
distinct_id: this.instance.get_distinct_id(),
groups: this.instance.getGroups(),
$anon_distinct_id: this.$anon_distinct_id,
})

const encoded_data = _.base64Encode(json_data)
this.instance._send_request(
this.instance.get_config('api_host') + '/decide/?v=2',
{ data: encoded_data },
{ method: 'POST' },
this.instance._prepare_callback((response) => {
// reset anon_distinct_id after at least a single request with it
// makes it through
this.$anon_distinct_id = undefined

this.receivedFeatureFlags(response)

// :TRICKY: Reload - start another request if queued!
Expand Down