@observertc/observer-js 1.0.0-beta.15 → 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/README.md +254 -25
- package/dist/index.d.mts +554 -140
- package/dist/index.d.ts +554 -140
- package/dist/index.js +436 -105
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +433 -104
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(src_exports, {
|
|
|
35
35
|
CallConcurrentIssueDetector: () => CallConcurrentIssueDetector,
|
|
36
36
|
CallConcurrentIssueTypes: () => CallConcurrentIssueTypes,
|
|
37
37
|
CallHealthAggregator: () => CallHealthAggregator,
|
|
38
|
+
CallSummaryCollector: () => CallSummaryCollector,
|
|
38
39
|
ClientEventTypes: () => ClientEventTypes,
|
|
39
40
|
ClientMetaTypes: () => ClientMetaTypes,
|
|
40
41
|
ClientPopulationIssueDetector: () => ClientPopulationIssueDetector,
|
|
@@ -93,16 +94,17 @@ __export(src_exports, {
|
|
|
93
94
|
concludeObserverIssue: () => concludeObserverIssue,
|
|
94
95
|
correlation: () => correlation,
|
|
95
96
|
counterDelta: () => counterDelta,
|
|
97
|
+
createCallSummary: () => createCallSummary,
|
|
96
98
|
createDefaultMediasoupRemoteTrackResolverFactory: () => createDefaultMediasoupRemoteTrackResolverFactory,
|
|
97
99
|
createInMemorySink: () => createInMemorySink,
|
|
98
100
|
createJsonlFileSink: () => createJsonlFileSink,
|
|
99
101
|
createJsonlFileSinkFactory: () => createJsonlFileSinkFactory,
|
|
100
102
|
createLogger: () => createLogger,
|
|
101
103
|
createP2pRemoteTrackResolverFactory: () => createP2pRemoteTrackResolverFactory,
|
|
104
|
+
defaultCallSummaryConfig: () => defaultCallSummaryConfig,
|
|
102
105
|
defaultClientHealthThresholds: () => defaultClientHealthThresholds,
|
|
103
106
|
isClientIssueResolutionEntry: () => isClientIssueResolutionEntry,
|
|
104
107
|
issuePayloadAsString: () => issuePayloadAsString,
|
|
105
|
-
issuePayloadOf: () => issuePayloadOf,
|
|
106
108
|
mannKendall: () => mannKendall,
|
|
107
109
|
mannKendallVerdict: () => mannKendallVerdict,
|
|
108
110
|
median: () => median,
|
|
@@ -155,8 +157,8 @@ function createLogger(moduleName) {
|
|
|
155
157
|
}
|
|
156
158
|
}();
|
|
157
159
|
}
|
|
158
|
-
function setObserverLogger(
|
|
159
|
-
mainLogger =
|
|
160
|
+
function setObserverLogger(logger10) {
|
|
161
|
+
mainLogger = logger10;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
// src/ObservedCall.ts
|
|
@@ -2750,6 +2752,10 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2750
2752
|
}
|
|
2751
2753
|
close() {
|
|
2752
2754
|
if (this.closed) return;
|
|
2755
|
+
if (this.closeTimer) {
|
|
2756
|
+
clearTimeout(this.closeTimer);
|
|
2757
|
+
this.closeTimer = void 0;
|
|
2758
|
+
}
|
|
2753
2759
|
this._flushPendingInjections();
|
|
2754
2760
|
const activeIssues = [...this.activeIssues.values()];
|
|
2755
2761
|
for (const issue of activeIssues) {
|
|
@@ -2912,6 +2918,7 @@ var ObservedClient = class extends import_events2.EventEmitter {
|
|
|
2912
2918
|
this.closeTimer = setTimeout(() => {
|
|
2913
2919
|
this.close();
|
|
2914
2920
|
}, this.settings.closeClientIfIdleForMs);
|
|
2921
|
+
this.closeTimer.unref?.();
|
|
2915
2922
|
}
|
|
2916
2923
|
this._activeSample = void 0;
|
|
2917
2924
|
try {
|
|
@@ -3383,6 +3390,24 @@ var Detectors = class {
|
|
|
3383
3390
|
constructor(...detectors) {
|
|
3384
3391
|
this._detectors = detectors;
|
|
3385
3392
|
}
|
|
3393
|
+
/**
|
|
3394
|
+
* Every registered detector, in registration order.
|
|
3395
|
+
*
|
|
3396
|
+
* This is **the** way to get hold of an instance: `addDetector` / `addObserverDetector` are
|
|
3397
|
+
* chainable and return the owning entity, so the registry is where instances live. Read it to
|
|
3398
|
+
* inspect a detector's state, or to pick one out and {@link remove} it.
|
|
3399
|
+
*
|
|
3400
|
+
* A copy, not the live array — a caller iterating this while removing would otherwise skip
|
|
3401
|
+
* entries, and that is exactly what "remove the ones that look like X" does.
|
|
3402
|
+
*/
|
|
3403
|
+
get instances() {
|
|
3404
|
+
return [...this._detectors];
|
|
3405
|
+
}
|
|
3406
|
+
/** Iterate the registry directly: `for (const detector of call.detectors)`. */
|
|
3407
|
+
[Symbol.iterator]() {
|
|
3408
|
+
return this.instances[Symbol.iterator]();
|
|
3409
|
+
}
|
|
3410
|
+
/** The names in registration order. Duplicates are meaningful — see {@link getAll}. */
|
|
3386
3411
|
get listOfNames() {
|
|
3387
3412
|
return this._detectors.map((d) => d.name);
|
|
3388
3413
|
}
|
|
@@ -3392,12 +3417,48 @@ var Detectors = class {
|
|
|
3392
3417
|
add(detector) {
|
|
3393
3418
|
this._detectors.push(detector);
|
|
3394
3419
|
}
|
|
3420
|
+
/** The first detector registered under `name`. See {@link getAll} when several can share one. */
|
|
3395
3421
|
get(name) {
|
|
3396
3422
|
return this._detectors.find((detector) => detector.name === name);
|
|
3397
3423
|
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Every detector registered under `name`.
|
|
3426
|
+
*
|
|
3427
|
+
* More than one is legitimate: `ClientPopulationIssueDetector` is meant to be added once per
|
|
3428
|
+
* `groupBy` axis, and two instances of it share a name.
|
|
3429
|
+
*/
|
|
3430
|
+
getAll(name) {
|
|
3431
|
+
return this._detectors.filter((detector) => detector.name === name);
|
|
3432
|
+
}
|
|
3433
|
+
has(name) {
|
|
3434
|
+
return this._detectors.some((detector) => detector.name === name);
|
|
3435
|
+
}
|
|
3436
|
+
/** Remove one specific instance. Returns `false` if it was not registered here. */
|
|
3398
3437
|
remove(detector) {
|
|
3399
|
-
|
|
3438
|
+
const remaining = this._detectors.filter((candidate) => candidate !== detector);
|
|
3439
|
+
if (remaining.length === this._detectors.length) return false;
|
|
3440
|
+
this._detectors = remaining;
|
|
3400
3441
|
this._close(detector);
|
|
3442
|
+
return true;
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* Remove **every** detector registered under `name`, returning how many were removed.
|
|
3446
|
+
*
|
|
3447
|
+
* All of them rather than the first, because a name can legitimately be registered more than once
|
|
3448
|
+
* (see {@link getAll}) and "remove the `client-population-issue-detector`" cannot sensibly mean
|
|
3449
|
+
* "remove whichever axis happens to be first in the array". Removing all of them is the only
|
|
3450
|
+
* behaviour that leaves the registry in a state the caller can predict from the name alone.
|
|
3451
|
+
*
|
|
3452
|
+
* Each removed detector gets `close()`, so trackers unsubscribe from the issue registry, bus
|
|
3453
|
+
* listeners drop, and timers clear — a detector removed without closing keeps being fed issues
|
|
3454
|
+
* forever.
|
|
3455
|
+
*/
|
|
3456
|
+
removeByName(name) {
|
|
3457
|
+
const removed = this._detectors.filter((detector) => detector.name === name);
|
|
3458
|
+
if (removed.length === 0) return 0;
|
|
3459
|
+
this._detectors = this._detectors.filter((detector) => detector.name !== name);
|
|
3460
|
+
for (const detector of removed) this._close(detector);
|
|
3461
|
+
return removed.length;
|
|
3401
3462
|
}
|
|
3402
3463
|
update() {
|
|
3403
3464
|
for (const detector of this._detectors) {
|
|
@@ -3515,7 +3576,6 @@ var UnconsumedTrackDetector = class _UnconsumedTrackDetector {
|
|
|
3515
3576
|
type: UnconsumedTrackTypes.unconsumedPublishedTrack,
|
|
3516
3577
|
timestamp: now,
|
|
3517
3578
|
payload: {
|
|
3518
|
-
type: UnconsumedTrackTypes.unconsumedPublishedTrack,
|
|
3519
3579
|
trackId: outboundTrack.id,
|
|
3520
3580
|
kind: outboundTrack.kind,
|
|
3521
3581
|
publisherClientId: peerConnection?.client.clientId,
|
|
@@ -3661,7 +3721,6 @@ var TrackDeliveryMismatchDetector = class _TrackDeliveryMismatchDetector {
|
|
|
3661
3721
|
type,
|
|
3662
3722
|
timestamp: now,
|
|
3663
3723
|
payload: {
|
|
3664
|
-
type,
|
|
3665
3724
|
trackId: outboundTrackId,
|
|
3666
3725
|
publisherClientId: delivery.publisherClientId,
|
|
3667
3726
|
publisherSending: delivery.publisherSending,
|
|
@@ -3914,12 +3973,9 @@ var CallConcurrentIssueDetector = class _CallConcurrentIssueDetector {
|
|
|
3914
3973
|
this._call.addIssue({
|
|
3915
3974
|
type: issueType,
|
|
3916
3975
|
timestamp: now,
|
|
3976
|
+
conclusion,
|
|
3917
3977
|
payload: {
|
|
3918
|
-
type: issueType,
|
|
3919
3978
|
issueType: type,
|
|
3920
|
-
scope: "call",
|
|
3921
|
-
conclusion,
|
|
3922
|
-
callId: this._call.callId,
|
|
3923
3979
|
clients: group.totalClients,
|
|
3924
3980
|
affectedClients: group.clientIds.length,
|
|
3925
3981
|
affectedRatio: group.affectedRatio,
|
|
@@ -4043,10 +4099,9 @@ var IssueFanOutDetector = class _IssueFanOutDetector {
|
|
|
4043
4099
|
this._call.addIssue({
|
|
4044
4100
|
type,
|
|
4045
4101
|
timestamp: now,
|
|
4102
|
+
conclusion,
|
|
4046
4103
|
payload: {
|
|
4047
|
-
type,
|
|
4048
4104
|
issueType,
|
|
4049
|
-
conclusion,
|
|
4050
4105
|
trackId: publisher.id,
|
|
4051
4106
|
kind: publisher.kind,
|
|
4052
4107
|
publisherClientId: publisher.getPeerConnection().client.clientId,
|
|
@@ -4193,18 +4248,14 @@ var PublisherFaultCorroborationDetector = class _PublisherFaultCorroborationDete
|
|
|
4193
4248
|
this._call.addIssue({
|
|
4194
4249
|
type: PublisherFaultTypes.corroboratedPublisherFault,
|
|
4195
4250
|
timestamp: now,
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
// Higher than any single-ended finding: two independent parties, one conclusion.
|
|
4205
|
-
confidence: 0.9
|
|
4206
|
-
}
|
|
4207
|
-
}
|
|
4251
|
+
conclusion: {
|
|
4252
|
+
faultDomain: "published-track",
|
|
4253
|
+
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`,
|
|
4254
|
+
recommendation: "the source is implicated, not inferred: check that publisher's capture, encoder and uplink before looking at the SFU or the receivers",
|
|
4255
|
+
// Higher than any single-ended finding: two independent parties, one conclusion.
|
|
4256
|
+
confidence: 0.9
|
|
4257
|
+
},
|
|
4258
|
+
payload: { ...fault }
|
|
4208
4259
|
});
|
|
4209
4260
|
}
|
|
4210
4261
|
}
|
|
@@ -4260,6 +4311,14 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4260
4311
|
value: void 0
|
|
4261
4312
|
};
|
|
4262
4313
|
remoteTrackResolver;
|
|
4314
|
+
/**
|
|
4315
|
+
* The accumulating record of this call's life, or `undefined` when no summary was configured.
|
|
4316
|
+
*
|
|
4317
|
+
* Live — read it at any point during the call. It is also delivered once on `call-summary` when
|
|
4318
|
+
* the call closes. See `CallSummary`: an absent section means "not collected", never "nothing
|
|
4319
|
+
* happened".
|
|
4320
|
+
*/
|
|
4321
|
+
summary;
|
|
4263
4322
|
/**
|
|
4264
4323
|
* Published tracks that currently have **no** subscriber linked to them.
|
|
4265
4324
|
*
|
|
@@ -4299,6 +4358,12 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4299
4358
|
get score() {
|
|
4300
4359
|
return this.calculatedScore.value;
|
|
4301
4360
|
}
|
|
4361
|
+
/**
|
|
4362
|
+
* Build a call-scoped detector onto this call. Chainable.
|
|
4363
|
+
*
|
|
4364
|
+
* To get a handle on what was built — to inspect it, or to remove that exact instance later — read
|
|
4365
|
+
* it back off the registry: `call.detectors.getAll(name)`, or `call.detectors.instances`.
|
|
4366
|
+
*/
|
|
4302
4367
|
addDetector(name, config = {}) {
|
|
4303
4368
|
if (this.closed) return this;
|
|
4304
4369
|
let detector;
|
|
@@ -4331,20 +4396,61 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4331
4396
|
this.detectors.add(detector);
|
|
4332
4397
|
return this;
|
|
4333
4398
|
}
|
|
4399
|
+
/**
|
|
4400
|
+
* Start accumulating this call's summary, if the observer was configured for summaries.
|
|
4401
|
+
*
|
|
4402
|
+
* Called by `createObservedCall`; you should not need it. It takes no configuration of its own on
|
|
4403
|
+
* purpose: the collector subscribes to exactly the events the observer's `include` requires, so a
|
|
4404
|
+
* per-call section outside that set would be created and then never written to — an empty section
|
|
4405
|
+
* that reads as "nothing happened". One shape per observer is the only shape that can be filled.
|
|
4406
|
+
*
|
|
4407
|
+
* The collector builds it rather than this method, so the resolved configuration never has to
|
|
4408
|
+
* leave the one object that owns it. Returns `undefined` when summaries are off, and is
|
|
4409
|
+
* idempotent: an existing summary is kept, not restarted.
|
|
4410
|
+
*/
|
|
4411
|
+
enableSummary() {
|
|
4412
|
+
return this.summary ??= this.observer.callSummaryCollector?.createSummary(this.callId);
|
|
4413
|
+
}
|
|
4414
|
+
/**
|
|
4415
|
+
* Remove a detector from **this call** by name, returning how many were removed.
|
|
4416
|
+
*
|
|
4417
|
+
* **Every** instance under the name goes — a name can legitimately be registered more than once.
|
|
4418
|
+
* When you want one of them specifically, go through the registry, which deals in instances:
|
|
4419
|
+
*
|
|
4420
|
+
* ```ts
|
|
4421
|
+
* const [ first ] = call.detectors.getAll('issue-fan-out-detector');
|
|
4422
|
+
*
|
|
4423
|
+
* call.detectors.remove(first);
|
|
4424
|
+
* ```
|
|
4425
|
+
*
|
|
4426
|
+
* Either route `close()`s the detector, so it unsubscribes from `activeIssuesRegistry` — without
|
|
4427
|
+
* that the registry keeps feeding a detector nobody is running any more, and its tracked set grows
|
|
4428
|
+
* for the life of the call.
|
|
4429
|
+
*
|
|
4430
|
+
* To stop building it on *future* calls too, use `observer.removeCallDetector(name)`.
|
|
4431
|
+
*/
|
|
4432
|
+
removeDetector(name) {
|
|
4433
|
+
return this.detectors.removeByName(name);
|
|
4434
|
+
}
|
|
4334
4435
|
/**
|
|
4335
4436
|
* Raise a call-level (server-side) finding; surfaced on the Observer bus as `call-issue`.
|
|
4336
4437
|
*
|
|
4337
|
-
* `payload`
|
|
4338
|
-
* serialise for.
|
|
4438
|
+
* `payload` is an **object** and holds evidence only — it is delivered to an in-process handler,
|
|
4439
|
+
* so there is nothing to serialise for. `scope` is stamped here, and the `callId` is already on
|
|
4440
|
+
* the event, so neither belongs in the payload. Put the interpretation in `conclusion`.
|
|
4339
4441
|
*/
|
|
4340
4442
|
addIssue(issue) {
|
|
4341
4443
|
if (this.closed) return;
|
|
4342
|
-
this._notify("call-issue", { ...this.eventScope, issue });
|
|
4444
|
+
this._notify("call-issue", { ...this.eventScope, issue: { ...issue, scope: "call" } });
|
|
4343
4445
|
}
|
|
4344
4446
|
close() {
|
|
4345
4447
|
if (this.closed) return;
|
|
4346
4448
|
this.update();
|
|
4347
4449
|
this.closed = true;
|
|
4450
|
+
if (this.closeTimer) {
|
|
4451
|
+
clearTimeout(this.closeTimer);
|
|
4452
|
+
this.closeTimer = void 0;
|
|
4453
|
+
}
|
|
4348
4454
|
let minSampleTimestamps;
|
|
4349
4455
|
let maxSampleTimestamps;
|
|
4350
4456
|
const clients = [...this.observedClients.values()];
|
|
@@ -4356,6 +4462,10 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4356
4462
|
if (this.startedAt === void 0) this.startedAt = minSampleTimestamps;
|
|
4357
4463
|
if (this.endedAt === void 0) this.endedAt = maxSampleTimestamps;
|
|
4358
4464
|
this.closedAt = Date.now();
|
|
4465
|
+
if (this.summary) {
|
|
4466
|
+
this.observer.callSummaryCollector?.finalise(this);
|
|
4467
|
+
this._notify("call-summary", { ...this.eventScope, summary: this.summary });
|
|
4468
|
+
}
|
|
4359
4469
|
this.detectors.clear();
|
|
4360
4470
|
this.activeIssuesRegistry.clear();
|
|
4361
4471
|
this.emit("close");
|
|
@@ -4403,6 +4513,7 @@ var ObservedCall = class extends import_events3.EventEmitter {
|
|
|
4403
4513
|
this.closeTimer = setTimeout(() => {
|
|
4404
4514
|
this.close();
|
|
4405
4515
|
}, this.settings.closeCallIfEmptyForMs);
|
|
4516
|
+
this.closeTimer.unref?.();
|
|
4406
4517
|
}
|
|
4407
4518
|
}
|
|
4408
4519
|
++this.totalRemovedClients;
|
|
@@ -5115,6 +5226,166 @@ var ActiveIssuesRegistry = class {
|
|
|
5115
5226
|
}
|
|
5116
5227
|
};
|
|
5117
5228
|
|
|
5229
|
+
// src/summaries/CallSummary.ts
|
|
5230
|
+
var defaultCallSummaryConfig = {
|
|
5231
|
+
include: [],
|
|
5232
|
+
maxIssues: 500,
|
|
5233
|
+
maxClientIds: 1e4
|
|
5234
|
+
};
|
|
5235
|
+
function createCallSummary(callId, config) {
|
|
5236
|
+
const summary = { callId, attachments: {} };
|
|
5237
|
+
if (config.include.includes("clients")) {
|
|
5238
|
+
summary.clients = { clientIds: [], peak: 0, joined: 0, left: 0 };
|
|
5239
|
+
}
|
|
5240
|
+
if (config.include.includes("issues")) {
|
|
5241
|
+
summary.issues = [];
|
|
5242
|
+
}
|
|
5243
|
+
if (config.include.includes("turnServers")) {
|
|
5244
|
+
summary.turnServers = { serverUrls: [], clientsRelayed: 0 };
|
|
5245
|
+
}
|
|
5246
|
+
if (config.include.includes("scores")) {
|
|
5247
|
+
summary.scores = { samples: 0 };
|
|
5248
|
+
}
|
|
5249
|
+
return summary;
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5252
|
+
// src/summaries/CallSummaryCollector.ts
|
|
5253
|
+
var logger8 = createLogger("CallSummaryCollector");
|
|
5254
|
+
var CallSummaryCollector = class {
|
|
5255
|
+
constructor(_observer, _config) {
|
|
5256
|
+
this._observer = _observer;
|
|
5257
|
+
this._config = _config;
|
|
5258
|
+
this._subscribeBuiltIns();
|
|
5259
|
+
this._subscribeEnrichers();
|
|
5260
|
+
}
|
|
5261
|
+
_observer;
|
|
5262
|
+
_config;
|
|
5263
|
+
_scratch = /* @__PURE__ */ new WeakMap();
|
|
5264
|
+
_listeners = [];
|
|
5265
|
+
_closed = false;
|
|
5266
|
+
/**
|
|
5267
|
+
* Build a summary for `callId` and start tracking it.
|
|
5268
|
+
*
|
|
5269
|
+
* Creating it here, rather than letting the call create one and hand it over, keeps the resolved
|
|
5270
|
+
* configuration inside the single object that owns it — and makes it impossible to end up with a
|
|
5271
|
+
* summary whose sections nobody subscribed to fill.
|
|
5272
|
+
*/
|
|
5273
|
+
createSummary(callId) {
|
|
5274
|
+
const summary = createCallSummary(callId, this._config);
|
|
5275
|
+
this._scratch.set(summary, { scores: [], turnClientIds: /* @__PURE__ */ new Set() });
|
|
5276
|
+
return summary;
|
|
5277
|
+
}
|
|
5278
|
+
/**
|
|
5279
|
+
* Finalise `call`'s summary: fold in what only makes sense once, and stamp the closing times.
|
|
5280
|
+
*
|
|
5281
|
+
* Percentiles are computed here rather than on every update — a median recomputed per tick over a
|
|
5282
|
+
* growing array is quadratic work to produce a number nobody reads until the end.
|
|
5283
|
+
*/
|
|
5284
|
+
finalise(call) {
|
|
5285
|
+
const summary = call.summary;
|
|
5286
|
+
if (!summary) return;
|
|
5287
|
+
const scratch = this._scratch.get(summary);
|
|
5288
|
+
summary.startedAt = call.startedAt;
|
|
5289
|
+
summary.endedAt = call.endedAt;
|
|
5290
|
+
summary.durationInMs = summary.startedAt !== void 0 && summary.endedAt !== void 0 ? Math.max(0, summary.endedAt - summary.startedAt) : void 0;
|
|
5291
|
+
summary.closedAt = Date.now();
|
|
5292
|
+
if (summary.scores && scratch) {
|
|
5293
|
+
summary.scores.samples = scratch.scores.length;
|
|
5294
|
+
if (0 < scratch.scores.length) {
|
|
5295
|
+
summary.scores.min = Math.min(...scratch.scores);
|
|
5296
|
+
summary.scores.max = Math.max(...scratch.scores);
|
|
5297
|
+
summary.scores.median = percentile(scratch.scores, 0.5);
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
if (summary.turnServers && scratch) {
|
|
5301
|
+
summary.turnServers.clientsRelayed = scratch.turnClientIds.size;
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
/** Drop every bus subscription. Called when the observer closes. */
|
|
5305
|
+
close() {
|
|
5306
|
+
if (this._closed) return;
|
|
5307
|
+
this._closed = true;
|
|
5308
|
+
for (const { event, listener } of this._listeners) {
|
|
5309
|
+
this._observer.off(event, listener);
|
|
5310
|
+
}
|
|
5311
|
+
this._listeners.length = 0;
|
|
5312
|
+
}
|
|
5313
|
+
/**
|
|
5314
|
+
* Subscribe `listener` to `event`, routed to the summary of the call the event names.
|
|
5315
|
+
*
|
|
5316
|
+
* The `observedCall` is read off the payload rather than closed over, which is what lets one
|
|
5317
|
+
* subscription serve every call.
|
|
5318
|
+
*/
|
|
5319
|
+
_on(event, handler) {
|
|
5320
|
+
const listener = (...args) => {
|
|
5321
|
+
const summary = args[0].observedCall.summary;
|
|
5322
|
+
if (!summary) return;
|
|
5323
|
+
const scratch = this._scratch.get(summary);
|
|
5324
|
+
if (!scratch) return;
|
|
5325
|
+
try {
|
|
5326
|
+
handler(summary, scratch, ...args);
|
|
5327
|
+
} catch (err) {
|
|
5328
|
+
logger8.warn("A call-summary handler for %s threw; continuing. %o", event, err);
|
|
5329
|
+
}
|
|
5330
|
+
};
|
|
5331
|
+
this._observer.on(event, listener);
|
|
5332
|
+
this._listeners.push({ event, listener });
|
|
5333
|
+
}
|
|
5334
|
+
_subscribeBuiltIns() {
|
|
5335
|
+
const include = this._config.include;
|
|
5336
|
+
if (include.includes("clients")) {
|
|
5337
|
+
this._on("client-added", (summary, _scratch, { observedCall, observedClient }) => {
|
|
5338
|
+
const clients = summary.clients;
|
|
5339
|
+
if (!clients) return;
|
|
5340
|
+
clients.joined += 1;
|
|
5341
|
+
clients.peak = Math.max(clients.peak, observedCall.observedClients.size);
|
|
5342
|
+
if (clients.clientIds.length < this._config.maxClientIds) {
|
|
5343
|
+
clients.clientIds.push(observedClient.clientId);
|
|
5344
|
+
} else {
|
|
5345
|
+
summary.truncated = { ...summary.truncated, clientIds: (summary.truncated?.clientIds ?? 0) + 1 };
|
|
5346
|
+
}
|
|
5347
|
+
});
|
|
5348
|
+
this._on("client-closed", (summary) => {
|
|
5349
|
+
if (summary.clients) summary.clients.left += 1;
|
|
5350
|
+
});
|
|
5351
|
+
}
|
|
5352
|
+
if (include.includes("issues")) {
|
|
5353
|
+
this._on("call-issue", (summary, _scratch, { issue }) => {
|
|
5354
|
+
const issues = summary.issues;
|
|
5355
|
+
if (!issues) return;
|
|
5356
|
+
if (issues.length < this._config.maxIssues) issues.push(issue);
|
|
5357
|
+
else summary.truncated = { ...summary.truncated, issues: (summary.truncated?.issues ?? 0) + 1 };
|
|
5358
|
+
});
|
|
5359
|
+
}
|
|
5360
|
+
if (include.includes("scores") || include.includes("turnServers")) {
|
|
5361
|
+
this._on("call-updated", (summary, scratch, { observedCall }) => {
|
|
5362
|
+
if (summary.scores && observedCall.score !== void 0) scratch.scores.push(observedCall.score);
|
|
5363
|
+
if (!summary.turnServers) return;
|
|
5364
|
+
for (const clientId of observedCall.clientsUsedTurn) scratch.turnClientIds.add(clientId);
|
|
5365
|
+
for (const client of observedCall.observedClients.values()) {
|
|
5366
|
+
for (const peerConnection of client.observedPeerConnections.values()) {
|
|
5367
|
+
for (const pair of peerConnection.selectedIceCandiadtePairForTurn) {
|
|
5368
|
+
const url = pair.getLocalCandidate()?.url;
|
|
5369
|
+
if (url && !summary.turnServers.serverUrls.includes(url)) {
|
|
5370
|
+
summary.turnServers.serverUrls.push(url);
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
}
|
|
5375
|
+
});
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
_subscribeEnrichers() {
|
|
5379
|
+
const enrich = this._config.enrich;
|
|
5380
|
+
if (!enrich) return;
|
|
5381
|
+
for (const name of Object.keys(enrich)) {
|
|
5382
|
+
const enricher = enrich[name];
|
|
5383
|
+
if (!enricher) continue;
|
|
5384
|
+
this._on(name, (summary, _scratch, ...args) => enricher(summary, ...args));
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
};
|
|
5388
|
+
|
|
5118
5389
|
// src/detectors/SfuCongestionDetector.ts
|
|
5119
5390
|
var SfuCongestionDetector = class _SfuCongestionDetector {
|
|
5120
5391
|
constructor(_observer, config = {}) {
|
|
@@ -5243,13 +5514,10 @@ var SfuCongestionDetector = class _SfuCongestionDetector {
|
|
|
5243
5514
|
absoluteIncrease: evaluation.absoluteIncrease,
|
|
5244
5515
|
relativeIncrease: evaluation.relativeIncrease
|
|
5245
5516
|
};
|
|
5246
|
-
this._observer.
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
payload
|
|
5251
|
-
},
|
|
5252
|
-
observer: this._observer
|
|
5517
|
+
this._observer.addIssue({
|
|
5518
|
+
type: this._config.emittedObserverIssueType,
|
|
5519
|
+
timestamp: Date.now(),
|
|
5520
|
+
payload: { ...payload }
|
|
5253
5521
|
});
|
|
5254
5522
|
}
|
|
5255
5523
|
/**
|
|
@@ -5363,11 +5631,9 @@ var ObserverConcurrentIssueDetector = class _ObserverConcurrentIssueDetector {
|
|
|
5363
5631
|
this._observer.addIssue({
|
|
5364
5632
|
type: issueType,
|
|
5365
5633
|
timestamp: now,
|
|
5634
|
+
conclusion,
|
|
5366
5635
|
payload: {
|
|
5367
|
-
type: issueType,
|
|
5368
5636
|
issueType: type,
|
|
5369
|
-
scope: "observer",
|
|
5370
|
-
conclusion,
|
|
5371
5637
|
clients: group.totalClients,
|
|
5372
5638
|
affectedClients: group.clientIds.length,
|
|
5373
5639
|
affectedRatio: group.affectedRatio,
|
|
@@ -5540,16 +5806,13 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5540
5806
|
this._observer.addIssue({
|
|
5541
5807
|
type: ClientPopulationIssueTypes.clientPopulationIssue,
|
|
5542
5808
|
timestamp: now,
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
confidence: this._confidenceOf(rollup)
|
|
5551
|
-
}
|
|
5552
|
-
}
|
|
5809
|
+
conclusion: {
|
|
5810
|
+
faultDomain: "client-population",
|
|
5811
|
+
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})`,
|
|
5812
|
+
recommendation: "this is not an SFU symptom \u2014 look at what those clients share: a recent release, a browser version, or shared/virtualised hardware",
|
|
5813
|
+
confidence: this._confidenceOf(rollup)
|
|
5814
|
+
},
|
|
5815
|
+
payload: { ...rollup }
|
|
5553
5816
|
});
|
|
5554
5817
|
}
|
|
5555
5818
|
}
|
|
@@ -5651,7 +5914,7 @@ var TurnServerHealthDetector = class _TurnServerHealthDetector {
|
|
|
5651
5914
|
this._observer.addIssue({
|
|
5652
5915
|
type: TurnServerHealthTypes.turnServerDegraded,
|
|
5653
5916
|
timestamp: now,
|
|
5654
|
-
payload: {
|
|
5917
|
+
payload: { ...health, otherServers }
|
|
5655
5918
|
});
|
|
5656
5919
|
}
|
|
5657
5920
|
for (const serverUrl of [...this._streaks.keys()]) {
|
|
@@ -5759,7 +6022,6 @@ var TurnServerOutageDetector = class _TurnServerOutageDetector {
|
|
|
5759
6022
|
type: TurnServerOutageTypes.turnServerOutage,
|
|
5760
6023
|
timestamp: now,
|
|
5761
6024
|
payload: {
|
|
5762
|
-
type: TurnServerOutageTypes.turnServerOutage,
|
|
5763
6025
|
serverUrl,
|
|
5764
6026
|
peakClients: peak,
|
|
5765
6027
|
currentClients: live,
|
|
@@ -6033,17 +6295,13 @@ var SimulcastReceiverValidator = class _SimulcastReceiverValidator {
|
|
|
6033
6295
|
this._observer.addIssue({
|
|
6034
6296
|
type: LOWEST_COMMON_DENOMINATOR_ISSUE,
|
|
6035
6297
|
timestamp: decidedAt,
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
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",
|
|
6044
|
-
confidence: 0.8
|
|
6045
|
-
}
|
|
6046
|
-
}
|
|
6298
|
+
conclusion: {
|
|
6299
|
+
faultDomain: "infrastructure",
|
|
6300
|
+
summary: "one bad receiver is dragging a publisher's bitrate down for everyone \u2014 the SFU is not selecting layers per consumer",
|
|
6301
|
+
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",
|
|
6302
|
+
confidence: 0.8
|
|
6303
|
+
},
|
|
6304
|
+
payload: { checks: this._checks, ...outcome.evidence }
|
|
6047
6305
|
});
|
|
6048
6306
|
}
|
|
6049
6307
|
this.onDone(this.report);
|
|
@@ -6146,17 +6404,13 @@ var RemoteTrackResolverValidator = class _RemoteTrackResolverValidator {
|
|
|
6146
6404
|
this._observer.addIssue({
|
|
6147
6405
|
type: UNRESOLVED_TRACK_LINKS_ISSUE,
|
|
6148
6406
|
timestamp: decidedAt,
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
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",
|
|
6157
|
-
confidence: 0.9
|
|
6158
|
-
}
|
|
6159
|
-
}
|
|
6407
|
+
conclusion: {
|
|
6408
|
+
faultDomain: "infrastructure",
|
|
6409
|
+
summary: "a RemoteTrackResolver is configured but has never linked a subscriber to a publisher",
|
|
6410
|
+
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",
|
|
6411
|
+
confidence: 0.9
|
|
6412
|
+
},
|
|
6413
|
+
payload: { checks: this._checks, ...outcome.evidence }
|
|
6160
6414
|
});
|
|
6161
6415
|
}
|
|
6162
6416
|
this.onDone(this.report);
|
|
@@ -6267,17 +6521,16 @@ var CodecConsistencyValidator = class _CodecConsistencyValidator {
|
|
|
6267
6521
|
this._observer.addIssue({
|
|
6268
6522
|
type: CODEC_MISMATCH_ISSUE,
|
|
6269
6523
|
timestamp: decidedAt,
|
|
6524
|
+
conclusion: {
|
|
6525
|
+
faultDomain: "infrastructure",
|
|
6526
|
+
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",
|
|
6527
|
+
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",
|
|
6528
|
+
confidence: 0.85
|
|
6529
|
+
},
|
|
6270
6530
|
payload: {
|
|
6271
|
-
type: CODEC_MISMATCH_ISSUE,
|
|
6272
6531
|
verdict: outcome.verdict,
|
|
6273
6532
|
checks: this._checks,
|
|
6274
|
-
evidence: outcome.evidence
|
|
6275
|
-
conclusion: {
|
|
6276
|
-
faultDomain: "infrastructure",
|
|
6277
|
-
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",
|
|
6278
|
-
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",
|
|
6279
|
-
confidence: 0.85
|
|
6280
|
-
}
|
|
6533
|
+
evidence: outcome.evidence
|
|
6281
6534
|
}
|
|
6282
6535
|
});
|
|
6283
6536
|
}
|
|
@@ -6291,7 +6544,7 @@ function mediaKindOf(mimeType) {
|
|
|
6291
6544
|
}
|
|
6292
6545
|
|
|
6293
6546
|
// src/Observer.ts
|
|
6294
|
-
var
|
|
6547
|
+
var logger9 = createLogger("Observer");
|
|
6295
6548
|
var Observer = class extends import_events6.EventEmitter {
|
|
6296
6549
|
observedTURN = new ObservedTURN();
|
|
6297
6550
|
observedCalls = /* @__PURE__ */ new Map();
|
|
@@ -6347,6 +6600,14 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6347
6600
|
* ```
|
|
6348
6601
|
*/
|
|
6349
6602
|
callDetectorConfigs = /* @__PURE__ */ new Map();
|
|
6603
|
+
/**
|
|
6604
|
+
* Owns every call's summary: the resolved `config.callSummary`, the bus subscriptions that keep
|
|
6605
|
+
* the summaries current (one per event type, not one per call), and the summaries themselves.
|
|
6606
|
+
*
|
|
6607
|
+
* `undefined` when `config.callSummary` was absent or `null` — so its presence *is* the answer to
|
|
6608
|
+
* "are summaries on", and nothing is subscribed to anything.
|
|
6609
|
+
*/
|
|
6610
|
+
callSummaryCollector;
|
|
6350
6611
|
constructor(config = {}) {
|
|
6351
6612
|
super();
|
|
6352
6613
|
this.setMaxListeners(Infinity);
|
|
@@ -6358,6 +6619,12 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6358
6619
|
closeClientIfIdleForMs: 6e4,
|
|
6359
6620
|
...config
|
|
6360
6621
|
};
|
|
6622
|
+
if (this.config.callSummary) {
|
|
6623
|
+
this.callSummaryCollector = new CallSummaryCollector(this, {
|
|
6624
|
+
...defaultCallSummaryConfig,
|
|
6625
|
+
...this.config.callSummary
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
6361
6628
|
}
|
|
6362
6629
|
get numberOfCalls() {
|
|
6363
6630
|
return this.observedCalls.size;
|
|
@@ -6365,6 +6632,12 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6365
6632
|
get appData() {
|
|
6366
6633
|
return this.config.appData;
|
|
6367
6634
|
}
|
|
6635
|
+
/**
|
|
6636
|
+
* Build a cross-call detector onto `observer.detectors`. Chainable.
|
|
6637
|
+
*
|
|
6638
|
+
* To get a handle on what was built — to inspect it, or to remove that exact instance later — read
|
|
6639
|
+
* it back off the registry: `observer.detectors.getAll(name)`, or `observer.detectors.instances`.
|
|
6640
|
+
*/
|
|
6368
6641
|
addObserverDetector(name, config = {}) {
|
|
6369
6642
|
if (this.closed) return this;
|
|
6370
6643
|
let detector;
|
|
@@ -6390,7 +6663,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6390
6663
|
break;
|
|
6391
6664
|
}
|
|
6392
6665
|
default: {
|
|
6393
|
-
|
|
6666
|
+
logger9.warn("Unknown detector name %s; skipping", name);
|
|
6394
6667
|
return this;
|
|
6395
6668
|
}
|
|
6396
6669
|
}
|
|
@@ -6408,10 +6681,41 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6408
6681
|
this.callDetectorConfigs.set(name, config);
|
|
6409
6682
|
return this;
|
|
6410
6683
|
}
|
|
6411
|
-
/**
|
|
6412
|
-
|
|
6684
|
+
/**
|
|
6685
|
+
* Remove an observer-scoped detector by name, returning how many were removed.
|
|
6686
|
+
*
|
|
6687
|
+
* **Every** instance registered under the name goes, since a name can legitimately be registered
|
|
6688
|
+
* more than once (`ClientPopulationIssueDetector` is meant to be added once per `groupBy` axis).
|
|
6689
|
+
* When you want one of them specifically, go through the registry, which deals in instances:
|
|
6690
|
+
*
|
|
6691
|
+
* ```ts
|
|
6692
|
+
* const [ byBrowser, byOs ] = observer.detectors.getAll('client-population-issue-detector');
|
|
6693
|
+
*
|
|
6694
|
+
* observer.detectors.remove(byOs); // keeps the browser axis running
|
|
6695
|
+
* ```
|
|
6696
|
+
*
|
|
6697
|
+
* Either route `close()`s the detector, so it unsubscribes from the issue registry and drops any
|
|
6698
|
+
* timers or bus listeners it held.
|
|
6699
|
+
*/
|
|
6700
|
+
removeObserverDetector(name) {
|
|
6701
|
+
return this.detectors.removeByName(name);
|
|
6702
|
+
}
|
|
6703
|
+
/**
|
|
6704
|
+
* Stop building `name` on calls created from now on.
|
|
6705
|
+
*
|
|
6706
|
+
* By default this also removes it from the calls **already open**, so that "remove this detector"
|
|
6707
|
+
* means the same thing whether you say it before or after a call started — the alternative leaves
|
|
6708
|
+
* a fleet where the detector is live on some calls and not others, decided by join time. Pass
|
|
6709
|
+
* `{ includeOpenCalls: false }` to change only what future calls are built with.
|
|
6710
|
+
*
|
|
6711
|
+
* Returns the number of live detector instances removed (`0` when only the config changed).
|
|
6712
|
+
*/
|
|
6713
|
+
removeCallDetector(name, { includeOpenCalls = true } = {}) {
|
|
6413
6714
|
this.callDetectorConfigs.delete(name);
|
|
6414
|
-
return
|
|
6715
|
+
if (!includeOpenCalls) return 0;
|
|
6716
|
+
let removed = 0;
|
|
6717
|
+
for (const call of this.observedCalls.values()) removed += call.removeDetector(name);
|
|
6718
|
+
return removed;
|
|
6415
6719
|
}
|
|
6416
6720
|
/**
|
|
6417
6721
|
* Start a structural check. It runs on each `observer.update()` until it can decide, reports once
|
|
@@ -6444,23 +6748,53 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6444
6748
|
break;
|
|
6445
6749
|
}
|
|
6446
6750
|
if (!validator) {
|
|
6447
|
-
|
|
6751
|
+
logger9.warn("Unknown validator name %s; skipping", name);
|
|
6448
6752
|
return this;
|
|
6449
6753
|
}
|
|
6450
6754
|
this.validators.add(validator);
|
|
6451
6755
|
return this;
|
|
6452
6756
|
}
|
|
6757
|
+
/**
|
|
6758
|
+
* Stop a running validation, by name or by instance. Returns how many were cancelled.
|
|
6759
|
+
*
|
|
6760
|
+
* Cancelling is **not** silent discarding. The validator finishes with `inconclusive` and the given
|
|
6761
|
+
* `reason`, emits `validation-ready` like any other completion, and removes itself. That matters
|
|
6762
|
+
* because anything waiting on the verdict — a deploy gate, a dashboard, a promise — would otherwise
|
|
6763
|
+
* wait forever, and because "we stopped asking" is a materially different outcome from "we asked
|
|
6764
|
+
* and learned nothing", which is exactly what `inconclusive` with a reason records.
|
|
6765
|
+
*
|
|
6766
|
+
* ```ts
|
|
6767
|
+
* observer.cancelValidator('simulcast-receivers', 'sfu redeployed');
|
|
6768
|
+
*
|
|
6769
|
+
* // or one specific instance — `observer.validators` holds what is running
|
|
6770
|
+
* for (const validator of observer.validators) observer.cancelValidator(validator, 'shutting down');
|
|
6771
|
+
* ```
|
|
6772
|
+
*
|
|
6773
|
+
* Pass a real reason. The default tells the reader nothing they could not already infer.
|
|
6774
|
+
*/
|
|
6775
|
+
cancelValidator(target, reason = "cancelled") {
|
|
6776
|
+
const running = [...this.validators];
|
|
6777
|
+
const matching = typeof target === "string" ? running.filter((validator) => validator.name === target) : running.filter((validator) => validator === target);
|
|
6778
|
+
for (const validator of matching) {
|
|
6779
|
+
try {
|
|
6780
|
+
validator.cancel(reason);
|
|
6781
|
+
} catch (err) {
|
|
6782
|
+
logger9.warn("Error cancelling validator %s: %o", validator.name, err);
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6785
|
+
return matching.length;
|
|
6786
|
+
}
|
|
6453
6787
|
getObservedCall(callId) {
|
|
6454
6788
|
if (this.closed || !this.observedCalls.has(callId)) return;
|
|
6455
6789
|
return this.observedCalls.get(callId);
|
|
6456
6790
|
}
|
|
6457
6791
|
createObservedCall(settings) {
|
|
6458
6792
|
if (this.closed) {
|
|
6459
|
-
|
|
6793
|
+
logger9.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
|
|
6460
6794
|
return void 0;
|
|
6461
6795
|
}
|
|
6462
6796
|
if (this.observedCalls.has(settings.callId)) {
|
|
6463
|
-
|
|
6797
|
+
logger9.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
|
|
6464
6798
|
return this.observedCalls.get(settings.callId);
|
|
6465
6799
|
}
|
|
6466
6800
|
const callSettings = {
|
|
@@ -6475,6 +6809,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6475
6809
|
callActiveIssuesRegistry
|
|
6476
6810
|
);
|
|
6477
6811
|
observedCall.remoteTrackResolver = this.config.createRemoteTrackResolver?.(observedCall);
|
|
6812
|
+
observedCall.enableSummary();
|
|
6478
6813
|
for (const [name, detectorConfig] of this.callDetectorConfigs) {
|
|
6479
6814
|
observedCall.addDetector(name, detectorConfig);
|
|
6480
6815
|
}
|
|
@@ -6497,11 +6832,11 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6497
6832
|
}
|
|
6498
6833
|
createObservedMediasoupRouter(settings) {
|
|
6499
6834
|
if (this.closed) {
|
|
6500
|
-
|
|
6835
|
+
logger9.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
|
|
6501
6836
|
return void 0;
|
|
6502
6837
|
}
|
|
6503
6838
|
if (this.observedMediasoupRouters.has(settings.router.id)) {
|
|
6504
|
-
|
|
6839
|
+
logger9.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
|
|
6505
6840
|
return this.observedMediasoupRouters.get(settings.router.id);
|
|
6506
6841
|
}
|
|
6507
6842
|
const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
|
|
@@ -6532,13 +6867,14 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6532
6867
|
}
|
|
6533
6868
|
close() {
|
|
6534
6869
|
if (this.closed) {
|
|
6535
|
-
return
|
|
6870
|
+
return logger9.debug("Attempted to close twice");
|
|
6536
6871
|
}
|
|
6537
6872
|
this.closed = true;
|
|
6538
6873
|
for (const call of [...this.observedCalls.values()]) call.close();
|
|
6539
6874
|
this.detectors.clear();
|
|
6540
|
-
for (const validator of [...this.validators]) validator.cancel();
|
|
6875
|
+
for (const validator of [...this.validators]) validator.cancel("observer closed");
|
|
6541
6876
|
this.validators.clear();
|
|
6877
|
+
this.callSummaryCollector?.close();
|
|
6542
6878
|
this.activeIssuesRegistry.clear();
|
|
6543
6879
|
this._notify("observer-closed", { ...this.eventScope });
|
|
6544
6880
|
}
|
|
@@ -6549,7 +6885,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6549
6885
|
try {
|
|
6550
6886
|
this.acceptMiddlewares.process({ sample, context });
|
|
6551
6887
|
} catch (err) {
|
|
6552
|
-
|
|
6888
|
+
logger9.warn("An accept middleware threw; dropping the sample. %o", err);
|
|
6553
6889
|
}
|
|
6554
6890
|
if (!sample.callId) {
|
|
6555
6891
|
this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
|
|
@@ -6601,7 +6937,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6601
6937
|
try {
|
|
6602
6938
|
validator.update();
|
|
6603
6939
|
} catch (err) {
|
|
6604
|
-
|
|
6940
|
+
logger9.warn("Error running validator %s: %o", validator.name, err);
|
|
6605
6941
|
}
|
|
6606
6942
|
}
|
|
6607
6943
|
this._notify("observer-updated", { ...this.eventScope });
|
|
@@ -6614,7 +6950,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6614
6950
|
*/
|
|
6615
6951
|
addIssue(issue) {
|
|
6616
6952
|
if (this.closed) return;
|
|
6617
|
-
this._notify("observer-issue", { ...this.eventScope, issue });
|
|
6953
|
+
this._notify("observer-issue", { ...this.eventScope, issue: { ...issue, scope: "observer" } });
|
|
6618
6954
|
}
|
|
6619
6955
|
/** Emit an Observer-bus event. */
|
|
6620
6956
|
_notify(type, ...args) {
|
|
@@ -6732,18 +7068,11 @@ var CallHealthAggregator = class {
|
|
|
6732
7068
|
}
|
|
6733
7069
|
};
|
|
6734
7070
|
|
|
6735
|
-
// src/common/
|
|
6736
|
-
function issuePayloadOf(issue) {
|
|
6737
|
-
const payload = issue.payload;
|
|
6738
|
-
if (payload === void 0) return void 0;
|
|
6739
|
-
return typeof payload === "string" ? parseJsonObject(payload) : payload;
|
|
6740
|
-
}
|
|
7071
|
+
// src/common/Issue.ts
|
|
6741
7072
|
function issuePayloadAsString(issue) {
|
|
6742
|
-
|
|
6743
|
-
if (payload === void 0) return void 0;
|
|
6744
|
-
if (typeof payload === "string") return payload;
|
|
7073
|
+
if (issue.payload === void 0) return void 0;
|
|
6745
7074
|
try {
|
|
6746
|
-
return JSON.stringify(payload);
|
|
7075
|
+
return JSON.stringify(issue.payload);
|
|
6747
7076
|
} catch {
|
|
6748
7077
|
return void 0;
|
|
6749
7078
|
}
|
|
@@ -7038,6 +7367,7 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7038
7367
|
CallConcurrentIssueDetector,
|
|
7039
7368
|
CallConcurrentIssueTypes,
|
|
7040
7369
|
CallHealthAggregator,
|
|
7370
|
+
CallSummaryCollector,
|
|
7041
7371
|
ClientEventTypes,
|
|
7042
7372
|
ClientMetaTypes,
|
|
7043
7373
|
ClientPopulationIssueDetector,
|
|
@@ -7096,16 +7426,17 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7096
7426
|
concludeObserverIssue,
|
|
7097
7427
|
correlation,
|
|
7098
7428
|
counterDelta,
|
|
7429
|
+
createCallSummary,
|
|
7099
7430
|
createDefaultMediasoupRemoteTrackResolverFactory,
|
|
7100
7431
|
createInMemorySink,
|
|
7101
7432
|
createJsonlFileSink,
|
|
7102
7433
|
createJsonlFileSinkFactory,
|
|
7103
7434
|
createLogger,
|
|
7104
7435
|
createP2pRemoteTrackResolverFactory,
|
|
7436
|
+
defaultCallSummaryConfig,
|
|
7105
7437
|
defaultClientHealthThresholds,
|
|
7106
7438
|
isClientIssueResolutionEntry,
|
|
7107
7439
|
issuePayloadAsString,
|
|
7108
|
-
issuePayloadOf,
|
|
7109
7440
|
mannKendall,
|
|
7110
7441
|
mannKendallVerdict,
|
|
7111
7442
|
median,
|