@observertc/observer-js 1.0.0-beta.16 → 1.0.0-beta.18

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.
package/dist/index.d.ts CHANGED
@@ -2376,8 +2376,117 @@ type OperationSystem = {
2376
2376
  };
2377
2377
 
2378
2378
  /**
2379
- * A finding raised **by the observer** — `observedCall.addIssue()` / `observer.addIssue()`, surfaced
2380
- * on the bus as `call-issue` / `observer-issue`.
2379
+ * Turning a correlation into a **conclusion**.
2380
+ *
2381
+ * Every detector in this library ultimately reports the same shape of observation: *N clients have
2382
+ * issue X open at once, and here is what they have in common*. That is useful but not yet
2383
+ * actionable — someone still has to know that congestion spread across unrelated calls means the
2384
+ * server, while CPU limitation spread across unrelated calls means a bad client release. This module
2385
+ * holds that interpretation step so it is stated once, consistently, instead of being re-derived by
2386
+ * whoever reads the alert at 3am.
2387
+ *
2388
+ * ### Two functions, because there are two questions
2389
+ *
2390
+ * A detector already knows its scope — it was constructed with an `ObservedCall` or with the
2391
+ * `Observer`. Handing that scope back to a single generic function meant every caller supplied
2392
+ * fields the other scope needed and its own scope ignored: a call-scoped detector passing
2393
+ * `affectedCalls: 1, totalCalls: 1` forever, an observer-scoped one passing a participant ratio that
2394
+ * was deliberately never read. Placeholders like that are a standing invitation to read them as if
2395
+ * they meant something.
2396
+ *
2397
+ * So there are two entry points, each taking only the facts its scope actually has:
2398
+ *
2399
+ * - {@link concludeCallIssue} — within one call. The axis is *how much of the meeting*, and whether
2400
+ * the affected clients all subscribe to one published track.
2401
+ * - {@link concludeObserverIssue} — across calls. The axis is *how many independent calls*, which is
2402
+ * the only thing that separates "one bad room" from "our infrastructure".
2403
+ *
2404
+ * Neither the issue family nor the spread concludes anything alone: congestion in one call is a
2405
+ * meeting problem, congestion in six calls is an infrastructure problem, and the issue type is
2406
+ * identical in both.
2407
+ */
2408
+ /** Where the fault most likely sits, given who is affected. */
2409
+ type IssueFaultDomain =
2410
+ /** Independent calls affected at once — they share only the servers and the network. */
2411
+ 'infrastructure'
2412
+ /** One call, broadly affected — something that call shares (its SFU worker, room, or host). */
2413
+ | 'call'
2414
+ /** The subscribers of one published track — the publisher's path or the forwarding of it. */
2415
+ | 'published-track'
2416
+ /** A single endpoint — its own device or last mile. */
2417
+ | 'endpoint'
2418
+ /** Independent calls affected, but by something endpoints own — a client build, not a server. */
2419
+ | 'client-population'
2420
+ /** Not enough signal to attribute. */
2421
+ | 'unknown';
2422
+ /** A stated verdict, attached to the raised issue payload. */
2423
+ type IssueConclusion = {
2424
+ /** Where to look. */
2425
+ faultDomain: IssueFaultDomain;
2426
+ /** One line, written to be readable in an alert without opening a dashboard. */
2427
+ summary: string;
2428
+ /** What to check first. Omitted when the issue family is unknown to this module. */
2429
+ recommendation?: string;
2430
+ /**
2431
+ * How much the spread alone justifies the verdict, `0..1`. Not a probability — a coarse ranking
2432
+ * so alerting can threshold on it. More independent calls, or a tighter onset, means higher.
2433
+ */
2434
+ confidence: number;
2435
+ };
2436
+ /** The facts a **call-scoped** conclusion is drawn from. */
2437
+ type CallIssueSpread = {
2438
+ issueType: string;
2439
+ /** Distinct clients of this call with the issue open. */
2440
+ affectedClients: number;
2441
+ /** Participants in the call — the denominator. */
2442
+ totalClients: number;
2443
+ /** True when the onsets clustered — a shared trigger rather than drift. */
2444
+ onsetBurst: boolean;
2445
+ /**
2446
+ * Set when the affected clients are the subscriber set of **one published track**.
2447
+ *
2448
+ * The strongest call-scoped statement available: those clients share a publisher and nothing
2449
+ * else, so the receivers are exonerated and the source's path is implicated.
2450
+ */
2451
+ publishedTrackId?: string;
2452
+ };
2453
+ /** The facts an **observer-scoped** conclusion is drawn from. */
2454
+ type ObserverIssueSpread = {
2455
+ issueType: string;
2456
+ /** Distinct clients across the fleet with the issue open. */
2457
+ affectedClients: number;
2458
+ /** Clients in the fleet. Reported for context; it does not gate anything at this scope. */
2459
+ totalClients: number;
2460
+ /** Distinct calls containing at least one affected client. The dimension that matters here. */
2461
+ affectedCalls: number;
2462
+ /** Calls in flight. */
2463
+ totalCalls: number;
2464
+ /** True when the onsets clustered. */
2465
+ onsetBurst: boolean;
2466
+ };
2467
+ /**
2468
+ * Draw the conclusion for a group of clients **within one call**.
2469
+ *
2470
+ * Ordered most-to-least specific: a track-scoped group is a stronger statement than a call-wide one,
2471
+ * and a single affected endpoint is not a statement about the call at all.
2472
+ */
2473
+ declare function concludeCallIssue(spread: CallIssueSpread): IssueConclusion;
2474
+ /**
2475
+ * Draw the conclusion for a group of clients spanning **several calls**.
2476
+ *
2477
+ * One affected call is not an observer-scoped finding — it has an obvious local explanation and the
2478
+ * call-scoped detector has already reported it — so that case returns `call` and says so rather than
2479
+ * dressing it up as a fleet event.
2480
+ *
2481
+ * Which domain breadth implicates depends on the family, and this is the whole reason the module
2482
+ * exists: `congestion` across unrelated calls points at the servers, `cpulimitation` across unrelated
2483
+ * calls points at what those *endpoints* share — a client release, a browser version, shared
2484
+ * virtualised hardware — and pointing an SFU team at the second one wastes a night.
2485
+ */
2486
+ declare function concludeObserverIssue(spread: ObserverIssueSpread): IssueConclusion;
2487
+
2488
+ /**
2489
+ * What every server-raised finding carries, whatever raised it.
2381
2490
  *
2382
2491
  * ### Why this is not `ClientIssue`
2383
2492
  *
@@ -2385,38 +2494,209 @@ type OperationSystem = {
2385
2494
  * string. Server-raised findings were reusing it, which forced every detector to `JSON.stringify` a
2386
2495
  * perfectly good object on the way out and every handler to `JSON.parse` it back on the way in —
2387
2496
  * paying serialisation on a path where nothing is ever serialised, and losing type information in
2388
- * both directions.
2389
- *
2390
- * An observer issue goes straight to an in-process event handler, so it carries the object.
2497
+ * both directions. These go straight to an in-process handler, so they carry the object.
2391
2498
  */
