Skip to content

Commit

Permalink
Add a/b split
Browse files Browse the repository at this point in the history
  • Loading branch information
brookslybrand committed Jul 24, 2024
1 parent d34d428 commit edcc822
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 112 deletions.
46 changes: 46 additions & 0 deletions app/modules/search/ab-session.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createCookieSessionStorage } from "@remix-run/node";

export let unencryptedSession = createCookieSessionStorage({
cookie: {
name: "ab_session",
path: "/",
sameSite: "lax",
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 1 week
},
});

const SESSION_KEY = "ab-docsearch-bucket";

export async function bucketUser(request: Request) {
let session = await unencryptedSession.getSession(
request.headers.get("Cookie")
);

let { searchParams } = new URL(request.url);
let bucket = searchParams.get("bucket");

// if the bucket isn't being overridden by a query parameter, use the session
if (!isBucketValue(bucket)) {
bucket = session.get(SESSION_KEY);
}

// if no bucket in the session, assign the user
if (!isBucketValue(bucket)) {
bucket = Math.random() > 0.5 ? "orama" : "docsearch";
}

let safeBucket = isBucketValue(bucket) ? bucket : "docsearch";

session.set(SESSION_KEY, safeBucket);

return {
bucket: safeBucket,
headers: {
"Set-Cookie": await unencryptedSession.commitSession(session),
},
};
}

function isBucketValue(bucket: any): bucket is "docsearch" | "orama" {
return bucket === "docsearch" || bucket === "orama";
}
37 changes: 37 additions & 0 deletions app/modules/search/docsearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { DocSearchProps } from "@docsearch/react";
import { useHydrated } from "~/ui/utils";
import { Suspense, lazy } from "react";

const OriginalDocSearch = lazy(() =>
import("@docsearch/react").then((module) => ({
default: module.DocSearch,
}))
);

let docSearchProps = {
appId: "RB6LOUCOL0",
indexName: "reactrouter",
apiKey: "b50c5d7d9f4610c9785fa945fdc97476",
} satisfies DocSearchProps;

// TODO: Refactor a bit when we add Vite with css imports per component
// This will allow us to have two versions of the component, one that has
// the button with display: none, and the other with button styles
export function DocSearch() {
let hydrated = useHydrated();

if (!hydrated) {
// The Algolia doc search container is hard-coded at 40px. It doesn't
// render anything on the server, so we get a mis-match after hydration.
// This placeholder prevents layout shift when the search appears.
return <div className="h-10" />;
}

return (
<Suspense fallback={<div className="h-10" />}>
<div className="animate-[fadeIn_100ms_ease-in_1]">
<OriginalDocSearch {...docSearchProps} />
</div>
</Suspense>
);
}
149 changes: 38 additions & 111 deletions app/modules/search/index.tsx
Original file line number Diff line number Diff line change
@@ -1,144 +1,71 @@
import { Suspense, createContext, lazy, useContext, useState } from "react";
import { useHydrated, useLayoutEffect } from "~/ui/utils";
import { Suspense, lazy } from "react";
import { type loader as rootLoader } from "~/root";
import { useHydrated } from "~/ui/utils";
import { useRouteLoaderData } from "@remix-run/react";

import "@orama/searchbox/dist/index.css";
import "@docsearch/css/dist/style.css";
import "~/styles/orama-search.css";
import { useColorScheme } from "~/modules/color-scheme/components";
import "~/styles/search.css";

