From e82450dfe990ac63ef17f66c67f5a3749a0fbf3d Mon Sep 17 00:00:00 2001 From: Seth Call Date: Fri, 23 Jan 2026 23:17:35 -0600 Subject: [PATCH] Support more utm tracking --- .../app/admin/jammers_subscription_cohorts.rb | 80 +++++++++++++++++-- jam-ui/src/helpers/MetaTracking.js | 17 ++-- ...0260123205632_add_extended_utm_to_users.rb | 31 +++++++ ruby/lib/jam_ruby/models/user.rb | 6 ++ web/app/assets/javascripts/meta_tracking.js | 23 ++++-- 5 files changed, 139 insertions(+), 18 deletions(-) create mode 100644 ruby/db/migrate/20260123205632_add_extended_utm_to_users.rb diff --git a/admin/app/admin/jammers_subscription_cohorts.rb b/admin/app/admin/jammers_subscription_cohorts.rb index 0fc2bf1f6..2f3443492 100644 --- a/admin/app/admin/jammers_subscription_cohorts.rb +++ b/admin/app/admin/jammers_subscription_cohorts.rb @@ -32,7 +32,14 @@ ActiveAdmin.register_page "Jammers Subscription Cohorts" do filter_type = params[:filter_type] || 'All' filter_campaign = params[:filter_campaign] + filter_campaign_id = params[:filter_campaign_id] + filter_ad_set = params[:filter_ad_set] + filter_ad_name = params[:filter_ad_name] + campaigns = User.where("origin_utm_source ILIKE '%meta%'").distinct.pluck(:origin_utm_campaign).compact.sort + campaign_ids = User.where("origin_utm_source ILIKE '%meta%'").distinct.pluck(:origin_id).compact.sort + ad_sets = User.where("origin_utm_source ILIKE '%meta%'").distinct.pluck(:origin_term).compact.sort + ad_names = User.where("origin_utm_source ILIKE '%meta%'").distinct.pluck(:origin_content).compact.sort div style: "margin-bottom: 20px; padding: 10px; background-color: #f4f4f4; border-radius: 4px;" do form action: admin_jammers_subscription_cohorts_path, method: :get do @@ -44,11 +51,43 @@ ActiveAdmin.register_page "Jammers Subscription Cohorts" do end if filter_type == 'Advertising' - span "Campaign: ", style: "font-weight: bold; margin-right: 5px;" - select name: 'filter_campaign', onchange: 'this.form.submit()', style: "margin-right: 15px;" do - option "All Campaigns", value: '' - campaigns.each do |c| - option c, value: c, selected: filter_campaign == c + div style: "margin-top: 10px;" do + span "Campaign Name: ", style: "font-weight: bold; margin-right: 5px;" + select name: 'filter_campaign', onchange: 'this.form.submit()', style: "margin-right: 15px;" do + option "All", value: '' + option "Null", value: 'NULL', selected: filter_campaign == 'NULL' + campaigns.each do |c| + option c, value: c, selected: filter_campaign == c + end + end + + span "Campaign ID: ", style: "font-weight: bold; margin-right: 5px;" + select name: 'filter_campaign_id', onchange: 'this.form.submit()', style: "margin-right: 15px;" do + option "All", value: '' + option "Null", value: 'NULL', selected: filter_campaign_id == 'NULL' + campaign_ids.each do |c| + option c, value: c, selected: filter_campaign_id == c + end + end + end + + div style: "margin-top: 10px;" do + span "Ad Set: ", style: "font-weight: bold; margin-right: 5px;" + select name: 'filter_ad_set', onchange: 'this.form.submit()', style: "margin-right: 15px;" do + option "All", value: '' + option "Null", value: 'NULL', selected: filter_ad_set == 'NULL' + ad_sets.each do |c| + option c, value: c, selected: filter_ad_set == c + end + end + + span "Ad Name: ", style: "font-weight: bold; margin-right: 5px;" + select name: 'filter_ad_name', onchange: 'this.form.submit()', style: "margin-right: 15px;" do + option "All", value: '' + option "Null", value: 'NULL', selected: filter_ad_name == 'NULL' + ad_names.each do |c| + option c, value: c, selected: filter_ad_name == c + end end end end @@ -87,8 +126,37 @@ ActiveAdmin.register_page "Jammers Subscription Cohorts" do query = query.where("users.origin_utm_source = 'organic'") elsif filter_type == 'Advertising' query = query.where("users.origin_utm_source ILIKE '%meta%'") + if filter_campaign.present? - query = query.where("users.origin_utm_campaign = ?", filter_campaign) + if filter_campaign == 'NULL' + query = query.where("users.origin_utm_campaign IS NULL") + else + query = query.where("users.origin_utm_campaign = ?", filter_campaign) + end + end + + if filter_campaign_id.present? + if filter_campaign_id == 'NULL' + query = query.where("users.origin_id IS NULL") + else + query = query.where("users.origin_id = ?", filter_campaign_id) + end + end + + if filter_ad_set.present? + if filter_ad_set == 'NULL' + query = query.where("users.origin_term IS NULL") + else + query = query.where("users.origin_term = ?", filter_ad_set) + end + end + + if filter_ad_name.present? + if filter_ad_name == 'NULL' + query = query.where("users.origin_content IS NULL") + else + query = query.where("users.origin_content = ?", filter_ad_name) + end end end diff --git a/jam-ui/src/helpers/MetaTracking.js b/jam-ui/src/helpers/MetaTracking.js index 046119065..bfa752cd9 100644 --- a/jam-ui/src/helpers/MetaTracking.js +++ b/jam-ui/src/helpers/MetaTracking.js @@ -32,11 +32,18 @@ const MetaTracking = { }, handleUtm: function (searchParams) { - const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; - utmParams.forEach(param => { - const value = this.getQueryParam(param, searchParams); - if (value) { - this.setCookie(param, value, 90); + if (!searchParams) return; + + const query = searchParams.substring(1); + const vars = query.split('&'); + vars.forEach(v => { + const pair = v.split('='); + if (pair.length === 2) { + const key = decodeURIComponent(pair[0]); + const value = decodeURIComponent(pair[1]); + if (key.indexOf('utm_') === 0) { + this.setCookie(key, value, 90); + } } }); }, diff --git a/ruby/db/migrate/20260123205632_add_extended_utm_to_users.rb b/ruby/db/migrate/20260123205632_add_extended_utm_to_users.rb new file mode 100644 index 000000000..5fd6be8e7 --- /dev/null +++ b/ruby/db/migrate/20260123205632_add_extended_utm_to_users.rb @@ -0,0 +1,31 @@ +class AddExtendedUtmToUsers < ActiveRecord::Migration[5.0] + def up + execute <<-SQL + ALTER TABLE users ADD COLUMN origin_id character varying; + ALTER TABLE users ADD COLUMN origin_term character varying; + ALTER TABLE users ADD COLUMN origin_content character varying; + + CREATE INDEX index_users_on_origin_id ON users (origin_id); + CREATE INDEX index_users_on_origin_term ON users (origin_term); + CREATE INDEX index_users_on_origin_content ON users (origin_content); + + CREATE INDEX index_users_on_origin_utm_source ON users (origin_utm_source); + CREATE INDEX index_users_on_origin_utm_medium ON users (origin_utm_medium); + SQL + end + + def down + execute <<-SQL + DROP INDEX IF EXISTS index_users_on_origin_utm_medium; + DROP INDEX IF EXISTS index_users_on_origin_utm_source; + + DROP INDEX IF EXISTS index_users_on_origin_content; + DROP INDEX IF EXISTS index_users_on_origin_term; + DROP INDEX IF EXISTS index_users_on_origin_id; + + ALTER TABLE users DROP COLUMN IF EXISTS origin_content; + ALTER TABLE users DROP COLUMN IF EXISTS origin_term; + ALTER TABLE users DROP COLUMN IF EXISTS origin_id; + SQL + end +end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 5a35c2502..9acff8b6e 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -1664,11 +1664,17 @@ module JamRuby user.origin_utm_source = origin["utm_source"] user.origin_utm_medium = origin["utm_medium"] user.origin_utm_campaign = origin["utm_campaign"] + user.origin_id = origin["utm_id"] + user.origin_term = origin["utm_term"] + user.origin_content = origin["utm_content"] user.origin_referrer = origin["referrer"] else user.origin_utm_source = 'organic' user.origin_utm_medium = 'organic' user.origin_utm_campaign = nil + user.origin_id = nil + user.origin_term = nil + user.origin_content = nil user.origin_referrer = nil end diff --git a/web/app/assets/javascripts/meta_tracking.js b/web/app/assets/javascripts/meta_tracking.js index 08bb0b508..1cd8a8684 100644 --- a/web/app/assets/javascripts/meta_tracking.js +++ b/web/app/assets/javascripts/meta_tracking.js @@ -35,14 +35,23 @@ }, handleUtm: function (searchParams) { - var utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; var self = this; - // forEach not supported in IE8, but this is modern enough or we can use for loop - for (var i = 0; i < utmParams.length; i++) { - var param = utmParams[i]; - var value = self.getQueryParam(param, searchParams); - if (value) { - self.setCookie(param, value, 90); + if (!searchParams) return; + + // Logically, we want to capture all utm_ parameters. + // We can either iterate a list or dynamic regex. + // Given the requirement to be robust, let's look for "utm_" + + var query = searchParams.substring(1); // remove '?' + var vars = query.split('&'); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split('='); + if (pair.length === 2) { + var key = decodeURIComponent(pair[0]); + var value = decodeURIComponent(pair[1]); + if (key.indexOf('utm_') === 0) { + self.setCookie(key, value, 90); + } } } },