fix: complete caller analytics reliability remediation
This commit is contained in:
@@ -1,36 +1,20 @@
|
||||
import { PrismaClient, Prisma, analyticsHash, analyticsRates, COUNT_KEYS, type AnalyticsCounts } from '@lisglosips/database';
|
||||
|
||||
export async function evaluateCallerAlerts(db: PrismaClient) {
|
||||
const end = new Date(Math.floor(Date.now() / 60000) * 60000); const start = new Date(+end - 15 * 60000);
|
||||
const aggregates = Prisma.join(COUNT_KEYS.map(k => Prisma.sql`SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(counts,${`$.${k}`})) AS SIGNED)) AS ${Prisma.raw(k)}`));
|
||||
const rows = await db.$queryRaw<Array<AnalyticsCounts & { customer_id: string; caller: string; view: string }>>(Prisma.sql`SELECT customer_id, caller, view, ${aggregates} FROM caller_analysis_minute_buckets WHERE started_at>=${start} AND started_at<${end} GROUP BY customer_id, caller, view HAVING SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(counts,'$.totalCalls')) AS SIGNED))>=50`);
|
||||
for (const row of rows) {
|
||||
const counts = Object.fromEntries(COUNT_KEYS.map(k => [k, Number(row[k])])) as unknown as AnalyticsCounts;
|
||||
const rates = analyticsRates(counts);
|
||||
if (rates.unknownCalls || rates.activeCalls / rates.totalCalls > 0.2) continue;
|
||||
const reasons = [
|
||||
rates.connectionRate !== null && rates.connectionRate < Number(process.env.ANALYTICS_LOW_CONNECTION_RATE ?? 20) ? '低接通率' : '',
|
||||
rates.overallAnswerRate !== null && rates.overallAnswerRate < Number(process.env.ANALYTICS_LOW_OVERALL_RATE ?? 10) ? '低总体应答率' : '',
|
||||
rates.connectedCalls >= 30 && rates.connectedAnswerRate !== null && rates.connectedAnswerRate < Number(process.env.ANALYTICS_LOW_CONNECTED_RATE ?? 20) ? '低已接通应答率' : ''
|
||||
].filter(Boolean);
|
||||
const previous = await db.$queryRaw<Array<AnalyticsCounts>>(Prisma.sql`SELECT ${aggregates} FROM caller_analysis_minute_buckets WHERE started_at>=${new Date(+start - 15 * 60000)} AND started_at<${start} AND customer_id=${row.customer_id} AND caller=${row.caller} AND view=${row.view}`);
|
||||
const p = analyticsRates(Object.fromEntries(COUNT_KEYS.map(k => [k, Number(previous[0]?.[k] ?? 0)])) as unknown as AnalyticsCounts);
|
||||
if (p.totalCalls >= 50 && !p.unknownCalls && p.activeCalls / p.totalCalls <= 0.2) {
|
||||
for (const [key, label] of [['connectionRate','接通率骤降'],['overallAnswerRate','总体应答率骤降'],['connectedAnswerRate','已接通应答率骤降']] as const) {
|
||||
if (key === 'connectedAnswerRate' && (p.connectedCalls < 30 || rates.connectedCalls < 30)) continue;
|
||||
if (p[key] !== null && rates[key] !== null && p[key]! - rates[key]! > Number(process.env.ANALYTICS_RATE_DROP_POINTS ?? 15)) reasons.push(label);
|
||||
}
|
||||
}
|
||||
const tail = await db.$queryRaw<Array<{ counts: AnalyticsCounts }>>`SELECT counts FROM caller_analysis_states WHERE customer_id=${row.customer_id} AND caller=${row.caller} AND view=${row.view} AND started_at>=${start} AND started_at<${end} ORDER BY started_at DESC, id DESC LIMIT 20`;
|
||||
if (tail.length === 20 && tail.every(t => t.counts.failedCalls === 1)) reasons.push('连续20次已结束未接通');
|
||||
const id = analyticsHash([row.customer_id, row.caller, row.view]);
|
||||
const existing = await db.$queryRaw<Array<{ status: string; payload: { streak?: number; recovered?: number; since?: string } }>>`SELECT status,payload FROM caller_analysis_alerts WHERE id=${id}`;
|
||||
const streak = reasons.length ? (existing[0]?.payload.streak ?? 0) + 1 : 0;
|
||||
const recovered = reasons.length ? 0 : (existing[0]?.payload.recovered ?? 0) + 1;
|
||||
const status = streak >= 3 ? 'ACTIVE' : existing[0]?.status === 'ACTIVE' && recovered < 3 ? 'ACTIVE' : 'OBSERVING';
|
||||
const payload = JSON.stringify({ reasons, rates, streak, recovered, windowStart: start, windowEnd: end, since: existing[0]?.payload.since ?? new Date().toISOString() });
|
||||
await db.$executeRaw`INSERT INTO caller_analysis_alerts(id,customer_id,view,caller,status,payload,updated_at) VALUES (${id},${row.customer_id},${row.view},${row.caller},${status},${payload},NOW(3)) ON DUPLICATE KEY UPDATE status=VALUES(status),payload=VALUES(payload),updated_at=VALUES(updated_at)`;
|
||||
}
|
||||
// Old low-sample windows are no longer active evidence, not permanent alarms.
|
||||
await db.$executeRaw`UPDATE caller_analysis_alerts SET status='EXPIRED' WHERE updated_at<${new Date(Date.now() - 120000)} AND status='ACTIVE'`;
|
||||
import { COUNT_KEYS, Prisma, PrismaClient, analyticsHash, analyticsRates, type AnalyticsCounts } from '@lisglosips/database';
|
||||
type Row={customer_id:string;caller:string;view:string}&AnalyticsCounts;
|
||||
type Prior={status:string;payload:{streak?:number;recovered?:number;since?:string;evaluationKey?:string}};
|
||||
const sums=()=>Prisma.join(COUNT_KEYS.map(k=>Prisma.sql`SUM(COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(counts,${`$.${k}`})) AS SIGNED),0)) AS ${Prisma.raw(k)}`));
|
||||
const numeric=(row:Partial<Row>):AnalyticsCounts=>Object.fromEntries(COUNT_KEYS.map(k=>[k,Number(row[k]??0)]))as unknown as AnalyticsCounts;
|
||||
export async function suspendCallerAlerts(db:PrismaClient,reason:string){await db.$executeRaw`UPDATE caller_analysis_alerts SET status='SUSPENDED',payload=JSON_SET(payload,'$.suspendedReason',${reason},'$.suspendedAt',${new Date().toISOString()},'$.streak',0,'$.recovered',0),updated_at=NOW(3) WHERE status IN ('ACTIVE','OBSERVING')`;}
|
||||
export async function evaluateCallerAlerts(db:PrismaClient){
|
||||
const end=new Date(Math.floor(Date.now()/60000)*60000),start=new Date(+end-15*60000),evaluationKey=end.toISOString(),aggregates=sums();
|
||||
const rows=await db.$queryRaw<Row[]>(Prisma.sql`SELECT customer_id,caller,view,${aggregates} FROM caller_analysis_minute_buckets WHERE started_at>=${start} AND started_at<${end} GROUP BY customer_id,caller,view HAVING SUM(COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(counts,'$.totalCalls')) AS SIGNED),0))>=50`);
|
||||
for(const row of rows){const counts=numeric(row),rates=analyticsRates(counts);if(rates.unknownCalls||rates.durationUnknownCalls||rates.activeCalls/rates.totalCalls>0.2)continue;const reasons=new Map<string,string>();
|
||||
if(rates.connectionRate!==null&&rates.connectionRate<Number(process.env.ANALYTICS_LOW_CONNECTION_RATE??20))reasons.set('LOW_CONNECTION','低接通率');
|
||||
if(rates.overallAnswerRate!==null&&rates.overallAnswerRate<Number(process.env.ANALYTICS_LOW_OVERALL_RATE??10))reasons.set('LOW_OVERALL','低总体应答率');
|
||||
if(rates.connectedCalls>=30&&rates.connectedAnswerRate!==null&&rates.connectedAnswerRate<Number(process.env.ANALYTICS_LOW_CONNECTED_RATE??20))reasons.set('LOW_CONNECTED','低已接通应答率');
|
||||
const previous=await db.$queryRaw<Row[]>(Prisma.sql`SELECT ${aggregates} FROM caller_analysis_minute_buckets WHERE started_at>=${new Date(+start-15*60000)} AND started_at<${start} AND customer_id=${row.customer_id} AND caller=${row.caller} AND view=${row.view}`),p=analyticsRates(numeric(previous[0]??{}));
|
||||
if(p.totalCalls>=50&&!p.unknownCalls&&!p.durationUnknownCalls&&p.activeCalls/p.totalCalls<=0.2)for(const[key,label]of[['connectionRate','接通率骤降'],['overallAnswerRate','总体应答率骤降'],['connectedAnswerRate','已接通应答率骤降']]as const){if(key==='connectedAnswerRate'&&(p.connectedCalls<30||rates.connectedCalls<30))continue;if(p[key]!==null&&rates[key]!==null&&p[key]!-rates[key]!>Number(process.env.ANALYTICS_RATE_DROP_POINTS??15))reasons.set(`DROP_${key}`,label);}
|
||||
const tail=await db.$queryRaw<Array<{counts:AnalyticsCounts}>>`SELECT counts FROM caller_analysis_states WHERE customer_id=${row.customer_id} AND caller=${row.caller} AND view=${row.view} AND started_at>=${start} AND started_at<${end} ORDER BY started_at DESC,id DESC LIMIT 20`;if(tail.length===20&&tail.every(t=>t.counts.failedCalls===1))reasons.set('CONSECUTIVE_FAILURE','连续20次已结束未接通');
|
||||
for(const ruleId of['LOW_CONNECTION','LOW_OVERALL','LOW_CONNECTED','DROP_connectionRate','DROP_overallAnswerRate','DROP_connectedAnswerRate','CONSECUTIVE_FAILURE']){const id=analyticsHash([row.customer_id,row.caller,row.view,ruleId,'v2']),existing=await db.$queryRaw<Prior[]>`SELECT status,payload FROM caller_analysis_alerts WHERE id=${id}`,old=existing[0];if(old?.payload.evaluationKey===evaluationKey)continue;const hit=reasons.has(ruleId),streak=hit?(old?.payload.streak??0)+1:0,recovered=hit?0:(old?.payload.recovered??0)+1,status=streak>=3?'ACTIVE':old?.status==='ACTIVE'&&recovered<3?'ACTIVE':'OBSERVING',payload=JSON.stringify({ruleId,reason:reasons.get(ruleId)??null,rates,streak,recovered,evaluationKey,windowStart:start,windowEnd:end,scope:{customerId:row.customer_id,caller:row.caller,view:row.view,minutes:15},since:old?.payload.since??new Date().toISOString(),cooldownSeconds:60});await db.$executeRaw`INSERT INTO caller_analysis_alerts(id,customer_id,view,caller,status,payload,updated_at) VALUES (${id},${row.customer_id},${row.view},${row.caller},${status},${payload},NOW(3)) ON DUPLICATE KEY UPDATE status=VALUES(status),payload=VALUES(payload),updated_at=VALUES(updated_at)`;}
|
||||
}
|
||||
await db.$executeRaw`UPDATE caller_analysis_alerts SET status='STALE' WHERE updated_at<${new Date(Date.now()-120000)} AND status IN ('ACTIVE','OBSERVING','SUSPENDED')`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user