236 lines
8.0 KiB
Elixir
236 lines
8.0 KiB
Elixir
defmodule PlausibleWeb.Tracker do
|
|
@moduledoc """
|
|
Helper module for building the dynamic tracker script. Used by PlausibleWeb.TrackerPlug.
|
|
"""
|
|
|
|
use Plausible
|
|
use Plausible.Repo
|
|
alias Plausible.Site.TrackerScriptConfiguration
|
|
import Ecto.Query
|
|
|
|
path = Application.app_dir(:plausible, "priv/tracker/js/plausible-web.js")
|
|
# On CI, the file might not be present for static checks so we create an empty one
|
|
File.touch!(path)
|
|
|
|
@plausible_main_script File.read!(path)
|
|
@external_resource "priv/tracker/js/plausible-web.js"
|
|
|
|
@spec get_plausible_main_script(String.t(), Keyword.t()) :: String.t() | nil
|
|
def get_plausible_main_script(id, cache_opts \\ []) do
|
|
on_ee do
|
|
# On cloud:
|
|
# 1. Check if tracker script ID is in the cache
|
|
# 2. If it is, generate the script on the fly
|
|
#
|
|
# Note that EE is relying on CDN caching the script
|
|
if PlausibleWeb.TrackerScriptCache.get(id, cache_opts) do
|
|
get_tracker_script_configuration_by_id(id)
|
|
|> build_script()
|
|
end
|
|
else
|
|
# On self-hosted, we have a pre-warmed cache for the script
|
|
PlausibleWeb.TrackerScriptCache.get(id, cache_opts)
|
|
end
|
|
end
|
|
|
|
# Exposed for testing
|
|
def plausible_main_config(
|
|
%TrackerScriptConfiguration{site: %{domain: _domain}} = tracker_script_configuration
|
|
) do
|
|
%{
|
|
domain: tracker_script_configuration.site.domain,
|
|
endpoint: tracker_ingestion_endpoint(),
|
|
outboundLinks: tracker_script_configuration.outbound_links,
|
|
fileDownloads: tracker_script_configuration.file_downloads,
|
|
formSubmissions: tracker_script_configuration.form_submissions
|
|
}
|
|
end
|
|
|
|
def build_script(
|
|
%TrackerScriptConfiguration{site: %{domain: _domain}} = tracker_script_configuration
|
|
) do
|
|
config_js_content =
|
|
tracker_script_configuration
|
|
|> plausible_main_config()
|
|
|> Enum.flat_map(fn
|
|
{key, value} when is_binary(value) -> ["#{key}:#{JSON.encode!(value)}"]
|
|
# :TRICKY: Save bytes by using short-hand for true
|
|
{key, true} -> ["#{key}:!0"]
|
|
# Not enabled values can be omitted
|
|
{_key, false} -> []
|
|
end)
|
|
|> Enum.sort_by(&String.length/1, :desc)
|
|
|> Enum.join(",")
|
|
|
|
@plausible_main_script
|
|
|> String.replace("\"<%= @config_js %>\"", "{#{config_js_content}}")
|
|
end
|
|
|
|
def build_script(nil), do: nil
|
|
|
|
defp broadcast_script_upsert(tracker_script_configuration) do
|
|
PlausibleWeb.TrackerScriptCache.broadcast_put(
|
|
tracker_script_configuration.id,
|
|
PlausibleWeb.TrackerScriptCache.cache_content(tracker_script_configuration)
|
|
)
|
|
end
|
|
|
|
def update_script_configuration(site, config_update, changeset_type) do
|
|
Repo.transact(fn ->
|
|
with {:ok, original_config} <- get_or_create_tracker_script_configuration(site),
|
|
changeset <- changeset(original_config, config_update, changeset_type),
|
|
{:ok, updated_config} <-
|
|
Repo.update(changeset),
|
|
%TrackerScriptConfiguration{} = reloaded_config <-
|
|
maybe_reload_tracker_script_configuration(updated_config) do
|
|
sync_goals(site, original_config, reloaded_config)
|
|
|
|
on_ee do
|
|
if should_purge_cache?(changeset) do
|
|
purge_cache!(reloaded_config.id)
|
|
end
|
|
else
|
|
:ok = broadcast_script_upsert(reloaded_config)
|
|
end
|
|
|
|
{:ok, reloaded_config}
|
|
end
|
|
end)
|
|
end
|
|
|
|
on_ee do
|
|
def purge_tracker_script_cache!(site) do
|
|
tracker_script_configuration = get_or_create_tracker_script_configuration!(site)
|
|
purge_cache!(tracker_script_configuration.id)
|
|
end
|
|
|
|
defp should_purge_cache?(changeset) do
|
|
Map.keys(changeset.changes) != [:installation_type]
|
|
end
|
|
|
|
defp purge_cache!(config_id) do
|
|
Plausible.Workers.PurgeCDNCache.new(
|
|
%{id: config_id},
|
|
# See PurgeCDNCache.ex for more details
|
|
schedule_in: 10,
|
|
replace: [scheduled: [:scheduled_at]]
|
|
)
|
|
|> Oban.insert!()
|
|
end
|
|
else
|
|
def purge_tracker_script_cache!(_site), do: nil
|
|
end
|
|
|
|
def get_tracker_script_configuration(site) do
|
|
Repo.get_by(TrackerScriptConfiguration, site_id: site.id)
|
|
end
|
|
|
|
def update_script_configuration!(site, config_update, changeset_type) do
|
|
{:ok, updated_config} = update_script_configuration(site, config_update, changeset_type)
|
|
updated_config
|
|
end
|
|
|
|
def get_or_create_tracker_script_configuration(site, params \\ %{}) do
|
|
configuration = get_tracker_script_configuration(site)
|
|
|
|
if configuration do
|
|
{:ok, configuration}
|
|
else
|
|
Repo.transact(fn ->
|
|
with {:ok, created_config} <-
|
|
Repo.insert(
|
|
TrackerScriptConfiguration.installation_changeset(
|
|
%TrackerScriptConfiguration{site_id: site.id},
|
|
params
|
|
)
|
|
),
|
|
%TrackerScriptConfiguration{} = reloaded_config <-
|
|
maybe_reload_tracker_script_configuration(created_config) do
|
|
sync_goals(site, %{}, reloaded_config)
|
|
|
|
:ok = broadcast_script_upsert(reloaded_config)
|
|
{:ok, reloaded_config}
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
def get_or_create_tracker_script_configuration!(site, params \\ %{}) do
|
|
{:ok, config} = get_or_create_tracker_script_configuration(site, params)
|
|
config
|
|
end
|
|
|
|
on_ee do
|
|
def supported_installation_types do
|
|
["manual", "wordpress", "gtm", "npm"]
|
|
end
|
|
else
|
|
def supported_installation_types do
|
|
["manual", "wordpress", "npm"]
|
|
end
|
|
end
|
|
|
|
def fallback_installation_type do
|
|
"manual"
|
|
end
|
|
|
|
def get_tracker_script_configuration_base_query() do
|
|
from(t in TrackerScriptConfiguration,
|
|
join: s in assoc(t, :site),
|
|
select: %{t | site: %{domain: s.domain}}
|
|
)
|
|
end
|
|
|
|
def get_tracker_script_configuration_by_id(id) do
|
|
get_tracker_script_configuration_base_query()
|
|
|> where([t], t.id == ^id)
|
|
|> Plausible.Repo.one()
|
|
end
|
|
|
|
on_ee do
|
|
defp maybe_reload_tracker_script_configuration(tracker_script_configuration),
|
|
do: tracker_script_configuration
|
|
else
|
|
# This loads the necessary associations (:site), that aren't returned with inserts and updates
|
|
defp maybe_reload_tracker_script_configuration(tracker_script_configuration),
|
|
do: get_tracker_script_configuration_by_id(tracker_script_configuration.id)
|
|
end
|
|
|
|
# Sync plausible goals with the updated script config
|
|
defp sync_goals(site, original_config, updated_config) do
|
|
[:track_404_pages, :outbound_links, :file_downloads, :form_submissions]
|
|
|> Enum.map(fn key ->
|
|
{key, Map.get(original_config, key, false), Map.get(updated_config, key, false)}
|
|
end)
|
|
|> Enum.each(fn
|
|
{:track_404_pages, false, true} -> Plausible.Goals.create_404(site)
|
|
{:track_404_pages, true, false} -> Plausible.Goals.delete_404(site)
|
|
{:outbound_links, false, true} -> Plausible.Goals.create_outbound_links(site)
|
|
{:outbound_links, true, false} -> Plausible.Goals.delete_outbound_links(site)
|
|
{:file_downloads, false, true} -> Plausible.Goals.create_file_downloads(site)
|
|
{:file_downloads, true, false} -> Plausible.Goals.delete_file_downloads(site)
|
|
{:form_submissions, false, true} -> Plausible.Goals.create_form_submissions(site)
|
|
{:form_submissions, true, false} -> Plausible.Goals.delete_form_submissions(site)
|
|
_ -> nil
|
|
end)
|
|
end
|
|
|
|
defp changeset(tracker_script_configuration, config_update, :installation) do
|
|
TrackerScriptConfiguration.installation_changeset(tracker_script_configuration, config_update)
|
|
end
|
|
|
|
defp changeset(tracker_script_configuration, config_update, :plugins_api) do
|
|
TrackerScriptConfiguration.plugins_api_changeset(tracker_script_configuration, config_update)
|
|
end
|
|
|
|
defp tracker_ingestion_endpoint() do
|
|
# :TRICKY: Normally we would use PlausibleWeb.Endpoint.url() here, but
|
|
# that requires the endpoint to be started. We start the TrackerScriptCache
|
|
# before the endpoint is started, so we need to use the base_url directly.
|
|
|
|
endpoint_config = Application.fetch_env!(:plausible, PlausibleWeb.Endpoint)
|
|
base_url = Keyword.get(endpoint_config, :base_url)
|
|
"#{base_url}/api/event"
|
|
end
|
|
end
|