Skip to content

Commit c1f9a59

Browse files
committed
initial commit
right now, all that happens is the genserver pulls configs at startup, stores them in ETS, and reloads them every 60s
0 parents  commit c1f9a59

File tree

10 files changed

+215
-0
lines changed

10 files changed

+215
-0
lines changed

.formatter.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
statsig_ex-*.tar
24+

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# StatsigEx
2+
3+
**This is very much a work in progress**
4+
5+
I want to see how quickly we can replicate the funcationality in
6+
Statsig's erlang client and fix some of the issues they claim it has

config/config.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for
9+
# third-party users, it should be done in your "mix.exs" file.
10+
11+
# You can configure your application as:
12+
#
13+
# config :statsig_ex, key: :value
14+
#
15+
# and access this configuration in your application as:
16+
#
17+
# Application.get_env(:statsig_ex, :key)
18+
#
19+
# You can also configure a third-party app:
20+
#
21+
# config :logger, level: :info
22+
#
23+
24+
# It is also possible to import configuration files, relative to this
25+
# directory. For example, you can emulate configuration per environment
26+
# by uncommenting the line below and defining dev.exs, test.exs and such.
27+
# Configuration from the imported file will override the ones defined
28+
# here (which is why it is important to import them last).
29+
#
30+
# import_config "#{Mix.env()}.exs"

lib/statsig_ex.ex

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
defmodule StatsigEx do
2+
use GenServer
3+
4+
def start_link(opts \\ []) do
5+
# should pull from an env var here
6+
opts = Keyword.put_new(opts, :api_key, {:env, "STATSIG_API_KEY"})
7+
8+
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
9+
end
10+
11+
def init(opts) do
12+
:ets.new(ets_name(), [:named_table])
13+
14+
state = %{
15+
api_key: get_api_key(Keyword.fetch!(opts, :api_key)),
16+
last_sync: 0,
17+
events: []
18+
}
19+
20+
{:ok, last_sync} = reload_configs(state.api_key, state.last_sync)
21+
22+
# reload every 60s
23+
Process.send_after(self(), :reload, 60_000)
24+
{:ok, Map.put(state, :last_sync, last_sync)}
25+
end
26+
27+
def state, do: GenServer.call(__MODULE__, :state)
28+
29+
def ets_name, do: :statsig_ex_store
30+
31+
# for debugging
32+
def handle_call(:state, _from, state) do
33+
{:reply, state, state}
34+
end
35+
36+
def handle_call({:log, event}, _from, state) do
37+
# for now, just throw things in there, don't worry about the shape
38+
{:reply, :ok, Map.put(state, :events, [event | state.events])}
39+
end
40+
41+
def handle_info(:reload, %{api_key: key, last_sync: time} = state) do
42+
{:ok, sync_time} = reload_configs(key, time)
43+
IO.puts("reloading!")
44+
Process.send_after(self(), :reload, 60_000)
45+
{:noreply, Map.put(state, :last_sync, sync_time)}
46+
end
47+
48+
def handle_info(:flush, %{api_key: key, events: events} = state) do
49+
remaining = flush_events(key, events)
50+
{:noreply, Map.put(state, :events, remaining)}
51+
end
52+
53+
defp reload_configs(api_key, since) do
54+
# call Statsig API to get configs (eventually we can make the http client configurable)
55+
{:ok, resp} =
56+
HTTPoison.get(
57+
"https://statsigapi.net/v1/download_config_specs?sinceTime=#{since}",
58+
[{"STATSIG-API-KEY", api_key}, {"Content-Type", "application/json"}]
59+
)
60+
61+
# should probably crash on startup but be resilient on reload; will fix later
62+
config = Jason.decode!(resp.body)
63+
config |> Map.get("feature_gates", []) |> save_configs(:gate)
64+
config |> Map.get("dynamic_configs", []) |> save_configs(:config)
65+
66+
# return the time of this last fetch
67+
{:ok, Map.get(config, "time", since)}
68+
end
69+
70+
def flush_events(_key, []), do: []
71+
72+
def flush_events(key, events) do
73+
# send in batches; keep any that fail
74+
events
75+
|> Enum.chunk_every(500)
76+
|> Enum.reduce([], fn chunk, unsent ->
77+
# this probably doesn't work yet, but I'm not really worried about it right now
78+
{:ok, _resp} =
79+
HTTPoison.post(
80+
"https://statsigapi.net/v1/rgstr",
81+
Jason.encode!(%{"events" => chunk}),
82+
[{"STATSIG-API-KEY", key}, {"Content-Type", "application/json"}]
83+
)
84+
end)
85+
end
86+
87+
defp get_api_key({:env, var}), do: System.get_env(var)
88+
defp get_api_key(key), do: key
89+
90+
defp save_configs([], _), do: :ok
91+
92+
defp save_configs([%{"name" => name} = head | tail], type) when is_binary(name) do
93+
:ets.insert(ets_name(), {{name, type}, head})
94+
save_configs(tail, type)
95+
end
96+
97+
defp save_configs([_head | tail], type), do: save_configs(tail, type)
98+
end

lib/statsig_ex/api_client.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule StatsigEx.APIClient do
2+
# we'll refactor to use this later
3+
end

mix.exs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule StatsigEx.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :statsig_ex,
7+
version: "0.1.0",
8+
elixir: "~> 1.8",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger],
18+
applications: [:httpoison]
19+
]
20+
end
21+
22+
# Run "mix help deps" to learn about dependencies.
23+
defp deps do
24+
[
25+
{:httpoison, "~> 1.4"},
26+
{:jason, "~> 1.2"}
27+
]
28+
end
29+
end

mix.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
%{
2+
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
3+
"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"},
4+
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
5+
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
6+
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
7+
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
8+
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
9+
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
10+
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
11+
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
12+
}

test/statsig_ex_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule StatsigExTest do
2+
use ExUnit.Case
3+
doctest StatsigEx
4+
5+
test "greets the world" do
6+
assert StatsigEx.hello() == :world
7+
end
8+
end

test/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)