# Sprout Analytics
Insights on how your React Native Expo app is used, with none of the infra.
- Package: @bijectionhq/sprout-analytics
- Category: Integrations
- Status: Preview
- License: Apache-2.0
- Bijection source (repository access required): https://github.com/bijectionhq/sprout-analytics/tree/232ade98230e1789388d57787278684d1d441a33/packages/analytics
- Upstream: https://github.com/ambjn/sprout/tree/d234c5483457d9188ae36a3bcefad8907b429019/packages/analytics
- Documentation: https://docs.bijection.com/components/using
- Directory: https://bijection.com/components/sprout-analytics
## Get the source
Bijection repository access is required. Request access at https://bijection.com/contact.
```sh
git clone https://github.com/bijectionhq/sprout-analytics.git
cd sprout-analytics
git checkout 232ade98230e1789388d57787278684d1d441a33
cd packages/analytics
```
## Before using this component
Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute.
## Upstream package documentation
Recorded 2026-10-23. This describes @sprout-convex/analytics; check the Bijection adaptation separately.
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
- 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
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 README
```
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ ~
~ _ ~
~ ___ _ __ _ __ ___ _ _| |_ ~
~ / __| '_ \| '__/ _ \| | | | __| ~
~ \__ \ |_) | | | (_) | |_| | |_ ~
~ |___/ .__/|_| \___/ \__,_|\__| ~
~ |_| ~
~ ~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```
# @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](https://www.convex.dev/components)
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 ` 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`](./packages/analytics/README.md).
## Monorepo commands
Run from the repo root:
```sh
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](https://www.convex.dev/components): 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](https://www.convex.dev) 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
```sh
bun add @sprout-convex/analytics
```
## Quick start
From your app's root (a Convex + Expo project with `bunx convex dev` already
set up):
```sh
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.
What it generates, for reference
```ts
// 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;
```
```ts
// 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;
```
```ts
// 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 }),
});
```
```tsx
// 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
```tsx
import { track } from "@sprout-convex/analytics";
const UpgradeButton = ({ planId }: { planId: string }) => (
{
track("upgrade_tapped", { planId, source: "settings" });
router.push("/upgrade");
}}
>
Upgrade
);
```
### `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.
```tsx
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:
```tsx
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:
```tsx
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:
```tsx
import { captureException } from "@sprout-convex/analytics";
class ScreenErrorBoundary extends React.Component {
componentDidCatch(error: unknown, info: React.ErrorInfo) {
captureException(error, { properties: { componentStack: info.componentStack } });
}
// ...
}
```
### Advanced: `getSprout` / `shutdownSprout`
```tsx
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`:
| Option | Default | Description |
|---|---|---|
| `convexSiteUrl` | *(required, unless `ingestUrl` set)* | Your Convex deployment's site URL (`https://.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
```sh
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 ` | discovered from `.env`/`.env.local` | Overrides `EXPO_PUBLIC_CONVEX_URL` discovery. |
| `--slug ` | `SPROUT_SLUG` env var, or prompted | App slug registered with Sprout. |
| `--dashboard-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:
```sh
npx convex run sproutSetup:register '{"writeKey":"","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: ` 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](#quick-start).
Full package README (same content, kept as the package's own docs page on
npm): [`packages/analytics/README.md`](./packages/analytics/README.md).
## License
Apache-2.0