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
2 changes: 1 addition & 1 deletion circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dependencies:
test:
override:
- |
if [ ${CIRCLE_PR_USERNAME} ]; then MIX_ENV=test mix test --cover; else MIX_ENV=test mix test --include requires_env --cover; fi
if [ ${CIRCLE_PR_USERNAME} ]; then MIX_ENV=test mix coveralls.circle; else MIX_ENV=test mix coveralls.circle --include requires_env; fi

post:
- mix inch.report
Expand Down
6 changes: 3 additions & 3 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ config :arc,
bucket: System.get_env("S3_BUCKET"),
asset_host: System.get_env("CLOUDFRONT_DOMAIN")

# Configures Segment for analytics
config :code_corps, :analytics, CodeCorps.Analytics.Segment

config :segment,
write_key: System.get_env("SEGMENT_WRITE_KEY")

Expand All @@ -72,6 +69,9 @@ config :sentry,
included_environments: ~w(prod staging)a,
use_error_logger: true

config :ja_resource,
repo: CodeCorps.Repo

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"
2 changes: 1 addition & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ config :code_corps, allowed_origins: ["http://localhost:4200"]
config :guardian, Guardian,
secret_key: "e62fb6e2746f6b1bf8b5b735ba816c2eae1d5d76e64f18f3fc647e308b0c159e"

config :code_corps, :analytics, CodeCorps.Analytics.InMemory
config :code_corps, :analytics, CodeCorps.Analytics.InMemoryAPI

config :sentry,
environment_name: Mix.env || :dev
3 changes: 3 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ config :guardian, Guardian,
# Do not print debug messages in production
config :logger, level: :info

# Configures Segment for analytics
config :code_corps, :analytics, CodeCorps.Analytics.SegmentAPI

config :sentry,
environment_name: Mix.env || :prod

Expand Down
3 changes: 3 additions & 0 deletions config/staging.exs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ config :guardian, Guardian,
# Do not print debug messages in production
config :logger, level: :info

# Configures Segment for analytics
config :code_corps, :analytics, CodeCorps.Analytics.SegmentAPI

config :sentry,
environment_name: Mix.env || :staging

Expand Down
2 changes: 1 addition & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ config :code_corps, allowed_origins: ["http://localhost:4200"]
config :guardian, Guardian,
secret_key: "e62fb6e2746f6b1bf8b5b735ba816c2eae1d5d76e64f18f3fc647e308b0c159e"

config :code_corps, :analytics, CodeCorps.Analytics.InMemory
config :code_corps, :analytics, CodeCorps.Analytics.InMemoryAPI

config :code_corps, :icon_color_generator, CodeCorps.RandomIconColor.TestGenerator

Expand Down
7 changes: 0 additions & 7 deletions lib/code_corps/analytics/in_memory.ex

This file was deleted.

10 changes: 10 additions & 0 deletions lib/code_corps/analytics/in_memory_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule CodeCorps.Analytics.InMemoryAPI do
@moduledoc """
In-memory interface to simulate calling out to the Segment API.

Each function should have the same signature as `CodeCorps.Analytics.SegmentAPI` and simply return `nil`.
"""

def identify(_user_id, _traits), do: nil
def track(_user_id, _event_name, _properties), do: nil
end
143 changes: 90 additions & 53 deletions lib/code_corps/analytics/segment.ex
Original file line number Diff line number Diff line change
@@ -1,82 +1,115 @@
defmodule CodeCorps.Analytics.Segment do
@moduledoc """
Provides analytics tracking for Segment.com with an interface for making [`identify`](https://github.com/stueccles/analytics-elixir#identify) and [`track`](https://github.com/stueccles/analytics-elixir#track) calls via the [`analytics-elixir` package](https://github.com/stueccles/analytics-elixir).

You can read more about [`identify`](https://segment.com/docs/spec/identify/) and [`track`](https://segment.com/docs/spec/track/) in [Segment's documentation](https://segment.com/docs/).

By default, in `dev` and `test` envrionments, this module will use `CodeCorps.Analytics.InMemoryAPI` which does not make a request to Segment's REST API.

In `prod` and `staging` environments, the module will use `CodeCorps.Analytics.SegmentAPI` which _will_ make requests to Segment's REST API.

In your `config/prod.exs` you might set this like so:

```elixir
config :code_corps, :analytics, CodeCorps.Analytics.SegmentAPI
```
"""

