Skip to content

Latest commit

 

History

History
205 lines (159 loc) · 14.9 KB

stats-schema.md

File metadata and controls

205 lines (159 loc) · 14.9 KB

Anonymous usage statistics

Audience: operators of self-hosted Ever Gauzy, Ever Teams, Ever Works, Ever Rec and Ever Traduora installations; privacy and security reviewers. Prerequisites: access to the product's settings as an operator (Gauzy: super admin; Works: platform admin; Rec: an admin listed in the admin module; Traduora: an address in TR_EVER_ADMIN_EMAILS); shell access if you want to change environment variables.

The statistics module (ever-stats) sends one small, signed, anonymous report per day so the maintainers can learn which versions and features are in use. It is on by default, it can be turned off in two ways, you can see every byte it sent, and it can never carry your business data: the report format is a closed schema and every string in it must match an allow-list.


1. What is sent

One JSON document per day, schema ever.stats.v1, at most 16 KiB, integers only for numbers. Example (Ever Gauzy), generated from the SDK's golden fixture contracts/fixtures/stats/valid/gauzy.json:

{
  "schema": "ever.stats.v1",
  "report_id": "9f1c1d4a-7b2e-4f0a-9d3c-2a6b1e5f8c01",
  "instance_id": "3d2b1a0c-5e4f-4a6b-8c7d-9e0f1a2b3c4d",
  "sent_at": "2026-10-03",
  "module_version": "1.0.0",
  "product": "gauzy",
  "instance_kind": "backend",
  "serves": [
    "gauzy",
    "teams"
  ],
  "version": "0.750.1",
  "channel": "stable",
  "install_source": "self-hosted",
  "country": "ZZ",
  "period": "2026-09",
  "final": true,
  "counts": {
    "tenants": 3,
    "organizations": 4,
    "users": 41,
    "users_active_30d": 30,
    "employees": 27,
    "employees_active": 25,
    "teams": 6,
    "projects": 18,
    "tasks": 912,
    "contacts": 57,
    "integrations_in_use": {
      "github": 1,
      "ever_connect": 0
    }
  },
  "features": {
    "time_tracking": true,
    "invoice": true,
    "payment": true,
    "open_stats": false
  },
  "aggregates": {
    "invoiced_minor": {
      "EUR": 18230055,
      "USD": 950000
    },
    "invoices": 214,
    "payments_minor": {
      "EUR": 17500000
    },
    "payments": 190,
    "hours_tracked_min": 259140
  }
}
Field Meaning
instance_id a random UUID generated by your installation on first boot; not derived from a URL, hostname or licence; you can reset it (see How to see what was sent)
product, instance_kind, serves which product; backend or frontend; which products one API serves (Gauzy + Teams share one backend)
sent_at the UTC date the report was built; no time of day, so a report cannot be matched to a request log line
version, module_version product version; statistics module version; both major.minor.patch only, because a build suffix of a fork (for example 1.2.3-acme-corp) could name a company
channel stable, rc, beta, dev or custom: the builder turns any pre-release or build suffix of the version into one of these
install_source self-hosted unless you set EVER_INSTALL_SOURCE (e.g. partner:<slug>, ever.sh, desktop)
country ZZ (undeclared) unless you set EVER_STATS_COUNTRY; never derived from your tenants' data
period, final the UTC month the counts cover; final: true on the re-send for the previous month (days 1–3)
counts instance-wide totals from the allow-list below (never per tenant, never per record)
features which product feature switches are on (booleans only)
aggregates instance-wide sums per currency in minor units (cents), or plain integers

Every installation sends the same fields whatever its size, a one-person installation and the desktop app's embedded server included; counts and aggregates are empty only for a frontend-only installation (Ever Teams web), which holds no data of its own. Reports are only ever looked at in aggregate: Ever Platform never publishes a figure that covers fewer than 10 installations.

1.1 Allow-list per product

Product counts features aggregates
Gauzy tenants, organizations, users, users_active_30d, employees, employees_active, teams, projects, tasks, contacts, integrations_in_use.<integration> (number of tenants using each integration) every FEATURE_* switch of the product, lower-cased without the FEATURE_ prefix (time_tracking, invoice, payment, …) invoiced_minor.<CCY>, invoices, payments_minor.<CCY>, payments, hours_tracked_min
Teams (web) — — — (version and install source only)
Works users, tenants, organizations, works, works_by_kind.<kind>, agents, missions, teams, fleet_nodes, plugins_enabled app_works_enabled, app_launcher_enabled, dynamic_plugins, deploy_ever_works_enabled, subscriptions_enabled, mcp_enabled deployments, deployments_by_provider.<provider>, runs, credits_consumed
Rec users, workspaces, teams gauzy_mirror, google_login screenshots, videos, storage_mb
Traduora users, projects, terms, translations, locales, project_clients google_login, signups_enabled translations_changed

