Class: Report::CallRecordingGaps
- Inherits:
-
Object
- Object
- Report::CallRecordingGaps
- Defined in:
- app/services/report/call_recording_gaps.rb
Overview
Daily reconciliation report: of yesterday's connected (TALKING) call legs that
SHOULD have a recording, which are MISSING one ("gaps").
A gap is a TALKING CallLogEvent with a real caller-id + at least
MIN_RECORDABLE_SECONDS of talk time — the same recordability test
Phone::CallLogEventToRecordMatcher applies — that the matcher could not link
to a CallRecord (so call_record_id stays nil).
Each gap is then classified, because "unmatched leg" is NOT the same as "lost
audio" — a transferred call has several CDR legs but one recording, so a sibling
leg legitimately holds the audio:
- STATUS_ABSENT — no recording for this call exists in the archive at all.
These are the real "audio never reached R2 / was never
imported" gaps and the number the report leads with. - STATUS_UNLINKED — a recording for this call IS in the archive but isn't
linked to this particular leg (e.g. a transfer/ring
sibling claimed it). A reconciliation artifact, not lost
audio.
The matcher runs every 15 min after each call-log import, so the links are
already current; this report just classifies yesterday's recordable legs.
Mirrors AccountDailyActivityReport: for_scheduled_run + America/Chicago day
window + memoized data the mailer view reads.
Defined Under Namespace
Classes: Gap
Constant Summary collapse
- TZ =
'America/Chicago'- STATUS_ABSENT =
No recording for this call exists in the archive — genuinely missing audio.
:absent- STATUS_UNLINKED =
A recording exists in the archive but is linked to a different leg of the
same call (transfer/consult sibling), not this one. :unlinked- NON_RECORDED_EXTENSIONS =
Agent extensions that are NOT recorded, so their TALKING legs must not count
as gaps. 602 = "BPO 24x7", an outsourced 24/7 answering service whose calls
are handled (and recorded, if at all) by the BPO, never in Heatwave —
verified 0% recording coverage over 45 days (33 legs, 0 recordings). Without
this, a quiet weekend whose only call is a BPO call reads as a 100% gap. [602].freeze
- MIN_RECORDABLE_SECONDS =
Sub-second-grade connects (1s of talk: answered-and-immediately-dropped) carry
no usable audio — Switchvox emits an empty/invalid WAV that the importer skips,
so no recording can ever exist. Counting them as recordable just manufactures a
permanent "missing audio" gap. Floor talk time at this many seconds. 2
Instance Attribute Summary collapse
-
#date ⇒ Date
readonly
The day being reconciled.
- #window_end ⇒ ActiveSupport::TimeWithZone readonly
- #window_start ⇒ ActiveSupport::TimeWithZone readonly
Class Method Summary collapse
-
.for_scheduled_run ⇒ Report::CallRecordingGaps
Previous calendar day in America/Chicago — the scheduled default.
Instance Method Summary collapse
- #absent_count ⇒ Integer
-
#absent_rate ⇒ Float
Share of recordable calls with NO archived audio — the genuine-loss rate.
-
#coverage_rate ⇒ Float
Share of recordable calls that DO have a recording.
-
#expected ⇒ Integer
Recordable TALKING legs we expected to have a recording.
- #gap_count ⇒ Integer
-
#gap_rate ⇒ Float
Share of recordable calls that are missing a recording.
-
#gaps ⇒ Array<Gap>
...the ones that did NOT — the gaps, sorted by time.
-
#gaps_absent ⇒ Array<Gap>
Gaps with no recording anywhere in the archive — genuinely missing audio.
-
#gaps_unlinked ⇒ Array<Gap>
Gaps whose audio IS archived but linked to a sibling leg of the same call.
-
#initialize(date:) ⇒ CallRecordingGaps
constructor
A new instance of CallRecordingGaps.
-
#matched ⇒ Integer
...of those, the count that linked to a CallRecord.
-
#to_csv ⇒ String
CSV of the gaps for the email attachment.
- #unlinked_count ⇒ Integer
Constructor Details
#initialize(date:) ⇒ CallRecordingGaps
Returns a new instance of CallRecordingGaps.
65 66 67 68 69 70 |
# File 'app/services/report/call_recording_gaps.rb', line 65 def initialize(date:) @date = date tz = Time.find_zone(TZ) @window_start = tz.local(date.year, date.month, date.day).beginning_of_day @window_end = @window_start.end_of_day end |
Instance Attribute Details
#date ⇒ Date (readonly)
Returns the day being reconciled.
60 61 62 |
# File 'app/services/report/call_recording_gaps.rb', line 60 def date @date end |
#window_end ⇒ ActiveSupport::TimeWithZone (readonly)
62 63 64 |
# File 'app/services/report/call_recording_gaps.rb', line 62 def window_end @window_end end |
#window_start ⇒ ActiveSupport::TimeWithZone (readonly)
62 63 64 |
# File 'app/services/report/call_recording_gaps.rb', line 62 def window_start @window_start end |
Class Method Details
.for_scheduled_run ⇒ Report::CallRecordingGaps
Previous calendar day in America/Chicago — the scheduled default.
57 |
# File 'app/services/report/call_recording_gaps.rb', line 57 def self.for_scheduled_run = new(date: Time.find_zone(TZ).now.to_date - 1) |
Instance Method Details
#absent_count ⇒ Integer
96 |
# File 'app/services/report/call_recording_gaps.rb', line 96 def absent_count = gaps_absent.size |
#absent_rate ⇒ Float
Share of recordable calls with NO archived audio — the genuine-loss rate.
111 |
# File 'app/services/report/call_recording_gaps.rb', line 111 def absent_rate = expected.zero? ? 0.0 : (absent_count.to_f / expected * 100).round(1) |
#coverage_rate ⇒ Float
Share of recordable calls that DO have a recording.
103 |
# File 'app/services/report/call_recording_gaps.rb', line 103 def coverage_rate = expected.zero? ? 100.0 : (matched.to_f / expected * 100).round(1) |
#expected ⇒ Integer
Recordable TALKING legs we expected to have a recording.
74 |
# File 'app/services/report/call_recording_gaps.rb', line 74 def expected = data[:expected] |
#gap_count ⇒ Integer
85 |
# File 'app/services/report/call_recording_gaps.rb', line 85 def gap_count = gaps.size |
#gap_rate ⇒ Float
Share of recordable calls that are missing a recording.
107 |
# File 'app/services/report/call_recording_gaps.rb', line 107 def gap_rate = expected.zero? ? 0.0 : (gap_count.to_f / expected * 100).round(1) |
#gaps ⇒ Array<Gap>
...the ones that did NOT — the gaps, sorted by time.
82 |
# File 'app/services/report/call_recording_gaps.rb', line 82 def gaps = data[:gaps] |
#gaps_absent ⇒ Array<Gap>
Gaps with no recording anywhere in the archive — genuinely missing audio.
89 |
# File 'app/services/report/call_recording_gaps.rb', line 89 def gaps_absent = gaps.select { |g| g.status == STATUS_ABSENT } |
#gaps_unlinked ⇒ Array<Gap>
Gaps whose audio IS archived but linked to a sibling leg of the same call.
93 |
# File 'app/services/report/call_recording_gaps.rb', line 93 def gaps_unlinked = gaps.select { |g| g.status == STATUS_UNLINKED } |
#matched ⇒ Integer
...of those, the count that linked to a CallRecord.
78 |
# File 'app/services/report/call_recording_gaps.rb', line 78 def matched = data[:matched] |
#to_csv ⇒ String
CSV of the gaps for the email attachment.
115 116 117 118 119 120 121 122 123 124 |
# File 'app/services/report/call_recording_gaps.rb', line 115 def to_csv require 'csv' CSV.generate do |csv| csv << %w[occurred_at extension from to direction duration cdr_call_id status] gaps.each do |g| csv << [g.occurred_at&.to_fs(:db), g.extension, g.from, g.to, g.direction, g.duration, g.cdr_call_id, g.status] end end end |
#unlinked_count ⇒ Integer
99 |
# File 'app/services/report/call_recording_gaps.rb', line 99 def unlinked_count = gaps_unlinked.size |