f4d89a262c
Канон PRODUCT-AI (Spec#22): пилотные метрики L1-подсказок для расчёта
голосового гейта L2 (>=20% активных пользователей с >=2 подтверждённых
ghost-бронирований в месяц). Локальные счётчики в Mnesia, без LLM.
- таблица ai_hint_metric (миграция 20260820180000): счётчики
{user, день UTC, pattern, kind}; ленивый prune при >500 строк на
пользователя, retention 92 дня
- POST /v1/ai/hint-metrics (Bearer): батч <=20 событий shown/confirm/dismiss,
202 {accepted:N}; ETS rate limit 30 req/min на пользователя
- GET /v1/admin/ai-metrics/stats (админ-порт): by_kind, active_users_30d,
users_with_confirms_ge2_30d, gate_ratio_30d, top_patterns_by_confirm
- eunit logic_ai_metrics_tests (5/5); синхронизация списка миграций
в migration_engine_tests
Refs EventHub/EventHubBack#78
76 lines
3.3 KiB
Erlang
76 lines
3.3 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% EUnit: пилотные метрики ИИ-подсказок (logic_ai_metrics).
|
|
%%%-------------------------------------------------------------------
|
|
-module(logic_ai_metrics_tests).
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include("records.hrl").
|
|
|
|
-define(TABLES, [ai_hint_metric]).
|
|
|
|
setup() ->
|
|
catch ets:delete(eventhub_ai_metrics_rl),
|
|
eh_test_support:start_mnesia(),
|
|
eh_test_support:ensure_tables(?TABLES),
|
|
ok.
|
|
|
|
cleanup(_) ->
|
|
eh_test_support:delete_tables(?TABLES),
|
|
eh_test_support:stop_mnesia(),
|
|
catch ets:delete(eventhub_ai_metrics_rl),
|
|
ok.
|
|
|
|
logic_ai_metrics_test_() ->
|
|
{foreach, fun setup/0, fun cleanup/1, [
|
|
{"invalid body rejected", fun test_invalid_body/0},
|
|
{"unknown kind rejected", fun test_unknown_kind/0},
|
|
{"counters increment per key", fun test_increment/0},
|
|
{"batch over limit rejected", fun test_batch_limit/0},
|
|
{"summary counts kinds and gate ratio", fun test_summary/0}
|
|
]}.
|
|
|
|
event(Kind, Pattern) ->
|
|
#{<<"kind">> => Kind, <<"pattern">> => Pattern}.
|
|
|
|
test_invalid_body() ->
|
|
?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, [])),
|
|
?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, not_a_list)),
|
|
?assertEqual({error, invalid_body},
|
|
logic_ai_metrics:record_events(<<"u1">>, [#{<<"kind">> => <<"shown">>}])).
|
|
|
|
test_unknown_kind() ->
|
|
?assertEqual({error, invalid_body},
|
|
logic_ai_metrics:record_events(<<"u1">>, [event(<<"click">>, <<"p">>)])),
|
|
?assertEqual({error, invalid_body},
|
|
logic_ai_metrics:record_events(<<"u1">>, [event(<<"shown">>, <<>>)])).
|
|
|
|
test_increment() ->
|
|
{ok, 2} = logic_ai_metrics:record_events(<<"u1">>,
|
|
[event(<<"shown">>, <<"cal|2|10">>), event(<<"confirm">>, <<"cal|2|10">>)]),
|
|
{ok, 1} = logic_ai_metrics:record_events(<<"u1">>, [event(<<"shown">>, <<"cal|2|10">>)]),
|
|
[Row] = mnesia:dirty_match_object(#ai_hint_metric{
|
|
id = {<<"u1">>, '_', <<"cal|2|10">>, <<"shown">>}, _ = '_'}),
|
|
?assertEqual(2, Row#ai_hint_metric.count),
|
|
[_] = mnesia:dirty_match_object(#ai_hint_metric{
|
|
id = {<<"u1">>, '_', <<"cal|2|10">>, <<"confirm">>}, _ = '_'}).
|
|
|
|
test_batch_limit() ->
|
|
Events = [event(<<"shown">>, iolist_to_binary(["p", integer_to_list(N)]))
|
|
|| N <- lists:seq(1, 21)],
|
|
?assertEqual({error, invalid_body}, logic_ai_metrics:record_events(<<"u1">>, Events)).
|
|
|
|
test_summary() ->
|
|
{ok, _} = logic_ai_metrics:record_events(<<"u1">>,
|
|
[event(<<"shown">>, <<"a">>), event(<<"confirm">>, <<"a">>),
|
|
event(<<"confirm">>, <<"a">>), event(<<"dismiss">>, <<"b">>)]),
|
|
{ok, _} = logic_ai_metrics:record_events(<<"u2">>,
|
|
[event(<<"shown">>, <<"a">>), event(<<"confirm">>, <<"a">>)]),
|
|
S = logic_ai_metrics:summary(),
|
|
?assertEqual(#{<<"shown">> => 2, <<"confirm">> => 3, <<"dismiss">> => 1},
|
|
maps:from_list(maps:to_list(maps:get(<<"by_kind">>, S)))),
|
|
?assertEqual(2, maps:get(<<"active_users_30d">>, S)),
|
|
?assertEqual(1, maps:get(<<"users_with_confirms_ge2_30d">>, S)),
|
|
?assertEqual(0.5, maps:get(<<"gate_ratio_30d">>, S)),
|
|
[Top | _] = maps:get(<<"top_patterns_by_confirm">>, S),
|
|
?assertEqual(<<"a">>, maps:get(<<"pattern">>, Top)),
|
|
?assertEqual(3, maps:get(<<"confirms">>, Top)).
|