Skip to content

Commit

Permalink
feat: ✨ Conversion Pixel (gitcoinco#1310)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Berry authored Jun 11, 2020
1 parent bde75e7 commit 2fd3e8c
Show file tree
Hide file tree
Showing 48 changed files with 1,161 additions and 26 deletions.
10 changes: 5 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ GEM
activerecord (>= 4.2)
request_store (~> 1.1)
parallel (1.19.1)
parser (2.7.1.2)
parser (2.7.1.3)
ast (~> 2.4.0)
pastel (0.7.3)
equatable (~> 0.6)
Expand Down Expand Up @@ -423,7 +423,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
reverse_markdown (1.4.0)
reverse_markdown (2.0.0)
nokogiri
rexml (3.2.4)
rollbar (2.25.0)
Expand Down Expand Up @@ -495,7 +495,7 @@ GEM
sixarm_ruby_unaccent (1.2.0)
slack-notifier (2.3.2)
smart_properties (1.15.0)
solargraph (0.39.7)
solargraph (0.39.8)
backport (~> 1.1)
benchmark
bundler (>= 1.17.2)
Expand All @@ -504,7 +504,7 @@ GEM
maruku (~> 0.7, >= 0.7.3)
nokogiri (~> 1.9, >= 1.9.1)
parser (~> 2.3)
reverse_markdown (~> 1.0, >= 1.0.5)
reverse_markdown (>= 1.0.5, < 3)
rubocop (~> 0.52)
thor (~> 1.0)
tilt (~> 2.0)
Expand All @@ -524,7 +524,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
standard (0.4.2)
standard (0.4.6)
rubocop (~> 0.83.0)
rubocop-performance (~> 1.5.2)
statsd-ruby (1.4.0)
Expand Down
2 changes: 1 addition & 1 deletion app/components/page_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
<%= render(tabs) if tabs %>
<%= tag.hr(class: "my-3") if !tabs && header %>
<div class="page-section"><%= body %></div>
<%= render "/#{subject_view_directory}/sidebar", subject: subject if sidebar %>
<%= render sidebar_partial ? sidebar_partial : "/#{subject_view_directory}/sidebar", subject: subject if sidebar %>
</div>
<% end %>
4 changes: 2 additions & 2 deletions app/components/page_component.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class PageComponent < ApplicationComponent
with_content_areas :header, :body

def initialize(subject: nil, tabs: false, sidebar: false, classes: nil)
def initialize(subject: nil, tabs: false, sidebar: false, sidebar_partial: nil, classes: [])
@subject = subject
@tabs = tabs
@sidebar = sidebar
Expand All @@ -15,7 +15,7 @@ def subject_view_directory

private

attr_reader :subject, :sidebar, :tabs, :class_names
attr_reader :subject, :sidebar, :tabs, :sidebar_partial, :class_names

def classes
classes = ["page"]
Expand Down
20 changes: 20 additions & 0 deletions app/controllers/pixel_conversions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class PixelConversionsController < ApplicationController
include Untrackable

skip_before_action :verify_authenticity_token
before_action :set_cors_headers
before_action :set_no_caching_headers

def create
CreatePixelConversionJob.perform_later pixel_conversion_params.to_h
head :accepted
end

private

def pixel_conversion_params
params.permit(:pixel_id, :impression_id, :test, metadata: {}).tap do |whitelisted|
whitelisted[:conversion_referrer] = request.referrer
end
end
end
77 changes: 77 additions & 0 deletions app/controllers/pixels_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
class PixelsController < ApplicationController
before_action :authenticate_user!
before_action :set_organization
before_action :set_pixel, only: [:show, :edit, :update, :destroy]
before_action :authorize_edit!, except: [:index]

def index
pixels = @organization.pixels.order(name: :asc)
@pagy, @pixels = pagy(pixels)
end

def new
@pixel = @organization.pixels.build(user: current_user)
end

def create
@pixel = @organization.pixels.build(pixel_params)

respond_to do |format|
if @pixel.save
format.html { redirect_to pixels_path(@organization), notice: "Pixel was successfully created" }
format.json { render :show, status: :created, location: pixel_path(@organization, @pixel) }
else
format.html { render :new }
format.json { render json: @pixel.errors, status: :unprocessable_entity }
end
end
end

def update
@pixel.update(pixel_params)

respond_to do |format|
if @pixel.save
format.html { redirect_to pixel_path(@organization, @pixel), notice: "Pixel was successfully updated." }
format.json { render :show, status: :ok, location: pixel_path(@organization, @pixel) }
else
format.html { render :edit }
format.json { render json: @pixel.errors, status: :unprocessable_entity }
end
end
end

def destroy
respond_to do |format|
if @pixel.destroy
format.html { redirect_to pixels_path(@organization), notice: "Pixel was deleted successfully" }
format.json { head :no_content }
else
format.html { redirect_to pixels_path(@organization), notice: @organization.errors.messages.to_s }
format.json { render json: @organization_user.errors, status: :unprocessable_entity }
end
end
end

private

def authorize_edit!
unless authorized_user.can_edit_organization_users?(@organization)
redirect_to organization_users_path(@organization), notice: "You do not have permission to update membership settings."
end
end

def set_organization
@organization = Current.organization
end

def set_pixel
@pixel = @organization.pixels.find(params[:id])
end

def pixel_params
params.require(:pixel).permit(:description, :name, :value, :user_id).tap do |whitelisted|
whitelisted[:user_id] = params[:pixel][:user_id] if authorized_user.can_admin_system?
end
end
end
2 changes: 1 addition & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def advertisers_for_select(organization = Current.organization)
end

def organization_users_for_select
Current.organization.users.advertisers.sort_by(&:name).map { |user| [user.name, user.id] }
Current.organization.users.advertisers.sort_by(&:name).map { |user| ["#{user.name} <#{user.email}>", user.id] }
end

def account_managers_for_select
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/organization_pixels_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module OrganizationPixelsHelper
end
1 change: 1 addition & 0 deletions app/helpers/organizations_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ def organization_tabs(organization)
{name: "Details", path: organization_path(organization), active: :exact},
{name: "Members", path: organization_users_path(organization)},
{name: "Transactions", path: organization_transactions_path(organization)},
{name: "Pixels", path: pixels_path(organization)},
{name: "Reports", path: organization_reports_path(organization)},
{name: "Comments", path: organization_comments_path(organization), validation: authorized_user.can_view_comments?},
{name: "Settings", path: edit_organization_path(organization), validation: authorized_user.can_admin_system?}
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/pixel_conversions_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module PixelConversionsHelper
end
6 changes: 6 additions & 0 deletions app/javascript/packs/code_fund_conversion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import ConversionTracker from '../src/conversion-tracker'

window.CodeFund = new ConversionTracker(window.CodeFundConfig || {})

// Example
// CodeFund.recordConversion('12345')
91 changes: 91 additions & 0 deletions app/javascript/src/conversion-tracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
export default class {
constructor (config = {}) {
const defaultConfig = {
baseUrl: 'https://codefund.io',
daysToLive: 30,
localStorageKey: 'CodeFund.utm_impression',
successStatuses: [200, 202]
}
const customConfig = config || {}
this.config = { ...defaultConfig, ...customConfig }
this.impressionId = this.urlParams.get('utm_impression')
}

// Notifies CodeFund of the pixelId being converted for the saved impression
// TODO: update to use POST exclusively and support metadata
recordConversion (pixelId, options = { test: false, metadata: {} }) {
const { test, metadata } = options
const url = `${this.baseUrl}/pixels/${pixelId}/impressions/${this.impressionId}?test=${test}`
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (!this.successStatuses.include(xhr.status))
console.log('CodeFund failed to record the conversion!', xhr.status)
}
}
xhr.open('GET', url)
xhr.send()
}

// Indicates if the passed date (represented as a string from localStorage) has expired based on daysToLive
expired (createdAtDateString) {
if (!createdAtDateString) return true
const createdAt = new Date(Date.parse(createdAtDateString)) // 'Tue Jun 09 2020 14:33:59 GMT-0400 (EDT)'
if (createdAt.getTime() !== createdAt.getTime()) return true // invalid date, getTime returned NaN but NaN never equals itself
const expiresAt = new Date(createdAt.getTime())
expiresAt.setDate(expiresAt.getDate() + this.daysToLive)
const today = new Date()
return today > expiresAt
}

// Saves the impressionId to localStorage
set impressionId (id) {
if (!id) return localStorage.removeItem(this.localStorageKey)
if (this.impressionId) return
try {
const createdAt = new Date()
const data = { id, createdAt }
return localStorage.setItem(this.localStorageKey, JSON.stringify(data))
} catch (ex) {
console.log(
`CodeFund failed to write the utm_impression id to localStorage! ${ex.message}`
)
}
}

// Fetches the impressionId from localStorage
get impressionId () {
try {
const rawData = localStorage.getItem(this.localStorageKey)
const data = JSON.parse(rawData) || {}
const { id, createdAt } = data
if (!this.expired(createdAt)) return id
localStorage.removeItem(this.localStorageKey)
} catch (ex) {
console.log(
`CodeFund failed to read the utm_impression value from localStorage! ${ex.message}`
)
}
return null
}

get urlParams () {
return new URL(window.location).searchParams
}

get baseUrl () {
return this.config.baseUrl
}

get daysToLive () {
return this.config.daysToLive
}

get localStorageKey () {
return this.config.localStorageKey
}

get successStatuses () {
return this.config.successStatuses
}
}
14 changes: 14 additions & 0 deletions app/jobs/create_pixel_conversion_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class CreatePixelConversionJob < ApplicationJob
queue_as :default

def perform(params = {})
ScoutApm::Transaction.ignore! if rand > (ENV["SCOUT_SAMPLE_RATE"] || 1).to_f

Pixel.find_by(id: params[:pixel_id])&.record_conversion(
params[:impression_id],
conversion_referrer: params[:conversion_referrer],
test: params[:test],
metadata: params[:metadata] || {}
)
end
end
1 change: 1 addition & 0 deletions app/models/campaign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class Campaign < ApplicationRecord
belongs_to :region, optional: true
belongs_to :creative, -> { includes :creative_images }, optional: true
belongs_to :user
has_many :pixel_conversions

# validations ...............................................................
validates :name, length: {maximum: 255, allow_blank: false}
Expand Down
1 change: 1 addition & 0 deletions app/models/creative.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Creative < ApplicationRecord
has_many :campaigns
has_many :creative_images
has_many :images, through: :creative_images
has_many :pixel_conversions
has_many :standard_images, -> { metadata_format CreativeImage::STANDARD_FORMATS }, through: :creative_images, source: :image

# validations ...............................................................
Expand Down
1 change: 1 addition & 0 deletions app/models/impression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Impression < ApplicationRecord
belongs_to :campaign, optional: true
belongs_to :creative, optional: true
belongs_to :property, optional: true
has_many :pixel_conversions

# validations ...............................................................

Expand Down
2 changes: 2 additions & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class Organization < ApplicationRecord
has_many :organization_reports
has_many :organization_transactions
has_many :organization_users, dependent: :destroy
has_many :pixels, dependent: :destroy
has_many :pixel_conversions
has_many :scheduled_organization_reports
has_many :users, through: :organization_users
has_many :administrators, -> { where organization_users: {role: ENUMS::ORGANIZATION_ROLES::ADMINISTRATOR} }, through: :organization_users, source: "user"
Expand Down
Loading

0 comments on commit 2fd3e8c

Please sign in to comment.