| 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") |
| |
| 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 |
| |
| |
| |
| |
| |
| if Plausible.Site.TrackerScriptIdCache.get(id, cache_opts) do |
| get_tracker_script_configuration_by_id(id) |
| |> build_script() |
| end |
| else |
| |
| Plausible.Site.TrackerScriptCache.get(id, cache_opts) |
| end |
| end |
|
|
| |
| 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)}"] |
| |
| {key, true} -> ["#{key}:!0"] |
| |
| {_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 |
|
|
| on_ee do |
| defp broadcast_script_insert(tracker_script_configuration), |
| do: Plausible.Site.TrackerScriptIdCache.broadcast_put(tracker_script_configuration.id, true) |
| end |
|
|
| on_ce do |
| defp broadcast_script_upsert(tracker_script_configuration) do |
| case Plausible.Site.TrackerScriptCache.put( |
| tracker_script_configuration.id, |
| Plausible.Site.TrackerScriptCache.cache_content(tracker_script_configuration) |
| ) do |
| script when is_binary(script) -> :ok |
| end |
| end |
| 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}, |
| |
| 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) |
|
|
| on_ee do |
| :ok = broadcast_script_insert(reloaded_config) |
| else |
| :ok = broadcast_script_upsert(reloaded_config) |
| end |
|
|
| {: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 |
| |
| defp maybe_reload_tracker_script_configuration(tracker_script_configuration), |
| do: get_tracker_script_configuration_by_id(tracker_script_configuration.id) |
| end |
|
|
| |
| 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 |
| |
| |
| |
|
|
| endpoint_config = Application.fetch_env!(:plausible, PlausibleWeb.Endpoint) |
| base_url = Keyword.get(endpoint_config, :base_url) |
| "#{base_url}/api/event" |
| end |
| end |
|
|