# Interface Migration Guide: Metrics V1 -> Telemetry V2

This guide focuses on interface migration for applications moving from metrics v1 to telemetry v2 in the `0.3.0-beta.x` line.

## Scope

This migration changes:

- `SofyaTranscriber` public metrics APIs
- `ITranscriptionService` metrics contracts
- public metrics events
- runtime telemetry payload shape
- debug audit/report schema version

This migration is breaking by design. Metrics v1 APIs are removed, not shimmed.

## Quick Checklist

1. Replace `getMetrics()` and grouped getters with `getTelemetrySnapshot()`.
2. Replace `metrics` event listeners with `telemetry`.
3. Add optional `telemetry_row` listener if you need export/timeline rows.
4. Migrate types from `TranscriptionMetricsSnapshot`-shaped reads to `TelemetrySnapshot`.
5. Update debug audit/report consumers to schema version `2`.

## Public API Mapping

| v1 | v2 |
| --- | --- |
| `getMetrics()` | `getTelemetrySnapshot()` |
| `getTranscriptUiMetrics()` | `getTelemetrySnapshot().counters/histograms/status` |
| `getSessionMetrics()` | `getTelemetrySnapshot().window/counters/histograms/gauges` |
| `getConnectionMetrics()` | `getTelemetrySnapshot().status/counters` |
| `getRecoveryMetrics()` | `getTelemetrySnapshot().status/counters/histograms` |
| `getBufferingMetrics()` | `getTelemetrySnapshot().status/counters/gauges` |
| `getBrowserNetworkMetrics()` | `getTelemetrySnapshot().status/counters/gauges` |
| `getAudioCaptureMetrics()` | `getTelemetrySnapshot().counters/gauges` |
| `on("metrics", ...)` | `on("telemetry", ...)` |
| none | `getTelemetryRows()` |
| none | `clearTelemetryRows()` |
| none | `resetTelemetry()` |
| none | `on("telemetry_row", ...)` |

## Event Migration

### Before (v1)

```typescript
transcriber.on("metrics", (metrics) => {
  if (metrics.connection.connectionState === "reconnecting") {
    // ...
  }
});
```

### After (v2)

```typescript
transcriber.on("telemetry", (snapshot) => {
  if (snapshot.status.connectionState === "reconnecting") {
    // ...
  }
});

transcriber.on("telemetry_row", (row) => {
  // Optional: export-ready row stream
  console.log(row.metric, row.value, row.tags);
});
```

## Method Migration

### Before (v1)

```typescript
const metrics = transcriber.getMetrics();
const recovery = transcriber.getRecoveryMetrics();
const buffering = transcriber.getBufferingMetrics();
```

### After (v2)

```typescript
const snapshot = transcriber.getTelemetrySnapshot();
if (!snapshot) return;

const reconnects = snapshot.counters["ws.reconnects"] ?? 0;
const avgRecoveryMs = snapshot.histograms["recovery.time_ms"]?.avg ?? null;
const pendingBufferedBytes = snapshot.status.pendingBufferedAudioBytes;
const rows = transcriber.getTelemetryRows();
```

## Field Mapping Reference (Common Reads)

### Connection / transport

| v1 read | v2 replacement |
| --- | --- |
| `metrics.connection.connectionState` | `snapshot.status.connectionState` |
| `metrics.connection.websocketState` | `snapshot.status.websocketState` |
| `metrics.connection.browserOnline` | `snapshot.status.browserOnline` |
| `metrics.connection.transportBufferedAmountBytes` | `snapshot.status.transportBufferedAmountBytes` |
| `metrics.connection.lastDisconnectCode` | `snapshot.status.lastDisconnectCode` |
| `metrics.connection.lastDisconnectReason` | `snapshot.status.lastDisconnectReason` |
| `metrics.connection.connectionOpenCount` | `snapshot.counters["ws.connect.success"]` |
| `metrics.connection.connectionCloseCount` | `snapshot.counters["ws.disconnects"]` |

### Recovery

| v1 read | v2 replacement |
| --- | --- |
| `metrics.recovery.reconnectCount` | `snapshot.counters["ws.reconnects"]` |
| `metrics.recovery.recoverySuccessCount` | `snapshot.counters["recovery.completed.count"]` |
| `metrics.recovery.terminalDisconnectCount` | `snapshot.counters["recovery.failed.count"]` |
| `metrics.recovery.currentReconnectAttempt` | `snapshot.status.reconnectAttempt` |
| `metrics.recovery.nextReconnectDelayMs` | `snapshot.status.nextReconnectDelayMs` |
| `metrics.recovery.remainingReconnectAttempts` | `snapshot.status.remainingReconnectAttempts` |
| `metrics.recovery.avgRecoveryDurationMs` | `snapshot.histograms["recovery.time_ms"]?.avg` |
| `metrics.recovery.p95RecoveryDurationMs` | `snapshot.histograms["recovery.time_ms"]?.p95` |

### Buffering

