Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Sprout Analytics

Insights on how your React Native Expo app is used, with none of the infra.

Bijection source
git clone https://github.com/bijectionhq/sprout-analytics.git
cd sprout-analytics
git checkout 232ade98230e1789388d57787278684d1d441a33
cd packages/analytics

Bijection repository access is required. Request access.

Description

Insights on how your React Native Expo app is used, with none of the infra.

Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. Exercise the component in your deployment according to your project’s verification rules.

Open source

Maintained source: bijectionhq/sprout-analytics. Based on ambjn/sprout. License: Apache-2.0. Consult the repository for license terms, notices, and contributor attribution.

From the upstream publisher

Insights on how your React Native Expo app is used, with none of the infra.

Explore the demo ↗

Upstream overview

From the upstream package documentation.

Sprout Analytics is a Convex component that adds session tracking, custom event tracking, screen tracking, user identification, and error capture to React Native Expo apps without any external ingestion service or third-party database. It ships an Expo SDK, a Convex component with batched HTTP ingest, sharded rollup counters, and fingerprinted error issues, plus a CLI that wires everything into an existing Convex and Expo project in one command. Your analytics data stays entirely within your own Convex deployment.

Use cases

From the upstream package documentation.

  • You want product analytics (events, screen views, sessions) in a React Native Expo app but do not want to send data to a third-party service like Mixpanel or Amplitude
  • You need custom events and session tracking without standing up a separate analytics backend or managing another set of service credentials.
  • You want a self-contained analytics setup that requires no external SLA, no provisioning, and no separate backend, only a Convex deployment you already have running.
  • You need an offline-durable event queue that survives app kills and retries delivery with exactly-once ingest semantics.
  • You need to throttle or kill ingest for a specific app on demand (a leaked write key, a cost spike) without redeploying anything.

How it works

From the upstream package documentation.

Sprout brings app analytics, insights, and error tracking to React Native Expo apps as a Convex component, so session tracking, custom events, screen views, user identification, and crash reporting all run inside your own Convex deployment instead of a third-party SaaS pipeline.

The Expo SDK exposes four calls: track, screen, identify, and captureException, that queue onto an offline-durable buffer and flush in batches with exactly-once delivery, so nothing's lost across app kills or dropped connectivity. Sessions are tracked automatically via AppState boundaries, with no manual session management required in application code. Errors are fingerprinted by type and stack trace, so repeat crashes collapse into one issue with an open/resolved/ignored workflow instead of flooding a flat event stream.

A CLI handles setup end-to-end: sprout init wires the component, ingest route, registration mutation, and Expo root layout into an existing Convex + Expo app in a single pass, and sprout dashboard serves a prebuilt local dashboard: overview charts, sessions, events, and issues, against your live deployment, with nothing extra to host. Ingest is protected by a write key, with an optional kill switch and hourly rate limit you can flip on demand without redeploying anything.

Upstream package FAQ

Does Sprout Analytics send data to any external analytics service?

No. Sprout Analytics runs entirely inside your own Convex deployment. The Expo SDK batches and sends events to an HTTP ingest endpoint you register on your own Convex site URL, and all data is stored in your Convex database. No third-party ingestion service is involved.

What happens to events when the device is offline or the app is killed?

The Sprout Analytics SDK maintains an offline-durable event queue backed by AsyncStorage. Events that cannot be flushed immediately are persisted locally and retried with exactly-once delivery semantics. The queue survives app kills and is picked up again on the next initSprout call, with configurable batch size, queue size, and flush interval.

What are the peer dependencies required to use Sprout Analytics?

Sprout Analytics requires an existing Expo app with Convex already configured and npx convex dev working. It also requires expo-router for automatic screen autocapture, and expo-device, expo-constants, and @react-native-async-storage/async-storage, which are standard in a typical Expo project. Manual screen tracking with the screen function still works without expo-router.

How is the Convex component mounted and the ingest route registered?

