PR 1393 continued (#1542)

* Add `utm_content` and `utm_term`.
Support `utm_content` and `utm_term` as requested in #515.

* Add dropdown for UTM options

* Remove utm_content and term from filter modal for now

Co-authored-by: Blender Defender <defenderblender@gmail.com>
This commit is contained in:
Uku Taht 2021-12-16 11:02:09 +02:00 committed by GitHub
parent 231c72e8e8
commit 48ad7485c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 222 additions and 15 deletions

View File

@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file.
- Delete a site and all related data through the Sites API - Delete a site and all related data through the Sites API
- Subscribed users can see their Paddle invoices from the last 12 months under the user settings - Subscribed users can see their Paddle invoices from the last 12 months under the user settings
- Allow custom styles to be passed to embedded iframe plausible/analytics#1522 - Allow custom styles to be passed to embedded iframe plausible/analytics#1522
- New UTM Tags `utm_content` and `utm_term` plausible/analytics#515
### Fixed ### Fixed
- UI fix where multi-line text in pills would not be underlined properly on small screens. - UI fix where multi-line text in pills would not be underlined properly on small screens.

View File

@ -30,6 +30,8 @@ export function parseQuery(querystring, site) {
'utm_medium': q.get('utm_medium'), 'utm_medium': q.get('utm_medium'),
'utm_source': q.get('utm_source'), 'utm_source': q.get('utm_source'),
'utm_campaign': q.get('utm_campaign'), 'utm_campaign': q.get('utm_campaign'),
'utm_content': q.get('utm_content'),
'utm_term': q.get('utm_term'),
'referrer': q.get('referrer'), 'referrer': q.get('referrer'),
'screen': q.get('screen'), 'screen': q.get('screen'),
'browser': q.get('browser'), 'browser': q.get('browser'),
@ -159,6 +161,8 @@ export const formattedFilters = {
'utm_medium': 'UTM Medium', 'utm_medium': 'UTM Medium',
'utm_source': 'UTM Source', 'utm_source': 'UTM Source',
'utm_campaign': 'UTM Campaign', 'utm_campaign': 'UTM Campaign',
'utm_content': 'UTM Content',
'utm_term': 'UTM Term',
'referrer': 'Referrer URL', 'referrer': 'Referrer URL',
'screen': 'Screen size', 'screen': 'Screen size',
'browser': 'Browser', 'browser': 'Browser',

View File

@ -30,7 +30,7 @@ export default function Router({site, loggedIn, currentUserRole}) {
<ScrollToTop /> <ScrollToTop />
<Dash site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} /> <Dash site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} />
<Switch> <Switch>
<Route exact path={["/:domain/sources", "/:domain/utm_mediums", "/:domain/utm_sources", "/:domain/utm_campaigns"]}> <Route exact path={["/:domain/sources", "/:domain/utm_medium", "/:domain/utm_source", "/:domain/utm_campaign", "/:domain/utm_content", "/:domain/utm_term" ]}>
<SourcesModal site={site} /> <SourcesModal site={site} />
</Route> </Route>
<Route exact path="/:domain/referrers/Google"> <Route exact path="/:domain/referrers/Google">

View File

@ -10,7 +10,9 @@ const TITLES = {
sources: 'Top Sources', sources: 'Top Sources',
utm_mediums: 'Top UTM mediums', utm_mediums: 'Top UTM mediums',
utm_sources: 'Top UTM sources', utm_sources: 'Top UTM sources',
utm_campaigns: 'Top UTM campaigns' utm_campaigns: 'Top UTM campaigns',
utm_contents: 'Top UTM contents',
utm_terms: 'Top UTM Terms'
} }
class SourcesModal extends React.Component { class SourcesModal extends React.Component {
@ -84,6 +86,8 @@ class SourcesModal extends React.Component {
if (filter === 'utm_mediums') query.set('utm_medium', source.name) if (filter === 'utm_mediums') query.set('utm_medium', source.name)
if (filter === 'utm_sources') query.set('utm_source', source.name) if (filter === 'utm_sources') query.set('utm_source', source.name)
if (filter === 'utm_campaigns') query.set('utm_campaign', source.name) if (filter === 'utm_campaigns') query.set('utm_campaign', source.name)
if (filter === 'utm_contents') query.set('utm_content', source.name)
if (filter === 'utm_terms') query.set('utm_term', source.name)
console.log(source) console.log(source)

View File

@ -138,9 +138,11 @@ class AllSources extends React.Component {
} }
const UTM_TAGS = { const UTM_TAGS = {
utm_medium: {label: 'UTM Medium', endpoint: 'utm_mediums'}, utm_medium: {label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: 'utm_mediums'},
utm_source: {label: 'UTM Source', endpoint: 'utm_sources'}, utm_source: {label: 'UTM Source', shortLabel: 'UTM Source', endpoint: 'utm_sources'},
utm_campaign: {label: 'UTM Campaign', endpoint: 'utm_campaigns'}, utm_campaign: {label: 'UTM Campaign', shortLabel: 'UTM Campai', endpoint: 'utm_campaigns'},
utm_content: {label: 'UTM Content', shortLabel: 'UTM Conten', endpoint: 'utm_contents'},
utm_term: {label: 'UTM Term', shortLabel: 'UTM Term', endpoint: 'utm_terms'},
} }
class UTMSources extends React.Component { class UTMSources extends React.Component {
@ -266,6 +268,11 @@ class UTMSources extends React.Component {
} }
} }
import { Fragment } from 'react'
import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/solid'
import classNames from 'classnames'
export default class SourceList extends React.Component { export default class SourceList extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
@ -284,15 +291,57 @@ export default class SourceList extends React.Component {
} }
renderTabs() { renderTabs() {
const activeClass = 'inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold active-prop-heading' const activeClass = 'inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold active-prop-heading truncate text-left'
const defaultClass = 'hover:text-indigo-600 cursor-pointer' const defaultClass = 'hover:text-indigo-600 cursor-pointer truncate text-left'
const dropdownOptions = ['utm_medium', 'utm_source', 'utm_campaign']
let buttonText = UTM_TAGS[this.state.tab] ? UTM_TAGS[this.state.tab].label : 'UTM Params'
return ( return (
<ul className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2"> <div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
<li className={this.state.tab === 'all' ? activeClass : defaultClass} onClick={this.setTab('all')}>All</li> <div className={this.state.tab === 'all' ? activeClass : defaultClass} onClick={this.setTab('all')}>All</div>
<li className={this.state.tab === 'utm_medium' ? activeClass : defaultClass} onClick={this.setTab('utm_medium')}>Medium</li>
<li className={this.state.tab === 'utm_source' ? activeClass : defaultClass} onClick={this.setTab('utm_source')}>Source</li> <Menu as="div" className="relative inline-block text-left">
<li className={this.state.tab === 'utm_campaign' ? activeClass : defaultClass} onClick={this.setTab('utm_campaign')}>Campaign</li> <div>
</ul> <Menu.Button className="inline-flex justify-between focus:outline-none">
<span style={{width: '4.6rem'}} className={this.state.tab.startsWith('utm_') ? activeClass : defaultClass}>{buttonText}</span>
<ChevronDownIcon className="-mr-1 ml-px h-4 w-4" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-10">
<div className="py-1">
{ dropdownOptions.map((option) => {
return (
<Menu.Item key={option}>
{({ active }) => (
<span
onClick={this.setTab(option)}
className={classNames(
active ? 'bg-gray-100 text-gray-900 cursor-pointer' : 'text-gray-700',
'block px-4 py-2 text-sm',
this.state.tab === option ? 'font-bold' : ''
)}
>
{UTM_TAGS[option].label}
</span>
)}
</Menu.Item>
)
})}
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
) )
} }
@ -305,6 +354,10 @@ export default class SourceList extends React.Component {
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} /> return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
} else if (this.state.tab === 'utm_campaign') { } else if (this.state.tab === 'utm_campaign') {
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} /> return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
} else if (this.state.tab === 'utm_content') {
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
} else if (this.state.tab === 'utm_term') {
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
} }
} }
} }