| v1 read | v2 replacement |
| --- | --- |
| `metrics.buffering.isBufferingAudio` | `snapshot.status.isBufferingAudio` |
| `metrics.buffering.isDrainingAudio` | `snapshot.status.isDrainingAudio` |
| `metrics.buffering.pendingBufferedAudioBytes` | `snapshot.status.pendingBufferedAudioBytes` |
| `metrics.buffering.persistedBufferedAudioBytes` | `snapshot.status.persistedBufferedAudioBytes` |
| `metrics.buffering.persistedBufferedAudioSegments` | `snapshot.status.persistedBufferedAudioSegments` |
| `metrics.buffering.totalBufferedAudioBytes` | `snapshot.status.totalBufferedAudioBytes` |
| `metrics.buffering.lastDrainProgressAt` | `snapshot.status.lastDrainProgressAt` |
| `metrics.buffering.drainCycles` | `snapshot.status.drainCycles` |
| `metrics.buffering.drainExitReason` | `snapshot.status.drainExitReason` |
| `metrics.buffering.finishDeliveryState` | `snapshot.status.finishDeliveryState` |
| `metrics.buffering.bufferStallCount` | `snapshot.counters["buffer.stall.count"]` |
| `metrics.buffering.bufferingDurationMs` | `snapshot.gauges["buffering.duration_ms"]` |
| `metrics.buffering.drainingDurationMs` | `snapshot.gauges["draining.duration_ms"]` |

### Transcript

| v1 read | v2 replacement |
| --- | --- |
| `metrics.transcriptUi.partialCount` | `snapshot.counters["tx.partial.received"]` |
| `metrics.transcriptUi.finalCount` | `snapshot.counters["tx.final.received"]` |
| `metrics.transcriptUi.diarizationCount` | `snapshot.counters["tx.diarization.received"]` |
| `metrics.transcriptUi.utteranceCount` | `snapshot.counters["tx.utterance.count"]` |
| `metrics.transcriptUi.avgFirstPartialLatencyMs` | `snapshot.histograms["tx.first_partial.time_ms"]?.avg` |
| `metrics.transcriptUi.p95FirstPartialLatencyMs` | `snapshot.histograms["tx.first_partial.time_ms"]?.p95` |
| `metrics.transcriptUi.avgFinalLatencyMs` | `snapshot.histograms["tx.first_final.time_ms"]?.avg` |
| `metrics.transcriptUi.p95FinalLatencyMs` | `snapshot.histograms["tx.first_final.time_ms"]?.p95` |
| `metrics.transcriptUi.avgStabilizationLatencyMs` | `snapshot.histograms["tx.stabilization.time_ms"]?.avg` |
| `metrics.transcriptUi.p95StabilizationLatencyMs` | `snapshot.histograms["tx.stabilization.time_ms"]?.p95` |

### Browser and audio

| v1 read | v2 replacement |
| --- | --- |
| `metrics.browserNetwork.effectiveType` | `snapshot.status.networkEffectiveType` |
| `metrics.browserNetwork.downlinkMbps` | `snapshot.gauges["network.downlink_mbps"]` |
| `metrics.browserNetwork.rttMs` | `snapshot.gauges["network.rtt_ms"]` |
| `metrics.browserNetwork.offlineTransitionCount` | `snapshot.counters["browser.offline.events"]` |
| `metrics.browserNetwork.onlineTransitionCount` | `snapshot.counters["browser.online.events"]` |
| `metrics.browserNetwork.offlineDurationMs` | `snapshot.gauges["browser.offline.duration_ms"]` |
| `metrics.browserNetwork.flapCount` | `snapshot.counters["browser.flap.count"]` |
| `metrics.audioCapture.clippingEventCount` | `snapshot.counters["audio.capture.clipping_events"]` |
| `metrics.audioCapture.silenceRatio` | `snapshot.gauges["audio.capture.silence_ratio"]` |
| `metrics.audioCapture.speechActivityRatio` | `snapshot.gauges["audio.capture.speech_activity_ratio"]` |
| `metrics.audioCapture.audioCallbackGapCount` | `snapshot.counters["audio.capture.gap.count"]` |
| `metrics.audioCapture.longestAudioCallbackGapMs` | `snapshot.gauges["audio.capture.gap.longest_ms"]` |

## Removed/Non-Equivalent Fields

These v1 fields had no durable runtime data source and are not first-class v2 fields:

- `currentPartialText`
- `lastFinalText`
- `connectedDurationMs`
- `reconnectingDurationMs`
- `disconnectedDurationMs`
- `avgBufferGrowthRateBytesPerSecond`
- `avgBufferDrainRateBytesPerSecond`

If your UI still needs these, compute them app-side from `telemetry` snapshots and `telemetry_row` stream.

## `ITranscriptionService` Contract Migration

Update adapter/service implementations from v1 metrics contract to v2 telemetry contract.

### New optional interface members

```typescript
getTelemetrySnapshot?(): TelemetrySnapshot | null;
getTelemetryRows?(): TelemetryRow[];
clearTelemetryRows?(): void;
resetTelemetry?(): void;
```

### Unsupported provider behavior

Providers that do not support runtime telemetry should return:

- a null-safe telemetry snapshot with `schemaVersion: 2`, empty counters/gauges/histograms, and unsupported capabilities
- an empty `getTelemetryRows()` array
- no-op `clearTelemetryRows()` and `resetTelemetry()`

## Debug Audit / Report Interface Changes

- `DebugAuditFile.schemaVersion` is now `2`
- `SessionReport.schemaVersion` is now `2`

If your audit consumers validated schema version `1`, update validators and parsers.

## Compatibility Adapter Pattern (Optional)

If you need phased migration, add an app-local adapter that converts `TelemetrySnapshot` to your old view-model. The SDK itself no longer exposes v1 APIs/events.

## Related Docs

- [UPGRADING_FROM_0.0.20.md](/Users/gabriel/Projects/lib.sofya.transcription/docs/UPGRADING_FROM_0.0.20.md)
- [METRICS_AND_REPORT_GUIDE.md](/Users/gabriel/Projects/lib.sofya.transcription/docs/METRICS_AND_REPORT_GUIDE.md)
