From b3bcc2af4599f5690f34cc822a71f2de0ed0e012 Mon Sep 17 00:00:00 2001 From: admin123 Date: Thu, 23 Apr 2026 17:41:33 +0800 Subject: [PATCH] fix: derive missing A3 batch metrics --- src/shared/backend-metrics-client.ts | 89 +++++++++++++++++++++++++++- tests/backend-metrics-client.test.ts | 34 +++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/src/shared/backend-metrics-client.ts b/src/shared/backend-metrics-client.ts index 309ba43..b36f62e 100644 --- a/src/shared/backend-metrics-client.ts +++ b/src/shared/backend-metrics-client.ts @@ -76,11 +76,13 @@ export function mapBackendMetricsSearchResponse(payload: unknown): BackendMetric return [ { - a3IncreaseCount: formatDecimalValue(row.avg_a3_increase_cnt), + a3IncreaseCount: formatDecimalValue( + readAverageA3IncreaseCount(row) + ), afterViewSearchCount: formatDecimalValue(row.avg_after_view_search_cnt), afterViewSearchRate: formatRateValue(row.avg_after_view_search_rate), cpSearch: formatDecimalValue(row.cp_search), - cpa3: formatDecimalValue(row.cpa3), + cpa3: formatDecimalValue(readCpa3Value(row)), newA3Rate: formatRateValue(row.avg_new_a3_rate), starId: row.star_id } @@ -88,6 +90,84 @@ export function mapBackendMetricsSearchResponse(payload: unknown): BackendMetric }); } +function readAverageA3IncreaseCount(row: Record): number | null { + const directAverage = readFiniteNumber(row.avg_a3_increase_cnt); + if (directAverage !== null) { + return directAverage; + } + + const totalNewA3 = readTotalNewA3Value(row); + const videoCount = + readFiniteNumber(row.video_count) ?? readNestedVideoCount(row.videos); + if (totalNewA3 === null || videoCount === null || videoCount <= 0) { + return null; + } + + return totalNewA3 / videoCount; +} + +function readCpa3Value(row: Record): number | null { + const directCpa3 = readFiniteNumber(row.cpa3); + if (directCpa3 !== null) { + return directCpa3; + } + + const totalCost = readFiniteNumber(row.total_estimated_video_cost); + const totalNewA3 = readTotalNewA3Value(row); + if (totalCost === null || totalNewA3 === null || totalNewA3 <= 0) { + return null; + } + + return totalCost / totalNewA3; +} + +function readTotalNewA3Value(row: Record): number | null { + const derivedFromTotals = deriveTotalNewA3FromTotals(row); + if (derivedFromTotals !== null) { + return derivedFromTotals; + } + + return deriveTotalNewA3FromVideos(row.videos); +} + +function deriveTotalNewA3FromTotals(row: Record): number | null { + const totalPlayCount = readFiniteNumber(row.total_play_cnt); + const averageNewA3Rate = readFiniteNumber(row.avg_new_a3_rate); + if (totalPlayCount === null || averageNewA3Rate === null) { + return null; + } + + return totalPlayCount * averageNewA3Rate; +} + +function deriveTotalNewA3FromVideos(value: unknown): number | null { + if (!Array.isArray(value)) { + return null; + } + + let total = 0; + let hasFiniteValue = false; + value.forEach((video) => { + if (!isRecord(video)) { + return; + } + + const newA3 = readFiniteNumber(video.new_a3); + if (newA3 === null) { + return; + } + + hasFiniteValue = true; + total += newA3; + }); + + return hasFiniteValue ? total : null; +} + +function readNestedVideoCount(value: unknown): number | null { + return Array.isArray(value) ? value.length : null; +} + function readResponseRows(payload: unknown): unknown[] | null { if (!isRecord(payload) || payload.success !== true) { return null; @@ -130,3 +210,8 @@ async function defaultFetch(input: string, init?: RequestInit) { function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } + +function readFiniteNumber(value: unknown): number | null { + const number = typeof value === "number" ? value : Number(value); + return Number.isFinite(number) ? number : null; +} diff --git a/tests/backend-metrics-client.test.ts b/tests/backend-metrics-client.test.ts index b09846a..73620d9 100644 --- a/tests/backend-metrics-client.test.ts +++ b/tests/backend-metrics-client.test.ts @@ -63,6 +63,40 @@ describe("backend-metrics-client", () => { ]); }); + test("derives A3 count and CPA3 from the live aggregate response shape", () => { + expect( + mapBackendMetricsSearchResponse({ + data: { + data: [ + { + avg_after_view_search_cnt: 25982, + avg_after_view_search_rate: 0.0010872130261527625, + avg_new_a3_rate: 0.11075860229946684, + cp_search: 21.168501270110077, + cpe: 0.630604497471276, + cpm: 23.014670324994974, + star_id: "7021245050621263906", + total_estimated_video_cost: 1100000, + total_play_cnt: 47795601, + video_count: 2 + } + ] + }, + success: true + }) + ).toEqual([ + { + a3IncreaseCount: "2,646,886.98", + afterViewSearchCount: "25,982.00", + afterViewSearchRate: "0.11%", + cpSearch: "21.17", + cpa3: "0.21", + newA3Rate: "11.08%", + starId: "7021245050621263906" + } + ]); + }); + test("posts star ids with bearer auth when searching backend metrics", async () => { const fetchImpl = async (_input: string, init?: RequestInit) => ({ json: async () => ({