From 8e35b8e09fe19bbde9ff13b370ced52d2262d3fc Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Thu, 30 Oct 2025 19:59:31 +0100 Subject: [PATCH] Move UTM parameter tracking logic into a concern --- .app_version | 2 +- app/controllers/concerns/utm_trackable.rb | 33 ++++ .../users/registrations_controller.rb | 33 +--- spec/requests/users/registrations_spec.rb | 175 ++++++++++++++++++ 4 files changed, 218 insertions(+), 25 deletions(-) create mode 100644 app/controllers/concerns/utm_trackable.rb diff --git a/.app_version b/.app_version index 85e60ed1..cd46610f 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.34.0 +0.34.1 diff --git a/app/controllers/concerns/utm_trackable.rb b/app/controllers/concerns/utm_trackable.rb new file mode 100644 index 00000000..88ea41fb --- /dev/null +++ b/app/controllers/concerns/utm_trackable.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + + extend ActiveSupport::Concern + + UTM_PARAMS = %w[utm_source utm_medium utm_campaign utm_term utm_content].freeze + + def store_utm_params + UTM_PARAMS.each do |param| + session[param] = params[param] if params[param].present? + end + end + + def assign_utm_params(record) + utm_data = extract_utm_data_from_session + + return unless utm_data.any? + + record.update_columns(utm_data) + clear_utm_session + end + + private + + def extract_utm_data_from_session + UTM_PARAMS.each_with_object({}) do |param, hash| + hash[param] = session[param] if session[param].present? + end + end + + def clear_utm_session + UTM_PARAMS.each { |param| session.delete(param) } + end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 455d3300..3412bb08 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true class Users::RegistrationsController < Devise::RegistrationsController + include UtmTrackable + before_action :set_invitation, only: %i[new create] before_action :check_registration_allowed, only: %i[new create] - before_action :store_utm_params, only: %i[new] + before_action :store_utm_params, only: %i[new], unless: -> { DawarichSettings.self_hosted? } def new build_resource({}) @@ -67,8 +69,8 @@ class Users::RegistrationsController < Devise::RegistrationsController def invitation_token @invitation_token ||= params[:invitation_token] || - params.dig(:user, :invitation_token) || - session[:invitation_token] + params.dig(:user, :invitation_token) || + session[:invitation_token] end def accept_invitation_for_user(user) @@ -82,33 +84,16 @@ class Users::RegistrationsController < Devise::RegistrationsController if service.call flash[:notice] = "Welcome to #{@invitation.family.name}! You're now part of the family." else - flash[:alert] = "Account created successfully, but there was an issue accepting the invitation: #{service.error_message}" + flash[:alert] = + "Account created successfully, but there was an issue accepting the invitation: #{service.error_message}" end rescue StandardError => e Rails.logger.error "Error accepting invitation during registration: #{e.message}" - flash[:alert] = "Account created successfully, but there was an issue accepting the invitation. Please try accepting it again." + flash[:alert] = + 'Account created successfully, but there was an issue accepting the invitation. Please try accepting it again.' end def sign_up_params super end - - def store_utm_params - utm_params = %w[utm_source utm_medium utm_campaign utm_term utm_content] - utm_params.each do |param| - session[param] = params[param] if params[param].present? - end - end - - def assign_utm_params(user) - utm_params = %w[utm_source utm_medium utm_campaign utm_term utm_content] - utm_data = {} - - utm_params.each do |param| - utm_data[param] = session[param] if session[param].present? - session.delete(param) # Clean up session after assignment - end - - user.update_columns(utm_data) if utm_data.any? - end end diff --git a/spec/requests/users/registrations_spec.rb b/spec/requests/users/registrations_spec.rb index 96b1469f..efeac67c 100644 --- a/spec/requests/users/registrations_spec.rb +++ b/spec/requests/users/registrations_spec.rb @@ -325,4 +325,179 @@ RSpec.describe 'Users::Registrations', type: :request do end end end + + describe 'UTM Parameter Tracking' do + let(:utm_params) do + { + utm_source: 'google', + utm_medium: 'cpc', + utm_campaign: 'winter_2025', + utm_term: 'location_tracking', + utm_content: 'banner_ad' + } + end + + context 'when self-hosted mode is disabled' do + before do + allow(DawarichSettings).to receive(:self_hosted?).and_return(false) + end + + it 'captures UTM parameters from registration page URL' do + get new_user_registration_path, params: utm_params + + expect(response).to have_http_status(:ok) + expect(session[:utm_source]).to eq('google') + expect(session[:utm_medium]).to eq('cpc') + expect(session[:utm_campaign]).to eq('winter_2025') + expect(session[:utm_term]).to eq('location_tracking') + expect(session[:utm_content]).to eq('banner_ad') + end + + it 'stores UTM parameters in user record after registration' do + # Visit registration page with UTM params + get new_user_registration_path, params: utm_params + + # Create account + unique_email = "utm-user-#{Time.current.to_i}@example.com" + post user_registration_path, params: { + user: { + email: unique_email, + password: 'password123', + password_confirmation: 'password123' + } + } + + # Verify UTM params were saved to user + user = User.find_by(email: unique_email) + expect(user.utm_source).to eq('google') + expect(user.utm_medium).to eq('cpc') + expect(user.utm_campaign).to eq('winter_2025') + expect(user.utm_term).to eq('location_tracking') + expect(user.utm_content).to eq('banner_ad') + end + + it 'clears UTM parameters from session after registration' do + # Visit registration page with UTM params + get new_user_registration_path, params: utm_params + + # Create account + unique_email = "utm-cleanup-#{Time.current.to_i}@example.com" + post user_registration_path, params: { + user: { + email: unique_email, + password: 'password123', + password_confirmation: 'password123' + } + } + + # Verify session was cleaned up + expect(session[:utm_source]).to be_nil + expect(session[:utm_medium]).to be_nil + expect(session[:utm_campaign]).to be_nil + expect(session[:utm_term]).to be_nil + expect(session[:utm_content]).to be_nil + end + + it 'handles partial UTM parameters' do + partial_utm = { utm_source: 'twitter', utm_campaign: 'spring_promo' } + + get new_user_registration_path, params: partial_utm + + unique_email = "partial-utm-#{Time.current.to_i}@example.com" + post user_registration_path, params: { + user: { + email: unique_email, + password: 'password123', + password_confirmation: 'password123' + } + } + + user = User.find_by(email: unique_email) + expect(user.utm_source).to eq('twitter') + expect(user.utm_campaign).to eq('spring_promo') + expect(user.utm_medium).to be_nil + expect(user.utm_term).to be_nil + expect(user.utm_content).to be_nil + end + + it 'does not store empty UTM parameters' do + empty_utm = { + utm_source: '', + utm_medium: '', + utm_campaign: 'campaign_only' + } + + get new_user_registration_path, params: empty_utm + + unique_email = "empty-utm-#{Time.current.to_i}@example.com" + post user_registration_path, params: { + user: { + email: unique_email, + password: 'password123', + password_confirmation: 'password123' + } + } + + user = User.find_by(email: unique_email) + expect(user.utm_source).to be_nil + expect(user.utm_medium).to be_nil + expect(user.utm_campaign).to eq('campaign_only') + end + + it 'works with family invitations' do + get new_user_registration_path, params: utm_params.merge(invitation_token: invitation.token) + + post user_registration_path, params: { + user: { + email: invitation.email, + password: 'password123', + password_confirmation: 'password123' + }, + invitation_token: invitation.token + } + + user = User.find_by(email: invitation.email) + expect(user.utm_source).to eq('google') + expect(user.utm_campaign).to eq('winter_2025') + expect(user.family).to eq(family) + end + end + + context 'when self-hosted mode is enabled' do + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('SELF_HOSTED').and_return('true') + end + + it 'does not capture UTM parameters' do + # With valid invitation to allow registration in self-hosted mode + get new_user_registration_path, params: utm_params.merge(invitation_token: invitation.token) + + expect(session[:utm_source]).to be_nil + expect(session[:utm_medium]).to be_nil + expect(session[:utm_campaign]).to be_nil + end + + it 'does not store UTM parameters in user record' do + # With valid invitation to allow registration in self-hosted mode + get new_user_registration_path, params: utm_params.merge(invitation_token: invitation.token) + + post user_registration_path, params: { + user: { + email: invitation.email, + password: 'password123', + password_confirmation: 'password123' + }, + invitation_token: invitation.token + } + + user = User.find_by(email: invitation.email) + expect(user.utm_source).to be_nil + expect(user.utm_medium).to be_nil + expect(user.utm_campaign).to be_nil + expect(user.utm_term).to be_nil + expect(user.utm_content).to be_nil + end + end + end end