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

@sheepit-ai/sdk-js

v0.9.1

Published

Sheepit browser SDK — events, flags, experiments, identity

Readme

@sheepit-ai/sdk-js

Feature flags, experiments, and event tracking for the browser.

npm version license

Browser SDK for Sheepit. One client tracks events, reads feature flags, reads experiment variants, and captures browser errors. For React, use @sheepit-ai/react on top of it.

API compatibility

Upgrade your API before upgrading the SDK: page-close body authentication requires hosted Sheepit's API deployment or BYOC 0.5.0+. Before deploying this SDK, verify your API reports ingest_body_key: true on /v1/ingest/capabilities using an authenticated request. The SDK does not fetch or negotiate this capability. Older APIs are unsupported for the new page-close transport and return 401, losing the batch. Keepalive HTTP rejections reach transport.keepalive_rejected diagnostics if the document is still alive. Previously published SDKs retain the API-supported legacy query-key path.

For headerless page-close body authentication, forwarders must preserve Content-Length on unencoded JSON POSTs to /v1/ingest. Admission requires a valid length of at most 65,536 UTF-8 bytes and no Content-Encoding; rewriting to chunked transfer or adding compression produces a 401 before parsing. Header-authenticated flushes retain their existing compression and body limits. The pre-auth anonymous IP limit (100/min, versus 1000/min for header-authenticated requests) intentionally protects this parsing window. Project rate limits still apply once after verified authentication. The page-close batch goes out in requests of at most 100 events and about 60 KB each, within the browser's 64 KiB in-flight keepalive quota; see Page close.

Page close no longer depends on CORS credentials: 0.8.0 sends it with keepalive fetch (credentials: "omit"), which the API accepts from any origin. A cookie-carrying sendBeacon is only the fallback, so it can still be lost from an origin outside the API's strict CORS allowlist. See Page close.

Install

npm install @sheepit-ai/sdk-js

Quick start

import { Sheepit } from "@sheepit-ai/sdk-js";

const sheepit = Sheepit.init({
  apiKey: "lp_pub_...", // publishable key
  release: "<commit SHA>", // optional: injected at build time, see "Release attribution"
});

sheepit.track("signup_started", { plan: "pro" });

That event is queued and sent within flushInterval (5 s by default).

Key type

Use a publishable key (lp_pub_…). Sheepit.create() / Sheepit.init() throw on a secret key (lp_sec_…, use @sheepit-ai/server) or a developer key (lp_dev_…, use @sheepit-ai/cli).

Usage

import { Sheepit } from "@sheepit-ai/sdk-js";

declare const user: { id: string; plan: string };

const sheepit = Sheepit.create({ apiKey: "lp_pub_..." });

// Events
sheepit.track("checkout_started", { cart_value: 42 });

// Identity: after login, associate this device and its later events with a user id.
// On web, traits are not used for flag targeting yet.
sheepit.identify(user.id, { plan: user.plan });

// Flags: synchronous; returns the default until configuration has loaded
const newCheckout = sheepit.flag("new_checkout_flow", false);

// Experiments: synchronous; returns { variant: "control" } when there is no assignment
const { variant, payload } = sheepit.experiment("checkout_copy");

// Logout: clears the local identity, cached config and assignments (since 0.6.0),
// and starts a fresh anonymous session
sheepit.reset();

// Send queued events now, e.g. before a hard navigation
await sheepit.flush();

// Teardown: stops timers and listeners; later calls are no-ops
sheepit.destroy();

Sheepit.create(config) returns an independent instance. Sheepit.init(config) creates a singleton; Sheepit.getInstance() and Sheepit.isInitialized() read it.

How flags and experiments resolve

