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

@apex-inc/capacitor-plugin

v2.3.0

Published

Apex Capacitor plugin — iOS/Android attribution, events, deep linking, SKAN, and offline-tolerant tracking for Capacitor apps.

Readme

@apex-inc/capacitor-plugin

Apex Capacitor plugin — iOS and Android attribution, events, deep linking, SKAN, and offline-tolerant tracking for Capacitor apps.

Ships alongside apex.js (the web snippet) so a Capacitor app gets unified identity and events across WebView + native APIs with one dependency pattern.

Scope. This plugin is for Capacitor apps. Native iOS (Swift), native Android (Kotlin), React Native, and Flutter apps need their own SDKs — see https://apex.inc/docs/mobile/which-sdk.

Try it in 5 minutes. The sample app is a three-screen Capacitor + React app that exercises every API in this plugin. git clone, npm install, npm run dev, watch events land in your Apex dashboard.

Install

npm install @apex-inc/capacitor-plugin
npx cap sync

Initialize

Call initialize() once at app startup, before any other plugin method.

import { Apex } from "@apex-inc/capacitor-plugin";

await Apex.initialize({
  workspaceKey: "prj_your_key",
  // Optional:
  // apiKey: "apex_sk_…",       // see "API keys" below — unlocks verified identity stitching
  // apiUrl: "https://app.apex.inc",
  // autoTrackScreenViews: true, // default — screen_view auto-fires from SPA navigation
  // sessionTimeoutMinutes: 30,
  // offlineQueueMaxSize: 1000,
  // testMode: false,
  // debug: false,
});

app_open auto-fires on initialize(). screen_view auto-fires from the webview's History API (pushState / replaceState / back-forward / hashchange — covers Ionic, React Router, Vue Router, and plain SPA routing, on device and in browser preview). Screen names default to the URL path (/product/42); for richer names, set autoTrackScreenViews: false and wire trackScreenView("Product Detail") into your router instead.

API keys

identify() and server-trusted flows work best with a workspace SDK key (apex_sk_…). Mint one in the Apex dashboard under Settings → Workspace → AI agents & API keys, and pass it as apiKey in initialize(). Without a key, identity stitches from untrusted contexts may be quarantined (held un-verified) rather than applied — events still flow, but traits and journey triggers wait until a verified stitch. Treat the value like a publishable key: assume it can be extracted from the app bundle; Apex rate-limits per key + per visitor to keep leaked keys low-yield.

Track events

await Apex.track({
  type: "app_open",
  data: { from: "push_notification" },
});

// In-app purchase — typed payload
await Apex.track({
  type: "in_app_purchase",
  purchase: {
    productId: "com.example.app.pro_monthly",
    amount: 9.99,
    currency: "USD",
    transactionId: "abc123",
  },
});

Every event gets a client-generated UUIDv4 id automatically. Server-side idempotency means replayed events never double-count.

Push notifications (iOS)

const { permission, token } = await Apex.registerForPushNotifications();
if (permission === "granted" && token) {
  console.log(`APNs token: ${token}`);
  // The plugin already POSTed it to /api/mobile/push-token for you.
}

// Subsequent token rotations:
await Apex.addListener("pushTokenReceived", ({ token }) => {
  console.log("New token", token);
});

// Foreground push payloads:
await Apex.addListener("pushReceived", (event) => {
  console.log("Push:", event.title, event.body, event.data);
});

You also need three small additions to your iOS app's AppDelegate so the plugin can observe the OS-side callbacks. Paste this into ios/App/App/AppDelegate.swift:

import UIKit

extension AppDelegate {
  func application(_ application: UIApplication,
                   didRegisterForRemoteNotificationsWithDeviceToken token: Data) {
    NotificationCenter.default.post(
      name: Notification.Name("ApexCapacitor.didRegisterForRemoteNotifications"),
      object: nil,
      userInfo: ["deviceToken": token]
    )
  }

  func application(_ application: UIApplication,
                   didFailToRegisterForRemoteNotificationsWithError error: Error) {
    NotificationCenter.default.post(
      name: Notification.Name("ApexCapacitor.didFailToRegisterForRemoteNotifications"),
      object: nil,
      userInfo: ["error": error]
    )
  }

