-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #320 from oaknational/feat/feature-flag-bootstrap
fix: bootstrap posthog feature flags
- Loading branch information
Showing
11 changed files
with
154 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
"use server"; | ||
|
||
import { auth } from "@clerk/nextjs/server"; | ||
import { posthogAiBetaServerClient } from "@oakai/core/src/analytics/posthogAiBetaServerClient"; | ||
import { aiLogger } from "@oakai/logger"; | ||
import cookie from "cookie"; | ||
import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; | ||
import invariant from "tiny-invariant"; | ||
|
||
const log = aiLogger("analytics:feature-flags"); | ||
|
||
/** | ||
* We use posthog feature flags to toggle functionality without deploying code changes. | ||
* Fething feature flags on the frontend hasn't been reliable for us: | ||
* - you have to wait for a round trip to the posthog API | ||
* - we currently have a bug where denying cookie consent prevents feature flags from loading | ||
* | ||
* Instead, we can bootstrap feature flags by evaluating them on the server. | ||
* https://posthog.com/docs/feature-flags/bootstrapping | ||
*/ | ||
|
||
function getDistinctIdFromCookie(headers: ReadonlyHeaders) { | ||
const cookieHeader = headers.get("cookie"); | ||
invariant(cookieHeader, "No cookie header"); | ||
const cookies = cookie.parse(cookieHeader) as Record<string, string>; | ||
const phCookieKey = `ph_${process.env.NEXT_PUBLIC_POSTHOG_API_KEY}_posthog`; | ||
const phCookie = cookies[phCookieKey]; | ||
if (!phCookie) { | ||
return null; | ||
} | ||
return (JSON.parse(phCookie) as { distinct_id: string })["distinct_id"]; | ||
} | ||
|
||
export async function getBootstrappedFeatures(headers: ReadonlyHeaders) { | ||
const { userId } = auth(); | ||
|
||
const distinctId = userId ?? getDistinctIdFromCookie(headers) ?? "0"; | ||
log.info("Evaluating feature flags for", distinctId); | ||
const features = await posthogAiBetaServerClient.getAllFlags(distinctId, { | ||
// Only bootstrap flags which don't depend on user properties | ||
// These are typically flags representing new features | ||
onlyEvaluateLocally: true, | ||
}); | ||
log.info("Bootstrapping feature flags", features); | ||
return features; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
57 changes: 57 additions & 0 deletions
57
packages/core/src/analytics/posthogAiBetaServerClient/featureFlagEvaluation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { aiLogger } from "@oakai/logger"; | ||
import { kv } from "@vercel/kv"; | ||
import type { PostHog } from "posthog-node"; | ||
|
||
const KV_KEY = "posthog-feature-flag-local-evaluation"; | ||
|
||
const log = aiLogger("analytics:feature-flags"); | ||
|
||
const setKv = async (response: Response) => { | ||
const value = await response.text(); | ||
await kv.set(KV_KEY, value, { ex: 60 }); | ||
}; | ||
|
||
const getKv = async () => { | ||
const value = await kv.get(KV_KEY); | ||
if (!value) { | ||
return null; | ||
} | ||
return { | ||
status: 200, | ||
json: () => Promise.resolve(value), | ||
text: () => Promise.resolve(JSON.stringify(value)), | ||
}; | ||
}; | ||
|
||
export const cachedFetch: PostHog["fetch"] = async (url, options) => { | ||
if (url.includes("api/feature_flag/local_evaluation")) { | ||
const kvCachedResponse = await getKv(); | ||
if (kvCachedResponse) { | ||
log.info("evaluations fetched from KV"); | ||
return kvCachedResponse; | ||
} | ||
const result = await fetch(url, options); | ||
|
||
if (result.ok) { | ||
const cachedResult = result.clone(); | ||
await setKv(cachedResult); | ||
log.info("evaluations cached to KV"); | ||
} else { | ||
log.error("failed to load evaluations", { status: result.status }); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
if (url.includes("/decide")) { | ||
log.warn("WARN: feature flag loaded through API"); | ||
} | ||
|
||
return await fetch(url, options); | ||
}; | ||
|
||
const ONE_DAY = 24 * 60 * 60 * 1000; | ||
const ONE_MINUTE = 60 * 1000; | ||
export const featureFlagsPollingInterval = | ||
// prevent polling timeout from stacking when HMR replaces posthogAiBetaServerClient | ||
process.env.NODE_ENV === "development" ? ONE_DAY : ONE_MINUTE; |
30 changes: 30 additions & 0 deletions
30
packages/core/src/analytics/posthogAiBetaServerClient/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { PostHog } from "posthog-node"; | ||
import invariant from "tiny-invariant"; | ||
|
||
import { | ||
cachedFetch, | ||
featureFlagsPollingInterval, | ||
} from "./featureFlagEvaluation"; | ||
|
||
const host = process.env.NEXT_PUBLIC_POSTHOG_HOST as string; | ||
const apiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY || "*"; | ||
const personalApiKey = process.env.POSTHOG_PERSONAL_KEY_FLAGS; | ||
invariant(personalApiKey, "POSTHOG_PERSONAL_KEY_FLAGS is required"); | ||
|
||
const enableLocalEvaluation = process.env.NODE_ENV !== "test"; | ||
|
||
/** | ||
* This is the posthog nodejs client configured to send events to the | ||
* posthog AI BETA instance. | ||
*/ | ||
export const posthogAiBetaServerClient = new PostHog(apiKey, { | ||
host, | ||
|
||
// We evaluate user feature flags on the server to prevent round-trips to posthog. | ||
// See https://posthog.com/docs/feature-flags/local-evaluation | ||
// As we use edge functions, we can't hold the flag definitions in memory. | ||
// Instead we cache them in KV through a custom fetch implementation. | ||
fetch: cachedFetch, | ||
featureFlagsPollingInterval, | ||
personalApiKey: enableLocalEvaluation ? personalApiKey : undefined, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters