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

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.mjs CHANGED
@@ -37,8 +37,8 @@ function createLogger(moduleName) {
37
37
  }
38
38
  }();
39
39
  }
40
- function setObserverLogger(logger9) {
41
- mainLogger = logger9;
40
+ function setObserverLogger(logger10) {
41
+ mainLogger = logger10;
42
42
  }
43
43
 
44
44
  // src/ObservedCall.ts
@@ -3456,7 +3456,6 @@ var UnconsumedTrackDetector = class _UnconsumedTrackDetector {
3456
3456
  type: UnconsumedTrackTypes.unconsumedPublishedTrack,
3457
3457
  timestamp: now,
3458
3458
  payload: {
3459
- type: UnconsumedTrackTypes.unconsumedPublishedTrack,
3460
3459
  trackId: outboundTrack.id,
3461
3460
  kind: outboundTrack.kind,
3462
3461
  publisherClientId: peerConnection?.client.clientId,
@@ -3602,7 +3601,6 @@ var TrackDeliveryMismatchDetector = class _TrackDeliveryMismatchDetector {
3602
3601
  type,
3603
3602
  timestamp: now,
3604
3603
  payload: {
3605
- type,
3606
3604
  trackId: outboundTrackId,
3607
3605
  publisherClientId: delivery.publisherClientId,
3608
3606
  publisherSending: delivery.publisherSending,
@@ -3855,12 +3853,9 @@ var CallConcurrentIssueDetector = class _CallConcurrentIssueDetector {
3855
3853
  this._call.addIssue({
3856
3854
  type: issueType,
3857
3855
  timestamp: now,
3856
+ conclusion,
3858
3857
  payload: {
3859
- type: issueType,
3860
3858
  issueType: type,
3861
- scope: "call",
3862
- conclusion,
3863
- callId: this._call.callId,
3864
3859
  clients: group.totalClients,
3865
3860
  affectedClients: group.clientIds.length,
3866
3861
  affectedRatio: group.affectedRatio,
@@ -3984,10 +3979,9 @@ var IssueFanOutDetector = class _IssueFanOutDetector {
3984
3979
  this._call.addIssue({
3985
3980
  type,
3986
3981
  timestamp: now,
3982
+ conclusion,
3987
3983
  payload: {
3988
- type,
3989
3984
  issueType,
3990
- conclusion,
3991
3985
  trackId: publisher.id,
3992
3986
  kind: publisher.kind,
3993
3987
  publisherClientId: publisher.getPeerConnection().client.clientId,
@@ -4134,18 +4128,14 @@ var PublisherFaultCorroborationDetector = class _PublisherFaultCorroborationDete
4134
4128
  this._call.addIssue({
4135
4129
  type: PublisherFaultTypes.corroboratedPublisherFault,
4136
4130
  timestamp: now,
4137
- payload: {
4138
- type: PublisherFaultTypes.corroboratedPublisherFault,
4139
- callId: this._call.callId,
4140
- ...fault,
4141
- conclusion: {
4142
- faultDomain: "published-track",
4143
- summary: `${fault.publisherClientId} reports ${fault.publisherIssueTypes.join(", ")} on track ${fault.trackId} while ${fault.affectedReceivers} of ${fault.receivers} subscribers report ${fault.receiverIssueTypes.join(", ")} \u2014 both ends agree`,
4144
- recommendation: "the source is implicated, not inferred: check that publisher's capture, encoder and uplink before looking at the SFU or the receivers",
4145
- // Higher than any single-ended finding: two independent parties, one conclusion.
4146
- confidence: 0.9
4147
- }
4148
- }
4131
+ conclusion: {
4132
+ faultDomain: "published-track",
4133
+ summary: `${fault.publisherClientId} reports ${fault.publisherIssueTypes.join(", ")} on track ${fault.trackId} while ${fault.affectedReceivers} of ${fault.receivers} subscribers report ${fault.receiverIssueTypes.join(", ")} \u2014 both ends agree`,
4134
+ recommendation: "the source is implicated, not inferred: check that publisher's capture, encoder and uplink before looking at the SFU or the receivers",
4135
+ // Higher than any single-ended finding: two independent parties, one conclusion.
4136
+ confidence: 0.9
4137
+ },
4138
+ payload: { ...fault }
4149
4139
  });
4150
4140
  }
4151
4141
  }
@@ -4201,6 +4191,14 @@ var ObservedCall = class extends EventEmitter3 {
4201
4191
  value: void 0
4202
4192
  };
4203
4193
  remoteTrackResolver;
4194
+ /**
4195
+ * The accumulating record of this call's life, or `undefined` when no summary was configured.
4196
+ *
4197
+ * Live — read it at any point during the call. It is also delivered once on `call-summary` when
4198
+ * the call closes. See `CallSummary`: an absent section means "not collected", never "nothing
4199
+ * happened".
4200
+ */
4201
+ summary;
4204
4202
  /**
4205
4203
  * Published tracks that currently have **no** subscriber linked to them.
4206
4204
  *
@@ -4278,6 +4276,21 @@ var ObservedCall = class extends EventEmitter3 {
4278
4276
  this.detectors.add(detector);
4279
4277
  return this;
4280
4278
  }
4279
+ /**
4280
+ * Start accumulating this call's summary, if the observer was configured for summaries.
4281
+ *
4282
+ * Called by `createObservedCall`; you should not need it. It takes no configuration of its own on
4283
+ * purpose: the collector subscribes to exactly the events the observer's `include` requires, so a
4284
+ * per-call section outside that set would be created and then never written to — an empty section
4285
+ * that reads as "nothing happened". One shape per observer is the only shape that can be filled.
4286
+ *
4287
+ * The collector builds it rather than this method, so the resolved configuration never has to
4288
+ * leave the one object that owns it. Returns `undefined` when summaries are off, and is
4289
+ * idempotent: an existing summary is kept, not restarted.
4290
+ */
4291
+ enableSummary() {
4292
+ return this.summary ??= this.observer.callSummaryCollector?.createSummary(this.callId);
4293
+ }
4281
4294
  /**
4282
4295
  * Remove a detector from **this call** by name, returning how many were removed.
4283
4296
  *
@@ -4302,12 +4315,13 @@ var ObservedCall = class extends EventEmitter3 {
4302
4315
  /**
4303
4316
  * Raise a call-level (server-side) finding; surfaced on the Observer bus as `call-issue`.
4304
4317
  *
4305
- * `payload` takes an **object** — it is delivered to in-process handlers, so there is nothing to
4306
- * serialise for. Pass a string only if you already have one.
4318
+ * `payload` is an **object** and holds evidence only — it is delivered to an in-process handler,
4319
+ * so there is nothing to serialise for. `scope` is stamped here, and the `callId` is already on
4320
+ * the event, so neither belongs in the payload. Put the interpretation in `conclusion`.
4307
4321
  */
4308
4322
  addIssue(issue) {
4309
4323
  if (this.closed) return;
4310
- this._notify("call-issue", { ...this.eventScope, issue });
4324
+ this._notify("call-issue", { ...this.eventScope, issue: { ...issue, scope: "call" } });
4311
4325
  }
4312
4326
  close() {
4313
4327
  if (this.closed) return;
@@ -4328,6 +4342,10 @@ var ObservedCall = class extends EventEmitter3 {
4328
4342
  if (this.startedAt === void 0) this.startedAt = minSampleTimestamps;
4329
4343
  if (this.endedAt === void 0) this.endedAt = maxSampleTimestamps;
4330
4344
  this.closedAt = Date.now();
4345
+ if (this.summary) {
4346
+ this.observer.callSummaryCollector?.finalise(this);
4347
+ this._notify("call-summary", { ...this.eventScope, summary: this.summary });
4348
+ }
4331
4349
  this.detectors.clear();
4332
4350
  this.activeIssuesRegistry.clear();
4333
4351
  this.emit("close");
@@ -5088,6 +5106,166 @@ var ActiveIssuesRegistry = class {
5088
5106
  }
5089
5107
  };
5090
5108
 
5109
+ // src/summaries/CallSummary.ts
5110
+ var defaultCallSummaryConfig = {
5111
+ include: [],
5112
+ maxIssues: 500,
5113
+ maxClientIds: 1e4
5114
+ };
5115
+ function createCallSummary(callId, config) {
5116
+ const summary = { callId, attachments: {} };
5117
+ if (config.include.includes("clients")) {
5118
+ summary.clients = { clientIds: [], peak: 0, joined: 0, left: 0 };
5119
+ }
5120
+ if (config.include.includes("issues")) {
5121
+ summary.issues = [];
5122
+ }
5123
+ if (config.include.includes("turnServers")) {
5124
+ summary.turnServers = { serverUrls: [], clientsRelayed: 0 };
5125
+ }
5126
+ if (config.include.includes("scores")) {
5127
+ summary.scores = { samples: 0 };
5128
+ }
5129
+ return summary;
5130
+ }
5131
+
5132
+ // src/summaries/CallSummaryCollector.ts
5133
+ var logger8 = createLogger("CallSummaryCollector");
5134
+ var CallSummaryCollector = class {
5135
+ constructor(_observer, _config) {
5136
+ this._observer = _observer;
5137
+ this._config = _config;
5138
+ this._subscribeBuiltIns();
5139
+ this._subscribeEnrichers();
5140
+ }
5141
+ _observer;
5142
+ _config;
5143
+ _scratch = /* @__PURE__ */ new WeakMap();
5144
+ _listeners = [];
5145
+ _closed = false;
5146
+ /**
5147
+ * Build a summary for `callId` and start tracking it.
5148
+ *
5149
+ * Creating it here, rather than letting the call create one and hand it over, keeps the resolved
5150
+ * configuration inside the single object that owns it — and makes it impossible to end up with a
5151
+ * summary whose sections nobody subscribed to fill.
5152
+ */
5153
+ createSummary(callId) {
5154
+ const summary = createCallSummary(callId, this._config);
5155
+ this._scratch.set(summary, { scores: [], turnClientIds: /* @__PURE__ */ new Set() });
5156
+ return summary;
5157
+ }
5158
+ /**
5159
+ * Finalise `call`'s summary: fold in what only makes sense once, and stamp the closing times.
5160
+ *
5161
+ * Percentiles are computed here rather than on every update — a median recomputed per tick over a
5162
+ * growing array is quadratic work to produce a number nobody reads until the end.
5163
+ */
5164
+ finalise(call) {
5165
+ const summary = call.summary;
5166
+ if (!summary) return;
5167
+ const scratch = this._scratch.get(summary);
5168
+ summary.startedAt = call.startedAt;
5169
+ summary.endedAt = call.endedAt;
5170
+ summary.durationInMs = summary.startedAt !== void 0 && summary.endedAt !== void 0 ? Math.max(0, summary.endedAt - summary.startedAt) : void 0;
5171
+ summary.closedAt = Date.now();
5172
+ if (summary.scores && scratch) {
5173
+ summary.scores.samples = scratch.scores.length;
5174
+ if (0 < scratch.scores.length) {
5175
+ summary.scores.min = Math.min(...scratch.scores);
5176
+ summary.scores.max = Math.max(...scratch.scores);
5177
+ summary.scores.median = percentile(scratch.scores, 0.5);
5178
+ }
5179
+ }
5180
+ if (summary.turnServers && scratch) {
5181
+ summary.turnServers.clientsRelayed = scratch.turnClientIds.size;
5182
+ }
5183
+ }
5184
+ /** Drop every bus subscription. Called when the observer closes. */
5185
+ close() {
5186
+ if (this._closed) return;
5187
+ this._closed = true;
5188
+ for (const { event, listener } of this._listeners) {
5189
+ this._observer.off(event, listener);
5190
+ }
5191
+ this._listeners.length = 0;
5192
+ }
5193
+ /**
5194
+ * Subscribe `listener` to `event`, routed to the summary of the call the event names.
5195
+ *
5196
+ * The `observedCall` is read off the payload rather than closed over, which is what lets one
5197
+ * subscription serve every call.
5198
+ */
5199
+ _on(event, handler) {
5200
+ const listener = (...args) => {
5201
+ const summary = args[0].observedCall.summary;
5202
+ if (!summary) return;
5203
+ const scratch = this._scratch.get(summary);
5204
+ if (!scratch) return;
5205
+ try {
5206
+ handler(summary, scratch, ...args);
5207
+ } catch (err) {
5208
+ logger8.warn("A call-summary handler for %s threw; continuing. %o", event, err);
5209
+ }
5210
+ };
5211
+ this._observer.on(event, listener);
5212
+ this._listeners.push({ event, listener });
5213
+ }
5214
+ _subscribeBuiltIns() {
5215
+ const include = this._config.include;
5216
+ if (include.includes("clients")) {
5217
+ this._on("client-added", (summary, _scratch, { observedCall, observedClient }) => {
5218
+ const clients = summary.clients;
5219
+ if (!clients) return;
5220
+ clients.joined += 1;
5221
+ clients.peak = Math.max(clients.peak, observedCall.observedClients.size);
5222
+ if (clients.clientIds.length < this._config.maxClientIds) {
5223
+ clients.clientIds.push(observedClient.clientId);
5224
+ } else {
5225
+ summary.truncated = { ...summary.truncated, clientIds: (summary.truncated?.clientIds ?? 0) + 1 };
5226
+ }
5227
+ });
5228
+ this._on("client-closed", (summary) => {
5229
+ if (summary.clients) summary.clients.left += 1;
5230
+ });
5231
+ }
5232
+ if (include.includes("issues")) {
5233
+ this._on("call-issue", (summary, _scratch, { issue }) => {
5234
+ const issues = summary.issues;
5235
+ if (!issues) return;
5236
+ if (issues.length < this._config.maxIssues) issues.push(issue);
5237
+ else summary.truncated = { ...summary.truncated, issues: (summary.truncated?.issues ?? 0) + 1 };
5238
+ });
5239
+ }
5240
+ if (include.includes("scores") || include.includes("turnServers")) {
5241
+ this._on("call-updated", (summary, scratch, { observedCall }) => {
5242
+ if (summary.scores && observedCall.score !== void 0) scratch.scores.push(observedCall.score);
5243
+ if (!summary.turnServers) return;
5244
+ for (const clientId of observedCall.clientsUsedTurn) scratch.turnClientIds.add(clientId);
5245
+ for (const client of observedCall.observedClients.values()) {
5246
+ for (const peerConnection of client.observedPeerConnections.values()) {
5247
+ for (const pair of peerConnection.selectedIceCandiadtePairForTurn) {
5248
+ const url = pair.getLocalCandidate()?.url;
5249
+ if (url && !summary.turnServers.serverUrls.includes(url)) {
5250
+ summary.turnServers.serverUrls.push(url);
5251
+ }
5252
+ }
5253
+ }
5254
+ }
5255
+ });
5256
+ }
5257
+ }
5258
+ _subscribeEnrichers() {
5259
+ const enrich = this._config.enrich;
5260
+ if (!enrich) return;
5261
+ for (const name of Object.keys(enrich)) {
5262
+ const enricher = enrich[name];
5263
+ if (!enricher) continue;
5264
+ this._on(name, (summary, _scratch, ...args) => enricher(summary, ...args));
5265
+ }
5266
+ }
5267
+ };
5268
+
5091
5269
  // src/detectors/SfuCongestionDetector.ts
5092
5270
  var SfuCongestionDetector = class _SfuCongestionDetector {
5093
5271
  constructor(_observer, config = {}) {
@@ -5216,13 +5394,10 @@ var SfuCongestionDetector = class _SfuCongestionDetector {
5216
5394
  absoluteIncrease: evaluation.absoluteIncrease,
5217
5395
  relativeIncrease: evaluation.relativeIncrease
5218
5396
  };
5219
- this._observer.emit("observer-issue", {
5220
- issue: {
5221
- type: this._config.emittedObserverIssueType,
5222
- timestamp: Date.now(),
5223
- payload
5224
- },
5225
- observer: this._observer
5397
+ this._observer.addIssue({
5398
+ type: this._config.emittedObserverIssueType,
5399
+ timestamp: Date.now(),
5400
+ payload: { ...payload }
5226
5401
  });
5227
5402
  }
5228
5403
  /**
@@ -5336,11 +5511,9 @@ var ObserverConcurrentIssueDetector = class _ObserverConcurrentIssueDetector {
5336
5511
  this._observer.addIssue({
5337
5512
  type: issueType,
5338
5513
  timestamp: now,
5514
+ conclusion,
5339
5515
  payload: {
5340
- type: issueType,
5341
5516
  issueType: type,
5342
- scope: "observer",
5343
- conclusion,
5344
5517
  clients: group.totalClients,
5345
5518
  affectedClients: group.clientIds.length,
5346
5519
  affectedRatio: group.affectedRatio,
@@ -5513,16 +5686,13 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
5513
5686
  this._observer.addIssue({
5514
5687
  type: ClientPopulationIssueTypes.clientPopulationIssue,
5515
5688
  timestamp: now,
5516
- payload: {
5517
- type: ClientPopulationIssueTypes.clientPopulationIssue,
5518
- ...rollup,
5519
- conclusion: {
5520
- faultDomain: "client-population",
5521
- summary: `'${issueType}' is ${this._riskText(rollup.relativeRisk)} more likely on ${population} than on the rest of the fleet (${rollup.affectedClients}/${rollup.clients} vs ${rollup.controlAffectedClients}/${rollup.controlClients})`,
5522
- recommendation: "this is not an SFU symptom \u2014 look at what those clients share: a recent release, a browser version, or shared/virtualised hardware",
5523
- confidence: this._confidenceOf(rollup)
5524
- }
5525
- }
5689
+ conclusion: {
5690
+ faultDomain: "client-population",
5691
+ summary: `'${issueType}' is ${this._riskText(rollup.relativeRisk)} more likely on ${population} than on the rest of the fleet (${rollup.affectedClients}/${rollup.clients} vs ${rollup.controlAffectedClients}/${rollup.controlClients})`,
5692
+ recommendation: "this is not an SFU symptom \u2014 look at what those clients share: a recent release, a browser version, or shared/virtualised hardware",
5693
+ confidence: this._confidenceOf(rollup)
5694
+ },
5695
+ payload: { ...rollup }
5526
5696
  });
5527
5697
  }
5528
5698
  }
@@ -5624,7 +5794,7 @@ var TurnServerHealthDetector = class _TurnServerHealthDetector {
5624
5794
  this._observer.addIssue({
5625
5795
  type: TurnServerHealthTypes.turnServerDegraded,
5626
5796
  timestamp: now,
5627
- payload: { type: TurnServerHealthTypes.turnServerDegraded, ...health, otherServers }
5797
+ payload: { ...health, otherServers }
5628
5798
  });
5629
5799
  }
5630
5800
  for (const serverUrl of [...this._streaks.keys()]) {
@@ -5732,7 +5902,6 @@ var TurnServerOutageDetector = class _TurnServerOutageDetector {
5732
5902
  type: TurnServerOutageTypes.turnServerOutage,
5733
5903
  timestamp: now,
5734
5904
  payload: {
5735
- type: TurnServerOutageTypes.turnServerOutage,
5736
5905
  serverUrl,
5737
5906
  peakClients: peak,
5738
5907
  currentClients: live,
@@ -6006,17 +6175,13 @@ var SimulcastReceiverValidator = class _SimulcastReceiverValidator {
6006
6175
  this._observer.addIssue({
6007
6176
  type: LOWEST_COMMON_DENOMINATOR_ISSUE,
6008
6177
  timestamp: decidedAt,
6009
- payload: {
6010
- type: LOWEST_COMMON_DENOMINATOR_ISSUE,
6011
- checks: this._checks,
6012
- ...outcome.evidence,
6013
- conclusion: {
6014
- faultDomain: "infrastructure",
6015
- summary: "one bad receiver is dragging a publisher's bitrate down for everyone \u2014 the SFU is not selecting layers per consumer",
6016
- recommendation: "check that simulcast/SVC is enabled and layers are chosen per consumer, and that the SFU terminates receiver reports instead of forwarding them; this is a build/config property, not a transient",
6017
- confidence: 0.8
6018
- }
6019
- }
6178
+ conclusion: {
6179
+ faultDomain: "infrastructure",
6180
+ summary: "one bad receiver is dragging a publisher's bitrate down for everyone \u2014 the SFU is not selecting layers per consumer",
6181
+ recommendation: "check that simulcast/SVC is enabled and layers are chosen per consumer, and that the SFU terminates receiver reports instead of forwarding them; this is a build/config property, not a transient",
6182
+ confidence: 0.8
6183
+ },
6184
+ payload: { checks: this._checks, ...outcome.evidence }
6020
6185
  });
6021
6186
  }
6022
6187
  this.onDone(this.report);
@@ -6119,17 +6284,13 @@ var RemoteTrackResolverValidator = class _RemoteTrackResolverValidator {
6119
6284
  this._observer.addIssue({
6120
6285
  type: UNRESOLVED_TRACK_LINKS_ISSUE,
6121
6286
  timestamp: decidedAt,
6122
- payload: {
6123
- type: UNRESOLVED_TRACK_LINKS_ISSUE,
6124
- checks: this._checks,
6125
- ...outcome.evidence,
6126
- conclusion: {
6127
- faultDomain: "infrastructure",
6128
- summary: "a RemoteTrackResolver is configured but has never linked a subscriber to a publisher",
6129
- recommendation: "check the id the resolver joins on (mediasoup producerId, or your own convention) \u2014 until it links, IssueFanOutDetector, TrackDeliveryMismatchDetector, UnconsumedTrackDetector and SimulcastReceiverValidator all silently do nothing",
6130
- confidence: 0.9
6131
- }
6132
- }
6287
+ conclusion: {
6288
+ faultDomain: "infrastructure",
6289
+ summary: "a RemoteTrackResolver is configured but has never linked a subscriber to a publisher",
6290
+ recommendation: "check the id the resolver joins on (mediasoup producerId, or your own convention) \u2014 until it links, IssueFanOutDetector, TrackDeliveryMismatchDetector, UnconsumedTrackDetector and SimulcastReceiverValidator all silently do nothing",
6291
+ confidence: 0.9
6292
+ },
6293
+ payload: { checks: this._checks, ...outcome.evidence }
6133
6294
  });
6134
6295
  }
6135
6296
  this.onDone(this.report);
@@ -6240,17 +6401,16 @@ var CodecConsistencyValidator = class _CodecConsistencyValidator {
6240
6401
  this._observer.addIssue({
6241
6402
  type: CODEC_MISMATCH_ISSUE,
6242
6403
  timestamp: decidedAt,
6404
+ conclusion: {
6405
+ faultDomain: "infrastructure",
6406
+ summary: outcome.verdict === "codec-split" ? "participants of one call are using different codecs \u2014 an SFU that forwards without transcoding cannot serve all of them" : "the deployment is consistently negotiating a codec other than the expected one",
6407
+ recommendation: outcome.verdict === "codec-split" ? "check codec preferences and any SDP munging; a split usually means one endpoint could not negotiate the preferred codec and the others were not renegotiated with it" : "check codec preferences and endpoint support \u2014 a silent fallback keeps working, at a higher bitrate than you budgeted for",
6408
+ confidence: 0.85
6409
+ },
6243
6410
  payload: {
6244
- type: CODEC_MISMATCH_ISSUE,
6245
6411
  verdict: outcome.verdict,
6246
6412
  checks: this._checks,
6247
- evidence: outcome.evidence,
6248
- conclusion: {
6249
- faultDomain: "infrastructure",
6250
- summary: outcome.verdict === "codec-split" ? "participants of one call are using different codecs \u2014 an SFU that forwards without transcoding cannot serve all of them" : "the deployment is consistently negotiating a codec other than the expected one",
6251
- recommendation: outcome.verdict === "codec-split" ? "check codec preferences and any SDP munging; a split usually means one endpoint could not negotiate the preferred codec and the others were not renegotiated with it" : "check codec preferences and endpoint support \u2014 a silent fallback keeps working, at a higher bitrate than you budgeted for",
6252
- confidence: 0.85
6253
- }
6413
+ evidence: outcome.evidence
6254
6414
  }
6255
6415
  });
6256
6416
  }
@@ -6264,7 +6424,7 @@ function mediaKindOf(mimeType) {
6264
6424
  }
6265
6425
 
6266
6426
  // src/Observer.ts
6267
- var logger8 = createLogger("Observer");
6427
+ var logger9 = createLogger("Observer");
6268
6428
  var Observer = class extends EventEmitter6 {
6269
6429
  observedTURN = new ObservedTURN();
6270
6430
  observedCalls = /* @__PURE__ */ new Map();
@@ -6320,6 +6480,14 @@ var Observer = class extends EventEmitter6 {
6320
6480
  * ```
6321
6481
  */
6322
6482
  callDetectorConfigs = /* @__PURE__ */ new Map();
6483
+ /**
6484
+ * Owns every call's summary: the resolved `config.callSummary`, the bus subscriptions that keep
6485
+ * the summaries current (one per event type, not one per call), and the summaries themselves.
6486
+ *
6487
+ * `undefined` when `config.callSummary` was absent or `null` — so its presence *is* the answer to
6488
+ * "are summaries on", and nothing is subscribed to anything.
6489
+ */
6490
+ callSummaryCollector;
6323
6491
  constructor(config = {}) {
6324
6492
  super();
6325
6493
  this.setMaxListeners(Infinity);
@@ -6331,6 +6499,12 @@ var Observer = class extends EventEmitter6 {
6331
6499
  closeClientIfIdleForMs: 6e4,
6332
6500
  ...config
6333
6501
  };
6502
+ if (this.config.callSummary) {
6503
+ this.callSummaryCollector = new CallSummaryCollector(this, {
6504
+ ...defaultCallSummaryConfig,
6505
+ ...this.config.callSummary
6506
+ });
6507
+ }
6334
6508
  }
6335
6509
  get numberOfCalls() {
6336
6510
  return this.observedCalls.size;
@@ -6369,7 +6543,7 @@ var Observer = class extends EventEmitter6 {
6369
6543
  break;
6370
6544
  }
6371
6545
  default: {
6372
- logger8.warn("Unknown detector name %s; skipping", name);
6546
+ logger9.warn("Unknown detector name %s; skipping", name);
6373
6547
  return this;
6374
6548
  }
6375
6549
  }
@@ -6454,7 +6628,7 @@ var Observer = class extends EventEmitter6 {
6454
6628
  break;
6455
6629
  }
6456
6630
  if (!validator) {
6457
- logger8.warn("Unknown validator name %s; skipping", name);
6631
+ logger9.warn("Unknown validator name %s; skipping", name);
6458
6632
  return this;
6459
6633
  }
6460
6634
  this.validators.add(validator);
@@ -6485,7 +6659,7 @@ var Observer = class extends EventEmitter6 {
6485
6659
  try {
6486
6660
  validator.cancel(reason);
6487
6661
  } catch (err) {
6488
- logger8.warn("Error cancelling validator %s: %o", validator.name, err);
6662
+ logger9.warn("Error cancelling validator %s: %o", validator.name, err);
6489
6663
  }
6490
6664
  }
6491
6665
  return matching.length;
@@ -6496,11 +6670,11 @@ var Observer = class extends EventEmitter6 {
6496
6670
  }
6497
6671
  createObservedCall(settings) {
6498
6672
  if (this.closed) {
6499
- logger8.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
6673
+ logger9.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
6500
6674
  return void 0;
6501
6675
  }
6502
6676
  if (this.observedCalls.has(settings.callId)) {
6503
- logger8.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
6677
+ logger9.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
6504
6678
  return this.observedCalls.get(settings.callId);
6505
6679
  }
6506
6680
  const callSettings = {
@@ -6515,6 +6689,7 @@ var Observer = class extends EventEmitter6 {
6515
6689
  callActiveIssuesRegistry
6516
6690
  );
6517
6691
  observedCall.remoteTrackResolver = this.config.createRemoteTrackResolver?.(observedCall);
6692
+ observedCall.enableSummary();
6518
6693
  for (const [name, detectorConfig] of this.callDetectorConfigs) {
6519
6694
  observedCall.addDetector(name, detectorConfig);
6520
6695
  }
@@ -6537,11 +6712,11 @@ var Observer = class extends EventEmitter6 {
6537
6712
  }
6538
6713
  createObservedMediasoupRouter(settings) {
6539
6714
  if (this.closed) {
6540
- logger8.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
6715
+ logger9.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
6541
6716
  return void 0;
6542
6717
  }
6543
6718
  if (this.observedMediasoupRouters.has(settings.router.id)) {
6544
- logger8.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
6719
+ logger9.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
6545
6720
  return this.observedMediasoupRouters.get(settings.router.id);
6546
6721
  }
6547
6722
  const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
@@ -6572,13 +6747,14 @@ var Observer = class extends EventEmitter6 {
6572
6747
  }
6573
6748
  close() {
6574
6749
  if (this.closed) {
6575
- return logger8.debug("Attempted to close twice");
6750
+ return logger9.debug("Attempted to close twice");
6576
6751
  }
6577
6752
  this.closed = true;
6578
6753
  for (const call of [...this.observedCalls.values()]) call.close();
6579
6754
  this.detectors.clear();
6580
6755
  for (const validator of [...this.validators]) validator.cancel("observer closed");
6581
6756
  this.validators.clear();
6757
+ this.callSummaryCollector?.close();
6582
6758
  this.activeIssuesRegistry.clear();
6583
6759
  this._notify("observer-closed", { ...this.eventScope });
6584
6760
  }
@@ -6589,7 +6765,7 @@ var Observer = class extends EventEmitter6 {
6589
6765
  try {
6590
6766
  this.acceptMiddlewares.process({ sample, context });
6591
6767
  } catch (err) {
6592
- logger8.warn("An accept middleware threw; dropping the sample. %o", err);
6768
+ logger9.warn("An accept middleware threw; dropping the sample. %o", err);
6593
6769
  }
6594
6770
  if (!sample.callId) {
6595
6771
  this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
@@ -6641,7 +6817,7 @@ var Observer = class extends EventEmitter6 {
6641
6817
  try {
6642
6818
  validator.update();
6643
6819
  } catch (err) {
6644
- logger8.warn("Error running validator %s: %o", validator.name, err);
6820
+ logger9.warn("Error running validator %s: %o", validator.name, err);
6645
6821
  }
6646
6822
  }
6647
6823
  this._notify("observer-updated", { ...this.eventScope });
@@ -6654,7 +6830,7 @@ var Observer = class extends EventEmitter6 {
6654
6830
  */
6655
6831
  addIssue(issue) {
6656
6832
  if (this.closed) return;
6657
- this._notify("observer-issue", { ...this.eventScope, issue });
6833
+ this._notify("observer-issue", { ...this.eventScope, issue: { ...issue, scope: "observer" } });
6658
6834
  }
6659
6835
  /** Emit an Observer-bus event. */
6660
6836
  _notify(type, ...args) {
@@ -6772,18 +6948,11 @@ var CallHealthAggregator = class {
6772
6948
  }
6773
6949
  };
6774
6950
 
6775
- // src/common/ObserverIssue.ts
6776
- function issuePayloadOf(issue) {
6777
- const payload = issue.payload;
6778
- if (payload === void 0) return void 0;
6779
- return typeof payload === "string" ? parseJsonObject(payload) : payload;
6780
- }
6951
+ // src/common/Issue.ts
6781
6952
  function issuePayloadAsString(issue) {
6782
- const payload = issue.payload;
6783
- if (payload === void 0) return void 0;
6784
- if (typeof payload === "string") return payload;
6953
+ if (issue.payload === void 0) return void 0;
6785
6954
  try {
6786
- return JSON.stringify(payload);
6955
+ return JSON.stringify(issue.payload);
6787
6956
  } catch {
6788
6957
  return void 0;
6789
6958
  }
@@ -7077,6 +7246,7 @@ export {
7077
7246
  CallConcurrentIssueDetector,
7078
7247
  CallConcurrentIssueTypes,
7079
7248
  CallHealthAggregator,
7249
+ CallSummaryCollector,
7080
7250
  ClientEventTypes,
7081
7251
  ClientMetaTypes,
7082
7252
  ClientPopulationIssueDetector,
@@ -7135,16 +7305,17 @@ export {
7135
7305
  concludeObserverIssue,
7136
7306
  correlation,
7137
7307
  counterDelta,
7308
+ createCallSummary,
7138
7309
  createDefaultMediasoupRemoteTrackResolverFactory,
7139
7310
  createInMemorySink,
7140
7311
  createJsonlFileSink,
7141
7312
  createJsonlFileSinkFactory,
7142
7313
  createLogger,
7143
7314
  createP2pRemoteTrackResolverFactory,
7315
+ defaultCallSummaryConfig,
7144
7316
  defaultClientHealthThresholds,
7145
7317
  isClientIssueResolutionEntry,
7146
7318
  issuePayloadAsString,
7147
- issuePayloadOf,
7148
7319
  mannKendall,
7149
7320
  mannKendallVerdict,
7150
7321
  median,