From 48ad7485c88bfbb2cab2e7e662b277f7fd4e47c3 Mon Sep 17 00:00:00 2001 From: Uku Taht Date: Thu, 16 Dec 2021 11:02:09 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + assets/js/dashboard/query.js | 4 + assets/js/dashboard/router.js | 2 +- assets/js/dashboard/stats/modals/sources.js | 6 +- .../js/dashboard/stats/sources/source-list.js | 75 ++++++++++++++++--- lib/plausible/event/clickhouse_schema.ex | 4 + lib/plausible/session/clickhouse_schema.ex | 4 + lib/plausible/session/store.ex | 2 + lib/plausible/stats/base.ex | 2 + lib/plausible/stats/breakdown.ex | 24 +++++- lib/plausible/stats/clickhouse.ex | 32 ++++++++ lib/plausible/stats/filter_suggestions.ex | 12 +++ lib/plausible/stats/filters.ex | 2 + .../controllers/api/external_controller.ex | 2 + .../controllers/api/stats_controller.ex | 38 ++++++++++ lib/plausible_web/router.ex | 2 + .../templates/auth/user_settings.html.eex | 2 +- ...0211017093035_add_utm_content_and_term.exs | 15 ++++ test/plausible/session/store_test.exs | 4 + test/support/factory.ex | 4 + 20 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 priv/clickhouse_repo/migrations/20211017093035_add_utm_content_and_term.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 019553734..60ac98a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/assets/js/dashboard/query.js b/assets/js/dashboard/query.js index 74ec396c7..366d00f26 100644 --- a/assets/js/dashboard/query.js +++ b/assets/js/dashboard/query.js @@ -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', diff --git a/assets/js/dashboard/router.js b/assets/js/dashboard/router.js index 84df38d8a..7b05c0bcb 100644 --- a/assets/js/dashboard/router.js +++ b/assets/js/dashboard/router.js @@ -30,7 +30,7 @@ export default function Router({site, loggedIn, currentUserRole}) { - + diff --git a/assets/js/dashboard/stats/modals/sources.js b/assets/js/dashboard/stats/modals/sources.js index 334483a31..fd47b2bd6 100644 --- a/assets/js/dashboard/stats/modals/sources.js +++ b/assets/js/dashboard/stats/modals/sources.js @@ -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) diff --git a/assets/js/dashboard/stats/sources/source-list.js b/assets/js/dashboard/stats/sources/source-list.js index c19bf2fa8..3a16f125f 100644 --- a/assets/js/dashboard/stats/sources/source-list.js +++ b/assets/js/dashboard/stats/sources/source-list.js @@ -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 ( -
    -
  • All
  • -
  • Medium
  • -
  • Source
  • -
  • Campaign
  • -
