easyop 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c649ea27731299fcf70421b41e0a19cca45612aa621310bf1bb87a70f5100da6
4
- data.tar.gz: 7589af6ce926df84ccafb1df8b705430aaabc82b0929a0b9d8ed9bb4b7db818d
3
+ metadata.gz: 5fda2bb2b17350fc5da2939e9bc4ec4bd816c2e9828f440adcf6750555f02e06
4
+ data.tar.gz: af2eb053c15796a702dc01b6a44ebacbd15ec644ff5dce1e7fd97734e9e35199
5
5
  SHA512:
6
- metadata.gz: fc1a7e1c0427da2abb6588c61ece675431708b3b2a2bb623e62356ebfb6edf69025f6c13004ef6967d62f9381cb6c003793958f1c98326c34ba6266a9a56f774
7
- data.tar.gz: d40f2a49035f449f95bfcefaae66dacc6c4d2e015d03d66c88f5f74e1680f8d7713d3f079ece4011d77ea238fc6181cf7072fbfe651a4be9be0876e5c8839eb2
6
+ metadata.gz: a9461b76dfd41ad12bf5a14b5e2e0a1f59a073402a23a5f89d3b5f773314c63b290e8d15e946394c013f73717a97ba51df717e861d6e9d67cb3109c243e32e25
7
+ data.tar.gz: 5d1387e43e468478267f29bca65289652a092b1727355f48b71604863daf35933d0199bb810729ac07abdf57deaf0e264febab3e0e3fca6bd3a2ed71584335be
data/CHANGELOG.md CHANGED
@@ -7,42 +7,133 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.7] — 2026-04-15
11
+
12
+ ### Added
13
+
14
+ - **`execution_index` for `Easyop::Plugins::Recording`** — an optional `:integer` column that records the 1-based call order of each child operation within its parent. Root operations store `nil`. Each child's counter resets independently under its own parent, so siblings of different parents both start at `1`. Add via migration:
15
+
16
+ ```ruby
17
+ add_column :operation_logs, :execution_index, :integer
18
+ add_index :operation_logs, [:parent_reference_id, :execution_index],
19
+ name: 'index_operation_logs_on_parent_ref_and_exec_index'
20
+ ```
21
+
22
+ Example tree:
23
+ ```
24
+ FullCheckout (execution_index: nil — root)
25
+ ValidateCart (execution_index: 1)
26
+ ApplyDiscount (execution_index: 2)
27
+ LookupCode (execution_index: 1) ← resets under new parent
28
+ DeductAmt (execution_index: 2)
29
+ CreateOrder (execution_index: 3)
30
+ ```
31
+
32
+ The column is fully optional and backward-compatible — missing columns are silently skipped.
33
+
34
+ ## [0.1.6] — 2026-04-15
35
+
36
+ ### Added
37
+
38
+ - **`record_result: true` form for `Easyop::Plugins::Recording`** — passing `true` at install time (or via the class-level `record_result true` DSL) now records the full ctx snapshot after the operation completes. `FILTERED_KEYS` are applied and `INTERNAL_CTX_KEYS` are excluded. The `true` form captures computed values set during `#call`, making it a full output snapshot.
39
+
40
+ - **`record_params` class-level DSL** — parallel to `record_result`, operations can now declare params recording behaviour per-class:
41
+
42
+ ```ruby
43
+ record_params false # disable params recording entirely
44
+ record_params attrs: %i[email name] # selective keys only
45
+ record_params { |ctx| { id: ctx.user_id } } # block form
46
+ record_params :build_safe_params # private method form
47
+ record_params true # explicit full ctx (default)
48
+ ```
49
+
50
+ Inherited through the class hierarchy; subclasses can override.
51
+
52
+ - **`record_params:` install option extended** — the `plugin Easyop::Plugins::Recording, record_params:` option now accepts the same forms as the DSL: `false`, `true`, `{ attrs: }`, `Proc`, `Symbol`.
53
+
54
+ - **Input-only `params_data` by default** — when `record_params` is `true` (the default), `params_data` now records only the keys that were present in `ctx` **before** the operation body ran. Computed values written during `#call` are excluded from `params_data`. Custom forms (attrs/block/symbol) are user-controlled and evaluated after the call, so they can access computed values.
55
+
56
+ ### Changed
57
+
58
+ - **`record_result` default is now `false`** — previously defaulted to `nil` (falsy). Explicit `false` makes intent clear and prevents accidental result logging.
59
+
10
60
  ## [0.1.5] — 2026-04-14
11
61
 
12
62
  ### Added
