Добавить авто-регистрацию тикетов (hash/dedupe/500/WS). Refs EventHub/EventHubBack#35
CI / test (push) Successful in 21m45s
CI / deploy-ift (push) Successful in 6m8s
CI / e2e-ift (push) Failing after 6m13s
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped

This commit is contained in:
2026-07-17 00:44:09 +03:00
parent 0a52712258
commit a696d25486
12 changed files with 590 additions and 59 deletions
+34 -14
View File
@@ -4,7 +4,7 @@
%%% GET – получить список тикетов.
%%% Администраторы видят все тикеты,
%%% обычные пользователи – только свои.
%%% POST создать новый тикет об ошибке.
%%% POST создать/дедуплицировать тикет об ошибке через logic_ticket:report_error.
%%% @end
%%%-------------------------------------------------------------------
-module(handler_tickets).
@@ -47,7 +47,7 @@ trails() ->
#{ % POST create
path => <<"/v1/tickets">>,
method => <<"POST">>,
description => <<"Create a new ticket (bug report)">>,
description => <<"Create or bump a ticket (bug report / frontend error)">>,
tags => [<<"Tickets">>],
requestBody => #{
required => true,
@@ -57,14 +57,16 @@ trails() ->
properties => #{
error_message => #{type => string},
stacktrace => #{type => string},
context => #{type => string}
context => #{type => object},
source => #{type => string, enum => [<<"frontend">>, <<"manual">>]}
}
}}}
},
responses => #{
201 => #{description => <<"Ticket created">>},
201 => #{description => <<"Ticket created or updated">>},
400 => #{description => <<"Missing required fields or invalid JSON">>},
401 => #{description => <<"Unauthorized">>}
401 => #{description => <<"Unauthorized">>},
429 => #{description => <<"Rate limited">>}
}
}
].
@@ -84,7 +86,8 @@ ticket_schema() ->
last_seen => #{type => string, format => <<"date-time">>},
status => #{type => string, enum => [<<"open">>, <<"in_progress">>, <<"resolved">>, <<"closed">>]},
assigned_to => #{type => string, nullable => true},
resolution_note => #{type => string, nullable => true}
resolution_note => #{type => string, nullable => true},
source => #{type => string, enum => [<<"backend">>, <<"frontend">>, <<"manual">>]}
}
}.
@@ -118,21 +121,23 @@ list_tickets(Req) ->
handler_utils:send_error(Req1, Code, Message)
end.
%% @doc POST /v1/tickets — создание тикета.
%% @doc POST /v1/tickets — создание/дедуп тикета.
-spec create_ticket(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
create_ticket(Req) ->
case handler_utils:auth_user(Req) of
{ok, UserId, Req1} ->
{ok, Body, Req2} = cowboy_req:read_body(Req1),
try jsx:decode(Body, [return_maps]) of
#{<<"error_message">> := _} = Data ->
TicketData = maps:merge(
#{<<"reporter_id">> => UserId, <<"status">> => <<"open">>},
Data
),
case core_ticket:create_ticket(TicketData) of
#{<<"error_message">> := ErrorMessage} = Data ->
Stacktrace = maps:get(<<"stacktrace">>, Data, <<>>),
Source = resolve_source(Data, Stacktrace),
Context0 = maps:get(<<"context">>, Data, #{}),
Context = merge_context(Context0, UserId),
case logic_ticket:report_error(Source, ErrorMessage, Stacktrace, Context) of
{ok, Ticket} ->
handler_utils:send_json(Req2, 201, handler_utils:ticket_to_json(Ticket));
{error, rate_limited} ->
handler_utils:send_error(Req2, 429, <<"Too many new tickets">>);
{error, Reason} ->
handler_utils:send_error(Req2, 500, Reason)
end;
@@ -143,4 +148,19 @@ create_ticket(Req) ->
end;
{error, Code, Message, Req1} ->
handler_utils:send_error(Req1, Code, Message)
end.
end.
resolve_source(#{<<"source">> := <<"frontend">>}, _) -> frontend;
resolve_source(#{<<"source">> := <<"manual">>}, _) -> manual;
resolve_source(#{<<"source">> := <<"backend">>}, _) -> backend;
resolve_source(_, Stacktrace) when is_binary(Stacktrace), Stacktrace =/= <<>> ->
frontend;
resolve_source(_, _) ->
manual.
merge_context(Ctx, UserId) when is_map(Ctx) ->
Ctx#{<<"reporter_id">> => UserId};
merge_context(Ctx, UserId) when is_binary(Ctx) ->
#{<<"reporter_id">> => UserId, <<"raw">> => Ctx};
merge_context(_, UserId) ->
#{<<"reporter_id">> => UserId}.