Skip to content

Inject gtm code conditionally #2167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ testem.log
packages/playground/website/cypress/downloads
vite.config.ts.timestamp-*.mjs

# Environment files - keep defaults but ignore local overrides
.env.local

# System Files
.DS_Store
Thumbs.db
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ A browser should open and take you to your very own client-side WordPress at [ht

Any changes you make to `.ts` files will be live-reloaded. Changes to `Dockerfile` require a full rebuild.

## Self-hosting WordPress Playground

When self-hosting WordPress Playground, you may want to customize certain aspects like analytics tracking. See the [configuration documentation](packages/playground/website/CONFIGURATION.md) for details on available options and how to apply them.

From here, the [documentation](https://wordpress.github.io/wordpress-playground/) will help you learn how WordPress Playground works and how to use it to build amazing things!

And here's a few more interesting CLI commands you can run in this repo:
Expand Down
6 changes: 6 additions & 0 deletions packages/playground/website/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# WordPress Playground configuration
# Copy this file to .env to customize your local deployment

# Google Analytics/GTM Configuration
# Leave empty to disable analytics
VITE_GOOGLE_ANALYTICS_ID=G-SVTNFCP8T7
71 changes: 71 additions & 0 deletions packages/playground/website/CONFIGURATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# WordPress Playground Configuration

This document outlines how to configure your self-hosted WordPress Playground instance.

## Environment Variables

WordPress Playground uses environment variables for configuration. These can be set in the following files:

- `.env` - Default configuration (included in repository)
- `.env.local` - Local overrides (not committed to Git)

## Available Configuration Options

### Google Analytics

The Google Analytics/GTM integration can be configured using:

```
VITE_GOOGLE_ANALYTICS_ID=your-ga4-id
```

To disable Google Analytics completely, set the value to an empty string:

```
VITE_GOOGLE_ANALYTICS_ID=
```

The Google Analytics script is automatically injected into the `<head>` section of all HTML pages during the build process. If the environment variable is not set or is empty, no analytics code will be included in the final HTML output, improving privacy and performance for self-hosted instances that don't require tracking.

This configuration applies to:

- The main application (`index.html`)
- The WordPress PR previewer (`public/wordpress.html`)
- The Gutenberg PR previewer (`public/gutenberg.html`)
- All demo and builder HTML files

## How to Configure Your Self-Hosted Instance

1. Clone the repository
2. Create a `.env.local` file with your custom configuration
3. Build the project according to the main README instructions

Example `.env.local` file:

```
# Custom Google Analytics ID for my self-hosted instance
VITE_GOOGLE_ANALYTICS_ID=G-MYANALYTICS123
```

## Building With Custom Configuration

The environment variables are applied at build time. Make sure your custom `.env.local` file is in place before running:

```bash
# Standard build
npm run build:website

# Verbose build with analytics logging
npm run build:website -- --verbose
```

## Technical Implementation

The analytics integration uses a custom Vite plugin that inserts the Google Analytics script at the end of the `<head>` section in all HTML files during the build process. This approach:

1. Keeps analytics configuration separate from the code
2. Ensures no analytics code is included in the HTML when disabled
3. Requires no placeholder comments in the HTML source files
4. Provides a clean way to customize analytics for self-hosted instances
5. Maintains clean indentation and formatting in the output HTML
6. Operates silently by default (logs can be enabled with `--verbose`)
12 changes: 0 additions & 12 deletions packages/playground/website/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,6 @@
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet"
/>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-SVTNFCP8T7"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-SVTNFCP8T7');
</script>
</head>
<body>
<main id="root" aria-label="WordPress Playground">
Expand Down
13 changes: 13 additions & 0 deletions packages/playground/website/playwright/e2e/website-ui.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,16 @@ test('should keep query arguments when updating settings', async ({
await wordpress.locator('body').evaluate((body) => body.baseURI)
).toMatch('/wp-admin/');
});

test('should not load GTM code when VITE_GOOGLE_ANALYTICS_ID is missing', async ({
website,
}) => {
await website.goto('./');

// By default, the VITE_GOOGLE_ANALYTICS_ID is not set, so GTM should not be loaded
// Check if GTM script is not present in the head
const gtmScript = await website.page
.locator('script[src*="googletagmanager.com"]')
.count();
expect(gtmScript).toBe(0);
});
Comment on lines +266 to +277
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreilupu can we please add a test that confirms GTM is loaded, and window.gtag is defined, if the VITE_GOOGLE_ANALYTICS_ID environment variable is provided?

14 changes: 1 addition & 13 deletions packages/playground/website/public/gutenberg.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,6 @@
href="https://fonts.googleapis.com/css?family=Noto+Serif:400,700"
/>
<link rel="stylesheet" href="./previewer.css" />
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-SVTNFCP8T7"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-SVTNFCP8T7');
</script>
</head>

<body>
Expand Down Expand Up @@ -164,7 +152,7 @@
step: 'login',
username: 'admin',
password: 'password',
}
},
],
};
// If there's a import-site query parameter, pass that to the blueprint
Expand Down
12 changes: 0 additions & 12 deletions packages/playground/website/public/wordpress.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,6 @@
href="https://fonts.googleapis.com/css?family=Noto+Serif:400,700"
/>
<link rel="stylesheet" href="./previewer.css" />
<script
async
src="https://www.googletagmanager.com/gtag/js?id=G-SVTNFCP8T7"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-SVTNFCP8T7');
</script>
</head>
<body>
<div id="main">
Expand Down
135 changes: 135 additions & 0 deletions packages/playground/website/vite-analytics-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/* eslint-disable no-console */
import { Plugin } from 'vite';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

