Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ jobs:
- name: Compile with warnings as errors
run: mix compile --warnings-as-errors

- name: Check public API snapshot
run: mix posthog.public_api --check

package:
name: Hex package build
runs-on: ubuntu-latest
Expand Down
208 changes: 208 additions & 0 deletions lib/mix/tasks/posthog.public_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
defmodule Mix.Tasks.Posthog.PublicApi do
@moduledoc """
Checks the committed public API snapshot.

The snapshot is generated from compiled BEAM metadata. Public modules are
discovered from the current Mix application, `Code.fetch_docs/1` is used to
identify documented modules and function visibility, and `__info__/1` is used
to enumerate exported functions and macros.

mix posthog.public_api --check
mix posthog.public_api --update

## Options

* `--check` - Compare the generated snapshot with the committed snapshot.
This is the default.
* `--update` - Regenerate the committed snapshot.
* `--snapshot` - Snapshot path. Defaults to `public_api.snapshot`.
"""

use Mix.Task

@shortdoc "Checks the committed public API snapshot"
@default_snapshot "public_api.snapshot"

@impl true
def run(args) do
{opts, _, _} =
OptionParser.parse(args,
strict: [check: :boolean, update: :boolean, snapshot: :string]
)

check? = Keyword.get(opts, :check, false)
update? = Keyword.get(opts, :update, false)
snapshot_path = Keyword.get(opts, :snapshot, @default_snapshot)

if check? and update? do
Mix.raise("Pass only one of --check or --update")
end

Mix.Task.run("compile")

snapshot = generate_snapshot()

if update? do
write_snapshot(snapshot_path, snapshot)
else
check_snapshot(snapshot_path, snapshot)
end
end

defp generate_snapshot do
modules = public_modules()

[
"# This file is generated by `mix posthog.public_api --update`.",
"# Do not edit manually.",
"",
"# Public modules/functions/macros for #{Mix.Project.config()[:app]}",
""
| Enum.flat_map(modules, &module_snapshot/1)
]
|> Enum.join("\n")
|> then(&(&1 <> "\n"))
end

defp public_modules do
app = Mix.Project.config()[:app]

Application.load(app)

case :application.get_key(app, :modules) do
{:ok, modules} ->
modules
|> Enum.filter(&posthog_module?/1)
|> Enum.reject(&hidden_module?/1)
|> Enum.sort_by(&inspect/1)

:undefined ->
Mix.raise("Could not load compiled modules for #{inspect(app)}")
end
end

defp posthog_module?(module) do
module
|> Atom.to_string()
|> String.starts_with?("Elixir.PostHog")
end

defp hidden_module?(module) do
module
|> fetch_docs()
|> elem(0)
|> Kernel.==(:hidden)
end

defp module_snapshot(module) do
{module_doc, docs} = fetch_docs(module)
docs_by_id = docs_by_id(docs)

[
inspect(module),
" module_doc: #{doc_visibility(module_doc)}"
| exported_snapshot(module, :functions, :function, docs_by_id) ++
exported_snapshot(module, :macros, :macro, docs_by_id) ++ [""]
]
end

defp exported_snapshot(module, info_key, kind, docs_by_id) do
exports = module.__info__(info_key)

lines =
exports
|> Enum.sort()
|> Enum.reject(fn {name, arity} ->
Map.get(docs_by_id, {kind, name, arity}, :none) == :hidden
end)
|> Enum.map(fn {name, arity} ->
doc = Map.get(docs_by_id, {kind, name, arity}, :none)

" #{name}/#{arity} doc: #{doc_visibility(doc)}"
end)

[" #{info_key}:" | empty_or_lines(lines)]
end

defp empty_or_lines([]), do: [" (none)"]
defp empty_or_lines(lines), do: lines

defp docs_by_id(docs) do
Map.new(docs, fn {{kind, name, arity}, _line, _signature, doc, _metadata} ->
{{kind, name, arity}, doc}
end)
end

defp fetch_docs(module) do
case Code.fetch_docs(module) do
{:docs_v1, _anno, _beam_language, _format, module_doc, _metadata, docs} ->
{module_doc, docs}

{:error, reason} ->
Mix.raise("Could not fetch docs for #{inspect(module)}: #{inspect(reason)}")
end
end

defp doc_visibility(:hidden), do: "hidden"
defp doc_visibility(:none), do: "none"
defp doc_visibility(_doc), do: "documented"

defp write_snapshot(path, snapshot) do
path
|> Path.dirname()
|> File.mkdir_p!()

File.write!(path, snapshot)
Mix.shell().info("Updated #{path}")
end

defp check_snapshot(path, snapshot) do
case File.read(path) do
{:ok, ^snapshot} ->
Mix.shell().info("Public API snapshot is up to date")

