openvidu/openvidu-dashboard/openspec/specs/analytics-api/spec.md

19 KiB

analytics-api Specification

Purpose

Usage analytics of the deployment read from the rollup collections: dense time series, breakdowns, KPI summary and CSV export, with the source collection chosen by granularity and every query backed by the { scope, bucket } index.

Requirements

Requirement: Series endpoint

Edition: CE. GET /api/v1/analytics/series SHALL accept metrics (comma-separated, at least one), from, to, tz, granularity and scope (default global) and answer the time-series envelope extended with scope, one series per requested metric named as the metric, where egressEnded expands into one series per stored end status named egressEnded.<status> (egressEnded.complete). Unknown metrics, duplicated metrics and a malformed scope SHALL be rejected with 400.

Scenario: Default request

  • GIVEN the current time is 2026-10-13T12:00:00Z
  • WHEN the client requests GET /api/v1/analytics/series?metrics=roomsCreated,bytesUp
  • THEN the response is 200 with scope: "global", granularity: "5m", from: "2026-10-12T12:00:00.000Z", to: "2026-10-13T12:00:00.000Z" and two series roomsCreated and bytesUp of 288 points each

Scenario: Egress end statuses

  • GIVEN egresses that ended as complete and failed in the range (EGRESS_COMPLETE and EGRESS_FAILED in the LiveKit events)
  • WHEN the client requests metrics=egressEnded
  • THEN the response has the series egressEnded.complete and egressEnded.failed and no series for statuses absent from the range

Scenario: Unknown metric

  • WHEN the client requests metrics=roomsCreated,roomsDeleted
  • THEN the response is 400 with code VALIDATION_ERROR and details.issues[0].path equal to ["metrics"]

Requirement: Metric catalogue and fill values

Edition: CE. The CE metrics SHALL be the eighteen of the "CE metrics" scenario; counters and sums fill empty buckets with 0, rates and averages with null. Participant figures count STANDARD sessions only, except the kind breakdown. ingressSessions SHALL sum the rollups' ingress.sessionsStarted and ingressDurationMs their ingress.durationMs.

Scenario: Connection success rate

  • GIVEN a bucket with participants.joined: 40 and participants.active: 38 and an empty bucket
  • WHEN metrics=connectionSuccessRate is requested
  • THEN the first point is 95.0 (percentage with one decimal) and the empty bucket holds null

Scenario: Participant minutes

  • GIVEN a bucket with participants.minutes: 1234
  • WHEN metrics=participantMinutes is requested
  • THEN the point is 1234 and empty buckets hold 0

Scenario: Services are not participants

  • GIVEN an hour in which 40 STANDARD and 6 EGRESS participants joined
  • WHEN metrics=participantsJoined and the breakdown dimensions=kind are requested
  • THEN the series point is 40 and the breakdown has STANDARD: 40 and EGRESS: 6

Scenario: Ingress metrics on CE

  • GIVEN a valid token on the CE dashboard and one hour of the last 24 h whose hourly document has ingress.sessionsStarted: 3 and ingress.durationMs: 5400000
  • WHEN the client requests metrics=ingressSessions,ingressDurationMs&granularity=1h
  • THEN the response is 200 with the series ingressSessions and ingressDurationMs of 24 points, that hour holds 3 and 5400000 and the other hours hold 0

Scenario: CE metrics

  • WHEN a CE client requests analytics series
  • THEN the metrics it can request are roomsCreated, roomsEnded, participantsJoined, participantsActive, participantsAborted, connectionSuccessRate, uniqueIdentities, participantMinutes, tracksPublished, egressStarted, egressEnded, egressDurationMs, ingressSessions, ingressDurationMs, bytesUp, bytesDown, apiCalls and apiErrors

Requirement: Granularity and source collection

Edition: CE. Analytics series SHALL accept granularity auto, 5m, 15m, 1h, 6h and 1d, reject 1m, read analytics_5min for 5m and 15m, analytics_hourly for 1h and 6h and analytics_daily for 1d, and SHALL never read entity or stats collections. Sub-hour values SHALL require a range of at most 24 h whose from is inside the 5-minute rollup retention. auto SHALL resolve to 5m (≤ 24 h inside that retention), 1h (≤ 7 d), 6h (≤ 90 d), otherwise 1d.

Scenario: Automatic resolution by range

  • WHEN series are requested with granularity=auto for ranges of 24 h, 7 d, 30 d and 100 d
  • THEN the responses use 5m, 1h, 6h and 1d and the integration test observes reads of analytics_5min, analytics_hourly, analytics_hourly and analytics_daily respectively

Scenario: Sub-hour granularity on a long range

  • WHEN the client requests granularity=5m with from=now-48h
  • THEN the response is 400 with code VALIDATION_ERROR and a message stating that 5m and 15m require a range of at most 24 hours

Scenario: Day-old range beyond the stats retention

  • GIVEN the stats and 5-minute rollup retentions are 7 d
  • WHEN the client requests granularity=auto for a 24 h range that ended 30 days ago
  • THEN the response uses granularity: "1h" read from analytics_hourly, and an explicit granularity=5m for the same range is rejected with 400 stating "5m and 15m require a range inside the 5-minute rollup retention (7 days)"

Scenario: 5-minute rollups kept apart from the stats

  • GIVEN a stats retention of 7 d and a 5-minute rollup retention of 2 d
  • WHEN the client requests granularity=auto for a 24 h range that ended 3 days ago
  • THEN the response uses granularity: "1h"

Scenario: Retention named in hours

  • GIVEN a 5-minute rollup retention of 36 h
  • WHEN the client requests granularity=5m for a 24 h range that ended 2 days ago
  • THEN the response is 400 stating "5m and 15m require a range inside the 5-minute rollup retention (36 hours)"

Scenario: Minute granularity and the bucket cap

  • WHEN the client requests granularity=1m for any range, or granularity=1h for a 60-day range
  • THEN both responses are 400, the first stating that the finest analytics granularity is 5m and the second stating the 1 000-bucket limit

Requirement: Hourly-only metrics

Edition: CE. participantMinutes, tracksPublished, egressEnded, egressDurationMs, ingressSessions, ingressDurationMs, apiCalls and apiErrors SHALL be served only at 1h or coarser; a request for any of them with an explicit or resolved granularity below 1h SHALL be rejected with 400 naming the metric and the granularity.

Scenario: Hourly-only metric at five minutes

  • WHEN the client requests metrics=egressStarted,egressEnded with granularity=auto over 24 h
  • THEN the response is 400 with code VALIDATION_ERROR and a message stating that egressEnded requires a granularity of at least 1h, and the same metrics with granularity=1h answer 200 with 24 points per series

Requirement: Timezone-aware buckets

Edition: CE. 6h and 1d buckets SHALL start at local midnight boundaries of tz; in a zone other than UTC, 1d buckets SHALL be built by summing hourly documents per local day while the range is inside the hourly retention and SHALL otherwise fall back to UTC-day documents. uniqueIdentities SHALL always be reported per UTC day, marked basis: "utcDay" on the series.

Scenario: Daily buckets in New York

  • GIVEN tz=America/New_York and granularity=1d over 7 days
  • WHEN the series is requested
  • THEN every ts is a local midnight serialised in UTC (…T04:00:00.000Z or …T05:00:00.000Z) and each point equals the sum of the 24 hourly documents of that local day (23 or 25 on a DST change day)

Scenario: Unique identities in Madrid

  • GIVEN tz=Europe/Madrid and granularity=1d
  • WHEN metrics=uniqueIdentities,roomsCreated is requested
  • THEN the uniqueIdentities series carries basis: "utcDay" with ts at …T00:00:00.000Z and roomsCreated has no basis and ts at local midnight

Requirement: Dense series and bucket aggregation

Edition: CE. Every series SHALL contain exactly one point per bucket from from floored to the bucket start up to and including the bucket containing to; when a bucket spans several rollup documents, counters SHALL be summed and connectionSuccessRate SHALL be computed from the summed numerators and denominators.

Scenario: Sparse documents in a day

  • GIVEN analytics_5min holds documents for only three 5-minute buckets of the last 24 h
  • WHEN metrics=roomsCreated,connectionSuccessRate is requested with granularity=5m
  • THEN both series have 288 points, roomsCreated holds 0 outside the three buckets and connectionSuccessRate holds null

Scenario: Fifteen-minute rate from three documents

  • GIVEN three consecutive 5-minute documents with participants.active 1, 2 and 0 and participants.joined 2, 2 and 1
  • WHEN granularity=15m is requested with metrics=connectionSuccessRate,participantsJoined
  • THEN the bucket holds connectionSuccessRate: 60 (3 / 5, one decimal) and participantsJoined: 5

Requirement: Unique identities only at daily granularity

Edition: CE. uniqueIdentities SHALL be served only with granularity=1d; requesting it with any other explicit or resolved granularity SHALL be rejected with 400 naming the metric.

Scenario: Unique identities on a 24 h range

  • WHEN the client requests metrics=uniqueIdentities with granularity=auto over 24 h
  • THEN the response is 400 with code VALIDATION_ERROR and a message stating that uniqueIdentities requires granularity=1d