The sprout init CLI command handles the wiring automatically: it mounts the Sprout component in convex/convex.config.ts using app.use, registers a POST /sprout/ingest route in convex/http.ts via Sprout.registerRoutes, generates a write key, and adds initSprout and useSproutScreenTracking to the Expo root layout. Any step it cannot automate is printed as a code snippet to apply manually.

Can the Sprout Analytics dashboard be shared with teammates or exposed publicly?

The Sprout dashboard is designed to run on localhost and is protected only by a shared dashboard key, not per-user authentication. The sprout dashboard command warns before binding to any address other than 127.0.0.1, and the README explicitly recommends keeping it on localhost. It is intended as a local developer tool, not a hosted shared service.

Use with agents and CLI

Give your coding agent the component’s source identity and setup guidance.

agent-prompt.md
Help me integrate Sprout Analytics with Bijection.

Read: https://bijection.com/components/sprout-analytics/SKILL.md

Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.

Upstream README.md

View repository ↗

Documentation from ambjn/sprout, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~                                          ~
~                                _         ~
~      ___ _ __  _ __ ___  _   _| |_       ~
~     / __| '_ \| '__/ _ \| | | | __|      ~
~     \__ \ |_) | | | (_) | |_| | |_       ~
~     |___/ .__/|_|  \___/ \__,_|\__|      ~
~         |_|                              ~
~                                          ~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@sprout-convex/analytics

Insights on how your React Native Expo app is used, with none of the infra.


Monorepo for @sprout-convex/analytics: a Convex component pairing an Expo SDK, a CLI, and a local dashboard, so analytics run entirely inside your own Convex deployment.

Layout

PathWhat it is
packages/analyticsThe published package. Expo SDK (track/screen/identify/captureException), the Convex component, and the sprout CLI.
apps/dashboardSource for the dashboard UI. Built at publish time and bundled into packages/analytics/dist/dashboard; not published on its own.

Sprout CLI

Once @sprout-convex/analytics is installed in an Expo + Convex app, bunx sprout <command> is available:

CommandWhat it does
sprout initWires the component, ingest route, registration mutation, dashboard queries, and Expo root layout into an existing Convex + Expo app in one pass.
sprout dashboardServes the prebuilt dashboard UI against your Convex deployment on http://127.0.0.1:4321, opening it in your browser.
sprout helpPrints the command and flag summary shown above.

Full setup guide, SDK usage, and dashboard flags live in packages/analytics/README.md.

Monorepo commands

Run from the repo root:

bun run build       # builds packages/analytics: SDK, CLI, and the bundled dashboard
bun run test        # bun test across the monorepo
bun run typecheck   # typechecks every workspace package

Insights on how your React Native Expo app is used, with none of the infra.

  • No ingestion service to run
  • No database to provision
  • No SLA to babysit

It's one Convex component: session and screen tracking, custom events, and error tracking with fingerprinted issues, breadcrumbs, and stack traces — all running inside your own Convex deployment, so your data never leaves it.

Status: 1.0. The SDK, Convex component, CLI, and dashboard are all implemented and work end-to-end. This is the first stable release.

What you get

  • Expo SDK — track / screen / identify / captureException, automatic session boundaries via AppState, global JS error capture, screen autocapture for expo-router, and an offline-durable event queue that survives app kills and retries with exactly-once ingest.
  • Convex component — batched ingest behind your own HTTP endpoint (write key + kill switch + optional rate limit), sharded rollup counters that don't contend under concurrent writes, and fingerprinted error issues with open/resolved/ignored workflow.
  • CLI — sprout init wires the component, ingest route, registration mutation, dashboard queries, and Expo root layout into an existing Convex + Expo app in one pass; sprout dashboard serves the dashboard against your deployment. Nothing to host.
  • Local dashboard — a prebuilt insights UI (overview, events, sessions, issues) served by sprout dashboard.

