All files / app/features/experimentation FeatureFlagProvider.tsx

0% Statements 0/31
0% Branches 0/12
0% Functions 0/10
0% Lines 0/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73                                                                                                                                                 
import { configureCache, GrowthBook } from "@growthbook/growthbook";
import { GrowthBookProvider } from "@growthbook/growthbook-react";
import { useAuthContext } from "features/auth/AuthProvider";
import Sentry from "platform/sentry";
import { ReactNode, useEffect } from "react";
import { reportExposure } from "service/experiments";
 
const INIT_TIMEOUT_MS = 1500;
const REFRESH_INTERVAL_MS = 60 * 1000;
// Within this window, init serves the cached payload without a network fetch.
const CACHE_STALE_TTL_MS = 5 * 60 * 1000;
 
configureCache({ staleTTL: CACHE_STALE_TTL_MS });
 
const growthbook = new GrowthBook({
  apiHost: process.env.NEXT_PUBLIC_GROWTHBOOK_API_HOST,
  clientKey: process.env.NEXT_PUBLIC_GROWTHBOOK_CLIENT_KEY,
  enableDevMode: process.env.NEXT_PUBLIC_COUCHERS_ENV !== "prod",
  trackingCallback: (experiment, result) => {
    return reportExposure({
      experimentKey: experiment.key,
      experimentName: experiment.name ?? "",
      variationId: result.variationId,
      variationKey: result.key,
      variationName: result.name ?? "",
      hashAttribute: result.hashAttribute,
      hashValue: result.hashValue,
      featureId: result.featureId ?? "",
      inExperiment: result.inExperiment,
      bucket: result.bucket,
      hashUsed: result.hashUsed,
      stickyBucketUsed: result.stickyBucketUsed,
    }).catch((e) => {
      Sentry.captureException(e);
    });
  },
});
 
export default function FeatureFlagProvider({ children }: { children: ReactNode }) {
  const { authState } = useAuthContext();
 
  useEffect(() => {
    // Dev-only: when set, flags resolve from feature-flags.dev.json instead of GrowthBook,
    // mirroring the backend's FEATURE_FLAGS_FILE_OVERRIDE_PATH. Overridden flags return the file
    // value; unknown flags fall through to their in-code default. GrowthBook is never contacted in
    // this mode. The inline NODE_ENV check lets webpack compile the whole branch out of production
    // builds, keeping the file out of production bundles.
    if (process.env.NODE_ENV === "development" && process.env.NEXT_PUBLIC_FEATURE_FLAGS_OVERRIDE === "1") {
      void import("feature-flags.dev.json").then((mod) => {
        const overrides = mod.default as Record<string, unknown>;
        growthbook.initSync({
          payload: {
            features: Object.fromEntries(
              Object.entries(overrides).map(([key, value]) => [key, { defaultValue: value }]),
            ),
          },
        });
      });
      return;
    }
    void growthbook.init({ timeout: INIT_TIMEOUT_MS });
    const id = setInterval(() => void growthbook.refreshFeatures(), REFRESH_INTERVAL_MS);
    return () => clearInterval(id);
  }, []);
 
  useEffect(() => {
    const userId = authState.userId;
    growthbook.setAttributes(userId != null ? { id: userId.toString() } : {});
  }, [authState.userId]);
 
  return <GrowthBookProvider growthbook={growthbook}>{children}</GrowthBookProvider>;
}