Clycyo
Frameworks5 min read

Rails Analytics: Turbo-Compatible, Cookieless Setup

Add analytics to Ruby on Rails with Turbo Drive navigation handled correctly, plus server-side events from ActiveJob and webhooks.

Every Rails 7+ app ships with Turbo, and Turbo Drive quietly breaks naive analytics: it intercepts link clicks, swaps the body over fetch, and never fires a full page load again. Tools that only listen for the load event record one pageview per session and call it a day. Here is the Turbo-correct setup.

Installation in the layout

<%# app/views/layouts/application.html.erb %>
<head>
  <script
    defer
    src="https://clycyo.com/tracker.js"
    data-tracking-id="<%= Rails.configuration.x.clycyo_tracking_id %>"
  ></script>
</head>

Turbo Drive advances the URL with the History API on every visit, which the tracker instruments directly — each Turbo navigation records a pageview with its own timing, no turbo:load listeners required. Turbo Frames updates that do not change the URL are correctly not counted; Streams likewise.

Events from Stimulus controllers

// app/javascript/controllers/analytics_controller.js
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static values = { event: String, props: Object };

  fire() {
    window.webanalytics?.track(this.eventValue, this.propsValue);
  }
}
<button data-controller="analytics"
        data-action="analytics#fire"
        data-analytics-event-value="plan_selected"
        data-analytics-props-value='{"plan":"pro"}'>
  Choose Pro
</button>

One generic controller covers every CTA and form in the app, declaratively.

Identity + the server-side join

<%# After sign-in, once %>
<script>
  window.webanalytics?.identify("<%= current_user.email %>");
</script>

Persist getVisitorId() to the user via a small fetch, then your Stripe webhook job posts revenue server-side:

# app/jobs/report_revenue_job.rb
Net::HTTP.post(
  URI("https://clycyo.com/api/collect"),
  {
    tracking_id: Rails.configuration.x.clycyo_tracking_id,
    type: "event",
    visitor_id: user.clycyo_visitor_id,
    event_name: "subscription_paid",
    event_properties: { revenue: invoice.amount_paid / 100.0,
                        currency: invoice.currency.upcase,
                        order_id: invoice.id },
  }.to_json,
  "Content-Type" => "application/json"
)

ActiveJob retries are safe — order_id deduplicates. The result is channel-attributed revenue flowing from the same webhook you already handle for Pay/Cashier-style billing.

Bonus: what the 1.1 KB buys back

Rails apps tend to be fast servers behind slow tag managers. Replacing the GA/GTM stack with one small tag improves LCP on every page and usually retires the cookie banner too — a double conversion win documented in our banner-cost analysis.