Documentation
Everything you need to set up and use Bananalytics Analytics
How are you running Bananalytics?
Quick Start
One command on a server you own. It installs Docker if it is missing, asks for your domain, generates a database password, pulls the images and starts everything behind HTTPS.
curl -fsSL https://bananalytics.xyz/install.sh | sudo bashPoint your domain's A record at the server first — Caddy needs it in place to obtain a TLS certificate, and the installer says so plainly if it is not there yet. It finishes by printing your URL; open it and you land on /setup. No domain yet, a server that already runs nginx, upgrades, uninstall — all of that is under Self-Hosting.
Or run it on your own machine
To try it locally, or to work on Bananalytics itself, skip the installer and drive Compose directly.
git clone https://github.com/TableTennisCoder/bananalytics.git
cd bananalytics/server
cp .env.example .envSet the two required values in .env. There are no defaults for them, so Compose stops with a message naming the missing one rather than starting up insecure. Locally, * is a fine CORS value:
BANANA_DB_PASSWORD=$(openssl rand -hex 24)
BANANA_CORS_ORIGINS=*docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --buildThe dev overlay publishes the API on 8080 and the dashboard on 3000 and skips HTTPS, which you do not want in production — plain docker compose up -d puts everything behind Caddy instead. Postgres and the Go server start together and database migrations apply automatically — no manual SQL. Verify with docker compose logs bananalytics — you should see migrations: applied successfully and server starting port=8080.
Then, whichever way you got here
1. Create your admin account.Open your server's URL, or http://localhost:3000 if you started it locally. The first time you visit, you'll be redirected to /setup to register the first admin user (name, email, password). This page is one-time only — once an admin exists, it returns 410 Gone and everyone has to sign in via /login.
2. Create your first project. After signup you're dropped into the dashboard. Click "New Project" (or use the project switcher in the topbar), give it a name, and submit. You'll immediately see two keys:
rk_…— write key. Goes into your React Native app (the SDK'sapiKeyoption). Used for ingesting events. Safe to ship in the bundle.sk_…— secret key. Used for querying your data via the API (e.g./v1/query/events). The dashboard stores it server-side per session — never expose it in client code.
Copy both with the buttons in the modal. You can always re-view and rotate them later from Settings → API Keys on any project.
3. Drop the write key into your app. Jump to the React Native SDK section below — install the package, paste your rk_… key, and you're tracking events.
Until the first event arrives the dashboard says so, and shows those same two lines already filled in with your key and your server's address. It swaps itself for the real dashboard the moment something lands.
Self-Hosting
Four containers on one machine, started with a single command. Caddy is the only thing exposed to the internet; everything else lives on the internal Docker network.
Server requirements
| Minimum | Recommended | |
|---|---|---|
| CPU | 1 vCPU | 2 vCPU |
| RAM | 2 GB (add swap) | 4 GB |
| Disk | 20 GB SSD | 40 GB SSD |
| OS | Anything that runs Docker Engine 24+ with the Compose plugin | |
The three containers idle at about 60 MB on an empty database. Under load the number is set almost entirely by Postgres, which grows its page cache to fill whatever you give it — measured at 3 GB with 4.2 million events stored. Give the box 8 GB and it will use it well; on 4 GB it still works, it just reads more from disk. Budget about 1 GB of disk per million events, and see Capacity & Scaling before you grow.
Before you start
- Docker Engine + Compose plugin installed, and your user able to run
docker. - A domain with an A record already pointing at the server. Caddy cannot obtain a certificate before DNS resolves, so do this first and verify with
dig analytics.yourdomain.com +short. - Ports 80 and 443 reachable from the internet. Nothing else needs to be open — the database, API and dashboard are never published to the host.
What you're deploying
| Service | Port | What it does |
|---|---|---|
| caddy | 80, 443 (public) | HTTPS reverse proxy. Routes /v1/*, /health to the backend; everything else to the dashboard. Auto-provisions Let's Encrypt certificates. |
| dashboard | 3000 (internal) | Next.js admin UI. Login, project management, funnels, breakdowns, revenue, retention, geography, settings. |
| bananalytics | 8080 (internal) | Go API server. Event ingestion (/v1/ingest), queries (/v1/query/*), auth (/v1/auth/*), GeoIP enrichment, rate limiting. |
| postgres | 5432 (internal) | PostgreSQL 16. Users, projects, sessions and the partitioned events table. Migrations apply automatically when the backend starts. |
The installer, in full
The one command from Quick Start covers the common case. Everything it can be told to do differently is here.
curl -fsSL https://bananalytics.xyz/install.sh | sudo bashRun the same command again later to upgrade; it never overwrites your configuration or data. For automation, pass --domain analytics.example.com --yes after bash -s --. To remove it again, --uninstall.
No domain yet?The installer offers your server's IP instead. That still gets you an encrypted connection — Caddy issues its own certificate — but your browser warns once, because no public authority can vouch for an IP address. Re-run with --domain later to switch.
Server already hosting something? Caddy needs ports 80 and 443, so the installer stops before changing anything if nginx or Apache holds them. Install behind what is already there with --behind-proxy 9000: it then listens on 127.0.0.1:9000 over plain HTTP and your existing proxy forwards a hostname to it and terminates TLS. A fresh server is simpler, but it is not required.
Prefer to read a script before running it as root? Sensible — here it is. The manual steps below do the same thing by hand.
1. Clone the repo
sudo mkdir -p /opt/bananalytics
sudo chown $USER:$USER /opt/bananalytics
cd /opt/bananalytics
git clone https://github.com/TableTennisCoder/bananalytics.git .2. Configure
cd /opt/bananalytics/server
cp .env.example .envTwo values are required and have no defaults — Compose refuses to start without them rather than falling back to something insecure. Generate the password with openssl rand -hex 24 and keep it hex or alphanumeric, since it goes into a connection URL.
# Required
BANANA_DB_PASSWORD=your-generated-password
BANANA_CORS_ORIGINS=https://analytics.yourdomain.com
# Domain Caddy provisions the certificate for
BANANA_DOMAIN=analytics.yourdomain.comNative mobile apps send no Origin header, so BANANA_CORS_ORIGINS only needs the origins that call the API from a browser. See Configuration for every variable.
3. (Optional) GeoIP database
Without it the geography page and the globe stay empty; nothing else is affected. Put your MaxMind key in .env and run:
./scripts/download-geoip.shSee GeoIP Setup for the license key.
4. Build and start
docker compose up -d --buildThe first build takes ~3–4 minutes. Then check:
docker compose ps
docker compose logs --tail 50
# Look for:
# bananalytics → "migrations: applied successfully"
# bananalytics → "server starting port=8080"
# dashboard → "Ready in Xms"
# caddy → "certificate obtained successfully"5. Claim your instance — do this immediately
/setup is publicly reachable until the first admin exists. Whoever reaches it first owns the instance, so open it as soon as the stack is up:
https://analytics.yourdomain.com/setupRegister your admin user, then create a project and copy its keys. The secret key is shown once. From then on /setup returns 410 Gone permanently.
6. Schedule backups
Nothing backs up on its own. The repo ships a dump script — schedule it, and point BANANA_BACKUP_REMOTE at an rclone remote, because a copy on the same disk as the database does not survive losing that disk.
# crontab -e
30 3 * * * cd /opt/bananalytics/server && ./scripts/backup.sh 2>&1 | logger -t bananalytics-backupRestoring replaces the database with a dump: ./scripts/restore.sh backups/bananalytics-….sql.gz. Test it once on a throwaway machine before you ever need it.
Deploying updates
cd /opt/bananalytics
git pull
cd server
docker compose up -d --buildDatabase migrations run automatically when the backend starts, so there is never a separate migration step. ~30 seconds for code-only changes, ~3 minutes when dependencies changed.
Realistic time: ~10 minutes, most of it the first Docker build. No manual database creation, no migration scripts, no certificates to renew.
Capacity & Scaling
Bananalytics is designed to run lean. The Go backend is a single static binary, Postgres uses a partitioned events table, and the SDK batches events to keep network traffic minimal. The result: you can run the entire stack — including your Next.js dashboard — on a $5 server for a long time before you need to scale up.
What's actually using your RAM
On a typical Hetzner CX22 (2 vCPU, 4 GB RAM) running everything co-located, here's the memory budget at idle vs. modest load:
| Service | Idle | Under load |
|---|---|---|
| Ubuntu base + sshd | ~300 MB | ~300 MB |
| Docker daemon | ~100 MB | ~100 MB |
| PostgreSQL 16 | ~23 MB | up to 3 GB |
| bananalytics (Go) | ~8 MB | ~13 MB |
| Next.js dashboard | ~30 MB | ~30 MB |
| Total | ~460 MB | ~3.4 GB |
Go and Next.js are rounding errors. Postgres is the whole budget, and it will use whatever cache you give it — which is what you want, because that cache is why queries stay fast.
Event throughput
Measured against the real stack, not estimated. Ingest is limited by rate limiting long before it is limited by the database:
- With default rate limits:~500 events/second. This is a policy, not a ceiling —
BANANA_IP_RATE_LIMIT_RPMdefaults to 300 requests per minute per IP, which at 100 events per batch works out to exactly that. Mobile SDKs each send from their own IP, so this only binds if you proxy through one server. - Actual capacity, 2 cores: ~2,650 events/second
- Actual capacity, 12 cores: ~3,700 events/second while dashboard queries run at the same time, zero failures
Postgres, not the Go server, is what saturates first — under load it took 11 of 12 cores while the backend used half of one.
Dashboard query speed
Queries read pre-aggregated daily rollups rather than scanning the raw event stream, so response time tracks the number of distinct people, not the number of events. Measured on 4.2 million events across a 90-day window:
| View | Response |
|---|---|
| Live | 14 ms |
| Overview | 273 ms |
| Breakdown | ~450 ms |
| Geography | 489 ms |
| Revenue | 512 ms |
| Events over time | 752 ms |
| Funnels | ~4 s |
Funnels are the exception: they need per-person event sequences, which no daily aggregate can express, so they still read raw events. The same is true of breakdowns on a custom properties path.
Aggregates refresh every 60 seconds by default (BANANA_ROLLUP_INTERVAL), so dashboard numbers can be up to a minute behind. The Live view always reads raw events and is never stale.
Disk is the real limit
Measured at 4.2 million real events with a full SDK payload, one row costs 1,084 bytes— 819 bytes of data plus 265 bytes spread across the indexes. Including WAL and free space:
| Events stored | Approx disk used |
|---|---|
| 1 million | ~1.2 GB |
| 10 million | ~12 GB |
| 30 million | ~37 GB — a 40 GB disk is full |
| 50 million | ~62 GB — needs a bigger box or a volume |
Practical capacity of a CX22:~32 million events on its 40 GB disk. Query speed is no longer what limits you — the rollups took care of that — so this is genuinely a disk number. If you average 50 events per active user per day:
- 1K MAU → ~50K events/day → years of headroom
- 10K MAU → ~500K events/day → ~2 months on disk
- 100K MAU → ~5M events/day → ~6 days on disk — needs upgrade
Letting old raw events expire
Disk is the thing that runs out, and you can decide how much of it you spend. Set BANANA_RAW_RETENTION_MONTHS and whole monthly partitions older than that window are dropped automatically — a table drop, not a delete, so it finishes in milliseconds and leaves nothing to vacuum.
The aggregates are not touched, so this is not simply throwing data away:
- Keep their full history: overview, events over time, top events, breakdowns, geography, revenue, DAU/WAU/MAU
- Only reach back as far as the window: retention cohorts, funnels, sessions, the event explorer
Six months keeps day-30 cohorts and quarterly funnels comfortably intact while cutting two years of storage by about a third. The default is 0— keep everything forever — because software running on your own server should not delete your data because of a setting you never chose. The minimum is 2 months, and nothing is ever dropped before the rollups covering it have been built.
There is no undo. Keep your backups.
Recommended specs by app stage
| App stage | Hetzner box | Cost / mo | Notes |
|---|---|---|---|
| MVP — first 1K users | CX22 · 2/4/40 | €4.75 | 6+ months runway |
| 10K–50K MAU | CX32 · 4/8/80 | ~€7 | More disk runway, smoother queries |
| 50K–200K MAU | CX42 · 8/16/160 | ~€15 | Better Postgres caching, big-query headroom |
| 200K+ MAU | Dedicated DB box | ~€30+ | Split Postgres onto its own VM via private network |
When to upgrade
Watch for these signals:
- Disk > 70% full → attach a Hetzner Volume (€0.044/GB/mo, separate block storage) and mount it on
/var/lib/docker/volumes/server_pgdata. Cheaper than upgrading the whole VM. - Postgres queries > 2s on the dashboard → bump to CX32 (more shared_buffers cache hits).
- Ingest p99 latency > 100ms → add CPU; or move ratelimit/auth caches to Postgres-backed store and load-balance two backends behind your reverse proxy.
Monitoring commands
# Container resource usage (run on the server)
docker stats
# Disk usage
df -h
docker exec server-postgres-1 psql -U bananalytics -d bananalytics \
-c "SELECT pg_size_pretty(pg_database_size('bananalytics'));"
# Slow queries (last 24h)
docker compose logs bananalytics --since 24h | grep -i "duration_ms.*[0-9]\{4,\}"Tip: Hetzner lets you resize the VM with the data volume intact — ~30 seconds of downtime. No reason to over-provision now. Start small, grow as needed.
GeoIP Setup
Bananalytics uses MaxMind's free GeoLite2 database to map user IP addresses to countries and cities. This powers the 3D globe, the geography dashboard, and the "Top Country" KPI on your overview.
Without this setup, all geo features will be empty. The server runs fine — it just won't enrich events with location data. Lookups happen locally on your server, so no IP data ever leaves your infrastructure.
1. Get a free MaxMind license key
Sign up at maxmind.com/en/geolite2/signup (free, takes 30 seconds). After confirming your email, go to "My License Key" → "Generate new license key".
2. Download the database
The repository ships with a download script. Run it in the server/ directory:
export MAXMIND_LICENSE_KEY=your_key_here
./scripts/download-geoip.sh$env:MAXMIND_LICENSE_KEY = "your_key_here"
.\scripts\download-geoip.ps1The script downloads ~70 MB to ./geoip/GeoLite2-City.mmdb. That directory is already mounted into the Docker container and gitignored.
3. Restart the server
docker compose restart bananalyticsYou should see GeoIP database loaded in the logs (instead of GeoIP disabled). All new events will now be enriched with country, city, and coordinates.
4. Keep it fresh (optional)
MaxMind updates the database weekly. To stay current, re-run the script monthly — or add a cron job:
0 3 1-7 * 0 cd /path/to/bananalytics/server && \
MAXMIND_LICENSE_KEY=xxx ./scripts/download-geoip.sh && \
docker compose restart bananalyticsPrivacy note: The lookup happens entirely on your server. The MaxMind database is local — no IP addresses are ever sent to MaxMind or any third party. Only the resolved country / city are stored alongside the event.
Configuration
| Variable | Default | Description |
|---|---|---|
| BANANA_DB_PASSWORD | required | Postgres password. No default — compose refuses to start without it. Only read when the data directory is first created |
| BANANA_CORS_ORIGINS | required | Allowed browser origins, comma-separated. Native apps send no Origin and are unaffected |
| BANANA_DOMAIN | localhost | Domain Caddy provisions the TLS certificate for |
| BANANA_DB_USER | bananalytics | Postgres user |
| BANANA_DB_NAME | bananalytics | Database name |
| BANANA_DB_DSN | derived | Connection string. Built from the three values above by docker-compose |
| BANANA_PORT | 8080 | HTTP server port |
| BANANA_LOG_LEVEL | info | debug, info, warn, error |
| BANANA_RATE_LIMIT_RPM | 1000 | Requests/min per API key |
| BANANA_IP_RATE_LIMIT_RPM | 300 | Requests/min per IP |
| BANANA_DB_MAX_CONNS | 25 | Max DB connections |
| BANANA_ROLLUP_INTERVAL | 60s | How often aggregates rebuild |
| BANANA_RAW_RETENTION_MONTHS | 0 | Months of raw events to keep; 0 keeps them forever |
| BANANA_GEOIP_DB | Path to GeoLite2-City.mmdb | |
| MAXMIND_LICENSE_KEY | Used by scripts/download-geoip.sh | |
| BANANA_BACKUP_DIR | ./backups | Where scripts/backup.sh writes dumps |
| BANANA_BACKUP_RETENTION_DAYS | 14 | How long local dumps are kept |
| BANANA_BACKUP_REMOTE | rclone remote for off-site copies |
Changing the database password later: Postgres only reads BANANA_DB_PASSWORD when it initialises an empty data directory. Editing .env afterwards leaves the existing user untouched and the backend then fails to connect — change it in the database too:
docker compose exec postgres psql -U bananalytics -d postgres \
-c "ALTER USER bananalytics WITH PASSWORD 'your-new-password';"AI Setup
Copy the prompt below and paste it into Claude Code, Cursor, Copilot, or any AI coding agent. It will integrate Bananalytics into your React Native app in one run.
Integrate Bananalytics analytics into this React Native / Expo app. Bananalytics is a self-hosted, privacy-first product analytics tool. Follow these steps exactly: ## 1. Install dependencies ...
How it works
- Copy the prompt above into Claude Code, Cursor, or any AI coding agent
- Replace YOUR_WRITE_KEY and YOUR_ENDPOINT with your actual values
- Answer the AI when it asks what events you want to track
- Done — the AI installs the SDK, adds tracking calls, and instruments your app
React Native SDK
Install the SDK in your React Native app.
npm install @bananalytics/react-native
npm install @react-native-async-storage/async-storage
npm install react-native-get-random-valuesreact-native-get-random-values polyfills crypto.getRandomValues() for the uuid dependency. Without it, Bananalytics.init() crashes on first run. See Troubleshooting for details.
import { Bananalytics } from '@bananalytics/react-native';
Bananalytics.init({
apiKey: 'rk_your_write_key',
endpoint: 'https://your-server.com',
debug: true,
});// Track custom events
Bananalytics.track('button_clicked', { button: 'signup' });
// Track screen views
Bananalytics.screen('HomeScreen');
// Identify users — links their earlier anonymous events to them
Bananalytics.identify('user-123', { plan: 'pro' });
// Track revenue (negative amounts record refunds)
Bananalytics.trackRevenue(9.99, 'EUR', { product_id: 'pro_monthly' });
// Flush events immediately
await Bananalytics.flush();React Provider
import { BananalyticsProvider, useBananalytics, useTrackScreen } from '@bananalytics/react-native';
function App() {
return (
<BananalyticsProvider config={{ apiKey: 'rk_...', endpoint: '...' }}>
<HomeScreen />
</BananalyticsProvider>
);
}
function HomeScreen() {
useTrackScreen('HomeScreen');
const bananalytics = useBananalytics();
return (
<Button onPress={() => bananalytics.track('tapped')} title="Tap" />
);
}Configuration Options
| Option | Default | Description |
|---|---|---|
| apiKey | required | Write-only API key |
| endpoint | required | Backend URL |
| flushInterval | 30000 | Auto-flush interval (ms) |
| flushAt | 20 | Events before auto-flush |
| maxQueueSize | 1000 | Max events in memory |
| maxRetries | 3 | Retry attempts |
| debug | false | Console logging |
| trackAppLifecycle | true | Auto-track foreground/background |
| sessionTimeout | 1800000 | Session timeout (ms) |
Troubleshooting
Common issues integrating Bananalytics into a React Native or Expo app, and how to fix them.
crypto.getRandomValues() is not supported
Bananalytics uses uuid internally, which calls crypto.getRandomValues(). That API is not available in React Native by default — without a polyfill, Bananalytics.init() hard-crashes the app.
1. Install the polyfill
npm install react-native-get-random-values2. Import it as the FIRST line of your entry file
The polyfill must run before @bananalytics/react-native is loaded — otherwise uuid resolves before globalThis.crypto is patched. Add it as the very first line of your app entry — src/app/_layout.tsx (Expo Router) or App.tsx (bare RN), before all other imports including CSS.
import "react-native-get-random-values"; // must be first — polyfills crypto.getRandomValues() for uuid
import "@/global.css";
import { useEffect } from "react";
import { Bananalytics } from "@bananalytics/react-native";
Bananalytics.init({
apiKey: 'rk_...',
endpoint: 'https://your-server.com',
});useTrackScreen / useBananalytics throws
The hook-based useTrackScreen() and useBananalytics() exports require <BananalyticsProvider> wrapping the tree. If you use the static Bananalytics.init() approach (recommended), do not import these hooks — calling them without the provider throws.
Instead, define a tiny local hook that wraps Bananalytics.screen():
import { useEffect } from "react";
import { Bananalytics } from "@bananalytics/react-native";
export function useTrackScreen(
name: string,
props?: Record<string, string | number | boolean>,
): void {
useEffect(() => {
Bananalytics.screen(name, props);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [name]);
}Then use it in any screen: useTrackScreen('HomeScreen'). No provider required.
Static API vs Provider — pick one
- Static (recommended for Expo Router): call
Bananalytics.init()once at module scope. UseBananalytics.track / screen / identifydirectly. No provider, no hooks from the SDK. - Provider: wrap your tree with
<BananalyticsProvider>and useuseBananalytics()/useTrackScreen(). Mixing both styles in the same app is what causes most setup bugs — pick one and stick with it.
Event Strategy
A good event strategy is the difference between a dashboard full of noise and one that drives decisions. Here is how to instrument your app to get the most out of Bananalytics.
Core Events You Should Track
These events power the dashboard features and give you a complete picture of user behavior.
Onboarding & Activation
Measure how users get from install to value. Build funnels to find where they drop off.
app_openedDistinguish first launch from returning users
first_open: boolean
signup_startedSee which auth methods convert best
method: 'email' | 'google' | 'apple'
signup_completedMeasure signup friction. Funnel: started → completed
method, time_to_complete_ms
onboarding_step_viewedFind which onboarding step loses users
step: number, step_name: string
onboarding_completedTrack activation rate
steps_completed: number
Core Product Usage
Track the actions that define your product's value. These power retention cohorts.
feature_usedSee which features drive retention
feature: string
content_viewedUnderstand what users engage with
content_id, content_type, source
search_performedDiscover unmet needs (zero-result searches)
query, results_count
item_createdMeasure creation activity as engagement signal
item_type, item_id
share_tappedTrack organic virality loops
content_type, share_method
Revenue & Conversion
Track the money path. Build funnels from browse to purchase to optimize conversion.
product_viewedTop of the purchase funnel
product_id, price, category
add_to_cartMid-funnel intent signal
product_id, quantity, price
checkout_startedHigh-intent moment — track abandonment
cart_value, item_count
purchase_completedRevenue tracking. Compare to checkout_started for drop-off
order_id, total, currency, items
subscription_startedSaaS conversion tracking
plan, price, trial: boolean
subscription_cancelledUnderstand churn reasons
plan, reason, days_active
Engagement & Retention Signals
These events feed your retention heatmap and help predict churn.
session_startedSession count per user = engagement health
(auto-tracked)
notification_receivedMeasure push notification effectiveness
type, campaign_id
notification_tappedTap rate = notification quality signal
type, campaign_id
rating_promptedOptimize when to ask for reviews
days_since_install
rating_submittedTrack app store rating health
stars, days_since_install
Errors & Friction
Track where users hit walls. These often reveal the biggest conversion opportunities.
error_occurredSurface bugs that affect real users
error_code, screen, message
payment_failedLost revenue you can recover
error_type, retry_count
form_abandonedFind the field that kills your form
form_name, last_field_filled
permission_deniedUsers refusing permissions = feature blockers
permission_type
Using Events with Dashboard Features
| Dashboard Feature | Events to Track | Insight You Get |
|---|---|---|
| Funnels | signup_started → signup_completed → first_purchase | Where users drop off in your conversion flow |
| Retention | Any recurring action (session_started, feature_used) | How many users come back on day 1, 7, 30 |
| Live View | All events in real-time | Verify tracking works, monitor launches & campaigns |
| Geography | All events (geo is extracted from IP) | Where your users are, localization priorities |
| Sessions | session_started + any user-identified events | Debug individual user journeys |
Best Practices
- Use past tense for event names —
purchase_completednotpurchase. It is clear that the action happened. - Use snake_case consistently — Bananalytics groups events by name. Mixed casing creates duplicates.
- Keep properties flat —
{ price: 49.99, currency: "USD" }not{ payment: { price: 49.99 } }. Easier to query. - Call
identify()early — As soon as the user logs in. This links anonymous events to a real user for session tracking. - Track screens with
screen()— It auto-createsscreen_viewevents, which powers the top events dashboard and retention. - Start with 10-15 events max — You can always add more. Too many events early on create noise and make dashboards hard to read.
// After user logs in
Bananalytics.identify('user-123', { plan: 'free' });
// Screen views (auto-tracked with useTrackScreen hook)
Bananalytics.screen('HomeScreen');
Bananalytics.screen('ProductScreen');
// Core conversion funnel
Bananalytics.track('product_viewed', {
product_id: 'prod_abc', price: 49.99, category: 'shoes'
});
Bananalytics.track('add_to_cart', {
product_id: 'prod_abc', quantity: 1, price: 49.99
});
Bananalytics.track('checkout_started', {
cart_value: 49.99, item_count: 1
});
Bananalytics.track('purchase_completed', {
order_id: 'ord_xyz', total: 49.99, currency: 'USD'
});
// Engagement signals
Bananalytics.track('search_performed', {
query: 'running shoes', results_count: 24
});
Bananalytics.track('share_tapped', {
content_type: 'product', share_method: 'instagram'
});
// Error tracking
Bananalytics.track('payment_failed', {
error_type: 'card_declined', retry_count: 0
});API Reference
Include your API key in every request:
curl -H "Authorization: Bearer sk_your_secret_key" http://localhost:8080/v1/query/eventsrk_* — Write Key (ingestion)sk_* — Secret Key (queries)Error Codes
| Status | Code | Description |
|---|---|---|
| 400 | BAD_REQUEST | Invalid request body or parameters |
| 400 | VALIDATION_FAILED | Event validation failed |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds 5MB |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Server error |
Privacy & Compliance
Bananalytics is designed with privacy in mind. All data stays on your infrastructure — no third-party services, no data sharing.
- Self-hosted: Data never leaves your server
- Opt-out support: Built-in consent management in the SDK
- PII sanitization: Auto-strips email, phone, SSN from auto-captured events
- No cookies: Uses device storage, not browser cookies
- GDPR-friendly: You control the data, you handle deletion requests