diff --git a/admin/app/admin/jammers_subscription_cohorts.rb b/admin/app/admin/jammers_subscription_cohorts.rb new file mode 100644 index 000000000..2f3443492 --- /dev/null +++ b/admin/app/admin/jammers_subscription_cohorts.rb @@ -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 \ No newline at end of file diff --git a/jam-ui/src/helpers/MetaTracking.js b/jam-ui/src/helpers/MetaTracking.js index 27720b803..c901e5c42 100644 --- a/jam-ui/src/helpers/MetaTracking.js +++ b/jam-ui/src/helpers/MetaTracking.js @@ -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')) { 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 4aa6558a6..a89fedbfb 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -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 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); + } } } },