Skip to content

Commit

Permalink
Merge pull request #6074 from samvera/ga4-hyrax3
Browse files Browse the repository at this point in the history
[3.x] Google Analytics 4 Reporting
  • Loading branch information
dlpierce authored Jun 23, 2023
2 parents f5a0d16 + fdc117f commit 7e42b42
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 40 deletions.
2 changes: 2 additions & 0 deletions .dassie/config/analytics.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
analytics:
ga4:
analytics_id: <%= ENV['GOOGLE_ANALYTICS_ID'] %>
google:
analytics_id: <%= ENV['GOOGLE_ANALYTICS_ID'] %>
app_name: <%= ENV['GOOGLE_OAUTH_APP_NAME'] %>
Expand Down
126 changes: 88 additions & 38 deletions app/assets/javascripts/hyrax/analytics_events.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,125 @@
class TrackingTags {
constructor(provider) {
this.provider = provider
switch(this.provider) {
case 'matomo':
this.tracker = new MatomoTagTracker();
break;
case 'google':
this.tracker = new UATagTracker();
break;
case 'ga4':
this.tracker = new GA4TagTracker();
break;
default:
console.error('Unsupport analytics provider ' + this.provider + ', supported values are: matomo, google, ga4');
}
}

// Track an event with the configured provider
trackTagEvent(category, action, name) {
this.tracker.trackEvent(category, action, name);
}

// Track a page view with the configured provider
trackPageView() {
this.tracker.trackPageView();
}

// Deprecated: use trackTagEvent and trackPageView instead.
analytics() {
if(this.provider === "matomo") {
return _paq;
}
else {
return _gaq;
}
return this;
}

pageView() {
if(this.provider === "matomo") {
return 'trackPageView'
// Deprecated: use trackTagEvent and trackPageView instead.
push(params) {
if (params[0] == 'trackPageView' || params[0] == '_trackPageView') {
this.tracker.trackPageView();
} else {
return '_trackPageview'
this.tracker.trackTagEvent(params[1], params[2], params[3]);
}
}

// Deprecated
pageView() {
return 'trackPageView';
}

// Deprecated
trackEvent() {
if(this.provider === "matomo") {
return 'trackEvent'
} else {
return '_trackEvent'
}
return 'trackEvent';
}
}

class GA4TagTracker {
trackEvent(category, action, name) {
gtag('event', action, {
'category': category,
'name': name
});
}

trackPageView() {
// No operation necessary, this event is automatically collected
}
}

class UATagTracker {
trackEvent(category, action, name) {
_gaq.push(['_trackEvent', category, action, name]);
}

trackPageView() {
_gaq.push(['_trackPageView']);
}
}

class MatomoTagTracker {
trackEvent(category, action, name) {
_paq.push(['trackEvent', category, action, name]);
}

trackPageView() {
_paq.push(['trackPageView']);
}
}

function trackPageView() {
window.trackingTags.analytics().push([window.trackingTags.pageView()]);
window.trackingTags.trackPageView();
}

function trackAnalyticsEvents() {
$('span.analytics-event').each(function(){
var eventSpan = $(this)
window.trackingTags.analytics().push([window.trackingTags.trackEvent(), eventSpan.data('category'), eventSpan.data('action'), eventSpan.data('name')]);
var eventSpan = $(this);
window.trackingTags.trackTagEvent(eventSpan.data('category'), eventSpan.data('action'), eventSpan.data('name'));
})
}

function setupTracking() {
var provider = $('meta[name="analytics-provider"]').prop('content')
if (provider === undefined) {
return;
}
window.trackingTags = new TrackingTags(provider)
trackPageView()
trackAnalyticsEvents()
var provider = $('meta[name="analytics-provider"]').prop('content')
if (provider === undefined) {
return;
}
window.trackingTags = new TrackingTags(provider);
trackPageView();
trackAnalyticsEvents();
}

if (typeof Turbolinks !== 'undefined') {
$(document).on('turbolinks:load', function() {
setupTracking()
})
setupTracking();
});
} else {
$(document).on('ready', function() {
setupTracking()
})
setupTracking();
});
}

$(document).on('click', '#file_download', function(e) {
var provider = $('meta[name="analytics-provider"]').prop('content')
if (provider === undefined) {
return;
}
window.trackingTags = new TrackingTags(provider)
window.trackingTags.analytics().push([trackingTags.trackEvent(), 'file-set', 'file-set-download', $(this).data('label')]);
window.trackingTags.analytics().push([trackingTags.trackEvent(), 'file-set-in-work', 'file-set-in-work-download', $(this).data('work-id')]);
window.trackingTags.trackTagEvent('file-set', 'file-set-download', $(this).data('label'));
window.trackingTags.trackTagEvent('file-set-in-work', 'file-set-in-work-download', $(this).data('work-id'));
$(this).data('collection-ids').forEach(function (collection) {
window.trackingTags.analytics().push([trackingTags.trackEvent(), 'file-set-in-collection', 'file-set-in-collection-download', collection]);
window.trackingTags.analytics().push([trackingTags.trackEvent(), 'work-in-collection', 'work-in-collection-download', collection]);
window.trackingTags.trackTagEvent('file-set-in-collection', 'file-set-in-collection-download', collection);
window.trackingTags.trackTagEvent('work-in-collection', 'work-in-collection-download', collection);
});
});
204 changes: 204 additions & 0 deletions app/services/hyrax/analytics/ga4.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# frozen_string_literal: true
require 'oauth2'
require 'signet/oauth_2/client'

# rubocop:disable Metrics/ModuleLength
module Hyrax
module Analytics
module Ga4
extend ActiveSupport::Concern
# rubocop:disable Metrics/BlockLength
class_methods do
# Loads configuration options from config/analytics.yml. Expected structure:
# `analytics:`
# ` ga4:`
# ` app_name: <%= ENV['GOOGLE_OAUTH_APP_NAME']`
# ` app_version: <%= ENV['GOOGLE_OAUTH_APP_VERSION']`
# ` privkey_path: <%= ENV['GOOGLE_OAUTH_PRIVATE_KEY_PATH']`
# ` privkey_secret: <%= ENV['GOOGLE_OAUTH_PRIVATE_KEY_SECRET']`
# ` client_email: <%= ENV['GOOGLE_OAUTH_CLIENT_EMAIL']`
# @return [Config]
def config
@config ||= Config.load_from_yaml
end

class Config
def self.load_from_yaml
filename = Rails.root.join('config', 'analytics.yml')
yaml = YAML.safe_load(ERB.new(File.read(filename)).result)
unless yaml
Hyrax.logger.error("Unable to fetch any keys from #{filename}.")
return new({})
end
config = yaml.fetch('analytics')&.fetch('ga4', nil)
unless config
Deprecation.warn("Deprecated analytics configuration format found. Please update config/analytics.yml.")
config = yaml.fetch('analytics')
# this has to exist here with a placeholder so it can be set in the Hyrax initializer
# it is only for backward compatibility
config['analytics_id'] = '-'
end
new config
end

REQUIRED_KEYS = %w[analytics_id app_name app_version privkey_path privkey_secret client_email].freeze

def initialize(config)
@config = config
end

# @return [Boolean] are all the required values present?
def valid?
config_keys = @config.keys
REQUIRED_KEYS.all? { |required| config_keys.include?(required) }
end

REQUIRED_KEYS.each do |key|
class_eval %{ def #{key}; @config.fetch('#{key}'); end }
end

# This method allows setting the analytics id in the initializer
# @deprecated set the analytics id in either ENV['GOOGLE_ANALYTICS_ID'] or config/analytics.yaml
def analytics_id=(value)
@config['analytics_id'] = value
end
end

# Generate an OAuth2 token for Google Analytics
# @return [OAuth2::AccessToken] An OAuth2 access token for GA
def token(scope = 'https://www.googleapis.com/auth/analytics.readonly')
access_token = auth_client(scope).fetch_access_token!
OAuth2::AccessToken.new(oauth_client, access_token['access_token'], expires_in: access_token['expires_in'])
end

def oauth_client
OAuth2::Client.new('', '', authorize_url: 'https://accounts.google.com/o/oauth2/auth',
token_url: 'https://accounts.google.com/o/oauth2/token')
end

def auth_client(scope)
raise "Private key file for Google analytics was expected at '#{config.privkey_path}', but no file was found." unless File.exist?(config.privkey_path)
private_key = File.read(config.privkey_path)
Signet::OAuth2::Client.new token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
audience: 'https://accounts.google.com/o/oauth2/token',
scope: scope,
issuer: config.client_email,
signing_key: OpenSSL::PKCS12.new(private_key, config.privkey_secret).key,
sub: config.client_email
end

# Return a user object linked to a Google Analytics account
# @return [Legato::User] A user account with GA access
def user
Legato::User.new(token)
end

# Return a Google Analytics profile matching specified ID
# @ return [Legato::Management::Profile] A user profile associated with GA
def profile
return unless config.valid?
@profile = user.profiles.detect do |profile|
profile.web_property_id == config.analytics_id
end
raise 'User does not have access to this property' unless @profile
@profile
end

# rubocop:disable Metrics/MethodLength
def to_date_range(period)
case period
when "day"
start_date = Time.zone.today
end_date = Time.zone.today
when "week"
start_date = Time.zone.today - 7.days
end_date = Time.zone.today
when "month"
start_date = Time.zone.today - 1.month
end_date = Time.zone.today
when "year"
start_date = Time.zone.today - 1.year
end_date = Time.zone.today
end

[start_date, end_date]
end
# rubocop:enabl e Metrics/MethodLength

def keyword_conversion(date)
case date
when "last12"
start_date = Time.zone.today - 11.months
end_date = Time.zone.today

[start_date, end_date]
else
date.split(",")
end
end

def date_period(period, date)
if period == "range"
date.split(",")
else
to_date_range(period)
end
end

# Configure analytics_start_date in ENV file
def default_date_range
"#{Hyrax.config.analytics_start_date},#{Time.zone.today + 1.day}"
end

# The number of events by day for an action
def daily_events(action, date = default_date_range)
date = date.split(",")
EventsDaily.summary(profile, date[0], date[1], action)
end

# The number of events by day for an action and ID
def daily_events_for_id(id, action, date = default_date_range)
date = date.split(",")
EventsDaily.by_id(profile, date[0], date[1], id, action)
end

# A list of events sorted by highest event count
def top_events(action, date = default_date_range)
date = date.split(",")
Events.send('list', profile, date[0], date[1], action)
end

def unique_visitors(date = default_date_range); end

def unique_visitors_for_id(id, date = default_date_range); end

def new_visitors(period = 'month', date = default_date_range)
date = date_period(period, date)
Visits.new_visits(profile, date[0], date[1])
end

def new_visits_by_day(date = default_date_range, _period = 'day')
date = date.split(",")
VisitsDaily.new_visits(profile, date[0], date[1])
end

def returning_visitors(period = 'month', date = default_date_range)
date = date_period(period, date)
Visits.return_visits(profile, date[0], date[1])
end

def returning_visits_by_day(date = default_date_range, _period = 'day')
date = date.split(",")
VisitsDaily.return_visits(profile, date[0], date[1])
end

def total_visitors(period = 'month', date = default_date_range)
date = date_period(period, date)
Visits.total_visits(profile, date[0], date[1])
end
end
# rubocop:enable Metrics/BlockLength
end
end
end
# rubocop:enable Metrics/ModuleLength
7 changes: 5 additions & 2 deletions app/views/layouts/_head_tag_content.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ signed in %>
<%= render 'shared/appearance_styles' %>
<% if Hyrax.config.analytics? %>
<% if Hyrax.config.analytics_provider == 'google' %>
<% case Hyrax.config.analytics_provider %>
<% when 'ga4' %>
<%= render partial: 'shared/ga4', formats: [:html] %>
<% when 'google' %>
<%= render partial: 'shared/ga', formats: [:html] %>
<% elsif Hyrax.config.analytics_provider == 'matomo' %>
<% when 'matomo' %>
<%= render partial: 'shared/matomo', formats: [:html] %>
<% end %>
<% end %>
Expand Down
Loading

0 comments on commit 7e42b42

Please sign in to comment.