  func application(_ application: UIApplication,
                   didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                   fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    NotificationCenter.default.post(
      name: Notification.Name("ApexCapacitor.didReceiveRemoteNotification"),
      object: nil,
      userInfo: userInfo as? [AnyHashable: Any] ?? [:]
    )
    completionHandler(.noData)
  }
}

Then enable the Push Notifications capability in Xcode (Signing & Capabilities → + → Push Notifications). Sandbox builds (Xcode-built installs to a real device) hit Apple's sandbox APNs endpoint — configure that environment in your Apex workspace settings.

iOS App Tracking Transparency

// Present the ATT prompt once (no-op on Android).
const { status } = await Apex.requestTrackingAuthorization();

// Check status later without prompting.
const { status: current } = await Apex.getTrackingStatus();

Advertising identifiers

// Returns IDFA (iOS, ATT authorized), GAID (Android), or fallback.
const { id, fallback } = await Apex.getAdvertisingId();
if (!id && fallback === "idfv") {
  console.log("Using IDFV fallback — user denied ATT");
}

SKAdNetwork conversion values (iOS 4.0+)

await Apex.updateConversionValue({
  fineValue: 42,
  coarseValue: "high",
});

Deep links

// Cold start: read the URL that opened the app (if any).
const { url } = await Apex.getInitialDeepLink();

// Warm-start / subsequent links while running:
await Apex.addListener("deepLink", ({ url }) => {
  handleRoute(url);
});

Sessions

const { sessionId } = await Apex.startSession();

await Apex.addListener("sessionEnd", ({ sessionId, durationSeconds }) => {
  console.log(`Session ${sessionId} ended after ${durationSeconds}s`);
});

Experiments

Resolve a visitor's variant for a mobile experiment, then branch your UI:

const { variant, payload, eligible } = await Apex.getVariant({
  experimentId: "exp_123",
});
if (!eligible) return renderControl();
if (variant === "variant_b") return render(payload);
return renderControl();

On-device variant screenshots (debug builds)

captureVariantScreenshot() rasterizes the current screen and attaches it to the experiment as a device-source asset. The shot lands on the dashboard experiment card (cover) and the detail Variant previews gallery, alongside the web/agent captures. Use it for authed in-app mobile screens that servers can't reach — public web URLs are auto-captured server-side on create, and the agent CLI / attach_experiment_asset covers localhost.

captureVariantScreenshot(options: {
  experimentId: string;
  variantKey: string;
  label?: string;
  commitSha?: string;
}): Promise<{ captured: boolean; url?: string; reason?: string }>;

It's debug-gated: you must Apex.initialize({ ..., debug: true }). In production it no-ops ({ captured: false, reason: "debug_disabled" }) and never screenshots real end users — it's a QA/review tool. (Web returns { captured: false, reason: "web_unsupported" } — capture is native-only.)

Capture on the screen that renders the variant, keyed to the resolved variant:

const variant = useApexVariant(experimentId); // or: const { variant } = await Apex.getVariant({ experimentId })
useEffect(() => {
  if (variant) {
    Apex.captureVariantScreenshot({ experimentId, variantKey: variant });
  }
}, [variant]);

Offline durability

Events are persisted to IndexedDB (web), Core Data (iOS), or Room (Android). If the device is offline, they queue up to the configured offlineQueueMaxSize (default 1000). When the network returns, the plugin drains the queue in batches with exponential-backoff retry.

const { count, oldestEventAt } = await Apex.getQueueSize();

// Manually trigger a flush (rarely needed — happens automatically):
const { flushed, remaining } = await Apex.flushQueue();

Test mode

// At init:
await Apex.initialize({ workspaceKey: "prj_test", testMode: true });

// Or toggle at runtime:
await Apex.setTestMode({ enabled: true });

Test-mode events are stored separately on the server and never pollute production analytics.

Web fallback

The plugin runs in a browser or PWA too — identifiers return null, ATT is a no-op, events still flow to the server via the normal offline queue. This lets you run shared code paths between web + mobile without Capacitor.isNativePlatform() checks everywhere.

License

Apache-2.0 — see LICENSE. See also the workspace plan document for the full MMP roadmap.