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
3 changes: 2 additions & 1 deletion context/skills/integration/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ variants:
- https://posthog.com/docs/libraries/ruby.md

- id: elixir
template: description-elixir-docs-only.md
framework: elixir
example_paths: example-apps/elixir
display_name: Elixir
description: PostHog integration for Elixir and Phoenix applications using the posthog SDK
tags: [elixir, phoenix, plug]
Expand Down
27 changes: 0 additions & 27 deletions context/skills/integration/description-elixir-docs-only.md

This file was deleted.

4 changes: 4 additions & 0 deletions context/skip-patterns.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ global:
# Angular
- .angular/

# Elixir
- _build/
- deps/

# Regex patterns - skip if path matches
# Note: Patterns are JavaScript regex syntax
regex:
Expand Down
4 changes: 4 additions & 0 deletions example-apps/elixir/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copy to .env and fill in, then export the values before running the app:
# export $(grep -v '^#' .env | xargs)
POSTHOG_PROJECT_TOKEN=your_posthog_project_token
POSTHOG_HOST=https://us.i.posthog.com
6 changes: 6 additions & 0 deletions example-apps/elixir/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Build artifacts and dependencies
/_build/
/deps/

# Secrets
.env
168 changes: 168 additions & 0 deletions example-apps/elixir/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# PostHog Phoenix (Elixir) example

