Class: RailsPulse::Operation

Inherits:
ApplicationRecord show all
Defined in:
app/models/rails_pulse/operation.rb

Constant Summary collapse

OPERATION_TYPES =
%w[
  sql
  controller
  template
  partial
  layout
  collection
  cache_read
  cache_write
  http
  job
  mailer
  storage
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.persist_bulk(ops, context) ⇒ Object

Bulk-insert a batch of operation hashes captured during a request or job run. ‘context` is merged into every row to bind it to its parent — either `{ request_id: id }` or `{ job_run_id: id, request_id: nil }`.

SQL operations are handled in two phases to minimise DB round-trips:

1. Normalise each unique SQL source once and collect hashed→normalised pairs.
2. Resolve (or create) the corresponding Query records in bulk rather than
   one create_or_find_by per operation, which matters for N+1-heavy requests.

insert_all! is used instead of individual creates to bypass ActiveRecord callbacks and validations — normalisation and truncation are done inline above. All rows must have the same key set, so keys are union-normalised before insert.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'app/models/rails_pulse/operation.rb', line 52

def self.persist_bulk(ops, context)
  return if ops.empty?

  # Phase 1: normalise each unique SQL source once.
  # norm_cache avoids re-normalising repeated SQL (e.g. N+1 queries).
  # norm_map feeds the bulk Query resolver: { hashed_sql => normalized_sql }.
  norm_cache = {}
  norm_map = {}
  ops.each do |op|
    next unless op[:operation_type] == "sql"
    sql_source = op[:actual_sql].presence || op[:label].presence
    next unless sql_source
    unless norm_cache.key?(sql_source)
      normalized = RailsPulse::SqlQueryNormalizer.normalize(sql_source)
      hashed = Digest::MD5.hexdigest(normalized)
      norm_cache[sql_source] = [ hashed, normalized ]
      norm_map[hashed] ||= normalized
    end
  end

  # Phase 2: resolve Query IDs in bulk (1 SELECT in steady state).
  query_id_map = RailsPulse::Query.bulk_find_or_create(norm_map)

  now = Time.current
  rows = ops.map do |op|
    op = op.merge(context).merge(created_at: now, updated_at: now)
    if op[:operation_type] == "sql"
      sql_source = op[:actual_sql].presence || op[:label].presence
      if sql_source && (meta = norm_cache[sql_source])
        hashed, normalized = meta
        op = op.merge(label: normalized.truncate(255), query_id: query_id_map[hashed])
      end
    end
    op[:label] = op[:label]&.truncate(255)
    op
  end
  # insert_all! requires every row to have identical keys.
  all_keys = rows.flat_map(&:keys).uniq
  rows = rows.map { |r| all_keys.each_with_object({}) { |k, h| h[k] = r[k] } }
  insert_all!(rows)
end

.ransackable_associations(auth_object = nil) ⇒ Object



98
99
100
# File 'app/models/rails_pulse/operation.rb', line 98

def self.ransackable_associations(auth_object = nil)
  %w[]
end

.ransackable_attributes(auth_object = nil) ⇒ Object



94
95
96
# File 'app/models/rails_pulse/operation.rb', line 94

def self.ransackable_attributes(auth_object = nil)
  %w[id occurred_at label duration start_time average_query_time_ms query_count operation_type query_id]
end

Instance Method Details

#to_breadcrumbObject



127
128
129
# File 'app/models/rails_pulse/operation.rb', line 127

def to_breadcrumb
  label.to_s.truncate(60)
end

#to_sObject



131
132
133
# File 'app/models/rails_pulse/operation.rb', line 131

def to_s
  id
end