Sprout Analytics
Insights on how your React Native Expo app is used, with none of the infra.
git clone https://github.com/bijectionhq/sprout-analytics.git
cd sprout-analytics
git checkout 232ade98230e1789388d57787278684d1d441a33
cd packages/analyticsBijection 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.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- 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.
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
| Path | What it is |
|---|---|
packages/analytics | The published package. Expo SDK (track/screen/identify/captureException), the Convex component, and the sprout CLI. |
apps/dashboard | Source 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:
| Command | What it does |
|---|---|
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 prebuilt dashboard UI against your Convex deployment on http://127.0.0.1:4321, opening it in your browser. |
sprout help | Prints 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 viaAppState, 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 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 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 devworking). expo-router, for screen autocapture (everything else still works without it; you'd just callscreen()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 underlyingSproutCoreinstance, ornullbeforeinitSproutresolves. An escape hatch for cases the top-leveltrack/screen/identify/captureExceptioncalls don't cover.shutdownSprout()stops the flush timer and unhooksAppState/error listeners, useful on logout or account switch. The on-device queue isn't cleared, so a laterinitSprout()picks up right where it left off.
Configuration
Options accepted by initSprout:
| Option | Default | Description |
|---|---|---|
convexSiteUrl | (required, unless ingestUrl set) | Your Convex deployment's site URL (https://<deployment>.convex.site). Ingest path defaults to /sprout/ingest. |
ingestUrl | derived from convexSiteUrl | Full ingest URL, for overriding the default path. |
writeKey | (required) | The key generated by sprout init, from EXPO_PUBLIC_SPROUT_WRITE_KEY. |
storage | AsyncStorage | Custom queue persistence; must implement getItem/setItem. |
flushIntervalMs | 10_000 | How often the queue flushes to the ingest endpoint. |
maxBatchSize | 50 | Max events sent per flush. |
maxQueueSize | 500 | Oldest events are dropped past this, to bound on-device storage. |
sessionTimeoutMs | 1_800_000 (30 min) | Foreground gap after which a new session starts. |
onInternalError | none | Called 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.
| Flag | Default | Description |
|---|---|---|
--port | 4321 | Port to serve on. |
--host | 127.0.0.1 | Bind 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.local | Overrides EXPO_PUBLIC_CONVEX_URL discovery. |
--slug <slug> | SPROUT_SLUG env var, or prompted | App slug registered with Sprout. |
--dashboard-key <key> | SPROUT_DASHBOARD_KEY env var, or prompted | Must match what npx convex env set SPROUT_DASHBOARD_KEY stored on your deployment. |
--no-open | opens automatically | Skip 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: falseshuts 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