Requirements

  • An existing Expo app with Convex already set up (npx convex dev working).
  • expo-router, for screen autocapture (everything else still works without it; you'd just call screen() manually).
  • expo-device, expo-constants, and @react-native-async-storage/async-storage, all peer dependencies already present in a standard Expo app.

Install

bun add @sprout-convex/analytics

Quick start

From your app's root (a Convex + Expo project with bunx convex dev already set up):

bunx sprout init

This mounts the component, generates a write key and dashboard key, wires the ingest HTTP route and a registration mutation, adds initSprout + useSproutScreenTracking to your root layout, and offers to run the Convex side effects (convex env set SPROUT_DASHBOARD_KEY, convex run sproutSetup:register) for you. Anything it can't wire automatically it prints as a snippet instead.

// convex/convex.config.ts
import { defineApp } from "convex/server";
import sprout from "@sprout-convex/analytics/convex.config";

const app = defineApp();
app.use(sprout);
export default app;
// convex/http.ts
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { Sprout } from "@sprout-convex/analytics/client";

const http = httpRouter();
new Sprout(components.analytics).registerRoutes(http); // POST /sprout/ingest
export default http;
// convex/sproutSetup.ts — one-time registration, run via
// `bunx convex run sproutSetup:register '{"writeKey":"..."}'`
import { internalMutation } from "./_generated/server";
import { Sprout } from "@sprout-convex/analytics/client";
import { components } from "./_generated/api";

const sprout = new Sprout(components.analytics);

export const register = internalMutation({
  /* ... */
  handler: async (ctx, { writeKey }) =>
    sprout.createApp(ctx, { slug: "my-app", name: "My App", writeKey }),
});
// app/_layout.tsx
import { initSprout } from "@sprout-convex/analytics";
import { useSproutScreenTracking } from "@sprout-convex/analytics/hooks";

void initSprout({
  convexSiteUrl: process.env.EXPO_PUBLIC_CONVEX_SITE_URL!,
  writeKey: process.env.EXPO_PUBLIC_SPROUT_WRITE_KEY!,
});

export default function RootLayout() {
  useSproutScreenTracking(); // screen autocapture for expo-router
  // ...
}

Usage

All four calls below are fire-and-forget (no await) and queue onto the same offline-durable buffer initSprout sets up: batched, retried with exactly-once ingest, nothing lost across app kills or dropped connectivity. Property values are restricted to the exported PropertyValue type (string | number | boolean | null), so keep them flat (no nested objects/arrays).

track: custom events

import { track } from "@sprout-convex/analytics";

const UpgradeButton = ({ planId }: { planId: string }) => (
  <Pressable
    onPress={() => {
      track("upgrade_tapped", { planId, source: "settings" });
      router.push("/upgrade");
    }}
  >
    <Text>Upgrade</Text>
  </Pressable>
);

screen: manual screen views

useSproutScreenTracking() in your root layout already autocaptures expo-router navigation, so most apps never call this directly. Reach for it when a "view" doesn't correspond to a route change: a modal, a step in a wizard, a tab inside a single screen.

import { screen } from "@sprout-convex/analytics";

const OnboardingWizard = () => {
  const [step, setStep] = useState(0);

  useEffect(() => {
    screen(`onboarding/step-${step}`, { totalSteps: 4 });
  }, [step]);

  // ...
};

identify: associate events with a user

Call this once you know who the user is (after login, or on app start if they're already signed in) so their events and issues are attributable across sessions and devices:

import { identify } from "@sprout-convex/analytics";

const handleLogin = async (email: string, password: string) => {
  const user = await signIn(email, password);
  identify(user.id, { plan: user.plan, email: user.email });
  router.replace("/");
};

captureException: report a caught error

Uncaught JS errors and fatal exceptions are captured automatically once initSprout() has run. Use captureException for errors your own code already catches and swallows, so they still show up as an issue:

import { captureException } from "@sprout-convex/analytics";

const loadProfile = async (userId: string) => {
  try {
    return await api.getProfile(userId);
  } catch (error) {
    captureException(error, { properties: { userId, screen: "profile" } });
    return null; // fall back gracefully, but don't lose visibility into why
  }
};

It also composes with a React error boundary:

import { captureException } from "@sprout-convex/analytics";

class ScreenErrorBoundary extends React.Component<Props, State> {
  componentDidCatch(error: unknown, info: React.ErrorInfo) {
    captureException(error, { properties: { componentStack: info.componentStack } });
  }
  // ...
}

Advanced: getSprout / shutdownSprout

import { getSprout, shutdownSprout } from "@sprout-convex/analytics";

const handleLogout = () => {
  shutdownSprout();
  signOut();
};
  • getSprout() returns the underlying SproutCore instance, or null before initSprout resolves. An escape hatch for cases the top-level track/screen/identify/captureException calls don't cover.
  • shutdownSprout() stops the flush timer and unhooks AppState/error listeners, useful on logout or account switch. The on-device queue isn't cleared, so a later initSprout() picks up right where it left off.

Configuration

Options accepted by initSprout:

OptionDefaultDescription
convexSiteUrl(required, unless ingestUrl set)Your Convex deployment's site URL (https://<deployment>.convex.site). Ingest path defaults to /sprout/ingest.
ingestUrlderived from convexSiteUrlFull ingest URL, for overriding the default path.
writeKey(required)The key generated by sprout init, from EXPO_PUBLIC_SPROUT_WRITE_KEY.
storageAsyncStorageCustom queue persistence; must implement getItem/setItem.
flushIntervalMs10_000How often the queue flushes to the ingest endpoint.
maxBatchSize50Max events sent per flush.
maxQueueSize500Oldest events are dropped past this, to bound on-device storage.
sessionTimeoutMs1_800_000 (30 min)Foreground gap after which a new session starts.
onInternalErrornoneCalled on SDK-internal failures (storage, serialization, dropped batches); never thrown into your app.

Dashboard

bunx sprout dashboard

Run this from your app's root (same directory as .env/.env.local) so it can find EXPO_PUBLIC_CONVEX_URL. It reads SPROUT_SLUG and SPROUT_DASHBOARD_KEY from the shell environment, or prompts for them if they're missing, then serves a prebuilt dashboard UI on http://127.0.0.1:4321 and opens it in your browser.

FlagDefaultDescription
--port4321Port to serve on.
--host127.0.0.1Bind address. Anything other than localhost prompts a confirmation first, since there's no per-user auth, only the shared dashboard key.
--convex-url <url>discovered from .env/.env.localOverrides EXPO_PUBLIC_CONVEX_URL discovery.
--slug <slug>SPROUT_SLUG env var, or promptedApp slug registered with Sprout.
--dashboard-key <key>SPROUT_DASHBOARD_KEY env var, or promptedMust match what npx convex env set SPROUT_DASHBOARD_KEY stored on your deployment.
--no-openopens automaticallySkip auto-opening a browser tab.

If it can't reach your deployment (misconfigured key, nothing registered yet), it falls back to a clearly-labeled sample dataset instead of erroring, so you can still see what the dashboard looks like before wiring up real data.

Keep this on localhost. SPROUT_DASHBOARD_KEY ships to the browser bundle under this model, so treat it like a database console for your analytics, not something to deploy publicly.

Rate limiting & kill switch

Re-run registration any time to change an app's ingest settings, without touching existing data:

npx convex run sproutSetup:register '{"writeKey":"<existing write key>","ingestEnabled":false}'
  • ingestEnabled: false shuts off ingest immediately (e.g. a leaked write key). The server returns 403; the SDK backs off and keeps events queued on-device rather than dropping them, so turning ingest back on later resumes delivery instead of losing data.
  • maxEventsPerHour: <n> caps ingest volume; requests past the cap get 429 and are handled the same way (queued, backed off, retried).

Both are optional and additive to the one-time call from Quick start.

Full package README (same content, kept as the package's own docs page on npm): packages/analytics/README.md.

License

Apache-2.0

Keywords