mirror of https://github.com/OpenVidu/openvidu.git
392 lines
19 KiB
Markdown
392 lines
19 KiB
Markdown
# 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
|