Add CLItrail to Server-rendered apps (Rails, Laravel, Django, Express)
Where the CLItrail tag goes in Server-rendered apps (Rails, Laravel, Django, Express), in its own idiom, with the consent, CSP and client-side routing details and how to check it works.
#Where the tag goes
Edit app/views/layouts/application.html.erb (Rails), resources/views/layouts/app.blade.php (Laravel), templates/base.html (Django), your views layout (Express). Put the tag on every page that shows your install command; site-wide is best, after your existing analytics tags.
<head>
<meta charset="utf-8" />
<!-- …your analytics tags first -->
<script defer src="https://YOUR_SERVICE/browser.js" data-website="YOUR_WEBSITE_ID"></script>
</head>
#Only after consent
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. In Django, point the src at {% static 'clitrail-consent.js' %}.
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);
};
<head>
<meta charset="utf-8" />
<!-- …your analytics tags first -->
<script defer src="/clitrail-consent.js"></script>
</head>
#Client-side routing
Server-rendered pages load in full. With Hotwire Turbo (the Rails default), pages change without a reload: record them on turbo:load, which Turbo fires on the first load and after every visit. Laravel Livewire's wire:navigate fires livewire:navigated and Inertia's router fires navigate: use the same function on those.
// Records Turbo visits; the first load is the page the tag already recorded.
(() => {
let recorded;
const recordVisit = () => {
recorded ??= new URL(performance.getEntriesByType('navigation')[0]?.name ?? location.href).pathname;
const sdk = window.InstallAttribution;
if (!sdk?.ready || location.pathname === recorded) return;
recorded = location.pathname;
sdk.ready = sdk.init({ service: 'https://YOUR_SERVICE', project: 'YOUR_WEBSITE_ID' });
};
document.addEventListener('turbo:load', recordVisit);
})();
#Content Security Policy
Rails: config/initializers/content_security_policy.rb (policy.script_src, policy.connect_src, policy.worker_src). Laravel and Django: the CSP middleware or settings you use. Express: helmet({ contentSecurityPolicy }) directives. In each, 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
- Start your app server locally (register its origin) or deploy.
- 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 }. - In the CLItrail dashboard, open Visits & identities: the visit is listed.
- Anything else names the cause:
disabled(no consent yet, ordata-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 ashttp://localhost:3000, under Settings → Additional domains). IfInstallAttributionis 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
- Put the tag in the one layout every public page uses, after your analytics tags.
- In ERB, Blade and Django templates the tag is plain HTML: no helper is needed.
Server-rendered apps (Rails, Laravel, Django, Express) documentation: turbo.hotwired.dev/reference/events
Using other install paths or destinations? The setup generator puts this snippet together with your hooks and destination checklist.