This is a [Phoenix](https://www.phoenixframework.org/) example demonstrating PostHog integration with product analytics, feature flags, user identification, and error tracking using the server-side [`posthog`](https://hex.pm/packages/posthog) Elixir SDK.

## Features

- **Product analytics**: Track user events and behaviors
- **User identification**: Associate events with users via the `distinct_id` on capture
- **Feature flags**: Control feature rollouts with PostHog feature flags
- **Error tracking**: Automatic exception + `Logger.error` capture (built into the SDK)
- **Server-side tracking**: All tracking happens server-side with the Elixir SDK
- **Single client per app**: The SDK starts one supervised client for the whole app
- **Context plug**: `PostHog.Integrations.Plug` tags every request automatically

> **Note on error tracking:** unlike the Java/Python examples, the PostHog Elixir
> SDK captures errors **automatically** — it hooks into Elixir's `Logger`, so any
> `Logger.error` call and any uncaught exception is reported without an explicit
> call. There is no documented `capture_exception` function, so the profile page
> demonstrates the idiomatic path (`Logger.error`) plus an explicit
> `error_occurred` event.

## Getting started

### 1. Install dependencies

```bash
mix deps.get
```

### 2. Configure environment variables

Copy `.env.example` to `.env`, fill in your values, then export them:

```bash
export POSTHOG_PROJECT_TOKEN=your_posthog_project_token
export POSTHOG_HOST=https://us.i.posthog.com
```

Get your PostHog project token from your [PostHog project settings](https://app.posthog.com/project/settings).

### 3. Run the development server

```bash
mix phx.server
```

Open [http://localhost:8000](http://localhost:8000) with your browser to see the app.

## Project structure

```
elixir/
├── mix.exs # Project + the posthog dependency
├── .env.example # Environment variable template
├── .gitignore
├── config/
│ ├── config.exs # Endpoint + PostHog SDK config (enable, otp apps)
│ └── runtime.exs # Reads POSTHOG_* env vars at boot
└── lib/
├── posthog_example/
│ └── application.ex # OTP app; warns loudly on a blank token
├── posthog_example_web.ex # controller / html / router helpers
└── posthog_example_web/
├── endpoint.ex # Plug pipeline incl. PostHog.Integrations.Plug
├── router.ex # Routes for the burrito app
└── controllers/
├── burrito_controller.ex # Events, identify, flags, errors
├── burrito_html.ex # Tiny HEEx pages
├── layouts.ex # Root layout
└── error_html.ex # Error pages
```

## Key integration points

### Configuration from the environment (config/runtime.exs)

`runtime.exs` runs at boot, so it can read environment variables. The token and host never live in source.

```elixir
config :posthog,
api_key: System.get_env("POSTHOG_PROJECT_TOKEN"),
api_host: System.get_env("POSTHOG_HOST") || "https://us.i.posthog.com"
```

### SDK init — one client per app (config/config.exs)

The SDK starts and supervises a single client for the whole app automatically. You only turn it on and tell it which OTP app is "in app" for error grouping:

```elixir
config :posthog,
enable: true,
in_app_otp_apps: [:posthog_example]
```

A blank token doesn't crash the app — `application.ex` logs a clear warning and boots anyway.

### Context plug (lib/posthog_example_web/endpoint.ex)

Add the built-in plug before your router. It wraps each request in a PostHog context: it attaches request metadata (`$current_url`, `$host`, `$pathname`, …) and reads the `X-PostHog-Distinct-Id` / `X-PostHog-Session-Id` tracing headers to link backend and frontend events.

```elixir
plug PostHog.Integrations.Plug
plug PostHogExampleWeb.Router
```

### User identification (lib/posthog_example_web/controllers/burrito_controller.ex)

The Elixir SDK has no separate server-side `identify` step — you identify a user by passing their `distinct_id` on `capture`. It must match the id your frontend `posthog.identify(...)` uses so both sides attach to the same person.

```elixir
PostHog.capture("user_logged_in", %{
distinct_id: distinct_id,
login_method: "email"
})
```

### Event tracking (lib/posthog_example_web/controllers/burrito_controller.ex)

`distinct_id` is required; every other key in the map becomes an event property.

```elixir
PostHog.capture("burrito_considered", %{
distinct_id: distinct_id,
total_considerations: count
})
```

### Feature flags (lib/posthog_example_web/controllers/burrito_controller.ex)

Evaluate flags once per request, then read individual flags off the returned snapshot.

```elixir
show_new_feature =
case PostHog.FeatureFlags.evaluate_flags(distinct_id) do
{:ok, snapshot} ->
PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "new-dashboard-feature")

_error ->
false
end
```

### Error tracking (lib/posthog_example_web/controllers/burrito_controller.ex)

Error tracking is **automatic** — the SDK is built on Elixir's `Logger`, so uncaught exceptions and `Logger.error` calls are captured with no explicit call. There is no documented `capture_exception`, so we log the error (auto-captured) and also send an explicit analytics event:

```elixir
try do
raise "Something went wrong while loading the burrito profile!"
rescue
error ->
Logger.error("Burrito profile error: #{Exception.message(error)}")

PostHog.capture("error_occurred", %{
distinct_id: distinct_id,
error_message: Exception.message(error)
})
end
```

Disable automatic capture with `config :posthog, enable_error_tracking: false`.

## Learn more

- [PostHog Elixir SDK](https://posthog.com/docs/libraries/elixir)
- [PostHog feature flags](https://posthog.com/docs/feature-flags)
- [PostHog documentation](https://posthog.com/docs)
- [Phoenix framework](https://www.phoenixframework.org/)
25 changes: 25 additions & 0 deletions example-apps/elixir/config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Config

# Endpoint configuration. The secret_key_base here is a throwaway dev value;
# override it in prod via runtime.exs / an environment variable.
config :posthog_example, PostHogExampleWeb.Endpoint,
url: [host: "localhost"],
adapter: Bandit.PhoenixAdapter,
http: [ip: {127, 0, 0, 1}, port: 8000],
render_errors: [formats: [html: PostHogExampleWeb.ErrorHTML], layout: false],
secret_key_base:
"dev_only_secret_key_base_replace_in_prod_0123456789abcdefghijklmnopqrstuvwxyz",
server: true

# PostHog SDK — one client per app, started automatically by the SDK's
# own supervisor. The token and host are read from the environment in
# config/runtime.exs so secrets never live in source control.
#
# enable -> master on/off switch for the SDK
# in_app_otp_apps -> which OTP apps' stack frames count as "in app" for
# automatic error tracking (grouping / noise control)
config :posthog,
enable: true,
in_app_otp_apps: [:posthog_example]

config :phoenix, :json_library, Jason
22 changes: 22 additions & 0 deletions example-apps/elixir/config/runtime.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Config

# runtime.exs runs at boot in every environment, which makes it the right
# place to read environment variables (they are not available at compile time).

# Configuration from the environment. Never hardcode the token.
# POSTHOG_PROJECT_TOKEN -> your project's write key
# POSTHOG_HOST -> defaults to PostHog US Cloud
config :posthog,
api_key: System.get_env("POSTHOG_PROJECT_TOKEN"),
api_host: System.get_env("POSTHOG_HOST") || "https://us.i.posthog.com"

# In production, require a real secret_key_base from the environment.
if config_env() == :prod do
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise "SECRET_KEY_BASE is missing. Generate one with `mix phx.gen.secret`."

config :posthog_example, PostHogExampleWeb.Endpoint,
http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "8000")],
secret_key_base: secret_key_base
end
39 changes: 39 additions & 0 deletions example-apps/elixir/lib/posthog_example/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule PostHogExample.Application do
@moduledoc false

use Application
require Logger

@impl true
def start(_type, _args) do
warn_if_token_missing()

children = [
PostHogExampleWeb.Endpoint
]

opts = [strategy: :one_for_one, name: PostHogExample.Supervisor]
Supervisor.start_link(children, opts)
end

@impl true
def config_change(changed, _new, removed) do
PostHogExampleWeb.Endpoint.config_change(changed, removed)
:ok
end

# Fail loudly, but don't break the app: with a blank token the SDK simply
# won't deliver events. We log a clear warning and let the app boot.
defp warn_if_token_missing do
case Application.get_env(:posthog, :api_key) do
token when token in [nil, ""] ->
Logger.warning(
"POSTHOG_PROJECT_TOKEN is not set — PostHog events will not be delivered. " <>
"The app will still run. Set it in your environment to enable analytics."
)

_token ->
:ok
end
end
end
52 changes: 52 additions & 0 deletions example-apps/elixir/lib/posthog_example_web.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule PostHogExampleWeb do
@moduledoc """
Entry points for the web layer: controllers, HTML views, and the router.

Use it with `use PostHogExampleWeb, :controller`, `use PostHogExampleWeb, :html`,
or `use PostHogExampleWeb, :router`.
"""

def router do
quote do
use Phoenix.Router, helpers: false

import Plug.Conn
import Phoenix.Controller
end
end

def controller do
quote do
use Phoenix.Controller,
formats: [:html],
layouts: [html: PostHogExampleWeb.Layouts]

import Plug.Conn
unquote(verified_routes())
end
end

def html do
quote do
use Phoenix.Component

import Phoenix.Controller, only: [get_csrf_token: 0]
unquote(verified_routes())
end
end

def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: PostHogExampleWeb.Endpoint,
router: PostHogExampleWeb.Router
end
end

@doc """
Dispatches to the appropriate helper (controller / html / router / ...).
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end
Loading
Loading