@absolutejs/voice 0.0.22-beta.284 → 0.0.22-beta.286
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.js
CHANGED
|
@@ -10798,6 +10798,18 @@ var findDuplicateTurnIds = (snapshots) => {
|
|
|
10798
10798
|
}
|
|
10799
10799
|
return [...duplicates].sort();
|
|
10800
10800
|
};
|
|
10801
|
+
var percentile = (values, rank) => {
|
|
10802
|
+
if (values.length === 0) {
|
|
10803
|
+
return;
|
|
10804
|
+
}
|
|
10805
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
10806
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(rank / 100 * sorted.length) - 1));
|
|
10807
|
+
return sorted[index];
|
|
10808
|
+
};
|
|
10809
|
+
var getResumeLatencies = (snapshots) => snapshots.filter((snapshot) => snapshot.reconnect.status === "resumed" && typeof snapshot.reconnect.lastResumedAt === "number").map((snapshot) => {
|
|
10810
|
+
const previousReconnect = snapshots.filter((candidate) => candidate.at <= snapshot.at && candidate.reconnect.status === "reconnecting" && typeof candidate.reconnect.lastDisconnectAt === "number").at(-1);
|
|
10811
|
+
return previousReconnect?.reconnect.lastDisconnectAt === undefined ? undefined : snapshot.reconnect.lastResumedAt - previousReconnect.reconnect.lastDisconnectAt;
|
|
10812
|
+
}).filter((value) => typeof value === "number" && value >= 0);
|
|
10801
10813
|
var runVoiceReconnectContract = (options) => {
|
|
10802
10814
|
const snapshots = [...options.snapshots].sort((left, right) => left.at - right.at);
|
|
10803
10815
|
const issues = [];
|
|
@@ -10808,6 +10820,7 @@ var runVoiceReconnectContract = (options) => {
|
|
|
10808
10820
|
const resumed = statuses.includes("resumed");
|
|
10809
10821
|
const exhausted = statuses.includes("exhausted");
|
|
10810
10822
|
const duplicateTurnIds = findDuplicateTurnIds(snapshots);
|
|
10823
|
+
const resumeLatencyP95Ms = percentile(getResumeLatencies(snapshots), 95);
|
|
10811
10824
|
const requireReconnect = options.requireReconnect ?? true;
|
|
10812
10825
|
const requireResume = options.requireResume ?? true;
|
|
10813
10826
|
const requireReplayProtection = options.requireReplayProtection ?? true;
|
|
@@ -10861,6 +10874,7 @@ var runVoiceReconnectContract = (options) => {
|
|
|
10861
10874
|
checkedAt: Date.now(),
|
|
10862
10875
|
issues,
|
|
10863
10876
|
pass,
|
|
10877
|
+
resumeLatencyP95Ms,
|
|
10864
10878
|
snapshotCount: snapshots.length,
|
|
10865
10879
|
statuses,
|
|
10866
10880
|
summary: {
|
|
@@ -12628,7 +12642,7 @@ var DEFAULT_WARN_RATIO = 0.8;
|
|
|
12628
12642
|
var DEFAULT_MIN_PASSING_RUNS = 3;
|
|
12629
12643
|
var roundMs = (value) => Math.max(1, Math.ceil(value));
|
|
12630
12644
|
var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
12631
|
-
var
|
|
12645
|
+
var percentile2 = (values, rank) => {
|
|
12632
12646
|
if (values.length === 0) {
|
|
12633
12647
|
return;
|
|
12634
12648
|
}
|
|
@@ -12651,7 +12665,7 @@ var normalizeSample = (input) => {
|
|
|
12651
12665
|
return input;
|
|
12652
12666
|
};
|
|
12653
12667
|
var createThreshold = (metric, values, options) => {
|
|
12654
|
-
const baselineP95Ms =
|
|
12668
|
+
const baselineP95Ms = percentile2(values, 95);
|
|
12655
12669
|
const maxObservedMs = values.length > 0 ? Math.max(...values) : undefined;
|
|
12656
12670
|
const recommendedMs = baselineP95Ms === undefined ? undefined : roundMs(Math.max(options.minimumMs, baselineP95Ms * options.headroomMultiplier));
|
|
12657
12671
|
const warnAfterMs = recommendedMs === undefined ? undefined : roundMs(Math.max(options.minimumMs, recommendedMs * options.warnRatio));
|
|
@@ -17030,7 +17044,7 @@ var createVoiceTurnLatencyRoutes = (options) => {
|
|
|
17030
17044
|
// src/liveLatency.ts
|
|
17031
17045
|
import { Elysia as Elysia27 } from "elysia";
|
|
17032
17046
|
var escapeHtml26 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
17033
|
-
var
|
|
17047
|
+
var percentile3 = (values, percentileValue) => {
|
|
17034
17048
|
if (values.length === 0) {
|
|
17035
17049
|
return;
|
|
17036
17050
|
}
|
|
@@ -17066,8 +17080,8 @@ var summarizeVoiceLiveLatency = async (options) => {
|
|
|
17066
17080
|
averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
|
|
17067
17081
|
checkedAt: Date.now(),
|
|
17068
17082
|
failed,
|
|
17069
|
-
p50LatencyMs:
|
|
17070
|
-
p95LatencyMs:
|
|
17083
|
+
p50LatencyMs: percentile3(latencies, 50),
|
|
17084
|
+
p95LatencyMs: percentile3(latencies, 95),
|
|
17071
17085
|
recent,
|
|
17072
17086
|
status: latencies.length === 0 ? "empty" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
|
|
17073
17087
|
total: latencies.length,
|
|
@@ -17143,7 +17157,7 @@ var TRACE_TYPES = [
|
|
|
17143
17157
|
];
|
|
17144
17158
|
var getNumber6 = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
17145
17159
|
var getString12 = (value) => typeof value === "string" && value.trim() ? value : undefined;
|
|
17146
|
-
var
|
|
17160
|
+
var percentile4 = (values, percentileValue) => {
|
|
17147
17161
|
if (values.length === 0) {
|
|
17148
17162
|
return;
|
|
17149
17163
|
}
|
|
@@ -17351,8 +17365,8 @@ var summarizeStage = (stage, measurements, options) => {
|
|
|
17351
17365
|
label: STAGE_LABELS[stage],
|
|
17352
17366
|
maxMs: latencies.length > 0 ? Math.max(...latencies) : undefined,
|
|
17353
17367
|
measurements: stageMeasurements,
|
|
17354
|
-
p50Ms:
|
|
17355
|
-
p95Ms:
|
|
17368
|
+
p50Ms: percentile4(latencies, 50),
|
|
17369
|
+
p95Ms: percentile4(latencies, 95),
|
|
17356
17370
|
stage,
|
|
17357
17371
|
status: stageMeasurements.length === 0 ? "empty" : failed > 0 ? "fail" : warnings > 0 ? "warn" : "pass",
|
|
17358
17372
|
total: stageMeasurements.length,
|
|
@@ -23790,7 +23804,7 @@ var rate3 = (count, total) => count / Math.max(1, total);
|
|
|
23790
23804
|
var uniqueSorted5 = (values) => [
|
|
23791
23805
|
...new Set(values.filter((value) => typeof value === "string"))
|
|
23792
23806
|
].sort();
|
|
23793
|
-
var
|
|
23807
|
+
var percentile5 = (values, rank) => {
|
|
23794
23808
|
if (values.length === 0) {
|
|
23795
23809
|
return 0;
|
|
23796
23810
|
}
|
|
@@ -23848,7 +23862,7 @@ var summarizeKind = (kind, events, thresholds, required) => {
|
|
|
23848
23862
|
unit: "rate"
|
|
23849
23863
|
}),
|
|
23850
23864
|
p95ElapsedMs: createMetric2({
|
|
23851
|
-
actual:
|
|
23865
|
+
actual: percentile5(latencies, 95),
|
|
23852
23866
|
label: "P95 latency",
|
|
23853
23867
|
threshold: thresholds.maxP95ElapsedMs,
|
|
23854
23868
|
unit: "ms"
|
|
@@ -27857,6 +27871,7 @@ var voiceOperationsRecordHref = (base, sessionId) => {
|
|
|
27857
27871
|
};
|
|
27858
27872
|
var buildOperationsRecordLinks = (input) => {
|
|
27859
27873
|
const failedSessionSet = new Set(input.failedSessionIds);
|
|
27874
|
+
const minLiveLatencyAt = typeof input.liveLatencyMaxAgeMs === "number" && Number.isFinite(input.liveLatencyMaxAgeMs) && input.liveLatencyMaxAgeMs > 0 ? Date.now() - input.liveLatencyMaxAgeMs : undefined;
|
|
27860
27875
|
const providerErrors = input.events.filter((event) => event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")).map((event) => ({
|
|
27861
27876
|
detail: getString18(event.payload.error),
|
|
27862
27877
|
href: voiceOperationsRecordHref(input.base, event.sessionId),
|
|
@@ -27864,7 +27879,7 @@ var buildOperationsRecordLinks = (input) => {
|
|
|
27864
27879
|
sessionId: event.sessionId,
|
|
27865
27880
|
status: "fail"
|
|
27866
27881
|
}));
|
|
27867
|
-
const failingLatency = input.events.filter((event) => event.type === "client.live_latency").map((event) => ({
|
|
27882
|
+
const failingLatency = input.events.filter((event) => event.type === "client.live_latency" && (minLiveLatencyAt === undefined || event.at >= minLiveLatencyAt)).map((event) => ({
|
|
27868
27883
|
event,
|
|
27869
27884
|
latencyMs: getNumber10(event.payload.latencyMs) ?? getNumber10(event.payload.elapsedMs)
|
|
27870
27885
|
})).filter((entry) => entry.latencyMs !== undefined && entry.latencyMs > input.liveLatencyWarnAfterMs).map(({ event, latencyMs }) => ({
|
|
@@ -27973,6 +27988,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
27973
27988
|
events,
|
|
27974
27989
|
failedSessionIds: failedSessionItems.map((session) => session.sessionId),
|
|
27975
27990
|
liveLatencyFailAfterMs: options.liveLatencyFailAfterMs ?? 3200,
|
|
27991
|
+
liveLatencyMaxAgeMs: options.liveLatencyMaxAgeMs,
|
|
27976
27992
|
liveLatencyWarnAfterMs: options.liveLatencyWarnAfterMs ?? 1800
|
|
27977
27993
|
});
|
|
27978
27994
|
const checks = [
|
|
@@ -28164,15 +28180,17 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
28164
28180
|
} : undefined;
|
|
28165
28181
|
const monitoringSummary = monitoring ? {
|
|
28166
28182
|
criticalOpen: monitoring.summary.criticalOpen,
|
|
28183
|
+
elapsedMs: monitoring.elapsedMs,
|
|
28167
28184
|
open: monitoring.summary.open,
|
|
28168
|
-
status: monitoring.status,
|
|
28185
|
+
status: options.monitoringRunFailAfterMs !== undefined && monitoring.elapsedMs > options.monitoringRunFailAfterMs ? "fail" : monitoring.status,
|
|
28169
28186
|
total: monitoring.summary.total
|
|
28170
28187
|
} : undefined;
|
|
28171
28188
|
const monitoringNotifierDeliverySummary = monitoringNotifierDelivery ? {
|
|
28189
|
+
elapsedMs: monitoringNotifierDelivery.elapsedMs,
|
|
28172
28190
|
failed: monitoringNotifierDelivery.summary.failed,
|
|
28173
28191
|
notifiers: monitoringNotifierDelivery.summary.notifiers,
|
|
28174
28192
|
sent: monitoringNotifierDelivery.summary.sent,
|
|
28175
|
-
status: monitoringNotifierDelivery.status,
|
|
28193
|
+
status: options.monitoringNotifierDeliveryFailAfterMs !== undefined && monitoringNotifierDelivery.elapsedMs > options.monitoringNotifierDeliveryFailAfterMs ? "fail" : monitoringNotifierDelivery.status,
|
|
28176
28194
|
total: monitoringNotifierDelivery.summary.total
|
|
28177
28195
|
} : undefined;
|
|
28178
28196
|
const telephonyWebhookSecuritySummary = telephonyWebhookSecurity ? {
|
|
@@ -28182,12 +28200,17 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
28182
28200
|
status: telephonyWebhookSecurity.status,
|
|
28183
28201
|
warned: telephonyWebhookSecurity.summary.warned
|
|
28184
28202
|
} : undefined;
|
|
28185
|
-
const reconnectContractSummary = reconnectContracts ? {
|
|
28186
|
-
|
|
28187
|
-
|
|
28188
|
-
|
|
28189
|
-
|
|
28190
|
-
|
|
28203
|
+
const reconnectContractSummary = reconnectContracts ? (() => {
|
|
28204
|
+
const failedReports = reconnectContracts.filter((report) => !report.pass || options.reconnectResumeFailAfterMs !== undefined && report.resumeLatencyP95Ms !== undefined && report.resumeLatencyP95Ms > options.reconnectResumeFailAfterMs);
|
|
28205
|
+
const resumeLatencies = reconnectContracts.map((report) => report.resumeLatencyP95Ms).filter((value) => typeof value === "number");
|
|
28206
|
+
return {
|
|
28207
|
+
failed: failedReports.length,
|
|
28208
|
+
passed: reconnectContracts.length - failedReports.length,
|
|
28209
|
+
resumeLatencyP95Ms: resumeLatencies.length > 0 ? Math.max(...resumeLatencies) : undefined,
|
|
28210
|
+
status: failedReports.length > 0 ? "fail" : reconnectContracts.length === 0 ? "warn" : "pass",
|
|
28211
|
+
total: reconnectContracts.length
|
|
28212
|
+
};
|
|
28213
|
+
})() : undefined;
|
|
28191
28214
|
const bargeInSummary = bargeInReports ? {
|
|
28192
28215
|
failed: bargeInReports.reduce((total, report) => total + report.failed, 0),
|
|
28193
28216
|
passed: bargeInReports.reduce((total, report) => total + report.passed, 0),
|
|
@@ -28404,7 +28427,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
28404
28427
|
}
|
|
28405
28428
|
if (reconnectContractSummary) {
|
|
28406
28429
|
checks.push({
|
|
28407
|
-
detail: reconnectContractSummary.status === "pass" ? `${reconnectContractSummary.passed} reconnect contract(s) are passing.` : reconnectContractSummary.total === 0 ? "No reconnect contracts are configured." : `${reconnectContractSummary.failed} reconnect contract(s) failed.`,
|
|
28430
|
+
detail: reconnectContractSummary.status === "pass" ? `${reconnectContractSummary.passed} reconnect contract(s) are passing.` : reconnectContractSummary.total === 0 ? "No reconnect contracts are configured." : options.reconnectResumeFailAfterMs !== undefined && reconnectContractSummary.resumeLatencyP95Ms !== undefined && reconnectContractSummary.resumeLatencyP95Ms > options.reconnectResumeFailAfterMs ? `Reconnect resume p95 ${reconnectContractSummary.resumeLatencyP95Ms}ms exceeded ${options.reconnectResumeFailAfterMs}ms.` : `${reconnectContractSummary.failed} reconnect contract(s) failed.`,
|
|
28408
28431
|
href: options.links?.reconnectContracts ?? options.links?.sessions ?? "/sessions",
|
|
28409
28432
|
label: "Reconnect recovery contracts",
|
|
28410
28433
|
proofSource: proofSource("reconnectContracts", "reconnect"),
|
|
@@ -28573,7 +28596,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
28573
28596
|
}
|
|
28574
28597
|
if (monitoring && monitoringSummary) {
|
|
28575
28598
|
checks.push({
|
|
28576
|
-
detail: monitoringSummary.status === "pass" ? `${monitoringSummary.total} monitor(s) are passing with no open issues.` : `${monitoringSummary.open} monitor issue(s) open, ${monitoringSummary.criticalOpen} critical.`,
|
|
28599
|
+
detail: monitoringSummary.status === "pass" ? `${monitoringSummary.total} monitor(s) are passing with no open issues.` : options.monitoringRunFailAfterMs !== undefined && monitoringSummary.elapsedMs !== undefined && monitoringSummary.elapsedMs > options.monitoringRunFailAfterMs ? `Monitor run took ${monitoringSummary.elapsedMs}ms, above ${options.monitoringRunFailAfterMs}ms.` : `${monitoringSummary.open} monitor issue(s) open, ${monitoringSummary.criticalOpen} critical.`,
|
|
28577
28600
|
href: options.links?.monitoring ?? "/voice/monitors",
|
|
28578
28601
|
label: "Monitoring issues",
|
|
28579
28602
|
status: monitoringSummary.status,
|
|
@@ -28589,7 +28612,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
28589
28612
|
}
|
|
28590
28613
|
if (monitoringNotifierDelivery && monitoringNotifierDeliverySummary) {
|
|
28591
28614
|
checks.push({
|
|
28592
|
-
detail: monitoringNotifierDeliverySummary.status === "pass" ? `${monitoringNotifierDeliverySummary.sent} monitor notification(s) delivered.` : `${monitoringNotifierDeliverySummary.failed} monitor notification delivery failure(s).`,
|
|
28615
|
+
detail: monitoringNotifierDeliverySummary.status === "pass" ? `${monitoringNotifierDeliverySummary.sent} monitor notification(s) delivered.` : options.monitoringNotifierDeliveryFailAfterMs !== undefined && monitoringNotifierDeliverySummary.elapsedMs !== undefined && monitoringNotifierDeliverySummary.elapsedMs > options.monitoringNotifierDeliveryFailAfterMs ? `Monitor notification delivery took ${monitoringNotifierDeliverySummary.elapsedMs}ms, above ${options.monitoringNotifierDeliveryFailAfterMs}ms.` : `${monitoringNotifierDeliverySummary.failed} monitor notification delivery failure(s).`,
|
|
28593
28616
|
href: options.links?.monitoringNotifierDelivery ?? "/api/voice/monitor-issues/notifications",
|
|
28594
28617
|
label: "Monitor notifier delivery",
|
|
28595
28618
|
status: monitoringNotifierDeliverySummary.status,
|
|
@@ -28821,7 +28844,8 @@ var createVoiceMemoryMonitorNotifierDeliveryReceiptStore = (initial = []) => {
|
|
|
28821
28844
|
};
|
|
28822
28845
|
};
|
|
28823
28846
|
var buildVoiceMonitorRunReport = async (options) => {
|
|
28824
|
-
const
|
|
28847
|
+
const startedAt = Date.now();
|
|
28848
|
+
const checkedAt = options.now ?? startedAt;
|
|
28825
28849
|
const runs = await Promise.all(options.monitors.map(async (monitor) => {
|
|
28826
28850
|
const evaluation = await monitor.evaluate({
|
|
28827
28851
|
evidence: options.evidence,
|
|
@@ -28861,6 +28885,7 @@ var buildVoiceMonitorRunReport = async (options) => {
|
|
|
28861
28885
|
const criticalOpen = openIssues.filter((issue) => issue.severity === "critical").length;
|
|
28862
28886
|
return {
|
|
28863
28887
|
checkedAt,
|
|
28888
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
28864
28889
|
issues,
|
|
28865
28890
|
runs,
|
|
28866
28891
|
status: criticalOpen > 0 ? "fail" : openIssues.length > 0 || rollupStatus4(runs) === "warn" ? "warn" : rollupStatus4(runs),
|
|
@@ -28921,7 +28946,8 @@ var createVoiceMonitorWebhookNotifier = (options) => ({
|
|
|
28921
28946
|
}
|
|
28922
28947
|
});
|
|
28923
28948
|
var deliverVoiceMonitorIssueNotifications = async (options) => {
|
|
28924
|
-
const
|
|
28949
|
+
const startedAt = Date.now();
|
|
28950
|
+
const checkedAt = options.now ?? startedAt;
|
|
28925
28951
|
const statuses = new Set(options.statuses ?? ["open"]);
|
|
28926
28952
|
const issues = (await options.issueStore.list()).filter((issue) => statuses.has(issue.status));
|
|
28927
28953
|
const receipts = [];
|
|
@@ -28947,6 +28973,7 @@ var deliverVoiceMonitorIssueNotifications = async (options) => {
|
|
|
28947
28973
|
const skipped = allReceipts.filter((receipt) => receipt.status === "skipped").length;
|
|
28948
28974
|
return {
|
|
28949
28975
|
checkedAt,
|
|
28976
|
+
elapsedMs: Math.max(0, Date.now() - startedAt),
|
|
28950
28977
|
receipts: allReceipts,
|
|
28951
28978
|
status: failed > 0 ? "fail" : allReceipts.length === 0 ? "warn" : "pass",
|
|
28952
28979
|
summary: {
|
|
@@ -181,11 +181,13 @@ export type VoiceProductionReadinessReport = {
|
|
|
181
181
|
};
|
|
182
182
|
monitoring?: {
|
|
183
183
|
criticalOpen: number;
|
|
184
|
+
elapsedMs?: number;
|
|
184
185
|
open: number;
|
|
185
186
|
status: VoiceProductionReadinessStatus;
|
|
186
187
|
total: number;
|
|
187
188
|
};
|
|
188
189
|
monitoringNotifierDelivery?: {
|
|
190
|
+
elapsedMs?: number;
|
|
189
191
|
failed: number;
|
|
190
192
|
notifiers: number;
|
|
191
193
|
sent: number;
|
|
@@ -259,6 +261,7 @@ export type VoiceProductionReadinessReport = {
|
|
|
259
261
|
reconnectContracts?: {
|
|
260
262
|
failed: number;
|
|
261
263
|
passed: number;
|
|
264
|
+
resumeLatencyP95Ms?: number;
|
|
262
265
|
status: VoiceProductionReadinessStatus;
|
|
263
266
|
total: number;
|
|
264
267
|
};
|
|
@@ -482,6 +485,9 @@ export type VoiceProductionReadinessRoutesOptions = {
|
|
|
482
485
|
liveLatencyWarnAfterMs?: number;
|
|
483
486
|
liveLatencyFailAfterMs?: number;
|
|
484
487
|
liveLatencyMaxAgeMs?: number;
|
|
488
|
+
monitoringRunFailAfterMs?: number;
|
|
489
|
+
monitoringNotifierDeliveryFailAfterMs?: number;
|
|
490
|
+
reconnectResumeFailAfterMs?: number;
|
|
485
491
|
};
|
|
486
492
|
export declare const summarizeVoiceProductionReadinessGate: (report: VoiceProductionReadinessReport, options?: VoiceProductionReadinessGateOptions) => VoiceProductionReadinessGateReport;
|
|
487
493
|
export declare const evaluateVoiceProductionReadinessEvidence: (report: VoiceProductionReadinessReport, input?: VoiceProductionReadinessAssertionInput) => VoiceProductionReadinessAssertionReport;
|
|
@@ -83,6 +83,7 @@ export type VoiceMonitorNotifierDeliveryReceiptStore = {
|
|
|
83
83
|
};
|
|
84
84
|
export type VoiceMonitorNotifierDeliveryReport = {
|
|
85
85
|
checkedAt: number;
|
|
86
|
+
elapsedMs: number;
|
|
86
87
|
receipts: VoiceMonitorNotifierDeliveryReceipt[];
|
|
87
88
|
status: VoiceMonitorStatus;
|
|
88
89
|
summary: {
|
|
@@ -95,6 +96,7 @@ export type VoiceMonitorNotifierDeliveryReport = {
|
|
|
95
96
|
};
|
|
96
97
|
export type VoiceMonitorRunReport = {
|
|
97
98
|
checkedAt: number;
|
|
99
|
+
elapsedMs: number;
|
|
98
100
|
issues: VoiceMonitorIssue[];
|
|
99
101
|
runs: VoiceMonitorRun[];
|
|
100
102
|
status: VoiceMonitorStatus;
|