feat(vin-decode): WMI opportunity radar, per-source p50, unknown-VIN spike alarm
Systematize Faz 4 + close three observability gaps: - WMI opportunity radar (getWmiOpportunities): group query_logs by substring(vin,1,3) = WMI, rank low-success WMIs by distinct-user demand. brandMatched=0 flags fully-uncovered manufacturer codes. Surfaced on the Trends page (#wmi) + a dashboard header shortcut. Replaces the manual Faz 4. - Per-source p50: add percentile_cont(0.50) to getProviderAttempts and getProviderDeepStats; show P50·P95 on the dashboard and provider drill-down. - unknown_vin_spike anomaly: track "tanınamadı" rate in the 15min/baseline windows, fire when it jumps >=2x baseline (or surges from ~0), and attribute the dominant failing source (+aborted count) in the Telegram message. Runs in the existing 5min anomaly cron; worker/telegram unchanged (generic type). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,13 @@ export default async function VinDecodePage({
|
||||
>
|
||||
Trends →
|
||||
</Link>
|
||||
<Link
|
||||
href="/projects/sase/vin-decode/trends#wmi"
|
||||
className={buttonVariants({ variant: "outline", size: "sm" })}
|
||||
title="Sıradaki eklenecek WMI adayları (talebe göre)"
|
||||
>
|
||||
WMI fırsatları →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -222,7 +229,7 @@ export default async function VinDecodePage({
|
||||
label: a.provider,
|
||||
href: `/projects/sase/vin-decode/providers/${encodeURIComponent(a.provider === "pcat" ? "parts-catalogs" : a.provider === "vin_api" ? "vin-api" : a.provider)}`,
|
||||
count: a.attemptCount,
|
||||
meta: `Ø ${ms(a.avgMs)} · P95 ${ms(a.p95Ms)}`,
|
||||
meta: `P50 ${ms(a.p50Ms)} · P95 ${ms(a.p95Ms)}`,
|
||||
}))}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@@ -109,16 +109,16 @@ export default async function ProviderDeepPage({
|
||||
value={`${(stats.wins.successRate * 100).toFixed(1)}%`}
|
||||
/>
|
||||
<Kpi
|
||||
label="Avg RT"
|
||||
value={stats.wins.avgMs != null ? `${stats.wins.avgMs}ms` : "—"}
|
||||
hint={`P95 ${stats.wins.p95 != null ? `${stats.wins.p95}ms` : "—"}`}
|
||||
label="P50 / P95 RT"
|
||||
value={stats.wins.p50 != null ? `${stats.wins.p50}ms` : "—"}
|
||||
hint={`P95 ${stats.wins.p95 != null ? `${stats.wins.p95}ms` : "—"} · Ø ${stats.wins.avgMs != null ? `${stats.wins.avgMs}ms` : "—"}`}
|
||||
/>
|
||||
<Kpi
|
||||
label="Chain'de görüldü"
|
||||
value={stats.attempts.total.toLocaleString("tr-TR")}
|
||||
hint={
|
||||
stats.attempts.total > 0
|
||||
? `Ø ${stats.attempts.avgMs}ms · P95 ${stats.attempts.p95 ?? "—"}ms`
|
||||
? `P50 ${stats.attempts.p50 ?? "—"}ms · P95 ${stats.attempts.p95 ?? "—"}ms`
|
||||
: "chain key'ı yok"
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getWeeklyGrowth,
|
||||
getProviderShareTrend,
|
||||
getUnderSupportedBrands,
|
||||
getWmiOpportunities,
|
||||
getPeakHeatmap,
|
||||
getCacheHitTrend,
|
||||
getEfficiencyTrend,
|
||||
@@ -42,12 +43,13 @@ const PROVIDER_COLORS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export default async function TrendsPage() {
|
||||
const [volume, growth, shareTrend, underSupported, heatmap, cache, efficiency] =
|
||||
const [volume, growth, shareTrend, underSupported, wmiOpps, heatmap, cache, efficiency] =
|
||||
await Promise.all([
|
||||
getLongTermVolume(90),
|
||||
getWeeklyGrowth(12),
|
||||
getProviderShareTrend(30),
|
||||
getUnderSupportedBrands(30, 20, 0.75),
|
||||
getWmiOpportunities(30, 15, 0.7),
|
||||
getPeakHeatmap(30),
|
||||
getCacheHitTrend(30),
|
||||
getEfficiencyTrend(30),
|
||||
@@ -252,6 +254,82 @@ export default async function TrendsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 4b. WMI opportunity radar — data-driven Faz 4 */}
|
||||
<Card id="wmi">
|
||||
<CardHeader>
|
||||
<CardDescription>
|
||||
4b. WMI fırsat radarı (son 30g, hacim ≥15, başarı ≤70%) — sıradaki
|
||||
ekleme adayları, talebe göre sıralı
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{wmiOpps.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Düşük başarılı WMI yok — talep edilen üretici kodları yeterince
|
||||
karşılanıyor.
|
||||
</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>WMI</TableHead>
|
||||
<TableHead>Durum / marka</TableHead>
|
||||
<TableHead className="text-right">Talep (kullanıcı)</TableHead>
|
||||
<TableHead className="text-right">Toplam</TableHead>
|
||||
<TableHead className="text-right">Fail</TableHead>
|
||||
<TableHead className="text-right">Başarı</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{wmiOpps.map((w) => (
|
||||
<TableRow key={w.wmi}>
|
||||
<TableCell className="font-mono text-xs">{w.wmi}</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{w.brandMatched === 0 ? (
|
||||
<Badge variant="destructive">eşleşmesiz WMI</Badge>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-mono">{w.topBrandSlug ?? "?"}</span>
|
||||
{w.topBrandName && (
|
||||
<span className="ml-1 text-muted-foreground">
|
||||
{w.topBrandName}
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-1 text-muted-foreground">· decode zayıf</span>
|
||||
</>
|
||||
)}
|
||||
{w.topResultKind && (
|
||||
<span className="ml-1 text-muted-foreground">
|
||||
({w.topResultKind})
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-medium tabular-nums">
|
||||
{w.uniqueUsers}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{w.total.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-destructive">
|
||||
{w.failed.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-destructive">
|
||||
{(w.successRate * 100).toFixed(1)}%
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
"eşleşmesiz WMI" = hiç markaya bağlanamayan üretici kodu →
|
||||
eklenince tamamen yeni kapsama. Talep (farklı kullanıcı) sütununa
|
||||
göre sıralı: en çok kişinin isteyip alamadığı WMI en üstte. Faz 4'ün
|
||||
el yordamı yerine veri-güdümlü hali.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 5. Peak heatmap */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -12,7 +12,8 @@ export type AnomalyType =
|
||||
| "p95_latency_spike"
|
||||
| "volume_drop"
|
||||
| "volume_spike"
|
||||
| "timeout_dominance";
|
||||
| "timeout_dominance"
|
||||
| "unknown_vin_spike";
|
||||
|
||||
export type AnomalyHit = {
|
||||
type: AnomalyType;
|
||||
@@ -36,6 +37,7 @@ type WindowStats = {
|
||||
p95: number | null;
|
||||
avgMs: number | null;
|
||||
timeouts: number;
|
||||
unknownVins: number;
|
||||
};
|
||||
|
||||
async function windowStats(start: Date, end: Date): Promise<WindowStats> {
|
||||
@@ -47,6 +49,7 @@ async function windowStats(start: Date, end: Date): Promise<WindowStats> {
|
||||
p95: number | null;
|
||||
avg_ms: number | null;
|
||||
timeouts: bigint;
|
||||
unknown_vins: bigint;
|
||||
}>
|
||||
>`
|
||||
SELECT
|
||||
@@ -63,7 +66,16 @@ async function windowStats(start: Date, end: Date): Promise<WindowStats> {
|
||||
OR error_message ILIKE '%timeout%'
|
||||
OR (timings->>'aborted')::boolean = true
|
||||
)
|
||||
) AS timeouts
|
||||
) AS timeouts,
|
||||
count(*) FILTER (
|
||||
WHERE success = false
|
||||
AND (
|
||||
error_message ILIKE '%unknown vin%'
|
||||
OR error_message ILIKE '%tanınamad%'
|
||||
OR error_message ILIKE '%destekl%'
|
||||
OR (timings->>'result_kind') = 'unknown'
|
||||
)
|
||||
) AS unknown_vins
|
||||
FROM query_logs
|
||||
WHERE created_at >= ${start} AND created_at < ${end}
|
||||
`;
|
||||
@@ -74,6 +86,7 @@ async function windowStats(start: Date, end: Date): Promise<WindowStats> {
|
||||
p95: null,
|
||||
avg_ms: null,
|
||||
timeouts: 0n,
|
||||
unknown_vins: 0n,
|
||||
};
|
||||
const total = Number(r.total);
|
||||
const succeeded = Number(r.succeeded);
|
||||
@@ -87,6 +100,7 @@ async function windowStats(start: Date, end: Date): Promise<WindowStats> {
|
||||
p95: r.p95,
|
||||
avgMs: r.avg_ms,
|
||||
timeouts: Number(r.timeouts),
|
||||
unknownVins: Number(r.unknown_vins),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,6 +128,7 @@ async function baselineStats(currentEnd: Date): Promise<WindowStats> {
|
||||
p95: null,
|
||||
avgMs: null,
|
||||
timeouts: 0,
|
||||
unknownVins: 0,
|
||||
};
|
||||
}
|
||||
const total = samples.reduce((a, b) => a + b.total, 0);
|
||||
@@ -122,6 +137,7 @@ async function baselineStats(currentEnd: Date): Promise<WindowStats> {
|
||||
const p95Values = samples.map((s) => s.p95).filter((v): v is number => v != null);
|
||||
const avgValues = samples.map((s) => s.avgMs).filter((v): v is number => v != null);
|
||||
const timeouts = samples.reduce((a, b) => a + b.timeouts, 0);
|
||||
const unknownVins = samples.reduce((a, b) => a + b.unknownVins, 0);
|
||||
return {
|
||||
total,
|
||||
succeeded,
|
||||
@@ -131,9 +147,37 @@ async function baselineStats(currentEnd: Date): Promise<WindowStats> {
|
||||
p95: p95Values.length ? Math.round(p95Values.reduce((a, b) => a + b, 0) / p95Values.length) : null,
|
||||
avgMs: avgValues.length ? Math.round(avgValues.reduce((a, b) => a + b, 0) / avgValues.length) : null,
|
||||
timeouts,
|
||||
unknownVins,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribution for an unknown-VIN / failure spike: which `source` dominates the
|
||||
* failures in the window, and how many of those were aborted (budget/timeout).
|
||||
* A proxy/decoder outage shows up here as a surge of source='none'/'aborted'
|
||||
* or one provider's failures — turns "success dropped" into "EMEX is down".
|
||||
*/
|
||||
async function dominantFailingSource(start: Date, end: Date): Promise<string | null> {
|
||||
const rows = await saseDb.$queryRaw<
|
||||
Array<{ source: string | null; cnt: bigint; aborted: bigint }>
|
||||
>`
|
||||
SELECT
|
||||
coalesce(source, 'none') AS source,
|
||||
count(*) AS cnt,
|
||||
count(*) FILTER (WHERE (timings->>'aborted')::boolean = true) AS aborted
|
||||
FROM query_logs
|
||||
WHERE created_at >= ${start} AND created_at < ${end} AND success = false
|
||||
GROUP BY coalesce(source, 'none')
|
||||
ORDER BY count(*) DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
const r = rows[0];
|
||||
if (!r) return null;
|
||||
const cnt = Number(r.cnt);
|
||||
const aborted = Number(r.aborted);
|
||||
return aborted > 0 ? `${r.source} (${cnt}, ${aborted} aborted)` : `${r.source} (${cnt})`;
|
||||
}
|
||||
|
||||
export async function detectVinAnomalies(): Promise<{
|
||||
current: WindowStats;
|
||||
baseline: WindowStats;
|
||||
@@ -243,6 +287,35 @@ export async function detectVinAnomalies(): Promise<{
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Unknown-VIN spike ("tanınamadı" patlaması) — proxy/decoder outage. The
|
||||
// generic success-rate-drop catches this too, but this names it explicitly
|
||||
// and attributes the dominant failing source. Fire when the unknown-VIN
|
||||
// share jumps ≥2× baseline, or surges from a near-zero baseline.
|
||||
if (current.total >= MIN_CURRENT_VOLUME) {
|
||||
const curRate = current.unknownVins / current.total;
|
||||
const baseRate = baseline.total > 0 ? baseline.unknownVins / baseline.total : 0;
|
||||
const spiked =
|
||||
curRate >= 0.15 &&
|
||||
(baseRate < 0.02 ? current.unknownVins >= 5 : curRate >= baseRate * 2);
|
||||
if (spiked) {
|
||||
const attribution = await dominantFailingSource(currentStart, now);
|
||||
hits.push({
|
||||
type: "unknown_vin_spike",
|
||||
severity: curRate >= 0.4 ? "critical" : "high",
|
||||
message:
|
||||
`"Tanınamadı" oranı ${pct(baseRate)} → ${pct(curRate)} ` +
|
||||
`(${current.unknownVins}/${current.total}, son ${CURRENT_WINDOW_MIN}dk)` +
|
||||
(attribution ? ` · baskın source: ${attribution}` : ""),
|
||||
baseline: baseRate,
|
||||
observed: curRate,
|
||||
current_volume: current.total,
|
||||
baseline_volume: baseline.total,
|
||||
detected_at: ts,
|
||||
dedupe_key: `vin:unknown_vin_spike:${bucket}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { current, baseline, anomalies: hits };
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@ export type ProviderAttemptRow = {
|
||||
provider: string;
|
||||
attemptCount: number;
|
||||
avgMs: number;
|
||||
p50Ms: number | null;
|
||||
p95Ms: number | null;
|
||||
};
|
||||
|
||||
@@ -175,12 +176,19 @@ export async function getProviderAttempts(range: TimeRange): Promise<ProviderAtt
|
||||
const start = rangeStart(range);
|
||||
|
||||
const rows = await saseDb.$queryRaw<
|
||||
Array<{ provider: string; cnt: bigint; avg_ms: number | null; p95_ms: number | null }>
|
||||
Array<{
|
||||
provider: string;
|
||||
cnt: bigint;
|
||||
avg_ms: number | null;
|
||||
p50_ms: number | null;
|
||||
p95_ms: number | null;
|
||||
}>
|
||||
>`
|
||||
SELECT
|
||||
key AS provider,
|
||||
count(*) AS cnt,
|
||||
avg((value::text)::numeric)::int AS avg_ms,
|
||||
percentile_cont(0.50) WITHIN GROUP (ORDER BY (value::text)::numeric)::int AS p50_ms,
|
||||
percentile_cont(0.95) WITHIN GROUP (ORDER BY (value::text)::numeric)::int AS p95_ms
|
||||
FROM query_logs, jsonb_each(timings) AS j(key, value)
|
||||
WHERE created_at >= ${start}
|
||||
@@ -194,6 +202,7 @@ export async function getProviderAttempts(range: TimeRange): Promise<ProviderAtt
|
||||
provider: r.provider,
|
||||
attemptCount: Number(r.cnt),
|
||||
avgMs: r.avg_ms ?? 0,
|
||||
p50Ms: r.p50_ms,
|
||||
p95Ms: r.p95_ms,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -164,6 +164,87 @@ export async function getUnderSupportedBrands(
|
||||
});
|
||||
}
|
||||
|
||||
// ─── WMI opportunity radar — systematizes "which WMI to add next" (Faz 4) ──
|
||||
// WMI = World Manufacturer Identifier = first 3 chars of the VIN (ISO 3779).
|
||||
// Grouping by substring(vin,1,3) works on 100% of historical rows regardless
|
||||
// of whether timings.wmi has been backfilled. We surface WMIs with meaningful
|
||||
// demand but low decode success — ranked by *distinct users* (real demand,
|
||||
// not one user retrying). `brandMatched = 0` means the WMI never resolved to
|
||||
// any brand at all → a fully-uncovered manufacturer code (strongest "add this"
|
||||
// signal). result_kind (when present in timings) labels the dominant outcome.
|
||||
export type WmiOpportunity = {
|
||||
wmi: string;
|
||||
total: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
successRate: number;
|
||||
uniqueUsers: number;
|
||||
brandMatched: number; // how many of these requests carried a brand_id
|
||||
topBrandSlug: string | null;
|
||||
topBrandName: string | null;
|
||||
topResultKind: string | null;
|
||||
};
|
||||
|
||||
export async function getWmiOpportunities(
|
||||
days = 30,
|
||||
minVolume = 15,
|
||||
maxSuccessRate = 0.7,
|
||||
): Promise<WmiOpportunity[]> {
|
||||
const start = new Date(Date.now() - days * 24 * 60 * 60_000);
|
||||
const rows = await saseDb.$queryRaw<
|
||||
Array<{
|
||||
wmi: string;
|
||||
total: bigint;
|
||||
succeeded: bigint;
|
||||
failed: bigint;
|
||||
users: bigint;
|
||||
brand_matched: bigint;
|
||||
top_brand_slug: string | null;
|
||||
top_brand_name: string | null;
|
||||
top_result_kind: string | null;
|
||||
}>
|
||||
>`
|
||||
SELECT
|
||||
substring(q.vin from 1 for 3) AS wmi,
|
||||
count(*) AS total,
|
||||
count(*) FILTER (WHERE q.success = true) AS succeeded,
|
||||
count(*) FILTER (WHERE q.success = false) AS failed,
|
||||
count(DISTINCT q.user_id) AS users,
|
||||
count(*) FILTER (WHERE q.brand_id IS NOT NULL) AS brand_matched,
|
||||
mode() WITHIN GROUP (ORDER BY b.slug)
|
||||
FILTER (WHERE b.slug IS NOT NULL) AS top_brand_slug,
|
||||
mode() WITHIN GROUP (ORDER BY b.name)
|
||||
FILTER (WHERE b.name IS NOT NULL) AS top_brand_name,
|
||||
mode() WITHIN GROUP (ORDER BY (q.timings->>'result_kind'))
|
||||
FILTER (WHERE (q.timings->>'result_kind') IS NOT NULL) AS top_result_kind
|
||||
FROM query_logs q
|
||||
LEFT JOIN brands b ON b.id = q.brand_id
|
||||
WHERE q.created_at >= ${start}
|
||||
AND char_length(q.vin) >= 3
|
||||
GROUP BY substring(q.vin from 1 for 3)
|
||||
HAVING count(*) >= ${minVolume}
|
||||
AND count(*) FILTER (WHERE q.success = true)::float / count(*) <= ${maxSuccessRate}
|
||||
ORDER BY count(DISTINCT q.user_id) DESC, count(*) FILTER (WHERE q.success = false) DESC
|
||||
LIMIT 30
|
||||
`;
|
||||
return rows.map((r) => {
|
||||
const total = Number(r.total);
|
||||
const succeeded = Number(r.succeeded);
|
||||
return {
|
||||
wmi: r.wmi,
|
||||
total,
|
||||
succeeded,
|
||||
failed: Number(r.failed),
|
||||
successRate: total > 0 ? succeeded / total : 0,
|
||||
uniqueUsers: Number(r.users),
|
||||
brandMatched: Number(r.brand_matched),
|
||||
topBrandSlug: r.top_brand_slug,
|
||||
topBrandName: r.top_brand_name,
|
||||
topResultKind: r.top_result_kind,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Peak heatmap (hour-of-day × day-of-week, last 30d) ───────────────────
|
||||
export type HeatmapCell = {
|
||||
dayOfWeek: number; // 0 = Mon, 6 = Sun (ISO style)
|
||||
@@ -318,9 +399,9 @@ export type ProviderDeepStats = {
|
||||
provider: string;
|
||||
range: "7d" | "30d";
|
||||
// Won-by-this-provider window
|
||||
wins: { total: number; succeeded: number; successRate: number; avgMs: number | null; p95: number | null };
|
||||
wins: { total: number; succeeded: number; successRate: number; avgMs: number | null; p50: number | null; p95: number | null };
|
||||
// Attempted-in-chain window (timings key present)
|
||||
attempts: { total: number; avgMs: number; p95: number | null };
|
||||
attempts: { total: number; avgMs: number; p50: number | null; p95: number | null };
|
||||
// Daily win volume for the window
|
||||
dailyVolume: Array<{ date: string; count: number }>;
|
||||
topBrands: Array<{ brandSlug: string | null; brandName: string | null; count: number }>;
|
||||
@@ -360,6 +441,7 @@ export async function getProviderDeepStats(
|
||||
total: bigint;
|
||||
succeeded: bigint;
|
||||
avg_ms: number | null;
|
||||
p50: number | null;
|
||||
p95: number | null;
|
||||
}>
|
||||
>`
|
||||
@@ -367,6 +449,7 @@ export async function getProviderDeepStats(
|
||||
count(*) AS total,
|
||||
count(*) FILTER (WHERE success = true) AS succeeded,
|
||||
avg(response_time_ms)::int AS avg_ms,
|
||||
percentile_cont(0.50) WITHIN GROUP (ORDER BY response_time_ms)::int AS p50,
|
||||
percentile_cont(0.95) WITHIN GROUP (ORDER BY response_time_ms)::int AS p95
|
||||
FROM query_logs
|
||||
WHERE created_at >= ${start} AND source = ${provider}
|
||||
@@ -381,14 +464,15 @@ export async function getProviderDeepStats(
|
||||
// Attempts: timings key present (provider showed up in the chain even if it
|
||||
// didn't win). Some providers have no timings key (e.g. 'cache'), so this
|
||||
// can be empty.
|
||||
let attempts = { total: 0, avgMs: 0, p95: null as number | null };
|
||||
let attempts = { total: 0, avgMs: 0, p50: null as number | null, p95: null as number | null };
|
||||
if (timingsKey) {
|
||||
const [attemptsRow] = await saseDb.$queryRaw<
|
||||
Array<{ total: bigint; avg_ms: number | null; p95_ms: number | null }>
|
||||
Array<{ total: bigint; avg_ms: number | null; p50_ms: number | null; p95_ms: number | null }>
|
||||
>`
|
||||
SELECT
|
||||
count(*) AS total,
|
||||
avg((timings->>${timingsKey})::numeric)::int AS avg_ms,
|
||||
percentile_cont(0.50) WITHIN GROUP (ORDER BY (timings->>${timingsKey})::numeric)::int AS p50_ms,
|
||||
percentile_cont(0.95) WITHIN GROUP (ORDER BY (timings->>${timingsKey})::numeric)::int AS p95_ms
|
||||
FROM query_logs
|
||||
WHERE created_at >= ${start}
|
||||
@@ -399,6 +483,7 @@ export async function getProviderDeepStats(
|
||||
attempts = {
|
||||
total: Number(attemptsRow.total),
|
||||
avgMs: attemptsRow.avg_ms ?? 0,
|
||||
p50: attemptsRow.p50_ms,
|
||||
p95: attemptsRow.p95_ms,
|
||||
};
|
||||
}
|
||||
@@ -514,6 +599,7 @@ export async function getProviderDeepStats(
|
||||
succeeded: winsSucceeded,
|
||||
successRate: winsTotal > 0 ? winsSucceeded / winsTotal : 0,
|
||||
avgMs: winsRow?.avg_ms ?? null,
|
||||
p50: winsRow?.p50 ?? null,
|
||||
p95: winsRow?.p95 ?? null,
|
||||
},
|
||||
attempts,
|
||||
|
||||
Reference in New Issue
Block a user