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 and more than 8 seconds
of talk time (MIN_RECORDABLE_SECONDS) — shorter answered-and-dropped connects
carry no usable audio — that Phone::CallLogEventToRecordMatcher could not link
to a CallRecord (so call_record_id stays nil). The several CDR entries
Switchvox logs for one physical call are collapsed first
(see #collapse_duplicate_calls) so a phantom twin isn't counted as its own gap.
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 =
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 =
Short connects (≤8s 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. A leg must talk for MORE than 8 seconds to be
recordable; theduration_secs < MIN_RECORDABLE_SECONDStest below therefore
skips everything at or under 8s. 9
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.
70 71 72 73 74 75 |
# File 'app/services/report/call_recording_gaps.rb', line 70 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.
65 66 67 |
# File 'app/services/report/call_recording_gaps.rb', line 65 def date @date end |
#window_end ⇒ ActiveSupport::TimeWithZone (readonly)
67 68 69 |
# File 'app/services/report/call_recording_gaps.rb', line 67 def window_end @window_end end |
#window_start ⇒ ActiveSupport::TimeWithZone (readonly)
67 68 69 |
# File 'app/services/report/call_recording_gaps.rb', line 67 def window_start @window_start end |
Class Method Details
.for_scheduled_run ⇒ Report::CallRecordingGaps
Previous calendar day in America/Chicago — the scheduled default.
62 |
# File 'app/services/report/call_recording_gaps.rb', line 62 def self.for_scheduled_run = new(date: Time.find_zone(TZ).now.to_date - 1) |
Instance Method Details
#absent_count ⇒ Integer
101 |
# File 'app/services/report/call_recording_gaps.rb', line 101 def absent_count = gaps_absent.size |
#absent_rate ⇒ Float
Share of recordable calls with NO archived audio — the genuine-loss rate.
116 |
# File 'app/services/report/call_recording_gaps.rb', line 116 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.
108 |
# File 'app/services/report/call_recording_gaps.rb', line 108 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.
79 |
# File 'app/services/report/call_recording_gaps.rb', line 79 def expected = data[:expected] |
#gap_count ⇒ Integer
90 |
# File 'app/services/report/call_recording_gaps.rb', line 90 def gap_count = gaps.size |
#gap_rate ⇒ Float
Share of recordable calls that are missing a recording.
112 |
# File 'app/services/report/call_recording_gaps.rb', line 112 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.
87 |
# File 'app/services/report/call_recording_gaps.rb', line 87 def gaps = data[:gaps] |
#gaps_absent ⇒ Array<Gap>
Gaps with no recording anywhere in the archive — genuinely missing audio.
94 |
# File 'app/services/report/call_recording_gaps.rb', line 94 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.
98 |
# File 'app/services/report/call_recording_gaps.rb', line 98 def gaps_unlinked = gaps.select { |g| g.status == STATUS_UNLINKED } |
#matched ⇒ Integer
...of those, the count that linked to a CallRecord.
83 |
# File 'app/services/report/call_recording_gaps.rb', line 83 def matched = data[:matched] |
#to_csv ⇒ String
CSV of the gaps for the email attachment.
120 121 122 123 124 125 126 127 128 129 |
# File 'app/services/report/call_recording_gaps.rb', line 120 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
104 |
# File 'app/services/report/call_recording_gaps.rb', line 104 def unlinked_count = gaps_unlinked.size |