diff --git a/admin/app/admin/jammers_subscription_cohorts.rb b/admin/app/admin/jammers_subscription_cohorts.rb index 336e86ae9..24075e5ed 100644 --- a/admin/app/admin/jammers_subscription_cohorts.rb +++ b/admin/app/admin/jammers_subscription_cohorts.rb @@ -29,8 +29,36 @@ 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] + campaigns = User.where("origin_utm_source ILIKE '%facebook%'").distinct.pluck(:origin_utm_campaign).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' + 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 + end + end + end + noscript { input type: :submit, value: "Filter" } + end + end + h2 "Users Grouped By Month as Paying Subscribers" - table_for User.select(%Q{date_trunc('month', users.created_at) as month, + + 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, @@ -54,7 +82,17 @@ ActiveAdmin.register_page "Jammers Subscription Cohorts" do j.created_at LIMIT 1 -- Select only that single row ) j ON TRUE }) - .group("date_trunc('month', users.created_at)") + + if filter_type == 'Organic' + query = query.where("users.origin_utm_source = 'organic'") + elsif filter_type == 'Advertising' + query = query.where("users.origin_utm_source ILIKE '%facebook%'") + if filter_campaign.present? + query = query.where("users.origin_utm_campaign = ?", filter_campaign) + 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') } diff --git a/ruby/db/migrate/20260114075739_add_facebook_tracking_to_users.rb b/ruby/db/migrate/20260114075739_add_facebook_tracking_to_users.rb new file mode 100644 index 000000000..dae615be5 --- /dev/null +++ b/ruby/db/migrate/20260114075739_add_facebook_tracking_to_users.rb @@ -0,0 +1,11 @@ +class AddFacebookTrackingToUsers < ActiveRecord::Migration + def up + add_column :users, :facebook_click_id, :string + add_column :users, :facebook_browser_id, :string + end + + def down + remove_column :users, :facebook_click_id + remove_column :users, :facebook_browser_id + end +end diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index ac8de9372..328354bb8 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -56,6 +56,7 @@ require "jam_ruby/lib/em_helper" require "jam_ruby/lib/nav" require "jam_ruby/lib/html_sanitize" require "jam_ruby/lib/guitar_center" +require "jam_ruby/lib/capi_transmitter" require "jam_ruby/subscription_definitions" require "jam_ruby/resque/resque_jam_error" require "jam_ruby/resque/resque_hooks" diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index d7a3f74b8..5c566e8e2 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -76,7 +76,7 @@ module JamRuby after_save :update_teacher_pct attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :used_current_month, :used_month_play_time, - :v2_photo_url, :v2_photo_uploaded + :v2_photo_url, :v2_photo_uploaded, :facebook_click_id, :facebook_browser_id # updating_password corresponds to a lost_password attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card, :purchase_required, :user_type @@ -542,6 +542,21 @@ module JamRuby @updating_progression_field = true if self[field_name].nil? self[field_name] = time + + # CAPI Hooks + begin + case field_name.to_s + when 'first_ran_client_at' + CapiTransmitter.send_event('OpenClient', self) + when 'first_certified_gear_at' + CapiTransmitter.send_event('CompleteGearWizard', self) + when 'first_music_session_at' + CapiTransmitter.send_event('FirstMusicSession', self) + end + rescue => e + Rails.logger.error("Error sending CAPI event: #{e.message}") + end + self.save end end @@ -1579,6 +1594,8 @@ module JamRuby license_end = options[:license_end] import_source = options[:import_source] desired_plan_code = options[:desired_plan_code] + facebook_click_id = options[:facebook_click_id] + facebook_browser_id = options[:facebook_browser_id] if desired_plan_code == '' desired_plan_code = nil @@ -1640,6 +1657,8 @@ module JamRuby musician = true end user.musician = !!musician + user.facebook_click_id = facebook_click_id + user.facebook_browser_id = facebook_browser_id if origin user.origin_utm_source = origin["utm_source"] diff --git a/web/app/assets/javascripts/meta_tracking.js b/web/app/assets/javascripts/meta_tracking.js new file mode 100644 index 000000000..764c839e2 --- /dev/null +++ b/web/app/assets/javascripts/meta_tracking.js @@ -0,0 +1,94 @@ +/** + * meta_tracking.js + * A standalone module to capture and persist Meta attribution signals (fbclid, _fbp) in cookies. + * + * Logic adapted for legacy environment (no React hooks). + * - Checks URL for `fbclid` and sets `_fbc` cookie. + * - Checks for `_fbp` cookie; if missing, generates and sets it. + */ + +(function(window, document) { + 'use strict'; + + var MetaTracking = { + init: function() { + var location = window.location; + this.handleFbc(location.search); + this.handleFbp(); + }, + + // 1. Parsing and storing _fbc (Click ID) + handleFbc: function(searchParams) { + var fbclid = this.getQueryParam('fbclid', searchParams); + + if (fbclid) { + var version = 'fb'; + var subdomainIndex = 1; // 1 = example.com + var creationTime = new Date().getTime(); // Unix timestamp in ms + + // Format: fb.1.timestamp.id + var fbcValue = version + '.' + subdomainIndex + '.' + creationTime + '.' + fbclid; + + this.setCookie('_fbc', fbcValue, 90); + } + }, + + // 2. Handling _fbp (Browser ID) + handleFbp: function() { + if (!this.getCookie('_fbp')) { + var version = 'fb'; + var subdomainIndex = 1; + var creationTime = new Date().getTime(); + var randomInt = Math.floor(Math.random() * 10000000000); // 10-digit random number + + // Format: fb.1.timestamp.randomDigits + var fbpValue = version + '.' + subdomainIndex + '.' + creationTime + '.' + randomInt; + + this.setCookie('_fbp', fbpValue, 90); + } + }, + + // Helper: Get query param by name + getQueryParam: function(name, search) { + name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); + var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); + var results = regex.exec(search); + return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); + }, + + // Helper: Set cookie + setCookie: function(name, value, days) { + var expires = ""; + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toUTCString(); + } + // Ensure path is root and domain is included if needed (defaults to current host) + document.cookie = name + "=" + (value || "") + expires + "; path=/"; + }, + + // Helper: Get cookie + getCookie: function(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; + } + }; + + // Initialize on ready + if (document.readyState === 'complete' || document.readyState === 'interactive') { + MetaTracking.init(); + } else { + // IE9+ support for DOMContentLoaded + document.addEventListener('DOMContentLoaded', function() { + MetaTracking.init(); + }); + } + +})(window, document); diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 39bb00c90..14f9df910 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -158,7 +158,9 @@ class ApiUsersController < ApiController affiliate_referral_id: cookies[:affiliate_visitor], origin: origin_cookie, test_drive_package: params[:test_drive_package], - timezone: current_timezone + timezone: current_timezone, + facebook_click_id: cookies[:_fbc], + facebook_browser_id: cookies[:_fbp] } options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options) diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index df463954f..bd345cd61 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -192,7 +192,11 @@ class UsersController < ApplicationController affiliate_partner: @affiliate_partner, timezone: current_timezone, origin: origin_cookie, - desired_plan_code: desired_plan_code ) + timezone: current_timezone, + origin: origin_cookie, + desired_plan_code: desired_plan_code, + facebook_click_id: cookies[:_fbc], + facebook_browser_id: cookies[:_fbp]) rend = _render('new') # check for errors diff --git a/web/config/application.rb b/web/config/application.rb index 235f72847..3575d202c 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -172,6 +172,11 @@ if defined?(Bundler) config.google_secret = 'UwzIcvtErv9c2-GIsNfIo7bA' config.google_email = '785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8@developer.gserviceaccount.com' config.google_public_server_key = "AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo" + + # Facebook Conversions API + config.facebook_pixel_id = "1234567890" # Replace with valid default or remove if strictly env-based + config.facebook_access_token = "placeholder" # Replace with valid default or remove if strictly env-based + config.facebook_conversion_api_tls = true # Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more. config.recurly_private_api_key = '55f2fdfa4d014e64a94eaba1e93f39bb' diff --git a/web/lib/tasks/test_capi.rake b/web/lib/tasks/test_capi.rake new file mode 100644 index 000000000..9b6edf158 --- /dev/null +++ b/web/lib/tasks/test_capi.rake @@ -0,0 +1,25 @@ +namespace :capi do + desc "Test Facebook CAPI connection" + task :test_connection => :environment do + puts "Testing CAPI connection..." + pixel_id = APP_CONFIG[:facebook_pixel_id] + access_token = APP_CONFIG[:facebook_access_token] + + puts "Pixel ID: #{pixel_id}" + puts "Access Token: #{access_token.try(:truncate, 10)}" + + if pixel_id.blank? || access_token.blank? || access_token == 'placeholder' + puts "WARNING: Configuration missing or placeholder." + end + + # Mock user + user = User.last + if user + puts "Sending test event 'TestEvent' for User ID: #{user.id}" + CapiTransmitter.send_event('TestEvent', user) + puts "Check Rails log for output." + else + puts "No user found in database." + end + end +end diff --git a/web/lib/user_manager.rb b/web/lib/user_manager.rb index 91c85ac67..a7b0ec74b 100644 --- a/web/lib/user_manager.rb +++ b/web/lib/user_manager.rb @@ -52,6 +52,8 @@ class UserManager < BaseManager license_end = options[:license_end] import_source = options[:import_source] desired_plan_code = options[:desired_plan_code] + facebook_click_id = options[:facebook_click_id] + facebook_browser_id = options[:facebook_browser_id] recaptcha_failed = false unless options[:skip_recaptcha] # allow callers to opt-of recaptcha @@ -114,7 +116,10 @@ class UserManager < BaseManager license_start: license_start, license_end: license_end, import_source: import_source, - desired_plan_code: desired_plan_code) + import_source: import_source, + desired_plan_code: desired_plan_code, + facebook_click_id: facebook_click_id, + facebook_browser_id: facebook_browser_id) user end