axn 0.1.0.pre.alpha.4.1 → 0.1.0.pre.alpha.4.2

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Rakefile +3 -2
  4. data/docs/.vitepress/config.mjs +0 -1
  5. data/docs/advanced/rough.md +16 -4
  6. data/docs/reference/class.md +10 -0
  7. data/docs/reference/configuration.md +93 -48
  8. data/docs/strategies/form.md +11 -2
  9. data/docs/strategies/transaction.md +4 -3
  10. data/lib/axn/async/adapters/sidekiq/auto_configure.rb +15 -0
  11. data/lib/axn/async/adapters/sidekiq.rb +6 -2
  12. data/lib/axn/async/enqueue_all_orchestrator.rb +6 -6
  13. data/lib/axn/async/exception_reporting.rb +3 -3
  14. data/lib/axn/async.rb +2 -2
  15. data/lib/axn/configuration.rb +25 -2
  16. data/lib/axn/core/automatic_logging.rb +0 -62
  17. data/lib/axn/core/context/facade.rb +1 -1
  18. data/lib/axn/core/contract.rb +76 -55
  19. data/lib/axn/core/contract_for_subfields.rb +9 -6
  20. data/lib/axn/core/default_call.rb +1 -14
  21. data/lib/axn/core/field_resolvers/model.rb +1 -1
  22. data/lib/axn/core/flow/handlers/invoker.rb +16 -5
  23. data/lib/axn/core/flow/handlers/matcher.rb +4 -7
  24. data/lib/axn/core/flow.rb +0 -2
  25. data/lib/axn/core/hooks.rb +0 -66
  26. data/lib/axn/core/memoization.rb +1 -1
  27. data/lib/axn/core/nesting_tracking.rb +8 -7
  28. data/lib/axn/core/validation/validators/validate_validator.rb +1 -1
  29. data/lib/axn/core.rb +7 -28
  30. data/lib/axn/executor.rb +454 -0
  31. data/lib/axn/extension_config.rb +13 -0
  32. data/lib/axn/extras/strategies/client.rb +46 -0
  33. data/lib/axn/{util/logging.rb → internal/call_logger.rb} +12 -7
  34. data/lib/axn/{util → internal}/callable.rb +1 -1
  35. data/lib/axn/{util → internal}/contract_error_handling.rb +1 -1
  36. data/lib/axn/internal/exception_context.rb +148 -0
  37. data/lib/axn/internal/field_config.rb +25 -0
  38. data/lib/axn/{util → internal}/global_id_serialization.rb +1 -1
  39. data/lib/axn/{util → internal}/memoization.rb +1 -1
  40. data/lib/axn/internal/{logging.rb → piping_error.rb} +5 -2
  41. data/lib/axn/internal/subfield_path.rb +33 -0
  42. data/lib/axn/{core → internal}/timing.rb +1 -19
  43. data/lib/axn/internal/tracing.rb +39 -0
  44. data/lib/axn/strategies/form.rb +16 -3
  45. data/lib/axn/version.rb +1 -1
  46. data/lib/axn.rb +21 -7
  47. metadata +15 -14
  48. data/docs/recipes/formatting-context-for-error-tracking.md +0 -186
  49. data/lib/axn/core/contract_validation.rb +0 -77
  50. data/lib/axn/core/contract_validation_for_subfields.rb +0 -165
  51. data/lib/axn/core/flow/exception_execution.rb +0 -73
  52. data/lib/axn/core/tracing.rb +0 -110
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb94e06f97e37bada60d52f5fbb6c3b733d9e8aacf3ec8104ec04b92b8a29f3b
4
- data.tar.gz: 9af75de4a098a59d9f3b30bc0eb0bf50b42a4f580b52476883aa8495402ca072
3
+ metadata.gz: bbed8a136915346d270fb287bd513684ddfd9b70692fa8a06384924a34198d6b
4
+ data.tar.gz: a9f8bb70324642e6da0ecba6522eaa8587f53259ad3488ddc72ed20927a73ad3
5
5
  SHA512:
6
- metadata.gz: 3ee1ed0a4f58cdde4755b65f46fcc409764a0423fcf552df9b426f882886190359fe6db3c59b63572e78b45ef305f0f810f813938a41d37da08a2df0f7a7bec4
7
- data.tar.gz: 00dc6904eb6b72a4fa29b36a0adfa3531f731637bbe9425c1d17cde013d23fa6a867b79dd88bf289bd428853854bf54c9470a3971f7a22ee22f4dde600bb2453
6
+ metadata.gz: 260b77024b257ad90ac69ae67deb6468a6c30d4bd47d877eefb709879e8e1a16a6f0cf519607951f3de3d9fe2663bb84cab37a58383846f495ddc17eed053614
7
+ data.tar.gz: 583c4c638a5bdc2c9d5686551e305972756d7737450c3ec8a02f3a5b236582a59e41e2290532ed8d69b9fc4c145364db0ac8f0a4a8cbda83736bf10cd7a5093f
data/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  ## Unreleased
4
4
  * N/A
5
5
 
6
+ ## 0.1.0-alpha.4.2
7
+ * [FEAT] Add extensible field metadata support for `expects`/`exposes`:
8
+ * **New** `Axn.extension_config` registry for library-facing configuration (separate from app-facing `Axn.config`)
9
+ * **New** `description:` metadata option for field declarations (e.g., `expects :name, description: "The user's name"`)
10
+ * **New** `FieldConfig#metadata` and `SubfieldConfig#metadata` attributes with `#description` accessor
11
+ * **New** `Axn.extension_config.register_field_metadata_key(:key)` for wrapper gems to register custom metadata keys
12
+ * **New** Unknown validation keys now raise `ArgumentError` (catches typos like `nummericality:`)
13
+ * **New** Metadata can only be provided when declaring a single field (multi-field + metadata raises `ArgumentError`)
14
+ * [FEAT] Add `readers:` option validation: `readers: false` now raises `ArgumentError` when used without `on:` (only valid for subfields)
15
+ * [BUGFIX] `set_default_async(:sidekiq)` now properly triggers `AutoConfigure.register!`
16
+ * [BREAKING] Refactored context API for exception reporting and handlers:
17
+ * **Removed** `context_for_logging(direction)` instance method
18
+ * **Added** public `execution_context` method returning structured hash: `{ inputs: {...}, outputs: {...}, **extra_keys }`
19
+ * **Added** private `inputs_for_logging` / `outputs_for_logging` methods for automatic pre/post logging (do NOT include extra context)
20
+ * **Renamed** `set_logging_context` → `set_execution_context`, `clear_logging_context` → `clear_execution_context`, hook `additional_logging_context` → `additional_execution_context`
21
+ * **Reserved keys:** `:inputs` and `:outputs` cannot be set via `set_execution_context` or the hook—they always come from the action's contract
22
+ * **Internal:** Class method `context_for_logging(data:, direction:)` renamed to `_context_slice(data:, direction:)`
23
+ * Exception context now includes both `inputs` and `outputs` with additional context merged at the top level (not nested inside `inputs`)
24
+
6
25
  ## 0.1.0-alpha.4.1
7
26
  * [BREAKING][BUGFIX] `fail!` in async jobs no longer triggers retries - business logic failures complete without retry (Sidekiq and ActiveJob adapters)
8
27
  * [FEAT] Add `async_exception_reporting` config to control when `on_exception` triggers in async context (`:every_attempt`, `:first_and_exhausted`, `:only_exhausted`)
data/Rakefile CHANGED
@@ -139,8 +139,9 @@ namespace :benchmark do
139
139
  end
140
140
  end
141
141
 
142
- # Require verify to pass before release
143
- Rake::Task["release"].enhance([:verify])
142
+ # Require verify to pass before release. This relies on the default gem release task
143
+ # (from bundler/gem_tasks) depending on "build"; verify runs before build, so before push.
144
+ Rake::Task["build"].enhance([:verify])
144
145
 
145
146
  # Automatically run benchmark:release after rake release
146
147
  Rake::Task["release"].enhance do
@@ -58,7 +58,6 @@ export default defineConfig({
58
58
  { text: 'Validating User Input', link: '/recipes/validating-user-input' },
59
59
  { text: 'Testing Actions', link: '/recipes/testing' },
60
60
  { text: 'RuboCop Integration', link: '/recipes/rubocop-integration' },
61
- { text: 'Formatting Context for Error Tracking', link: '/recipes/formatting-context-for-error-tracking' },
62
61
  ]
63
62
  },