View File

@ -17,6 +17,8 @@ defmodule Plausible.ClickhouseEvent do
field :utm_medium, :string, default: "" field :utm_medium, :string, default: ""
field :utm_source, :string, default: "" field :utm_source, :string, default: ""
field :utm_campaign, :string, default: "" field :utm_campaign, :string, default: ""
field :utm_content, :string, default: ""
field :utm_term, :string, default: ""
field :country_code, :string, default: "" field :country_code, :string, default: ""
field :subdivision1_code, :string, default: "" field :subdivision1_code, :string, default: ""
@ -53,6 +55,8 @@ defmodule Plausible.ClickhouseEvent do
:utm_medium, :utm_medium,
:utm_source, :utm_source,
:utm_campaign, :utm_campaign,
:utm_content,
:utm_term,
:country_code, :country_code,
:subdivision1_code, :subdivision1_code,
:subdivision2_code, :subdivision2_code,

View File

@ -21,6 +21,8 @@ defmodule Plausible.ClickhouseSession do
field :utm_medium, :string field :utm_medium, :string
field :utm_source, :string field :utm_source, :string
field :utm_campaign, :string field :utm_campaign, :string
field :utm_content, :string
field :utm_term, :string
field :referrer, :string field :referrer, :string
field :referrer_source, :string field :referrer_source, :string
@ -60,6 +62,8 @@ defmodule Plausible.ClickhouseSession do
:utm_medium, :utm_medium,
:utm_source, :utm_source,
:utm_campaign, :utm_campaign,
:utm_content,
:utm_term,
:country_code, :country_code,
:country_geoname_id, :country_geoname_id,
:subdivision1_code, :subdivision1_code,

