@webex/internal-plugin-metrics 3.12.0-next.54 → 3.12.0-next.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,447 @@
1
+ ---
2
+ type: Module Spec
3
+ title: 'WebRTC stats telemetry specification'
4
+ description: Responsibilities, boundaries, design, invariants, and verification for the WebRTC stats telemetry reporter.
5
+ tags: [module, specification]
6
+ ---
7
+ <!-- sdd-generated-metadata
8
+ doc_kind: module-spec
9
+ generated_from: module-spec@0.3.0
10
+ generated_by: claude-code
11
+ approved_by: rarajes2@cisco.com
12
+ updated_at: 2026-10-07T04:50:31Z
13
+ validation_status: pass
14
+ -->
15
+
16
+ # WebRTC stats telemetry
17
+
18
+ This source-local document at `src/rtcMetrics/docs/README.md` owns the stable specification for
19
+ **the WebRTC stats telemetry reporter**. Ground every claim in repository evidence and link to the
20
+ [repository architecture](../../../docs/architecture.md) instead of repeating broader facts.
21
+
22
+ Related context: [documentation index](../../../docs/index.md) ·
23
+ [documentation agent instructions](../../../AGENTS.md)
24
+
25
+ ## Metadata
26
+
27
+ | Field | Value |
28
+ | ------------- | -------------------------------------------- |
29
+ | Owner | Cisco Webex JS SDK — telemetry |
30
+ | Source path | `src/rtcMetrics/` |
31
+ | Resource kind | module |
32
+ | Status | Active |
33
+ | Last verified | 2026-10-05 at `d6202dd73d` |
34
+ | Module id | `rtc-metrics` |
35
+ | Parent spec | [`src/docs/README.md`](../../docs/README.md) |
36
+ | Doc kind | Module spec |
37
+ | Coverage score | Pending coverage assessment |
38
+ | Validation status | pass (2026-10-07; validator runtime 01a1147a-c744-7083-9b15-95e91170c79a; 0 blocking, 0 important, 0 medium, 0 minor; conformance and source-fidelity gates pass, no code/spec mismatches found) |
39
+
40
+ ## Applicability
41
+
42
+ Sections preceded by an `Include if` comment are conditional. Retain a
43
+ conditional section only when source evidence or a confirmed developer answer
44
+ satisfies its condition.
45
+
46
+ | Condition ID | Status | Evidence or reason | Owned section |
47
+ | ------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
48
+ | `module.has_tiers` | N/A | The repository defines no operational or review tier policy for modules | Tier |
49
+ | `module.has_ui` | N/A | The module renders nothing | UI use-case flow |
50
+ | `module.crosses_service_boundaries` | Applicable | `src/rtcMetrics/index.ts` posts directly to the unified telemetry service | Cross-boundary use-case flow |
51
+ | `module.holds_client_state` | Applicable | `src/rtcMetrics/index.ts` holds the metrics queue, the connection identifier and the force-send flag | Client state model |
52
+ | `module.enforces_domain_rules` | Applicable | Candidate address anonymization and connection-identifier rotation in `src/rtcMetrics/index.ts` | Business rules and invariants |
53
+ | `module.is_concurrent_async` | Applicable | A repeating browser interval drives submission in `src/rtcMetrics/index.ts` | Concurrency and reactive flow |
54
+ | `module.owns_persistence` | N/A | No store, schema or migration; the queue is in-memory and emptied on submission | Data, schema, and migration |
55
+ | `module.stateful_transitions` | Applicable | The connection lifecycle rotates the identifier and re-arms the force-send flag in `src/rtcMetrics/index.ts` | State machine |
56
+ | `module.exposes_wire_protocol` | Applicable | The submitted batch shape is declared in `src/metrics.types.ts` and assembled in `src/rtcMetrics/index.ts` | Protocol and wire format |
57
+ | `module.ui_multi_screen` | N/A | No user interface | UI flow |
58
+ | `module.large_data_model` | N/A | One attribute object plus an opaque array of serialized stats items | Data model |
59
+ | `module.returns_caller_errors` | N/A | Every operation returns void and the submission promise is not returned to a caller | Caller-visible failure modes |
60
+ | `module.module_specific_conventions` | Applicable | Stats items are handled as JSON strings rather than objects throughout `src/rtcMetrics/index.ts` | Module-specific rules |
61
+ | `module.published_package` | Applicable | Re-exported from `src/index.ts`, which is the file that crosses the package boundary to the meetings plugin | Export stability |
62
+ | `module.embedded_in_host` | N/A | A plain class, not a registered plugin; the parent module owns the registration contract | Host integration and theming |
63
+ | `module.has_design_tradeoff` | Applicable | Immediate submission of the first useful stats report, versus waiting for the normal interval | Key design trade-off |
64
+ | `module.has_submodules` | N/A | Derived from the manifest module tree: this module has no child modules | Sub-modules |
65
+
66
+ ## Evidence register
67
+
68
+ | Evidence | What it establishes |
69
+ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
70
+ | `src/rtcMetrics/index.ts` | The public surface, the queue and interval behavior, anonymization, connection rotation and the submitted batch |
71
+ | `src/rtcMetrics/constants.ts` | The fixed application identifier sent as a request header |
72
+ | `src/metrics.types.ts` | The identifier-pair input type and the submitted attribute shape |
73
+ | `src/call-diagnostic/call-diagnostic-metrics.util.ts` | The IP anonymization helper this module reuses |
74
+ | `src/index.ts` | That the class is part of the published export surface |
75
+ | `test/unit/spec/rtcMetrics/index.ts` | Behavioral verification of queueing, the interval, anonymization, connection rotation and the force-send path |
76
+
77
+ ## Purpose and boundary
78
+
79
+ - Responsibility: collecting WebRTC media statistics items produced by the media engine, anonymizing
80
+ candidate addresses, and submitting them in batches to the Webex unified telemetry service.
81
+ - In scope: the metrics queue; the repeating submission interval; anonymization of local and remote
82
+ candidate addresses; rotating the connection identifier when a media connection fails; forcing an
83
+ immediate submission on the next stats report; assembling the submitted attribute object.
84
+ - Out of scope: producing the statistics themselves, which come from the media engine through the
85
+ meetings layer; the meaning of individual stats fields, which this module never parses beyond
86
+ candidate addresses; and the upstream ingestion contract, which the telemetry service owns.
87
+ - Consumers: `packages/@webex/plugin-meetings/src/meeting/index.ts` and
88
+ `packages/@webex/plugin-meetings/src/media/index.ts`, which construct one instance per media
89
+ connection. Exported publicly from `src/index.ts`.
90
+
91
+ ## Structure and key files
92
+
93
+ | Path | Responsibility |
94
+ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `src/rtcMetrics/index.ts` | The reporter class: queue, interval, anonymization, connection lifecycle and submission; authoritative for all module behavior |
96
+ | `src/rtcMetrics/constants.ts` | The registered application identifier; authoritative for the value sent in the request header |
97
+
98
+ ## Public surface
99
+
100
+ | Surface | Consumer | Compatibility commitment | Source |
101
+ | ---------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ----------------------------- |
102
+ | `RtcMetrics` constructor | `packages/@webex/plugin-meetings/src/meeting/index.ts` | Takes the SDK instance, a meeting or call identifier pair, and a correlation identifier. Starts the interval immediately | `src/rtcMetrics/index.ts` |
103
+ | `addMetrics` | `packages/@webex/plugin-meetings/src/media/index.ts` | Accepts a named payload array of serialized stats items; ignores an empty payload | `src/rtcMetrics/index.ts` |
104
+ | `sendMetricsInQueue` | `packages/@webex/plugin-meetings/src/meeting/index.ts` | Submits and clears the queue only when it is non-empty | `src/rtcMetrics/index.ts` |
105
+ | `sendNextMetrics` | `packages/@webex/plugin-meetings/src/meeting/index.ts` | Arms immediate submission on the next stats report | `src/rtcMetrics/index.ts` |
106
+ | `updateCallId` | `packages/@webex/plugin-meetings/src/meeting/index.ts` | Replaces the call identifier used by later submissions | `src/rtcMetrics/index.ts` |
107
+ | `closeMetrics` | `packages/@webex/plugin-meetings/src/meeting/index.ts` | Flushes the queue, then clears the interval. Must be called or the interval leaks | `src/rtcMetrics/index.ts` |
108
+ | `anonymizeIp` | Tests, and internally per stats item | Anonymizes candidate addresses in one serialized stats item and returns it re-serialized | `src/rtcMetrics/index.ts` |
109
+
110
+ The submitted batch shape is declared in `src/metrics.types.ts`; it is not restated here.
111
+
112
+ ## Dependencies
113
+
114
+ | Dependency | Why it is required | Failure behavior |
115
+ | ------------------------------------------------------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
116
+ | `src/call-diagnostic/call-diagnostic-metrics.util.ts` | Supplies the IP anonymization helper applied to candidate addresses | An unanonymizable value becomes undefined, which drops the field rather than emitting the raw address |
117
+ | The host SDK request pipeline | Performs the HTTP POST to the unified telemetry service | The returned promise is not awaited or handled, so a failed submission is silently lost |
118
+ | The host SDK device registration | Supplies the user identifier included in every batch | A missing device makes the field undefined; submission still proceeds |
119
+ | `uuid` | Generates each connection identifier | No failure path |
120
+ | Browser `setInterval` | Drives periodic submission; used explicitly so the handle is typed as a browser timer | Not available outside a browser, so the class is a browser-only surface |
121
+
122
+ ## Requirements
123
+
124
+ | ID | WHAT | WHY | Source evidence | Test or example evidence | Assumptions or gaps | Confidence |
125
+ | --------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------- | ---------- |
126
+ | `MOD-001` | Queued stats items are submitted every thirty seconds and the queue is emptied after each submission | Per-item submission would multiply request volume during a call; a fixed window bounds it without losing data | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
127
+ | `MOD-002` | An interval tick with an empty queue submits nothing | An idle media connection must not generate empty telemetry requests for the whole duration of a call | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
128
+ | `MOD-003` | Local and remote candidate addresses are anonymized before a stats report is queued | Candidate addresses are personally identifying network data and must not leave the client intact | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
129
+ | `MOD-004` | The first stats report of a connection is submitted immediately rather than waiting for the interval | The media engine produces the first useful data a few seconds in; submitting at once means a user who abandons the call early is still represented | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
130
+ | `MOD-005` | A failed connection state change flushes the queue and rotates to a new connection identifier | Statistics from a dead connection must not be attributed to its replacement, so the identifier is the boundary between attempts | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
131
+ | `MOD-006` | A successful connection keeps the same connection identifier across submissions | Consumers correlate all batches from one media connection by that identifier | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
132
+ | `MOD-007` | Each batch carries either a meeting identifier or a call identifier, never both, with the meeting identifier preferred | The two identify different products, and ingestion routes on exactly one of them | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
133
+ | `MOD-008` | The call identifier can be replaced after construction | A calling session learns its call identifier after the media connection is already reporting | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
134
+ | `MOD-009` | Closing the reporter flushes the queue and then clears the interval | A call ending must not discard the last window, and the interval must not outlive the connection | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none | Present |
135
+ | `MOD-010` | Stats-report items must be valid JSON: anonymization parses each item and a malformed one throws from the add operation before the item is queued. The connection-failure detector parses only the first item of the payload, treats an unparseable or absent first item as "not failed" without throwing, and still queues the original item | Anonymization has to reach candidate address fields, so it cannot pass an unparseable stats report through; the failure detector is a best-effort trigger and must not drop data it cannot read | `src/rtcMetrics/index.ts` | none found | No case feeds a malformed stats-report item or an unparseable connection-state payload; both parse paths are unverified | Weak |
136
+ | `MOD-011` | Submission failures are not surfaced to the caller: the telemetry request promise is neither returned nor caught | Telemetry must never affect a call. Every operation returns void; a rejected request is left unhandled and that batch is lost | `src/rtcMetrics/index.ts` | none found | No case rejects the request and asserts the call is unaffected | Weak |
137
+
138
+ ## Design overview
139
+
140
+ The reporter is a plain class, not an SDK plugin, because one instance exists per media connection
141
+ rather than per SDK instance. The meetings layer constructs it when a media connection is created
142
+ and closes it when the connection ends.
143
+
144
+ Its internal shape is a queue plus two timers' worth of policy. `metricsQueue` accumulates whatever
145
+ the media engine hands over; the thirty-second browser interval submits and empties it. Everything
146
+ else in the class exists to handle the two cases where the plain interval is the wrong answer.
147
+
148
+ The first is the start of a connection. The media engine produces its first useful statistics report
149
+ a few seconds after the connection comes up, and a user who is unhappy with media quality often
150
+ closes the browser within the first window. `shouldSendMetricsOnNextStatsReport` is therefore armed
151
+ on construction and after every connection rotation, and the first stats report that arrives
152
+ triggers an immediate submission before the flag is cleared. `sendNextMetrics` re-arms it so the
153
+ meetings layer can force the same behavior after a significant media event, such as moving from the
154
+ lobby into the meeting.
155
+
156
+ The second is connection failure. When a connection-state-change item reports a failed connection,
157
+ the queue is flushed and `resetConnection` generates a new connection identifier, which is the field
158
+ consumers use to group batches from one connection. Flushing before rotating is what keeps the
159
+ failed connection's statistics attributed to the failed connection.
160
+
161
+ Stats items are handled as JSON strings end to end. `addMetrics` receives them serialized, the
162
+ anonymization step parses one item, rewrites candidate addresses and re-serializes it, and the
163
+ submitted batch carries the array of strings unchanged. Anonymization parses without a guard, so a
164
+ malformed stats-report item throws out of `addMetrics` before anything is queued; the
165
+ connection-failure check uses a guarded parse of the first item and falls back to "not failed". The module never builds an object model of a
166
+ statistics report, which is why a schema change in the media engine does not require a change here.
167
+
168
+ ## Data flow and sequence coverage
169
+
170
+ The call style is an in-process method call for collection and a direct HTTP POST for submission.
171
+ There are two operation groups, and they are diagrammed separately because their actor order and
172
+ state outcome differ: collection is driven by the media engine, while submission is driven by a timer
173
+ or by a connection event.
174
+
175
+ | Operation group | Entry and outcome | Diagram or evidence | Failure and recovery coverage |
176
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
177
+ | Collect a stats item | The media engine calls the add operation; the item is anonymized if it is a stats report and appended | First sequence diagram below | An empty payload is ignored; a malformed stats-report item throws from `addMetrics` and is not queued; an unparseable connection-state payload is queued but never triggers rotation |
178
+ | Submit a batch | An interval tick, a forced first report, a connection failure or a close flushes the queue to the service | Second sequence diagram below | An empty queue submits nothing; a rejected request is not handled, so that batch is lost |
179
+
180
+ ```mermaid
181
+ sequenceDiagram
182
+ participant Engine as Media engine
183
+ participant Rtc as RtcMetrics
184
+ participant Anon as IP anonymizer
185
+ participant Queue as Metrics queue
186
+ Engine->>Rtc: addMetrics(named payload array)
187
+ alt payload is empty
188
+ Rtc-->>Engine: return without queueing
189
+ end
190
+ opt payload is a stats report
191
+ Rtc->>Anon: anonymize each item's candidate addresses
192
+ alt item is not valid JSON
193
+ Anon-->>Engine: parse error thrown, nothing queued
194
+ else valid JSON
195
+ Anon-->>Rtc: re-serialized items
196
+ end
197
+ end
198
+ Rtc->>Queue: append item
199
+ opt first stats report of the connection
200
+ Rtc->>Rtc: submit immediately and clear the force-send flag
201
+ end
202
+ opt connection state change first item parses and reports failed
203
+ Rtc->>Rtc: submit, then rotate the connection identifier
204
+ end
205
+ ```
206
+
207
+ ```mermaid
208
+ sequenceDiagram
209
+ participant Timer as Thirty-second interval
210
+ participant Rtc as RtcMetrics
211
+ participant Queue as Metrics queue
212
+ participant Telemetry as Unified telemetry service
213
+ Timer->>Rtc: tick
214
+ alt queue is empty
215
+ Rtc-->>Timer: submit nothing
216
+ else queue has items
217
+ Rtc->>Queue: read items
218
+ Rtc->>Telemetry: POST batch with type, version, identifiers and items
219
+ Rtc->>Queue: clear
220
+ opt request rejected
221
+ Note over Rtc,Telemetry: the promise is not handled, so this batch is lost
222
+ end
223
+ end
224
+ ```
225
+
226
+ ## Class and component relationships
227
+
228
+ ```mermaid
229
+ classDiagram
230
+ class Meeting {
231
+ +correlationId
232
+ +createRtcMetrics()
233
+ +closeRtcMetrics()
234
+ }
235
+ class RtcMetrics {
236
+ +metricsQueue
237
+ +intervalId
238
+ +correlationId
239
+ +connectionId
240
+ +shouldSendMetricsOnNextStatsReport
241
+ +addMetrics()
242
+ +sendMetricsInQueue()
243
+ +sendNextMetrics()
244
+ +updateCallId()
245
+ +closeMetrics()
246
+ +anonymizeIp()
247
+ -resetConnection()
248
+ -sendMetrics()
249
+ }
250
+ class CallDiagnosticUtils {
251
+ +anonymizeIPAddress()
252
+ }
253
+ class RtcMetricsConstants {
254
+ +APP_ID
255
+ }
256
+ Meeting --> RtcMetrics : one per media connection
257
+ RtcMetrics --> CallDiagnosticUtils : anonymizes candidate addresses
258
+ RtcMetrics --> RtcMetricsConstants : reads the application identifier
259
+ ```
260
+
261
+ ## Use cases and flows
262
+
263
+ | Use case | Actor or caller | Primary steps and outcome | Failure or boundary behavior | Evidence |
264
+ | -------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
265
+ | `UC-001` | Media engine | A stats report arrives during a healthy call; candidate addresses are anonymized and the item is queued until the next interval tick | An empty payload is ignored entirely | `src/rtcMetrics/index.ts`, `test/unit/spec/rtcMetrics/index.ts` |
266
+ | `UC-002` | Media engine | The first stats report of a connection arrives; it is submitted immediately instead of waiting up to thirty seconds | The flag is cleared, so the second report waits for the interval | `src/rtcMetrics/index.ts`, `test/unit/spec/rtcMetrics/index.ts` |
267
+ | `UC-003` | Media engine | A connection-state item reports a failed connection; the queue is flushed and a new connection identifier is generated and re-armed | Items queued afterwards are attributed to the new connection identifier | `src/rtcMetrics/index.ts`, `test/unit/spec/rtcMetrics/index.ts` |
268
+ | `UC-004` | Calling session | The call identifier becomes known after construction; it is set and appears in later batches | A meeting identifier, if present, still takes precedence over the call identifier | `src/rtcMetrics/index.ts`, `test/unit/spec/rtcMetrics/index.ts` |
269
+ | `UC-005` | Meetings layer | The call ends; closing flushes the final window and clears the interval | Closing with an empty queue clears the interval without submitting | `src/rtcMetrics/index.ts`, `test/unit/spec/rtcMetrics/index.ts` |
270
+
271
+ <!-- Include if: the module calls another service or crosses an event or network boundary. [condition-id: module.crosses_service_boundaries] -->
272
+
273
+ ### Cross-boundary use-case flow
274
+
275
+ All five use cases converge on one outbound boundary: a POST to the unified telemetry service's
276
+ versioned metric resource, through the host SDK request pipeline.
277
+
278
+ - Boundary and transport: HTTPS POST to the `unifiedTelemetry` service, resource `metric/v2`, with a
279
+ media type header and the registered application identifier from `src/rtcMetrics/constants.ts`.
280
+ - Ordering: batches are submitted in queue order and one submission is started per flush. Nothing
281
+ serializes concurrent flushes, so a forced submission and an interval tick in the same turn can
282
+ produce two in-flight requests; because the queue is cleared between them, they carry disjoint data.
283
+ - Compatibility: the batch declares its own version field, so ingestion can distinguish payload
284
+ generations. The statistics items themselves are opaque strings and pass through unchanged.
285
+ - Timeout and retry: none. This module does not use the batcher family, so there is no queueing,
286
+ backoff or re-enqueue. A failed request loses that batch.
287
+ - Recovery: the next window submits new data. There is no replay of a lost batch, which is an
288
+ accepted limitation for sampled media statistics.
289
+
290
+ <!-- Include if: the module holds client-side state. [condition-id: module.holds_client_state] -->
291
+
292
+ ## Client state model
293
+
294
+ | State or slice | Owner | Initial state | Transition triggers | Reset or persistence boundary |
295
+ | ------------------------------------ | ---------------------- | ---------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
296
+ | Metrics queue | The reporter instance | Empty array | An item is appended by the add operation | Emptied on every submission; never persisted |
297
+ | Connection identifier | The reporter instance | Generated at construction | Regenerated when a connection-state item reports a failed connection | Lives for one media connection attempt |
298
+ | Force-send flag | The reporter instance | Armed at construction | Cleared after the first stats report is submitted; re-armed on rotation or on demand | Per connection attempt |
299
+ | Interval handle | The reporter instance | Started at construction | None | Cleared only by closing the reporter |
300
+ | Meeting and call identifiers | The reporter instance | Supplied at construction | The call identifier can be replaced through the public setter | Lives for the reporter's lifetime |
301
+
302
+ <!-- Include if: the module enforces domain rules or entity invariants. [condition-id: module.enforces_domain_rules] -->
303
+
304
+ ## Business rules and invariants
305
+
306
+ | ID | Invariant | WHY | Enforcement source | Test evidence |
307
+ | --------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------- | ------------------------------------ |
308
+ | `INV-001` | No candidate address leaves the client un-anonymized | Candidate addresses are personally identifying network data | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` |
309
+ | `INV-002` | Anonymization is applied only to stats reports, and only to local and remote candidate entries | Other item kinds carry no address fields, so parsing and rewriting them would be wasted work and risk | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` |
310
+ | `INV-003` | A submitted batch is never empty | Empty requests would add call-duration-proportional noise to ingestion | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` |
311
+ | `INV-004` | The queue is flushed before the connection identifier rotates | Otherwise a failed connection's statistics would be attributed to its replacement | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` |
312
+ | `INV-005` | A batch carries at most one of the meeting and call identifiers | Ingestion routes on exactly one; both would be ambiguous | `src/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` |
313
+
314
+ <!-- Include if: the module is concurrent, asynchronous, reactive, or event-driven. [condition-id: module.is_concurrent_async] -->
315
+
316
+ ## Concurrency and reactive flow
317
+
318
+ - Execution model: a single repeating browser interval, started in the constructor, plus synchronous
319
+ calls from the media engine and the meetings layer. The browser `setInterval` is used explicitly so
320
+ the handle is typed as a browser timer rather than a Node.js one.
321
+ - Ordering guarantees: items are appended and submitted in arrival order. Within one submission the
322
+ queue is read, sent and cleared without yielding, so an item appended during the request belongs to
323
+ the next batch.
324
+ - Idempotency and retry: none. There is no deduplication key and no retry; each batch is submitted at
325
+ most once and a rejected request is not re-sent.
326
+ - Shared-state protection: single-threaded by construction. Nothing locks the queue, and nothing
327
+ needs to, because every mutation happens in a synchronous block.
328
+ - Blocking restrictions: no operation awaits the submission. Collection, forced submission and close
329
+ all return void, so a slow telemetry request can never delay the media path.
330
+
331
+ <!-- Include if: the module has non-trivial state transitions. [condition-id: module.stateful_transitions] -->
332
+
333
+ ## State machine
334
+
335
+ The state is the connection's reporting lifecycle, keyed by the connection identifier.
336
+
337
+ ```mermaid
338
+ stateDiagram-v2
339
+ [*] --> AwaitingFirstReport: construct and arm force-send
340
+ AwaitingFirstReport --> Reporting: first stats report submitted immediately
341
+ AwaitingFirstReport --> AwaitingFirstReport: non-stats item queued
342
+ Reporting --> Reporting: interval tick submits a non-empty queue
343
+ Reporting --> Reporting: interval tick with an empty queue submits nothing
344
+ Reporting --> AwaitingFirstReport: connection failed, so flush then rotate the identifier
345
+ AwaitingFirstReport --> AwaitingFirstReport: force-send re-armed on demand
346
+ Reporting --> AwaitingFirstReport: force-send re-armed on demand
347
+ Reporting --> [*]: close flushes then clears the interval
348
+ AwaitingFirstReport --> [*]: close flushes then clears the interval
349
+ ```
350
+
351
+ Guards and rejected transitions:
352
+
353
+ - Only a stats report can leave `AwaitingFirstReport`; queueing any other item kind keeps the flag
354
+ armed.
355
+ - Only a connection-state item whose parsed value reports a failed connection triggers rotation. An
356
+ unparseable connection-state payload is still queued, but the guarded parse returns nothing and no
357
+ rotation occurs.
358
+ - A malformed stats-report item is rejected before queueing: the unguarded anonymization parse throws
359
+ out of `addMetrics`, so neither the queue nor the force-send flag changes.
360
+ - There is no terminal state other than close. A failed connection returns to the start of the
361
+ lifecycle rather than ending it.
362
+
363
+ <!-- Include if: callers depend on a wire protocol or serialized format. [condition-id: module.exposes_wire_protocol] -->
364
+
365
+ ## Protocol and wire format
366
+
367
+ | Message or frame | Version | Producer or serializer | Consumer or parser | Compatibility and ordering rule |
368
+ | ------------------------------ | ------- | --------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
369
+ | WebRTC media metrics batch | `1.1.0` | `src/rtcMetrics/index.ts` | Webex unified telemetry ingestion | The version is carried in the payload. Field removal or rename is breaking for ingestion; additions are compatible |
370
+ | Serialized stats item | n/a | The media engine | Webex unified telemetry ingestion | Opaque to this module except for candidate address fields. Items are submitted in arrival order within a batch |
371
+
372
+ The batch is posted as the single element of a metrics array, with the media type and the registered
373
+ application identifier carried as request headers rather than in the body. The attribute shape is
374
+ declared in `src/metrics.types.ts` and the identifier value in `src/rtcMetrics/constants.ts`; neither
375
+ is restated here.
376
+
377
+ ## Pitfalls and constraints
378
+
379
+ - `sendMetrics` neither returns nor catches the telemetry request, so a failed POST becomes an
380
+ unhandled promise rejection in the host page rather than a logged failure. With unhandled exception
381
+ telemetry enabled, that rejection is itself captured and reported as an application failure.
382
+ - Only a connection-state value of exactly `failed` rotates the connection identifier. `disconnected`
383
+ and `closed` states are queued like any other item and keep the current identifier.
384
+ - When both a meeting identifier and a call identifier are present, only the meeting identifier is
385
+ sent; `updateCallId` therefore has no visible effect on a meeting-scoped instance.
386
+ - The 30-second interval starts in the constructor and is cleared only by `closeMetrics`. An instance
387
+ that is dropped without being closed keeps its timer and its queue alive.
388
+ - Callers must pass stats-report items as serialized JSON. A malformed item throws out of
389
+ `addMetrics` into the media-engine callback that delivered it.
390
+
391
+ <!-- Include if: the module has conventions beyond repository-wide rules. [condition-id: module.module_specific_conventions] -->
392
+
393
+ ## Module-specific rules
394
+
395
+ - Do: keep stats items as JSON strings. Parse one item, rewrite only what must change, and
396
+ re-serialize. Building an object model of a statistics report would couple this module to a schema
397
+ the media engine owns.
398
+ - Do: flush before rotating the connection identifier, and flush before clearing the interval. Both
399
+ orderings are load-bearing, not incidental.
400
+ - Do: use the browser interval explicitly, as the current code does, so the handle keeps its browser
401
+ timer type.
402
+ - Do not: add a field to the batch without confirming the unified telemetry service accepts it. The
403
+ payload declares its own version for exactly this reason.
404
+ - Do not: return or await the submission promise from any public operation. Every operation returns
405
+ void so that telemetry cannot delay or fail the media path.
406
+ - Do not: rely on this class outside a browser. It calls the browser interval API directly at
407
+ construction time.
408
+
409
+ <!-- Include if: the module is published or consumed as a package. [condition-id: module.published_package] -->
410
+
411
+ ## Export stability
412
+
413
+ | Export or entry point | Consumer | Stability | Versioning and deprecation rule | Declaration or API report |
414
+ | --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ----------------------------- |
415
+ | `RtcMetrics` | `packages/@webex/plugin-meetings/src/media/index.ts` | Internal plugin export; additive | Constructor arity and the public operation names are depended on by the meetings layer; change them together in one monorepo change | `src/index.ts` |
416
+ | `IdType` | `packages/@webex/plugin-meetings/src/meeting/index.ts` | Internal type export | Declared in `src/metrics.types.ts` and re-exported from the package entry | `src/metrics.types.ts` |
417
+
418
+ <!-- Include if: the module has a non-obvious design trade-off consumers or maintainers must preserve. [condition-id: module.has_design_tradeoff] -->
419
+
420
+ ## Key design trade-off
421
+
422
+ | Chosen trade-off | Preserved invariant or benefit | Cost or limitation | Decision evidence |
423
+ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
424
+ | Submit the first stats report immediately instead of waiting for the first interval | A user who abandons a call in the first thirty seconds — often because media quality is bad — is still represented in the data | One extra request per connection, and the first batch is smaller than a full window | `src/rtcMetrics/index.ts` |
425
+ | Post directly rather than through the batcher family used by the rest of the package | No queueing, retry or prepare step stands between the media engine and a sampled statistics batch | No retry and no backoff: a failed request loses that batch permanently, where a batcher would have re-enqueued it | `src/rtcMetrics/index.ts` |
426
+ | Rotate the connection identifier on failure rather than keeping one identifier per call | Consumers can tell one media connection attempt from the next, which is what makes a reconnect analyzable | Correlating a whole call requires joining batches on the correlation identifier instead of the connection identifier | `src/rtcMetrics/index.ts` |
427
+ | Treat stats items as opaque strings | A schema change in the media engine needs no change here | The module cannot validate or reduce what it forwards, and candidate anonymization has to parse and re-serialize to reach the fields | `src/rtcMetrics/index.ts` |
428
+
429
+ ## Verification
430
+
431
+ | Requirement or invariant | Test level | Positive evidence | Negative or boundary evidence | Gap |
432
+ | ------------------------ | ---------- | ------------------------------------ | ------------------------------------- | --------------------------------------------------------------------------------------------- |
433
+ | `MOD-001` | Unit | `test/unit/spec/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none |
434
+ | `MOD-002` / `INV-003` | Unit | `test/unit/spec/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none |
435
+ | `MOD-003` / `INV-001` | Unit | `test/unit/spec/rtcMetrics/index.ts` | none found | No case asserts that a non-candidate stats item is left untouched, so `INV-002` is partly unverified |
436
+ | `MOD-004` | Unit | `test/unit/spec/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none |
437
+ | `MOD-005` / `INV-004` | Unit | `test/unit/spec/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none |
438
+ | `MOD-006` | Unit | `test/unit/spec/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none |
439
+ | `MOD-007` / `INV-005` | Unit | `test/unit/spec/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none |
440
+ | `MOD-008` | Unit | `test/unit/spec/rtcMetrics/index.ts` | none found | No case asserts the meeting identifier still wins after the call identifier is replaced |
441
+ | `MOD-009` | Unit | `test/unit/spec/rtcMetrics/index.ts` | `test/unit/spec/rtcMetrics/index.ts` | none |
442
+ | `MOD-010` | Unit | none found | none found | Neither the throwing stats-report parse nor the guarded connection-state parse has a case |
443
+ | `MOD-011` | Unit | none found | none found | No case rejects the telemetry request and asserts the caller is unaffected |
444
+
445
+ Record coverage gaps explicitly and link follow-up work. A module specification
446
+ is complete only when its public surface, invariants, failure modes, and test
447
+ evidence agree with the implementation.