File size: 8,610 Bytes
88211d3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | 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 Plausible.Site.TrackerScriptIdCache.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
Plausible.Site.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
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},
# 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)
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
# 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
|