From 9145658597ef20536159b159786f3806678009c2 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 29 Aug 2025 20:46:52 +0200 Subject: [PATCH] chore: Refactor UTM params to stay compliant with standards (#12312) We were using UTM params on various branding urls which weren't compliant to standard utm params and hence were ignored by analytics tooling. this PR ensures that the params stays compliant with defined standard ref: https://en.wikipedia.org/wiki/UTM_parameters ## Changes - updated utm tags on widget and survey urls - added utm on helpcenter branding --------- Co-authored-by: Muhsin Keloth --- app/helpers/portal_helper.rb | 11 ++++++ app/javascript/shared/components/Branding.vue | 12 ++++--- .../public/api/v1/portals/_footer.html.erb | 12 +++---- spec/helpers/portal_helper_spec.rb | 36 +++++++++++++++++++ 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/app/helpers/portal_helper.rb b/app/helpers/portal_helper.rb index 3ed303556..e98f0a72d 100644 --- a/app/helpers/portal_helper.rb +++ b/app/helpers/portal_helper.rb @@ -74,6 +74,17 @@ module PortalHelper end end + def generate_portal_brand_url(brand_url, referer) + url = URI.parse(brand_url.to_s) + query_params = Rack::Utils.parse_query(url.query) + query_params['utm_medium'] = 'helpcenter' + query_params['utm_campaign'] = 'branding' + query_params['utm_source'] = URI.parse(referer).host if referer.present? && referer.match?(URI::DEFAULT_PARSER.make_regexp) + + url.query = query_params.to_query + url.to_s + end + def render_category_content(content) ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text end diff --git a/app/javascript/shared/components/Branding.vue b/app/javascript/shared/components/Branding.vue index 9ca976d80..2f1a8d5d0 100644 --- a/app/javascript/shared/components/Branding.vue +++ b/app/javascript/shared/components/Branding.vue @@ -33,13 +33,15 @@ export default { brandRedirectURL() { try { const referrerHost = this.$store.getters['appConfig/getReferrerHost']; - const baseURL = `${this.globalConfig.widgetBrandURL}?utm_source=${ - referrerHost ? 'widget_branding' : 'survey_branding' - }`; + const url = new URL(this.globalConfig.widgetBrandURL); if (referrerHost) { - return `${baseURL}&utm_referrer=${referrerHost}`; + url.searchParams.set('utm_source', referrerHost); + url.searchParams.set('utm_medium', 'widget'); + } else { + url.searchParams.set('utm_medium', 'survey'); } - return baseURL; + url.searchParams.set('utm_campaign', 'branding'); + return url.toString(); } catch (e) { // Suppressing the error as getter is not defined in some cases } diff --git a/app/views/public/api/v1/portals/_footer.html.erb b/app/views/public/api/v1/portals/_footer.html.erb index 8de87b8f2..bfba6235b 100644 --- a/app/views/public/api/v1/portals/_footer.html.erb +++ b/app/views/public/api/v1/portals/_footer.html.erb @@ -8,12 +8,12 @@ alt="<%= @global_config['BRAND_NAME'] %>" src="<%= @global_config['LOGO_THUMBNAIL'] %>" /> -

- <%= I18n.t('public_portal.footer.made_with') %> - - <%= @global_config['BRAND_NAME'] %> - -

+

+ <%= I18n.t('public_portal.footer.made_with') %> + + <%= @global_config['BRAND_NAME'] %> + +

diff --git a/spec/helpers/portal_helper_spec.rb b/spec/helpers/portal_helper_spec.rb index 241b4c6dd..d55c83be7 100644 --- a/spec/helpers/portal_helper_spec.rb +++ b/spec/helpers/portal_helper_spec.rb @@ -291,4 +291,40 @@ describe PortalHelper do end end end + + describe '#generate_portal_brand_url' do + it 'builds URL with UTM params and referer host as source (happy path)' do + result = helper.generate_portal_brand_url('https://brand.example.com', 'https://app.chatwoot.com/some/page') + uri = URI.parse(result) + params = Rack::Utils.parse_query(uri.query) + expect(uri.scheme).to eq('https') + expect(uri.host).to eq('brand.example.com') + expect(params['utm_medium']).to eq('helpcenter') + expect(params['utm_campaign']).to eq('branding') + expect(params['utm_source']).to eq('app.chatwoot.com') + end + + it 'returns utm string when brand_url is nil or empty' do + expect(helper.generate_portal_brand_url(nil, + 'https://app.chatwoot.com')).to eq( + '?utm_campaign=branding&utm_medium=helpcenter&utm_source=app.chatwoot.com' + ) + expect(helper.generate_portal_brand_url('', + 'https://app.chatwoot.com')).to eq( + '?utm_campaign=branding&utm_medium=helpcenter&utm_source=app.chatwoot.com' + ) + end + + it 'omits utm_source when referer is nil or invalid' do + r1 = helper.generate_portal_brand_url('https://brand.example.com', nil) + p1 = Rack::Utils.parse_query(URI.parse(r1).query) + expect(p1.key?('utm_source')).to be(false) + + r2 = helper.generate_portal_brand_url('https://brand.example.com', '::not-a-valid-url') + p2 = Rack::Utils.parse_query(URI.parse(r2).query) + expect(p2.key?('utm_source')).to be(false) + expect(p2['utm_medium']).to eq('helpcenter') + expect(p2['utm_campaign']).to eq('branding') + end + end end