alias CodeCorps.Comment
alias CodeCorps.OrganizationMembership
alias CodeCorps.Task
alias CodeCorps.User
alias CodeCorps.UserCategory
alias CodeCorps.UserRole
alias CodeCorps.UserSkill
alias Ecto.Changeset

def identify(user = %User{}) do
Segment.Analytics.identify(user.id, traits(user))
end
@api Application.get_env(:code_corps, :analytics)

def track(conn, :added, user_category = %UserCategory{}) do
conn |> do_track("Added User Category", properties(user_category))
end
def track(conn, :added, user_role = %UserRole{}) do
conn |> do_track("Added User Role", properties(user_role))
end
def track(conn, :added, user_skill = %UserSkill{}) do
conn |> do_track("Added User Skill", properties(user_skill))
end
def track(conn, :created, comment = %Comment{}) do
conn |> do_track("Created Comment", properties(comment))
end
def track(conn, :created, organization_membership = %OrganizationMembership{role: "pending"}) do
conn |> do_track("Requested Organization Membership", properties(organization_membership))
end
def track(conn, :created, organization_membership = %OrganizationMembership{}) do
conn |> do_track("Created Organization Membership", properties(organization_membership))
end
def track(conn, :created, task = %Task{}) do
conn |> do_track("Created Task", properties(task))
end
def track(conn, :edited, comment = %Comment{}) do
conn |> do_track("Edited Comment", properties(comment))
end
def track(conn, :edited, task = %Task{}) do
conn |> do_track("Edited Task", properties(task))
end
def track(conn, :removed, user_category = %UserCategory{}) do
conn |> do_track("Removed User Category", properties(user_category))
end
def track(conn, :removed, user_role = %UserRole{}) do
conn |> do_track("Removed User Role", properties(user_role))
end
def track(conn, :removed, user_skill = %UserSkill{}) do
conn |> do_track("Removed User Skill", properties(user_skill))
end
def track(conn, _event, _struct) do
conn # return conn without event to track
@actions_without_properties [:updated_profile, :signed_in, :signed_out, :signed_up]

@doc """
Uses the action on the record to determine the event name that should be passed in for the `track` call.
"""
@spec get_event_name(atom, struct) :: String.t
def get_event_name(action, _) when action in @actions_without_properties do
friendly_action_name(action)
end
def get_event_name(:created, %OrganizationMembership{}), do: "Requested Organization Membership"
def get_event_name(:edited, %OrganizationMembership{}), do: "Approved Organization Membership"
def get_event_name(:created, %UserCategory{}), do: "Added User Category"
def get_event_name(:created, %UserSkill{}), do: "Added User Skill"
def get_event_name(:created, %UserRole{}), do: "Added User Role"
def get_event_name(action, model) do
[friendly_action_name(action), friendly_model_name(model)] |> Enum.join(" ")
end

def track(conn, :updated_profile) do
conn |> do_track("Updated Profile")
@doc """
Calls `identify` in the configured API module.
"""
@spec identify(User.t) :: any
def identify(user = %User{}) do
@api.identify(user.id, traits(user))
end
def track(conn, :signed_in) do
conn |> do_track("Signed In")

@doc """
Calls `track` in the configured API module.

Receives either an `:ok` or `:error` tuple from an attempted `Ecto.Repo` operation.
"""
@spec track({:ok, Ecto.Schema.t} | {:error, Ecto.Changeset.t}, atom, Plug.Conn.t) :: any
def track({:ok, record}, action, %Plug.Conn{} = conn) when action in @actions_without_properties do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to pattern match on the record's type at all for extra type safety?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outside of having one function signature for every single record type, I'm not sure there is.

action_name = get_event_name(action, record)
do_track(conn, action_name)

{:ok, record}
end
def track(conn, :signed_out) do
conn |> do_track("Signed Out")
def track({:ok, record}, action, %Plug.Conn{} = conn) do
action_name = get_event_name(action, record)
do_track(conn, action_name, properties(record))

{:ok, record}
end
def track(conn, :signed_up) do
conn |> do_track("Signed Up")
def track({:error, %Changeset{} = changeset}, _action, _conn), do: {:error, changeset}
def track({:error, errors}, :deleted, _conn), do: {:error, errors}

@doc """
Calls `track` with the "Signed In" event in the configured API module.
"""
@spec track_sign_in(Plug.Conn.t) :: any
def track_sign_in(conn), do: conn |> do_track("Signed In")