View File

@ -113,6 +113,8 @@ defmodule Plausible.Session.Store do
utm_medium: event.utm_medium, utm_medium: event.utm_medium,
utm_source: event.utm_source, utm_source: event.utm_source,
utm_campaign: event.utm_campaign, utm_campaign: event.utm_campaign,
utm_content: event.utm_content,
utm_term: event.utm_term,
country_code: event.country_code, country_code: event.country_code,
subdivision1_code: event.subdivision1_code, subdivision1_code: event.subdivision1_code,
subdivision2_code: event.subdivision2_code, subdivision2_code: event.subdivision2_code,

View File

@ -314,6 +314,8 @@ defmodule Plausible.Stats.Base do
defp db_prop_val(:utm_medium, @no_ref), do: "" defp db_prop_val(:utm_medium, @no_ref), do: ""
defp db_prop_val(:utm_source, @no_ref), do: "" defp db_prop_val(:utm_source, @no_ref), do: ""
defp db_prop_val(:utm_campaign, @no_ref), do: "" defp db_prop_val(:utm_campaign, @no_ref), do: ""
defp db_prop_val(:utm_content, @no_ref), do: ""
defp db_prop_val(:utm_term, @no_ref), do: ""
defp db_prop_val(_, val), do: val defp db_prop_val(_, val), do: val
defp utc_boundaries(%Query{period: "realtime"}, _timezone) do defp utc_boundaries(%Query{period: "realtime"}, _timezone) do

View File

