diff --git a/app/assets/javascripts/initializePage.js.erb b/app/assets/javascripts/initializePage.js.erb
index 317839055..2ebd6b221 100644
--- a/app/assets/javascripts/initializePage.js.erb
+++ b/app/assets/javascripts/initializePage.js.erb
@@ -52,6 +52,7 @@ function callInitalizers(){
nextPage = 0;
fetching = false;
done = false;
+ adClicked = false;
setTimeout(function(){
done = false;
},300);
diff --git a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb
index 14d0147e2..cfff836c2 100644
--- a/app/assets/javascripts/initializers/initializeBaseTracking.js.erb
+++ b/app/assets/javascripts/initializers/initializeBaseTracking.js.erb
@@ -141,7 +141,20 @@ function trackCustomImpressions() {
}
}, 15000)
}
- }, 1500)
+
+ // display add
+ var adBox = document.getElementById('sponsorship-arbitrary-display-widget')
+ if (adBox
+ && tokenMeta
+ && !isBot
+ && windowBigEnough
+ && checkUserLoggedIn()) {
+ var csrfToken = tokenMeta.getAttribute('content');
+ trackAdImpression(csrfToken, adBox)
+ adBox.removeEventListener('click', trackAdClick, false );
+ adBox.addEventListener('click', function() { trackAdClick(csrfToken, adBox) });
+ }
+ }, 1800)
}
function trackHTMLVariantTrial(dataBody, csrfToken) {
@@ -192,5 +205,45 @@ function trackFifteenSecondsOnPage(articleId, csrfToken) {
},
credentials: 'same-origin',
})
+}
+function trackAdImpression(token, adBox) {
+ var dataBody = {
+ display_ad_event: {
+ display_ad_id: adBox.dataset.id,
+ context_type: "home",
+ category: "impression",
+ }
+ }
+ window.fetch('/display_ad_events', {
+ method: 'POST',
+ headers: {
+ 'X-CSRF-Token': token,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(dataBody),
+ credentials: 'same-origin',
+ })
+}
+
+function trackAdClick(token, adBox) {
+ if (!adClicked) {
+ var dataBody = {
+ display_ad_event: {
+ display_ad_id: adBox.dataset.id,
+ context_type: "home",
+ category: "click",
+ }
+ }
+ window.fetch('/display_ad_events', {
+ method: 'POST',
+ headers: {
+ 'X-CSRF-Token': token,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(dataBody),
+ credentials: 'same-origin',
+ })
+ }
+ adClicked = true;
}
\ No newline at end of file
diff --git a/app/controllers/display_ad_events_controller.rb b/app/controllers/display_ad_events_controller.rb
new file mode 100644
index 000000000..b4ca580a5
--- /dev/null
+++ b/app/controllers/display_ad_events_controller.rb
@@ -0,0 +1,29 @@
+class DisplayAdEventsController < ApplicationController
+ # No policy needed. All views are for all users
+ def create
+ # Only tracking for logged in users at the moment
+ display_ad_event_create_params = display_ad_event_params.merge(user_id: current_user.id)
+ @display_ad_event = DisplayAdEvent.create(display_ad_event_create_params)
+
+ update_display_ads_data
+
+ head :ok
+ end
+
+ private
+
+ def update_display_ads_data
+ return if Rails.env.production? && rand(20) != 1 # We need to do this operation only once in a while.
+ @display_ad = DisplayAd.find(display_ad_event_params[:display_ad_id])
+ num_impressions = @display_ad.display_ad_events.where(category: "impression").size
+ num_clicks = @display_ad.display_ad_events.where(category: "click").size
+ rate = num_clicks.to_f / num_impressions.to_f
+
+ @display_ad.
+ update_columns(success_rate: rate, clicks_count: num_clicks, impressions_count: num_impressions)
+ end
+
+ def display_ad_event_params
+ params.require(:display_ad_event).permit(%i[context_type category display_ad_id])
+ end
+end
\ No newline at end of file
diff --git a/app/models/display_ad.rb b/app/models/display_ad.rb
index dd815b54d..d3d71674c 100644
--- a/app/models/display_ad.rb
+++ b/app/models/display_ad.rb
@@ -1,11 +1,24 @@
class DisplayAd < ApplicationRecord
belongs_to :organization
+ has_many :display_ad_events
+
validates :organization_id, presence: true
validates :placement_area, presence: true,
inclusion: { in: %w[sidebar_left sidebar_right] }
validates :body_markdown, presence: true
before_save :process_markdown
+ scope :approved_and_published, -> { where(approved: true, published: true) }
+
+
+ def self.for_display(area)
+ if rand(8) == 1
+ approved_and_published.where(placement_area: area).order("success_rate DESC").sample
+ else
+ approved_and_published.where(placement_area: area).order("success_rate DESC").limit(rand(1..15)).sample
+ end
+ end
+
private
def process_markdown
diff --git a/app/models/display_ad_event.rb b/app/models/display_ad_event.rb
new file mode 100644
index 000000000..9029f0720
--- /dev/null
+++ b/app/models/display_ad_event.rb
@@ -0,0 +1,8 @@
+class DisplayAdEvent < ApplicationRecord
+
+ belongs_to :display_ad
+ belongs_to :user
+
+ validates :category, inclusion: {in: %w[impression click]}
+ validates :context_type, inclusion: {in: %w[home]}
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index f0d9e96b9..77687ef8a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -45,6 +45,7 @@ class User < ApplicationRecord
has_many :poll_votes
has_many :poll_skips
has_many :backup_data, foreign_key: "instance_user_id", inverse_of: :instance_user, class_name: "BackupData"
+ has_many :display_ad_events
has_many :access_grants, class_name: "Doorkeeper::AccessGrant", foreign_key: :resource_owner_id, inverse_of: :resource_owner, dependent: :delete_all
has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id, inverse_of: :resource_owner, dependent: :delete_all
diff --git a/app/views/articles/_sidebar.html.erb b/app/views/articles/_sidebar.html.erb
index 73feecc56..73333e667 100644
--- a/app/views/articles/_sidebar.html.erb
+++ b/app/views/articles/_sidebar.html.erb
@@ -51,9 +51,9 @@
<% end %>
<% cache("display-area-left-#{rand(5)}", expires_in: 5.minutes) do %>
- <% @left_sidebar_ad = DisplayAd.where(approved: true, published: true, placement_area: "sidebar_left").order(Arel.sql("RANDOM()")).first %>
+ <% @left_sidebar_ad = DisplayAd.for_display("sidebar_left") %>
<% if @left_sidebar_ad %>
-
+
<% end %>
<% cache("main-article-right-sidebar-discussions-#{params[:timeframe]}", expires_in: (params[:timeframe].blank? ? 120 : 360).seconds) do %>
- <% @sidebar_ad = DisplayAd.where(approved: true, published: true, placement_area: "sidebar_right").order(Arel.sql("RANDOM()")).first %>
+ <% @sidebar_ad = DisplayAd.for_display("sidebar_right") %>
<% if @sidebar_ad %>
diff --git a/config/routes.rb b/config/routes.rb
index aae544b6d..7f192005c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -160,6 +160,7 @@ Rails.application.routes.draw do
resources :poll_skips, only: [:create]
resources :profile_pins, only: %i[create update]
resources :partnerships, only: %i[index create show], param: :option
+ resources :display_ad_events, only: [:create]
get "/chat_channel_memberships/find_by_chat_channel_id" => "chat_channel_memberships#find_by_chat_channel_id"
get "/listings/dashboard" => "classified_listings#dashboard"
diff --git a/db/migrate/20190713213412_create_display_ad_events.rb b/db/migrate/20190713213412_create_display_ad_events.rb
new file mode 100644
index 000000000..ede4839d7
--- /dev/null
+++ b/db/migrate/20190713213412_create_display_ad_events.rb
@@ -0,0 +1,16 @@
+class CreateDisplayAdEvents < ActiveRecord::Migration[5.2]
+ def change
+ create_table :display_ad_events do |t|
+ t.integer :display_ad_id
+ t.integer :user_id
+ t.string :category
+ t.string :context_type
+ t.bigint :context_id
+ t.integer :counts_for, default: 1
+
+ t.timestamps
+ end
+ add_index("display_ad_events", "display_ad_id")
+ add_index("display_ad_events", "user_id")
+ end
+end
diff --git a/db/migrate/20190713225409_add_success_rate_to_display_ads.rb b/db/migrate/20190713225409_add_success_rate_to_display_ads.rb
new file mode 100644
index 000000000..1be92104a
--- /dev/null
+++ b/db/migrate/20190713225409_add_success_rate_to_display_ads.rb
@@ -0,0 +1,5 @@
+class AddSuccessRateToDisplayAds < ActiveRecord::Migration[5.2]
+ def change
+ add_column :display_ads, :success_rate, :float, default: 0.0
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 492fdfca0..44cf75984 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -338,6 +338,19 @@ ActiveRecord::Schema.define(version: 2019_07_23_094834) do
t.index ["priority", "run_at"], name: "delayed_jobs_priority"
end
+ create_table "display_ad_events", force: :cascade do |t|
+ t.string "category"
+ t.bigint "context_id"
+ t.string "context_type"
+ t.integer "counts_for", default: 1
+ t.datetime "created_at", null: false
+ t.integer "display_ad_id"
+ t.datetime "updated_at", null: false
+ t.integer "user_id"
+ t.index ["display_ad_id"], name: "index_display_ad_events_on_display_ad_id"
+ t.index ["user_id"], name: "index_display_ad_events_on_user_id"
+ end
+
create_table "display_ads", force: :cascade do |t|
t.boolean "approved", default: false
t.text "body_markdown"
@@ -350,6 +363,7 @@ ActiveRecord::Schema.define(version: 2019_07_23_094834) do
t.string "placement_area"
t.text "processed_html"
t.boolean "published", default: false
+ t.float "success_rate", default: 0.0
t.datetime "updated_at", null: false
end
diff --git a/spec/factories/display_ad_events.rb b/spec/factories/display_ad_events.rb
new file mode 100644
index 000000000..a4c9a9e38
--- /dev/null
+++ b/spec/factories/display_ad_events.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :display_ad_event do
+ category { "impression" }
+ context_type { "home" }
+ end
+end
diff --git a/spec/models/display_ad_event_spec.rb b/spec/models/display_ad_event_spec.rb
new file mode 100644
index 000000000..f49c2826f
--- /dev/null
+++ b/spec/models/display_ad_event_spec.rb
@@ -0,0 +1,21 @@
+require "rails_helper"
+
+RSpec.describe DisplayAdEvent, type: :model do
+ let(:user) { create(:user) }
+ let(:organization) { create(:organization) }
+ let(:display_ad) { create(:display_ad, organization_id: organization.id) }
+
+ it "creates a click event" do
+ event = build(:display_ad_event, category: "click", user_id: user.id, display_ad_id: display_ad.id)
+ expect(event).to be_valid
+ end
+ it "creates an impression event" do
+ event = build(:display_ad_event, category: "impression", user_id: user.id, display_ad_id: display_ad.id)
+ expect(event).to be_valid
+ end
+
+ it "does not create an invalid event" do
+ event = build(:display_ad_event, category: "wazoo", user_id: user.id, display_ad_id: display_ad.id)
+ expect(event).not_to be_valid
+ end
+end
diff --git a/spec/models/display_ad_spec.rb b/spec/models/display_ad_spec.rb
index 9f4d0091a..44ff9fbbb 100644
--- a/spec/models/display_ad_spec.rb
+++ b/spec/models/display_ad_spec.rb
@@ -23,4 +23,13 @@ RSpec.describe DisplayAd, type: :model do
display_ad.placement_area = "sidebar_left"
expect(display_ad).to be_valid
end
+
+ it "displays published and approved posts" do
+ create(:display_ad, organization_id: organization.id, published: true, approved: true)
+ create(:display_ad, organization_id: organization.id, published: true, approved: true)
+ create(:display_ad, organization_id: organization.id, published: false, approved: true)
+ create(:display_ad, organization_id: organization.id, published: true, approved: false)
+ expect(DisplayAd.for_display(DisplayAd.last.placement_area).published).to eq(true)
+ expect(DisplayAd.for_display(DisplayAd.last.placement_area).approved).to eq(true)
+ end
end
diff --git a/spec/requests/display_ad_events_spec.rb b/spec/requests/display_ad_events_spec.rb
new file mode 100644
index 000000000..aa8b944b0
--- /dev/null
+++ b/spec/requests/display_ad_events_spec.rb
@@ -0,0 +1,65 @@
+require "rails_helper"
+
+RSpec.describe "DisplayAdEvents", type: :request do
+ let(:user) { create(:user, :trusted) }
+ let(:organization) { create(:organization) }
+ let(:display_ad) { create(:display_ad, organization_id: organization.id) }
+
+ describe "POST /display_ad_events" do
+ context "when user signed in" do
+ before do
+ sign_in user
+ end
+
+ it "creates a display ad click event" do
+ post "/display_ad_events", params: {
+ display_ad_event: {
+ display_ad_id: display_ad.id,
+ context_type: "home",
+ category: "click",
+ }
+ }
+ expect(display_ad.reload.clicks_count).to eq(1)
+ end
+ it "creates a display ad impression event" do
+ post "/display_ad_events", params: {
+ display_ad_event: {
+ display_ad_id: display_ad.id,
+ context_type: "home",
+ category: "impression",
+ }
+ }
+ expect(display_ad.reload.impressions_count).to eq(1)
+ end
+ it "creates a display ad success rate" do
+ 4.times do
+ post "/display_ad_events", params: {
+ display_ad_event: {
+ display_ad_id: display_ad.id,
+ context_type: "home",
+ category: "impression",
+ }
+ }
+ end
+ post "/display_ad_events", params: {
+ display_ad_event: {
+ display_ad_id: display_ad.id,
+ context_type: "home",
+ category: "click",
+ }
+ }
+ expect(display_ad.reload.success_rate).to eq(0.25)
+ end
+ it "assigns event to current user" do
+ post "/display_ad_events", params: {
+ display_ad_event: {
+ display_ad_id: display_ad.id,
+ context_type: "home",
+ category: "impression",
+ }
+ }
+ expect(DisplayAdEvent.last.user_id).to eq(user.id)
+ end
+ end
+ end
+end
\ No newline at end of file