defp friendly_action_name(:deleted), do: "Removed"
defp friendly_action_name(action) do
action
|> Atom.to_string
|> String.split("_")
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
end
def track(conn, _event) do
conn # return conn without event to track

defp friendly_model_name(model) do
model.__struct__
|> Module.split
|> List.last
|> Macro.underscore
|> String.split("_")
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
end

defp do_track(conn, event_name, properties) do
Segment.Analytics.track(conn.assigns[:current_user].id, event_name, properties)
@api.track(conn.assigns[:current_user].id, event_name, properties)
conn
end

defp do_track(conn, event_name) do
Segment.Analytics.track(conn.assigns[:current_user].id, event_name, %{})
@api.track(conn.assigns[:current_user].id, event_name, %{})
conn
end

defp properties(comment = %Comment{}) do
comment = comment |> CodeCorps.Repo.preload(:task)
%{
comment_id: comment.id,
task: comment.task.title,
Expand All @@ -86,6 +119,7 @@ defmodule CodeCorps.Analytics.Segment do
}
end
defp properties(organization_membership = %OrganizationMembership{}) do
organization_membership = organization_membership |> CodeCorps.Repo.preload(:organization)
%{
organization: organization_membership.organization.name,
organization_id: organization_membership.organization.id
Expand All @@ -100,18 +134,21 @@ defmodule CodeCorps.Analytics.Segment do
}
end
defp properties(user_category = %UserCategory{}) do
user_category = user_category |> CodeCorps.Repo.preload(:category)
%{
category: user_category.category.name,
category_id: user_category.category.id
}
end
defp properties(user_role = %UserRole{}) do
user_role = user_role |> CodeCorps.Repo.preload(:role)
%{
role: user_role.role.name,
role_id: user_role.role.id
}
end
defp properties(user_skill = %UserSkill{}) do
user_skill = user_skill |> CodeCorps.Repo.preload(:skill)
%{
skill: user_skill.skill.title,
skill_id: user_skill.skill.id
Expand Down
13 changes: 13 additions & 0 deletions lib/code_corps/analytics/segment_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule CodeCorps.Analytics.SegmentAPI do
@moduledoc """
Interface to the Segment API through the [`analytics-elixir` package](https://github.com/stueccles/analytics-elixir).
"""

def identify(user_id, traits) do
Segment.Analytics.identify(user_id, traits)
end

def track(user_id, event_name, properties) do
Segment.Analytics.track(user_id, event_name, properties)
end
end
18 changes: 18 additions & 0 deletions lib/code_corps/helpers/query.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule CodeCorps.Helpers.Query do
import CodeCorps.Helpers.String, only: [coalesce_id_string: 1, coalesce_string: 1]
import Ecto.Query, only: [where: 3]

def id_filter(query, id_list) do
ids = id_list |> coalesce_id_string
query |> where([object], object.id in ^ids)
end

def organization_filter(query, organization_id) do
query |> where([object], object.organization_id == ^organization_id)
end

def role_filter(query, roles_list) do
roles = roles_list |> coalesce_string
query |> where([object], object.role in ^roles)
end
end
15 changes: 15 additions & 0 deletions lib/code_corps/helpers/slug.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule CodeCorps.Helpers.Slug do
alias Ecto.Changeset

def generate_slug(changeset, value_key, slug_key) do
case changeset do
%Changeset{valid?: true, changes: changes} ->
case Map.fetch(changes, value_key) do
{:ok, value} -> Changeset.put_change(changeset, slug_key, Inflex.parameterize(value))
_ -> changeset
end
_ ->
changeset
end
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule CodeCorps.ControllerHelpers do
defmodule CodeCorps.Helpers.String do
def coalesce_id_string(string) do
string
|> String.split(",")
Expand Down
2 changes: 2 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule CodeCorps.Mixfile do
:earmark,
:ex_aws,
:httpoison,
:ja_resource,
:scrivener_ecto,
:segment,
:sentry,
Expand Down Expand Up @@ -75,6 +76,7 @@ defmodule CodeCorps.Mixfile do
{:hackney, ">= 1.4.4", override: true},
{:inch_ex, "~> 0.5", only: [:dev, :test]}, # Inch CI
{:inflex, "~> 1.7.0"},
{:ja_resource, "~> 0.1.0"},
{:ja_serializer, "~> 0.11.0"}, # JSON API
{:mix_test_watch, "~> 0.2", only: :dev}, # Test watcher
{:poison, "~> 1.2 or ~> 2.0"},
Expand Down
7 changes: 4 additions & 3 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"ex_aws": {:hex, :ex_aws, "0.5.0", "6ca02f1e8fe8340aa2eee66d9f08efcd6ff1f9f4ef7264669d0756e0b3917218", [:mix], [{:httpoison, "~> 0.8", [hex: :httpoison, optional: true]}, {:jsx, "~> 2.5", [hex: :jsx, optional: true]}, {:poison, "~> 1.2 or ~> 2.0", [hex: :poison, optional: true]}, {:sweet_xml, "~> 0.5", [hex: :sweet_xml, optional: true]}]},
"ex_doc": {:hex, :ex_doc, "0.13.2", "1059a588d2ad3ffab25a0b85c58abf08e437d3e7a9124ac255e1d15cec68ab79", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
"ex_machina": {:hex, :ex_machina, "1.0.2", "1cc49e1a09e3f7ab2ecb630c17f14c2872dc4ec145d6d05a9c3621936a63e34f", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: true]}]},
"excoveralls": {:hex, :excoveralls, "0.5.6", "35a903f6f78619ee7f951448dddfbef094b3a0d8581657afaf66465bc930468e", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]},
"exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]},
"excoveralls": {:hex, :excoveralls, "0.5.7", "5d26e4a7cdf08294217594a1b0643636accc2ad30e984d62f1d166f70629ff50", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]},
"exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]},
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
"gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []},
"guardian": {:hex, :guardian, "0.13.0", "37c5b5302617276093570ee938baca146f53e1d5de1f5c2b8effb1d2fea596d2", [:mix], [{:jose, "~> 1.8", [hex: :jose, optional: false]}, {:phoenix, "~> 1.2.0", [hex: :phoenix, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:poison, ">= 1.3.0", [hex: :poison, optional: false]}, {:uuid, ">=1.1.1", [hex: :uuid, optional: false]}]},
Expand All @@ -28,9 +28,10 @@
"idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []},
"inch_ex": {:hex, :inch_ex, "0.5.4", "a2b032ad141a335a0a119f49b157b36326f5928d16a1d129b0f582398fdc25d2", [:mix], [{:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]},
"inflex": {:hex, :inflex, "1.7.0", "4466a34b7d8e871d8164619ba0f3b8410ec782e900f0ae1d3d27a5875a29532e", [:mix], []},
"ja_resource": {:hex, :ja_resource, "0.1.0", "eed6443e7337147264bf28badabad48eb0cc32493ff06a48ac19d4b3657f5029", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:ja_serializer, "~> 0.9", [hex: :ja_serializer, optional: false]}, {:phoenix, "~> 1.1", [hex: :phoenix, optional: false]}, {:plug, "~> 1.2", [hex: :plug, optional: false]}]},
"ja_serializer": {:hex, :ja_serializer, "0.11.0", "6c8ded7cfd4cd226812e97445bedd2f6d47e19c5d8b987f58cf552518c98fbd1", [:mix], [{:inflex, "~> 1.4", [hex: :inflex, optional: false]}, {:plug, "> 1.0.0", [hex: :plug, optional: false]}, {:poison, "~> 1.4 or ~> 2.0", [hex: :poison, optional: false]}, {:scrivener, "~> 1.2 or ~> 2.0", [hex: :scrivener, optional: true]}]},
"jose": {:hex, :jose, "1.8.0", "1ee027c5c0ff3922e3bfe58f7891509e8f87f771ba609ee859e623cc60237574", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, optional: false]}]},
"jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []},
"jsx": {:hex, :jsx, "2.8.0", "749bec6d205c694ae1786d62cea6cc45a390437e24835fd16d12d74f07097727", [:mix, :rebar], []},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
Expand Down
7 changes: 3 additions & 4 deletions test/controllers/comment_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@ defmodule CodeCorps.CommentControllerTest do
assert data["relationships"]["task"]["data"]["id"] == "#{comment.task_id}"
end

test "does not show resource and instead throw error when id is nonexistent", %{conn: conn} do
assert_error_sent 404, fn ->
get conn, comment_path(conn, :show, -1)
end
test "renders 404 when id is nonexistent", %{conn: conn} do
path = conn |> comment_path(:show, -1)
assert conn |> get(path) |> json_response(404)
end
end

Expand Down
Loading