Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ You can also use a graphical client like [Postgres.app](https://postgresapp.com/
export AWS_ACCESS_KEY_ID=GK3b...
export AWS_SECRET_ACCESS_KEY=a03cf77e181...
export AWS_DEFAULT_REGION=garage
export UMAMI_USERNAME=some_username
export UMAMI_PASSWORD=a_strong_password
```
You will need to source this file in your shell before starting the server:
```sh
Expand Down
5 changes: 5 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ config :admin, :base_host, "localhost:3114"
#
# In the future, if we change the setup and the entity that handles the "domain" in local dev is able to forward the requests to the backend correctly, this can be removed
config :admin, :backend_origin, "http://localhost:3001"
config :admin, :umami_origin, "http://localhost:8000"

# Publication index (development defaults)
config :admin, :publication_reindex_headers, [{"meilisearch-rebuild", "secret"}]

config :admin, :umami,
username: System.get_env("UMAMI_USERNAME"),
password: System.get_env("UMAMI_PASSWORD")
6 changes: 6 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ if config_env() == :prod do
# In production these configs have the same value.
config :admin, base_host: base_host
config :admin, backend_origin: "https://#{base_host}"
config :admin, umami_origin: "https://umami.#{base_host}"

# Get the value of the header secret to communicate with Meilisearch
config :admin, :publication_reindex_headers, [
Expand All @@ -170,4 +171,9 @@ if config_env() == :prod do

# override the ses region to use Frankfurt since SES does not exist in eu-central-2
config :ex_aws, :ses, region: "eu-central-1"

# Configure Umami
config :admin, :umami,
username: System.get_env("UMAMI_USERNAME"),
password: System.get_env("UMAMI_PASSWORD")
end
7 changes: 7 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@ config :admin, :publication_reindex_opts,
plug: {Req.Test, Admin.Publications.SearchIndex},
# in test we do not want to retry a failed call, as it is probably expected to fail. This speeds up the tests.
retry: false

config :admin, :umami,
username: "test",
password: "testest"

config :admin, :umami_origin, "http://localhost:8000"
config :admin, :umami_req_options, plug: {Req.Test, Admin.UmamiApi}, retry: false
2 changes: 2 additions & 0 deletions coveralls.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"skip_files": [
"lib/admin/release.ex",
"lib/admin/tools/uptime.ex",
"lib/admin_web/controllers/error_html.ex",
"lib/admin_web/telemetry.ex",
"lib/admin_web/live/dev_live/index.ex",
"lib/admin/application.ex",
"lib/admin/sentry_filter.ex",
"lib/admin/analytics/event_store.ex",
"lib/admin_web/live/analytics_live/example.ex",
"lib/admin_web/live/analytics_live/event_generator.ex"
Expand Down
5 changes: 3 additions & 2 deletions lib/admin/accounts/user_notifier.ex
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,14 @@ defmodule Admin.Accounts.UserNotifier do
)
end

def deliver_call_to_action(user, subject, message_text, button_text, button_url) do
def deliver_call_to_action(user, subject, message_text, button_text, button_url, pixel) do
html_body =
EmailTemplates.render("call_to_action", %{
name: user.name,
message: message_text,
button_text: button_text,
button_url: button_url
button_url: button_url,
pixel: pixel
})

deliver(
Expand Down
7 changes: 5 additions & 2 deletions lib/admin/mailing_worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
alias Admin.Accounts.Scope
alias Admin.Accounts.UserNotifier
alias Admin.Notifications
require Logger

@impl Oban.Worker
def perform(%Oban.Job{
Expand Down Expand Up @@ -41,7 +40,10 @@
{:error, :notification_not_found} ->
{:cancel, :notification_not_found}

{:error, :invalid_target_audience} ->

Check warning on line 43 in lib/admin/mailing_worker.ex

View workflow job for this annotation

GitHub Actions / Test on OTP 28.0.2 / Elixir 1.19.4

pattern_match

The pattern can never match the type {:error, :notification_not_found}.

Check warning on line 43 in lib/admin/mailing_worker.ex

View workflow job for this annotation

GitHub Actions / Test on OTP 27.3.4 / Elixir 1.19.4

pattern_match

The pattern can never match the type {:error, :notification_not_found}.
{:cancel, :invalid_target_audience}

{:error, error} ->

Check warning on line 46 in lib/admin/mailing_worker.ex

View workflow job for this annotation

GitHub Actions / Test on OTP 28.0.2 / Elixir 1.19.4

pattern_match_cov

The pattern pattern {'error', _error@1} can never match the type, because it is covered by previous clauses.

Check warning on line 46 in lib/admin/mailing_worker.ex

View workflow job for this annotation

GitHub Actions / Test on OTP 27.3.4 / Elixir 1.19.4

pattern_match_cov

The pattern pattern {'error', _error@1} can never match the type, because it is covered by previous clauses.
{:error, "Failed to send notification: #{inspect(error)}"}
end
end
Expand Down Expand Up @@ -83,7 +85,8 @@
localized_email.subject,
localized_email.message,
localized_email.button_text,
localized_email.button_url
localized_email.button_url,
notification.pixel
)

# save message log
Expand Down
29 changes: 24 additions & 5 deletions lib/admin/notifications.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule Admin.Notifications do
alias Admin.Notifications.LocalizedEmail
alias Admin.Notifications.Log
alias Admin.Notifications.Notification
alias Admin.Notifications.Pixel

# Notifications
def new_notification, do: %Notification{}
Expand Down Expand Up @@ -50,7 +51,7 @@ defmodule Admin.Notifications do
change_notification(scope, %Notification{}, attrs)
|> Repo.insert() do
broadcast_notification(scope, {:created, notification})
{:ok, notification |> Repo.preload([:logs, :localized_emails])}
{:ok, notification |> Repo.preload([:logs, :localized_emails, :pixel])}
end
end

Expand Down Expand Up @@ -99,12 +100,12 @@ defmodule Admin.Notifications do
"""
def list_notifications(%Scope{} = _scope) do
Repo.all(from n in Notification, order_by: [desc: :updated_at])
|> Repo.preload([:logs, :localized_emails])
|> Repo.preload([:logs, :localized_emails, :pixel])
end

def list_notifications_by_status(%Scope{} = _scope) do
Repo.all(from n in Notification, order_by: [desc: :sent_at])
|> Repo.preload([:logs, :localized_emails])
|> Repo.preload([:logs, :localized_emails, :pixel])
end

def list_recently_sent_notifications(%Scope{} = _scope) do
Expand All @@ -129,7 +130,7 @@ defmodule Admin.Notifications do

"""
def get_notification!(%Scope{} = _scope, id) do
Repo.get_by!(Notification, id: id) |> Repo.preload([:logs, :localized_emails])
Repo.get_by!(Notification, id: id) |> Repo.preload([:logs, :localized_emails, :pixel])
end

@doc """
Expand All @@ -145,7 +146,7 @@ defmodule Admin.Notifications do

"""
def get_notification(%Scope{} = _scope, id) do
case Repo.get_by(Notification, id: id) |> Repo.preload([:logs, :localized_emails]) do
case Repo.get_by(Notification, id: id) |> Repo.preload([:logs, :localized_emails, :pixel]) do
%Notification{} = notification -> {:ok, notification}
nil -> {:error, :notification_not_found}
end
Expand Down Expand Up @@ -356,6 +357,9 @@ defmodule Admin.Notifications do
{:ok, audience}
end

# support legacy audience, this is what the pervious audience is converted to.
def get_target_audience(%Scope{} = _scope, "custom", _opts), do: {:ok, []}

def get_target_audience(%Scope{} = _scope, target_audience, _opts) do
Logger.error("Invalid target audience: #{target_audience}")
{:error, :invalid_target_audience}
Expand All @@ -365,4 +369,19 @@ defmodule Admin.Notifications do
only_langs = Keyword.get(opts, :only_langs, Admin.Languages.all_values()) |> MapSet.new()
audience |> Enum.filter(fn user -> MapSet.member?(only_langs, user.lang) end)
end

def create_pixel(%Scope{} = scope, %Admin.Notifications.Notification{} = notification) do
with {:ok, pixel_resp} <- Admin.UmamiApi.create_pixel(notification.name),
pixel =
Pixel.changeset(%Pixel{}, %{
id: pixel_resp["id"],
name: pixel_resp["name"],
slug: pixel_resp["slug"],
notification_id: notification.id
}),
{:ok, pixel} <- Admin.Repo.insert(pixel) do
broadcast_localized_email(scope, notification.id, {:updated, pixel})
{:ok, pixel}
end
end
end
2 changes: 2 additions & 0 deletions lib/admin/notifications/notification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ defmodule Admin.Notifications.Notification do
field :total_recipients, :integer, default: 0
field :sent_at, :utc_datetime

has_one :pixel, Admin.Notifications.Pixel

has_many :logs, Admin.Notifications.Log

has_many :localized_emails, Admin.Notifications.LocalizedEmail,
Expand Down
24 changes: 24 additions & 0 deletions lib/admin/notifications/pixel.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Admin.Notifications.Pixel do
@moduledoc """
Represents a notification pixel used for tracking in emails.
"""

use Admin.Schema
import Ecto.Changeset

schema "notification_pixels" do
# the pixel id is given by Umami, it is not generated by us.
field :name, :string
field :slug, :string

belongs_to :notification, Admin.Notifications.Notification

timestamps()
end

def changeset(pixel, attrs) do
pixel
|> cast(attrs, [:id, :slug, :name, :notification_id])
|> validate_required([:id, :slug, :name, :notification_id])
end
end
1 change: 0 additions & 1 deletion lib/admin/release.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ defmodule Admin.Release do
installed.
"""
@app :admin
require Logger

alias Admin.Tools.Uptime

Expand Down
15 changes: 15 additions & 0 deletions lib/admin/text_transforms.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Admin.TextTransforms do
@moduledoc """
Provides text transformation functions.
"""

def slugify(text) do
text
# make everything lowercase
|> String.downcase()
# remove non-word characters (keep spaces and dashes)
|> String.replace(~r/[^\w\s-]/, "")
# replace spaces with dashes
|> String.replace(~r/\s+/, "-")
end
end
Loading
Loading