Skip to content

Instantly share code, notes, and snippets.

@Arp-G
Last active February 7, 2023 05:51
Show Gist options
  • Save Arp-G/e857f23e7ac213e3900caa6b76d28432 to your computer and use it in GitHub Desktop.
Save Arp-G/e857f23e7ac213e3900caa6b76d28432 to your computer and use it in GitHub Desktop.
Add Segment Typewriter validations to elixir
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