@ -138,7 +138,9 @@ defmodule Plausible.Stats.Breakdown do
"visit:source", "visit:source",
"visit:utm_medium", "visit:utm_medium",
"visit:utm_source", "visit:utm_source",
"visit:utm_campaign" "visit:utm_campaign",
"visit:utm_content",
"visit:utm_term"
] do ] do
query = Query.treat_page_filter_as_entry_page(query) query = Query.treat_page_filter_as_entry_page(query)
breakdown_sessions(site, query, property, metrics, pagination) breakdown_sessions(site, query, property, metrics, pagination)
@ -412,6 +414,26 @@ defmodule Plausible.Stats.Breakdown do
) )
end end
defp do_group_by(q, "visit:utm_content") do
from(
s in q,
group_by: s.utm_content,
select_merge: %{
"utm_content" => fragment("if(empty(?), ?, ?)", s.utm_content, @no_ref, s.utm_content)
}
)
end
defp do_group_by(q, "visit:utm_term") do
from(
s in q,
group_by: s.utm_term,
select_merge: %{
"utm_term" => fragment("if(empty(?), ?, ?)", s.utm_term, @no_ref, s.utm_term)
}
)
end
defp do_group_by(q, "visit:device") do defp do_group_by(q, "visit:device") do
from( from(
s in q, s in q,

View File

@ -248,6 +248,22 @@ defmodule Plausible.Stats.Clickhouse do
q q
end end
q =
if query.filters["utm_content"] do
utm_content = query.filters["utm_content"]
from(s in q, where: s.utm_content == ^utm_content)
else
q
end
q =
if query.filters["utm_term"] do
utm_term = query.filters["utm_term"]
from(s in q, where: s.utm_term == ^utm_term)
else
q
end
q = include_path_filter_entry(q, query.filters["entry_page"]) q = include_path_filter_entry(q, query.filters["entry_page"])
q = include_path_filter_exit(q, query.filters["exit_page"]) q = include_path_filter_exit(q, query.filters["exit_page"])
@ -342,6 +358,22 @@ defmodule Plausible.Stats.Clickhouse do
q q
end end
q =
if query.filters["utm_content"] do
utm_content = query.filters["utm_content"]
from(e in q, where: e.utm_content == ^utm_content)
else
q
end
q =
if query.filters["utm_term"] do
utm_term = query.filters["utm_term"]
from(e in q, where: e.utm_term == ^utm_term)
else
q
end
q = q =
if query.filters["referrer"] do if query.filters["referrer"] do
ref = query.filters["referrer"] ref = query.filters["referrer"]

View File

@ -200,6 +200,18 @@ defmodule Plausible.Stats.FilterSuggestions do
where: fragment("? ilike ?", e.utm_campaign, ^filter_query) where: fragment("? ilike ?", e.utm_campaign, ^filter_query)
) )
"utm_content" ->
from(e in q,
select: {e.utm_content},
where: fragment("? ilike ?", e.utm_content, ^filter_query)
)
"utm_term" ->
from(e in q,
select: {e.utm_term},
where: fragment("? ilike ?", e.utm_term, ^filter_query)
)
"referrer" -> "referrer" ->
from(e in q, from(e in q,
select: {e.referrer}, select: {e.referrer},

View File

@ -5,6 +5,8 @@ defmodule Plausible.Stats.Filters do
"utm_medium", "utm_medium",
"utm_source", "utm_source",
"utm_campaign", "utm_campaign",
"utm_content",
"utm_term",
"screen", "screen",
"device", "device",
"browser", "browser",

View File

@ -104,6 +104,8 @@ defmodule PlausibleWeb.Api.ExternalController do
utm_medium: query["utm_medium"], utm_medium: query["utm_medium"],
utm_source: query["utm_source"], utm_source: query["utm_source"],
utm_campaign: query["utm_campaign"], utm_campaign: query["utm_campaign"],
utm_content: query["utm_content"],
utm_term: query["utm_term"],
country_code: location_details[:country_code], country_code: location_details[:country_code],
country_geoname_id: location_details[:country_geoname_id], country_geoname_id: location_details[:country_geoname_id],
subdivision1_code: location_details[:subdivision1_code], subdivision1_code: location_details[:subdivision1_code],

View File

@ -287,6 +287,44 @@ defmodule PlausibleWeb.Api.StatsController do
end end
end end
def utm_contents(conn, params) do
site = conn.assigns[:site]
query =
Query.from(site.timezone, params)
|> Filters.add_prefix()
|> maybe_hide_noref("visit:utm_content", params)
pagination = parse_pagination(params)
metrics = ["visitors", "bounce_rate", "visit_duration"]
res =
Stats.breakdown(site, query, "visit:utm_content", metrics, pagination)
|> maybe_add_cr(site, query, pagination, "utm_content", "visit:utm_content")
|> transform_keys(%{"utm_content" => "name", "visitors" => "count"})
json(conn, res)
end
def utm_terms(conn, params) do
site = conn.assigns[:site]
query =
Query.from(site.timezone, params)
|> Filters.add_prefix()
|> maybe_hide_noref("visit:utm_term", params)
pagination = parse_pagination(params)
metrics = ["visitors", "bounce_rate", "visit_duration"]
res =
Stats.breakdown(site, query, "visit:utm_term", metrics, pagination)
|> maybe_add_cr(site, query, pagination, "utm_term", "visit:utm_term")
|> transform_keys(%{"utm_term" => "name", "visitors" => "count"})
json(conn, res)
end
def utm_sources(conn, params) do def utm_sources(conn, params) do
site = conn.assigns[:site] site = conn.assigns[:site]

View File

@ -56,6 +56,8 @@ defmodule PlausibleWeb.Router do
get "/:domain/utm_mediums", StatsController, :utm_mediums get "/:domain/utm_mediums", StatsController, :utm_mediums
get "/:domain/utm_sources", StatsController, :utm_sources get "/:domain/utm_sources", StatsController, :utm_sources
get "/:domain/utm_campaigns", StatsController, :utm_campaigns get "/:domain/utm_campaigns", StatsController, :utm_campaigns
get "/:domain/utm_contents", StatsController, :utm_contents
get "/:domain/utm_terms", StatsController, :utm_terms
get "/:domain/referrers/:referrer", StatsController, :referrer_drilldown get "/:domain/referrers/:referrer", StatsController, :referrer_drilldown
get "/:domain/pages", StatsController, :pages get "/:domain/pages", StatsController, :pages
get "/:domain/entry-pages", StatsController, :entry_pages get "/:domain/entry-pages", StatsController, :entry_pages

View File

@ -273,7 +273,7 @@
<p class="dark:text-gray-100">Deleting your account removes all sites and stats you've collected</p> <p class="dark:text-gray-100">Deleting your account removes all sites and stats you've collected</p>
<%= if @subscription && @subscription.status == "active" do %> <%= if @subscription && @subscription.status == "active" do %>
<span class="mt-6 bg-gray-300 button dark:bg-gray-800 hover:shadow-none hover:bg-gray-300">Delete my account</span> <span class="mt-6 bg-gray-300 button dark:bg-gray-600 hover:shadow-none hover:bg-gray-300">Delete my account</span>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Your account cannot be deleted because you have an active subscription. If you want to delete your account, please cancel your subscription first.</p> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Your account cannot be deleted because you have an active subscription. If you want to delete your account, please cancel your subscription first.</p>
<% else %> <% else %>
<%= link("Delete my account", to: "/me", class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete", data: [confirm: "Deleting your account will also delete all the sites and data that you own. This action cannot be reversed. Are you sure?"]) %> <%= link("Delete my account", to: "/me", class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete", data: [confirm: "Deleting your account will also delete all the sites and data that you own. This action cannot be reversed. Are you sure?"]) %>

View File

@ -0,0 +1,15 @@
defmodule Plausible.ClickhouseRepo.Migrations.AddUtmContentAndTerm do
use Ecto.Migration
def change do
alter table(:events) do
add :utm_content, :string
add :utm_term, :string
end
alter table(:sessions) do
add :utm_content, :string
add :utm_term, :string
end
end
end

View File

@ -21,6 +21,8 @@ defmodule Plausible.Session.StoreTest do
utm_medium: "medium", utm_medium: "medium",
utm_source: "source", utm_source: "source",
utm_campaign: "campaign", utm_campaign: "campaign",
utm_content: "content",
utm_term: "term",
browser: "browser", browser: "browser",
browser_version: "55", browser_version: "55",
country_code: "EE", country_code: "EE",
@ -47,6 +49,8 @@ defmodule Plausible.Session.StoreTest do
assert session.utm_medium == event.utm_medium assert session.utm_medium == event.utm_medium
assert session.utm_source == event.utm_source assert session.utm_source == event.utm_source
assert session.utm_campaign == event.utm_campaign assert session.utm_campaign == event.utm_campaign
assert session.utm_content == event.utm_content
assert session.utm_term == event.utm_term
assert session.country_code == event.country_code assert session.country_code == event.country_code
assert session.screen_size == event.screen_size assert session.screen_size == event.screen_size
assert session.operating_system == event.operating_system assert session.operating_system == event.operating_system

View File

@ -48,6 +48,8 @@ defmodule Plausible.Factory do
utm_medium: "", utm_medium: "",
utm_source: "", utm_source: "",
utm_campaign: "", utm_campaign: "",
utm_content: "",
utm_term: "",
entry_page: "/", entry_page: "/",
pageviews: 1, pageviews: 1,
events: 1, events: 1,
@ -88,6 +90,8 @@ defmodule Plausible.Factory do
utm_medium: "", utm_medium: "",
utm_source: "", utm_source: "",
utm_campaign: "", utm_campaign: "",
utm_content: "",
utm_term: "",
browser: "", browser: "",
browser_version: "", browser_version: "",
country_code: "", country_code: "",