The SDK does no evaluation or bucketing locally. It fetches GET /v1/config, which returns values the API has already evaluated for this device, and caches them in localStorage. flag() and experiment() read that cache synchronously.

  • Configuration is polled every configRefreshInterval (30 s by default). Config requests bypass the browser HTTP cache (cache: "no-store") and revalidate with the SDK's own If-None-Match, so a flag change or kill switch reaches an open page within one poll interval.
  • The first flag() read of each flag emits $flag_exposure. A config refresh that returns the same value does not emit again; one that changes the value does, on the next read (exposure_reason: "value_changed"). The first experiment() read of each experiment emits $experiment_exposure, and it emits again whenever a refresh moves the user to another arm (exposure_reason: "variant_changed"). Both dedupes reset when the user id changes and on reset().
  • Calling identify() with a different user id clears the stored experiment assignments and (since 0.6.0) makes the next config poll fetch fresh values for the new identity.

Server bootstrap (signed-in pages)

Your backend calls POST /v1/config/resolve with a secret key and embeds the response's data in the page as <script type="application/json" id="sheepit-bootstrap"> (escape <, >, &, U+2028 and U+2029, and serve the page Cache-Control: private, no-store). Pass it to create():

import { Sheepit } from "@sheepit-ai/sdk-js";

const el = document.getElementById("sheepit-bootstrap");
const sheepit = Sheepit.create({
  apiKey: "lp_pub_prd_...",
  // No cast needed: create() validates the payload and ignores a malformed one.
  bootstrap: el ? JSON.parse(el.textContent ?? "null") : undefined,
});
sheepit.flag("new-checkout"); // served from the bootstrap: no GET /v1/config has happened
  • create() identifies subject.user_id itself (calling identify() with the same id afterwards is harmless) and serves the payload's flags and experiment arms synchronously.
  • No GET /v1/config goes out until the server has acknowledged that identify and the page has called flag() or experiment(), or until ttl_seconds (at most 300 s) has passed since create(). The read condition is dropped when pending_device_experiments is non-empty, and the identify condition when the identify ends without an acknowledgement. Device registration and the identify still go out at page load.
  • Values and experiment arms then update live from that GET. A moved arm records one $experiment_exposure in the new arm.
  • A stale payload, one for another environment than the key's (compared by the key's three-character slug segment), or a malformed one is ignored: the SDK behaves as if no bootstrap was passed and records $sdk_bootstrap with outcome: "rejected_*".
  • identify() of another user, reset(), or another tab changing the device ends the bootstrap and normal polling resumes.

Reacting to config changes

sheepit.onConfigChange(listener) calls listener whenever the values flag() and experiment() return may have changed: a new /v1/config with different values, a reset(), or a debug override. It returns an unsubscribe function.

import { Sheepit } from "@sheepit-ai/sdk-js";

declare function render(showNewCheckout: boolean): void;

const sheepit = Sheepit.getInstance();
const unsubscribe = sheepit.onConfigChange(() => {
  render(sheepit.flag("new_checkout_flow", false) === true);
});
// later
unsubscribe();

Page views in single-page apps

Call sheepit.recordPageView(path) on every route change. It emits $page_leave for the previous route. (<PageViewTracker> in @sheepit-ai/react does this and also emits $page_view.)

API

| Method | Returns | Notes | | ------------------------------------------------------------------ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | track(event, properties?) | void | Queues an event | | identify(userId, traits?) | void | Associates the device and later events with userId; traits not used for targeting on web yet | | flag(key, default = false) | FlagValue | boolean \| string \| number \| Record<string, unknown> | | experiment(key) | { variant, payload? } | variant is "control" when there is no assignment | | reset() | void | Logout: clears local identity; since 0.6.0 also cached config, evaluated flags, and assignments | | onConfigChange(listener) | () => void | Subscribe to flag/experiment value changes; returns unsubscribe | | flush() | Promise<void> | Sends queued events; makes one real attempt even during a 429 backoff (throttled to ≤1/s) — required before a serverless function exits | | destroy() | void | Stops timers and listeners; fire-and-forget flush | | recordPageView(path) | void | SPA route change | | overrideFlag(key, value) / clearOverrides() / getOverrides() | — | Local overrides; applied only when debug: true | | status() | SDKStatus | Queue depths, online, lastFlushAt, device/user id, counts, SDK version | | diagnostics() / getRecentDiagnostics() | — | Internal SDK events (see onDiagnostic) | | getFirstTouch() (optional) | AttributionContext \| undefined | Copy of the device's first-touch attribution snapshot, for code that needs it outside track() |

