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

@apdl-oss/sdk

v0.3.4

Published

Client SDK for the Autonomous Product Development Loop platform

Readme

@apdl-oss/sdk

Browser TypeScript SDK for the Autonomous Product Development Loop platform. The SDK sends product analytics events to the ingestion service, evaluates feature flag variants client-side, receives real-time configuration updates from the config service over SSE, provides a local UI renderer, and exposes experiment context for flag targeting. It uses the same FNV-1a bucketing as the Python SDK and the config service, so a user buckets identically no matter where a flag is evaluated.

  • 🪄 Auto-capture: page views, clicks, form submissions, scroll depth, rage clicks, frontend errors, web vitals
  • 🚩 Local feature flag variant evaluation (no network round-trip on the hot path)
  • 🔁 Real-time flag updates over SSE, with a persisted local flag cache
  • 🧩 Local UI component renderer (backend UI-config delivery is not in 0.3.0)
  • 🔒 Privacy controls: consent management, PII scrubbing, cookieless mode
  • ⚛️ First-party React/Next adapter (@apdl-oss/sdk/react) — a provider + hook, no wrapper boilerplate
  • 🧯 Zero-config setup: env conventions, SSR-safe init, idempotent singleton, fail-soft validation
  • 📦 Ships ESM, CJS, and an IIFE browser bundle, with full TypeScript types

Installation

npm install @apdl-oss/sdk

Or drop the IIFE bundle into any page (exposes a global APDL):

<script src="https://unpkg.com/@apdl-oss/sdk/dist/apdl.iife.js"></script>

Initialization

import { APDL } from '@apdl-oss/sdk';

const apdl = APDL.init({
  endpoint: 'https://api.example.com',
  auth: {
    clientKey: 'client_demo_0123456789abcdef',
  },
  autoCapture: true,
  privacyMode: 'standard',
});

APDL.init(config) (also exported as the bare init(config)) is the primary public entrypoint. It is:

  • SSR-safe — on the server (no window) it returns an inert no-op client and opens no sockets, timers, or fetches, so it is safe to call at module scope in frameworks like Next.js.
  • An idempotent singleton — repeated calls with the same clientKey return the same client, so it is immune to React StrictMode double-invoke and HMR re-runs (no duplicate listeners, SSE connections, or flush loops). The instance is evicted on shutdown(), so a later init() starts fresh.
  • Fail-soft — when endpoint/clientKey are absent it warns once and returns a no-op client instead of throwing, so an unset env var does not crash every route. Malformed values (bad key format, removed fields) still throw.

Zero-config setup (env conventions)

If endpoint / auth.clientKey are omitted, they are read from environment variables, so init() can be called with no arguments:

| Field | Browser (bundler-inlined) | Server | |---|---|---| | endpoint | NEXT_PUBLIC_APDL_URL | APDL_URL | | clientKey | NEXT_PUBLIC_APDL_CLIENT_KEY | APDL_CLIENT_KEY |

For Next.js, add the browser-safe values to .env.local and restart the development server:

NEXT_PUBLIC_APDL_URL=https://api.example.com
NEXT_PUBLIC_APDL_CLIENT_KEY=client_demo_0123456789abcdef

The SDK uses direct, statically analyzable references to these public variables, so Next.js includes them in the browser bundle. Never put a secret server key in a NEXT_PUBLIC_* variable.

For module-scope use without any useEffect, import the lazy apdl singleton. It no-ops on the server and auto-starts on the first browser tick, reading config from the env conventions above:

import { apdl } from '@apdl-oss/sdk'; // no 'use client', no useEffect

apdl.track('cta_clicked', { id: 'hero' });
const variant = apdl.getVariant('new-checkout-flow');

React & Next.js

Install the package and drop the provider in once — it owns the 'use client' boundary, the singleton lifecycle, and SSR safety internally:

// app/layout.tsx — the entire integration
import { APDLProvider } from '@apdl-oss/sdk/react';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <APDLProvider autoCapture>{children}</APDLProvider>;
}

