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

@se-studio/ab-testing

v1.0.0

Published

Server-side A/B testing framework for Next.js applications with Contentful CMS

Readme

@se-studio/ab-testing

Server-side A/B testing framework for Next.js applications with Contentful CMS.

Features

  • Server-side rendering: No flicker or layout shift - variants are rendered on the server
  • Edge-optimized: Middleware runs at the edge for minimal latency
  • CMS-driven: Test configuration managed entirely in Contentful
  • Provider-agnostic: Bring your own blob storage (Vercel KV, Netlify Blobs, etc.)
  • Flexible analytics: Use the useAbTestAssignments hook to integrate with any analytics platform

Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Contentful    │────▶│  Webhook Handler │────▶│   Blob Store    │
│   PageTest      │     │  (API Route)     │     │  (Project-      │
│   Entries       │     │                  │     │   specific)     │
└─────────────────┘     └──────────────────┘     └────────┬────────┘
                                                          │
                                                          ▼
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   User Request  │────▶│   Middleware     │────▶│   Variant Page  │
│   /pricing      │     │   (Edge)         │     │   (SSR)         │
└─────────────────┘     └──────────────────┘     └─────────────────┘
                                │
                                ▼
                        ┌──────────────────┐
                        │   Cookie Set     │
                        │   ab-test-info   │
                        └────────┬─────────┘
                                 │
                                 ▼
                        ┌──────────────────┐
                        │ useAbTestAssign- │
                        │ ments Hook       │
                        │ → Your Analytics │
                        └──────────────────┘

Installation

pnpm add @se-studio/ab-testing

Quick Start

1. Implement Your Blob Store

The package requires you to implement the IBlobStore<AbTest> interface for your hosting platform.

Vercel KV Example:

// src/server/abTestStore.ts
import { kv } from '@vercel/kv';
import type { IBlobStore, AbTest } from '@se-studio/ab-testing';

const STORE_KEY = 'ab-tests';

export function getAbTestStore(): IBlobStore<AbTest> {
  return {
    async get(key) {
      const data = await kv.hget<AbTest>(STORE_KEY, key);
      return data ?? undefined;
    },
    async set(key, value) {
      await kv.hset(STORE_KEY, { [key]: value });
    },
    async bulkWrite(entries) {
      await kv.del(STORE_KEY);
      if (entries.length > 0) {
        const data = Object.fromEntries(entries);
        await kv.hset(STORE_KEY, data);
      }
    },
    async size() {
      return kv.hlen(STORE_KEY);
    },
    async values() {
      const data = await kv.hgetall<Record<string, AbTest>>(STORE_KEY);
      return data ? Object.values(data) : [];
    },
  };
}

Netlify Blobs Example:

// src/server/abTestStore.ts
import { getStore } from '@netlify/blobs';
import type { IBlobStore, AbTest } from '@se-studio/ab-testing';

export function getAbTestStore(): IBlobStore<AbTest> {
  const store = getStore('ab-testing');
  const BLOB_NAME = 'config';

  return {
    async get(key) {
      const data = await store.get(BLOB_NAME, { type: 'json' });
      return data?.[key];
    },
    async set(key, value) {
      const data = (await store.get(BLOB_NAME, { type: 'json' })) ?? {};
      data[key] = value;
      await store.setJSON(BLOB_NAME, data);
    },
    async bulkWrite(entries) {
      const data = Object.fromEntries(entries);
      await store.setJSON(BLOB_NAME, data);
    },
    async size() {
      const data = await store.get(BLOB_NAME, { type: 'json' });
      return data ? Object.keys(data).length : 0;
    },
    async values() {
      const data = await store.get(BLOB_NAME, { type: 'json' });
      return data ? Object.values(data) : [];
    },
  };
}

2. Set Up the Middleware

// src/middleware.ts
import { createAbTestMiddleware } from '@se-studio/ab-testing/middleware';
import { getAbTestStore } from './server/abTestStore';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const abTestHandler = createAbTestMiddleware({
  getStore: getAbTestStore,
  cacheTtlMs: 60000, // 60 second cache
});

export async function middleware(request: NextRequest) {
  // Skip static assets, API routes, etc.
  if (request.nextUrl.pathname.startsWith('/_next') ||
      request.nextUrl.pathname.startsWith('/api')) {
    return NextResponse.next();
  }

  // Process A/B tests
  const abResponse = await abTestHandler(request);
  if (abResponse) return abResponse;

  return NextResponse.next();
}

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

3. Create the Webhook Handler

// src/app/api/webhooks/ab-test/route.ts
import { createWebhookHandler } from '@se-studio/ab-testing/webhook';
import { getAbTestStore } from '@/server/abTestStore';
import { revalidateTag } from 'next/cache';

// Your Contentful fetch function
async function fetchPageTests() {
  // Fetch all PageTest entries from Contentful
  // Return array of RawPageTest objects
}

export const dynamic = 'force-dynamic';

export const POST = createWebhookHandler({
  fetchPageTests,
  getStore: getAbTestStore,
  webhookSecret: process.env.CONTENTFUL_WEBHOOK_SECRET,
  revalidate: () => revalidateTag('PageTests'),
});