Requirement: Breakdowns endpoint

Edition: CE. GET /api/v1/analytics/breakdowns SHALL accept dimensions (comma-separated, at least one), from, to, tz and scope and answer { from, to, scope, breakdowns: [{ dimension, total, items: [{ key, count, percent }] }] } with items sorted by count descending, percent as one decimal of total, keys as stored (protocol enums in UPPER_SNAKE, dashboard vocabularies in lower case) and an unknown dimension rejected with 400.

Scenario: SDK breakdown

  • GIVEN rollups in range with participants.bySdk summing to js: 600, swift: 300, android: 100
  • WHEN dimensions=sdk is requested
  • THEN the breakdown has total: 1000 and items js 600 / 60.0, swift 300 / 30.0, android 100 / 10.0

Scenario: Dimension without data

  • GIVEN no egress in the range
  • WHEN dimensions=egressType is requested
  • THEN the breakdown has total: 0 and items: []

Requirement: Breakdown and summary source and range snapping

Edition: CE. Breakdowns and the summary SHALL read analytics_hourly for ranges up to 90 days, snapping from down and to up to whole hours, and analytics_daily beyond, snapping to whole UTC days; the response from/to SHALL report the snapped range. Entity and stats collections SHALL never be read by these endpoints.

Scenario: One-hour preset

  • GIVEN the current time is 12:40:00Z
  • WHEN breakdowns are requested with from=now-1h&to=now
  • THEN the response has from: "…T11:00:00.000Z", to: "…T13:00:00.000Z" and the integration test observes two hourly documents read per scope

Scenario: Long range reads daily documents

  • WHEN the summary is requested for the last 120 days
  • THEN analytics_daily is read, from and to are UTC midnights and no hourly document is read

Requirement: Summary endpoint

Edition: CE. GET /api/v1/analytics/summary SHALL accept from, to, tz and scope and answer the fifteen KPI values of the "KPI keys" scenario, omitting any value the stored documents cannot derive. ingressSessions and ingressDurationMs SHALL be the sums of ingress.sessionsStarted and ingress.durationMs over the snapped range, 0 without ingress activity.

Scenario: Summary values

  • GIVEN hourly documents in range summing to rooms.created: 120, rooms.ended: 110, participants.joined: 900, participants.active: 880, apiCalls.total: 5000, apiErrors.total: 50
  • WHEN the summary is requested
  • THEN the body has roomsCreated: 120, roomsEnded: 110, participantSessions: 900, connectionSuccessRate: 97.8, apiCalls: 5000 and apiErrorRate: 1.0

Scenario: Average room size and duration

  • GIVEN hourly documents summing to rooms.ended: 110, rooms.participantsSum: 440 and rooms.durationMsSum: 39600000
  • WHEN the summary is requested
  • THEN the body has avgRoomSize: 4.0 and avgRoomDurationMs: 360000

Scenario: Writer without the room sums

  • GIVEN hourly documents written by a writer that stores no rooms.participantsSum
  • WHEN the summary is requested
  • THEN avgRoomSize and avgRoomDurationMs are absent from the body and the other values are present

Scenario: Unique identities over several days

  • GIVEN analytics_daily documents for the three UTC days overlapping the range with uniqueIdentities 100, 120 and 90
  • WHEN the summary is requested
  • THEN the body has uniqueIdentities: { value: 310, basis: "utcDays", days: 3 }

Scenario: Ingress totals

  • GIVEN a valid token on the CE dashboard and hourly documents in range summing to ingress.sessionsStarted: 12 and ingress.durationMs: 7200000
  • WHEN the summary is requested
  • THEN the response is 200 with ingressSessions: 12 and ingressDurationMs: 7200000

Scenario: No ingress in the range

  • GIVEN hourly documents in range without ingress counters
  • WHEN the summary is requested
  • THEN the body has ingressSessions: 0 and ingressDurationMs: 0

Scenario: KPI keys

  • WHEN the client requests GET /api/v1/analytics/summary over a range whose stored documents derive every value
  • THEN the response carries the KPI values roomsCreated, roomsEnded, participantSessions, uniqueIdentities, connectionSuccessRate, avgRoomSize, avgRoomDurationMs, bytesUp, bytesDown, egressStarted, egressDurationMs, ingressSessions, ingressDurationMs, apiCalls and apiErrorRate

Requirement: PRO metrics, dimensions and scopes