With NEXT_PUBLIC_APDL_URL / NEXT_PUBLIC_APDL_CLIENT_KEY set, the example above is a complete setup. You can also pass props explicitly (<APDLProvider endpoint={...} clientKey={...} autoCapture>).

Read the client anywhere with the useAPDL hook — no instance threading:

import { useAPDL } from '@apdl-oss/sdk/react';

function HeroCTA() {
  const apdl = useAPDL();
  const variant = apdl.getVariant('new-checkout-flow');
  return <button onClick={() => apdl.track('cta_clicked', { id: 'hero' })}>Buy</button>;
}

react (>= 18) is an optional peer dependency, required only when importing @apdl-oss/sdk/react. Outside a provider, useAPDL() returns an inert no-op client, so calls never throw.

Config Fields

The SDK uses one initialization contract:

| Field | Required | Description | |---|---:|---| | endpoint | Yes¹ | Absolute HTTP(S) origin of the APDL gateway, with no credentials, path, query, or fragment. The SDK posts events to /v1/events and reads flags + SSE from /v1/flags and /v1/stream on this one origin. | | auth.clientKey | Yes¹ | Browser-safe APDL client key used for service authentication and project identification. |

¹ Resolved from the env conventions above when omitted. If still absent, init() returns a no-op client (fail-soft); new APDLClient(config) and resolveConfig(config, { strict: true }) throw.

auth.clientKey must use the canonical APDL client key format:

client_{project_id}_{token}

The token must be 16+ alphanumeric characters. The SDK derives the project ID from the client key internally. Do not pass projectId, apiKey, host, configHost, or the old endpoints object; those fields are not part of the public config contract and the SDK rejects them.

Optional fields include:

| Field | Description | |---|---| | autoCapture | true, false, or a per-signal capture config. | | batchSize | Integer events per batch, from 1 through 100. | | flushInterval | Integer queue flush interval from 100 through 3,600,000 milliseconds. | | privacyMode | 'standard' or 'cookieless'. | | consent | Initial consent state for analytics, personalization, and experiments. | | persistence | 'localStorage' for project-scoped browser storage or 'memory' for no browser storage. | | maxQueueSize | Integer maximum from 1 through 100,000 events owned in memory. A new event is rejected synchronously when full; an already accepted event is never evicted to make room. | | debug | Enables SDK diagnostics when true. |

Configuration is validated at runtime for JavaScript and parsed-JSON callers. Unknown fields, malformed types, non-finite or fractional numeric values, out-of-range values, and unsupported enum members fail during initialization. The former persistence: 'cookie' and privacyMode: 'strict' values are not implemented and are rejected instead of being mapped to different behavior.

Automatic events use fixed property allowlists enforced before and after custom scrubbers. Clicks contain only bounded tag and coordinate metadata; form submits contain only the HTTP method; input changes contain only tag, type, and a value presence boolean. DOM text, form values/actions/names/IDs, CSS metadata, page titles, query strings, fragments, and full referrers are not collected. Browser context contains a query-free HTTP(S) URL and path, while click and rage-click context omits page location entirely. Known credential, one-time-code, file, and payment controls identified from native types and semantic hints are excluded from click capture. Use manual events when an application needs an explicitly chosen semantic label.

Local Development Endpoints

When running the local APDL services, initialize the SDK with the local gateway URL (make dev-core starts the gateway on port 8000):

const apdl = APDL.init({
  endpoint: 'http://localhost:8000',
  auth: {
    clientKey: 'client_demo_0123456789abcdef',
  },
  autoCapture: true,
  privacyMode: 'standard',
});

Start the local services from the repository root:

make run-ingestion
make run-config

Event Tracking

apdl.track('purchase_completed', {
  product_id: 'sku-123',
  revenue: 49.99,
});

apdl.page('Pricing', {
  path: '/pricing',
});

