mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 01:22:15 +03:00
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:
parent
231c72e8e8
commit
48ad7485c8
@ -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
|
||||
- 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
|
||||
- New UTM Tags `utm_content` and `utm_term` plausible/analytics#515
|
||||
|
||||
### Fixed
|
||||
- UI fix where multi-line text in pills would not be underlined properly on small screens.
|
||||
|
@ -30,6 +30,8 @@ export function parseQuery(querystring, site) {
|
||||
'utm_medium': q.get('utm_medium'),
|
||||
'utm_source': q.get('utm_source'),
|
||||
'utm_campaign': q.get('utm_campaign'),
|
||||
'utm_content': q.get('utm_content'),
|
||||
'utm_term': q.get('utm_term'),
|
||||
'referrer': q.get('referrer'),
|
||||
'screen': q.get('screen'),
|
||||
'browser': q.get('browser'),
|
||||
@ -159,6 +161,8 @@ export const formattedFilters = {
|
||||
'utm_medium': 'UTM Medium',
|
||||
'utm_source': 'UTM Source',
|
||||
'utm_campaign': 'UTM Campaign',
|
||||
'utm_content': 'UTM Content',
|
||||
'utm_term': 'UTM Term',
|
||||
'referrer': 'Referrer URL',
|
||||
'screen': 'Screen size',
|
||||
'browser': 'Browser',
|
||||
|
@ -30,7 +30,7 @@ export default function Router({site, loggedIn, currentUserRole}) {
|
||||
<ScrollToTop />
|
||||
<Dash site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} />
|
||||
<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} />
|
||||
</Route>
|
||||
<Route exact path="/:domain/referrers/Google">
|
||||
|
@ -10,7 +10,9 @@ const TITLES = {
|
||||
sources: 'Top Sources',
|
||||
utm_mediums: 'Top UTM mediums',
|
||||
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 {
|
||||
@ -84,6 +86,8 @@ class SourcesModal extends React.Component {
|
||||
if (filter === 'utm_mediums') query.set('utm_medium', 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_contents') query.set('utm_content', source.name)
|
||||
if (filter === 'utm_terms') query.set('utm_term', source.name)
|
||||
|
||||
console.log(source)
|
||||
|
||||
|
@ -138,9 +138,11 @@ class AllSources extends React.Component {
|
||||
}
|
||||
|
||||
const UTM_TAGS = {
|
||||
utm_medium: {label: 'UTM Medium', endpoint: 'utm_mediums'},
|
||||
utm_source: {label: 'UTM Source', endpoint: 'utm_sources'},
|
||||
utm_campaign: {label: 'UTM Campaign', endpoint: 'utm_campaigns'},
|
||||
utm_medium: {label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: 'utm_mediums'},
|
||||
utm_source: {label: 'UTM Source', shortLabel: 'UTM Source', endpoint: 'utm_sources'},
|
||||
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 {
|
||||
@ -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 {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
@ -284,15 +291,57 @@ export default class SourceList extends React.Component {
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
const activeClass = 'inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold active-prop-heading'
|
||||
const defaultClass = 'hover:text-indigo-600 cursor-pointer'
|
||||
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 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 (
|
||||
<ul 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>
|
||||
<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>
|
||||
<li className={this.state.tab === 'utm_campaign' ? activeClass : defaultClass} onClick={this.setTab('utm_campaign')}>Campaign</li>
|
||||
</ul>
|
||||
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
|
||||
<div className={this.state.tab === 'all' ? activeClass : defaultClass} onClick={this.setTab('all')}>All</div>
|
||||
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<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} />
|
||||
} 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} />
|
||||
} 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} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ defmodule Plausible.ClickhouseEvent do
|
||||
field :utm_medium, :string, default: ""
|
||||
field :utm_source, :string, default: ""
|
||||
field :utm_campaign, :string, default: ""
|
||||
field :utm_content, :string, default: ""
|
||||
field :utm_term, :string, default: ""
|
||||
|
||||
field :country_code, :string, default: ""
|
||||
field :subdivision1_code, :string, default: ""
|
||||
@ -53,6 +55,8 @@ defmodule Plausible.ClickhouseEvent do
|
||||
:utm_medium,
|
||||
:utm_source,
|
||||
:utm_campaign,
|
||||
:utm_content,
|
||||
:utm_term,
|
||||
:country_code,
|
||||
:subdivision1_code,
|
||||
:subdivision2_code,
|
||||
|
@ -21,6 +21,8 @@ defmodule Plausible.ClickhouseSession do
|
||||
field :utm_medium, :string
|
||||
field :utm_source, :string
|
||||
field :utm_campaign, :string
|
||||
field :utm_content, :string
|
||||
field :utm_term, :string
|
||||
field :referrer, :string
|
||||
field :referrer_source, :string
|
||||
|
||||
@ -60,6 +62,8 @@ defmodule Plausible.ClickhouseSession do
|
||||
:utm_medium,
|
||||
:utm_source,
|
||||
:utm_campaign,
|
||||
:utm_content,
|
||||
:utm_term,
|
||||
:country_code,
|
||||
:country_geoname_id,
|
||||
:subdivision1_code,
|
||||
|
@ -113,6 +113,8 @@ defmodule Plausible.Session.Store do
|
||||
utm_medium: event.utm_medium,
|
||||
utm_source: event.utm_source,
|
||||
utm_campaign: event.utm_campaign,
|
||||
utm_content: event.utm_content,
|
||||
utm_term: event.utm_term,
|
||||
country_code: event.country_code,
|
||||
subdivision1_code: event.subdivision1_code,
|
||||
subdivision2_code: event.subdivision2_code,
|
||||
|
@ -314,6 +314,8 @@ defmodule Plausible.Stats.Base do
|
||||
defp db_prop_val(:utm_medium, @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_content, @no_ref), do: ""
|
||||
defp db_prop_val(:utm_term, @no_ref), do: ""
|
||||
defp db_prop_val(_, val), do: val
|
||||
|
||||
defp utc_boundaries(%Query{period: "realtime"}, _timezone) do
|
||||
|
@ -138,7 +138,9 @@ defmodule Plausible.Stats.Breakdown do
|
||||
"visit:source",
|
||||
"visit:utm_medium",
|
||||
"visit:utm_source",
|
||||
"visit:utm_campaign"
|
||||
"visit:utm_campaign",
|
||||
"visit:utm_content",
|
||||
"visit:utm_term"
|
||||
] do
|
||||
query = Query.treat_page_filter_as_entry_page(query)
|
||||
breakdown_sessions(site, query, property, metrics, pagination)
|
||||
@ -412,6 +414,26 @@ defmodule Plausible.Stats.Breakdown do
|
||||
)
|
||||
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
|
||||
from(
|
||||
s in q,
|
||||
|
@ -248,6 +248,22 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
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_exit(q, query.filters["exit_page"])
|
||||
@ -342,6 +358,22 @@ defmodule Plausible.Stats.Clickhouse do
|
||||
q
|
||||
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 =
|
||||
if query.filters["referrer"] do
|
||||
ref = query.filters["referrer"]
|
||||
|
@ -200,6 +200,18 @@ defmodule Plausible.Stats.FilterSuggestions do
|
||||
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" ->
|
||||
from(e in q,
|
||||
select: {e.referrer},
|
||||
|
@ -5,6 +5,8 @@ defmodule Plausible.Stats.Filters do
|
||||
"utm_medium",
|
||||
"utm_source",
|
||||
"utm_campaign",
|
||||
"utm_content",
|
||||
"utm_term",
|
||||
"screen",
|
||||
"device",
|
||||
"browser",
|
||||
|
@ -104,6 +104,8 @@ defmodule PlausibleWeb.Api.ExternalController do
|
||||
utm_medium: query["utm_medium"],
|
||||
utm_source: query["utm_source"],
|
||||
utm_campaign: query["utm_campaign"],
|
||||
utm_content: query["utm_content"],
|
||||
utm_term: query["utm_term"],
|
||||
country_code: location_details[:country_code],
|
||||
country_geoname_id: location_details[:country_geoname_id],
|
||||
subdivision1_code: location_details[:subdivision1_code],
|
||||
|
@ -287,6 +287,44 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
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
|
||||
site = conn.assigns[:site]
|
||||
|
||||
|
@ -56,6 +56,8 @@ defmodule PlausibleWeb.Router do
|
||||
get "/:domain/utm_mediums", StatsController, :utm_mediums
|
||||
get "/:domain/utm_sources", StatsController, :utm_sources
|
||||
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/pages", StatsController, :pages
|
||||
get "/:domain/entry-pages", StatsController, :entry_pages
|
||||
|
@ -273,7 +273,7 @@
|
||||
<p class="dark:text-gray-100">Deleting your account removes all sites and stats you've collected</p>
|
||||
|
||||
<%= 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>
|
||||
<% 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?"]) %>
|
||||
|
@ -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
|
@ -21,6 +21,8 @@ defmodule Plausible.Session.StoreTest do
|
||||
utm_medium: "medium",
|
||||
utm_source: "source",
|
||||
utm_campaign: "campaign",
|
||||
utm_content: "content",
|
||||
utm_term: "term",
|
||||
browser: "browser",
|
||||
browser_version: "55",
|
||||
country_code: "EE",
|
||||
@ -47,6 +49,8 @@ defmodule Plausible.Session.StoreTest do
|
||||
assert session.utm_medium == event.utm_medium
|
||||
assert session.utm_source == event.utm_source
|
||||
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.screen_size == event.screen_size
|
||||
assert session.operating_system == event.operating_system
|
||||
|
@ -48,6 +48,8 @@ defmodule Plausible.Factory do
|
||||
utm_medium: "",
|
||||
utm_source: "",
|
||||
utm_campaign: "",
|
||||
utm_content: "",
|
||||
utm_term: "",
|
||||
entry_page: "/",
|
||||
pageviews: 1,
|
||||
events: 1,
|
||||
@ -88,6 +90,8 @@ defmodule Plausible.Factory do
|
||||
utm_medium: "",
|
||||
utm_source: "",
|
||||
utm_campaign: "",
|
||||
utm_content: "",
|
||||
utm_term: "",
|
||||
browser: "",
|
||||
browser_version: "",
|
||||
country_code: "",
|
||||
|
Loading…
Reference in New Issue
Block a user