Upload folder using huggingface_hub (part 4)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/plausible_web.ex +152 -0
- lib/plausible_web/views/stats_view.ex +80 -0
- lib/plausible_web/views/text_helpers.ex +53 -0
- lib/plausible_web/views/unsubscribe_view.ex +3 -0
- lib/sentry_filter.ex +69 -0
- lib/workers/accept_traffic_until_notification.ex +106 -0
- lib/workers/check_usage.ex +170 -0
- lib/workers/clean_invitations.ex +25 -0
- lib/workers/clean_user_sessions.ex +27 -0
- lib/workers/clickhouse_clean_sites.ex +81 -0
- lib/workers/expire_domain_change_transitions.ex +22 -0
- lib/workers/export_analytics.ex +121 -0
- lib/workers/import_analytics.ex +96 -0
- lib/workers/local_import_analytics_cleaner.ex +32 -0
- lib/workers/locations_sync.ex +15 -0
- lib/workers/lock_sites.ex +24 -0
- lib/workers/notify_annual_renewal.ex +76 -0
- lib/workers/notify_exported_analytics.ex +43 -0
- lib/workers/purge_cdn_cache.ex +71 -0
- lib/workers/rotate_salts.ex +9 -0
- lib/workers/schedule_email_reports.ex +100 -0
- lib/workers/send_check_stats_emails.ex +55 -0
- lib/workers/send_email_report.ex +210 -0
- lib/workers/send_site_setup_emails.ex +121 -0
- lib/workers/send_trial_notifications.ex +85 -0
- lib/workers/set_legacy_time_on_page_cutoff.ex +96 -0
- lib/workers/traffic_change_notifier.ex +181 -0
- mix.exs +246 -0
- mix.lock +171 -0
- priv/custom_sources.json +84 -0
- priv/data_migrations/AcquisitionChannel/sql/acquisition_channel_add_materialized_column.sql.eex +4 -0
- priv/data_migrations/AcquisitionChannel/sql/acquisition_channel_backfill_materialized_column.sql.eex +2 -0
- priv/data_migrations/AcquisitionChannel/sql/acquisition_channel_functions.sql.eex +235 -0
- priv/data_migrations/AcquisitionChannel/sql/acquisition_channel_update_materialized_column.sql.eex +4 -0
- priv/data_migrations/FunnelDupeGoals/sql/list-funnels-with-dupe-goal-ids.sql.eex +62 -0
- priv/data_migrations/LocationsSync/sql/add-alias-column.sql.eex +5 -0
- priv/data_migrations/LocationsSync/sql/create-location-data-table.sql.eex +13 -0
- priv/data_migrations/LocationsSync/sql/get-location-data-table-comment.sql.eex +1 -0
- priv/data_migrations/LocationsSync/sql/truncate-location-data-table.sql.eex +1 -0
- priv/data_migrations/LocationsSync/sql/update-location-data-dictionary.sql.eex +11 -0
- priv/data_migrations/LocationsSync/sql/update-location-data-table-comment.sql.eex +3 -0
- priv/data_migrations/NumericIDs/sql/attach-tmp-events-v2.sql.eex +1 -0
- priv/data_migrations/NumericIDs/sql/attach-tmp-sessions-v2.sql.eex +1 -0
- priv/data_migrations/NumericIDs/sql/check-replicas.sql.eex +1 -0
- priv/data_migrations/NumericIDs/sql/create-domains-lookup.sql.eex +1 -0
- priv/data_migrations/NumericIDs/sql/create-events-v2.sql.eex +39 -0
- priv/data_migrations/NumericIDs/sql/create-sessions-v2.sql.eex +45 -0
- priv/data_migrations/NumericIDs/sql/create-tmp-events-v2.sql.eex +6 -0
- priv/data_migrations/NumericIDs/sql/create-tmp-sessions-v2.sql.eex +6 -0
- priv/data_migrations/NumericIDs/sql/drop-domains-lookup.sql.eex +1 -0
lib/plausible_web.ex
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule PlausibleWeb do
|
| 2 |
+
@moduledoc false
|
| 3 |
+
|
| 4 |
+
use Plausible
|
| 5 |
+
|
| 6 |
+
def live_view(opts \\ []) do
|
| 7 |
+
quote do
|
| 8 |
+
use Plausible
|
| 9 |
+
use Phoenix.LiveView, global_prefixes: ~w(x-)
|
| 10 |
+
use PlausibleWeb.Live.Flash
|
| 11 |
+
|
| 12 |
+
use PlausibleWeb.Live.AuthContext
|
| 13 |
+
|
| 14 |
+
unless :no_sentry_context in unquote(opts) do
|
| 15 |
+
use PlausibleWeb.Live.SentryContext
|
| 16 |
+
end
|
| 17 |
+
|
| 18 |
+
on_ee do
|
| 19 |
+
use Plausible.Audit.LiveContext
|
| 20 |
+
end
|
| 21 |
+
|
| 22 |
+
alias PlausibleWeb.Router.Helpers, as: Routes
|
| 23 |
+
alias Phoenix.LiveView.JS
|
| 24 |
+
|
| 25 |
+
import PlausibleWeb.Components.Generic
|
| 26 |
+
import PlausibleWeb.Live.Components.Form
|
| 27 |
+
end
|
| 28 |
+
end
|
| 29 |
+
|
| 30 |
+
def live_component do
|
| 31 |
+
quote do
|
| 32 |
+
use Phoenix.LiveComponent, global_prefixes: ~w(x-)
|
| 33 |
+
import PlausibleWeb.Components.Generic
|
| 34 |
+
import PlausibleWeb.Live.Components.Form
|
| 35 |
+
alias Phoenix.LiveView.JS
|
| 36 |
+
alias PlausibleWeb.Router.Helpers, as: Routes
|
| 37 |
+
end
|
| 38 |
+
end
|
| 39 |
+
|
| 40 |
+
def component do
|
| 41 |
+
quote do
|
| 42 |
+
use Phoenix.Component, global_prefixes: ~w(x-)
|
| 43 |
+
import PlausibleWeb.Components.Generic
|
| 44 |
+
import PlausibleWeb.Live.Components.Form
|
| 45 |
+
alias Phoenix.LiveView.JS
|
| 46 |
+
alias PlausibleWeb.Router.Helpers, as: Routes
|
| 47 |
+
end
|
| 48 |
+
end
|
| 49 |
+
|
| 50 |
+
def controller do
|
| 51 |
+
quote do
|
| 52 |
+
use Phoenix.Controller, formats: [html: "View", json: "View"]
|
| 53 |
+
|
| 54 |
+
import Plug.Conn
|
| 55 |
+
import PlausibleWeb.ControllerHelpers
|
| 56 |
+
alias PlausibleWeb.Router.Helpers, as: Routes
|
| 57 |
+
end
|
| 58 |
+
end
|
| 59 |
+
|
| 60 |
+
def view do
|
| 61 |
+
quote do
|
| 62 |
+
use Phoenix.View,
|
| 63 |
+
root: "lib/plausible_web/templates"
|
| 64 |
+
|
| 65 |
+
# Import convenience functions from controllers
|
| 66 |
+
import Phoenix.Controller, only: [view_module: 1]
|
| 67 |
+
|
| 68 |
+
use Phoenix.Component, global_prefixes: ~w(x-)
|
| 69 |
+
|
| 70 |
+
import PlausibleWeb.Components.Generic
|
| 71 |
+
import PlausibleWeb.Live.Components.Form
|
| 72 |
+
alias PlausibleWeb.Router.Helpers, as: Routes
|
| 73 |
+
end
|
| 74 |
+
end
|
| 75 |
+
|
| 76 |
+
on_ee do
|
| 77 |
+
def extra_view do
|
| 78 |
+
quote do
|
| 79 |
+
use Phoenix.View,
|
| 80 |
+
root: "extra/lib/plausible_web/templates"
|
| 81 |
+
|
| 82 |
+
# Import convenience functions from controllers
|
| 83 |
+
import Phoenix.Controller, only: [view_module: 1]
|
| 84 |
+
|
| 85 |
+
use Phoenix.Component
|
| 86 |
+
|
| 87 |
+
import PlausibleWeb.Components.Generic
|
| 88 |
+
import PlausibleWeb.Live.Components.Form
|
| 89 |
+
alias PlausibleWeb.Router.Helpers, as: Routes
|
| 90 |
+
end
|
| 91 |
+
end
|
| 92 |
+
end
|
| 93 |
+
|
| 94 |
+
def router do
|
| 95 |
+
quote do
|
| 96 |
+
use Phoenix.Router
|
| 97 |
+
import Plug.Conn
|
| 98 |
+
import Phoenix.Controller
|
| 99 |
+
end
|
| 100 |
+
end
|
| 101 |
+
|
| 102 |
+
def channel do
|
| 103 |
+
quote do
|
| 104 |
+
use Phoenix.Channel
|
| 105 |
+
end
|
| 106 |
+
end
|
| 107 |
+
|
| 108 |
+
def plugins_api_controller do
|
| 109 |
+
quote do
|
| 110 |
+
use Phoenix.Controller, formats: [:json]
|
| 111 |
+
import Plug.Conn
|
| 112 |
+
import PlausibleWeb.Router.Helpers
|
| 113 |
+
|
| 114 |
+
alias PlausibleWeb.Plugins.API.Schemas
|
| 115 |
+
alias PlausibleWeb.Plugins.API.Views
|
| 116 |
+
alias PlausibleWeb.Plugins.API.Errors
|
| 117 |
+
alias Plausible.Plugins.API
|
| 118 |
+
|
| 119 |
+
plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true, replace_params: false)
|
| 120 |
+
|
| 121 |
+
use OpenApiSpex.ControllerSpecs
|
| 122 |
+
end
|
| 123 |
+
end
|
| 124 |
+
|
| 125 |
+
def plugins_api_view do
|
| 126 |
+
quote do
|
| 127 |
+
use Phoenix.View, root: ""
|
| 128 |
+
|
| 129 |
+
alias PlausibleWeb.Router.Helpers
|
| 130 |
+
import PlausibleWeb.Plugins.API.Views.Pagination, only: [render_metadata_links: 4]
|
| 131 |
+
end
|
| 132 |
+
end
|
| 133 |
+
|
| 134 |
+
def open_api_schema do
|
| 135 |
+
quote do
|
| 136 |
+
require OpenApiSpex
|
| 137 |
+
alias OpenApiSpex.Schema
|
| 138 |
+
alias PlausibleWeb.Plugins.API.Schemas
|
| 139 |
+
end
|
| 140 |
+
end
|
| 141 |
+
|
| 142 |
+
@doc """
|
| 143 |
+
When used, dispatch to the appropriate controller/view/etc.
|
| 144 |
+
"""
|
| 145 |
+
defmacro __using__(which) when is_atom(which) do
|
| 146 |
+
apply(__MODULE__, which, [])
|
| 147 |
+
end
|
| 148 |
+
|
| 149 |
+
defmacro __using__([{which, opts}]) when is_atom(which) do
|
| 150 |
+
apply(__MODULE__, which, [List.wrap(opts)])
|
| 151 |
+
end
|
| 152 |
+
end
|
lib/plausible_web/views/stats_view.ex
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule PlausibleWeb.StatsView do
|
| 2 |
+
use PlausibleWeb, :view
|
| 3 |
+
use Plausible
|
| 4 |
+
|
| 5 |
+
require Plausible.Billing
|
| 6 |
+
|
| 7 |
+
def plausible_url do
|
| 8 |
+
PlausibleWeb.Endpoint.url()
|
| 9 |
+
end
|
| 10 |
+
|
| 11 |
+
def large_number_format(n, opts \\ []) do
|
| 12 |
+
k = if Keyword.get(opts, :capitalize_k?, false), do: "K", else: "k"
|
| 13 |
+
|
| 14 |
+
cond do
|
| 15 |
+
n >= 1_000 && n < 1_000_000 ->
|
| 16 |
+
thousands = trunc(n / 100) / 10
|
| 17 |
+
|
| 18 |
+
if thousands == trunc(thousands) || n >= 100_000 do
|
| 19 |
+
"#{trunc(thousands)}" <> k
|
| 20 |
+
else
|
| 21 |
+
"#{thousands}" <> k
|
| 22 |
+
end
|
| 23 |
+
|
| 24 |
+
n >= 1_000_000 && n < 1_000_000_000 ->
|
| 25 |
+
millions = trunc(n / 100_000) / 10
|
| 26 |
+
|
| 27 |
+
if millions == trunc(millions) || n > 100_000_000 do
|
| 28 |
+
"#{trunc(millions)}M"
|
| 29 |
+
else
|
| 30 |
+
"#{millions}M"
|
| 31 |
+
end
|
| 32 |
+
|
| 33 |
+
n >= 1_000_000_000 && n < 1_000_000_000_000 ->
|
| 34 |
+
billions = trunc(n / 100_000_000) / 10
|
| 35 |
+
|
| 36 |
+
if billions == trunc(billions) || n > 100_000_000_000 do
|
| 37 |
+
"#{trunc(billions)}B"
|
| 38 |
+
else
|
| 39 |
+
"#{billions}B"
|
| 40 |
+
end
|
| 41 |
+
|
| 42 |
+
is_integer(n) ->
|
| 43 |
+
Integer.to_string(n)
|
| 44 |
+
end
|
| 45 |
+
end
|
| 46 |
+
|
| 47 |
+
def stats_container_class(conn) do
|
| 48 |
+
cond do
|
| 49 |
+
conn.assigns[:embedded] && conn.params["width"] == "manual" -> "px-6"
|
| 50 |
+
conn.assigns[:embedded] -> "max-w-screen-xl mx-auto px-6"
|
| 51 |
+
!conn.assigns[:embedded] -> "container print:max-w-full"
|
| 52 |
+
end
|
| 53 |
+
end
|
| 54 |
+
|
| 55 |
+
@doc """
|
| 56 |
+
Returns a readable stats URL.
|
| 57 |
+
|
| 58 |
+
Native Phoenix router functions percent-encode all diacritics, resulting in
|
| 59 |
+
ugly URLs, e.g. `https://plausible.io/café.com` transforms into
|
| 60 |
+
`https://plausible.io/caf%C3%A9.com`.
|
| 61 |
+
|
| 62 |
+
This function encodes only the slash (`/`) character from the site's domain.
|
| 63 |
+
|
| 64 |
+
## Examples
|
| 65 |
+
|
| 66 |
+
iex> PlausibleWeb.StatsView.pretty_stats_url(%Plausible.Site{domain: "user.gittea.io/repo"})
|
| 67 |
+
"http://localhost:8000/user.gittea.io%2Frepo"
|
| 68 |
+
|
| 69 |
+
iex> PlausibleWeb.StatsView.pretty_stats_url(%Plausible.Site{domain: "anakin.test"})
|
| 70 |
+
"http://localhost:8000/anakin.test"
|
| 71 |
+
|
| 72 |
+
iex> PlausibleWeb.StatsView.pretty_stats_url(%Plausible.Site{domain: "café.test"})
|
| 73 |
+
"http://localhost:8000/café.test"
|
| 74 |
+
|
| 75 |
+
"""
|
| 76 |
+
def pretty_stats_url(%Plausible.Site{domain: domain}) when is_binary(domain) do
|
| 77 |
+
pretty_domain = String.replace(domain, "/", "%2F")
|
| 78 |
+
"#{plausible_url()}/#{pretty_domain}"
|
| 79 |
+
end
|
| 80 |
+
end
|
lib/plausible_web/views/text_helpers.ex
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule PlausibleWeb.TextHelpers do
|
| 2 |
+
@moduledoc false
|
| 3 |
+
|
| 4 |
+
@spec pretty_join([String.t()]) :: String.t()
|
| 5 |
+
|
| 6 |
+
@doc """
|
| 7 |
+
Turns a list of strings into a string and replaces the last comma
|
| 8 |
+
with the word "and".
|
| 9 |
+
|
| 10 |
+
### Examples:
|
| 11 |
+
|
| 12 |
+
iex> ["one"] |> PlausibleWeb.TextHelpers.pretty_join()
|
| 13 |
+
"one"
|
| 14 |
+
|
| 15 |
+
iex> ["one", "two"] |> PlausibleWeb.TextHelpers.pretty_join()
|
| 16 |
+
"one and two"
|
| 17 |
+
|
| 18 |
+
iex> ["one", "two", "three"] |> PlausibleWeb.TextHelpers.pretty_join()
|
| 19 |
+
"one, two and three"
|
| 20 |
+
"""
|
| 21 |
+
def pretty_join([str]), do: str
|
| 22 |
+
|
| 23 |
+
def pretty_join(list) do
|
| 24 |
+
[last_string | rest] = Enum.reverse(list)
|
| 25 |
+
|
| 26 |
+
rest_string =
|
| 27 |
+
rest
|
| 28 |
+
|> Enum.reverse()
|
| 29 |
+
|> Enum.join(", ")
|
| 30 |
+
|
| 31 |
+
"#{rest_string} and #{last_string}"
|
| 32 |
+
end
|
| 33 |
+
|
| 34 |
+
def pretty_list(list) do
|
| 35 |
+
list
|
| 36 |
+
|> Enum.map(&String.replace("#{&1}", "_", " "))
|
| 37 |
+
|> pretty_join()
|
| 38 |
+
end
|
| 39 |
+
|
| 40 |
+
def format_date_range(date_range) do
|
| 41 |
+
"#{format_date(date_range.first)} - #{format_date(date_range.last)}"
|
| 42 |
+
end
|
| 43 |
+
|
| 44 |
+
def format_date(date) do
|
| 45 |
+
Calendar.strftime(date, "%b %-d, %Y")
|
| 46 |
+
end
|
| 47 |
+
|
| 48 |
+
def number_format(number) when is_integer(number) do
|
| 49 |
+
Cldr.Number.to_string!(number)
|
| 50 |
+
end
|
| 51 |
+
|
| 52 |
+
def number_format(other), do: other
|
| 53 |
+
end
|
lib/plausible_web/views/unsubscribe_view.ex
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule PlausibleWeb.UnsubscribeView do
|
| 2 |
+
use PlausibleWeb, :view
|
| 3 |
+
end
|
lib/sentry_filter.ex
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.SentryFilter do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Sentry callbacks for filtering and grouping events
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
@spec before_send(Sentry.Event.t()) :: Sentry.Event.t()
|
| 7 |
+
def before_send(event)
|
| 8 |
+
|
| 9 |
+
def before_send(%{original_exception: %Bamboo.PostmarkAdapter.Error{} = e} = event) do
|
| 10 |
+
if Bamboo.PostmarkAdapter.Error.is_hard_bounce(e), do: false, else: event
|
| 11 |
+
end
|
| 12 |
+
|
| 13 |
+
def before_send(%{original_exception: %Phoenix.NotAcceptableError{}}), do: false
|
| 14 |
+
def before_send(%{original_exception: %Plug.CSRFProtection.InvalidCSRFTokenError{}}), do: false
|
| 15 |
+
def before_send(%{original_exception: %Plug.Static.InvalidPathError{}}), do: false
|
| 16 |
+
|
| 17 |
+
def before_send(
|
| 18 |
+
%{
|
| 19 |
+
exception: [%{type: "DBConnection.ConnectionError"}],
|
| 20 |
+
original_exception: %{reason: reason}
|
| 21 |
+
} = event
|
| 22 |
+
) do
|
| 23 |
+
%{event | fingerprint: ["db_connection", reason]}
|
| 24 |
+
end
|
| 25 |
+
|
| 26 |
+
def before_send(
|
| 27 |
+
%{
|
| 28 |
+
exception: [%{type: "Mint.TransportError"}],
|
| 29 |
+
original_exception: %{reason: reason}
|
| 30 |
+
} = event
|
| 31 |
+
) do
|
| 32 |
+
%{event | fingerprint: ["mint_transport", reason]}
|
| 33 |
+
end
|
| 34 |
+
|
| 35 |
+
def before_send(
|
| 36 |
+
%{
|
| 37 |
+
exception: [%{type: "Finch.TransportError"}],
|
| 38 |
+
original_exception: %{reason: reason}
|
| 39 |
+
} = event
|
| 40 |
+
) do
|
| 41 |
+
%{event | fingerprint: ["mint_transport", reason]}
|
| 42 |
+
end
|
| 43 |
+
|
| 44 |
+
def before_send(
|
| 45 |
+
%{source: :logger, message: %{formatted: "Ch.Connection (#PID<" <> rest}} = event
|
| 46 |
+
) do
|
| 47 |
+
if String.ends_with?(rest, ")) disconnected: ** (Mint.HTTPError) the connection is closed") do
|
| 48 |
+
false
|
| 49 |
+
else
|
| 50 |
+
event
|
| 51 |
+
end
|
| 52 |
+
end
|
| 53 |
+
|
| 54 |
+
def before_send(%{extra: %{request: %Plausible.Ingestion.Request{}}} = event) do
|
| 55 |
+
%{event | fingerprint: ["ingestion_request"]}
|
| 56 |
+
end
|
| 57 |
+
|
| 58 |
+
def before_send(%{source: :logger, message: %{formatted: "Ranch listener" <> rest}} = event) do
|
| 59 |
+
if String.contains?(rest, "had its request process") do
|
| 60 |
+
false
|
| 61 |
+
else
|
| 62 |
+
event
|
| 63 |
+
end
|
| 64 |
+
end
|
| 65 |
+
|
| 66 |
+
def before_send(event) do
|
| 67 |
+
event
|
| 68 |
+
end
|
| 69 |
+
end
|
lib/workers/accept_traffic_until_notification.ex
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.AcceptTrafficUntil do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
A worker meant to be run once a day that sends out e-mail notifications to site
|
| 4 |
+
owners assuming:
|
| 5 |
+
- their sites still receive traffic (i.e. have stats for yesterday)
|
| 6 |
+
- `site.accept_traffic_until` is approaching either tomorrow or exactly in 7 days
|
| 7 |
+
|
| 8 |
+
Users having no sites or sites that receive no traffic, won't be notified.
|
| 9 |
+
We make a tiny effort here to make sure we send the same notification at most once a day.
|
| 10 |
+
"""
|
| 11 |
+
use Oban.Worker, queue: :check_accept_traffic_until
|
| 12 |
+
import Ecto.Query
|
| 13 |
+
|
| 14 |
+
alias Plausible.Repo
|
| 15 |
+
alias Plausible.ClickhouseRepo
|
| 16 |
+
|
| 17 |
+
def dry_run(date) do
|
| 18 |
+
perform(nil, date, true)
|
| 19 |
+
end
|
| 20 |
+
|
| 21 |
+
@impl Oban.Worker
|
| 22 |
+
def perform(_job, today \\ Date.utc_today(), dry_run? \\ false) do
|
| 23 |
+
tomorrow = today |> Date.add(+1)
|
| 24 |
+
next_week = today |> Date.add(+7)
|
| 25 |
+
|
| 26 |
+
# send at most one notification per user, per day
|
| 27 |
+
sent_today_query =
|
| 28 |
+
from s in "sent_accept_traffic_until_notifications",
|
| 29 |
+
where: s.user_id == parent_as(:users).id and s.sent_on == ^today,
|
| 30 |
+
select: true
|
| 31 |
+
|
| 32 |
+
notifications =
|
| 33 |
+
Repo.all(
|
| 34 |
+
from t in Plausible.Teams.Team,
|
| 35 |
+
inner_join: u in assoc(t, :owners),
|
| 36 |
+
as: :users,
|
| 37 |
+
inner_join: s in assoc(t, :sites),
|
| 38 |
+
where: t.accept_traffic_until == ^tomorrow or t.accept_traffic_until == ^next_week,
|
| 39 |
+
where: not exists(sent_today_query),
|
| 40 |
+
select: %{
|
| 41 |
+
id: u.id,
|
| 42 |
+
email: u.email,
|
| 43 |
+
deadline: t.accept_traffic_until,
|
| 44 |
+
site_ids: fragment("array_agg(?.id)", s),
|
| 45 |
+
name: u.name,
|
| 46 |
+
team: t
|
| 47 |
+
},
|
| 48 |
+
group_by: [u.id, t.id]
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
for notification <- notifications do
|
| 52 |
+
case {has_stats?(notification.site_ids, today), notification.deadline} do
|
| 53 |
+
{true, ^tomorrow} ->
|
| 54 |
+
if dry_run? do
|
| 55 |
+
IO.puts("Will send final notification to #{notification.email}")
|
| 56 |
+
else
|
| 57 |
+
notification
|
| 58 |
+
|> store_sent(today)
|
| 59 |
+
|> PlausibleWeb.Email.approaching_accept_traffic_until_tomorrow()
|
| 60 |
+
|> Plausible.Mailer.send()
|
| 61 |
+
end
|
| 62 |
+
|
| 63 |
+
{true, ^next_week} ->
|
| 64 |
+
if dry_run? do
|
| 65 |
+
IO.puts("Will send weekly notification to #{notification.email}")
|
| 66 |
+
else
|
| 67 |
+
notification
|
| 68 |
+
|> store_sent(today)
|
| 69 |
+
|> PlausibleWeb.Email.approaching_accept_traffic_until()
|
| 70 |
+
|> Plausible.Mailer.send()
|
| 71 |
+
end
|
| 72 |
+
|
| 73 |
+
_ ->
|
| 74 |
+
nil
|
| 75 |
+
end
|
| 76 |
+
end
|
| 77 |
+
|
| 78 |
+
{:ok, Enum.count(notifications)}
|
| 79 |
+
end
|
| 80 |
+
|
| 81 |
+
defp has_stats?(site_ids, today) do
|
| 82 |
+
ago_2d = Date.add(today, -2)
|
| 83 |
+
|
| 84 |
+
ClickhouseRepo.exists?(
|
| 85 |
+
from e in "events_v2",
|
| 86 |
+
where: fragment("toDate(?) >= ?", e.timestamp, ^ago_2d),
|
| 87 |
+
where: e.site_id in ^site_ids
|
| 88 |
+
)
|
| 89 |
+
end
|
| 90 |
+
|
| 91 |
+
defp store_sent(notification, today) do
|
| 92 |
+
Repo.insert_all(
|
| 93 |
+
"sent_accept_traffic_until_notifications",
|
| 94 |
+
[
|
| 95 |
+
%{
|
| 96 |
+
user_id: notification.id,
|
| 97 |
+
sent_on: today
|
| 98 |
+
}
|
| 99 |
+
],
|
| 100 |
+
on_conflict: :nothing,
|
| 101 |
+
conflict_target: [:user_id, :sent_on]
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
notification
|
| 105 |
+
end
|
| 106 |
+
end
|
lib/workers/check_usage.ex
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.CheckUsage do
|
| 2 |
+
use Plausible.Repo
|
| 3 |
+
use Oban.Worker, queue: :check_usage
|
| 4 |
+
require Plausible.Billing.Subscription.Status
|
| 5 |
+
alias Plausible.Billing.{Subscription, Quota}
|
| 6 |
+
alias Plausible.Teams
|
| 7 |
+
|
| 8 |
+
defmacro yesterday() do
|
| 9 |
+
quote do
|
| 10 |
+
fragment("now() - INTERVAL '1 day'")
|
| 11 |
+
end
|
| 12 |
+
end
|
| 13 |
+
|
| 14 |
+
defmacro last_day_of_month(day) do
|
| 15 |
+
quote do
|
| 16 |
+
fragment(
|
| 17 |
+
"(date_trunc('month', ?::date) + interval '1 month' - interval '1 day')::date",
|
| 18 |
+
unquote(day)
|
| 19 |
+
)
|
| 20 |
+
end
|
| 21 |
+
end
|
| 22 |
+
|
| 23 |
+
defmacro day_of_month(date) do
|
| 24 |
+
quote do
|
| 25 |
+
fragment("EXTRACT(day from ?::date)", unquote(date))
|
| 26 |
+
end
|
| 27 |
+
end
|
| 28 |
+
|
| 29 |
+
defmacro least(left, right) do
|
| 30 |
+
quote do
|
| 31 |
+
fragment("least(?, ?)", unquote(left), unquote(right))
|
| 32 |
+
end
|
| 33 |
+
end
|
| 34 |
+
|
| 35 |
+
@impl Oban.Worker
|
| 36 |
+
def perform(_job, usage_mod \\ Teams.Billing, today \\ Date.utc_today()) do
|
| 37 |
+
yesterday = today |> Date.shift(day: -1)
|
| 38 |
+
|
| 39 |
+
active_subscribers =
|
| 40 |
+
Repo.all(
|
| 41 |
+
from(t in Teams.Team,
|
| 42 |
+
as: :team,
|
| 43 |
+
inner_join: o in assoc(t, :owners),
|
| 44 |
+
left_join: bm in assoc(t, :billing_members),
|
| 45 |
+
inner_lateral_join: s in subquery(Teams.last_subscription_join_query()),
|
| 46 |
+
on: true,
|
| 47 |
+
left_join: ep in Plausible.Billing.EnterprisePlan,
|
| 48 |
+
on: ep.team_id == t.id and ep.paddle_plan_id == s.paddle_plan_id,
|
| 49 |
+
where:
|
| 50 |
+
s.status in [
|
| 51 |
+
^Subscription.Status.active(),
|
| 52 |
+
^Subscription.Status.past_due(),
|
| 53 |
+
^Subscription.Status.deleted()
|
| 54 |
+
],
|
| 55 |
+
where: not is_nil(s.last_bill_date),
|
| 56 |
+
# Accounts for situations like last_bill_date==2021-01-31 AND today==2021-03-01. Since February never reaches the 31st day, the account is checked on 2021-03-01.
|
| 57 |
+
where: s.next_bill_date >= ^today,
|
| 58 |
+
where:
|
| 59 |
+
least(day_of_month(s.last_bill_date), day_of_month(last_day_of_month(^yesterday))) ==
|
| 60 |
+
day_of_month(^yesterday),
|
| 61 |
+
order_by: t.id,
|
| 62 |
+
preload: [subscription: s, enterprise_plan: ep, owners: o, billing_members: bm]
|
| 63 |
+
)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
for subscriber <- active_subscribers do
|
| 67 |
+
case {subscriber.grace_period, subscriber.enterprise_plan} do
|
| 68 |
+
{nil, nil} ->
|
| 69 |
+
check_regular_subscriber(subscriber, usage_mod)
|
| 70 |
+
|
| 71 |
+
{nil, _} ->
|
| 72 |
+
check_enterprise_subscriber(subscriber, usage_mod)
|
| 73 |
+
|
| 74 |
+
{_, nil} ->
|
| 75 |
+
maybe_remove_grace_period(subscriber, usage_mod)
|
| 76 |
+
|
| 77 |
+
_ ->
|
| 78 |
+
:skip
|
| 79 |
+
end
|
| 80 |
+
end
|
| 81 |
+
|
| 82 |
+
:ok
|
| 83 |
+
end
|
| 84 |
+
|
| 85 |
+
defp check_site_usage_for_enterprise(subscriber) do
|
| 86 |
+
limit = subscriber.enterprise_plan.site_limit
|
| 87 |
+
|
| 88 |
+
usage = Teams.Billing.site_usage(subscriber)
|
| 89 |
+
|
| 90 |
+
if Quota.below_limit?(usage, limit) do
|
| 91 |
+
{:below_limit, {usage, limit}}
|
| 92 |
+
else
|
| 93 |
+
{:over_limit, {usage, limit}}
|
| 94 |
+
end
|
| 95 |
+
end
|
| 96 |
+
|
| 97 |
+
def maybe_remove_grace_period(subscriber, usage_mod) do
|
| 98 |
+
case check_pageview_usage_last_cycle(subscriber, usage_mod) do
|
| 99 |
+
{:below_limit, _} ->
|
| 100 |
+
Plausible.Teams.remove_grace_period(subscriber)
|
| 101 |
+
:ok
|
| 102 |
+
|
| 103 |
+
_ ->
|
| 104 |
+
:skip
|
| 105 |
+
end
|
| 106 |
+
end
|
| 107 |
+
|
| 108 |
+
defp check_regular_subscriber(subscriber, usage_mod) do
|
| 109 |
+
case check_pageview_usage_two_cycles(subscriber, usage_mod) do
|
| 110 |
+
{:over_limit, pageview_usage} ->
|
| 111 |
+
suggested_volume =
|
| 112 |
+
Plausible.Billing.Plans.suggest_volume(subscriber, pageview_usage.last_cycle.total)
|
| 113 |
+
|
| 114 |
+
for owner <- subscriber.owners ++ subscriber.billing_members do
|
| 115 |
+
PlausibleWeb.Email.over_limit_email(owner, subscriber, pageview_usage, suggested_volume)
|
| 116 |
+
|> Plausible.Mailer.send()
|
| 117 |
+
end
|
| 118 |
+
|
| 119 |
+
Plausible.Teams.start_grace_period(subscriber)
|
| 120 |
+
|
| 121 |
+
_ ->
|
| 122 |
+
nil
|
| 123 |
+
end
|
| 124 |
+
end
|
| 125 |
+
|
| 126 |
+
def check_enterprise_subscriber(subscriber, usage_mod) do
|
| 127 |
+
pageview_usage = check_pageview_usage_two_cycles(subscriber, usage_mod)
|
| 128 |
+
site_usage = check_site_usage_for_enterprise(subscriber)
|
| 129 |
+
|
| 130 |
+
case {pageview_usage, site_usage} do
|
| 131 |
+
{{:below_limit, _}, {:below_limit, _}} ->
|
| 132 |
+
nil
|
| 133 |
+
|
| 134 |
+
{{_, pageview_usage}, {_, {site_usage, site_allowance}}} ->
|
| 135 |
+
for owner <- subscriber.owners ++ subscriber.billing_members do
|
| 136 |
+
PlausibleWeb.Email.enterprise_over_limit_internal_email(
|
| 137 |
+
owner,
|
| 138 |
+
pageview_usage,
|
| 139 |
+
site_usage,
|
| 140 |
+
site_allowance
|
| 141 |
+
)
|
| 142 |
+
|> Plausible.Mailer.send()
|
| 143 |
+
end
|
| 144 |
+
|
| 145 |
+
Plausible.Teams.start_manual_lock_grace_period(subscriber)
|
| 146 |
+
end
|
| 147 |
+
end
|
| 148 |
+
|
| 149 |
+
def check_pageview_usage_two_cycles(subscriber, usage_mod) do
|
| 150 |
+
usage = usage_mod.monthly_pageview_usage(subscriber)
|
| 151 |
+
limit = Teams.Billing.monthly_pageview_limit(subscriber.subscription)
|
| 152 |
+
|
| 153 |
+
if Quota.exceeds_last_two_usage_cycles?(usage, limit) do
|
| 154 |
+
{:over_limit, usage}
|
| 155 |
+
else
|
| 156 |
+
{:below_limit, usage}
|
| 157 |
+
end
|
| 158 |
+
end
|
| 159 |
+
|
| 160 |
+
defp check_pageview_usage_last_cycle(subscriber, usage_mod) do
|
| 161 |
+
usage = usage_mod.monthly_pageview_usage(subscriber)
|
| 162 |
+
limit = Teams.Billing.monthly_pageview_limit(subscriber.subscription)
|
| 163 |
+
|
| 164 |
+
if :last_cycle in Quota.exceeded_cycles(usage, limit) do
|
| 165 |
+
{:over_limit, usage}
|
| 166 |
+
else
|
| 167 |
+
{:below_limit, usage}
|
| 168 |
+
end
|
| 169 |
+
end
|
| 170 |
+
end
|
lib/workers/clean_invitations.ex
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.CleanInvitations do
|
| 2 |
+
use Plausible.Repo
|
| 3 |
+
use Oban.Worker, queue: :clean_invitations
|
| 4 |
+
|
| 5 |
+
@cutoff Duration.new!(hour: -48)
|
| 6 |
+
|
| 7 |
+
@impl Oban.Worker
|
| 8 |
+
def perform(_job) do
|
| 9 |
+
cutoff_time =
|
| 10 |
+
NaiveDateTime.utc_now(:second)
|
| 11 |
+
|> NaiveDateTime.shift(@cutoff)
|
| 12 |
+
|
| 13 |
+
Repo.delete_all(
|
| 14 |
+
from ti in Plausible.Teams.Invitation,
|
| 15 |
+
where: ti.inserted_at < ^cutoff_time
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
Repo.delete_all(
|
| 19 |
+
from ti in Plausible.Teams.SiteTransfer,
|
| 20 |
+
where: ti.inserted_at < ^cutoff_time
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
:ok
|
| 24 |
+
end
|
| 25 |
+
end
|
lib/workers/clean_user_sessions.ex
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.CleanUserSessions do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Job removing expired user sessions. A grace period is applied.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
use Plausible.Repo
|
| 7 |
+
use Oban.Worker, queue: :clean_user_sessions
|
| 8 |
+
|
| 9 |
+
@grace_period Duration.new!(day: -7)
|
| 10 |
+
|
| 11 |
+
@spec grace_period_duration() :: Duration.t()
|
| 12 |
+
def grace_period_duration(), do: @grace_period
|
| 13 |
+
|
| 14 |
+
@impl Oban.Worker
|
| 15 |
+
def perform(_job) do
|
| 16 |
+
grace_cutoff =
|
| 17 |
+
NaiveDateTime.utc_now(:second)
|
| 18 |
+
|> NaiveDateTime.shift(@grace_period)
|
| 19 |
+
|
| 20 |
+
Repo.delete_all(
|
| 21 |
+
from us in Plausible.Auth.UserSession,
|
| 22 |
+
where: us.timeout_at < ^grace_cutoff
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
:ok
|
| 26 |
+
end
|
| 27 |
+
end
|
lib/workers/clickhouse_clean_sites.ex
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.ClickhouseCleanSites do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Cleans deleted site data from ClickHouse asynchronously.
|
| 4 |
+
|
| 5 |
+
We batch up data deletions from ClickHouse as deleting a single site is
|
| 6 |
+
just as expensive as deleting many.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
use Plausible.Repo
|
| 10 |
+
use Plausible.ClickhouseRepo
|
| 11 |
+
use Plausible.IngestRepo
|
| 12 |
+
use Oban.Worker, queue: :clickhouse_clean_sites
|
| 13 |
+
|
| 14 |
+
import Ecto.Query
|
| 15 |
+
|
| 16 |
+
require Logger
|
| 17 |
+
|
| 18 |
+
@tables_to_clear [
|
| 19 |
+
"events_v2",
|
| 20 |
+
"sessions_v2",
|
| 21 |
+
"ingest_counters",
|
| 22 |
+
"imported_browsers",
|
| 23 |
+
"imported_devices",
|
| 24 |
+
"imported_entry_pages",
|
| 25 |
+
"imported_exit_pages",
|
| 26 |
+
"imported_locations",
|
| 27 |
+
"imported_operating_systems",
|
| 28 |
+
"imported_pages",
|
| 29 |
+
"imported_custom_events",
|
| 30 |
+
"imported_sources",
|
| 31 |
+
"imported_visitors"
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
@settings if Mix.env() in [:test, :ce_test, :e2e_test], do: [mutations_sync: 2], else: []
|
| 35 |
+
|
| 36 |
+
def perform(_job) do
|
| 37 |
+
deleted_sites = get_deleted_sites_with_clickhouse_data()
|
| 38 |
+
|
| 39 |
+
if not Enum.empty?(deleted_sites) do
|
| 40 |
+
Logger.notice(
|
| 41 |
+
"Clearing ClickHouse data for the following #{length(deleted_sites)} sites which have been deleted: #{inspect(deleted_sites)}"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
for table <- @tables_to_clear do
|
| 45 |
+
IngestRepo.query!(
|
| 46 |
+
"ALTER TABLE {$0:Identifier} DELETE WHERE site_id IN {$1:Array(UInt64)}",
|
| 47 |
+
[table, deleted_sites],
|
| 48 |
+
settings: @settings
|
| 49 |
+
)
|
| 50 |
+
end
|
| 51 |
+
end
|
| 52 |
+
|
| 53 |
+
:ok
|
| 54 |
+
end
|
| 55 |
+
|
| 56 |
+
def get_deleted_sites_with_clickhouse_data() do
|
| 57 |
+
pg_sites =
|
| 58 |
+
from(s in Plausible.Site.regular(), select: s.id)
|
| 59 |
+
|> Plausible.Repo.all()
|
| 60 |
+
|> MapSet.new()
|
| 61 |
+
|
| 62 |
+
{:ok, ch} =
|
| 63 |
+
Plausible.ClickhouseRepo.get_config_without_ch_query_execution_timeout()
|
| 64 |
+
|> Ch.start_link()
|
| 65 |
+
|
| 66 |
+
%Ch.Result{columns: ["site_id"], rows: rows} =
|
| 67 |
+
DBConnection.run(
|
| 68 |
+
ch,
|
| 69 |
+
fn conn ->
|
| 70 |
+
Ch.query!(conn, "FROM events_v2 SELECT site_id GROUP BY site_id", [],
|
| 71 |
+
timeout: :infinity
|
| 72 |
+
)
|
| 73 |
+
end,
|
| 74 |
+
timeout: :infinity
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
ch_sites = rows |> MapSet.new(fn [site_id] -> site_id end)
|
| 78 |
+
|
| 79 |
+
MapSet.difference(ch_sites, pg_sites) |> MapSet.to_list()
|
| 80 |
+
end
|
| 81 |
+
end
|
lib/workers/expire_domain_change_transitions.ex
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.ExpireDomainChangeTransitions do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Periodic worker that expires domain change transition period.
|
| 4 |
+
Old domains are frozen for a given time, so users can still access them
|
| 5 |
+
before redeploying their scripts and integrations.
|
| 6 |
+
"""
|
| 7 |
+
use Plausible.Repo
|
| 8 |
+
use Oban.Worker, queue: :domain_change_transition
|
| 9 |
+
|
| 10 |
+
require Logger
|
| 11 |
+
|
| 12 |
+
@impl Oban.Worker
|
| 13 |
+
def perform(_job) do
|
| 14 |
+
{:ok, n} = Plausible.Site.Domain.expire_change_transitions()
|
| 15 |
+
|
| 16 |
+
if n > 0 do
|
| 17 |
+
Logger.warning("Expired #{n} from the domain change transition period.")
|
| 18 |
+
end
|
| 19 |
+
|
| 20 |
+
:ok
|
| 21 |
+
end
|
| 22 |
+
end
|
lib/workers/export_analytics.ex
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.ExportAnalytics do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Worker for running CSV export jobs. Supports S3 and local storage.
|
| 4 |
+
To avoid blocking the queue, a timeout of 15 minutes is enforced.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
use Oban.Worker,
|
| 8 |
+
queue: :analytics_exports,
|
| 9 |
+
max_attempts: 3
|
| 10 |
+
|
| 11 |
+
alias Plausible.Exports
|
| 12 |
+
|
| 13 |
+
@doc "This base query filters export jobs for a site"
|
| 14 |
+
def base_query(site_id) do
|
| 15 |
+
import Ecto.Query, only: [from: 2]
|
| 16 |
+
|
| 17 |
+
from j in Oban.Job,
|
| 18 |
+
where: j.worker == ^Oban.Worker.to_string(__MODULE__),
|
| 19 |
+
where: j.args["site_id"] == ^site_id
|
| 20 |
+
end
|
| 21 |
+
|
| 22 |
+
@impl true
|
| 23 |
+
def timeout(_job), do: :timer.minutes(15)
|
| 24 |
+
|
| 25 |
+
@impl true
|
| 26 |
+
def perform(%Oban.Job{args: args} = job) do
|
| 27 |
+
%{
|
| 28 |
+
"storage" => storage,
|
| 29 |
+
"site_id" => site_id
|
| 30 |
+
} = args
|
| 31 |
+
|
| 32 |
+
site = Plausible.Repo.get!(Plausible.Site, site_id)
|
| 33 |
+
true = Plausible.Sites.regular?(site)
|
| 34 |
+
%Date.Range{} = date_range = Exports.date_range(site.id, site.timezone)
|
| 35 |
+
|
| 36 |
+
queries =
|
| 37 |
+
Exports.export_queries(site,
|
| 38 |
+
date_range: date_range,
|
| 39 |
+
timezone: site.timezone,
|
| 40 |
+
extname: ".csv"
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# since each worker / `perform` attempt runs in a separate process
|
| 44 |
+
# it's ok to use start_link to keep connection lifecycle
|
| 45 |
+
# bound to that of the worker
|
| 46 |
+
{:ok, ch} =
|
| 47 |
+
Plausible.ClickhouseRepo.get_config_without_ch_query_execution_timeout()
|
| 48 |
+
|> Ch.start_link()
|
| 49 |
+
|
| 50 |
+
try do
|
| 51 |
+
case storage do
|
| 52 |
+
"s3" -> perform_s3_export(ch, site, queries, args)
|
| 53 |
+
"local" -> perform_local_export(ch, queries, args)
|
| 54 |
+
end
|
| 55 |
+
after
|
| 56 |
+
Exports.oban_notify(site_id)
|
| 57 |
+
end
|
| 58 |
+
|
| 59 |
+
email_success(job.args)
|
| 60 |
+
|
| 61 |
+
:ok
|
| 62 |
+
catch
|
| 63 |
+
class, reason ->
|
| 64 |
+
if job.attempt >= job.max_attempts, do: email_failure(job.args)
|
| 65 |
+
:erlang.raise(class, reason, __STACKTRACE__)
|
| 66 |
+
end
|
| 67 |
+
|
| 68 |
+
defp perform_s3_export(ch, site, queries, args) do
|
| 69 |
+
%{
|
| 70 |
+
"s3_bucket" => s3_bucket,
|
| 71 |
+
"s3_path" => s3_path
|
| 72 |
+
} = args
|
| 73 |
+
|
| 74 |
+
created_on = Plausible.Timezones.to_date_in_timezone(DateTime.utc_now(), site.timezone)
|
| 75 |
+
filename = Exports.archive_filename(site.domain, created_on)
|
| 76 |
+
|
| 77 |
+
DBConnection.run(
|
| 78 |
+
ch,
|
| 79 |
+
fn conn ->
|
| 80 |
+
conn
|
| 81 |
+
|> Exports.stream_archive(queries, format: "CSVWithNames", timeout: :infinity)
|
| 82 |
+
|> Plausible.S3.export_upload_multipart(s3_bucket, s3_path, filename)
|
| 83 |
+
end,
|
| 84 |
+
timeout: :infinity
|
| 85 |
+
)
|
| 86 |
+
end
|
| 87 |
+
|
| 88 |
+
defp perform_local_export(ch, queries, args) do
|
| 89 |
+
%{"local_path" => local_path} = args
|
| 90 |
+
tmp_path = Plug.Upload.random_file!("tmp-plausible-export")
|
| 91 |
+
|
| 92 |
+
DBConnection.run(
|
| 93 |
+
ch,
|
| 94 |
+
fn conn ->
|
| 95 |
+
Exports.stream_archive(conn, queries, format: "CSVWithNames")
|
| 96 |
+
|> Stream.into(File.stream!(tmp_path))
|
| 97 |
+
|> Stream.run()
|
| 98 |
+
end,
|
| 99 |
+
timeout: :infinity
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
File.mkdir_p!(Path.dirname(local_path))
|
| 103 |
+
if File.exists?(local_path), do: File.rm!(local_path)
|
| 104 |
+
Plausible.File.mv!(tmp_path, local_path)
|
| 105 |
+
end
|
| 106 |
+
|
| 107 |
+
defp email_failure(args) do
|
| 108 |
+
args |> Map.put("status", "failure") |> email()
|
| 109 |
+
end
|
| 110 |
+
|
| 111 |
+
defp email_success(args) do
|
| 112 |
+
args |> Map.put("status", "success") |> email()
|
| 113 |
+
end
|
| 114 |
+
|
| 115 |
+
defp email(args) do
|
| 116 |
+
# email delivery can potentially fail and cause already successful
|
| 117 |
+
# export to be repeated which is costly, hence email is delivered
|
| 118 |
+
# in a separate job
|
| 119 |
+
Oban.insert!(Plausible.Workers.NotifyExportedAnalytics.new(args))
|
| 120 |
+
end
|
| 121 |
+
end
|
lib/workers/import_analytics.ex
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.ImportAnalytics do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Worker for running analytics import jobs.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
use Plausible.Repo
|
| 7 |
+
|
| 8 |
+
use Oban.Worker,
|
| 9 |
+
queue: :analytics_imports,
|
| 10 |
+
max_attempts: 3,
|
| 11 |
+
unique: [fields: [:args], keys: [:import_id], period: 60]
|
| 12 |
+
|
| 13 |
+
alias Plausible.Imported.ImportSources
|
| 14 |
+
alias Plausible.Imported.Importer
|
| 15 |
+
alias Plausible.Imported.SiteImport
|
| 16 |
+
|
| 17 |
+
@impl Oban.Worker
|
| 18 |
+
def perform(%Oban.Job{
|
| 19 |
+
args: %{"import_id" => import_id} = args
|
| 20 |
+
}) do
|
| 21 |
+
site_import =
|
| 22 |
+
SiteImport
|
| 23 |
+
|> Repo.get!(import_id)
|
| 24 |
+
|> Repo.preload(:site)
|
| 25 |
+
|
| 26 |
+
import_api = ImportSources.by_name(site_import.source)
|
| 27 |
+
|
| 28 |
+
case import_api.run_import(site_import, args) do
|
| 29 |
+
{:ok, site_import} ->
|
| 30 |
+
import_complete(site_import)
|
| 31 |
+
|
| 32 |
+
:ok
|
| 33 |
+
|
| 34 |
+
{:error, error, error_opts} ->
|
| 35 |
+
Sentry.capture_message("Failed to import from #{site_import.source}",
|
| 36 |
+
extra: %{
|
| 37 |
+
import_id: site_import.id,
|
| 38 |
+
site: site_import.site.domain,
|
| 39 |
+
error: inspect(error)
|
| 40 |
+
}
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
import_fail(site_import, error_opts)
|
| 44 |
+
|
| 45 |
+
{:discard, error}
|
| 46 |
+
end
|
| 47 |
+
end
|
| 48 |
+
|
| 49 |
+
@impl Oban.Worker
|
| 50 |
+
def backoff(_job) do
|
| 51 |
+
# 5 minutes
|
| 52 |
+
300
|
| 53 |
+
end
|
| 54 |
+
|
| 55 |
+
def import_complete(site_import) do
|
| 56 |
+
site_import = Repo.preload(site_import, [:imported_by, site: :team])
|
| 57 |
+
|
| 58 |
+
PlausibleWeb.Email.import_success(site_import, site_import.imported_by)
|
| 59 |
+
|> Plausible.Mailer.send()
|
| 60 |
+
|
| 61 |
+
Plausible.Sites.clear_stats_start_date!(site_import.site)
|
| 62 |
+
|
| 63 |
+
Importer.notify(site_import, :complete)
|
| 64 |
+
|
| 65 |
+
:ok
|
| 66 |
+
end
|
| 67 |
+
|
| 68 |
+
def import_fail_transient(site_import) do
|
| 69 |
+
Plausible.Purge.delete_imported_stats!(site_import)
|
| 70 |
+
|
| 71 |
+
Importer.notify(site_import, :transient_fail)
|
| 72 |
+
end
|
| 73 |
+
|
| 74 |
+
def import_fail(site_import, opts) do
|
| 75 |
+
skip_purge? = Keyword.get(opts, :skip_purge?, false)
|
| 76 |
+
skip_mark_failed? = Keyword.get(opts, :skip_mark_failed?, false)
|
| 77 |
+
|
| 78 |
+
if not skip_purge? do
|
| 79 |
+
Plausible.Purge.delete_imported_stats!(site_import)
|
| 80 |
+
end
|
| 81 |
+
|
| 82 |
+
if not skip_mark_failed? do
|
| 83 |
+
import_api = ImportSources.by_name(site_import.source)
|
| 84 |
+
|
| 85 |
+
site_import =
|
| 86 |
+
site_import
|
| 87 |
+
|> import_api.mark_failed()
|
| 88 |
+
|> Repo.preload([:site, :imported_by])
|
| 89 |
+
|
| 90 |
+
Importer.notify(site_import, :fail)
|
| 91 |
+
|
| 92 |
+
PlausibleWeb.Email.import_failure(site_import, site_import.imported_by)
|
| 93 |
+
|> Plausible.Mailer.send()
|
| 94 |
+
end
|
| 95 |
+
end
|
| 96 |
+
end
|
lib/workers/local_import_analytics_cleaner.ex
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.LocalImportAnalyticsCleaner do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Worker for cleaning local files left after analytics import jobs.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
use Oban.Worker, queue: :analytics_imports, unique: [period: 3600]
|
| 7 |
+
|
| 8 |
+
@impl Oban.Worker
|
| 9 |
+
def perform(%Oban.Job{args: args}) do
|
| 10 |
+
%{"import_id" => import_id, "paths" => paths} = args
|
| 11 |
+
|
| 12 |
+
if import_in_progress?(import_id) do
|
| 13 |
+
{:snooze, _one_hour = 3600}
|
| 14 |
+
else
|
| 15 |
+
Enum.each(paths, fn path ->
|
| 16 |
+
# credo:disable-for-next-line Credo.Check.Refactor.Nesting
|
| 17 |
+
if File.exists?(path), do: File.rm!(path)
|
| 18 |
+
end)
|
| 19 |
+
end
|
| 20 |
+
end
|
| 21 |
+
|
| 22 |
+
defp import_in_progress?(import_id) do
|
| 23 |
+
import Ecto.Query
|
| 24 |
+
require Plausible.Imported.SiteImport
|
| 25 |
+
alias Plausible.Imported.SiteImport
|
| 26 |
+
|
| 27 |
+
SiteImport
|
| 28 |
+
|> where(id: ^import_id)
|
| 29 |
+
|> where([i], i.status in ^[SiteImport.pending(), SiteImport.importing()])
|
| 30 |
+
|> Plausible.Repo.exists?()
|
| 31 |
+
end
|
| 32 |
+
end
|
lib/workers/locations_sync.ex
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.LocationsSync do
|
| 2 |
+
@moduledoc false
|
| 3 |
+
|
| 4 |
+
use Plausible.Repo
|
| 5 |
+
use Oban.Worker, queue: :locations_sync
|
| 6 |
+
|
| 7 |
+
@impl Oban.Worker
|
| 8 |
+
def perform(_job) do
|
| 9 |
+
if Plausible.DataMigration.LocationsSync.out_of_date?() do
|
| 10 |
+
Plausible.DataMigration.LocationsSync.run()
|
| 11 |
+
end
|
| 12 |
+
|
| 13 |
+
:ok
|
| 14 |
+
end
|
| 15 |
+
end
|
lib/workers/lock_sites.ex
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.LockSites do
|
| 2 |
+
use Plausible.Repo
|
| 3 |
+
use Oban.Worker, queue: :lock_sites
|
| 4 |
+
|
| 5 |
+
alias Plausible.Teams
|
| 6 |
+
|
| 7 |
+
@impl Oban.Worker
|
| 8 |
+
def perform(_job) do
|
| 9 |
+
teams =
|
| 10 |
+
Repo.all(
|
| 11 |
+
from t in Teams.Team,
|
| 12 |
+
as: :team,
|
| 13 |
+
left_lateral_join: s in subquery(Teams.last_subscription_join_query()),
|
| 14 |
+
on: true,
|
| 15 |
+
preload: [subscription: s]
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
for team <- teams do
|
| 19 |
+
Plausible.Billing.SiteLocker.update_for(team)
|
| 20 |
+
end
|
| 21 |
+
|
| 22 |
+
:ok
|
| 23 |
+
end
|
| 24 |
+
end
|
lib/workers/notify_annual_renewal.ex
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.NotifyAnnualRenewal do
|
| 2 |
+
use Plausible.Repo
|
| 3 |
+
use Oban.Worker, queue: :notify_annual_renewal
|
| 4 |
+
|
| 5 |
+
require Plausible.Billing.Subscription.Status
|
| 6 |
+
|
| 7 |
+
alias Money.Subscription
|
| 8 |
+
alias Plausible.Billing.Subscription
|
| 9 |
+
alias Plausible.Teams
|
| 10 |
+
|
| 11 |
+
@yearly_plans Plausible.Billing.Plans.yearly_product_ids()
|
| 12 |
+
|
| 13 |
+
@impl Oban.Worker
|
| 14 |
+
@doc """
|
| 15 |
+
Sends a notification at most 7 days and at least 1 day before the renewal of an annual subscription
|
| 16 |
+
"""
|
| 17 |
+
def perform(_job) do
|
| 18 |
+
sent_notification =
|
| 19 |
+
from(
|
| 20 |
+
s in "sent_renewal_notifications",
|
| 21 |
+
where: s.timestamp > fragment("now() - INTERVAL '1 month'")
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
teams =
|
| 25 |
+
Repo.all(
|
| 26 |
+
from t in Teams.Team,
|
| 27 |
+
as: :team,
|
| 28 |
+
inner_join: o in assoc(t, :owners),
|
| 29 |
+
left_join: bm in assoc(t, :billing_members),
|
| 30 |
+
inner_lateral_join: s in subquery(Teams.last_subscription_join_query()),
|
| 31 |
+
on: true,
|
| 32 |
+
left_join: sent in ^sent_notification,
|
| 33 |
+
on: o.id == sent.user_id,
|
| 34 |
+
where: is_nil(sent.id),
|
| 35 |
+
where: s.paddle_plan_id in @yearly_plans,
|
| 36 |
+
where:
|
| 37 |
+
s.next_bill_date > fragment("now()::date") and
|
| 38 |
+
s.next_bill_date <= fragment("now()::date + INTERVAL '7 days'"),
|
| 39 |
+
preload: [owners: o, subscription: s, billing_members: bm]
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
for team <- teams do
|
| 43 |
+
recipients = team.owners ++ team.billing_members
|
| 44 |
+
|
| 45 |
+
case team.subscription.status do
|
| 46 |
+
Subscription.Status.active() ->
|
| 47 |
+
for recipient <- recipients do
|
| 48 |
+
template = PlausibleWeb.Email.yearly_renewal_notification(team, recipient)
|
| 49 |
+
Plausible.Mailer.send(template)
|
| 50 |
+
end
|
| 51 |
+
|
| 52 |
+
Subscription.Status.deleted() ->
|
| 53 |
+
for recipient <- recipients do
|
| 54 |
+
template = PlausibleWeb.Email.yearly_expiration_notification(team, recipient)
|
| 55 |
+
Plausible.Mailer.send(template)
|
| 56 |
+
end
|
| 57 |
+
|
| 58 |
+
_ ->
|
| 59 |
+
Sentry.capture_message("Invalid subscription for renewal",
|
| 60 |
+
team: team
|
| 61 |
+
)
|
| 62 |
+
end
|
| 63 |
+
|
| 64 |
+
for recipient <- recipients do
|
| 65 |
+
Repo.insert_all("sent_renewal_notifications", [
|
| 66 |
+
%{
|
| 67 |
+
user_id: recipient.id,
|
| 68 |
+
timestamp: NaiveDateTime.utc_now()
|
| 69 |
+
}
|
| 70 |
+
])
|
| 71 |
+
end
|
| 72 |
+
end
|
| 73 |
+
|
| 74 |
+
:ok
|
| 75 |
+
end
|
| 76 |
+
end
|
lib/workers/notify_exported_analytics.ex
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.NotifyExportedAnalytics do
|
| 2 |
+
@moduledoc "This worker delivers emails for successful and failed exports"
|
| 3 |
+
|
| 4 |
+
use Oban.Worker,
|
| 5 |
+
queue: :notify_exported_analytics,
|
| 6 |
+
max_attempts: 5
|
| 7 |
+
|
| 8 |
+
@impl true
|
| 9 |
+
def perform(%Oban.Job{args: args}) do
|
| 10 |
+
%{
|
| 11 |
+
"status" => status,
|
| 12 |
+
"storage" => storage,
|
| 13 |
+
"email_to" => email_to,
|
| 14 |
+
"site_id" => site_id
|
| 15 |
+
} = args
|
| 16 |
+
|
| 17 |
+
user = Plausible.Repo.get_by!(Plausible.Auth.User, email: email_to)
|
| 18 |
+
|
| 19 |
+
site =
|
| 20 |
+
Plausible.Site
|
| 21 |
+
|> Plausible.Repo.get!(site_id)
|
| 22 |
+
|> Plausible.Repo.preload(:team)
|
| 23 |
+
|
| 24 |
+
email =
|
| 25 |
+
case status do
|
| 26 |
+
"success" ->
|
| 27 |
+
case storage do
|
| 28 |
+
"s3" ->
|
| 29 |
+
%{expires_at: expires_at} = Plausible.Exports.get_s3_export!(site_id, _retries = 10)
|
| 30 |
+
PlausibleWeb.Email.export_success(user, site, expires_at)
|
| 31 |
+
|
| 32 |
+
"local" ->
|
| 33 |
+
PlausibleWeb.Email.export_success(user, site, _expires_at = nil)
|
| 34 |
+
end
|
| 35 |
+
|
| 36 |
+
"failure" ->
|
| 37 |
+
PlausibleWeb.Email.export_failure(user, site)
|
| 38 |
+
end
|
| 39 |
+
|
| 40 |
+
Plausible.Mailer.deliver_now!(email)
|
| 41 |
+
:ok
|
| 42 |
+
end
|
| 43 |
+
end
|
lib/workers/purge_cdn_cache.ex
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.PurgeCDNCache do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Worker for purging CDN cache for tracker scripts on cloud.
|
| 4 |
+
|
| 5 |
+
Uses Bunny CDN's API to purge cache by tag.
|
| 6 |
+
Docs ref: https://docs.bunny.net/reference/pullzonepublic_purgecachepostbytag
|
| 7 |
+
|
| 8 |
+
Note that purging by id "*" is equivalent to purging ALL cache.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
use Oban.Worker,
|
| 12 |
+
queue: :purge_cdn_cache,
|
| 13 |
+
max_attempts: 5,
|
| 14 |
+
# To avoid running into API rate limits, we:
|
| 15 |
+
# - Schedule jobs with a delay
|
| 16 |
+
# - Bump the scheduled time every time a new one is scheduled with the same args
|
| 17 |
+
unique: [
|
| 18 |
+
states: [:scheduled],
|
| 19 |
+
fields: [:args]
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
require Logger
|
| 23 |
+
|
| 24 |
+
@impl Oban.Worker
|
| 25 |
+
def perform(%Oban.Job{args: %{"id" => id}}) do
|
| 26 |
+
pullzone_id = Application.get_env(:plausible, __MODULE__, [])[:pullzone_id]
|
| 27 |
+
api_key = Application.get_env(:plausible, __MODULE__, [])[:api_key]
|
| 28 |
+
|
| 29 |
+
purge_cache(id, pullzone_id, api_key)
|
| 30 |
+
end
|
| 31 |
+
|
| 32 |
+
@impl Oban.Worker
|
| 33 |
+
def backoff(%Oban.Job{attempt: attempt}) do
|
| 34 |
+
# Exponential backoff starting at 3 minutes
|
| 35 |
+
trunc(:math.pow(2, attempt - 1) * 180)
|
| 36 |
+
end
|
| 37 |
+
|
| 38 |
+
defp purge_cache(id, pullzone_id, api_key) when is_nil(pullzone_id) or is_nil(api_key) do
|
| 39 |
+
Logger.warning("Ignoring purge CDN cache for tracker script #{id}: Configuration missing")
|
| 40 |
+
{:discard, "Configuration missing"}
|
| 41 |
+
end
|
| 42 |
+
|
| 43 |
+
defp purge_cache(id, pullzone_id, api_key) do
|
| 44 |
+
options =
|
| 45 |
+
[
|
| 46 |
+
headers: [
|
| 47 |
+
{"content-type", "application/json"},
|
| 48 |
+
{"AccessKey", api_key}
|
| 49 |
+
],
|
| 50 |
+
body: Jason.encode!(%{"CacheTag" => "tracker_script::#{id}"})
|
| 51 |
+
]
|
| 52 |
+
|> Keyword.merge(Application.get_env(:plausible, __MODULE__)[:req_opts] || [])
|
| 53 |
+
|
| 54 |
+
case Req.post("https://api.bunny.net/pullzone/#{pullzone_id}/purgeCache", options) do
|
| 55 |
+
{:ok, %{status: 204}} ->
|
| 56 |
+
Logger.notice("Successfully purged CDN cache for tracker script #{id}")
|
| 57 |
+
{:ok, :success}
|
| 58 |
+
|
| 59 |
+
{:ok, %{status: status}} ->
|
| 60 |
+
Logger.warning(
|
| 61 |
+
"Failed to purge CDN cache for tracker script #{id}: Unexpected status: #{status}"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
{:error, "Unexpected status: #{status}"}
|
| 65 |
+
|
| 66 |
+
{:error, reason} ->
|
| 67 |
+
Logger.warning("Failed to purge CDN cache for tracker script #{id}: #{inspect(reason)}")
|
| 68 |
+
{:error, reason}
|
| 69 |
+
end
|
| 70 |
+
end
|
| 71 |
+
end
|
lib/workers/rotate_salts.ex
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.RotateSalts do
|
| 2 |
+
use Plausible.Repo
|
| 3 |
+
use Oban.Worker, queue: :rotate_salts
|
| 4 |
+
|
| 5 |
+
@impl Oban.Worker
|
| 6 |
+
def perform(_job) do
|
| 7 |
+
Plausible.Session.Salts.rotate()
|
| 8 |
+
end
|
| 9 |
+
end
|
lib/workers/schedule_email_reports.ex
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.ScheduleEmailReports do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Job scheduling dispatch of weekly and monthly email reports.
|
| 4 |
+
"""
|
| 5 |
+
use Plausible.Repo
|
| 6 |
+
use Oban.Worker, queue: :schedule_email_reports
|
| 7 |
+
alias Plausible.Workers.SendEmailReport
|
| 8 |
+
|
| 9 |
+
@impl Oban.Worker
|
| 10 |
+
@doc """
|
| 11 |
+
Email reports should be sent on Monday at 9am according to the timezone
|
| 12 |
+
of a site.
|
| 13 |
+
"""
|
| 14 |
+
def perform(_job) do
|
| 15 |
+
schedule_weekly_emails()
|
| 16 |
+
schedule_monthly_emails()
|
| 17 |
+
end
|
| 18 |
+
|
| 19 |
+
defp schedule_weekly_emails() do
|
| 20 |
+
weekly_jobs =
|
| 21 |
+
from(
|
| 22 |
+
j in Oban.Job,
|
| 23 |
+
where:
|
| 24 |
+
j.worker == "Plausible.Workers.SendEmailReport" and
|
| 25 |
+
fragment("(? ->> 'interval')", j.args) == "weekly"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
sites =
|
| 29 |
+
Repo.all(
|
| 30 |
+
from s in Plausible.Site,
|
| 31 |
+
inner_join: t in assoc(s, :team),
|
| 32 |
+
join: wr in Plausible.Site.WeeklyReport,
|
| 33 |
+
on: wr.site_id == s.id,
|
| 34 |
+
left_join: job in subquery(weekly_jobs),
|
| 35 |
+
on:
|
| 36 |
+
fragment("(? -> 'site_id')::int", job.args) == s.id and
|
| 37 |
+
job.state not in ["completed", "discarded"],
|
| 38 |
+
where: is_nil(job),
|
| 39 |
+
where: not t.locked,
|
| 40 |
+
preload: [weekly_report: wr]
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
for site <- sites do
|
| 44 |
+
SendEmailReport.new(%{site_id: site.id, interval: "weekly"},
|
| 45 |
+
scheduled_at: monday_9am(site.timezone)
|
| 46 |
+
)
|
| 47 |
+
|> Oban.insert!()
|
| 48 |
+
end
|
| 49 |
+
|
| 50 |
+
:ok
|
| 51 |
+
end
|
| 52 |
+
|
| 53 |
+
def monday_9am(timezone) do
|
| 54 |
+
DateTime.now!(timezone)
|
| 55 |
+
|> DateTime.shift(week: 1)
|
| 56 |
+
|> Plausible.Times.beginning_of_week()
|
| 57 |
+
|> DateTime.shift(hour: 9)
|
| 58 |
+
end
|
| 59 |
+
|
| 60 |
+
defp schedule_monthly_emails() do
|
| 61 |
+
monthly_jobs =
|
| 62 |
+
from(
|
| 63 |
+
j in Oban.Job,
|
| 64 |
+
where:
|
| 65 |
+
j.worker == "Plausible.Workers.SendEmailReport" and
|
| 66 |
+
fragment("(? ->> 'interval')", j.args) == "monthly"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
sites =
|
| 70 |
+
Repo.all(
|
| 71 |
+
from s in Plausible.Site,
|
| 72 |
+
inner_join: t in assoc(s, :team),
|
| 73 |
+
join: mr in Plausible.Site.MonthlyReport,
|
| 74 |
+
on: mr.site_id == s.id,
|
| 75 |
+
left_join: job in subquery(monthly_jobs),
|
| 76 |
+
on:
|
| 77 |
+
fragment("(? -> 'site_id')::int", job.args) == s.id and
|
| 78 |
+
job.state not in ["completed", "discarded"],
|
| 79 |
+
where: is_nil(job),
|
| 80 |
+
where: not t.locked,
|
| 81 |
+
preload: [monthly_report: mr]
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
for site <- sites do
|
| 85 |
+
SendEmailReport.new(%{site_id: site.id, interval: "monthly"},
|
| 86 |
+
scheduled_at: first_of_month_9am(site.timezone)
|
| 87 |
+
)
|
| 88 |
+
|> Oban.insert!()
|
| 89 |
+
end
|
| 90 |
+
|
| 91 |
+
:ok
|
| 92 |
+
end
|
| 93 |
+
|
| 94 |
+
def first_of_month_9am(timezone) do
|
| 95 |
+
DateTime.now!(timezone)
|
| 96 |
+
|> DateTime.shift(month: 1)
|
| 97 |
+
|> Plausible.Times.beginning_of_month()
|
| 98 |
+
|> DateTime.shift(hour: 9)
|
| 99 |
+
end
|
| 100 |
+
end
|
lib/workers/send_check_stats_emails.ex
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.SendCheckStatsEmails do
|
| 2 |
+
use Plausible.Repo
|
| 3 |
+
use Oban.Worker, queue: :check_stats_emails
|
| 4 |
+
|
| 5 |
+
@impl Oban.Worker
|
| 6 |
+
def perform(_job) do
|
| 7 |
+
q =
|
| 8 |
+
from(u in Plausible.Auth.User,
|
| 9 |
+
left_join: ce in "check_stats_emails",
|
| 10 |
+
on: ce.user_id == u.id,
|
| 11 |
+
where: is_nil(ce.id),
|
| 12 |
+
where:
|
| 13 |
+
u.inserted_at > fragment("(now() at time zone 'utc') - '14 days'::interval") and
|
| 14 |
+
u.inserted_at < fragment("(now() at time zone 'utc') - '7 days'::interval") and
|
| 15 |
+
u.last_seen < fragment("(now() at time zone 'utc') - '7 days'::interval")
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
for user <- Repo.all(q) do
|
| 19 |
+
if eligible_for_check_stats_email?(user) do
|
| 20 |
+
send_check_stats_email(user)
|
| 21 |
+
end
|
| 22 |
+
end
|
| 23 |
+
|
| 24 |
+
:ok
|
| 25 |
+
end
|
| 26 |
+
|
| 27 |
+
defp eligible_for_check_stats_email?(user) do
|
| 28 |
+
sites =
|
| 29 |
+
from(tm in Plausible.Teams.Membership,
|
| 30 |
+
inner_join: t in assoc(tm, :team),
|
| 31 |
+
inner_join: s in assoc(t, :sites),
|
| 32 |
+
left_join: gm in assoc(tm, :guest_memberships),
|
| 33 |
+
where: tm.user_id == ^user.id,
|
| 34 |
+
where: tm.role != :guest or gm.site_id == s.id,
|
| 35 |
+
select: s
|
| 36 |
+
)
|
| 37 |
+
|> Repo.all()
|
| 38 |
+
|> Repo.preload(:weekly_report)
|
| 39 |
+
|
| 40 |
+
not Enum.any?(sites, fn site -> site.weekly_report end) and
|
| 41 |
+
Enum.any?(sites, &Plausible.Sites.has_stats?/1)
|
| 42 |
+
end
|
| 43 |
+
|
| 44 |
+
defp send_check_stats_email(user) do
|
| 45 |
+
PlausibleWeb.Email.check_stats_email(user)
|
| 46 |
+
|> Plausible.Mailer.send()
|
| 47 |
+
|
| 48 |
+
Repo.insert_all("check_stats_emails", [
|
| 49 |
+
%{
|
| 50 |
+
user_id: user.id,
|
| 51 |
+
timestamp: NaiveDateTime.utc_now()
|
| 52 |
+
}
|
| 53 |
+
])
|
| 54 |
+
end
|
| 55 |
+
end
|
lib/workers/send_email_report.ex
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.SendEmailReport do
|
| 2 |
+
use Plausible
|
| 3 |
+
use Plausible.Repo
|
| 4 |
+
use Oban.Worker, queue: :send_email_reports, max_attempts: 1
|
| 5 |
+
|
| 6 |
+
alias Plausible.Stats.{QueryResult, QueryBuilder, ParsedQueryParams, QueryInclude}
|
| 7 |
+
|
| 8 |
+
import Ecto.Query, only: [from: 2]
|
| 9 |
+
|
| 10 |
+
@weekly "weekly"
|
| 11 |
+
@monthly "monthly"
|
| 12 |
+
|
| 13 |
+
def perform(%Oban.Job{args: %{"interval" => interval, "site_id" => site_id}})
|
| 14 |
+
when interval in [@weekly, @monthly] do
|
| 15 |
+
report_type = report_type(interval)
|
| 16 |
+
|
| 17 |
+
site =
|
| 18 |
+
from(s in Plausible.Site,
|
| 19 |
+
where: s.id == ^site_id,
|
| 20 |
+
inner_join: r in assoc(s, ^report_type),
|
| 21 |
+
inner_join: t in assoc(s, :team),
|
| 22 |
+
preload: [{^report_type, r}, {:team, t}]
|
| 23 |
+
)
|
| 24 |
+
|> Repo.one()
|
| 25 |
+
|
| 26 |
+
with %Plausible.Site{} <- site,
|
| 27 |
+
%{} = report <- Map.fetch!(site, report_type),
|
| 28 |
+
true <- ok_to_send?(site) do
|
| 29 |
+
date_range = date_range(site, interval)
|
| 30 |
+
report_name = report_name(interval, date_range.first)
|
| 31 |
+
date_label = Calendar.strftime(date_range.last, "%-d %b %Y")
|
| 32 |
+
stats = stats(site, date_range)
|
| 33 |
+
|
| 34 |
+
report
|
| 35 |
+
|> Map.fetch!(:recipients)
|
| 36 |
+
|> Enum.each(fn email ->
|
| 37 |
+
assigns = %{
|
| 38 |
+
site: site,
|
| 39 |
+
report_name: report_name,
|
| 40 |
+
date_label: date_label,
|
| 41 |
+
unsubscribe_link: unsubscribe_link(site, email, interval),
|
| 42 |
+
site_member?: site_member?(site, email),
|
| 43 |
+
interval: interval,
|
| 44 |
+
stats: stats
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
email
|
| 48 |
+
|> PlausibleWeb.Email.stats_report(assigns)
|
| 49 |
+
|> Plausible.Mailer.send()
|
| 50 |
+
end)
|
| 51 |
+
else
|
| 52 |
+
_ -> :discard
|
| 53 |
+
end
|
| 54 |
+
end
|
| 55 |
+
|
| 56 |
+
defp report_type(@weekly), do: :weekly_report
|
| 57 |
+
defp report_type(@monthly), do: :monthly_report
|
| 58 |
+
|
| 59 |
+
defp site_member?(site, email) do
|
| 60 |
+
user = Plausible.Auth.find_user_by(email: email)
|
| 61 |
+
user && Plausible.Teams.Memberships.site_member?(site, user)
|
| 62 |
+
end
|
| 63 |
+
|
| 64 |
+
defp unsubscribe_link(site, email, interval) do
|
| 65 |
+
PlausibleWeb.Endpoint.url() <>
|
| 66 |
+
"/sites/#{URI.encode_www_form(site.domain)}/#{interval}-report/unsubscribe?email=#{email}"
|
| 67 |
+
end
|
| 68 |
+
|
| 69 |
+
defp report_name(@weekly, _), do: "Weekly"
|
| 70 |
+
defp report_name(@monthly, first), do: Calendar.strftime(first, "%B")
|
| 71 |
+
|
| 72 |
+
defp stats(site, date_range) do
|
| 73 |
+
shared_params = %ParsedQueryParams{
|
| 74 |
+
metrics: [:visitors],
|
| 75 |
+
input_date_range: {:date_range, date_range.first, date_range.last},
|
| 76 |
+
pagination: %{limit: 5, offset: 0}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
stats = stats_aggregates(site, shared_params)
|
| 80 |
+
pages = pages(site, shared_params)
|
| 81 |
+
sources = sources(site, shared_params)
|
| 82 |
+
goals = goals(site, shared_params)
|
| 83 |
+
|
| 84 |
+
stats
|
| 85 |
+
|> Map.put(:pages, pages)
|
| 86 |
+
|> Map.put(:sources, sources)
|
| 87 |
+
|> Map.put(:goals, goals)
|
| 88 |
+
end
|
| 89 |
+
|
| 90 |
+
defp stats_aggregates(site, %ParsedQueryParams{} = shared_params) do
|
| 91 |
+
query =
|
| 92 |
+
QueryBuilder.build!(
|
| 93 |
+
site,
|
| 94 |
+
struct!(shared_params,
|
| 95 |
+
metrics: [:pageviews, :visitors, :bounce_rate],
|
| 96 |
+
include: %QueryInclude{compare: :previous_period}
|
| 97 |
+
)
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
%QueryResult{
|
| 101 |
+
results: [
|
| 102 |
+
%{
|
| 103 |
+
metrics: [pageviews, visitors, bounce_rate],
|
| 104 |
+
comparison: %{
|
| 105 |
+
change: [pageviews_change, visitors_change, bounce_rate_change]
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
]
|
| 109 |
+
} = Plausible.Stats.query(site, query)
|
| 110 |
+
|
| 111 |
+
%{
|
| 112 |
+
pageviews: %{value: pageviews, change: pageviews_change},
|
| 113 |
+
visitors: %{value: visitors, change: visitors_change},
|
| 114 |
+
bounce_rate: %{value: bounce_rate, change: bounce_rate_change}
|
| 115 |
+
}
|
| 116 |
+
end
|
| 117 |
+
|
| 118 |
+
defp pages(site, %ParsedQueryParams{} = shared_params) do
|
| 119 |
+
query = QueryBuilder.build!(site, struct!(shared_params, dimensions: ["event:page"]))
|
| 120 |
+
|
| 121 |
+
site
|
| 122 |
+
|> Plausible.Stats.query(query)
|
| 123 |
+
|> Map.fetch!(:results)
|
| 124 |
+
|> Enum.map(fn %{metrics: [visitors], dimensions: [page]} ->
|
| 125 |
+
%{
|
| 126 |
+
page: page,
|
| 127 |
+
visitors: visitors
|
| 128 |
+
}
|
| 129 |
+
end)
|
| 130 |
+
end
|
| 131 |
+
|
| 132 |
+
defp sources(site, %ParsedQueryParams{} = shared_params) do
|
| 133 |
+
query =
|
| 134 |
+
QueryBuilder.build!(
|
| 135 |
+
site,
|
| 136 |
+
struct!(shared_params,
|
| 137 |
+
dimensions: ["visit:source"],
|
| 138 |
+
filters: [[:is_not, "visit:source", ["Direct / None"]]]
|
| 139 |
+
)
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
site
|
| 143 |
+
|> Plausible.Stats.query(query)
|
| 144 |
+
|> Map.fetch!(:results)
|
| 145 |
+
|> Enum.map(fn %{metrics: [visitors], dimensions: [source]} ->
|
| 146 |
+
%{
|
| 147 |
+
source: source,
|
| 148 |
+
visitors: visitors
|
| 149 |
+
}
|
| 150 |
+
end)
|
| 151 |
+
end
|
| 152 |
+
|
| 153 |
+
defp goals(site, %ParsedQueryParams{} = shared_params) do
|
| 154 |
+
query = QueryBuilder.build!(site, struct!(shared_params, dimensions: ["event:goal"]))
|
| 155 |
+
|
| 156 |
+
site
|
| 157 |
+
|> Plausible.Stats.query(query)
|
| 158 |
+
|> Map.fetch!(:results)
|
| 159 |
+
|> Enum.map(fn %{metrics: [visitors], dimensions: [goal_name]} ->
|
| 160 |
+
%{
|
| 161 |
+
goal: goal_name,
|
| 162 |
+
visitors: visitors
|
| 163 |
+
}
|
| 164 |
+
end)
|
| 165 |
+
end
|
| 166 |
+
|
| 167 |
+
defp date_range(site, @weekly) do
|
| 168 |
+
first =
|
| 169 |
+
site.timezone
|
| 170 |
+
|> DateTime.now!()
|
| 171 |
+
|> Date.shift(day: -7)
|
| 172 |
+
|> Date.beginning_of_week()
|
| 173 |
+
|
| 174 |
+
last =
|
| 175 |
+
site.timezone
|
| 176 |
+
|> DateTime.now!()
|
| 177 |
+
|> DateTime.to_date()
|
| 178 |
+
|> Date.shift(day: -7)
|
| 179 |
+
|> Date.end_of_week()
|
| 180 |
+
|
| 181 |
+
Date.range(first, last)
|
| 182 |
+
end
|
| 183 |
+
|
| 184 |
+
defp date_range(site, @monthly) do
|
| 185 |
+
first =
|
| 186 |
+
site.timezone
|
| 187 |
+
|> DateTime.now!()
|
| 188 |
+
|> Date.shift(month: -1)
|
| 189 |
+
|> Date.beginning_of_month()
|
| 190 |
+
|
| 191 |
+
last =
|
| 192 |
+
site.timezone
|
| 193 |
+
|> DateTime.now!()
|
| 194 |
+
|> DateTime.shift(month: -1)
|
| 195 |
+
|> DateTime.to_date()
|
| 196 |
+
|> Date.end_of_month()
|
| 197 |
+
|
| 198 |
+
Date.range(first, last)
|
| 199 |
+
end
|
| 200 |
+
|
| 201 |
+
on_ee do
|
| 202 |
+
defp ok_to_send?(site) do
|
| 203 |
+
Plausible.Sites.regular?(site) or
|
| 204 |
+
(Plausible.Sites.consolidated?(site) and
|
| 205 |
+
Plausible.ConsolidatedView.ok_to_display?(site.team))
|
| 206 |
+
end
|
| 207 |
+
else
|
| 208 |
+
defp ok_to_send?(_site), do: always(true)
|
| 209 |
+
end
|
| 210 |
+
end
|
lib/workers/send_site_setup_emails.ex
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.SendSiteSetupEmails do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Job sending site setup emails.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
use Plausible.Repo
|
| 7 |
+
use Oban.Worker, queue: :site_setup_emails
|
| 8 |
+
|
| 9 |
+
@impl Oban.Worker
|
| 10 |
+
def perform(_job) do
|
| 11 |
+
send_create_site_emails()
|
| 12 |
+
send_setup_help_emails()
|
| 13 |
+
send_setup_success_emails()
|
| 14 |
+
|
| 15 |
+
:ok
|
| 16 |
+
end
|
| 17 |
+
|
| 18 |
+
defp send_create_site_emails() do
|
| 19 |
+
q =
|
| 20 |
+
from u in Plausible.Auth.User,
|
| 21 |
+
as: :user,
|
| 22 |
+
where:
|
| 23 |
+
not exists(
|
| 24 |
+
from tm in Plausible.Teams.Membership,
|
| 25 |
+
where: tm.user_id == parent_as(:user).id,
|
| 26 |
+
select: true
|
| 27 |
+
),
|
| 28 |
+
where:
|
| 29 |
+
not exists(
|
| 30 |
+
from se in "create_site_emails",
|
| 31 |
+
where: se.user_id == parent_as(:user).id,
|
| 32 |
+
select: true
|
| 33 |
+
),
|
| 34 |
+
where:
|
| 35 |
+
u.inserted_at > fragment("(now() at time zone 'utc') - '72 hours'::interval") and
|
| 36 |
+
u.inserted_at < fragment("(now() at time zone 'utc') - '48 hours'::interval")
|
| 37 |
+
|
| 38 |
+
for user <- Repo.all(q) do
|
| 39 |
+
send_create_site_email(user)
|
| 40 |
+
end
|
| 41 |
+
end
|
| 42 |
+
|
| 43 |
+
defp send_setup_help_emails() do
|
| 44 |
+
q =
|
| 45 |
+
from(s in Plausible.Site.regular(),
|
| 46 |
+
left_join: se in "setup_help_emails",
|
| 47 |
+
on: se.site_id == s.id,
|
| 48 |
+
where: is_nil(se.id),
|
| 49 |
+
where: s.inserted_at > fragment("(now() at time zone 'utc') - '72 hours'::interval"),
|
| 50 |
+
preload: [:owners, :team]
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
for site <- Repo.all(q) do
|
| 54 |
+
owners = site.owners
|
| 55 |
+
setup_completed = Plausible.Sites.has_stats?(site)
|
| 56 |
+
hours_passed = NaiveDateTime.diff(DateTime.utc_now(), site.inserted_at, :hour)
|
| 57 |
+
|
| 58 |
+
if !setup_completed && hours_passed > 47 do
|
| 59 |
+
send_setup_help_email(owners, site)
|
| 60 |
+
end
|
| 61 |
+
end
|
| 62 |
+
end
|
| 63 |
+
|
| 64 |
+
defp send_setup_success_emails() do
|
| 65 |
+
q =
|
| 66 |
+
from(s in Plausible.Site.regular(),
|
| 67 |
+
left_join: se in "setup_success_emails",
|
| 68 |
+
on: se.site_id == s.id,
|
| 69 |
+
where: is_nil(se.id),
|
| 70 |
+
inner_join: t in assoc(s, :team),
|
| 71 |
+
where: s.inserted_at > fragment("(now() at time zone 'utc') - '72 hours'::interval"),
|
| 72 |
+
preload: [:owners, team: t]
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
for site <- Repo.all(q) do
|
| 76 |
+
if Plausible.Sites.has_stats?(site) do
|
| 77 |
+
send_setup_success_email(site)
|
| 78 |
+
end
|
| 79 |
+
end
|
| 80 |
+
end
|
| 81 |
+
|
| 82 |
+
defp send_create_site_email(user) do
|
| 83 |
+
PlausibleWeb.Email.create_site_email(user)
|
| 84 |
+
|> Plausible.Mailer.send()
|
| 85 |
+
|
| 86 |
+
Repo.insert_all("create_site_emails", [
|
| 87 |
+
%{
|
| 88 |
+
user_id: user.id,
|
| 89 |
+
timestamp: NaiveDateTime.utc_now()
|
| 90 |
+
}
|
| 91 |
+
])
|
| 92 |
+
end
|
| 93 |
+
|
| 94 |
+
defp send_setup_success_email(site) do
|
| 95 |
+
for owner <- site.owners do
|
| 96 |
+
PlausibleWeb.Email.site_setup_success(owner, site)
|
| 97 |
+
|> Plausible.Mailer.send()
|
| 98 |
+
end
|
| 99 |
+
|
| 100 |
+
Repo.insert_all("setup_success_emails", [
|
| 101 |
+
%{
|
| 102 |
+
site_id: site.id,
|
| 103 |
+
timestamp: NaiveDateTime.utc_now()
|
| 104 |
+
}
|
| 105 |
+
])
|
| 106 |
+
end
|
| 107 |
+
|
| 108 |
+
defp send_setup_help_email(users, site) do
|
| 109 |
+
for user <- users do
|
| 110 |
+
PlausibleWeb.Email.site_setup_help(user, site.team, site)
|
| 111 |
+
|> Plausible.Mailer.send()
|
| 112 |
+
end
|
| 113 |
+
|
| 114 |
+
Repo.insert_all("setup_help_emails", [
|
| 115 |
+
%{
|
| 116 |
+
site_id: site.id,
|
| 117 |
+
timestamp: NaiveDateTime.utc_now()
|
| 118 |
+
}
|
| 119 |
+
])
|
| 120 |
+
end
|
| 121 |
+
end
|
lib/workers/send_trial_notifications.ex
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.SendTrialNotifications do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Job sending trial end notification emails.
|
| 4 |
+
"""
|
| 5 |
+
use Plausible.Repo
|
| 6 |
+
|
| 7 |
+
use Oban.Worker,
|
| 8 |
+
queue: :trial_notification_emails,
|
| 9 |
+
max_attempts: 1
|
| 10 |
+
|
| 11 |
+
alias Plausible.Teams
|
| 12 |
+
|
| 13 |
+
@impl Oban.Worker
|
| 14 |
+
def perform(_job) do
|
| 15 |
+
teams =
|
| 16 |
+
Repo.all(
|
| 17 |
+
from t in Teams.Team,
|
| 18 |
+
inner_join: o in assoc(t, :owners),
|
| 19 |
+
left_join: bm in assoc(t, :billing_members),
|
| 20 |
+
left_join: s in assoc(t, :subscription),
|
| 21 |
+
where: not is_nil(t.trial_expiry_date),
|
| 22 |
+
where: is_nil(s.id),
|
| 23 |
+
order_by: t.inserted_at,
|
| 24 |
+
preload: [owners: o, billing_members: bm]
|
| 25 |
+
)
|
| 26 |
+
|> Enum.filter(&Teams.has_active_sites?/1)
|
| 27 |
+
|
| 28 |
+
for team <- teams do
|
| 29 |
+
recipients = team.owners ++ team.billing_members
|
| 30 |
+
|
| 31 |
+
case Date.diff(team.trial_expiry_date, Date.utc_today()) do
|
| 32 |
+
7 ->
|
| 33 |
+
send_one_week_reminder(recipients, team)
|
| 34 |
+
|
| 35 |
+
1 ->
|
| 36 |
+
send_tomorrow_reminder(recipients, team)
|
| 37 |
+
|
| 38 |
+
0 ->
|
| 39 |
+
send_today_reminder(recipients, team)
|
| 40 |
+
|
| 41 |
+
-1 ->
|
| 42 |
+
send_over_reminder(recipients, team)
|
| 43 |
+
|
| 44 |
+
_ ->
|
| 45 |
+
nil
|
| 46 |
+
end
|
| 47 |
+
end
|
| 48 |
+
|
| 49 |
+
:ok
|
| 50 |
+
end
|
| 51 |
+
|
| 52 |
+
defp send_one_week_reminder(users, team) do
|
| 53 |
+
for user <- users do
|
| 54 |
+
PlausibleWeb.Email.trial_one_week_reminder(user, team)
|
| 55 |
+
|> Plausible.Mailer.send()
|
| 56 |
+
end
|
| 57 |
+
end
|
| 58 |
+
|
| 59 |
+
defp send_tomorrow_reminder(users, team) do
|
| 60 |
+
usage = Plausible.Teams.Billing.usage_cycle(team, :last_30_days)
|
| 61 |
+
suggested_volume = Plausible.Billing.Plans.suggest_volume(team, usage.total)
|
| 62 |
+
|
| 63 |
+
for user <- users do
|
| 64 |
+
PlausibleWeb.Email.trial_ending_tomorrow_email(user, team, usage, suggested_volume)
|
| 65 |
+
|> Plausible.Mailer.send()
|
| 66 |
+
end
|
| 67 |
+
end
|
| 68 |
+
|
| 69 |
+
defp send_today_reminder(users, team) do
|
| 70 |
+
usage = Plausible.Teams.Billing.usage_cycle(team, :last_30_days)
|
| 71 |
+
suggested_volume = Plausible.Billing.Plans.suggest_volume(team, usage.total)
|
| 72 |
+
|
| 73 |
+
for user <- users do
|
| 74 |
+
PlausibleWeb.Email.trial_ending_today_email(user, team, usage, suggested_volume)
|
| 75 |
+
|> Plausible.Mailer.send()
|
| 76 |
+
end
|
| 77 |
+
end
|
| 78 |
+
|
| 79 |
+
defp send_over_reminder(users, team) do
|
| 80 |
+
for user <- users do
|
| 81 |
+
PlausibleWeb.Email.trial_over_email(user, team)
|
| 82 |
+
|> Plausible.Mailer.send()
|
| 83 |
+
end
|
| 84 |
+
end
|
| 85 |
+
end
|
lib/workers/set_legacy_time_on_page_cutoff.ex
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.SetLegacyTimeOnPageCutoff do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Sets sites `legacy_time_on_page_cutoff` depending on whether they have
|
| 4 |
+
sent us engagement data in the past.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
use Plausible.Repo
|
| 8 |
+
use Plausible.ClickhouseRepo
|
| 9 |
+
use Oban.Worker, queue: :legacy_time_on_page_cutoff
|
| 10 |
+
|
| 11 |
+
import Ecto.Query
|
| 12 |
+
|
| 13 |
+
require Logger
|
| 14 |
+
|
| 15 |
+
@impl Oban.Worker
|
| 16 |
+
def perform(%Oban.Job{args: args}) do
|
| 17 |
+
cutoff_date = Map.get(args, "cutoff_date", Date.utc_today())
|
| 18 |
+
|
| 19 |
+
{small_sites, large_sites} =
|
| 20 |
+
sites_with_engagement_data(cutoff_date)
|
| 21 |
+
|> filter_sites_needing_update()
|
| 22 |
+
|
| 23 |
+
to_update = small_sites ++ large_sites
|
| 24 |
+
|
| 25 |
+
if length(to_update) > 0 do
|
| 26 |
+
Logger.notice(
|
| 27 |
+
"Setting legacy_time_on_page_cutoff for #{length(to_update)} sites (#{length(small_sites)} small, #{length(large_sites)} large)"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
{count, _} =
|
| 31 |
+
Repo.update_all(
|
| 32 |
+
from(s in Plausible.Site.regular(),
|
| 33 |
+
where: s.id in ^to_update,
|
| 34 |
+
where: is_nil(s.legacy_time_on_page_cutoff)
|
| 35 |
+
),
|
| 36 |
+
set: [legacy_time_on_page_cutoff: cutoff_date]
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
Logger.notice(
|
| 40 |
+
"Successfully set legacy_time_on_page_cutoff=#{cutoff_date} for #{count} sites"
|
| 41 |
+
)
|
| 42 |
+
else
|
| 43 |
+
Logger.debug("No sites legacy_time_on_page_cutoff needs updating")
|
| 44 |
+
end
|
| 45 |
+
|
| 46 |
+
:ok
|
| 47 |
+
end
|
| 48 |
+
|
| 49 |
+
defp sites_with_engagement_data(cutoff_date) do
|
| 50 |
+
site_info_q =
|
| 51 |
+
from(
|
| 52 |
+
e in "events_v2",
|
| 53 |
+
where: e.timestamp >= fragment("toStartOfHour(? - toIntervalHour(48))", ^cutoff_date),
|
| 54 |
+
where: e.timestamp < fragment("toStartOfHour(? - toIntervalHour(24))", ^cutoff_date),
|
| 55 |
+
group_by: e.site_id,
|
| 56 |
+
select: %{
|
| 57 |
+
site_id: e.site_id,
|
| 58 |
+
hours_with_engagement:
|
| 59 |
+
fragment(
|
| 60 |
+
"uniqIf(toStartOfHour(timestamp), name = 'engagement' AND engagement_time > 0)"
|
| 61 |
+
),
|
| 62 |
+
is_small_site: fragment("count() < 2000")
|
| 63 |
+
}
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
q =
|
| 67 |
+
from(
|
| 68 |
+
s in subquery(site_info_q),
|
| 69 |
+
select: %{
|
| 70 |
+
small_sites:
|
| 71 |
+
fragment("groupArrayIf(site_id, (is_small_site and hours_with_engagement > 0))"),
|
| 72 |
+
large_sites:
|
| 73 |
+
fragment("groupArrayIf(site_id, (not is_small_site and hours_with_engagement = 24))")
|
| 74 |
+
}
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
result = ClickhouseRepo.one(q)
|
| 78 |
+
|
| 79 |
+
{MapSet.new(result.small_sites), MapSet.new(result.large_sites)}
|
| 80 |
+
end
|
| 81 |
+
|
| 82 |
+
defp filter_sites_needing_update({small_sites, large_sites}) do
|
| 83 |
+
needing_update =
|
| 84 |
+
from(s in Plausible.Site.regular(),
|
| 85 |
+
where: is_nil(s.legacy_time_on_page_cutoff),
|
| 86 |
+
select: s.id
|
| 87 |
+
)
|
| 88 |
+
|> Repo.all()
|
| 89 |
+
|> MapSet.new()
|
| 90 |
+
|
| 91 |
+
{
|
| 92 |
+
small_sites |> MapSet.intersection(needing_update) |> MapSet.to_list(),
|
| 93 |
+
large_sites |> MapSet.intersection(needing_update) |> MapSet.to_list()
|
| 94 |
+
}
|
| 95 |
+
end
|
| 96 |
+
end
|
lib/workers/traffic_change_notifier.ex
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.Workers.TrafficChangeNotifier do
|
| 2 |
+
@moduledoc """
|
| 3 |
+
Oban service sending out traffic drop/spike notifications
|
| 4 |
+
"""
|
| 5 |
+
use Plausible
|
| 6 |
+
use Plausible.Repo
|
| 7 |
+
alias Plausible.Stats.{Clickhouse, ParsedQueryParams, QueryBuilder}
|
| 8 |
+
alias Plausible.Site.TrafficChangeNotification
|
| 9 |
+
|
| 10 |
+
alias PlausibleWeb.Router.Helpers, as: Routes
|
| 11 |
+
|
| 12 |
+
use Oban.Worker, queue: :spike_notifications
|
| 13 |
+
@min_interval_hours 12
|
| 14 |
+
|
| 15 |
+
@impl Oban.Worker
|
| 16 |
+
def perform(_job, now \\ NaiveDateTime.utc_now(:second)) do
|
| 17 |
+
today = NaiveDateTime.to_date(now)
|
| 18 |
+
|
| 19 |
+
notifications =
|
| 20 |
+
Repo.all(
|
| 21 |
+
from sn in TrafficChangeNotification,
|
| 22 |
+
where:
|
| 23 |
+
is_nil(sn.last_sent) or
|
| 24 |
+
sn.last_sent < ^NaiveDateTime.add(now, -@min_interval_hours, :hour),
|
| 25 |
+
inner_join: s in assoc(sn, :site),
|
| 26 |
+
inner_join: t in assoc(s, :team),
|
| 27 |
+
where: not t.locked,
|
| 28 |
+
where: is_nil(t.accept_traffic_until) or t.accept_traffic_until > ^today,
|
| 29 |
+
preload: [site: {s, team: t}]
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
for notification <- notifications, ok_to_send?(notification.site) do
|
| 33 |
+
handle_notification(notification, now)
|
| 34 |
+
end
|
| 35 |
+
|
| 36 |
+
:ok
|
| 37 |
+
end
|
| 38 |
+
|
| 39 |
+
defp handle_notification(%TrafficChangeNotification{type: :spike} = notification, now) do
|
| 40 |
+
current_visitors = Clickhouse.current_visitors(notification.site)
|
| 41 |
+
|
| 42 |
+
if current_visitors >= notification.threshold do
|
| 43 |
+
stats =
|
| 44 |
+
notification.site
|
| 45 |
+
|> get_traffic_spike_stats()
|
| 46 |
+
|> Map.put(:current_visitors, current_visitors)
|
| 47 |
+
|
| 48 |
+
notify_spike(notification, stats, now)
|
| 49 |
+
end
|
| 50 |
+
end
|
| 51 |
+
|
| 52 |
+
defp handle_notification(%TrafficChangeNotification{type: :drop} = notification, now) do
|
| 53 |
+
current_visitors = Clickhouse.current_visitors_12h(notification.site)
|
| 54 |
+
|
| 55 |
+
if current_visitors < notification.threshold do
|
| 56 |
+
notify_drop(notification, current_visitors, now)
|
| 57 |
+
end
|
| 58 |
+
end
|
| 59 |
+
|
| 60 |
+
defp notify_spike(notification, stats, now) do
|
| 61 |
+
for recipient_email <- notification.recipients do
|
| 62 |
+
send_spike_notification(recipient_email, notification.site, stats)
|
| 63 |
+
end
|
| 64 |
+
|
| 65 |
+
notification
|
| 66 |
+
|> TrafficChangeNotification.was_sent(now)
|
| 67 |
+
|> Repo.update()
|
| 68 |
+
end
|
| 69 |
+
|
| 70 |
+
defp notify_drop(notification, current_visitors, now) do
|
| 71 |
+
for recipient_email <- notification.recipients do
|
| 72 |
+
send_drop_notification(recipient_email, notification.site, current_visitors)
|
| 73 |
+
end
|
| 74 |
+
|
| 75 |
+
notification
|
| 76 |
+
|> TrafficChangeNotification.was_sent(now)
|
| 77 |
+
|> Repo.update()
|
| 78 |
+
end
|
| 79 |
+
|
| 80 |
+
defp send_spike_notification(recipient_email, site, stats) do
|
| 81 |
+
dashboard_link =
|
| 82 |
+
if site_member?(site, recipient_email) do
|
| 83 |
+
Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, []) <>
|
| 84 |
+
"?__team=#{site.team.identifier}"
|
| 85 |
+
end
|
| 86 |
+
|
| 87 |
+
template =
|
| 88 |
+
PlausibleWeb.Email.spike_notification(
|
| 89 |
+
recipient_email,
|
| 90 |
+
site,
|
| 91 |
+
stats,
|
| 92 |
+
dashboard_link
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
Plausible.Mailer.send(template)
|
| 96 |
+
end
|
| 97 |
+
|
| 98 |
+
defp send_drop_notification(recipient_email, site, current_visitors) do
|
| 99 |
+
site_member? = site_member?(site, recipient_email)
|
| 100 |
+
|
| 101 |
+
dashboard_link =
|
| 102 |
+
if site_member? do
|
| 103 |
+
Routes.stats_url(PlausibleWeb.Endpoint, :stats, site.domain, []) <>
|
| 104 |
+
"?__team=#{site.team.identifier}"
|
| 105 |
+
end
|
| 106 |
+
|
| 107 |
+
installation_link =
|
| 108 |
+
if site_member? and Plausible.Sites.regular?(site) do
|
| 109 |
+
Routes.site_url(PlausibleWeb.Endpoint, :installation, site.domain,
|
| 110 |
+
flow: PlausibleWeb.Flows.review()
|
| 111 |
+
) <> "&__team=#{site.team.identifier}"
|
| 112 |
+
end
|
| 113 |
+
|
| 114 |
+
template =
|
| 115 |
+
PlausibleWeb.Email.drop_notification(
|
| 116 |
+
recipient_email,
|
| 117 |
+
site,
|
| 118 |
+
current_visitors,
|
| 119 |
+
dashboard_link,
|
| 120 |
+
installation_link
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
Plausible.Mailer.send(template)
|
| 124 |
+
end
|
| 125 |
+
|
| 126 |
+
defp get_traffic_spike_stats(site) do
|
| 127 |
+
%{}
|
| 128 |
+
|> put_sources(site)
|
| 129 |
+
|> put_pages(site)
|
| 130 |
+
end
|
| 131 |
+
|
| 132 |
+
@base_query_params %ParsedQueryParams{
|
| 133 |
+
metrics: [:visitors],
|
| 134 |
+
pagination: %{limit: 3, offset: 0},
|
| 135 |
+
input_date_range: :realtime
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
defp put_sources(stats, site) do
|
| 139 |
+
query =
|
| 140 |
+
QueryBuilder.build!(
|
| 141 |
+
site,
|
| 142 |
+
struct!(@base_query_params,
|
| 143 |
+
dimensions: ["visit:source"],
|
| 144 |
+
filters: [[:is_not, "visit:source", ["Direct / None"]]]
|
| 145 |
+
)
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
%{results: sources} = Plausible.Stats.query(site, query)
|
| 149 |
+
|
| 150 |
+
Map.put(stats, :sources, sources)
|
| 151 |
+
end
|
| 152 |
+
|
| 153 |
+
defp put_pages(stats, site) do
|
| 154 |
+
query = QueryBuilder.build!(site, struct!(@base_query_params, dimensions: ["event:page"]))
|
| 155 |
+
|
| 156 |
+
%{results: pages} = Plausible.Stats.query(site, query)
|
| 157 |
+
|
| 158 |
+
Map.put(stats, :pages, pages)
|
| 159 |
+
end
|
| 160 |
+
|
| 161 |
+
defp site_member?(site, recipient_email) do
|
| 162 |
+
from(tm in Plausible.Teams.Membership,
|
| 163 |
+
inner_join: u in assoc(tm, :user),
|
| 164 |
+
left_join: gm in assoc(tm, :guest_memberships),
|
| 165 |
+
where: tm.team_id == ^site.team_id,
|
| 166 |
+
where: tm.role != :guest or gm.site_id == ^site.id,
|
| 167 |
+
where: u.email == ^recipient_email
|
| 168 |
+
)
|
| 169 |
+
|> Repo.exists?()
|
| 170 |
+
end
|
| 171 |
+
|
| 172 |
+
on_ee do
|
| 173 |
+
defp ok_to_send?(site) do
|
| 174 |
+
Plausible.Sites.regular?(site) or
|
| 175 |
+
(Plausible.Sites.consolidated?(site) and
|
| 176 |
+
Plausible.ConsolidatedView.ok_to_display?(site.team))
|
| 177 |
+
end
|
| 178 |
+
else
|
| 179 |
+
defp ok_to_send?(_site), do: always(true)
|
| 180 |
+
end
|
| 181 |
+
end
|
mix.exs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
defmodule Plausible.MixProject do
|
| 2 |
+
use Mix.Project
|
| 3 |
+
|
| 4 |
+
def project do
|
| 5 |
+
[
|
| 6 |
+
name: "Plausible",
|
| 7 |
+
source_url: "https://github.com/plausible/analytics",
|
| 8 |
+
docs: docs(),
|
| 9 |
+
app: :plausible,
|
| 10 |
+
version: System.get_env("APP_VERSION", "0.0.1"),
|
| 11 |
+
elixir: "~> 1.18",
|
| 12 |
+
elixirc_paths: elixirc_paths(Mix.env()),
|
| 13 |
+
start_permanent: Mix.env() in [:prod, :ce, :load],
|
| 14 |
+
aliases: aliases(),
|
| 15 |
+
deps: deps(),
|
| 16 |
+
test_coverage: [
|
| 17 |
+
tool: ExCoveralls
|
| 18 |
+
],
|
| 19 |
+
listeners: [Phoenix.CodeReloader],
|
| 20 |
+
releases: [
|
| 21 |
+
plausible: [
|
| 22 |
+
include_executables_for: [:unix],
|
| 23 |
+
config_providers: [
|
| 24 |
+
{Config.Reader,
|
| 25 |
+
path: {:system, "RELEASE_ROOT", "/import_extra_config.exs"}, imports: []}
|
| 26 |
+
]
|
| 27 |
+
]
|
| 28 |
+
],
|
| 29 |
+
dialyzer: [
|
| 30 |
+
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
|
| 31 |
+
plt_add_apps: [:mix, :ex_unit]
|
| 32 |
+
]
|
| 33 |
+
]
|
| 34 |
+
end
|
| 35 |
+
|
| 36 |
+
# Configuration for the OTP application.
|
| 37 |
+
#
|
| 38 |
+
# Type `mix help compile.app` for more information.
|
| 39 |
+
def application do
|
| 40 |
+
[
|
| 41 |
+
mod: {Plausible.Application, []},
|
| 42 |
+
extra_applications:
|
| 43 |
+
[
|
| 44 |
+
:logger,
|
| 45 |
+
:runtime_tools,
|
| 46 |
+
:tls_certificate_check,
|
| 47 |
+
:opentelemetry_exporter
|
| 48 |
+
] ++ if(Mix.env() in [:dev, :load], do: [:tools, :observer, :wx], else: [])
|
| 49 |
+
]
|
| 50 |
+
end
|
| 51 |
+
|
| 52 |
+
def cli do
|
| 53 |
+
[
|
| 54 |
+
preferred_envs: [
|
| 55 |
+
"test.e2e": :e2e_test,
|
| 56 |
+
"test.e2e.ui": :e2e_test
|
| 57 |
+
]
|
| 58 |
+
]
|
| 59 |
+
end
|
| 60 |
+
|
| 61 |
+
# Specifies which paths to compile per environment.
|
| 62 |
+
defp elixirc_paths(env) when env in [:test, :e2e_test, :dev],
|
| 63 |
+
do: ["lib", "test/support", "extra/lib"]
|
| 64 |
+
|
| 65 |
+
defp elixirc_paths(env) when env in [:ce_test, :ce_dev],
|
| 66 |
+
do: ["lib", "test/support"]
|
| 67 |
+
|
| 68 |
+
defp elixirc_paths(:ce), do: ["lib"]
|
| 69 |
+
defp elixirc_paths(_), do: ["lib", "extra/lib"]
|
| 70 |
+
|
| 71 |
+
# Specifies your project dependencies.
|
| 72 |
+
#
|
| 73 |
+
# Type `mix help deps` for examples and options.
|
| 74 |
+
defp deps do
|
| 75 |
+
[
|
| 76 |
+
{:bamboo, "~> 2.3", override: true},
|
| 77 |
+
{:bamboo_postmark, git: "https://github.com/plausible/bamboo_postmark.git", branch: "main"},
|
| 78 |
+
{:bamboo_mua, "~> 0.2.0"},
|
| 79 |
+
{:bcrypt_elixir, "~> 3.3"},
|
| 80 |
+
{:bypass, "~> 2.1", only: [:dev, :test, :ce_test, :e2e_test]},
|
| 81 |
+
{:ecto_ch, "~> 0.8.6"},
|
| 82 |
+
{:cloak, "~> 1.1"},
|
| 83 |
+
{:cloak_ecto, "~> 1.2"},
|
| 84 |
+
{:combination, "~> 0.0.3"},
|
| 85 |
+
{:cors_plug, "~> 3.0"},
|
| 86 |
+
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
| 87 |
+
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
|
| 88 |
+
{:double, "~> 0.8.0", only: [:dev, :test, :ce_test, :ce_dev, :e2e_test]},
|
| 89 |
+
{:ecto, "~> 3.13.5"},
|
| 90 |
+
{:ecto_sql, "~> 3.13.2"},
|
| 91 |
+
{:envy, "~> 1.1.1"},
|
| 92 |
+
{:eqrcode, "~> 0.2.1"},
|
| 93 |
+
{:ex_machina, "~> 2.3", only: [:dev, :test, :ce_dev, :ce_test, :e2e_test]},
|
| 94 |
+
{:excoveralls, "~> 0.10", only: :test},
|
| 95 |
+
{:finch, "~> 0.23"},
|
| 96 |
+
{:lazy_html, "~> 0.1.8"},
|
| 97 |
+
{:fun_with_flags, "~> 1.13.0"},
|
| 98 |
+
{:fun_with_flags_ui, "~> 1.0"},
|
| 99 |
+
{:locus, "~> 2.3"},
|
| 100 |
+
{:gen_cycle, "~> 1.0.4"},
|
| 101 |
+
{:hackney, "~> 1.8"},
|
| 102 |
+
{:jason, "~> 1.3"},
|
| 103 |
+
{:location, git: "https://github.com/plausible/location.git"},
|
| 104 |
+
{:mox, "~> 1.0", only: [:test, :ce_test, :e2e_test]},
|
| 105 |
+
{:nanoid, "~> 2.1.0"},
|
| 106 |
+
{:nimble_totp, "~> 1.0"},
|
| 107 |
+
{:oban, "~> 2.20.1"},
|
| 108 |
+
{:observer_cli, "~> 1.7"},
|
| 109 |
+
{:opentelemetry, "~> 1.7"},
|
| 110 |
+
{:opentelemetry_api, "~> 1.5"},
|
| 111 |
+
{:opentelemetry_api_experimental,
|
| 112 |
+
git: "https://github.com/open-telemetry/opentelemetry-erlang.git",
|
| 113 |
+
ref: "b241ebf4feb7558738d009583bae8602b3c49fd0",
|
| 114 |
+
sparse: "apps/opentelemetry_api_experimental",
|
| 115 |
+
override: true},
|
| 116 |
+
{:opentelemetry_ecto, "~> 1.2"},
|
| 117 |
+
{:opentelemetry_exporter, "~> 1.10"},
|
| 118 |
+
{:opentelemetry_experimental,
|
| 119 |
+
git: "https://github.com/open-telemetry/opentelemetry-erlang.git",
|
| 120 |
+
ref: "b241ebf4feb7558738d009583bae8602b3c49fd0",
|
| 121 |
+
sparse: "apps/opentelemetry_experimental",
|
| 122 |
+
override: true},
|
| 123 |
+
{:opentelemetry_phoenix, "~> 2.0.1"},
|
| 124 |
+
{:opentelemetry_oban, "~> 1.1"},
|
| 125 |
+
{:opentelemetry_cowboy, "~> 1.0"},
|
| 126 |
+
# # https://github.com/open-telemetry/opentelemetry-erlang-contrib/issues/428
|
| 127 |
+
{:opentelemetry_semantic_conventions, "~> 1.27", override: true},
|
| 128 |
+
{:phoenix, "~> 1.8.2"},
|
| 129 |
+
{:phoenix_view, "~> 2.0"},
|
| 130 |
+
{:phoenix_ecto, "~> 4.5"},
|
| 131 |
+
{:phoenix_html, "~> 4.1"},
|
| 132 |
+
{:phoenix_live_reload, "~> 1.2", only: [:dev, :ce_dev]},
|
| 133 |
+
{:phoenix_pubsub, "~> 2.0"},
|
| 134 |
+
{:phoenix_live_view, "~> 1.1.17"},
|
| 135 |
+
{:php_serializer, "~> 2.0"},
|
| 136 |
+
{:plug, "~> 1.13", override: true},
|
| 137 |
+
{:prima, "~> 0.2.6"},
|
| 138 |
+
{:plug_cowboy, "~> 2.3"},
|
| 139 |
+
{:polymorphic_embed, "~> 5.0"},
|
| 140 |
+
{:postgrex, "~> 0.22.2"},
|
| 141 |
+
{:prom_ex, "~> 1.8"},
|
| 142 |
+
{:peep, "~> 3.0"},
|
| 143 |
+
{:public_suffix, git: "https://github.com/axelson/publicsuffix-elixir"},
|
| 144 |
+
{:recon, "~> 2.5"},
|
| 145 |
+
{:ref_inspector, "~> 2.0"},
|
| 146 |
+
{:referrer_blocklist, git: "https://github.com/plausible/referrer-blocklist.git"},
|
| 147 |
+
{:sentry, "~> 11.0.4"},
|
| 148 |
+
{:simple_saml, "~> 1.2"},
|
| 149 |
+
{:xml_builder, "~> 2.1"},
|
| 150 |
+
{:siphash, "~> 3.2"},
|
| 151 |
+
{:timex, "~> 3.7"},
|
| 152 |
+
{:ua_inspector, "~> 3.12"},
|
| 153 |
+
{:ex_doc, "~> 0.28", only: :dev, runtime: false},
|
| 154 |
+
{:ex_money, "~> 5.24.2"},
|
| 155 |
+
{:mjml_eex, "~> 0.12.0"},
|
| 156 |
+
{:mjml, "~> 4.0.0"},
|
| 157 |
+
{:heroicons, "~> 0.5.0"},
|
| 158 |
+
{:zxcvbn, git: "https://github.com/techgaun/zxcvbn-elixir.git"},
|
| 159 |
+
{:open_api_spex, "~> 3.22.1"},
|
| 160 |
+
{:joken, "~> 2.5"},
|
| 161 |
+
{:paginator, git: "https://github.com/duffelhq/paginator.git"},
|
| 162 |
+
{:esbuild, "~> 0.7", runtime: Mix.env() in [:dev, :ce_dev]},
|
| 163 |
+
{:tailwind, "~> 0.4.0", runtime: Mix.env() in [:dev, :ce_dev]},
|
| 164 |
+
{:ex_json_logger, "~> 1.4.0"},
|
| 165 |
+
{:ecto_network, "~> 1.6.0"},
|
| 166 |
+
{:ex_aws, "~> 2.5"},
|
| 167 |
+
{:ex_aws_s3, "~> 2.5"},
|
| 168 |
+
{:sweet_xml, "~> 0.7.4"},
|
| 169 |
+
{:zstream, "~> 0.6.4"},
|
| 170 |
+
{:con_cache,
|
| 171 |
+
git: "https://github.com/aerosol/con_cache", branch: "ensure-dirty-ops-emit-telemetry"},
|
| 172 |
+
{:req, "~> 0.6"},
|
| 173 |
+
{:opentelemetry_req, "~> 1.0"},
|
| 174 |
+
{:happy_tcp, github: "ruslandoga/happy_tcp", only: [:ce, :ce_dev, :ce_test, :e2e_test]},
|
| 175 |
+
{:ex_json_schema, "~> 0.11.1"},
|
| 176 |
+
{:odgn_json_pointer, "~> 3.1.0"},
|
| 177 |
+
{:phoenix_bakery, "~> 0.1.2", only: [:ce, :ce_dev, :ce_test, :e2e_test]},
|
| 178 |
+
{:site_encrypt, github: "sasa1977/site_encrypt", only: [:ce, :ce_dev, :ce_test, :e2e_test]},
|
| 179 |
+
{:phoenix_html_helpers, "~> 1.0"},
|
| 180 |
+
{:libcluster, "~> 3.5"},
|
| 181 |
+
{:decimal, "~> 3.0", override: true},
|
| 182 |
+
{:logger_backends, "~> 1.0.0"}
|
| 183 |
+
]
|
| 184 |
+
end
|
| 185 |
+
|
| 186 |
+
defp aliases do
|
| 187 |
+
[
|
| 188 |
+
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
|
| 189 |
+
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
| 190 |
+
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
| 191 |
+
test: ["ecto.create --quiet", "ecto.migrate", "test", "clean_clickhouse"],
|
| 192 |
+
"e2e.setup": [
|
| 193 |
+
"cmd npm install --prefix ./e2e",
|
| 194 |
+
"cmd npm exec playwright install --with-deps chromium --prefix ./e2e"
|
| 195 |
+
],
|
| 196 |
+
# accepts Playwright CLI arguments (https://playwright.dev/docs/test-cli), for example
|
| 197 |
+
# mix test.e2e --ui
|
| 198 |
+
# mix test.e2e --debug segments.spec.ts
|
| 199 |
+
"test.e2e": [
|
| 200 |
+
"esbuild default",
|
| 201 |
+
"ecto.create --quiet",
|
| 202 |
+
"ecto.migrate",
|
| 203 |
+
"clean_postgres",
|
| 204 |
+
"clean_clickhouse",
|
| 205 |
+
"cmd npm run --prefix ./e2e test -- "
|
| 206 |
+
],
|
| 207 |
+
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
|
| 208 |
+
"assets.typecheck": ["cmd npm --prefix assets run typecheck"],
|
| 209 |
+
"assets.build": [
|
| 210 |
+
"tailwind default",
|
| 211 |
+
"esbuild default"
|
| 212 |
+
],
|
| 213 |
+
"assets.deploy": [
|
| 214 |
+
"tailwind default --minify",
|
| 215 |
+
"esbuild default --minify",
|
| 216 |
+
"phx.digest"
|
| 217 |
+
]
|
| 218 |
+
]
|
| 219 |
+
end
|
| 220 |
+
|
| 221 |
+
defp docs do
|
| 222 |
+
[
|
| 223 |
+
main: "readme",
|
| 224 |
+
logo: "priv/static/images/ee/favicon-32x32.png",
|
| 225 |
+
extras:
|
| 226 |
+
Path.wildcard("guides/**/*.md") ++
|
| 227 |
+
[
|
| 228 |
+
"README.md": [filename: "readme", title: "Introduction"],
|
| 229 |
+
"CONTRIBUTING.md": [filename: "contributing", title: "Contributing"]
|
| 230 |
+
],
|
| 231 |
+
groups_for_extras: [
|
| 232 |
+
Features: Path.wildcard("guides/features/*.md")
|
| 233 |
+
],
|
| 234 |
+
before_closing_body_tag: fn
|
| 235 |
+
:html ->
|
| 236 |
+
"""
|
| 237 |
+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
| 238 |
+
<script>mermaid.initialize({startOnLoad: true})</script>
|
| 239 |
+
"""
|
| 240 |
+
|
| 241 |
+
_ ->
|
| 242 |
+
""
|
| 243 |
+
end
|
| 244 |
+
]
|
| 245 |
+
end
|
| 246 |
+
end
|
mix.lock
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%{
|
| 2 |
+
"acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"},
|
| 3 |
+
"attrs": {:hex, :attrs, "0.6.0", "25d738b47829f964a786ef73897d2550b66f3e7d1d7c49a83bc8fd81c71bed93", [:mix], [], "hexpm", "9c30ac15255c2ba8399263db55ba32c2f4e5ec267b654ce23df99168b405c82e"},
|
| 4 |
+
"bamboo": {:hex, :bamboo, "2.5.0", "973f5cb1471a1d2d7d9da7b8e4f6096afb6a133f85394631184fd40be8adb8ab", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "35c8635ff6677a81ab7258944ff15739280f3254a041b6f0229dddeb9b90ad3d"},
|
| 5 |
+
"bamboo_mua": {:hex, :bamboo_mua, "0.2.4", "1143cb64e0455bd496d4da14132308d1e44a825c2ba5e295c6c2adc33dff3af0", [:mix], [{:bamboo, "~> 2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:mail, "~> 0.4.0", [hex: :mail, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: false]}], "hexpm", "6802c491f8585b047d7f75f0d57f44398b44d105614115a3346710c2de75cc1f"},
|
| 6 |
+
"bamboo_postmark": {:git, "https://github.com/plausible/bamboo_postmark.git", "c6a773d1b7a4e5c9ec802ace64b5bb526504c25a", [branch: "main"]},
|
| 7 |
+
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
| 8 |
+
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
| 9 |
+
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
|
| 10 |
+
"castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"},
|
| 11 |
+
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
| 12 |
+
"certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"},
|
| 13 |
+
"ch": {:hex, :ch, "0.7.1", "116c08094b30d095c3bd6a8fe4ebe19fdaaf3dce84e2413cfdd6af157baf6303", [:mix], [{:db_connection, "~> 2.9.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "3c1c900291ff9c4c077cd1dc0c265051a3f1d26320d58b37ed9e91b33d41a868"},
|
| 14 |
+
"chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"},
|
| 15 |
+
"cldr_utils": {:hex, :cldr_utils, "2.29.7", "2189bc0117efe91c684558e79174b45eb43135595b7d1fe9b57f53917be195c1", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "4bddcd597fee34e2d2829ae9ef62bcfef8d97ae5f6b75f0c6ee37a3db31aa73a"},
|
| 16 |
+
"cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"},
|
| 17 |
+
"cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"},
|
| 18 |
+
"combination": {:hex, :combination, "0.0.3", "746aedca63d833293ec6e835aa1f34974868829b1486b1e1cb0685f0b2ae1f41", [:mix], [], "hexpm", "72b099f463df42ef7dc6371d250c7070b57b6c5902853f69deb894f79eda18ca"},
|
| 19 |
+
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
| 20 |
+
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
| 21 |
+
"con_cache": {:git, "https://github.com/aerosol/con_cache", "d050e133742e512cf2caa4598607e60dd0161072", [branch: "ensure-dirty-ops-emit-telemetry"]},
|
| 22 |
+
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
|
| 23 |
+
"cowboy": {:hex, :cowboy, "2.16.1", "fa04080b602ff25c40a7700f2dc0152dbc1ba26b42093ae0fa9bb7a337d5a242", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "b8ea4dd317a043e3177ec840cfa3bcb47cfb41035d3abb24d954dc7d51def399"},
|
| 24 |
+
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
| 25 |
+
"cowlib": {:hex, :cowlib, "2.18.0", "4a3ef8cb012c21c7cbb7c925f75405e26d7fcd393fd78c46187539dbc9a48310", [:make, :rebar3], [], "hexpm", "c4f89d33a61162d4cfdcb5a1af7e562d80a700b2573cc71020ce6521b152dc1a"},
|
| 26 |
+
"credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"},
|
| 27 |
+
"ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"},
|
| 28 |
+
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
|
| 29 |
+
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
|
| 30 |
+
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
|
| 31 |
+
"digital_token": {:hex, :digital_token, "2.0.0", "fb3c74411790994895c4a61387ab322b64a682995ef2f54a4627c92d46be5d62", [:mix], [], "hexpm", "cbd2fff52770284a8251540a4b4e529e9738c6fe052d7f3c3428eb5c817385cd"},
|
| 32 |
+
"double": {:hex, :double, "0.8.2", "8e1cfcccdaef76c18846bc08e555555a2a699b806fa207b6468572a60513cc6a", [:mix], [], "hexpm", "90287642b2ec86125e0457aaba2ab0e80f7d7050cc80a0cef733e59bd70aa67c"},
|
| 33 |
+
"earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"},
|
| 34 |
+
"ecto": {:hex, :ecto, "3.13.6", "352135b474f91d1ab99a1b502171d207e9db60421c9e3d0ecab4c7ab96b24d14", [:mix], [{:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8afa059bc16cd2c94739ec0a11e3e5df69d828125119109bef35f20a21a76af2"},
|
| 35 |
+
"ecto_ch": {:hex, :ecto_ch, "0.8.6", "f31b507e86690c003f46e75d6e742e6b5d8ce34b6b10a86604b1c3aa785e0b56", [:mix], [{:ch, "~> 0.5.0 or ~> 0.6.0 or ~> 0.7.0", [hex: :ch, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "6ca9f1cf9680452b1925c6a3a7b5e3d8b12e38ee134b03c6a45a8b26434fad97"},
|
| 36 |
+
"ecto_network": {:hex, :ecto_network, "1.6.0", "f3159c0adb280321b72dc711b3fc7bcc48e2772f71db292a1c3529c587603611", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "000411a6d3a585845f6809383f20afd418c090e104f862241124ac9518ac1073"},
|
| 37 |
+
"ecto_sql": {:hex, :ecto_sql, "3.13.4", "b6e9d07557ddba62508a9ce4a484989a5bb5e9a048ae0e695f6d93f095c25d60", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b38cf0749ca4d1c5a8bcbff79bbe15446861ca12a61f9fba604486cb6b62a14"},
|
| 38 |
+
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
| 39 |
+
"envy": {:hex, :envy, "1.1.1", "0bc9bd654dec24fcdf203f7c5aa1b8f30620f12cfb28c589d5e9c38fe1b07475", [:mix], [], "hexpm", "7061eb1a47415fd757145d8dec10dc0b1e48344960265cb108f194c4252c3a89"},
|
| 40 |
+
"eqrcode": {:hex, :eqrcode, "0.2.1", "d12838813e8fc87b8940cc05f9baadb189031f6009facdc56ff074375ec73b6e", [:mix], [], "hexpm", "d5828a222b904c68360e7dc2a40c3ef33a1328b7c074583898040f389f928025"},
|
| 41 |
+
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
|
| 42 |
+
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
| 43 |
+
"ex_aws": {:hex, :ex_aws, "2.5.1", "7418917974ea42e9e84b25e88b9f3d21a861d5f953ad453e212f48e593d8d39f", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1b95431f70c446fa1871f0eb9b183043c5a625f75f9948a42d25f43ae2eff12b"},
|
| 44 |
+
"ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
|
| 45 |
+
"ex_cldr": {:hex, :ex_cldr, "2.47.4", "64e1efda73d6c5fa0342d5b6554f39b740fa02ed540dc03997facf573101ad9e", [:mix], [{:cldr_utils, "~> 2.29", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "918aabc032955f3eac70abbdf2c5469433132edfaaaccee55451f074ee1ccdba"},
|
| 46 |
+
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.17.2", "4892b5255a205e8133297b702abbe13c9b51d0281fa8c2ea100f51df9657c3c7", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "797095c106a2fe6632981531e29cfb1d2f8ee7de626f4d6243f974d6f74a0112"},
|
| 47 |
+
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.38.3", "64a99531df09397174b7f477785c43016f223d01856725b541a19d36b8149523", [:mix], [{:decimal, "~> 1.6 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 1.0 or ~> 2.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.45", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.17", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3a0d87ef2747c66d78ae8967023d415d3d76aa310981047ad601e3287e8fe73c"},
|
| 48 |
+
"ex_doc": {:hex, :ex_doc, "0.35.1", "de804c590d3df2d9d5b8aec77d758b00c814b356119b3d4455e4b8a8687aecaf", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2121c6402c8d44b05622677b761371a759143b958c6c19f6558ff64d0aed40df"},
|
| 49 |
+
"ex_json_logger": {:hex, :ex_json_logger, "1.4.0", "ad1dcc1cfe6940ee1d9d489b20757c89769626ce34c4957548d6fbe155cd96f1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "7548a1ecba290746e06214d2b3d8783c76760c779a8903a8e44bfd23a7340444"},
|
| 50 |
+
"ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"},
|
| 51 |
+
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
|
| 52 |
+
"ex_money": {:hex, :ex_money, "5.24.2", "131eda8a404b654dbf787ca71a8b782e11112e64726d85c27540856f94d4aee5", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.46", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.38", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.19", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:gringotts, "~> 1.1", [hex: :gringotts, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "707bf02c99537d6cd93e5e5fd7f4faedde3ea2a7a932032b48e5ba0173307920"},
|
| 53 |
+
"excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"},
|
| 54 |
+
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
| 55 |
+
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
| 56 |
+
"finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},
|
| 57 |
+
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
|
| 58 |
+
"fun_with_flags": {:hex, :fun_with_flags, "1.13.0", "8e8eddd6b723691211387923b160bc779fa593f59485209a058c90ea4bbb1cd5", [:mix], [{:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dbbbff607e6f096d3857c88530d1a4e178d9eeec5dc00118e2cc1d076578710a"},
|
| 59 |
+
"fun_with_flags_ui": {:hex, :fun_with_flags_ui, "1.1.0", "61a394cd7d80c09943a4b02e07c9ab8899781f3392a157b45527d22731bf871a", [:mix], [{:cowboy, ">= 2.0.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:fun_with_flags, "~> 1.12", [hex: :fun_with_flags, repo: "hexpm", optional: false]}, {:plug, "~> 1.12", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 2.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "99b578a250141537ff7774abb832427f3406a4faff3cfc4f074788beeef38f5d"},
|
| 60 |
+
"gen_cycle": {:hex, :gen_cycle, "1.0.4", "2315199f1256116328fad6a9d4371c2fb0bc39c7f5111b88d2de582300c931cc", [:rebar3], [], "hexpm", "67817e31b352bb00715f80a5571b3c069e26e994b2ebafa376acf76a2d0f66d8"},
|
| 61 |
+
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
|
| 62 |
+
"gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"},
|
| 63 |
+
"grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"},
|
| 64 |
+
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
|
| 65 |
+
"happy_tcp": {:git, "https://github.com/ruslandoga/happy_tcp.git", "d639436bf8e7b456abc86bac13b722a2b6d60725", []},
|
| 66 |
+
"heroicons": {:hex, :heroicons, "0.5.6", "95d730e7179c633df32d95c1fdaaecdf81b0da11010b89b737b843ac176a7eb5", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "ca267f02a5fa695a4178a737b649fb6644a2e399639d4ba7964c18e8a58c2352"},
|
| 67 |
+
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
|
| 68 |
+
"hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"},
|
| 69 |
+
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
| 70 |
+
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
| 71 |
+
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
|
| 72 |
+
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
| 73 |
+
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
|
| 74 |
+
"lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"},
|
| 75 |
+
"libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"},
|
| 76 |
+
"location": {:git, "https://github.com/plausible/location.git", "a89bf79985c3c3d0830477ae587001156a646ce8", []},
|
| 77 |
+
"locus": {:hex, :locus, "2.3.12", "aa3992023363a523e5c6d0d104da1cd1026626d16b32a480903083201057f281", [:rebar3], [{:tls_certificate_check, "~> 1.9", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "e9bacd644127c5aaac478461e8b4bcd49479e7bb9f665b48ab4ebac2b7adc1ec"},
|
| 78 |
+
"logger_backends": {:hex, :logger_backends, "1.0.1", "08a9bd09eed271eac41ff35dd7603b6abf9e0f8b6ffe40359846cb0bfd37704f", [:mix], [], "hexpm", "d7903244fe8d84eeb3d3d42f8d7de080fd0409b613040df54c78b3591672d652"},
|
| 79 |
+
"mail": {:hex, :mail, "0.4.4", "2ed92e3b2dedb2013ba8eef9fb7ace3892e0eae1f45c0658738a72c195f7649b", [:mix], [], "hexpm", "bd44bf3e253d8be9c7f2e59b3253aff1efc1c9fa7d8ab4430c96780683faa8e2"},
|
| 80 |
+
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
|
| 81 |
+
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
|
| 82 |
+
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
|
| 83 |
+
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
| 84 |
+
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
| 85 |
+
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
|
| 86 |
+
"mint": {:hex, :mint, "1.9.1", "3bc120b743ed2e99ad920910f2613e9faebabb2257731b0e2ea4d8ccd9eceede", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "831101bd560b086316fab5f7adb21a4f3455717d8e4bc8368b052e09aa9163e0"},
|
| 87 |
+
"mjml": {:hex, :mjml, "4.0.0", "3418c1a975112766f92b8786a17d53640a4a1c8c7dedb621016a08e1e1447d20", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7.0", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "0886a411f9a850bc4ac6b878c9c0dbea168669176fde1f6583acf55fa8b628dd"},
|
| 88 |
+
"mjml_eex": {:hex, :mjml_eex, "0.12.0", "ee7e58a0b2f361812c024ddd2dd57ccd4107e486632016fcf707d4d0eaa04d62", [:mix], [{:erlexec, "~> 2.0.7", [hex: :erlexec, repo: "hexpm", optional: true]}, {:mjml, "~> 4.0", [hex: :mjml, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6ca41f6252669b709d31d8b7c74db00c8bc3784dea1913b406a68c12763c5bcb"},
|
| 89 |
+
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
|
| 90 |
+
"mua": {:hex, :mua, "0.2.5", "e99aa9646964a0109a2efcc8e684c6f8d90c60fb0191f52e1784cea296584daf", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "0e2b18024d0db8943a68e84fb5e2253d3225c8f61d8387cbfc581d66e34d8493"},
|
| 91 |
+
"nanoid": {:hex, :nanoid, "2.1.0", "d192a5bf1d774258bc49762b480fca0e3128178fa6d35a464af2a738526607fd", [:mix], [], "hexpm", "ebc7a342d02d213534a7f93a091d569b9fea7f26fcd3a638dc655060fc1f76ac"},
|
| 92 |
+
"nimble_csv": {:hex, :nimble_csv, "1.3.0", "b7f998dc62b222bce9596e46f028c7a5af04cb5dde6df2ea197c583227c54971", [:mix], [], "hexpm", "41ccdc18f7c8f8bb06e84164fc51635321e80d5a3b450761c4997d620925d619"},
|
| 93 |
+
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
| 94 |
+
"nimble_ownership": {:hex, :nimble_ownership, "1.0.2", "fa8a6f2d8c592ad4d79b2ca617473c6aefd5869abfa02563a77682038bf916cf", [:mix], [], "hexpm", "098af64e1f6f8609c6672127cfe9e9590a5d3fcdd82bc17a377b8692fd81a879"},
|
| 95 |
+
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
| 96 |
+
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
| 97 |
+
"nimble_totp": {:hex, :nimble_totp, "1.0.0", "79753bae6ce59fd7cacdb21501a1dbac249e53a51c4cd22b34fa8438ee067283", [:mix], [], "hexpm", "6ce5e4c068feecdb782e85b18237f86f66541523e6bad123e02ee1adbe48eda9"},
|
| 98 |
+
"oban": {:hex, :oban, "2.20.1", "39d0b68787e5cf251541c0d657a698f6142a24d8744e1e40b2cf045d4fa232a6", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17a45277dbeb41a455040b41dd8c467163fad685d1366f2f59207def3bcdd1d8"},
|
| 99 |
+
"observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"},
|
| 100 |
+
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
|
| 101 |
+
"odgn_json_pointer": {:hex, :odgn_json_pointer, "3.1.0", "be81198ad8c9bce01fc872d6400d88a4a55ce711f7c8838e1166bd21f736d29a", [:mix], [], "hexpm", "85730306bb2e5796c993813f35e9bafeb561526f5e80a517793c33d1635ade94"},
|
| 102 |
+
"open_api_spex": {:hex, :open_api_spex, "3.22.1", "38c99d8bf107dc7ffb112dc669f33e0a287396c6c9fd6bb7eeb27e3fe8dbba0e", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "fa51ecd04ececbad89a8ede55ebd9db7aa9e55cc7ddbb46455522e0f3c098290"},
|
| 103 |
+
"opentelemetry": {:hex, :opentelemetry, "1.7.0", "20d0f12d3d1c398d3670fd44fd1a7c495dd748ab3e5b692a7906662e2fb1a38a", [:rebar3], [{:opentelemetry_api, "~> 1.5.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "a9173b058c4549bf824cbc2f1d2fa2adc5cdedc22aa3f0f826951187bbd53131"},
|
| 104 |
+
"opentelemetry_api": {:hex, :opentelemetry_api, "1.5.0", "1a676f3e3340cab81c763e939a42e11a70c22863f645aa06aafefc689b5550cf", [:mix, :rebar3], [], "hexpm", "f53ec8a1337ae4a487d43ac89da4bd3a3c99ddf576655d071deed8b56a2d5dda"},
|
| 105 |
+
"opentelemetry_api_experimental": {:git, "https://github.com/open-telemetry/opentelemetry-erlang.git", "b241ebf4feb7558738d009583bae8602b3c49fd0", [ref: "b241ebf4feb7558738d009583bae8602b3c49fd0", sparse: "apps/opentelemetry_api_experimental"]},
|
| 106 |
+
"opentelemetry_cowboy": {:hex, :opentelemetry_cowboy, "1.0.0", "786c7cde66a2493323c79d2c94e679ff501d459a9b403d8b60b9bef116333117", [:rebar3], [{:cowboy_telemetry, "~> 0.4", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.1", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:otel_http, "~> 0.2", [hex: :otel_http, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7575716eaccacd0eddc3e7e61403aecb5d0a6397183987d6049094aeb0b87a7c"},
|
| 107 |
+
"opentelemetry_ecto": {:hex, :opentelemetry_ecto, "1.2.0", "2382cb47ddc231f953d3b8263ed029d87fbf217915a1da82f49159d122b64865", [:mix], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "70dfa2e79932e86f209df00e36c980b17a32f82d175f0068bf7ef9a96cf080cf"},
|
| 108 |
+
"opentelemetry_experimental": {:git, "https://github.com/open-telemetry/opentelemetry-erlang.git", "b241ebf4feb7558738d009583bae8602b3c49fd0", [ref: "b241ebf4feb7558738d009583bae8602b3c49fd0", sparse: "apps/opentelemetry_experimental"]},
|
| 109 |
+
"opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.10.0", "972e142392dbfa679ec959914664adefea38399e4f56ceba5c473e1cabdbad79", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.7.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.5.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "33a116ed7304cb91783f779dec02478f887c87988077bfd72840f760b8d4b952"},
|
| 110 |
+
"opentelemetry_oban": {:hex, :opentelemetry_oban, "1.1.1", "519e9ba60d3dc3483ad2df3fade131d47056e0dae74f0724c8a40b9718f089d1", [:mix], [{:oban, "~> 2.0", [hex: :oban, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.1", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ae6aed431626a94a4bb6bf5b268247ced687ec8f99eced6887e3754f9d3a2089"},
|
| 111 |
+
"opentelemetry_phoenix": {:hex, :opentelemetry_phoenix, "2.0.1", "c664cdef205738cffcd409b33599439a4ffb2035ef6e21a77927ac1da90463cb", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.1", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:otel_http, "~> 0.2", [hex: :otel_http, repo: "hexpm", optional: false]}, {:plug, ">= 1.11.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a24fdccdfa6b890c8892c6366beab4a15a27ec0c692b0f77ec2a862e7b235f6e"},
|
| 112 |
+
"opentelemetry_process_propagator": {:hex, :opentelemetry_process_propagator, "0.3.0", "ef5b2059403a1e2b2d2c65914e6962e56371570b8c3ab5323d7a8d3444fb7f84", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "7243cb6de1523c473cba5b1aefa3f85e1ff8cc75d08f367104c1e11919c8c029"},
|
| 113 |
+
"opentelemetry_req": {:hex, :opentelemetry_req, "1.0.0", "f8ef479a05311bdc030925a07be869ede45e8158cc2c80d9855e35a7d1efc89c", [:mix], [{:jason, "~> 1.3", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:otel_http, "~> 0.2", [hex: :otel_http, repo: "hexpm", optional: false]}, {:req, ">= 0.3.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "6404ae802d694f7c3e5b43bf41af99364ecf20c3f5fea144fff8b7f873b22bf7"},
|
| 114 |
+
"opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "1.27.0", "acd0194a94a1e57d63da982ee9f4a9f88834ae0b31b0bd850815fe9be4bbb45f", [:mix, :rebar3], [], "hexpm", "9681ccaa24fd3d810b4461581717661fd85ff7019b082c2dff89c7d5b1fc2864"},
|
| 115 |
+
"opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.1.2", "410ab4d76b0921f42dbccbe5a7c831b8125282850be649ee1f70050d3961118a", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.3", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "641ab469deb181957ac6d59bce6e1321d5fe2a56df444fc9c19afcad623ab253"},
|
| 116 |
+
"otel_http": {:hex, :otel_http, "0.2.0", "b17385986c7f1b862f5d577f72614ecaa29de40392b7618869999326b9a61d8a", [:rebar3], [], "hexpm", "f2beadf922c8cfeb0965488dd736c95cc6ea8b9efce89466b3904d317d7cc717"},
|
| 117 |
+
"paginator": {:git, "https://github.com/duffelhq/paginator.git", "3508d6ad77a95ac1faf15d5fd7f959fab3e17da2", []},
|
| 118 |
+
"parent": {:hex, :parent, "0.12.1", "495c4386f06de0df492e0a7a7199c10323a55e9e933b27222060dd86dccd6d62", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2ab589ef1f37bfcedbfb5ecfbab93354972fb7391201b8907a866dadd20b39d1"},
|
| 119 |
+
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
| 120 |
+
"peep": {:hex, :peep, "3.4.2", "49d4ca116d994779351959dfee971fb2c7d6506f1821374f3cc4fd39a3d3fadb", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "cae224b09e224bf5584f5375108becf71e2288572a099a122e66735289cd33f4"},
|
| 121 |
+
"phoenix": {:hex, :phoenix, "1.8.9", "a63ed0962ed5b903b146dab0ae8eb8387fe478f8171a5e26d56a165f35996fe1", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "3477e2dd5a4f61820341169031bdfe21275f659923bea9c5c0ea2aa1c3fcc046"},
|
| 122 |
+
"phoenix_bakery": {:hex, :phoenix_bakery, "0.1.2", "ca57673caea1a98f1cc763f94032796a015774d27eaa3ce5feef172195470452", [:mix], [{:brotli, "~> 0.3.0", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "45cc8cecc5c3002b922447c16389761718c07c360432328b04680034e893ea5b"},
|
| 123 |
+
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
| 124 |
+
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
| 125 |
+
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
|
| 126 |
+
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"},
|
| 127 |
+
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.27", "9afcab28b0c82afdc51044e661bcd5b8de53d242593d34c964a37710b40a42af", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "415735d0b2c612c9104108b35654e977626a0cb346711e1e4f1ed16e3c827ede"},
|
| 128 |
+
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
| 129 |
+
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
| 130 |
+
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
| 131 |
+
"php_serializer": {:hex, :php_serializer, "2.0.0", "b43f31aca22ed7321f32da2b94fe2ddf9b6739a965cb51541969119e572e821d", [:mix], [], "hexpm", "61e402e99d9062c0225a3f4fcf7e43b4cba1b8654944c0e7c139c3ca9de481da"},
|
| 132 |
+
"plug": {:hex, :plug, "1.20.2", "adbee2441232412e37fbb357fd5e4cd533fdd253b29f2e1992262b0f1fb01462", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b16baf55877d60891002ffc1ce0b3ff7d6f30a38a23e02e4d4293c4ac266f136"},
|
| 133 |
+
"plug_cowboy": {:hex, :plug_cowboy, "2.8.1", "5aa391a5e8d1ac3192e36a3bcaff12b5fd6ef6c7e29b53a38e63a860783e77d0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4c200288673d5bc86a0ab7dc6a2a069176a74e5d573ef62740a1c517458a5f26"},
|
| 134 |
+
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
|
| 135 |
+
"polymorphic_embed": {:hex, :polymorphic_embed, "5.0.3", "37444e0af941026a2c29b0539b6471bdd6737a6492a19264bf2bb0118e3ac242", [:mix], [{:attrs, "~> 0.6", [hex: :attrs, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}], "hexpm", "2fed44f57abf0a0fc7642e0eb0807a55b65de1562712cc0620772cbbb80e49c1"},
|
| 136 |
+
"postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"},
|
| 137 |
+
"prima": {:hex, :prima, "0.2.6", "a40993d108760a711c2da183cff20234497e977f6155aad1ee5e17a3b00a46b6", [:mix], [{:esbuild, "~> 0.7", [hex: :esbuild, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "00ec378bd7b0c58d58043e59581f9aa5ba35a0d5031e011e36c449d3a0b7418c"},
|
| 138 |
+
"prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"},
|
| 139 |
+
"public_suffix": {:git, "https://github.com/axelson/publicsuffix-elixir", "fa40c243d4b5d8598b90cff268bc4e33f3bb63f1", []},
|
| 140 |
+
"ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"},
|
| 141 |
+
"recon": {:hex, :recon, "2.5.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"},
|
| 142 |
+
"ref_inspector": {:hex, :ref_inspector, "2.0.0", "f3e97e51d9782de4c792f56eed26c80903bc39174c878285392ce76d5e67fe98", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "bf62f3f1a87d6b8b30f457a480668f965373e64f184611282b5e89d8dd81fd33"},
|
| 143 |
+
"referrer_blocklist": {:git, "https://github.com/plausible/referrer-blocklist.git", "d6f52c225cccb4f04b80e3a5d588868ec234139d", []},
|
| 144 |
+
"req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"},
|
| 145 |
+
"rustler_precompiled": {:hex, :rustler_precompiled, "0.7.3", "42cb9449785cd86c87453e39afdd27a0bdfa5c77a4ec5dc5ce45112e06b9f89b", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "cbc4b3777682e5f6f43ed39b0e0b4a42dccde8053aba91b4514e8f5ff9a5ac6d"},
|
| 146 |
+
"saxy": {:hex, :saxy, "1.6.0", "02cb4e9bd045f25ac0c70fae8164754878327ee393c338a090288210b02317ee", [:mix], [], "hexpm", "ef42eb4ac983ca77d650fbdb68368b26570f6cc5895f0faa04d34a6f384abad3"},
|
| 147 |
+
"sentry": {:hex, :sentry, "11.0.4", "60371c96cefd247e0fc98840bba2648f64f19aa0b8db8e938f5a98421f55b619", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:opentelemetry, ">= 0.0.0", [hex: :opentelemetry, repo: "hexpm", optional: true]}, {:opentelemetry_api, ">= 0.0.0", [hex: :opentelemetry_api, repo: "hexpm", optional: true]}, {:opentelemetry_exporter, ">= 0.0.0", [hex: :opentelemetry_exporter, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, ">= 0.0.0", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "feaafc284dc204c82aadaddc884227aeaa3480decb274d30e184b9d41a700c66"},
|
| 148 |
+
"simple_saml": {:hex, :simple_saml, "1.2.0", "d875939e21cd73cdb3f747e701e4d2d5f0f4e35610d37df50629087454fa43f9", [:mix], [{:simple_xml, "~> 1.3", [hex: :simple_xml, repo: "hexpm", optional: false]}, {:x509, "~> 0.9.0", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "24663665e53a91bfc38359ed9f26f575a389bde9fc94ec369f9f2259046feaf0"},
|
| 149 |
+
"simple_xml": {:hex, :simple_xml, "1.3.2", "84b892527b5183748694583a658c729c84bb89fc8ac650d7025ebc2c49e74018", [:mix], [{:saxy, "~> 1.5", [hex: :saxy, repo: "hexpm", optional: false]}, {:x509, "~> 0.9.0", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "8c15fa49955a8c82b50f3053617e8a4cd771fcd0a502d660e56993c38ac86ca5"},
|
| 150 |
+
"siphash": {:hex, :siphash, "3.2.0", "ec03fd4066259218c85e2a4b8eec4bb9663bc02b127ea8a0836db376ba73f2ed", [:make, :mix], [], "hexpm", "ba3810701c6e95637a745e186e8a4899087c3b079ba88fb8f33df054c3b0b7c3"},
|
| 151 |
+
"site_encrypt": {:git, "https://github.com/sasa1977/site_encrypt.git", "8d47801c51eeea11ac385cb73eef4ed9a6565367", []},
|
| 152 |
+
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
| 153 |
+
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
| 154 |
+
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
| 155 |
+
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
|
| 156 |
+
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
| 157 |
+
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
|
| 158 |
+
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
| 159 |
+
"timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"},
|
| 160 |
+
"tls_certificate_check": {:hex, :tls_certificate_check, "1.30.0", "ef9bdfcb5b551b747cad231a65ebd449623628bb72471c1d2aefcbbc5730683d", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "dfbada8fddb80ae19b8aeaac34d95cfce98c91393b0db9c3ee079b488fd68d81"},
|
| 161 |
+
"tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"},
|
| 162 |
+
"ua_inspector": {:hex, :ua_inspector, "3.12.0", "c8c5cfb0ce19b5cd609efb577bf75e3ed60deef8ed26bc0577aa2ff60ea49bcd", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "81fa0e28effb50ae9fd74ffe51622af3846149aee32c11d60b16ceab62b45682"},
|
| 163 |
+
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
|
| 164 |
+
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
| 165 |
+
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
|
| 166 |
+
"x509": {:hex, :x509, "0.9.2", "a75aa605348abd905990f3d2dc1b155fcde4e030fa2f90c4a91534405dce0f6e", [:mix], [], "hexpm", "4c5ede75697e565d4b0f5be04c3b71bb1fd3a090ea243af4bd7dae144e48cfc7"},
|
| 167 |
+
"xml_builder": {:hex, :xml_builder, "2.4.0", "b20d23077266c81f593360dc037ea398461dddb6638a329743da6c73afa56725", [:mix], [], "hexpm", "833e325bb997f032b5a1b740d2fd6feed3c18ca74627f9f5f30513a9ae1a232d"},
|
| 168 |
+
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
| 169 |
+
"zstream": {:hex, :zstream, "0.6.7", "ae74c7544f4e0563ffbe71324bf1c9bbc0ab33bb580a0ae718da511fb8a5c9d6", [:mix], [], "hexpm", "48c43ae0f00cfcda1ccb69c1d044755663d43b2ee8a0a65763648bf2078d634d"},
|
| 170 |
+
"zxcvbn": {:git, "https://github.com/techgaun/zxcvbn-elixir.git", "5c9684590f4ecf00af2d391f90db9ce93bd4e996", []},
|
| 171 |
+
}
|
priv/custom_sources.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bsky.app":"Bluesky",
|
| 3 |
+
"go.bsky.app":"Bluesky",
|
| 4 |
+
"android-app://com.reddit.frontpage":"Reddit",
|
| 5 |
+
"baidu.com":"Baidu",
|
| 6 |
+
"chatgpt.com":"ChatGPT",
|
| 7 |
+
"chat.openai.com":"ChatGPT",
|
| 8 |
+
"claude.ai":"Claude",
|
| 9 |
+
"copilot.microsoft.com":"Microsoft Copilot",
|
| 10 |
+
"copilot.com":"Microsoft Copilot",
|
| 11 |
+
"copilot":"Microsoft Copilot",
|
| 12 |
+
"deepseek.com":"DeepSeek",
|
| 13 |
+
"gemini.google.com":"Google Gemini",
|
| 14 |
+
"grok.com":"Grok",
|
| 15 |
+
"x.ai":"Grok",
|
| 16 |
+
"phind.com":"Phind",
|
| 17 |
+
"chaos.social":"Mastodon",
|
| 18 |
+
"fosstodon.org":"Mastodon",
|
| 19 |
+
"hachyderm.io":"Mastodon",
|
| 20 |
+
"indieweb.social":"Mastodon",
|
| 21 |
+
"infosec.exchange":"Mastodon",
|
| 22 |
+
"mas.to":"Mastodon",
|
| 23 |
+
"mastodon.online":"Mastodon",
|
| 24 |
+
"mastodon.social":"Mastodon",
|
| 25 |
+
"mastodon.world":"Mastodon",
|
| 26 |
+
"mathstodon.xyz":"Mastodon",
|
| 27 |
+
"mstdn.jp":"Mastodon",
|
| 28 |
+
"mstdn.social":"Mastodon",
|
| 29 |
+
"scholar.social":"Mastodon",
|
| 30 |
+
"sigmoid.social":"Mastodon",
|
| 31 |
+
"social.tchncs.de":"Mastodon",
|
| 32 |
+
"android-app://org.joinmastodon.android": "Mastodon",
|
| 33 |
+
"discord.com":"Discord",
|
| 34 |
+
"discordapp.com":"Discord",
|
| 35 |
+
"linktr.ee":"Linktree",
|
| 36 |
+
"m.sogou.com":"Sogou",
|
| 37 |
+
"kagi.com":"Kagi",
|
| 38 |
+
"l.threads.com":"Threads",
|
| 39 |
+
"ntp.msn.com":"Bing",
|
| 40 |
+
"perplexity.ai":"Perplexity",
|
| 41 |
+
"pplx.ai":"Perplexity",
|
| 42 |
+
"ptb.discord.com":"Discord",
|
| 43 |
+
"search.brave.com":"Brave",
|
| 44 |
+
"sogou.com":"Sogou",
|
| 45 |
+
"statics.teams.cdn.office.net":"Microsoft Teams",
|
| 46 |
+
"teams.microsoft.com":"Microsoft Teams",
|
| 47 |
+
"t.me":"Telegram",
|
| 48 |
+
"wap.sogou.com":"Sogou",
|
| 49 |
+
"ya.ru":"Yandex",
|
| 50 |
+
"yandex.com.tr":"Yandex",
|
| 51 |
+
"yandex.eu":"Yandex",
|
| 52 |
+
"yandex.fr":"Yandex",
|
| 53 |
+
"yandex.kz":"Yandex",
|
| 54 |
+
"yandex.tm":"Yandex",
|
| 55 |
+
"yandex.uz":"Yandex",
|
| 56 |
+
"fb": "Facebook",
|
| 57 |
+
"fb-ads": "Facebook",
|
| 58 |
+
"fbads": "Facebook",
|
| 59 |
+
"fbad": "Facebook",
|
| 60 |
+
"facebook-ads": "Facebook",
|
| 61 |
+
"facebook_ads": "Facebook",
|
| 62 |
+
"fcb": "Facebook",
|
| 63 |
+
"facebook_ad": "Facebook",
|
| 64 |
+
"facebook_feed_ad": "Facebook",
|
| 65 |
+
"ig": "Instagram",
|
| 66 |
+
"yt": "Youtube",
|
| 67 |
+
"yt-ads": "Youtube",
|
| 68 |
+
"reddit-ads": "Reddit",
|
| 69 |
+
"google_ads": "Google",
|
| 70 |
+
"google-ads": "Google",
|
| 71 |
+
"googleads": "Google",
|
| 72 |
+
"gads": "Google",
|
| 73 |
+
"google ads": "Google",
|
| 74 |
+
"adwords": "Google",
|
| 75 |
+
"x.com":"X (Twitter)",
|
| 76 |
+
"mobile.twitter.com": "X (Twitter)",
|
| 77 |
+
"twitter-ads": "X (Twitter)",
|
| 78 |
+
"twitter": "X (Twitter)",
|
| 79 |
+
"tiktokads": "TikTok",
|
| 80 |
+
"tik.tok": "TikTok",
|
| 81 |
+
"wikipedia": "Wikipedia",
|
| 82 |
+
"canary.discord.com":"Discord",
|
| 83 |
+
"webk.telegram.org":"Telegram"
|
| 84 |
+
}
|
priv/data_migrations/AcquisitionChannel/sql/acquisition_channel_add_materialized_column.sql.eex
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ALTER TABLE <%= @table %>
|
| 2 |
+
<%= @on_cluster_statement %>
|
| 3 |
+
ADD COLUMN IF NOT EXISTS acquisition_channel LowCardinality(String)
|
| 4 |
+
MATERIALIZED acquisition_channel(referrer_source, utm_medium, utm_campaign, utm_source, click_id_param)
|
priv/data_migrations/AcquisitionChannel/sql/acquisition_channel_backfill_materialized_column.sql.eex
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ALTER TABLE <%= @table %>
|
| 2 |
+
MATERIALIZE COLUMN acquisition_channel
|
priv/data_migrations/AcquisitionChannel/sql/acquisition_channel_functions.sql.eex
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS acquisition_channel_source_category
|
| 2 |
+
<%= @on_cluster_statement %>
|
| 3 |
+
(
|
| 4 |
+
referrer_source String,
|
| 5 |
+
category LowCardinality(String)
|
| 6 |
+
)
|
| 7 |
+
<%= if @on_cluster_statement != "" do %>
|
| 8 |
+
ENGINE = ReplicatedMergeTree('/clickhouse/{cluster}/tables/{shard}/plausible_prod/acquisition_channel_source_category', '{replica}')
|
| 9 |
+
<% else %>
|
| 10 |
+
ENGINE = MergeTree()
|
| 11 |
+
<% end %>
|
| 12 |
+
ORDER BY referrer_source
|
| 13 |
+
<%= @table_settings %>;
|
| 14 |
+
|
| 15 |
+
TRUNCATE TABLE acquisition_channel_source_category SETTINGS alter_sync=2;
|
| 16 |
+
|
| 17 |
+
INSERT INTO acquisition_channel_source_category(referrer_source, category)
|
| 18 |
+
SELECT t.1 AS referrer_source, t.2 AS category
|
| 19 |
+
FROM (
|
| 20 |
+
SELECT arrayJoin({source_categories:Array(Tuple(String, String))}) AS t
|
| 21 |
+
)
|
| 22 |
+
SETTINGS insert_quorum = <%= @insert_quorum %>;
|
| 23 |
+
|
| 24 |
+
CREATE OR REPLACE DICTIONARY acquisition_channel_source_category_dict
|
| 25 |
+
<%= @on_cluster_statement %>
|
| 26 |
+
(
|
| 27 |
+
`referrer_source` String,
|
| 28 |
+
`category` String
|
| 29 |
+
)
|
| 30 |
+
PRIMARY KEY referrer_source
|
| 31 |
+
SOURCE(CLICKHOUSE(TABLE acquisition_channel_source_category <%= @dictionary_connection_params %>))
|
| 32 |
+
LIFETIME(0)
|
| 33 |
+
LAYOUT(hashed());
|
| 34 |
+
|
| 35 |
+
CREATE TABLE IF NOT EXISTS acquisition_channel_paid_sources
|
| 36 |
+
<%= @on_cluster_statement %>
|
| 37 |
+
(
|
| 38 |
+
referrer_source String
|
| 39 |
+
)
|
| 40 |
+
<%= if @on_cluster_statement != "" do %>
|
| 41 |
+
ENGINE = ReplicatedMergeTree('/clickhouse/{cluster}/tables/{shard}/plausible_prod/acquisition_channel_paid_sources', '{replica}')
|
| 42 |
+
<% else %>
|
| 43 |
+
ENGINE = MergeTree()
|
| 44 |
+
<% end %>
|
| 45 |
+
ORDER BY referrer_source
|
| 46 |
+
<%= @table_settings %>;
|
| 47 |
+
|
| 48 |
+
TRUNCATE TABLE acquisition_channel_paid_sources SETTINGS alter_sync=2;
|
| 49 |
+
|
| 50 |
+
INSERT INTO acquisition_channel_paid_sources(referrer_source)
|
| 51 |
+
SELECT arrayJoin({paid_sources:Array(String)}) AS referrer_source
|
| 52 |
+
SETTINGS insert_quorum = <%= @insert_quorum %>;
|
| 53 |
+
|
| 54 |
+
CREATE OR REPLACE DICTIONARY acquisition_channel_paid_sources_dict
|
| 55 |
+
<%= @on_cluster_statement %>
|
| 56 |
+
(
|
| 57 |
+
`referrer_source` String
|
| 58 |
+
)
|
| 59 |
+
PRIMARY KEY referrer_source
|
| 60 |
+
SOURCE(CLICKHOUSE(TABLE acquisition_channel_paid_sources <%= @dictionary_connection_params %>))
|
| 61 |
+
LIFETIME(0)
|
| 62 |
+
LAYOUT(hashed());
|
| 63 |
+
|
| 64 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_has_category_shopping <%= @on_cluster_statement %> AS
|
| 65 |
+
(referrer_source) ->
|
| 66 |
+
dictGet('acquisition_channel_source_category_dict', 'category', referrer_source) = 'SOURCE_CATEGORY_SHOPPING';
|
| 67 |
+
|
| 68 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_has_category_social <%= @on_cluster_statement %> AS
|
| 69 |
+
(referrer_source) ->
|
| 70 |
+
dictGet('acquisition_channel_source_category_dict', 'category', referrer_source) = 'SOURCE_CATEGORY_SOCIAL';
|
| 71 |
+
|
| 72 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_has_category_video <%= @on_cluster_statement %> AS
|
| 73 |
+
(referrer_source) ->
|
| 74 |
+
dictGet('acquisition_channel_source_category_dict', 'category', referrer_source) = 'SOURCE_CATEGORY_VIDEO';
|
| 75 |
+
|
| 76 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_has_category_search <%= @on_cluster_statement %> AS
|
| 77 |
+
(referrer_source) ->
|
| 78 |
+
dictGet('acquisition_channel_source_category_dict', 'category', referrer_source) = 'SOURCE_CATEGORY_SEARCH';
|
| 79 |
+
|
| 80 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_has_category_email <%= @on_cluster_statement %> AS
|
| 81 |
+
(referrer_source) ->
|
| 82 |
+
dictGet('acquisition_channel_source_category_dict', 'category', referrer_source) = 'SOURCE_CATEGORY_EMAIL';
|
| 83 |
+
|
| 84 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_has_category_ai_assistants <%= @on_cluster_statement %> AS
|
| 85 |
+
(referrer_source) ->
|
| 86 |
+
dictGet('acquisition_channel_source_category_dict', 'category', referrer_source) = 'SOURCE_CATEGORY_AI_ASSISTANTS';
|
| 87 |
+
|
| 88 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_paid_utm_source <%= @on_cluster_statement %> AS
|
| 89 |
+
(referrer_source) ->
|
| 90 |
+
dictHas('acquisition_channel_paid_sources_dict', referrer_source);
|
| 91 |
+
|
| 92 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_cross_network <%= @on_cluster_statement %> AS
|
| 93 |
+
(utm_campaign) ->
|
| 94 |
+
position(utm_campaign, 'cross-network') > 0;
|
| 95 |
+
|
| 96 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_paid_shopping <%= @on_cluster_statement %> AS
|
| 97 |
+
(referrer_source, utm_medium, utm_campaign) ->
|
| 98 |
+
acquisition_channel_paid_medium(utm_medium) AND
|
| 99 |
+
(
|
| 100 |
+
acquisition_channel_has_category_shopping(referrer_source)
|
| 101 |
+
OR acquisition_channel_shopping_campaign(utm_campaign)
|
| 102 |
+
);
|
| 103 |
+
|
| 104 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_paid_search <%= @on_cluster_statement %> AS
|
| 105 |
+
(referrer_source, utm_medium, utm_source, click_id_param) ->
|
| 106 |
+
(
|
| 107 |
+
acquisition_channel_has_category_search(referrer_source)
|
| 108 |
+
AND (
|
| 109 |
+
acquisition_channel_paid_medium(utm_medium)
|
| 110 |
+
OR acquisition_channel_paid_utm_source(utm_source)
|
| 111 |
+
)
|
| 112 |
+
) OR (
|
| 113 |
+
referrer_source == 'google'
|
| 114 |
+
AND click_id_param == 'gclid'
|
| 115 |
+
) OR (
|
| 116 |
+
referrer_source == 'bing'
|
| 117 |
+
AND click_id_param == 'msclkid'
|
| 118 |
+
);
|
| 119 |
+
|
| 120 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_paid_social <%= @on_cluster_statement %> AS
|
| 121 |
+
(referrer_source, utm_medium, utm_source) ->
|
| 122 |
+
acquisition_channel_has_category_social(referrer_source)
|
| 123 |
+
AND (
|
| 124 |
+
acquisition_channel_paid_medium(utm_medium)
|
| 125 |
+
OR acquisition_channel_paid_utm_source(utm_source)
|
| 126 |
+
);
|
| 127 |
+
|
| 128 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_paid_video <%= @on_cluster_statement %> AS
|
| 129 |
+
(referrer_source, utm_medium, utm_source) ->
|
| 130 |
+
acquisition_channel_has_category_video(referrer_source)
|
| 131 |
+
AND (
|
| 132 |
+
acquisition_channel_paid_medium(utm_medium)
|
| 133 |
+
OR acquisition_channel_paid_utm_source(utm_source)
|
| 134 |
+
);
|
| 135 |
+
|
| 136 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_display <%= @on_cluster_statement %> AS
|
| 137 |
+
(utm_medium) ->
|
| 138 |
+
utm_medium IN ('display', 'banner', 'expandable', 'interstitial', 'cpm');
|
| 139 |
+
|
| 140 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_paid_medium <%= @on_cluster_statement %> AS
|
| 141 |
+
(utm_medium) ->
|
| 142 |
+
match(utm_medium, '^(.*cp.*|ppc|retargeting|paid.*)$');
|
| 143 |
+
|
| 144 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_shopping_campaign <%= @on_cluster_statement %> AS
|
| 145 |
+
(utm_campaign) ->
|
| 146 |
+
match(utm_campaign, '^(.*(([^a-df-z]|^)shop|shopping).*)$');
|
| 147 |
+
|
| 148 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_organic_shopping <%= @on_cluster_statement %> AS
|
| 149 |
+
(referrer_source, utm_campaign) ->
|
| 150 |
+
acquisition_channel_has_category_shopping(referrer_source)
|
| 151 |
+
OR acquisition_channel_shopping_campaign(utm_campaign);
|
| 152 |
+
|
| 153 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_organic_social <%= @on_cluster_statement %> AS
|
| 154 |
+
(referrer_source, utm_medium) ->
|
| 155 |
+
acquisition_channel_has_category_social(referrer_source)
|
| 156 |
+
OR utm_medium IN (
|
| 157 |
+
'social',
|
| 158 |
+
'social-network',
|
| 159 |
+
'social-media',
|
| 160 |
+
'sm',
|
| 161 |
+
'social network',
|
| 162 |
+
'social media'
|
| 163 |
+
);
|
| 164 |
+
|
| 165 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_organic_video <%= @on_cluster_statement %> AS
|
| 166 |
+
(referrer_source, utm_medium) ->
|
| 167 |
+
acquisition_channel_has_category_video(referrer_source) OR position(utm_medium, 'video') > 0;
|
| 168 |
+
|
| 169 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_email <%= @on_cluster_statement %> AS
|
| 170 |
+
(referrer_source, utm_source, utm_medium) ->
|
| 171 |
+
acquisition_channel_has_category_email(referrer_source)
|
| 172 |
+
OR acquisition_channel_contains_email(utm_source)
|
| 173 |
+
OR acquisition_channel_contains_email(utm_medium);
|
| 174 |
+
|
| 175 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_affiliates <%= @on_cluster_statement %> AS
|
| 176 |
+
(utm_medium) ->
|
| 177 |
+
utm_medium == 'affiliate';
|
| 178 |
+
|
| 179 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_audio <%= @on_cluster_statement %> AS
|
| 180 |
+
(utm_medium) ->
|
| 181 |
+
utm_medium == 'audio';
|
| 182 |
+
|
| 183 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_sms <%= @on_cluster_statement %> AS
|
| 184 |
+
(column) ->
|
| 185 |
+
column == 'sms';
|
| 186 |
+
|
| 187 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_mobile_push_notifications <%= @on_cluster_statement %> AS
|
| 188 |
+
(utm_medium, referrer_source) ->
|
| 189 |
+
endsWith(utm_medium, 'push') OR
|
| 190 |
+
multiSearchAny(utm_medium, ['mobile', 'notification']) OR
|
| 191 |
+
referrer_source == 'firebase';
|
| 192 |
+
|
| 193 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_referral <%= @on_cluster_statement %> AS
|
| 194 |
+
(utm_medium, referrer_source) ->
|
| 195 |
+
utm_medium IN ('referral', 'app', 'link') OR
|
| 196 |
+
not empty(referrer_source);
|
| 197 |
+
|
| 198 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_contains_email <%= @on_cluster_statement %> AS
|
| 199 |
+
(column) ->
|
| 200 |
+
match(column, 'e[-_ ]?mail|newsletter');
|
| 201 |
+
|
| 202 |
+
CREATE OR REPLACE FUNCTION acquisition_channel <%= @on_cluster_statement %> AS
|
| 203 |
+
(referrer_source, utm_medium, utm_campaign, utm_source, click_id_param) ->
|
| 204 |
+
acquisition_channel_lowered(
|
| 205 |
+
lower(referrer_source),
|
| 206 |
+
lower(utm_medium),
|
| 207 |
+
lower(utm_campaign),
|
| 208 |
+
lower(utm_source),
|
| 209 |
+
click_id_param
|
| 210 |
+
);
|
| 211 |
+
|
| 212 |
+
CREATE OR REPLACE FUNCTION acquisition_channel_lowered <%= @on_cluster_statement %> AS
|
| 213 |
+
(referrer_source, utm_medium, utm_campaign, utm_source, click_id_param) ->
|
| 214 |
+
multiIf(
|
| 215 |
+
acquisition_channel_cross_network(utm_campaign), 'Cross-network',
|
| 216 |
+
acquisition_channel_display(utm_medium), 'Display',
|
| 217 |
+
acquisition_channel_paid_shopping(referrer_source, utm_medium, utm_campaign), 'Paid Shopping',
|
| 218 |
+
acquisition_channel_paid_search(referrer_source, utm_medium, utm_source, click_id_param), 'Paid Search',
|
| 219 |
+
acquisition_channel_paid_social(referrer_source, utm_medium, utm_source), 'Paid Social',
|
| 220 |
+
acquisition_channel_paid_video(referrer_source, utm_medium, utm_source), 'Paid Video',
|
| 221 |
+
acquisition_channel_paid_medium(utm_medium), 'Paid Other',
|
| 222 |
+
acquisition_channel_organic_shopping(referrer_source, utm_campaign), 'Organic Shopping',
|
| 223 |
+
acquisition_channel_organic_social(referrer_source, utm_medium), 'Organic Social',
|
| 224 |
+
acquisition_channel_organic_video(referrer_source, utm_medium), 'Organic Video',
|
| 225 |
+
acquisition_channel_has_category_ai_assistants(referrer_source), 'AI Assistants',
|
| 226 |
+
acquisition_channel_has_category_search(referrer_source), 'Organic Search',
|
| 227 |
+
acquisition_channel_email(referrer_source, utm_source, utm_medium), 'Email',
|
| 228 |
+
acquisition_channel_affiliates(utm_medium), 'Affiliates',
|
| 229 |
+
acquisition_channel_audio(utm_medium), 'Audio',
|
| 230 |
+
acquisition_channel_sms(utm_source), 'SMS',
|
| 231 |
+
acquisition_channel_sms(utm_medium), 'SMS',
|
| 232 |
+
acquisition_channel_mobile_push_notifications(utm_medium, referrer_source), 'Mobile Push Notifications',
|
| 233 |
+
acquisition_channel_referral(utm_medium, referrer_source), 'Referral',
|
| 234 |
+
'Direct'
|
| 235 |
+
);
|
priv/data_migrations/AcquisitionChannel/sql/acquisition_channel_update_materialized_column.sql.eex
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ALTER TABLE <%= @table %>
|
| 2 |
+
<%= @on_cluster_statement %>
|
| 3 |
+
MODIFY COLUMN acquisition_channel LowCardinality(String)
|
| 4 |
+
MATERIALIZED acquisition_channel(referrer_source, utm_medium, utm_campaign, utm_source, click_id_param)
|
priv/data_migrations/FunnelDupeGoals/sql/list-funnels-with-dupe-goal-ids.sql.eex
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
WITH DuplicateGoals AS (
|
| 2 |
+
SELECT
|
| 3 |
+
g.site_id,
|
| 4 |
+
CASE
|
| 5 |
+
WHEN g.event_name IS NOT NULL THEN 'Event: ' || g.event_name
|
| 6 |
+
WHEN g.page_path IS NOT NULL THEN 'Page: ' || g.page_path
|
| 7 |
+
END AS goal_name,
|
| 8 |
+
ARRAY_AGG(g.id) AS duplicate_ids
|
| 9 |
+
FROM
|
| 10 |
+
goals g
|
| 11 |
+
WHERE
|
| 12 |
+
g.site_id IS NOT NULL
|
| 13 |
+
GROUP BY
|
| 14 |
+
g.site_id,
|
| 15 |
+
goal_name
|
| 16 |
+
HAVING COUNT(*) > 1
|
| 17 |
+
),
|
| 18 |
+
FunnelStepsWithDuplicateGoals AS (
|
| 19 |
+
SELECT
|
| 20 |
+
fs.id,
|
| 21 |
+
fs.goal_id,
|
| 22 |
+
fs.funnel_id,
|
| 23 |
+
f.name,
|
| 24 |
+
CASE
|
| 25 |
+
WHEN g.event_name IS NOT NULL THEN 'Event: ' || g.event_name
|
| 26 |
+
WHEN g.page_path IS NOT NULL THEN 'Page: ' || g.page_path
|
| 27 |
+
END AS goal_name,
|
| 28 |
+
dg.duplicate_ids
|
| 29 |
+
FROM
|
| 30 |
+
funnel_steps fs
|
| 31 |
+
JOIN goals g ON fs.goal_id = g.id
|
| 32 |
+
JOIN funnels f ON fs.funnel_id = f.id
|
| 33 |
+
JOIN DuplicateGoals dg ON f.site_id = dg.site_id
|
| 34 |
+
WHERE
|
| 35 |
+
fs.goal_id = ANY(dg.duplicate_ids) -- Ch
|
| 36 |
+
)
|
| 37 |
+
SELECT
|
| 38 |
+
s.domain,
|
| 39 |
+
f.site_id,
|
| 40 |
+
f.name as funnel_name,
|
| 41 |
+
f.id,
|
| 42 |
+
fs.id,
|
| 43 |
+
fs.goal_id,
|
| 44 |
+
fs.goal_name,
|
| 45 |
+
fs.duplicate_ids,
|
| 46 |
+
array_agg(fs2.goal_id)
|
| 47 |
+
FROM
|
| 48 |
+
funnels f
|
| 49 |
+
JOIN FunnelStepsWithDuplicateGoals fs ON f.id = fs.funnel_id
|
| 50 |
+
JOIN funnel_steps fs2 ON f.id = fs2.funnel_id
|
| 51 |
+
JOIN sites s ON s.id = f.site_id
|
| 52 |
+
GROUP BY
|
| 53 |
+
s.domain,
|
| 54 |
+
f.site_id,
|
| 55 |
+
f.name,
|
| 56 |
+
f.id,
|
| 57 |
+
fs.id,
|
| 58 |
+
fs.goal_id,
|
| 59 |
+
fs.goal_name,
|
| 60 |
+
fs.duplicate_ids
|
| 61 |
+
ORDER BY fs.id ASC
|
| 62 |
+
;
|
priv/data_migrations/LocationsSync/sql/add-alias-column.sql.eex
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ALTER TABLE <%= @table %>
|
| 2 |
+
<%= if @cluster? do %>ON CLUSTER '{cluster}'<% end %>
|
| 3 |
+
ADD COLUMN IF NOT EXISTS
|
| 4 |
+
<%= @column_name %> String
|
| 5 |
+
ALIAS dictGet('location_data_dict', 'name', tuple('<%= @type %>', <%= @input_column %>))
|
priv/data_migrations/LocationsSync/sql/create-location-data-table.sql.eex
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS location_data <%= if @cluster? do %>ON CLUSTER '{cluster}'<% end %>
|
| 2 |
+
(
|
| 3 |
+
`type` LowCardinality(String),
|
| 4 |
+
`id` String,
|
| 5 |
+
`name` String
|
| 6 |
+
)
|
| 7 |
+
<%= if @cluster? do %>
|
| 8 |
+
ENGINE = ReplicatedMergeTree('/clickhouse/{cluster}/tables/{shard}/plausible_prod/location_data', '{replica}')
|
| 9 |
+
<% else %>
|
| 10 |
+
ENGINE = MergeTree()
|
| 11 |
+
<% end %>
|
| 12 |
+
ORDER BY (type, id)
|
| 13 |
+
SETTINGS index_granularity = 128 <%= @table_settings %>
|
priv/data_migrations/LocationsSync/sql/get-location-data-table-comment.sql.eex
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
select comment from system.tables where database = currentDatabase() and table = 'location_data'
|
priv/data_migrations/LocationsSync/sql/truncate-location-data-table.sql.eex
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
TRUNCATE TABLE IF EXISTS location_data SETTINGS alter_sync=2
|
priv/data_migrations/LocationsSync/sql/update-location-data-dictionary.sql.eex
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE OR REPLACE DICTIONARY location_data_dict
|
| 2 |
+
<%= if @cluster? do %>ON CLUSTER '{cluster}'<% end %>
|
| 3 |
+
(
|
| 4 |
+
`type` String,
|
| 5 |
+
`id` String,
|
| 6 |
+
`name` String
|
| 7 |
+
)
|
| 8 |
+
PRIMARY KEY type, id
|
| 9 |
+
SOURCE(CLICKHOUSE(TABLE location_data <%= @dictionary_connection_params %>))
|
| 10 |
+
LIFETIME(0)
|
| 11 |
+
LAYOUT(complex_key_cache(size_in_cells 500000))
|
priv/data_migrations/LocationsSync/sql/update-location-data-table-comment.sql.eex
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ALTER TABLE location_data
|
| 2 |
+
<%= if @cluster? do %>ON CLUSTER '{cluster}'<% end %>
|
| 3 |
+
MODIFY COMMENT '<%= @version %>'
|
priv/data_migrations/NumericIDs/sql/attach-tmp-events-v2.sql.eex
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ALTER TABLE events_v2 ATTACH PARTITION ID '<%= @partition %>' FROM tmp_events_v2
|
priv/data_migrations/NumericIDs/sql/attach-tmp-sessions-v2.sql.eex
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ALTER TABLE sessions_v2 ATTACH PARTITION ID '<%= @partition %>' FROM tmp_sessions_v2
|
priv/data_migrations/NumericIDs/sql/check-replicas.sql.eex
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
SELECT 1 FROM system.replicas WHERE table = 'events'
|
priv/data_migrations/NumericIDs/sql/create-domains-lookup.sql.eex
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS domains_lookup(site_id UInt64, domain String) ENGINE Memory;
|
priv/data_migrations/NumericIDs/sql/create-events-v2.sql.eex
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS events_v2 <%= if @cluster? do %>ON CLUSTER '{cluster}'<% end %>
|
| 2 |
+
(
|
| 3 |
+
`timestamp` DateTime CODEC(Delta(4), LZ4),
|
| 4 |
+
`name` LowCardinality(String),
|
| 5 |
+
`site_id` UInt64,
|
| 6 |
+
`user_id` UInt64,
|
| 7 |
+
`session_id` UInt64,
|
| 8 |
+
`hostname` String,
|
| 9 |
+
`pathname` String CODEC(ZSTD(3)),
|
| 10 |
+
`referrer` String,
|
| 11 |
+
`referrer_source` String,
|
| 12 |
+
`country_code` FixedString(2),
|
| 13 |
+
`screen_size` LowCardinality(String),
|
| 14 |
+
`operating_system` LowCardinality(String),
|
| 15 |
+
`browser` LowCardinality(String),
|
| 16 |
+
`utm_medium` String,
|
| 17 |
+
`utm_source` String,
|
| 18 |
+
`utm_campaign` String,
|
| 19 |
+
`meta.key` Array(String),
|
| 20 |
+
`meta.value` Array(String),
|
| 21 |
+
`browser_version` LowCardinality(String),
|
| 22 |
+
`operating_system_version` LowCardinality(String),
|
| 23 |
+
`subdivision1_code` LowCardinality(String),
|
| 24 |
+
`subdivision2_code` LowCardinality(String),
|
| 25 |
+
`city_geoname_id` UInt32,
|
| 26 |
+
`utm_content` String,
|
| 27 |
+
`utm_term` String,
|
| 28 |
+
`transferred_from` String
|
| 29 |
+
)
|
| 30 |
+
<%= if @cluster? do %>
|
| 31 |
+
ENGINE = ReplicatedMergeTree('/clickhouse/{cluster}/tables/{shard}/{database}/events_v2', '{replica}')
|
| 32 |
+
<% else %>
|
| 33 |
+
ENGINE = MergeTree()
|
| 34 |
+
<% end %>
|
| 35 |
+
PARTITION BY toYYYYMM(timestamp)
|
| 36 |
+
PRIMARY KEY (site_id, toDate(timestamp), name, user_id)
|
| 37 |
+
ORDER BY (site_id, toDate(timestamp), name, user_id, timestamp)
|
| 38 |
+
SAMPLE BY user_id
|
| 39 |
+
<%= @table_settings %>
|
priv/data_migrations/NumericIDs/sql/create-sessions-v2.sql.eex
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS sessions_v2 <%= if @cluster? do %>ON CLUSTER '{cluster}'<% end %>
|
| 2 |
+
(
|
| 3 |
+
`session_id` UInt64,
|
| 4 |
+
`sign` Int8,
|
| 5 |
+
`site_id` UInt64,
|
| 6 |
+
`user_id` UInt64,
|
| 7 |
+
`hostname` String,
|
| 8 |
+
`timestamp` DateTime CODEC(DoubleDelta, LZ4),
|
| 9 |
+
`start` DateTime CODEC(DoubleDelta, LZ4),
|
| 10 |
+
`is_bounce` UInt8,
|
| 11 |
+
`entry_page` String,
|
| 12 |
+
`exit_page` String,
|
| 13 |
+
`pageviews` Int32,
|
| 14 |
+
`events` Int32,
|
| 15 |
+
`duration` UInt32,
|
| 16 |
+
`referrer` String,
|
| 17 |
+
`referrer_source` String,
|
| 18 |
+
`country_code` LowCardinality(FixedString(2)),
|
| 19 |
+
`screen_size` LowCardinality(String),
|
| 20 |
+
`operating_system` LowCardinality(String),
|
| 21 |
+
`browser` LowCardinality(String),
|
| 22 |
+
`utm_medium` String,
|
| 23 |
+
`utm_source` String,
|
| 24 |
+
`utm_campaign` String,
|
| 25 |
+
`browser_version` LowCardinality(String),
|
| 26 |
+
`operating_system_version` LowCardinality(String),
|
| 27 |
+
`subdivision1_code` LowCardinality(String),
|
| 28 |
+
`subdivision2_code` LowCardinality(String),
|
| 29 |
+
`city_geoname_id` UInt32,
|
| 30 |
+
`utm_content` String,
|
| 31 |
+
`utm_term` String,
|
| 32 |
+
`transferred_from` String,
|
| 33 |
+
`entry_meta.key` Array(String),
|
| 34 |
+
`entry_meta.value` Array(String)
|
| 35 |
+
)
|
| 36 |
+
<%= if @cluster? do %>
|
| 37 |
+
ENGINE = ReplicatedCollapsingMergeTree('/clickhouse/{cluster}/tables/{shard}/plausible_prod/sessions_v2', '{replica}', sign)
|
| 38 |
+
<% else %>
|
| 39 |
+
ENGINE = CollapsingMergeTree(sign)
|
| 40 |
+
<% end %>
|
| 41 |
+
PARTITION BY toYYYYMM(start)
|
| 42 |
+
PRIMARY KEY (site_id, toDate(start), user_id, session_id)
|
| 43 |
+
ORDER BY (site_id, toDate(start), user_id, session_id)
|
| 44 |
+
SAMPLE BY user_id
|
| 45 |
+
<%= @table_settings %>
|
priv/data_migrations/NumericIDs/sql/create-tmp-events-v2.sql.eex
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS
|
| 2 |
+
tmp_events_v2 AS events_v2 ENGINE = MergeTree()
|
| 3 |
+
ORDER BY (site_id, toDate(timestamp), name, user_id, timestamp)
|
| 4 |
+
PRIMARY KEY (site_id, toDate(timestamp), name, user_id)
|
| 5 |
+
PARTITION BY toYYYYMM(timestamp)
|
| 6 |
+
<%= @table_settings %>
|
priv/data_migrations/NumericIDs/sql/create-tmp-sessions-v2.sql.eex
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CREATE TABLE IF NOT EXISTS
|
| 2 |
+
tmp_sessions_v2 AS sessions_v2 ENGINE = CollapsingMergeTree(sign)
|
| 3 |
+
PARTITION BY toYYYYMM(start)
|
| 4 |
+
PRIMARY KEY (site_id, toDate(start), user_id, session_id)
|
| 5 |
+
ORDER BY (site_id, toDate(start), user_id, session_id)
|
| 6 |
+
<%= @table_settings %>
|
priv/data_migrations/NumericIDs/sql/drop-domains-lookup.sql.eex
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
DROP TABLE IF EXISTS domains_lookup SYNC;
|