Edition: CE. scoreAvg, poorSamples and lostSamples SHALL require analytics-quality; tracksSubscribeFailed, disconnectReason, roomEndReason and apiMethod SHALL require analytics-diagnostics; scope=node:<id> SHALL require per-node-breakdown. ingressSessions and ingressDurationMs are CE metrics and require no feature. On CE a request containing any of the gated items SHALL be rejected whole with 403 PRO_FEATURE.

Scenario: Mixed request on CE

  • GIVEN a valid token on the CE dashboard
  • WHEN the client requests metrics=roomsCreated,scoreAvg
  • THEN the response is 403 with code PRO_FEATURE, details.feature: "analytics-quality" and no series

Scenario: Node scope on CE

  • WHEN the CE client requests the summary with scope=node:n1
  • THEN the response is 403 with details.feature: "per-node-breakdown"

Scenario: Diagnostics dimension on CE

  • GIVEN a valid token on the CE dashboard
  • WHEN the client requests GET /api/v1/analytics/breakdowns?dimensions=sdk,disconnectReason
  • THEN the response is 403 with code PRO_FEATURE, details.feature: "analytics-diagnostics" and no breakdowns

Requirement: CSV export

Edition: CE. GET /api/v1/analytics/export.csv SHALL accept kind (series or breakdowns) with the parameters of the matching endpoint and answer 200 text/csv; charset=utf-8 as an attachment named analytics-<kind>-<from>-<to>.csv (colons removed), RFC 4180 with CRLF line ends and a header row, with the same validation and PRO gating as the JSON endpoints.

Scenario: Breakdown CSV

  • WHEN the client requests export.csv?kind=breakdowns&dimensions=sdk,os
  • THEN the body has the header dimension,key,count,percent followed by the sdk rows and then the os rows, each sorted by count descending

Scenario: PRO metric in a CE export

  • WHEN the CE client requests export.csv?kind=series&metrics=scoreAvg
  • THEN the response is 403 JSON with code PRO_FEATURE, not a CSV body

Scenario: Series CSV on CE

  • WHEN the client requests export.csv?kind=series&metrics=bytesUp,roomsCreated&granularity=1h over 24 h
  • THEN the body has the header bucket,bytesUp,roomsCreated and 24 data rows whose first cell is the bucket start in ISO 8601 UTC

Requirement: Index-backed analytics queries

Edition: CE. Every rollup read SHALL be a bounded index scan on { scope, bucket } of the chosen collection; integration tests SHALL assert the plans with explain().

Scenario: Explain plans on the generated database

  • GIVEN a database generated by the data generator at scenario M
  • WHEN the integration suite explains every analytics query
  • THEN no plan contains COLLSCAN and rollup plans report the { scope, bucket } index with totalKeysExamined at most the number of buckets in range plus one

Requirement: Ingress input type breakdown

Edition: CE. The breakdowns endpoint SHALL accept the dimension ingressInputType, which sums ingress.byInputType of the hourly and daily rollups in range under the keys rtmp, whip and url: the ingress sessions started in the range by input type. It is not gated by any feature.

Scenario: Sessions by input type

  • GIVEN rollups in range with ingress.byInputType summing to rtmp: 8, whip: 3 and url: 1
  • WHEN dimensions=ingressInputType is requested on CE
  • THEN the breakdown has total: 12 and items rtmp 8 / 66.7, whip 3 / 25.0 and url 1 / 8.3

Scenario: No ingress in the range

  • GIVEN no ingress session started in the range
  • WHEN dimensions=ingressInputType is requested
  • THEN the breakdown has total: 0 and items: []

Requirement: Media statistics coverage

Edition: CE. The summary SHALL carry mediaStats: "none" when every nodes document of its scope whose heartbeat span (firstSeenAt to lastSeenAt) overlaps the range has mediaStats: false, mediaStats: "partial" when only some do, and no mediaStats property when none does or no node document overlaps the range. The global scope covers every node, a node:<id> scope that node only.

Scenario: No server reports media statistics

  • GIVEN two nodes seen during the range, both with mediaStats: false
  • WHEN the summary is requested
  • THEN the body has mediaStats: "none" and still carries bytesUp and bytesDown

Scenario: Some servers report media statistics

  • GIVEN a node with mediaStats: false and a node with mediaStats: true seen during the range
  • WHEN the summary is requested with the global scope
  • THEN the body has mediaStats: "partial"

Scenario: Node scope

  • GIVEN the same two nodes and scope=node:<id> of the node with mediaStats: true
  • WHEN the summary is requested
  • THEN the body has no mediaStats property

Scenario: Node seen outside the range

  • GIVEN a node with mediaStats: false whose lastSeenAt is earlier than from, and a node with mediaStats: true seen during the range
  • WHEN the summary is requested
  • THEN the body has no mediaStats property