Добавить авто-регистрацию тикетов (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
+88 -4
View File
@@ -27,7 +27,9 @@
subscription_to_json/1,
trails_for_crud/4,
is_superadmin/1,
pagination_headers/2
pagination_headers/2,
maybe_report_internal_error/3,
report_and_send_error/4
]).
-export([admin_to_json/1, audit_to_json/1]).
@@ -91,15 +93,91 @@ send_json(Req, Status, Data, ExtraHeaders) ->
Req1 = cowboy_req:reply(Status, Headers, Body, Req),
{ok, Body, Req1}.
%% @doc Отправляет JSON-ошибку.
-spec send_error(cowboy_req:req(), cowboy:http_status(), binary()) ->
%% @doc Отправляет JSON-ошибку. При Status >= 500 асинхронно регистрирует тикет.
-spec send_error(cowboy_req:req(), cowboy:http_status(), binary() | term()) ->
{ok, binary(), cowboy_req:req()}.
send_error(Req, Status, Message) when Status >= 500 ->
MsgBin = ensure_error_binary(Message),
maybe_report_internal_error(Req, MsgBin, #{}),
do_send_error(Req, Status, MsgBin);
send_error(Req, Status, Message) ->
do_send_error(Req, Status, ensure_error_binary(Message)).
%% @doc Отправить ошибку и явно зарегистрировать тикет с доп. контекстом.
-spec report_and_send_error(cowboy_req:req(), cowboy:http_status(),
binary() | term(), map()) -> {ok, binary(), cowboy_req:req()}.
report_and_send_error(Req, Status, Message, Context) when Status >= 500 ->
MsgBin = ensure_error_binary(Message),
maybe_report_internal_error(Req, MsgBin, Context),
do_send_error(Req, Status, MsgBin);
report_and_send_error(Req, Status, Message, _Context) ->
do_send_error(Req, Status, ensure_error_binary(Message)).
%% @doc Асинхронно зарегистрировать внутреннюю ошибку как тикет (backend).
-spec maybe_report_internal_error(cowboy_req:req() | map(), binary() | term(), map()) -> ok.
maybe_report_internal_error(ReqOrCtx, Message, ExtraContext) ->
case get(eventhub_reporting_ticket) of
true -> ok;
_ ->
case should_skip_ticket_report(ReqOrCtx) of
true -> ok;
false ->
MsgBin = ensure_error_binary(Message),
Context = maps:merge(request_error_context(ReqOrCtx), ExtraContext),
Stack = maps:get(<<"stacktrace">>, Context, <<>>),
Context1 = maps:remove(<<"stacktrace">>, Context),
spawn(fun() ->
try logic_ticket:report_error(backend, MsgBin, Stack, Context1)
catch
_:_ -> ok
end
end),
ok
end
end.
should_skip_ticket_report(Ctx) when is_map(Ctx) ->
Route = maps:get(<<"route">>, Ctx, maps:get(route, Ctx, <<>>)),
is_ticket_route(ensure_error_binary(Route));
should_skip_ticket_report(Req) ->
try is_ticket_route(cowboy_req:path(Req))
catch
_:_ -> false
end.
is_ticket_route(Path) when is_binary(Path) ->
binary:match(Path, <<"/tickets">>) =/= nomatch;
is_ticket_route(_) ->
false.
do_send_error(Req, Status, Message) ->
Body = jsx:encode(#{error => Message}),
Headers = #{<<"content-type">> => <<"application/json">>},
Req1 = cowboy_req:reply(Status, Headers, Body, Req),
{ok, Body, Req1}.
ensure_error_binary(V) when is_binary(V) -> V;
ensure_error_binary(V) when is_atom(V) -> atom_to_binary(V, utf8);
ensure_error_binary(V) when is_list(V) ->
try unicode:characters_to_binary(V)
catch _:_ -> list_to_binary(io_lib:format("~p", [V]))
end;
ensure_error_binary(V) ->
list_to_binary(io_lib:format("~p", [V])).
request_error_context(Ctx) when is_map(Ctx) -> Ctx;
request_error_context(Req) ->
try
Method = cowboy_req:method(Req),
Path = cowboy_req:path(Req),
#{
<<"method">> => Method,
<<"route">> => Path
}
catch
_:_ -> #{}
end.
%%%===================================================================
%%% Парсинг параметров запроса
%%%===================================================================
@@ -330,9 +408,15 @@ ticket_to_json(Ticket) ->
status => Ticket#ticket.status,
assigned_to => Ticket#ticket.assigned_to,
resolution_note => Ticket#ticket.resolution_note,
closed_at => datetime_to_iso8601(Ticket#ticket.closed_at)
closed_at => datetime_to_iso8601(Ticket#ticket.closed_at),
source => ticket_source(Ticket)
}.
ticket_source(#ticket{source = Source}) when is_binary(Source), Source =/= <<>> ->
Source;
ticket_source(_) ->
<<"backend">>.
%% @doc Преобразует #calendar{} в JSON-карту.
-spec calendar_to_json(#calendar{}) -> map().
calendar_to_json(Calendar) ->