-
—
-
Tiempo Prom. QA
-
Horas promedio por ticket
+
+
—
+
+
Tiempo P50 QA
+
+
Percentil 50 (Mediana)
El 50% de las pruebas se completan en esta cantidad de horas netas o menos.
+
+
+
+
Horas en prueba
-
-
—
-
Tiempo Prom. en Cola
-
Horas esperando READY TO QA
+
+
—
+
+
Tiempos P90 y P99 QA
+
+
Cuellos de Botella QA
P90: El 90% logran probarse en este tiempo.
P99: El 99% logra terminar en este tiempo.
+
+
+
+
P99: — hs
+
+
+
—
Tickets Devueltos
Volvieron a En Curso
+
+
—
+
+
Tiempo P50 RTQA
+
+
Percentil 50 (Mediana)
El 50% de los tickets espera esta cantidad de horas (o menos) en la cola antes de ser tomado por QA.
+
+
+
+
Horas en cola
+
+
+
—
+
+
Tiempos P90 y P99 Cola
+
+
Cuellos de Botella RTQA
Casos atascados sin tester asignado.
P90: El 90% de los tickets sale de la cola en este tiempo.
P99: Bloqueos máximos en la cola.
+
+
+
+
P99: — hs
+
diff --git a/js/app.js b/js/app.js
index 09c9104..b840064 100644
--- a/js/app.js
+++ b/js/app.js
@@ -357,10 +357,16 @@ function renderQASection() {
// ── KPIs DEL PERÍODO ──────────────────────────────────────────────────────
document.getElementById('kpi-qa-tickets').textContent = qa.tickets ?? '—';
- document.getElementById('kpi-qa-avg').textContent = qa.avgTimeQa != null ? qa.avgTimeQa + 'hs' : '—';
- document.getElementById('kpi-qa-rtqa').textContent = qa.avgTimeRtqa != null ? qa.avgTimeRtqa + 'hs' : '—';
document.getElementById('kpi-qa-returned').textContent = qa.ticketsReturned ?? '—';
+ document.getElementById('kpi-qa-p50').textContent = qa.p50Qa != null ? qa.p50Qa + 'hs' : '—';
+ document.getElementById('kpi-qa-p90').textContent = qa.p90Qa != null ? qa.p90Qa + 'hs' : '—';
+ document.getElementById('kpi-qa-p99-sub').textContent = qa.p99Qa != null ? 'P99: ' + qa.p99Qa + ' hs' : 'P99: — hs';
+
+ document.getElementById('kpi-qa-rtqa-p50').textContent = qa.p50Rtqa != null ? qa.p50Rtqa + 'hs' : '—';
+ document.getElementById('kpi-qa-rtqa-p90').textContent = qa.p90Rtqa != null ? qa.p90Rtqa + 'hs' : '—';
+ document.getElementById('kpi-qa-rtqa-p99-sub').textContent = qa.p99Rtqa != null ? 'P99: ' + qa.p99Rtqa + ' hs' : 'P99: — hs';
+
const pctRet = qa.tickets > 0 ? ((qa.ticketsReturned / qa.tickets) * 100).toFixed(1) : 0;
document.getElementById('kpi-qa-returned-pct').textContent =
`${pctRet}% del total pasó por QA más de una vez`;
diff --git a/netlify/functions/data.js b/netlify/functions/data.js
index 6a08d61..58be3b5 100644
--- a/netlify/functions/data.js
+++ b/netlify/functions/data.js
@@ -138,6 +138,7 @@ function processIssues(issues, period = '90d') {
const qaData = {
tickets: 0, ticketsReturned: 0, totalTimeQa: 0, totalTimeRtqa: 0,
monthlyTotalQa: {}, monthlyCountQa: {}, monthlyTotalRtqa: {}, monthlyCountRtqa: {}, issues: [],
+ timesQa: [], timesRtqa: []
};
const deliveryData = {
@@ -253,6 +254,7 @@ function processIssues(issues, period = '90d') {
if (timeQaMin > 0) {
qaData.tickets++;
qaData.totalTimeQa += timeQaMin;
+ qaData.timesQa.push(timeQaMin);
if (timesRet > 1) qaData.ticketsReturned++;
if (qaMonth) {
qaData.monthlyTotalQa[qaMonth] = (qaData.monthlyTotalQa[qaMonth] || 0) + timeQaMin;
@@ -264,10 +266,13 @@ function processIssues(issues, period = '90d') {
});
}
- if (timeRtqaMin > 0 && qaMonth) {
- qaData.totalTimeRtqa += timeRtqaMin;
- qaData.monthlyTotalRtqa[qaMonth] = (qaData.monthlyTotalRtqa[qaMonth] || 0) + timeRtqaMin;
- qaData.monthlyCountRtqa[qaMonth] = (qaData.monthlyCountRtqa[qaMonth] || 0) + 1;
+ if (timeRtqaMin > 0) {
+ qaData.timesRtqa.push(timeRtqaMin);
+ if (qaMonth) {
+ qaData.totalTimeRtqa += timeRtqaMin;
+ qaData.monthlyTotalRtqa[qaMonth] = (qaData.monthlyTotalRtqa[qaMonth] || 0) + timeRtqaMin;
+ qaData.monthlyCountRtqa[qaMonth] = (qaData.monthlyCountRtqa[qaMonth] || 0) + 1;
+ }
}
const timeCrMin = f[config.fields.crTotalTime] ?? 0;
@@ -341,6 +346,16 @@ function processIssues(issues, period = '90d') {
const p90Cr = minsToHrs(getPercentile(crTimesSorted, 0.9));
const p99Cr = minsToHrs(getPercentile(crTimesSorted, 0.99));
+ const qaTimesSorted = qaData.timesQa.sort((a, b) => a - b);
+ const p50Qa = minsToHrs(getPercentile(qaTimesSorted, 0.5));
+ const p90Qa = minsToHrs(getPercentile(qaTimesSorted, 0.9));
+ const p99Qa = minsToHrs(getPercentile(qaTimesSorted, 0.99));
+
+ const rtqaTimesSorted = qaData.timesRtqa.sort((a, b) => a - b);
+ const p50Rtqa = minsToHrs(getPercentile(rtqaTimesSorted, 0.5));
+ const p90Rtqa = minsToHrs(getPercentile(rtqaTimesSorted, 0.9));
+ const p99Rtqa = minsToHrs(getPercentile(rtqaTimesSorted, 0.99));
+
return {
lastUpdated: new Date().toISOString(),
assigneeData, monthlyDone, issuesList, warnings, currentStates,
@@ -357,6 +372,7 @@ function processIssues(issues, period = '90d') {
},
qaData: {
...qaData, monthlyAvgQa, monthlyAvgRtqa,
+ p50Qa, p90Qa, p99Qa, p50Rtqa, p90Rtqa, p99Rtqa,
avgTimeQa: qaData.tickets > 0 ? minsToHrs(qaData.totalTimeQa / qaData.tickets) : 0,
avgTimeRtqa: qaData.tickets > 0 ? minsToHrs(qaData.totalTimeRtqa / qaData.tickets) : 0
}