Skip to content

Commit 2649a9a

Browse files
dmarticusclaude
andauthored
feat(flags): add fresh option to feature flag methods (#3119)
* feat(flags): add `fresh` option to feature flag methods Add a `fresh` option to `getFeatureFlag()`, `getFeatureFlagResult()`, and `isFeatureEnabled()` that only returns values loaded from the server, not cached localStorage values. By default, these methods may return cached values from localStorage if the `/flags` endpoint hasn't responded yet. This reduces flicker but means you might briefly see stale values (e.g., a flag that was disabled on the server). With `{ fresh: true }`, the methods return `undefined` until the `/flags` endpoint responds, ensuring you always get the current server state. Usage: ```js // Default behavior (may return cached values) posthog.getFeatureFlag('my-flag') // Only return fresh values from server posthog.getFeatureFlag('my-flag', { fresh: true }) ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor(flags): centralize FeatureFlagOptions type Extract the inline `{ send_event?: boolean; fresh?: boolean }` options type into a centralized `FeatureFlagOptions` type in @posthog/types. This reduces duplication across 9 occurrences in 3 files and provides a single source of truth for the feature flag lookup options. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * include changeset --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2fafa6b commit 2649a9a

File tree

8 files changed

+143
-16
lines changed

8 files changed

+143
-16
lines changed

.changeset/tidy-mails-cough.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'posthog-js': patch
3+
'@posthog/types': patch
4+
---
5+
6+
Adds a fresh option to getFeatureFlag(), getFeatureFlagResult(), and isFeatureEnabled() that only returns values loaded from the server, not cached localStorage values.

packages/browser/src/__tests__/featureflags.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,56 @@ describe('featureflags', () => {
157157
)
158158
})
159159