Release attribution

Pass release so events and crashes are attributed to the build that produced them. It is sent as context.app.version and must equal the version sheepit ci release registered (default for web and server deploys: the full commit SHA). appVersion is the older name and still works; release wins when both are set. The value is trimmed and otherwise never changed, because the match is an exact string comparison.

Browser bundles cannot read the environment, so inject it at build time:

// Next.js (set NEXT_PUBLIC_SHEEPIT_RELEASE=$CI_COMMIT_SHA in CI)
Sheepit.init({ apiKey: "lp_pub_...", release: process.env.NEXT_PUBLIC_SHEEPIT_RELEASE });

// Vite (set VITE_SHEEPIT_RELEASE=$CI_COMMIT_SHA in CI)
Sheepit.init({ apiKey: "lp_pub_...", release: import.meta.env.VITE_SHEEPIT_RELEASE });

process does not exist in a plain browser or Vite bundle; use the form your bundler replaces.

A value over 64 characters is dropped (no release is sent, never a truncated one) and reported through onDiagnostic (lifecycle.release_too_long); the browser SDK never throws for it. Every batch also carries context.sdk = { name, version }.

Configuration

| Option | Type | Default | | ----------------------------- | -------------------------------------------- | -------------------------- | | apiKey | string (required) | — | | environment | string | "production" | | apiUrl | string | "https://api.sheepit.ai" | | release | string | none | | appVersion | string (older name of release) | none | | flushInterval | number (ms) | 5000 | | flushSize | number (events) | 20 | | maxQueueSize | number (events) | 1000 | | retryAttempts | number | 3 | | configRefreshInterval | number (ms, minimum 5000) | 30000 | | debug | boolean | false | | onEvent | (name, properties?) => void | none | | getSuperProperties | () => Record<string, unknown> \| undefined | none | | onDiagnostic | (event: DiagnosticEvent) => void | none | | diagnosticBufferSize | number | 100 | | autoCaptureErrors | boolean \| ErrorCaptureConfig | true | | autoCaptureHttpErrors | boolean \| HttpErrorCaptureConfig | false | | autoCaptureClickFrustration | boolean \| ClickFrustrationConfig | true | | bootstrap | BootstrapPayload | none |

  • autoCaptureErrors sends window.onerror and unhandledrejection as $error.
  • autoCaptureHttpErrors wraps window.fetch and sends 4xx/5xx responses and network failures as $http_error. Off by default.
  • autoCaptureClickFrustration sends $rage_click and $dead_click.
  • getSuperProperties is re-evaluated on every track() call (never cached) and merged onto event_properties — event-specific properties win on key collision. Covers events fired synchronously during create() ($session_start) and from flag()/experiment(), since those call track() internally too. A throwing callback logs a warning and is otherwise ignored; a returned value that isn't JSON-serializable (a BigInt, a circular reference) is dropped key-by-key with a warning rather than breaking the event.
  • The SDK does not collect Web Vitals or other performance metrics.

First-touch attribution outside track()

