diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalIntegrationsSettings.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalIntegrationsSettings.vue
index f35d470c3..cab5c7ad1 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalIntegrationsSettings.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalIntegrationsSettings.vue
@@ -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',
diff --git a/app/models/concerns/portal_config_schema.rb b/app/models/concerns/portal_config_schema.rb
index f403f428d..27b6709b0 100644
--- a/app/models/concerns/portal_config_schema.rb
+++ b/app/models/concerns/portal_config_schema.rb
@@ -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] },
diff --git a/app/models/portal.rb b/app/models/portal.rb
index 57132b2f0..fc066bf51 100644
--- a/app/models/portal.rb
+++ b/app/models/portal.rb
@@ -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?
diff --git a/app/views/layouts/_portal_analytics.html.erb b/app/views/layouts/_portal_analytics.html.erb
index 277c66d55..30f1e6e82 100644
--- a/app/views/layouts/_portal_analytics.html.erb
+++ b/app/views/layouts/_portal_analytics.html.erb
@@ -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? %>
-
-
-
-<% end %>
<% if @portal.ga4_measurement_id.present? %>
diff --git a/app/views/layouts/_portal_analytics_noscript.html.erb b/app/views/layouts/_portal_analytics_noscript.html.erb
index cda3667a3..733524276 100644
--- a/app/views/layouts/_portal_analytics_noscript.html.erb
+++ b/app/views/layouts/_portal_analytics_noscript.html.erb
@@ -1,9 +1,3 @@
-<% if @portal.gtm_container_id.present? %>
-
-
-
-<% end %>
<% if @portal.meta_pixel_id.present? %>