easyop 0.1.4 → 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: 2b880af1b623158d3acfac130198bd96ce8c268195fd073554283738a880a45a
4
- data.tar.gz: 0c4f5554b456ac252b13734d0964155e2a80909e81946e28f89c529cc60f9081
3
+ metadata.gz: 5fda2bb2b17350fc5da2939e9bc4ec4bd816c2e9828f440adcf6750555f02e06
4
+ data.tar.gz: af2eb053c15796a702dc01b6a44ebacbd15ec644ff5dce1e7fd97734e9e35199
5
5
  SHA512:
6
- metadata.gz: c408d6d42a357ba1a6df4da21e13a7b0498d36fa4252e86c68aff289e8843d94d056c430f654f023e6d801ee650fc1bc4c400cb6d058a6b209129e1fbda644fd
7
- data.tar.gz: e89087c84c8fbc77abe96b6713a144dd34b78c2cfd35066f28a14e37516f0e7ae8b0f0cbbcfe0ca9eb9de646c7890552b64d7d2848c9976ff6f51637531a4a8a
6
+ metadata.gz: a9461b76dfd41ad12bf5a14b5e2e0a1f59a073402a23a5f89d3b5f773314c63b290e8d15e946394c013f73717a97ba51df717e861d6e9d67cb3109c243e32e25
7
+ data.tar.gz: 5d1387e43e468478267f29bca65289652a092b1727355f48b71604863daf35933d0199bb810729ac07abdf57deaf0e264febab3e0e3fca6bd3a2ed71584335be
data/CHANGELOG.md CHANGED
@@ -7,6 +7,134 @@ 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
+
60
+ ## [0.1.5] — 2026-04-14
61
+
62
+ ### Added
63
+
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.
65
+
66
+ ```ruby
67
+ class ApplicationOperation < ...
68
+ filter_params :api_token, /access.?key/i
69
+ end
70
+
71
+ class Orders::CreateOrder < ApplicationOperation
72
+ filter_params :card_number # stacks on top of parent's filter list
73
+ end
74
+ ```
75
+
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.
77
+
78
+ ```ruby
79
+ plugin Easyop::Plugins::Recording,
80
+ model: OperationLog,
81
+ filter_keys: [:stripe_token, /secret/i]
82
+ ```
83
+
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`.
85
+
86
+ ```ruby
87
+ Easyop.configure do |c|
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
110
+ end
111
+ ```
112
+
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.
137
+
10
138
  ## [0.1.4] — 2026-04-14
11
139
 
12
140
  ### Added
@@ -258,6 +386,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
258
386
  - `examples/easyop_test_app/` — full Rails 8 blog application demonstrating all features in real-world code
259
387
  - `examples/usage.rb` — 13 runnable plain-Ruby examples
260
388
 
261
- [Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.4...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
392
+ [0.1.5]: https://github.com/pniemczyk/easyop/compare/v0.1.4...v0.1.5
262
393
  [0.1.4]: https://github.com/pniemczyk/easyop/compare/v0.1.3...v0.1.4
263
394
  [0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
data/README.md CHANGED
@@ -645,8 +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) |
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]` |
650
651
 
651
652
  **Required model columns:**
652
653
 
@@ -655,9 +656,12 @@ create_table :operation_logs do |t|
655
656
  t.string :operation_name, null: false
656
657
  t.boolean :success, null: false
657
658
  t.string :error_message
658
- t.text :params_data # JSON — ctx attrs (sensitive keys scrubbed)
659
+ t.text :params_data # JSON — ctx attrs (sensitive keys replaced with [FILTERED])
659
660
  t.float :duration_ms
660
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
661
665
  end
662
666
  ```
663
667
 
@@ -696,7 +700,65 @@ root_log = OperationLog.find_by(operation_name: "FullCheckout", parent_reference
696
700
  OperationLog.for_tree(root_log.root_reference_id)
697
701
  ```
698
702
 
699
- The plugin automatically scrubs these keys from `params_data` before persisting: `:password`, `:password_confirmation`, `:token`, `:secret`, `:api_key`. ActiveRecord objects are serialized as `{ id:, class: }` rather than their full representation.
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:
704
+
705
+ 1. **Built-in `FILTERED_KEYS`** — always applied: `:password`, `:password_confirmation`, `:token`, `:secret`, `:api_key`
706
+ 2. **Global config** — applied to every recorded operation:
707
+ ```ruby
708
+ Easyop.configure { |c| c.recording_filter_keys = [:api_token, /token/i] }
709
+ ```
710
+ 3. **Plugin `filter_keys:` option** — applied to all subclasses that share the plugin install:
711
+ ```ruby
712
+ plugin Easyop::Plugins::Recording, model: OperationLog, filter_keys: [:stripe_secret]
713
+ ```
714
+ 4. **`filter_params` DSL** — per-class, inheritable, and stackable at any level of the hierarchy:
715
+ ```ruby
716
+ class ApplicationOperation < ...
717
+ filter_params :internal_token, /access.?key/i
718
+ end
719
+ class Payments::ChargeCard < ApplicationOperation
720
+ filter_params :card_number # stacks on top of parent's list
721
+ end
722
+ ```
723
+
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.
700
762
 
701
763
  **`record_result` DSL — capture output data:**
702
764
 
@@ -706,9 +768,14 @@ Add an optional `result_data :text` column to persist selected ctx values after
706
768
  add_column :operation_logs, :result_data, :text # stored as JSON
707
769
  ```
708
770
 
709
- 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):
710
772
 
711
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
+
712
779
  # Attrs form — one or more ctx keys
713
780
  class PlaceOrder < ApplicationOperation
714
781
  record_result attrs: :order_id
@@ -739,7 +806,8 @@ Set a plugin-level default inherited by all subclasses:
739
806
 
740
807
  ```ruby
741
808
  plugin Easyop::Plugins::Recording, model: OperationLog,
742
- record_result: { attrs: :metadata }
809
+ record_result: true
810
+ # or: record_result: { attrs: :metadata }
743
811
  # or: record_result: ->(ctx) { { id: ctx.record_id } }
744
812
  # or: record_result: :build_result
745
813
  ```
@@ -1459,7 +1527,7 @@ Edit `lib/easyop/version.rb` and increment the version string following [Semanti
1459
1527
  ```ruby
1460
1528
  # lib/easyop/version.rb
1461
1529
  module Easyop
1462
- VERSION = "0.1.4" # was 0.1.3
1530
+ VERSION = "0.1.5" # was 0.1.4
1463
1531
  end
1464
1532
  ```
1465
1533
 
@@ -1470,45 +1538,45 @@ In `CHANGELOG.md`, move everything under `[Unreleased]` into a new versioned sec
1470
1538
  ```markdown
1471
1539
  ## [Unreleased]
1472
1540
 
1473
- ## [0.1.4] — YYYY-MM-DD # ← new section
1541
+ ## [0.1.5] — YYYY-MM-DD # ← new section
1474
1542
 
1475
1543
  ### Added
1476
1544
  - …
1477
1545
 
1478
- ## [0.1.3] — 2026-04-14
1546
+ ## [0.1.4] — 2026-04-14
1479
1547
  ```
1480
1548
 
1481
1549
  Add a comparison link at the bottom of the file:
1482
1550
 
1483
1551
  ```markdown