The schema is published in the SDK (contracts/schemas/ever.stats.v1.json), vendored into every product with a test that fails if the copies differ, and served for reading at GET https://api.ever.co/v1/stats/schema (your installation never fetches it; it uses its bundled copy). It sets additionalProperties: false at every level and a per-product oneOf, so a Gauzy report cannot carry a Works key and no unknown field can be added by accident. The keys of the nested maps (integrations_in_use, works_by_kind, deployments_by_provider) come from closed lists too: a custom integration, kind or provider plugin is counted under other, never under its own name. The platform rejects anything off-schema (422 schema_violation) and drops it.

1.2 What is never included

Names, e-mail addresses, phone numbers, postal addresses, tax or registration numbers, invoice or document contents, per-record amounts, task or project titles, free text of any kind, URLs, hostnames, IP addresses, precise location, per-tenant breakdowns. The only string fields are schema, report_id, instance_id, sent_at, module_version, product, instance_kind, serves[], version, channel, install_source, country, period, each constrained by a pattern or an enumeration. Continuous-integration tests in every product build a report from a database seeded with realistic names and assert that none of them appears in the serialized payload.


2. When it is sent

  • Once per day at a fixed time derived from instance_id (± 10 min), starting 24 h after first boot (10 min after boot if a send is overdue).
  • On days 1–3 of a month, one final: true re-send for the previous month.
  • If the platform is unreachable, answers 5xx or 429, the module retries after 1 h, 4 h, 12 h, then the next day (or later, when Retry-After says so). A report the platform refuses (400, 413, 415, 422) is not sent again until the module is upgraded; 409 key_mismatch means another key holds the instance_id, and only Reset instance identity (a new id and key) resumes reporting. Nothing is queued beyond the last 12 reports kept locally for the Last payload view.
  • Multi-replica deployments elect one sender through a database lease; there is never more than one report per day per installation.

The request: POST <EVER_STATS_API_URL>/v1/stats/reports, no credentials, headers Ever-Stats-Key: <base64url public key> and Ever-Stats-Signature: ed25519=<base64url signature over the exact request body>, both required; Ever-Stats-Key-Id is optional. The signature uses the statistics key, an Ed25519 key pair your installation generates on first boot for statistics only; the platform remembers the public key on first sight so that nobody else can submit reports under your instance_id. It is separate from the key of an Ever Platform connection: the connection key only authenticates the connection's calls and rotates on its own, and rotating it changes nothing here. The statistics key proves continuity, not identity: the platform learns nothing about who you are from it.


3. Defaults

Where Default Who can change it
Self-hosted installation on the operator: environment variable or settings toggle
Ever-operated cloud on, managed by Ever shown read-only ("Managed by Ever Cloud") with the last payload visible
Desktop / Electron builds with an embedded server on (install_source: desktop) the toggle in the desktop settings form

Tenants and organization admins inside a multi-tenant installation cannot change it; it is an instance-level setting.


4. How to disable

4.1 Environment (module not loaded)

Set EVER_STATS_ENABLED=false (Traduora: TR_EVER_STATS_ENABLED=false; Teams web: the server-side EVER_STATS_ENABLED=false) and restart. The module is not loaded: no route, no timer, no request. GET /api/ever-stats/status answers 404.

4.2 Settings page (module loaded, sending off)

