Analytics Architecture on Acquia
Acquia is a managed Drupal hosting platform. Analytics implementation follows Drupal's module and hook system, with additional considerations for Acquia's infrastructure layer: Varnish caching, CDN edge nodes, and Cloud IDE deployment workflows.
The rendering pipeline that affects tracking:
Request → Acquia CDN → Varnish Cache → Drupal (PHP) → Theme Layer → HTML Output
Varnish serves cached pages for anonymous users, which means server-side tracking logic only executes on cache misses. All client-side analytics scripts still fire normally since they execute in the browser after the cached HTML is delivered.
Acquia Personalization (formerly Acquia Lift) adds its own JavaScript agent that collects behavioral data. If you run both Personalization and a third-party analytics platform, coordinate the data layer to avoid duplicate event capture.
Installing Tracking Scripts
Method 1: Drupal Hook (Recommended)
Create a custom module that injects scripts via hook_page_attachments_alter:
// modules/custom/my_analytics/my_analytics.module
function my_analytics_page_attachments_alter(array &$attachments) {
$attachments['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#attributes' => [
'src' => 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX',
'async' => TRUE,
],
],
'gtm_script',
];
}
Method 2: Theme Template
Add scripts directly in your theme's html.html.twig:
{# docroot/themes/custom/mytheme/templates/html.html.twig #}
<head>
{{ page.head }}
<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','GTM-XXXXX');
</script>
</head>
Method 3: Acquia Cloud Hooks
For environment-specific tracking (e.g., production-only), use Cloud Hooks to toggle configuration:
#!/bin/bash
# hooks/common/post-code-deploy/enable-analytics.sh
site="$1"
target_env="$2"
if [ "$target_env" = "prod" ]; then
drush @${site}.${target_env} config-set my_analytics.settings enabled 1 -y
else
drush @${site}.${target_env} config-set my_analytics.settings enabled 0 -y
fi
Then check the config in your module:
function my_analytics_page_attachments_alter(array &$attachments) {
$config = \Drupal::config('my_analytics.settings');
if (!$config->get('enabled')) {
return;
}
// ... attach scripts
}
Data Layer Implementation
Build the data layer in a preprocess function so it is available before analytics scripts execute:
// my_analytics.module
function my_analytics_preprocess_html(&$variables) {
$node = \Drupal::routeMatch()->getParameter('node');
$user = \Drupal::currentUser();
$data_layer = [
'platform' => 'acquia',
'drupalVersion' => \Drupal::VERSION,
'environment' => getenv('AH_SITE_ENVIRONMENT') ?: 'local',
'pageType' => $node ? $node->getType() : 'other',
'userRole' => $user->isAuthenticated() ? 'authenticated' : 'anonymous',
'language' => \Drupal::languageManager()->getCurrentLanguage()->getId(),
];
if ($node) {
$data_layer['contentId'] = $node->id();
$data_layer['contentTitle'] = $node->getTitle();
$data_layer['contentCreated'] = date('c', $node->getCreatedTime());
// Taxonomy terms
if ($node->hasField('field_tags')) {
$terms = [];
foreach ($node->get('field_tags')->referencedEntities() as $term) {
$terms[] = $term->getName();
}
$data_layer['contentTags'] = $terms;
}
}
$variables['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => 'window.dataLayer = window.dataLayer || []; window.dataLayer.push(' . json_encode($data_layer) . ');',
'#weight' => -100,
],
'data_layer',
];
}
The AH_SITE_ENVIRONMENT variable is set by Acquia Cloud and returns dev, test, or prod, which you can use to filter analytics data by environment.
Acquia Personalization Integration
If Acquia Personalization is enabled, it injects its own tracking agent. Coordinate with your analytics platform:
// Bridge Acquia Personalization segments into dataLayer
function my_analytics_preprocess_html(&$variables) {
// Acquia Personalization exposes segments via JS
$variables['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#value' => '
document.addEventListener("acquiaLiftDecision", function(e) {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: "personalization_decision",
segmentId: e.detail.segment_id,
decisionId: e.detail.decision_id,
slotId: e.detail.slot_id
});
});
',
],
'acquia_lift_bridge',
];
}
Common Issues
Varnish Cache Serving Stale Data Layer Values
Anonymous pages are cached by Varnish. If your data layer includes user-specific or session-specific data, those values get cached with the page.
Solution: Move dynamic values to a client-side AJAX call:
// Fetch user-specific data after page load
fetch('/api/analytics-context', { credentials: 'same-origin' })
.then(r => r.json())
.then(data => {
window.dataLayer.push({
event: 'context_loaded',
...data
});
});
Mark the API route as uncacheable in your module's routing:
# my_analytics.routing.yml
my_analytics.context:
path: '/api/analytics-context'
defaults:
_controller: '\Drupal\my_analytics\Controller\ContextController::get'
options:
no_cache: TRUE
CDN Stripping Query Parameters
Acquia's CDN may strip or normalize URL query parameters used for campaign tracking (e.g., utm_source). Preserve them by configuring the CDN to pass through marketing parameters. In Acquia Cloud, this is set via the Platform UI under CDN settings, or by contacting Acquia support for custom Varnish VCL rules.
Scripts Loading Twice on Authenticated vs. Anonymous
Drupal's page cache (Internal Page Cache module) and Varnish operate differently for authenticated users. If you attach scripts conditionally, verify they fire once in both states:
function my_analytics_page_attachments_alter(array &$attachments) {
// Prevent double-loading by checking for existing attachment
foreach ($attachments['#attached']['html_head'] as $item) {
if (isset($item[1]) && $item[1] === 'gtm_script') {
return;
}
}
// ... attach script
}
Cloud IDE Deployment Not Updating Scripts
When deploying via Acquia Cloud IDE, changes to custom modules require a cache rebuild:
drush cr
If using config management, export and import:
drush config-export -y
drush config-import -y
Platform-Specific Considerations
Acquia Cloud environments -- dev, stage, and prod environments have separate Varnish and CDN layers. Use the AH_SITE_ENVIRONMENT variable to conditionally load tracking IDs per environment, preventing dev traffic from polluting production analytics.
Drupal BigPipe -- If BigPipe is enabled (default in Drupal 9+), the initial HTML response is sent before all content is rendered. Scripts in html_head still load normally, but lazy-loaded BigPipe placeholders may trigger DOM mutations after your analytics snippet initializes. Use a MutationObserver or GTM's DOM Ready trigger rather than relying on synchronous DOM state.
Multisite deployments -- Acquia supports Drupal multisite. Each site in the multisite shares the same codebase but can have different config. Store analytics IDs in Drupal configuration (not hardcoded) and override per site using config splits:
# config/splits/site_a/my_analytics.settings.yml
enabled: true
gtm_id: 'GTM-AAAA'
# config/splits/site_b/my_analytics.settings.yml
enabled: true
gtm_id: 'GTM-BBBB'
Node.js services on Acquia -- Acquia Cloud Next supports Node.js applications alongside Drupal. If you run a decoupled frontend, implement analytics in the Node.js layer independently, since the Drupal hooks described above only apply to server-rendered Drupal pages.