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

@drip-apex/app-sdk

v0.2.0

Published

Apex flags and events for hybrid Ionic, Angular, Capacitor and Cordova apps.

Readme

Apex app SDK

@drip-apex/app-sdk adds server-assigned flags, exposures, goals and purchases to hybrid apps built with Ionic, Angular, Capacitor or Cordova. It has zero runtime dependencies and uses WebView APIs: fetch, timers, Web Crypto, localStorage and visibility events. It does not change the DOM or load the storefront SDK.

npm install @drip-apex/app-sdk

For Cordova, allow the events host in your app's CSP before testing. Without connect-src https://events.drip-apex.com, every SDK request fails with a network error. Preserve any other origins your app needs:

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; connect-src 'self' https://events.drip-apex.com">

Cordova iOS apps need cordova-ios 8+ to build with current Xcode. Older shells lack UIScene support and can crash at launch on current iOS; this is independent of Apex. When reinstalling the same SDK version, clear Angular's .angular/cache before rebuilding so the app loads the updated package.

Configure once at app initialization. Create a publishable apx_pk_… key in Apex Settings → Developers. Never put a private API key or shop secret in the app. Await configure for storage hydration and the first assignment fetch to settle. initTimeoutMs (default 3,000 ms) bounds the wait for that fetch; initialization never rejects. Reads before resolution use defaults or cached assignments, with variation(key).reason reporting default, cached, or stale as appropriate.

import { Apex } from '@drip-apex/app-sdk';

await Apex.configure({
  publishableKey: 'apx_pk_live_…',
  shopId: 'YOUR_SHOP_UUID',
  applicationId: 'ch.coop.supercard',
  appVersion: '1.0.0',
  screen: () => 'home',
  qaMode: false,
});

await Apex.identify('app-user-id'); // Wait for this user's assignments (or initTimeoutMs).
const enabled = Apex.flag('welcome', false); // New user's value after a successful load.
// Use await Apex.identify(null) on logout to restore the device UUID.
await Apex.track('signup_completed');
await Apex.trackPurchase('order-42', 42.5, 'CHF');

Attributes passed to identify(userId, attributes) are reserved for a future contract and are not sent in v1. identify() never rejects and resolves when the new identity's assignments load or initTimeoutMs elapses. Await it before reading flags for the new user. Reads before the matching assignments load return the previous user's values without counting exposures, including reads immediately after calling identify. logExposure is also deferred during this period; it does not replay an old assignment's exposure. Read the flag or call logExposure again after the new assignments load to count them normally. If the wait times out, previous values remain uncounted until a matching response succeeds. A later identify supersedes an earlier response. The identified user is persisted per shop/environment. On a cold launch, the SDK restores that user's cached assignments immediately, including their exposure and purchase attribution, before waiting for the network. Without a saved user it restores the anonymous device cache. identify(null) clears the saved user for the next launch. A new in-session identify still serves the previous assignments until its fetch succeeds. Purchases during this pending period keep the existing attribution rule: the base purchase uses the new current identity, while experiment copies use the previous served assignment and its identity. After the new assignments load, both use the new identity.

Angular service

Provide one service at the root, configure through Angular app initialization, and inject that service into components. Change callbacks execute asynchronously; update Angular state through your usual change detection or signals.

import { Injectable, NgZone, inject, provideAppInitializer } from '@angular/core';
import { Apex } from '@drip-apex/app-sdk';

@Injectable({ providedIn: 'root' })
export class ApexService {
  private readonly zone = inject(NgZone);
  init(): Promise<void> {
    return this.zone.runOutsideAngular(() => Apex.configure({
      publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_UUID',
      applicationId: 'ch.coop.supercard',
      screen: () => 'home', // Replace with your router's route, excluding query/fragment.
    }));
  }
  welcome() { return Apex.flag('welcome', false); }
  subscribe(update: () => void) {
    return Apex.onChange(change => {
      if (change.type === 'changed') this.zone.run(update);
    });
  }
}

// Add this provider to ApplicationConfig.providers:
export const apexInitializer = provideAppInitializer(() => inject(ApexService).init());

