@observertc/observer-js 1.2.0 → 1.3.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -37,18 +37,189 @@ function createLogger(moduleName) {
37
37
  }
38
38
  }();
39
39
  }
40
- function setObserverLogger(logger11) {
41
- mainLogger = logger11;
40
+ function setObserverLogger(logger13) {
41
+ mainLogger = logger13;
42
42
  }
43
43
 
44
44
  // src/ObservedCall.ts
45
- import { EventEmitter as EventEmitter3 } from "events";
45
+ import { EventEmitter as EventEmitter4 } from "events";
46
+
47
+ // src/sinks/SinkRegistry.ts
48
+ import { v4 as uuidv4 } from "uuid";
49
+ var logger = createLogger("SinkRegistry");
50
+ var SinkRegistry = class {
51
+ constructor(parent) {
52
+ this.parent = parent;
53
+ }
54
+ parent;
55
+ // This registry's sinks and its descendants'.
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ _sinks = /* @__PURE__ */ new Map();
58
+ /** Number of open sinks in this registry and its descendants. */
59
+ get size() {
60
+ return this._sinks.size;
61
+ }
62
+ keys() {
63
+ return this._sinks.keys();
64
+ }
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ values() {
67
+ return this._sinks.values();
68
+ }
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ get(id) {
71
+ return this._sinks.get(id);
72
+ }
73
+ /**
74
+ * Construct `new SinkClass(settings)` and register it. The id (`settings.id`, generated when
75
+ * omitted) is checked first, so nothing is constructed — no file is opened — when it is taken:
76
+ * `undefined` is returned and a warning logged.
77
+ */
78
+ create(SinkClass, settings) {
79
+ const lineage = this._lineage();
80
+ const id = settings.id ?? uuidv4();
81
+ if (lineage[lineage.length - 1]._sinks.has(id)) {
82
+ logger.warn("Sink id %s is already in use; sink ids must be unique per observer. No sink was created", id);
83
+ return void 0;
84
+ }
85
+ const sink = new SinkClass({ ...settings, id });
86
+ const onError = (err) => logger.warn("Sink %s failed: %o", id, err);
87
+ sink.on("error", onError);
88
+ sink.once("close", () => sink.off("error", onError));
89
+ sink.once("ended", () => {
90
+ for (const registry of lineage) {
91
+ if (registry._sinks.get(id) === sink) registry._sinks.delete(id);
92
+ }
93
+ });
94
+ for (const registry of lineage) registry._sinks.set(id, sink);
95
+ return sink;
96
+ }
97
+ /** Write `record` to the sink `id`. `false` when there is no such sink, or on backpressure. */
98
+ write(id, record) {
99
+ const sink = this._sinks.get(id);
100
+ if (!sink) {
101
+ logger.debug("No sink %s; record dropped", id);
102
+ return false;
103
+ }
104
+ return sink.write(record);
105
+ }
106
+ /** End every sink of this registry and of its descendants. */
107
+ clear() {
108
+ for (const sink of [...this._sinks.values()]) {
109
+ try {
110
+ sink.end();
111
+ } catch (err) {
112
+ logger.warn("Sink %s failed to end: %o", sink.id, err);
113
+ }
114
+ }
115
+ }
116
+ /** This registry and its ancestors, up to the root. */
117
+ _lineage() {
118
+ return [this, ...this.parent?._lineage() ?? []];
119
+ }
120
+ };
121
+
122
+ // src/sinks/ExtensionStatsRegistry.ts
123
+ var logger2 = createLogger("ExtensionStats");
124
+ var ExtensionStatsRegistry = class {
125
+ constructor(id, _sinks) {
126
+ this.id = id;
127
+ this._sinks = _sinks;
128
+ }
129
+ id;
130
+ _sinks;
131
+ /** The last data accepted for each extension stats id. */
132
+ values = /* @__PURE__ */ new Map();
133
+ closed = false;
134
+ /**
135
+ * Refuse further `create` / `accept`, and end the extension stats' sinks (the sinks whose id is
136
+ * an extension stats id). Other sinks of the owner, e.g. client sample sinks, are left to the
137
+ * owner's `sinks.clear()`. Called by the owner when it closes.
138
+ */
139
+ close() {
140
+ if (this.closed) return;
141
+ this.closed = true;
142
+ for (const id of this.values.keys()) {
143
+ this._sinks.get(id)?.end();
144
+ }
145
+ }
146
+ create(settings) {
147
+ if (this.closed) {
148
+ logger2.warn("Attempted to create extension stats %s on closed %s", settings.id, this.id);
149
+ return void 0;
150
+ }
151
+ if (this.values.has(settings.id)) {
152
+ logger2.warn("Extension stats with id %s already exists on %s", settings.id, this.id);
153
+ return void 0;
154
+ }
155
+ this.values.set(settings.id, void 0);
156
+ if (!settings.createSink) return void 0;
157
+ const [SinkClass, sinkSettings] = settings.createSink;
158
+ this._sinks.get(settings.id)?.end();
159
+ return this._sinks.create(SinkClass, { ...sinkSettings, id: settings.id });
160
+ }
161
+ accept(id, data) {
162
+ if (this.closed) {
163
+ logger2.debug("Extension stats %s dropped: %s is closed", id, this.id);
164
+ return;
165
+ }
166
+ this.values.set(id, data);
167
+ try {
168
+ this._sinks.get(id)?.write(data);
169
+ } catch (err) {
170
+ logger2.warn("Failed to write extension stats %s to sink: %o", id, err);
171
+ }
172
+ }
173
+ get(id) {
174
+ return this.values.get(id);
175
+ }
176
+ };
177
+
178
+ // src/sinks/JsonlFileSink.ts
179
+ import * as fs from "fs";
180
+ import { EventEmitter } from "events";
181
+ var JsonlFileSink = class extends EventEmitter {
182
+ id;
183
+ /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
184
+ path;
185
+ _stream;
186
+ _serializeSample;
187
+ _ended = false;
188
+ constructor(options) {
189
+ super();
190
+ this.id = options.id ?? options.path;
191
+ this.path = options.path;
192
+ this._stream = fs.createWriteStream(this.path, { flags: "a" });
193
+ this._stream.on("close", () => this.emit("close"));
194
+ this._stream.on("finish", () => this.emit("finish"));
195
+ this._stream.on("drain", () => this.emit("drain"));
196
+ this._stream.on("error", (err) => this.emit("error", err));
197
+ this._serializeSample = options.serializeSample ?? ((record) => JSON.stringify(record));
198
+ }
199
+ get ended() {
200
+ return this._ended;
201
+ }
202
+ write(record) {
203
+ if (this._ended) return false;
204
+ return this._stream.write(`${this._serializeSample(record)}
205
+ `);
206
+ }
207
+ end() {
208
+ if (this._ended) return;
209
+ this._ended = true;
210
+ this.emit("ended");
211
+ this._stream.end();
212
+ }
213
+ };
214
+ function createJsonlFileSink(options) {
215
+ return new JsonlFileSink(options);
216
+ }
46
217
 
47
218
  // src/ObservedClient.ts
48
- import { EventEmitter as EventEmitter2 } from "events";
219
+ import { EventEmitter as EventEmitter3 } from "events";
49
220
 
50
221
  // src/ObservedPeerConnection.ts
51
- import { EventEmitter } from "events";
222
+ import { EventEmitter as EventEmitter2 } from "events";
52
223
 
53
224
  // src/utils/stats.ts
54
225
  function percentile(values, p) {
@@ -205,8 +376,9 @@ function percentileOfHistogram(histogram, bucketWidth, p, options = {}) {
205
376
  continue;
206
377
  }
207
378
  if (options.openEnded && index === histogram.length - 1 && options.max !== void 0) return options.max;
208
- const estimate = index * bucketWidth + (rank - cumulative) / count * bucketWidth;
209
- return Math.min(Math.max(estimate, options.min ?? estimate), options.max ?? estimate);
379
+ const lower = Math.max(index * bucketWidth, options.min ?? -Infinity);
380
+ const upper = Math.min((index + 1) * bucketWidth, options.max ?? Infinity);
381
+ return lower + (rank - cumulative) / count * Math.max(0, upper - lower);
210
382
  }
211
383
  return options.max;
212
384
  }
