How to make web analytics valuable for Rails apps

Traditional web analytics aren't always the most useful for web apps, where a single controller with a dynamic route can serve an infinite number of different URLs. For example, /organization/fewer-and-faster and /organizations/boxout-sports are really the same page, but they show up in analytics as two separate pages.

A few weeks ago, we got tired of seeing this long tail of dynamic Flipper Cloud pages in Plausible, our preferred web analytics tool. We initially planned to remove web analytics entirely from authenticated app pages and only use it on marketing and documentation pages, but decided to try cloaking the dynamic URLs first so that /organizations/fewer-and-faster would just show up as /organizations/:organization_id.

It's been a few weeks, and we're loving our cloaked URLs. Now our data shows that toggling a feature is the 4th most visited page in our app (it's usually 3rd, but @jnunemaker's recent blog post garnered some well-deserved attention). Previously, this wouldn't even show up in the top 10 because it would have been hundreds of different URLs for each organization and feature.

We thought it was worth sharing how we did it.

Client side

Plausible supports custom locations to aggregate pages that contain identifiers, so our first step was updating our client side tracking code to use Plausible's manual script:

  <script async defer data-domain="flippercloud.io"
-   src="https://plausible.io/js/script.js"
+   src="https://plausible.io/js/script.manual.js"
  </script>

Now it's up to us to report page views, so we add a little snippet to do just that, with one little twist: if a <link name="analytics-url"> tag exists in the header, we use that instead of window.location as the current URL that gets reported to plausible.

<script>
  window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }

  document.addEventListener('DOMContentLoaded', event => {
    const link = document.querySelector('link[rel="analytics-url"]')  
    plausible('pageview', { u: (link ?? window.location).href });
  })
</script>

Easy peasy. Now, we just need the generated pages to set the analytics-url link.

Server side

In our Ruby on Rails application, we updated the layout to set our new link tag:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <%= tag.link rel: "analytics-url", href: url_for_analytics %>
    <!-- … -->

app/layouts/default.html.erb

The layout calls url_for_analytics, which is a helper we need to define. I couldn't figure out a straight forward way to get the currently matched route definition–like get '/somepath/:someparam' => 'controller#action' – which would already have placeholders for identifiers, so I had to do a little trickery. Calling url_for(request.path_parameters) will generate a new URL using the current params. So we can just replace all the values with our placeholders.

module ApplicationHelper
  # Convert the current url into a template with route placeholders
  # e.g. "/organizations/fewer-and-faster" => "/organizations/:id"
  def url_for_analytics
    return request.original_url if @reveal_path_to_analytics

    params = request.path_parameters.except(:controller, :action)
    placeholders = Hash[params.keys.map { |k| [k, ":#{k}"] }]
    url_for placeholders.merge(only_path: false)
  end

  # Call this in a view to reveal the full path (including parameters) to analytics
  def reveal_path_to_analytics
    @reveal_path_to_analytics = true
  end
end

The last detail to discuss is the reveal_path_to_analytics helper. While we want to cloak most URLs, we wanted to report the full URLs for our docs.

# app/views/documentation/show.html
<% reveal_path_to_analytics %>

<!-- … the docs … -->

That's it! A few lines of JavaScript and a dozen or so lines of Ruby and our web analytics are now much more useful.