The SDK auto-attaches first-touch (UTM + referrer + landing page from the visitor's very first session) to every event's context. sheepit.getFirstTouch() returns a copy of that same snapshot for code that needs to thread it through a request that doesn't go through track() — e.g. attaching it to your own signup/account-creation call:

const attribution = sheepit.getFirstTouch(); // AttributionContext | undefined
await fetch("/api/signup", { method: "POST", body: JSON.stringify({ email, attribution }) });

Delivery

Events are sent when the queue reaches flushSize, on the flushInterval timer, and when you call flush(). When the tab is hidden the SDK starts an ordinary async flush; on pagehide it sends the remaining queue with a cookie-less fetch(..., { keepalive: true }) (this one send ignores everything below — there is no later chance to retry). A batch that still fails after retryAttempts tries is moved to a localStorage queue (up to 500 events, browser only — see "Node / serverless" below), which is resent when the browser reports it is back online, or once at SDK init if it's already online.

A 400 response drops the batch. A 429 opens a backoff window (honoring Retry-After, floored at 1s, with jitter — and the floor never disables escalation: a small or zero Retry-After on a sustained 429 still backs off further each time, it just can't undercut the escalating minimum). The size-trigger, interval timer, and reconnect-drain flushes fully respect the window and send nothing until it clears, but an explicit flush() (or destroy()) still makes one real attempt during the window — since a serverless function may exit before a deferred flush ever gets another chance. This is deliberately paced too: calling flush() in a loop while blocked still gets at most one real attempt per second, by design, so a customer-initiated retry loop can't hot-loop the endpoint the way an internal one used to. The window itself (not the escalation streak) survives a page reload — it's persisted per (apiUrl, environment, publishable key), same scoping as the device-registration backoff, so two projects/environments on one origin never share one project's rate limit.

Page close

The pagehide send uses keepalive fetch with credentials: "omit", which the API's SDK CORS policy accepts from any origin. navigator.sendBeacon is the fallback only where keepalive is missing (Firefox before 133) or where a large batch has spent the browser's 64 KiB keepalive quota. A beacon is always sent with cookies, so from an origin outside the API's strict CORS allowlist the browser drops it while sendBeacon still reports success. The batch goes out in requests of at most 100 events and about 60 KB; one event over 64 KiB is dropped, since no browser sends it.

Each request emits a transport.page_close diagnostic whose data.outcome names what happened: keepalive_sent, keepalive_threw (falls back to sendBeacon), keepalive_failed, beacon_queued, beacon_refused, over_quota_beacon_queued, over_quota_beacon_refused, no_transport, event_too_large or serialize_failed. keepalive_failed and transport.keepalive_rejected arrive only while the document is still alive. A failed keepalive is not resent by beacon: a request can be rejected after its POST was delivered, and page-close events carry no id the server could deduplicate on. The SDK counts its own keepalive bytes still in flight, so a second pagehide (after a back-forward-cache restore) shares the same 64 KiB rather than overrunning it. The SDK's own $page_leave event carries page_close_transport (keepalive or beacon) when it leaves in the page-close batch.

Node / serverless

There is no localStorage in Node, so the offline queue above is a no-op there — a batch that can't be delivered (network error, 5xx, or a 429 that flush() hit mid-backoff) is logged and dropped rather than silently claimed as "persisted." Always await flush() before your handler returns.

Runtime support

ES2020 build, ESM and CommonJS. Browser APIs are guarded, so importing the package during server-side rendering is safe. Node.js 20+ for tooling (engines).

Type-safe flag keys

@sheepit-ai/cli generates a Flags constant from your project's flags:

npx @sheepit-ai/cli init      # writes sheepit.config.json
export SHEEPIT_API_KEY=lp_dev_... SHEEPIT_API_URL=https://api.sheepit.ai   # both required
npx @sheepit-ai/cli codegen   # writes src/generated/flags.ts by default
import { Sheepit } from "@sheepit-ai/sdk-js";
import { Flags } from "./generated/flags";

const sheepit = Sheepit.getInstance();
const newCheckout = sheepit.flag(Flags.new_checkout_flow, false);

Other exports

  • @sheepit-ai/sdk-js/boot-error: getBootErrorScript({ endpoint, stackLimit? }) returns an inline script for your <head> that catches errors thrown before the SDK loads and POSTs them as JSON (BootErrorPayload) to your own endpoint (a same-origin route such as "/api/log-boot-error"). It does not send anything to Sheepit; your route decides where the payload goes.

    import { getBootErrorScript } from "@sheepit-ai/sdk-js/boot-error";
    
    const inlineScript: string = getBootErrorScript({ endpoint: "/api/log-boot-error" });
  • @sheepit-ai/sdk-js/flags (FlagManager) and @sheepit-ai/sdk-js/experiments (ExperimentManager): the internal stores, exported separately.

  • GoaTech, IGoaTech, GoaTechConfig: deprecated aliases of Sheepit, ISheepit, SheepitConfig. Use the Sheepit* names.

Links

License

MIT. Copyright (c) 2026 GoaTech AI LLC. See LICENSE.