mirror of
https://github.com/plausible/analytics.git
synced 2024-11-23 11:12:15 +03:00
Merge pull request #16 from plausible-insights/goals
Add goals and conversions
This commit is contained in:
commit
f36b7afbb8
@ -31,3 +31,22 @@ if (flash) {
|
||||
flash.style.display = 'none'
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
const registerForm = document.getElementById('register-form')
|
||||
|
||||
if (registerForm) {
|
||||
registerForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
setTimeout(submitForm, 1000);
|
||||
var formSubmitted = false;
|
||||
|
||||
function submitForm() {
|
||||
if (!formSubmitted) {
|
||||
formSubmitted = true;
|
||||
registerForm.submit();
|
||||
}
|
||||
}
|
||||
|
||||
plausible('trigger', 'Signup', {callback: submitForm});
|
||||
})
|
||||
}
|
||||
|
@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
function ignore(reason) {
|
||||
console.warn('[Plausible] Ignoring pageview because ' + reason);
|
||||
console.warn('[Plausible] Ignoring event because ' + reason);
|
||||
}
|
||||
|
||||
function getUrl() {
|
||||
@ -77,7 +77,7 @@
|
||||
}))
|
||||
}
|
||||
|
||||
function trigger(eventName) {
|
||||
function trigger(eventName, options) {
|
||||
if (/localhost$/.test(window.location.hostname)) return ignore('website is running locally');
|
||||
if (window.location.protocol === 'file:') return ignore('website is running locally');
|
||||
if (window.document.visibilityState === 'prerender') return ignore('document is prerendering');
|
||||
@ -95,13 +95,14 @@
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == XMLHttpRequest.DONE) {
|
||||
setUserData(payload)
|
||||
options && options.callback && options.callback()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function page() {
|
||||
trigger('pageview')
|
||||
function page(options) {
|
||||
trigger('pageview', options)
|
||||
}
|
||||
|
||||
function trackPushState() {
|
||||
|
@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
function ignore(reason) {
|
||||
console.warn('[Plausible] Ignoring pageview because ' + reason);
|
||||
console.warn('[Plausible] Ignoring event because ' + reason);
|
||||
}
|
||||
|
||||
function getUrl() {
|
||||
|
@ -124,6 +124,15 @@ if (domainEl) {
|
||||
document.getElementById('browsers-stats').innerHTML = res
|
||||
router.updateLinkHandlers()
|
||||
})
|
||||
|
||||
if (document.getElementById('conversion-stats')) {
|
||||
fetch(`/stats/${domain}/conversions${location.search}`)
|
||||
.then(res => res.text())
|
||||
.then((res) => {
|
||||
document.getElementById('conversion-stats').innerHTML = res
|
||||
router.updateLinkHandlers()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setInterval(function() {
|
||||
|
34
lib/plausible/goal/schema.ex
Normal file
34
lib/plausible/goal/schema.ex
Normal file
@ -0,0 +1,34 @@
|
||||
defmodule Plausible.Goal do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
schema "goals" do
|
||||
field :domain, :string
|
||||
field :event_name, :string
|
||||
field :page_path, :string
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(goal, attrs \\ %{}) do
|
||||
goal
|
||||
|> cast(attrs, [:domain, :event_name, :page_path])
|
||||
|> validate_required([:domain])
|
||||
|> validate_event_name_and_page_path()
|
||||
end
|
||||
|
||||
defp validate_event_name_and_page_path(changeset) do
|
||||
if present?(changeset, :event_name) || present?(changeset, :page_path) do
|
||||
changeset
|
||||
else
|
||||
changeset
|
||||
|> add_error(:event_name, "this field is required")
|
||||
|> add_error(:page_path, "this field is required")
|
||||
end
|
||||
end
|
||||
|
||||
defp present?(changeset, field) do
|
||||
value = get_field(changeset, field)
|
||||
value && value != ""
|
||||
end
|
||||
end
|
21
lib/plausible/goals.ex
Normal file
21
lib/plausible/goals.ex
Normal file
@ -0,0 +1,21 @@
|
||||
defmodule Plausible.Goals do
|
||||
use Plausible.Repo
|
||||
alias Plausible.Goal
|
||||
|
||||
def create(site, params) do
|
||||
params = Map.merge(params, %{"domain" => site.domain})
|
||||
|
||||
Goal.changeset(%Goal{}, params) |> Repo.insert
|
||||
end
|
||||
|
||||
def for_site(domain) do
|
||||
Repo.all(
|
||||
from g in Goal,
|
||||
where: g.domain == ^domain
|
||||
)
|
||||
end
|
||||
|
||||
def delete(id) do
|
||||
Repo.one(from g in Goal, where: g.id == ^id) |> Repo.delete!
|
||||
end
|
||||
end
|
@ -18,6 +18,13 @@ defmodule Plausible.Sites do
|
||||
)
|
||||
end
|
||||
|
||||
def has_goals?(site) do
|
||||
Repo.exists?(
|
||||
from g in Plausible.Goal,
|
||||
where: g.domain == ^site.domain
|
||||
)
|
||||
end
|
||||
|
||||
def is_owner?(user_id, site) do
|
||||
Repo.exists?(
|
||||
from sm in Plausible.Site.Membership,
|
||||
|
@ -181,6 +181,66 @@ defmodule Plausible.Stats do
|
||||
)
|
||||
end
|
||||
|
||||
def goal_conversions(site, query, _limit \\ 5) do
|
||||
goals = Repo.all(from g in Plausible.Goal, where: g.domain == ^site.domain)
|
||||
fetch_pageview_goals(goals, site, query)
|
||||
++ fetch_event_goals(goals, site, query)
|
||||
|> sort_conversions()
|
||||
end
|
||||
|
||||
defp fetch_event_goals(goals, site, query) do
|
||||
{:ok, first} = NaiveDateTime.new(query.date_range.first, ~T[00:00:00])
|
||||
first_datetime = Timex.to_datetime(first, site.timezone)
|
||||
|
||||
{:ok, last} = NaiveDateTime.new(query.date_range.last |> Timex.shift(days: 1), ~T[00:00:00])
|
||||
last_datetime = Timex.to_datetime(last, site.timezone)
|
||||
|
||||
events = Enum.map(goals, fn goal -> goal.event_name end)
|
||||
|> Enum.filter(&(&1))
|
||||
|
||||
if Enum.count(events) > 0 do
|
||||
Repo.all(
|
||||
from e in Plausible.Event,
|
||||
where: e.hostname == ^site.domain,
|
||||
where: e.timestamp >= ^first_datetime and e.timestamp < ^last_datetime,
|
||||
where: e.name in ^events,
|
||||
group_by: e.name,
|
||||
select: {e.name, count(e.user_id, :distinct)}
|
||||
)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_pageview_goals(goals, site, query) do
|
||||
{:ok, first} = NaiveDateTime.new(query.date_range.first, ~T[00:00:00])
|
||||
first_datetime = Timex.to_datetime(first, site.timezone)
|
||||
|
||||
{:ok, last} = NaiveDateTime.new(query.date_range.last |> Timex.shift(days: 1), ~T[00:00:00])
|
||||
last_datetime = Timex.to_datetime(last, site.timezone)
|
||||
|
||||
pages = Enum.map(goals, fn goal -> goal.page_path end)
|
||||
|> Enum.filter(&(&1))
|
||||
|
||||
if Enum.count(pages) > 0 do
|
||||
Repo.all(
|
||||
from e in Plausible.Event,
|
||||
where: e.hostname == ^site.domain,
|
||||
where: e.timestamp >= ^first_datetime and e.timestamp < ^last_datetime,
|
||||
where: e.name == "pageview",
|
||||
where: e.pathname in ^pages,
|
||||
group_by: e.pathname,
|
||||
select: {fragment("concat('Visit ', ?)", e.pathname), count(e.user_id, :distinct)}
|
||||
)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp sort_conversions(conversions) do
|
||||
Enum.sort_by(conversions, fn {_, count} -> -count end)
|
||||
end
|
||||
|
||||
defp base_query(site, query) do
|
||||
{:ok, first} = NaiveDateTime.new(query.date_range.first, ~T[00:00:00])
|
||||
first_datetime = Timex.to_datetime(first, site.timezone)
|
||||
|
@ -1,7 +1,7 @@
|
||||
defmodule PlausibleWeb.SiteController do
|
||||
use PlausibleWeb, :controller
|
||||
use Plausible.Repo
|
||||
alias Plausible.Sites
|
||||
alias Plausible.{Sites, Goals}
|
||||
|
||||
plug PlausibleWeb.RequireAccountPlug
|
||||
|
||||
@ -33,6 +33,59 @@ defmodule PlausibleWeb.SiteController do
|
||||
|> render("snippet.html", site: site, layout: {PlausibleWeb.LayoutView, "focus.html"})
|
||||
end
|
||||
|
||||
def goals(conn, %{"website" => website}) do
|
||||
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)
|
||||
goals = Goals.for_site(site.domain)
|
||||
|
||||
conn
|
||||
|> assign(:skip_plausible_tracking, true)
|
||||
|> render("goal_settings.html",
|
||||
site: site,
|
||||
goals: goals,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
end
|
||||
|
||||
def new_goal(conn, %{"website" => website}) do
|
||||
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)
|
||||
changeset = Plausible.Goal.changeset(%Plausible.Goal{})
|
||||
|
||||
conn
|
||||
|> assign(:skip_plausible_tracking, true)
|
||||
|> render("new_goal.html",
|
||||
site: site,
|
||||
changeset: changeset,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
end
|
||||
|
||||
def create_goal(conn, %{"website" => website, "goal" => goal}) do
|
||||
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)
|
||||
|
||||
case Plausible.Goals.create(site, goal) do
|
||||
{:ok, _} ->
|
||||
conn
|
||||
|> put_flash(:success, "Goal created succesfully")
|
||||
|> redirect(to: "/#{website}/goals")
|
||||
{:error, :goal, changeset, _} ->
|
||||
conn
|
||||
|> assign(:skip_plausible_tracking, true)
|
||||
|> render("new_goal.html",
|
||||
site: site,
|
||||
changeset: changeset,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_goal(conn, %{"website" => website, "id" => goal_id}) do
|
||||
Plausible.Goals.delete(goal_id)
|
||||
|
||||
conn
|
||||
|> put_flash(:success, "Goal deleted succesfully")
|
||||
|> redirect(to: "/#{website}/goals")
|
||||
end
|
||||
|
||||
def settings(conn, %{"website" => website}) do
|
||||
site = Sites.get_for_user!(conn.assigns[:current_user].id, website)
|
||||
|> Repo.preload(:google_auth)
|
||||
|
@ -25,12 +25,14 @@ defmodule PlausibleWeb.StatsController do
|
||||
{conn, params} = fetch_period(conn, site)
|
||||
query = Stats.Query.from(site.timezone, params)
|
||||
current_visitors = Stats.current_visitors(site)
|
||||
has_goals = user && Plausible.Sites.has_goals?(site)
|
||||
|
||||
conn
|
||||
|> assign(:skip_plausible_tracking, !demo)
|
||||
|> put_session(site.domain <> "_offer_email_report", nil)
|
||||
|> render("stats.html",
|
||||
site: site,
|
||||
has_goals: has_goals,
|
||||
query: query,
|
||||
current_visitors: current_visitors,
|
||||
title: "Plausible · " <> site.domain,
|
||||
@ -310,6 +312,20 @@ defmodule PlausibleWeb.StatsController do
|
||||
end
|
||||
end
|
||||
|
||||
def conversions_preview(conn, %{"domain" => domain}) do
|
||||
site = Repo.get_by(Plausible.Site, domain: domain)
|
||||
|
||||
if site && current_user_can_access?(conn, site) do
|
||||
{conn, params} = fetch_period(conn, site)
|
||||
query = Stats.Query.from(site.timezone, params)
|
||||
goals = Stats.goal_conversions(site, query)
|
||||
|
||||
render(conn, "conversions_preview.html", layout: false, query: query, site: site, goals: goals)
|
||||
else
|
||||
render_error(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
defp current_user_can_access?(_conn, %Plausible.Site{public: true}) do
|
||||
true
|
||||
end
|
||||
|
@ -93,6 +93,10 @@ defmodule PlausibleWeb.Router do
|
||||
put "/sites/:website/monthly-report", SiteController, :update_monthly_settings
|
||||
get "/:website/snippet", SiteController, :add_snippet
|
||||
get "/:website/settings", SiteController, :settings
|
||||
get "/:website/goals", SiteController, :goals
|
||||
get "/:website/goals/new", SiteController, :new_goal
|
||||
post "/:website/goals", SiteController, :create_goal
|
||||
delete "/:website/goals/:id", SiteController, :delete_goal
|
||||
put "/:website/settings", SiteController, :update_settings
|
||||
put "/:website/settings/google", SiteController, :update_google_auth
|
||||
delete "/:website", SiteController, :delete_site
|
||||
@ -103,6 +107,7 @@ defmodule PlausibleWeb.Router do
|
||||
get "/stats/:domain/screen-sizes", StatsController, :screen_sizes_preview
|
||||
get "/stats/:domain/operating-systems", StatsController, :operating_systems_preview
|
||||
get "/stats/:domain/browsers", StatsController, :browsers_preview
|
||||
get "/stats/:domain/conversions", StatsController, :conversions_preview
|
||||
get "/stats/:domain/main-graph", StatsController, :main_graph
|
||||
get "/:website/*path", StatsController, :stats
|
||||
end
|
||||
|
@ -1,4 +1,4 @@
|
||||
<%= form_for @changeset, "/register", [class: "max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 py-6 mb-4 mt-8"], fn f -> %>
|
||||
<%= form_for @changeset, "/register", [class: "max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 py-6 mb-4 mt-8", id: "register-form"], fn f -> %>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2>Enter your details to get started</h2>
|
||||
</div>
|
||||
|
@ -23,5 +23,7 @@
|
||||
<p class="text-center text-grey text-xs mt-4">
|
||||
©2019 Plausible Insights. All rights reserved.
|
||||
</p>
|
||||
|
||||
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
23
lib/plausible_web/templates/site/goal_settings.html.eex
Normal file
23
lib/plausible_web/templates/site/goal_settings.html.eex
Normal file
@ -0,0 +1,23 @@
|
||||
<%= if get_flash(@conn, :success) do %>
|
||||
<div id="flash" class="max-w-sm w-full rounded mx-auto text-center bg-green-dark text-green-lightest text-sm font-bold px-4 w-full transition overflow-hidden" role="alert">
|
||||
<p class="py-3"><%= get_flash(@conn, :success) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8">
|
||||
<h2>Goals for <%= @site.domain %></h2>
|
||||
<div class="my-6">
|
||||
<%= if Enum.count(@goals) > 0 do %>
|
||||
<%= for goal <- @goals do %>
|
||||
<div class="border-b border-grey-light py-3 flex justify-between">
|
||||
<small class="font-bold"><%= goal_name(goal) %></small>
|
||||
<%= button("❌", to: "/#{@site.domain}/goals/#{goal.id}", method: :delete, class: "text-sm", data: [confirm: "Are you sure you want to remove goal #{goal_name(goal)}? This will just affect the UI, all of your analytics data will stay intact."]) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div>No goals configured for this site yet</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link("+ Add goal", to: "/#{@site.domain}/goals/new", class: "button mt-4 w-full") %>
|
||||
</div>
|
37
lib/plausible_web/templates/site/new_goal.html.eex
Normal file
37
lib/plausible_web/templates/site/new_goal.html.eex
Normal file
@ -0,0 +1,37 @@
|
||||
<%= form_for @changeset, "/#{@site.domain}/goals", [class: "max-w-sm w-full mx-auto bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4 mt-8"], fn f -> %>
|
||||
<h2>Add goal for <%= @site.domain %></h2>
|
||||
<div class="mt-6 text-sm font-bold">Goal trigger</div>
|
||||
<div class="my-3 w-full flex rounded border border-grey-light">
|
||||
<div class="w-1/2 text-center py-2 border-r border-grey shadow-inner font-bold bg-grey-lighter cursor-pointer" id="pageview-tab">Pageview</div>
|
||||
<div class="w-1/2 text-center py-2 bg-grey-lightest cursor-pointer" id="event-tab">Custom event</div>
|
||||
</div>
|
||||
<div class="my-6">
|
||||
<div id="pageview-fields">
|
||||
<%= label f, :page_path, class: "block text-sm font-bold" %>
|
||||
<%= text_input f, :page_path, class: "transition mt-3 bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light", placeholder: "/success" %>
|
||||
<%= error_tag f, :page_path %>
|
||||
</div>
|
||||
<div id="event-fields" class="hidden">
|
||||
<%= label f, :event_name, class: "block text-sm font-bold" %>
|
||||
<%= text_input f, :event_name, class: "transition mt-3 bg-grey-lighter appearance-none border border-transparent rounded w-full p-2 text-grey-darker leading-normal appearance-none focus:outline-none focus:bg-white focus:border-grey-light", placeholder: "Signup" %>
|
||||
<%= error_tag f, :event_name %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= submit "Add goal →", class: "button mt-4 w-full" %>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
document.getElementById('pageview-tab').onclick = function() {
|
||||
document.getElementById('pageview-fields').classList.remove('hidden')
|
||||
document.getElementById('pageview-tab').classList.add('shadow-inner', 'font-bold')
|
||||
document.getElementById('event-fields').classList.add('hidden')
|
||||
document.getElementById('event-tab').classList.remove('shadow-inner', 'font-bold')
|
||||
}
|
||||
document.getElementById('event-tab').onclick = function() {
|
||||
document.getElementById('event-fields').classList.remove('hidden')
|
||||
document.getElementById('event-tab').classList.add('shadow-inner', 'font-bold')
|
||||
document.getElementById('pageview-fields').classList.add('hidden')
|
||||
document.getElementById('pageview-tab').classList.remove('shadow-inner', 'font-bold')
|
||||
}
|
||||
</script>
|
@ -0,0 +1,27 @@
|
||||
<div class="text-center">
|
||||
<h2>Conversions</h2>
|
||||
<div class="text-grey-darker mt-1">by unique visitors</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<%= if Enum.count(@goals) > 0 do %>
|
||||
<%= for {key, count} <- @goals do %>
|
||||
<div class="flex items-center justify-between my-2">
|
||||
<span class="truncate" style="max-width: 80%;"><%= key %></span>
|
||||
<span title="<%= count %>"><%= large_number_format(count) %></span>
|
||||
</div>
|
||||
<%= bar(count, @goals, :indigo) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="text-center">No conversions for this period</div>
|
||||
<div class="text-center mt-2"><%= link("Manage goals", to: "/#{@site.domain}/goals", class: "text-indigo") %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if Enum.count(@goals) >= 5 do %>
|
||||
<div class="text-center">
|
||||
<a href="/<%= @site.domain %>/conversions<%= query_params(@query) %>" data-pushstate class="font-bold text-sm text-grey-dark hover:text-red transition tracking-wide">
|
||||
<svg style="fill: #8795a1;" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-maximize"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
|
||||
MORE
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
@ -3,8 +3,7 @@
|
||||
<div class="text-center bg-blue-lighter text-blue-darkest text-sm font-bold px-4 w-full rounded transition" style="top: 91px" role="alert">
|
||||
<%= link("Click here to enable weekly email reports →", to: "/#{@site.domain}/settings#email-reports", class: "py-2 block") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="pt-12"></div>
|
||||
<% end %> <div class="pt-12"></div>
|
||||
<div class="w-full sm:flex justify-between items-center">
|
||||
<div class="w-full flex items-center">
|
||||
<h2 class="text-left mr-8">Analytics for <%= link(@site.domain, to: "//" <> @site.domain, target: "_blank") %></h2>
|
||||
@ -91,14 +90,22 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full block md:flex items-start justify-between mt-6">
|
||||
<div class="w-full md:w-31percent bg-white shadow-md rounded mt-4 p-4" id="screen-sizes-stats">
|
||||
<div class="w-full md:w-31percent bg-white shadow-md rounded mt-4 p-4" id="browsers-stats">
|
||||
<div class="loading my-32 mx-auto"><div></div></div>
|
||||
</div>
|
||||
<div class="w-full md:w-31percent bg-white shadow-md rounded mt-4 p-4" id="operating-systems-stats">
|
||||
<div class="loading my-32 mx-auto"><div></div></div>
|
||||
</div>
|
||||
<div class="w-full md:w-31percent bg-white shadow-md rounded mt-4 p-4" id="browsers-stats">
|
||||
<div class="w-full md:w-31percent bg-white shadow-md rounded mt-4 p-4" id="screen-sizes-stats">
|
||||
<div class="loading my-32 mx-auto"><div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @has_goals do %>
|
||||
<div class="w-full block md:flex items-start justify-between mt-6">
|
||||
<div class="w-full md:w-31percent bg-white shadow-md rounded mt-4 p-4" id="conversion-stats">
|
||||
<div class="loading my-32 mx-auto"><div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -1,6 +1,14 @@
|
||||
defmodule PlausibleWeb.SiteView do
|
||||
use PlausibleWeb, :view
|
||||
|
||||
def goal_name(%Plausible.Goal{page_path: page_path}) when is_binary(page_path) do
|
||||
"Visit " <> page_path
|
||||
end
|
||||
|
||||
def goal_name(%Plausible.Goal{event_name: name}) when is_binary(name) do
|
||||
name
|
||||
end
|
||||
|
||||
def snippet() do
|
||||
"""
|
||||
<script>
|
||||
|
16
priv/repo/migrations/20191031051340_add_goals.exs
Normal file
16
priv/repo/migrations/20191031051340_add_goals.exs
Normal file
@ -0,0 +1,16 @@
|
||||
defmodule Plausible.Repo.Migrations.AddGoals do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create table(:goals) do
|
||||
add :domain, :text, null: false
|
||||
add :name, :text, null: false
|
||||
add :event_name, :text
|
||||
add :page_path, :text
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:goals, [:domain, :name])
|
||||
end
|
||||
end
|
9
priv/repo/migrations/20191031063001_remove_goal_name.exs
Normal file
9
priv/repo/migrations/20191031063001_remove_goal_name.exs
Normal file
@ -0,0 +1,9 @@
|
||||
defmodule Plausible.Repo.Migrations.RemoveGoalName do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:goals) do
|
||||
remove :name
|
||||
end
|
||||
end
|
||||
end
|
@ -319,6 +319,61 @@ defmodule Plausible.StatsTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "goal_conversions" do
|
||||
test "shows custom event conversions" do
|
||||
site = insert(:site)
|
||||
insert(:goal, %{domain: site.domain, event_name: "Register"})
|
||||
insert(:goal, %{domain: site.domain, event_name: "Newsletter signup"})
|
||||
insert(:event, name: "Register", hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 01:00:00])
|
||||
insert(:event, name: "Register", hostname: site.domain, user_id: UUID.uuid4(), timestamp: ~N[2019-01-01 01:00:00])
|
||||
insert(:event, name: "Newsletter signup", hostname: site.domain, user_id: UUID.uuid4(), timestamp: ~N[2019-01-01 01:00:00])
|
||||
insert(:event, name: "Irrelevant", hostname: site.domain, user_id: UUID.uuid4(), timestamp: ~N[2019-01-01 01:00:00])
|
||||
query = Stats.Query.from(site.timezone, %{"period" => "day", "date" => "2019-01-01"})
|
||||
|
||||
conversions = Stats.goal_conversions(site, query)
|
||||
|
||||
assert conversions == [
|
||||
{"Register", 2},
|
||||
{"Newsletter signup", 1}
|
||||
]
|
||||
end
|
||||
|
||||
test "shows pageview conversions" do
|
||||
site = insert(:site)
|
||||
insert(:goal, %{domain: site.domain, page_path: "/success"})
|
||||
insert(:goal, %{domain: site.domain, page_path: "/register"})
|
||||
insert(:event, name: "pageview", pathname: "/success", hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 01:00:00])
|
||||
insert(:event, name: "pageview", pathname: "/success", hostname: site.domain, user_id: UUID.uuid4(), timestamp: ~N[2019-01-01 01:00:00])
|
||||
insert(:event, name: "pageview", pathname: "/register", hostname: site.domain, user_id: UUID.uuid4(), timestamp: ~N[2019-01-01 01:00:00])
|
||||
insert(:event, name: "pageview", pathname: "/irrelevant", hostname: site.domain, user_id: UUID.uuid4(), timestamp: ~N[2019-01-01 01:00:00])
|
||||
query = Stats.Query.from(site.timezone, %{"period" => "day", "date" => "2019-01-01"})
|
||||
|
||||
conversions = Stats.goal_conversions(site, query)
|
||||
|
||||
assert conversions == [
|
||||
{"Visit /success", 2},
|
||||
{"Visit /register", 1}
|
||||
]
|
||||
end
|
||||
|
||||
test "shows mixed conversions in order of occurence" do
|
||||
site = insert(:site)
|
||||
insert(:goal, %{domain: site.domain, page_path: "/success"})
|
||||
insert(:goal, %{domain: site.domain, event_name: "Signup"})
|
||||
insert(:event, name: "Signup", hostname: site.domain, user_id: @user_id, timestamp: ~N[2019-01-01 01:00:00])
|
||||
insert(:event, name: "pageview", pathname: "/success", hostname: site.domain, user_id: UUID.uuid4(), timestamp: ~N[2019-01-01 01:00:00])
|
||||
insert(:event, name: "pageview", pathname: "/success", hostname: site.domain, user_id: UUID.uuid4(), timestamp: ~N[2019-01-01 01:00:00])
|
||||
query = Stats.Query.from(site.timezone, %{"period" => "day", "date" => "2019-01-01"})
|
||||
|
||||
conversions = Stats.goal_conversions(site, query)
|
||||
|
||||
assert conversions == [
|
||||
{"Visit /success", 2},
|
||||
{"Signup", 1}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp months_ago(months) do
|
||||
Timex.now() |> Timex.shift(months: -months)
|
||||
end
|
||||
|
@ -123,4 +123,74 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
refute Repo.exists?(from e in Plausible.Event, where: e.id == ^pageview.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /:website/goals/new" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "shows form to create a new goal", %{conn: conn, site: site} do
|
||||
conn = get(conn, "/#{site.domain}/goals/new")
|
||||
|
||||
assert html_response(conn, 200) =~ "Add goal"
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /:website/goals" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "creates a pageview goal for the website", %{conn: conn, site: site} do
|
||||
post(conn, "/#{site.domain}/goals", %{
|
||||
goal: %{
|
||||
page_path: "/success",
|
||||
event_name: ""
|
||||
}
|
||||
})
|
||||
|
||||
goal = Repo.one(Plausible.Goal)
|
||||
|
||||
assert goal.page_path == "/success"
|
||||
assert goal.event_name == nil
|
||||
end
|
||||
|
||||
test "creates a custom event goal for the website", %{conn: conn, site: site} do
|
||||
post(conn, "/#{site.domain}/goals", %{
|
||||
goal: %{
|
||||
page_path: "",
|
||||
event_name: "Signup"
|
||||
}
|
||||
})
|
||||
|
||||
goal = Repo.one(Plausible.Goal)
|
||||
|
||||
assert goal.event_name == "Signup"
|
||||
assert goal.page_path == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /:website/goals" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "lists goals for the site", %{conn: conn, site: site} do
|
||||
insert(:goal, domain: site.domain, event_name: "Custom event")
|
||||
insert(:goal, domain: site.domain, page_path: "/register")
|
||||
|
||||
conn = get(conn, "/#{site.domain}/goals")
|
||||
|
||||
|
||||
assert html_response(conn, 200) =~ "Goals for " <> site.domain
|
||||
assert html_response(conn, 200) =~ "Custom event"
|
||||
assert html_response(conn, 200) =~ "Visit /register"
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /:website/goals/:id" do
|
||||
setup [:create_user, :log_in, :create_site]
|
||||
|
||||
test "lists goals for the site", %{conn: conn, site: site} do
|
||||
goal = insert(:goal, domain: site.domain, event_name: "Custom event")
|
||||
|
||||
delete(conn, "/#{site.domain}/goals/#{goal.id}")
|
||||
|
||||
assert Repo.aggregate(Plausible.Goal, :count, :id) == 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -42,6 +42,10 @@ defmodule Plausible.Factory do
|
||||
}
|
||||
end
|
||||
|
||||
def goal_factory do
|
||||
%Plausible.Goal{}
|
||||
end
|
||||
|
||||
def subscription_factory do
|
||||
%Plausible.Billing.Subscription{
|
||||
paddle_subscription_id: sequence(:paddle_subscription_id, &"subscription-#{&1}"),
|
||||
|
Loading…
Reference in New Issue
Block a user