Last active
February 7, 2023 05:51
-
-
Save Arp-G/e857f23e7ac213e3900caa6b76d28432 to your computer and use it in GitHub Desktop.
Add Segment Typewriter validations to elixir
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule SegmentSchemaValidator do | |
@moduledoc """ | |
This module can be used to validate segment payloads generated by elixir. | |
It can then help us to add elixir tests which validates segment payloads against the tracking plan. | |
It relies on the `assets/js/plan.json` file that contains a JSON schema representation of the tracking plans for segment events. | |
To use this module | |
* Add the segment typewriter to your application and generate a tracking plan, refer to this [page](https://segment.com/docs/protocols/apis-and-extensions/typewriter/#browser-quickstart) | |
* Make sure you add the [ex_json_schema](https://hex.pm/packages/ex_json_schema) dependency to your `mix.exs` file: `{:ex_json_schema, "~> 0.9.2"}` | |
* Copy this module to your project and make any changes if needed | |
* You can now add tests, for example: | |
``` | |
@tag :segment_tracking | |
test "pushes proper segment tracking event on button click do | |
{:ok, view, _html} = conn |> live("/") | |
view |> element("#some_button") |> render_click() | |
# A sample event payload to validate | |
tracking_payload = %{ | |
event_name: :event_name, | |
event_payload: %{ text: "button_clicked" } | |
} | |
# Assert an event was pushed | |
assert_push_event(view, "track_segment_event", segment_tracking_payload_received) | |
# Assert the event push matches the expectation | |
assert tracking_payload == segment_tracking_payload_received | |
# Assert the segment payload is complaint with the segment typewriter tracking plan | |
assert :ok == SegmentSchemaValidator.validate_payload(tracking_payload) | |
end | |
``` | |
When tracking plan is updated on segment, do the following: | |
* Run the following command from the project's root directory to update the tracking plan json file | |
and regenerate the typewriter client : `cd /assets && npx typewriter --update` | |
""" | |
# Recompile this module if tracking plan changes | |
# Ref: https://github.com/elixir-lang/elixir/issues/2455#issue-36557631 | |
@external_resource "assets/js/plan.json" | |
@schemas "assets/js/plan.json" | |
|> File.read!() | |
|> Jason.decode!() | |
|> Map.get("rules") | |
|> Enum.map(&Map.get(&1, "jsonSchema")) | |
|> Map.new(fn %{"$id" => event_name} = schema -> | |
{event_name, ExJsonSchema.Schema.resolve(schema)} | |
end) | |
@doc """ | |
Validate if a given payload is complaint with segment's tracking plan | |
iex(17)> ConnectWeb.SegmentSchemaValidator.validate_payload(%{event_name: :cnx_post_created, event_payload: %{...}}) | |
:ok | |
iex(17)> ConnectWeb.SegmentSchemaValidator.validate_payload(%{event_name: :bad_event, event_payload: %{...}}) | |
{:error, "Schema for event bad_event not found in tracking plan"} | |
""" | |
@spec validate_payload(%{event_name: atom(), event_payload: map()}) :: :ok | {:error, any()} | |
def validate_payload(%{event_name: event_name, event_payload: event_payload}) do | |
event_name = to_string(event_name) | |
# ExJson expects string keys | |
event_payload = stringify_keys(event_payload) | |
case Map.get(@schemas, event_name) do | |
nil -> {:error, "Schema for event #{event_name} not found in tracking plan"} | |
schema -> ExJsonSchema.Validator.validate(schema, event_payload, error_formatter: false) | |
end | |
end | |
# Convert map atom keys to strings | |
# Ref: https://gist.github.com/kipcole9/0bd4c6fb6109bfec9955f785087f53fb#file-map-helpers-L55 | |
defp stringify_keys(nil), do: nil | |
defp stringify_keys(map = %{}) do | |
map | |
|> Enum.map(fn {k, v} -> {Atom.to_string(k), stringify_keys(v)} end) | |
|> Enum.into(%{}) | |
end | |
# Walk the list and stringify the keys of of any map members | |
defp stringify_keys([head | rest]), do: [stringify_keys(head) | stringify_keys(rest)] | |
defp stringify_keys(not_a_map), do: not_a_map | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment