Configuration

Every option is optional. new ClientMonitor() with no arguments is valid and gives you sensible defaults with all detectors enabled.

The full object

import { ClientMonitor } from "@observertc/client-monitor-js";

const monitor = new ClientMonitor({
    // ── Identity ────────────────────────────────────────────────────────────
    clientId: "unique-client-id",
    callId: "unique-call-id",

    // ── Timing ──────────────────────────────────────────────────────────────
    collectingPeriodInMs: 2000,   // default: 2000 — how often getStats() runs
    samplingPeriodInMs: 4000,     // no default — omit to disable automatic sampling

    // ── Integration flags ───────────────────────────────────────────────────
    integrateNavigatorMediaDevices: true,  // default: true
    addClientJointEventOnCreated: true,    // default: true
    addClientLeftEventOnClose: true,       // default: true
    bufferingEventsForSamples: false,      // default: false — required for manual sampling

    // ── Detectors ───────────────────────────────────────────────────────────
    audioDesyncDetector: {
        fractionalCorrectionAlertOnThreshold: 0.1,
        fractionalCorrectionAlertOffThreshold: 0.05,
    },
    congestionDetector: {
        sensitivity: "medium",              // 'low' | 'medium' | 'high'
    },
    cpuPerformanceDetector: {
        incomingDecodedFramesRatioThresholds: {
            alertOn: 0.7,
            alertOff: 0.85,
            minReceivedFrames: 10,
        },
        durationOfCollectingStatsThreshold: {
            lowWatermark: 5000,
            highWatermark: 10000,
        },
    },
    dryInboundTrackDetector:  { thresholdInMs: 5000 },
    dryOutboundTrackDetector: { thresholdInMs: 5000 },
    videoFreezesDetector: {},
    playoutDiscrepancyDetector: {
        lowSkewThreshold: 2,
        highSkewThreshold: 5,
    },
    syntheticSamplesDetector: {
        minSynthesizedSamplesDuration: 1000,
    },
    longPcConnectionEstablishmentDetector: {
        thresholdInMs: 5000,
    },

    // ── Logging ─────────────────────────────────────────────────────────────
    logger: myLogger,

    // ── Application data (never shipped in samples) ──────────────────────────
    appData: { userId: "user-123", roomId: "room-456" },
});

Timing

OptionDefaultMeaning
collectingPeriodInMs2000How often getStats() is polled on every source. Drives all derived metrics, detectors and scores.
samplingPeriodInMs(none)How often a ClientSample is created and sample-created fires. Omit it to disable automatic sampling.

Choosing periods

collectingPeriodInMs controls resolution — how quickly a detector can notice something. samplingPeriodInMs controls bandwidth — how much telemetry you upload.

  • Interactive debugging: 1000 / 2000
  • Default production: 2000 / 4000
  • High-scale, cost-sensitive: 3000 / 10000

Sampling less often does not lose issues: events and issues are buffered between samples and all of them ship in the next one.

Both can be changed while running:

monitor.setCollectingPeriod(3000);
monitor.setSamplingPeriod(10000);

Integration flags

OptionDefaultEffect
integrateNavigatorMediaDevicestrueWatches navigator.mediaDevices and records device lists / changes as client metadata.
addClientJointEventOnCreatedtrueEmits a CLIENT_JOINED client event when the monitor is created. observer-js uses this to set joinedAt.
addClientLeftEventOnClosetrueEmits CLIENT_LEFT on close().
bufferingEventsForSamplesfalseBuffers events/issues even when automatic sampling is off. Required if you call createSample() manually.

Detector configuration: the three-state rule

Every detector slot in ClientMonitorConfig is typed Config | null, and the value you pass decides whether the detector exists at all:

You passResult
(key omitted) or undefinedDetector is constructed with its documented defaults
An objectDetector is constructed with your overrides merged in
nullDetector is not constructed at all — no instance, no per-tick work
const monitor = new ClientMonitor({
    congestionDetector: null,                    // never built
    videoFreezesDetector: {},                    // built with defaults
    dryInboundTrackDetector: { thresholdInMs: 10_000 },  // built with an override
    // cpuPerformanceDetector omitted             → built with defaults
});

Once constructed, a detector can also be silenced without being removed:

monitor.detectors.disable("cpu-performance-detector");
monitor.detectors.enable("cpu-performance-detector");
monitor.detectors.disableAll();

See Detectors for the registry API and every threshold’s meaning.

Removed in 4.3.0

createIssue?: boolean and disabled?: boolean were removed from every detector’s config block. Use null (do not construct) or the runtime detector.disabled flag instead.

Detector thresholds at a glance

Config keyDetectorKey thresholds
audioDesyncDetectorAudio desyncfractionalCorrectionAlertOnThreshold 0.1, fractionalCorrectionAlertOffThreshold 0.05
congestionDetectorCongestionsensitivity: 'low' | 'medium' | 'high'
cpuPerformanceDetectorCPU pressureincomingDecodedFramesRatioThresholds { alertOn: 0.7, alertOff: 0.85, minReceivedFrames: 10 }; durationOfCollectingStatsThreshold { lowWatermark: 5000, highWatermark: 10000 }
dryInboundTrackDetectorInbound track stalledthresholdInMs 5000
dryOutboundTrackDetectorOutbound track stalledthresholdInMs 5000
videoFreezesDetectorVideo freeze(no thresholds — driven by freezeCount)
playoutDiscrepancyDetectorFrames received but not renderedlowSkewThreshold 2, highSkewThreshold 5
syntheticSamplesDetectorAudio being synthesizedminSynthesizedSamplesDuration 1000
longPcConnectionEstablishmentDetectorSlow ICE/DTLS setupthresholdInMs 5000

Migrating from ≤ 4.3.1

cpuPerformanceDetector.fpsVolatilityThresholds was replaced by incomingDecodedFramesRatioThresholds in 4.3.2. Frame-rate volatility false-triggered on screen share, whose fps legitimately swings when the shared content goes static. If you still pass fpsVolatilityThresholds, update your config.

appData vs attachments

Two different bags, on every monitor object in the hierarchy, with opposite purposes:

appDataattachments
Included in ClientSampleNoYes
Reaches your backendNoYes
Costs bandwidth / storageNoYes
Intended forLocal UI state, feature flags, runtime routingSession identity, room context, A/B flags, custom metrics
// Local only.
trackMonitor.appData = { renderTargetId: "video-el-7", muteRequested: false };

// Shipped with every sample and readable server-side.
trackMonitor.attachments = { roomId: "room-456", role: "presenter", mediaType: "screen-share" };

Both exist on ClientMonitor, PeerConnectionMonitor, every track monitor, every RTP monitor and every connection monitor.

Minimal configurations

// Only what you need to correlate samples.
const monitor = new ClientMonitor({ clientId: "my-client", collectingPeriodInMs: 1000 });

// Everything default — useful for local debugging with no backend.
const monitor = new ClientMonitor();