13
63
 
14
- - **`scrub_params` DSL for `Easyop::Plugins::Recording`** — declare additional params keys/patterns to scrub from `params_data` on a per-class basis. Accepts `Symbol`, `String`, or `Regexp`. Additive with `SCRUBBED_KEYS` and never replaces the built-in list. Inherited by subclasses; any level of the hierarchy can override.
64
+ - **`filter_params` DSL for `Easyop::Plugins::Recording`** — declare additional params keys/patterns to filter in `params_data` on a per-class basis. Matched keys are kept but their value is replaced with `"[FILTERED]"`. Accepts `Symbol`, `String`, or `Regexp`. Additive with `FILTERED_KEYS` and never replaces the built-in list. Inherited by subclasses; any level of the hierarchy can override.
15
65
 
16
66
  ```ruby
17
67
  class ApplicationOperation < ...
18
- scrub_params :api_token, /access.?key/i
68
+ filter_params :api_token, /access.?key/i
19
69
  end
20
70
 
21
71
  class Orders::CreateOrder < ApplicationOperation
22
- scrub_params :card_number # stacks on top of parent's scrub list
72
+ filter_params :card_number # stacks on top of parent's filter list
23
73
  end
24
74
  ```
25
75
 
26
- - **`scrub_keys:` option on `plugin Easyop::Plugins::Recording`** — supply a list of extra keys/patterns to scrub at plugin install time. Inherited by all classes that share the same `plugin` declaration.
76
+ - **`filter_keys:` option on `plugin Easyop::Plugins::Recording`** — supply a list of extra keys/patterns to filter at plugin install time. Inherited by all classes that share the same `plugin` declaration.
27
77
 
28
78
  ```ruby
29
79
  plugin Easyop::Plugins::Recording,
30
80
  model: OperationLog,
31
- scrub_keys: [:stripe_token, /secret/i]
81
+ filter_keys: [:stripe_token, /secret/i]
32
82
  ```
33
83
 
34
- - **`Easyop::Configuration#recording_scrub_keys`** — new global config key. Set once at boot and every recorded operation will scrub these keys in addition to `SCRUBBED_KEYS` and any class-level declarations. Accepts `Symbol`, `String`, or `Regexp`.
84
+ - **`Easyop::Configuration#recording_filter_keys`** — new global config key. Set once at boot and every recorded operation will filter these keys in addition to `FILTERED_KEYS` and any class-level declarations. Accepts `Symbol`, `String`, or `Regexp`.
35
85
 
36
86
  ```ruby
37
87
  Easyop.configure do |c|
38
- c.recording_scrub_keys = [:api_token, /token/i]
88
+ c.recording_filter_keys = [:api_token, /token/i]
89
+ end
90
+ ```
91
+
92
+ - **`[FILTERED]` value replacement** — sensitive keys are now kept in `params_data` with their value replaced by `"[FILTERED]"` instead of being removed entirely. This means audit logs show *which* sensitive fields were passed without exposing their values.
93
+
94
+ **Filter precedence (all layers are additive):**
95
+ 1. `FILTERED_KEYS` — always applied (built-in list)
96
+ 2. `Easyop.config.recording_filter_keys` — global config
97
+ 3. `filter_keys:` plugin option + `filter_params` DSL — class hierarchy
98
+
99
+ - **`params_data` records only input keys by default** — `params_data` now snapshots the ctx keys present *before* the operation body runs. Values computed during `#call` (e.g. `ctx.user = User.create!(...)`) are excluded from `params_data`; use `record_result` to capture them. Custom `record_params` forms (attrs, block, symbol) are evaluated *after* the call and can access computed values when explicitly requested. FILTERED_KEYS and INTERNAL_CTX_KEYS are always excluded.
100
+
101
+ - **`record_result: true` form** — passing `true` records the full ctx snapshot (all non-internal keys, FILTERED_KEYS applied). Works at both the plugin install level and the class-level DSL.
102
+
103
+ ```ruby
104
+ # Plugin level — all operations inherit this default
105
+ plugin Easyop::Plugins::Recording, model: OperationLog, record_result: true
106
+
107
+ # Class level DSL
108
+ class Orders::CreateOrder < ApplicationOperation
109
+ record_result true
39
110
  end
40
111
  ```
41
112
 
42
- **Scrub precedence (all layers are additive):**
43
- 1. `SCRUBBED_KEYS` — always applied (built-in list)
44
- 2. `Easyop.config.recording_scrub_keys` — global config
45
- 3. `scrub_keys:` plugin option + `scrub_params` DSL — class hierarchy
113
+ - **`record_params` class-level DSL** parallel to `record_result`, `record_params` can now be declared on any class to control what ends up in `params_data`. Supports four forms:
114
+
115
+ ```ruby
116
+ record_params false # disable params entirely
117
+ record_params true # explicit full ctx (default)
118
+ record_params attrs: :user_id # selective keys
119
+ record_params attrs: [:user_id, :event_id]
120
+ record_params { |ctx| { custom: ctx[:name] } } # block
121
+ record_params :build_params # private method name
122
+ ```
123
+
124
+ FILTERED_KEYS are **always applied** to the extracted hash regardless of form (keys are kept, values replaced with `"[FILTERED]"`). Config is inheritable and overridable per subclass.
125
+
126
+ - **`record_params:` install option extended** — the plugin install-level `record_params:` option now accepts the same forms as the DSL: `Hash` (`{ attrs: }` ), `Proc`, and `Symbol`, in addition to `true`/`false`.
127
+
128
+ ```ruby
129
+ plugin Easyop::Plugins::Recording,
130
+ model: OperationLog,
131
+ record_params: { attrs: %i[user_id plan] }
132
+ ```
133
+
134
+ ### Changed
135
+
136
+ - **`record_result` default is now `false`** (was `nil`). Behaviour is identical — no `result_data` is written unless explicitly configured — but the default value is now consistent with the boolean-style `record_params: true` API convention.
46
137
 
47
138
  ## [0.1.4] — 2026-04-14
48
139
 
@@ -295,7 +386,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
295
386
  - `examples/easyop_test_app/` — full Rails 8 blog application demonstrating all features in real-world code
296
387
  - `examples/usage.rb` — 13 runnable plain-Ruby examples
297
388
 