+
+
All
+ + +
+ + {buttonText} + +
+ + + +
+ { dropdownOptions.map((option) => { + return ( + + {({ active }) => ( + + {UTM_TAGS[option].label} + + )} + + ) + })} +
+
+
+
+
) } @@ -305,6 +354,10 @@ export default class SourceList extends React.Component { return } else if (this.state.tab === 'utm_campaign') { return + } else if (this.state.tab === 'utm_content') { + return + } else if (this.state.tab === 'utm_term') { + return } } } diff --git a/lib/plausible/event/clickhouse_schema.ex b/lib/plausible/event/clickhouse_schema.ex index 8b2ae8f30..f5d5fe678 100644 --- a/lib/plausible/event/clickhouse_schema.ex +++ b/lib/plausible/event/clickhouse_schema.ex @@ -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, diff --git a/lib/plausible/session/clickhouse_schema.ex b/lib/plausible/session/clickhouse_schema.ex index 727d5e562..ccf653049 100644 --- a/lib/plausible/session/clickhouse_schema.ex +++ b/lib/plausible/session/clickhouse_schema.ex @@ -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, diff --git a/lib/plausible/session/store.ex b/lib/plausible/session/store.ex index ceab3cc1b..aa9daff06 100644 --- a/lib/plausible/session/store.ex +++ b/lib/plausible/session/store.ex @@ -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, diff --git a/lib/plausible/stats/base.ex b/lib/plausible/stats/base.ex index e2ea1b1d8..4bb34a9c4 100644 --- a/lib/plausible/stats/base.ex +++ b/lib/plausible/stats/base.ex @@ -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 diff --git a/lib/plausible/stats/breakdown.ex b/lib/plausible/stats/breakdown.ex index a0040053a..72eb1e651 100644 --- a/lib/plausible/stats/breakdown.ex +++ b/lib/plausible/stats/breakdown.ex @@ -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, diff --git a/lib/plausible/stats/clickhouse.ex b/lib/plausible/stats/clickhouse.ex index 698e716da..5fbb89255 100644 --- a/lib/plausible/stats/clickhouse.ex +++ b/lib/plausible/stats/clickhouse.ex @@ -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"] diff --git a/lib/plausible/stats/filter_suggestions.ex b/lib/plausible/stats/filter_suggestions.ex index def8f305e..569a3ceee 100644 --- a/lib/plausible/stats/filter_suggestions.ex +++ b/lib/plausible/stats/filter_suggestions.ex @@ -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}, diff --git a/lib/plausible/stats/filters.ex b/lib/plausible/stats/filters.ex index bdfde2f83..2091fe88d 100644 --- a/lib/plausible/stats/filters.ex +++ b/lib/plausible/stats/filters.ex @@ -5,6 +5,8 @@ defmodule Plausible.Stats.Filters do "utm_medium", "utm_source", "utm_campaign", + "utm_content", + "utm_term", "screen", "device", "browser", diff --git a/lib/plausible_web/controllers/api/external_controller.ex b/lib/plausible_web/controllers/api/external_controller.ex index b111b938f..35b823f28 100644 --- a/lib/plausible_web/controllers/api/external_controller.ex +++ b/lib/plausible_web/controllers/api/external_controller.ex @@ -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], diff --git a/lib/plausible_web/controllers/api/stats_controller.ex b/lib/plausible_web/controllers/api/stats_controller.ex index 0675b2419..62e38798f 100644 --- a/lib/plausible_web/controllers/api/stats_controller.ex +++ b/lib/plausible_web/controllers/api/stats_controller.ex @@ -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] diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index abd1a3032..df3d6d43a 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -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 diff --git a/lib/plausible_web/templates/auth/user_settings.html.eex b/lib/plausible_web/templates/auth/user_settings.html.eex index 8c4e94e40..a8020f035 100644 --- a/lib/plausible_web/templates/auth/user_settings.html.eex +++ b/lib/plausible_web/templates/auth/user_settings.html.eex @@ -273,7 +273,7 @@

Deleting your account removes all sites and stats you've collected

<%= if @subscription && @subscription.status == "active" do %> - Delete my account + Delete my account

Your account cannot be deleted because you have an active subscription. If you want to delete your account, please cancel your subscription first.

<% 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?"]) %> diff --git a/priv/clickhouse_repo/migrations/20211017093035_add_utm_content_and_term.exs b/priv/clickhouse_repo/migrations/20211017093035_add_utm_content_and_term.exs new file mode 100644 index 000000000..bd7f07158 --- /dev/null +++ b/priv/clickhouse_repo/migrations/20211017093035_add_utm_content_and_term.exs @@ -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 diff --git a/test/plausible/session/store_test.exs b/test/plausible/session/store_test.exs index 33f3220d5..aa39cb2c5 100644 --- a/test/plausible/session/store_test.exs +++ b/test/plausible/session/store_test.exs @@ -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 diff --git a/test/support/factory.ex b/test/support/factory.ex index b51cb0a6f..754668f83 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -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: "",