2392
- type ObserverIssue = {
2499
+ type IssueBase = {
2393
2500
  /** What was found, e.g. `'CROSS_CALL_ISSUE_ONSET_BURST'`. */
2394
2501
  type: string;
2395
2502
  /** Observer clock, when the finding was raised. */
2396
2503
  timestamp: number;
2397
2504
  /**
2398
- * The evidence behind the finding.
2505
+ * What the finding *means* — where to look, and how much the evidence justifies it.
2399
2506
  *
2400
- * Prefer an object — that is the point of this type. `string` is still accepted so an application
2401
- * can forward a payload it already has serialised (e.g. relaying a `ClientIssue`) without a
2402
- * pointless parse-then-restringify round trip. Use {@link issuePayloadOf} to read either form.
2507
+ * A first-class field rather than a key inside {@link payload}, because it is the one part every
2508
+ * finding has in common and the one part an alerting rule reads. Burying it in the evidence made
2509
+ * `payload.conclusion.faultDomain` the path to the most important thing in the object.
2510
+ */
2511
+ conclusion?: IssueConclusion;
2512
+ /**
2513
+ * The evidence, and **only** the evidence.
2514
+ *
2515
+ * Deliberately does not repeat `type`, `scope`, or the ids already present on the event that
2516
+ * delivers it. A payload that restates its envelope invites the two to disagree — and they did,
2517
+ * because nothing kept them in step.
2518
+ */
2519
+ payload?: Record<string, unknown>;
2520
+ };
2521
+ /**
2522
+ * A finding about **one call**, raised by `observedCall.addIssue()` and delivered as `call-issue`.
2523
+ *
2524
+ * The call is the event's scope (`{ observedCall, observer }`), so the payload does not carry a
2525
+ * `callId` — read it from the event.
2526
+ */
2527
+ type CallIssue = IssueBase & {
2528
+ scope: 'call';
2529
+ };
2530
+ /**
2531
+ * A finding about the **fleet**, raised by `observer.addIssue()` and delivered as `observer-issue`.
2532
+ *
2533
+ * Raised by detectors and validators that reason across calls, so no single call owns it. Where the
2534
+ * finding does concern specific calls — a cross-call correlation, say — they are named in the
2535
+ * evidence, because that *is* the evidence.
2536
+ */
2537
+ type ObserverIssue = IssueBase & {
2538
+ scope: 'observer';
2539
+ };
2540
+ /**
2541
+ * Either kind, discriminated by {@link IssueBase} + `scope`.
2542
+ *
2543
+ * `scope` is on the issue and not merely implied by which event fired, so a finding stays
2544
+ * self-describing once it leaves the bus — funnelled into one handler, a log line, or a queue.
2545
+ */
2546
+ type Issue = CallIssue | ObserverIssue;
2547
+ /**
2548
+ * The payload as a JSON string, for the boundaries that genuinely need text — a log line, an HTTP
2549
+ * body, a message queue.
2550
+ *
2551
+ * Returns `undefined` for a missing payload, or for one that cannot be serialised (a circular
2552
+ * reference from something an application attached): the caller wanted text, not an exception.
2553
+ */
2554
+ declare function issuePayloadAsString(issue: Pick<IssueBase, 'payload'>): string | undefined;
2555
+
2556
+ /**
2557
+ * The bus events that carry an `observedCall`, i.e. the ones an enricher can attribute to a summary.
2558
+ *
2559
+ * Derived from the event map rather than listed by hand, so it cannot drift: adding a call-scoped
2560
+ * event makes it enrichable automatically, and an enricher on an observer-scoped event
2561
+ * (`observer-issue`, `validation-ready`) will not compile — there is no single call it belongs to,
2562
+ * and quietly writing a fleet-wide fact into every open summary would be worse than a type error.
2563
+ */
2564
+ type CallScopedEventName = {
2565
+ [K in keyof ObserverEvents]: ObserverEvents[K][0] extends ObservedCallScope ? K : never;
2566
+ }[keyof ObserverEvents];
2567
+ /** A function that folds one event into the summary. Runs on every occurrence, for its own call. */
2568
+ type CallSummaryEnricher<K extends CallScopedEventName> = (summary: CallSummary, ...args: ObserverEvents[K]) => void;
2569
+ /** The enricher map: any subset of the call-scoped events, each fully typed against its payload. */
2570
+ type CallSummaryEnrichers = {
2571
+ [K in CallScopedEventName]?: CallSummaryEnricher<K>;
2572
+ };
2573
+ /** The built-in sections. Ask for what you want; anything absent is simply not collected. */
2574
+ type CallSummarySection = 'clients' | 'issues' | 'turnServers' | 'scores';
2575
+ type CallSummaryConfig = {
2576
+ /**
2577
+ * Which built-in sections to accumulate. Empty (the default) collects none of them — a summary
2578
+ * with only `enrich` is a perfectly good summary.
2403
2579
  */
2404
- payload?: string | Record<string, unknown>;
2580
+ include: CallSummarySection[];
2581
+ /** Fold arbitrary state in from any call-scoped event. See {@link CallSummaryEnrichers}. */
2582
+ enrich?: CallSummaryEnrichers;
2583
+ /** Cap on the retained issue log. Default `500`. See the note on truncation. */
2584
+ maxIssues: number;
2585
+ /** Cap on retained client ids. Default `10_000`. */
2586
+ maxClientIds: number;
2405
2587
  };
2406
2588
  /**
2407
- * The payload of an issue as an object, parsing it only if it happens to be a string.
2589
+ * Who was in the call over its whole life — not just who is in it now.
2408
2590
  *
2409
- * Handlers shouldn't have to care which form arrived. Returns `undefined` for a missing payload or a
2410
- * string that isn't valid JSON — reading evidence must never throw inside an issue handler.
2591
+ * Deliberately identifiers and counts only. Anything *about* a client — browser, platform, region —
2592
+ * is already on `observedClient` while the call is live, and belongs in `attachments` via an enricher
2593
+ * if you want it kept. Duplicating it here would mean the library deciding which client attributes
2594
+ * matter, and it would mean reading the highest-frequency event on the bus to do it.
2411
2595
  */
2412
- declare function issuePayloadOf(issue: Pick<ObserverIssue, 'payload'>): Record<string, unknown> | undefined;
2596
+ type CallSummaryClients = {
2597
+ /** Every client id seen, in join order, capped by `maxClientIds`. */
2598
+ clientIds: string[];
2599
+ /** The most participants present at any one moment. */
2600
+ peak: number;
2601
+ joined: number;
2602
+ left: number;
2603
+ };
2604
+ /** Which TURN relays carried this call's media. */
2605
+ type CallSummaryTurnServers = {
2606
+ serverUrls: string[];
2607
+ /** Distinct clients seen relaying through any of them. */
2608
+ clientsRelayed: number;
2609
+ };
2610
+ /** The call score over time. Percentiles, not a mean — see `utils/stats`. */
2611
+ type CallSummaryScores = {
2612
+ min?: number;
2613
+ max?: number;
2614
+ median?: number;
2615
+ /** How many score readings went into the above. `0` means nothing was measured. */
2616
+ samples: number;
2617
+ };
2618
+ /** What had to be dropped to stay within the caps. Absent when nothing was. */
2619
+ type CallSummaryTruncation = {
2620
+ issues?: number;
2621
+ clientIds?: number;
2622
+ };
2413
2623
  /**
2414
- * The payload as a JSON string, serialising it only if it is an object.
2624
+ * An accumulating record of one call's life, finalised when the call closes.
2625
+ *
2626
+ * ### Why this exists
2627
+ *
2628
+ * Everything else in this library is about *now*. Detectors answer "is something wrong right now",
2629
+ * validators answer a structural question once, and both read state that the call throws away when
2630
+ * it ends. Nothing kept the answer to *"what happened in that meeting?"* — who was in it, what was
2631
+ * raised, how it scored — and that is the question asked after the call, by support, by billing, by
2632
+ * whoever is writing the incident note.
2415
2633
  *
2416
- * For the boundaries that genuinely need text — a log line, an HTTP body, a message queue. Keep it at
2417
- * the edge rather than in the detector, so in-process handlers never pay for it.
2634
+ * ### It is opt-in, and its sections are opt-in
2635
+ *
2636
+ * `observedCall.summary` is `undefined` unless a summary was configured, and each section is present
2637
+ * only if it was requested. **An absent section means "not collected", never "nothing happened"** —
2638
+ * the same rule as `inconclusive` on a validator. Reading `summary.issues` as "this call had no
2639
+ * issues" when `'issues'` was never in `include` is the one misreading this type invites, so it does
2640
+ * not offer a default-empty section to make it easy.
2641
+ *
2642
+ * ### Read it live, receive it once
2643
+ *
2644
+ * The object is live: read `observedCall.summary` at any point during the call. It is also delivered
2645
+ * on the `call-summary` event, emitted inside `close()` while the call is still reachable — after
2646
+ * that the call is gone from `observer.observedCalls` and there is nothing left to ask.
2418
2647
  */
2419
- declare function issuePayloadAsString(issue: Pick<ObserverIssue, 'payload'>): string | undefined;
2648
+ type CallSummary = {
2649
+ callId: string;
2650
+ /** First client join (client clock), as `ObservedCall` computed it. */
2651
+ startedAt?: number;
2652
+ /** Last client leave. */
2653
+ endedAt?: number;
2654
+ /** `endedAt - startedAt`, when both are known. */
2655
+ durationInMs?: number;
2656
+ /** When the summary itself was finalised (observer clock). Set by `close()`. */
2657
+ closedAt?: number;
2658
+ clients?: CallSummaryClients;
2659
+ /**
2660
+ * Every issue raised against this call, in the order they were raised, capped by `maxIssues`.
2661
+ *
2662
+ * Just the issues — no derived tallies. A count is `issues.length`, a per-type count is one
2663
+ * `filter`, and either is cheaper to write at the call site than to keep correct here. The one
2664
+ * thing you cannot derive is what the cap discarded, which is why `truncated.issues` exists:
2665
+ * the issues actually raised is `issues.length + (truncated?.issues ?? 0)`.
2666
+ */
2667
+ issues?: CallIssue[];
2668
+ turnServers?: CallSummaryTurnServers;
2669
+ scores?: CallSummaryScores;
2670
+ /**
2671
+ * Whatever your enrichers put here. The library never writes to it, so it cannot collide with a
2672
+ * section added in a future version.
2673
+ *
2674
+ * **`attachments`, not `appData`, and the distinction is load-bearing.** `appData` is the live
2675
+ * working state an application hangs off an entity for the entity's lifetime, and it may hold
2676
+ * references that cannot be serialised — a mediasoup router, an `RTCPeerConnection`, a socket. A
2677
+ * summary is the opposite: it outlives the call precisely so it can be *shipped* — archived,
2678
+ * queued, written to a column — and it is handed to you on `call-summary` at the moment the call
2679
+ * it came from is being torn down. Anything unserialisable in it is a reference to something
2680
+ * already gone.
2681
+ *
2682
+ * So put serialisable facts here, the same contract as `attachments` on a `ClientSample`. If you
2683
+ * need the live object, read it off `observedCall` / `observedClient` inside the enricher and
2684
+ * attach what you can serialise: the router's `id`, not the router.
2685
+ */
2686
+ attachments: Record<string, unknown>;
2687
+ /**
2688
+ * What the caps discarded, and how much.
2689
+ *
2690
+ * Present **only** when something was actually dropped. A silently truncated summary is worse
2691
+ * than no summary — someone will count `log.length` and report it as the issue count — so the
2692
+ * shortfall is stated rather than left to be inferred from a suspiciously round number.
2693
+ */
2694
+ truncated?: CallSummaryTruncation;
2695
+ };
2696
+ /** The defaults a summary is created with. Caps are generous but finite; see {@link CallSummary}. */
2697
+ declare const defaultCallSummaryConfig: CallSummaryConfig;
2698
+ /** A fresh summary for `callId`, with only the requested sections present. */
2699
+ declare function createCallSummary(callId: string, config: CallSummaryConfig): CallSummary;
2420
2700
 
2421
2701
  /**
2422
2702
  * What a validator concluded, once it is done.
@@ -2964,7 +3244,15 @@ type ObserverEvents = {
2964
3244
  'call-empty': [ObservedCallScope];
2965
3245
  'call-not-empty': [ObservedCallScope];
2966
3246
  'call-issue': [ObservedCallScope & {
2967
- issue: ObserverIssue;
3247
+ issue: CallIssue;
3248
+ }];
3249
+ /**
3250
+ * A call's summary was finalised. Emitted from inside `close()`, while the call is still in
3251
+ * `observer.observedCalls` — after that there is nothing left to ask. Only fires for calls that
3252
+ * had a summary configured.
3253
+ */
3254
+ 'call-summary': [ObservedCallScope & {
3255
+ summary: CallSummary;
2968
3256
  }];
2969
3257
  'client-added': [ObservedClientScope];
2970
3258
  'client-sink-created': [ObservedClientScope & {
@@ -4575,6 +4863,14 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
4575
4863
  readonly clientsUsedTurn: Set<string>;
4576
4864
  readonly calculatedScore: CalculatedScore;
4577
4865
  remoteTrackResolver?: RemoteTrackResolver;
4866
+ /**
4867
+ * The accumulating record of this call's life, or `undefined` when no summary was configured.
4868
+ *
4869
+ * Live — read it at any point during the call. It is also delivered once on `call-summary` when
4870
+ * the call closes. See `CallSummary`: an absent section means "not collected", never "nothing
4871
+ * happened".
4872
+ */
4873
+ summary?: CallSummary;
4578
4874
  /**
4579
4875
  * Published tracks that currently have **no** subscriber linked to them.
4580
4876
  *
@@ -4614,6 +4910,19 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
4614
4910
  * it back off the registry: `call.detectors.getAll(name)`, or `call.detectors.instances`.
4615
4911
  */
4616
4912
  addDetector<K extends keyof AvailableCallScopeDetectorsConfigs>(name: K, config?: Partial<AvailableCallScopeDetectorsConfigs[K]>): this;
4913
+ /**
4914
+ * Start accumulating this call's summary, if the observer was configured for summaries.
4915
+ *
4916
+ * Called by `createObservedCall`; you should not need it. It takes no configuration of its own on
4917
+ * purpose: the collector subscribes to exactly the events the observer's `include` requires, so a
4918
+ * per-call section outside that set would be created and then never written to — an empty section
4919
+ * that reads as "nothing happened". One shape per observer is the only shape that can be filled.
4920
+ *
4921
+ * The collector builds it rather than this method, so the resolved configuration never has to
4922
+ * leave the one object that owns it. Returns `undefined` when summaries are off, and is
4923
+ * idempotent: an existing summary is kept, not restarted.
4924
+ */
4925
+ enableSummary(): CallSummary | undefined;
4617
4926
  /**
4618
4927
  * Remove a detector from **this call** by name, returning how many were removed.
4619
4928
  *
@@ -4636,14 +4945,15 @@ declare class ObservedCall<AppData extends Record<string, unknown> = Record<stri
4636
4945
  /**
4637
4946
  * Raise a call-level (server-side) finding; surfaced on the Observer bus as `call-issue`.
4638
4947
  *
4639
- * `payload` takes an **object** — it is delivered to in-process handlers, so there is nothing to
4640
- * serialise for. Pass a string only if you already have one.
4948
+ * `payload` is an **object** and holds evidence only — it is delivered to an in-process handler,
4949
+ * so there is nothing to serialise for. `scope` is stamped here, and the `callId` is already on
4950
+ * the event, so neither belongs in the payload. Put the interpretation in `conclusion`.
4641
4951
  */
4642
- addIssue(issue: ObserverIssue): void;
4952
+ addIssue(issue: Omit<CallIssue, 'scope'>): void;
4643
4953
  close(): void;
4644
4954
  getObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(clientId: string): ObservedClient<ClientAppData> | undefined;
4645
- createObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedClientSettings<ClientAppData>): ObservedClient<ClientAppData> | undefined;
4646
- getOrCreateObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedClientSettings<ClientAppData>): ObservedClient<ClientAppData> | undefined;
4955
+ createObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedClientSettings<ClientAppData>, acceptCtx?: AcceptContext): ObservedClient<ClientAppData> | undefined;
4956
+ getOrCreateObservedClient<ClientAppData extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedClientSettings<ClientAppData>, acceptCtx?: AcceptContext): ObservedClient<ClientAppData> | undefined;
4647
4957
  update(context?: AcceptContext): void;
