Class: Report::DailyImportSummary

Inherits:
Object
  • Object
show all
Defined in:
app/services/report/daily_import_summary.rb

Overview

Daily operational digest of the call-recording import pipeline for the prior
day, emailed to heatwaveteam@ each morning as an "is something amiss" canary:

  • how many CallRecords were imported (Switchvox vs Twilio), and how many
    were voicemails;
  • the transcription outcomes for those imports (completed / error / skipped /
    still in-flight) + a success rate;
  • the CURRENT SFTP/R2 file state — recordings waiting to import and recordings
    parked in errors/ — both of which should sit at ~0 when healthy.

Anything non-zero in the file-state or transcription-error lines (or zero
imports on a normal day) is surfaced in #issues. Mirrors
CallRecordingGaps: for_scheduled_run + an America/Chicago day window

  • memoized data the mailer view reads.

Constant Summary collapse

TZ =
'America/Chicago'
CALL_SOURCES =

CallRecord#recording_source is set EXPLICITLY by the importers (the model's
historical "NULL = switchvox" note is stale). Call recordings come from the
SFTP/R2 importer (switchvox) and the Twilio API (twilio); voicemail
recordings arrive separately via the VoicemailsMailbox / Switchvox webhook
(switchvox_voicemail, pbx_voicemail).

%w[switchvox twilio].freeze
VOICEMAIL_SOURCES =
%w[switchvox_voicemail pbx_voicemail].freeze
PENDING_BACKLOG =

The importer runs hourly (6am-7pm CT), so a handful of recordings sitting at
the ingest root is NORMAL — late-evening uploads waiting for the 6am cycle, or
anything uploaded since the last run. Only flag the root as a problem when it's
a real backlog (many files at once) or a recording has sat unimported for a
full day (it has missed every cycle = genuinely stuck).

25
PENDING_STUCK_AFTER =
1.day

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(date:) ⇒ DailyImportSummary

Returns a new instance of DailyImportSummary.

Parameters:

  • date (Date)

    the calendar day (America/Chicago) to summarize



46
47
48
49
50
51
# File 'app/services/report/daily_import_summary.rb', line 46

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

#dateDate (readonly)

Returns the day being summarized.

Returns:

  • (Date)

    the day being summarized



41
42
43
# File 'app/services/report/daily_import_summary.rb', line 41

def date
  @date
end

#window_endActiveSupport::TimeWithZone (readonly)

Returns:

  • (ActiveSupport::TimeWithZone)


43
44
45
# File 'app/services/report/daily_import_summary.rb', line 43

def window_end
  @window_end
end

#window_startActiveSupport::TimeWithZone (readonly)

Returns:

  • (ActiveSupport::TimeWithZone)


43
44
45
# File 'app/services/report/daily_import_summary.rb', line 43

def window_start
  @window_start
end

Class Method Details

.for_scheduled_runReport::DailyImportSummary

Previous calendar day in America/Chicago — the scheduled default.



38
# File 'app/services/report/daily_import_summary.rb', line 38

def self.for_scheduled_run = new(date: Time.find_zone(TZ).now.to_date - 1)

Instance Method Details

#all_clear?Boolean

Returns:

  • (Boolean)


152
# File 'app/services/report/daily_import_summary.rb', line 152

def all_clear? = issues.empty?

#business_day?Boolean

Returns:

  • (Boolean)


161
# File 'app/services/report/daily_import_summary.rb', line 161

def business_day? = date.on_weekday?

#failed_filesInteger?

Recordings the importer parked in errors/ (failed import). Healthy ≈ 0.

Returns:

  • (Integer, nil)


131
# File 'app/services/report/daily_import_summary.rb', line 131

def failed_files = file_state[:failed]

#file_state_errorString?

Returns populated when the R2 listing raised.

Returns:

  • (String, nil)

    populated when the R2 listing raised



134
# File 'app/services/report/daily_import_summary.rb', line 134

def file_state_error = file_state[:error]

#imported_callsInteger

Switchvox + Twilio CALL recordings.

Returns:

  • (Integer)


62
# File 'app/services/report/daily_import_summary.rb', line 62

def imported_calls = scope.where(recording_source: CALL_SOURCES).count

#imported_switchvoxInteger

Returns:

  • (Integer)


65
# File 'app/services/report/daily_import_summary.rb', line 65

def imported_switchvox = scope.where(recording_source: 'switchvox').count

#imported_totalInteger

Every CallRecord created in the window (call recordings + voicemails + any
other source).

Returns:

  • (Integer)


58
# File 'app/services/report/daily_import_summary.rb', line 58

def imported_total = scope.count

#imported_twilioInteger

Returns:

  • (Integer)


68
# File 'app/services/report/daily_import_summary.rb', line 68

def imported_twilio = scope.where(recording_source: 'twilio').count

#issuesArray<String>

Human-readable problems worth a look; empty when all healthy.

Returns:

  • (Array<String>)


140
141
142
143
144
145
146
147
148
149
# File 'app/services/report/daily_import_summary.rb', line 140

def issues
  [].tap do |list|
    list << "#{pending_files} recording(s) waiting to import — possible importer backlog or stall" if pending_backlog?
    list << "#{failed_files} recording(s) parked in errors/ — failed import" if failed_files.to_i.positive?
    list << "#{transcription_errors} transcription failure(s)" if transcription_errors.positive?
    list << 'no call recordings imported on a business day — the import pipeline may be down' if imported_calls.zero? && business_day?
    list << "#{other_imports} record(s) with an unrecognised recording_source" if other_imports.positive?
    list << "SFTP/R2 file-state check failed: #{file_state_error}" if file_state_error
  end
end

#other_importsInteger

Anything created in the window that isn't a recognised call/voicemail source —
non-zero here means a new source string slipped in unnoticed.

Returns:

  • (Integer)


77
# File 'app/services/report/daily_import_summary.rb', line 77

def other_imports = imported_total - imported_calls - voicemails

#pending_backlog?Boolean

True only when the ingest root looks genuinely backed up — many files at once,
or a recording stuck for a full day. A few stragglers awaiting the next import
cycle do NOT trip this (that's the normal hourly-importer state).

Returns:

  • (Boolean)


122
123
124
125
126
127
# File 'app/services/report/daily_import_summary.rb', line 122

def pending_backlog?
  return false if pending_files.nil?

  pending_files >= PENDING_BACKLOG ||
    (pending_oldest.present? && pending_oldest < Time.current - PENDING_STUCK_AFTER)
end

#pending_filesInteger?

Recordings uploaded by the PBX but not yet imported (delimiter-scoped to the
ingest root, so it never scans the processed/ archive). Healthy ≈ 0.

Returns:



112
# File 'app/services/report/daily_import_summary.rb', line 112

def pending_files = file_state[:pending]

#pending_oldestTime?

The oldest pending recording's upload time, or nil when none / unknown.

Returns:

  • (Time, nil)


116
# File 'app/services/report/daily_import_summary.rb', line 116

def pending_oldest = file_state[:pending_oldest]

#transcribed_okInteger

Returns:

  • (Integer)


87
# File 'app/services/report/daily_import_summary.rb', line 87

def transcribed_ok = transcription_breakdown['completed'].to_i

#transcription_breakdownHash{String=>Integer}

Returns transcription_state => count.

Returns:

  • (Hash{String=>Integer})

    transcription_state => count



82
83
84
# File 'app/services/report/daily_import_summary.rb', line 82

def transcription_breakdown
  @transcription_breakdown ||= scope.group(:transcription_state).count.transform_keys(&:to_s)
end

#transcription_errorsInteger

Returns:

  • (Integer)


90
# File 'app/services/report/daily_import_summary.rb', line 90

def transcription_errors = transcription_breakdown['error'].to_i

#transcription_in_flightInteger

Still pending/processing at report time (transcription is async).

Returns:

  • (Integer)


98
# File 'app/services/report/daily_import_summary.rb', line 98

def transcription_in_flight = transcription_breakdown.values_at('pending', 'processing').compact.sum

#transcription_skippedInteger

no_audio + too_short — deliberately not transcribed.

Returns:

  • (Integer)


94
# File 'app/services/report/daily_import_summary.rb', line 94

def transcription_skipped = transcription_breakdown.values_at('no_audio', 'too_short').compact.sum

#transcription_success_rateFloat?

completed / (completed + error). Nil when nothing reached a transcribe verdict.

Returns:

  • (Float, nil)

    percentage 0.0–100.0



102
103
104
105
# File 'app/services/report/daily_import_summary.rb', line 102

def transcription_success_rate
  decided = transcribed_ok + transcription_errors
  decided.zero? ? nil : (transcribed_ok.to_f / decided * 100).round(1)
end

#voicemailsInteger

Voicemail recordings (switchvox_voicemail + pbx_voicemail).

Returns:

  • (Integer)


72
# File 'app/services/report/daily_import_summary.rb', line 72

def voicemails = scope.where(recording_source: VOICEMAIL_SOURCES).count

#weekend?Boolean

WarmlyYours has no Sunday call volume and only a trickle on Saturdays, so zero
call imports on a weekend is EXPECTED. The "pipeline down" alarm is gated to
business days; the view shows a reassuring note on a quiet weekend instead.

Returns:

  • (Boolean)


158
# File 'app/services/report/daily_import_summary.rb', line 158

def weekend? = date.on_weekend?