diff --git a/app/assets/stylesheets/dashboard.scss b/app/assets/stylesheets/dashboard.scss index 2b2c16be3..79f4b5de7 100644 --- a/app/assets/stylesheets/dashboard.scss +++ b/app/assets/stylesheets/dashboard.scss @@ -36,11 +36,7 @@ display: inline-block; width: fit-content; margin-bottom: 10px; - @include themeable( - color, - theme-color, - $black - ); + @include themeable(color, theme-color, $black); @include themeable( background, theme-container-background, @@ -55,16 +51,8 @@ } &.active { - @include themeable( - background, - theme-secondary-color, - $dark-gray - ); - @include themeable( - color, - theme-container-background, - white - ); + @include themeable(background, theme-secondary-color, $dark-gray); + @include themeable(color, theme-container-background, white); } &.back-nav { @@ -402,7 +390,7 @@ & li.ellipsis-menu-item { list-style: none; - + & form * { background-color: transparent; } @@ -721,6 +709,7 @@ padding: 20px; margin: 5px; @include themeable(background-color, theme-container-background, #fff); + h4 { font-size: 0.8em; margin: 0px; @@ -730,6 +719,28 @@ .stat-percentage { font-size: 0.8em; } + + table { + margin: 1rem auto; + text-align: left; + width: 50%; + + thead { + line-height: 3rem; + + th:last-child { + text-align: right; + } + } + + tbody { + line-height: 2rem; + + td:last-child { + text-align: right; + } + } + } } .featured-stat { diff --git a/app/controllers/api/v0/analytics_controller.rb b/app/controllers/api/v0/analytics_controller.rb index b79c90370..173d76d91 100644 --- a/app/controllers/api/v0/analytics_controller.rb +++ b/app/controllers/api/v0/analytics_controller.rb @@ -35,6 +35,15 @@ module Api render json: data.to_json end + def referrers + analytics = AnalyticsService.new( + @owner, + start_date: params[:start], end_date: params[:end], article_id: params[:article_id], + ) + data = analytics.referrers + render json: data.to_json + end + private def authorize_pro_user diff --git a/app/controllers/page_views_controller.rb b/app/controllers/page_views_controller.rb index 3ee66ee71..ef287a1d0 100644 --- a/app/controllers/page_views_controller.rb +++ b/app/controllers/page_views_controller.rb @@ -1,28 +1,27 @@ class PageViewsController < ApplicationController # No policy needed. All views are for all users def create - if user_signed_in? - PageView.create(user_id: current_user.id, - article_id: page_view_params[:article_id], - referrer: page_view_params[:referrer], - user_agent: page_view_params[:user_agent]) - else - PageView.create(counts_for_number_of_views: 10, - article_id: page_view_params[:article_id], - referrer: page_view_params[:referrer], - user_agent: page_view_params[:user_agent]) - end + page_view_create_params = if user_signed_in? + page_view_params.merge(user_id: current_user.id) + else + page_view_params.merge(counts_for_number_of_views: 10) + end + + PageView.create(page_view_create_params) + update_article_page_views + head :ok end def update if user_signed_in? page_view = PageView.where(article_id: params[:id], user_id: current_user.id).last - page_view ||= PageView.create(user_id: current_user.id, - article_id: params[:id]) # pageview is sometimes missing if failure on prior creation. + # pageview is sometimes missing if failure on prior creation. + page_view ||= PageView.create(user_id: current_user.id, article_id: params[:id]) page_view.update_column(:time_tracked_in_seconds, page_view.time_tracked_in_seconds + 15) end + head :ok end @@ -34,7 +33,6 @@ class PageViewsController < ApplicationController @article = Article.find(page_view_params[:article_id]) new_page_views_count = @article.page_views.sum(:counts_for_number_of_views) @article.update_column(:page_views_count, new_page_views_count) if new_page_views_count > @article.page_views_count - return if Rails.env.production? && rand(20) != 1 # We need to do this operation even less often. update_organic_page_views end @@ -44,11 +42,19 @@ class PageViewsController < ApplicationController end def update_organic_page_views - organic_count = @article.page_views.where(referrer: "https://www.google.com/").sum(:counts_for_number_of_views) + return if Rails.env.production? && rand(20) != 1 # We need to do this operation only once in a while. + + page_views_from_google_com = @article.page_views.where(referrer: "https://www.google.com/") + + organic_count = page_views_from_google_com.sum(:counts_for_number_of_views) @article.update_column(:organic_page_views_count, organic_count) if organic_count > @article.organic_page_views_count - organic_count_past_week_count = @article.page_views.where(referrer: "https://www.google.com/").where("created_at > ?", 1.week.ago).sum(:counts_for_number_of_views) + + organic_count_past_week_count = page_views_from_google_com. + where("created_at > ?", 1.week.ago).sum(:counts_for_number_of_views) @article.update_column(:organic_page_views_past_week_count, organic_count_past_week_count) if organic_count_past_week_count > @article.organic_page_views_past_week_count - organic_count_past_month_count = @article.page_views.where(referrer: "https://www.google.com/").where("created_at > ?", 1.month.ago).sum(:counts_for_number_of_views) + + organic_count_past_month_count = page_views_from_google_com. + where("created_at > ?", 1.month.ago).sum(:counts_for_number_of_views) @article.update_column(:organic_page_views_past_month_count, organic_count_past_month_count) if organic_count_past_month_count > @article.organic_page_views_past_month_count end end diff --git a/app/javascript/analytics/client.js b/app/javascript/analytics/client.js new file mode 100644 index 000000000..6b3457738 --- /dev/null +++ b/app/javascript/analytics/client.js @@ -0,0 +1,45 @@ +import handleFetchAPIErrors from '../src/utils/errors'; + +function callAnalyticsAPI(path, date, { organizationId, articleId }, callback) { + let url = `${path}?start=${date.toISOString().split('T')[0]}`; + + if (organizationId) { + url = `${url}&organization_id=${organizationId}`; + } + if (articleId) { + url = `${url}&article_id=${articleId}`; + } + + fetch(url) + .then(handleFetchAPIErrors) + .then(response => response.json()) + .then(callback) + // eslint-disable-next-line no-console + .catch(error => console.error(error)); // we should come up with better error handling +} + +export function callHistoricalAPI( + date, + { organizationId, articleId }, + callback, +) { + callAnalyticsAPI( + '/api/analytics/historical', + date, + { organizationId, articleId }, + callback, + ); +} + +export function callReferrersAPI( + date, + { organizationId, articleId }, + callback, +) { + callAnalyticsAPI( + '/api/analytics/referrers', + date, + { organizationId, articleId }, + callback, + ); +} diff --git a/app/javascript/analytics/dashboard.js b/app/javascript/analytics/dashboard.js index c8facfeef..889a351b8 100644 --- a/app/javascript/analytics/dashboard.js +++ b/app/javascript/analytics/dashboard.js @@ -1,5 +1,5 @@ import Chart from 'chart.js'; -import handleFetchAPIErrors from '../src/utils/errors'; +import { callHistoricalAPI, callReferrersAPI } from './client'; function resetActive(activeButton) { const buttons = document.getElementsByClassName('timerange-button'); @@ -84,7 +84,7 @@ function drawCharts(data, timeRangeLabel) { const readers = parsedData.map(date => date.page_views.total); drawChart({ - canvas: document.getElementById('reactionsChart'), + canvas: document.getElementById('reactions-chart'), title: `Reactions ${timeRangeLabel}`, labels, datasets: [ @@ -120,7 +120,7 @@ function drawCharts(data, timeRangeLabel) { }); drawChart({ - canvas: document.getElementById('commentsChart'), + canvas: document.getElementById('comments-chart'), title: `Comments ${timeRangeLabel}`, labels, datasets: [ @@ -135,7 +135,7 @@ function drawCharts(data, timeRangeLabel) { }); drawChart({ - canvas: document.getElementById('followersChart'), + canvas: document.getElementById('followers-chart'), title: `New Followers ${timeRangeLabel}`, labels, datasets: [ @@ -150,7 +150,7 @@ function drawCharts(data, timeRangeLabel) { }); drawChart({ - canvas: document.getElementById('readersChart'), + canvas: document.getElementById('readers-chart'), title: `Reads ${timeRangeLabel}`, labels, datasets: [ @@ -165,48 +165,65 @@ function drawCharts(data, timeRangeLabel) { }); } -function callAnalyticsApi(date, timeRangeLabel, { organizationId, articleId }) { - let url = `/api/analytics/historical?start=${ - date.toISOString().split('T')[0] - }`; +function renderReferrers(data) { + const container = document.getElementById('referrers-container'); + const tableBody = data.domains + .filter(referrer => referrer.domain) + .map(referrer => { + return ` + + ${referrer.domain} + ${referrer.count} + + `; + }); - if (organizationId) { - url = `${url}&organization_id=${organizationId}`; - } - if (articleId) { - url = `${url}&article_id=${articleId}`; + // add referrers with empty domains if present + const emptyDomainReferrer = data.domains.filter( + referrer => !referrer.domain, + )[0]; + if (emptyDomainReferrer) { + tableBody.push(` + + All other external referrers + ${emptyDomainReferrer.count} + + `); } - fetch(url) - .then(handleFetchAPIErrors) - .then(response => response.json()) - .then(data => { - drawCharts(data, timeRangeLabel); - writeCards(data, timeRangeLabel); - }) - // eslint-disable-next-line no-console - .catch(error => console.error(error)); // we should come up with better error handling + container.innerHTML = tableBody.join(''); +} + +function callAnalyticsAPI(date, timeRangeLabel, { organizationId, articleId }) { + callHistoricalAPI(date, { organizationId, articleId }, data => { + writeCards(data, timeRangeLabel); + drawCharts(data, timeRangeLabel); + }); + + callReferrersAPI(date, { organizationId, articleId }, data => { + renderReferrers(data); + }); } function drawWeekCharts({ organizationId, articleId }) { resetActive(document.getElementById('week-button')); const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - callAnalyticsApi(oneWeekAgo, 'this Week', { organizationId, articleId }); + callAnalyticsAPI(oneWeekAgo, 'this Week', { organizationId, articleId }); } function drawMonthCharts({ organizationId, articleId }) { resetActive(document.getElementById('month-button')); const oneMonthAgo = new Date(); oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1); - callAnalyticsApi(oneMonthAgo, 'this Month', { organizationId, articleId }); + callAnalyticsAPI(oneMonthAgo, 'this Month', { organizationId, articleId }); } function drawInfinityCharts({ organizationId, articleId }) { resetActive(document.getElementById('infinity-button')); // April 1st is when the DEV analytics feature went into place const beginningOfTime = new Date('2019-4-1'); - callAnalyticsApi(beginningOfTime, '', { organizationId, articleId }); + callAnalyticsAPI(beginningOfTime, '', { organizationId, articleId }); } export default function initCharts({ organizationId, articleId }) { diff --git a/app/models/page_view.rb b/app/models/page_view.rb index 0c39b419d..087ed2951 100644 --- a/app/models/page_view.rb +++ b/app/models/page_view.rb @@ -4,6 +4,8 @@ class PageView < ApplicationRecord belongs_to :user, optional: true belongs_to :article + before_create :extract_domain_and_path + algoliasearch index_name: "UserHistory", per_environment: true, if: :belongs_to_pro_user? do attributes :referrer, :user_agent, :article_tags @@ -46,6 +48,14 @@ class PageView < ApplicationRecord private + def extract_domain_and_path + return unless referrer + + parsed_url = Addressable::URI.parse(referrer) + self.domain = parsed_url.domain + self.path = parsed_url.path + end + def belongs_to_pro_user? user&.pro? end diff --git a/app/services/analytics_service.rb b/app/services/analytics_service.rb index 1a64cec3e..d8b6e4994 100644 --- a/app/services/analytics_service.rb +++ b/app/services/analytics_service.rb @@ -8,7 +8,7 @@ class AnalyticsService load_data end - # Totals computes total counts for comments, reactions, follows and page views + # Computes total counts for comments, reactions, follows and page views def totals { comments: { total: comment_data.size }, @@ -18,7 +18,7 @@ class AnalyticsService } end - # Grouped by day computes counts for comments, reactions, follows and page views per each day + # Computes counts for comments, reactions, follows and page views per each day def grouped_by_day return {} unless start_date && end_date @@ -49,6 +49,16 @@ class AnalyticsService end end + # Returns the list of referrers + def referrers(top: 20) + # count_all is the name of the field autogenerated by Rails with COUNT(*) + counts = page_view_data.group(:domain).order(count_all: :desc).limit(top).count + # we transform this in a list of hashes in case we need to add more keys + domains = counts.map { |domain, count| { domain: domain, count: count } } + + { domains: domains } + end + private attr_reader( diff --git a/app/views/articles/stats.html.erb b/app/views/articles/stats.html.erb index 60bfdf56b..3a1f6213c 100644 --- a/app/views/articles/stats.html.erb +++ b/app/views/articles/stats.html.erb @@ -14,49 +14,7 @@

Stats for "<%= @article.title %>"

-
- - - -
- -
-
-
-
-
-
- -
-
-
-

Readers Summary

-
- -
-
-
-

New Followers Summary

-
- -
-
-
-
-
-

Reactions Summary ❤🦄🔖

-
- -
-
-
-

Comments Summary 💬

-
- -
-
-
-
+ <%= render "shared/stats" %> <%= javascript_pack_tag "analyticsArticle", defer: true %> diff --git a/app/views/dashboards/pro.html.erb b/app/views/dashboards/pro.html.erb index 72bc10ed5..8d0e67ba7 100644 --- a/app/views/dashboards/pro.html.erb +++ b/app/views/dashboards/pro.html.erb @@ -22,49 +22,7 @@

This dashboard will highlight deep insights especially relevant to developer relations authors and serious bloggers.

-
- - - -
- -
-
-
-
-
-
- -
-
-
-

Readers Summary

-
- -
-
-
-

New Followers Summary

-
- -
-
-
-
-
-

Reactions Summary ❤🦄🔖

-
- -
-
-
-

Comments Summary 💬

-
- -
-
-
-
+ <%= render "shared/stats" %> <%= javascript_pack_tag "analyticsDashboard", defer: true %> diff --git a/app/views/shared/_stats.html.erb b/app/views/shared/_stats.html.erb new file mode 100644 index 000000000..c1d2b7744 --- /dev/null +++ b/app/views/shared/_stats.html.erb @@ -0,0 +1,59 @@ +
+ + + +
+ +
+
+
+
+
+
+ +
+
+
+

Readers Summary

+
+ +
+
+
+

New Followers Summary

+
+ +
+
+
+
+
+

Reactions Summary ❤🦄🔖

+
+ +
+
+
+

Comments Summary 💬

+
+ +
+
+
+
+
+

Traffic Source Summary 🚦

+ + + + + + + + + + +
SourceViews
+
+
+
diff --git a/config/routes.rb b/config/routes.rb index 93d7ef9e3..d4f01745f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,9 +90,11 @@ Rails.application.routes.draw do post "/update_or_create", to: "github_repos#update_or_create" end end + get "/analytics/totals", to: "analytics#totals" get "/analytics/historical", to: "analytics#historical" get "/analytics/past_day", to: "analytics#past_day" + get "/analytics/referrers", to: "analytics#referrers" end end diff --git a/db/migrate/20190624093012_add_domain_and_path_to_page_views.rb b/db/migrate/20190624093012_add_domain_and_path_to_page_views.rb new file mode 100644 index 000000000..c6f75ad0a --- /dev/null +++ b/db/migrate/20190624093012_add_domain_and_path_to_page_views.rb @@ -0,0 +1,6 @@ +class AddDomainAndPathToPageViews < ActiveRecord::Migration[5.2] + def change + add_column :page_views, :domain, :string + add_column :page_views, :path, :string + end +end diff --git a/db/migrate/20190625143841_add_index_to_page_view_domain.rb b/db/migrate/20190625143841_add_index_to_page_view_domain.rb new file mode 100644 index 000000000..73c39250c --- /dev/null +++ b/db/migrate/20190625143841_add_index_to_page_view_domain.rb @@ -0,0 +1,7 @@ +class AddIndexToPageViewDomain < ActiveRecord::Migration[5.2] + disable_ddl_transaction! + + def change + add_index :page_views, :domain, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index e82556ac0..f65e89924 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_06_19_153428) do +ActiveRecord::Schema.define(version: 2019_06_25_143841) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -598,6 +598,8 @@ ActiveRecord::Schema.define(version: 2019_06_19_153428) do t.bigint "article_id" t.integer "counts_for_number_of_views", default: 1 t.datetime "created_at", null: false + t.string "domain" + t.string "path" t.string "referrer" t.integer "time_tracked_in_seconds", default: 15 t.datetime "updated_at", null: false @@ -605,6 +607,7 @@ ActiveRecord::Schema.define(version: 2019_06_19_153428) do t.bigint "user_id" t.index ["article_id"], name: "index_page_views_on_article_id" t.index ["created_at"], name: "index_page_views_on_created_at" + t.index ["domain"], name: "index_page_views_on_domain" t.index ["user_id"], name: "index_page_views_on_user_id" end diff --git a/lib/assets/.keep b/lib/assets/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/tasks/.keep b/lib/tasks/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake deleted file mode 100644 index 8ce314585..000000000 --- a/lib/tasks/db.rake +++ /dev/null @@ -1,30 +0,0 @@ -# namespace :db do -# -# desc "Copy production database to local" -# -# task :copy_production => :environment do -# puts "FIRING UP!" -# # Download latest dump -# system("echo hey") -# system("heroku pg:backups --remote heroku capture") -# system("curl -o latest.dump `heroku pg:backups --remote heroku public-url`") -# -# -# # get user and database name -# # config = Rails.configuration.database_configuration["development"] -# # database = config["database"] -# # user = config["username"] -# # -# # # import -# # system("pg_restore --verbose --clean --no-acl --no-owner -h localhost -d #{database} #{Rails.root}/tmp/latest.dump") -# end -# -# end -# -# -# heroku pg:backups --remote heroku capture -# curl -o latest.dump `heroku pg:backups --remote heroku public-url` -# rake db:reset -# pg_restore --verbose --no-acl --no-owner -t articles -t users -t podcasts -t podcast_episodes -t sponsors -t identities -t organizations -h localhost -d PracticalDeveloper_development latest.dump -# rake db:migrate -# pg_restore --verbose --clean --no-acl --no-owner -t articles -t users -t podcasts -t podcast_episodes -t sponsors -t identities -t organizations -h localhost -d PracticalDeveloper_development latest.dump diff --git a/lib/tasks/linters.rake b/lib/tasks/linters.rake deleted file mode 100644 index b7cc84fd4..000000000 --- a/lib/tasks/linters.rake +++ /dev/null @@ -1,29 +0,0 @@ -if %w[development test].include? Rails.env - namespace :lint do - desc "eslint" - task :eslint do - cmd = "cd client && npm run eslint . -- --ext .jsx,.js" - puts "Running eslint via `#{cmd}`" - sh cmd - end - - desc "jscs" - task :jscs do - cmd = "cd client && npm run jscs ." - puts "Running jscs via `#{cmd}`" - sh cmd - end - - desc "JS Linting" - task js: %i[eslint jscs] do - puts "Completed running all JavaScript Linters" - end - - task lint: [:js] do - puts "Completed all linting" - end - end - - desc "Runs all linters. Run `rake -D lint` to see all available lint options" - task lint: ["lint:lint"] -end diff --git a/lib/tasks/temporary/page_views.rake b/lib/tasks/temporary/page_views.rake new file mode 100644 index 000000000..da10ec07a --- /dev/null +++ b/lib/tasks/temporary/page_views.rake @@ -0,0 +1,23 @@ +namespace :page_views do + desc "Update domain and path from page views referrers" + task update_domain_path: :environment do + puts "Going to update #{PageView.count} users" + + ActiveRecord::Base.transaction do + PageView.find_each do |pv| + next unless pv.referrer + next if pv.domain # allows retries + + begin + parsed_url = Addressable::URI.parse(pv.referrer) + pv.update!(domain: parsed_url.domain, path: parsed_url.path) + rescue StandardError => e + Rails.logger.error("#{pv.id}: #{e.message}") + next + end + end + end + + puts "All done now!" + end +end diff --git a/spec/factories/page_views.rb b/spec/factories/page_views.rb index 235805412..24f144573 100644 --- a/spec/factories/page_views.rb +++ b/spec/factories/page_views.rb @@ -2,5 +2,6 @@ FactoryBot.define do factory :page_view do user article + referrer { Faker::Internet.url } end end diff --git a/spec/models/page_view_spec.rb b/spec/models/page_view_spec.rb index 330c65ea9..87a5e6507 100644 --- a/spec/models/page_view_spec.rb +++ b/spec/models/page_view_spec.rb @@ -1,6 +1,22 @@ require "rails_helper" RSpec.describe PageView, type: :model do + let(:article) { create(:article) } + it { is_expected.to belong_to(:user).optional } it { is_expected.to belong_to(:article) } + + describe "#domain" do + it "is automatically set when a new page view is created" do + pv = create(:page_view, referrer: "http://example.com/page") + expect(pv.reload.domain).to eq("example.com") + end + end + + describe "#path" do + it "is automatically set when a new page view is created" do + pv = create(:page_view, referrer: "http://example.com/page") + expect(pv.reload.path).to eq("/page") + end + end end diff --git a/spec/requests/api/v0/analytics_spec.rb b/spec/requests/api/v0/analytics_spec.rb index 620746b03..f03c9e40e 100644 --- a/spec/requests/api/v0/analytics_spec.rb +++ b/spec/requests/api/v0/analytics_spec.rb @@ -38,4 +38,8 @@ RSpec.describe "Api::V0::Analytics", type: :request do describe "GET /api/analytics/past_day" do include_examples "GET /api/analytics/:endpoint authorization examples", "past_day" end + + describe "GET /api/analytics/referrers" do + include_examples "GET /api/analytics/:endpoint authorization examples", "referrers" + end end diff --git a/spec/services/analytics_service_spec.rb b/spec/services/analytics_service_spec.rb index 82a6ce479..7b704a78d 100644 --- a/spec/services/analytics_service_spec.rb +++ b/spec/services/analytics_service_spec.rb @@ -9,6 +9,11 @@ RSpec.describe AnalyticsService, type: :service do 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) @@ -165,11 +170,6 @@ RSpec.describe AnalyticsService, type: :service do end describe "#grouped_by_day" do - def format_date(datetime) - # Postgre's DATE(..) method uses UTC. - datetime.utc.to_date.iso8601 - end - it "returns stats grouped by day" do stats = described_class.new( user, start_date: "2019-04-01", end_date: "2019-04-04" @@ -370,4 +370,63 @@ RSpec.describe AnalyticsService, type: :service do 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 count" 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) + + expected_result = [ + { domain: Addressable::URI.parse(top_url).domain, count: 3 }, + { 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 { create(:page_view, user: user, article: article, referrer: Faker::Internet.url) } # rubocop:disable FactoryBot/CreateList + 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