4648
4958
  private _onClientUpdate;
4649
4959
  private _clientJoined;
@@ -5040,6 +5350,62 @@ type AvailableValidatorConfigs = {
5040
5350
  /** A validator name that can be started. */
5041
5351
  type ValidatorName = keyof AvailableValidatorConfigs;
5042
5352
 
5353
+ /**
5354
+ * Keeps every configured `CallSummary` up to date, from **observer-level** bus subscriptions.
5355
+ *
5356
+ * ### Why one collector and not one per call
5357
+ *
5358
+ * The obvious implementation subscribes each call's summary to the events it needs. But the bus is
5359
+ * observer-wide: a listener attached for call A is invoked for every event of every call, so that
5360
+ * design costs `calls × events` listeners *and* `calls` invocations per event — quadratic in the
5361
+ * thing most likely to be large. At 500 concurrent calls and eight subscribed events that is 4 000
5362
+ * listeners doing 500 no-op calls each, per event.
5363
+ *
5364
+ * So the collector attaches **one listener per event type, once**, and routes each event to the
5365
+ * summary of the call it names. Cost is O(subscribed event types), independent of how many calls are
5366
+ * in flight, and an event for a call with no summary costs one `undefined` check.
5367
+ *
5368
+ * ### Only call-scoped events
5369
+ *
5370
+ * Routing needs `observedCall` on the payload, which is exactly what `CallScopedEventName` selects.
5371
+ * Observer-scoped events have no single call to attribute to; see that type for why fanning them out
5372
+ * to every open summary would be worse than refusing.
5373
+ */
5374
+ declare class CallSummaryCollector {
5375
+ private readonly _observer;
5376
+ private readonly _config;
5377
+ private readonly _scratch;
5378
+ private readonly _listeners;
5379
+ private _closed;
5380
+ constructor(_observer: Observer, _config: CallSummaryConfig);
5381
+ /**
5382
+ * Build a summary for `callId` and start tracking it.
5383
+ *
5384
+ * Creating it here, rather than letting the call create one and hand it over, keeps the resolved
5385
+ * configuration inside the single object that owns it — and makes it impossible to end up with a
5386
+ * summary whose sections nobody subscribed to fill.
5387
+ */
5388
+ createSummary(callId: string): CallSummary;
5389
+ /**
5390
+ * Finalise `call`'s summary: fold in what only makes sense once, and stamp the closing times.
5391
+ *
5392
+ * Percentiles are computed here rather than on every update — a median recomputed per tick over a
5393
+ * growing array is quadratic work to produce a number nobody reads until the end.
5394
+ */
5395
+ finalise(call: ObservedCall): void;
5396
+ /** Drop every bus subscription. Called when the observer closes. */
5397
+ close(): void;
5398
+ /**
5399
+ * Subscribe `listener` to `event`, routed to the summary of the call the event names.
5400
+ *
5401
+ * The `observedCall` is read off the payload rather than closed over, which is what lets one
5402
+ * subscription serve every call.
5403
+ */
5404
+ private _on;
5405
+ private _subscribeBuiltIns;
5406
+ private _subscribeEnrichers;
5407
+ }
5408
+
5043
5409
  type SampleRejectedReason = 'observer-closed' | 'missing-callId' | 'missing-clientId';
5044
5410
 
5045
5411
  /**
@@ -5064,11 +5430,13 @@ type AcceptMiddleware = Middleware<AcceptMiddlewarePayload>;
5064
5430
  type CallAppDataFactory = (params: {
5065
5431
  callId: string;
5066
5432
  observer: Observer;
5433
+ acceptCtx?: AcceptContext;
5067
5434
  }) => Record<string, unknown>;
5068
5435
  /** Produces the initial `appData` for a client created without an explicit `appData`. */
5069
5436
  type ClientAppDataFactory = (params: {
5070
5437
  clientId: string;
5071
5438
  observedCall: ObservedCall;
5439
+ acceptCtx?: AcceptContext;
5072
5440
  }) => Record<string, unknown>;
5073
5441
  type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unknown>> = {
5074
5442
  appData?: AppData;
@@ -5083,6 +5451,36 @@ type ObserverConfig<AppData extends Record<string, unknown> = Record<string, unk
5083
5451
  * yourself, and note that observer-scoped detectors and validators run *nowhere else*.
5084
5452
  */
5085
5453
  autoUpdateOnCallUpdate?: boolean;
5454
+ /**
5455
+ * Accumulate a {@link CallSummary} on every call this observer creates.
5456
+ *
5457
+ * **Absent or `null` means no summaries at all** — no accumulation, and not one bus subscription.
5458
+ * Pass an object (`{}` is valid) to switch it on; anything you leave out takes its default from
5459
+ * `defaultCallSummaryConfig`, including `include: []`, which collects *no* built-in section. A
5460
+ * summary that only runs `enrich` is a perfectly good summary.
5461
+ *
5462
+ * ```ts
5463
+ * const observer = new Observer({
5464
+ * callSummary: {
5465
+ * include: [ 'clients', 'issues' ],
5466
+ * enrich: {
5467
+ * 'client-joined': (summary, { observedClient }) => {
5468
+ * ((summary.attachments.regions ??= []) as string[]).push(String(observedClient.appData.region));
5469
+ * },
5470
+ * },
5471
+ * },
5472
+ * });
5473
+ *
5474
+ * observer.on('call-summary', ({ summary }) => archive(summary));
5475
+ * ```
5476
+ *
5477
+ * This is construction-time and fixed for the observer's life, unlike detectors, which are added
5478
+ * per call as an application decides what to watch. A summary is a record of what happened, and a
5479
+ * record you can turn on halfway through is a record with a hole in it — calls that started
5480
+ * earlier would carry different sections from calls that started later, with nothing on either to
5481
+ * say which. One shape for every call, or none.
5482
+ */
5483
+ callSummary?: Partial<CallSummaryConfig> | null;
5086
5484
  inboundTrackDegradationThresholds?: {
5087
5485
  deltaFreezeCount: number;
5088
5486
  framesDroppedRatio: number;
@@ -5177,6 +5575,14 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5177
5575
  * ```
5178
5576
  */
5179
5577
  readonly callDetectorConfigs: Map<keyof AvailableCallScopeDetectorsConfigs, Partial<UnconsumedTrackDetectorConfig | TrackDeliveryMismatchDetectorConfig | CallConcurrentIssueDetectorConfig | IssueFanOutDetectorConfig | PublisherFaultCorroborationDetectorConfig>>;
5578
+ /**
5579
+ * Owns every call's summary: the resolved `config.callSummary`, the bus subscriptions that keep
5580
+ * the summaries current (one per event type, not one per call), and the summaries themselves.
5581
+ *
5582
+ * `undefined` when `config.callSummary` was absent or `null` — so its presence *is* the answer to
5583
+ * "are summaries on", and nothing is subscribed to anything.
5584
+ */
5585
+ readonly callSummaryCollector?: CallSummaryCollector;
5180
5586
  constructor(config?: Partial<ObserverConfig<AppData>>);
5181
5587
  get numberOfCalls(): number;
5182
5588
  get appData(): AppData | undefined;
@@ -5257,8 +5663,13 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5257
5663
  */
5258
5664
  cancelValidator(target: keyof AvailableValidatorConfigs | RunningValidator, reason?: string): number;
5259
5665
  getObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(callId: string): ObservedCall<T> | undefined;
5260
- createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
5261
- getOrCreateObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>): ObservedCall<T> | undefined;
5666
+ /**
5667
+ * @param acceptCtx the `accept()` context, when this call is being created to receive a sample.
5668
+ * Passed on to `ObserverConfig.createCallAppData`, so the factory can read whatever the caller (or
5669
+ * an accept middleware) put there — a tenant, a region, a trace id.
5670
+ */
5671
+ createObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>, acceptCtx?: AcceptContext): ObservedCall<T> | undefined;
5672
+ getOrCreateObservedCall<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedCallSettings<T>, acceptCtx?: AcceptContext): ObservedCall<T> | undefined;
5262
5673
  createObservedMediasoupRouter<T extends Record<string, unknown> = Record<string, unknown>>(settings: ObservedMediasoupRouterSettings<T> & {
5263
5674
  matchPeerConnectionByWebRtcTransportId?: boolean;
5264
5675
  }): ObservedMediasoupRouter<Record<string, unknown>> | undefined;
@@ -5271,7 +5682,7 @@ declare class Observer<AppData extends Record<string, unknown> = Record<string,
5271
5682
  *
5272
5683
  * `payload` takes an **object**; see `ObserverIssue`.
5273
5684
  */
5274
- addIssue(issue: ObserverIssue): void;
5685
+ addIssue(issue: Omit<ObserverIssue, 'scope'>): void;
5275
5686
  /** Emit an Observer-bus event. */
5276
5687
  private _notify;
5277
5688
  }
@@ -5386,116 +5797,6 @@ declare class CallHealthAggregator {
5386
5797
  private _clientHealth;
5387
5798
  }
5388
5799
 
5389
- /**
5390
- * Turning a correlation into a **conclusion**.
5391
- *
5392
- * Every detector in this library ultimately reports the same shape of observation: *N clients have
5393
- * issue X open at once, and here is what they have in common*. That is useful but not yet
5394
- * actionable — someone still has to know that congestion spread across unrelated calls means the
5395
- * server, while CPU limitation spread across unrelated calls means a bad client release. This module
5396
- * holds that interpretation step so it is stated once, consistently, instead of being re-derived by
5397
- * whoever reads the alert at 3am.
5398
- *
5399
- * ### Two functions, because there are two questions
5400
- *
5401
- * A detector already knows its scope — it was constructed with an `ObservedCall` or with the
5402
- * `Observer`. Handing that scope back to a single generic function meant every caller supplied
5403
- * fields the other scope needed and its own scope ignored: a call-scoped detector passing
5404
- * `affectedCalls: 1, totalCalls: 1` forever, an observer-scoped one passing a participant ratio that
5405
- * was deliberately never read. Placeholders like that are a standing invitation to read them as if
5406
- * they meant something.
5407
- *
5408
- * So there are two entry points, each taking only the facts its scope actually has:
5409
- *
5410
- * - {@link concludeCallIssue} — within one call. The axis is *how much of the meeting*, and whether
5411
- * the affected clients all subscribe to one published track.
5412
- * - {@link concludeObserverIssue} — across calls. The axis is *how many independent calls*, which is
5413
- * the only thing that separates "one bad room" from "our infrastructure".
5414
- *
5415
- * Neither the issue family nor the spread concludes anything alone: congestion in one call is a
5416
- * meeting problem, congestion in six calls is an infrastructure problem, and the issue type is
5417
- * identical in both.
5418
- */
5419
- /** Where the fault most likely sits, given who is affected. */
5420
- type IssueFaultDomain =
5421
- /** Independent calls affected at once — they share only the servers and the network. */
5422
- 'infrastructure'
5423
- /** One call, broadly affected — something that call shares (its SFU worker, room, or host). */
5424
- | 'call'
5425
- /** The subscribers of one published track — the publisher's path or the forwarding of it. */
5426
- | 'published-track'
5427
- /** A single endpoint — its own device or last mile. */
5428
- | 'endpoint'
5429
- /** Independent calls affected, but by something endpoints own — a client build, not a server. */
5430
- | 'client-population'
5431
- /** Not enough signal to attribute. */
5432
- | 'unknown';
5433
- /** A stated verdict, attached to the raised issue payload. */
5434
- type IssueConclusion = {
5435
- /** Where to look. */
5436
- faultDomain: IssueFaultDomain;
5437
- /** One line, written to be readable in an alert without opening a dashboard. */
5438
- summary: string;
5439
- /** What to check first. Omitted when the issue family is unknown to this module. */
5440
- recommendation?: string;
5441
- /**
5442
- * How much the spread alone justifies the verdict, `0..1`. Not a probability — a coarse ranking
5443
- * so alerting can threshold on it. More independent calls, or a tighter onset, means higher.
5444
- */
5445
- confidence: number;
5446
- };
5447
- /** The facts a **call-scoped** conclusion is drawn from. */
5448
- type CallIssueSpread = {
5449
- issueType: string;
5450
- /** Distinct clients of this call with the issue open. */
5451
- affectedClients: number;
5452
- /** Participants in the call — the denominator. */
5453
- totalClients: number;
5454
- /** True when the onsets clustered — a shared trigger rather than drift. */
5455
- onsetBurst: boolean;
5456
- /**
5457
- * Set when the affected clients are the subscriber set of **one published track**.
5458
- *
5459
- * The strongest call-scoped statement available: those clients share a publisher and nothing
5460
- * else, so the receivers are exonerated and the source's path is implicated.
5461
- */
5462
- publishedTrackId?: string;
5463
- };
5464
- /** The facts an **observer-scoped** conclusion is drawn from. */
5465
- type ObserverIssueSpread = {
5466
- issueType: string;
5467
- /** Distinct clients across the fleet with the issue open. */
5468
- affectedClients: number;
5469
- /** Clients in the fleet. Reported for context; it does not gate anything at this scope. */
5470
- totalClients: number;
5471
- /** Distinct calls containing at least one affected client. The dimension that matters here. */
5472
- affectedCalls: number;
5473
- /** Calls in flight. */
5474
- totalCalls: number;
5475
- /** True when the onsets clustered. */
5476
- onsetBurst: boolean;
5477
- };
5478
- /**
5479
- * Draw the conclusion for a group of clients **within one call**.
5480
- *
5481
- * Ordered most-to-least specific: a track-scoped group is a stronger statement than a call-wide one,
5482
- * and a single affected endpoint is not a statement about the call at all.
5483
- */
5484
- declare function concludeCallIssue(spread: CallIssueSpread): IssueConclusion;
5485
- /**
5486
- * Draw the conclusion for a group of clients spanning **several calls**.
5487
- *
5488
- * One affected call is not an observer-scoped finding — it has an obvious local explanation and the
5489
- * call-scoped detector has already reported it — so that case returns `call` and says so rather than
5490
- * dressing it up as a fleet event.
5491
- *
5492
- * Which domain breadth implicates depends on the family, and this is the whole reason the module
5493
- * exists: `congestion` across unrelated calls points at the servers, `cpulimitation` across unrelated
5494
- * calls points at what those *endpoints* share — a client release, a browser version, shared
5495
- * virtualised hardware — and pointing an SFU team at the second one wastes a night.
5496
- */
5497
- declare function concludeObserverIssue(spread: ObserverIssueSpread): IssueConclusion;
5498
-
5499
5800
  /** An entry retained by {@link SlidingWindow}. */
5500
5801
  type SlidingWindowEntry<T> = {
5501
5802
  timestamp: number;
@@ -5742,4 +6043,4 @@ declare function createInMemorySink(samples?: ClientSample[]): InMemorySink;
5742
6043
  declare function createDefaultMediasoupRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
5743
6044
  declare function createP2pRemoteTrackResolverFactory(): RemoteTrackResolverFactory;
5744
6045
 
5745
- export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssueSpread, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CorroboratedPublisherFault, type Detector, Detectors, InMemorySink, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, ObservedCall, type ObservedCallScope, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type PageHinkleyResult, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, RESOLVED_ISSUE_SUFFIX, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RunningValidator, type SampleRejectedReason, type ScoreCalculator, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, defaultClientHealthThresholds, isClientIssueResolutionEntry, issuePayloadAsString, issuePayloadOf, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfSorted, robustZScore, setObserverLogger, summarize };
6046
+ export { type AcceptContext, type AcceptMiddleware, type AcceptMiddlewarePayload, type ActiveClientIssue, type ActiveIssueTracker, ActiveIssuesRegistry, type AvailableCallScopeDetectorsConfigs, type AvailableDetectorsConfigs, type AvailableObserverScopeDetectorsConfigs, type AvailableValidatorConfigs, CODEC_MISMATCH_ISSUE, type CallAppDataFactory, CallConcurrentIssueDetector, type CallConcurrentIssueDetectorConfig, type CallConcurrentIssueGroup, CallConcurrentIssueTypes, type CallHealth, CallHealthAggregator, type CallIssue, type CallIssueSpread, type CallScopedEventName, type CallSummary, type CallSummaryClients, CallSummaryCollector, type CallSummaryConfig, type CallSummaryEnricher, type CallSummaryEnrichers, type CallSummaryScores, type CallSummarySection, type CallSummaryTruncation, type CallSummaryTurnServers, type ClientAppDataFactory, type ClientEvent, ClientEventTypes, type ClientHealth, type ClientHealthThresholds, type ClientIssue, type ClientMetaData, ClientMetaTypes, type ClientPopulation, type ClientPopulationAxis, ClientPopulationIssueDetector, type ClientPopulationIssueDetectorConfig, ClientPopulationIssueTypes, type ClientSample, ClientSampleSink, type ClientSampleSinkEvents, type ClientSampleSinkFactory, type CodecConsistencyReportPayload, CodecConsistencyValidator, type CodecConsistencyValidatorConfig, type CodecEvidence, type CorroboratedPublisherFault, type Detector, Detectors, InMemorySink, type Issue, type IssueBase, type IssueConclusion, IssueFanOutDetector, type IssueFanOutDetectorConfig, IssueFanOutTypes, type IssueFaultDomain, JsonlFileSink, type JsonlFileSinkFactoryOptions, type JsonlFileSinkOptions, LOWEST_COMMON_DENOMINATOR_ISSUE, type Logger, type MannKendallResult, type MediasoupConsumerSample, type MediasoupConsumerSampleEvent, type MediasoupDataConsumerSample, type MediasoupDataProducerSample, type MediasoupDirectTransportSample, type MediasoupDirectTransportSampleEventMap, type MediasoupPipeTransportSample, type MediasoupPipeTransportSampleEventMap, type MediasoupPlainTransportSample, type MediasoupPlainTransportSampleEventMap, type MediasoupProducerSample, type MediasoupProducerSampleEvent, type MediasoupRouterSample, type MediasoupSampleEnricher, type MediasoupTransportSample, type MediasoupWebRtcTransportSample, type MediasoupWebRtcTransportSampleEventMap, type Middleware, ObservedCall, type ObservedCallScope, ObservedCertificate, ObservedClient, ObservedClientIssueRegistry, type ObservedClientScope, ObservedCodec, ObservedDataChannel, ObservedIceCandidate, ObservedIceCandidatePair, ObservedIceTransport, ObservedInboundRtp, ObservedInboundTrack, ObservedMediaPlayout, ObservedMediaSource, ObservedMediasoupRouter, type ObservedMediasoupRouterEvents, type ObservedMediasoupRouterScope, type ObservedMediasoupRouterSettings, ObservedOutboundRtp, ObservedOutboundTrack, ObservedPeerConnection, type ObservedPeerConnectionScope, ObservedPeerConnectionTransport, ObservedRemoteInboundRtp, ObservedRemoteOutboundRtp, Observer, ObserverConcurrentIssueDetector, type ObserverConcurrentIssueDetectorConfig, type ObserverConcurrentIssueGroup, ObserverConcurrentIssueTypes, type ObserverEventBase, type ObserverEvents, type ObserverIssue, type ObserverIssueSpread, type ObserverLogger, type PageHinkleyResult, PublisherFaultCorroborationDetector, type PublisherFaultCorroborationDetectorConfig, PublisherFaultTypes, RESOLVED_ISSUE_SUFFIX, type RemoteTrackLinkEvidence, RemoteTrackResolver, type RemoteTrackResolverFactory, type RemoteTrackResolverReportPayload, RemoteTrackResolverValidator, type RemoteTrackResolverValidatorConfig, type RemoteTrackResolvers, type ResolvedActiveClientIssue, type RunningValidator, type SampleRejectedReason, type ScoreCalculator, SfuCongestionDetector, type SfuCongestionDetectorBucket, type SfuCongestionDetectorConfig, type SfuCongestionDetectorEvaluation, type SfuCongestionDetectorReport, type SimulcastReceiverEvidence, type SimulcastReceiverReportPayload, SimulcastReceiverValidator, type SimulcastReceiverValidatorConfig, SlidingWindow, type SlidingWindowEntry, type StatsSummary, TrackDeliveryMismatchDetector, type TrackDeliveryMismatchDetectorConfig, TrackDeliveryMismatchTypes, TrendTester, type TrendTesterConfig, type TurnServerHealth, TurnServerHealthDetector, type TurnServerHealthDetectorConfig, TurnServerHealthTypes, TurnServerOutageDetector, type TurnServerOutageDetectorConfig, TurnServerOutageTypes, UNRESOLVED_TRACK_LINKS_ISSUE, UnconsumedTrackDetector, type UnconsumedTrackDetectorConfig, UnconsumedTrackTypes, type ValidationReport, type Validator, type ValidatorName, baseIssueType, concludeCallIssue, concludeObserverIssue, correlation, counterDelta, createCallSummary, createDefaultMediasoupRemoteTrackResolverFactory, createInMemorySink, createJsonlFileSink, createJsonlFileSinkFactory, createLogger, createP2pRemoteTrackResolverFactory, defaultCallSummaryConfig, defaultClientHealthThresholds, isClientIssueResolutionEntry, issuePayloadAsString, mannKendall, mannKendallVerdict, median, medianAbsoluteDeviation, pageHinkley, percentile, percentileOfSorted, robustZScore, setObserverLogger, summarize };