// 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); }); }