298
- [Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.5...HEAD
389
+ [Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.7...HEAD
390
+ [0.1.7]: https://github.com/pniemczyk/easyop/compare/v0.1.6...v0.1.7
391
+ [0.1.6]: https://github.com/pniemczyk/easyop/compare/v0.1.5...v0.1.6
299
392
  [0.1.5]: https://github.com/pniemczyk/easyop/compare/v0.1.4...v0.1.5
300
393
  [0.1.4]: https://github.com/pniemczyk/easyop/compare/v0.1.3...v0.1.4
301
394
  [0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
data/README.md CHANGED
@@ -645,9 +645,9 @@ end
645
645
  | Option | Default | Description |
646
646
  |---|---|---|
647
647
  | `model:` | required | ActiveRecord class to write logs into |
648
- | `record_params:` | `true` | Set `false` to skip serializing ctx params |
649
- | `record_result:` | `nil` | Plugin-level default for result capture (Hash/Proc/Symbol see below) |
650
- | `scrub_keys:` | `[]` | Extra keys/patterns to scrub from `params_data` (Symbol, String, Regexp) — additive with built-in list |
648
+ | `record_params:` | `true` | Control params serialization: `false` skips it; `true` uses full ctx; also accepts `{ attrs: }`, `Proc`, or `Symbol` |
649
+ | `record_result:` | `false` | Plugin-level default for result capture: `false` skips; `true` uses full ctx; also accepts `{ attrs: }`, `Proc`, or `Symbol` |
650
+ | `filter_keys:` | `[]` | Extra keys/patterns to filter in `params_data` (Symbol, String, Regexp) — values replaced with `[FILTERED]` |
651
651
 
652
652
  **Required model columns:**
653
653
 
@@ -656,9 +656,12 @@ create_table :operation_logs do |t|
656
656
  t.string :operation_name, null: false
657
657
  t.boolean :success, null: false
658
658
  t.string :error_message
659
- t.text :params_data # JSON — ctx attrs (sensitive keys scrubbed)
659
+ t.text :params_data # JSON — ctx attrs (sensitive keys replaced with [FILTERED])
660
660
  t.float :duration_ms
661
661
  t.datetime :performed_at, null: false
662
+
663
+ # Optional — add when using the record_result DSL to capture output data:
664
+ t.text :result_data # JSON — selected ctx keys after the operation runs
662
665
  end
663
666
  ```
664
667
 
@@ -697,28 +700,65 @@ root_log = OperationLog.find_by(operation_name: "FullCheckout", parent_reference
697
700
  OperationLog.for_tree(root_log.root_reference_id)
698
701
  ```
699
702
 
700
- **Scrubbing params** — all layers are additive (none replaces the built-in list):
703
+ **Filtering params** — sensitive keys are kept in `params_data` but their value is replaced with `"[FILTERED]"`, so the audit log shows which fields were passed without exposing their values. All layers are additive:
701
704
 
702
- 1. **Built-in `SCRUBBED_KEYS`** — always applied: `:password`, `:password_confirmation`, `:token`, `:secret`, `:api_key`
705
+ 1. **Built-in `FILTERED_KEYS`** — always applied: `:password`, `:password_confirmation`, `:token`, `:secret`, `:api_key`
703
706
  2. **Global config** — applied to every recorded operation:
704
707
  ```ruby
705
- Easyop.configure { |c| c.recording_scrub_keys = [:api_token, /token/i] }
708
+ Easyop.configure { |c| c.recording_filter_keys = [:api_token, /token/i] }
706
709
  ```
707
- 3. **Plugin `scrub_keys:` option** — applied to all subclasses that share the plugin install:
710
+ 3. **Plugin `filter_keys:` option** — applied to all subclasses that share the plugin install:
708
711
  ```ruby
709
- plugin Easyop::Plugins::Recording, model: OperationLog, scrub_keys: [:stripe_secret]
712
+ plugin Easyop::Plugins::Recording, model: OperationLog, filter_keys: [:stripe_secret]
710
713
  ```
711
- 4. **`scrub_params` DSL** — per-class, inheritable, and stackable at any level of the hierarchy:
714
+ 4. **`filter_params` DSL** — per-class, inheritable, and stackable at any level of the hierarchy:
712
715
  ```ruby
713
716
  class ApplicationOperation < ...
714
- scrub_params :internal_token, /access.?key/i
717
+ filter_params :internal_token, /access.?key/i
715
718
  end
716
719
  class Payments::ChargeCard < ApplicationOperation
717
- scrub_params :card_number # stacks on top of parent's list
720
+ filter_params :card_number # stacks on top of parent's list
718
721
  end
719
722
  ```
720
723
 
721
- Internal tracing keys (`__recording_*`) are always excluded. ActiveRecord objects are serialized as `{ id:, class: }` rather than their full representation.
724
+ Internal tracing keys (`__recording_*`) are always fully removed. ActiveRecord objects are serialized as `{ id:, class: }` rather than their full representation.
725
+
726
+ **`record_params` DSL — control what goes into `params_data`:**
727
+
728
+ By default, `params_data` records only the keys that were **present in ctx when the operation was called** (inputs), not values computed during the call body. This means `ctx.user = User.create!(...)` set during `#call` will not appear in `params_data` — use `record_result` to capture output values. FILTERED_KEYS and INTERNAL_CTX_KEYS are always excluded.
729
+
730
+ Use the `record_params` DSL to further customize or suppress params recording:
731
+
732
+ ```ruby
733
+ # Disable entirely — no params_data written
734
+ class Users::Authenticate < ApplicationOperation
735
+ record_params false
736
+ end
737
+
738
+ # Selective keys — only these ctx attrs are recorded
739
+ class Tickets::GenerateTickets < ApplicationOperation
740
+ record_params attrs: %i[event_id seat_count]
741
+ end
742
+
743
+ # Block — full control over the extracted hash
744
+ class Reports::GeneratePdf < ApplicationOperation
745
+ record_params { |ctx| { report_type: ctx.report_type, page_count: ctx.pages } }
746
+ end
747
+
748
+ # Symbol — delegates to a private method on the instance
749
+ class Payments::ChargeCard < ApplicationOperation
750
+ record_params :safe_params
751
+
752
+ private
753
+ def safe_params
754
+ { user_id: ctx.user.id, amount_cents: ctx.amount_cents }
755
+ end
756
+ end
757
+ ```
758
+
759
+ FILTERED_KEYS are **always applied** to the extracted hash regardless of form — including custom attrs, blocks, and symbol methods. Plugin install-level `record_params:` accepts the same forms as the DSL.
760
+
761
+ > **Input vs. output**: The `true` (default) form records only keys present *before* the call body runs — values computed during `#call` (like `ctx.user`) are excluded. Custom forms (attrs, block, symbol) are evaluated *after* the call, so they can access computed values. Use `record_result` to capture outputs alongside inputs.
722
762
 
723
763
  **`record_result` DSL — capture output data:**
724
764
 
@@ -728,9 +768,14 @@ Add an optional `result_data :text` column to persist selected ctx values after
728
768
  add_column :operation_logs, :result_data, :text # stored as JSON
729
769
  ```
730
770
 
731
- Then declare what to record using the `record_result` DSL (three forms):
771
+ Then declare what to record using the `record_result` DSL (four forms):
732
772
 
733
773
  ```ruby
774
+ # True form — full ctx snapshot (FILTERED_KEYS applied, internal keys excluded)
775
+ class Users::Register < ApplicationOperation
776
+ record_result true
777
+ end
778
+
734
779
  # Attrs form — one or more ctx keys
735
780
  class PlaceOrder < ApplicationOperation
736
781
  record_result attrs: :order_id
@@ -761,7 +806,8 @@ Set a plugin-level default inherited by all subclasses:
761
806
 
762
807
  ```ruby
763
808
  plugin Easyop::Plugins::Recording, model: OperationLog,
764
- record_result: { attrs: :metadata }
809
+ record_result: true
810
+ # or: record_result: { attrs: :metadata }
765
811
  # or: record_result: ->(ctx) { { id: ctx.record_id } }
766
812
  # or: record_result: :build_result
767
813
  ```
@@ -15,19 +15,20 @@ module Easyop
15
15
  # Easyop.configure { |c| c.event_bus = :active_support }
16
16
  attr_accessor :event_bus
17
17
 
18
- # Extra keys to scrub from params_data across all recorded operations.
19
- # Appended to Recording::SCRUBBED_KEYS — never replaces the built-in list.
18
+ # Extra keys to filter from params_data across all recorded operations.
19
+ # Appended to Recording::FILTERED_KEYS — never replaces the built-in list.
20
+ # Matched keys are kept in params_data but their value is replaced with "[FILTERED]".
20
21
  # Accepts Symbol, String, or Regexp (matched against the stringified key name).
21
22
  #
22
23
  # @example
23
- # Easyop.configure { |c| c.recording_scrub_keys = [:api_token, /token/i] }
24
- attr_accessor :recording_scrub_keys
24
+ # Easyop.configure { |c| c.recording_filter_keys = [:api_token, /token/i] }
25
+ attr_accessor :recording_filter_keys
25
26
 
26
27
  def initialize
27
- @type_adapter = :native
28
- @strict_types = false
29
- @event_bus = nil # nil = Memory bus (see Easyop::Events::Registry)
30
- @recording_scrub_keys = []
28
+ @type_adapter = :native
29
+ @strict_types = false
30
+ @event_bus = nil # nil = Memory bus (see Easyop::Events::Registry)
31
+ @recording_filter_keys = []
31
32
  end
32
33
  end
33
34
 
@@ -29,6 +29,9 @@ module Easyop
29
29
  # Optional result column:
30
30
  # result_data :text # stored as JSON — selected ctx keys after call
31
31
  #
32
+ # Optional execution order column:
33
+ # execution_index :integer # nil for root; 1-based within each parent
34
+ #
32
35
  # Opt out per operation class:
33
36
  # class MyOp < ApplicationOperation
34
37
  # recording false
@@ -36,28 +39,34 @@ module Easyop
36
39
  #
37
40
  # Options:
38
41
  # model: (required) ActiveRecord class
39
- # record_params: true pass false to skip params serialization
40
- # record_result: nil configure result capture at plugin level (Hash/Proc/Symbol)
41
- # scrub_keys: [] additional keys/patterns to scrub from params_data
42
- # (Symbol, String, or Regexp — additive with SCRUBBED_KEYS)
42
+ # record_params: true pass false to skip params serialization; also accepts
43
+ # Hash ({ attrs: :key }), Proc, or Symbol (method name)
44
+ # record_result: false configure result capture at plugin level; also accepts
45
+ # true (full ctx snapshot), Hash, Proc, or Symbol
46
+ # filter_keys: [] additional keys/patterns to filter from params_data
47
+ # (Symbol, String, or Regexp — additive with FILTERED_KEYS)
48
+ # Filtered keys are kept in params_data but their value
49
+ # is replaced with "[FILTERED]".
43
50
  module Recording
44
- # Sensitive keys always scrubbed from params_data before persisting.
45
- SCRUBBED_KEYS = %i[password password_confirmation token secret api_key].freeze
51
+ # Sensitive keys always filtered in params_data before persisting.
52
+ # Their values are replaced with "[FILTERED]" rather than removed.
53
+ FILTERED_KEYS = %i[password password_confirmation token secret api_key].freeze
46
54
 
47
55
  # Internal ctx keys used for flow tracing — excluded from params_data.
48
56
  INTERNAL_CTX_KEYS = %i[
49
57
  __recording_root_reference_id
50
58
  __recording_parent_operation_name
51
59
  __recording_parent_reference_id
60
+ __recording_child_counts
52
61
  ].freeze
53
62
 
54
- def self.install(base, model:, record_params: true, record_result: nil, scrub_keys: [], **_options)
63
+ def self.install(base, model:, record_params: true, record_result: false, filter_keys: [], **_options)
55
64
  base.extend(ClassMethods)
56
65
  base.prepend(RunWrapper)
57
- base.instance_variable_set(:@_recording_model, model)
66
+ base.instance_variable_set(:@_recording_model, model)
58
67
  base.instance_variable_set(:@_recording_record_params, record_params)
59
68
  base.instance_variable_set(:@_recording_record_result, record_result)
60
- base.instance_variable_set(:@_recording_scrub_keys, Array(scrub_keys))
69
+ base.instance_variable_set(:@_recording_filter_keys, Array(filter_keys))
61
70
  end
62
71
 
63
72
  module ClassMethods
@@ -76,30 +85,48 @@ module Easyop
76
85
  (superclass.respond_to?(:_recording_model) ? superclass._recording_model : nil)
77
86
  end
78
87
 
79
- def _recording_record_params?
88
+ # DSL for controlling params capture. Forms:
89
+ # record_params false # disable params recording entirely
90
+ # record_params true # explicit full ctx (default)
91
+ # record_params attrs: :key # selective keys
92
+ # record_params { |ctx| {...} } # block
93
+ # record_params :build_params # private method name
94
+ def record_params(value = nil, attrs: nil, &block)
95
+ @_recording_record_params = if block
96
+ block
97
+ elsif attrs
98
+ { attrs: attrs }
99
+ elsif !value.nil?
100
+ value
101
+ else
102
+ true
103
+ end
104
+ end
105
+
106
+ def _recording_record_params_config
80
107
  if instance_variable_defined?(:@_recording_record_params)
81
108
  @_recording_record_params
82
- elsif superclass.respond_to?(:_recording_record_params?)
83
- superclass._recording_record_params?
109
+ elsif superclass.respond_to?(:_recording_record_params_config)
110
+ superclass._recording_record_params_config
84
111
  else
85
112
  true
86
113
  end
87
114
  end
88
115
 
89
- # DSL for capturing result data after the operation runs.
90
- # Three forms:
91
- # record_result attrs: :key # one or more ctx keys
92
- # record_result { |ctx| { k: ctx.k } } # block
93
- # record_result :build_result # private instance method name
116
+ # DSL for capturing result data after the operation runs. Forms:
117
+ # record_result true # full ctx snapshot (FILTERED_KEYS applied)
118
+ # record_result attrs: :key # one or more ctx keys
119
+ # record_result { |ctx| {...} } # block
120
+ # record_result :build_result # private instance method name
94
121
  def record_result(value = nil, attrs: nil, &block)
95
122
  @_recording_record_result = if block
96
123
  block
97
124
  elsif attrs
98
125
  { attrs: attrs }
99
- elsif value.is_a?(Symbol)
126
+ elsif !value.nil?
100
127
  value
101
128
  else
102
- value
129
+ nil
103
130
  end
104
131
  end
105
132
 
@@ -109,34 +136,35 @@ module Easyop
109
136
  elsif superclass.respond_to?(:_recording_record_result_config)
110
137
  superclass._recording_record_result_config
111
138
  else
112
- nil
139
+ false
113
140
  end
114
141
  end
115
142
 
116
- # DSL to declare additional keys/patterns to scrub from params_data.
117
- # Accepts Symbol, String, or Regexp. Additive with SCRUBBED_KEYS and
118
- # any scrub_keys declared on parent classes or at the plugin install level.
143
+ # DSL to declare additional keys/patterns to filter in params_data.
144
+ # Accepts Symbol, String, or Regexp. Additive with FILTERED_KEYS and
145
+ # any filter_keys declared on parent classes or at the plugin install level.
146
+ # Matched keys are kept in params_data but their value is replaced with "[FILTERED]".
119
147
  #
120
148
  # @example
121
149
  # class ApplicationOperation < ...
122
- # scrub_params :api_token, /access.?key/i
150
+ # filter_params :api_token, /access.?key/i
123
151
  # end
124
- def scrub_params(*keys)
125
- @_recording_scrub_keys = _own_recording_scrub_keys + keys
152
+ def filter_params(*keys)
153
+ @_recording_filter_keys = _own_recording_filter_keys + keys
126
154
  end
127
155
 
128
- # Returns the merged scrub list: parent class keys + this class's own keys.
129
- # Does NOT include SCRUBBED_KEYS or the global config list — those are
156
+ # Returns the merged filter list: parent class keys + this class's own keys.
157
+ # Does NOT include FILTERED_KEYS or the global config list — those are
130
158
  # merged at persist time so they stay hot-reloadable.
131
- def _recording_scrub_keys
132
- parent = superclass.respond_to?(:_recording_scrub_keys) ? superclass._recording_scrub_keys : []
133
- parent + _own_recording_scrub_keys
159
+ def _recording_filter_keys
160
+ parent = superclass.respond_to?(:_recording_filter_keys) ? superclass._recording_filter_keys : []
161
+ parent + _own_recording_filter_keys
134
162
  end
135
163
 
136
164
  private
137
165
 
138
- def _own_recording_scrub_keys
139
- instance_variable_defined?(:@_recording_scrub_keys) ? @_recording_scrub_keys : []
166
+ def _own_recording_filter_keys
167
+ instance_variable_defined?(:@_recording_filter_keys) ? @_recording_filter_keys : []
140
168
  end
141
169
  end
142
170
 
@@ -146,6 +174,12 @@ module Easyop
146
174
  return super unless (model = self.class._recording_model)
147
175
  return super unless self.class.name # skip anonymous classes
148
176
 
177
+ # Snapshot the keys present in ctx RIGHT NOW — before any internal
178
+ # tracing keys are written and before the operation body runs. This
179
+ # lets _recording_safe_params emit only what was passed IN, not values
180
+ # computed during the call (those belong in result_data).
181
+ input_keys = ctx.to_h.keys
182
+
149
183
  # -- Flow tracing --
150
184
  # Each operation gets its own reference_id. The root_reference_id is
151
185
  # shared across the entire execution tree via ctx (set once, inherited).
@@ -156,6 +190,14 @@ module Easyop
156
190
  parent_operation_name = ctx[:__recording_parent_operation_name]
157
191
  parent_reference_id = ctx[:__recording_parent_reference_id]
158
192
 
193
+ # Execution index: 1-based position among siblings (nil for root operations).
194
+ # Counts are tracked per-parent in a hash stored in ctx so sibling chains
195
+ # and nested sub-trees each maintain independent counters.
196
+ execution_index = if parent_reference_id
197
+ counts = ctx[:__recording_child_counts] ||= {}
198
+ counts[parent_reference_id] = (counts[parent_reference_id] || 0) + 1
199
+ end
200
+
159
201
  # Set THIS operation as the parent for any children that run inside super.
160
202
  # Save the previous values so we can restore them after (for siblings).
161
203
  prev_parent_name = parent_operation_name
@@ -180,7 +222,9 @@ module Easyop
180
222
  root_reference_id: root_reference_id,
181
223
  reference_id: reference_id,
182
224
  parent_operation_name: parent_operation_name,
183
- parent_reference_id: parent_reference_id)
225
+ parent_reference_id: parent_reference_id,
226
+ execution_index: execution_index,
227
+ input_keys: input_keys)
184
228
  end
185
229
  end
186
230
 
@@ -188,7 +232,8 @@ module Easyop
188
232
 
189
233
  def _recording_persist!(ctx, model, duration_ms,
190
234
  root_reference_id: nil, reference_id: nil,
191
- parent_operation_name: nil, parent_reference_id: nil)
235
+ parent_operation_name: nil, parent_reference_id: nil,
236
+ execution_index: nil, input_keys: nil)
192
237
  attrs = {
193
238
  operation_name: self.class.name,
194
239
  success: ctx.success?,
@@ -198,9 +243,11 @@ module Easyop
198
243
  root_reference_id: root_reference_id,
199
244
  reference_id: reference_id,
200
245
  parent_operation_name: parent_operation_name,
201
- parent_reference_id: parent_reference_id
246
+ parent_reference_id: parent_reference_id,
247
+ execution_index: execution_index
202
248
  }
