Repository: PostHog/posthog-elixir
Pull request #21: chore: Add some scripts and a test console app
Author: login: haacked
Created: 2025-04-16T20:42:38Z | Merged: 2025-04-16T21:33:11Z

See RFC https://github.com/PostHog/meta/pull/309/files for some context on these scripts.

The app makes it easier to test some of the changes we're making to the library.

No production changes in this PR


Diff:
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
index 0e1687d..7b1fd6e 100644
--- a/.github/workflows/cd.yml
+++ b/.github/workflows/cd.yml
@@ -7,7 +7,7 @@ on:
 jobs:
   label-version-bump:
     name: Bump version based on PR label
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
     if: |
       github.event.pull_request.merged
       && (
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a17f4d8..cdc2aa7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ on:
 jobs:
   test:
     name: Build and test
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
 
     strategy:
       matrix:
diff --git a/.gitignore b/.gitignore
index 4105090..7c288a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,4 +24,7 @@ posthog-*.tar
 
 
 # Ignore .env used for local development
-.env
\ No newline at end of file
+.env
+
+# Ignore .DS_Store
+.DS_Store
diff --git a/README.md b/README.md
index f03857a..0fd5061 100644
--- a/README.md
+++ b/README.md
@@ -137,6 +137,8 @@ Posthog.feature_flags("user_123",
 
 ## Local Development
 
+Run `bin/setup` to install development dependencies or run the following commands manually:
+
 We recommend using `asdf` to manage Elixir and Erlang versions:
 
 ```sh
@@ -148,6 +150,14 @@ mix deps.get
 mix compile
 ```
 
+Run tests:
+
+```sh
+bin/test
+```
+
+(This runs `mix test`).
+
 ### Troubleshooting
 
 If you encounter WX library issues during Erlang installation:
@@ -160,6 +170,10 @@ asdf install
 
 To persist this setting, add it to your shell configuration file (`~/.bashrc`, `~/.zshrc`, or `~/.profile`).
 
+## Examples
+
+There's an example console project in `examples/feature_flag_demo` that shows how to use the client. Follow the instructions in [the README](examples/feature_flag_demo/README.md) to run it.
+
 ## Contributing
 
 Contributions are welcome! Please feel free to submit a Pull Request.
diff --git a/bin/helpers/_utils.sh b/bin/helpers/_utils.sh
new file mode 100644
index 0000000..30fc468
--- /dev/null
+++ b/bin/helpers/_utils.sh
@@ -0,0 +1,15 @@
+error() {
+    echo "$@" >&2
+}
+
+fatal() {
+    error "$@"
+    exit 1
+}
+
+set_source_and_root_dir() {
+    { set +x; } 2>/dev/null
+    source_dir="$( cd -P "$( dirname "$0" )" >/dev/null 2>&1 && pwd )"
+    root_dir=$(cd $source_dir && cd ../ && pwd)
+    cd $root_dir
+}
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..a97aa94
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+#/ Usage: bin/setup
+#/ Description: Sets up the dependencies needed to develop this project
+source bin/helpers/_utils.sh
+set_source_and_root_dir
+
+if ! command -v asdf &> /dev/null; then
+    brew install asdf
+fi
+
+asdf plugin add erlang
+asdf plugin add elixir
+asdf install
+
+mix deps.get
+mix compile
diff --git a/bin/test b/bin/test
new file mode 100755
index 0000000..f0d6505
--- /dev/null
+++ b/bin/test
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+#/ Usage: bin/test
+#/ Description: Runs all the unit tests for this project
+source bin/helpers/_utils.sh
+set_source_and_root_dir
+
+mix test
\ No newline at end of file
diff --git a/examples/.gitignore b/examples/.gitignore
new file mode 100644
index 0000000..41b44f5
--- /dev/null
+++ b/examples/.gitignore
@@ -0,0 +1,31 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+**/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+**/cover/
+
+# The directory Mix downloads your dependencies sources to.
+**/deps/
+
+# Where third-party dependencies like ExDoc output generated docs.
+**/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+**/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+**/*.tar
+
+
+# Ignore .env used for local development
+.env
+
+# Ignore .DS_Store
+.DS_Store
diff --git a/examples/feature_flag_demo/README.md b/examples/feature_flag_demo/README.md
new file mode 100644
index 0000000..22ba68a
--- /dev/null
+++ b/examples/feature_flag_demo/README.md
@@ -0,0 +1,50 @@
+# PostHog Feature Flag Demo
+
+A simple console application that demonstrates how to use PostHog's feature flag functionality.
+
+## Setup
+
+1. Make sure you have the required environment variables set:
+
+```bash
+export POSTHOG_API_KEY="your_project_api_key"
+export POSTHOG_API_URL="https://app.posthog.com"  # or your self-hosted instance
+```
+
+2. Install dependencies:
+
+```bash
+mix deps.get
+```
+
+## Usage
+
+Basic usage:
+
+```bash
+mix run run.exs --flag "your-feature-flag" --distinct-id "user123"
+```
+
+With group properties:
+
+```bash
+mix run run.exs --flag "your-feature-flag" --distinct-id "user123" \
+  --groups '{"company": "company123"}' \
+  --group_properties '{"company": {"industry": "tech"}}' \
+  --person_properties '{"email": "user@example.com"}'
+```
+
+## Example Output
+
+If the feature flag is enabled:
+
+```bash
+Feature flag 'your-feature-flag' is ENABLED
+Value: true
+```
+
+If the feature flag is disabled:
+
+```bash
+Feature flag 'your-feature-flag' is DISABLED
+```
diff --git a/examples/feature_flag_demo/config/config.exs b/examples/feature_flag_demo/config/config.exs
new file mode 100644
index 0000000..8ef2483
--- /dev/null
+++ b/examples/feature_flag_demo/config/config.exs
@@ -0,0 +1,9 @@
+import Config
+
+# Remove trailing slash from API URL if present
+api_url = System.get_env("POSTHOG_API_URL", "https://app.posthog.com")
+api_url = if String.ends_with?(api_url, "/"), do: String.slice(api_url, 0..-2), else: api_url
+
+config :posthog,
+  api_url: api_url,
+  api_key: System.get_env("POSTHOG_API_KEY")
diff --git a/examples/feature_flag_demo/lib/feature_flag_demo.ex b/examples/feature_flag_demo/lib/feature_flag_demo.ex
new file mode 100644
index 0000000..6e68087
--- /dev/null
+++ b/examples/feature_flag_demo/lib/feature_flag_demo.ex
@@ -0,0 +1,112 @@
+defmodule FeatureFlagDemo do
+  @moduledoc """
+  A simple console application to demonstrate PostHog feature flag functionality.
+  """
+
+  def main(args) do
+    # Print configuration for debugging
+    api_url = System.get_env("POSTHOG_API_URL", "https://app.posthog.com")
+    api_key = System.get_env("POSTHOG_API_KEY")
+
+    IO.puts("Using API URL: #{api_url}")
+    IO.puts("Using API Key: #{String.slice(api_key || "", 0, 8)}...") # Only show first 8 chars of key for security
+
+    args
+    |> parse_args()
+    |> process()
+  end
+
+  defp parse_args(args) do
+    {opts, _, _} = OptionParser.parse(args,
+      switches: [
+        flag: :string,
+        distinct_id: :string,
+        groups: :string,
+        group_properties: :string,
+        person_properties: :string
+      ],
+      aliases: [
+        d: :distinct_id
+      ]
+    )
+    opts
+  end
+
+  defp process([]) do
+    IO.puts("""
+    Usage: mix run run.exs --flag FLAG_NAME --distinct-id USER_ID [options]
+
+    Options:
+      --flag FLAG_NAME              The name of the feature flag to check
+      --distinct-id USER_ID         The distinct ID of the user
+      --groups GROUPS               JSON string of group properties (optional)
+      --group_properties PROPERTIES JSON string of group properties (optional)
+      --person_properties PROPERTIES JSON string of person properties (optional)
+    """)
+  end
+
+  defp process(opts) do
+    flag = Keyword.get(opts, :flag)
+    distinct_id = Keyword.get(opts, :distinct_id)
+
+    if is_nil(flag) or is_nil(distinct_id) do
+      IO.puts("Error: --flag and --distinct-id are required")
+      process([])
+    else
+      check_feature_flag(flag, distinct_id, opts)
+    end
+  end
+
+  defp check_feature_flag(flag, distinct_id, opts) do
+    groups = parse_json(Keyword.get(opts, :groups))
+    group_properties = parse_json(Keyword.get(opts, :group_properties))
+    person_properties = parse_json(Keyword.get(opts, :person_properties))
+
+    IO.puts("Checking feature flag '#{flag}' for user '#{distinct_id}'...")
+
+    case Posthog.feature_flag(flag, distinct_id,
+           groups: groups,
+           group_properties: group_properties,
+           person_properties: person_properties
+         ) do
+      {:ok, %{enabled: true, value: value}} ->
+        IO.puts("Feature flag '#{flag}' is ENABLED")
+        IO.puts("Value: #{inspect(value)}")
+
+      {:ok, %{enabled: false}} ->
+        IO.puts("Feature flag '#{flag}' is DISABLED")
+
+      {:error, %{status: 403}} ->
+        IO.puts("""
+        Error: Authentication failed (403 Forbidden)
+
+        Please check that:
+        1. Your POSTHOG_API_KEY is set correctly
+        2. Your POSTHOG_API_URL is set correctly (if using a self-hosted instance)
+        3. The API key has the necessary permissions
+        4. Your local PostHog instance is running and accessible
+
+        You can set these environment variables:
+        export POSTHOG_API_KEY="your_project_api_key"
+        export POSTHOG_API_URL="http://localhost:8000"  # Note: no trailing slash
+        """)
+
+      {:error, %{status: status, body: body}} ->
+        IO.puts("""
+        Error: Received status #{status}
+        Response body: #{inspect(body)}
+        """)
+
+      {:error, reason} ->
+        IO.puts("Error checking feature flag: #{inspect(reason)}")
+    end
+  end
+
+  defp parse_json(nil), do: %{}
+  defp parse_json(json) when is_binary(json) do
+    case Jason.decode(json) do
+      {:ok, result} -> result
+      {:error, _} -> %{}
+    end
+  end
+end
diff --git a/examples/feature_flag_demo/mix.exs b/examples/feature_flag_demo/mix.exs
new file mode 100644
index 0000000..f752617
--- /dev/null
+++ b/examples/feature_flag_demo/mix.exs
@@ -0,0 +1,26 @@
+defmodule FeatureFlagDemo.MixProject do
+  use Mix.Project
+
+  def project do
+    [
+      app: :feature_flag_demo,
+      version: "0.1.0",
+      elixir: "~> 1.16",
+      start_permanent: Mix.env() == :prod,
+      deps: deps()
+    ]
+  end
+
+  def application do
+    [
+      extra_applications: [:logger]
+    ]
+  end
+
+  defp deps do
+    [
+      {:posthog, path: "../.."},
+      {:jason, "~> 1.4"}
+    ]
+  end
+end 
\ No newline at end of file
diff --git a/examples/feature_flag_demo/mix.lock b/examples/feature_flag_demo/mix.lock
new file mode 100644
index 0000000..4b1b50c
--- /dev/null
+++ b/examples/feature_flag_demo/mix.lock
@@ -0,0 +1,11 @@
+%{
+  "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"},
+  "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"},
+  "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
+  "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
+  "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
+  "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
+  "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
+  "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
+  "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
+}
diff --git a/examples/feature_flag_demo/run.exs b/examples/feature_flag_demo/run.exs
new file mode 100644
index 0000000..9bf00cd
--- /dev/null
+++ b/examples/feature_flag_demo/run.exs
@@ -0,0 +1 @@
+FeatureFlagDemo.main(System.argv()) 
\ No newline at end of file


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 9 out of 14 changed files in this pull request and generated no comments.

<details>
<summary>Files not reviewed (5)</summary>

* **examples/.gitignore**: Language not supported
* **examples/feature_flag_demo/config/config.exs**: Language not supported
* **examples/feature_flag_demo/lib/feature_flag_demo.ex**: Language not supported
* **examples/feature_flag_demo/mix.exs**: Language not supported
* **examples/feature_flag_demo/run.exs**: Language not supported
</details>



state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T20:54:33Z
user: login: rafaeelaudibert
body: Lgtm, but there's some references to C#, mind fixing?
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-16T21:06:06Z
user: login: rafaeelaudibert
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-16T21:21:04Z

Review comments:
diff_hunk: @@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+#/ Usage: bin/test [<configuration>]
+#/ Description: Runs all the tests for the target frameworks: netstandard2.1 and net8.0
path: bin/setup
user: login: rafaeelaudibert
body: Incorectly referencing C# here
created_at: 2025-04-16T21:03:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+#/ Usage: bin/test [<configuration>]
+#/ Description: Runs all the tests for the target frameworks: netstandard2.1 and net8.0
path: bin/test
user: login: rafaeelaudibert
body: Same here
created_at: 2025-04-16T21:04:17Z
author_association: MEMBER

PR comments:
user: login: haacked
created_at: 2025-04-16T21:12:56Z
body: > Lgtm, but there's some references to C#, mind fixing?

Fixed! That's what I get for my copy pasta sins.
author_association: CONTRIBUTOR
user: login: rafaeelaudibert
created_at: 2025-04-16T21:20:52Z
body: > > Lgtm, but there's some references to C#, mind fixing?
> 
> Fixed! That's what I get for my copy pasta sins.

You're forgiven because I got my copy pasta tshirt merch this Monday and it's so so cool
author_association: MEMBER