Class: ForestAdminAgent::AuditTrail::ActionCapture

Inherits:
Object
  • Object
show all
Includes:
Recording
Defined in:
lib/forest_admin_agent/audit_trail/action_capture.rb

Overview

Records smart-action runs into the same table as the field-level history: the submitted form on the previous_values side, what the action answered on the new_values side.

Capture cannot see them — the customizer has no Execute hook — and an action's writes are only audited when they go through the Forest data layer, so a direct ORM write stays invisible. What lands here is the run itself: who ran which action, on which records, with which form, and how it ended.

Same protocol as a write: #pending before the action, #confirm after. The route owns the gate, so a pending insert that fails refuses the run under critical: true.

Constant Summary collapse

EXECUTED =
'action'.freeze
FAILED =
'action_failed'.freeze
NO_RECORD =

A global action, and a bulk run over a selection wider than the cap, name no single target: they get one row attached to no record rather than none at all.

''.freeze
RESULT_FIELDS =

What of the action's answer is worth keeping — an allowlist, not a denylist: a result also carries the file's contents, a webhook's body and headers and arbitrary response headers. File bytes have no business in an audit table and the other two routinely hold credentials, and an allowlist means a field added to a result later is not stored until someone decides it should be. html is left out too: operator-facing markup, sometimes large, and the message already says what happened.

%i[type message name mime_type method url path].freeze
URL_FIELDS =

Either can carry userinfo credentials or a signed one-time token, which would then sit permanently in the one table nobody deletes from.

%i[url path].freeze

Constants included from Recording

Recording::DONE, Recording::IDENTITY, Recording::PENDING, Recording::REDACTED

Instance Method Summary collapse

Methods included from Recording

#audit_safely, #correlation_key_for, #identity_of, #now, #redact

Constructor Details

#initialize(store, redact = {}) ⇒ ActionCapture

Returns a new instance of ActionCapture.



32
33
34
35
# File 'lib/forest_admin_agent/audit_trail/action_capture.rb', line 32

def initialize(store, redact = {})
  @store = store
  @redact = redact || {}
end

Instance Method Details

#confirm(ids, result: nil, failed: false) ⇒ Object

Best-effort: the action has already run, so a failure here loses the answer, never the run.



60
61
62
63
64
65
66
67
68
# File 'lib/forest_admin_agent/audit_trail/action_capture.rb', line 60

def confirm(ids, result: nil, failed: false)
  return if ids.nil? || ids.empty?

  audit_safely do
    answer = summarize(result)

    ids.each { |id| @store.confirm(id, operation: failed ? FAILED : EXECUTED, new_values: answer) }
  end
end

#pending(caller:, collection:, action_name:, form_values:, record_ids:) ⇒ Object

One row per targeted record, provisionally an action#confirm settles which it really was. Returns the row ids to confirm.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/forest_admin_agent/audit_trail/action_capture.rb', line 39

def pending(caller:, collection:, action_name:, form_values:, record_ids:)
  return [] unless @store

  timestamp = now
  correlation_key = correlation_key_for(caller)
  identity = identity_of(caller)
   = redact(form_values || {}, @redact[collection] || [])
  ids = record_ids.empty? ? [NO_RECORD] : record_ids

  @store.append_all(
    ids.map do |record_id|
      AuditRecord.new(
        timestamp: timestamp, operation: EXECUTED, collection: collection, record_id: record_id,
        status: PENDING, action_name: action_name, correlation_key: correlation_key,
        previous_values: , new_values: {}, **identity
      )
    end
  )
end