// In a component:
// private readonly experiments = inject(ApexService);
// enabled = this.experiments.welcome();
// private readonly subscription = this.experiments.subscribe(() => {
//   this.enabled = this.experiments.welcome();
// });
// ngOnDestroy() { this.subscription.cancel(); }

For apps that react to onChange, start configuration in NgZone.runOutsideAngular so SDK timers and network completions do not trigger unnecessary change detection. Re-enter with zone.run only to update UI, as the service above does. Older Angular versions without provideAppInitializer can provide APP_INITIALIZER with multi: true and a factory that captures const service = inject(ApexService) synchronously and returns () => service.init().

What your developers code

  • Read a flag where the app selects its content or code variant. Exposures are automatic on compatible flag reads, deduped per experiment, variation and epoch in a session. A missing epoch still exposes with assignment_epoch: null.
  • Call track("goal_name") once at each goal action. Custom events are unattributed; purchase attribution is handled separately.
  • Call trackPurchase(orderId, revenue, currency) at order confirmation with a stable order ID, a finite non-negative number in currency units and a three-letter currency code. The SDK enqueues a base row plus one row for every served assignment. These rows share an event ID and retain their IDs on retry.
  • Call identify(userId) on login and identify(null) on logout.
  • Set qaMode: true in test builds. Non-production environments are also QA. Store builds use environment: "production" and qaMode: false.
  • After configure, use Apex.deviceId for an anonymous QA registration or event lookup. After login use Apex.userId. Register that identity with the flag's QA devices in Apex to pin a variation. Use the trusted-terminal tracking verify endpoint described in the wire contract to inspect recent events. During identify transitions, also check the previous assignment identity. The app's publishable key cannot read management APIs.

Content and configuration tests can use remote values. Layout and flow tests still require both versions in your app code behind the flag. Apex cannot remove that code from the app for you.

Typed objects and manual exposures

type Welcome = { title: string; button: { label: string; enabled: boolean } };
const fallback: Welcome = { title: 'Welcome', button: { label: 'Start', enabled: false } };
const validate = (v: unknown): v is Welcome => {
  if (!v || typeof v !== 'object') return false;
  const x = v as Partial<Welcome>;
  return typeof x.title === 'string' && typeof x.button?.label === 'string'
    && typeof x.button?.enabled === 'boolean';
};
const content = Apex.flagObject('welcome', fallback, { validate, exposure: 'manual' });
// When the content is visible:
Apex.logExposure('welcome');

A validator checks unknown JSON without throwing into app code. Without it, flagObject checks the outer JSON type only; TypeScript types do not validate nested fields at runtime. Returned values are copies. Unknown keys remain present. Assigned object reads expose only after successful validation. Failed or throwing validators return the default without exposure. This differs from the native typed getter. Missing assignments do not expose. Primitive reads require matching boolean/string/number types, finite numbers and safe integers; no string coercion. Missing or incompatible reads return the default and log once per key per session, only after an assignment set has loaded from cache or the network. Reads before that first load use defaults without warnings. variation(key) exposes by default. Use variation(key, { exposure: 'manual' }) to read the descriptor without recording an exposure, then call logExposure(key) when visible. A configured client returns a default variation with reason: "default" when unassigned; the unconfigured facade returns null.

Storage and lifecycle

The default adapter uses localStorage. To use Capacitor Preferences, supply an adapter; Capacitor remains an app dependency, not an SDK dependency:

import { Preferences } from '@capacitor/preferences';
import { Apex, type ApexStorage } from '@drip-apex/app-sdk';

