-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Callum
committed
Jun 19, 2019
0 parents
commit c01089b
Showing
10 changed files
with
612 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
] |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 protected]", | ||
"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" | ||
} | ||
``` |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
use Mix.Config | ||
|
||
config :mixpanel_api_ex, config: [token: "foobar", active: false] |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: "[email protected]"}) | ||
""" | ||
@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 |
Oops, something went wrong.