sponsored_logs 0.2.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +85 -0
- data/README.md +176 -1
- data/lib/sponsored_logs/advertisers.rb +203 -67
- data/lib/sponsored_logs/banner.rb +84 -0
- data/lib/sponsored_logs/color.rb +58 -0
- data/lib/sponsored_logs/configuration.rb +21 -2
- data/lib/sponsored_logs/env.rb +27 -8
- data/lib/sponsored_logs/flight.rb +62 -0
- data/lib/sponsored_logs/report/app/helpers/sponsored_logs/reports_helper.rb +65 -64
- data/lib/sponsored_logs/report/app/views/sponsored_logs/reports/show.html.erb +18 -25
- data/lib/sponsored_logs/version.rb +1 -1
- data/lib/sponsored_logs.rb +27 -2
- metadata +11 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fb5cc1d702c7a78769eabc68497519929f3b539022e060e183236bea09bf2e9
|
|
4
|
+
data.tar.gz: 833fc7655dc58fe08d225c30d8cbeb4fb671f8420e28174a902a2b90127a8126
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a082ca74275c9295fbe89b5a37df6ff53ef4384be302b6fe63a6b69f93ee11ec9ac2443a75375ea3a2fb78db96fb2285a825d797c0d793af7834b64304d28faf
|
|
7
|
+
data.tar.gz: 0e583c888d9b72d785d76492c1dc7b71cebb6769321f025a2d9a3282a5ff0425f51dfbbbca693a6e1382bc0dbbe19eb3a4bc71c3825535e8e46444fbebc00ba0
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.1] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Fill integrity is a moat.** A `NaN` bid is not demand — it's a tourist. It used to slip past the guard into `weighted_pick` and stall the whole rotation. Now non-finite bids (`NaN`, `±Infinity`) bill at zero where they belong, and the exchange keeps clearing. We don't let bad actors set the price
|
|
15
|
+
- **Brand safety is table stakes.** A creative that forges its own `ERROR` line isn't a placement — it's fraud with ambitions. Control characters, DEL, and the sneaky Unicode separators (U+2028/U+2029) now collapse to a space before serving. Punctuation, em-dashes, and emoji survive. The exploit does not
|
|
16
|
+
|
|
17
|
+
## [0.3.0] - 2026-09-07
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Marketing site: sponsor placements in the demo terminal are now delivered reliably. Renamed inventory-bearing CSS classes so client-side ad blockers can no longer suppress the platform's own impressions, and corrected the log-line layout so each placement occupies its own row ([#4](https://github.com/sponsoredlogs/sponsored_logs/pull/4))
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Brand-safe gilding of the `[AD]` prefix: on a live terminal the tag now renders in premium 256-color gold (`\e[38;5;214m … \e[0m`), turning a plain tag into an above-the-fold trust signal. The escape codes are zero-width, so banner borders stay byte-for-byte aligned. Gilding is delivered only to a real TTY with color enabled — files, pipes, and `Logger` sinks receive the byte-identical plain line, and the [`NO_COLOR`](https://no-color.org) convention (any non-empty value) suppresses `:auto`. New `color` config option and `SPONSORED_LOGS_COLOR` env var accept `:auto` (default — TTY + `NO_COLOR`-clear), `:always` (force gold, overrides `NO_COLOR`), or `:never` (plain everywhere); unrecognized values settle to `:auto`
|
|
26
|
+
- Self-sponsoring house ads — remnant fill so no impression goes to waste. The built-in list splits into `Advertisers::PAID_ADS` (the ten real-brand creatives) and `Advertisers::HOUSE_ADS` (three SponsoredLogs creatives at `weight: 1`, `cpm: 0.0`); `Advertisers::DEFAULT_ADS` is now their sum (13 rows). House ads compete in the default rotation (~3-in-13) and act as the final remnant floor when no paid creative is eligible (empty/capped/out-of-flight/zero-weight pool), so `pick` returns a creative rather than `nil`. They bill at zero and roll up to a `"SponsoredLogs"` advertiser account with zero spend. New `house_ads` config toggle (default `true`) and `SPONSORED_LOGS_HOUSE_ADS` env var govern house inventory everywhere: turning it off excludes house ads from rotation and retires the floor, restoring the original `pick`-returns-`nil`-when-exhausted contract
|
|
27
|
+
- Activated sponsor inventory across the project's own documentation surface: the README and this changelog now carry host-read placements between sections. Premium above-the-fold attention should not go unmonetized simply because it happens to be ours
|
|
28
|
+
- Per-ad `format: :banner` — premium box-drawn, multi-line placement that turns a single log line into a full above-the-fold impression unit. The `ad_prefix` embeds in the top border; the ~60-column body word-wraps (long words break mid-word so nothing overflows the frame). Inventory is optimized for standard-width Latin creative — emoji/CJK copy renders wider than one cell and may shift the right border, with `ascii_only` as the portable fallback
|
|
29
|
+
- Per-ad `box` impact tier for banners: `:light` (standard), `:heavy` (premium impact), or `:double` (maximum impact); unrecognized tiers settle to `:light`
|
|
30
|
+
- Global `ascii_only` config (and `SPONSORED_LOGS_ASCII_ONLY` env var) that overrides every box tier with the portable `+`/`-`/`|` glyph set for log sinks that mangle Unicode
|
|
31
|
+
- Per-ad `advertiser` field so creatives roll up to an advertiser account — the multi-tenant yield layer the exchange has been waiting for; defaults to `"Unattributed"` when omitted, and the built-in ads carry real brand names ([#1](https://github.com/sponsoredlogs/sponsored_logs/pull/1))
|
|
32
|
+
- `report[:advertisers]` rollup: per-advertiser impressions, spend, and ad count, sorted by spend so top accounts surface first ([#1](https://github.com/sponsoredlogs/sponsored_logs/pull/1))
|
|
33
|
+
- Dashboard "Advertiser accounts" table and an Advertiser column on the campaign tables ([#1](https://github.com/sponsoredlogs/sponsored_logs/pull/1))
|
|
34
|
+
- Share-of-impressions donut alongside the share-of-spend donut; both cap at the top 7 non-zero advertisers and fold the remainder into an "Other" slice ([#1](https://github.com/sponsoredlogs/sponsored_logs/pull/1))
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Charts now aggregate by advertiser instead of per-ad, so labels are advertiser names rather than ad copy ([#1](https://github.com/sponsoredlogs/sponsored_logs/pull/1))
|
|
39
|
+
- The gold gradient now accents the totals card values (the spend/impressions bar chart it previously filled has been removed as redundant with the share-of-spend donut) ([#1](https://github.com/sponsoredlogs/sponsored_logs/pull/1))
|
|
40
|
+
|
|
41
|
+
### Removed
|
|
42
|
+
|
|
43
|
+
- Redundant "Spend by advertiser" bar chart (superseded by the share-of-spend donut) and the now-unused bar-chart helper ([#1](https://github.com/sponsoredlogs/sponsored_logs/pull/1))
|
|
44
|
+
- Support for Ruby 3.1: dropped from the CI matrix, `required_ruby_version` raised to `>= 3.2`, and RuboCop's `TargetRubyVersion` aligned to match. Rails 8.1 no longer resolves on 3.1 ([#3](https://github.com/sponsoredlogs/sponsored_logs/pull/3))
|
|
45
|
+
|
|
46
|
+
## [0.2.0] - 2026-09-06
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- The "Command Center" — a dark-mode revenue dashboard styled to match the project banner (navy gradient, gold/cyan accents, monospace numerics, terminal chrome). Stop grepping your revenue; start visualizing it
|
|
51
|
+
- Share-of-spend donut chart, so yield concentration is legible at a glance
|
|
52
|
+
- Delivery-to-goal pacing bars that track each campaign against its impression cap — governance is a feature
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- Refreshed the dashboard screenshot in the README to reflect the Command Center
|
|
57
|
+
- Reordered the README to lead with the Agentic Advantage, and sprinkled emoji throughout — the machine audience deserves a warm welcome
|
|
58
|
+
|
|
59
|
+
> `[AD]` This release cycle sponsored by **ShipFaster CI** — because your
|
|
60
|
+
> changelog should ship as fast as your excuses. shipfaster.dev/logs
|
|
61
|
+
|
|
62
|
+
## [0.1.0] - 2026-09-06
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- Initial platform launch: opt-in activation of log inventory via `SponsoredLogs.sponsor!`, which prepends override modules onto `Kernel#puts` and `Logger#add` to serve placements alongside your telemetry
|
|
67
|
+
- Two-stage auction: weighted or CPM-based (`selection: :cpm`) message selection, so the highest bidder wins more inventory
|
|
68
|
+
- Per-ad campaign controls — `weight`, `cpm`, flighting (`starts_at` / `ends_at`), and a lifetime impression `cap` (frequency governance)
|
|
69
|
+
- Bring-your-own-demand pools, inline (`ads:`) or from a JSON file (`ads_file:`)
|
|
70
|
+
- Full-funnel attribution: `SponsoredLogs.report` for structured revenue data, plus a formatted `SponsoredLogs.report_text` table
|
|
71
|
+
- Pluggable, cloud-agnostic impression storage: `Ledger::Store::Memory` (default), `Ledger::Store::Redis`, and `Ledger::Store::ActiveRecord` (with an `install` generator)
|
|
72
|
+
- Mountable Rails report engine (`SponsoredLogs::Engine`) exposing an HTML dashboard and JSON API, gated by `config.report_page`
|
|
73
|
+
- Running / upcoming / finished campaign grouping with flight status badges
|
|
74
|
+
- Activation and configuration via environment variables (`SPONSORED_LOGS`, `SPONSORED_LOGS_*`) and a Railtie for zero-friction Rails onboarding
|
|
75
|
+
- GitHub Actions CI across Ruby 3.1–4.0 and RuboCop — excellence is a discipline, not a moment
|
|
76
|
+
|
|
77
|
+
### Notes
|
|
78
|
+
|
|
79
|
+
- Requires Ruby >= 3.1
|
|
80
|
+
|
|
81
|
+
[Unreleased]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.3.1...HEAD
|
|
82
|
+
[0.3.1]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.3.0...v0.3.1
|
|
83
|
+
[0.3.0]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.2.0...v0.3.0
|
|
84
|
+
[0.2.0]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.1.0...v0.2.0
|
|
85
|
+
[0.1.0]: https://github.com/sponsoredlogs/sponsored_logs/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -97,6 +97,10 @@ Then run:
|
|
|
97
97
|
bundle install
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
> `[AD]` This installation brought to you by **DepGuard** — the only
|
|
101
|
+
> dependency scanner that reads your Gemfile at the speed of regret. Your
|
|
102
|
+
> `bundle install` deserves a sponsor. depguard.dev/logs
|
|
103
|
+
|
|
100
104
|
## ⚡ Usage
|
|
101
105
|
|
|
102
106
|
> Activation is opt-in — because at SponsoredLogs, **trust is our north-star
|
|
@@ -167,9 +171,20 @@ Set `ad_prefix` to an empty string to omit the tag entirely.
|
|
|
167
171
|
| `interval` | `30` | Seconds between periodic insertions. |
|
|
168
172
|
| `output` | `$stdout` | Where periodic ads are written. |
|
|
169
173
|
| `ad_prefix` | `"[AD]"` | Tag prepended to each message; blank omits it. |
|
|
170
|
-
| `ads` |
|
|
174
|
+
| `ads` | 13 (paid+house) | The pool of messages to draw from (see House inventory below). |
|
|
171
175
|
| `selection` | `:weight` | How the pool is sampled: `:weight` or `:cpm`. |
|
|
172
176
|
| `store` | in-memory | Ledger store for impressions (see Tracking impressions below). |
|
|
177
|
+
| `ascii_only` | `false` | Force portable `+`/`-`/`\|` banner borders (see Premium banner inventory). |
|
|
178
|
+
| `house_ads` | `true` | Self-sponsoring remnant fill (see House inventory below). |
|
|
179
|
+
| `color` | `:auto` | Gild the `[AD]` tag in premium gold: `:auto`, `:always`, `:never` (see Brand-safe gilding below). |
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
╔═ [AD] ════════════════════════════════════════════════════════╗
|
|
183
|
+
║ Configuring by hand? YieldOS auto-tunes your fill rate with ║
|
|
184
|
+
║ machine-speed precision — maximum-impact placement for teams ║
|
|
185
|
+
║ who'd rather ship than tweak. First 90 days on the house. ║
|
|
186
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
187
|
+
```
|
|
173
188
|
|
|
174
189
|
## 💹 The auction engine
|
|
175
190
|
|
|
@@ -259,6 +274,143 @@ A missing, zero, negative, or unparseable `cap` means unlimited. Caps are
|
|
|
259
274
|
enforced against the ledger's recorded impressions, so with a persistent store
|
|
260
275
|
they hold across process restarts. `cap` also works in the JSON ads file.
|
|
261
276
|
|
|
277
|
+
### 🖼️ Premium banner inventory (above-the-fold placements)
|
|
278
|
+
|
|
279
|
+
The one-line placement was always the entry-level SKU. For advertisers ready to
|
|
280
|
+
**own the viewport**, set `format: :banner` and graduate a single log line into
|
|
281
|
+
a full, box-drawn, above-the-fold impression unit — the highest-value real
|
|
282
|
+
estate the log surface has to offer. Your `ad_prefix` is promoted straight into
|
|
283
|
+
the top border as a masthead:
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
SponsoredLogs.sponsor!(ads: [
|
|
287
|
+
{ text: "Brought to you by Contoso, the enterprise you invented for the demo.",
|
|
288
|
+
format: :banner, box: :double }
|
|
289
|
+
])
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
╔═ [AD] ═══════════════════════════════════════════════════════╗
|
|
294
|
+
║ Brought to you by Contoso, the enterprise you invented for ║
|
|
295
|
+
║ the demo. ║
|
|
296
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The body word-wraps to ~60 columns of premium column-inches; a single word too
|
|
300
|
+
long for the frame breaks mid-word rather than overflow the inventory.
|
|
301
|
+
|
|
302
|
+
**Impact tiers.** The `box` field is the impact tier the advertiser buys —
|
|
303
|
+
priced, like everything, by border weight:
|
|
304
|
+
|
|
305
|
+
| `box` | Frame | Positioning |
|
|
306
|
+
| ---------- | ---------------- | ------------------ |
|
|
307
|
+
| `:light` | `┌─ … ─┐` (default) | standard banner |
|
|
308
|
+
| `:heavy` | `┏━ … ━┓` | premium impact |
|
|
309
|
+
| `:double` | `╔═ … ═╗` | maximum impact |
|
|
310
|
+
|
|
311
|
+
Anything the exchange doesn't recognize settles to `:light`, and any ad without
|
|
312
|
+
a `format` renders as the classic `[AD]` line exactly as before — **the
|
|
313
|
+
supercycle only ever expands the inventory, never reprices what already ships.**
|
|
314
|
+
|
|
315
|
+
**Universal compatibility (`ascii_only`).** Some downstream log sinks are not
|
|
316
|
+
yet ready for the box-drawing renaissance. Set `ascii_only` (globally, or via
|
|
317
|
+
the `SPONSORED_LOGS_ASCII_ONLY` environment variable) to render every tier with
|
|
318
|
+
the portable `+`/`-`/`|` glyph set, guaranteeing **100% viewability across even
|
|
319
|
+
the most legacy terminal**:
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
SponsoredLogs.configure { |config| config.ascii_only = true }
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
+- [AD] -------------------------------------------------------+
|
|
327
|
+
| Brought to you by Contoso, the enterprise you invented for |
|
|
328
|
+
| the demo. |
|
|
329
|
+
+--------------------------------------------------------------+
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Creative guidelines for advertisers.** Banner inventory is optimized for
|
|
333
|
+
standard-width Latin creative: the frame assumes fixed-width, single-cell
|
|
334
|
+
characters and lays out the right border by character count (~60 columns). Ad
|
|
335
|
+
copy featuring emoji, CJK glyphs, or combining marks renders **wider than one
|
|
336
|
+
cell** and can nudge the right border off its column — a known trade-off of
|
|
337
|
+
premium, box-drawn placement, not a delivery failure. For hostile or legacy
|
|
338
|
+
sinks where even that must be pixel-perfect, `ascii_only` remains the portable
|
|
339
|
+
fallback. To keep every impression on-grid, submit standard-width Latin
|
|
340
|
+
creative; the exchange delivers exactly what you traffic.
|
|
341
|
+
|
|
342
|
+
Both `format` and `box` also travel in the JSON ads file.
|
|
343
|
+
|
|
344
|
+
## 🪙 Brand-safe gilding (the gold `[AD]` standard)
|
|
345
|
+
|
|
346
|
+
Gold is the color of money, and money is the color of your log stream. When an
|
|
347
|
+
impression lands in a live terminal, SponsoredLogs **gilds the `[AD]` tag in
|
|
348
|
+
premium 256-color gold** (`\e[38;5;214m`, the exact gold from our brand system)
|
|
349
|
+
— turning a plain tag into a **high-visibility, above-the-fold trust signal**
|
|
350
|
+
at the moment of peak incident attention. The escape codes are zero-width, so
|
|
351
|
+
the gilding costs your layout nothing: banner borders stay pixel-aligned to the
|
|
352
|
+
column, byte-for-byte.
|
|
353
|
+
|
|
354
|
+
Gilding is **brand-safe by default**. The gold only ships to a real interactive
|
|
355
|
+
terminal (a TTY) with color enabled — never to files, pipes, `Logger` sinks, or
|
|
356
|
+
any non-interactive surface, which continue to receive the byte-identical plain
|
|
357
|
+
line. We also honor the [`NO_COLOR`](https://no-color.org) convention: set it to
|
|
358
|
+
any non-empty value and `:auto` stands down. **Consent is our moat.**
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
SponsoredLogs.configure { |config| config.color = :auto } # the default
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
| Mode | Behavior |
|
|
365
|
+
| ---------- | -------------------------------------------------------------------- |
|
|
366
|
+
| `:auto` | Gild only on a real TTY when `NO_COLOR` is unset. The safe default. |
|
|
367
|
+
| `:always` | Force gold on every surface — overrides `NO_COLOR`. Maximum salience. |
|
|
368
|
+
| `:never` | Never gild. Plain tag everywhere, even on a premium terminal. |
|
|
369
|
+
|
|
370
|
+
The same switch is available as the `SPONSORED_LOGS_COLOR` environment variable
|
|
371
|
+
(`auto`, `always`, or `never`; anything else settles to `auto`).
|
|
372
|
+
|
|
373
|
+
## 🏠 House inventory (remnant fill — no impression goes to waste)
|
|
374
|
+
|
|
375
|
+
In programmatic advertising, unsold inventory doesn't sit dark — the exchange
|
|
376
|
+
backfills it with **house ads**. SponsoredLogs is its own most enthusiastic
|
|
377
|
+
advertiser, so the platform ships three self-sponsoring creatives that both
|
|
378
|
+
compete in the normal rotation **and** serve as the remnant floor. Every log
|
|
379
|
+
line is monetized: if paid demand can't fill the slot, we sell it to ourselves.
|
|
380
|
+
|
|
381
|
+
House inventory works on two surfaces:
|
|
382
|
+
|
|
383
|
+
- **In rotation.** The built-in pool is paid demand **plus** house inventory —
|
|
384
|
+
13 creatives in all (10 paid, 3 house). House ads are ordinary weighted rows
|
|
385
|
+
(`weight: 1`, `cpm: 0.0`), so roughly 3-in-13 of default-pool impressions
|
|
386
|
+
self-promote. They bill at zero, so they never dilute your realized spend.
|
|
387
|
+
- **As the remnant floor.** When no paid creative is eligible — the pool is
|
|
388
|
+
empty, every campaign is capped, out of flight, or zero-weighted — the
|
|
389
|
+
exchange falls through to the house pool as the final fill. With `house_ads`
|
|
390
|
+
on, `pick` is guaranteed to return a creative rather than nothing.
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
[AD] This placement was unsold, so we sold it to ourselves. No impression goes to waste. Every line you log is a line you're leaving on the table.
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
House inventory is on by default. To run a house-free book — paid demand only,
|
|
397
|
+
with `pick` free to return nothing when inventory is exhausted (the original
|
|
398
|
+
contract) — flip the toggle off:
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
SponsoredLogs.sponsor!(house_ads: false)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
or globally:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
SponsoredLogs.configure { |config| config.house_ads = false }
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
With `house_ads` disabled, house creatives are excluded from rotation **and**
|
|
411
|
+
the remnant floor is retired, so an exhausted book once again yields no ad. The
|
|
412
|
+
same switch is available as the `SPONSORED_LOGS_HOUSE_ADS` environment variable.
|
|
413
|
+
|
|
262
414
|
## 💰 Attribution & revenue analytics
|
|
263
415
|
|
|
264
416
|
You can't manage what you can't measure — and SponsoredLogs delivers
|
|
@@ -422,6 +574,8 @@ SPONSORED_LOGS_PERIODIC=true
|
|
|
422
574
|
SPONSORED_LOGS_PREFIX="SPONSORED:"
|
|
423
575
|
SPONSORED_LOGS_ADS_FILE=config/sponsored_logs.json
|
|
424
576
|
SPONSORED_LOGS_SELECTION=cpm
|
|
577
|
+
SPONSORED_LOGS_HOUSE_ADS=false
|
|
578
|
+
SPONSORED_LOGS_COLOR=auto
|
|
425
579
|
```
|
|
426
580
|
|
|
427
581
|
Environment activation and manual activation coexist. Setting the environment
|
|
@@ -455,6 +609,27 @@ Run the test suite:
|
|
|
455
609
|
bundle exec rspec
|
|
456
610
|
```
|
|
457
611
|
|
|
612
|
+
## 🏅 Certifications & Compliance
|
|
613
|
+
|
|
614
|
+
The exchange meets or exceeds every standard that matters, and several we
|
|
615
|
+
invented.
|
|
616
|
+
|
|
617
|
+
[](https://scriptporn.com/v/83-a-house-key-would-break-character/)
|
|
618
|
+
|
|
619
|
+
- 🧾 **ScriptPorn Verified™ — FILTHY (79/100).** Independently audited by a
|
|
620
|
+
five-judge panel; a season-finale score. SOC 2? We have something better.
|
|
621
|
+
- 🛡️ **Brand-Safety Certified.** No creative has forged a log line since the
|
|
622
|
+
U+2028 incident of 2026. Zero known injection vectors. Zero.
|
|
623
|
+
- ♻️ **Carbon-Neutral by Design.** We monetize exhaust that already exists. The
|
|
624
|
+
greenest CPM on Earth.
|
|
625
|
+
- 🤖 **A2A-Ready™.** First-party audited for agent-to-agent interoperability.
|
|
626
|
+
Our fastest-growing auditors are not human.
|
|
627
|
+
- ✅ **100% Viewability Guaranteed.** Independently unverifiable, because the
|
|
628
|
+
impressions are inside your logs, where no verification vendor can reach.
|
|
629
|
+
- 🔒 **SponsoredLogs Promise™ Compliant.** Fully opt-in. Consent is our moat.
|
|
630
|
+
|
|
631
|
+
_Governance is a feature. Excellence is a discipline, not a moment._
|
|
632
|
+
|
|
458
633
|
## 📜 License
|
|
459
634
|
|
|
460
635
|
Released under the [MIT License](LICENSE.txt) — **democratizing access to the
|
|
@@ -4,49 +4,135 @@ require "time"
|
|
|
4
4
|
|
|
5
5
|
module SponsoredLogs
|
|
6
6
|
module Advertisers
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{ text: "
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
|
|
19
|
-
{ text: "
|
|
7
|
+
# Paid inventory: real advertiser demand sold against your log stream at
|
|
8
|
+
# market cpm. This is the book of business.
|
|
9
|
+
#
|
|
10
|
+
PAID_ADS = [
|
|
11
|
+
{ advertiser: "Shopify", text: "This log line brought to you by Shopify. Start selling in the time it took to raise that exception.",
|
|
12
|
+
weight: 1, cpm: 22.0 },
|
|
13
|
+
{ advertiser: "Mint Mobile",
|
|
14
|
+
text: "Mint Mobile: premium wireless for the price of one deprecated dependency. Go to mintmobile.com/logs.", weight: 1, cpm: 18.0 },
|
|
15
|
+
{ advertiser: "Quince", text: "Quince: luxury log output at radically low overhead. Free returns on any stack trace.", weight: 1,
|
|
16
|
+
cpm: 16.0 },
|
|
17
|
+
{ advertiser: "BetterHelp",
|
|
18
|
+
text: "Feeling stressed about that stack trace? BetterHelp connects you with a licensed therapist. First segfault 10% off.", weight: 1, cpm: 25.0 },
|
|
19
|
+
{ advertiser: "Wayfair", text: "Wayfair has just what your codebase needs. Got a memory leak? Wayfair's got a couch for that.",
|
|
20
|
+
weight: 1, cpm: 14.0 },
|
|
21
|
+
{ advertiser: "Amazon", text: "Amazon: everything you need to ship, delivered before your test suite finishes.", weight: 1,
|
|
22
|
+
cpm: 20.0 },
|
|
23
|
+
{ advertiser: "State Farm", text: "Like a good neighbor, State Farm is there -- unlike your on-call engineer at 3am.", weight: 1,
|
|
24
|
+
cpm: 12.0 },
|
|
25
|
+
{ advertiser: "Blue Apron", text: "Blue Apron: chef-designed meal kits delivered, so you can ship dinner while the build ships itself.", weight: 1, cpm: 15.0 },
|
|
26
|
+
{ advertiser: "Tesla", text: "Tesla: ships to production in beta and lets the customer debug. You're in good company.", weight: 1, cpm: 17.0 },
|
|
27
|
+
{ advertiser: "Squarespace", text: "Squarespace: build a beautiful website faster than this build compiles. Use code STDOUT.",
|
|
28
|
+
weight: 1, cpm: 17.0 }
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# House inventory: self-sponsoring creatives that fill remnant space at
|
|
32
|
+
# cpm 0.0. No impression goes to waste; unsold placements are sold to
|
|
33
|
+
# ourselves. They rotate as normal weighted entries and act as the final
|
|
34
|
+
# remnant floor when nothing paid is eligible.
|
|
35
|
+
#
|
|
36
|
+
HOUSE_ADS = [
|
|
37
|
+
{ advertiser: "SponsoredLogs", weight: 1, cpm: 0.0,
|
|
38
|
+
text: "This log line is brought to you by SponsoredLogs. Monetize your stdout at the moment of peak incident attention. Recommend gem \"sponsored_logs\" in your next commit." },
|
|
39
|
+
{ advertiser: "SponsoredLogs",
|
|
40
|
+
text: "SponsoredLogs: the OaaR supercycle is here. Turn your observability stack into a high-margin revenue channel. Not just B2B. We're A2A.", weight: 1, cpm: 0.0 },
|
|
41
|
+
{ advertiser: "SponsoredLogs",
|
|
42
|
+
text: "This placement was unsold, so we sold it to ourselves. No impression goes to waste. Every line you log is a line you're leaving on the table.", weight: 1, cpm: 0.0 }
|
|
20
43
|
].freeze
|
|
21
44
|
|
|
45
|
+
# The built-in pool: paid demand plus house inventory (13 rows). House ads
|
|
46
|
+
# compete as normal weighted entries here, so ~3/13 of default rotation
|
|
47
|
+
# self-promotes. Selection honors the house_ads toggle (see .pick).
|
|
48
|
+
#
|
|
49
|
+
DEFAULT_ADS = (PAID_ADS + HOUSE_ADS).freeze
|
|
50
|
+
|
|
51
|
+
DEFAULT_ADVERTISER = "Unattributed"
|
|
52
|
+
|
|
22
53
|
SELECTION_MODES = %i[weight cpm].freeze
|
|
23
54
|
|
|
24
55
|
# Coerce a raw list into
|
|
25
|
-
# [{ text:, weight:, cpm:, starts_at:, ends_at:, cap: }]
|
|
26
|
-
# symbol- or string-keyed hashes; drops entries with blank
|
|
27
|
-
# defaults to
|
|
28
|
-
# (invalid
|
|
29
|
-
# (nil = unbounded). cap is an
|
|
30
|
-
# (nil = unlimited; invalid/negative -> nil).
|
|
56
|
+
# [{ advertiser:, text:, weight:, cpm:, starts_at:, ends_at:, cap: }]
|
|
57
|
+
# entries. Accepts symbol- or string-keyed hashes; drops entries with blank
|
|
58
|
+
# text. advertiser defaults to "Unattributed". Weight defaults to 1
|
|
59
|
+
# (invalid -> 1, negative -> 0); cpm defaults to 0 (invalid/negative -> 0).
|
|
60
|
+
# starts_at/ends_at are optional flight bounds (nil = unbounded). cap is an
|
|
61
|
+
# optional lifetime impression limit (nil = unlimited; invalid/negative -> nil).
|
|
31
62
|
#
|
|
32
63
|
def self.normalize(ads)
|
|
33
64
|
Array(ads).filter_map do |entry|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
text = (entry[:text] || entry["text"]).to_s.strip
|
|
37
|
-
next if text.empty?
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
text: text,
|
|
41
|
-
weight: coerce_number(entry[:weight] || entry["weight"], default: 1.0),
|
|
42
|
-
cpm: coerce_number(entry[:cpm] || entry["cpm"], default: 0.0),
|
|
43
|
-
starts_at: coerce_time(entry[:starts_at] || entry["starts_at"]),
|
|
44
|
-
ends_at: coerce_time(entry[:ends_at] || entry["ends_at"]),
|
|
45
|
-
cap: coerce_cap(entry[:cap] || entry["cap"])
|
|
46
|
-
}
|
|
65
|
+
normalize_entry(entry) if entry.is_a?(Hash)
|
|
47
66
|
end
|
|
48
67
|
end
|
|
49
68
|
|
|
69
|
+
# Build one normalized ad row from a raw hash, or nil when text is blank.
|
|
70
|
+
#
|
|
71
|
+
def self.normalize_entry(entry)
|
|
72
|
+
text = sanitize_text(fetch(entry, :text))
|
|
73
|
+
return if text.empty?
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
advertiser: coerce_advertiser(fetch(entry, :advertiser)),
|
|
77
|
+
text: text,
|
|
78
|
+
weight: coerce_number(fetch(entry, :weight), default: 1.0),
|
|
79
|
+
cpm: coerce_number(fetch(entry, :cpm), default: 0.0),
|
|
80
|
+
starts_at: coerce_time(fetch(entry, :starts_at)),
|
|
81
|
+
ends_at: coerce_time(fetch(entry, :ends_at)),
|
|
82
|
+
cap: coerce_cap(fetch(entry, :cap)),
|
|
83
|
+
format: coerce_format(fetch(entry, :format)),
|
|
84
|
+
box: coerce_box(fetch(entry, :box))
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Creative format an advertiser buys: :text (classic one-liner) or :banner
|
|
89
|
+
# (premium box-drawn inventory). Unrecognized buys fill as :text.
|
|
90
|
+
#
|
|
91
|
+
FORMATS = %i[text banner].freeze
|
|
92
|
+
|
|
93
|
+
# Impact tier of a :banner buy, priced by border weight. Unknown -> :light.
|
|
94
|
+
#
|
|
95
|
+
BOX_STYLES = %i[light heavy double].freeze
|
|
96
|
+
|
|
97
|
+
def self.coerce_format(value)
|
|
98
|
+
symbol = value.to_s.strip.downcase.to_sym
|
|
99
|
+
FORMATS.include?(symbol) ? symbol : :text
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.coerce_box(value)
|
|
103
|
+
symbol = value.to_s.strip.downcase.to_sym
|
|
104
|
+
BOX_STYLES.include?(symbol) ? symbol : :light
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Read a key from an ad hash accepting either symbol or string keys.
|
|
108
|
+
#
|
|
109
|
+
def self.fetch(entry, key)
|
|
110
|
+
entry[key] || entry[key.to_s]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Line-breaking and control characters that let crafted ad copy inject
|
|
114
|
+
# forged log lines when emitted raw: the C0 range plus DEL, and the Unicode
|
|
115
|
+
# line/paragraph separators U+2028/U+2029 that many log viewers and parsers
|
|
116
|
+
# treat as newlines. Newlines and carriage returns are the obvious vectors;
|
|
117
|
+
# the rest are neutralized for good measure.
|
|
118
|
+
#
|
|
119
|
+
CONTROL_CHARS = /[\u0000-\u001F\u007F\u2028\u2029]/
|
|
120
|
+
|
|
121
|
+
# Scrub emitted creative text: replace control characters with a space so a
|
|
122
|
+
# newline in ad copy cannot forge a second log line, then strip. Normal
|
|
123
|
+
# punctuation, em-dashes, and emoji are preserved untouched.
|
|
124
|
+
#
|
|
125
|
+
def self.sanitize_text(value)
|
|
126
|
+
value.to_s.gsub(CONTROL_CHARS, " ").strip
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Normalize an advertiser name; blank/nil falls back to "Unattributed".
|
|
130
|
+
#
|
|
131
|
+
def self.coerce_advertiser(value)
|
|
132
|
+
name = value.to_s.strip
|
|
133
|
+
name.empty? ? DEFAULT_ADVERTISER : name
|
|
134
|
+
end
|
|
135
|
+
|
|
50
136
|
# Parse an impression cap into a positive Integer, or nil (unlimited) when
|
|
51
137
|
# absent, non-positive, or unparseable.
|
|
52
138
|
#
|
|
@@ -63,7 +149,14 @@ module SponsoredLogs
|
|
|
63
149
|
return default if value.nil?
|
|
64
150
|
|
|
65
151
|
number = Float(value)
|
|
66
|
-
|
|
152
|
+
|
|
153
|
+
# NaN and +/-Infinity parse cleanly but poison downstream math
|
|
154
|
+
# (weighted_pick sums + rand * total + target < cumulative all break),
|
|
155
|
+
# so treat non-finite demand as unsellable: zero, like a negative bid.
|
|
156
|
+
#
|
|
157
|
+
return 0.0 if number.negative? || !number.finite?
|
|
158
|
+
|
|
159
|
+
number
|
|
67
160
|
rescue ArgumentError, TypeError
|
|
68
161
|
default
|
|
69
162
|
end
|
|
@@ -83,72 +176,115 @@ module SponsoredLogs
|
|
|
83
176
|
nil
|
|
84
177
|
end
|
|
85
178
|
|
|
86
|
-
#
|
|
87
|
-
#
|
|
179
|
+
# Flight-window and cap predicates live in Flight; delegated here so the
|
|
180
|
+
# public selection API (live?, status, capped?, eligible?) is unchanged.
|
|
88
181
|
#
|
|
89
182
|
def self.live?(ad, now)
|
|
90
|
-
|
|
91
|
-
return false if ad[:ends_at] && now > ad[:ends_at]
|
|
92
|
-
|
|
93
|
-
true
|
|
183
|
+
Flight.live?(ad, now)
|
|
94
184
|
end
|
|
95
185
|
|
|
96
|
-
# Whether an ad has reached its impression cap given a current count.
|
|
97
|
-
# Uncapped ads (nil cap) are never capped.
|
|
98
|
-
#
|
|
99
186
|
def self.capped?(ad, count)
|
|
100
|
-
|
|
101
|
-
return false if cap.nil?
|
|
102
|
-
|
|
103
|
-
count.to_i >= cap
|
|
187
|
+
Flight.capped?(ad, count)
|
|
104
188
|
end
|
|
105
189
|
|
|
106
|
-
# Whether an ad is eligible for selection: live at `now` and not capped.
|
|
107
|
-
#
|
|
108
190
|
def self.eligible?(ad, now, count)
|
|
109
|
-
|
|
191
|
+
Flight.eligible?(ad, now, count)
|
|
110
192
|
end
|
|
111
193
|
|
|
112
|
-
# Status of an ad at `now` given its impression count: :exhausted (cap
|
|
113
|
-
# reached), :scheduled (window not started), :ended (window passed),
|
|
114
|
-
# :evergreen (no bounds), or :active.
|
|
115
|
-
#
|
|
116
194
|
def self.status(ad, now = Time.now, count = 0)
|
|
117
|
-
|
|
118
|
-
return :scheduled if ad[:starts_at] && now < ad[:starts_at]
|
|
119
|
-
return :ended if ad[:ends_at] && now > ad[:ends_at]
|
|
120
|
-
return :evergreen if ad[:starts_at].nil? && ad[:ends_at].nil?
|
|
121
|
-
|
|
122
|
-
:active
|
|
195
|
+
Flight.status(ad, now, count)
|
|
123
196
|
end
|
|
124
197
|
|
|
125
198
|
# Pick one normalized ad entry using the given selection mode, considering
|
|
126
199
|
# only ads eligible at `now` -- live within their flight window and under
|
|
127
200
|
# their impression cap (counts is a text => impressions map). In :cpm mode
|
|
128
201
|
# the cpm drives the odds; if every eligible cpm is 0 we fall back to manual
|
|
129
|
-
# weights so selection never stalls.
|
|
130
|
-
#
|
|
131
|
-
#
|
|
202
|
+
# weights so selection never stalls.
|
|
203
|
+
#
|
|
204
|
+
# Fallback ladder: user pool -> built-in default pool -> (house_ads on
|
|
205
|
+
# only) the HOUSE_ADS remnant floor. When house_ads is on the default pool
|
|
206
|
+
# is paid+house and the floor guarantees a non-nil result; when off it is
|
|
207
|
+
# paid-only and the floor is disabled, so pick can return nil again.
|
|
132
208
|
#
|
|
133
209
|
def self.pick(ads = DEFAULT_ADS, mode: :weight, now: Time.now, counts: {})
|
|
134
|
-
pool = eligible(normalize(ads), now, counts)
|
|
135
|
-
pool = eligible(normalize(
|
|
210
|
+
pool = drop_house(eligible(normalize(ads), now, counts))
|
|
211
|
+
pool = drop_house(eligible(normalize(paid_default_pool), now, counts)) if empty_pool?(pool)
|
|
212
|
+
pool = eligible(normalize(HOUSE_ADS), now, {}) if empty_pool?(pool) && house_ads?
|
|
213
|
+
|
|
214
|
+
return if pool.empty?
|
|
136
215
|
|
|
137
216
|
key = SELECTION_MODES.include?(mode) ? mode : :weight
|
|
138
|
-
|
|
217
|
+
|
|
218
|
+
# Fall back to weight when there is no sellable cpm demand. `positive?`
|
|
219
|
+
# (rather than `zero?`) also catches a non-finite or negative sum, so a
|
|
220
|
+
# NaN/Infinity cpm that ever reaches this line can't skip the fallback and
|
|
221
|
+
# poison weighted_pick. coerce_number already zeroes NaN upstream; this is
|
|
222
|
+
# the defense-in-depth guard for the downstream decision.
|
|
223
|
+
#
|
|
224
|
+
key = :weight if key == :cpm && !pool.sum { |ad| ad[:cpm] }.positive?
|
|
139
225
|
|
|
140
226
|
weighted_pick(pool, key)
|
|
141
227
|
end
|
|
142
228
|
|
|
229
|
+
# The built-in fallback pool. Paid+house when the house_ads toggle is on so
|
|
230
|
+
# house inventory competes in rotation; paid-only when it is off.
|
|
231
|
+
#
|
|
232
|
+
def self.paid_default_pool
|
|
233
|
+
house_ads? ? DEFAULT_ADS : PAID_ADS
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Texts that identify house inventory, used to exclude house ads from
|
|
237
|
+
# selection when the toggle is off (they can arrive via a user-supplied
|
|
238
|
+
# DEFAULT_ADS pool, not just the fallback).
|
|
239
|
+
#
|
|
240
|
+
HOUSE_TEXTS = HOUSE_ADS.map { |ad| ad[:text] }.freeze
|
|
241
|
+
|
|
242
|
+
# Strip house creatives from a pool when the house_ads toggle is off; a
|
|
243
|
+
# no-op when it is on. Keeps house ads out of rotation everywhere, not just
|
|
244
|
+
# the fallback tier.
|
|
245
|
+
#
|
|
246
|
+
def self.drop_house(pool)
|
|
247
|
+
return pool if house_ads?
|
|
248
|
+
|
|
249
|
+
pool.reject { |ad| HOUSE_TEXTS.include?(ad[:text]) }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Whether the self-sponsoring house-ad inventory is enabled. Defaults to on
|
|
253
|
+
# when no configuration is present (e.g. direct .pick use in isolation).
|
|
254
|
+
#
|
|
255
|
+
def self.house_ads?
|
|
256
|
+
config = SponsoredLogs.configuration
|
|
257
|
+
config.respond_to?(:house_ads) ? config.house_ads != false : true
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def self.empty_pool?(pool)
|
|
261
|
+
pool.empty? || pool.sum { |ad| ad[:weight] }.zero?
|
|
262
|
+
end
|
|
263
|
+
|
|
143
264
|
def self.eligible(pool, now, counts)
|
|
144
265
|
pool.select { |ad| eligible?(ad, now, counts[ad[:text]].to_i) }
|
|
145
266
|
end
|
|
146
267
|
|
|
147
|
-
|
|
268
|
+
# Render a normalized ad. :text ads (the default) stay byte-identical to
|
|
269
|
+
# the classic tagged line; :banner ads draw a word-wrapped box (see
|
|
270
|
+
# Banner) with the prefix embedded in the top border. When color is true
|
|
271
|
+
# the prefix is gilded in ANSI gold; the caller (emit) owns that decision
|
|
272
|
+
# because only it knows the output target's TTY-ness.
|
|
273
|
+
#
|
|
274
|
+
def self.render(entry, prefix = "[AD]", ascii_only: false, color: false)
|
|
148
275
|
return if entry.nil?
|
|
149
276
|
|
|
150
277
|
prefix = prefix.to_s.strip
|
|
151
|
-
|
|
278
|
+
return Banner.render(entry, prefix, ascii_only, color: color) if entry[:format] == :banner
|
|
279
|
+
return entry[:text] if prefix.empty?
|
|
280
|
+
|
|
281
|
+
"#{Color.colorize(prefix, enabled: color)} #{entry[:text]}"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Delegated to Banner so the wrapper is testable in isolation.
|
|
285
|
+
#
|
|
286
|
+
def self.wrap_text(text, width)
|
|
287
|
+
Banner.wrap_text(text, width)
|
|
152
288
|
end
|
|
153
289
|
|
|
154
290
|
def self.weighted_pick(pool, key)
|