SM-08 remove GTM portal analytics path

This commit is contained in:
Kunthawat Greethong
2026-08-15 18:07:55 +07:00
parent d9bf4c45de
commit 5619cc38d5
7 changed files with 44 additions and 28 deletions

View File

@@ -26,12 +26,6 @@ const { replaceInstallationName } = useBranding();
// Provider keys and formats, mirroring Portal::ANALYTICS_CONFIG_FORMATS. Admin-only
// (also enforced on the backend) since these inject tracking scripts into public pages.
const ANALYTICS_PROVIDERS = [
{
key: 'gtm_container_id',
i18nKey: 'GTM',
icon: 'i-logos-google-tag-manager',
format: /^GTM-[A-Z0-9]+$/,
},
{
key: 'ga4_measurement_id',
i18nKey: 'GA4',

View File

@@ -48,7 +48,6 @@ module PortalConfigSchema
'analytics' => {
'type' => %w[object null],
'properties' => {
'gtm_container_id' => { 'type' => %w[string null] },
'ga4_measurement_id' => { 'type' => %w[string null] },
'hotjar_site_id' => { 'type' => %w[string null] },
'plausible_domain' => { 'type' => %w[string null] },

View File

@@ -56,7 +56,6 @@ class Portal < ApplicationRecord
# Analytics id fields and the format each must match. Formats keep values safe to
# interpolate into markup. Add a provider here and its snippet in _portal_analytics.html.erb.
ANALYTICS_CONFIG_FORMATS = {
'gtm_container_id' => /\AGTM-[A-Z0-9]+\z/,
'ga4_measurement_id' => /\AG-[A-Z0-9]+\z/,
'hotjar_site_id' => /\A\d+\z/,
'plausible_domain' => /\A[a-z0-9]([a-z0-9.-]*[a-z0-9])?\z/i,
@@ -71,7 +70,9 @@ class Portal < ApplicationRecord
def analytics
value = config_value('analytics')
value.is_a?(Hash) ? value : {}
return {} unless value.is_a?(Hash)
value.deep_stringify_keys.slice(*ANALYTICS_CONFIG_FORMATS.keys)
end
# Reader per analytics id (e.g. portal.ga4_measurement_id) so the snippet partials stay simple.
@@ -159,11 +160,18 @@ class Portal < ApplicationRecord
def normalize_config
self.config = persisted_config.merge((config || {}).deep_stringify_keys)
normalize_analytics_config
config['allowed_locales'] = allowed_locale_codes
config['default_locale'] = default_locale
config['draft_locales'] = draft_locale_codes
end
def normalize_analytics_config
return unless config['analytics'].is_a?(Hash)
config['analytics'] = config['analytics'].deep_stringify_keys.slice(*ANALYTICS_CONFIG_FORMATS.keys)
end
def validate_config
denied_keys = config.keys - CONFIG_JSON_KEYS
errors.add(:config, "in portal on #{denied_keys.join(',')} is not supported.") if denied_keys.any?

View File

@@ -1,16 +1,3 @@
<%# Analytics tags. Every value below is validated against Portal::ANALYTICS_CONFIG_FORMATS, %>
<%# so it is restricted to a safe shape (no quotes/angle brackets) before interpolation. %>
<% if @portal.gtm_container_id.present? %>
<!-- Google Tag Manager -->
<script>
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','<%= j @portal.gtm_container_id %>');
</script>
<!-- End Google Tag Manager -->
<% end %>
<% if @portal.ga4_measurement_id.present? %>
<!-- Google Analytics 4 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= @portal.ga4_measurement_id %>"></script>

View File

@@ -1,9 +1,3 @@
<% if @portal.gtm_container_id.present? %>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=<%= @portal.gtm_container_id %>"
title="Google Tag Manager" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<% end %>
<% if @portal.meta_pixel_id.present? %>
<!-- Meta Pixel (noscript) -->
<noscript><img height="1" width="1" style="display:none"

View File

@@ -190,6 +190,15 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do
expect(portal.reload.config['analytics']).to eq('ga4_measurement_id' => 'G-ADMIN12345')
end
it 'rejects administrators from configuring Google Tag Manager' do
put "/api/v1/accounts/#{account.id}/portals/#{portal.slug}",
params: { portal: { config: { analytics: { gtm_container_id: 'GTM-LEGACY123' } } } },
headers: admin.create_new_auth_token
expect(response).to have_http_status(:unprocessable_entity)
expect(portal.reload.analytics).to eq({})
end
it 'preserves drafted locales when draft_locales is omitted' do
portal.update!(config: { allowed_locales: %w[en es fr], draft_locales: ['es'], default_locale: 'en' })

View File

@@ -61,6 +61,31 @@ RSpec.describe Portal do
expect(portal.custom_domain).to be_nil
end
it 'does not expose legacy Google Tag Manager configuration' do
portal.assign_attributes(config: {
'analytics' => {
'gtm_container_id' => 'GTM-LEGACY123',
'ga4_measurement_id' => 'G-VALID123'
}
})
expect(portal.analytics).to eq('ga4_measurement_id' => 'G-VALID123')
end
it 'scrubs legacy Google Tag Manager configuration during normalization' do
portal.assign_attributes(config: {
'allowed_locales' => ['en'],
'default_locale' => 'en',
'analytics' => {
'gtm_container_id' => 'GTM-LEGACY123',
'ga4_measurement_id' => 'G-VALID123'
}
})
expect(portal).to be_valid
expect(portal.config['analytics']).to eq('ga4_measurement_id' => 'G-VALID123')
end
context 'with locale_translations' do
it 'allows valid locale translations' do
portal.update(config: { allowed_locales: %w[en es], default_locale: 'en',