const storage: ApexStorage = {
  async get(key) { return (await Preferences.get({ key })).value; },
  async set(key, value) { await Preferences.set({ key, value }); },
};
await Apex.configure({ publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_UUID', storage });

Adapters must atomically replace a string value or reject without changing it. The SDK stages queued events in memory and coalesces bursts into the latest queue snapshot, with at most one storage write in flight and one pending snapshot. track() and trackPurchase() wait for the write containing their events before resolving. A failed write restores the previous durable queue and logs the failure; purchase groups remain atomic. If an identify identity write fails, the SDK restores the previous in-memory identity, logs once that the identity could not be saved, and stops assignment fetches and event sends until a later identify successfully saves its identity. Queue/cache read failures log and use available defaults.

Assignments refresh on configure, identify, explicit refresh(), and foreground transitions, matching the native SDKs. There is no periodic assignment polling; 60 seconds is the cached assignment freshness window, not a network interval. Foreground refresh uses the configured jitter and respects retry deadlines. Startup schedules delivery of a hydrated nonempty queue on the first tick. Foreground transitions also flush a nonempty queue, respecting event retry deadlines.

The SDK listens to browser visibilitychange, Cordova document pause/resume, and globalThis.Capacitor?.Plugins?.App?.addListener('appStateChange', ...) when available. It does not import or require a shell plugin. If your Capacitor version exposes App only as an imported module, forward its callback explicitly:

import { App } from '@capacitor/app';
App.addListener('appStateChange', ({ isActive }) => {
  Apex.setAppState(isActive ? 'foreground' : 'background');
});
// Or call Apex.setAppState('background' / 'foreground') from your own shell hooks.

Repeated state signals and rapid overlay transitions coalesce within one second. Backgrounding first completes any pending queue write, then flushes the persisted queue with best-effort fetch keepalive if the UTF-8 request body fits 64 KiB; larger batches use normal fetch. Disk remains authoritative until the server acknowledges a batch and the acknowledgement is persisted. Thirty continuous minutes in the background rotate the session and clear exposure/warning dedupe. Lifecycle listeners are removed on shutdown. Every cold launch creates a new session ID and allows a new exposure for each served assignment, even when relaunching within thirty minutes. This matches both native clients: iOS initializes sessionId with UUID().uuidString and Android with UUID.randomUUID().toString(); neither persists sessions across launches. The thirty-minute rule applies within a running client.

track, trackPurchase, refresh and flush return promises for their local work, not server delivery. identify additionally waits for matching assignments or initTimeoutMs, without rejecting. configure resolves after storage hydration and the first assignment fetch settles (including failures), or its configurable 3,000 ms fetch wait expires. A late response still updates flags. Use onChange to observe changed keys or a fatal stop ({ type: "stopped", code }). Subscriptions have an idempotent cancel(). To use a separate context, construct new ApexClient(options) and await its ready promise. Call shutdown() when retiring it. Do not run multiple clients with the same storage and shop/environment namespace simultaneously.

Delivery and options

| Option | Default / limit | | --- | --- | | environment | production | | qaMode | false; other environments also stamp qa_mode: true | | flushIntervalSeconds | 5; clamped to 0.1–86,400 | | batchSize | 20; clamped to 1–20 | | queueCapacity | 2,000; clamped to 1–2,000 | | queueRetentionHours | 47; clamped to 0–47, below the 48-hour replay window | | refreshJitterSeconds | [0, 10] | | initTimeoutMs | 3,000; zero skips assignment waiting on configure and identify | | platform | Detected ios, android, or web; explicit override takes precedence | | applicationId / appVersion | unknown / empty string | | screen | home; an event's string properties.screen takes precedence | | logger | Optional; callback errors are isolated | | fetch / baseUrl | WebView fetch / https://events.drip-apex.com |

Queue capacity evicts the oldest rows, dropping a whole older purchase group if the cut would split it. A new purchase larger than capacity or 200 rows is dropped whole with a diagnostic. Delivery keeps each purchase group in one request, expanding the batch target when needed within the 200-event server request limit. Expired rows are removed at enqueue/startup/flush. Future timestamps survive clock rollback. Events use app://<lowercase applicationId>/<encoded screen path> with no query or fragment. Platform detection uses Capacitor getPlatform(), then Cordova device.platform (case-insensitive), then iPhone/iPad/iPod or Android user-agent detection, finally web. Explicit platform overrides detection. The production Worker already accepts all three values; no Worker change is required.

Fatal 401 codes invalid_publishable_key and publishable_key_not_allowed stop both network lanes while reads retain values. Retryable responses respect Retry-After seconds or HTTP dates. Missing lookup-unavailable retry headers use five seconds; ordinary 429 responses use exponential backoff with jitter. Other network/server failures use exponential 5/10/20/40/60-second backoff with 0–20% positive jitter, capped at 60 seconds. Unknown 401/403 use 300 seconds; explicitly retryable assignment 4xx use 30 seconds. Other non-retryable assignment 4xx end that request without automatic retries. Event non-retryable 4xx drop the batch, except 401/403/408/429 which are retained. Only HTTP 202 with ok: true acknowledges events. A failed acknowledgement write retains the rows for replay. Requests time out after 30 seconds and refuse redirects.

Supply logger: line => ... to receive developer diagnostics. Assignment fetch and event delivery failures report network errors or HTTP status, plus whether a retry is scheduled or the request/batch was dropped. Each lane and failure class (network error or distinct HTTP status) logs at most once per sixty seconds per client. Fatal auth logs its reason and that the SDK stopped; an obviously malformed publishable-key prefix also logs at configure. Keys, Authorization headers, response bodies and exception details are never included in these diagnostics. Without a logger the SDK emits no logs, including to the console.

track rejects malformed revenue and property keys email, phone, password, ssn, credit_card, card_number (case/diacritic insensitive). Numeric revenue strings must match the Worker's strict decimal grammar, including comma decimals. Purchases strip caller order/revenue/currency aliases before adding explicit arguments. Properties must be JSON, including finite numbers.

Authentication uses only Authorization: Bearer <publishableKey> on assignment and event requests. Cookie auth does not work from capacitor://localhost. The SDK never sends X-Apex-Publishable-Key; that header is not allowed by the production WebView CORS policy. No cookie credentials are needed.

Importing or configuring the SDK during SSR is safe: it logs once per context, performs no storage/network work, and returns default flags. Configure again in the browser to activate it.

Builds are ESM and CJS targeting ES2019, with declarations compatible with TypeScript 4.9+ and Angular 15+ webpack/esbuild builders, with no Node APIs in either runtime bundle. The package tests bundle consumers with esbuild, verify both declaration formats, and use offline TypeScript 4.9 when installed. If the 4.9 check is skipped, the orchestrator must rerun the package tests with a locally available TypeScript 4.9 installation in node_modules before claiming that compiler was verified. Older Angular versions use APP_INITIALIZER as described above. Publishing is gated on main and npm trusted publishing; the repository owner must configure the @drip-apex/app-sdk trusted publisher for publish-app-sdk.yml in the npm-production environment before the first release.

Public failure contracts

All instance and singleton methods contain invalid app input and app callback errors: no synchronous throw and no rejected SDK promise. Invalid input leaves identity, assignments, queue and lifecycle state unchanged. This is validation at the method boundary; a callback failure has its own recovery behavior below.

| Method | Invalid input / failure outcome | Return | | --- | --- | --- | | new ApexClient(options), Apex.configure(options) | Invalid options stop the new context before storage, listeners or network. One diagnostic through a callable logger. Reconfigure retires the previous context first. | ready / configure resolves undefined | | identify(id, attributes?) | Only nonempty string IDs up to 1,024 characters or null (anonymous) are accepted. Attributes, when supplied, must be a JSON record of strings up to 1,024 characters; they remain unused by evaluation. Invalid input logs and does not start a pending identity change. | Resolves undefined | | flag(key, default, options?) | Invalid key, primitive default or options returns the exact supplied default, without exposure. A missing or incompatible assignment warns once per key after assignments load. | Supplied default | | flagObject(key, default, options?) | Invalid key, JSON default or options returns the exact supplied default without exposure. Failed or throwing validators return the default, warn once per key, and never expose. Successful decoding isolates the returned value from cached data. | Supplied default | | variation(key, options?) | Invalid key or options returns null without exposure. A valid missing key retains the existing reason: 'default' descriptor. Unsafe assigned JSON returns null with the typed warning. | null / default descriptor | | logExposure(key) | Invalid key is ignored without exposure. Pending identify suppresses all exposure entry points. | undefined | | track(name, properties?) | Nonempty names up to 256 characters; properties omitted/undefined mean {}. Non-record properties (including null, arrays and primitives), unsafe JSON, PII or invalid revenue log and drop before enqueue. | Resolves undefined | | trackPurchase(id, revenue, currency, properties?) | Nonempty string ID up to 1,024 characters, finite safe nonnegative revenue, three-letter currency after trimming/uppercasing. Properties have the same record contract as track. Invalid input logs and drops the whole purchase before enqueue. | Resolves undefined | | onChange(handler) | Nonfunctions log and return an inert subscription. A throwing handler logs; every other handler still runs. | { cancel() }; cancel is idempotent | | setAppState(state) | Only foreground and background are accepted. Invalid input logs and leaves state/timers unchanged. | undefined | | refresh(), flush() | No arguments required; extra arguments ignored. Transport failure uses the documented lane retry policy; stopped contexts do nothing. | Resolves undefined | | shutdown() | Extra arguments ignored; stops networking and settles waiters, then waits for outstanding serialized storage work. | Resolves undefined | | sdkVersion, deviceId, userId | Read-only; no input or side effect. Singleton IDs are undefined before configure; instance IDs are empty before hydration. | String (or singleton undefined) |

JSON input rejects cycles, nonfinite/unsafe integer numbers, functions, custom prototypes, accessors, and __proto__, constructor, or prototype keys at any level. Limits are 32 nesting levels, 10,000 visited nodes, 10,000 array slots (including sparse slots), and 1 MiB of combined UTF-16 string/key characters. Primitive flag defaults follow these limits too. Flag keys are nonempty strings up to 1,024 characters. Options are records with only the documented keys; callbacks must be functions, numeric options finite safe numbers, jitter a nonnegative ordered pair, and baseUrl an HTTP(S) URL without embedded credentials. Existing numeric option clamping still applies.

Throwing loggers are isolated. Throwing screen() callbacks, unusable screen strings, or malformed Unicode screen paths fall back to home. Storage adapter get/set may throw synchronously or reject: device persistence failure stops initialization, cache read failure gives defaults, event write failure keeps the previous durable queue, acknowledgement write failure retains IDs for safe replay, and identity write failure uses the suspension/recovery contract above. A queue restored under a smaller capacity is activated only after successful trimming; if that write fails, the new context stops with disk untouched. Reconfigure after storage recovery to retry the restore. Atomic replace-or-reject is required of the adapter; an adapter that hangs forever can keep a storage operation (and shutdown) pending.

Capacity and retention removals log their drop reasons with each coalesced write. Purchases larger than queue capacity or 200 rows are rejected whole with a diagnostic. A purchase group is delivered in one request: the batch target may expand to include its last whole group, without exceeding the server's 200-event request limit. Restored oversized legacy groups are dropped whole with a diagnostic. Event IDs intentionally match across the base and attributed purchase rows. Exactly-once server ingestion uses (event_id, experiment_id, variation_id) as its delivery key; network retry or failed local acknowledgement persistence can retransmit the same key.

Long sessions bound warning keys at 256, diagnostic rate-limit keys at 128, listeners at 1,024, identity cache entries at four, and exposure dedupe keys at 4,096 per session. Once the exposure bound is reached, new tuples are dropped with a diagnostic, preserving dedupe for tuples already counted. Session rotation clears exposure and typed-warning dedupe. At most three assignment requests remain outstanding (older requests are aborted on saturation), plus one event request; abort/timeout releases SDK bookkeeping even if a custom fetch ignores its signal. A non-aborted superseded fatal auth reply still stops both lanes.

Deterministic stress verification

npm test includes 2,000 seeds × 200 operations (400,000 operations), input and assigned-value fuzzing, simulated process death with atomic disk survival, slow / failed persistence, unordered and abort-ignoring transports, and all lifecycle sources. Tracking-promise resolution and process death both check that resolved events remain on disk, delivered, or removed by a logged drop rule. Slow-adapter burst tests cover 500 and 2,000 events and require at most two writes, including bursts arriving during an existing write. Failures report the seed and operation. Replay one seed with APEX_CHAOS_FIRST_SEED=424 APEX_CHAOS_SEEDS=424 npm test; run a larger soak with APEX_CHAOS_SEEDS=20000 APEX_CHAOS_OPERATIONS=1000 npm test.

The CI performance guard starts with a saturated 2,000-row durable backlog, then feeds 10,000 additional events through flush and atomic storage writes. It requires all 12,000 rows delivered within a generous 15-second wall-clock budget on the CI runner. This exercises the shipped 2,000-row capacity rather than allowing an impossible 10,000-row simultaneous queue. A separate 5,000-cycle session checks internal map/set, waiter, request and timer bounds.