160+
describe('fresh option', () => {
161+
it('should return undefined when fresh: true and flags have not been loaded from remote', () => {
162+
// Flags exist in persistence (from previous session)
163+
featureFlags._hasLoadedFlags = true
164+
// But they haven't been loaded from the server yet
165+
featureFlags._flagsLoadedFromRemote = false
166+
167+
expect(featureFlags.getFeatureFlag('beta-feature')).toEqual(true)
168+
expect(featureFlags.getFeatureFlag('beta-feature', { fresh: true })).toEqual(undefined)
169+
170+
expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(true)
171+
expect(featureFlags.isFeatureEnabled('beta-feature', { fresh: true })).toEqual(undefined)
172+
173+
expect(featureFlags.getFeatureFlagResult('beta-feature')).toEqual({
174+
key: 'beta-feature',
175+
enabled: true,
176+
variant: undefined,
177+
payload: { some: 'payload' },
178+
})
179+
expect(featureFlags.getFeatureFlagResult('beta-feature', { fresh: true })).toEqual(undefined)
180+
})
181+
182+
it('should return flag value when fresh: true and flags have been loaded from remote', () => {
183+
featureFlags._hasLoadedFlags = true
184+
featureFlags._flagsLoadedFromRemote = true
185+
186+
expect(featureFlags.getFeatureFlag('beta-feature', { fresh: true })).toEqual(true)
187+
expect(featureFlags.isFeatureEnabled('beta-feature', { fresh: true })).toEqual(true)
188+
expect(featureFlags.getFeatureFlagResult('beta-feature', { fresh: true })).toEqual({
189+
key: 'beta-feature',
190+
enabled: true,
191+
variant: undefined,
192+
payload: { some: 'payload' },
193+
})
194+
})
195+
196+
it('should return undefined for fresh: true when only localStorage cache exists', () => {
197+
// Simulate: flags exist in localStorage from previous session
198+
// but no network request has completed yet
199+
featureFlags._hasLoadedFlags = false
200+
featureFlags._flagsLoadedFromRemote = false
201+
202+
// Without fresh option, cached values are returned
203+
expect(featureFlags.getFeatureFlag('beta-feature')).toEqual(true)
204+
205+
// With fresh option, undefined is returned
206+
expect(featureFlags.getFeatureFlag('beta-feature', { fresh: true })).toEqual(undefined)
207+
})
208+
})
209+
160210
it('should return the right feature flag and call capture', () => {
161211
featureFlags._hasLoadedFlags = false
162212

packages/browser/src/posthog-core.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
ExceptionAutoCaptureConfig,
5757
FeatureFlagDetail,
5858
FeatureFlagsCallback,
59+
FeatureFlagOptions,
5960
FeatureFlagResult,
6061
JsonType,
6162
OverrideConfig,
@@ -1679,8 +1680,9 @@ export class PostHog implements PostHogInterface {
16791680
*
16801681
* @param {Object|String} prop Key of the feature flag.
16811682
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
1683+
* If {fresh: true}, we won't return cached values from localStorage - only values loaded from the server.
16821684
*/
1683-
getFeatureFlag(key: string, options?: { send_event?: boolean }): boolean | string | undefined {
1685+
getFeatureFlag(key: string, options?: FeatureFlagOptions): boolean | string | undefined {
16841686
return this.featureFlags.getFeatureFlag(key, options)
16851687
}
16861688

@@ -1736,9 +1738,10 @@ export class PostHog implements PostHogInterface {
17361738
* @param {string} key Key of the feature flag.
17371739
* @param {Object} [options] Options for the feature flag lookup.
17381740
* @param {boolean} [options.send_event=true] If false, won't send the $feature_flag_called event.
1741+
* @param {boolean} [options.fresh=false] If true, won't return cached values from localStorage - only values loaded from the server.
17391742
* @returns {FeatureFlagResult | undefined} The feature flag result including key, enabled, variant, and payload.
17401743
*/
1741-
getFeatureFlagResult(key: string, options?: { send_event?: boolean }): FeatureFlagResult | undefined {
1744+
getFeatureFlagResult(key: string, options?: FeatureFlagOptions): FeatureFlagResult | undefined {
17421745
return this.featureFlags.getFeatureFlagResult(key, options)
17431746
}
17441747

@@ -1771,8 +1774,9 @@ export class PostHog implements PostHogInterface {
17711774
*
17721775
* @param {Object|String} prop Key of the feature flag.
17731776
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
1777+
* If {fresh: true}, we won't return cached values from localStorage - only values loaded from the server.
17741778
*/
1775-
isFeatureEnabled(key: string, options?: { send_event: boolean }): boolean | undefined {
1779+
isFeatureEnabled(key: string, options?: FeatureFlagOptions): boolean | undefined {
17761780
return this.featureFlags.isFeatureEnabled(key, options)
17771781
}
17781782

packages/browser/src/posthog-featureflags.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
EarlyAccessFeatureStage,
1414
FeatureFlagDetail,
1515
FeatureFlagResult,
16+
FeatureFlagOptions,
1617
OverrideFeatureFlagsOptions,
1718
} from './types'
1819
import { PostHogPersistence } from './posthog-persistence'
@@ -528,17 +529,32 @@ export class PostHogFeatureFlags {
528529
})
529530
}
530531

531-
/*
532+
/**
532533
* Get feature flag's value for user.
533534
*
535+
* By default, this method may return cached values from localStorage if the `/flags` endpoint
536+
* hasn't responded yet. This reduces flicker but means you might briefly see stale values
537+
* (e.g., a flag that was disabled on the server).
538+
*
534539
* ### Usage:
535540
*
536541
* if(posthog.getFeatureFlag('my-flag') === 'some-variant') { // do something }
537542
*
538-
* @param {Object|String} key Key of the feature flag.
539-
* @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_called event to PostHog.
543+
* // Only use fresh values from the server (returns undefined until /flags responds)
544+
* if(posthog.getFeatureFlag('my-flag', { fresh: true }) === 'some-variant') { // do something }
545+
*
546+
* @param {String} key Key of the feature flag.
547+
* @param {Object} options Optional settings.
548+
* @param {boolean} [options.send_event=true] If false, won't send a $feature_flag_called event to PostHog.
549+
* @param {boolean} [options.fresh=false] If true, only returns values loaded from the server, not cached localStorage values.
550+
* Use this when you need to ensure the flag value reflects the current server state,
551+
* such as after disabling a flag. Returns undefined until the /flags endpoint responds.
552+
* @returns {boolean | string | undefined} The flag value, or undefined if not found or not yet loaded.
540553
*/
541-
getFeatureFlag(key: string, options: { send_event?: boolean } = {}): boolean | string | undefined {
554+
getFeatureFlag(key: string, options: FeatureFlagOptions = {}): boolean | string | undefined {
555+
if (options.fresh && !this._flagsLoadedFromRemote) {
556+
return undefined
557+
}
542558
if (!this._hasLoadedFlags && !(this.getFlags() && this.getFlags().length > 0)) {
543559
logger.warn('getFeatureFlag for key "' + key + '" failed. Feature flags didn\'t load in time.')
544560
return undefined
@@ -578,19 +594,32 @@ export class PostHogFeatureFlags {
578594
* Get a feature flag result including both the flag value and payload, while properly tracking the call.
579595
* This method emits the `$feature_flag_called` event by default.
580596
*
597+
* By default, this method may return cached values from localStorage if the `/flags` endpoint
598+
* hasn't responded yet. This reduces flicker but means you might briefly see stale values
599+
* (e.g., a flag that was disabled on the server).
600+
*
581601
* ### Usage:
582602
*
583603
* const result = posthog.getFeatureFlagResult('my-flag')
584604
* if (result?.enabled) {
585605
* console.log('Flag is enabled with payload:', result.payload)
586606
* }
587607
*
608+
* // Only use fresh values from the server
609+
* const freshResult = posthog.getFeatureFlagResult('my-flag', { fresh: true })
610+
*
588611
* @param {String} key Key of the feature flag.
589612
* @param {Object} [options] Options for the feature flag lookup.
590613
* @param {boolean} [options.send_event=true] If false, won't send the $feature_flag_called event.
614+
* @param {boolean} [options.fresh=false] If true, only returns values loaded from the server, not cached localStorage values.
615+
* Use this when you need to ensure the flag value reflects the current server state.
616+
* Returns undefined until the /flags endpoint responds.
591617
* @returns {FeatureFlagResult | undefined} The feature flag result including key, enabled, variant, and payload.
592618
*/
593-
getFeatureFlagResult(key: string, options: { send_event?: boolean } = {}): FeatureFlagResult | undefined {
619+
getFeatureFlagResult(key: string, options: FeatureFlagOptions = {}): FeatureFlagResult | undefined {
620+
if (options.fresh && !this._flagsLoadedFromRemote) {
621+
return undefined
622+
}
594623
if (!this._hasLoadedFlags && !(this.getFlags() && this.getFlags().length > 0)) {
595624
logger.warn('getFeatureFlagResult for key "' + key + '" failed. Feature flags didn\'t load in time.')
596625
return undefined
@@ -731,16 +760,29 @@ export class PostHogFeatureFlags {
731760
/**
732761
* See if feature flag is enabled for user.
733762
*
763+
* By default, this method may return cached values from localStorage if the `/flags` endpoint
764+
* hasn't responded yet. This reduces flicker but means you might briefly see stale values
765+
* (e.g., a flag that was disabled on the server).
766+
*
734767
* ### Usage:
735768
*
736769
* if(posthog.isFeatureEnabled('beta-feature')) { // do something }
737770
*
738-
* @param key Key of the feature flag.
739-
* @param [options] If {send_event: false}, we won't send an $feature_flag_call event to PostHog.
740-
* @returns A boolean value indicating whether or not the specified feature flag is enabled. If flag information has not yet been loaded,
741-
* or if the specified feature flag is disabled or does not exist, returns undefined.
771+
* // Only use fresh values from the server
772+
* if(posthog.isFeatureEnabled('beta-feature', { fresh: true })) { // do something }
773+
*
774+
* @param {String} key Key of the feature flag.
775+
* @param {Object} [options] Optional settings.
776+
* @param {boolean} [options.send_event=true] If false, won't send a $feature_flag_called event to PostHog.
777+
* @param {boolean} [options.fresh=false] If true, only returns values loaded from the server, not cached localStorage values.
778+
* Use this when you need to ensure the flag value reflects the current server state.
779+
* Returns undefined until the /flags endpoint responds.
780+
* @returns {boolean | undefined} Whether the flag is enabled, or undefined if not found or not yet loaded.
742781
*/
743-
isFeatureEnabled(key: string, options: { send_event?: boolean } = {}): boolean | undefined {
782+
isFeatureEnabled(key: string, options: FeatureFlagOptions = {}): boolean | undefined {
783+
if (options.fresh && !this._flagsLoadedFromRemote) {
784+
return undefined
785+
}
744786
if (!this._hasLoadedFlags && !(this.getFlags() && this.getFlags().length > 0)) {
745787
logger.warn('isFeatureEnabled for key "' + key + '" failed. Feature flags didn\'t load in time.')
746788
return undefined

packages/browser/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type {
2323
FeatureFlagMetadata,
2424
EvaluationReason,
2525
FeatureFlagResult,
26+
FeatureFlagOptions,
2627
RemoteConfigFeatureFlagCallback,
2728
EarlyAccessFeature,
2829
EarlyAccessFeatureStage,

packages/types/src/feature-flags.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,22 @@ export type FeatureFlagOverrideOptions = {
8888
suppressWarning?: boolean
8989
}
9090

91+
/**
92+
* Options for feature flag lookup methods (getFeatureFlag, isFeatureEnabled, getFeatureFlagResult).
93+
*/
94+
export type FeatureFlagOptions = {
95+
/**
96+
* Whether to send a $feature_flag_called event. Defaults to true.
97+
*/
98+
send_event?: boolean
99+
/**
100+
* If true, only return values loaded from the server, not cached localStorage values.
101+
* Returns undefined if flags haven't been loaded from the server yet.
102+
* Defaults to false.
103+
*/
104+
fresh?: boolean
105+
}
106+
91107
/**
92108
* Options for overriding feature flags on the client-side.
93109
*

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export type {
2828
FeatureFlagMetadata,
2929
EvaluationReason,
3030
FeatureFlagResult,
31+
FeatureFlagOptions,
3132
RemoteConfigFeatureFlagCallback,
3233
EarlyAccessFeature,
3334
EarlyAccessFeatureStage,

packages/types/src/posthog.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
EarlyAccessFeatureCallback,
1414
EarlyAccessFeatureStage,
1515
FeatureFlagResult,
16+
FeatureFlagOptions,
1617
OverrideFeatureFlagsOptions,
1718
} from './feature-flags'
1819
import type { SessionIdChangedCallback } from './session-recording'
@@ -231,9 +232,11 @@ export interface PostHog {
231232
*
232233
* @param key - The feature flag key
233234
* @param options - Options for the feature flag lookup
235+
* @param options.send_event - Whether to send a $feature_flag_called event (default: true)
236+
* @param options.fresh - If true, only return values loaded from the server, not cached localStorage values (default: false)
234237
* @returns The feature flag value (boolean for simple flags, string for multivariate)
235238
*/
236-
getFeatureFlag(key: string, options?: { send_event?: boolean }): boolean | string | undefined
239+
getFeatureFlag(key: string, options?: FeatureFlagOptions): boolean | string | undefined
237240

238241
/**
239242
* Get the payload of a feature flag.
@@ -251,18 +254,22 @@ export interface PostHog {
251254
*
252255
* @param key - The feature flag key
253256
* @param options - Options for the feature flag lookup
257+
* @param options.send_event - Whether to send a $feature_flag_called event (default: true)
258+
* @param options.fresh - If true, only return values loaded from the server, not cached localStorage values (default: false)
254259
* @returns The feature flag result including key, enabled, variant, and payload, or undefined if not loaded
255260
*/
256-
getFeatureFlagResult(key: string, options?: { send_event?: boolean }): FeatureFlagResult | undefined
261+
getFeatureFlagResult(key: string, options?: FeatureFlagOptions): FeatureFlagResult | undefined
257262

258263
/**
259264
* Check if a feature flag is enabled.
260265
*
261266
* @param key - The feature flag key
262267
* @param options - Options for the feature flag lookup
268+
* @param options.send_event - Whether to send a $feature_flag_called event (default: true)
269+
* @param options.fresh - If true, only return values loaded from the server, not cached localStorage values (default: false)
263270
* @returns Whether the feature flag is enabled
264271
*/
265-
isFeatureEnabled(key: string, options?: { send_event?: boolean }): boolean | undefined
272+
isFeatureEnabled(key: string, options?: FeatureFlagOptions): boolean | undefined
266273

267274
/**
268275
* Reload feature flags from the server.

0 commit comments

Comments
 (0)