Track success of DEV Shop ads (#3589)

* Add basic display ad event tracking

* Finalize client-side eventlistening functionality
This commit is contained in:
Ben Halpern 2019-07-31 13:44:47 -04:00 committed by GitHub
parent 3071ac8c76
commit 5d380f93cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 246 additions and 4 deletions

View file

@ -52,6 +52,7 @@ function callInitalizers(){
nextPage = 0;
fetching = false;
done = false;
adClicked = false;
setTimeout(function(){
done = false;
},300);

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -51,9 +51,9 @@
</div>
<% 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 %>
<div class="widget">
<div class="widget" id="sponsorship-arbitrary-display-widget" data-id="<%= @left_sidebar_ad.id %>">
<div class="widget-body" style="margin-top:-6px">
<%= @left_sidebar_ad.processed_html.html_safe %>
</div>

View file

@ -19,7 +19,7 @@
</div>
<% 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 %>
<div class="widget">
<div class="widget-body" style="margin-top:-6px">

View file

@ -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"

View file

@ -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

View file

@ -0,0 +1,5 @@
class AddSuccessRateToDisplayAds < ActiveRecord::Migration[5.2]
def change
add_column :display_ads, :success_rate, :float, default: 0.0
end
end

View file

@ -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

View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :display_ad_event do
category { "impression" }
context_type { "home" }
end
end

View file

@ -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

View file

@ -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

View file

@ -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