---
title: "PostHog"
description: "Report experiment exposures to PostHog from your application, so you can compare the variations next to the events you already track there."
canonical_url: "https://docs.configdirector.com/integrations/analytics/posthog"
---
# PostHog

> Report experiment exposures to PostHog from your application, so you can compare the variations next to the events you already track there.

## Introduction

To compare an [experiment](https://docs.configdirector.com/getting-started/experiments)'s variations in PostHog, your application tells PostHog which variation each user saw. You add a handler once, and PostHog joins those exposures to the events you already send it.

Nothing passes through ConfigDirector, and ConfigDirector never holds your PostHog API key.

## What PostHog reads

PostHog's experiments count a user as exposed when it receives a [`$feature_flag_called` event](https://posthog.com/docs/experiments/exposures#what-is-an-exposure) with two properties:

<table>
<thead>
  <tr>
    <th>
      Property
    </th>
    
    <th>
      What you send
    </th>
    
    <th>
      Example
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        $feature_flag
      </code>
    </td>
    
    <td>
      The config key
    </td>
    
    <td>
      <code>
        checkout-cta-copy
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        $feature_flag_response
      </code>
    </td>
    
    <td>
      The variation's name
    </td>
    
    <td>
      <code>
        treatment-a
      </code>
    </td>
  </tr>
</tbody>
</table>

PostHog's SDKs normally send this event themselves when they read one of its own flags. Your handler sends it instead, because ConfigDirector serves the variation.

If you would rather use an event you already send, such as `$pageview`, PostHog also [accepts a custom exposure event](https://posthog.com/docs/experiments/running-experiments-without-feature-flags): choose it in the experiment's settings, and send the variation in a `$feature/checkout-cta-copy` property.

## Add the handler

Add a handler for the `configEvaluated` hook where you create the client, in the [browser](https://docs.configdirector.com/sdks/browser/javascript#hooks) or in [Node.js](https://docs.configdirector.com/sdks/server/node-js#hooks). The examples use PostHog's US host; on its EU cloud, use `https://eu.i.posthog.com`:

<code-group>

```ts [Browser]
import { createClient } from "@configdirector/client-sdk";
import posthog from "posthog-js";

posthog.init("YOUR-POSTHOG-PROJECT-API-KEY", { api_host: "https://us.i.posthog.com" });

export const client = createClient("YOUR-CLIENT-SDK-KEY", {
  hooks: {
    configEvaluated: ({ evaluation }) => {
      const variationName = evaluation.variation?.name;
      if (!variationName) return;

      posthog.capture("$feature_flag_called", {
        $feature_flag: evaluation.key,
        $feature_flag_response: variationName,
      });
    },
  },
});

await client.initialize({ id: "user-42" });
```

```ts [Node.js]
import { createClient } from "@configdirector/server-sdk";
import { PostHog } from "posthog-node";

const posthog = new PostHog("YOUR-POSTHOG-PROJECT-API-KEY", { host: "https://us.i.posthog.com" });

export const client = createClient("YOUR-SERVER-SDK-KEY", {
  hooks: {
    configEvaluated: ({ evaluation }) => {
      const variationName = evaluation.variation?.name;
      const userId = evaluation.context?.id;
      if (!variationName || !userId) return;

      posthog.capture({
        distinctId: userId,
        event: "$feature_flag_called",
        properties: {
          $feature_flag: evaluation.key,
          $feature_flag_response: variationName,
        },
      });
    },
  },
});

await client.initialize();
```

</code-group>

Only an experiment's evaluations carry a variation, so the check skips every other config, along with the in-code default value. It also skips a variation without a name, so [name each one](https://docs.configdirector.com/getting-started/experiments#create-the-experiment).

In the browser, PostHog attaches the exposure to the user you have identified to it. In Node.js, the handler passes the context's `id` as the `distinctId`, so it has to be the ID your other PostHog events use. A context without an `id` is skipped, since it does not take part in the split on a server.

The handler needs version 1.11.0 or later of the JavaScript SDK, or 1.13.0 or later of the Node.js SDK. React, Vue and React Native take the same handler in their `hooks`. The [Experiments guide](https://docs.configdirector.com/getting-started/experiments#send-exposures-to-your-analytics-tool) lists the hook for every other SDK.

## With OpenFeature

In an OpenFeature application, a hook does the same job, with the `variant` as the variation's name. Every config has a variant, not only experiments: on any other config it is ConfigDirector's ID for the value. So attach the hook to the reads of your experiments instead of registering it for every read:

<code-group>

```ts [Web]
import { OpenFeature, type Hook } from "@openfeature/web-sdk";
import posthog from "posthog-js";

posthog.init("YOUR-POSTHOG-PROJECT-API-KEY", { api_host: "https://us.i.posthog.com" });

const reportExposure: Hook = {
  after: (hookContext, details) => {
    if (!details.variant) return;

    posthog.capture("$feature_flag_called", {
      $feature_flag: hookContext.flagKey,
      $feature_flag_response: details.variant,
    });
  },
};

const client = OpenFeature.getClient();

const buttonLabel = client.getStringValue("checkout-cta-copy", "Start free trial", {
  hooks: [reportExposure],
});
```

```ts [Node.js]
import { OpenFeature, type Hook } from "@openfeature/server-sdk";
import { PostHog } from "posthog-node";

const posthog = new PostHog("YOUR-POSTHOG-PROJECT-API-KEY", { host: "https://us.i.posthog.com" });

const reportExposure: Hook = {
  after: (hookContext, details) => {
    const userId = hookContext.context.targetingKey;
    if (!details.variant || !userId) return;

    posthog.capture({
      distinctId: userId,
      event: "$feature_flag_called",
      properties: {
        $feature_flag: hookContext.flagKey,
        $feature_flag_response: details.variant,
      },
    });
  },
};

const client = OpenFeature.getClient();

const buttonLabel = await client.getStringValue(
  "checkout-cta-copy",
  "Start free trial",
  { targetingKey: "user-42" },
  { hooks: [reportExposure] },
);
```

</code-group>

A variation without a name also has ConfigDirector's ID for the value as its variant, so name each one. The hook needs version 1.11.0 or later of the [Web provider](https://docs.configdirector.com/sdks/openfeature/web#evaluation-details-and-hooks), or 1.13.0 or later of the [Node.js provider](https://docs.configdirector.com/sdks/openfeature/node-js#evaluation-details-and-hooks).

## When the handler runs

The handler runs once per evaluation, whenever your application reads the config. So read an experiment where the user reaches the change rather than at startup, or users who never see it count as exposed.

A config read on every render sends an event each time. That does not skew the results, because PostHog [counts one exposure per user per variant](https://posthog.com/docs/experiments/exposures#exposure-deduplication). PostHog's [billing page](https://posthog.com/docs/billing/common-questions) lists `$feature_flag_called` as not billed.

## Compare the variations

The outcomes you compare are the events your application already sends to PostHog, so there is nothing more to add to your code. PostHog [counts an event](https://posthog.com/docs/experiments/exposures#metric-events-dont-need-a-variant-property) only if it happens after the user's first exposure, and it attributes the event to that user's variant.

[Create an experiment in PostHog](https://posthog.com/docs/experiments/creating-an-experiment) that matches yours, because PostHog [compares these values exactly](https://posthog.com/docs/experiments/running-experiments-without-feature-flags#common-issues), including capitalization:

- Set its feature flag key to your config key. PostHog's flag keys allow only letters, numbers, hyphens and underscores, so this does not work for a config key with a dot in it.
- Name its variants after your variations. PostHog treats a variant named `control` as the baseline, or else the first one.

PostHog creates a feature flag for the experiment, which your application never reads, since ConfigDirector still decides who gets which variation.

The handler also runs outside the split: before the run starts, after you ship and when a conditional rule serves a variation. So launch the PostHog experiment after you start the run and end it when the run ends. Otherwise a user served one variation before the run and another during it counts as [exposed to both](https://posthog.com/docs/experiments/running-experiments-without-feature-flags#multiple-variant-handling), and PostHog leaves them out of the analysis by default. A conditional rule above the split can still make PostHog's numbers higher than the **Exposures** card, which counts only the contexts the split served.

Keep each variation's name for the length of the run, because PostHog compares against the variant names you set.

To mark each run's start, split change, ship and stop on these charts without code, set up a [PostHog integration](https://docs.configdirector.com/integrations/analytics-integrations#posthog).


## Sitemap

See the full [sitemap](https://docs.configdirector.com/sitemap.md) for all pages.
