npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@matb85/abcd

v0.6.1

Published

File-convention A/B testing SDK for Expo and React Native

Readme

@matb85/abcd

Detour Experiments: A/B testing and feature flags for Expo and React Native, declared in TypeScript.

An experiment or a flag is a declaration in the package that owns the code. The app lists every declaration it uses in a registry, and hands the registry to <DetourExperimentsProvider>. Feature flags and every kind of experiment work from there. A component experiment swaps a whole component per user through one committed file: while it runs, Header.tsx is the experiment file, so import Header from "./Header" keeps working and renders the assigned variant.

Install

npx @matb85/abcd init

The package ships the detour-experiments command. init installs the package with your package manager if it is missing, writes detour-experiments.config.json and an empty registry (src/experiments.ts, or experiments.ts in an app with no src folder), stores the App's CLI Key, and adds the MCP server to your editors.

Then wrap the app root:

import { DetourExperimentsProvider } from "@matb85/abcd";
import registry from "./experiments";

const detourConfig = {
  apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY,
  appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID,
};

export default function Root() {
  return (
    <DetourExperimentsProvider registry={registry} detourConfig={detourConfig}>
      <App />
    </DetourExperimentsProvider>
  );
}

Users are bucketed by the device id Detour keeps, so there is no user id to pass. That id, the event queue and the remote config live in AsyncStorage, the storage Detour uses, so a device keeps its bucket across restarts. Install @react-native-async-storage/async-storage if the app does not have it. For another storage, set detourConfig.storage as in Detour's custom storage.

Every variant and flag is decided once per launch, so a user never watches one change. At startup the SDK loads what it decides from: the device id and the remote config. The provider renders at once. Only a component that reads an experiment or a flag while the SDK is still loading waits, by suspending until it has loaded: a few tens of milliseconds, and on the first launch after install up to 400 ms for the first config (firstLaunchTimeoutMs). What you change on the dashboard reaches a device at a later cold start, never while the app is running; refreshTimeoutMs makes a launch wait for the fresh config, so a change lands at the very next one.

An app with an experiment or a flag on a screen it can open on covers that wait with its splash screen. useExperimentsReady() is false while the SDK is loading and true once it has loaded, so the app hides its splash screen when it turns true:

import * as SplashScreen from "expo-splash-screen";
import { Stack } from "expo-router";
import { useEffect } from "react";
import { useExperimentsReady } from "@matb85/abcd";

SplashScreen.preventAutoHideAsync();

function RootNavigator() {
  const ready = useExperimentsReady();

  useEffect(() => {
    if (ready) SplashScreen.hide();
  }, [ready]);

  if (!ready) return null;
  return <Stack />;
}

Render RootNavigator inside <DetourExperimentsProvider>. Call SplashScreen.preventAutoHideAsync() at module scope, next to the imports: by the first render the splash screen can already be hiding. The hook depends on no splash library, so an app with another one, such as react-native-bootsplash, hides it the same way. On the web, in a server render and under a test runner the hook is always true, because no hook waits there.

detourConfig carries the App ID and the Publishable Key. In an Expo app they come from EXPO_PUBLIC_DETOUR_APP_ID and EXPO_PUBLIC_DETOUR_API_KEY, the variables the Detour SDK uses, so an app already set up for Detour has no variable to add. Read them in the app's own code, as above: Expo inlines EXPO_PUBLIC_* only there, not inside a package, so the provider cannot read them itself. A bare React Native app fills detourConfig from wherever it keeps its configuration.

With the Detour SDK

detourConfig is the object DetourProvider takes as config. An app that uses both builds it once and passes it to both providers, so they share the App and the device id:

import { DetourProvider, type Config } from "@swmansion/react-native-detour";
import { DetourExperimentsProvider } from "@matb85/abcd";
import registry from "./experiments";

const detourConfig: Config = {
  apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!,
  appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!,
  shouldUseClipboard: true,
};

export default function Root() {
  return (
    <DetourProvider config={detourConfig}>
      <DetourExperimentsProvider registry={registry} detourConfig={detourConfig}>
        <App />
      </DetourExperimentsProvider>
    </DetourProvider>
  );
}

Detour Experiments reads appID, apiKey and storage from it, and ignores Detour's other fields.

A custom storage goes in the same object, and both providers read it from there:

const mmkv = createMMKV();
const storage = {
  getItem: (key: string) => mmkv.getString(key) ?? null,
  setItem: (key: string, value: string) => mmkv.set(key, value),
};

const detourConfig: Config = {
  apiKey: process.env.EXPO_PUBLIC_DETOUR_API_KEY!,
  appID: process.env.EXPO_PUBLIC_DETOUR_APP_ID!,
  storage,
};

export default function Root() {
  return (
    <DetourProvider config={detourConfig}>
      <DetourExperimentsProvider registry={registry} detourConfig={detourConfig}>
        <App />
      </DetourExperimentsProvider>
    </DetourProvider>
  );
}

Feature flags

detour-experiments new flag DarkMode
import { useFeatureFlag } from "@matb85/abcd";
import { DarkMode } from "./DarkMode";

const { enabled } = useFeatureFlag(DarkMode);

No further setup. A new flag starts off, at a 0% rollout. Turn it on, or roll it out to a percentage of users, from the dashboard or with detour-experiments update DarkMode --rollout 30: the rollout is the share of users who get the flag on. A device picks the change up at a later cold start, so a flag is not an instant kill switch.

Experiments

A code-level experiment needs no setup either:

// src/CheckoutCopy.ts
import { defineExperiment } from "@matb85/abcd";

export const CheckoutCopy = defineExperiment({
  key: "CheckoutCopy",
  metrics: ["purchase"],
});
const { variant, logOutcome } = useExperiment(CheckoutCopy);
logOutcome({ metric: "purchase" });

A component experiment needs none either:

detour-experiments new experiment PromoBanner --component src/components/Header.tsx --metric cta_click

This moves the code of Header.tsx into Header.A.tsx and Header.B.tsx, rewrites Header.tsx as the experiment file (the declaration, and the component that renders the assigned variant), adds it to the registry and creates the dashboard record. The experiment file is committed and yours to edit; nothing is generated at build time.

conclude writes the winner back over Header.tsx. After that, plain git blame points at the conclude commit; git blame -C follows the lines back to their authors.

Upgrading from Header.experiment.tsx

Earlier versions kept a component experiment in Header.experiment.tsx, a standalone declaration in <Key>.experiment.ts or <Key>.flag.ts, and needed three setup lines for imports to resolve. There is no migration command, and the old names are no longer recognised:

  1. Rename each X.experiment.tsx to X.tsx, and each X.experiment.ts or X.flag.ts to X.ts. Update the imports that name the old files, in the registry and in the variants.
  2. Delete the three setup lines: .experiment from moduleSuffixes in the tsconfig, experiment.tsx / experiment.jsx from Metro's sourceExts, and from Jest's moduleFileExtensions.
  3. Run detour-experiments doctor.

Lifecycle

detour-experiments new experiment PromoBanner --component src/components/Header.tsx --metric cta_click
detour-experiments doctor PromoBanner              # also the command to run in CI
detour-experiments update PromoBanner --rollout 50 # after a build with the experiment has shipped
detour-experiments stats PromoBanner
detour-experiments conclude PromoBanner --winner B

A new experiment starts at a 0% rollout: no user is enrolled, so everyone sees A until you raise it. The rollout is the share of users in the experiment. The enrolled are split evenly across all variants, A included, unless you set weights (update --weights A=40,B=30,C=30; --weights even returns to the even split). The rollout and the weights are two settings: raising the rollout in stages moves nobody who is already enrolled, while changing the weights moves users between variants. A device picks a new rollout up at a later cold start.

Features

  • Flags and code-level experiments out of the box: install, add the provider, declare.
  • Zero-refactor imports: for a component experiment, ./Header still resolves to Header.tsx, which is now the experiment file. No resolver configuration.
  • Typed from declarations: variants, metrics, payloads and provider overrides are inferred; nothing is generated.
  • Deterministic bucketing: MurmurHash3 keeps each device on the same variant.
  • No flicker: variants and flags are decided once per launch and never change under a running app; a screen that reads one while the SDK is loading waits until it has loaded instead of rendering twice.
  • Screen capture (Expo Router): detour-experiments capture photographs each variant's screens and publishes them to the dashboard.
  • MCP: detour-experiments mcp --config <path> gives an editor agent the CLI's operations on experiments and flags as tools; init, login and capture-consent stay in the terminal.