Events are batched and sent to the gateway endpoint at /v1/events. Properties, traits, and custom context must be canonical JSON: finite numbers, strings, booleans, nulls, arrays, and plain string-keyed objects. Cycles, BigInt, accessors, sparse arrays, unsupported values, malformed timestamps, unknown context fields, excessive nesting/cardinality, and events over 64 KiB are rejected synchronously before queue ownership. Requests are split below 512 KiB. Event timestamps may be at most seven days old and at most five minutes ahead of the browser clock, matching the ingestion and offline-storage window; the SDK rejects out-of-window time instead of rewriting it. Network errors, HTTP 408/425/429, and 5xx retain the same stable message IDs for retry; other non-2xx responses are permanent and cannot poison later queue entries. If both a retryable send and offline persistence fail, the batch is requeued once in memory and returned in the drain's pending report; the SDK does not spin or silently discard it. IndexedDB overflow is also explicit: each count/byte eviction is returned in DeliveryReport.dropped with the evicted event and one canonical reason.

User Identification

apdl.identify('user-42', {
  email: '[email protected]',
  plan: 'pro',
});

apdl.group('account-7', {
  tier: 'enterprise',
});

apdl.reset();

Identified user traits participate in feature flag evaluation.

identify(userId) keeps the current project-scoped anonymous ID on the canonical identify event and on later events. An identify event containing both IDs is the only anonymous-to-user alias assertion; there is no separate alias event or previous_id field. reset() clears the user, rotates the anonymous ID, and does not undo the historical relationship for the old ID. Accepted assertions are irreversible; the wire contract has no unmerge event.

Alias-backed analytics converge asynchronously: the relationship becomes query-visible after the ingestion writer durably stores the identify event, at which point earlier retained events with that project and anonymous ID resolve to the identified user. Calls made without analytics consent do not emit an alias assertion. Conflicting user claims for one anonymous ID fail closed and remain separate actors until an operator rebuilds the alias state.

Feature Flags

const variant = apdl.getVariant('new-checkout-flow');

if (variant === 'treatment') {
  renderTreatmentCheckout();
}

For diagnostics, use getVariantDetails:

const result = apdl.getVariantDetails('new-checkout-flow', {
  page: '/checkout',
  component: 'checkout-form',
});

console.log(result.variant, result.reason);

Flag evaluation automatically emits a deduplicated $feature_flag_exposure event. The SDK fetches initial flag configuration from the gateway endpoint at /v1/flags and listens for real-time updates on /v1/stream. The SSE request uses X-API-Key header authentication through a fetch stream; the client key is never placed in a URL. Reconnects resume with the standard Last-Event-ID header.

React to real-time variant changes pushed over SSE:

const unsubscribe = apdl.onVariantChange('new-checkout-flow', (variant) => {
  rerenderCheckout(variant);
});
// later: unsubscribe();

Experiment Context

Use the experiments namespace to provide stable targeting attributes for flag evaluation:

apdl.experiments.setContext({
  attributes: {
    plan: 'pro',
    region: 'us',
  },
});

const context = apdl.experiments.getContext();

apdl.experiments.clearContext();

Experiment context must use the canonical shape { attributes: Record<string, unknown> }. These attributes are merged into the feature flag evaluation context and may be included in feature flag exposure event metadata.

Privacy & Consent

// Inspect or update consent at runtime (e.g. from a cookie banner)
apdl.consent.get();
apdl.consent.update({ analytics: false });
apdl.consent.onUpdate((state) => console.log('consent changed', state));

// Register or remove custom PII scrubbers applied to every outgoing event
const scrubSsn = (event) => {
  delete event.properties?.ssn;
  return event;
};
apdl.privacy.addScrubber(scrubSsn);
apdl.privacy.removeScrubber(scrubSsn);

Baseline email, payment-card, and SSN scrubbers run in every privacy mode. privacyMode: 'cookieless' additionally derives a daily-rotating anonymous ID without persisting that identifier.

Revoking analytics consent is an immediate delivery fence: the SDK aborts the active analytics request when possible, clears its in-memory queue and this project's IndexedDB queue, and stops analytics auto-capture and health capture. No retained event is restored or sent across a revoke/regrant boundary. Regranting consent starts capture again for new events only.

