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.
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SponsoredLogs
4
+ VERSION = "0.1.0"
5
+ 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: []