Skip to content
Open
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
5 changes: 5 additions & 0 deletions .sampo/changesets/emails-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
hex/posthog: minor
---

Add `PostHog.Emails` to send transactional email and read its status. `PostHog.Emails.send/2` queues one email from a template made in PostHog (`template_id` and `template_data`) or from content your code rendered (`template_key`, `subject`, and `html` and/or `text`). `PostHog.Emails.get/2` reads what happened to it. Both need a project secret API key in `secret_key`. New config options: `app_host`, `email_request_timeout_ms`, `email_request_max_retries` and `migrated_from`.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ SDK usage examples and code snippets live in the official documentation so they

- [Elixir library docs](https://posthog.com/docs/libraries/elixir)
- [Phoenix framework docs](https://posthog.com/docs/libraries/phoenix)
- [Transactional email (HexDocs)](https://hexdocs.pm/posthog/PostHog.Emails.html)
- [OpenTelemetry span context (HexDocs)](https://hexdocs.pm/posthog/PostHog.OpenTelemetry.ContextSpanProcessor.html)
- [API reference (HexDocs)](https://hexdocs.pm/posthog)

Expand Down
22 changes: 15 additions & 7 deletions lib/posthog/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ defmodule PostHog.Config do

## Remarks

String `:api_key`, `:secret_key`, and `:api_host` values are trimmed before validation. A blank
`:api_host` falls back to the default PostHog US ingestion host.
String `:api_key`, `:secret_key`, `:app_host`, and `:api_host` values are trimmed before
validation. A blank `:api_host` falls back to the default PostHog US ingestion host, and a blank
`:app_host` counts as unset.
"""
@spec validate(options()) ::
{:ok, config()} | {:error, NimbleOptions.ValidationError.t()}
Expand Down Expand Up @@ -179,7 +180,14 @@ defmodule PostHog.Config do
end)
|> then(fn normalized_options ->
if Keyword.has_key?(normalized_options, :secret_key) do
Keyword.update!(normalized_options, :secret_key, &normalize_secret_key/1)
Keyword.update!(normalized_options, :secret_key, &normalize_optional_string/1)
else
normalized_options
end
end)
|> then(fn normalized_options ->
if Keyword.has_key?(normalized_options, :app_host) do
Keyword.update!(normalized_options, :app_host, &normalize_optional_string/1)
else
normalized_options
end
Expand All @@ -197,14 +205,14 @@ defmodule PostHog.Config do
defp normalize_api_key(nil), do: ""
defp normalize_api_key(api_key), do: api_key

defp normalize_secret_key(secret_key) when is_binary(secret_key) do
case String.trim(secret_key) do
defp normalize_optional_string(value) when is_binary(value) do
case String.trim(value) do
"" -> nil
value -> value
trimmed -> trimmed
end
end

defp normalize_secret_key(secret_key), do: secret_key
defp normalize_optional_string(value), do: value

defp normalize_api_host(api_host) when is_binary(api_host) do
api_host
Expand Down
65 changes: 63 additions & 2 deletions lib/posthog/config_schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,45 @@ defmodule PostHog.Config.Schema do
type: {:or, [:string, nil]},
default: nil,
doc: """
A privileged project secret (`phs_`) or appropriately scoped personal API key (`phx_`)
required only to fetch local feature flag definitions directly from PostHog. A configured
A privileged project secret (`phs_`) or appropriately scoped personal API key (`phx_`).
Fetches local feature flag definitions directly from PostHog. A configured
`flag_definition_cache_provider` can supply definitions without this key.
`PostHog.Emails` needs a project secret API key with the `hog_flow:write` scope to send
email, or `hog_flow:read` to read an email's status.
"""
],
app_host: [
type: {:or, [{:custom, __MODULE__, :http_url, []}, nil]},
default: nil,
doc: """
The PostHog app host that `PostHog.Emails` sends to, such as `https://posthog.example.com`
for a self-hosted instance. Without it, a PostHog Cloud `api_host` maps to its region's app
host, and any other `api_host` makes email calls fail with `missing_app_host`, so email
requests never send the secret key to a reverse proxy for ingestion. Local flag
evaluation still fetches definitions from `api_host` with `secret_key`; set
`enable_local_evaluation: false` when the proxy must not see the key.
"""
],
email_request_timeout_ms: [
type: :pos_integer,
default: 15_000,
doc: "Maximum time in milliseconds to wait for each `PostHog.Emails` request."
],
email_request_max_retries: [
type: :non_neg_integer,
default: 2,
doc: """
Number of retries for a `PostHog.Emails` request after a network error, a rate limit, an
idempotency key still in flight, or an unavailable service. Set to 0 when a job runner
already retries the send.
"""
],
migrated_from: [
type: {:or, [{:custom, __MODULE__, :header_token, []}, nil]},
default: nil,
doc: """
The email provider you moved from, such as `"resend"`. Sent with every `PostHog.Emails`
request as `X-PostHog-Migrated-From`.
"""
],
enable_local_evaluation: [
Expand Down Expand Up @@ -151,6 +187,31 @@ defmodule PostHog.Config.Schema do
] ++ shared()
end

@doc false
def http_url(value) when is_binary(value) do
case URI.parse(value) do
%URI{scheme: scheme, host: host}
when scheme in ["http", "https"] and host not in [nil, ""] ->
{:ok, value}

_other ->
{:error, "expected an absolute http or https URL, got: #{inspect(value)}"}
end
end

def http_url(value), do: {:error, "expected a string, got: #{inspect(value)}"}

@doc false
def header_token(value) when is_binary(value) do
if value =~ ~r/\A[\x21-\x7e]+\z/ do
{:ok, value}
else
{:error, "expected visible ASCII characters without spaces, got: #{inspect(value)}"}
end
end

def header_token(value), do: {:error, "expected a string, got: #{inspect(value)}"}

def convenience do
[
enable: [
Expand Down
Loading
Loading