Experiment consent is also fail-closed. Denial returns a null assignment with reason consent_denied, suppresses and removes exposures, clears experiment context and flag caches, and prevents the initial flag fetch and SSE stream. Regranting starts from a fresh authoritative flag snapshot. Personalization denial prevents slot discovery and rendering and removes already rendered SDK components; regranting resumes discovery for application-owned UI configs.

With persistence: 'localStorage', browser persistence is project-scoped. Anonymous identity, session, consent, flag cache, and offline event records use the project ID derived from the client key, so two APDL projects on one origin cannot restore each other's state. persistence: 'memory' does not read or write localStorage and does not open IndexedDB; all state ends with the client. Retryable delivery failures therefore remain in DeliveryReport.pending for an explicit same-session retry instead of being reported as persisted.

With persistence: 'localStorage', failed analytics deliveries may be retained in IndexedDB for up to seven days. Restore takes a five-minute client lease without deleting the record; deletion occurs only after an accepted or permanently rejected server response. Retryable failures release the lease, crashed-client leases are reclaimable after expiry, and a stale client cannot acknowledge a record reclaimed by another tab. Each record is scoped to the canonical project ID derived from the client key; the key itself is never persisted. A client cannot drain or clear another project's records on the same origin, and current analytics consent is checked again before any retained event is restored. Legacy, invalid, and expired records are discarded. Each project retains at most the newest 1,000 events and 5 MiB of UTF-8 JSON event payloads; older records are evicted deterministically without counting or deleting another project's records. Active leases are never evicted. Every overflow or invalid-storage rejection is returned in the immutable delivery report rather than counted as persisted. A single oversized or non-JSON-serializable event is not retained.

Local UI Renderer (No 0.3.0 Backend Delivery)

The package includes component registration, rendering, and slot-discovery utilities. APDL 0.3.0 does not have a canonical Config UI-config endpoint and does not publish UI configurations over SSE, so applications must pass a locally owned UIConfig to apdl.ui.render(...). The Agents personalization graph is disabled for the same reason.

// Register a custom component
apdl.ui.register({
  type: 'countdown-banner',
  schema: { properties: { deadline: { type: 'string' } } },
  render: (props, ctx) => { /* return an HTMLElement */ },
});

apdl.ui.render(locallyOwnedConfig, document.querySelector('#offer')!);

// React when the SDK discovers a UI slot on the page
apdl.ui.onSlotUpdate((slotId, element) => { /* ... */ });

Debugging & Shutdown

apdl.debug.enable();          // verbose console logging
apdl.debug.getQueue();        // inspect queued events
const report = await apdl.debug.flush();

console.log(report.delivered, report.persisted);
console.log(report.permanentRejections, report.pending);
console.log(report.dropped);

const finalReport = await apdl.shutdown();

flush() drains all currently owned in-memory events, and concurrent flushes join the same operation. Its frozen DeliveryReport distinguishes delivered, offline-persisted, permanently rejected, consent-discarded, and still-pending events. Its dropped entries separately identify offline evictions and invalid storage rejections by stable event messageId; persisted counts only records that survived in durable IndexedDB. The exact eviction reasons are offline_count_limit and offline_byte_limit; an invalid storage candidate is reported as offline_invalid_event. shutdown() stops accepting tracking immediately, joins concurrent callers, tears down capture and SSE, and returns the final drain report. Calls to track, identify, group, page, or reset after shutdown throw.

SDK Development

Run SDK commands from sdk/javascript:

npm run setup
npm test
npm run lint
npm run build
npm run release:check

Or use the repository-level make targets:

make setup-sdk
make test-sdk
make lint-sdk
make build-sdk
make release-sdk

npm run lint runs the strict tsc typecheck (the lint gate), npm run build produces the ESM, CJS, and IIFE bundles in dist/, and npm run release:check runs linting, tests, build, and an npm package dry run. Tests live in __tests__/**/*.test.ts; the flag-evaluation suite pins golden hash values from the canonical config-service implementation, guaranteeing this SDK buckets identically to the server and the Python SDK.

License

MIT