Cache feature flags per-request, set default values flag to false by default.
This commit is contained in:
parent
4623fd406e
commit
08a56ef480
|
@ -18,4 +18,7 @@ export type ContextMap = {
|
||||||
oauthClient: OAuth2Client
|
oauthClient: OAuth2Client
|
||||||
clients: Record<string, GoogleSpreadsheet>
|
clients: Record<string, GoogleSpreadsheet>
|
||||||
}
|
}
|
||||||
|
featureFlagCache?: {
|
||||||
|
[key: string]: Record<string, any>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,10 @@ export function init(opts?: PostHogOptions) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function shutdown() {
|
||||||
|
posthog?.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class Flag<T> {
|
export abstract class Flag<T> {
|
||||||
static boolean(defaultValue: boolean): Flag<boolean> {
|
static boolean(defaultValue: boolean): Flag<boolean> {
|
||||||
return new BooleanFlag(defaultValue)
|
return new BooleanFlag(defaultValue)
|
||||||
|
@ -87,7 +91,14 @@ class NumberFlag extends Flag<number> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
constructor(private readonly flagSchema: T) {}
|
// This is used to safely cache flags sets in the current request context.
|
||||||
|
// Because multiple sets could theoretically exist, we don't want the cache of
|
||||||
|
// one to leak into another.
|
||||||
|
private readonly setId: string
|
||||||
|
|
||||||
|
constructor(private readonly flagSchema: T) {
|
||||||
|
this.setId = crypto.randomUUID()
|
||||||
|
}
|
||||||
|
|
||||||
defaults(): FlagValues<T> {
|
defaults(): FlagValues<T> {
|
||||||
return Object.keys(this.flagSchema).reduce((acc, key) => {
|
return Object.keys(this.flagSchema).reduce((acc, key) => {
|
||||||
|
@ -119,6 +130,15 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
|
|
||||||
async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
|
async fetch(ctx?: UserCtx): Promise<FlagValues<T>> {
|
||||||
return await tracer.trace("features.fetch", async span => {
|
return await tracer.trace("features.fetch", async span => {
|
||||||
|
const requestContext = context.getCurrentContext()
|
||||||
|
const cachedFlags = requestContext?.featureFlagCache?.[this.setId] as
|
||||||
|
| FlagValues<T>
|
||||||
|
| undefined
|
||||||
|
if (cachedFlags) {
|
||||||
|
span?.addTags({ fromCache: true })
|
||||||
|
return cachedFlags
|
||||||
|
}
|
||||||
|
|
||||||
const tags: Record<string, any> = {}
|
const tags: Record<string, any> = {}
|
||||||
const flagValues = this.defaults()
|
const flagValues = this.defaults()
|
||||||
const currentTenantId = context.getTenantId()
|
const currentTenantId = context.getTenantId()
|
||||||
|
@ -187,10 +207,7 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
tags[`identity.tenantId`] = identity?.tenantId
|
tags[`identity.tenantId`] = identity?.tenantId
|
||||||
tags[`identity._id`] = identity?._id
|
tags[`identity._id`] = identity?._id
|
||||||
|
|
||||||
// Until we're confident this performs well, we're only enabling it in QA
|
if (posthog && identity?.type === IdentityType.USER) {
|
||||||
// and test environments.
|
|
||||||
const usePosthog = env.isTest() || env.isQA()
|
|
||||||
if (usePosthog && posthog && identity?.type === IdentityType.USER) {
|
|
||||||
tags[`readFromPostHog`] = true
|
tags[`readFromPostHog`] = true
|
||||||
|
|
||||||
const personProperties: Record<string, string> = {}
|
const personProperties: Record<string, string> = {}
|
||||||
|
@ -204,7 +221,6 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
personProperties,
|
personProperties,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
console.log("posthog flags", JSON.stringify(posthogFlags))
|
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
|
for (const [name, value] of Object.entries(posthogFlags.featureFlags)) {
|
||||||
if (!this.isFlagName(name)) {
|
if (!this.isFlagName(name)) {
|
||||||
|
@ -236,6 +252,11 @@ export class FlagSet<V extends Flag<any>, T extends { [key: string]: V }> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestContext) {
|
||||||
|
requestContext.featureFlagCache ??= {}
|
||||||
|
requestContext.featureFlagCache[this.setId] = flagValues
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(flagValues)) {
|
for (const [key, value] of Object.entries(flagValues)) {
|
||||||
tags[`flags.${key}.value`] = value
|
tags[`flags.${key}.value`] = value
|
||||||
}
|
}
|
||||||
|
@ -255,5 +276,5 @@ export const flags = new FlagSet({
|
||||||
GOOGLE_SHEETS: Flag.boolean(false),
|
GOOGLE_SHEETS: Flag.boolean(false),
|
||||||
USER_GROUPS: Flag.boolean(false),
|
USER_GROUPS: Flag.boolean(false),
|
||||||
ONBOARDING_TOUR: Flag.boolean(false),
|
ONBOARDING_TOUR: Flag.boolean(false),
|
||||||
DEFAULT_VALUES: Flag.boolean(true),
|
DEFAULT_VALUES: Flag.boolean(false),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { IdentityContext, IdentityType, UserCtx } from "@budibase/types"
|
import { IdentityContext, IdentityType, UserCtx } from "@budibase/types"
|
||||||
import { Flag, FlagSet, FlagValues, init } from "../"
|
import { Flag, FlagSet, FlagValues, init, shutdown } from "../"
|
||||||
import { context } from "../.."
|
import { context } from "../.."
|
||||||
import environment, { withEnv } from "../../environment"
|
import environment, { withEnv } from "../../environment"
|
||||||
import nodeFetch from "node-fetch"
|
import nodeFetch from "node-fetch"
|
||||||
|
@ -197,6 +197,8 @@ describe("feature flags", () => {
|
||||||
throw new Error("No expected value")
|
throw new Error("No expected value")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
shutdown()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue