Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Sep 20, 2024
1 parent 1ad247d commit 7f3b787
Show file tree
Hide file tree
Showing 13 changed files with 293 additions and 8 deletions.
31 changes: 31 additions & 0 deletions apps/renderer/src/hooks/biz/useAb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useAtomValue } from "jotai"
import PostHog from "posthog-js"
import { useFeatureFlagEnabled } from "posthog-js/react"

import { jotaiStore } from "~/lib/jotai"
import type { FeatureKeys } from "~/modules/ab/atoms"
import { debugFeaturesAtom, enableDebugOverrideAtom, IS_DEBUG_ENV } from "~/modules/ab/atoms"

export const useAb = (feature: FeatureKeys) => {
const isEnableDebugOverrides = useAtomValue(enableDebugOverrideAtom)
const debugFeatureOverrides = useAtomValue(debugFeaturesAtom)

const isEnabled = useFeatureFlagEnabled(feature)

if (IS_DEBUG_ENV && isEnableDebugOverrides) return debugFeatureOverrides[feature]

return isEnabled
}

export const getAbValue = (feature: FeatureKeys) => {
const enabled = PostHog.getFeatureFlag(feature)
const debugOverride = jotaiStore.get(debugFeaturesAtom)

const isEnableOverride = jotaiStore.get(enableDebugOverrideAtom)

if (isEnableOverride) {
return debugOverride[feature]
}

return enabled
}
5 changes: 2 additions & 3 deletions apps/renderer/src/initialize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,12 @@ export const initializeApp = async () => {
})

// should after hydrateSettings
const { dataPersist: enabledDataPersist, sendAnonymousData } = getGeneralSettings()
const { dataPersist: enabledDataPersist } = getGeneralSettings()

initSentry()
initPostHog()
await apm("i18n", initI18n)

if (sendAnonymousData) initPostHog()

