Skip to content

Push hourly usage snapshots to Control Plane #7673

@matthewelwell

Description

@matthewelwell

Summary

Each Flagsmith Instance (Private Cloud or Self-Hosted) should report its own usage to the central Control Plane once an hour. The Control Plane already accepts these reports at POST /v1/usage; this issue is the sending half that
lives inside Flagsmith.

The report is a "usage snapshot": a point-in-time count of seats, projects, and API calls. Flagsmith staff use it to compare what a customer actually uses against what their licence entitles them to.

How it works

A recurring task on the task processor runs hourly. For each licensed organisation on this Instance, the run:

  1. Builds one usage snapshot from that organisation's own data.
  2. Reads the licence signature (OrganisationLicence.signature).
  3. POSTs the snapshot to the Control Plane with the signature as a bearer token.

In this version we expect one licensed organisation per Instance, so most runs send a single snapshot. The loop keeps us correct if there is more than one.

What goes in a snapshot

Both deployment types send:

  • timestamp — the hour the snapshot covers (UTC). We send the most recent complete hour.
  • deployment_typeprivate_cloud or self_hosted (see open question).
  • seat_count — total users in the organisation, dormant included (organisation.num_seats).
  • instance_version — the Flagsmith version running, as plain semver (e.g. 2.140.1). No v prefix, no latest/unknown.

Self-Hosted adds:

  • project_count — non-deleted projects (organisation.projects.filter(deleted_at__isnull=True).count()).

Private Cloud adds:

  • api_call_total — total SDK API calls this period.
  • A breakdown by type: flags, identities, traits, environment_documents.
  • A per-project breakdown: project_id, project_name, api_call_count.

All API-call counts come from app_analytics — reuse analytics_db_service.get_usage_data(), which already supports per-type and per-project filtering (the same source as the GET /organisations/{id}/usage-data/ endpoint).

Sending it

  • Endpoint: POST /v1/usage on the Control Plane (base URL from config).
  • Auth: Authorization: Bearer <base64url(OrganisationLicence.signature)>.
  • Expected responses:
    • 202 — accepted (also returned for a duplicate hour — safe to ignore).
    • 400 — payload rejected. Log it; the snapshot was malformed.
    • 401 — auth failed (bad/expired/revoked licence). Log it.
    • 429 — rate limited. Back off; do not retry aggressively.
    • 5xx — server error. Retry on the next scheduled run, not in a tight loop.

Re-sending the same hour is harmless — the Control Plane drops duplicates by (customer, timestamp). A missed run that catches up next hour is fine.

Configuration

New settings, all optional so existing deployments stay silent until turned on:

  • CONTROL_PLANE_URL — base URL. If unset, the task is a no-op.
  • CONTROL_PLANE_DEPLOYMENT_TYPEprivate_cloud or self_hosted. Needed because the codebase cannot currently tell the two apart (only SaaS / enterprise / OSS are detectable). See open question.

The licence signature is read from the database (OrganisationLicence), so it needs no new setting.

If the Control Plane URL is missing, skip the run quietly — never crash the task processor.

Open question

  • Deployment type. There is no existing flag for Private Cloud vs Self-Hosted (both are non-SaaS; only is_saas() / is_enterprise() / is_oss() exist). The simplest fix is a new CONTROL_PLANE_DEPLOYMENT_TYPE setting per Instance. This may become moot if the payload shape is unified — see Flagsmith/control-plane#15.

Acceptance criteria

  • A recurring task is registered on the task processor and runs hourly.
  • It builds a correct snapshot per licensed organisation and deployment type.
  • It authenticates with the licence signature and POSTs to /v1/usage.
  • It reuses app_analytics for API-call counts (no new counting logic).
  • It handles 202/400/401/429/5xx without crashing and logs failures.
  • It is a no-op when the Control Plane URL is not configured.
  • instance_version is sent as clean semver.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions