Add below-article html variant (#1669)

* Add below-article html variant

* Fix tests for variant number

* Fix stories show tests for html_variants
This commit is contained in:
Ben Halpern 2019-01-28 14:31:43 -05:00 committed by GitHub
parent 1cdbfe3af1
commit ea63374408
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 149 additions and 95 deletions

View file

@ -79,38 +79,61 @@ function logImpressions() {
function trackCustomImpressions() {
setTimeout(function(){
var stickyNav = document.getElementById('article-show-primary-sticky-nav');
var sidebarHTMLVariant = document.getElementById('html-variant-article-show-sidebar');
var ArticleElement = document.getElementById('article-body');
var tokenMeta = document.querySelector("meta[name='csrf-token']")
var randomNumber = Math.floor(Math.random() * 10); // 1 in 10; Only track 1 in 10 impressions
var isBot = /bot|google|baidu|bing|msn|duckduckbot|teoma|slurp|yandex/i.test(navigator.userAgent) // is crawler
if (sidebarHTMLVariant && ArticleElement && tokenMeta && !isBot) {
var windowBigEnough = window.innerWidth > 1250
// Sidebar HTML variant tracking
var stickyNav = document.getElementById('article-show-primary-sticky-nav');
var sidebarHTMLVariant = document.getElementById('html-variant-article-show-sidebar');
if (sidebarHTMLVariant && ArticleElement && tokenMeta && !isBot && windowBigEnough) {
var dataBody = {
html_variant_id: sidebarHTMLVariant.dataset.variantId,
article_id: ArticleElement.dataset.articleId,
};
var csrfToken = tokenMeta.getAttribute('content');
if (randomNumber === 1) {
window.fetch('/html_variant_trials', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify(dataBody),
credentials: 'same-origin',
});
}
trackHTMLVariantTrial(dataBody, csrfToken)
var successLinks = stickyNav.querySelectorAll('a,button'); //track all links and button clicks within nav
for(var i = 0; i < successLinks.length; i++)
{
successLinks[i].addEventListener('click', function() { trackHtmlVariantSuccess(dataBody, csrfToken) });
}
}
// Below article HTML variant tracking
var belowArticleHTMLVariant = document.getElementById('html-variant-article-show-below-article');
if (belowArticleHTMLVariant && ArticleElement && tokenMeta && !isBot && windowBigEnough) {
var dataBody = {
html_variant_id: belowArticleHTMLVariant.dataset.variantId,
article_id: ArticleElement.dataset.articleId,
};
var csrfToken = tokenMeta.getAttribute('content');
trackHTMLVariantTrial(dataBody, csrfToken)
var successLinks = belowArticleHTMLVariant.querySelectorAll('a,button'); //track all links and button clicks within nav
for(var i = 0; i < successLinks.length; i++)
{
successLinks[i].addEventListener('click', function() { trackHtmlVariantSuccess(dataBody, csrfToken) });
}
}
}, 1500)
}
function trackHTMLVariantTrial(dataBody, csrfToken) {
var randomNumber = Math.floor(Math.random() * 10); // 1 in 10; Only track 1 in 10 impressions
if (randomNumber === 1) {
window.fetch('/html_variant_trials', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify(dataBody),
credentials: 'same-origin',
});
}
}
function trackHtmlVariantSuccess(dataBody, csrfToken) {
window.fetch('/html_variant_successes', {
method: 'POST',

View file

@ -918,6 +918,13 @@ article{
}
}
.html-variant-wrapper {
width: 88%;
margin: 0px auto 25px;
@media screen and ( min-width: 500px ){
width: 82%;
}
}
}
.dev-ios-native-body {
@ -955,41 +962,6 @@ article{
border-top: 27px solid $green;
}
}
.showpage-signin-cta{
background: lighten($light-gray, 1%);
border: 1px solid darken($light-medium-gray, 15%);
box-shadow: $bold-shadow;
width: 680px;
max-width:82%;
margin: auto;
text-align: center;
padding: 40px 5%;
font-size: 1.1em;
line-height: 1.33em;
margin-bottom: 30px;
border-radius: 3px;
position: relative;
a{
color: $black;
text-decoration: underline;
display: inline-block;
&.primary-cta-link{
font-size: calc(1.3em + 0.2vw);
background: $bold-blue;
border-radius: 100px;
text-decoration: none;
padding: 16px 0px;
color: white;
width: 300px;
}
}
.showpage-signin-cta-subtext {
font-family: $monospace;
font-size:calc(0.62em + 0.13vw);
color: lighten($medium-gray, 10%);
font-weight: bold;
}
}
.show-comments-header{
width:800px;

View file

@ -4,11 +4,11 @@ class HtmlVariantsController < ApplicationController
def index
authorize HtmlVariant
@html_variants = if params[:state] == "mine"
current_user.html_variants.order("created_at DESC").includes(:user)
current_user.html_variants.order("created_at DESC").includes(:user).page(params[:page]).per(15)
elsif params[:state] == "admin"
HtmlVariant.where(published: true).order("created_at DESC").includes(:user)
HtmlVariant.where(published: true, approved: false).order("created_at DESC").includes(:user).page(params[:page]).per(15)
else
HtmlVariant.where(published: true, approved: true).order("success_rate DESC").includes(:user)
HtmlVariant.where(published: true, approved: true).order("success_rate DESC").includes(:user).page(params[:page]).per(15)
end
end
@ -38,7 +38,6 @@ class HtmlVariantsController < ApplicationController
def create
authorize HtmlVariant
@html_variant = HtmlVariant.new(html_variant_params)
@html_variant.group = "article_show_sidebar_cta"
@html_variant.user_id = current_user.id
if @html_variant.save
flash[:success] = "HTML Variant Created"

View file

@ -205,6 +205,7 @@ class StoriesController < ApplicationController
def handle_article_show
@article_show = true
@variant_number = params[:variant_version] || rand(2)
@comment = Comment.new
assign_user_and_org
@comments_to_show_count = @article.cached_tag_list_array.include?("discuss") ? 50 : 30

View file

@ -1,7 +1,7 @@
class HtmlVariant < ApplicationRecord
validates :html, presence: true
validates :name, uniqueness: true
validates :group, inclusion: { in: %w(article_show_sidebar_cta) }
validates :group, inclusion: { in: %w(article_show_sidebar_cta article_show_below_article_cta) }
validates :success_rate, presence: true
validate :no_edits
belongs_to :user, optional: true
@ -14,27 +14,27 @@ class HtmlVariant < ApplicationRecord
save!
end
def self.find_for_test(tags = [])
def self.find_for_test(tags = [], group = "article_show_sidebar_cta")
tags_array = tags + ["", nil]
if rand(10) == 1 # 10% return completely random
find_random_for_test(tags_array)
find_random_for_test(tags_array, group)
else # 90% chance return one in top 10
find_top_for_test(tags_array)
find_top_for_test(tags_array, group)
end
end
def self.find_top_for_test(tags_array)
where(group: "article_show_sidebar_cta", approved: true, published: true, target_tag: tags_array).order("success_rate DESC").limit(rand(1..15)).sample
def self.find_top_for_test(tags_array, group)
where(group: group, approved: true, published: true, target_tag: tags_array).order("success_rate DESC").limit(rand(1..15)).sample
end
def self.find_random_for_test(tags_array)
where(group: "article_show_sidebar_cta", approved: true, published: true, target_tag: tags_array).order("RANDOM()").first
def self.find_random_for_test(tags_array, group)
where(group: group, approved: true, published: true, target_tag: tags_array).order("RANDOM()").first
end
private
def no_edits
if (approved && html_changed? || name_changed? || group_changed?) && persisted?
if (approved && (html_changed? || name_changed? || group_changed?)) && persisted?
errors.add(:base, "cannot change once published and approved")
end
end

View file

@ -1,33 +1,33 @@
class HtmlVariantPolicy < ApplicationPolicy
def index?
user_admin?
minimal_admin?
end
def show?
user_admin?
minimal_admin?
end
def edit?
user_admin?
minimal_admin?
end
def update?
user_admin?
minimal_admin?
end
def new?
user_admin?
minimal_admin?
end
def create?
user_admin?
minimal_admin?
end
def destroy?
user_admin?
minimal_admin?
end
def permitted_attributes
%i[html name published approved target_tag]
%i[html name published approved target_tag group]
end
end

View file

@ -40,11 +40,13 @@
</div>
</div>
</div>
<% unless user_signed_in? %>
<div class="showpage-signin-cta cta" id="showpage-signin-cta">
<a href="/">dev.to</a> is where software developers read, write, and level up.
<span style="display:block;margin-top:30px;"><a href="/enter" id="primary-cta-link" class="primary-cta-link">Sign Up Now</a></span>
<br/>
<span class="showpage-signin-cta-subtext">(open source and free forever ❤️)</span>
</div>
<% if @variant_number.to_i == 0 && !user_signed_in? && @article.body_markdown.size > 900 %>
<% cache("below-article-html-variant-#{rand(20)}", :expires_in => 8.hours) do %>
<% @html_variant = HtmlVariant.find_for_test(@article.cached_tag_list_array, "article_show_below_article_cta") %>
<% if @html_variant %>
<div class="html-variant-wrapper" id="html-variant-article-show-below-article" data-variant-id="<%= @html_variant.id %>" >
<%= @html_variant.html.html_safe %>
</div>
<% end %>
<% end %>
<% end %>

View file

@ -61,7 +61,7 @@
</div>
<% end %>
</div>
<% if !user_signed_in? %>
<% if @variant_number.to_i == 1 && !user_signed_in? %>
<% cache("html-variant-sidebar#{rand(20)}", :expires_in => 8.hours) do %>
<% @html_variant = HtmlVariant.find_for_test(@article.cached_tag_list_array) %>
<% if @html_variant %>

View file

@ -26,14 +26,20 @@
<div class="form-sub-details">
<%= f.label :published %>
<%= f.check_box :published %>
<%= f.label :target_tag %>
<%= f.text_field :target_tag, placeholder: 'One tag, e.g. javascript. Optional targeting.' %>
<%= f.label :group %>
<%= f.select(:group, options_for_select(["article_show_sidebar_cta", "article_show_below_article_cta"], @html_variant.group || "article_show_sidebar_cta")) %>
</div>
<%= f.submit %>
<% end %>
<% if @html_variant.html.present? %>
<h1 style="text-align:center;">Preview:</h1>
<div class="html-variant-wrapper" style="width: 310px;margin:20px auto;">
<div class="html-variant-wrapper" style="width: 360px;margin:20px auto;">
<%= @html_variant.html.html_safe %>
</div>
<% if @html_variant.group == "article_show_below_article_cta" %>
<div class="html-variant-wrapper" style="width: 880px;margin:20px auto;">
<%= @html_variant.html.html_safe %>
</div>
<% end %>
<% end %>

View file

@ -68,11 +68,14 @@
border-radius: 3px;
margin: 15px;
width: 450px;
height: 550px;
height: 620px;
float: left;
overflow: hidden;
position: relative;
}
.html-variants-page-single-big {
width: 962px;
}
.html-variants-page-single-variant h3 {
margin-top: 0px;
font-size:1.3em;
@ -82,6 +85,10 @@
border: 0px;
width: 310px;
height: 350px;
border: 1px solid #888888;
}
.html-variants-page-single-big iframe {
width: 800px;
}
.html-variants-page-single-variant-details {
padding: 10px;

View file

@ -1,7 +1,7 @@
<div class="html-variants-page-single-variant">
@<%= html_variant.user.username %>:
<div class="html-variants-page-single-variant <%= "html-variants-page-single-big" if html_variant.group == "article_show_below_article_cta" %>">
<%= html_variant.group %> | @<%= html_variant.user.username %>:
<h3>
<%= html_variant.name %>
<a href="/html_variants/<%= html_variant.id %>"><%= html_variant.name %></a>
</h3>
<iframe src="/html_variants/<%= html_variant.id %>"></iframe>
<div class="html-variants-page-single-variant-details">

View file

@ -20,3 +20,4 @@
<% end %>
</div>
<%= paginate @html_variants %>

View file

@ -1,9 +1,22 @@
<style>
body {
all: unset;
}
</style>
<%= render "layouts/styles" %>
<style>
body{padding-top: 20px !important; }
</style>
<div class="primary-sticky-nav">
<div class="html-variant-wrapper">
<%= @html_variant.html.html_safe %>
<% if @html_variant.group == "article_show_below_article_cta" %>
<div class="home">
<div class="container">
<%= @html_variant.html.html_safe %>
</div>
</div>
</div>
<% else %>
<div class="primary-sticky-nav">
<div class="html-variant-wrapper">
<%= @html_variant.html.html_safe %>
</div>
</div>
<% end %>

View file

@ -57,7 +57,8 @@ RSpec.describe "HtmlVariants", type: :request do
html_variant: {
name: "New post",
html: "Yo ho ho#{rand(100)}", tag_list: "yoyo",
published: true
published: true,
group: "article_show_sidebar_cta"
}
}
expect(HtmlVariant.all.size).to eq(1)
@ -72,7 +73,7 @@ RSpec.describe "HtmlVariants", type: :request do
published: true
}
}
expect(HtmlVariant.all.size).to eq(1)
expect(HtmlVariant.all.size).to eq(0)
end
end

View file

@ -31,22 +31,51 @@ RSpec.describe "StoriesShow", type: :request do
expect(response.body).to include "<em>with <b><a href=\"#{user2.path}\">"
end
# sidebar HTML variant
it "renders html variant" do
html_variant = create(:html_variant, published: true, approved: true)
get article.path
get article.path + "?variant_version=1"
expect(response.body).to include html_variant.html
end
it "Does not render variant when no variants published" do
html_variant = create(:html_variant, published: false, approved: true)
get article.path
get article.path + "?variant_version=1"
expect(response.body).not_to include html_variant.html
end
it "does not render html variant when user logged in" do
html_variant = create(:html_variant, published: true, approved: true)
sign_in user
get article.path
get article.path + "?variant_version=1"
expect(response.body).not_to include html_variant.html
end
# Below article HTML variant
it "renders below article html variant" do
html_variant = create(:html_variant, published: true, approved: true, group: "article_show_below_article_cta")
article.update_column(:body_markdown, rand(36**1000).to_s(36).to_s) # min length for article
get article.path + "?variant_version=0"
expect(response.body).to include html_variant.html
end
it "Does not render below article html variant for short article" do
html_variant = create(:html_variant, published: true, approved: true, group: "article_show_below_article_cta")
article.update_column(:body_markdown, rand(36**100).to_s(36).to_s) # ensure too short
get article.path + "?variant_version=0"
expect(response.body).not_to include html_variant.html
end
it "Does not render below article variant when no variants published" do
html_variant = create(:html_variant, published: false, approved: true, group: "article_show_below_article_cta")
get article.path + "?variant_version=0"
expect(response.body).not_to include html_variant.html
end
it "does not render below article html variant when user logged in" do
html_variant = create(:html_variant, published: true, approved: true, group: "article_show_below_article_cta")
sign_in user
get article.path + "?variant_version=0"
expect(response.body).not_to include html_variant.html
end