203
- attrs[:params_data] = _recording_safe_params(ctx) if self.class._recording_record_params?
249
+ params_config = self.class._recording_record_params_config
250
+ attrs[:params_data] = _recording_safe_params(ctx, params_config, input_keys) unless params_config == false
204
251
  attrs[:result_data] = _recording_safe_result(ctx) if self.class._recording_record_result_config
205
252
 
206
253
  # Only write columns the model actually has
@@ -210,26 +257,43 @@ module Easyop
210
257
  _recording_warn(e)
211
258
  end
212
259
 
213
- def _recording_safe_params(ctx)
214
- # Merge all scrub layers (additive, never replaces built-ins):
215
- # 1. SCRUBBED_KEYS — built-in sensitive keys, always applied
216
- # 2. Easyop.config — global extra keys set in an initializer
217
- # 3. self.class — plugin install scrub_keys: + class scrub_params DSL
218
- extra = Easyop.config.recording_scrub_keys.to_a + self.class._recording_scrub_keys
219
- ctx.to_h
220
- .except(*INTERNAL_CTX_KEYS)
221
- .reject { |k, _| _recording_scrub_key?(k, extra) }
222
- .transform_values { |v| v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v }
223
- .to_json
260
+ def _recording_safe_params(ctx, config, input_keys = nil)
261
+ raw = case config
262
+ when true
263
+ # Restrict to keys that existed when the operation was invoked —
264
+ # values computed during the call body are excluded here and
265
+ # should be captured via record_result instead.
266
+ # INTERNAL_CTX_KEYS are stripped from input_keys because nested
267
+ # operations inherit parent tracing keys before their snapshot.
268
+ base = ctx.to_h.except(*INTERNAL_CTX_KEYS)
269
+ if input_keys
270
+ clean = input_keys - INTERNAL_CTX_KEYS
271
+ base.slice(*clean)
272
+ else
273
+ base
274
+ end
275
+ when Hash
276
+ keys = Array(config[:attrs])
277
+ keys.each_with_object({}) { |k, h| h[k] = ctx[k] }
278
+ when Proc
279
+ result = config.call(ctx)
280
+ return nil unless result.is_a?(Hash)
281
+ result
282
+ when Symbol
283
+ result = send(config)
284
+ return nil unless result.is_a?(Hash)
285
+ result
286
+ end
287
+ _recording_apply_and_serialize(raw).to_json
224
288
  rescue