4. Create Your Analytics Reporter (Project-Specific)

The package provides a useAbTestAssignments hook that returns test assignments for the current page. You create your own reporter component with your project's specific analytics integrations.

// src/components/AbTestReporter.tsx (project-specific)
'use client';

import { useEffect } from 'react';
import { useAbTestAssignments } from '@se-studio/ab-testing';
import { sendEvent } from '@/lib/analytics';

export function AbTestReporter() {
  const assignments = useAbTestAssignments();

  useEffect(() => {
    for (const assignment of assignments) {
      // Send to GTM/GA4
      sendEvent('experiment_impression', {
        experiment_id: assignment.testId,
        experiment_name: assignment.test_label,
        variant_id: assignment.test_path,
      });
    }
  }, [assignments]);

  return null;
}

With HubSpot Integration (HSD-style):

// src/components/AbTestReporter.tsx
'use client';

import { useEffect } from 'react';
import { useAbTestAssignments } from '@se-studio/ab-testing';
import { sendEvent } from '@/lib/analytics';
import { sendHubspotCustomEvent } from '@/lib/hubspotCustomEvents';

export function AbTestReporter() {
  const assignments = useAbTestAssignments();

  useEffect(() => {
    for (const assignment of assignments) {
      // Send to GTM/GA4
      sendEvent('experiment_impression', {
        experiment_id: assignment.testId,
        experiment_name: assignment.test_label,
        variant_id: assignment.test_path,
      });

      // Send to HubSpot (if configured)
      if (assignment.hubspot_event) {
        sendHubspotCustomEvent(assignment.hubspot_event, {
          experiment_name: assignment.test_label,
          experiment_id: assignment.testId,
        });
      }
    }
  }, [assignments]);

  return null;
}

Add to your layout:

// src/app/layout.tsx
import { AbTestReporter } from '@/components/AbTestReporter';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <AbTestReporter />
      </body>
    </html>
  );
}

Contentful Content Model

PageTest

| Field | Type | Description | |-------|------|-------------| | cmsLabel | Symbol | Internal label for the test | | control | Reference (Page/PageVariant) | The control page to test against | | enabled | Boolean | Whether the test is active | | trackingLabel | Symbol (optional) | Override for analytics label | | searchParameters | Symbol (optional) | URL params to match (e.g., "utm_source=google") | | configuration | JSON | Array of {weight, hubspot_event_name?} | | variants | References (PageVariant[]) | Variants to test |

PageVariant

Existing content type that references an original page and defines component swaps.

API Reference

Types

interface AbTest {
  id: string;
  cmsLabel: string;
  controlSlug: string;
  searchParameters?: string;
  trackingLabel?: string;
  enabled: boolean;
  configuration: AbTestVariantConfig[];
  variants: AbTestVariant[];
}

interface IBlobStore<T> {
  get(key: string): Promise<T | undefined>;
  set(key: string, value: T): Promise<void>;
  bulkWrite(entries: [string, T][]): Promise<void>;
  size(): Promise<number>;
  values(): Promise<T[]>;
}

interface ActiveAbTestAssignment {
  testId: string;
  test_label: string;
  test_path: string;
  hubspot_event?: string;
  original_path?: string;
}

Middleware

import { createAbTestMiddleware } from '@se-studio/ab-testing/middleware';

const handler = createAbTestMiddleware({
  getStore: () => store,      // Required: blob store factory
  cacheTtlMs: 60000,          // Optional: cache TTL (default: 60s)
  cookieName: 'ab-test-info', // Optional: cookie name
  cookieMaxAge: 2592000,      // Optional: cookie max age (default: 30 days)
  shouldProcess: (path) => true, // Optional: filter requests
  devTestData: [],            // Optional: test data for development
});

Webhook

import { createWebhookHandler } from '@se-studio/ab-testing/webhook';

export const POST = createWebhookHandler({
  fetchPageTests: () => Promise<RawPageTest[]>, // Required
  getStore: () => store,                         // Required
  webhookSecret: 'secret',                       // Optional
  revalidate: () => void,                        // Optional
  onSkippedTest: (id, reason) => void,          // Optional
});

Hook

import { useAbTestAssignments } from '@se-studio/ab-testing';

// Returns array of assignments for the current page
const assignments = useAbTestAssignments({
  cookieName: 'ab-test-info', // Optional: custom cookie name
});

// Each assignment contains:
// - testId: string
// - test_label: string
// - test_path: string (variant slug or "control")
// - hubspot_event?: string
// - original_path?: string

Analytics Integration

Google Tag Manager (GTM)

Push events to window.dataLayer:

window.dataLayer.push({
  event: 'experiment_impression',
  experiment_id: assignment.testId,
  experiment_name: assignment.test_label,
  variant_id: assignment.test_path,
});

Google Analytics 4 (GA4)

Register custom dimensions in GA4:

  1. Go to Admin > Data display > Custom definitions
  2. Create event-scoped dimensions for:
    • experiment_id
    • experiment_name
    • variant_id

License

MIT