Skip to content

Commit b45d5e9

Browse files
authored
Supervise user agent parsing (#5243)
* Supervise user agent parsing * Instrument UA parse timeout with a counter * Test
1 parent 1b4aefb commit b45d5e9

File tree

4 files changed

+73
-5
lines changed

4 files changed

+73
-5
lines changed

lib/plausible/application.ex

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ defmodule Plausible.Application do
1515

1616
children =
1717
[
18+
{Task.Supervisor, name: Plausible.UserAgentParseTaskSupervisor},
1819
Plausible.Session.BalancerSupervisor,
1920
Plausible.Cache.Stats,
2021
Plausible.PromEx,

lib/plausible/ingestion/event.ex

+33-5
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,21 @@ defmodule Plausible.Ingestion.Event do
8888
[:plausible, :ingest, :event, :dropped]
8989
end
9090

91+
@spec telemetry_pipeline_step_duration() :: [atom()]
9192
def telemetry_pipeline_step_duration() do
9293
[:plausible, :ingest, :pipeline, :step]
9394
end
9495

96+
@spec telemetry_ua_parse_timeout() :: [atom()]
97+
def telemetry_ua_parse_timeout() do
98+
[:plausible, :ingest, :user_agent_parse, :timeout]
99+
end
100+
101+
@spec emit_telemetry_ua_parse_timeout() :: :ok
102+
def emit_telemetry_ua_parse_timeout() do
103+
:telemetry.execute(telemetry_ua_parse_timeout(), %{}, %{})
104+
end
105+
95106
@spec emit_telemetry_buffered(t()) :: :ok
96107
def emit_telemetry_buffered(event) do
97108
:telemetry.execute(telemetry_event_buffered(), %{}, %{
@@ -238,13 +249,13 @@ defmodule Plausible.Ingestion.Event do
238249

239250
defp put_user_agent(%__MODULE__{} = event, _context) do
240251
case parse_user_agent(event.request) do
241-
%UAInspector.Result{client: %UAInspector.Result.Client{name: "Headless Chrome"}} ->
252+
{:ok, %UAInspector.Result{client: %UAInspector.Result.Client{name: "Headless Chrome"}}} ->
242253
drop(event, :bot)
243254

244-
%UAInspector.Result.Bot{} ->
255+
{:ok, %UAInspector.Result.Bot{}} ->
245256
drop(event, :bot)
246257

247-
%UAInspector.Result{} = user_agent ->
258+
{:ok, %UAInspector.Result{} = user_agent} ->
248259
update_session_attrs(event, %{
249260
operating_system: os_name(user_agent),
250261
operating_system_version: os_version(user_agent),
@@ -430,14 +441,31 @@ defmodule Plausible.Ingestion.Event do
430441
|> Enum.find(fn param_name -> Map.has_key?(query_params, param_name) end)
431442
end
432443

444+
@parse_user_agent_timeout 200
433445
defp parse_user_agent(%Request{user_agent: user_agent}) when is_binary(user_agent) do
434-
Plausible.Cache.Adapter.get(:user_agents, user_agent, fn ->
435-
UAInspector.parse(user_agent)
446+
Plausible.Cache.Adapter.fetch(:user_agents, user_agent, fn ->
447+
parse_user_agent_safe(user_agent)
436448
end)
437449
end
438450

439451
defp parse_user_agent(request), do: request
440452

453+
defp parse_user_agent_safe(user_agent) do
454+
task =
455+
Task.Supervisor.async_nolink(Plausible.UserAgentParseTaskSupervisor, fn ->
456+
UAInspector.parse(user_agent)
457+
end)
458+
459+
case Task.yield(task, @parse_user_agent_timeout) || Task.shutdown(task) do
460+
{:ok, result} ->
461+
{:ok, result}
462+
463+
nil ->
464+
emit_telemetry_ua_parse_timeout()
465+
{:error, :timeout}
466+
end
467+
end
468+
441469
defp browser_name(ua) do
442470
case ua.client do
443471
:unknown -> ""

lib/plausible/telemetry/plausible_metrics.ex

+4
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ defmodule Plausible.PromEx.Plugins.PlausibleMetrics do
7474
metric_prefix ++ [:ingest, :events, :dropped, :total],
7575
event_name: Ingestion.Event.telemetry_event_dropped(),
7676
tags: [:reason]
77+
),
78+
counter(
79+
metric_prefix ++ [:ingest, :user_agent_parse, :timeout, :total],
80+
event_name: Ingestion.Event.telemetry_ua_parse_timeout()
7781
)
7882
]
7983
)

test/plausible/ingestion/event_test.exs

+35
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,41 @@ defmodule Plausible.Ingestion.EventTest do
4545
end
4646
end
4747

48+
test "times out parsing user agent", %{test: test} do
49+
on_exit(:detach, fn ->
50+
:telemetry.detach("ua-timeout-#{test}")
51+
end)
52+
53+
test_pid = self()
54+
event = Event.telemetry_ua_parse_timeout()
55+
56+
:telemetry.attach(
57+
"ua-timeout-#{test}",
58+
event,
59+
fn ^event, _, _, _ ->
60+
send(test_pid, :telemetry_handled)
61+
end,
62+
%{}
63+
)
64+
65+
site = new_site()
66+
67+
payload = %{
68+
name: "pageview",
69+
url: "http://#{site.domain}"
70+
}
71+
72+
conn =
73+
:post
74+
|> build_conn("/api/events", payload)
75+
|> Plug.Conn.put_req_header("user-agent", :binary.copy("a", 1024 * 8))
76+
77+
assert {:ok, request} = Request.build(conn)
78+
79+
assert {:ok, %{buffered: [_], dropped: []}} = Event.build_and_buffer(request)
80+
assert_receive :telemetry_handled
81+
end
82+
4883
test "drops verification agent" do
4984
site = new_site()
5085

0 commit comments

Comments
 (0)