{:ok, current} ->
Mix.raise("""
Public API snapshot is out of date.

Run `mix posthog.public_api --update` and commit #{path}.
To inspect the change locally, run `mix posthog.public_api --update` and then `git diff -- #{path}`.

Diff:
#{snapshot_diff(path, current, snapshot)}
""")

{:error, :enoent} ->
Mix.raise("""
Public API snapshot does not exist.

Run `mix posthog.public_api --update` and commit #{path}.
""")

{:error, reason} ->
Mix.raise("Could not read #{path}: #{inspect(reason)}")
end
end

defp snapshot_diff(path, current, snapshot) do
generated_path =
System.tmp_dir!()
|> Path.join("#{Path.basename(path)}.generated-#{System.unique_integer([:positive])}")

try do
File.write!(generated_path, snapshot)

{diff, _status} =
System.cmd("git", ["diff", "--no-index", "--", path, generated_path],
stderr_to_stdout: true
)

diff
rescue
_error ->
"Could not generate diff. Current snapshot bytes: #{byte_size(current)}. Generated snapshot bytes: #{byte_size(snapshot)}."
after
File.rm(generated_path)
end
end
end
163 changes: 163 additions & 0 deletions public_api.snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# This file is generated by `mix posthog.public_api --update`.
# Do not edit manually.

# Public modules/functions/macros for posthog

PostHog
module_doc: documented
functions:
bare_capture/2 doc: none
bare_capture/4 doc: documented
capture/1 doc: none
capture/3 doc: documented
config/0 doc: none
config/1 doc: documented
get_context/0 doc: none
get_context/1 doc: documented
get_event_context/1 doc: none
get_event_context/2 doc: documented
set_context/1 doc: none
set_context/2 doc: documented
set_event_context/2 doc: none
set_event_context/3 doc: documented
macros:
(none)

PostHog.API.Client
module_doc: documented
functions:
user_agent/0 doc: documented
macros:
(none)

PostHog.Config
module_doc: documented
functions:
validate/1 doc: documented
validate!/1 doc: documented
macros:
(none)

PostHog.Error
module_doc: documented
functions:
(none)
macros:
(none)

PostHog.FeatureFlags
module_doc: documented
functions:
check/1 doc: none
check/3 doc: documented
check!/1 doc: none
check!/3 doc: documented
evaluate_flags/0 doc: none
evaluate_flags/2 doc: documented
flags/1 doc: none
flags/2 doc: documented
flags_for/0 doc: none
flags_for/2 doc: documented
get_feature_flag_result/1 doc: none
get_feature_flag_result/4 doc: documented
get_feature_flag_result!/1 doc: none
get_feature_flag_result!/4 doc: documented
set_in_context/1 doc: documented
set_in_context/2 doc: documented
macros:
(none)

PostHog.FeatureFlags.Evaluations
module_doc: documented
functions:
accessed/1 doc: documented
enabled?/2 doc: documented
event_properties/1 doc: documented
get_flag/2 doc: documented
get_flag_payload/2 doc: documented
keys/1 doc: documented
only/2 doc: documented
only_accessed/1 doc: documented
macros:
(none)

PostHog.FeatureFlags.Result
module_doc: documented
functions:
value/1 doc: documented
macros:
(none)

PostHog.Handler
module_doc: documented
functions:
(none)
macros:
(none)

PostHog.Integrations.LLMAnalytics.Req
module_doc: documented
functions:
attach/1 doc: none
attach/2 doc: documented
macros:
(none)

PostHog.Integrations.Plug
module_doc: documented
functions:
(none)
macros:
(none)

PostHog.LLMAnalytics
module_doc: documented
functions:
capture_current_span/1 doc: none
capture_current_span/3 doc: documented
capture_span/1 doc: none
capture_span/3 doc: documented
get_root_span/0 doc: none
get_root_span/1 doc: documented
get_session/0 doc: none
get_session/1 doc: documented
get_trace/0 doc: none
get_trace/1 doc: documented
set_root_span/1 doc: none
set_root_span/2 doc: documented
set_session/0 doc: none
set_session/2 doc: documented
set_trace/0 doc: none
set_trace/2 doc: documented
start_span/0 doc: none
start_span/2 doc: documented
macros:
(none)

PostHog.Supervisor
module_doc: documented
functions:
child_spec/1 doc: documented
start_link/1 doc: documented
macros:
(none)

PostHog.Test
module_doc: documented
functions:
all_captured/0 doc: none
all_captured/1 doc: documented
allow/2 doc: none
allow/3 doc: documented
set_posthog_shared/0 doc: none
set_posthog_shared/1 doc: documented
macros:
(none)

PostHog.UnexpectedResponseError
module_doc: documented
functions:
(none)
macros:
(none)

Loading