1484
- [Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.4...HEAD
1552
+ [Unreleased]: https://github.com/pniemczyk/easyop/compare/v0.1.5...HEAD
1553
+ [0.1.5]: https://github.com/pniemczyk/easyop/compare/v0.1.4...v0.1.5
1485
1554
  [0.1.4]: https://github.com/pniemczyk/easyop/compare/v0.1.3...v0.1.4
1486
- [0.1.3]: https://github.com/pniemczyk/easyop/compare/v0.1.2...v0.1.3
1487
1555
  ```
1488
1556
 
1489
1557
  ### 3. Commit the release changes
1490
1558
 
1491
1559
  ```bash
1492
1560
  git add lib/easyop/version.rb CHANGELOG.md
1493
- git commit -m "Release v0.1.4"
1561
+ git commit -m "Release v0.1.5"
1494
1562
  ```
1495
1563
 
1496
1564
  ### 4. Tag the commit
1497
1565
 
1498
1566
  ```bash
1499
- git tag -a v0.1.4 -m "Release v0.1.4"
1567
+ git tag -a v0.1.5 -m "Release v0.1.5"
1500
1568
  ```
1501
1569
 
1502
1570
  ### 5. Push the commit and tag
1503
1571
 
1504
1572
  ```bash
1505
1573
  git push origin master
1506
- git push origin v0.1.4
1574
+ git push origin v0.1.5
1507
1575
  ```
1508
1576
 
1509
1577
  ### 6. Build and push the gem (optional)
1510
1578
 
1511
1579
  ```bash
1512
1580
  gem build easyop.gemspec
1513
- gem push easyop-0.1.4.gem
1581
+ gem push easyop-0.1.5.gem
1514
1582
  ```
@@ -15,10 +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 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]".
21
+ # Accepts Symbol, String, or Regexp (matched against the stringified key name).
22
+ #
23
+ # @example
24
+ # Easyop.configure { |c| c.recording_filter_keys = [:api_token, /token/i] }
25
+ attr_accessor :recording_filter_keys
26
+
18
27
  def initialize
19
- @type_adapter = :native
20
- @strict_types = false
21
- @event_bus = nil # nil = Memory bus (see Easyop::Events::Registry)
28
+ @type_adapter = :native
29
+ @strict_types = false
30
+ @event_bus = nil # nil = Memory bus (see Easyop::Events::Registry)
31
+ @recording_filter_keys = []
22
32
  end
23
33
  end
24
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,25 +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)
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]".
41
50
  module Recording
42
- # Sensitive keys scrubbed from params_data before persisting.
43
- 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
44
54
 
45
55
  # Internal ctx keys used for flow tracing — excluded from params_data.
46
56
  INTERNAL_CTX_KEYS = %i[
47
57
  __recording_root_reference_id
48
58
  __recording_parent_operation_name
49
59
  __recording_parent_reference_id
60
+ __recording_child_counts
50
61
  ].freeze
51
62
 
52
- def self.install(base, model:, record_params: true, record_result: nil, **_options)
63
+ def self.install(base, model:, record_params: true, record_result: false, filter_keys: [], **_options)
53
64
  base.extend(ClassMethods)
54
65
  base.prepend(RunWrapper)
55
- base.instance_variable_set(:@_recording_model, model)
66
+ base.instance_variable_set(:@_recording_model, model)
56
67
  base.instance_variable_set(:@_recording_record_params, record_params)
57
68
  base.instance_variable_set(:@_recording_record_result, record_result)
69
+ base.instance_variable_set(:@_recording_filter_keys, Array(filter_keys))
58
70
  end
59
71
 
60
72
  module ClassMethods
@@ -73,30 +85,48 @@ module Easyop
73
85
  (superclass.respond_to?(:_recording_model) ? superclass._recording_model : nil)
74
86
  end
75
87
 
76
- 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
77
107
  if instance_variable_defined?(:@_recording_record_params)
78
108
  @_recording_record_params
79
- elsif superclass.respond_to?(:_recording_record_params?)
80
- superclass._recording_record_params?
109
+ elsif superclass.respond_to?(:_recording_record_params_config)
110
+ superclass._recording_record_params_config
81
111
  else
82
112
  true
83
113
  end
84
114
  end
85
115
 
86
- # DSL for capturing result data after the operation runs.
87
- # Three forms:
88
- # record_result attrs: :key # one or more ctx keys
89
- # record_result { |ctx| { k: ctx.k } } # block
90
- # 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
91
121
  def record_result(value = nil, attrs: nil, &block)
92
122
  @_recording_record_result = if block
93
123
  block
94
124
  elsif attrs
95
125
  { attrs: attrs }
96
- elsif value.is_a?(Symbol)
126
+ elsif !value.nil?
97
127
  value
98
128
  else
99
- value
129
+ nil
100
130
  end
101
131
  end
102
132
 
@@ -106,9 +136,36 @@ module Easyop
106
136
  elsif superclass.respond_to?(:_recording_record_result_config)
107
137
  superclass._recording_record_result_config
108
138
  else
109
- nil
139
+ false
110
140
  end
111
141
  end
142
+
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]".
147
+ #
148
+ # @example
149
+ # class ApplicationOperation < ...
150
+ # filter_params :api_token, /access.?key/i
151
+ # end
152
+ def filter_params(*keys)
153
+ @_recording_filter_keys = _own_recording_filter_keys + keys
154
+ end
155
+
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
158
+ # merged at persist time so they stay hot-reloadable.
159
+ def _recording_filter_keys
160
+ parent = superclass.respond_to?(:_recording_filter_keys) ? superclass._recording_filter_keys : []
161
+ parent + _own_recording_filter_keys
162
+ end
163
+
164
+ private
165
+
166
+ def _own_recording_filter_keys
167
+ instance_variable_defined?(:@_recording_filter_keys) ? @_recording_filter_keys : []
168
+ end
112
169
  end
113
170
 
114
171
  module RunWrapper
@@ -117,6 +174,12 @@ module Easyop
117
174
  return super unless (model = self.class._recording_model)
118
175
  return super unless self.class.name # skip anonymous classes
119
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
+
120
183
  # -- Flow tracing --
121
184
  # Each operation gets its own reference_id. The root_reference_id is
122
185
  # shared across the entire execution tree via ctx (set once, inherited).
@@ -127,6 +190,14 @@ module Easyop
127
190
  parent_operation_name = ctx[:__recording_parent_operation_name]
128
191
  parent_reference_id = ctx[:__recording_parent_reference_id]
129
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
+
130
201
  # Set THIS operation as the parent for any children that run inside super.
131
202
  # Save the previous values so we can restore them after (for siblings).
132
203
  prev_parent_name = parent_operation_name
@@ -151,7 +222,9 @@ module Easyop
151
222
  root_reference_id: root_reference_id,
152
223
  reference_id: reference_id,
153
224
  parent_operation_name: parent_operation_name,
154
- parent_reference_id: parent_reference_id)
225
+ parent_reference_id: parent_reference_id,
226
+ execution_index: execution_index,
227
+ input_keys: input_keys)
155
228
  end
156
229
  end
157
230
 
@@ -159,7 +232,8 @@ module Easyop
159
232
 
160
233
  def _recording_persist!(ctx, model, duration_ms,
161
234
  root_reference_id: nil, reference_id: nil,
162
- parent_operation_name: nil, parent_reference_id: nil)
235
+ parent_operation_name: nil, parent_reference_id: nil,
236
+ execution_index: nil, input_keys: nil)
163
237
  attrs = {
164
238
  operation_name: self.class.name,
165
239
  success: ctx.success?,
@@ -169,9 +243,11 @@ module Easyop
169
243
  root_reference_id: root_reference_id,
170
244
  reference_id: reference_id,
171
245
  parent_operation_name: parent_operation_name,
172
- parent_reference_id: parent_reference_id
246
+ parent_reference_id: parent_reference_id,
247
+ execution_index: execution_index
173
248
  }
174
- 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
175
251
  attrs[:result_data] = _recording_safe_result(ctx) if self.class._recording_record_result_config
176
252
 
177
253
  # Only write columns the model actually has
@@ -181,37 +257,93 @@ module Easyop
181
257
  _recording_warn(e)
182
258
  end
183
259
 
184
- def _recording_safe_params(ctx)
185
- ctx.to_h
186
- .except(*SCRUBBED_KEYS, *INTERNAL_CTX_KEYS)
187
- .transform_values { |v| v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v }
188
- .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
189
288
  rescue
190
289
  nil
191
290
  end
192
291
 
292
+ # Returns true when +key+ matches any entry in +filter_list+.
293
+ # Regexp entries match against the stringified key name (case-sensitive
294
+ # unless the Regexp itself uses /i). Symbol/String entries match by value.
295
+ def _recording_filter_key?(key, extra_list)
296
+ return true if FILTERED_KEYS.include?(key.to_sym)
297
+
298
+ extra_list.any? do |pattern|
299
+ case pattern
300
+ when Regexp then pattern.match?(key.to_s)
301
+ when Symbol then pattern == key.to_sym
302
+ else pattern.to_s == key.to_s
303
+ end
304
+ end
305
+ end
306
+
193
307
  def _recording_safe_result(ctx)
194
308
  config = self.class._recording_record_result_config
195
309
  return nil unless config
196
310
 
197
311
  raw = case config
312
+ when true
313
+ ctx.to_h.except(*INTERNAL_CTX_KEYS)
198
314
  when Hash
199
315
  keys = Array(config[:attrs])
200
316
  keys.each_with_object({}) { |k, h| h[k] = ctx[k] }
201
317
  when Proc
202
- config.call(ctx)
318
+ result = config.call(ctx)
319
+ return nil unless result.is_a?(Hash)
320
+ result
203
321
  when Symbol
204
- send(config)
322
+ result = send(config)
323
+ return nil unless result.is_a?(Hash)
324
+ result
205
325
  end
206
326
 
207
327
  return nil unless raw.is_a?(Hash)
208
-
209
- raw.transform_values { |v| v.is_a?(ActiveRecord::Base) ? { id: v.id, class: v.class.name } : v }
210
- .to_json
328
+ _recording_apply_and_serialize(raw).to_json
211
329
  rescue
212
330
  nil
213
331
  end
214
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
+
215
347
  def _recording_warn(err)
216
348
  return unless defined?(Rails) && Rails.respond_to?(:logger)
217
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.4"
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.4
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pawel Niemczyk