Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7980c65
Add tests using v4 response
haacked Apr 16, 2025
438a5c4
Add array payload tests
haacked Apr 16, 2025
e31282f
Normalize v4 response to v3 format
haacked Apr 16, 2025
0d5d40c
Rename `value` to `payload`.
haacked Apr 16, 2025
95a0e21
Extract method that provides access to the decide response
haacked Apr 16, 2025
9b25c99
Add `_decide_request` method
haacked Apr 16, 2025
1298008
Update test names
haacked Apr 16, 2025
294d126
Remove api version from non-decide requests
haacked Apr 17, 2025
42040e5
Add test for $feature_flag_called event
haacked Apr 17, 2025
48b66ea
Capture `$feature_flag_called` event
haacked Apr 17, 2025
34742be
Make sure we don't send event when send_feature_flag_event is false
haacked Apr 17, 2025
0d45209
Pass id, version, and reason to `$feature_flag_called` events
haacked Apr 17, 2025
4a98477
Add feature_flag_request_id to the `$feature_flag_called` event.
haacked Apr 17, 2025
9184746
Actually request decide v=4
haacked Apr 17, 2025
856c44d
De-duplicate $feature_flag_called events
haacked Apr 17, 2025
da80228
Add a script to format code
haacked Apr 17, 2025
d358cde
Format using older elixir
haacked Apr 17, 2025
f78b873
Stop using `Process`
haacked Apr 17, 2025
e6fd9a7
Replace Process with GenServer
haacked Apr 17, 2025
6ad6aa2
Change to LRU
haacked Apr 17, 2025
6b11bf9
Only format code with latest elixir
haacked Apr 17, 2025
eec1890
chore: Use more modern Elixir/Erlang locally
rafaeelaudibert Apr 17, 2025
c910c5e
test: Fix doctest complaining after more recent Elixir types
rafaeelaudibert Apr 17, 2025
800a0d6
chore: Update CI matrix
rafaeelaudibert Apr 17, 2025
794aeb1
chore: Remove Elixir 1.14.x + OTP 23.x pair
rafaeelaudibert Apr 17, 2025
6d13b6e
feat: Add docs around initing our `Posthog.Application`
rafaeelaudibert Apr 17, 2025
2d9d7b1
refactor: Improve `send_feature_flag_called_event` handling
rafaeelaudibert Apr 17, 2025
ee4d864
chore: Bump to Cachex v4+
rafaeelaudibert Apr 17, 2025
820a5f6
chore: Update demo with new deps
rafaeelaudibert Apr 17, 2025
f8c0a68
Update setup instructions for the sample project
haacked Apr 17, 2025
6f69a2d
Fix sample project to run
haacked Apr 17, 2025
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
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,13 @@ Check specific feature flag:
```elixir
# Boolean feature flag
{:ok, flag} = Posthog.feature_flag("new-dashboard", "user_123")
# Returns: %Posthog.FeatureFlag{name: "new-dashboard", value: true, enabled: true}
# Returns: %Posthog.FeatureFlag{name: "new-dashboard", payload: true, enabled: true}

# Multivariate feature flag
{:ok, flag} = Posthog.feature_flag("pricing-test", "user_123")
# Returns: %Posthog.FeatureFlag{
# name: "pricing-test",
# value: %{"price" => 99, "period" => "monthly"},
# payload: %{"price" => 99, "period" => "monthly"},
# enabled: "variant-a"
# }

Expand Down Expand Up @@ -158,6 +158,12 @@ bin/test

(This runs `mix test`).

Format code:

```sh
bin/fmt
```

### Troubleshooting

If you encounter WX library issues during Erlang installation:
Expand Down
7 changes: 7 additions & 0 deletions bin/fmt
Original file line number Diff line number Diff line change
@@ -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 format
2 changes: 1 addition & 1 deletion examples/feature_flag_demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ If the feature flag is enabled:

```bash
Feature flag 'your-feature-flag' is ENABLED
Value: true
Payload: true
```

If the feature flag is disabled:
Expand Down
4 changes: 2 additions & 2 deletions examples/feature_flag_demo/lib/feature_flag_demo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ defmodule FeatureFlagDemo do
group_properties: group_properties,
person_properties: person_properties
) do
{:ok, %{enabled: true, value: value}} ->
{:ok, %{enabled: true, payload: payload}} ->
IO.puts("Feature flag '#{flag}' is ENABLED")
IO.puts("Value: #{inspect(value)}")
IO.puts("Payload: #{inspect(payload)}")

{:ok, %{enabled: false}} ->
IO.puts("Feature flag '#{flag}' is DISABLED")
Expand Down
56 changes: 49 additions & 7 deletions lib/posthog.ex
Original file line number Diff line number Diff line change
Expand Up @@ -185,22 +185,64 @@ defmodule Posthog do

# Boolean feature flag
{:ok, flag} = Posthog.feature_flag("new-dashboard", "user_123")
# Returns: %Posthog.FeatureFlag{name: "new-dashboard", value: true, enabled: true}
# Returns: %Posthog.FeatureFlag{name: "new-dashboard", payload: true, enabled: true}

# Multivariate feature flag
{:ok, flag} = Posthog.feature_flag("pricing-test", "user_123")
# Returns: %Posthog.FeatureFlag{
# name: "pricing-test",
# value: %{"price" => 99, "period" => "monthly"},
# payload: %{"price" => 99, "period" => "monthly"},
# enabled: "variant-a"
# }
"""
@spec feature_flag(binary(), binary(), keyword()) :: result()
@spec feature_flag(binary(), binary(), Client.feature_flag_opts()) :: result()
def feature_flag(flag, distinct_id, opts \\ []) do
with {:ok, %{feature_flags: flags, feature_flag_payloads: feature_flag_payloads}} <-
feature_flags(distinct_id, opts),
enabled when not is_nil(enabled) <- flags[flag] do
{:ok, FeatureFlag.new(flag, enabled, Map.get(feature_flag_payloads, flag))}
with {:ok, response} <- Client._decide_request(distinct_id, opts),
enabled when not is_nil(enabled) <- response.feature_flags[flag] do
# Only capture if send_feature_flag_event is true (default)
if Keyword.get(opts, :send_feature_flag_event, true) do
# Create a unique key for this distinct_id and flag combination
cache_key = {:feature_flag_called, distinct_id, flag}

# Check if we've seen this combination before using Cachex
case Cachex.exists?(:posthog_feature_flag_cache, cache_key) do
{:ok, false} ->
properties = %{
"distinct_id" => distinct_id,
"$feature_flag" => flag,
"$feature_flag_response" => enabled
}

properties =
if Map.has_key?(response, :flags) do
Map.merge(properties, %{
"$feature_flag_id" => response.flags[flag]["metadata"]["id"],
"$feature_flag_version" => response.flags[flag]["metadata"]["version"],
"$feature_flag_reason" => response.flags[flag]["reason"]["description"]
})
else
properties
end

properties =
if Map.get(response, :request_id) do
Map.put(properties, "$feature_flag_request_id", response.request_id)
else
properties
end

Client.capture("$feature_flag_called", properties, [])

# Add new entry to cache using Cachex
Cachex.put(:posthog_feature_flag_cache, cache_key, true)

{:ok, true} ->
# Entry exists, no need to do anything
:ok
end
end

{:ok, FeatureFlag.new(flag, enabled, Map.get(response.feature_flag_payloads, flag))}
else
{:error, _} = err -> err
nil -> {:error, :not_found}
Expand Down
16 changes: 16 additions & 0 deletions lib/posthog/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule Posthog.Application do
@moduledoc false

use Application

def start(_type, _args) do
children = [
# Start Cachex for feature flag event deduplication.
# The 50,000 entries limit is the same used for posthog-python, but otherwise arbitrary.
{Cachex, name: :posthog_feature_flag_cache, limit: 50_000, policy: Cachex.Policy.LRW}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this make sense? Do we not wanna send $feature_flag_called in case we have this in the cache? This lasts across requests - it's a separate process.

Wont this mean that users with less than 50000 clients wont ever have more than 50000 FFs charged to them? (Users can make these supervisors survive servers restarts, Elixir is crazy)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's the combination of distinct_id and feature_flag. So yeah, if they only have 1 flag and less than 50,000 distinct users, they'd never be charged for more than 50,000.

I took this approach from posthog-python. Many of the other clients do something similar. It seems to me that we should put a time limit on each cache entry. Something like one minute. @dmarticus, do you have context on why we do this in posthog-python?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the hash includes the feature_flag too, so it's a slightly smaller problem.
The problem is that servers probably restart pretty often in Python; that isn't necessarily true in Elixir: you can keep the Cachex process running indefinitely, so... it might fill the cache pretty quickly.

Will leave that decision up to y'all, just letting you know the quirkyness of working with a distributed language :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other thing to consider, we don't charge based on the number of $feature_flag_called events, we charge based on the number of /decide calls made. So this cache doesn't affect the cost. Just reporting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't know that. In that case, I'm fine with keeping it this way. I'm working on improving the setup, I don't think the LRU we added actually works how we think, expect changes to this PR through the day.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one thing I worry about with such a big cache is people being confused why they don't see these events. My gut tells me the reason for this is if they're calling a feature flag in a tight loop, we don't want 10 of the same events all of a sudden.

So I'd suggest we add a default_ttl to the Cachex declaration. Something like 5 minutes would be fine. The 50,000 limit then just ensures we don't ever take up too much memory.

]

opts = [strategy: :one_for_one, name: Posthog.Supervisor]
Supervisor.start_link(children, opts)
end
end
71 changes: 58 additions & 13 deletions lib/posthog/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ defmodule Posthog.Client do
timestamp: timestamp()
]

@typedoc """
Feature flag specific options that should not be passed to capture events.

* `:send_feature_flag_event` - Whether to capture the `$feature_flag_called` event (default: true)
"""
@type feature_flag_opts :: [
send_feature_flag_event: boolean() | opts()
]

@lib_version Mix.Project.config()[:version]
@lib_name "posthog-elixir"

Expand Down Expand Up @@ -206,18 +215,60 @@ defmodule Posthog.Client do
@spec feature_flags(binary(), opts()) ::
{:ok, Posthog.FeatureFlag.flag_response()} | {:error, response() | term()}
def feature_flags(distinct_id, opts) do
case _decide_request(distinct_id, opts) do
{:ok, response} ->
{:ok,
%{
feature_flags: response.feature_flags,
feature_flag_payloads: response.feature_flag_payloads
}}

err ->
err
end
end

@doc false
def _decide_request(distinct_id, opts) do
body =
opts
|> Keyword.take(~w[groups group_properties person_properties]a)
|> Enum.reduce(%{distinct_id: distinct_id}, fn {k, v}, map -> Map.put(map, k, v) end)

case post!("/decide", body, headers(opts[:headers])) do
case post!("/decide?v=4", body, headers(opts[:headers])) do
{:ok, %{body: body}} ->
{:ok,
%{
feature_flags: Map.get(body, "featureFlags", %{}),
feature_flag_payloads: decode_feature_flag_payloads(body)
}}
if Map.has_key?(body, "flags") do
flags = body["flags"]

feature_flags =
Map.new(flags, fn {k, v} ->
{k, if(v["variant"], do: v["variant"], else: v["enabled"])}
end)

feature_flag_payloads =
Map.new(flags, fn {k, v} ->
{k,
if(v["metadata"]["payload"],
do: decode_feature_flag_payload(v["metadata"]["payload"]),
else: nil
)}
end)

{:ok,
%{
flags: flags,
feature_flags: feature_flags,
feature_flag_payloads: feature_flag_payloads,
request_id: body["requestId"]
}}
else
{:ok,
%{
feature_flags: Map.get(body, "featureFlags", %{}),
feature_flag_payloads: decode_feature_flag_payloads(body),
request_id: body["requestId"]
}}
end

err ->
err
Expand Down Expand Up @@ -268,7 +319,7 @@ defmodule Posthog.Client do
|> Map.put(:api_key, api_key())
|> encode(json_library())

url = api_url() <> path <> "?v=#{api_version()}"
url = api_url() <> path

:hackney.post(url, headers, body, [])
|> handle()
Expand Down Expand Up @@ -350,12 +401,6 @@ defmodule Posthog.Client do
Application.get_env(@app, :json_library, Jason)
end

@doc false
@spec api_version() :: pos_integer()
defp api_version do
Application.get_env(@app, :version, 3)
end

@doc false
@spec lib_properties() :: map()
defp lib_properties do
Expand Down
18 changes: 9 additions & 9 deletions lib/posthog/feature_flag.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@ defmodule Posthog.FeatureFlag do

The `FeatureFlag` struct contains:
* `name` - The name of the feature flag
* `value` - The payload value associated with the flag (can be any term)
* `payload` - The payload value associated with the flag (can be any term)
* `enabled` - The evaluation result (boolean for on/off flags, string for multivariate flags)

## Examples

# Boolean feature flag
%Posthog.FeatureFlag{
name: "new-dashboard",
value: true,
payload: true,
enabled: true
}

# Multivariate feature flag
%Posthog.FeatureFlag{
name: "pricing-test",
value: %{"price" => 99, "period" => "monthly"},
payload: %{"price" => 99, "period" => "monthly"},
enabled: "variant-a"
}
"""
defstruct [:name, :value, :enabled]
defstruct [:name, :payload, :enabled]

@typedoc """
Represents the enabled state of a feature flag.
Expand All @@ -55,12 +55,12 @@ defmodule Posthog.FeatureFlag do

Fields:
* `name` - The name of the feature flag (string)
* `value` - The payload value associated with the flag (any term)
* `payload` - The payload value associated with the flag (any term)
* `enabled` - The evaluation result (boolean or string)
"""
@type t :: %__MODULE__{
name: binary(),
value: term(),
payload: term(),
enabled: variant()
}

Expand All @@ -71,7 +71,7 @@ defmodule Posthog.FeatureFlag do

* `name` - The name of the feature flag
* `enabled` - The evaluation result (boolean or string)
* `value` - The payload value associated with the flag
* `payload` - The payload value associated with the flag

## Examples

Expand All @@ -83,8 +83,8 @@ defmodule Posthog.FeatureFlag do
%{"price" => 99, "period" => "monthly"})
"""
@spec new(binary(), variant(), term()) :: t()
def new(name, enabled, value) do
struct!(__MODULE__, name: name, enabled: enabled, value: value)
def new(name, enabled, payload) do
struct!(__MODULE__, name: name, enabled: enabled, payload: payload)
end

@doc """
Expand Down
6 changes: 4 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ defmodule Posthog.MixProject do

def application do
[
extra_applications: [:logger]
extra_applications: [:logger],
mod: {Posthog.Application, []}
]
end

Expand Down Expand Up @@ -58,7 +59,8 @@ defmodule Posthog.MixProject do
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:hackney, "~> 1.20"},
{:jason, "~> 1.4", optional: true},
{:mimic, "~> 1.11", only: :test}
{:mimic, "~> 1.11", only: :test},
{:cachex, "~> 3.6"}
]
end
end
5 changes: 5 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
%{
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.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", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"ham": {:hex, :ham, "0.3.0", "7cd031b4a55fba219c11553e7b13ba73bd86eab4034518445eff1e038cb9a44d", [:mix], [], "hexpm", "7d6c6b73d7a6a83233876cc1b06a4d9b5de05562b228effda4532f9a49852bf6"},
"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"},
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
Expand All @@ -16,6 +19,8 @@
"mimic": {:hex, :mimic, "1.11.0", "49b126687520b6e179acab305068ad7d72bfea8abe94908a6c0c8ca0a5b7bdc7", [:mix], [{:ham, "~> 0.2", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "8b16b1809ca947cffbaede146cd42da8c1c326af67a84b59b01c204d54e4f1a2"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
"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"},
"unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
}
Loading
Loading