@@ -1740,14 +1912,14 @@ var ObservedOutboundTrack = class {
1740
1912
 
1741
1913
  // src/ObservedPeerConnection.ts
1742
1914
  var ascending2 = (a, b) => a - b;
1743
- var logger = createLogger("ObservedPeerConnection");
1915
+ var logger3 = createLogger("ObservedPeerConnection");
1744
1916
  function publicAddressOfLocalCandidate(candidate) {
1745
1917
  if (!candidate) return void 0;
1746
1918
  if (candidate.candidateType !== "relay" && isPublicIpAddress(candidate.address)) return candidate.address;
1747
1919
  if (isPublicIpAddress(candidate.relatedAddress)) return candidate.relatedAddress;
1748
1920
  return void 0;
1749
1921
  }
1750
- var ObservedPeerConnection = class extends EventEmitter {
1922
+ var ObservedPeerConnection = class extends EventEmitter2 {
1751
1923
  constructor(peerConnectionId, client) {
1752
1924
  super();
1753
1925
  this.peerConnectionId = peerConnectionId;
@@ -2329,7 +2501,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2329
2501
  let observedCertificate = this.observedCertificates.get(stats.id);
2330
2502
  if (!observedCertificate) {
2331
2503
  if (!stats.timestamp || !stats.id || !stats.fingerprint) {
2332
- return logger.warn(
2504
+ return logger3.warn(
2333
2505
  `ObservedPeerConnection received an invalid CertificateStats (missing timestamp OR id OR fingerprint field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2334
2506
  stats
2335
2507
  );
@@ -2348,7 +2520,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2348
2520
  let observedCodec = this.observedCodecs.get(stats.id);
2349
2521
  if (!observedCodec) {
2350
2522
  if (!stats.timestamp || !stats.id || !stats.mimeType) {
2351
- return logger.warn(
2523
+ return logger3.warn(
2352
2524
  `ObservedPeerConnection received an invalid CodecStats (missing timestamp OR id OR mimeType field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2353
2525
  stats
2354
2526
  );
@@ -2367,7 +2539,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2367
2539
  let observedDataChannel = this.observedDataChannels.get(stats.id);
2368
2540
  if (!observedDataChannel) {
2369
2541
  if (!stats.timestamp || !stats.id) {
2370
- return logger.warn(
2542
+ return logger3.warn(
2371
2543
  `ObservedPeerConnection received an invalid DataChannelStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2372
2544
  stats
2373
2545
  );
@@ -2386,7 +2558,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2386
2558
  let observedIceCandidate = this.observedIceCandidates.get(stats.id);
2387
2559
  if (!observedIceCandidate) {
2388
2560
  if (!stats.timestamp || !stats.id) {
2389
- return logger.warn(
2561
+ return logger3.warn(
2390
2562
  `ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2391
2563
  stats
2392
2564
  );
@@ -2405,7 +2577,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2405
2577
  let observedIceCandidatePair = this.observedIceCandidatesPair.get(stats.id);
2406
2578
  if (!observedIceCandidatePair) {
2407
2579
  if (!stats.timestamp || !stats.id) {
2408
- return logger.warn(
2580
+ return logger3.warn(
2409
2581
  `ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2410
2582
  stats
2411
2583
  );
@@ -2424,7 +2596,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2424
2596
  let observedIceTransport = this.observedIceTransports.get(stats.id);
2425
2597
  if (!observedIceTransport) {
2426
2598
  if (!stats.timestamp || !stats.id) {
2427
- return logger.warn(
2599
+ return logger3.warn(
2428
2600
  `ObservedPeerConnection received an invalid IceCandidateStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2429
2601
  stats
2430
2602
  );
@@ -2443,7 +2615,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2443
2615
  let observedInboundRtp = this.observedInboundRtps.get(stats.ssrc);
2444
2616
  if (!observedInboundRtp) {
2445
2617
  if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind || !stats.trackIdentifier) {
2446
- return logger.warn(
2618
+ return logger3.warn(
2447
2619
  `ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR ssrc OR kind OR trackIdentifier field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2448
2620
  stats
2449
2621
  );
@@ -2469,7 +2641,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2469
2641
  let observedInboundTrack = this.observedInboundTracks.get(stats.id);
2470
2642
  if (!observedInboundTrack) {
2471
2643
  if (!stats.timestamp || !stats.id || !stats.kind) {
2472
- return logger.warn(
2644
+ return logger3.warn(
2473
2645
  `ObservedPeerConnection received an invalid InboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2474
2646
  stats
2475
2647
  );
@@ -2497,7 +2669,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2497
2669
  let observedMediaPlayout = this.observedMediaPlayouts.get(stats.id);
2498
2670
  if (!observedMediaPlayout) {
2499
2671
  if (!stats.timestamp || !stats.id || !stats.kind) {
2500
- return logger.warn(
2672
+ return logger3.warn(
2501
2673
  `ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind OR trackIdentifier field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2502
2674
  stats
2503
2675
  );
@@ -2521,7 +2693,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2521
2693
  let observedMediaSource = this.observedMediaSources.get(stats.id);
2522
2694
  if (!observedMediaSource) {
2523
2695
  if (!stats.timestamp || !stats.id || !stats.kind) {
2524
- return logger.warn(
2696
+ return logger3.warn(
2525
2697
  `ObservedPeerConnection received an invalid InboundRtpStats (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2526
2698
  stats
2527
2699
  );
@@ -2545,7 +2717,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2545
2717
  let observedOutboundRtp = this.observedOutboundRtps.get(stats.ssrc);
2546
2718
  if (!observedOutboundRtp) {
2547
2719
  if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
2548
- return logger.warn(
2720
+ return logger3.warn(
2549
2721
  `ObservedPeerConnection received an invalid OutboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2550
2722
  stats
2551
2723
  );
@@ -2570,7 +2742,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2570
2742
  let observedOutboundTrack = this.observedOutboundTracks.get(stats.id);
2571
2743
  if (!observedOutboundTrack) {
2572
2744
  if (!stats.timestamp || !stats.id || !stats.kind) {
2573
- return logger.warn(
2745
+ return logger3.warn(
2574
2746
  `ObservedPeerConnection received an invalid OutboundTrackSample (missing timestamp OR id OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2575
2747
  stats
2576
2748
  );
@@ -2598,7 +2770,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2598
2770
  let observedPeerConnectionTransport = this.observedPeerConnectionTransports.get(stats.id);
2599
2771
  if (!observedPeerConnectionTransport) {
2600
2772
  if (!stats.timestamp || !stats.id) {
2601
- return logger.warn(
2773
+ return logger3.warn(
2602
2774
  `ObservedPeerConnection received an invalid PeerConnectionTransportStats (missing timestamp OR id field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2603
2775
  stats
2604
2776
  );
@@ -2617,7 +2789,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2617
2789
  let observedRemoteInboundRtp = this.observedRemoteInboundRtps.get(stats.ssrc);
2618
2790
  if (!observedRemoteInboundRtp) {
2619
2791
  if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
2620
- return logger.warn(
2792
+ return logger3.warn(
2621
2793
  `ObservedPeerConnection received an invalid RemoteInboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2622
2794
  stats
2623
2795
  );
@@ -2642,7 +2814,7 @@ var ObservedPeerConnection = class extends EventEmitter {
2642
2814
  let observedRemoteOutboundRtp = this.observedRemoteOutboundRtps.get(stats.ssrc);
2643
2815
  if (!observedRemoteOutboundRtp) {
2644
2816
  if (!stats.timestamp || !stats.id || !stats.ssrc || !stats.kind) {
2645
- return logger.warn(
2817
+ return logger3.warn(
2646
2818
  `ObservedPeerConnection received an invalid RemoteOutboundRtpStats (missing timestamp OR id OR ssrc OR kind field). PeerConnectionId: ${this.peerConnectionId} ClientId: ${this.client.clientId}, CallId: ${this.client.call.callId}`,
2647
2819
  stats
2648
2820
  );
@@ -2742,35 +2914,27 @@ function baseIssueType(type) {
2742
2914
  }
2743
2915
 
2744
2916
  // src/ObservedClient.ts
2745
- var logger2 = createLogger("ObservedClient");
2917
+ var logger4 = createLogger("ObservedClient");
2746
2918
  var RTT_HISTOGRAM_BUCKET_WIDTH_IN_MS = 20;
2747
2919
  var RTT_HISTOGRAM_NUMBER_OF_BUCKETS = 50;
2748
2920
  var SCORE_HISTOGRAM_BUCKET_WIDTH = 1;
2749
2921
  var SCORE_HISTOGRAM_NUMBER_OF_BUCKETS = 5;
2750
- var ObservedClient = class extends EventEmitter2 {
2922
+ var ObservedClient = class extends EventEmitter3 {
2751
2923
  constructor(settings, call, activeIssues) {
2752
2924
  super();
2753
2925
  this.call = call;
2754
2926
  this.setMaxListeners(Infinity);
2755
2927
  this.eventScope = { observer: call.observer, observedCall: call, observedClient: this };
2756
- this.sink = call.observer.config.createClientSink?.({ clientId: settings.clientId, observedCall: call });
2757
2928
  this.clientId = settings.clientId;
2758
2929
  this.appData = settings.appData ?? {};
2759
2930
  this.settings = {
2760
2931
  closeClientIfIdleForMs: settings.closeClientIfIdleForMs
2761
2932
  };
2762
- if (this.sink) {
2763
- const onError = (err) => logger2.warn("ClientSampleSink error for client %s: %o", this.clientId, err);
2764
- this.sink.once("close", () => this.sink?.off("error", onError));
2765
- this.sink.on("error", onError);
2766
- }
2767
2933
  this.activeIssues = activeIssues;
2768
2934
  }
2769
2935
  call;
2770
2936
  clientId;
2771
2937
  observedPeerConnections = /* @__PURE__ */ new Map();
2772
- /** Per-client sink (from `ObserverConfig.createClientSink`), if any. */
2773
- sink;
2774
2938
  /** Ancestry base shared by all Observer-bus events originating at this client. */
2775
2939
  eventScope;
2776
2940
  calculatedScore = {
@@ -2909,6 +3073,13 @@ var ObservedClient = class extends EventEmitter2 {
2909
3073
  _closedPeerConnectionIds = /* @__PURE__ */ new Set();
2910
3074
  closeTimer;
2911
3075
  /** The distinct `publicAddress`es currently known by this client's open peer connections. */
3076
+ /**
3077
+ * The sink this client's samples are written to: whatever is registered under its client id in
3078
+ * `call.sinks` (the `createClientJsonlSink` file, or a sink the application added), if any.
3079
+ */
3080
+ get sink() {
3081
+ return this.call.sinks.get(this.clientId);
3082
+ }
2912
3083
  get publicAddresses() {
2913
3084
  const result = /* @__PURE__ */ new Set();
2914
3085
  for (const observedPeerConnection of this.observedPeerConnections.values()) {
@@ -3003,12 +3174,12 @@ var ObservedClient = class extends EventEmitter2 {
3003
3174
  try {
3004
3175
  this.sink?.end();
3005
3176
  } catch (err) {
3006
- logger2.warn("ClientSampleSink.end failed for client %s: %o", this.clientId, err);
3177
+ logger4.warn("Sink end failed for client %s: %o", this.clientId, err);
3007
3178
  }
3008
3179
  }
3009
3180
  accept(sample, context) {
3010
3181
  if (this.closed) {
3011
- logger2.warn("Attempted to accept a sample on a closed client %s", this.clientId);
3182
+ logger4.warn("Attempted to accept a sample on a closed client %s", this.clientId);
3012
3183
  return;
3013
3184
  }
3014
3185
  if (this.closeTimer) {
@@ -3198,7 +3369,7 @@ var ObservedClient = class extends EventEmitter2 {
3198
3369
  try {
3199
3370
  this.sink?.write(sample);
3200
3371
  } catch (err) {
3201
- logger2.warn("ClientSampleSink.write failed for client %s: %o", this.clientId, err);
3372
+ logger4.warn("Sink write failed for client %s: %o", this.clientId, err);
3202
3373
  }
3203
3374
  }
3204
3375
  injectMetaData(metaData) {
@@ -3381,10 +3552,10 @@ var ObservedClient = class extends EventEmitter2 {
3381
3552
  this._notify("client-rejoined", { ...this.eventScope, timestamp: event.timestamp });
3382
3553
  } else {
3383
3554
  this.joinedAt = event.timestamp;
3384
- logger2.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`);
3555
+ logger4.warn(`Client ${this.clientId} joinedAt timestamp was updated to ${event.timestamp}. the joined event will not be emitted.`);
3385
3556
  }
3386
3557
  }
3387
- logger2.debug("Client %s joined at %o", this.clientId, event);
3558
+ logger4.debug("Client %s joined at %o", this.clientId, event);
3388
3559
  break;
3389
3560
  }
3390
3561
  case "CLIENT_LEFT" /* CLIENT_LEFT */: {
@@ -3394,10 +3565,10 @@ var ObservedClient = class extends EventEmitter2 {
3394
3565
  this.emit("left");
3395
3566
  this._notify("client-left", { ...this.eventScope });
3396
3567
  } else {
3397
- logger2.warn(`Client ${this.clientId} leftAt timestamp was already set`);
3568
+ logger4.warn(`Client ${this.clientId} leftAt timestamp was already set`);
3398
3569
  }
3399
3570
  }
3400
- logger2.debug("Client %s left at %o", this.clientId, event);
3571
+ logger4.debug("Client %s left at %o", this.clientId, event);
3401
3572
  break;
3402
3573
  }
3403
3574
  case "PEER_CONNECTION_OPENED" /* PEER_CONNECTION_OPENED */: {
@@ -3409,7 +3580,7 @@ var ObservedClient = class extends EventEmitter2 {
3409
3580
  } else if (postBuffer) {
3410
3581
  postBuffer.push(event);
3411
3582
  } else {
3412
- logger2.warn("Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o", event);
3583
+ logger4.warn("Received PEER_CONNECTION_OPENED event without a corresponding observedPeerConnection: %o", event);
3413
3584
  }
3414
3585
  }
3415
3586
  break;
@@ -3426,7 +3597,7 @@ var ObservedClient = class extends EventEmitter2 {
3426
3597
  } else if (postBuffer) {
3427
3598
  postBuffer.push(event);
3428
3599
  } else {
3429
- logger2.warn("Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o", event);
3600
+ logger4.warn("Received PEER_CONNECTION_CLOSED event without a corresponding observedPeerConnection: %o", event);
3430
3601
  }
3431
3602
  }
3432
3603
  break;
@@ -3441,7 +3612,7 @@ var ObservedClient = class extends EventEmitter2 {
3441
3612
  } else if (postBuffer) {
3442
3613
  postBuffer.push(event);
3443
3614
  } else {
3444
- logger2.warn("Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3615
+ logger4.warn("Received MEDIA_TRACK_ADDED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3445
3616
  }
3446
3617
  }
3447
3618
  break;
@@ -3456,7 +3627,7 @@ var ObservedClient = class extends EventEmitter2 {
3456
3627
  } else if (postBuffer) {
3457
3628
  postBuffer.push(event);
3458
3629
  } else {
3459
- logger2.warn("Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3630
+ logger4.warn("Received MEDIA_TRACK_REMOVED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3460
3631
  }
3461
3632
  }
3462
3633
  break;
@@ -3471,7 +3642,7 @@ var ObservedClient = class extends EventEmitter2 {
3471
3642
  } else if (postBuffer) {
3472
3643
  postBuffer.push(event);
3473
3644
  } else {
3474
- logger2.warn("Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
3645
+ logger4.warn("Received DATA_CHANNEL_OPENED event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
3475
3646
  }
3476
3647
  }
3477
3648
  break;
@@ -3486,7 +3657,7 @@ var ObservedClient = class extends EventEmitter2 {
3486
3657
  } else if (postBuffer) {
3487
3658
  postBuffer.push(event);
3488
3659
  } else {
3489
- logger2.warn("Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
3660
+ logger4.warn("Received DATA_CHANNEL_CLOSE event without a corresponding observedPeerConnection or observedDataChannel: %o", event);
3490
3661
  }
3491
3662
  }
3492
3663
  break;
@@ -3506,7 +3677,7 @@ var ObservedClient = class extends EventEmitter2 {
3506
3677
  } else if (postBuffer) {
3507
3678
  postBuffer.push(event);
3508
3679
  } else {
3509
- logger2.warn("Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3680
+ logger4.warn("Received MEDIA_TRACK_MUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3510
3681
  }
3511
3682
  }
3512
3683
  break;
@@ -3526,7 +3697,7 @@ var ObservedClient = class extends EventEmitter2 {
3526
3697
  } else if (postBuffer) {
3527
3698
  postBuffer.push(event);
3528
3699
  } else {
3529
- logger2.warn("Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3700
+ logger4.warn("Received MEDIA_TRACK_UNMUTED event without a corresponding observedPeerConnection or observedTrack: %o", event);
3530
3701
  }
3531
3702
  }
3532
3703
  break;
@@ -3541,7 +3712,7 @@ var ObservedClient = class extends EventEmitter2 {
3541
3712
  } else if (postBuffer) {
3542
3713
  postBuffer.push(event);
3543
3714
  } else {
3544
- logger2.warn("Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3715
+ logger4.warn("Received ICE_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3545
3716
  }
3546
3717
  }
3547
3718
  break;
@@ -3556,7 +3727,7 @@ var ObservedClient = class extends EventEmitter2 {
3556
3727
  } else if (postBuffer) {
3557
3728
  postBuffer.push(event);
3558
3729
  } else {
3559
- logger2.warn("Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3730
+ logger4.warn("Received ICE_GATHERING_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3560
3731
  }
3561
3732
  }
3562
3733
  break;
@@ -3571,7 +3742,7 @@ var ObservedClient = class extends EventEmitter2 {
3571
3742
  } else if (postBuffer) {
3572
3743
  postBuffer.push(event);
3573
3744
  } else {
3574
- logger2.warn("Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3745
+ logger4.warn("Received PEER_CONNECTION_STATE_CHANGED event without a corresponding observedPeerConnection: %o", event);
3575
3746
  }
3576
3747
  }
3577
3748
  break;
@@ -3590,11 +3761,11 @@ var ObservedClient = class extends EventEmitter2 {
3590
3761
  let observedPeerConnection = this.observedPeerConnections.get(sample.peerConnectionId);
3591
3762
  if (!observedPeerConnection) {
3592
3763
  if (this._closedPeerConnectionIds.has(sample.peerConnectionId)) {
3593
- logger2.debug("Ignoring stats of peer connection %s, already closed by client %s", sample.peerConnectionId, this.clientId);
3764
+ logger4.debug("Ignoring stats of peer connection %s, already closed by client %s", sample.peerConnectionId, this.clientId);
3594
3765
  return void 0;
3595
3766
  }
3596
3767
  if (!sample.peerConnectionId) {
3597
- return logger2.warn(
3768
+ return logger4.warn(
3598
3769
  `ObservedClient received an invalid PeerConnectionSample (missing peerConnectionId field). ClientId: ${this.clientId}, CallId: ${this.call.callId}`,
3599
3770
  sample
3600
3771
  ), void 0;
@@ -3654,7 +3825,7 @@ var ObservedClient = class extends EventEmitter2 {
3654
3825
  try {
3655
3826
  this.sink?.write(finalSample);
3656
3827
  } catch (err) {
3657
- logger2.warn("ClientSampleSink.write failed during injection flush for client %s: %o", this.clientId, err);
3828
+ logger4.warn("Sink write failed during injection flush for client %s: %o", this.clientId, err);
3658
3829
  }
3659
3830
  }
3660
3831
  /** Emit an Observer-bus event scoped to this client (or a peer connection under it). */
@@ -3682,7 +3853,7 @@ var DefaultCallScoreCalculator = class {
3682
3853
  };
3683
3854
 
3684
3855
  // src/detectors/Detectors.ts
3685
- var logger3 = createLogger("Detectors");
3856
+ var logger5 = createLogger("Detectors");
3686
3857
  var Detectors = class {
3687
3858
  _detectors;
3688
3859
  constructor(...detectors) {
@@ -3763,7 +3934,7 @@ var Detectors = class {
3763
3934
  try {
3764
3935
  detector.update();
3765
3936
  } catch (err) {
3766
- logger3.warn(`Error updating detector ${detector?.name}`, err);
3937
+ logger5.warn(`Error updating detector ${detector?.name}`, err);
3767
3938
  }
3768
3939
  }
3769
3940
  }
@@ -3776,7 +3947,7 @@ var Detectors = class {
3776
3947
  try {
3777
3948
  detector.close?.();
3778
3949
  } catch (err) {
3779
- logger3.warn(`Error closing detector ${detector?.name}`, err);
3950
+ logger5.warn(`Error closing detector ${detector?.name}`, err);
3780
3951
  }
3781
3952
  }
3782
3953
  };
@@ -4743,8 +4914,8 @@ var AudioEchoDetector = class _AudioEchoDetector {
4743
4914
  };
4744
4915
 
4745
4916
  // src/ObservedCall.ts
4746
- var logger4 = createLogger("ObservedCall");
4747
- var ObservedCall = class extends EventEmitter3 {
4917
+ var logger6 = createLogger("ObservedCall");
4918
+ var ObservedCall = class extends EventEmitter4 {
4748
4919
  constructor(settings, observer, activeIssuesRegistry) {
4749
4920
  super();
4750
4921
  this.observer = observer;
@@ -4752,6 +4923,8 @@ var ObservedCall = class extends EventEmitter3 {
4752
4923
  this.setMaxListeners(Infinity);
4753
4924
  this.eventScope = { observer: this.observer, observedCall: this };
4754
4925
  this.callId = settings.callId;
4926
+ this.sinks = new SinkRegistry(this.observer.sinks);
4927
+ this.extensionStats = new ExtensionStatsRegistry(this.callId, this.sinks);
4755
4928
  this.appData = settings.appData ?? {};
4756
4929
  this.scoreCalculator = new DefaultCallScoreCalculator(this);
4757
4930
  this.detectors = new Detectors();
@@ -4808,6 +4981,8 @@ var ObservedCall = class extends EventEmitter3 {
4808
4981
  settings;
4809
4982
  /** Ancestry base shared by all Observer-bus events originating at this call. */
4810
4983
  eventScope;
4984
+ sinks;
4985
+ extensionStats;
4811
4986
  closeTimer;
4812
4987
  get numberOfClients() {
4813
4988
  return this.observedClients.size;
@@ -4850,7 +5025,7 @@ var ObservedCall = class extends EventEmitter3 {
4850
5025
  break;
4851
5026
  }
4852
5027
  default: {
4853
- logger4.warn("Unknown detector name %s; skipping", name);
5028
+ logger6.warn("Unknown detector name %s; skipping", name);
4854
5029
  return this;
4855
5030
  }
4856
5031
  }
@@ -4920,6 +5095,8 @@ var ObservedCall = class extends EventEmitter3 {
4920
5095
  if (client.joinedAt) minSampleTimestamps = Math.min(minSampleTimestamps ?? client.joinedAt, client.joinedAt);
4921
5096
  if (client.leftAt) maxSampleTimestamps = Math.max(maxSampleTimestamps ?? client.leftAt, client.leftAt);
4922
5097
  }
5098
+ this.extensionStats.close();
5099
+ this.sinks.clear();
4923
5100
  if (this.startedAt === void 0) this.startedAt = minSampleTimestamps;
4924
5101
  if (this.endedAt === void 0) this.endedAt = maxSampleTimestamps;
4925
5102
  this.closedAt = Date.now();
@@ -4932,17 +5109,26 @@ var ObservedCall = class extends EventEmitter3 {
4932
5109
  this.emit("close");
4933
5110
  this._notify("call-closed", { ...this.eventScope });
4934
5111
  }
5112
+ createExtensionStats(settings) {
5113
+ return this.extensionStats.create(settings);
5114
+ }
5115
+ acceptExtensionStats(id, data) {
5116
+ this.extensionStats.accept(id, data);
5117
+ }
5118
+ getExtensionStats(id) {
5119
+ return this.extensionStats.get(id);
5120
+ }
4935
5121
  getObservedClient(clientId) {
4936
5122
  if (this.closed || !this.observedClients.has(clientId)) return;
4937
5123
  return this.observedClients.get(clientId);
4938
5124
  }
4939
5125
  createObservedClient(settings, acceptCtx) {
4940
5126
  if (this.closed) {
4941
- logger4.warn("Attempted to create a client (clientId: %s) on a closed call %s", settings.clientId, this.callId);
5127
+ logger6.warn("Attempted to create a client (clientId: %s) on a closed call %s", settings.clientId, this.callId);
4942
5128
  return void 0;
4943
5129
  }
4944
5130
  if (this.observedClients.has(settings.clientId)) {
4945
- logger4.warn("Client with id %s already exists in call %s; returning the existing instance", settings.clientId, this.callId);
5131
+ logger6.warn("Client with id %s already exists in call %s; returning the existing instance", settings.clientId, this.callId);
4946
5132
  return this.observedClients.get(settings.clientId);
4947
5133
  }
4948
5134
  const clientSettings = {
@@ -4992,10 +5178,17 @@ var ObservedCall = class extends EventEmitter3 {
4992
5178
  clearTimeout(this.closeTimer);
4993
5179
  this.closeTimer = void 0;
4994
5180
  }
5181
+ const jsonlPath = this.observer.config.createClientJsonlSink?.({ observedCall: this, clientId: settings.clientId });
5182
+ let jsonlSink;
5183
+ if (jsonlPath !== void 0 && this.observer.sinks.get(settings.clientId)) {
5184
+ logger6.warn("No sample file for client %s in call %s: sink id %s is already in use", settings.clientId, this.callId, settings.clientId);
5185
+ } else if (jsonlPath !== void 0) {
5186
+ jsonlSink = this.sinks.create(JsonlFileSink, { id: settings.clientId, path: jsonlPath });
5187
+ }
4995
5188
  this.emit("newclient", result);
4996
5189
  this._notify("client-added", { ...this.eventScope, observedClient: result });
4997
- if (result.sink) {
4998
- this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: result.sink });
5190
+ if (jsonlSink) {
5191
+ this._notify("client-sink-created", { ...this.eventScope, observedClient: result, sink: jsonlSink });
4999
5192
  }
5000
5193
  if (wasEmpty) {
5001
5194
  this.emit("not-empty");
@@ -5048,10 +5241,10 @@ var ObservedCall = class extends EventEmitter3 {
5048
5241
  };
5049
5242
 
5050
5243
  // src/Observer.ts
5051
- import { EventEmitter as EventEmitter6 } from "events";
5244
+ import { EventEmitter as EventEmitter7 } from "events";
5052
5245
 
5053
5246
  // src/ObservedTURN.ts
5054
- import { EventEmitter as EventEmitter4 } from "events";
5247
+ import { EventEmitter as EventEmitter5 } from "events";
5055
5248
 
5056
5249
  // src/ObservedTurnServer.ts
5057
5250
  var ObservedTurnServer = class {
@@ -5111,8 +5304,8 @@ var ObservedTurnServer = class {
5111
5304
  };
5112
5305
 
5113
5306
  // src/ObservedTURN.ts
5114
- var logger5 = createLogger("ObservedTURN");
5115
- var ObservedTURN = class extends EventEmitter4 {
5307
+ var logger7 = createLogger("ObservedTURN");
5308
+ var ObservedTURN = class extends EventEmitter5 {
5116
5309
  totalBytesSent = 0;
5117
5310
  totalBytesReceived = 0;
5118
5311
  totalPacketsSent = 0;
@@ -5141,12 +5334,12 @@ var ObservedTURN = class extends EventEmitter4 {
5141
5334
  addPeerConnection(peerConnection) {
5142
5335
  const turnPairs = peerConnection.selectedIceCandiadtePairForTurn;
5143
5336
  if (turnPairs.length !== 1) {
5144
- return logger5.warn(`Expected exactly one TURN pair, but found for peerconnection ${peerConnection.peerConnectionId}`, turnPairs.length), void 0;
5337
+ return logger7.warn(`Expected exactly one TURN pair, but found for peerconnection ${peerConnection.peerConnectionId}`, turnPairs.length), void 0;
5145
5338
  }
5146
5339
  const candidatePair = turnPairs[0];
5147
5340
  const rawUrl = candidatePair.getLocalCandidate()?.url;
5148
5341
  if (!rawUrl) {
5149
- return logger5.warn(`No local candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), void 0;
5342
+ return logger7.warn(`No local candidate URL found for peerconnection ${peerConnection.peerConnectionId}`), void 0;
5150
5343
  }
5151
5344
  const turnServerUrl = rawUrl.split("?")[0];
5152
5345
  let turnServer = this.servers.get(turnServerUrl);
@@ -5222,14 +5415,16 @@ var MiddlewareProcessor = class {
5222
5415
  };
5223
5416
 
5224
5417
  // src/ObservedMediasoupRouter.ts
5225
- import { EventEmitter as EventEmitter5 } from "events";
5226
- var logger6 = createLogger("ObservedMediasoupRouter");
5227
- var ObservedMediasoupRouter = class extends EventEmitter5 {
5418
+ import { EventEmitter as EventEmitter6 } from "events";
5419
+ var logger8 = createLogger("ObservedMediasoupRouter");
5420
+ var ObservedMediasoupRouter = class extends EventEmitter6 {
5228
5421
  router;
5229
5422
  appData;
5230
5423
  sample;
5231
5424
  webrtcTransportIds = /* @__PURE__ */ new Set();
5232
5425
  closed = false;
5426
+ sinks;
5427
+ extensionStats;
5233
5428
  // Id -> sample indexes over the arrays in `this.sample`, so an entity can be reached in O(1)
5234
5429
  // instead of scanning (`sample.producers.find(...)`). They hold the *same* objects as the arrays.
5235
5430
  _transportSamples = /* @__PURE__ */ new Map();
@@ -5238,11 +5433,17 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
5238
5433
  _dataProducerSamples = /* @__PURE__ */ new Map();
5239
5434
  _dataConsumerSamples = /* @__PURE__ */ new Map();
5240
5435
  _enrich;
5241
- constructor(settings) {
5436
+ /**
5437
+ * @param parentSinks the parent of `sinks`: `observer.sinks`, passed by
5438
+ * `Observer.createObservedMediasoupRouter`. Internal; a router constructed directly has none.
5439
+ */
5440
+ constructor(settings, parentSinks) {
5242
5441
  super();
5243
5442
  this.router = settings.router;
5244
5443
  this.appData = settings.appData ?? {};
5245
5444
  this._enrich = settings.enrich;
5445
+ this.sinks = new SinkRegistry(parentSinks);
5446
+ this.extensionStats = new ExtensionStatsRegistry(this.router.id, this.sinks);
5246
5447
  this.sample = {
5247
5448
  routerId: this.router.id,
5248
5449
  attachments: settings.attachments ?? {},
@@ -5311,8 +5512,19 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
5311
5512
  if (this.closed) return;
5312
5513
  this.closed = true;
5313
5514
  if (this.sample.closedAt === void 0) this.sample.closedAt = Date.now();
5515
+ this.extensionStats.close();
5516
+ this.sinks.clear();
5314
5517
  this.emit("close");
5315
5518
  }
5519
+ createExtensionStats(settings) {
5520
+ return this.extensionStats.create(settings);
5521
+ }
5522
+ acceptExtensionStats(id, data) {
5523
+ this.extensionStats.accept(id, data);
5524
+ }
5525
+ getExtensionStats(id) {
5526
+ return this.extensionStats.get(id);
5527
+ }
5316
5528
  addTransport = (transport) => {
5317
5529
  let transportType = transport.type;
5318
5530
  if (!transportType) {
@@ -5321,7 +5533,7 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
5321
5533
  else if (transport.constructor.name === "PipeTransport") transportType = "pipe";
5322
5534
  else if (transport.constructor.name === "DirectTransport") transportType = "direct";
5323
5535
  }
5324
- if (!transportType) return logger6.warn(`Unknown transport type for transport ${transport.id}: ${transport.constructor.name}`);
5536
+ if (!transportType) return logger8.warn(`Unknown transport type for transport ${transport.id}: ${transport.constructor.name}`);
5325
5537
  switch (transportType) {
5326
5538
  case "webrtc":
5327
5539
  return this.addWebRtcTransport(transport);
@@ -5332,7 +5544,7 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
5332
5544
  case "direct":
5333
5545
  return this.addDirectTransport(transport);
5334
5546
  default:
5335
- return logger6.warn(`Unsupported transport type for transport ${transport.id}: ${transportType}`);
5547
+ return logger8.warn(`Unsupported transport type for transport ${transport.id}: ${transportType}`);
5336
5548
  }
5337
5549
  };
5338
5550
  addWebRtcTransport(transport) {
@@ -5574,7 +5786,7 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
5574
5786
  const attachments = enrich();
5575
5787
  if (attachments) sample.attachments = { ...sample.attachments, ...attachments };
5576
5788
  } catch (err) {
5577
- logger6.warn("Mediasoup sample enricher threw for router %s: %o", this.id, err);
5789
+ logger8.warn("Mediasoup sample enricher threw for router %s: %o", this.id, err);
5578
5790
  }
5579
5791
  }
5580
5792
  _attachTransportObserverListeners(transport, transportSample, onClose) {
@@ -5599,7 +5811,7 @@ var ObservedMediasoupRouter = class extends EventEmitter5 {
5599
5811
  };
5600
5812
 
5601
5813
  // src/issues/ActiveIssuesRegistry.ts
5602
- var logger7 = createLogger("ActiveIssuesRegistry");
5814
+ var logger9 = createLogger("ActiveIssuesRegistry");
5603
5815
  var ActiveIssuesRegistry = class {
5604
5816
  constructor(parent) {
5605
5817
  this.parent = parent;
@@ -5685,7 +5897,7 @@ var ActiveIssuesRegistry = class {
5685
5897
  try {
5686
5898
  apply(target);
5687
5899
  } catch (err) {
5688
- logger7.warn("An issue tracker threw while being notified; continuing with the rest. %o", err);
5900
+ logger9.warn("An issue tracker threw while being notified; continuing with the rest. %o", err);
5689
5901
  }
5690
5902
  }
5691
5903
  };
@@ -5714,7 +5926,7 @@ function createCallSummary(callId, config) {
5714
5926
  }
5715
5927
 
5716
5928
  // src/summaries/CallSummaryCollector.ts
5717
- var logger8 = createLogger("CallSummaryCollector");
5929
+ var logger10 = createLogger("CallSummaryCollector");
5718
5930
  var CallSummaryCollector = class {
5719
5931
  constructor(_observer, _config) {
5720
5932
  this._observer = _observer;
@@ -5789,7 +6001,7 @@ var CallSummaryCollector = class {
5789
6001
  try {
5790
6002
  handler(summary, scratch, ...args);
5791
6003
  } catch (err) {
5792
- logger8.warn("A call-summary handler for %s threw; continuing. %o", event, err);
6004
+ logger10.warn("A call-summary handler for %s threw; continuing. %o", event, err);
5793
6005
  }
5794
6006
  };
5795
6007
  this._observer.on(event, listener);
@@ -6214,7 +6426,7 @@ function geohash(location, precision = 3) {
6214
6426
  }
6215
6427
 
6216
6428
  // src/detectors/ClientPopulationIssueDetector.ts
6217
- var logger9 = createLogger("ClientPopulationIssueDetector");
6429
+ var logger11 = createLogger("ClientPopulationIssueDetector");
6218
6430
  var ClientPopulationIssueTypes = {
6219
6431
  /** One issue type is concentrated on one client population while the rest of the fleet is fine. */
6220
6432
  clientPopulationIssue: "CLIENT_POPULATION_ISSUE"
@@ -6236,7 +6448,7 @@ var ClientPopulationIssueDetector = class _ClientPopulationIssueDetector {
6236
6448
  ...config
6237
6449
  };
6238
6450
  if (this._config.groupBy === "location" && !this._config.resolveClientLocation) {
6239
- logger9.warn(
6451
+ logger11.warn(
6240
6452
  "%s is configured with groupBy: 'location' but no resolveClientLocation, so no client can be placed and nothing will ever be detected",
6241
6453
  this.name
6242
6454
  );
@@ -7070,11 +7282,13 @@ function mediaKindOf(mimeType) {
7070
7282
  }
7071
7283
 
7072
7284
  // src/Observer.ts
7073
- var logger10 = createLogger("Observer");
7074
- var Observer = class extends EventEmitter6 {
7285
+ var logger12 = createLogger("Observer");
7286
+ var Observer = class extends EventEmitter7 {
7075
7287
  observedTURN = new ObservedTURN();
7076
7288
  observedCalls = /* @__PURE__ */ new Map();
7077
7289
  observedMediasoupRouters = /* @__PURE__ */ new Map();
7290
+ sinks = new SinkRegistry();
7291
+ extensionStats = new ExtensionStatsRegistry("observer", this.sinks);
7078
7292
  /** Ancestry base shared by all Observer-bus events originating at the observer. */
7079
7293
  eventScope = { observer: this };
7080
7294
  config;
@@ -7189,7 +7403,7 @@ var Observer = class extends EventEmitter6 {
7189
7403
  break;
7190
7404
  }
7191
7405
  default: {
7192
- logger10.warn("Unknown detector name %s; skipping", name);
7406
+ logger12.warn("Unknown detector name %s; skipping", name);
7193
7407
  return this;
7194
7408
  }
7195
7409
  }
@@ -7274,7 +7488,7 @@ var Observer = class extends EventEmitter6 {
7274
7488
  break;
7275
7489
  }
7276
7490
  if (!validator) {
7277
- logger10.warn("Unknown validator name %s; skipping", name);
7491
+ logger12.warn("Unknown validator name %s; skipping", name);
7278
7492
  return this;
7279
7493
  }
7280
7494
  this.validators.add(validator);
@@ -7305,7 +7519,7 @@ var Observer = class extends EventEmitter6 {
7305
7519
  try {
7306
7520
  validator.cancel(reason);
7307
7521
  } catch (err) {
7308
- logger10.warn("Error cancelling validator %s: %o", validator.name, err);
7522
+ logger12.warn("Error cancelling validator %s: %o", validator.name, err);
7309
7523
  }
7310
7524
  }
7311
7525
  return matching.length;
@@ -7321,11 +7535,11 @@ var Observer = class extends EventEmitter6 {
7321
7535
  */
7322
7536
  createObservedCall(settings, acceptCtx) {
7323
7537
  if (this.closed) {
7324
- logger10.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
7538
+ logger12.warn("Attempted to create a call (callId: %s) on a closed observer", settings.callId);
7325
7539
  return void 0;
7326
7540
  }
7327
7541
  if (this.observedCalls.has(settings.callId)) {
7328
- logger10.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
7542
+ logger12.warn("Observed Call with id %s already exists; returning the existing instance", settings.callId);
7329
7543
  return this.observedCalls.get(settings.callId);
7330
7544
  }
7331
7545
  const callSettings = {
@@ -7363,14 +7577,14 @@ var Observer = class extends EventEmitter6 {
7363
7577
  }
7364
7578
  createObservedMediasoupRouter(settings) {
7365
7579
  if (this.closed) {
7366
- logger10.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
7580
+ logger12.warn("Attempted to create mediasoup router (id: %d) on a closed observer", settings.router.id);
7367
7581
  return void 0;
7368
7582
  }
7369
7583
  if (this.observedMediasoupRouters.has(settings.router.id)) {
7370
- logger10.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
7584
+ logger12.warn("Observed Mediasoup Router (id %s) already exists; returning the existing instance", settings.router.id);
7371
7585
  return this.observedMediasoupRouters.get(settings.router.id);
7372
7586
  }
7373
- const observedMediasoupRouter = new ObservedMediasoupRouter(settings);
7587
+ const observedMediasoupRouter = new ObservedMediasoupRouter(settings, this.sinks);
7374
7588
  const observedMediasoupRouterScope = {
7375
7589
  observedMediasoupRouter,
7376
7590
  observer: this
@@ -7398,7 +7612,7 @@ var Observer = class extends EventEmitter6 {
7398
7612
  }
7399
7613
  close() {
7400
7614
  if (this.closed) {
7401
- return logger10.debug("Attempted to close twice");
7615
+ return logger12.debug("Attempted to close twice");
7402
7616
  }
7403
7617
  this.closed = true;
7404
7618
  for (const call of [...this.observedCalls.values()]) call.close();
@@ -7407,8 +7621,19 @@ var Observer = class extends EventEmitter6 {
7407
7621
  this.validators.clear();
7408
7622
  this.callSummaryCollector?.close();
7409
7623
  this.activeIssuesRegistry.clear();
7624
+ this.extensionStats.close();
7625
+ this.sinks.clear();
7410
7626
  this._notify("observer-closed", { ...this.eventScope });
7411
7627
  }
7628
+ createExtensionStats(settings) {
7629
+ return this.extensionStats.create(settings);
7630
+ }
7631
+ acceptExtensionStats(id, data) {
7632
+ this.extensionStats.accept(id, data);
7633
+ }
7634
+ getExtensionStats(id) {
7635
+ return this.extensionStats.get(id);
7636
+ }
7412
7637
  accept(sample, context) {
7413
7638
  if (this.closed) {
7414
7639
  return this._notify("sample-rejected", { ...this.eventScope, reason: "observer-closed", sample });
@@ -7416,7 +7641,7 @@ var Observer = class extends EventEmitter6 {
7416
7641
  try {
7417
7642
  this.acceptMiddlewares.process({ sample, context });
7418
7643
  } catch (err) {
7419
- logger10.warn("An accept middleware threw; dropping the sample. %o", err);
7644
+ logger12.warn("An accept middleware threw; dropping the sample. %o", err);
7420
7645
  }
7421
7646
  if (!sample.callId) {
7422
7647
  this._notify("sample-rejected", { ...this.eventScope, reason: "missing-callId", sample });
@@ -7456,7 +7681,7 @@ var Observer = class extends EventEmitter6 {
7456
7681
  try {
7457
7682
  validator.update();
7458
7683
  } catch (err) {
7459
- logger10.warn("Error running validator %s: %o", validator.name, err);
7684
+ logger12.warn("Error running validator %s: %o", validator.name, err);
7460
7685
  }
7461
7686
  }
7462
7687
  this._notify("observer-updated", { ...this.eventScope });
@@ -7692,67 +7917,34 @@ var TrendTester = class {
7692
7917
  }
7693
7918
  };
7694
7919
 
7695
- // src/sinks/ClientSampleSink.ts
7696
- import { EventEmitter as EventEmitter7 } from "events";
7697
- var ClientSampleSink = class extends EventEmitter7 {
7698
- };
7699
-
7700
- // src/sinks/JsonlFileSink.ts
7701
- import * as fs from "fs";
7702
- import * as path from "path";
7703
- var JsonlFileSink = class extends ClientSampleSink {
7704
- /** The file this sink writes to. Read it (e.g. in a `close` handler) to upload/move the file. */
7705
- path;
7706
- _stream;
7707
- _serializeSample;
7708
- constructor(options) {
7709
- super();
7710
- this.path = options.path;
7711
- this._stream = fs.createWriteStream(this.path, { flags: "a" });
7712
- this._stream.on("close", () => this.emit("close"));
7713
- this._stream.on("finish", () => this.emit("finish"));
7714
- this._stream.on("drain", () => this.emit("drain"));
7715
- this._stream.on("error", (err) => this.emit("error", err));
7716
- this._serializeSample = options.serializeSample ?? ((sample) => JSON.stringify(sample));
7717
- }
7718
- write(sample) {
7719
- return this._stream.write(`${this._serializeSample(sample)}
7720
- `);
7721
- }
7722
- end() {
7723
- this._stream.end();
7724
- }
7725
- };
7726
- function createJsonlFileSink(options) {
7727
- return new JsonlFileSink(options);
7728
- }
7729
- function createJsonlFileSinkFactory(options) {
7730
- return ({ clientId, observedCall }) => {
7731
- const filename = options.getFileName?.({ callId: observedCall.callId, clientId }) ?? `${observedCall.callId}__${clientId}.jsonl`;
7732
- return createJsonlFileSink({
7733
- path: path.join(options.directory, filename),
7734
- serializeSample: options.serializeSample
7735
- });
7736
- };
7737
- }
7738
-
7739
7920
  // src/sinks/InMemorySink.ts
7740
- var InMemorySink = class extends ClientSampleSink {
7741
- constructor(samples = []) {
7921
+ import { EventEmitter as EventEmitter8 } from "events";
7922
+ var InMemorySink = class extends EventEmitter8 {
7923
+ id;
7924
+ samples;
7925
+ _ended = false;
7926
+ constructor(settings = {}) {
7742
7927
  super();
7743
- this.samples = samples;
7928
+ this.id = settings.id ?? "in-memory";
7929
+ this.samples = settings.samples ?? [];
7930
+ }
7931
+ get ended() {
7932
+ return this._ended;
7744
7933
  }
7745
- samples;
7746
7934
  write(sample) {
7935
+ if (this._ended) return false;
7747
7936
  this.samples.push(sample);
7748
7937
  return true;
7749
7938
  }
7750
7939
  end() {
7940
+ if (this._ended) return;
7941
+ this._ended = true;
7942
+ this.emit("ended");
7751
7943
  this.emit("close");
7752
7944
  }
7753
7945
  };
7754
7946
  function createInMemorySink(samples) {
7755
- return new InMemorySink(samples);
7947
+ return new InMemorySink({ samples });
7756
7948
  }
7757
7949
 
7758
7950
  // src/resolvers/RemoteTrackResolver.ts
@@ -7940,9 +8132,9 @@ export {
7940
8132
  ClientMetaTypes,
7941
8133
  ClientPopulationIssueDetector,
7942
8134
  ClientPopulationIssueTypes,
7943
- ClientSampleSink,
7944
8135
  CodecConsistencyValidator,
7945
8136
  Detectors,
8137
+ ExtensionStatsRegistry,
7946
8138
  GEOHASH_CELL_SIZES,
7947
8139
  InMemorySink,
7948
8140
  IssueFanOutDetector,
@@ -7986,6 +8178,7 @@ export {
7986
8178
  SCORE_HISTOGRAM_NUMBER_OF_BUCKETS,
7987
8179
  SfuCongestionDetector,
7988
8180
  SimulcastReceiverValidator,
8181
+ SinkRegistry,
7989
8182
  SlidingWindow,
7990
8183
  TrackDeliveryMismatchDetector,
7991
8184
  TrackDeliveryMismatchTypes,
@@ -8006,7 +8199,6 @@ export {
8006
8199
  createDefaultMediasoupRemoteTrackResolverFactory,
8007
8200
  createInMemorySink,
8008
8201
  createJsonlFileSink,
8009
- createJsonlFileSinkFactory,
8010
8202
  createLogger,
8011
8203
  createP2pRemoteTrackResolverFactory,
8012
8204
  defaultCallSummaryConfig,