225
289
  nil
226
290
  end
227
291
 
228
- # Returns true when +key+ matches any entry in +scrub_list+.
292
+ # Returns true when +key+ matches any entry in +filter_list+.
229
293
  # Regexp entries match against the stringified key name (case-sensitive
230
294
  # unless the Regexp itself uses /i). Symbol/String entries match by value.
231
- def _recording_scrub_key?(key, extra_list)
232
- return true if SCRUBBED_KEYS.include?(key.to_sym)
295
+ def _recording_filter_key?(key, extra_list)
296
+ return true if FILTERED_KEYS.include?(key.to_sym)
233
297
 
234
298
  extra_list.any? do |pattern|
235
299
  case pattern
@@ -245,23 +309,41 @@ module Easyop
245
309
  return nil unless config
246
310
 
247
311
  raw = case config
312
+ when true
313
+ ctx.to_h.except(*INTERNAL_CTX_KEYS)
248
314
  when Hash
249
315
  keys = Array(config[:attrs])
250
316
  keys.each_with_object({}) { |k, h| h[k] = ctx[k] }
251
317
  when Proc
252
- config.call(ctx)
318
+ result = config.call(ctx)
319
+ return nil unless result.is_a?(Hash)
320
+ result
253
321
  when Symbol
254
- send(config)
322
+ result = send(config)
323
+ return nil unless result.is_a?(Hash)
324
+ result
255
325
  end
