sponsored_logs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +461 -0
- data/lib/generators/sponsored_logs/install_generator.rb +24 -0
- data/lib/generators/sponsored_logs/templates/create_sponsored_logs_impressions.rb.tt +16 -0
- data/lib/sponsored_logs/ads_file.rb +35 -0
- data/lib/sponsored_logs/advertisers.rb +168 -0
- data/lib/sponsored_logs/configuration.rb +61 -0
- data/lib/sponsored_logs/engine.rb +21 -0
- data/lib/sponsored_logs/env.rb +28 -0
- data/lib/sponsored_logs/injector.rb +38 -0
- data/lib/sponsored_logs/ledger/report.rb +57 -0
- data/lib/sponsored_logs/ledger/store/active_record.rb +71 -0
- data/lib/sponsored_logs/ledger/store/base.rb +36 -0
- data/lib/sponsored_logs/ledger/store/memory.rb +43 -0
- data/lib/sponsored_logs/ledger/store/redis.rb +49 -0
- data/lib/sponsored_logs/railtie.rb +17 -0
- data/lib/sponsored_logs/report/app/controllers/sponsored_logs/reports_controller.rb +39 -0
- data/lib/sponsored_logs/report/app/helpers/sponsored_logs/reports_helper.rb +130 -0
- data/lib/sponsored_logs/report/app/views/sponsored_logs/reports/show.html.erb +72 -0
- data/lib/sponsored_logs/report/config/routes.rb +10 -0
- data/lib/sponsored_logs/version.rb +5 -0
- data/lib/sponsored_logs.rb +225 -0
- metadata +163 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SponsoredLogs
|
|
4
|
+
module ReportsHelper
|
|
5
|
+
STATUS_COLORS = {
|
|
6
|
+
active: "#16a34a",
|
|
7
|
+
scheduled: "#2563eb",
|
|
8
|
+
ended: "#6b7280",
|
|
9
|
+
evergreen: "#7c3aed"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
BAR_HEIGHT = 22
|
|
13
|
+
BAR_GAP = 10
|
|
14
|
+
LABEL_WIDTH = 320
|
|
15
|
+
TRACK_WIDTH = 360
|
|
16
|
+
VALUE_PAD = 8
|
|
17
|
+
BAR_COLOR = "#4f46e5"
|
|
18
|
+
|
|
19
|
+
# Render a horizontal bar chart as inline SVG from report ad rows.
|
|
20
|
+
# `value` picks the numeric field per row; `format` renders the label.
|
|
21
|
+
#
|
|
22
|
+
def bar_chart(ads, value:, format:)
|
|
23
|
+
rows = ads.map { |ad| [ad[:text], value.call(ad).to_f] }
|
|
24
|
+
.sort_by { |(_text, v)| -v }
|
|
25
|
+
return content_tag(:p, "No data yet.", class: "empty") if rows.empty?
|
|
26
|
+
|
|
27
|
+
max = rows.map { |(_t, v)| v }.max
|
|
28
|
+
max = 1.0 if max <= 0
|
|
29
|
+
|
|
30
|
+
height = rows.size * (BAR_HEIGHT + BAR_GAP)
|
|
31
|
+
width = LABEL_WIDTH + TRACK_WIDTH + 90
|
|
32
|
+
|
|
33
|
+
bars = rows.each_with_index.map do |(text, v), i|
|
|
34
|
+
y = i * (BAR_HEIGHT + BAR_GAP)
|
|
35
|
+
bar_w = ((v / max) * TRACK_WIDTH).round(2)
|
|
36
|
+
svg_bar(text, format.call(v), y, bar_w)
|
|
37
|
+
end.join
|
|
38
|
+
|
|
39
|
+
content_tag(
|
|
40
|
+
:svg,
|
|
41
|
+
raw(bars),
|
|
42
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
43
|
+
viewBox: "0 0 #{width} #{height}",
|
|
44
|
+
role: "img",
|
|
45
|
+
class: "chart",
|
|
46
|
+
style: "width:100%;max-width:#{width}px;height:auto;"
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Colored pill for an ad's flight status (:active/:scheduled/:ended/:evergreen).
|
|
51
|
+
#
|
|
52
|
+
def status_badge(status)
|
|
53
|
+
status ||= :evergreen
|
|
54
|
+
color = STATUS_COLORS.fetch(status, STATUS_COLORS[:evergreen])
|
|
55
|
+
content_tag(:span, status.to_s, class: "badge",
|
|
56
|
+
style: "background:#{color};")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Human-readable flight window; an em dash when the ad has no bounds.
|
|
60
|
+
#
|
|
61
|
+
def flight_window(starts_at, ends_at)
|
|
62
|
+
return "\u2014" if starts_at.nil? && ends_at.nil?
|
|
63
|
+
|
|
64
|
+
from = starts_at ? starts_at.strftime("%Y-%m-%d") : "\u2026"
|
|
65
|
+
to = ends_at ? ends_at.strftime("%Y-%m-%d") : "\u2026"
|
|
66
|
+
"#{from} \u2192 #{to}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A campaign detail table for a set of report rows, sorted by descending
|
|
70
|
+
# spend. Returns nil for an empty set so callers can skip the section.
|
|
71
|
+
#
|
|
72
|
+
def campaign_table(rows)
|
|
73
|
+
return if rows.nil? || rows.empty?
|
|
74
|
+
|
|
75
|
+
header = content_tag(:thead, content_tag(:tr,
|
|
76
|
+
safe_join([
|
|
77
|
+
content_tag(:th, "Creative"),
|
|
78
|
+
content_tag(:th, "Status"),
|
|
79
|
+
content_tag(:th, "Flight"),
|
|
80
|
+
content_tag(:th, "Impressions", class: "num"),
|
|
81
|
+
content_tag(:th, "CPM", class: "num"),
|
|
82
|
+
content_tag(:th, "Spend", class: "num")
|
|
83
|
+
])))
|
|
84
|
+
|
|
85
|
+
body = content_tag(:tbody, safe_join(
|
|
86
|
+
rows.sort_by { |ad| -ad[:spend] }.map { |ad| campaign_row(ad) }
|
|
87
|
+
))
|
|
88
|
+
|
|
89
|
+
content_tag(:table, safe_join([header, body]))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def campaign_row(ad)
|
|
95
|
+
content_tag(:tr, safe_join([
|
|
96
|
+
content_tag(:td, ad[:text]),
|
|
97
|
+
content_tag(:td, status_badge(ad[:status])),
|
|
98
|
+
content_tag(:td, flight_window(ad[:starts_at], ad[:ends_at]), class: "flight"),
|
|
99
|
+
content_tag(:td, ad[:impressions], class: "num"),
|
|
100
|
+
content_tag(:td, "$#{format("%.2f", ad[:cpm])}", class: "num"),
|
|
101
|
+
content_tag(:td, "$#{format("%.2f", ad[:spend])}", class: "num")
|
|
102
|
+
]))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def svg_bar(label, value_label, y, bar_w)
|
|
106
|
+
text_y = y + (BAR_HEIGHT / 2) + 4
|
|
107
|
+
label_text = esc(truncate_label(label))
|
|
108
|
+
value_text = esc(value_label)
|
|
109
|
+
|
|
110
|
+
%(
|
|
111
|
+
<text x="0" y="#{text_y}" class="bar-label">#{label_text}</text>
|
|
112
|
+
<rect x="#{LABEL_WIDTH}" y="#{y}" width="#{TRACK_WIDTH}" height="#{BAR_HEIGHT}" class="bar-track"/>
|
|
113
|
+
<rect x="#{LABEL_WIDTH}" y="#{y}" width="#{bar_w}" height="#{BAR_HEIGHT}" class="bar-fill"/>
|
|
114
|
+
<text x="#{LABEL_WIDTH + bar_w + VALUE_PAD}" y="#{text_y}" class="bar-value">#{value_text}</text>
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Truncate the raw text first, then escape, so we never slice through an
|
|
119
|
+
# HTML entity.
|
|
120
|
+
#
|
|
121
|
+
def truncate_label(text)
|
|
122
|
+
raw_text = text.to_s
|
|
123
|
+
raw_text.length > 46 ? "#{raw_text[0, 45]}\u2026" : raw_text
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def esc(value)
|
|
127
|
+
ERB::Util.html_escape(value.to_s)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Campaign Performance</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
|
8
|
+
margin: 2rem auto; max-width: 60rem; color: #1a1a1a; }
|
|
9
|
+
h1 { font-size: 1.4rem; margin-bottom: 0.25rem; }
|
|
10
|
+
.subtitle { color: #666; margin-top: 0; font-size: 0.9rem; }
|
|
11
|
+
.totals { display: flex; gap: 2rem; margin: 1.5rem 0; }
|
|
12
|
+
.totals div { background: #f4f4f6; border-radius: 8px; padding: 1rem 1.25rem; }
|
|
13
|
+
.totals .label { display: block; font-size: 0.75rem; text-transform: uppercase;
|
|
14
|
+
letter-spacing: 0.05em; color: #888; }
|
|
15
|
+
.totals .value { font-size: 1.6rem; font-weight: 600; }
|
|
16
|
+
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
|
17
|
+
th, td { text-align: left; padding: 0.6rem 0.75rem; border-bottom: 1px solid #eee; }
|
|
18
|
+
th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #888; }
|
|
19
|
+
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
20
|
+
.empty { color: #888; font-style: italic; margin-top: 1.5rem; }
|
|
21
|
+
h2 { font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.05em;
|
|
22
|
+
color: #888; margin: 2rem 0 0.5rem; }
|
|
23
|
+
.chart { display: block; }
|
|
24
|
+
.chart .bar-label { font-size: 12px; fill: #333; }
|
|
25
|
+
.chart .bar-value { font-size: 12px; fill: #555; font-variant-numeric: tabular-nums; }
|
|
26
|
+
.chart .bar-track { fill: #f0f0f3; rx: 4; }
|
|
27
|
+
.chart .bar-fill { fill: #4f46e5; rx: 4; }
|
|
28
|
+
.badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 999px;
|
|
29
|
+
color: #fff; font-size: 0.7rem; text-transform: uppercase;
|
|
30
|
+
letter-spacing: 0.03em; }
|
|
31
|
+
td.flight { font-variant-numeric: tabular-nums; color: #555; white-space: nowrap; }
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<h1>Campaign Performance</h1>
|
|
36
|
+
<p class="subtitle">Sponsored message delivery report</p>
|
|
37
|
+
|
|
38
|
+
<div class="totals">
|
|
39
|
+
<div>
|
|
40
|
+
<span class="label">Impressions</span>
|
|
41
|
+
<span class="value"><%= @report[:impressions] %></span>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<span class="label">Total spend</span>
|
|
45
|
+
<span class="value">$<%= format("%.2f", @report[:spend]) %></span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<% if @report[:ads].empty? %>
|
|
50
|
+
<p class="empty">No impressions delivered yet.</p>
|
|
51
|
+
<% else %>
|
|
52
|
+
<h2>Spend by advertiser</h2>
|
|
53
|
+
<%= bar_chart(@report[:ads], value: ->(ad) { ad[:spend] }, format: ->(v) { "$#{format('%.2f', v)}" }) %>
|
|
54
|
+
|
|
55
|
+
<h2>Impressions by advertiser</h2>
|
|
56
|
+
<%= bar_chart(@report[:ads], value: ->(ad) { ad[:impressions] }, format: ->(v) { v.to_i.to_s }) %>
|
|
57
|
+
|
|
58
|
+
<h2>Running campaigns</h2>
|
|
59
|
+
<%= campaign_table(@report[:ads]) %>
|
|
60
|
+
<% end %>
|
|
61
|
+
|
|
62
|
+
<% if @report[:upcoming].any? %>
|
|
63
|
+
<h2>Upcoming campaigns</h2>
|
|
64
|
+
<%= campaign_table(@report[:upcoming]) %>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
67
|
+
<% if @report[:finished].any? %>
|
|
68
|
+
<h2>Finished campaigns</h2>
|
|
69
|
+
<%= campaign_table(@report[:finished]) %>
|
|
70
|
+
<% end %>
|
|
71
|
+
</body>
|
|
72
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
SponsoredLogs::Engine.routes.draw do
|
|
4
|
+
root to: "reports#show"
|
|
5
|
+
|
|
6
|
+
# Allow an explicit .json (or other) format suffix, e.g.
|
|
7
|
+
# /sponsored_logs_report.json, since the root route does not capture it.
|
|
8
|
+
#
|
|
9
|
+
get "(.:format)", to: "reports#show"
|
|
10
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
require_relative "sponsored_logs/version"
|
|
6
|
+
require_relative "sponsored_logs/advertisers"
|
|
7
|
+
require_relative "sponsored_logs/ads_file"
|
|
8
|
+
require_relative "sponsored_logs/ledger/store/base"
|
|
9
|
+
require_relative "sponsored_logs/ledger/store/memory"
|
|
10
|
+
require_relative "sponsored_logs/ledger/store/redis"
|
|
11
|
+
require_relative "sponsored_logs/ledger/store/active_record"
|
|
12
|
+
require_relative "sponsored_logs/ledger/report"
|
|
13
|
+
require_relative "sponsored_logs/configuration"
|
|
14
|
+
require_relative "sponsored_logs/injector"
|
|
15
|
+
require_relative "sponsored_logs/env"
|
|
16
|
+
|
|
17
|
+
module SponsoredLogs
|
|
18
|
+
class << self
|
|
19
|
+
def configuration
|
|
20
|
+
@configuration ||= Configuration.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def configure
|
|
24
|
+
yield(configuration) if block_given?
|
|
25
|
+
configuration
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Activate ad insertion, applying any settings from the options hash (see
|
|
29
|
+
# Configuration#assign for the recognized keys). Settings can also be set
|
|
30
|
+
# ahead of time with configure { |c| ... }.
|
|
31
|
+
#
|
|
32
|
+
def sponsor!(opts = {})
|
|
33
|
+
configuration.assign(opts)
|
|
34
|
+
|
|
35
|
+
Injector.install!
|
|
36
|
+
@active = true
|
|
37
|
+
|
|
38
|
+
start_periodic_thread if configuration.periodic
|
|
39
|
+
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def unsponsor!
|
|
44
|
+
@active = false
|
|
45
|
+
stop_periodic_thread
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Activate from ENV if SPONSORED_LOGS is set, applying any
|
|
50
|
+
# SPONSORED_LOGS_* overrides. No-op when the flag is absent, so the
|
|
51
|
+
# manual sponsor!/unsponsor! path is entirely unaffected.
|
|
52
|
+
#
|
|
53
|
+
def sponsor_from_env!(env = ENV)
|
|
54
|
+
return self unless Env.activate?(env)
|
|
55
|
+
|
|
56
|
+
sponsor!(Env.options(env))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def active?
|
|
60
|
+
@active == true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def maybe_emit(target: $stdout)
|
|
64
|
+
return unless active?
|
|
65
|
+
return unless rand < configuration.probability
|
|
66
|
+
|
|
67
|
+
emit(target)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def emit(target = configuration.output)
|
|
71
|
+
ad = Advertisers.pick(
|
|
72
|
+
configuration.ads,
|
|
73
|
+
mode: configuration.selection,
|
|
74
|
+
counts: ledger.impression_counts
|
|
75
|
+
)
|
|
76
|
+
return if ad.nil?
|
|
77
|
+
|
|
78
|
+
ledger.record(ad)
|
|
79
|
+
line = Advertisers.render(ad, configuration.ad_prefix)
|
|
80
|
+
|
|
81
|
+
if target.is_a?(Logger)
|
|
82
|
+
# Raw << avoids re-triggering our own Logger#add patch (infinite loop).
|
|
83
|
+
#
|
|
84
|
+
target << "#{line}\n"
|
|
85
|
+
else
|
|
86
|
+
target.write("#{line}\n")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
line
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Rebuilt when the configured store changes, so swapping the store via
|
|
93
|
+
# sponsor!(store:) takes effect immediately.
|
|
94
|
+
#
|
|
95
|
+
def ledger
|
|
96
|
+
if @ledger.nil? || @ledger_store != configuration.store
|
|
97
|
+
@ledger = Ledger::Report.new(configuration.store)
|
|
98
|
+
@ledger_store = configuration.store
|
|
99
|
+
end
|
|
100
|
+
@ledger
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Accrued fake ad economics: per-ad impressions and spend, plus totals.
|
|
104
|
+
# Spend is rounded to cents here; the ledger keeps the raw values.
|
|
105
|
+
#
|
|
106
|
+
# Every ad appears in exactly one group, by status:
|
|
107
|
+
# - upcoming: :scheduled (window not started yet).
|
|
108
|
+
# - finished: :ended (window passed) or :exhausted (impression cap reached).
|
|
109
|
+
# - ads: running campaigns (:active / :evergreen) that have served.
|
|
110
|
+
#
|
|
111
|
+
# Running rows are impression-driven (served ads only); upcoming and
|
|
112
|
+
# finished also include configured ads that have not served, so scheduled
|
|
113
|
+
# and completed campaigns still appear.
|
|
114
|
+
#
|
|
115
|
+
def report
|
|
116
|
+
grouped = grouped_report_rows
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
impressions: ledger.total_impressions,
|
|
120
|
+
spend: ledger.total_spend.round(2),
|
|
121
|
+
ads: grouped[:running],
|
|
122
|
+
upcoming: grouped[:upcoming],
|
|
123
|
+
finished: grouped[:finished]
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A formatted, log-friendly table of the current report, ready to print or
|
|
128
|
+
# log. Ads are listed by descending spend.
|
|
129
|
+
#
|
|
130
|
+
def report_text
|
|
131
|
+
data = report
|
|
132
|
+
rows = data[:ads].sort_by { |ad| -ad[:spend] }
|
|
133
|
+
|
|
134
|
+
width = rows.map { |ad| ad[:text].length }.push(4).max
|
|
135
|
+
lines = [format("%-#{width}s %8s %7s %9s", "Ad", "Impr", "CPM", "Spend")]
|
|
136
|
+
lines << ("-" * (width + 30))
|
|
137
|
+
|
|
138
|
+
rows.each do |ad|
|
|
139
|
+
lines << format("%-#{width}s %8d %7.2f %9.2f", ad[:text], ad[:impressions], ad[:cpm], ad[:spend])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
lines << ("-" * (width + 30))
|
|
143
|
+
lines << format("%-#{width}s %8d %7s %9.2f", "TOTAL", data[:impressions], "", data[:spend])
|
|
144
|
+
lines.join("\n")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def reset_ledger!
|
|
148
|
+
ledger.reset
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
# Map of ad text => normalized config metadata (weight/cpm/flight/cap),
|
|
155
|
+
# used to enrich report rows and drive status.
|
|
156
|
+
#
|
|
157
|
+
def ad_metadata
|
|
158
|
+
Advertisers.normalize(configuration.ads).to_h { |ad| [ad[:text], ad] }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Partition every known ad (served or configured) into running / upcoming /
|
|
162
|
+
# finished report rows by flight-and-cap status.
|
|
163
|
+
#
|
|
164
|
+
def grouped_report_rows
|
|
165
|
+
now = Time.now
|
|
166
|
+
metas = ad_metadata
|
|
167
|
+
counts = ledger.impression_counts
|
|
168
|
+
served = ledger.entries.to_h { |entry| [entry.text, entry] }
|
|
169
|
+
|
|
170
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
|
171
|
+
|
|
172
|
+
(served.keys + metas.keys).uniq.each do |text|
|
|
173
|
+
meta = metas[text] || {}
|
|
174
|
+
status = Advertisers.status(meta, now, counts[text].to_i)
|
|
175
|
+
row = report_row(text, meta, served[text], status)
|
|
176
|
+
|
|
177
|
+
case status
|
|
178
|
+
when :scheduled then grouped[:upcoming] << row
|
|
179
|
+
when :ended, :exhausted then grouped[:finished] << row
|
|
180
|
+
else grouped[:running] << row if served[text]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
grouped
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# A single report row. Impressions/spend come from the ledger entry when the
|
|
188
|
+
# ad has served, otherwise zero; window/cap come from config metadata.
|
|
189
|
+
#
|
|
190
|
+
def report_row(text, meta, entry, status)
|
|
191
|
+
{
|
|
192
|
+
text: text,
|
|
193
|
+
impressions: entry ? entry.impressions : 0,
|
|
194
|
+
cpm: entry ? entry.cpm : meta[:cpm].to_f,
|
|
195
|
+
spend: entry ? entry.spend.round(2) : 0.0,
|
|
196
|
+
starts_at: meta[:starts_at],
|
|
197
|
+
ends_at: meta[:ends_at],
|
|
198
|
+
cap: meta[:cap],
|
|
199
|
+
status: status
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def start_periodic_thread
|
|
204
|
+
stop_periodic_thread
|
|
205
|
+
@periodic_thread = Thread.new do
|
|
206
|
+
loop do
|
|
207
|
+
sleep(configuration.interval)
|
|
208
|
+
break unless active? && configuration.periodic
|
|
209
|
+
|
|
210
|
+
emit(configuration.output)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def stop_periodic_thread
|
|
216
|
+
@periodic_thread&.kill
|
|
217
|
+
@periodic_thread = nil
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
SponsoredLogs.sponsor_from_env!
|
|
223
|
+
|
|
224
|
+
require_relative "sponsored_logs/railtie" if defined?(Rails::Railtie)
|
|
225
|
+
require_relative "sponsored_logs/engine" if defined?(Rails::Engine)
|
metadata
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sponsored_logs
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kerri Miller
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.6'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: activerecord
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rack-test
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: railties
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '7.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '7.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rubocop
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '1.60'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.60'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: sqlite3
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1.6'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1.6'
|
|
110
|
+
description: |
|
|
111
|
+
Randomly and periodically inserts host-read sponsor messages from the top 10
|
|
112
|
+
podcast advertisers into your application logs. Opt-in and fully configurable.
|
|
113
|
+
executables: []
|
|
114
|
+
extensions: []
|
|
115
|
+
extra_rdoc_files: []
|
|
116
|
+
files:
|
|
117
|
+
- LICENSE.txt
|
|
118
|
+
- README.md
|
|
119
|
+
- lib/generators/sponsored_logs/install_generator.rb
|
|
120
|
+
- lib/generators/sponsored_logs/templates/create_sponsored_logs_impressions.rb.tt
|
|
121
|
+
- lib/sponsored_logs.rb
|
|
122
|
+
- lib/sponsored_logs/ads_file.rb
|
|
123
|
+
- lib/sponsored_logs/advertisers.rb
|
|
124
|
+
- lib/sponsored_logs/configuration.rb
|
|
125
|
+
- lib/sponsored_logs/engine.rb
|
|
126
|
+
- lib/sponsored_logs/env.rb
|
|
127
|
+
- lib/sponsored_logs/injector.rb
|
|
128
|
+
- lib/sponsored_logs/ledger/report.rb
|
|
129
|
+
- lib/sponsored_logs/ledger/store/active_record.rb
|
|
130
|
+
- lib/sponsored_logs/ledger/store/base.rb
|
|
131
|
+
- lib/sponsored_logs/ledger/store/memory.rb
|
|
132
|
+
- lib/sponsored_logs/ledger/store/redis.rb
|
|
133
|
+
- lib/sponsored_logs/railtie.rb
|
|
134
|
+
- lib/sponsored_logs/report/app/controllers/sponsored_logs/reports_controller.rb
|
|
135
|
+
- lib/sponsored_logs/report/app/helpers/sponsored_logs/reports_helper.rb
|
|
136
|
+
- lib/sponsored_logs/report/app/views/sponsored_logs/reports/show.html.erb
|
|
137
|
+
- lib/sponsored_logs/report/config/routes.rb
|
|
138
|
+
- lib/sponsored_logs/version.rb
|
|
139
|
+
homepage: https://github.com/kerrizor/sponsored_logs
|
|
140
|
+
licenses:
|
|
141
|
+
- MIT
|
|
142
|
+
metadata:
|
|
143
|
+
source_code_uri: https://github.com/kerrizor/sponsored_logs
|
|
144
|
+
bug_tracker_uri: https://github.com/kerrizor/sponsored_logs/issues
|
|
145
|
+
rubygems_mfa_required: 'true'
|
|
146
|
+
rdoc_options: []
|
|
147
|
+
require_paths:
|
|
148
|
+
- lib
|
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
150
|
+
requirements:
|
|
151
|
+
- - ">="
|
|
152
|
+
- !ruby/object:Gem::Version
|
|
153
|
+
version: '3.1'
|
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - ">="
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '0'
|
|
159
|
+
requirements: []
|
|
160
|
+
rubygems_version: 4.0.10
|
|
161
|
+
specification_version: 4
|
|
162
|
+
summary: Inserts sponsor messages from leading advertisers into your logs.
|
|
163
|
+
test_files: []
|