const OriginalSearchBox = lazy(() =>
import("@orama/searchbox").then((module) => ({
default: module.SearchBox,
const DocSearchButton = lazy(() =>
import("./docsearch").then((module) => ({
default: module.DocSearch,
}))
);

const SearchModalContext = createContext<null | ((show: boolean) => void)>(
null
const OramaSearchButton = lazy(() =>
import("./orama").then((module) => ({
default: module.SearchButton,
}))
);
const OramaSearch = lazy(() =>
import("./orama").then((module) => ({
default: module.SearchModalProvider,
}))
);

export function SearchModalProvider({
children,
}: {
children: React.ReactNode;
}) {
let [showSearchModal, setShowSearchModal] = useState(false);
const isHydrated = useHydrated();
const colorScheme = useSearchModalColorScheme();
let bucket = useBucket();

return (
<SearchModalContext.Provider value={setShowSearchModal}>
if (bucket === "orama") {
return (
<Suspense fallback={null}>
{isHydrated ? (
<OriginalSearchBox
cloudConfig={{
// The search endpoint for the Orama index
url: "https://cloud.orama.run/v1/indexes/react-router-dev-nwm58f",
// The public API key for performing search. This is commit-safe.
key: "23DOEM1uyLIqnumsPZICJzw2Xn7GSFkj",
}}
show={showSearchModal}
onClose={() => setShowSearchModal(false)}
colorScheme={colorScheme}
theme="secondary"
themeConfig={{
light: {
"--background-color-fourth": "#f7f7f7",
},
dark: {
"--background-color-fourth": "#383838",
},
}}
resultsMap={{
description: "content",
}}
facetProperty="section"
backdrop
/>
) : null}
<OramaSearch>{children}</OramaSearch>
</Suspense>
{children}
</SearchModalContext.Provider>
);
}

function useSetShowSearchModal() {
let context = useContext(SearchModalContext);
if (!context) {
throw new Error("useSearchModal must be used within a SearchModalProvider");
);
}
return context;

return <>{children}</>;
}

export function SearchButton() {
let hydrated = useHydrated();
let setShowSearchModal = useSetShowSearchModal();
let bucket = useBucket();

if (!hydrated) {
// The Algolia doc search container is hard-coded at 40px. It doesn't
// render anything on the server, so we get a mis-match after hydration.
// This placeholder prevents layout shift when the search appears.
return <div className="h-10" />;
}

return (
<>
<Suspense fallback={<div className="h-10" />}>
<div className="animate-[fadeIn_100ms_ease-in_1]">
<button
onClick={() => setShowSearchModal(true)}
type="button"
className="DocSearch DocSearch-Button"
aria-label="Search"
>
<span className="DocSearch-Button-Container">
<svg
width="20"
height="20"
className="DocSearch-Search-Icon"
viewBox="0 0 20 20"
>
<path
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
stroke="currentColor"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
<span className="DocSearch-Button-Placeholder">Search</span>
</span>
<span className="DocSearch-Button-Keys">
<kbd className="DocSearch-Button-Key"></kbd>
<kbd className="DocSearch-Button-Key">K</kbd>
</span>
</button>
{bucket === "orama" ? <OramaSearchButton /> : <DocSearchButton />}
</div>
</>
</Suspense>
);
}

// TODO: integrate this with ColorSchemeScript so we're not setting multiple listeners on the same media query
function useSearchModalColorScheme() {
let colorScheme = useColorScheme();
let [systemColorScheme, setSystemColorScheme] = useState<
null | "light" | "dark"
>(null);
useLayoutEffect(() => {
if (colorScheme !== "system") {
setSystemColorScheme(null);
return;
}
let media = window.matchMedia("(prefers-color-scheme: dark)");
let handleMedia = () =>
setSystemColorScheme(media.matches ? "dark" : "light");
handleMedia();
media.addEventListener("change", handleMedia);
return () => {
media.removeEventListener("change", handleMedia);
};
}, [colorScheme]);
if (colorScheme !== "system") {
return colorScheme;
}
if (systemColorScheme) {
return systemColorScheme;
function useBucket() {
const data = useRouteLoaderData<typeof rootLoader>("root");

if (!data) {
throw new Error("useBucket must be used within root route loader");
}
return "dark";

return data.bucket;
}
Loading

0 comments on commit edcc822

Please sign in to comment.