Alex Martsinovich

Software Engineer
Home Posts Github LinkedIn

PostHog Elixir SDK is good

The PostHog Elixir SDK 2.0 just dropped.

What is PostHog? Why should you care about its SDK? Why do I think it's good? Bear with me, I'm about to tell you how I spent my summer.

Dear Diary

In May 2025 I found myself with a lot of free time and an appetite for writing a logging library from scratch. How exactly I developed this appetite is a whole other story, but the bottom line is that I wasn't happy with how all existing logging libraries shared a lot of design decisions and workarounds, a lot of which were based on solvable problems or not relevant anymore.

And right when I was entertaining these thoughts, PostHog released its Error Tracking solution to open beta. This was a perfect opportunity to write a library.

So I bought a Claude Code subscription and one-shotted the whole thing in two days. Actually, no. I locked in and put a ton of effort into it. I learned everything I could about Elixir logging. Whenever I hit a roadblock, I would take a step back and fix it at the origin, whether it was in Plug, LoggerJSON or Elixir itself. I also documented everything and released it as an educational Hex package LoggerHandlerKit.

All of this resulted in LogHog – an okay library focused solely on PostHog Error Tracking.

It didn't take long for PostHog's very own Elixir champion Rafa Audibert to reach out. Naturally, error tracking would be a good addition to the official library. Things quickly got out of control though, and a couple of months later we have a new, completely overhauled version of the SDK on our hands. And, dear reader, I think it's good.

What to expect

So what is PostHog and why does it need an SDK? After all, the Elixir ecosystem is not big on SDKs. Just using Req is a great experience and sets a pretty high bar to clear for any bespoke library.

I'm glad you asked! Let me walk you through what PostHog brings to the table for an Elixir application.

Event Capture

I don't work for PostHog, so I'm going to tell you the truth. PostHog is a big ClickHouse cluster run by a team whose life goal is to see how many products they can build on top of it.

*slaps roof of clickhouse* this bad boy can fit so many products in it

The most important, core PostHog feature is the ability to send events to the server, where they will be accessible through an SQL-like query language. Each event can have properties attached to it. You can shove anything in there. The only required property is distinct_id, which identifies users, persons, or whatever you decide it does. Events happen to someone, after all.

Just emitting events is already great and is what we would call product analytics. But PostHog has made events even more useful! Some events have special meaning and experiences built for them. If you emit $exception_list events, they will be available in Error Tracking. Capture $ai_generation and you have LLM Analytics.

Events are very versatile, and PostHog is building more and more products every day.

Context

Sending analytics events is not as simple as sprinkling HTTP requests over your codebase. You want to instrument your events with properties and they originate in very different places. In order to note a user's IP address, you need to tap into your Endpoint. For tenant ID, look inside the authentication stack.

If we had to pipe all relevant information through all the layers of our app, we would go insane. This is why there is an established pattern in Elixir to use the process dictionary for that kind of metadata.

The PostHog SDK comes with a context mechanism to address exactly this. You can set PostHog context and it will be attached to events captured in the same process.

PostHog.set_context(%{distinct_id: "distinct_id_of_the_user"})
PostHog.capture("page_opened")

Batching

Capturing events is important, but usually not important enough to block your app code until the API request returns. That's why you want to offload sending them to some background task. Sure, Task.start is always there, but it would also be nice to batch events together...

PostHog SDK manages a pool of sender processes for you that automatically batch and send all the events you capture. So when you call PostHog.capture, it's merely a GenServer.cast under the hood.

Integrations

Some things are solved problems. PostHog gives special meaning to properties like $current_url or $host, and the best place to grab those is from the Plug.Conn struct. This knowledge is already baked into the SDK as PostHog.Integrations.Plug. All you need to do is add it to your pipeline.

For now this is the only integration, but it won't be long until there are more.

Product-specific API

Sometimes you just need a little helper. The feature flags product, for example, goes beyond simple event capture. The recommended flow to evaluate a feature flag is the following:

  1. Make a call to /flags endpoint
  2. Capture $feature_flag_called event
  3. Use $feature/<flag_name> property for all events captured for this user.

The SDK encapsulates all of this knowledge and exposes it as a single PostHog.FeatureFlags.check function.

Error Tracking

Finally, the part that was transferred from LogHog. PostHog comes with a logger handler that captures relevant log events and makes them accessible in PostHog Error Tracking.

I have to say, though, that unlike other PostHog products, Error Tracking requires language-specific backend support for the best experience, and Elixir support is not yet implemented. But it works, so give it a try!

Test Infrastructure

You want to test your analytic events, right? Right?

Well, you're in luck because the PostHog SDK does the right thing and uses NimbleOwnership to scope all captured events to their owner processes. Expect your async tests to remain async:

test "capture event" do
  PostHog.capture("event", %{distinct_id: "distinct_id"})

  assert [event] = PostHog.Test.all_captured()

  assert %{
            event: "event",
            distinct_id: "distinct_id",
            properties: %{},
            timestamp: _
          } = event
end

Conclusion

The new PostHog SDK is here and I think it's good. It was written by people who care about both Elixir and PostHog. It's fresh off the press and not battle-tested yet, so tread with caution, but if you're looking to capture events in your app, maybe give it a try.