Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

410 file analytics #477

Merged
merged 6 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/models/file_download_stat_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

# OVERRIDE Hyrax hyrax-v3.5.0 to require Hyrax::Download so the method below doesn't fail

Hyrax::Download # rubocop:disable Lint/Void

module FileDownloadStatClass
# Hyrax::Download is sent to Hyrax::Analytics.profile as #hyrax__download
# see Legato::ProfileMethods.method_name_from_klass
def ga_statistics(start_date, file)
profile = Hyrax::Analytics.profile
unless profile
Hyrax.logger.error("Google Analytics profile has not been established. Unable to fetch statistics.")
return []
end
# OVERRIDE Hyrax hyrax-v3.5.0
profile.hyrax__download(sort: 'date',
start_date: start_date,
end_date: Date.yesterday,
limit: 10_000)
.for_file(file.id)
end
end

FileDownloadStat.singleton_class.send(:prepend, FileDownloadStatClass)
28 changes: 28 additions & 0 deletions app/models/hyrax/statistic_decorator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

# OVERRIDE Hyrax hyrax-v3.5.0 to require Hyrax::Pageview so the method below doesn't fail

Hyrax::Pageview # rubocop:disable Lint/Void

module Hyrax
module StatisticClassDecorator
# Hyrax::Pageview is sent to Hyrax::Analytics.profile as #hyrax__pageview
# see Legato::ProfileMethods.method_name_from_klass
def ga_statistics(start_date, object)
path = polymorphic_path(object)
profile = Hyrax::Analytics.profile
unless profile
Rails.logger.error("Google Analytics profile has not been established. Unable to fetch statistics.")
return []
end
# OVERRIDE Hyrax hyrax-v3.5.0
profile.hyrax__pageview(sort: 'date',
start_date: start_date,
end_date: Date.yesterday,
limit: 10_000)
.for_path(path)
end
end
end

Hyrax::Statistic.singleton_class.send(:prepend, Hyrax::StatisticClassDecorator)
114 changes: 114 additions & 0 deletions app/models/work_view_stat_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# frozen_string_literal: true

# OVERRIDE Hyrax hyrax-v3.5.0 to reference hyrax__pageviews as listed in app/models/hyrax/statistic_decorator.rb

# rubocop:disable Metrics/BlockLength
RSpec.describe WorkViewStat, type: :model do
let(:work_id) { work.id }
let(:user_id) { 123 }
let(:date) { Time.new.in_time_zone }
let(:work_stat) { described_class.create(work_views: "25", date: date, work_id: work_id, user_id: user_id) }
let(:work) { mock_model(GenericWork, id: 199) }

it "has attributes" do
expect(work_stat).to respond_to(:work_views)
expect(work_stat).to respond_to(:date)
expect(work_stat).to respond_to(:work_id)
expect(work_stat.work_id).to eq("199")
expect(work_stat.date).to eq(date)
expect(work_stat.work_views).to eq(25)
expect(work_stat.user_id).to eq(user_id)
end

describe ".ga_statistic" do
let(:start_date) { 2.days.ago }
let(:expected_path) { Rails.application.routes.url_helpers.hyrax_generic_work_path(work) }

before do
allow(Hyrax::Analytics).to receive(:profile).and_return(profile)
end
context "when a profile is available" do
let(:views) { double }
# OVERRIDE Hyrax hyrax-v3.5.0
let(:profile) { double(hyrax__pageviews: views) }

it "calls the Legato method with the correct path" do
expect(views).to receive(:for_path).with(expected_path)
described_class.ga_statistics(start_date, work)
end
end

context "when a profile not available" do
let(:profile) { nil }

it "calls the Legato method with the correct path" do
expect(described_class.ga_statistics(start_date, work)).to be_empty
end
end
end

describe "#statistics" do
let(:dates) do
ldates = []
4.downto(0) { |idx| ldates << (Time.zone.today - idx.day) }
ldates
end
let(:date_strs) do
dates.map { |date| date.strftime("%Y%m%d") }
end

let(:view_output) do
[[statistic_date(dates[0]), 4], [statistic_date(dates[1]), 8], [statistic_date(dates[2]), 6], [statistic_date(dates[3]), 10]]
end

# This is what the data looks like that's returned from Google Analytics (GA) via the Legato gem
# Due to the nature of querying GA, testing this data in an automated fashion is problematc.
# Sample data structures were created by sending real events to GA from a test instance of
# Scholarsphere. The data below are essentially a "cut and paste" from the output of query
# results from the Legato gem.
let(:sample_work_pageview_statistics) do
[
SpecStatistic.new(date: date_strs[0], pageviews: 4),
SpecStatistic.new(date: date_strs[1], pageviews: 8),
SpecStatistic.new(date: date_strs[2], pageviews: 6),
SpecStatistic.new(date: date_strs[3], pageviews: 10)
]
end

describe "cache empty" do
let(:stats) do
expect(described_class).to receive(:ga_statistics).and_return(sample_work_pageview_statistics)
described_class.statistics(work, Time.zone.today - 4.days, user_id)
end

it "includes cached ga data" do
expect(stats.map(&:to_flot)).to include(*view_output)
end

it "caches data" do
expect(stats.map(&:to_flot)).to include(*view_output)
expect(stats.first.user_id).to eq user_id

