Current section

Files

Jump to
posthog lib posthog integrations plug.ex
Raw

lib/posthog/integrations/plug.ex

defmodule PostHog.Integrations.Plug do
@moduledoc """
Provides a plug that automatically extracts and sets relevant metadata from
`Plug.Conn`.
For Phoenix apps, add it to your `endpoint.ex` somewhere before your router:
plug PostHog.Integrations.Plug
For Plug apps, add it directly to your router:
defmodule MyRouterPlug do
use Plug.Router
plug PostHog.Integrations.Plug
plug :match
plug :dispatch
...
end
"""
@doc false
def init(opts), do: opts
@doc false
def call(conn, _opts) do
context = conn_to_context(conn)
PostHog.Context.set(:all, :all, context)
conn
end
@tracing_headers [
{"x-posthog-distinct-id", :distinct_id},
{"x-posthog-session-id", :"$session_id"}
]
@max_header_value_length 1000
@control_chars_regex ~r/[\x{00}-\x{1F}\x{7F}-\x{9F}]/u
@doc false
def conn_to_context(conn) when is_struct(conn, Plug.Conn) do
query_string = if conn.query_string == "", do: nil, else: conn.query_string
%{
"$current_url":
%URI{
scheme: to_string(conn.scheme),
host: conn.host,
path: conn.request_path,
query: query_string
}
|> URI.to_string(),
"$host": conn.host,
"$pathname": conn.request_path,
"$ip": remote_ip(conn),
"$request_method": conn.method
}
|> put_if_present(:"$user_agent", first_header_value(conn, "user-agent"))
|> Map.merge(tracing_context(conn))
end
defp tracing_context(conn) when is_struct(conn, Plug.Conn) do
Enum.reduce(@tracing_headers, %{}, fn {header_name, context_key}, context ->
put_if_present(context, context_key, header_value(conn, header_name))
end)
end
defp header_value(conn, header_name) when is_struct(conn, Plug.Conn) do
case req_header_values(conn, header_name) do
[value | _] when is_binary(value) -> sanitize_value(value)
_ -> nil
end
end
defp first_header_value(conn, header_name) when is_struct(conn, Plug.Conn) do
case req_header_values(conn, header_name) do
[value | _] when is_binary(value) and value != "" -> value
_ -> nil
end
end
defp req_header_values(conn, header_name) do
normalized_header_name = String.downcase(header_name)
# Avoid compilation warnings for cases where Plug isn't available
# credo:disable-for-lines:1
case apply(Plug.Conn, :get_req_header, [conn, normalized_header_name]) do
[] ->
for {key, value} <- conn.req_headers,
is_binary(key),
String.downcase(key) == normalized_header_name do
value
end
values ->
values
end
end
defp sanitize_value(value) when is_binary(value) do
value =
value
|> String.replace(@control_chars_regex, "")
|> String.trim()
|> String.slice(0, @max_header_value_length)
if value == "", do: nil, else: value
end
defp put_if_present(map, _key, nil), do: map
defp put_if_present(map, key, value), do: Map.put(map, key, value)
defp remote_ip(conn) when is_struct(conn, Plug.Conn) do
# Avoid compilation warnings for cases where Plug isn't available
# credo:disable-for-lines:2
remote_ip =
case apply(Plug.Conn, :get_req_header, [conn, "x-forwarded-for"]) do
[x_forwarded_for | _] ->
x_forwarded_for |> String.split(",", parts: 2) |> List.first()
[] ->
case :inet.ntoa(conn.remote_ip) do
{:error, _} -> ""
address -> to_string(address)
end
end
String.trim(remote_ip)
end
end