Skill 82 · Instrument Product Analytics
Subchapter 82.8
references/elixir.mdMarkdown14 KBView on GitHub
AI agents: this is one page from PostHog’s docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt (opens in a new tab)
This library provides an Elixir HTTP client for PostHog. See the repository (opens in a new tab) for more information.
This library was built by the community but it’s being maintained by the PostHog core team since v1.0.0. Thank you to Nick Kezhaya (opens in a new tab) for building it originally. Thank you to Alex Martsinovich (opens in a new tab) for contributing v2.0.0.
The package can be installed by adding posthog to your list of dependencies in mix.exs:
Elixir
def deps do
[
{:posthog, "~> 2.0"}
]
endconfig/config.exs
config :posthog,
enable: true,
api_host: "https://us.i.posthog.com",
api_key: "<ph_project_token>",
in_app_otp_apps: [:my_app]You can see all the available configuration options in the PostHog.Config (opens in a new tab) module.
Optionally, you might want to enable the Plug integration (opens in a new tab) to attach request metadata and tracing context in Plug-based applications including Phoenix. You still need to capture events explicitly with PostHog.capture/2 or PostHog.capture/3.
For a test environment, you can pass in test_mode: true value to the config. This causes events to be dropped instead of sent to PostHog.
Identifying users is required. Backend events need a
distinct_idthat matches the ID your frontend uses when callingposthog.identify(). Without this, backend events are orphaned — they can’t be linked to frontend event captures, session replays (opens in a new tab), LLM traces (opens in a new tab), or error tracking (opens in a new tab).See our guide on identifying users (opens in a new tab) for how to set this up.
To capture an event, use PostHog.capture/2:
Elixir
PostHog.capture("user_signed_up", %{distinct_id: "distinct_id_of_the_user"})Tip: We recommend using a
[object] [verb]format for your event names, where[object]is the entity that the behavior relates to, and[verb]is the behavior itself. For example,project created,user signed up, orinvite sent.
Optionally, you can include additional information with the event by including a properties (opens in a new tab) object:
Elixir
PostHog.capture("user_signed_up", %{
distinct_id: "distinct_id_of_the_user",
login_type: "email",
is_free_trial: true
})Carrying distinct_id around all the time might not be the most convenient approach, so PostHog lets you store it and other properties in a context.
The context is stored in the Logger metadata and PostHog automatically attaches these properties to any events you capture with PostHog.capture/2, as long as they happen in the same process.
Elixir
PostHog.set_context(%{distinct_id: "distinct_id_of_the_user"})
PostHog.capture("page_opened")You can also scope the context to a specific event name:
Elixir
PostHog.set_event_context("sensitive_event", %{"$process_person_profile": false})Events are automatically batched and sent to PostHog via a background job.
PostHog.capture/2 is very powerful and enables you to send events that have special meaning.
In other libraries you’ll usually find helpers for these special events, but they must be explicitly sent in Elixir.
For example:
Elixir
PostHog.capture("$create_alias", %{distinct_id: "frontend_id", alias: "backend_id"})Elixir
PostHog.capture("$groupidentify", %{
distinct_id: "static_string_used_for_all_group_events",
"$group_type": "company",
"$group_key": "company_id_in_your_db"
})For Phoenix or Plug apps, add PostHog.Integrations.Plug before your router to attach request metadata and PostHog tracing headers to events captured during the request.
lib/my_app_web/endpoint.ex
plug PostHog.Integrations.Plug
plug MyAppWeb.RouterFor plain Plug routers, add it before :match and :dispatch:
Elixir
defmodule MyRouter do
use Plug.Router
plug PostHog.Integrations.Plug
plug :match
plug :dispatch
# ... routes
endThe plug adds request metadata such as $current_url, $host, $pathname, $request_method, $user_agent, and $ip. It also reads X-PostHog-Distinct-Id and X-PostHog-Session-Id as analytics context so backend events and errors can be linked to frontend users and sessions.
If you’re using PostHog JS (opens in a new tab) on the frontend, configure tracing_headers (opens in a new tab) for your Phoenix or Plug backend hostname so browser requests include these headers.
Tracing headers are client-controlled analytics context, not authentication or authorization. Pass an authenticated distinct_id explicitly for security-sensitive server-side decisions.
PostHog’s feature flags (opens in a new tab) enable you to safely deploy and roll back new features as well as target specific users and groups with them.
There are two steps to implement feature flags in Elixir:
Call PostHog.FeatureFlags.evaluate_flags/1 once for the user, then read values from the returned snapshot.
Elixir
{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user")
if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do
# Do something differently for this user
# Optional: fetch the payload
payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key")
endElixir
{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user")
enabled_variant = PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "flag-key")
if enabled_variant == "variant-key" do
# Do something differently for this user
# Optional: fetch the payload
payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "flag-key")
endPostHog.FeatureFlags.Evaluations.get_flag/2 returns the variant string for multivariate flags, true for enabled boolean flags, false for disabled flags, and nil when the flag wasn’t returned by the evaluation.
Note:
PostHog.FeatureFlags.check/2,PostHog.FeatureFlags.check!/2,PostHog.FeatureFlags.get_feature_flag_result/2, andPostHog.FeatureFlags.get_feature_flag_result!/2still work during the migration period, but they’re deprecated. Preferevaluate_flags/1for new code.
If you want use your feature flag to breakdown or filter events in your insights (opens in a new tab), you’ll need to include feature flag information in those events. This ensures that the feature flag value is attributed correctly to the event.
Note: This step is only required for events captured using our server-side SDKs or API (opens in a new tab).
There are two methods you can use to include feature flag information in your events:
Put the same snapshot object that you used for branching into context. Subsequent captures from the same process attach the exact flag values from that evaluation and don’t make another /flags request.
Elixir
{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user")
if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key") do
# Do something differently for this user
end
PostHog.FeatureFlags.set_in_context(snapshot)
PostHog.capture("event_name", %{distinct_id: "distinct_id_of_your_user"})By default, this attaches every flag in the snapshot using $feature/<flag-key> properties and $active_feature_flags.
To reduce event property bloat, put a filtered snapshot in context:
Elixir
{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("distinct_id_of_your_user")
# Attach only flags accessed with enabled?/2 or get_flag/2 before this call
PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "flag-key")
PostHog.FeatureFlags.set_in_context(
PostHog.FeatureFlags.Evaluations.only_accessed(snapshot)
)
# Or attach only specific flags
PostHog.FeatureFlags.set_in_context(
PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"])
)only_accessed/1 is order-dependent. If you call it before accessing any flags with enabled?/2 or get_flag/2, no feature flag properties are attached.
In the event properties, include $feature/feature_flag_name: variant_key:
Elixir
PostHog.capture("event_name", %{
"$feature/feature-flag-key" => "variant-key",
distinct_id: "distinct_id_of_your_user"
})By default, evaluate_flags/1 evaluates every flag for the user. If you only need a few flags, pass flag_keys to request only those flags:
Elixir
{:ok, snapshot} =
PostHog.FeatureFlags.evaluate_flags(%{
distinct_id: "distinct_id_of_your_user",
flag_keys: ["checkout-flow", "new-dashboard"]
})Capturing $feature_flag_called events enables PostHog to know when a flag was accessed by a user and provide analytics and insights (opens in a new tab) on the flag. With evaluate_flags/1, the SDK sends this event when you call PostHog.FeatureFlags.Evaluations.enabled?/2 or PostHog.FeatureFlags.Evaluations.get_flag/2 for a flag.
PostHog.FeatureFlags.Evaluations.get_flag_payload/2 doesn’t send $feature_flag_called events.
Local evaluation is available in version 2.15.0 and later. Follow the server-side local evaluation guide (opens in a new tab) to find your secure key and see how to pass the person properties, groups, and group properties that your flag conditions require.
Store the secure key in a server-side environment variable. Don’t expose it to client-side applications:
config/runtime.exs
config :posthog,
api_host: "https://us.i.posthog.com",
api_key: "<ph_project_token>",
secret_key: System.fetch_env!("POSTHOG_FEATURE_FLAGS_SECURE_API_KEY")When secret_key is set, the SDK fetches definitions when it starts and polls for updates every 30 seconds. PostHog.FeatureFlags.evaluate_flags/1 evaluates each flag locally first. If a flag can’t be evaluated locally, the SDK makes one /flags request to resolve the remaining flags. Set only_evaluate_locally: true in the evaluation map to prevent this remote fallback. The SDK then omits unresolved flags from the snapshot.
Use these configuration options to control local evaluation:
| Option | Default | Purpose |
|---|---|---|
enable_local_evaluation | true | Starts local evaluation when secret_key is set. |
feature_flags_poll_interval_ms | 30_000 | Sets the interval between definition refreshes. |
flag_definition_request_timeout_ms | 10_000 | Sets the timeout for each definition request. |
flag_definition_cache_provider_timeout_ms | 5_000 | Sets the timeout for each shared cache provider callback. |
For multiple server instances, you can implement PostHog.FeatureFlags.FlagDefinitionCacheProvider and set flag_definition_cache_provider: {module, state}. This optional provider shares definitions and coordinates which instance polls PostHog. See local evaluation in distributed environments (opens in a new tab) for the callback contract and configuration.
Error tracking is enabled by default. It will automatically captures exceptions thrown by the application.
As a matter of fact, since this is built on top of Elixir’s Logger module, it automatically captures any Logger.error calls.
You can always disable it by setting enable_error_tracking to false:
Elixir
config :posthog,
enable_error_tracking: falseBy default, PostHog starts its own supervision tree and attaches a logger handler.
In certain cases, you might want to run this supervision tree yourself. You can do this by disabling the default supervisor and adding PostHog.Supervisor to your application tree with its own configuration:
config.exs
config :posthog, enable: false
config :my_app, :posthog,
api_host: "https://us.i.posthog.com",
api_key: "<ph_project_token>"application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
posthog_config = Application.fetch_env!(:my_app, :posthog) |> PostHog.Config.validate!()
:logger.add_handler(:posthog, PostHog.Handler, %{config: posthog_config})
children = [
{PostHog.Supervisor, posthog_config}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
endIn even more advanced cases, you might want to interact with more than one PostHog project. In this case, you can run multiple PostHog supervision trees, one of which can be the default one:
config.exs
config :posthog,
api_host: "https://us.i.posthog.com",
api_key: "<ph_project_token>"
config :my_app, :another_posthog,
api_host: "https://us.i.posthog.com",
api_key: "a_different_project_api_key",
supervisor_name: AnotherPostHogapplication.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
posthog_config = Application.fetch_env!(:my_app, :another_posthog) |> PostHog.Config.validate!()
children = [
{PostHog.Supervisor, posthog_config}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
endThen, each function in the PostHog module accepts an optional first argument with the name of the PostHog supervisor tree that will process the capture:
Elixir
PostHog.capture(AnotherPostHog, "user_signed_up", %{distinct_id: "user123"})The library is maintained by the PostHog team since February 2025. Thanks to nkezhaya (opens in a new tab) for contributing v0.1.0. Thanks to martosaur (opens in a new tab) for contributing v2.0.0.
Ask PostHog AI
HelpfulCould be better