Product Where
Ever Gauzy Settings → Anonymous usage statistics (/pages/settings/usage-statistics) → toggle
Ever Teams Settings → Team → Ever Platform → statistics (reflects the Gauzy API's setting)
Ever Works Settings → Ever Platform (/settings/ever-platform) → Usage statistics
Ever Rec Portal → Settings → Ever Platform → Usage statistics
Ever Traduora Ever Platform (admin menu, /ever-platform) → Usage statistics

With the toggle off the scheduler never runs and Send now is disabled; the module makes no request. The state and its reason (env, ui, or cloud-managed) are shown on the page and by GET /api/ever-stats/status. Environment beats the toggle.


5. How to see what was sent

The same settings page shows:

Element What it shows
What is sent a live preview built right now from your database (POST /api/ever-stats/preview); nothing leaves the server
Last payload the exact bytes of the last report as transmitted, with sent_at and the HTTP status (202 accepted, 422 rejected by schema, 409 key mismatch, failed for transport errors); GET /api/ever-stats/last
Send now sends one report immediately (at most once per 10 min); disabled while the toggle is off
Reset instance identity generates a new statistics instance_id and statistics key; the old series can no longer be continued; an Ever Platform connection is not affected (its key is separate)

If the organization that owns the installation's connection to Ever Platform enabled the stats_link integration and you accepted it on the installation, app.ever.co → Instances shows the same last payload for that instance to that organization only. stats_link is never offered on Ever-operated cloud.


6. Reference

Item Value
Endpoint POST {EVER_STATS_API_URL}/v1/stats/reports (default base https://api.ever.co)
Schema ever.stats.v1 — JSON Schema 2020-12, closed, ≤ 16 KiB, integers only
Signature Ed25519 over the exact request body bytes; headers Ever-Stats-Key: <base64url public key> and Ever-Stats-Signature: ed25519=<base64url signature over the exact request body>, both required, including for Ever Teams web; Ever-Stats-Key-Id optional
Platform answers 202 {accepted, superseded?} (the same report, by report_id, sent again the same UTC day: 202 {accepted} and nothing changes); 400 validation_failed (the key header) or 400 signature_invalid (the signature, or a key id that names another key); 409 key_mismatch; 413 validation_failed (too_large); 415 unsupported_media_type; 422 schema_violation with errors[] (path, and code one of unknown_field, type, pattern, range, required, duplicate_key, schema_unknown); 429 rate_limited with Retry-After (more than 24 reports a day, reports for more than two months a day, too many requests or new statistics ids from one source address, or too many new ids that day)
Env EVER_STATS_ENABLED, EVER_STATS_API_URL, EVER_STATS_COUNTRY, EVER_STATS_SERVES (Gauzy), EVER_STATS_SEND_INTERVAL_S (tests), EVER_INSTALL_SOURCE, EVER_INSTANCE_ID and EVER_STATS_PRIVATE_KEY (stateless frontends: set both to keep one series across restarts, or neither for a new random identity per process)
Routes (Gauzy; Works, Rec and Traduora expose the same functions under their own API; Teams web has only status and last) GET /api/ever-stats/status, GET /api/ever-stats/last, POST /api/ever-stats/preview, PUT /api/ever-stats/enabled, POST /api/ever-stats/send-now (at most once per 10 min), POST /api/ever-stats/reset-identity
Retention on the platform reports are stored per instance and month; the source IP is never stored and never used to derive a country; the rate limit keeps a keyed hash of the address in memory only, under a key that rotates hourly

7. Building and signing a report with the SDK

Both SDKs run the platform's checks before anything is sent, so a report the SDK lets through is one the platform accepts, and a refusal names the same field with the same code: the 16 384-byte limit, a strict JSON reader (integers only, no key twice), the published schema (one closed key set per product) and the calendar date of sent_at. Every statistics fixture under contracts/fixtures/stats/ (vendored byte for byte from Ever Platform, with the expected answer of each in expected.json) gets the same answer from the platform, the mock, the TypeScript SDK and the Rust SDK.

TypeScript (@ever-co/connect-sdk):

import { generateStatsKey, sendStatsReport, signStatsReport, statsSignerFromSeed } from '@ever-co/connect-sdk';

const { seed } = generateStatsKey();             // once, on first boot; store the seed as a secret
const signer = statsSignerFromSeed(seed);
const signed = signStatsReport(report, signer);  // throws StatsValidationError: no bytes leave
const outcome = await sendStatsReport(signed, { baseUrl: process.env.EVER_STATS_API_URL ?? 'https://api.ever.co' });
// outcome.kind: 'accepted' | 'retry' (retryAfterS) | 'reset_identity' | 'dropped'

Rust (ever-connect-sdk, feature stats, no HTTP client of its own):

use ever_connect_sdk::stats::{classify_answer, reports_url, sign_report, StatsKey};

let key = StatsKey::from_seed(&stored_seed);
let signed = sign_report(&report, &key, false)?;   // StatsRefusal: no bytes leave
// POST signed.body with signed.headers to reports_url(base_url)? with your HTTP client, then:
let outcome = classify_answer(status, &answer_body, retry_after.as_deref(), attempt);

A refusal's message names fields and codes only; a key the schema does not know is shown as * (the exact path, which is the key itself, stays in its errors). walkStrings / walk_strings list every string of a report, keys included, for the test that no seeded name or e-mail address reaches the payload.