7.8 KiB
Design
Context
- Charts:
charts/chart-options.tsbuilds every time-series option.- The x-axis is an ECharts
timeaxis whose labels areHH:mm,MM-dd HH:mmorMM-ddby span, with no minimum interval, so a 3-minute range ticks every 20–30 s. - The y-axis has no minimum interval either.
- Step series use
step: 'end': each value is drawn from its bucket start to the next point, so the last point has no segment.
- The x-axis is an ECharts
- Skeletons:
ovd-skeletonpaints its bars at once and is removed on the first response, which flashes when the response takes tens of milliseconds. - Durations:
formatDuration()truncates at every unit.ovd-durationformats both running and stored durations with it. - About storage:
InfoService.storageEntry()lists each collection's indexes and dates it only when an index leads withts,bucketorcreatedAt. The analytics indexes of the time fields are:rooms.createdAt,ingresses.createdAt;participants { joinedAt: -1, _id: -1 };egresses { startedAt: -1, _id: -1 }andingress_sessions { startedAt: -1, _id: -1 };events.ts,analytics_5min.bucket,analytics_hourly.bucket;analytics_daily { scope, bucket };- the
track_statstime series, indexed on its metadata beforets; nodes, with no time index and one document per node;tracksandtrack_subscriptions, with their time field only behindparticipantSid,roomSid,subscriberSidortrackSid.
- Global search:
exactIdTarget()recognisesTR_throughentityIdKind()but drops it, because no route takes a track ID. - Browser and OS labels: the rollups key browsers by family, and an empty browser falls into
otheruntil the writer change addsnone. Pages that show a client's OS print the reported string (linuxfrom the Go SDK,Linuxfrom the JS SDK).
Goals / Non-Goals
Goals:
- One rule per chart property, applied in
timeSeriesOption()for every chart of every edition. - No new inputs on the chart, the skeleton or the duration components.
Non-Goals:
- Changing the label formats of the axes or the retention notices.
- New indexes for tracks and subscriptions.
Decisions
D1. The x-axis interval follows the label unit
minInterval is 60 000 ms when the labels show minutes (HH:mm, MM-dd HH:mm) and 86 400 000 ms when they show days.
It comes from the same span rule as the label format.
Rejected: passing the bucket width from every chart. The labels are the constraint, and the granularity is already in the chart note.
D2. Integer data gets integer ticks
yAxis.minInterval is 1 when every non-null value of every series is an integer.
Rejected: an integer input on the chart, which every count chart of every edition would have to set.
D3. A step series is extended to to
When the last point of a step series is not null and starts before to, timeSeriesOption() appends [to, value], so
step: 'end' draws the last bucket up to the end of the range. A dense series always ends in the bucket that holds
to.
Rejected: step: 'start', which would draw every value one bucket early.
D4. The skeleton fades in after 300 ms
Every loading placeholder keeps its height from the start and carries the global class ovd-skeleton-reveal, whose CSS
animation keeps its opacity at 0 for 300 ms and then shows it: ovd-skeleton, the skeleton rows of ovd-data-table
and the KPI tile skeleton. Pages keep their loading logic. Under prefers-reduced-motion the global rule shortens the
fade and the delay stays.
Rejected:
- delaying the loading flag in every loader, which touches every page and both loaders;
- keeping the previous content, which does not exist on a first load.
D5. Stored durations are rounded, running ones are not
formatDuration(ms, { mode }) rounds to the smallest unit it will show by default:
- it picks the unit from the magnitude, rounds, and formats again when rounding crosses a unit (59 600 ms → 60 s → "1 m 00 s");
mode: 'floor'keeps the current truncation;ovd-durationpassesfloorwhilerunningis true, both forliveDurationMsand forliveTotalMs.
Rejected: rounding live durations, which would make a counter jump to the next second halfway through.
D6. A fixed time field per collection
InfoService holds one map from collection to field, and optional filter, and reads find(filter).sort({ field: 1 }) .limit(1) with a projection of the field. This replaces the index discovery and saves a listIndexes per collection.
| Collection | Field |
|---|---|
rooms, ingresses |
createdAt |
participants |
joinedAt |
egresses, ingress_sessions |
startedAt |
events |
ts |
analytics_5min, analytics_hourly |
bucket |
analytics_daily |
bucket with { scope: 'global' } |
nodes |
firstSeenAt (one document per node, no index needed) |
tracks, track_subscriptions and track_stats have no entry, and About explains the "—". The track_stats query
was explained on a generated dataset: the time series has no index on its time alone, so the plan is a bounded sort
over a scan of the buckets.
Rejected:
- new
{ publishedAt }and{ subscribedAt }indexes, two more indexes on the most written collections for one About cell; - a sort without an index, a collection scan every 60 s.
D7. Plain version text
sidenav-footer.ts renders the version in a span.
D8. The room averages say why they are missing
missingHint() takes the reason: roomsEnded === 0 gives "No room ended in this range", anything else "Not provided by
this server version".
D9. Total duration tile
The Egress page tile is labelled "Total duration", with INFO_TEXTS.egressTotalDuration.
D10. Browser labels and the OS label
format/labels.ts gains:
BROWSER_LABELSforchrome,firefox,safari,edge,none("No browser") andother, used by the browser donut, which today prints the key;clientOsLabel(os), which compares the reported string, case-insensitively and trimmed, with the family names (windows,macos,mac os,linux,android,ios) and returns theOS_LABELSentry, or the string as reported.
The library exports both for the detail pages of every edition.
Rejected: mapping by substring as the writer does, which would turn "Chrome OS" into "Linux" on a page meant to show what the client reported.
D11. A locked track route
routes.ts adds tracks/:trackSid:
- with no navigation entry and
page.feature: 'participant-detail'; - with the breadcrumb param
trackSid, which titles the lock, as the route has no list above it; - opening a locked page that reuses the participant lock.
exactIdTarget() maps track to /tracks. An edition with the feature replaces the route by path, as it does for
the other detail routes.
API
No shape change: storage.<collection>.oldest exists and only covers more collections.
Performance budget
About reads at most eleven point queries every 60 s, with the existing cache. Each is verified in the info integration
test with explain(): an IXSCAN with one document examined, and a COLLSCAN only on nodes.
track_stats has no entry: its plan scans the buckets of the time series.
Risks / Trade-offs
- [Rounding changes durations across the UI, by up to half a unit] → Unit tests of the formatter for both modes, and updated fixtures where a duration reads differently.
- [A running duration that ends shows one second more than it last counted] → Accepted: the stored value is the measured one.
- [The
nonebrowser key only appears in rollups written after the writer change] → Older rollups keep those sessions underother, which still reads "Other".
Migration Plan
Ships with the 3.10 dashboard. The browser label of none relies on the typings of writer-review-fixes, applied
first.