From b48e244a99769c4e8d2482fc11497be899c36559 Mon Sep 17 00:00:00 2001 From: wxs Date: Fri, 10 Jul 2026 17:18:01 +0800 Subject: [PATCH] refactor: model independent spread metric rules --- src/content/market/spread-info.ts | 40 +++++++++--- src/content/market/types.ts | 22 +++++-- tests/spread-info.test.ts | 100 ++++++++++++++++++++++-------- 3 files changed, 121 insertions(+), 41 deletions(-) diff --git a/src/content/market/spread-info.ts b/src/content/market/spread-info.ts index 66673b0..44d351d 100644 --- a/src/content/market/spread-info.ts +++ b/src/content/market/spread-info.ts @@ -1,4 +1,9 @@ -import type { SpreadInfoMetrics, SpreadMetricThresholds } from "./types"; +import type { + SpreadInfoConfig, + SpreadInfoMetrics, + SpreadMetricFilterRule, + SpreadMetricThresholds +} from "./types"; interface FetchResponseLike { json(): Promise; @@ -10,13 +15,6 @@ type FetchLike = ( init?: RequestInit ) => Promise; -export interface SpreadInfoConfig { - flowType: 0 | 1; - onlyAssign: boolean; - range: 2 | 3; - type: 1 | 2; -} - interface SpreadInfoClientOptions { baseUrl?: string; configs?: SpreadInfoConfig[]; @@ -222,6 +220,32 @@ export function matchesSpreadThresholds( }); } +export function normalizeSpreadInfoConfig( + config: SpreadInfoConfig +): SpreadInfoConfig { + return config.type === 1 + ? { ...config, flowType: 0, onlyAssign: false } + : { ...config }; +} + +export function buildSpreadInfoConfigKey(config: SpreadInfoConfig): string { + const normalized = normalizeSpreadInfoConfig(config); + return [ + normalized.type, + normalized.onlyAssign ? 1 : 0, + normalized.flowType, + normalized.range + ].join(":"); +} + +export function matchesSpreadMetricRule( + metrics: MappedSpreadInfoResponse, + rule: SpreadMetricFilterRule +): boolean { + const numericValue = readDisplayNumber(metrics[rule.metric]); + return numericValue !== null && numericValue >= rule.threshold; +} + function buildSpreadInfoColumnHeader( config: SpreadInfoConfig, metric: SpreadInfoMetricDefinition diff --git a/src/content/market/types.ts b/src/content/market/types.ts index 789014d..e90247f 100644 --- a/src/content/market/types.ts +++ b/src/content/market/types.ts @@ -14,6 +14,21 @@ export interface BackendMetrics { export type SpreadInfoMetrics = Record; +export interface SpreadInfoConfig { + flowType: 0 | 1; + onlyAssign: boolean; + range: 2 | 3; + type: 1 | 2; +} + +export type SpreadFilterMetric = "finishRate" | "interactionRate"; + +export interface SpreadMetricFilterRule { + config: SpreadInfoConfig; + metric: SpreadFilterMetric; + threshold: number; +} + export interface SpreadMetricThresholds { averageCommentCount?: number; averageDuration?: number; @@ -25,12 +40,7 @@ export interface SpreadMetricThresholds { } export interface SpreadThresholdFilter { - config: { - flowType: 0 | 1; - onlyAssign: boolean; - range: 2 | 3; - type: 1 | 2; - }; + config: SpreadInfoConfig; thresholds: SpreadMetricThresholds; } diff --git a/tests/spread-info.test.ts b/tests/spread-info.test.ts index 4ff7fc7..1947ba2 100644 --- a/tests/spread-info.test.ts +++ b/tests/spread-info.test.ts @@ -1,11 +1,13 @@ import { describe, expect, test, vi } from "vitest"; import { + buildSpreadInfoConfigKey, buildSpreadInfoColumns, buildSpreadInfoUrl, createSpreadInfoClient, DEFAULT_SPREAD_INFO_CONFIGS, - matchesSpreadThresholds, + matchesSpreadMetricRule, + normalizeSpreadInfoConfig, mapSpreadInfoResponse } from "../src/content/market/spread-info"; @@ -167,46 +169,90 @@ describe("spread-info", () => { ); }); - test("matches thresholds using display values and requires every filled threshold", () => { + test("normalizes fixed personal-video parameters before grouping", () => { expect( - matchesSpreadThresholds( + normalizeSpreadInfoConfig({ + flowType: 1, + onlyAssign: true, + range: 3, + type: 1 + }) + ).toEqual({ + flowType: 0, + onlyAssign: false, + range: 3, + type: 1 + }); + }); + + test("uses all normalized video dimensions in the config key", () => { + expect( + buildSpreadInfoConfigKey({ + flowType: 1, + onlyAssign: true, + range: 3, + type: 1 + }) + ).toBe( + buildSpreadInfoConfigKey({ + flowType: 0, + onlyAssign: false, + range: 3, + type: 1 + }) + ); + + expect( + buildSpreadInfoConfigKey({ + flowType: 0, + onlyAssign: false, + range: 2, + type: 2 + }) + ).not.toBe( + buildSpreadInfoConfigKey({ + flowType: 0, + onlyAssign: false, + range: 3, + type: 2 + }) + ); + }); + + test("matches only the metric named by one filter rule", () => { + expect( + matchesSpreadMetricRule( { - averageDuration: "56", finishRate: "28.24%", - interactionRate: "4.02%", - playMedian: "10913233" + interactionRate: "4.02%" }, { - averageDuration: 50, - finishRate: 28, - interactionRate: 4, - playMedian: 10000000 + config: { + flowType: 0, + onlyAssign: false, + range: 2, + type: 1 + }, + metric: "finishRate", + threshold: 28 } ) ).toBe(true); expect( - matchesSpreadThresholds( - { - averageDuration: "56", - finishRate: "28.24%", - interactionRate: "4.02%", - playMedian: "10913233" - }, - { - averageDuration: 57 - } - ) - ).toBe(false); - - expect( - matchesSpreadThresholds( + matchesSpreadMetricRule( { finishRate: "28.24%" }, { - finishRate: 20, - interactionRate: 1 + config: { + flowType: 0, + onlyAssign: false, + range: 2, + type: 1 + }, + metric: "interactionRate", + threshold: 1 } ) ).toBe(false);