Skip to content
Self-hosted · Analytics and crash tracking

Insights.

Product analytics and crash tracking with consent built in.

A Haskell server over gRPC, PostgreSQL underneath, Grafana as the whole frontend. SDKs for Flutter, TypeScript and Haskell that persist nothing before consent and never break your app. Funnels, retention, cohorts, crash-free rates, a consent ledger and a generated privacy inventory, on your own machine.

MIT licensed. One NixOS module.

Why this exists

Hosted analytics asks you to ship your users' behaviour to someone else and to bolt consent on afterwards. Insights turns that around: consent is the first thing the SDK knows, the server refuses what was not granted, and the data stays in a database you own and can read with plain SQL.

What you get

The whole loop, from event to dashboard.

Everything a product team asks a tracker for, and the parts a privacy officer asks for, in one deployment.

01

Events

Dynamic events with typed properties, sessions and identity merge. Anonymous first, identified later, history intact.

02

Funnels, retention, cohorts

Declared next to the project in configuration, down to the property values an event must carry and the numbers to sum. Each declaration becomes a reporting view and a panel in the project's own dashboard; nothing is written in SQL.

03

Crash tracking

Fingerprinted issues with open, resolved and regressed states, crash-free users and sessions, breadcrumbs from the session's own events.

04

Consent ledger

Every grant and withdrawal recorded with its policy version. Two purposes: analytics and diagnostics.

05

Erasure

In-app erasure scoped to the device and its linked person, plus an operator function for requests that arrive by other means.

06

Inventory and manifest

PRIVACY.md and the Apple privacy manifest are generated from the contract, so a field cannot be collected undeclared.

07

Everything in Grafana

Eight shared dashboards, four alert rules, and a generated dashboard per environment in its own folder, on a vanilla Grafana with the built-in PostgreSQL datasource.

08

One NixOS module

The server, PostgreSQL with peer authentication, and Grafana provisioning. Projects are declared next to the products they measure, and ingest keys are generated by the service.

How it works

Four parts. One contract.

The proto under proto/peculiar/insights/v1 is the single source of truth for every language. Generated messages travel unchanged into the SDKs, and every collected field carries its data category and consent purpose.

SDKs send consented batches to the ingest server, which stores them in PostgreSQL, which Grafana readsSDKsFlutter · TS · HaskellIngest servergRPC · gRPC-Web · ConnectPostgreSQLpartitioned, reporting schemaGrafanadashboards · alertsconsent snapshot on every batchrefuses unconsented itemsread-only reporting role

01

The SDK batches.

Client-generated UUIDs, a sent-at timestamp for clock skew, and the consent snapshot on every batch. At least once, exactly once in effect.

02

The server decides.

Validation, consent enforcement, deduplication, identity merge and crash grouping, item by item, with a per-item outcome.

03

PostgreSQL keeps it.

Monthly partitions, per-project retention, a reporting schema of views and functions, and admin functions for triage and erasure.

04

Grafana shows it.

A read-only role over the reporting schema, provisioned dashboards and alerts, no community plugins.

SDKs

One vocabulary in four places.

Two objects everywhere: Insights is the process, Tracker records. A tracker is an immutable value; with (with_ in Dart) derives a child whose properties fold into everything it records.

starttrackersubjectwithtrackidentifyresetpeoplerecordErrorattemptlogspanflushcloseconsenterasediagnostics

Flutter

final insights = await FlutterInsights.start(
  url: Uri.parse("https://insights.example.org"),
  key: key,
  consent: const ConsentPolicy.ask(),
);

final checkout = insights.tracker.with_({"screen": "checkout"});
await checkout.track("order_placed", {
  "total": 42.5,
  "items": 3,
});

Haskell

withInsights config \insights -> do
  let tracker = insights.subject subject consent
  track tracker "order_placed"
    ["total" =: 42.5, "items" =: (3 :: Int)]
  attempt tracker (placeOrder order)

Flutter

iOS, Android, desktop and web. Error capture, lifecycle sessions, a durable queue, and an Apple privacy manifest shipped in the plugin.

TypeScript in the browser

gRPC-Web, IndexedDB queue, unhandled error capture, Global Privacy Control honoured as a denial.

TypeScript in Node

Native gRPC, file-backed queue, process error capture, per-request trackers for the users a backend serves.

Haskell

Records of functions for the application's Env, per-request trackers with the consent the backend holds, a stub server for tests.

How it is built

Principles, not promises.

P1

Consent before anything.

SDKs persist nothing and open no connection until a purpose is granted. A denial sends nothing.

P2

The server refuses unconsented data.

Every batch carries a consent snapshot. A defective client cannot store what was not granted.

P3

Configuration is declared.

Projects, retention, cohorts and analytics are configuration; keys are generated. Triage and erasure are Grafana actions only its Admins can take. There is no admin UI of its own to secure.

P4

Nothing hidden from the operator.

PostgreSQL holds the data, Grafana shows it, the migrations are readable SQL. No black box in the middle.

P5

Recording never breaks the app.

A recording call never throws in release. Problems flow into one diagnostics stream.

P6

MIT, and yours to run.

Self-hosted on your machine, your database, your dashboards. Easy to leave, nothing to export.

Start

Deploy it, then hand the guide to an agent.

Read the docs

Deploying with NixOS, the Grafana dashboards, operations, the consent model, and a document written for an agent to integrate the SDKs into your product.