-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Set up metrics tracking and track download events
Closes #1048
- Loading branch information
1 parent
08cb48d
commit 941f904
Showing
12 changed files
with
342 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -84,4 +84,5 @@ gem 'connection_pool' | |
|
||
group :production do | ||
gem 'newrelic_rpm' | ||
end | ||
end | ||
gem "device_detector", "~> 1.1" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
# frozen_string_literal: true | ||
|
||
# Methods for logging usage metrics based on requests for files | ||
module MetricsConcern | ||
def track_download(druid, file: nil) | ||
return unless enabled? | ||
|
||
ensure_visit | ||
properties = { druid:, file: }.compact | ||
metrics_service.track_event(event_data('download', properties)) | ||
end | ||
|
||
private | ||
|
||
# We're responsible for ensuring that every event is tied to a visit | ||
def ensure_visit | ||
return if existing_visit? | ||
|
||
set_visit_token unless visit_token | ||
set_visitor_token unless visitor_token | ||
|
||
metrics_service.track_visit(visit_data) | ||
end | ||
|
||
# Schema: https://github.com/ankane/ahoy#visits-1 | ||
def visit_data | ||
{ | ||
visit_token:, | ||
visitor_token:, | ||
js: false | ||
}.merge(visit_properties) | ||
end | ||
|
||
# Schema: https://github.com/ankane/ahoy#events-1 | ||
def event_data(name, properties = {}) | ||
{ | ||
visit_token:, | ||
visitor_token:, | ||
events: [ | ||
{ | ||
id: generate_id, | ||
time: Time.current, | ||
name:, | ||
properties: | ||
} | ||
] | ||
} | ||
end | ||
|
||
def existing_visit? | ||
visit_token && visitor_token | ||
end | ||
|
||
def visit_token | ||
cookies[:ahoy_visit] | ||
end | ||
|
||
def visitor_token | ||
cookies[:ahoy_visitor] | ||
end | ||
|
||
# Sessions last for 1 hour (default used by Zenodo) | ||
def set_visit_token | ||
cookies[:ahoy_visit] = { | ||
value: generate_id, | ||
expires: 1.hour.from_now, | ||
domain: 'stanford.edu' | ||
} | ||
end | ||
|
||
# Visitors are remembered for 2 years (Ahoy's default) | ||
def set_visitor_token | ||
cookies[:ahoy_visitor] = { | ||
value: generate_id, | ||
expires: 2.years.from_now, | ||
domain: 'stanford.edu' | ||
} | ||
end | ||
|
||
# Ahoy uses UUIDs for visit/visitor/event IDs | ||
def generate_id | ||
SecureRandom.uuid | ||
end | ||
|
||
def visit_properties | ||
@visit_properties ||= VisitProperties.new(request).generate | ||
end | ||
|
||
def metrics_service | ||
@metrics_service ||= MetricsService.new | ||
end | ||
|
||
def enabled? | ||
Settings.features.metrics == true | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# frozen_string_literal: true | ||
|
||
# Properties of a user's session that are useful for tracking SDR metrics | ||
# | ||
# Adapted from: lib/ahoy/visit_properties.rb | ||
# https://github.com/ankane/ahoy/blob/master/lib/ahoy/visit_properties.rb | ||
class VisitProperties | ||
attr_reader :request, :params, :referrer, :landing_page | ||
|
||
def initialize(request) | ||
@request = request | ||
@params = request.params | ||
@referrer = request.referer || '' | ||
@landing_page = request.original_url | ||
end | ||
|
||
def generate | ||
@generate ||= request_properties.merge(tech_properties) | ||
end | ||
|
||
private | ||
|
||
def request_properties | ||
{ | ||
ip:, | ||
user_agent:, | ||
referrer:, | ||
referring_domain:, | ||
landing_page: | ||
} | ||
end | ||
|
||
def tech_properties | ||
client = DeviceDetector.new(user_agent) | ||
|
||
# Convert device type to Ahoy's style | ||
device_type = | ||
case client.device_type | ||
when 'smartphone' then 'Mobile' | ||
when 'tv' then 'TV' | ||
else client.device_type&.titleize | ||
end | ||
|
||
{ | ||
browser: client.name, | ||
os: client.os_name, | ||
device_type: | ||
} | ||
end | ||
|
||
def referring_domain | ||
return if referrer.blank? | ||
|
||
URI.parse(referrer).host.first(255) | ||
rescue URI::InvalidURIError | ||
nil | ||
end | ||
|
||
# Mask IPs by zeroing last octet (IPv4) or 80 bits (IPv6) | ||
# Based on Google Analytics' IP masking | ||
# https://support.google.com/analytics/answer/2763052 | ||
def ip | ||
addr = IPAddr.new(@request.remote_ip) | ||
addr.ipv4? ? addr.mask(24).to_s : addr.mask(48).to_s | ||
end | ||
|
||
# User agents don't need to be valid UTF-8, but we would like them to be | ||
def user_agent | ||
@request.user_agent.encode('UTF-8', invalid: :replace, undef: :replace, replace: '') | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# frozen_string_literal: true | ||
|
||
# Tracks metrics via the SDR Metrics API | ||
# https://github.com/sul-dlss/sdr-metrics-api | ||
# | ||
# See also Ahoy's API spec: | ||
# https://github.com/ankane/ahoy#api-spec | ||
class MetricsService | ||
attr_reader :base_url | ||
|
||
def initialize(base_url: Settings.metrics_api_url) | ||
@base_url = base_url | ||
end | ||
|
||
def track_visit(data) | ||
post_json('/ahoy/visits', data) | ||
end | ||
|
||
def track_event(data) | ||
post_json('/ahoy/events', data) | ||
end | ||
|
||
private | ||
|
||
def post_json(url, data) | ||
connection.post(url) do |req| | ||
req.headers['Content-Type'] = 'application/json' | ||
req.headers['Ahoy-Visit'] = data[:visit_token] | ||
req.headers['Ahoy-Visitor'] = data[:visitor_token] | ||
req.body = data.to_json | ||
end | ||
rescue Faraday::ConnectionFailed => e | ||
Rails.logger.error("Error sending metrics: #{e}") | ||
nil | ||
end | ||
|
||
def connection | ||
@connection ||= Faraday.new(base_url) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
RSpec.describe MetricsConcern do | ||
controller do | ||
# rubocop:disable RSpec/DescribedClass | ||
include MetricsConcern | ||
# rubocop:enable RSpec/DescribedClass | ||
|
||
def download | ||
track_download params[:druid], file: params[:file] | ||
head :ok | ||
end | ||
end | ||
|
||
let(:metrics_service) { instance_double(MetricsService) } | ||
let(:visit_cookie) { 'abc123' } | ||
let(:visitor_cookie) { 'xyz789' } | ||
|
||
before do | ||
allow(Settings.features).to receive(:metrics).and_return(true) | ||
allow(controller).to receive(:metrics_service).and_return(metrics_service) | ||
routes.draw { get 'download' => 'anonymous#download' } | ||
cookies[:ahoy_visit] = visit_cookie | ||
cookies[:ahoy_visitor] = visitor_cookie | ||
end | ||
|
||
describe '#track_download' do | ||
before do | ||
allow(metrics_service).to receive(:track_visit) | ||
allow(metrics_service).to receive(:track_event) | ||
end | ||
|
||
it 'tracks a download event with the druid' do | ||
get 'download', params: { druid: 'fd063dh3727' } | ||
expect(metrics_service).to have_received(:track_event).with( | ||
visit_token: visit_cookie, | ||
visitor_token: visitor_cookie, | ||
events: [ | ||
{ | ||
id: be_kind_of(String), | ||
time: be_kind_of(Time), | ||
name: 'download', | ||
properties: { | ||
druid: 'fd063dh3727' | ||
} | ||
} | ||
] | ||
) | ||
end | ||
|
||
context 'when an individual file is passed' do | ||
it 'tracks the event with the druid and filename' do | ||
get 'download', params: { druid: 'fd063dh3727', file: 'file.txt' } | ||
expect(metrics_service).to have_received(:track_event).with( | ||
visit_token: visit_cookie, | ||
visitor_token: visitor_cookie, | ||
events: [ | ||
{ | ||
id: be_kind_of(String), | ||
time: be_kind_of(Time), | ||
name: 'download', | ||
properties: { | ||
druid: 'fd063dh3727', | ||
file: 'file.txt' | ||
} | ||
} | ||
] | ||
) | ||
end | ||
end | ||
|
||
context 'when a visit is not in progress' do | ||
let(:visit_cookie) { nil } | ||
|
||
it 'creates a new visit' do | ||
get 'download', params: { druid: 'fd063dh3727' } | ||
expect(metrics_service).to have_received(:track_visit) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.