@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.js
CHANGED
|
@@ -43,6 +43,7 @@ __export(src_exports, {
|
|
|
43
43
|
ClientSampleSink: () => ClientSampleSink,
|
|
44
44
|
CodecConsistencyValidator: () => CodecConsistencyValidator,
|
|
45
45
|
Detectors: () => Detectors,
|
|
46
|
+
GEOHASH_CELL_SIZES: () => GEOHASH_CELL_SIZES,
|
|
46
47
|
InMemorySink: () => InMemorySink,
|
|
47
48
|
IssueFanOutDetector: () => IssueFanOutDetector,
|
|
48
49
|
IssueFanOutTypes: () => IssueFanOutTypes,
|
|
@@ -103,6 +104,7 @@ __export(src_exports, {
|
|
|
103
104
|
createP2pRemoteTrackResolverFactory: () => createP2pRemoteTrackResolverFactory,
|
|
104
105
|
defaultCallSummaryConfig: () => defaultCallSummaryConfig,
|
|
105
106
|
defaultClientHealthThresholds: () => defaultClientHealthThresholds,
|
|
107
|
+
geohash: () => geohash,
|
|
106
108
|
isClientIssueResolutionEntry: () => isClientIssueResolutionEntry,
|
|
107
109
|
issuePayloadAsString: () => issuePayloadAsString,
|
|
108
110
|
mannKendall: () => mannKendall,
|
|
@@ -157,8 +159,8 @@ function createLogger(moduleName) {
|
|
|
157
159
|
}
|
|
158
160
|
}();
|
|
159
161
|
}
|
|
160
|
-
function setObserverLogger(
|
|
161
|
-
mainLogger =
|
|
162
|
+
function setObserverLogger(logger11) {
|
|
163
|
+
mainLogger = logger11;
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
// src/ObservedCall.ts
|
|
@@ -5706,7 +5708,54 @@ var ObserverConcurrentIssueDetector = class _ObserverConcurrentIssueDetector {
|
|
|
5706
5708
|
}
|
|
5707
5709
|
};
|
|
5708
5710
|
|
|
5711
|
+
// src/utils/geohash.ts
|
|
5712
|
+
var BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
|
|
5713
|
+
var GEOHASH_CELL_SIZES = [
|
|
5714
|
+
"",
|
|
5715
|
+
"~5000 km",
|
|
5716
|
+
"~1250 km",
|
|
5717
|
+
"~156 km",
|
|
5718
|
+
"~39 km",
|
|
5719
|
+
"~5 km",
|
|
5720
|
+
"~1.2 km",
|
|
5721
|
+
"~150 m"
|
|
5722
|
+
];
|
|
5723
|
+
function geohash(location, precision = 3) {
|
|
5724
|
+
const { latitude, longitude } = location;
|
|
5725
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return void 0;
|
|
5726
|
+
if (latitude < -90 || 90 < latitude) return void 0;
|
|
5727
|
+
if (longitude < -180 || 180 < longitude) return void 0;
|
|
5728
|
+
if (!Number.isFinite(precision) || precision < 1) return void 0;
|
|
5729
|
+
const chars = Math.min(12, Math.floor(precision));
|
|
5730
|
+
const latRange = { min: -90, max: 90 };
|
|
5731
|
+
const lonRange = { min: -180, max: 180 };
|
|
5732
|
+
let hash = "";
|
|
5733
|
+
let bits = 0;
|
|
5734
|
+
let value = 0;
|
|
5735
|
+
let isLongitude = true;
|
|
5736
|
+
while (hash.length < chars) {
|
|
5737
|
+
const range = isLongitude ? lonRange : latRange;
|
|
5738
|
+
const mid = (range.min + range.max) / 2;
|
|
5739
|
+
const coordinate = isLongitude ? longitude : latitude;
|
|
5740
|
+
if (mid <= coordinate) {
|
|
5741
|
+
value = (value << 1) + 1;
|
|
5742
|
+
range.min = mid;
|
|
5743
|
+
} else {
|
|
5744
|
+
value <<= 1;
|
|
5745
|
+
range.max = mid;
|
|
5746
|
+
}
|
|
5747
|
+
isLongitude = !isLongitude;
|
|
5748
|
+
if (++bits === 5) {
|
|
5749
|
+
hash += BASE32[value];
|
|
5750
|
+
bits = 0;
|
|
5751
|
+
value = 0;
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
return hash;
|
|
5755
|
+
}
|
|
5756
|
+
|
|
5709
5757
|
// src/detectors/ClientPopulationIssueDetector.ts
|
|
5758
|
+
var logger9 = createLogger("ClientPopulationIssueDetector");
|
|
5710
5759
|
var ClientPopulationIssueTypes = {
|
|
5711
5760
|
/** One issue type is concentrated on one client population while the rest of the fleet is fine. */
|
|
5712
5761
|
clientPopulationIssue: "CLIENT_POPULATION_ISSUE"
|
|
@@ -5717,6 +5766,7 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5717
5766
|
this._config = {
|
|
5718
5767
|
issueTypes: [],
|
|
5719
5768
|
groupBy: "browser",
|
|
5769
|
+
locationPrecision: 3,
|
|
5720
5770
|
includeVersion: true,
|
|
5721
5771
|
minPopulationSize: 20,
|
|
5722
5772
|
minAffectedClients: 5,
|
|
@@ -5726,6 +5776,12 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5726
5776
|
cooldownMs: 3e5,
|
|
5727
5777
|
...config
|
|
5728
5778
|
};
|
|
5779
|
+
if (this._config.groupBy === "location" && !this._config.resolveClientLocation) {
|
|
5780
|
+
logger9.warn(
|
|
5781
|
+
"%s is configured with groupBy: 'location' but no resolveClientLocation, so no client can be placed and nothing will ever be detected",
|
|
5782
|
+
this.name
|
|
5783
|
+
);
|
|
5784
|
+
}
|
|
5729
5785
|
for (const type of this._config.issueTypes) {
|
|
5730
5786
|
this._observer.activeIssuesRegistry.addIssueTracker(type, this);
|
|
5731
5787
|
}
|
|
@@ -5806,13 +5862,17 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5806
5862
|
const key = `${population}:${issueType}`;
|
|
5807
5863
|
if (now - (this._lastRaisedAt.get(key) ?? 0) < this._config.cooldownMs) continue;
|
|
5808
5864
|
this._lastRaisedAt.set(key, now);
|
|
5865
|
+
const isLocation = this._config.groupBy === "location";
|
|
5809
5866
|
this._observer.addIssue({
|
|
5810
5867
|
type: ClientPopulationIssueTypes.clientPopulationIssue,
|
|
5811
5868
|
timestamp: now,
|
|
5812
5869
|
conclusion: {
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5870
|
+
// Geography is not something endpoints own. A cluster of network symptoms in one
|
|
5871
|
+
// place is a path/transit story, so it is attributed to the infrastructure rather
|
|
5872
|
+
// than to a client build.
|
|
5873
|
+
faultDomain: isLocation ? "infrastructure" : "client-population",
|
|
5874
|
+
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})`,
|
|
5875
|
+
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",
|
|
5816
5876
|
confidence: this._confidenceOf(rollup)
|
|
5817
5877
|
},
|
|
5818
5878
|
payload: { ...rollup }
|
|
@@ -5827,6 +5887,10 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
|
|
|
5827
5887
|
}
|
|
5828
5888
|
/** `undefined` when the client never reported this attribute — see the class description. */
|
|
5829
5889
|
_populationOf(client) {
|
|
5890
|
+
if (this._config.groupBy === "location") {
|
|
5891
|
+
const location = this._config.resolveClientLocation?.(client);
|
|
5892
|
+
return location ? geohash(location, this._config.locationPrecision) : void 0;
|
|
5893
|
+
}
|
|
5830
5894
|
const attribute = client[this._config.groupBy];
|
|
5831
5895
|
if (!attribute?.name) return void 0;
|
|
5832
5896
|
return this._config.includeVersion && attribute.version ? `${attribute.name} ${attribute.version}` : attribute.name;
|
|
@@ -6547,7 +6611,7 @@ function mediaKindOf(mimeType) {
|
|
|
6547
6611
|
}
|
|
6548
6612
|
|
|
6549
6613
|
// src/Observer.ts
|
|
6550
|
-
var
|
|
6614
|
+
var logger10 = createLogger("Observer");
|
|
6551
6615
|
var Observer = class extends import_events6.EventEmitter {
|
|
6552
6616
|
observedTURN = new ObservedTURN();
|
|
6553
6617
|
observedCalls = /* @__PURE__ */ new Map();
|
|
@@ -6666,7 +6730,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6666
6730
|
break;
|
|
6667
6731
|
}
|
|
6668
6732
|
default: {
|
|
6669
|
-
|
|
6733
|
+
logger10.warn("Unknown detector name %s; skipping", name);
|
|
6670
6734
|
return this;
|
|
6671
6735
|
}
|
|
6672
6736
|
}
|
|
@@ -6751,7 +6815,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6751
6815
|
break;
|
|
6752
6816
|
}
|
|
6753
6817
|
if (!validator) {
|
|
6754
|
-
|
|
6818
|
+
logger10.warn("Unknown validator name %s; skipping", name);
|
|
6755
6819
|
return this;
|
|
6756
6820
|
}
|
|
6757
6821
|
this.validators.add(validator);
|
|
@@ -6782,7 +6846,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6782
6846
|
try {
|
|
6783
6847
|
validator.cancel(reason);
|
|
6784
6848
|
} catch (err) {
|
|
6785
|
-
|
|
6849
|
+
logger10.warn("Error cancelling validator %s: %o", validator.name, err);
|
|
6786
6850
|
}
|
|
6787
6851
|
}
|
|
6788
6852
|
return matching.length;
|
|
@@ -6798,11 +6862,11 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6798
6862
|
*/
|
|
6799
6863
|
createObservedCall(settings, acceptCtx) {
|
|
6800
6864
|
if (this.closed) {
|
|
6801
|
-
|
|
6865
|
+
logger10.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
|
|
6802
6866
|
return void 0;
|
|
6803
6867
|
}
|
|
6804
6868
|
if (this.observedCalls.has(settings.callId)) {
|
|
6805
|
-
|
|
6869
|
+
logger10.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
|
|
6806
6870
|
return this.observedCalls.get(settings.callId);
|
|
6807
6871
|
}
|
|
6808
6872
|
const callSettings = {
|
|
@@ -6840,11 +6904,11 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6840
6904
|
}
|
|
6841
6905
|
createObservedMediasoupRouter(settings) {
|
|
6842
6906
|
if (this.closed) {
|
|
6843
|
-
|
|
6907
|
+
logger10.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
|
|
6844
6908
|
return void 0;
|
|
6845
6909
|
}
|
|
6846
6910
|
if (this.observedMediasoupRouters.has(settings.router.id)) {
|
|
6847
|
-
|
|
6911
|
+
logger10.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
|
|
6848
6912
|
return this.observedMediasoupRouters.get(settings.router.id);
|
|
6849
6913
|
}
|
|
6850
6914
|
const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
|
|
@@ -6875,7 +6939,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6875
6939
|
}
|
|
6876
6940
|
close() {
|
|
6877
6941
|
if (this.closed) {
|
|
6878
|
-
return
|
|
6942
|
+
return logger10.debug("Attempted to close twice");
|
|
6879
6943
|
}
|
|
6880
6944
|
this.closed = true;
|
|
6881
6945
|
for (const call of [...this.observedCalls.values()]) call.close();
|
|
@@ -6893,7 +6957,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6893
6957
|
try {
|
|
6894
6958
|
this.acceptMiddlewares.process({ sample, context });
|
|
6895
6959
|
} catch (err) {
|
|
6896
|
-
|
|
6960
|
+
logger10.warn("An accept middleware threw; dropping the sample. %o", err);
|
|
6897
6961
|
}
|
|
6898
6962
|
if (!sample.callId) {
|
|
6899
6963
|
this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
|
|
@@ -6933,7 +6997,7 @@ var Observer = class extends import_events6.EventEmitter {
|
|
|
6933
6997
|
try {
|
|
6934
6998
|
validator.update();
|
|
6935
6999
|
} catch (err) {
|
|
6936
|
-
|
|
7000
|
+
logger10.warn("Error running validator %s: %o", validator.name, err);
|
|
6937
7001
|
}
|
|
6938
7002
|
}
|
|
6939
7003
|
this._notify("observer-updated", { ...this.eventScope });
|
|
@@ -7247,21 +7311,56 @@ var RemoteTrackResolver = class {
|
|
|
7247
7311
|
const onOutboundRemoved = (p) => {
|
|
7248
7312
|
if (p.observedCall === this.observedCall) this._removeOutboundTrack(p.observedOutboundTrack);
|
|
7249
7313
|
};
|
|
7314
|
+
const onInboundUpdated = (p) => {
|
|
7315
|
+
if (p.observedCall !== this.observedCall) return;
|
|
7316
|
+
if (this._pendingInboundTracks.has(p.observedInboundTrack)) this._addInboundTrack(p.observedInboundTrack);
|
|
7317
|
+
};
|
|
7318
|
+
const onOutboundUpdated = (p) => {
|
|
7319
|
+
if (p.observedCall !== this.observedCall) return;
|
|
7320
|
+
if (this._pendingOutboundTracks.has(p.observedOutboundTrack)) this._addOutboundTrack(p.observedOutboundTrack);
|
|
7321
|
+
};
|
|
7250
7322
|
observedCall.once("close", () => {
|
|
7251
7323
|
observer.off("inbound-track-added", onInboundAdded);
|
|
7252
7324
|
observer.off("inbound-track-removed", onInboundRemoved);
|
|
7253
7325
|
observer.off("outbound-track-added", onOutboundAdded);
|
|
7254
7326
|
observer.off("outbound-track-removed", onOutboundRemoved);
|
|
7327
|
+
observer.off("inbound-track-updated", onInboundUpdated);
|
|
7328
|
+
observer.off("outbound-track-updated", onOutboundUpdated);
|
|
7329
|
+
this._pendingInboundTracks.clear();
|
|
7330
|
+
this._pendingOutboundTracks.clear();
|
|
7255
7331
|
});
|
|
7256
7332
|
observer.on("inbound-track-added", onInboundAdded);
|
|
7257
7333
|
observer.on("inbound-track-removed", onInboundRemoved);
|
|
7258
7334
|
observer.on("outbound-track-added", onOutboundAdded);
|
|
7259
7335
|
observer.on("outbound-track-removed", onOutboundRemoved);
|
|
7336
|
+
observer.on("inbound-track-updated", onInboundUpdated);
|
|
7337
|
+
observer.on("outbound-track-updated", onOutboundUpdated);
|
|
7260
7338
|
}
|
|
7261
7339
|
observedCall;
|
|
7262
7340
|
resolvers;
|
|
7263
7341
|
_publisherIdToOutboundTrack = /* @__PURE__ */ new Map();
|
|
7264
7342
|
_subscriberIdToInboundTrack = /* @__PURE__ */ new Map();
|
|
7343
|
+
/**
|
|
7344
|
+
* Tracks whose publisher id the strategy could not resolve **yet**.
|
|
7345
|
+
*
|
|
7346
|
+
* A track announces itself once, but its `attachments` are replaced on every sample, so a key that
|
|
7347
|
+
* is missing from the first sample can appear on the second — and a strategy backed by an
|
|
7348
|
+
* application's own mapping (a server-side `ssrc -> producerId` table, say) is inherently racy
|
|
7349
|
+
* against sample arrival. Resolving only at `*-track-added` meant losing those tracks for their
|
|
7350
|
+
* entire life, silently: an unresolvable outbound track never even reaches
|
|
7351
|
+
* `unconsumedOutboundTracks`, so it is invisible to `UnconsumedTrackDetector` too.
|
|
7352
|
+
*
|
|
7353
|
+
* So they wait here and are retried on their own `*-track-updated`, i.e. exactly when new stats
|
|
7354
|
+
* arrived for them. A track leaves on its first successful resolution or on removal, which makes
|
|
7355
|
+
* a linked track cost one `Set.has` per update and bounds these sets by the unresolved tracks
|
|
7356
|
+
* alive right now.
|
|
7357
|
+
*/
|
|
7358
|
+
_pendingInboundTracks = /* @__PURE__ */ new Set();
|
|
7359
|
+
_pendingOutboundTracks = /* @__PURE__ */ new Set();
|
|
7360
|
+
/** Tracks still waiting for a resolvable publisher id. Diagnostics; normally both are empty. */
|
|
7361
|
+
get pendingTrackCounts() {
|
|
7362
|
+
return { inbound: this._pendingInboundTracks.size, outbound: this._pendingOutboundTracks.size };
|
|
7363
|
+
}
|
|
7265
7364
|
/** The published (outbound) track for a publisher id, if any. */
|
|
7266
7365
|
getOutboundTrackByPublisherId(publisherId) {
|
|
7267
7366
|
return this._publisherIdToOutboundTrack.get(publisherId);
|
|
@@ -7279,7 +7378,11 @@ var RemoteTrackResolver = class {
|
|
|
7279
7378
|
}
|
|
7280
7379
|
_addInboundTrack(inboundTrack) {
|
|
7281
7380
|
const publisherId = this.resolvers.resolveInboundTrackPublisherId(inboundTrack);
|
|
7282
|
-
if (!publisherId)
|
|
7381
|
+
if (!publisherId) {
|
|
7382
|
+
this._pendingInboundTracks.add(inboundTrack);
|
|
7383
|
+
return;
|
|
7384
|
+
}
|
|
7385
|
+
this._pendingInboundTracks.delete(inboundTrack);
|
|
7283
7386
|
const subscriberId = this.resolvers.resolveInboundTrackSubscriberId?.(inboundTrack);
|
|
7284
7387
|
if (subscriberId) {
|
|
7285
7388
|
this._subscriberIdToInboundTrack.set(subscriberId, inboundTrack);
|
|
@@ -7291,6 +7394,7 @@ var RemoteTrackResolver = class {
|
|
|
7291
7394
|
this.observedCall.unconsumedOutboundTracks.delete(outboundTrack);
|
|
7292
7395
|
}
|
|
7293
7396
|
_removeInboundTrack(inboundTrack) {
|
|
7397
|
+
this._pendingInboundTracks.delete(inboundTrack);
|
|
7294
7398
|
const outboundTrack = inboundTrack.remoteOutboundTrack;
|
|
7295
7399
|
outboundTrack?.remoteInboundTracks.delete(inboundTrack);
|
|
7296
7400
|
inboundTrack.remoteOutboundTrack = void 0;
|
|
@@ -7304,7 +7408,11 @@ var RemoteTrackResolver = class {
|
|
|
7304
7408
|
}
|
|
7305
7409
|
_addOutboundTrack(outboundTrack) {
|
|
7306
7410
|
const publisherId = this.resolvers.resolveOutboundTrackPublisherId(outboundTrack);
|
|
7307
|
-
if (!publisherId)
|
|
7411
|
+
if (!publisherId) {
|
|
7412
|
+
this._pendingOutboundTracks.add(outboundTrack);
|
|
7413
|
+
return;
|
|
7414
|
+
}
|
|
7415
|
+
this._pendingOutboundTracks.delete(outboundTrack);
|
|
7308
7416
|
this._publisherIdToOutboundTrack.set(publisherId, outboundTrack);
|
|
7309
7417
|
this.observedCall.unconsumedOutboundTracks.add(outboundTrack);
|
|
7310
7418
|
for (const observedClient of this.observedCall.observedClients.values()) {
|
|
@@ -7319,6 +7427,7 @@ var RemoteTrackResolver = class {
|
|
|
7319
7427
|
}
|
|
7320
7428
|
}
|
|
7321
7429
|
_removeOutboundTrack(outboundTrack) {
|
|
7430
|
+
this._pendingOutboundTracks.delete(outboundTrack);
|
|
7322
7431
|
const publisherId = this.resolvers.resolveOutboundTrackPublisherId(outboundTrack);
|
|
7323
7432
|
if (publisherId && this._publisherIdToOutboundTrack.get(publisherId) === outboundTrack) {
|
|
7324
7433
|
this._publisherIdToOutboundTrack.delete(publisherId);
|
|
@@ -7371,6 +7480,7 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7371
7480
|
ClientSampleSink,
|
|
7372
7481
|
CodecConsistencyValidator,
|
|
7373
7482
|
Detectors,
|
|
7483
|
+
GEOHASH_CELL_SIZES,
|
|
7374
7484
|
InMemorySink,
|
|
7375
7485
|
IssueFanOutDetector,
|
|
7376
7486
|
IssueFanOutTypes,
|
|
@@ -7431,6 +7541,7 @@ function createP2pRemoteTrackResolverFactory() {
|
|
|
7431
7541
|
createP2pRemoteTrackResolverFactory,
|
|
7432
7542
|
defaultCallSummaryConfig,
|
|
7433
7543
|
defaultClientHealthThresholds,
|
|
7544
|
+
geohash,
|
|
7434
7545
|
isClientIssueResolutionEntry,
|
|
7435
7546
|
issuePayloadAsString,
|
|
7436
7547
|
mannKendall,
|