sponsored_logs 0.3.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 +9 -1
- data/README.md +21 -0
- data/lib/sponsored_logs/advertisers.rb +39 -29
- data/lib/sponsored_logs/flight.rb +62 -0
- data/lib/sponsored_logs/version.rb +1 -1
- data/lib/sponsored_logs.rb +1 -0
- metadata +2 -1
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
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
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
|
+
|
|
10
17
|
## [0.3.0] - 2026-09-07
|
|
11
18
|
|
|
12
19
|
### Fixed
|
|
@@ -71,7 +78,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
71
78
|
|
|
72
79
|
- Requires Ruby >= 3.1
|
|
73
80
|
|
|
74
|
-
[Unreleased]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.3.
|
|
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
|
|
75
83
|
[0.3.0]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.2.0...v0.3.0
|
|
76
84
|
[0.2.0]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.1.0...v0.2.0
|
|
77
85
|
[0.1.0]: https://github.com/sponsoredlogs/sponsored_logs/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -609,6 +609,27 @@ Run the test suite:
|
|
|
609
609
|
bundle exec rspec
|
|
610
610
|
```
|
|
611
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
|
+
|
|
612
633
|
## 📜 License
|
|
613
634
|
|
|
614
635
|
Released under the [MIT License](LICENSE.txt) — **democratizing access to the
|
|
@@ -69,7 +69,7 @@ module SponsoredLogs
|
|
|
69
69
|
# Build one normalized ad row from a raw hash, or nil when text is blank.
|
|
70
70
|
#
|
|
71
71
|
def self.normalize_entry(entry)
|
|
72
|
-
text = fetch(entry, :text)
|
|
72
|
+
text = sanitize_text(fetch(entry, :text))
|
|
73
73
|
return if text.empty?
|
|
74
74
|
|
|
75
75
|
{
|
|
@@ -110,6 +110,22 @@ module SponsoredLogs
|
|
|
110
110
|
entry[key] || entry[key.to_s]
|
|
111
111
|
end
|
|
112
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
|
+
|
|
113
129
|
# Normalize an advertiser name; blank/nil falls back to "Unattributed".
|
|
114
130
|
#
|
|
115
131
|
def self.coerce_advertiser(value)
|
|
@@ -133,7 +149,14 @@ module SponsoredLogs
|
|
|
133
149
|
return default if value.nil?
|
|
134
150
|
|
|
135
151
|
number = Float(value)
|
|
136
|
-
|
|
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
|
|
137
160
|
rescue ArgumentError, TypeError
|
|
138
161
|
default
|
|
139
162
|
end
|
|
@@ -153,43 +176,23 @@ module SponsoredLogs
|
|
|
153
176
|
nil
|
|
154
177
|
end
|
|
155
178
|
|
|
156
|
-
#
|
|
157
|
-
#
|
|
179
|
+
# Flight-window and cap predicates live in Flight; delegated here so the
|
|
180
|
+
# public selection API (live?, status, capped?, eligible?) is unchanged.
|
|
158
181
|
#
|
|
159
182
|
def self.live?(ad, now)
|
|
160
|
-
|
|
161
|
-
return false if ad[:ends_at] && now > ad[:ends_at]
|
|
162
|
-
|
|
163
|
-
true
|
|
183
|
+
Flight.live?(ad, now)
|
|
164
184
|
end
|
|
165
185
|
|
|
166
|
-
# Whether an ad has reached its impression cap given a current count.
|
|
167
|
-
# Uncapped ads (nil cap) are never capped.
|
|
168
|
-
#
|
|
169
186
|
def self.capped?(ad, count)
|
|
170
|
-
|
|
171
|
-
return false if cap.nil?
|
|
172
|
-
|
|
173
|
-
count.to_i >= cap
|
|
187
|
+
Flight.capped?(ad, count)
|
|
174
188
|
end
|
|
175
189
|
|
|
176
|
-
# Whether an ad is eligible for selection: live at `now` and not capped.
|
|
177
|
-
#
|
|
178
190
|
def self.eligible?(ad, now, count)
|
|
179
|
-
|
|
191
|
+
Flight.eligible?(ad, now, count)
|
|
180
192
|
end
|
|
181
193
|
|
|
182
|
-
# Status of an ad at `now` given its impression count: :exhausted (cap
|
|
183
|
-
# reached), :scheduled (window not started), :ended (window passed),
|
|
184
|
-
# :evergreen (no bounds), or :active.
|
|
185
|
-
#
|
|
186
194
|
def self.status(ad, now = Time.now, count = 0)
|
|
187
|
-
|
|
188
|
-
return :scheduled if ad[:starts_at] && now < ad[:starts_at]
|
|
189
|
-
return :ended if ad[:ends_at] && now > ad[:ends_at]
|
|
190
|
-
return :evergreen if ad[:starts_at].nil? && ad[:ends_at].nil?
|
|
191
|
-
|
|
192
|
-
:active
|
|
195
|
+
Flight.status(ad, now, count)
|
|
193
196
|
end
|
|
194
197
|
|
|
195
198
|
# Pick one normalized ad entry using the given selection mode, considering
|
|
@@ -211,7 +214,14 @@ module SponsoredLogs
|
|
|
211
214
|
return if pool.empty?
|
|
212
215
|
|
|
213
216
|
key = SELECTION_MODES.include?(mode) ? mode : :weight
|
|
214
|
-
|
|
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?
|
|
215
225
|
|
|
216
226
|
weighted_pick(pool, key)
|
|
217
227
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module SponsoredLogs
|
|
6
|
+
# Flight-window and impression-cap predicates for a normalized ad. Extracted
|
|
7
|
+
# from Advertisers so the eligibility rules live in one cohesive place and the
|
|
8
|
+
# host module stays under Metrics/ModuleLength. Advertisers keeps thin
|
|
9
|
+
# delegators for its public surface (live?, status, capped?, eligible?).
|
|
10
|
+
#
|
|
11
|
+
module Flight
|
|
12
|
+
# Whether `now` falls before an ad's flight window opens. A nil starts_at is
|
|
13
|
+
# open-ended, so the ad has always started.
|
|
14
|
+
#
|
|
15
|
+
def self.before_start?(ad, now)
|
|
16
|
+
!ad[:starts_at].nil? && now < ad[:starts_at]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Whether `now` falls after an ad's flight window closes. A nil ends_at is
|
|
20
|
+
# open-ended, so the ad never ends.
|
|
21
|
+
#
|
|
22
|
+
def self.after_end?(ad, now)
|
|
23
|
+
!ad[:ends_at].nil? && now > ad[:ends_at]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Whether an ad is within its flight window at `now`. Missing bounds are
|
|
27
|
+
# open-ended (nil starts_at = always started; nil ends_at = never ends).
|
|
28
|
+
#
|
|
29
|
+
def self.live?(ad, now)
|
|
30
|
+
!before_start?(ad, now) && !after_end?(ad, now)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Whether an ad has reached its impression cap given a current count.
|
|
34
|
+
# Uncapped ads (nil cap) are never capped.
|
|
35
|
+
#
|
|
36
|
+
def self.capped?(ad, count)
|
|
37
|
+
cap = ad[:cap]
|
|
38
|
+
return false if cap.nil?
|
|
39
|
+
|
|
40
|
+
count.to_i >= cap
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Whether an ad is eligible for selection: live at `now` and not capped.
|
|
44
|
+
#
|
|
45
|
+
def self.eligible?(ad, now, count)
|
|
46
|
+
live?(ad, now) && !capped?(ad, count)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Status of an ad at `now` given its impression count: :exhausted (cap
|
|
50
|
+
# reached), :scheduled (window not started), :ended (window passed),
|
|
51
|
+
# :evergreen (no bounds), or :active.
|
|
52
|
+
#
|
|
53
|
+
def self.status(ad, now = Time.now, count = 0)
|
|
54
|
+
return :exhausted if capped?(ad, count)
|
|
55
|
+
return :scheduled if before_start?(ad, now)
|
|
56
|
+
return :ended if after_end?(ad, now)
|
|
57
|
+
return :evergreen if ad[:starts_at].nil? && ad[:ends_at].nil?
|
|
58
|
+
|
|
59
|
+
:active
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/sponsored_logs.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "logger"
|
|
|
5
5
|
require_relative "sponsored_logs/version"
|
|
6
6
|
require_relative "sponsored_logs/color"
|
|
7
7
|
require_relative "sponsored_logs/advertisers"
|
|
8
|
+
require_relative "sponsored_logs/flight"
|
|
8
9
|
require_relative "sponsored_logs/banner"
|
|
9
10
|
require_relative "sponsored_logs/ads_file"
|
|
10
11
|
require_relative "sponsored_logs/ledger/store/base"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sponsored_logs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerri Miller
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- lib/sponsored_logs/configuration.rb
|
|
128
128
|
- lib/sponsored_logs/engine.rb
|
|
129
129
|
- lib/sponsored_logs/env.rb
|
|
130
|
+
- lib/sponsored_logs/flight.rb
|
|
130
131
|
- lib/sponsored_logs/injector.rb
|
|
131
132
|
- lib/sponsored_logs/ledger/report.rb
|
|
132
133
|
- lib/sponsored_logs/ledger/store/active_record.rb
|