@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.mjs
CHANGED
|
@@ -37,8 +37,8 @@ function createLogger(moduleName) {
|
|
|
37
37
|
}
|
|
38
38
|
}();
|
|
39
39
|
}
|
|
40
|
-
function setObserverLogger(
|
|
41
|
-
mainLogger =
|
|
40
|
+
function setObserverLogger(logger10) {
|
|
41
|
+
mainLogger = logger10;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/ObservedCall.ts
|
|
@@ -2632,6 +2632,10 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2632
2632
|
}
|
|
2633
2633
|
close() {
|
|
2634
2634
|
if (this.closed) return;
|
|
2635
|
+
if (this.closeTimer) {
|
|
2636
|
+
clearTimeout(this.closeTimer);
|
|
2637
|
+
this.closeTimer = void 0;
|
|
2638
|
+
}
|
|
2635
2639
|
this._flushPendingInjections();
|
|
2636
2640
|
const activeIssues = [...this.activeIssues.values()];
|
|
2637
2641
|
for (const issue of activeIssues) {
|
|
@@ -2794,6 +2798,7 @@ var ObservedClient = class extends EventEmitter2 {
|
|
|
2794
2798
|
this.closeTimer = setTimeout(() => {
|
|
2795
2799
|
this.close();
|
|
2796
2800
|
}, this.settings.closeClientIfIdleForMs);
|
|
2801
|
+
this.closeTimer.unref?.();
|
|
2797
2802
|
}
|
|
2798
2803
|
this._activeSample = void 0;
|
|
2799
2804
|
try {
|
|
@@ -3265,6 +3270,24 @@ var Detectors = class {
|
|
|
3265
3270
|
constructor(...detectors) {
|
|
3266
3271
|
this._detectors = detectors;
|
|
3267
3272
|
}
|
|
3273
|
+
/**
|
|
3274
|
+
* Every registered detector, in registration order.
|
|
3275
|
+
*
|
|
3276
|
+
* This is **the** way to get hold of an instance: `addDetector` / `addObserverDetector` are
|
|
3277
|
+
* chainable and return the owning entity, so the registry is where instances live. Read it to
|
|
3278
|
+
* inspect a detector's state, or to pick one out and {@link remove} it.
|
|
3279
|
+
*
|
|
3280
|
+
* A copy, not the live array — a caller iterating this while removing would otherwise skip
|
|
3281
|
+
* entries, and that is exactly what "remove the ones that look like X" does.
|
|
3282
|
+
*/
|
|
3283
|
+
get instances() {
|
|
3284
|
+
return [...this._detectors];
|
|
3285
|
+
}
|
|
3286
|
+
/** Iterate the registry directly: `for (const detector of call.detectors)`. */
|
|
3287
|
+
[Symbol.iterator]() {
|
|
3288
|
+
return this.instances[Symbol.iterator]();
|
|
3289
|
+
}
|
|
3290
|
+
/** The names in registration order. Duplicates are meaningful — see {@link getAll}. */
|
|
3268
3291
|
get listOfNames() {
|
|
3269
3292
|
return this._detectors.map((d) => d.name);
|
|
3270
3293
|
}
|
|
@@ -3274,12 +3297,48 @@ var Detectors = class {
|
|
|
3274
3297
|
add(detector) {
|
|
3275
3298
|
this._detectors.push(detector);
|
|
3276
3299
|
}
|
|
3300
|
+
/** The first detector registered under `name`. See {@link getAll} when several can share one. */
|
|
3277
3301
|
get(name) {
|
|
3278
3302
|
return this._detectors.find((detector) => detector.name === name);
|
|
3279
3303
|
}
|
|
3304
|
+
/**
|
|
3305
|
+
* Every detector registered under `name`.
|
|
3306
|
+
*
|
|
3307
|
+
* More than one is legitimate: `ClientPopulationIssueDetector` is meant to be added once per
|
|
3308
|
+
* `groupBy` axis, and two instances of it share a name.
|
|
3309
|
+
*/
|
|
3310
|
+
getAll(name) {
|
|
3311
|
+
return this._detectors.filter((detector) => detector.name === name);
|
|
3312
|
+
}
|
|
3313
|
+
has(name) {
|
|
3314
|
+
return this._detectors.some((detector) => detector.name === name);
|
|
3315
|
+
}
|
|
3316
|
+
/** Remove one specific instance. Returns `false` if it was not registered here. */
|
|
3280
3317
|
remove(detector) {
|
|
3281
|
-
|
|
3318
|
+
const remaining = this._detectors.filter((candidate) => candidate !== detector);
|
|
3319
|
+
if (remaining.length === this._detectors.length) return false;
|
|
3320
|
+
this._detectors = remaining;
|
|
3282
3321
|
this._close(detector);
|
|
3322
|
+
return true;
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Remove **every** detector registered under `name`, returning how many were removed.
|
|
3326
|
+
*
|
|
3327
|
+
* All of them rather than the first, because a name can legitimately be registered more than once
|
|
3328
|
+
* (see {@link getAll}) and "remove the `client-population-issue-detector`" cannot sensibly mean
|
|
3329
|
+
* "remove whichever axis happens to be first in the array". Removing all of them is the only
|
|
3330
|
+
* behaviour that leaves the registry in a state the caller can predict from the name alone.
|
|
3331
|
+
*
|
|
3332
|
+
* Each removed detector gets `close()`, so trackers unsubscribe from the issue registry, bus
|
|
3333
|
+
* listeners drop, and timers clear — a detector removed without closing keeps being fed issues
|
|
3334
|
+
* forever.
|
|
3335
|
+
*/
|
|
3336
|
+
removeByName(name) {
|
|
3337
|
+
const removed = this._detectors.filter((detector) => detector.name === name);
|
|
3338
|
+
if (removed.length === 0) return 0;
|
|
3339
|
+
this._detectors = this._detectors.filter((detector) => detector.name !== name);
|
|
3340
|
+
for (const detector of removed) this._close(detector);
|
|
3341
|
+
return removed.length;
|
|
3283
3342
|
}
|
|
3284
3343
|
update() {
|
|
3285
3344
|
for (const detector of this._detectors) {
|
|
@@ -3397,7 +3456,6 @@ var UnconsumedTrackDetector = class _UnconsumedTrackDetector {
|
|
|
3397
3456
|
type: UnconsumedTrackTypes.unconsumedPublishedTrack,
|
|
3398
3457
|
timestamp: now,
|
|
3399
3458
|
payload: {
|
|
3400
|
-
type: UnconsumedTrackTypes.unconsumedPublishedTrack,
|
|
3401
3459
|
trackId: outboundTrack.id,
|
|
3402
3460
|
kind: outboundTrack.kind,
|
|
3403
3461
|
publisherClientId: peerConnection?.client.clientId,
|
|
@@ -3543,7 +3601,6 @@ var TrackDeliveryMismatchDetector = class _TrackDeliveryMismatchDetector {
|
|
|
3543
3601
|
type,
|
|
3544
3602
|
timestamp: now,
|
|
3545
3603
|
payload: {
|
|
3546
|
-
type,
|
|
3547
3604
|
trackId: outboundTrackId,
|
|
3548
3605
|
publisherClientId: delivery.publisherClientId,
|
|
3549
3606
|
publisherSending: delivery.publisherSending,
|
|
@@ -3796,12 +3853,9 @@ var CallConcurrentIssueDetector = class _CallConcurrentIssueDetector {
|
|
|
3796
3853
|
this._call.addIssue({
|
|
3797
3854
|
type: issueType,
|
|
3798
3855
|
timestamp: now,
|
|
3856
|
+
conclusion,
|
|
3799
3857
|
payload: {
|
|
3800
|
-
type: issueType,
|
|
3801
3858
|
issueType: type,
|
|
3802
|
-
scope: "call",
|
|
3803
|
-
conclusion,
|
|
3804
|
-
callId: this._call.callId,
|
|
3805
3859
|
clients: group.totalClients,
|
|
3806
3860
|
affectedClients: group.clientIds.length,
|
|
3807
3861
|
affectedRatio: group.affectedRatio,
|
|
@@ -3925,10 +3979,9 @@ var IssueFanOutDetector = class _IssueFanOutDetector {
|
|
|
3925
3979
|
this._call.addIssue({
|
|
3926
3980
|
type,
|
|
3927
3981
|
timestamp: now,
|
|
3982
|
+
conclusion,
|
|
3928
3983
|
payload: {
|
|
3929
|
-
type,
|
|
3930
3984
|
issueType,
|
|
3931
|
-
conclusion,
|
|
3932
3985
|
trackId: publisher.id,
|
|
3933
3986
|
kind: publisher.kind,
|
|
3934
3987
|
publisherClientId: publisher.getPeerConnection().client.clientId,
|
|
@@ -4075,18 +4128,14 @@ var PublisherFaultCorroborationDetector = class _PublisherFaultCorroborationDete
|
|
|
4075
4128
|
this._call.addIssue({
|
|
4076
4129
|
type: PublisherFaultTypes.corroboratedPublisherFault,
|
|
4077
4130
|
timestamp: now,
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
// Higher than any single-ended finding: two independent parties, one conclusion.
|
|
4087
|
-
confidence: 0.9
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
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 }
|
|
4090
4139
|
});
|
|
4091
4140
|
}
|
|
4092
4141
|
}
|
|
@@ -4142,6 +4191,14 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4142
4191
|
value: void 0
|
|
4143
4192
|
};
|
|
4144
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;
|
|
4145
4202
|
/**
|
|
4146
4203
|
* Published tracks that currently have **no** subscriber linked to them.
|
|
4147
4204
|
*
|
|
@@ -4181,6 +4238,12 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4181
4238
|
get score() {
|
|
4182
4239
|
return this.calculatedScore.value;
|
|
4183
4240
|
}
|
|
4241
|
+
/**
|
|
4242
|
+
* Build a call-scoped detector onto this call. Chainable.
|
|
4243
|
+
*
|
|
4244
|
+
* To get a handle on what was built — to inspect it, or to remove that exact instance later — read
|
|
4245
|
+
* it back off the registry: `call.detectors.getAll(name)`, or `call.detectors.instances`.
|
|
4246
|
+
*/
|
|
4184
4247
|
addDetector(name, config = {}) {
|
|
4185
4248
|
if (this.closed) return this;
|
|
4186
4249
|
let detector;
|
|
@@ -4213,20 +4276,61 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4213
4276
|
this.detectors.add(detector);
|
|
4214
4277
|
return this;
|
|
4215
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
|
+
}
|
|
4294
|
+
/**
|
|
4295
|
+
* Remove a detector from **this call** by name, returning how many were removed.
|
|
4296
|
+
*
|
|
4297
|
+
* **Every** instance under the name goes — a name can legitimately be registered more than once.
|
|
4298
|
+
* When you want one of them specifically, go through the registry, which deals in instances:
|
|
4299
|
+
*
|
|
4300
|
+
* ```ts
|
|
4301
|
+
* const [ first ] = call.detectors.getAll('issue-fan-out-detector');
|
|
4302
|
+
*
|
|
4303
|
+
* call.detectors.remove(first);
|
|
4304
|
+
* ```
|
|
4305
|
+
*
|
|
4306
|
+
* Either route `close()`s the detector, so it unsubscribes from `activeIssuesRegistry` — without
|
|
4307
|
+
* that the registry keeps feeding a detector nobody is running any more, and its tracked set grows
|
|
4308
|
+
* for the life of the call.
|
|
4309
|
+
*
|
|
4310
|
+
* To stop building it on *future* calls too, use `observer.removeCallDetector(name)`.
|
|
4311
|
+
*/
|
|
4312
|
+
removeDetector(name) {
|
|
4313
|
+
return this.detectors.removeByName(name);
|
|
4314
|
+
}
|
|
4216
4315
|
/**
|
|
4217
4316
|
* Raise a call-level (server-side) finding; surfaced on the Observer bus as `call-issue`.
|
|
4218
4317
|
*
|
|
4219
|
-
* `payload`
|
|
4220
|
-
* serialise for.
|
|
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`.
|
|
4221
4321
|
*/
|
|
4222
4322
|
addIssue(issue) {
|
|
4223
4323
|
if (this.closed) return;
|
|
4224
|
-
this._notify("call-issue", { ...this.eventScope, issue });
|
|
4324
|
+
this._notify("call-issue", { ...this.eventScope, issue: { ...issue, scope: "call" } });
|
|
4225
4325
|
}
|
|
4226
4326
|
close() {
|
|
4227
4327
|
if (this.closed) return;
|
|
4228
4328
|
this.update();
|
|
4229
4329
|
this.closed = true;
|
|
4330
|
+
if (this.closeTimer) {
|
|
4331
|
+
clearTimeout(this.closeTimer);
|
|
4332
|
+
this.closeTimer = void 0;
|
|
4333
|
+
}
|
|
4230
4334
|
let minSampleTimestamps;
|
|
4231
4335
|
let maxSampleTimestamps;
|
|
4232
4336
|
const clients = [...this.observedClients.values()];
|
|
@@ -4238,6 +4342,10 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4238
4342
|
if (this.startedAt === void 0) this.startedAt = minSampleTimestamps;
|
|
4239
4343
|
if (this.endedAt === void 0) this.endedAt = maxSampleTimestamps;
|
|
4240
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
|
+
}
|
|
4241
4349
|
this.detectors.clear();
|
|
4242
4350
|
this.activeIssuesRegistry.clear();
|
|
4243
4351
|
this.emit("close");
|
|
@@ -4285,6 +4393,7 @@ var ObservedCall = class extends EventEmitter3 {
|
|
|
4285
4393
|
this.closeTimer = setTimeout(() => {
|
|
4286
4394
|
this.close();
|
|
4287
4395
|
}, this.settings.closeCallIfEmptyForMs);
|
|
4396
|
+
this.closeTimer.unref?.();
|
|
4288
4397
|
}
|
|
4289
4398
|
}
|
|
4290
4399
|
++this.totalRemovedClients;
|
|
@@ -4997,6 +5106,166 @@ var ActiveIssuesRegistry = class {
|
|
|
4997
5106
|
}
|
|
4998
5107
|
};
|
|
4999
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
|
+
|
|
5000
5269
|
// src/detectors/SfuCongestionDetector.ts
|
|
5001
5270
|
var SfuCongestionDetector = class _SfuCongestionDetector {
|
|
5002
5271
|
constructor(_observer, config = {}) {
|
|
@@ -5125,13 +5394,10 @@ var SfuCongestionDetector = class _SfuCongestionDetector {
|
|
|
5125
5394
|
absoluteIncrease: evaluation.absoluteIncrease,
|
|
5126
5395
|
relativeIncrease: evaluation.relativeIncrease
|
|
5127
5396
|
};
|
|
5128
|
-
this._observer.
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
payload
|
|
5133
|
-
},
|
|
5134
|
-
observer: this._observer
|
|
5397
|
+
this._observer.addIssue({
|
|
5398
|
+
type: this._config.emittedObserverIssueType,
|
|
5399
|
+
timestamp: Date.now(),
|
|
5400
|
+
payload: { ...payload }
|
|
5135
5401
|
});
|
|
5136
5402
|
}
|
|
5137
5403
|
/**
|
|
@@ -5245,11 +5511,9 @@ var ObserverConcurrentIssueDetector = class _ObserverConcurrentIssueDetector {
|
|
|
5245
5511
|
this._observer.addIssue({
|
|
5246
5512
|
type: issueType,
|
|
5247
5513
|
timestamp: now,
|
|
5514
|
+
conclusion,
|
|
5248
5515
|
payload: {
|
|
5249
|
-
type: issueType,
|
|
5250
5516
|
issueType: type,
|
|
5251
|
-
scope: "observer",
|
|
5252
|
-
conclusion,
|
|
5253
5517
|
clients: group.totalClients,
|
|
5254
5518
|
affectedClients: group.clientIds.length,
|
|
5255
5519
|
affectedRatio: group.affectedRatio,
|
|
@@ -5422,16 +5686,13 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5422
5686
|
this._observer.addIssue({
|
|
5423
5687
|
type: ClientPopulationIssueTypes.clientPopulationIssue,
|
|
5424
5688
|
timestamp: now,
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
confidence: this._confidenceOf(rollup)
|
|
5433
|
-
}
|
|
5434
|
-
}
|
|
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 }
|
|
5435
5696
|
});
|
|
5436
5697
|
}
|
|
5437
5698
|
}
|
|
@@ -5533,7 +5794,7 @@ var TurnServerHealthDetector = class _TurnServerHealthDetector {
|
|
|
5533
5794
|
this._observer.addIssue({
|
|
5534
5795
|
type: TurnServerHealthTypes.turnServerDegraded,
|
|
5535
5796
|
timestamp: now,
|
|
5536
|
-
payload: {
|
|
5797
|
+
payload: { ...health, otherServers }
|
|
5537
5798
|
});
|
|
5538
5799
|
}
|
|
5539
5800
|
for (const serverUrl of [...this._streaks.keys()]) {
|
|
@@ -5641,7 +5902,6 @@ var TurnServerOutageDetector = class _TurnServerOutageDetector {
|
|
|
5641
5902
|
type: TurnServerOutageTypes.turnServerOutage,
|
|
5642
5903
|
timestamp: now,
|
|
5643
5904
|
payload: {
|
|
5644
|
-
type: TurnServerOutageTypes.turnServerOutage,
|
|
5645
5905
|
serverUrl,
|
|
5646
5906
|
peakClients: peak,
|
|
5647
5907
|
currentClients: live,
|
|
@@ -5915,17 +6175,13 @@ var SimulcastReceiverValidator = class _SimulcastReceiverValidator {
|
|
|
5915
6175
|
this._observer.addIssue({
|
|
5916
6176
|
type: LOWEST_COMMON_DENOMINATOR_ISSUE,
|
|
5917
6177
|
timestamp: decidedAt,
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
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",
|
|
5926
|
-
confidence: 0.8
|
|
5927
|
-
}
|
|
5928
|
-
}
|
|
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 }
|
|
5929
6185
|
});
|
|
5930
6186
|
}
|
|
5931
6187
|
this.onDone(this.report);
|
|
@@ -6028,17 +6284,13 @@ var RemoteTrackResolverValidator = class _RemoteTrackResolverValidator {
|
|
|
6028
6284
|
this._observer.addIssue({
|
|
6029
6285
|
type: UNRESOLVED_TRACK_LINKS_ISSUE,
|
|
6030
6286
|
timestamp: decidedAt,
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
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",
|
|
6039
|
-
confidence: 0.9
|
|
6040
|
-
}
|
|
6041
|
-
}
|
|
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 }
|
|
6042
6294
|
});
|
|
6043
6295
|
}
|
|
6044
6296
|
this.onDone(this.report);
|
|
@@ -6149,17 +6401,16 @@ var CodecConsistencyValidator = class _CodecConsistencyValidator {
|
|
|
6149
6401
|
this._observer.addIssue({
|
|
6150
6402
|
type: CODEC_MISMATCH_ISSUE,
|
|
6151
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
|
+
},
|
|
6152
6410
|
payload: {
|
|
6153
|
-
type: CODEC_MISMATCH_ISSUE,
|
|
6154
6411
|
verdict: outcome.verdict,
|
|
6155
6412
|
checks: this._checks,
|
|
6156
|
-
evidence: outcome.evidence
|
|
6157
|
-
conclusion: {
|
|
6158
|
-
faultDomain: "infrastructure",
|
|
6159
|
-
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",
|
|
6160
|
-
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",
|
|
6161
|
-
confidence: 0.85
|
|
6162
|
-
}
|
|
6413
|
+
evidence: outcome.evidence
|
|
6163
6414
|
}
|
|
6164
6415
|
});
|
|
6165
6416
|
}
|
|
@@ -6173,7 +6424,7 @@ function mediaKindOf(mimeType) {
|
|
|
6173
6424
|
}
|
|
6174
6425
|
|
|
6175
6426
|
// src/Observer.ts
|
|
6176
|
-
var
|
|
6427
|
+
var logger9 = createLogger("Observer");
|
|
6177
6428
|
var Observer = class extends EventEmitter6 {
|
|
6178
6429
|
observedTURN = new ObservedTURN();
|
|
6179
6430
|
observedCalls = /* @__PURE__ */ new Map();
|
|
@@ -6229,6 +6480,14 @@ var Observer = class extends EventEmitter6 {
|
|
|
6229
6480
|
* ```
|
|
6230
6481
|
*/
|
|
6231
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;
|
|
6232
6491
|
constructor(config = {}) {
|
|
6233
6492
|
super();
|
|
6234
6493
|
this.setMaxListeners(Infinity);
|
|
@@ -6240,6 +6499,12 @@ var Observer = class extends EventEmitter6 {
|
|
|
6240
6499
|
closeClientIfIdleForMs: 6e4,
|
|
6241
6500
|
...config
|
|
6242
6501
|
};
|
|
6502
|
+
if (this.config.callSummary) {
|
|
6503
|
+
this.callSummaryCollector = new CallSummaryCollector(this, {
|
|
6504
|
+
...defaultCallSummaryConfig,
|
|
6505
|
+
...this.config.callSummary
|
|
6506
|
+
});
|
|
6507
|
+
}
|
|
6243
6508
|
}
|
|
6244
6509
|
get numberOfCalls() {
|
|
6245
6510
|
return this.observedCalls.size;
|
|
@@ -6247,6 +6512,12 @@ var Observer = class extends EventEmitter6 {
|
|
|
6247
6512
|
get appData() {
|
|
6248
6513
|
return this.config.appData;
|
|
6249
6514
|
}
|
|
6515
|
+
/**
|
|
6516
|
+
* Build a cross-call detector onto `observer.detectors`. Chainable.
|
|
6517
|
+
*
|
|
6518
|
+
* To get a handle on what was built — to inspect it, or to remove that exact instance later — read
|
|
6519
|
+
* it back off the registry: `observer.detectors.getAll(name)`, or `observer.detectors.instances`.
|
|
6520
|
+
*/
|
|
6250
6521
|
addObserverDetector(name, config = {}) {
|
|
6251
6522
|
if (this.closed) return this;
|
|
6252
6523
|
let detector;
|
|
@@ -6272,7 +6543,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6272
6543
|
break;
|
|
6273
6544
|
}
|
|
6274
6545
|
default: {
|
|
6275
|
-
|
|
6546
|
+
logger9.warn("Unknown detector name %s; skipping", name);
|
|
6276
6547
|
return this;
|
|
6277
6548
|
}
|
|
6278
6549
|
}
|
|
@@ -6290,10 +6561,41 @@ var Observer = class extends EventEmitter6 {
|
|
|
6290
6561
|
this.callDetectorConfigs.set(name, config);
|
|
6291
6562
|
return this;
|
|
6292
6563
|
}
|
|
6293
|
-
/**
|
|
6294
|
-
|
|
6564
|
+
/**
|
|
6565
|
+
* Remove an observer-scoped detector by name, returning how many were removed.
|
|
6566
|
+
*
|
|
6567
|
+
* **Every** instance registered under the name goes, since a name can legitimately be registered
|
|
6568
|
+
* more than once (`ClientPopulationIssueDetector` is meant to be added once per `groupBy` axis).
|
|
6569
|
+
* When you want one of them specifically, go through the registry, which deals in instances:
|
|
6570
|
+
*
|
|
6571
|
+
* ```ts
|
|
6572
|
+
* const [ byBrowser, byOs ] = observer.detectors.getAll('client-population-issue-detector');
|
|
6573
|
+
*
|
|
6574
|
+
* observer.detectors.remove(byOs); // keeps the browser axis running
|
|
6575
|
+
* ```
|
|
6576
|
+
*
|
|
6577
|
+
* Either route `close()`s the detector, so it unsubscribes from the issue registry and drops any
|
|
6578
|
+
* timers or bus listeners it held.
|
|
6579
|
+
*/
|
|
6580
|
+
removeObserverDetector(name) {
|
|
6581
|
+
return this.detectors.removeByName(name);
|
|
6582
|
+
}
|
|
6583
|
+
/**
|
|
6584
|
+
* Stop building `name` on calls created from now on.
|
|
6585
|
+
*
|
|
6586
|
+
* By default this also removes it from the calls **already open**, so that "remove this detector"
|
|
6587
|
+
* means the same thing whether you say it before or after a call started — the alternative leaves
|
|
6588
|
+
* a fleet where the detector is live on some calls and not others, decided by join time. Pass
|
|
6589
|
+
* `{ includeOpenCalls: false }` to change only what future calls are built with.
|
|
6590
|
+
*
|
|
6591
|
+
* Returns the number of live detector instances removed (`0` when only the config changed).
|
|
6592
|
+
*/
|
|
6593
|
+
removeCallDetector(name, { includeOpenCalls = true } = {}) {
|
|
6295
6594
|
this.callDetectorConfigs.delete(name);
|
|
6296
|
-
return
|
|
6595
|
+
if (!includeOpenCalls) return 0;
|
|
6596
|
+
let removed = 0;
|
|
6597
|
+
for (const call of this.observedCalls.values()) removed += call.removeDetector(name);
|
|
6598
|
+
return removed;
|
|
6297
6599
|
}
|
|
6298
6600
|
/**
|
|
6299
6601
|
* Start a structural check. It runs on each `observer.update()` until it can decide, reports once
|
|
@@ -6326,23 +6628,53 @@ var Observer = class extends EventEmitter6 {
|
|
|
6326
6628
|
break;
|
|
6327
6629
|
}
|
|
6328
6630
|
if (!validator) {
|
|
6329
|
-
|
|
6631
|
+
logger9.warn("Unknown validator name %s; skipping", name);
|
|
6330
6632
|
return this;
|
|
6331
6633
|
}
|
|
6332
6634
|
this.validators.add(validator);
|
|
6333
6635
|
return this;
|
|
6334
6636
|
}
|
|
6637
|
+
/**
|
|
6638
|
+
* Stop a running validation, by name or by instance. Returns how many were cancelled.
|
|
6639
|
+
*
|
|
6640
|
+
* Cancelling is **not** silent discarding. The validator finishes with `inconclusive` and the given
|
|
6641
|
+
* `reason`, emits `validation-ready` like any other completion, and removes itself. That matters
|
|
6642
|
+
* because anything waiting on the verdict — a deploy gate, a dashboard, a promise — would otherwise
|
|
6643
|
+
* wait forever, and because "we stopped asking" is a materially different outcome from "we asked
|
|
6644
|
+
* and learned nothing", which is exactly what `inconclusive` with a reason records.
|
|
6645
|
+
*
|
|
6646
|
+
* ```ts
|
|
6647
|
+
* observer.cancelValidator('simulcast-receivers', 'sfu redeployed');
|
|
6648
|
+
*
|
|
6649
|
+
* // or one specific instance — `observer.validators` holds what is running
|
|
6650
|
+
* for (const validator of observer.validators) observer.cancelValidator(validator, 'shutting down');
|
|
6651
|
+
* ```
|
|
6652
|
+
*
|
|
6653
|
+
* Pass a real reason. The default tells the reader nothing they could not already infer.
|
|
6654
|
+
*/
|
|
6655
|
+
cancelValidator(target, reason = "cancelled") {
|
|
6656
|
+
const running = [...this.validators];
|
|
6657
|
+
const matching = typeof target === "string" ? running.filter((validator) => validator.name === target) : running.filter((validator) => validator === target);
|
|
6658
|
+
for (const validator of matching) {
|
|
6659
|
+
try {
|
|
6660
|
+
validator.cancel(reason);
|
|
6661
|
+
} catch (err) {
|
|
6662
|
+
logger9.warn("Error cancelling validator %s: %o", validator.name, err);
|
|
6663
|
+
}
|
|
6664
|
+
}
|
|
6665
|
+
return matching.length;
|
|
6666
|
+
}
|
|
6335
6667
|
getObservedCall(callId) {
|
|
6336
6668
|
if (this.closed || !this.observedCalls.has(callId)) return;
|
|
6337
6669
|
return this.observedCalls.get(callId);
|
|
6338
6670
|
}
|
|
6339
6671
|
createObservedCall(settings) {
|
|
6340
6672
|
if (this.closed) {
|
|
6341
|
-
|
|
6673
|
+
logger9.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
|
|
6342
6674
|
return void 0;
|
|
6343
6675
|
}
|
|
6344
6676
|
if (this.observedCalls.has(settings.callId)) {
|
|
6345
|
-
|
|
6677
|
+
logger9.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
|
|
6346
6678
|
return this.observedCalls.get(settings.callId);
|
|
6347
6679
|
}
|
|
6348
6680
|
const callSettings = {
|
|
@@ -6357,6 +6689,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6357
6689
|
callActiveIssuesRegistry
|
|
6358
6690
|
);
|
|
6359
6691
|
observedCall.remoteTrackResolver = this.config.createRemoteTrackResolver?.(observedCall);
|
|
6692
|
+
observedCall.enableSummary();
|
|
6360
6693
|
for (const [name, detectorConfig] of this.callDetectorConfigs) {
|
|
6361
6694
|
observedCall.addDetector(name, detectorConfig);
|
|
6362
6695
|
}
|
|
@@ -6379,11 +6712,11 @@ var Observer = class extends EventEmitter6 {
|
|
|
6379
6712
|
}
|
|
6380
6713
|
createObservedMediasoupRouter(settings) {
|
|
6381
6714
|
if (this.closed) {
|
|
6382
|
-
|
|
6715
|
+
logger9.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
|
|
6383
6716
|
return void 0;
|
|
6384
6717
|
}
|
|
6385
6718
|
if (this.observedMediasoupRouters.has(settings.router.id)) {
|
|
6386
|
-
|
|
6719
|
+
logger9.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
|
|
6387
6720
|
return this.observedMediasoupRouters.get(settings.router.id);
|
|
6388
6721
|
}
|
|
6389
6722
|
const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
|
|
@@ -6414,13 +6747,14 @@ var Observer = class extends EventEmitter6 {
|
|
|
6414
6747
|
}
|
|
6415
6748
|
close() {
|
|
6416
6749
|
if (this.closed) {
|
|
6417
|
-
return
|
|
6750
|
+
return logger9.debug("Attempted to close twice");
|
|
6418
6751
|
}
|
|
6419
6752
|
this.closed = true;
|
|
6420
6753
|
for (const call of [...this.observedCalls.values()]) call.close();
|
|
6421
6754
|
this.detectors.clear();
|
|
6422
|
-
for (const validator of [...this.validators]) validator.cancel();
|
|
6755
|
+
for (const validator of [...this.validators]) validator.cancel("observer closed");
|
|
6423
6756
|
this.validators.clear();
|
|
6757
|
+
this.callSummaryCollector?.close();
|
|
6424
6758
|
this.activeIssuesRegistry.clear();
|
|
6425
6759
|
this._notify("observer-closed", { ...this.eventScope });
|
|
6426
6760
|
}
|
|
@@ -6431,7 +6765,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6431
6765
|
try {
|
|
6432
6766
|
this.acceptMiddlewares.process({ sample, context });
|
|
6433
6767
|
} catch (err) {
|
|
6434
|
-
|
|
6768
|
+
logger9.warn("An accept middleware threw; dropping the sample. %o", err);
|
|
6435
6769
|
}
|
|
6436
6770
|
if (!sample.callId) {
|
|
6437
6771
|
this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
|
|
@@ -6483,7 +6817,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6483
6817
|
try {
|
|
6484
6818
|
validator.update();
|
|
6485
6819
|
} catch (err) {
|
|
6486
|
-
|
|
6820
|
+
logger9.warn("Error running validator %s: %o", validator.name, err);
|
|
6487
6821
|
}
|
|
6488
6822
|
}
|
|
6489
6823
|
this._notify("observer-updated", { ...this.eventScope });
|
|
@@ -6496,7 +6830,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6496
6830
|
*/
|
|
6497
6831
|
addIssue(issue) {
|
|
6498
6832
|
if (this.closed) return;
|
|
6499
|
-
this._notify("observer-issue", { ...this.eventScope, issue });
|
|
6833
|
+
this._notify("observer-issue", { ...this.eventScope, issue: { ...issue, scope: "observer" } });
|
|
6500
6834
|
}
|
|
6501
6835
|
/** Emit an Observer-bus event. */
|
|
6502
6836
|
_notify(type, ...args) {
|
|
@@ -6614,18 +6948,11 @@ var CallHealthAggregator = class {
|
|
|
6614
6948
|
}
|
|
6615
6949
|
};
|
|
6616
6950
|
|
|
6617
|
-
// src/common/
|
|
6618
|
-
function issuePayloadOf(issue) {
|
|
6619
|
-
const payload = issue.payload;
|
|
6620
|
-
if (payload === void 0) return void 0;
|
|
6621
|
-
return typeof payload === "string" ? parseJsonObject(payload) : payload;
|
|
6622
|
-
}
|
|
6951
|
+
// src/common/Issue.ts
|
|
6623
6952
|
function issuePayloadAsString(issue) {
|
|
6624
|
-
|
|
6625
|
-
if (payload === void 0) return void 0;
|
|
6626
|
-
if (typeof payload === "string") return payload;
|
|
6953
|
+
if (issue.payload === void 0) return void 0;
|
|
6627
6954
|
try {
|
|
6628
|
-
return JSON.stringify(payload);
|
|
6955
|
+
return JSON.stringify(issue.payload);
|
|
6629
6956
|
} catch {
|
|
6630
6957
|
return void 0;
|
|
6631
6958
|
}
|
|
@@ -6919,6 +7246,7 @@ export {
|
|
|
6919
7246
|
CallConcurrentIssueDetector,
|
|
6920
7247
|
CallConcurrentIssueTypes,
|
|
6921
7248
|
CallHealthAggregator,
|
|
7249
|
+
CallSummaryCollector,
|
|
6922
7250
|
ClientEventTypes,
|
|
6923
7251
|
ClientMetaTypes,
|
|
6924
7252
|
ClientPopulationIssueDetector,
|
|
@@ -6977,16 +7305,17 @@ export {
|
|
|
6977
7305
|
concludeObserverIssue,
|
|
6978
7306
|
correlation,
|
|
6979
7307
|
counterDelta,
|
|
7308
|
+
createCallSummary,
|
|
6980
7309
|
createDefaultMediasoupRemoteTrackResolverFactory,
|
|
6981
7310
|
createInMemorySink,
|
|
6982
7311
|
createJsonlFileSink,
|
|
6983
7312
|
createJsonlFileSinkFactory,
|
|
6984
7313
|
createLogger,
|
|
6985
7314
|
createP2pRemoteTrackResolverFactory,
|
|
7315
|
+
defaultCallSummaryConfig,
|
|
6986
7316
|
defaultClientHealthThresholds,
|
|
6987
7317
|
isClientIssueResolutionEntry,
|
|
6988
7318
|
issuePayloadAsString,
|
|
6989
|
-
issuePayloadOf,
|
|
6990
7319
|
mannKendall,
|
|
6991
7320
|
mannKendallVerdict,
|
|
6992
7321
|
median,
|