let dataHydratedTime: undefined | number
// Initialize the database
if (enabledDataPersist) {
Expand Down
9 changes: 6 additions & 3 deletions apps/renderer/src/initialize/posthog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { env } from "@follow/shared/env"
import type { CaptureOptions, Properties } from "posthog-js"
import { posthog } from "posthog-js"

import { getGeneralSettings } from "~/atoms/settings/general"
import { whoami } from "~/atoms/user"

declare global {
Expand All @@ -12,9 +14,6 @@ declare global {
}
}
export const initPostHog = async () => {
if (import.meta.env.DEV) return
const { default: posthog } = await import("posthog-js")

if (env.VITE_POSTHOG_KEY === undefined) return
posthog.init(env.VITE_POSTHOG_KEY, {
person_profiles: "identified_only",
Expand All @@ -25,6 +24,10 @@ export const initPostHog = async () => {
window.posthog = {
reset,
capture(event_name: string, properties?: Properties | null, options?: CaptureOptions) {
if (import.meta.env.DEV) return
if (!getGeneralSettings().sendAnonymousData) {
return
}
return capture.apply(posthog, [
event_name,
{
Expand Down
10 changes: 9 additions & 1 deletion apps/renderer/src/lib/img-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { env } from "@follow/shared/env"
import { imageRefererMatches } from "@follow/shared/image"

import { getAbValue } from "~/hooks/biz/useAb"

export const getImageProxyUrl = ({
url,
width,
Expand All @@ -9,7 +11,13 @@ export const getImageProxyUrl = ({
url: string
width: number
height: number
}) => `${env.VITE_IMGPROXY_URL}?url=${encodeURIComponent(url)}&width=${width}&height=${height}`
}) => {
if (getAbValue("Image_Proxy_V2")) {
return `${env.VITE_IMGPROXY_URL}?url=${encodeURIComponent(url)}&width=${width}&height=${height}`
} else {
return `${env.VITE_IMGPROXY_URL}/unsafe/fit-in/${width}x${height}/${encodeURIComponent(url)}`
}
}

export const replaceImgUrlIfNeed = (url: string) => {
for (const rule of imageRefererMatches) {
Expand Down
15 changes: 15 additions & 0 deletions apps/renderer/src/modules/ab/atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import FEATURES from "@constants/flags.json"
import { atom } from "jotai"
import { atomWithStorage } from "jotai/utils"

import { getStorageNS } from "~/lib/ns"

export type FeatureKeys = keyof typeof FEATURES

export const debugFeaturesAtom = atomWithStorage(getStorageNS("ab"), FEATURES, undefined, {
getOnInit: true,
})

export const IS_DEBUG_ENV = import.meta.env.DEV || import.meta.env["PREVIEW_MODE"]

export const enableDebugOverrideAtom = atom(IS_DEBUG_ENV)
30 changes: 30 additions & 0 deletions apps/renderer/src/modules/ab/hoc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { FC } from "react"
import { forwardRef } from "react"

import { useAb } from "~/hooks/biz/useAb"

import type { FeatureKeys } from "./atoms"

const Noop = () => null
export const withFeature =
(feature: FeatureKeys) =>
<T extends object>(
Component: FC<T>,

FallbackComponent: any = Noop,
) => {
// @ts-expect-error
const WithFeature = forwardRef((props: T, ref: any) => {
const isEnabled = useAb(feature)

if (isEnabled === undefined) return null

return isEnabled ? (
<Component {...props} ref={ref} />
) : (
<FallbackComponent {...props} ref={ref} />
)
})

return WithFeature
}
84 changes: 84 additions & 0 deletions apps/renderer/src/modules/ab/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useAtomValue, useSetAtom } from "jotai"

import { Divider } from "~/components/ui/divider"
import { Label } from "~/components/ui/label"
import { useModalStack } from "~/components/ui/modal"
import { RootPortal } from "~/components/ui/portal"
import { Switch } from "~/components/ui/switch"

import type { FeatureKeys } from "./atoms"
import { debugFeaturesAtom, enableDebugOverrideAtom, IS_DEBUG_ENV } from "./atoms"

export const FeatureFlagDebugger = () => {
if (IS_DEBUG_ENV) return <DebugToggle />

return null
}

const DebugToggle = () => {
const { present } = useModalStack()
return (
<RootPortal>
<div
tabIndex={-1}
onClick={() => {
present({
title: "A/B",
content: ABModalContent,
})
}}
className="fixed bottom-5 right-0 flex size-5 items-center justify-center opacity-40 duration-200 hover:opacity-100"
>
<i className="i-mingcute-switch-line" />
</div>
</RootPortal>
)
}

const SwitchInternal = ({ Key }: { Key: FeatureKeys }) => {
const enabled = useAtomValue(debugFeaturesAtom)[Key]
const setDebugFeatures = useSetAtom(debugFeaturesAtom)
return (
<Switch
checked={enabled}
onCheckedChange={(checked) => {
setDebugFeatures((prev) => ({ ...prev, [Key]: checked }))
}}
/>
)
}

const ABModalContent = () => {
const features = useAtomValue(debugFeaturesAtom)

const enableOverride = useAtomValue(enableDebugOverrideAtom)
const setEnableDebugOverride = useSetAtom(enableDebugOverrideAtom)
return (
<div>
<Label className="flex items-center justify-between">
Enable Override A/B
<Switch
checked={enableOverride}
onCheckedChange={(checked) => {
setEnableDebugOverride(checked)
}}
/>
</Label>

<Divider />

<div className={enableOverride ? "opacity-100" : "pointer-events-none opacity-40"}>
{Object.keys(features).map((key) => {
return (
<div key={key} className="flex w-full items-center justify-between">
<Label className="flex w-full items-center justify-between text-sm">
{key}
<SwitchInternal Key={key as any} />
</Label>
</div>
)
})}
</div>
</div>
)
}
2 changes: 2 additions & 0 deletions apps/renderer/src/providers/root-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Toaster } from "~/components/ui/sonner"
import { HotKeyScopeMap } from "~/constants"
import { jotaiStore } from "~/lib/jotai"
import { persistConfig, queryClient } from "~/lib/query-client"
import { FeatureFlagDebugger } from "~/modules/ab/providers"

import { ContextMenuProvider } from "./context-menu-provider"
import { EventProvider } from "./event-provider"
Expand Down Expand Up @@ -39,6 +40,7 @@ export const RootProviders: FC<PropsWithChildren> = ({ children }) => (
<ModalStackProvider />
<ContextMenuProvider />
<StableRouterProvider />
<FeatureFlagDebugger />
{import.meta.env.DEV && <Devtools />}
{children}
</I18nProvider>
Expand Down
3 changes: 2 additions & 1 deletion apps/renderer/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"paths": {
"~/*": ["src/*"],
"@pkg": ["../../package.json"],
"@locales/*": ["../../locales/*"]
"@locales/*": ["../../locales/*"],
"@constants/*": ["../../constants/*"]
}
}
}
1 change: 1 addition & 0 deletions configs/vite.render.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const viteRenderBaseConfig = {
"@env": resolve("src/env.ts"),
"@locales": resolve("locales"),
"@follow/electron-main": resolve("apps/main/src"),
"@constants": resolve("constants"),
},
},
base: "/",
Expand Down
3 changes: 3 additions & 0 deletions constants/flags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Image_Proxy_V2": true
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"prepare": "pnpm exec simple-git-hooks && shx test -f .env || shx cp .env.example .env",
"publish": "electron-vite build --outDir=dist && electron-forge publish",
"start": "electron-vite preview",
"sync:ab": "tsx scripts/pull-ab-flags.ts",
"test": "pnpm -F web run test",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"typecheck:node": "pnpm -F electron-main run typecheck",
Expand Down
107 changes: 107 additions & 0 deletions scripts/pull-ab-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import "dotenv/config"

import { readFileSync, writeFileSync } from "node:fs"
import path, { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { inspect } from "node:util"

import { ofetch } from "ofetch"

const { POSTHOG_TOKEN } = process.env

if (!POSTHOG_TOKEN) {
throw new Error("POSTHOG_TOKEN is not set")
}
// https://posthog.com/docs/api/feature-flags#post-api-organizations-parent_lookup_organization_id-feature_flags-copy_flags
const listRes: ListRes = await ofetch(
`https://app.posthog.com/api/projects/${process.env.POSTHOG_PROJECT_ID}/feature_flags/?limit=9999`,
{
method: "GET",
headers: {
Authorization: `Bearer ${POSTHOG_TOKEN}`,
},
},
)

interface ListRes {
count: number
next: null
previous: null
results: ResultsItem[]
}
interface ResultsItem {
id: number
name: string
key: string
filters: any[]
deleted: boolean
active: boolean
created_by: any[]
created_at: string
is_simple_flag: boolean
rollout_percentage: number
ensure_experience_continuity: boolean
experiment_set: any[]
surveys: any[]
features: any[]
rollback_conditions: any[]
performed_rollback: boolean
can_edit: boolean
usage_dashboard: number
analytics_dashboards: any[]
has_enriched_analytics: boolean
tags: any[]
}

const existFlags = {} as Record<string, boolean>

listRes.results.forEach((flag) => (existFlags[flag.key] = true))

const __dirname = resolve(dirname(fileURLToPath(import.meta.url)))
const localFlagsString = readFileSync(path.join(__dirname, "../constants/flags.json"), "utf8")
const localFlags = JSON.parse(localFlagsString as string) as Record<string, boolean>

const updateToRmoteFlags = {} as Record<string, boolean>

// If remote key has but local not has, add to Local
for (const key in existFlags) {
if (!(key in localFlags)) {
localFlags[key] = existFlags[key]
}
}

// Write to local flags
writeFileSync(path.join(__dirname, "../constants/flags.json"), JSON.stringify(localFlags, null, 2))

console.info("update local flags", inspect(localFlags))

// Local first
for (const key in localFlags) {
// existFlags[key] = localFlags[key]
if (existFlags[key] !== localFlags[key]) {
updateToRmoteFlags[key] = localFlags[key]
}
}

if (Object.keys(updateToRmoteFlags).length > 0) {
await Promise.allSettled(
Object.entries(updateToRmoteFlags).map(([key, flag]) => {
return fetch(
`https://app.posthog.com/api/projects/${process.env.POSTHOG_PROJECT_ID}/feature_flags/`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.POSTHOG_PRIVATE_KEY}`,
},
body: JSON.stringify({
key,
active: flag,
}),
},
)
}),
)

console.info("update flags", inspect(updateToRmoteFlags))
}

0 comments on commit 7f3b787

Please sign in to comment.