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: launchdarkly context + chore: refactor edge functions #1604

Merged
merged 14 commits into from
Oct 8, 2024
2 changes: 1 addition & 1 deletion packages/commons/fdr-utils/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"include": ["./src/**/*"],
"references": [
{
"path": "../../fdr-sdk"
"path": "../../fdr-sdk/tsconfig.build.json"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const Breadcrumb = memo(
return (
<Head>
<script
key="jsonld-breadcrumb"
type="application/ld+json"
id="jsonld-breadcrumb"
dangerouslySetInnerHTML={{
Expand Down
3 changes: 1 addition & 2 deletions packages/commons/search-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
},
"dependencies": {
"@fern-api/fdr-sdk": "workspace:*",
"@fern-ui/core-utils": "workspace:*",
"@vercel/edge-config": "^1.1.0"
"@fern-ui/core-utils": "workspace:*"
},
"devDependencies": {
"@fern-platform/configs": "workspace:*",
Expand Down
29 changes: 7 additions & 22 deletions packages/commons/search-utils/src/SearchConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import type {
InkeepSearchSettings,
InkeepWidgetBaseSettings,
} from "@inkeep/widgets";
import { getAll } from "@vercel/edge-config";
import type { DeepReadonly } from "ts-essentials";

const FEATURE_FLAGS = ["inkeep-enabled" as const];

export type InkeepSharedSettings = DeepReadonly<{
replaceSearch: boolean;
baseSettings: InkeepWidgetBaseSettings;
Expand All @@ -20,10 +17,6 @@ export type InkeepSharedSettings = DeepReadonly<{
modalSettings?: Partial<InkeepModalSettings>;
}>;

interface EdgeConfigResponse {
"inkeep-enabled"?: Record<string, InkeepSharedSettings>;
}

export declare namespace SearchConfig {
interface Unversioned {
type: "unversioned";
Expand Down Expand Up @@ -128,8 +121,8 @@ async function getAlgoliaSearchConfig(

export async function getSearchConfig(
client: FdrClient,
domain: string,
searchInfo: Algolia.SearchInfo,
inkeep: InkeepSharedSettings | undefined,
): Promise<SearchConfig> {
const algolia = await getAlgoliaSearchConfig(client, searchInfo);

Expand All @@ -138,20 +131,12 @@ export async function getSearchConfig(
return { isAvailable: false };
}

try {
const config = await getAll<EdgeConfigResponse>(FEATURE_FLAGS);
const maybeInkeep = config["inkeep-enabled"]?.[domain];

if (maybeInkeep?.baseSettings.integrationId != null) {
return {
isAvailable: true,
algolia,
inkeep: maybeInkeep,
};
}
} catch (e) {
// eslint-disable-next-line no-console
console.error("Error fetching edge config", e);
if (inkeep?.baseSettings.integrationId != null) {
return {
isAvailable: true,
algolia,
inkeep,
};
}

return { isAvailable: true, algolia };
Expand Down
2 changes: 1 addition & 1 deletion packages/commons/search-utils/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"include": ["./src/**/*"],
"references": [
{
"path": "../../fdr-sdk"
"path": "../../fdr-sdk/tsconfig.build.json"
},
{
"path": "../core-utils"
Expand Down
2 changes: 1 addition & 1 deletion packages/fdr-sdk/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"exclude": ["**/__test__/**"]
"exclude": ["node_modules", "**/__test__/**", "**/*.json"]
}
2 changes: 1 addition & 1 deletion packages/healthchecks/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"path": "../commons/core-utils"
},
{
"path": "../fdr-sdk"
"path": "../../fdr-sdk/tsconfig.build.json"
}
]
}
2 changes: 1 addition & 1 deletion packages/template-resolver/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"include": ["./src/**/*"],
"references": [
{
"path": "../fdr-sdk"
"path": "../fdr-sdk/tsconfig.build.json"
}
]
}
3 changes: 2 additions & 1 deletion packages/ui/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"exports": {
"./bundlers": "./src/mdx/bundlers/index.ts",
"./bundlers/next-mdx-remote": "./src/mdx/bundlers/next-mdx-remote.ts",
"./auth": "./src/auth/index.ts",
".": "./src/index.ts"
},
"sideEffects": [
Expand Down Expand Up @@ -40,6 +39,7 @@
"@fern-ui/components": "workspace:*",
"@fern-ui/core-utils": "workspace:*",
"@fern-ui/fdr-utils": "workspace:*",
"@fern-ui/fern-docs-utils": "workspace:*",
"@fern-ui/loadable": "workspace:*",
"@fern-ui/next-seo": "workspace:*",
"@fern-ui/react-commons": "workspace:*",
Expand Down Expand Up @@ -120,6 +120,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@fern-ui/fern-docs-auth": "workspace:*",
"@chromatic-com/storybook": "^1.3.5",
"@fern-platform/configs": "workspace:*",
"@mdx-js/esbuild": "^3.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/app/src/atoms/auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FernUser } from "@fern-ui/fern-docs-auth";
import { useAtomValue } from "jotai";
import { selectAtom } from "jotai/utils";
import { isEqual } from "lodash-es";
import type { FernUser } from "../auth";
import { DOCS_ATOM } from "./docs";

export const FERN_USER_ATOM = selectAtom(DOCS_ATOM, (docs) => docs.user, isEqual);
Expand Down
36 changes: 2 additions & 34 deletions packages/ui/app/src/atoms/docs.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,9 @@
import { DocsV1Read } from "@fern-api/fdr-sdk";
import * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { DEFAULT_FEATURE_FLAGS } from "@fern-ui/fern-docs-utils";
import { atomWithReducer, useHydrateAtoms } from "jotai/utils";
import type { PropsWithChildren, ReactNode } from "react";
import type { DocsProps, FeatureFlags } from "./types";

export const DEFAULT_FEATURE_FLAGS: FeatureFlags = {
isApiPlaygroundEnabled: false,
isApiScrollingDisabled: false,
isWhitelabeled: false,
isSeoDisabled: false,
isTocDefaultEnabled: false,
isSnippetTemplatesEnabled: false,
isHttpSnippetsEnabled: false,
isInlineFeedbackEnabled: false,
isDarkCodeEnabled: false,
proxyShouldUseAppBuildwithfernCom: false,
isImageZoomDisabled: false,
useJavaScriptAsTypeScript: false,
alwaysEnableJavaScriptFetch: false,
scrollInContainerEnabled: false,
useMdxBundler: false,
isBatchStreamToggleDisabled: false,
isAuthEnabledInDocs: false,
isAiChatbotEnabledInPreview: false,
isAudioFileDownloadSpanSummary: false,
isDocsLogoTextEnabled: false,
isAudioExampleInternal: false,
usesApplicationJsonInFormDataValue: false,
isBinaryOctetStreamAudioPlayer: false,
hasVoiceIdPlaygroundForm: false,
isCohereTheme: false,
isFileForgeHackEnabled: false,
is404PageHidden: false,
isNewSearchExperienceEnabled: false,
// TODO: remove this after pinecone demo, this is a temporary flag
grpcEndpoints: [],
};
import type { DocsProps } from "./types";

export const EMPTY_ANALYTICS_CONFIG: DocsV1Read.AnalyticsConfig = {
segment: undefined,
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/app/src/atoms/flags.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { FeatureFlags } from "@fern-ui/fern-docs-utils";
import { useAtomValue } from "jotai";
import { selectAtom } from "jotai/utils";
import { isEqual } from "lodash-es";
import { DOCS_ATOM } from "./docs";
import { FeatureFlags } from "./types";

export const FEATURE_FLAGS_ATOM = selectAtom(DOCS_ATOM, (docs) => docs.featureFlags, isEqual);
FEATURE_FLAGS_ATOM.debugLabel = "FEATURE_FLAGS_ATOM";
Expand Down
94 changes: 94 additions & 0 deletions packages/ui/app/src/atoms/launchdarkly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { atom, useAtomValue, useSetAtom } from "jotai";
import * as LDClient from "launchdarkly-js-client-sdk";
import { useCallback, useEffect, useState } from "react";
import useSWR from "swr";
import { useApiRoute } from "../hooks/useApiRoute";

// NOTE do not export this file in any index.ts file so that it can be properly tree-shaken
// otherwise we risk importing launchdarkly-js-client-sdk in all of our bundles

// TODO: consolidate the types with the edge-config package
interface LaunchDarklyInfo {
clientSideId: string;
kind: "multi";
user:
| { anonymous: true }
| {
key: string;
email?: string;
name?: string;
};
device: {
key: string;
[key: string]: unknown;
};
}

// this is a singleton atom that initializes the LaunchDarkly client-side SDK
const LD_CLIENT_ATOM = atom<LDClient.LDClient | undefined>(undefined);

const SET_LAUNCH_DARKLY_INFO_ATOM = atom(undefined, async (get, set, info: LaunchDarklyInfo) => {
const client = get(LD_CLIENT_ATOM);
const { clientSideId, ...context } = info;

if (client) {
await client.identify(context);
return;
} else {
const client = LDClient.initialize(clientSideId, context);
await client.waitForInitialization();
set(LD_CLIENT_ATOM, client);
}
});

// TODO: support non-boolean flags
export const useLaunchDarklyFlag = (flag: string): boolean => {
useInitLaunchDarklyClient();

const client = useAtomValue(LD_CLIENT_ATOM);

// TODO: bootstrap the flag value from the server, and/or local storage
const getFlagEnabled = useCallback(() => {
if (!client) {
return false;
}
// force the flag to be a boolean:
return !!client.variation(flag, false);
}, [client, flag]);

const [enabled, setEnabled] = useState(getFlagEnabled);

// listen for changes to the flag
useEffect(() => {
setEnabled(getFlagEnabled());

if (!client) {
return;
}

const listener = () => {
setEnabled(getFlagEnabled());
};

client.on("ready", listener);
client.on("change", listener);

return () => {
client.off("ready", listener);
client.off("change", listener);
};
}, [client, flag, getFlagEnabled]);

return enabled;
};

// since useSWR is cached globally, we can use this hook in multiple components without worrying about multiple requests
function useInitLaunchDarklyClient() {
const route = useApiRoute("/api/fern-docs/integrations/launchdarkly");
const setInfo = useSetAtom(SET_LAUNCH_DARKLY_INFO_ATOM);
useSWR(route, (key): Promise<LaunchDarklyInfo> => fetch(key).then((res) => res.json()), {
onSuccess(data) {
void setInfo(data);
},
});
}
36 changes: 2 additions & 34 deletions packages/ui/app/src/atoms/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,13 @@ import type { DocsV1Read, DocsV2Read, FdrAPI } from "@fern-api/fdr-sdk/client/ty
import type * as FernDocs from "@fern-api/fdr-sdk/docs";
import type * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { ColorsConfig, SidebarTab, VersionSwitcherInfo } from "@fern-ui/fdr-utils";
import type { FernUser } from "@fern-ui/fern-docs-auth";
import type { FeatureFlags } from "@fern-ui/fern-docs-utils";
import { NextSeoProps } from "@fern-ui/next-seo";
import { CustomerAnalytics } from "../analytics/types";
import { FernUser } from "../auth";
import { DocsContent } from "../resolver/DocsContent";
import { FernTheme } from "../themes/ThemedDocs";

export interface FeatureFlags {
isApiPlaygroundEnabled: boolean;
isApiScrollingDisabled: boolean;
isWhitelabeled: boolean;
isSeoDisabled: boolean;
isTocDefaultEnabled: boolean;
isSnippetTemplatesEnabled: boolean;
isHttpSnippetsEnabled: boolean;
isInlineFeedbackEnabled: boolean;
isDarkCodeEnabled: boolean;
proxyShouldUseAppBuildwithfernCom: boolean;
isImageZoomDisabled: boolean;
useJavaScriptAsTypeScript: boolean;
alwaysEnableJavaScriptFetch: boolean;
scrollInContainerEnabled: boolean;
useMdxBundler: boolean;
isBatchStreamToggleDisabled: boolean;
isAuthEnabledInDocs: boolean;
isAiChatbotEnabledInPreview: boolean;
isAudioFileDownloadSpanSummary: boolean;
isDocsLogoTextEnabled: boolean;
isAudioExampleInternal: boolean;
usesApplicationJsonInFormDataValue: boolean;
isBinaryOctetStreamAudioPlayer: boolean;
hasVoiceIdPlaygroundForm: boolean;
isCohereTheme: boolean;
isFileForgeHackEnabled: boolean;
is404PageHidden: boolean;
isNewSearchExperienceEnabled: boolean;
// TODO: remove this after pinecone demo, this is a temporary flag
grpcEndpoints: readonly string[];
}

export interface NavigationProps {
currentTabIndex: number | undefined;
tabs: SidebarTab[];
Expand Down
22 changes: 22 additions & 0 deletions packages/ui/app/src/components/LinkPreload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Head from "next/head";
import { ReactElement } from "react";
import { useIsLocalPreview } from "../contexts/local-preview";
import { useApiRoute } from "../hooks/useApiRoute";

export function LinkPreload({ href }: { href: string }): ReactElement {
return (
<Head>
<link key={href} rel="preload" href={href} as="fetch" crossOrigin="anonymous" />
</Head>
);
}

type FernDocsApiRoute = `/api/fern-docs/${string}`;
export function LinkPreloadApiRoute({ href }: { href: FernDocsApiRoute }): ReactElement | null {
const isLocalPreview = useIsLocalPreview();
const key = useApiRoute(href);
if (isLocalPreview) {
return null;
}
return <LinkPreload href={key} />;
}
3 changes: 3 additions & 0 deletions packages/ui/app/src/docs/DocsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ReactElement } from "react";
import { HydrateAtoms, useMessageHandler, useSetJustNavigated, type DocsProps } from "../atoms";
import { BgImageGradient } from "../components/BgImageGradient";
import { JavascriptProvider } from "../components/JavascriptProvider";
import { LinkPreloadApiRoute } from "../components/LinkPreload";
import { useBeforePopState } from "../hooks/useBeforePopState";
import { useConsoleMessage } from "../hooks/useConsoleMessage";
import { useRouteChangeComplete, useRouteChangeStart } from "../hooks/useRouteChanged";
Expand Down Expand Up @@ -45,6 +46,8 @@ export function DocsPage(pageProps: DocsProps): ReactElement | null {

return (
<>
<LinkPreloadApiRoute href="/api/fern-docs/search" />
<LinkPreloadApiRoute href="/api/fern-docs/auth/api-key-injection" />
<NextSeo />
<InitializeTheme />
<SearchDialog />
Expand Down
3 changes: 1 addition & 2 deletions packages/ui/app/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export { type CustomerAnalytics } from "./analytics/types";
export { DEFAULT_FEATURE_FLAGS } from "./atoms";
export type { DocsProps, FeatureFlags } from "./atoms";
export type { DocsProps } from "./atoms";
export * from "./docs/DocsPage";
export * from "./docs/NextApp";
export { getApiRouteSupplier } from "./hooks/useApiRoute";
Expand Down
Loading
Loading