@observertc/observer-js 1.0.0-beta.18 → 1.0.0-beta.21
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 +93 -3
- package/dist/index.d.mts +689 -71
- package/dist/index.d.ts +689 -71
- package/dist/index.js +129 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +127 -18
- 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(logger11) {
|
|
41
|
+
mainLogger = logger11;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// src/ObservedCall.ts
|
|
@@ -5586,7 +5586,54 @@ var ObserverConcurrentIssueDetector = class _ObserverConcurrentIssueDetector {
|
|
|
5586
5586
|
}
|
|
5587
5587
|
};
|
|
5588
5588
|
|
|
5589
|
+
// src/utils/geohash.ts
|
|
5590
|
+
var BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
|
|
5591
|
+
var GEOHASH_CELL_SIZES = [
|
|
5592
|
+
"",
|
|
5593
|
+
"~5000 km",
|
|
5594
|
+
"~1250 km",
|
|
5595
|
+
"~156 km",
|
|
5596
|
+
"~39 km",
|
|
5597
|
+
"~5 km",
|
|
5598
|
+
"~1.2 km",
|
|
5599
|
+
"~150 m"
|
|
5600
|
+
];
|
|
5601
|
+
function geohash(location, precision = 3) {
|
|
5602
|
+
const { latitude, longitude } = location;
|
|
5603
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return void 0;
|
|
5604
|
+
if (latitude < -90 || 90 < latitude) return void 0;
|
|
5605
|
+
if (longitude < -180 || 180 < longitude) return void 0;
|
|
5606
|
+
if (!Number.isFinite(precision) || precision < 1) return void 0;
|
|
5607
|
+
const chars = Math.min(12, Math.floor(precision));
|
|
5608
|
+
const latRange = { min: -90, max: 90 };
|
|
5609
|
+
const lonRange = { min: -180, max: 180 };
|
|
5610
|
+
let hash = "";
|
|
5611
|
+
let bits = 0;
|
|
5612
|
+
let value = 0;
|
|
5613
|
+
let isLongitude = true;
|
|
5614
|
+
while (hash.length < chars) {
|
|
5615
|
+
const range = isLongitude ? lonRange : latRange;
|
|
5616
|
+
const mid = (range.min + range.max) / 2;
|
|
5617
|
+
const coordinate = isLongitude ? longitude : latitude;
|
|
5618
|
+
if (mid <= coordinate) {
|
|
5619
|
+
value = (value << 1) + 1;
|
|
5620
|
+
range.min = mid;
|
|
5621
|
+
} else {
|
|
5622
|
+
value <<= 1;
|
|
5623
|
+
range.max = mid;
|
|
5624
|
+
}
|
|
5625
|
+
isLongitude = !isLongitude;
|
|
5626
|
+
if (++bits === 5) {
|
|
5627
|
+
hash += BASE32[value];
|
|
5628
|
+
bits = 0;
|
|
5629
|
+
value = 0;
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
return hash;
|
|
5633
|
+
}
|
|
5634
|
+
|
|
5589
5635
|
// src/detectors/ClientPopulationIssueDetector.ts
|
|
5636
|
+
var logger9 = createLogger("ClientPopulationIssueDetector");
|
|
5590
5637
|
var ClientPopulationIssueTypes = {
|
|
5591
5638
|
/** One issue type is concentrated on one client population while the rest of the fleet is fine. */
|
|
5592
5639
|
clientPopulationIssue: "CLIENT_POPULATION_ISSUE"
|
|
@@ -5597,6 +5644,7 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5597
5644
|
this._config = {
|
|
5598
5645
|
issueTypes: [],
|
|
5599
5646
|
groupBy: "browser",
|
|
5647
|
+
locationPrecision: 3,
|
|
5600
5648
|
includeVersion: true,
|
|
5601
5649
|
minPopulationSize: 20,
|
|
5602
5650
|
minAffectedClients: 5,
|
|
@@ -5606,6 +5654,12 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5606
5654
|
cooldownMs: 3e5,
|
|
5607
5655
|
...config
|
|
5608
5656
|
};
|
|
5657
|
+
if (this._config.groupBy === "location" && !this._config.resolveClientLocation) {
|
|
5658
|
+
logger9.warn(
|
|
5659
|
+
"%s is configured with groupBy: 'location' but no resolveClientLocation, so no client can be placed and nothing will ever be detected",
|
|
5660
|
+
this.name
|
|
5661
|
+
);
|
|
5662
|
+
}
|
|
5609
5663
|
for (const type of this._config.issueTypes) {
|
|
5610
5664
|
this._observer.activeIssuesRegistry.addIssueTracker(type, this);
|
|
5611
5665
|
}
|
|
@@ -5686,13 +5740,17 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5686
5740
|
const key = `${population}:${issueType}`;
|
|
5687
5741
|
if (now - (this._lastRaisedAt.get(key) ?? 0) < this._config.cooldownMs) continue;
|
|
5688
5742
|
this._lastRaisedAt.set(key, now);
|
|
5743
|
+
const isLocation = this._config.groupBy === "location";
|
|
5689
5744
|
this._observer.addIssue({
|
|
5690
5745
|
type: ClientPopulationIssueTypes.clientPopulationIssue,
|
|
5691
5746
|
timestamp: now,
|
|
5692
5747
|
conclusion: {
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5748
|
+
// Geography is not something endpoints own. A cluster of network symptoms in one
|
|
5749
|
+
// place is a path/transit story, so it is attributed to the infrastructure rather
|
|
5750
|
+
// than to a client build.
|
|
5751
|
+
faultDomain: isLocation ? "infrastructure" : "client-population",
|
|
5752
|
+
summary: `'${issueType}' is ${this._riskText(rollup.relativeRisk)} more likely ${isLocation ? `in cell ${population}` : `on ${population}`} than across the rest of the fleet (${rollup.affectedClients}/${rollup.clients} vs ${rollup.controlAffectedClients}/${rollup.controlClients})`,
|
|
5753
|
+
recommendation: isLocation ? "look at the path into that region \u2014 ISP, transit or peering \u2014 but first rule out the servers those clients share, since a region often maps onto one SFU or TURN deployment" : "this is not an SFU symptom \u2014 look at what those clients share: a recent release, a browser version, or shared/virtualised hardware",
|
|
5696
5754
|
confidence: this._confidenceOf(rollup)
|
|
5697
5755
|
},
|
|
5698
5756
|
payload: { ...rollup }
|
|
@@ -5707,6 +5765,10 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5707
5765
|
}
|
|
5708
5766
|
/** `undefined` when the client never reported this attribute — see the class description. */
|
|
5709
5767
|
_populationOf(client) {
|
|
5768
|
+
if (this._config.groupBy === "location") {
|
|
5769
|
+
const location = this._config.resolveClientLocation?.(client);
|
|
5770
|
+
return location ? geohash(location, this._config.locationPrecision) : void 0;
|
|
5771
|
+
}
|
|
5710
5772
|
const attribute = client[this._config.groupBy];
|
|
5711
5773
|
if (!attribute?.name) return void 0;
|
|
5712
5774
|
return this._config.includeVersion && attribute.version ? `${attribute.name} ${attribute.version}` : attribute.name;
|
|
@@ -6427,7 +6489,7 @@ function mediaKindOf(mimeType) {
|
|
|
6427
6489
|
}
|
|
6428
6490
|
|
|
6429
6491
|
// src/Observer.ts
|
|
6430
|
-
var
|
|
6492
|
+
var logger10 = createLogger("Observer");
|
|
6431
6493
|
var Observer = class extends EventEmitter6 {
|
|
6432
6494
|
observedTURN = new ObservedTURN();
|
|
6433
6495
|
observedCalls = /* @__PURE__ */ new Map();
|
|
@@ -6546,7 +6608,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6546
6608
|
break;
|
|
6547
6609
|
}
|
|
6548
6610
|
default: {
|
|
6549
|
-
|
|
6611
|
+
logger10.warn("Unknown detector name %s; skipping", name);
|
|
6550
6612
|
return this;
|
|
6551
6613
|
}
|
|
6552
6614
|
}
|
|
@@ -6631,7 +6693,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6631
6693
|
break;
|
|
6632
6694
|
}
|
|
6633
6695
|
if (!validator) {
|
|
6634
|
-
|
|
6696
|
+
logger10.warn("Unknown validator name %s; skipping", name);
|
|
6635
6697
|
return this;
|
|
6636
6698
|
}
|
|
6637
6699
|
this.validators.add(validator);
|
|
@@ -6662,7 +6724,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6662
6724
|
try {
|
|
6663
6725
|
validator.cancel(reason);
|
|
6664
6726
|
} catch (err) {
|
|
6665
|
-
|
|
6727
|
+
logger10.warn("Error cancelling validator %s: %o", validator.name, err);
|
|
6666
6728
|
}
|
|
6667
6729
|
}
|
|
6668
6730
|
return matching.length;
|
|
@@ -6678,11 +6740,11 @@ var Observer = class extends EventEmitter6 {
|
|
|
6678
6740
|
*/
|
|
6679
6741
|
createObservedCall(settings, acceptCtx) {
|
|
6680
6742
|
if (this.closed) {
|
|
6681
|
-
|
|
6743
|
+
logger10.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
|
|
6682
6744
|
return void 0;
|
|
6683
6745
|
}
|
|
6684
6746
|
if (this.observedCalls.has(settings.callId)) {
|
|
6685
|
-
|
|
6747
|
+
logger10.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
|
|
6686
6748
|
return this.observedCalls.get(settings.callId);
|
|
6687
6749
|
}
|
|
6688
6750
|
const callSettings = {
|
|
@@ -6720,11 +6782,11 @@ var Observer = class extends EventEmitter6 {
|
|
|
6720
6782
|
}
|
|
6721
6783
|
createObservedMediasoupRouter(settings) {
|
|
6722
6784
|
if (this.closed) {
|
|
6723
|
-
|
|
6785
|
+
logger10.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
|
|
6724
6786
|
return void 0;
|
|
6725
6787
|
}
|
|
6726
6788
|
if (this.observedMediasoupRouters.has(settings.router.id)) {
|
|
6727
|
-
|
|
6789
|
+
logger10.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
|
|
6728
6790
|
return this.observedMediasoupRouters.get(settings.router.id);
|
|
6729
6791
|
}
|
|
6730
6792
|
const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
|
|
@@ -6755,7 +6817,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6755
6817
|
}
|
|
6756
6818
|
close() {
|
|
6757
6819
|
if (this.closed) {
|
|
6758
|
-
return
|
|
6820
|
+
return logger10.debug("Attempted to close twice");
|
|
6759
6821
|
}
|
|
6760
6822
|
this.closed = true;
|
|
6761
6823
|
for (const call of [...this.observedCalls.values()]) call.close();
|
|
@@ -6773,7 +6835,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6773
6835
|
try {
|
|
6774
6836
|
this.acceptMiddlewares.process({ sample, context });
|
|
6775
6837
|
} catch (err) {
|
|
6776
|
-
|
|
6838
|
+
logger10.warn("An accept middleware threw; dropping the sample. %o", err);
|
|
6777
6839
|
}
|
|
6778
6840
|
if (!sample.callId) {
|
|
6779
6841
|
this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
|
|
@@ -6813,7 +6875,7 @@ var Observer = class extends EventEmitter6 {
|
|
|
6813
6875
|
try {
|
|
6814
6876
|
validator.update();
|
|
6815
6877
|
} catch (err) {
|
|
6816
|
-
|
|
6878
|
+
logger10.warn("Error running validator %s: %o", validator.name, err);
|
|
6817
6879
|
}
|
|
6818
6880
|
}
|
|
6819
6881
|
this._notify("observer-updated", { ...this.eventScope });
|
|
@@ -7127,21 +7189,56 @@ var RemoteTrackResolver = class {
|
|
|
7127
7189
|
const onOutboundRemoved = (p) => {
|
|
7128
7190
|
if (p.observedCall === this.observedCall) this._removeOutboundTrack(p.observedOutboundTrack);
|
|
7129
7191
|
};
|
|
7192
|
+
const onInboundUpdated = (p) => {
|
|
7193
|
+
if (p.observedCall !== this.observedCall) return;
|
|
7194
|
+
if (this._pendingInboundTracks.has(p.observedInboundTrack)) this._addInboundTrack(p.observedInboundTrack);
|
|
7195
|
+
};
|
|
7196
|
+
const onOutboundUpdated = (p) => {
|
|
7197
|
+
if (p.observedCall !== this.observedCall) return;
|
|
7198
|
+
if (this._pendingOutboundTracks.has(p.observedOutboundTrack)) this._addOutboundTrack(p.observedOutboundTrack);
|
|
7199
|
+
};
|
|
7130
7200
|
observedCall.once("close", () => {
|
|
7131
7201
|
observer.off("inbound-track-added", onInboundAdded);
|
|
7132
7202
|
observer.off("inbound-track-removed", onInboundRemoved);
|
|
7133
7203
|
observer.off("outbound-track-added", onOutboundAdded);
|
|
7134
7204
|
observer.off("outbound-track-removed", onOutboundRemoved);
|
|
7205
|
+
observer.off("inbound-track-updated", onInboundUpdated);
|
|
7206
|
+
observer.off("outbound-track-updated", onOutboundUpdated);
|
|
7207
|
+
this._pendingInboundTracks.clear();
|
|
7208
|
+
this._pendingOutboundTracks.clear();
|
|
7135
7209
|
});
|
|
7136
7210
|
observer.on("inbound-track-added", onInboundAdded);
|
|
7137
7211
|
observer.on("inbound-track-removed", onInboundRemoved);
|
|
7138
7212
|
observer.on("outbound-track-added", onOutboundAdded);
|
|
7139
7213
|
observer.on("outbound-track-removed", onOutboundRemoved);
|
|
7214
|
+
observer.on("inbound-track-updated", onInboundUpdated);
|
|
7215
|
+
observer.on("outbound-track-updated", onOutboundUpdated);
|
|
7140
7216
|
}
|
|
7141
7217
|
observedCall;
|
|
7142
7218
|
resolvers;
|
|
7143
7219
|
_publisherIdToOutboundTrack = /* @__PURE__ */ new Map();
|
|
7144
7220
|
_subscriberIdToInboundTrack = /* @__PURE__ */ new Map();
|
|
7221
|
+
/**
|
|
7222
|
+
* Tracks whose publisher id the strategy could not resolve **yet**.
|
|
7223
|
+
*
|
|
7224
|
+
* A track announces itself once, but its `attachments` are replaced on every sample, so a key that
|
|
7225
|
+
* is missing from the first sample can appear on the second — and a strategy backed by an
|
|
7226
|
+
* application's own mapping (a server-side `ssrc -> producerId` table, say) is inherently racy
|
|
7227
|
+
* against sample arrival. Resolving only at `*-track-added` meant losing those tracks for their
|
|
7228
|
+
* entire life, silently: an unresolvable outbound track never even reaches
|
|
7229
|
+
* `unconsumedOutboundTracks`, so it is invisible to `UnconsumedTrackDetector` too.
|
|
7230
|
+
*
|
|
7231
|
+
* So they wait here and are retried on their own `*-track-updated`, i.e. exactly when new stats
|
|
7232
|
+
* arrived for them. A track leaves on its first successful resolution or on removal, which makes
|
|
7233
|
+
* a linked track cost one `Set.has` per update and bounds these sets by the unresolved tracks
|
|
7234
|
+
* alive right now.
|
|
7235
|
+
*/
|
|
7236
|
+
_pendingInboundTracks = /* @__PURE__ */ new Set();
|
|
7237
|
+
_pendingOutboundTracks = /* @__PURE__ */ new Set();
|
|
7238
|
+
/** Tracks still waiting for a resolvable publisher id. Diagnostics; normally both are empty. */
|
|
7239
|
+
get pendingTrackCounts() {
|
|
7240
|
+
return { inbound: this._pendingInboundTracks.size, outbound: this._pendingOutboundTracks.size };
|
|
7241
|
+
}
|
|
7145
7242
|
/** The published (outbound) track for a publisher id, if any. */
|
|
7146
7243
|
getOutboundTrackByPublisherId(publisherId) {
|
|
7147
7244
|
return this._publisherIdToOutboundTrack.get(publisherId);
|
|
@@ -7159,7 +7256,11 @@ var RemoteTrackResolver = class {
|
|
|
7159
7256
|
}
|
|
7160
7257
|
_addInboundTrack(inboundTrack) {
|
|
7161
7258
|
const publisherId = this.resolvers.resolveInboundTrackPublisherId(inboundTrack);
|
|
7162
|
-
if (!publisherId)
|
|
7259
|
+
if (!publisherId) {
|
|
7260
|
+
this._pendingInboundTracks.add(inboundTrack);
|
|
7261
|
+
return;
|
|
7262
|
+
}
|
|
7263
|
+
this._pendingInboundTracks.delete(inboundTrack);
|
|
7163
7264
|
const subscriberId = this.resolvers.resolveInboundTrackSubscriberId?.(inboundTrack);
|
|
7164
7265
|
if (subscriberId) {
|
|
7165
7266
|
this._subscriberIdToInboundTrack.set(subscriberId, inboundTrack);
|
|
@@ -7171,6 +7272,7 @@ var RemoteTrackResolver = class {
|
|
|
7171
7272
|
this.observedCall.unconsumedOutboundTracks.delete(outboundTrack);
|
|
7172
7273
|
}
|
|
7173
7274
|
_removeInboundTrack(inboundTrack) {
|
|
7275
|
+
this._pendingInboundTracks.delete(inboundTrack);
|
|
7174
7276
|
const outboundTrack = inboundTrack.remoteOutboundTrack;
|
|
7175
7277
|
outboundTrack?.remoteInboundTracks.delete(inboundTrack);
|
|
7176
7278
|
inboundTrack.remoteOutboundTrack = void 0;
|
|
@@ -7184,7 +7286,11 @@ var RemoteTrackResolver = class {
|
|
|
7184
7286
|
}
|
|
7185
7287
|
_addOutboundTrack(outboundTrack) {
|
|
7186
7288
|
const publisherId = this.resolvers.resolveOutboundTrackPublisherId(outboundTrack);
|
|
7187
|
-
if (!publisherId)
|
|
7289
|
+
if (!publisherId) {
|
|
7290
|
+
this._pendingOutboundTracks.add(outboundTrack);
|
|
7291
|
+
return;
|
|
7292
|
+
}
|
|
7293
|
+
this._pendingOutboundTracks.delete(outboundTrack);
|
|
7188
7294
|
this._publisherIdToOutboundTrack.set(publisherId, outboundTrack);
|
|
7189
7295
|
this.observedCall.unconsumedOutboundTracks.add(outboundTrack);
|
|
7190
7296
|
for (const observedClient of this.observedCall.observedClients.values()) {
|
|
@@ -7199,6 +7305,7 @@ var RemoteTrackResolver = class {
|
|
|
7199
7305
|
}
|
|
7200
7306
|
}
|
|
7201
7307
|
_removeOutboundTrack(outboundTrack) {
|
|
7308
|
+
this._pendingOutboundTracks.delete(outboundTrack);
|
|
7202
7309
|
const publisherId = this.resolvers.resolveOutboundTrackPublisherId(outboundTrack);
|
|
7203
7310
|
if (publisherId && this._publisherIdToOutboundTrack.get(publisherId) === outboundTrack) {
|
|
7204
7311
|
this._publisherIdToOutboundTrack.delete(publisherId);
|
|
@@ -7250,6 +7357,7 @@ export {
|
|
|
7250
7357
|
ClientSampleSink,
|
|
7251
7358
|
CodecConsistencyValidator,
|
|
7252
7359
|
Detectors,
|
|
7360
|
+
GEOHASH_CELL_SIZES,
|
|
7253
7361
|
InMemorySink,
|
|
7254
7362
|
IssueFanOutDetector,
|
|
7255
7363
|
IssueFanOutTypes,
|
|
@@ -7310,6 +7418,7 @@ export {
|
|
|
7310
7418
|
createP2pRemoteTrackResolverFactory,
|
|
7311
7419
|
defaultCallSummaryConfig,
|
|
7312
7420
|
defaultClientHealthThresholds,
|
|
7421
|
+
geohash,
|
|
7313
7422
|
isClientIssueResolutionEntry,
|
|
7314
7423
|
issuePayloadAsString,
|
|
7315
7424
|
mannKendall,
|