* Add counts for number of views when showing referrers We're sampling unauthenticated page views (every 10th visit is recorded) and using the "counts_for_number_of_views" attribute to mark the 10x weight of these sampled events. The referrers section of the statistics page however was counting the number of page view rows (without regard to the weight) and it looked like external referrers (like a google search) were counting 10 times on the statistics page. Update the analytics service to show the sum of the weights, rather than the count of events, on the "Traffic Source Summary" of the article stats page. This is loaded from /api/analytics/referrers?article_id=:id on the front end. Needs a test that demonstrates this (currently all specs for this stayed green suggesting the coverage didn't include anything that represents this situation). * Avoid calling sum twice by reusing the value alias the .sum(:column) method in this chain creates an alias 'select sum(column) as sum_column' which can be used here to avoid recalculating the sum once for sorting and again for return value. Sort by the return value's alias. * Update the test to include weighted page views Since we want to demonstrate that the count shown for visits matches the sum of the weighted page views, add weighting to the top url visits and expect the sum of the weights to be presented.
445 lines
19 KiB
Ruby
445 lines
19 KiB
Ruby
require "rails_helper"
|
|
|
|
RSpec.describe AnalyticsService, type: :service do
|
|
let(:user) { create(:user) }
|
|
let(:organization) { create(:organization) }
|
|
let(:article) { create(:article, user: user, published: true) }
|
|
|
|
before do
|
|
# We use Zonebie to fortify the code and spot time zone related problems but in this
|
|
# particular case it can make these tests fail because,
|
|
# for example, "2019-07-21T23:57:12-12:00" is the 22th in UTC, and since
|
|
# the code delegates to DATE() in PostgreSQL which runs on UTC without time zone
|
|
# info, there's no easy way for these tests to work correctly in a time zone on
|
|
# the day line like "International Date Line West".
|
|
# For this reason data created on "2019-07-21T23:57:12-12:00" will appear on the 22nd in the DB
|
|
# and hence never be selected by the Analytics engine
|
|
# In the meantime for a lack of a better solution, we force this tests to run at midday in UTC
|
|
Timecop.freeze("2019-04-01T12:00:00Z")
|
|
end
|
|
|
|
after { Timecop.return }
|
|
|
|
def format_date(datetime)
|
|
# PostgreSQL DATE(..) function uses UTC.
|
|
datetime.utc.to_date.iso8601
|
|
end
|
|
|
|
describe "initialization" do
|
|
it "raises an error if start date is invalid" do
|
|
expect(-> { described_class.new(user, start_date: "2000-") }).to raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an error if end date is invalid" do
|
|
expect(-> { described_class.new(user, end_date: "2000-") }).to raise_error(ArgumentError)
|
|
end
|
|
|
|
it "raises an error if an article does not belong to the user" do
|
|
other_user = create(:user)
|
|
article = create(:article, user: other_user)
|
|
expect(-> { described_class.new(user, article_id: article.id) }).to raise_error(ArgumentError)
|
|
end
|
|
end
|
|
|
|
describe "#totals" do
|
|
let(:analytics_service) { described_class.new(user) }
|
|
|
|
it "returns totals stats for comments, reactions, follows and page views" do
|
|
totals = analytics_service.totals
|
|
expect(totals.keys.to_set).to eq(%i[comments reactions follows page_views].to_set)
|
|
end
|
|
|
|
it "returns totals stats for an org" do
|
|
totals = described_class.new(organization).totals
|
|
expect(totals.keys.to_set).to eq(%i[comments reactions follows page_views].to_set)
|
|
end
|
|
|
|
describe "comments stats" do
|
|
it "returns stats" do
|
|
stats = analytics_service.totals[:comments]
|
|
expect(stats.keys).to eq(%i[total])
|
|
end
|
|
|
|
it "returns the total number of comments" do
|
|
create(:comment, commentable: article, score: 1)
|
|
expected_stats = { total: 1 }
|
|
expect(analytics_service.totals[:comments]).to eq(expected_stats)
|
|
end
|
|
|
|
it "returns zero as total if there are no scored comments" do
|
|
create(:comment, commentable: article, score: 0)
|
|
expected_stats = { total: 0 }
|
|
expect(analytics_service.totals[:comments]).to eq(expected_stats)
|
|
end
|
|
end
|
|
|
|
describe "reactions stats" do
|
|
before { Reaction.where(reactable: article).delete_all }
|
|
|
|
it "returns stats" do
|
|
stats = described_class.new(user).totals[:reactions]
|
|
expect(stats.keys).to eq(%i[total like readinglist unicorn])
|
|
end
|
|
|
|
it "returns the total number of reactions" do
|
|
create(:reaction, reactable: article)
|
|
expect(analytics_service.totals[:reactions][:total]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as total if there are no public category reactions" do
|
|
reaction = create(:reaction, reactable: article)
|
|
reaction.update_columns(category: "thumbsdown")
|
|
expect(analytics_service.totals[:reactions][:total]).to eq(0)
|
|
end
|
|
|
|
it "returns the number of like reactions" do
|
|
create(:reaction, reactable: article, category: :like)
|
|
expect(analytics_service.totals[:reactions][:like]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as the number of like reactions" do
|
|
create(:reaction, reactable: article, category: :unicorn)
|
|
expect(analytics_service.totals[:reactions][:like]).to eq(0)
|
|
end
|
|
|
|
it "returns the number of readinglist reactions" do
|
|
create(:reaction, reactable: article, category: :readinglist)
|
|
expect(analytics_service.totals[:reactions][:readinglist]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as the number of readinglist reactions" do
|
|
create(:reaction, reactable: article, category: :unicorn)
|
|
expect(analytics_service.totals[:reactions][:readinglist]).to eq(0)
|
|
end
|
|
|
|
it "returns the number of unicorn reactions" do
|
|
create(:reaction, reactable: article, category: :unicorn)
|
|
expect(analytics_service.totals[:reactions][:unicorn]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as the number of unicorn reactions" do
|
|
create(:reaction, reactable: article, category: :like)
|
|
expect(analytics_service.totals[:reactions][:unicorn]).to eq(0)
|
|
end
|
|
end
|
|
|
|
describe "follows stats" do
|
|
it "returns stats" do
|
|
stats = analytics_service.totals[:follows]
|
|
expect(stats.keys).to eq(%i[total])
|
|
end
|
|
|
|
it "returns the total number of follows" do
|
|
create(:follow, followable: user)
|
|
expected_stats = { total: 1 }
|
|
expect(analytics_service.totals[:follows]).to eq(expected_stats)
|
|
end
|
|
|
|
it "returns zero as total if there are no follows" do
|
|
expected_stats = { total: 0 }
|
|
expect(analytics_service.totals[:follows]).to eq(expected_stats)
|
|
end
|
|
end
|
|
|
|
describe "page views stats" do
|
|
it "returns stats" do
|
|
stats = analytics_service.totals[:page_views]
|
|
expect(stats.keys).to eq(%i[total average_read_time_in_seconds total_read_time_in_seconds])
|
|
end
|
|
|
|
it "returns the total number of page views from page_views_count" do
|
|
article.update_columns(page_views_count: 1)
|
|
expect(analytics_service.totals[:page_views][:total]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as total if there are no page views" do
|
|
expect(analytics_service.totals[:page_views][:total]).to eq(0)
|
|
end
|
|
|
|
it "returns the average read time in seconds" do
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: 15)
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: 45)
|
|
expect(analytics_service.totals[:page_views][:average_read_time_in_seconds]).to eq(30)
|
|
end
|
|
|
|
it "returns zero as the average read time in seconds without views" do
|
|
expect(analytics_service.totals[:page_views][:average_read_time_in_seconds]).to eq(0)
|
|
end
|
|
|
|
it "returns the total read time in seconds" do
|
|
article.update_columns(page_views_count: 1)
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: 15)
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: 45)
|
|
# average read time * total_views
|
|
expect(analytics_service.totals[:page_views][:total_read_time_in_seconds]).to eq(30)
|
|
end
|
|
|
|
it "returns zero as the total read time in seconds with no page views" do
|
|
expect(analytics_service.totals[:page_views][:total_read_time_in_seconds]).to eq(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#grouped_by_day" do
|
|
it "returns stats grouped by day" do
|
|
stats = described_class.new(
|
|
user, start_date: "2019-04-01", end_date: "2019-04-04"
|
|
).grouped_by_day
|
|
expect(stats.keys).to eq(%w[2019-04-01 2019-04-02 2019-04-03 2019-04-04])
|
|
end
|
|
|
|
it "returns stats for comments, reactions, follows and page views for a specific day" do
|
|
stats = described_class.new(user, start_date: "2019-04-01").grouped_by_day
|
|
expect(stats["2019-04-01"].keys.to_set).to eq(%i[comments reactions page_views follows].to_set)
|
|
end
|
|
|
|
it "returns stats for an org" do
|
|
stats = described_class.new(
|
|
organization, start_date: "2019-04-01", end_date: "2019-04-04"
|
|
).grouped_by_day
|
|
expect(stats.keys).to eq(%w[2019-04-01 2019-04-02 2019-04-03 2019-04-04])
|
|
expect(stats["2019-04-01"].keys.to_set).to eq(%i[comments reactions page_views follows].to_set)
|
|
end
|
|
|
|
describe "comments stats on a specific day" do
|
|
it "returns stats" do
|
|
analytics_service = described_class.new(user, start_date: "2019-04-01", end_date: "2019-04-04")
|
|
stats = analytics_service.grouped_by_day["2019-04-01"][:comments]
|
|
expect(stats.keys).to eq(%i[total])
|
|
end
|
|
|
|
it "returns the total number of comments" do
|
|
comment = create(:comment, commentable: article, score: 1)
|
|
expected_stats = { total: 1 }
|
|
date = format_date(comment.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:comments]).to eq(expected_stats)
|
|
end
|
|
|
|
it "returns zero as total if there are no scored comments" do
|
|
comment = create(:comment, commentable: article, score: 0)
|
|
expected_stats = { total: 0 }
|
|
date = format_date(comment.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:comments]).to eq(expected_stats)
|
|
end
|
|
end
|
|
|
|
describe "reactions stats on a specific day" do
|
|
before { Reaction.where(reactable: article).delete_all }
|
|
|
|
it "returns stats" do
|
|
analytics_service = described_class.new(user, start_date: "2019-04-01")
|
|
stats = analytics_service.grouped_by_day["2019-04-01"][:reactions]
|
|
expect(stats.keys).to eq(%i[total like readinglist unicorn])
|
|
end
|
|
|
|
it "returns the total number of reactions" do
|
|
reaction = create(:reaction, reactable: article)
|
|
date = format_date(reaction.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:reactions][:total]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as total if there are no public category reactions" do
|
|
reaction = create(:reaction, reactable: article)
|
|
reaction.update_columns(category: "thumbsdown")
|
|
date = format_date(reaction.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:reactions][:total]).to eq(0)
|
|
end
|
|
|
|
it "returns the number of like reactions" do
|
|
reaction = create(:reaction, reactable: article, category: :like)
|
|
date = format_date(reaction.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:reactions][:like]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as the number of like reactions" do
|
|
reaction = create(:reaction, reactable: article, category: :unicorn)
|
|
date = format_date(reaction.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:reactions][:like]).to eq(0)
|
|
end
|
|
|
|
it "returns the number of readinglist reactions" do
|
|
reaction = create(:reaction, reactable: article, category: :readinglist)
|
|
date = format_date(reaction.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:reactions][:readinglist]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as the number of readinglist reactions" do
|
|
reaction = create(:reaction, reactable: article, category: :unicorn)
|
|
date = format_date(reaction.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:reactions][:readinglist]).to eq(0)
|
|
end
|
|
|
|
it "returns the number of unicorn reactions" do
|
|
reaction = create(:reaction, reactable: article, category: :unicorn)
|
|
date = format_date(reaction.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:reactions][:unicorn]).to eq(1)
|
|
end
|
|
|
|
it "returns zero as the number of unicorn reactions" do
|
|
reaction = create(:reaction, reactable: article, category: :like)
|
|
date = format_date(reaction.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:reactions][:unicorn]).to eq(0)
|
|
end
|
|
end
|
|
|
|
describe "follows stats for a specific day" do
|
|
it "returns stats" do
|
|
analytics_service = described_class.new(user, start_date: "2019-04-01")
|
|
stats = analytics_service.grouped_by_day["2019-04-01"][:follows]
|
|
expect(stats.keys).to eq(%i[total])
|
|
end
|
|
|
|
it "returns the total number of follows" do
|
|
follow = create(:follow, followable: user)
|
|
date = format_date(follow.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expected_stats = { total: 1 }
|
|
expect(analytics_service.grouped_by_day[date][:follows]).to eq(expected_stats)
|
|
end
|
|
|
|
it "returns zero as total if there are no follows" do
|
|
date = format_date(Time.current)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expected_stats = { total: 0 }
|
|
expect(analytics_service.grouped_by_day[date][:follows]).to eq(expected_stats)
|
|
end
|
|
end
|
|
|
|
describe "page views stats for a specific day" do
|
|
before { PageView.where(article: article).delete_all }
|
|
|
|
it "returns stats" do
|
|
analytics_service = described_class.new(user, start_date: "2019-04-01")
|
|
stats = analytics_service.grouped_by_day["2019-04-01"][:page_views]
|
|
expect(stats.keys).to eq(%i[total average_read_time_in_seconds total_read_time_in_seconds])
|
|
end
|
|
|
|
it "returns the total number of page views from counts_for_number_of_views" do
|
|
pv = create(:page_view, user: user, article: article, counts_for_number_of_views: 5)
|
|
date = format_date(pv.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:page_views][:total]).to eq(5)
|
|
end
|
|
|
|
it "returns zero as total if there are no page views" do
|
|
date = format_date(Time.current)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:page_views][:total]).to eq(0)
|
|
end
|
|
|
|
it "returns the average read time in seconds" do
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: 15)
|
|
pv = create(:page_view, user: user, article: article, time_tracked_in_seconds: 45)
|
|
date = format_date(pv.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][
|
|
:page_views][:average_read_time_in_seconds]).to eq(30)
|
|
end
|
|
|
|
it "returns zero as the average read time in seconds without views" do
|
|
date = format_date(Time.current)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][
|
|
:page_views][:average_read_time_in_seconds]).to eq(0)
|
|
end
|
|
|
|
it "returns the total read time in seconds" do
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: 15)
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: 45)
|
|
date = format_date(article.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
# average read time (30) * total_views (2)
|
|
expect(analytics_service.grouped_by_day[date][
|
|
:page_views][:total_read_time_in_seconds]).to eq(60)
|
|
end
|
|
|
|
it "returns zero as the total read time in seconds with no page views" do
|
|
date = format_date(Time.current)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][
|
|
:page_views][:total_read_time_in_seconds]).to eq(0)
|
|
end
|
|
|
|
it "works correctly if the page views contain nil in time_tracked_in_seconds" do
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: nil)
|
|
create(:page_view, user: user, article: article, time_tracked_in_seconds: nil)
|
|
date = format_date(article.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
expect(analytics_service.grouped_by_day[date][:page_views][:total]).to eq(2)
|
|
expect(analytics_service.grouped_by_day[date][:page_views][:average_read_time_in_seconds]).to eq(0)
|
|
expect(analytics_service.grouped_by_day[date][:page_views][:total_read_time_in_seconds]).to eq(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#referrers" do
|
|
let(:analytics_service) { described_class.new(user) }
|
|
|
|
context "when working on domains" do
|
|
before { PageView.where(article: article).delete_all }
|
|
|
|
it "returns unique domains with a count" do
|
|
url = Faker::Internet.url
|
|
create(:page_view, user: user, article: article, referrer: url)
|
|
create(:page_view, user: user, article: article, referrer: url)
|
|
domains = analytics_service.referrers[:domains]
|
|
expect(domains.first).to eq(domain: Addressable::URI.parse(url).domain, count: 2)
|
|
end
|
|
|
|
it "returns unique domains with a count when there is a date range" do
|
|
url = Faker::Internet.url
|
|
create(:page_view, user: user, article: article, referrer: url)
|
|
create(:page_view, user: user, article: article, referrer: url)
|
|
date = format_date(article.created_at)
|
|
analytics_service = described_class.new(user, start_date: date)
|
|
domains = analytics_service.referrers[:domains]
|
|
expect(domains.first).to eq(domain: Addressable::URI.parse(url).domain, count: 2)
|
|
end
|
|
|
|
it "returns nothing if there is no data" do
|
|
expect(analytics_service.referrers[:domains]).to be_empty
|
|
end
|
|
|
|
it "returns the domains ordered by number of views" do
|
|
other_url = Faker::Internet.url
|
|
create_list(:page_view, 2, user: user, article: article, referrer: other_url)
|
|
top_url = Faker::Internet.url
|
|
create_list(:page_view, 3, user: user, article: article, referrer: top_url, counts_for_number_of_views: 10)
|
|
|
|
expected_result = [
|
|
{ domain: Addressable::URI.parse(top_url).domain, count: 30 },
|
|
{ domain: Addressable::URI.parse(other_url).domain, count: 2 },
|
|
]
|
|
expect(analytics_service.referrers[:domains]).to eq(expected_result)
|
|
end
|
|
|
|
it "returns 20 domains at most by default" do
|
|
21.times do |n|
|
|
create(:page_view, user: user, article: article, referrer: "http://fakeurl#{n}.com")
|
|
end
|
|
expect(analytics_service.referrers[:domains].size).to eq(20)
|
|
end
|
|
|
|
it "returns the most visited domain if asked for only one result" do
|
|
top_url = Faker::Internet.url
|
|
create_list(:page_view, 3, user: user, article: article, referrer: top_url)
|
|
other_url = Faker::Internet.url
|
|
create_list(:page_view, 2, user: user, article: article, referrer: other_url)
|
|
|
|
top_domain = Addressable::URI.parse(top_url).domain
|
|
result = analytics_service.referrers(top: 1)[:domains]
|
|
expect(result).to eq([{ domain: top_domain, count: 3 }])
|
|
end
|
|
end
|
|
end
|
|
end
|