Class: TrackRelay::Linter
- Inherits:
-
Object
- Object
- TrackRelay::Linter
- Defined in:
- lib/track_relay/linter.rb
Overview
Audits the JSONL untyped-event sink written by Subscribers::Logger and produces a deduped report grouped by event name + sorted-param-name signature.
## Input contract (locked in Plan 05 / 01-CONTEXT.md)
The JSONL sink contains one event per line. Each line is JSON with the canonical shape:
{"event":"...", "params":["a","b"], "controller":"...", "action":"...", "timestamp":"..."}
‘params` carries only sorted, stringified parameter NAMES — values are never written to the sink (privacy contract from 01-CONTEXT.md). The linter reads the same shape and dedupes only on `event` + sorted `params`; `controller`, `action`, and `timestamp` are accepted but ignored for grouping (they are useful breadcrumbs for the human reading the JSONL directly, not signal for dedup).
## Output
-
#report → Array of Report structs, sorted by total occurrences descending. Each Report bundles every distinct param signature seen for that event name.
-
#print → human-readable summary written to an IO.
-
#to_json → machine-readable JSON with stable keys ‘total, signatures: [{params, count]}`. Plan 09’s CHANGELOG references this contract.
## Resilience
-
Missing files return an empty report (the JSONL may legitimately not exist yet on a fresh app).
-
Lines that are not valid JSON are skipped and counted in #malformed_lines.
-
Blank lines are silently skipped (not malformed).
Defined Under Namespace
Classes: Ga4Violation, Report, Signature
Instance Attribute Summary collapse
-
#malformed_lines ⇒ Integer
readonly
Count of lines that failed JSON parsing.
Instance Method Summary collapse
-
#ga4_violations ⇒ Array<Ga4Violation>
Audit each unique event name in the JSONL sink against Validators::Ga4Constraints (REQ-28, Plan 02-04 / Scout §8).
-
#initialize(jsonl_path) ⇒ Linter
constructor
A new instance of Linter.
-
#print(io = $stdout) ⇒ void
Write a human-readable summary to ‘io`.
-
#print_ga4(io = $stdout) ⇒ Boolean
Write the GA4 violation report to ‘io`.
-
#report ⇒ Array<Report>
Build the deduped report.
-
#to_json ⇒ String
Emit machine-readable JSON.
Constructor Details
#initialize(jsonl_path) ⇒ Linter
Returns a new instance of Linter.
64 65 66 67 |
# File 'lib/track_relay/linter.rb', line 64 def initialize(jsonl_path) @jsonl_path = jsonl_path @malformed_lines = 0 end |
Instance Attribute Details
#malformed_lines ⇒ Integer (readonly)
Returns count of lines that failed JSON parsing.
62 63 64 |
# File 'lib/track_relay/linter.rb', line 62 def malformed_lines @malformed_lines end |
Instance Method Details
#ga4_violations ⇒ Array<Ga4Violation>
Audit each unique event name in the JSONL sink against Validators::Ga4Constraints (REQ-28, Plan 02-04 / Scout §8).
Only the event-name shape is checked here:
- snake_case regex (`/\A[a-z][a-z0-9_]*\z/`)
- max 40 chars
- not in `GA4_RESERVED_NAMES`
Param-name validation is intentionally OUT OF SCOPE for this method — ‘params` in the JSONL is a sorted-NAMES-only privacy snapshot, but param-name shape is a per-line fact (each occurrence could fail differently) and the linter’s grouping model is built around event names. Use the call-time validation in Subscribers::Ga4MeasurementProtocol#deliver for per-payload checks; this method is the audit trail for “what event names did we ship that GA4 will silently drop?”.
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/track_relay/linter.rb', line 158 def ga4_violations counts = Hash.new(0) read_lines do |entry| name = entry["event"] next if name.nil? || name.empty? counts[name] += 1 end counts.map { |name, count| begin Validators::Ga4Constraints.validate_event_name!(name) nil rescue Ga4ConstraintError => e Ga4Violation.new(event_name: name, reason: e., count: count) end }.compact.sort_by { |v| -v.count } end |
#print(io = $stdout) ⇒ void
This method returns an undefined value.
Write a human-readable summary to ‘io`.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/track_relay/linter.rb', line 95 def print(io = $stdout) reports = report io.puts "# track_relay untyped event audit" io.puts "# source: #{@jsonl_path}" io.puts "# events: #{reports.size}; total occurrences: #{reports.sum(&:total)}" io.puts "" reports.each do |r| io.puts "event :#{r.event_name} (#{r.total} total)" r.signatures.each do |sig| io.puts " - params=[#{sig.params.join(", ")}] count=#{sig.count}" end io.puts "" end io.puts "# #{@malformed_lines} malformed line(s) skipped" if @malformed_lines.positive? end |
#print_ga4(io = $stdout) ⇒ Boolean
Write the GA4 violation report to ‘io`.
Returns ‘true` when there were no violations (rake task should exit 0), `false` otherwise (rake task exits non-zero).
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/track_relay/linter.rb', line 183 def print_ga4(io = $stdout) violations = ga4_violations io.puts "# track_relay GA4 event-name audit" io.puts "# source: #{@jsonl_path}" io.puts "# violations: #{violations.size}" io.puts "" if violations.empty? io.puts "# clean — every event name passes GA4 constraints" return true end violations.each do |v| io.puts "event :#{v.event_name} (#{v.count} occurrence#{"s" unless v.count == 1})" io.puts " reason: #{v.reason}" io.puts "" end false end |
#report ⇒ Array<Report>
Build the deduped report.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/track_relay/linter.rb', line 72 def report groups = Hash.new { |h, k| h[k] = Hash.new(0) } read_lines do |entry| event = entry["event"] signature = Array(entry["params"]).sort groups[event][signature] += 1 end groups.map { |event, signatures| sig_list = signatures.map { |params, count| Signature.new(params: params, count: count) } total = sig_list.sum(&:count) Report.new( event_name: event, signatures: sig_list.sort_by { |s| -s.count }, total: total ) }.sort_by { |r| -r.total } end |
#to_json ⇒ String
Emit machine-readable JSON.
Keys (‘event`, `total`, `signatures`, `params`, `count`) are stable — Plan 09’s CHANGELOG references this contract.
117 118 119 120 121 122 123 124 125 |
# File 'lib/track_relay/linter.rb', line 117 def to_json(*) JSON.generate(report.map { |r| { event: r.event_name, total: r.total, signatures: r.signatures.map { |s| {params: s.params, count: s.count} } } }) end |