CLItrail Docs

Add CLItrail to WordPress

Where the CLItrail tag goes in WordPress, in its own idiom, with the consent, CSP and client-side routing details and how to check it works.

Written for WordPress 7 (the same code works from 5.7)

#Where the tag goes

Edit wp-content/mu-plugins/clitrail.php (a must-use plugin, so it survives theme changes). Put the tag on every page that shows your install command; site-wide is best, after your existing analytics tags.

<?php
/**
 * Plugin Name: CLItrail
 * Description: Adds the CLItrail website tag to every page.
 */
add_action('wp_head', function () {
    wp_print_script_tag([
        'src' => 'https://YOUR_SERVICE/browser.js',
        'defer' => true,
        'data-website' => 'YOUR_WEBSITE_ID',
    ]);
});

Use the loader in place of the tag: nothing from CLItrail loads until startClitrail() runs. Load the loader before your consent manager's script and call startClitrail() from the manager's accept callback (most managers also run it on later visits once a choice is stored; check yours). Use the marketing category when installs go to Google Ads, Meta, TikTok or X Ads, and the analytics category when they only reach GA4 or your webhooks. Priority 1 prints the loader early in <head>, before enqueued scripts (WordPress prints those at priority 9).

Consent loader

// Loads CLItrail only after the visitor agrees. Call startClitrail() from your
// consent manager's "accepted" callback; nothing is fetched or stored before that.
window.startClitrail = () => {
  if (document.getElementById('clitrail')) return;
  const tag = document.createElement('script');
  tag.id = 'clitrail';
  tag.src = 'https://YOUR_SERVICE/browser.js';
  tag.dataset.website = 'YOUR_WEBSITE_ID';
  document.head.append(tag);
};
<?php
/**
 * Plugin Name: CLItrail
 * Description: Loads CLItrail after consent (clitrail-consent.js defines startClitrail()).
 */
add_action('wp_head', function () {
    wp_print_script_tag([
        'src' => plugins_url('clitrail-consent.js', __FILE__),
        'defer' => true,
    ]);
}, 1);

#Client-side routing

WordPress pages load in full, so the tag is enough. If your theme changes pages without a reload, follow its navigation event with the route helper.

#Content Security Policy

A security plugin or your server's headers: add https://YOUR_SERVICE to script-src and connect-src, and blob: to worker-src (only Safari before 26 needs that one).

script-src 'self' https://YOUR_SERVICE;
connect-src 'self' https://YOUR_SERVICE;
worker-src 'self' blob:;

#Verify it works

  1. Upload the file and open the site.
  2. Open a page with the tag in Chrome or Firefox (accept analytics first if you gate CLItrail on consent), open the developer console and run await InstallAttribution.ready. It resolves to { ok: true, urlId, hasAnalyticsContext, expiresAt }.
  3. In the CLItrail dashboard, open Visits & identities: the visit is listed.
  4. Anything else names the cause: disabled (no consent yet, or data-enabled="false"), opfs_unavailable (the page is not served over https or from localhost), Error (the service refused the visit: add the page's origin, including a development origin such as http://localhost:3000, under Settings → Additional domains). If InstallAttribution is undefined, the tag did not load: check the Network tab and your Content-Security-Policy.

Then run your installer on the same computer and check Install events, or run the hook with --doctor.

#Notes

  • wp_print_script_tag escapes each attribute and prints defer as a bare attribute.
  • On WordPress.com, custom code needs a plan that allows plugins; a site-wide header setting from your theme or a header-code plugin works the same way with the plain tag.

WordPress documentation: developer.wordpress.org/reference/functions/wp_print_script_tag

Using other install paths or destinations? The setup generator puts this snippet together with your hooks and destination checklist.