Support more utm tracking

This commit is contained in:
Seth Call 2026-01-23 23:17:35 -06:00
parent bfbd266466
commit 3a4b900ebd
5 changed files with 251 additions and 7 deletions

View File

@ -0,0 +1,181 @@
class Spacer
def self.spacer(val, row)
percentage = ((val * 100) / row.total.to_f).round(1).to_s
('%-5.5s' % percentage).gsub(' ', ' ') + '% - ' + val.to_s
end
end
=begin
select
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
select first_name, last_name, email
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
AND first_music_session_at is NULL;
=end
ActiveAdmin.register_page "Jammers Subscription Cohorts" do
menu :parent => 'Reports'
content :title => "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
span "Source: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_type', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: 'All', selected: filter_type == 'All'
option "Organic", value: 'Organic', selected: filter_type == 'Organic'
option "Advertising", value: 'Advertising', selected: filter_type == 'Advertising'
end
if filter_type == 'Advertising'
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
noscript { input type: :submit, value: "Filter" }
end
end
h2 "Users Grouped By Month as Paying Subscribers"
query = User.select(%Q{date_trunc('month', users.created_at) as month,
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed,
count(first_played_jamtrack_at) as played_jamtrack
})
.joins(%Q{LEFT JOIN LATERAL (
SELECT
j.created_at
FROM
jam_track_rights as j
WHERE
j.user_id = users.id
ORDER BY
j.created_at
LIMIT 1 -- Select only that single row
) j ON TRUE })
if filter_type == 'Organic'
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?
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
table_for query.group("date_trunc('month', users.created_at)")
.where("j.created_at IS NULL OR (j.created_at - users.created_at) >= INTERVAL '2 hours'")
.order("date_trunc('month', users.created_at) DESC") do |row|
column "Month", Proc.new { |user| user.month.strftime('%B %Y') }
column "Total", :total
column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) }
column "Downloaded", Proc.new { |user| raw(Spacer.spacer(user.downloaded, user)) }
column "Ran Client", Proc.new { |user| raw(Spacer.spacer(user.ran_client, user)) }
column "FTUE", Proc.new { |user| raw(Spacer.spacer(user.ftue, user)) }
column "Any Session", Proc.new { |user| raw(Spacer.spacer(user.any_session, user)) }
column "2+ Session", Proc.new { |user| raw(Spacer.spacer(user.real_session, user)) }
column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) }
column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) }
column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) }
column "Played JT", Proc.new { |user| raw(Spacer.spacer(user.played_jamtrack, user)) }
end
end
end

View File

@ -11,6 +11,7 @@ const MetaTracking = {
init: function () {
const location = window.location;
this.handleFbc(location.search);
this.handleUtm(location.search);
this.handleFbp();
},
@ -30,6 +31,22 @@ const MetaTracking = {
}
},
handleUtm: function (searchParams) {
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);
}
}
});
},
// 2. Handling _fbp (Browser ID)
handleFbp: function () {
if (!this.getCookie('_fbp')) {

View File

@ -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

View File

@ -1587,11 +1587,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

View File

@ -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);
}
}
}
},