// Plugin options interface
interface AnalyticsPluginOptions {
verbose?: boolean;
}

/**
* Vite plugin to inject Google Analytics into the head tag of HTML files
*
* @param options Plugin options
* @returns {Plugin} A Vite plugin that processes HTML files during build
*/
export function analyticsInjectionPlugin(
options: AnalyticsPluginOptions = {}
): Plugin {
// Default options
const { verbose = false } = options;

// Shared analytics script template
const getAnalyticsScript = (id: string) => {
return `
<script async src="https://www.googletagmanager.com/gtag/js?id=${id}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', '${id}');
</script>
`;
};

// Helper function for conditional logging
const log = (msg: string, isError = false) => {
// Only log if it's an error or verbose mode is enabled
if (isError || verbose) {
isError ? console.error(msg) : console.log(msg);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: linter complains about the console statement. It is configured this way to enforce using the logger module in Playground code, but this is just a build script and the rule doesn't really apply. Feel free to silence this check for these lines of for the entire file.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I've added the ignore rule for the entire file.

}
};

return {
name: 'vite-plugin-analytics-injection',
apply: 'build', // Only apply during build, not dev

writeBundle(options, bundle) {
const googleAnalyticsId = process.env.VITE_GOOGLE_ANALYTICS_ID;

if (!googleAnalyticsId) {
log(
'Google Analytics disabled - no tracking will be added to HTML files.'
);
return;
}

log('Processing HTML files for Google Analytics injection...');

// Files to process - include all HTML files that need analytics
const htmlFiles = [
'index.html',
'wordpress.html',
'gutenberg.html',
];
const outputDir = options.dir || '';

let processedCount = 0;
let skippedCount = 0;
let notFoundCount = 0;

htmlFiles.forEach((htmlFile) => {
const outputPath = join(outputDir, htmlFile);

if (existsSync(outputPath)) {
log(`Processing ${htmlFile} for analytics...`);

try {
// Read file
const content = readFileSync(outputPath, 'utf8');

// Check if the file already has analytics (to avoid duplicate injection)
if (
content.includes(
`gtag('config', '${googleAnalyticsId}')`
)
) {
log(
`Analytics already present in ${htmlFile}, skipping.`
);
skippedCount++;
return;
}

// Find the closing head tag
const headCloseIndex = content.indexOf('</head>');
if (headCloseIndex === -1) {
log(
`Could not find </head> tag in ${htmlFile}, skipping.`,
true
);
skippedCount++;
return;
}

// Insert the analytics script right before the closing head tag
const analyticsScript =
getAnalyticsScript(googleAnalyticsId);
const updatedContent =
content.substring(0, headCloseIndex) +
analyticsScript +
content.substring(headCloseIndex);

// Write back
writeFileSync(outputPath, updatedContent, 'utf8');
log(`Successfully injected analytics into ${htmlFile}`);
processedCount++;
} catch (error) {
log(`Error processing ${htmlFile}: ${error}`, true);
}
} else {
log(`File not found in build directory: ${outputPath}`);
notFoundCount++;
}
});

// Always show summary even in non-verbose mode
if (processedCount > 0) {
log(
`Analytics injection: ${processedCount} files processed, ${skippedCount} skipped, ${notFoundCount} not found.`
);
}
},
};
}
5 changes: 5 additions & 0 deletions packages/playground/website/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { buildVersionPlugin } from '../../vite-extensions/vite-build-version';
import { listAssetsRequiredForOfflineMode } from '../../vite-extensions/vite-list-assets-required-for-offline-mode';
import { addManifestJson } from '../../vite-extensions/vite-manifest';
import virtualModule from '../../vite-extensions/vite-virtual-module';
import { analyticsInjectionPlugin } from './vite-analytics-plugin';

const proxy: CommonServerOptions['proxy'] = {
'^/plugin-proxy': {
Expand All @@ -40,6 +41,9 @@ export default defineConfig(({ command, mode }) => {
? 'https://wordpress-playground-cors-proxy.net/?'
: 'http://127.0.0.1:5263/cors-proxy.php?';

// Check for verbose mode
const isVerbose = process.argv.includes('--verbose');

return {
// Split traffic from this server on dev so that the iframe content and
// outer content can be served from the same origin. In production it's
Expand Down Expand Up @@ -77,6 +81,7 @@ export default defineConfig(({ command, mode }) => {
},
},
plugins: [
analyticsInjectionPlugin({ verbose: isVerbose }),
react({
jsxRuntime: 'automatic',
}),
Expand Down
Loading