Skip to content

cuped-io/flame

Repository files navigation

flame

@cuped-io/flame on npm @cuped-io/flame-react on npm @cuped-io/flame-edge on npm License: MIT

Client-side SDKs for cuped.io — A/B testing with CUPED variance reduction.

This repo is a pnpm workspace with three packages:

Package npm Source Role
@cuped-io/flame npmjs.com ./packages/flame Core SDK. IIFE for <script> tag use; ESM/CJS for npm consumers.
@cuped-io/flame-react npmjs.com ./packages/flame-react React bindings: <CupedProvider>, useExperiment, <Experiment>, useTrack.
@cuped-io/flame-edge npmjs.com ./packages/flame-edge Edge resolver + signed cookie utilities for zero-flash SSR. Web Crypto only.

What it does

  1. Fetches active experiments for a project
  2. Assigns visitors to variants
  3. Applies variant changes to the DOM (8 change types: text, html, attribute, class, style, css, visibility, redirect)
  4. Auto-detects e-commerce events (add to cart, checkout, etc.)
  5. Tracks events with experiment assignments for server-side goal matching

Quick start

You'll need a DSN. Sign up at cuped.io, create a project, and copy the DSN from Settings → Install snippet (it looks like https://YOUR_KEY@api.cuped.io).

Option 1: Script tag (no-code DOM mutations)

Drop this in your <head> and define variants in the dashboard. Pin to a version and guard it with Subresource Integrity so a bad deploy can't reach your site (crossorigin is required for SRI to work on the cross-origin script):

<script
  src="https://cdn.cuped.io/flame@0.5.0.js"
  integrity="sha384-EyZs8bbbc37P0vhn5CdDiwF5Ph/dgG7+92yeULGePA4XIUw52Wx2qWQvs+hsdrLk"
  crossorigin="anonymous"
  data-dsn="https://YOUR_KEY@api.cuped.io"
></script>

The SRI hash for every published version is listed at cdn.cuped.io/flame.sri.json (a versions map plus a latest pointer). Pinned URLs are write-once and never removed, so your pin keeps resolving byte-for-byte across later releases.

Prefer auto-updates over pinning? https://cdn.cuped.io/flame.js is a floating "latest" that always serves the newest release. It can't be pinned with SRI (the bytes change on every release), so you trade integrity verification and rollback safety for automatic upgrades.

Option 2: React (CSR)

pnpm add @cuped-io/flame @cuped-io/flame-react
import { CupedProvider, useExperiment } from '@cuped-io/flame-react';

function App() {
  return (
    <CupedProvider dsn="https://YOUR_KEY@api.cuped.io">
      <Hero />
    </CupedProvider>
  );
}

function Hero() {
  const { variant } = useExperiment('hero-cta');
  return <button>{variant?.name === 'treatment' ? 'Buy now' : 'Get started'}</button>;
}

Option 3: Next.js App Router with zero-flash SSR

The recommended setup for Next.js. Variants are resolved at the edge before the first byte renders, so server HTML matches the assigned variant from request #1.

pnpm add @cuped-io/flame @cuped-io/flame-react @cuped-io/flame-edge

.env.local:

CUPED_DSN=https://YOUR_KEY@api.cuped.io
NEXT_PUBLIC_CUPED_DSN=https://YOUR_KEY@api.cuped.io
CUPED_COOKIE_SECRET=<generate with: openssl rand -base64 32>

middleware.ts:

import { createCupedMiddleware } from '@cuped-io/flame-edge/next';

export default createCupedMiddleware({
  dsn: process.env.CUPED_DSN!,
  secret: process.env.CUPED_COOKIE_SECRET!,
});

export const config = {
  matcher: ['/((?!_next/static|_next/image|api/|favicon.ico).*)'],
};

app/providers.tsx:

'use client';
import { CupedProvider } from '@cuped-io/flame-react';
import type { PrehydratedState } from '@cuped-io/flame';

export function Providers({
  children,
  prehydrated,
}: {
  children: React.ReactNode;
  prehydrated?: PrehydratedState;
}) {
  return (
    <CupedProvider
      dsn={process.env.NEXT_PUBLIC_CUPED_DSN!}
      prehydrated={prehydrated}
    >
      {children}
    </CupedProvider>
  );
}

app/layout.tsx:

import { cookies } from 'next/headers';
import { readPrehydratedForServerComponent } from '@cuped-io/flame-edge/next';
import { Providers } from './providers';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const prehydrated = await readPrehydratedForServerComponent(
    await cookies(),
    process.env.CUPED_COOKIE_SECRET!,
  );
  return (
    <html>
      <body>
        <Providers prehydrated={prehydrated ?? undefined}>{children}</Providers>
      </body>
    </html>
  );
}

That's it — useExperiment and <Experiment> work the same way as Option 2.

Examples

  • examples/script-tag — Static HTML page exercising all 8 change types.
  • examples/next-app — Next.js App Router app with edge middleware + signed prehydrated cookie. Includes verification steps.

Documentation

Development

pnpm install
pnpm test         # unit tests, all packages
pnpm typecheck
pnpm lint
pnpm build        # IIFE + ESM/CJS across packages

pnpm test:e2e:install   # one-time: fetch the Chromium browser
pnpm test:e2e           # Playwright browser tests against both examples

The Playwright suite (e2e/) drives the real script-tag and Next.js examples in a browser against a mock cuped API — assignment application, zero-flash SSR, and event delivery. See e2e/README.md.

Releases are managed by changesets and published to npm via OIDC trusted publishing with provenance attestations.

License

MIT — see LICENSE.

About

Client-side script for cuped.io

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors