Class: Tracebook::DailyRollupsJob

Inherits:
ApplicationJob show all
Defined in:
app/jobs/tracebook/daily_rollups_job.rb

Overview

Background job for aggregating daily metrics.

Summarizes interactions by date/provider/model/project into RollupDaily records for analytics and cost reporting. Should be scheduled nightly for each active provider/model combination.

Aggregated Metrics

  • Total interaction count
  • Success/error counts
  • Input/output token sums
  • Total cost in cents

Examples:

Schedule with Sidekiq Cron

Sidekiq::Cron::Job.create(
  name: "TraceBook OpenAI rollups",
  cron: "0 2 * * *",
  class: "Tracebook::DailyRollupsJob",
  kwargs: { date: Date.yesterday, provider: "openai", model: nil, project: nil }
)

Run manually for specific date/model

DailyRollupsJob.perform_now(
  date: Date.yesterday,
  provider: "openai",
  model: "gpt-4o",
  project: "support"
)

See Also:

Instance Method Summary collapse

Instance Method Details

#determine_currency(scope) ⇒ Object (private)



92
93
94
95
96
# File 'app/jobs/tracebook/daily_rollups_job.rb', line 92

def determine_currency(scope)
  scope.pick(:currency)
rescue NoMethodError
  scope.first&.currency
end

#integer_string?(value) ⇒ Boolean (private)

Returns:



88
89
90
# File 'app/jobs/tracebook/daily_rollups_job.rb', line 88

def integer_string?(value)
  value.match?(/\A-?\d+\z/)
end

#normalize_status_counts(counts) ⇒ Object (private)



72
73
74
75
76
77
78
79
# File 'app/jobs/tracebook/daily_rollups_job.rb', line 72

def normalize_status_counts(counts)
  counts.each_with_object(Hash.new(0)) do |(raw_key, count), normalized|
    status_name = status_name_for(raw_key)
    next unless status_name

    normalized[status_name] += count
  end
end

#perform(date:, provider:, model:, project: nil) ⇒ void

This method returns an undefined value.

Aggregates metrics for a specific date/provider/model/project.

Creates or updates a RollupDaily record with summarized statistics.

Parameters:

  • Date to aggregate (usually Date.yesterday)

  • Provider name (e.g., "openai")

  • Model identifier (nil for all models)

  • (defaults to: nil)

    Project name (nil for all projects)

Raises:

  • if rollup fails validation



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'app/jobs/tracebook/daily_rollups_job.rb', line 46

def perform(date:, provider:, model:, project: nil)
  scope = Interaction.where(provider: provider, model: model)
  scope = scope.where(project: project) if project
  scope = scope.where(created_at: date.beginning_of_day..date.end_of_day)

  counts = normalize_status_counts(scope.group(:status).count)
  tokens = scope.pluck(Arel.sql("COALESCE(input_tokens, 0)"), Arel.sql("COALESCE(output_tokens, 0)"))
  costs = scope.pluck(Arel.sql("COALESCE(cost_total_cents, 0)"))

  input_sum = tokens.sum { |(input, _)| input.to_i }
  output_sum = tokens.sum { |(_, output)| output.to_i }
  cost_sum = costs.sum(&:to_i)

  rollup = RollupDaily.find_or_initialize_by(date: date, provider: provider, model: model, project: project)
  rollup.interactions_count = scope.count
  rollup.success_count = counts.fetch("success", 0)
  rollup.error_count = counts.fetch("error", 0)
  rollup.input_tokens_sum = input_sum
  rollup.output_tokens_sum = output_sum
  rollup.cost_cents_sum = cost_sum
  rollup.currency = determine_currency(scope) || rollup.currency
  rollup.save!
end

#status_name_for(raw_key) ⇒ Object (private)



81
82
83
84
85
86
# File 'app/jobs/tracebook/daily_rollups_job.rb', line 81

def status_name_for(raw_key)
  key_string = raw_key.to_s
  return key_string if Interaction.statuses.key?(key_string)

  integer_string?(key_string) ? Interaction.statuses.invert[key_string.to_i] : nil
end