collabSupport/services/callReport/joinCallQuality.js
jmcqueen 90a56c4640 Fix Prisma overlay tunnel discovery with site-scoped query_params.
Use topology/links and anynetlinks filters with eq/in operators so
store WAN follow-ups return real peer tunnels instead of unscoped dumps.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 09:01:40 -04:00

152 lines
5 KiB
JavaScript

// services/callReport/joinCallQuality.js
// Join grouped CDR calls with Prisma WAN buckets.
import { isWithinWindow } from './businessWindow.js';
import { cdrStartTime } from '../cdrFeedParser.js';
import {
filterAbnormalCalls,
groupCallsByBucket,
groupCdrIntoCalls,
summarizeCalls,
} from './groupCdrCalls.js';
import { filterCallsByTarget, rebucketCalls } from './filterCallsByTarget.js';
import { annotateWanWithPath } from './voicePathAttribution.js';
const DEFAULT_INTERVAL_MS = 5 * 60 * 1000;
function shouldAttachWan(call) {
if (call.direction === 'inbound') return Boolean(call.reachedPhone);
if (call.direction === 'outbound') return Boolean(call.connected || call.reachedPhone);
return false;
}
function intervalMsFromSeries(summary) {
if (summary?.interval === '5min') return DEFAULT_INTERVAL_MS;
return DEFAULT_INTERVAL_MS;
}
function valueAtTime(series, time) {
if (!series || time == null) return null;
if (series.timedPoints?.length) {
const hit = series.timedPoints.find((p) => p.time === time);
return hit?.value ?? null;
}
return null;
}
function overlappingBucketTimes(series, startMs, endMs, intervalMs, winStartMs) {
const times = new Set();
if (series?.timedPoints?.length) {
for (const p of series.timedPoints) {
if (p.value == null || !p.time) continue;
const t = new Date(p.time).getTime();
if (!Number.isFinite(t)) continue;
if (t < endMs && (t + intervalMs) > startMs) times.add(p.time);
}
return times;
}
// Fallback for tests / legacy summaries without timedPoints.
const mosVals = series?.values || [];
if (!mosVals.length || !Number.isFinite(winStartMs)) return times;
const idx0 = Math.max(0, Math.floor((startMs - winStartMs) / intervalMs));
const idx1 = Math.max(idx0, Math.ceil((endMs - winStartMs) / intervalMs));
for (let i = idx0; i <= idx1 && i < mosVals.length; i++) {
if (mosVals[i] != null) times.add(`__idx:${i}`);
}
return times;
}
function worstFromTimedOverlap(appAudio, startMs, endMs, winStartMs) {
const mosSeries = appAudio?.mos;
if (!mosSeries) return null;
const intervalMs = intervalMsFromSeries(mosSeries);
const bucketTimes = overlappingBucketTimes(mosSeries, startMs, endMs, intervalMs, winStartMs);
if (!bucketTimes.size) return null;
let worstMos = null;
let worstLoss = null;
let worstJitter = null;
for (const time of bucketTimes) {
let m;
let l;
let j;
if (time.startsWith('__idx:')) {
const i = Number(time.slice(6));
m = mosSeries.values?.[i];
l = appAudio.loss?.values?.[i];
j = appAudio.jitter?.values?.[i];
} else {
m = valueAtTime(mosSeries, time);
l = valueAtTime(appAudio.loss, time);
j = valueAtTime(appAudio.jitter, time);
}
if (typeof m === 'number' && (worstMos == null || m < worstMos)) worstMos = m;
if (typeof l === 'number' && (worstLoss == null || l > worstLoss)) worstLoss = l;
if (typeof j === 'number' && (worstJitter == null || j > worstJitter)) worstJitter = j;
}
if (worstMos == null && worstLoss == null && worstJitter == null) return null;
return {
siteLevelApprox: true,
mos: worstMos,
loss: worstLoss,
jitter: worstJitter,
};
}
/**
* Find worst Prisma 5-min bucket overlapping [start, end], then
* annotate with likely path_type / pathId when available.
*/
export function worstPrismaBucketForCall(call, appAudio, window) {
if (!appAudio?.mos?.values?.length && !appAudio?.mos?.timedPoints?.length) {
// Still allow path_type-only annotation when MOS series is empty
// but byPathType has data (AppPerf* without MOS).
if (!appAudio?.byPathType && !appAudio?.byPath) return null;
const empty = { siteLevelApprox: true, mos: null, loss: null, jitter: null };
return annotateWanWithPath(empty, appAudio);
}
const startMs = call.start ? new Date(call.start).getTime() : NaN;
const endMs = Number.isFinite(startMs)
? startMs + (call.duration || 0) * 1000
: NaN;
if (!Number.isFinite(startMs)) return null;
const winStartMs = window?.startTime ? new Date(window.startTime).getTime() : startMs;
const bucket = worstFromTimedOverlap(appAudio, startMs, endMs, winStartMs);
return annotateWanWithPath(bucket, appAudio);
}
export function joinCallQuality({ cdrItems, appAudio, window, filter = null }) {
const calls = groupCdrIntoCalls(cdrItems);
const joined = calls.map((call) => {
const wan = shouldAttachWan(call)
? worstPrismaBucketForCall(call, appAudio, window)
: null;
return { ...call, wan };
});
const scoped = filter ? filterCallsByTarget(joined, filter) : joined;
if (filter) {
return rebucketCalls(scoped);
}
return {
calls: joined,
legs: joined,
buckets: groupCallsByBucket(joined),
abnormalCalls: filterAbnormalCalls(joined),
summary: summarizeCalls(joined),
};
}
export function filterCdrToWindow(items, window) {
return (items || []).filter((item) => {
const start = cdrStartTime(item);
if (!start) return true;
return isWithinWindow(start, window);
});
}