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 bash

Point 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.

1. Clone and configure
git clone https://github.com/TableTennisCoder/bananalytics.git
cd bananalytics/server
cp .env.example .env

Set 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=*
2. Start it
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build

The 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's apiKey option). 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

MinimumRecommended
CPU1 vCPU2 vCPU
RAM2 GB (add swap)4 GB
Disk20 GB SSD40 GB SSD
OSAnything 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

ServicePortWhat it does
caddy80, 443 (public)HTTPS reverse proxy. Routes /v1/*, /health to the backend; everything else to the dashboard. Auto-provisions Let's Encrypt certificates.
dashboard3000 (internal)Next.js admin UI. Login, project management, funnels, breakdowns, revenue, retention, geography, settings.
bananalytics8080 (internal)Go API server. Event ingestion (/v1/ingest), queries (/v1/query/*), auth (/v1/auth/*), GeoIP enrichment, rate limiting.
postgres5432 (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 bash

Run 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 .env

Two 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.com

Native 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.sh

See GeoIP Setup for the license key.

4. Build and start

docker compose up -d --build

The 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/setup

Register 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-backup

Restoring 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 --build

Database 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:

ServiceIdleUnder load
Ubuntu base + sshd~300 MB~300 MB
Docker daemon~100 MB~100 MB
PostgreSQL 16~23 MBup 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_RPM defaults 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:

ViewResponse
Live14 ms
Overview273 ms
Breakdown~450 ms
Geography489 ms
Revenue512 ms
Events over time752 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 storedApprox 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 stageHetzner boxCost / moNotes
MVP — first 1K usersCX22 · 2/4/40€4.756+ months runway
10K–50K MAUCX32 · 4/8/80~€7More disk runway, smoother queries
50K–200K MAUCX42 · 8/16/160~€15Better Postgres caching, big-query headroom
200K+ MAUDedicated 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

Quick health check
# 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:

macOS / Linux
export MAXMIND_LICENSE_KEY=your_key_here
./scripts/download-geoip.sh
Windows (PowerShell)
$env:MAXMIND_LICENSE_KEY = "your_key_here"
.\scripts\download-geoip.ps1

The 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 bananalytics

You 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:

Cron — first Sunday of every month, 3 AM
0 3 1-7 * 0 cd /path/to/bananalytics/server && \
  MAXMIND_LICENSE_KEY=xxx ./scripts/download-geoip.sh && \
  docker compose restart bananalytics

Privacy 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

VariableDefaultDescription
BANANA_DB_PASSWORDrequiredPostgres password. No default — compose refuses to start without it. Only read when the data directory is first created
BANANA_CORS_ORIGINSrequiredAllowed browser origins, comma-separated. Native apps send no Origin and are unaffected
BANANA_DOMAINlocalhostDomain Caddy provisions the TLS certificate for
BANANA_DB_USERbananalyticsPostgres user
BANANA_DB_NAMEbananalyticsDatabase name
BANANA_DB_DSNderivedConnection string. Built from the three values above by docker-compose
BANANA_PORT8080HTTP server port
BANANA_LOG_LEVELinfodebug, info, warn, error
BANANA_RATE_LIMIT_RPM1000Requests/min per API key
BANANA_IP_RATE_LIMIT_RPM300Requests/min per IP
BANANA_DB_MAX_CONNS25Max DB connections
BANANA_ROLLUP_INTERVAL60sHow often aggregates rebuild
BANANA_RAW_RETENTION_MONTHS0Months of raw events to keep; 0 keeps them forever
BANANA_GEOIP_DBPath to GeoLite2-City.mmdb
MAXMIND_LICENSE_KEYUsed by scripts/download-geoip.sh
BANANA_BACKUP_DIR./backupsWhere scripts/backup.sh writes dumps
BANANA_BACKUP_RETENTION_DAYS14How long local dumps are kept
BANANA_BACKUP_REMOTErclone 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.

Install
npm install @bananalytics/react-native
npm install @react-native-async-storage/async-storage
npm install react-native-get-random-values

react-native-get-random-values polyfills crypto.getRandomValues() for the uuid dependency. Without it, Bananalytics.init() crashes on first run. See Troubleshooting for details.

Initialize
import { Bananalytics } from '@bananalytics/react-native';

Bananalytics.init({
  apiKey: 'rk_your_write_key',
  endpoint: 'https://your-server.com',
  debug: true,
});
Track events
// 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

OptionDefaultDescription
apiKeyrequiredWrite-only API key
endpointrequiredBackend URL
flushInterval30000Auto-flush interval (ms)
flushAt20Events before auto-flush
maxQueueSize1000Max events in memory
maxRetries3Retry attempts
debugfalseConsole logging
trackAppLifecycletrueAuto-track foreground/background
sessionTimeout1800000Session 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
Install
npm install react-native-get-random-values
2. 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.

src/app/_layout.tsx
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():

src/lib/analytics.ts
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. Use Bananalytics.track / screen / identify directly. No provider, no hooks from the SDK.
  • Provider: wrap your tree with <BananalyticsProvider> and use useBananalytics() / 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_opened

Distinguish first launch from returning users

first_open: boolean

signup_started

See which auth methods convert best

method: 'email' | 'google' | 'apple'

signup_completed

Measure signup friction. Funnel: started → completed

method, time_to_complete_ms

onboarding_step_viewed

Find which onboarding step loses users

step: number, step_name: string

onboarding_completed

Track activation rate

steps_completed: number

Core Product Usage

Track the actions that define your product's value. These power retention cohorts.

feature_used

See which features drive retention

feature: string

content_viewed

Understand what users engage with

content_id, content_type, source

search_performed

Discover unmet needs (zero-result searches)

query, results_count

item_created

Measure creation activity as engagement signal

item_type, item_id

share_tapped

Track organic virality loops

content_type, share_method

Revenue & Conversion

Track the money path. Build funnels from browse to purchase to optimize conversion.

product_viewed

Top of the purchase funnel

product_id, price, category

add_to_cart

Mid-funnel intent signal

product_id, quantity, price

checkout_started

High-intent moment — track abandonment

cart_value, item_count

purchase_completed

Revenue tracking. Compare to checkout_started for drop-off

order_id, total, currency, items

subscription_started

SaaS conversion tracking

plan, price, trial: boolean

subscription_cancelled

Understand churn reasons

plan, reason, days_active

Engagement & Retention Signals

These events feed your retention heatmap and help predict churn.

session_started

Session count per user = engagement health

(auto-tracked)

notification_received

Measure push notification effectiveness

type, campaign_id

notification_tapped

Tap rate = notification quality signal

type, campaign_id

rating_prompted

Optimize when to ask for reviews

days_since_install

rating_submitted

Track app store rating health

stars, days_since_install

Errors & Friction

Track where users hit walls. These often reveal the biggest conversion opportunities.

error_occurred

Surface bugs that affect real users

error_code, screen, message

payment_failed

Lost revenue you can recover

error_type, retry_count

form_abandoned

Find the field that kills your form

form_name, last_field_filled

permission_denied

Users refusing permissions = feature blockers

permission_type

Using Events with Dashboard Features

Dashboard FeatureEvents to TrackInsight You Get
Funnelssignup_started → signup_completed → first_purchaseWhere users drop off in your conversion flow
RetentionAny recurring action (session_started, feature_used)How many users come back on day 1, 7, 30
Live ViewAll events in real-timeVerify tracking works, monitor launches & campaigns
GeographyAll events (geo is extracted from IP)Where your users are, localization priorities
Sessionssession_started + any user-identified eventsDebug individual user journeys

Best Practices

  • Use past tense for event namespurchase_completed not purchase. 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-creates screen_view events, 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.
Complete example: e-commerce app
// 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/events
rk_* — Write Key (ingestion)sk_* — Secret Key (queries)

Error Codes

StatusCodeDescription
400BAD_REQUESTInvalid request body or parameters
400VALIDATION_FAILEDEvent validation failed
401UNAUTHORIZEDMissing or invalid API key
413PAYLOAD_TOO_LARGERequest body exceeds 5MB
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORServer 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