diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c9c3df --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +mixpanel_plug-*.tar + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c366247 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# MixpanelPlug + +A plug-based approach to Mixpanel tracking with Elixir. Use MixpanelPlug to: + +- Track events with useful context like referrer, user agent information, and UTM properties +- Keep user profiles up to date on every request + +MixpanelPlug respects the ‘Do Not Track’ request header. When this is set, no tracking calls will be made. + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `mixpanel_plug` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:mixpanel_plug, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/mixpanel_plug](https://hexdocs.pm/mixpanel_plug). + +## Configuration + +Add configuration for `mixpanel_api_ex` to your `config/config.exs` file: + +```elixir +config :mixpanel_api_ex, config: [token: "your_mixpanel_token"] +``` + +## Usage + +In a Phoenix application, register the MixpanelPlug plug in `router.ex`: + +```diff +defmodule Example.Router do + use Example, :router + + pipeline :browser do + plug :accepts, ["html", "json"] + plug :fetch_session + plug :fetch_flash + plug :protect_from_forgery + plug :put_secure_browser_headers ++ plug MixpanelPlug + end +end +``` + +If the ‘Do Not Track’ (`dnt`) has been set to `1`, the property `do_not_track: true` will be assigned to the connection. Additionally, a call to `MixpanelPlug.update_profile/2` will be made with the value of `current_user` from the connection, if ‘Do Not Track’ is not set. For more information, please see the module documentation. + +For making tracking calls, use `MixpanelPlug.track_event`: + +```elixir +defmodule Example.UserController do + use Example, :controller + + import MixpanelPlug, only: [track_event: 3] + + def create(conn, %{"email" => email}) do + conn + |> track_event("Example User Created", %{"email" => email}) + |> render("user_created.html") + end +end +``` + +The properties added to the tracking call include the following, where appropriate: + +```elixir +%{ + "email" => "email@example.com", + "Current Path" => "/users", + "$browser" => "Mobile Safari", + "$browser_version" => "10.0", + "$device" => "iPhone", + "$os" => "iOS 10.3.1", + "utm_campaign" => "campaign", + "utm_content" => "content", + "utm_medium" => "medium", + "utm_source" => "source", + "utm_term" => "term" +} +``` diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..e6e46e0 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,31 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# third-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :mixpanel_plug, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:mixpanel_plug, :key) +# +# You can also configure a third-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# + +if Mix.env() === :test, do: import_config("test.exs") diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..72c93b0 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,3 @@ +use Mix.Config + +config :mixpanel_api_ex, config: [token: "foobar", active: false] diff --git a/lib/mixpanel_plug.ex b/lib/mixpanel_plug.ex new file mode 100644 index 0000000..47adde9 --- /dev/null +++ b/lib/mixpanel_plug.ex @@ -0,0 +1,206 @@ +defmodule MixpanelPlug do + @moduledoc """ + Mixpanel tracking as a plug. + + - Track events with useful context like referrer, user agent information, and UTM properties + - Keep user profiles up to date on every request + - Respects ‘Do Not Track’ request headers + """ + + import Plug.Conn + + @type user :: %{id: number | String.t(), name: String.t(), email: String.t()} + + @spec init(Plug.opts()) :: Plug.opts() + def init(opts), do: opts + + @doc """ + Checks for the ‘Do Not Track’ request header and if it exists, assigns its + value to the connection. In the case that tracking is permitted, + `update_profile` will be called with the value of an assign named + `current_user` from the connection. The value of `current_user` must pattern + match the type `t:user/0`. If this is not matched, the user’s profile will not + be updated. + """ + @spec call(Plug.Conn.t(%{assigns: %{current_user: user()}}), Plug.opts()) :: Plug.Conn.t() + def call(%{assigns: %{current_user: current_user}} = conn, _opts) do + conn + |> put_do_not_track() + |> update_profile(current_user) + end + + def call(conn, _opts), do: put_do_not_track(conn) + + @doc """ + Checks whether the 'Do Not Track' header has been set on the connection + + ## Examples + + iex> MixpanelPlug.tracking_disabled?(conn) + true + + """ + @spec tracking_disabled?(Plug.Conn.t()) :: boolean + def tracking_disabled?(conn) do + conn + |> get_req_header("dnt") + |> Enum.member?("1") + end + + @doc """ + Updates a user profile in Mixpanel. This method pattern matches the value of + the `user` struct against the type `t:user/0`. If this is not matched, the + user’s profile will not be updated. + + This is a noop if the ‘Do Not Track’ header is set. + + ## Examples + + MixpanelPlug.update_profile(conn, %{id: 1, name: "Callum", email: "callum@example.com"}) + + """ + @spec update_profile(Plug.Conn.t(), user :: user()) :: Plug.Conn.t() + def update_profile(%{assigns: %{do_not_track: true}} = conn, _user), do: conn + + def update_profile(conn, %{id: id, name: name, email: email} = _user) do + properties = %{ + "$name" => name, + "$email" => email, + "ID" => id + } + + conn + |> engage(id, "$set", properties) + |> put_analytics(:profile, properties) + end + + def update_profile(conn, _user), do: conn + + @doc """ + Tracks an event in Mixpanel. + + This is a noop if the ‘Do Not Track’ header is set. + + ## Examples + + MixpanelPlug.track(conn, "Added To Wishlist") + MixpanelPlug.track(conn, "Discount Code Used", %{"Value" => "10"}) + + """ + @spec track(Plug.Conn.t(), event :: String.t(), properties :: struct) :: Plug.Conn.t() + def track(conn, event, properties \\ %{}) + + def track(%{assigns: %{do_not_track: true}} = conn, _event, _properties), do: conn + + def track(conn, event, properties) do + properties = get_properties(conn, properties) + opts = get_opts(conn) + + Mixpanel.track(event, properties, opts) + + conn + |> update_analytics(:tracked_events, &[{event, properties, opts} | &1 || []]) + end + + defp engage(conn, distinct_id, operation, properties) do + Mixpanel.engage(distinct_id, operation, properties, ip: conn.remote_ip) + conn + end + + defp get_properties(conn, properties) do + properties + |> put_page_properties(conn) + |> put_referrer_properties(conn) + |> put_user_agent_properties(conn) + |> put_utm_properties(conn) + end + + defp put_do_not_track(conn) do + if tracking_disabled?(conn) do + assign(conn, :do_not_track, true) + else + conn + end + end + + defp put_page_properties(properties, conn) do + Map.put(properties, "Current Path", conn.request_path) + end + + defp put_referrer_properties(properties, conn) do + case List.first(get_req_header(conn, "referer")) do + nil -> + properties + + referrer -> + properties + |> Map.put_new("$referrer", referrer) + |> Map.put_new("$referring_domain", URI.parse(referrer).host) + end + end + + defp put_user_agent_properties(properties, conn) do + with ua <- get_req_header(conn, "user-agent"), + ua when is_binary(ua) <- List.first(ua), + ua <- UAParser.parse(ua) do + properties + |> Map.put_new("$os", to_string(ua.os)) + |> Map.put_new("$browser", to_string(ua.family)) + |> Map.put_new("$browser_version", to_string(ua.version)) + |> Map.put_new( + "$device", + case ua.device do + %{family: nil} -> nil + device -> to_string(device) + end + ) + else + _ -> properties + end + end + + defp put_utm_properties(properties, %{query_params: query_params}) do + properties + |> put_utm_property(query_params, "utm_source") + |> put_utm_property(query_params, "utm_medium") + |> put_utm_property(query_params, "utm_campaign") + |> put_utm_property(query_params, "utm_content") + |> put_utm_property(query_params, "utm_term") + end + + defp put_utm_property(properties, query_params, property_key) do + case Map.get(query_params, property_key) do + nil -> + properties + + property_value -> + Map.put_new(properties, property_key, property_value) + end + end + + defp get_opts(%{assigns: %{current_user: %{id: id}}} = conn) do + [ip: conn.remote_ip, distinct_id: id] + end + + defp get_opts(conn) do + [ip: conn.remote_ip] + end + + defp put_analytics(%{assigns: assigns} = conn, key, value) do + assigns = + assigns + |> Map.put_new(:analytics, %{}) + |> put_in([:analytics, key], value) + + %{conn | assigns: assigns} + end + + defp update_analytics(%{assigns: assigns} = conn, key, fun) do + assigns = + assigns + |> Map.put_new(:analytics, %{}) + |> update_in([:analytics, key], fun) + + %{conn | assigns: assigns} + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..69b7d33 --- /dev/null +++ b/mix.exs @@ -0,0 +1,47 @@ +defmodule MixpanelPlug.MixProject do + use Mix.Project + + def project do + [ + app: :mixpanel_plug, + version: "0.1.0", + elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + deps: deps(), + package: package(), + description: "A plug-based approach to Mixpanel tracking with Elixir", + source_url: "https://github.com/madebymany/mixpanel_plug", + docs: [ + main: "readme", + extras: ["README.md": [title: "README", filename: "readme"]] + ] + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger, :mixpanel_api_ex] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:plug_cowboy, "~> 2.0"}, + {:mixpanel_api_ex, "~> 1.0.1"}, + {:ua_parser, "~> 1.7"}, + {:ex_doc, "~> 0.19", only: :dev, runtime: false} + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end + + defp package do + [ + licenses: ["Apache 2.0"], + maintainers: ["Callum Jefferies"], + links: %{"GitHub" => "https://github.com/madebymany/mixpanel_plug"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..1b06972 --- /dev/null +++ b/mix.lock @@ -0,0 +1,27 @@ +%{ + "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, + "mixpanel_api_ex": {:hex, :mixpanel_api_ex, "1.0.1", "affd7d886529df7cef6a229a1a2003a47f1f40a4f07f703caed369df60c2d1cf", [:mix], [{:httpoison, "~> 1.2", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, + "plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "ua_parser": {:hex, :ua_parser, "1.7.0", "5014dc577898a452be16822b6244a00f7debf2b647a3fd4e9f817d7213d283ae", [:mix], [{:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, + "yamerl": {:hex, :yamerl, "0.7.0", "e51dba652dce74c20a88294130b48051ebbbb0be7d76f22de064f0f3ccf0aaf5", [:rebar3], [], "hexpm"}, +} diff --git a/test/mixpanel_plug_test.exs b/test/mixpanel_plug_test.exs new file mode 100644 index 0000000..432c964 --- /dev/null +++ b/test/mixpanel_plug_test.exs @@ -0,0 +1,181 @@ +defmodule MixpanelPlugTest do + use ExUnit.Case + use Plug.Test + + describe "call" do + test "assigns 'do not track' flag" do + conn = conn(:get, "/") + + conn = + conn + |> put_req_header("dnt", "1") + |> MixpanelPlug.call(%{}) + + assert %{assigns: %{do_not_track: true}} = conn + end + + test "records the profile of the current user when set with minimum properties" do + conn = conn(:get, "/") + + conn = + conn + |> assign(:current_user, %{id: 1, name: "Callum", email: "callum@example.com"}) + |> MixpanelPlug.call(%{}) + + refute is_nil(get_in(conn.assigns, [:analytics, :profile])) + end + end + + describe "tracking_disabled?" do + test "returns true when the 'dnt' header is set to '1'" do + conn = conn(:get, "/") + conn = put_req_header(conn, "dnt", "1") + + assert MixpanelPlug.tracking_disabled?(conn) === true + end + end + + describe "track" do + test "does nothing if 'do not track' is set" do + conn = conn(:get, "/") + + conn = + conn + |> assign(:do_not_track, true) + |> MixpanelPlug.track("test") + + refute Map.has_key?(conn.assigns, :analytics) + end + + test "sets properties related to the current user" do + conn = conn(:get, "/") + + conn = + conn + |> assign(:current_user, %{id: 1, name: "Callum", email: "callum@example.com"}) + |> MixpanelPlug.track("test") + + {_event, _properties, opts} = + List.first(get_in(conn.assigns, [:analytics, :tracked_events])) + + assert Access.get(opts, :distinct_id) === 1 + end + + test "sets properties related to the request location" do + conn = conn(:get, "/some_page_url") + conn = MixpanelPlug.track(conn, "test") + + {_event, properties, _opts} = + List.first(get_in(conn.assigns, [:analytics, :tracked_events])) + + assert Map.get(properties, "Current Path") === "/some_page_url" + end + + test "sets properties related to the referrer" do + conn = conn(:get, "/") + + conn = + conn + |> put_req_header("referrer", "http://example.com/example") + |> MixpanelPlug.track("test") + + {_event, properties, _opts} = + List.first(get_in(conn.assigns, [:analytics, :tracked_events])) + + assert Map.get(properties, "$referrer", "http://example.com/example") + assert Map.get(properties, "$referrer_domain", "example.com") + end + + test "sets properties related to the user agent" do + conn = + conn( + :get, + "/pooing?utm_source=source&utm_medium=medium&utm_campaign=campaign&utm_content=content&utm_term=term" + ) + + conn = + conn + |> put_req_header( + "user-agent", + "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1" + ) + |> put_req_header("referrer", "http://example.com/example") + |> fetch_query_params() + |> MixpanelPlug.track("test", %{"fuuuck" => "fuuuck"}) + + {_event, properties, _opts} = + List.first(get_in(conn.assigns, [:analytics, :tracked_events])) + + IO.inspect(properties) + + assert Map.has_key?(properties, "$os") + assert Map.has_key?(properties, "$browser") + assert Map.has_key?(properties, "$browser_version") + assert Map.has_key?(properties, "$device") + end + + test "sets utm properties" do + conn = + conn( + :get, + "/?utm_source=source&utm_medium=medium&utm_campaign=campaign&utm_content=content&utm_term=term" + ) + + conn = + conn + |> fetch_query_params() + |> MixpanelPlug.track("test") + + {_event, properties, _opts} = + List.first(get_in(conn.assigns, [:analytics, :tracked_events])) + + assert Map.get(properties, "utm_source") === "source" + assert Map.get(properties, "utm_medium") === "medium" + assert Map.get(properties, "utm_campaign") === "campaign" + assert Map.get(properties, "utm_content") === "content" + assert Map.get(properties, "utm_term") === "term" + end + + test "does not set utm properties when they do not have a value" do + conn = conn(:get, "/") + + conn = + conn + |> fetch_query_params() + |> MixpanelPlug.track("test") + + {_event, properties, _opts} = + List.first(get_in(conn.assigns, [:analytics, :tracked_events])) + + refute Map.has_key?(properties, "utm_source") + refute Map.has_key?(properties, "utm_medium") + refute Map.has_key?(properties, "utm_campaign") + refute Map.has_key?(properties, "utm_content") + refute Map.has_key?(properties, "utm_term") + end + end + + describe "update_profile" do + test "does nothing if 'do not track' is set" do + conn = conn(:get, "/") + + conn = + conn + |> assign(:do_not_track, true) + |> MixpanelPlug.update_profile(%{id: 1, name: "Callum", email: "callum@example.com"}) + + refute Map.has_key?(conn.assigns, :analytics) + end + + test "records user profile" do + user = %{id: 1, name: "Callum", email: "callum@example.com"} + + conn = conn(:get, "/") + conn = MixpanelPlug.update_profile(conn, user) + + assert get_in(conn.assigns, [:analytics, :profile, "ID"]) === 1 + assert get_in(conn.assigns, [:analytics, :profile, "$name"]) === "Callum" + assert get_in(conn.assigns, [:analytics, :profile, "$email"]) === "callum@example.com" + end + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()