# at this point all data should be cached
allow(described_class).to receive(:ga_statistics).with(Time.zone.today, work).and_raise("We should not call Google Analytics All data should be cached!")

stats2 = described_class.statistics(work, Time.zone.today - 5.days)
expect(stats2.map(&:to_flot)).to include(*view_output)
end
end

describe "cache loaded" do
let!(:work_view_stat) { described_class.create(date: (Time.zone.today - 5.days).to_datetime, work_id: work_id, work_views: "25") }

let(:stats) do
expect(described_class).to receive(:ga_statistics).and_return(sample_work_pageview_statistics)
described_class.statistics(work, Time.zone.today - 5.days)
end

it "includes cached data" do
expect(stats.map(&:to_flot)).to include([work_view_stat.date.to_i * 1000, work_view_stat.work_views], *view_output)
end
end
end
end
# rubocop:enable Metrics/BlockLength
109 changes: 109 additions & 0 deletions spec/models/file_download_stat_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

# OVERRIDE Hyrax hyrax-v3.5.0 to reference hyrax__pageviews as listed in app/models/file_download_stat_decorator.rb

RSpec.describe FileDownloadStat, type: :model do
let(:file_id) { file.id }
let(:date) { Time.current }
let(:file_stat) { described_class.new(downloads: "2", date: date, file_id: file_id) }
let(:file) { mock_model(FileSet, id: 99) }

it "has attributes" do
expect(file_stat).to respond_to(:downloads)
expect(file_stat).to respond_to(:date)
expect(file_stat).to respond_to(:file_id)
expect(file_stat.file_id).to eq("99")
expect(file_stat.date).to eq(date)
expect(file_stat.downloads).to eq(2)
end

describe ".ga_statistic" do
let(:start_date) { 2.days.ago }
let(:expected_path) { Rails.application.routes.url_helpers.hyrax_file_set_path(file) }

before do
allow(Hyrax::Analytics).to receive(:profile).and_return(profile)
end
context "when a profile is available" do
let(:views) { double }
# OVERRIDE Hyrax hyrax-v3.5.0
let(:profile) { double(hyrax__download: views) }

it "calls the Legato method with the correct path" do
expect(views).to receive(:for_file).with(99)
described_class.ga_statistics(start_date, file)
end
end

context "when a profile not available" do
let(:profile) { nil }

it "calls the Legato method with the correct path" do
expect(described_class.ga_statistics(start_date, file)).to be_empty
end
end
end

describe "#statistics" do
let(:dates) do
ldates = []
4.downto(0) { |idx| ldates << (Time.zone.today - idx.day) }
ldates
end
let(:date_strs) do
dates.map { |date| date.strftime("%Y%m%d") }
end

let(:download_output) do
[[statistic_date(dates[0]), 1], [statistic_date(dates[1]), 1], [statistic_date(dates[2]), 2], [statistic_date(dates[3]), 3]]
end

# This is what the data looks like that's returned from Google Analytics (GA) via the Legato gem
# Due to the nature of querying GA, testing this data in an automated fashion is problematc.
# Sample data structures were created by sending real events to GA from a test instance of
# Scholarsphere. The data below are essentially a "cut and paste" from the output of query
# results from the Legato gem.
let(:sample_download_statistics) do
[
SpecStatistic.new(eventCategory: "Files", eventAction: "Downloaded", eventLabel: "hyrax:x920fw85p", date: date_strs[0], totalEvents: "1"),
SpecStatistic.new(eventCategory: "Files", eventAction: "Downloaded", eventLabel: "hyrax:x920fw85p", date: date_strs[1], totalEvents: "1"),
SpecStatistic.new(eventCategory: "Files", eventAction: "Downloaded", eventLabel: "hyrax:x920fw85p", date: date_strs[2], totalEvents: "2"),
SpecStatistic.new(eventCategory: "Files", eventAction: "Downloaded", eventLabel: "hyrax:x920fw85p", date: date_strs[3], totalEvents: "3")
]
end

describe "cache empty" do
let(:stats) do
expect(described_class).to receive(:ga_statistics).and_return(sample_download_statistics)
described_class.statistics(file, Time.zone.today - 4.days)
end

it "includes cached ga data" do
expect(stats.map(&:to_flot)).to include(*download_output)
end

it "caches data" do
expect(stats.map(&:to_flot)).to include(*download_output)

# at this point all data should be cached
allow(described_class).to receive(:ga_statistics).with(Time.zone.today, file).and_raise("We should not call Google Analytics All data should be cached!")

stats2 = described_class.statistics(file, Time.zone.today - 4.days)
expect(stats2.map(&:to_flot)).to include(*download_output)
end
end

describe "cache loaded" do
let!(:file_download_stat) { described_class.create(date: (Time.zone.today - 5.days).to_datetime, file_id: file_id, downloads: "25") }

let(:stats) do
expect(described_class).to receive(:ga_statistics).and_return(sample_download_statistics)
described_class.statistics(file, Time.zone.today - 5.days)
end

it "includes cached data" do
expect(stats.map(&:to_flot)).to include([file_download_stat.date.to_i * 1000, file_download_stat.downloads], *download_output)
end
end
end
end