fix: complete caller analytics reliability remediation

This commit is contained in:
hectorzhao
2026-09-01 10:03:29 +08:00
parent a116926867
commit 7f8825cf3a
25 changed files with 850 additions and 202 deletions
+6 -1
View File
@@ -45,6 +45,9 @@ try {
}
assert.equal(result.status,200,JSON.stringify(result.body));
assert.deepEqual(Object.fromEntries(['totalCalls','connectedCalls','answeredCalls','failedCalls'].map(k=>[k,result.body.summary[k]])),{totalCalls:6,connectedCalls:4,answeredCalls:2,failedCalls:2});
assert.equal(result.body.analysisRevision,'caller-analytics-r2');
assert.equal(result.body.coverageStatus,'CONTINUOUS',JSON.stringify(result.body.health));
assert.equal(result.body.summary.durationUnknownCalls,0);
assert.equal((await get('/caller-analytics/overview',false)).status,401);
assert.equal((await get('/caller-analytics/overview?view=invalid')).status,400);
assert.equal(result.body.summary.connectedAnswerRate,50);
@@ -73,11 +76,13 @@ try {
assert.equal((await get('/caller-analytics/overview?customerId=another_customer',true,scopedToken)).status,403);
const options=await get('/caller-analytics/options',true,scopedToken);
assert.equal(options.status,200); assert.deepEqual(options.body.customers.map(c=>c.id),[fixture]);
assert.deepEqual(options.body.vendors.map(v=>v.id),[fixture]);
} finally {
await db.$executeRaw`DELETE FROM caller_analysis_access WHERE user_id=${testUserId}`;
await db.user.deleteMany({where:{id:testUserId}}); await db.role.deleteMany({where:{id:testUserId}});
}
const evidence={date:new Date().toISOString(),from,api:result,detail:detail.body,sip:JSON.parse(sip.stdout),bucketAudit:beforeReplay.body.summary,replayed:events.length,tests:['real SIP six scenarios','real API counts and three rates','real DB states and minute buckets','database replay idempotency','customer scope and 403 isolation','scoped filter options','unauthenticated 401','invalid query 400','positive subsecond duration']};
const sipEvents=JSON.parse(sip.stdout); assert.equal(sipEvents.filter(e=>e.direction==='UAC'&&e.code===200&&e.sdp).length,2,'Two successful calls must negotiate SDP through RTPEngine');
const evidence={date:new Date().toISOString(),from,api:result,detail:detail.body,sip:sipEvents,bucketAudit:beforeReplay.body.summary,replayed:events.length,tests:['real SIP six scenarios','two SDP offer/answer negotiations through RTPEngine','ACK-based positive duration including subsecond call','real API counts, completeness and three rates','real DB states and minute buckets','database replay idempotency','customer scope and 403 isolation','scoped customer/vendor filter options','unauthenticated 401','invalid query 400']};
await writeFile(process.env.CRA_REPORT || '/tmp/caller-analytics-real-evidence.json',JSON.stringify(evidence,null,2));
console.log(JSON.stringify({passed:true,summary:result.body.summary,shortMs:short.counts.talkMs,quality:result.body.qualityStatus}));
}
+19 -6
View File
@@ -14,7 +14,10 @@ def header(message, name):
match = re.search(r'^' + re.escape(name) + r':\s*(.+)$', message, re.I | re.M)
return match.group(1).strip() if match else ''
def reply(message, code, reason):
def sdp(port):
return '\r\n'.join(['v=0', 'o=cra 1 1 IN IP4 127.0.0.1', 's=caller-analytics-test', 'c=IN IP4 127.0.0.1', 't=0 0', f'm=audio {port} RTP/AVP 0', 'a=rtpmap:0 PCMU/8000', 'a=sendrecv', ''])
def reply(message, code, reason, body=''):
to = header(message, 'To')
if ';tag=' not in to:
to += ';tag=cra-uas'
@@ -23,7 +26,11 @@ def reply(message, code, reason):
lines = [f'SIP/2.0 {code} {reason}'] + [f'Via: {v.strip()}' for v in vias]
lines += [f'From: {header(message,"From")}', f'To: {to}', f'Call-ID: {header(message,"Call-ID")}', f'CSeq: {header(message,"CSeq")}']
lines += [f'Record-Route: {r.strip()}' for r in routes]
lines += ['Contact: <sip:uas@127.0.0.1:50630>', 'Content-Length: 0', '', '']
lines += ['Contact: <sip:uas@127.0.0.1:50630>']
if body:
lines += ['Content-Type: application/sdp', f'Content-Length: {len(body.encode())}', '', body]
else:
lines += ['Content-Length: 0', '', '']
return '\r\n'.join(lines).encode()
def uas():
@@ -37,8 +44,9 @@ def uas():
number = re.search(r'INVITE sip:([^@]+)',msg).group(1)
cases = {'99101':[(180,'Ringing'),(486,'Busy Here')], '99102':[(183,'Session Progress'),(487,'Request Terminated')], '99103':[(486,'Busy Here')], '99104':[(200,'OK')], '99105':[(180,'Ringing'),(200,'OK')], '99106':[(100,'Trying'),(486,'Busy Here')]}
for code, reason in cases[number]:
sock.sendto(reply(msg,code,reason),addr)
events.append({'callId':header(msg,'Call-ID'),'direction':'UAS','code':code,'time':time.time()})
answer = sdp(40000) if code == 200 else ''
sock.sendto(reply(msg,code,reason,answer),addr)
events.append({'callId':header(msg,'Call-ID'),'direction':'UAS','code':code,'sdp':bool(answer),'time':time.time()})
time.sleep(.15 if number!='99106' else 1.5)
elif method == 'BYE': sock.sendto(reply(msg,200,'OK'),addr)
@@ -50,15 +58,20 @@ try:
callid=f'cra-{number}-{uuid.uuid4().hex}@loopback'; tag=uuid.uuid4().hex[:8]
branch='z9hG4bK'+uuid.uuid4().hex
base=[f'Via: SIP/2.0/UDP 127.0.0.1:{port};branch={branch};rport', f'From: <sip:99100001@127.0.0.1>;tag={tag}', f'To: <sip:{number}@127.0.0.1>', f'Call-ID: {callid}', f'Contact: <sip:99100001@127.0.0.1:{port}>', 'Max-Forwards: 70']
invite='\r\n'.join([f'INVITE sip:{number}@127.0.0.1:15060 SIP/2.0',*base,'CSeq: 1 INVITE','Content-Length: 0','',''])
offer = sdp(40002) if number in ('99104','99105') else ''
invite_lines=[f'INVITE sip:{number}@127.0.0.1:15060 SIP/2.0',*base,'CSeq: 1 INVITE']
if offer: invite_lines += ['Content-Type: application/sdp',f'Content-Length: {len(offer.encode())}','',''+offer]
else: invite_lines += ['Content-Length: 0','','']
invite='\r\n'.join(invite_lines)
sock.sendto(invite.encode(),('127.0.0.1',15060))
while True:
msg=sock.recv(65535).decode(); code=int(msg.split()[1]); events.append({'callId':callid,'direction':'UAC','code':code,'time':time.time()})
msg=sock.recv(65535).decode(); code=int(msg.split()[1]); has_sdp='application/sdp' in msg.lower(); events.append({'callId':callid,'direction':'UAC','code':code,'sdp':has_sdp,'time':time.time()})
if code<200: continue
if code>=300:
ack=invite.replace('INVITE sip:','ACK sip:',1).replace('CSeq: 1 INVITE','CSeq: 1 ACK').replace(base[2],f'To: {header(msg,"To")}')
sock.sendto(ack.encode(),('127.0.0.1',15060)); break
routes = list(reversed(re.findall(r'^Record-Route:\s*(.+)$',msg,re.I|re.M)))
assert not offer or has_sdp, 'Successful SDP offer must receive an SDP answer'
for method,cseq in [('ACK',1),('BYE',2)]:
if method=='BYE': time.sleep(.3 if number=='99105' else 1.5)
seq=[f'{method} sip:uas@127.0.0.1:50630 SIP/2.0',f'Via: SIP/2.0/UDP 127.0.0.1:{port};branch=z9hG4bK{uuid.uuid4().hex};rport',base[1],f'To: {header(msg,"To")}',base[3],*['Route: '+r.strip() for r in routes],f'CSeq: {cseq} {method}','Max-Forwards: 70','Content-Length: 0','','']