defmodule PlausibleWeb.TrackerPlug do @moduledoc """ Plug to serve the Plausible tracker script. """ import Plug.Conn use Agent use Plausible base_variants = [ "hash", "outbound-links", "exclusions", "compat", "local", "manual", "file-downloads", "pageview-props", "tagged-events", "revenue", "pageleave" ] # Generates Power Set of all variants legacy_variants = 1..Enum.count(base_variants) |> Enum.map(fn x -> Combination.combine(base_variants, x) |> Enum.map(fn y -> Enum.sort(y) |> Enum.join(".") end) end) |> List.flatten() @base_legacy_filenames ["plausible", "script", "analytics"] @files_available ["plausible.js", "p.js"] ++ Enum.map(legacy_variants, fn v -> "plausible.#{v}.js" end) def init(opts) do Keyword.merge(opts, files_available: MapSet.new(@files_available)) end def call(conn, files_available: files_available) do case conn.request_path do "/js/pa-" <> path -> if String.ends_with?(path, ".js") do id = String.replace_trailing(path, ".js", "") request_tracker_script("pa-" <> id, conn) else conn end "/js/p.js" -> legacy_request_file("p.js", files_available, conn) "/js/" <> requested_filename -> sorted_script_variant(requested_filename) |> legacy_request_file(files_available, conn) _ -> conn end end def telemetry_event(name), do: [:plausible, :tracker_script, :request, name] defp request_tracker_script(id, conn) do case PlausibleWeb.Tracker.get_plausible_main_script(id) do script_tag when is_binary(script_tag) -> :telemetry.execute( telemetry_event(:v2), %{}, %{status: 200} ) conn |> put_resp_header("content-type", "application/javascript") |> put_resp_header("x-content-type-options", "nosniff") |> put_resp_header("cross-origin-resource-policy", "cross-origin") |> put_resp_header("access-control-allow-origin", "*") |> put_resp_header("cache-control", "public, max-age=60, no-transform") # CDN-Tag is used by BunnyCDN to tag cached resources. This allows us to purge # specific tracker scripts from the CDN cache. |> put_resp_header("cdn-tag", "tracker_script::#{id}") |> send_resp(200, script_tag) |> halt() nil -> :telemetry.execute( telemetry_event(:v2), %{}, %{status: 404} ) conn |> send_resp(404, "Not found") |> halt() end end defp legacy_request_file(filename, files_available, conn) do if filename && MapSet.member?(files_available, filename) do location = Application.app_dir(:plausible, "priv/tracker/js/" <> filename) :telemetry.execute( telemetry_event(:legacy), %{}, %{status: 200} ) conn |> put_resp_header("content-type", "application/javascript") |> put_resp_header("x-content-type-options", "nosniff") |> put_resp_header("cross-origin-resource-policy", "cross-origin") |> put_resp_header("access-control-allow-origin", "*") |> put_resp_header("cache-control", "public, max-age=86400, must-revalidate") |> send_file(200, location) |> halt() else conn end end # Variants which do not factor into output @ignore_variants ["js", "pageleave"] defp sorted_script_variant(requested_filename) do case String.split(requested_filename, ".") do [base_filename | rest] when base_filename in @base_legacy_filenames -> sorted_variants = rest |> Enum.reject(&(&1 in @ignore_variants)) |> Enum.sort() Enum.join(["plausible"] ++ sorted_variants ++ ["js"], ".") _ -> nil end end end