64
63
  {
@@ -17,14 +17,26 @@ For information about logging configuration, see the [Configuration reference](/
17
17
  - **Log levels**: [log_level](/reference/configuration#log-level)
18
18
  - **Automatic logging**: [Automatic Logging](/reference/configuration#automatic-logging)
19
19
 
20
- ### `context_for_logging`
20
+ ### `execution_context`
21
21
 
22
- The `context_for_logging` method returns a hash of the action's context, with:
23
- - Filtering to accessible attributes
24
- - Sensitive values removed (fields marked with `sensitive: true`)
22
+ The `execution_context` method returns a structured hash for exception reporting and handlers:
23
+
24
+ ```ruby
25
+ {
26
+ inputs: { ... }, # Filtered inbound fields (sensitive values removed)
27
+ outputs: { ... }, # Filtered outbound fields (sensitive values removed)
28
+ # ... any extra keys from set_execution_context or additional_execution_context hook
29
+ }
30
+ ```
25
31
 
26
32
  This is automatically passed to the `on_exception` hook. See [Adding Additional Context to Exception Logging](/reference/configuration#adding-additional-context-to-exception-logging) for customizing the context.
27
33
 
34
+ **Private methods for automatic logging:**
35
+ - `inputs_for_logging` - Returns only filtered inbound fields (used by pre-execution logs)
36
+ - `outputs_for_logging` - Returns only filtered outbound fields (used by post-execution logs)
37
+
38
+ These private methods do NOT include additional context from `set_execution_context` or the hook—they are specifically for automatic logging which only needs to show what the action was called with and what it produced.
39
+
28
40
  ### `#inspect` Support
29
41
 
30
42
  Action instances provide a readable `#inspect` output that shows:
@@ -101,6 +101,16 @@ end
101
101
  Defaults work the same way for subfields as they do for top-level fields - they are applied when the subfield is missing or explicitly `nil`, but not for blank values.
102
102
  :::
103
103
 
104
+ #### Disabling subfield readers
105
+
106
+ By default, subfields create top-level reader methods (e.g., `random` in the example above). You can disable this with `readers: false`:
107
+
108
+ ```ruby
109
+ expects :data, type: Hash, on: :event, readers: false
110
+ ```
111
+
112
+ This is useful when you have duplicate sub-keys across different parent fields, or when you want to access subfields only through the parent. Note that `readers: false` is only valid for subfields (i.e., when using `on:`) — using it on top-level fields will raise an `ArgumentError`.
113
+
104
114
  #### `preprocess`
105
115
  `expects` also supports a `preprocess` option that, if set to a callable, will be executed _before_ applying any defaults or validations. This can be useful for type coercion, e.g.:
106
116
 
@@ -5,12 +5,13 @@ Somewhere at boot (e.g. `config/initializers/actions.rb` in Rails), you can call
5
5
  ```ruby
6
6
  Axn.configure do |c|
7
7
  c.log_level = :info
8
- c.logger = ...
8
+ c.logger = Rails.logger
9
+
9
10
  c.on_exception = proc do |e, action:, context:|
10
- message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
11
-
12
- Rails.logger.warn(message)
13
- Honeybadger.notify(message, context: { axn_context: context })
11
+ Honeybadger.notify(
12
+ "[#{action.class.name}] #{e.class.name}: #{e.message}",
13
+ context: context
14
+ )
14
15
  end
15
16
  end
16
17
  ```
@@ -22,44 +23,82 @@ By default any swallowed errors are noted in the logs, but it's _highly recommen
22
23
  For example, if you're using Honeybadger this could look something like:
23
24
 
24
25
  ```ruby
25
- Axn.configure do |c|
26
- c.on_exception = proc do |e, action:, context:|
27
- message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
28
-
29
- Rails.logger.warn(message)
30
- Honeybadger.notify(message, context: { axn_context: context })
31
- end
26
+ Axn.configure do |c|
27
+ c.on_exception = proc do |e, action:, context:|
28
+ Honeybadger.notify(
29
+ "[#{action.class.name}] #{e.class.name}: #{e.message}",
30
+ context: context
31
+ )
32
32
  end
33
+ end
33
34
  ```
34
35
 
35
36
  **Note:** The `action:` and `context:` keyword arguments are *optional*—your proc can accept any combination of `e`, `action:`, and `context:`. Only the keyword arguments you explicitly declare will be passed to your handler. All of the following are valid:
36
37
 
37
38
  ```ruby
38
- # Only exception object
39
- c.on_exception = proc { |e| ... }
39
+ # Only exception object
40
+ c.on_exception = proc { |e| ... }
40
41
 
41
- # Exception and action
42
- c.on_exception = proc { |e, action:| ... }
42
+ # Exception and action
43
+ c.on_exception = proc { |e, action:| ... }
43
44
 
44
- # Exception and context
45
- c.on_exception = proc { |e, context:| ... }
45
+ # Exception and context
46
+ c.on_exception = proc { |e, context:| ... }
46
47
 
47
- # Exception, action, and context
48
- c.on_exception = proc { |e, action:, context:| ... }
48
+ # Exception, action, and context
49
+ c.on_exception = proc { |e, action:, context:| ... }
49
50
  ```
50
51
 
51
- A couple notes:
52
+ ### Context Structure
53
+
54
+ The `context` hash is automatically formatted and contains:
55
+
56
+ ```ruby
57
+ {
58
+ inputs: { ... }, # Action inputs (declared expects fields only), formatted recursively
59
+ outputs: { ... }, # Action outputs (declared exposes fields only), formatted recursively
60
+ # ... any extra keys from set_execution_context or additional_execution_context hook
61
+ # e.g. client_strategy__last_request: { url: ..., method: ..., status: ... }
62
+ current_attributes: { ... }, # Current.attributes (auto-included if defined and present)
63
+ async: { ... } # Async retry info (only present in async context)
64
+ }
65
+ ```
52
66
 
53
- * `context` will contain the arguments passed to the `action`, _but_ any marked as sensitive (e.g. `expects :foo, sensitive: true`) will be filtered out in the logs.
54
- * If your handler raises, the failure will _also_ be swallowed and logged
55
- * This handler is global across _all_ Axns. You can also specify per-Action handlers via [the class-level declaration](/reference/class#on-exception).
56
- * The `context` hash may contain complex objects (like ActiveRecord models, `ActionController::Parameters`, or `Axn::FormObject` instances) that aren't easily serialized by error tracking systems. See [Formatting Context for Error Tracking Systems](/recipes/formatting-context-for-error-tracking) for a recipe to convert these to readable formats.
67
+ Additional context (like `client_strategy__last_request` from the `:client` strategy) appears at the top level alongside `inputs` and `outputs`, not nested inside them. Formatting is applied recursively to nested hashes and arrays.
68
+
69
+ **What gets formatted automatically:**
70
+ - **ActiveRecord objects** GlobalID strings (e.g., `"gid://app/User/123"`)
71
+ - **ActionController::Parameters** → Plain hashes
72
+ - **Axn::FormObject instances** → Hash representation
73
+
74
+ **Example with all context fields:**
75
+
76
+ ```ruby
77
+ Axn.configure do |c|
78
+ c.on_exception = proc do |e, action:, context:|
79
+ # context[:inputs] - Your action's inputs (formatted)
80
+ # context[:outputs] - Your action's outputs (formatted)
81
+ # context[:client_strategy__last_request] - Example extra key from :client strategy
82
+ # context[:current_attributes] - Rails Current.attributes (if present)
83
+ # context[:async] - Retry info (if in async context)
84
+
85
+ Honeybadger.notify(e, context: context)
86
+ end
87
+ end
88
+ ```
89
+
90
+ ### Additional Notes
91
+
92
+ - Sensitive fields (marked with `expects :foo, sensitive: true`) are automatically filtered to `"[FILTERED]"`
93
+ - If your handler raises an exception, the failure will be swallowed and logged
94
+ - This handler is global across _all_ actions. You can also specify per-action handlers via [the class-level declaration](/reference/class#on-exception)
95
+ - Complex objects are automatically formatted for error tracking systems
57
96
 
58
97
  ### Adding Additional Context to Exception Logging
59
98
 
60
99
  When processing records in a loop or performing batch operations, you may want to include additional context (like which record is being processed) in exception logs. You can do this in two ways:
61
100
 
62
- **Option 1: Explicit setter** - Call `set_logging_context` during execution:
101
+ **Option 1: Explicit setter** - Call `set_execution_context` during execution:
63
102
 
64
103
  ```ruby
65
104
  class ProcessPendingRecords
@@ -67,14 +106,14 @@ class ProcessPendingRecords
67
106
 
68
107
  def call
69
108
  pending_records.each do |record|
70
- set_logging_context(current_record_id: record.id, batch_index: @index)
109
+ set_execution_context(current_record_id: record.id, batch_index: @index)
71
110
  # ... process record ...
72
111
  end
73
112
  end
74
113
  end
75
114
  ```
76
115
 
77
- **Option 2: Hook method** - Define a private `additional_logging_context` method that returns a hash:
116
+ **Option 2: Hook method** - Define a private `additional_execution_context` method that returns a hash:
78
117
 
79
118
  ```ruby
80
119
  class ProcessPendingRecords
@@ -89,7 +128,7 @@ class ProcessPendingRecords
89
128
 
90
129
  private
91
130
 
92
- def additional_logging_context
131
+ def additional_execution_context
93
132
  return {} unless @current_record
94
133
 
95
134
  {
@@ -100,16 +139,19 @@ class ProcessPendingRecords
100
139
  end
101
140
  ```
102
141
 
103
- Both approaches can be used together - they will be merged. The additional context is **only** included in exception logging (not in normal pre/post execution logs), and is evaluated lazily (the hook method is only called when an exception occurs).
142
+ Both approaches can be used together - they will be merged at the top level of the context hash. The additional context is **only** included in `execution_context` (used for exception reporting and handlers), not in normal pre/post execution logs, and is evaluated lazily (the hook method is only called when needed).
143
+
144
+ **Reserved keys:** The keys `:inputs` and `:outputs` are reserved. If you try to set them via `set_execution_context` or the hook, they will be ignored—the actual inputs and outputs always come from the action's contract.
104
145
 
105
- Action-specific `on_exception` handlers can also access this context by calling `context_for_logging` directly:
146
+ Action-specific `on_exception` handlers can access the full context by calling `execution_context`:
106
147
 
107
148
  ```ruby
108
149
  class ProcessPendingRecords
109
150
  include Axn
110
151
 
111
152
  on_exception do |exception:|
112
- log "Failed with this extra context: #{context_for_logging}"
153
+ ctx = execution_context
154
+ log "Failed processing. Inputs: #{ctx[:inputs]}, Extra: #{ctx[:current_record_id]}"
113
155
  # ... handle exception with context ...
114
156
  end
115
157
  end
@@ -342,22 +384,24 @@ When `on_exception` is triggered in an async context, the `context` hash include
342
384
  ```ruby
343
385
  Axn.configure do |c|
344
386
  c.on_exception = proc do |e, action:, context:|
387
+ # context[:async] is automatically included when in async context
388
+ # Available fields:
389
+ # context[:async][:adapter] # :sidekiq or :active_job
390
+ # context[:async][:attempt] # Current attempt (1-indexed)
391
+ # context[:async][:max_retries] # Max retry attempts
392
+ # context[:async][:job_id] # Job ID (if available)
393
+ # context[:async][:first_attempt] # true if first attempt
394
+ # context[:async][:retries_exhausted] # true if all retries exhausted
395
+
345
396
  if context[:async]
346
- # Available fields:
347
- # context[:async][:adapter] # :sidekiq or :active_job
348
- # context[:async][:attempt] # Current attempt (1-indexed)
349
- # context[:async][:max_retries] # Max retry attempts
350
- # context[:async][:job_id] # Job ID (if available)
351
- # context[:async][:first_attempt] # true if first attempt
352
- # context[:async][:retries_exhausted] # true if all retries exhausted
353
-
354
- Honeybadger.notify(e, context: {
355
- axn_context: context,
397
+ # Add custom retry info to context
398
+ enhanced_context = context.merge(
356
399
  retry_info: "Attempt #{context[:async][:attempt]} of #{context[:async][:max_retries]}"
357
- })
400
+ )
401
+ Honeybadger.notify(e, context: enhanced_context)
358
402
  else
359
- # Foreground execution - no retry context
360
- Honeybadger.notify(e, context: { axn_context: context })
403
+ # Foreground execution - context still includes inputs and current_attributes
404
+ Honeybadger.notify(e, context: context)
361
405
  end
362
406
  end
363
407
  end
@@ -549,9 +593,10 @@ Axn.configure do |c|
549
593
 
550
594
  # Exception handling
551
595
  c.on_exception = proc do |e, action:, context:|
552
- message = "[#{action.class.name}] Failing due to #{e.class.name}: #{e.message}"
553
- Rails.logger.warn(message)
554
- Honeybadger.notify(message, context: { axn_context: context })
596
+ Honeybadger.notify(
597
+ "[#{action.class.name}] #{e.class.name}: #{e.message}",
598
+ context: context
599
+ )
555
600
  end
556
601
 
557
602
  # Observability
@@ -62,7 +62,9 @@ end
62
62
 
63
63
  ### Inline Form Definition
64
64
 
65
- You can define the form class inline using a block:
65
+ You can pass the form in directly as a block instead of a separate class.
66
+
67
+ **Block only** — the form class is not assigned to a constant, but it is given a `name` (default: the action’s name + `Form`, e.g. `"CreateUser::Form"`) so it shows up clearly in logging and exception reporting:
66
68
 
67
69
  ```ruby
68
70
  class CreateUser
@@ -79,7 +81,14 @@ class CreateUser
79
81
  end
80
82
  ```
81
83
 
82
- This creates an anonymous form class that inherits from `Axn::FormObject`.
84
+ **Block + type string** — the form class is named using the given string (e.g. `"CreateUser::Form"`). If that constant doesn't exist yet, the block defines the class and we assign it to that name:
85
+
86
+ ```ruby
87
+ use :form, type: "CreateUser::Form" do
88
+ validates :email, presence: true
89
+ validates :name, presence: true
90
+ end
91
+ ```
83
92
 
84
93
  ### Custom Field Names
85
94
 
@@ -18,12 +18,13 @@ class TransferFunds
18
18
  end
19
19
  ```
20
20
 
21
- **Important**: The transaction wraps the entire action execution, including:
21
+ **Important**: The transaction wraps:
22
22
  - `before` hooks
23
23
  - The main `call` method
24
24
  - `after` hooks
25
- - Success/failure callbacks (`on_success`, `on_failure`, etc.)
26
25
 
27
- This means that if any part of the action (including hooks or callbacks) raises an exception or calls `fail!`, the entire transaction will be rolled back.
26
+ So the transaction is still open until all of the above complete. If any of them raise or call `fail!`, the transaction is rolled back.
27
+
28
+ **`on_success` runs after the transaction commits.** It is invoked only after the transaction block has finished successfully. Use `on_success` (not `after` hooks) for work that should run outside the transaction—for example, calling an external HTTP service—so the DB transaction can commit and release the connection before that work runs. Putting slow or unreliable external calls inside `call` or `after` keeps the transaction open until they complete and can block the connection.
28
29
 
29
30
  **Requirements**: Requires ActiveRecord to be available in your application.
@@ -42,6 +42,21 @@ module Axn
42
42
  false
43
43
  end
44
44
 
45
+ # Ensures middleware and death handler are registered for the current
46
+ # Axn.config.async_exception_reporting mode. Call when the Sidekiq adapter
47
+ # is included (e.g. via async :sidekiq or default async) so that default
48
+ # mode works without the app setting async_exception_reporting explicitly.
49
+ # Safe to call multiple times - register! is idempotent.
50
+ # No-ops when Sidekiq is a minimal stub (e.g. in unit tests without full Sidekiq).
51
+ def ensure_registered_for_current_config!
52
+ return unless defined?(::Sidekiq) && ::Sidekiq.respond_to?(:configure_server)
53
+
54
+ mode = Axn.config.async_exception_reporting
55
+ return if mode == :every_attempt
56
+
57
+ register!
58
+ end
59
+
45
60
  # Registers both middleware and death handler.
46
61
  # Safe to call multiple times - will not duplicate registrations.
47
62
  def register!
@@ -18,6 +18,10 @@ module Axn
18
18
  # Use Sidekiq::Job if available (Sidekiq 7+), otherwise error
19
19
  raise LoadError, "Sidekiq::Job is not available. Please check your Sidekiq version." unless defined?(::Sidekiq::Job)
20
20
 
21
+ # Ensure middleware and death handler are registered for current async_exception_reporting
22
+ # (e.g. when async :sidekiq is used without ever setting async_exception_reporting).
23
+ AutoConfigure.ensure_registered_for_current_config!
24
+
21
25
  include ::Sidekiq::Job
22
26
 
23
27
  # Sidekiq's processor calls .new on the worker class from outside the class hierarchy
@@ -42,7 +46,7 @@ module Axn
42
46
  normalized_options = _extract_and_normalize_async_options(kwargs)
43
47
 
44
48
  # Convert kwargs to string keys and handle GlobalID conversion
45
- job_kwargs = Axn::Util::GlobalIdSerialization.serialize(kwargs)
49
+ job_kwargs = Axn::Internal::GlobalIdSerialization.serialize(kwargs)
46
50
 
47
51
  # Process normalized async options if present
48
52
  if normalized_options
@@ -58,7 +62,7 @@ module Axn
58
62
  end
59
63
 
60
64
  def perform(*args)
61
- context = Axn::Util::GlobalIdSerialization.deserialize(args.first)
65
+ context = Axn::Internal::GlobalIdSerialization.deserialize(args.first)
62
66
 
63
67
  # Validate Sidekiq configuration once on first job execution in a real server context.
64
68
  # Skip validation when:
@@ -28,18 +28,18 @@ module Axn
28
28
  target = target_class_name.constantize
29
29
 
30
30
  # Deserialize static_args (convert GlobalID strings back to objects)
31
- deserialized_static_args = Axn::Util::GlobalIdSerialization.deserialize(static_args)
31
+ deserialized_static_args = Axn::Internal::GlobalIdSerialization.deserialize(static_args)
32
32
 
33
33
  count = self.class.execute_iteration(
34
34
  target,
35
35
  **deserialized_static_args,
36
- on_progress: method(:set_logging_context),
36
+ on_progress: method(:set_execution_context),
37
37
  )
38
38
 
39
39
  message_parts = ["Batch enqueued #{count} jobs for #{target.name}"]
40
40
  message_parts << "with explicit args: #{static_args.inspect}" if static_args.any?
41
41
 
42
- Axn::Util::Logging.log_at_level(
42
+ Axn::Internal::CallLogger.log_at_level(
43
43
  self.class,
44
44
  level: :info,
45
45
  message_parts:,
@@ -77,7 +77,7 @@ module Axn
77
77
  execute_iteration_without_logging(target, **static_args)
78
78
  else
79
79
  # Serialize static_args for Sidekiq (convert GlobalID objects, stringify keys)
80
- serialized_static_args = Axn::Util::GlobalIdSerialization.serialize(resolved_static)
80
+ serialized_static_args = Axn::Internal::GlobalIdSerialization.serialize(resolved_static)
81
81
 
82
82
  # Execute iteration in background via EnqueueAllOrchestrator
83
83
  call_async(target_class_name: target.name, static_args: serialized_static_args)
@@ -280,7 +280,7 @@ module Axn
280
280
  filter_result = begin
281
281
  config.filter_block.call(item)
282
282
  rescue StandardError => e
283
- Axn::Internal::Logging.piping_error(
283
+ Axn::Internal::PipingError.swallow(
284
284
  "filter block for :#{config.field}",
285
285
  exception: e,
286
286
  )
@@ -294,7 +294,7 @@ module Axn
294
294
  begin
295
295
  item.public_send(config.via)
296
296
  rescue StandardError => e
297
- Axn::Internal::Logging.piping_error(
297
+ Axn::Internal::PipingError.swallow(
298
298
  "via extraction (:#{config.via}) for :#{config.field}",
299
299
  exception: e,
300
300
  )
@@ -16,8 +16,8 @@ module Axn
16
16
  # @param extra_context [Hash] additional context to merge (e.g., discarded: true, _job_metadata)
17
17
  # @param log_prefix [String] prefix for error logging (e.g., "Sidekiq death handler")
18
18
  def trigger_on_exception(exception:, action_class:, retry_context:, job_args:, extra_context: {}, log_prefix: "async")
19
- # Filter sensitive values using the action class's context_for_logging
20
- filtered_context = action_class.context_for_logging(data: job_args, direction: :inbound)
19
+ # Filter sensitive values using the action class's internal _context_slice
20
+ filtered_context = action_class._context_slice(data: job_args, direction: :inbound)
21
21
 
22
22
  # Build final context with async info (avoid mutating extra_context)
23
23
  async_extra = extra_context[:async] || {}
@@ -31,7 +31,7 @@ module Axn
31
31
  # Trigger on_exception
32
32
  Axn.config.on_exception(exception, action: proxy_action, context:)
33
33
  rescue StandardError => e
34
- Axn::Internal::Logging.piping_error("in #{log_prefix}", exception: e)
34
+ Axn::Internal::PipingError.swallow("in #{log_prefix}", exception: e)
35
35
  end
36
36
  end
37
37
 
data/lib/axn/async.rb CHANGED
@@ -104,11 +104,11 @@ module Axn
104
104
  ActiveSupport::Notifications.instrument("axn.call_async", payload)
105
105
  rescue StandardError => e
106
106
  # Don't raise in notification emission to avoid interfering with async enqueueing
107
- Axn::Internal::Logging.piping_error("emitting notification for axn.call_async", action_class: self, exception: e)
107
+ Axn::Internal::PipingError.swallow("emitting notification for axn.call_async", action_class: self, exception: e)
108
108
  end
109
109
 
110
110
  def _log_async_invocation(kwargs, adapter_name:)
111
- Axn::Util::Logging.log_at_level(
111
+ Axn::Internal::CallLogger.log_at_level(
112
112
  self,
113
113
  level: log_calls_level,
114
114
  message_parts: ["Enqueueing async execution via #{adapter_name}"],
@@ -7,7 +7,7 @@ module Axn
7
7
 
8
8
  class Configuration
9
9
  attr_accessor :emit_metrics, :raise_piping_errors_in_dev
10
- attr_writer :logger, :env, :on_exception, :additional_includes, :log_level, :rails
10
+ attr_writer :logger, :env, :on_exception, :additional_includes, :log_level, :rails, :_include_retry_command_in_exceptions
11
11
 
12
12
  # Controls when on_exception is triggered in async context (Sidekiq/ActiveJob).
13
13
  # Options:
@@ -40,6 +40,13 @@ module Axn
40
40
 
41
41
  def additional_includes = @additional_includes ||= []
42
42
 
43
+ # EXPERIMENTAL: When true, automatically generates a retry command in exception context.
44
+ # This is marked experimental because the retry command generation may not work well
45
+ # for all action types (e.g., actions with complex object dependencies).
46
+ def _include_retry_command_in_exceptions
47
+ @_include_retry_command_in_exceptions.nil? ? false : @_include_retry_command_in_exceptions
48
+ end
49
+
43
50
  def _default_async_adapter = @default_async_adapter ||= false
44
51
  def _default_async_config = @default_async_config ||= {}
45
52
  def _default_async_config_block = @default_async_config_block
@@ -51,6 +58,7 @@ module Axn
51
58
  @default_async_config = config.any? ? config : {}
52
59
  @default_async_config_block = block_given? ? block : nil
53
60
 
61
+ _ensure_async_exception_reporting_registered_for_adapter(adapter)
54
62
  _apply_async_to_enqueue_all_orchestrator
55
63
  end
56
64
 
@@ -65,6 +73,7 @@ module Axn
65
73
  @enqueue_all_async_config = config.any? ? config : {}
66
74
  @enqueue_all_async_config_block = block_given? ? block : nil
67
75
 
76
+ _ensure_async_exception_reporting_registered_for_adapter(adapter)
68
77
  _apply_async_to_enqueue_all_orchestrator
69
78
  end
70
79
 
@@ -87,7 +96,7 @@ module Axn
87
96
  return unless @on_exception
88
97
 
89
98
  # Only pass the args and kwargs that the given block expects
90
- Axn::Util::Callable.call_with_desired_shape(@on_exception, args: [e], kwargs: { action:, context: })
99
+ Axn::Internal::Callable.call_with_desired_shape(@on_exception, args: [e], kwargs: { action:, context: })
91
100
  end
92
101
 
93
102
  def logger
@@ -128,6 +137,20 @@ module Axn
128
137
  )
129
138
  end
130
139
 
140
+ # Ensures the given async adapter has exception-reporting components registered
141
+ # for the current async_exception_reporting mode (e.g. Sidekiq middleware/death handler).
142
+ # Called when setting default or enqueue_all async adapter so the default mode works
143
+ # without the app having to set async_exception_reporting explicitly.
144
+ def _ensure_async_exception_reporting_registered_for_adapter(adapter)
145
+ return if adapter.nil? || adapter == false
146
+
147
+ case adapter
148
+ when :sidekiq
149
+ _auto_configure_sidekiq_for_async_exception_reporting(async_exception_reporting)
150
+ end
151
+ # Active Job has no global registration (per-class proxy with after_discard).
152
+ end
153
+
131
154
  # Auto-configures Sidekiq middleware and death handler when async_exception_reporting
132
155
  # is set to a mode that requires them.
133
156
  #