256
326
 
257
327
  return nil unless raw.is_a?(Hash)
258
-
259
- raw.transform_values { |v| v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v }
260
- .to_json
328
+ _recording_apply_and_serialize(raw).to_json
261
329
  rescue
262
330
  nil
263
331
  end
264
332
 
333
+ # Applies FILTERED_KEYS + class/global filter lists to +hash+, replacing
334
+ # matched values with "[FILTERED]" and serializing ActiveRecord objects.
335
+ def _recording_apply_and_serialize(hash)
336
+ extra = Easyop.config.recording_filter_keys.to_a + self.class._recording_filter_keys
337
+ hash.each_with_object({}) do |(k, v), h|
338
+ h[k] = _recording_filter_key?(k, extra) ? '[FILTERED]' : _recording_serialize_value(v)
339
+ end
340
+ end
341
+
342
+ # Serializes a single value: AR objects become {id:, class:}, everything else passthrough.
343
+ def _recording_serialize_value(v)
344
+ v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v
345
+ end
346
+
265
347
  def _recording_warn(err)
266
348
  return unless defined?(Rails) && Rails.respond_to?(:logger)
267
349
  Rails.logger.warn "[EasyOp::Recording] Failed to record #{self.class.name}: #{err.message}"
@@ -1,3 +1,3 @@
1
1
  module Easyop
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.7"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easyop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pawel Niemczyk