axn 0.1.0.pre.alpha.2.8.1 → 0.1.0.pre.alpha.4

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/.cursor/rules/axn-framework-patterns.mdc +43 -0
  4. data/.cursor/rules/general-coding-standards.mdc +27 -0
  5. data/.cursor/rules/spec/testing-patterns.mdc +40 -0
  6. data/CHANGELOG.md +57 -0
  7. data/Rakefile +114 -4
  8. data/docs/.vitepress/config.mjs +19 -10
  9. data/docs/advanced/conventions.md +3 -3
  10. data/docs/advanced/mountable.md +476 -0
  11. data/docs/advanced/profiling.md +351 -0
  12. data/docs/advanced/rough.md +27 -8
  13. data/docs/index.md +5 -3
  14. data/docs/intro/about.md +1 -1
  15. data/docs/intro/overview.md +6 -6
  16. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  17. data/docs/recipes/memoization.md +103 -18
  18. data/docs/recipes/rubocop-integration.md +38 -284
  19. data/docs/recipes/testing.md +14 -14
  20. data/docs/recipes/validating-user-input.md +1 -1
  21. data/docs/reference/async.md +429 -0
  22. data/docs/reference/axn-result.md +107 -0
  23. data/docs/reference/class.md +225 -64
  24. data/docs/reference/configuration.md +366 -34
  25. data/docs/reference/form-object.md +252 -0
  26. data/docs/reference/instance.md +14 -29
  27. data/docs/strategies/client.md +212 -0
  28. data/docs/strategies/form.md +235 -0
  29. data/docs/strategies/index.md +21 -21
  30. data/docs/strategies/transaction.md +1 -1
  31. data/docs/usage/setup.md +16 -2
  32. data/docs/usage/steps.md +7 -7
  33. data/docs/usage/using.md +23 -12
  34. data/docs/usage/writing.md +191 -12
  35. data/lib/axn/async/adapters/active_job.rb +74 -0
  36. data/lib/axn/async/adapters/disabled.rb +41 -0
  37. data/lib/axn/async/adapters/sidekiq.rb +67 -0
  38. data/lib/axn/async/adapters.rb +26 -0
  39. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  40. data/lib/axn/async/batch_enqueue.rb +99 -0
  41. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  42. data/lib/axn/async.rb +178 -0
  43. data/lib/axn/configuration.rb +113 -0
  44. data/lib/{action → axn}/context.rb +22 -4
  45. data/lib/axn/core/automatic_logging.rb +89 -0
  46. data/lib/axn/core/context/facade.rb +69 -0
  47. data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
  48. data/lib/{action → axn}/core/context/internal.rb +5 -5
  49. data/lib/{action → axn}/core/contract.rb +111 -73
  50. data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
  51. data/lib/{action → axn}/core/contract_validation.rb +27 -12
  52. data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
  53. data/lib/axn/core/default_call.rb +63 -0
  54. data/lib/axn/core/field_resolvers/extract.rb +32 -0
  55. data/lib/axn/core/field_resolvers/model.rb +63 -0
  56. data/lib/axn/core/field_resolvers.rb +24 -0
  57. data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
  58. data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
  59. data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
  60. data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
  61. data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
  62. data/lib/axn/core/flow/handlers/invoker.rb +47 -0
  63. data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
  64. data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
  65. data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
  66. data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
  67. data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
  68. data/lib/axn/core/flow/handlers.rb +20 -0
  69. data/lib/{action → axn}/core/flow/messages.rb +8 -8
  70. data/lib/{action → axn}/core/flow.rb +4 -4
  71. data/lib/{action → axn}/core/hooks.rb +17 -5
  72. data/lib/axn/core/logging.rb +48 -0
  73. data/lib/axn/core/memoization.rb +53 -0
  74. data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
  75. data/lib/{action → axn}/core/timing.rb +1 -1
  76. data/lib/axn/core/tracing.rb +90 -0
  77. data/lib/axn/core/use_strategy.rb +29 -0
  78. data/lib/{action → axn}/core/validation/fields.rb +26 -2
  79. data/lib/{action → axn}/core/validation/subfields.rb +14 -12
  80. data/lib/axn/core/validation/validators/model_validator.rb +36 -0
  81. data/lib/axn/core/validation/validators/type_validator.rb +80 -0
  82. data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
  83. data/lib/{action → axn}/core.rb +55 -55
  84. data/lib/{action → axn}/exceptions.rb +12 -2
  85. data/lib/axn/extras/strategies/client.rb +150 -0
  86. data/lib/axn/extras/strategies/vernier.rb +121 -0
  87. data/lib/axn/extras.rb +4 -0
  88. data/lib/axn/factory.rb +122 -34
  89. data/lib/axn/form_object.rb +90 -0
  90. data/lib/axn/internal/logging.rb +30 -0
  91. data/lib/axn/internal/registry.rb +87 -0
  92. data/lib/axn/mountable/descriptor.rb +76 -0
  93. data/lib/axn/mountable/helpers/class_builder.rb +193 -0
  94. data/lib/axn/mountable/helpers/mounter.rb +33 -0
  95. data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
  96. data/lib/axn/mountable/helpers/validator.rb +112 -0
  97. data/lib/axn/mountable/inherit_profiles.rb +72 -0
  98. data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
  99. data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
  100. data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
  101. data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
  102. data/lib/axn/mountable/mounting_strategies.rb +32 -0
  103. data/lib/axn/mountable.rb +119 -0
  104. data/lib/axn/rails/engine.rb +51 -0
  105. data/lib/axn/rails/generators/axn_generator.rb +86 -0
  106. data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
  107. data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
  108. data/lib/{action → axn}/result.rb +32 -13
  109. data/lib/axn/strategies/form.rb +98 -0
  110. data/lib/axn/strategies/transaction.rb +26 -0
  111. data/lib/axn/strategies.rb +20 -0
  112. data/lib/axn/testing/spec_helpers.rb +6 -8
  113. data/lib/axn/util/callable.rb +120 -0
  114. data/lib/axn/util/contract_error_handling.rb +32 -0
  115. data/lib/axn/util/execution_context.rb +34 -0
  116. data/lib/axn/util/global_id_serialization.rb +52 -0
  117. data/lib/axn/util/logging.rb +87 -0
  118. data/lib/axn/util/memoization.rb +20 -0
  119. data/lib/axn/version.rb +1 -1
  120. data/lib/axn.rb +26 -16
  121. data/lib/rubocop/cop/axn/README.md +23 -23
  122. data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
  123. metadata +106 -64
  124. data/.rspec +0 -3
  125. data/.rubocop.yml +0 -76
  126. data/.tool-versions +0 -1
  127. data/docs/reference/action-result.md +0 -37
  128. data/lib/action/attachable/base.rb +0 -43
  129. data/lib/action/attachable/steps.rb +0 -63
  130. data/lib/action/attachable/subactions.rb +0 -70
  131. data/lib/action/attachable.rb +0 -17
  132. data/lib/action/configuration.rb +0 -55
  133. data/lib/action/core/automatic_logging.rb +0 -93
  134. data/lib/action/core/context/facade.rb +0 -48
  135. data/lib/action/core/flow/handlers/invoker.rb +0 -73
  136. data/lib/action/core/flow/handlers.rb +0 -20
  137. data/lib/action/core/logging.rb +0 -37
  138. data/lib/action/core/tracing.rb +0 -17
  139. data/lib/action/core/use_strategy.rb +0 -30
  140. data/lib/action/core/validation/validators/model_validator.rb +0 -34
  141. data/lib/action/core/validation/validators/type_validator.rb +0 -30
  142. data/lib/action/enqueueable/via_sidekiq.rb +0 -76
  143. data/lib/action/enqueueable.rb +0 -13
  144. data/lib/action/strategies/transaction.rb +0 -19
  145. data/lib/action/strategies.rb +0 -48
  146. data/lib/axn/util.rb +0 -24
  147. data/package.json +0 -10
  148. data/yarn.lock +0 -1166
@@ -0,0 +1,351 @@
1
+ # Profiling
2
+
3
+ Axn supports performance profiling using [Vernier](https://github.com/Shopify/vernier), a Ruby sampling profiler that provides detailed insights into your action's performance characteristics.
4
+
5
+ ## Overview
6
+
7
+ Profiling helps you identify performance bottlenecks in your actions by capturing detailed execution traces. Vernier is particularly useful for:
8
+
9
+ - Identifying slow methods and code paths
10
+ - Understanding memory allocation patterns
11
+ - Analyzing call stacks and execution flow
12
+ - Optimizing performance-critical actions
13
+
14
+ ## Setup
15
+
16
+ ### 1. Install Vernier
17
+
18
+ Add the Vernier gem to your Gemfile:
19
+
20
+ ```ruby
21
+ # Gemfile
22
+ gem 'vernier', '~> 0.1'
23
+ ```
24
+
25
+ Then run:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ **Note:** Vernier is not included as a dependency of Axn, so you must explicitly add it to your Gemfile if you want to use profiling features.
32
+
33
+ ### 2. Enable Profiling
34
+
35
+ No global configuration is needed! Simply use the `:vernier` strategy on the actions you want to profile.
36
+
37
+ ## Basic Usage
38
+
39
+ Profiling is enabled per-action by using the `:vernier` strategy. This follows the same pattern as other Axn strategies like `:transaction` and `:form`.
40
+
41
+ ### Simple Profiling
42
+
43
+ Enable profiling on any action:
44
+
45
+ ```ruby
46
+ class UserCreation
47
+ include Axn
48
+
49
+ # Always profile this action
50
+ use :vernier
51
+
52
+ expects :user_params
53
+
54
+ def call
55
+ user = User.create!(user_params)
56
+ send_welcome_email(user)
57
+ end
58
+
59
+ private
60
+
61
+ def send_welcome_email(user)
62
+ UserMailer.welcome(user).deliver_now
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### Conditional Profiling
68
+
69
+ Profile only under specific conditions:
70
+
71
+ ```ruby
72
+ class DataProcessing
73
+ include Axn
74
+
75
+ # Profile only when processing large datasets
76
+ use :vernier, if: -> { record_count > 1000 }
77
+
78
+ expects :records, :record_count
79
+
80
+ def call
81
+ records.each { |record| process_record(record) }
82
+ end
83
+ end
84
+ ```
85
+
86
+ **Alternative using a method:**
87
+
88
+ ```ruby
89
+ class DataProcessing
90
+ include Axn
91
+
92
+ # Profile using a method
93
+ use :vernier, if: :should_profile?
94
+
95
+ expects :records, :record_count, :debug_mode, type: :boolean, default: false
96
+
97
+ def should_profile?
98
+ record_count > 1000 || debug_mode
99
+ end
100
+
101
+ def call
102
+ records.each { |record| process_record(record) }
103
+ end
104
+ end
105
+ ```
106
+
107
+
108
+ ## Advanced Usage
109
+
110
+ ### Sampling Rate Control
111
+
112
+ Adjust the sampling rate per action:
113
+
114
+ ```ruby
115
+ class DevelopmentAction
116
+ include Axn
117
+
118
+ # High sampling rate for development (more detailed data)
119
+ use :vernier, sample_rate: 0.5 if Rails.env.development?
120
+
121
+ def call
122
+ # Action logic
123
+ end
124
+ end
125
+
126
+ class ProductionAction
127
+ include Axn
128
+
129
+ # Low sampling rate for production (minimal overhead)
130
+ use :vernier, sample_rate: 0.01 if Rails.env.production?
131
+
132
+ def call
133
+ # Action logic
134
+ end
135
+ end
136
+ ```
137
+
138
+ ### Custom Output Directory
139
+
140
+ Organize profiles by environment or feature:
141
+
142
+ ```ruby
143
+ class MyAction
144
+ include Axn
145
+
146
+ # Custom output directory
147
+ use :vernier, output_dir: Rails.root.join("tmp", "profiles", Rails.env)
148
+
149
+ def call
150
+ # Action logic
151
+ end
152
+ end
153
+ ```
154
+
155
+ ### Multiple Conditions
156
+
157
+ Combine multiple profiling conditions:
158
+
159
+ ```ruby
160
+ class ComplexAction
161
+ include Axn
162
+
163
+ # Profile when debug mode is enabled OR when processing admin users
164
+ use :vernier, if: -> { debug_mode || user.admin? }
165
+
166
+ expects :user, :debug_mode, type: :boolean, default: false
167
+
168
+ def call
169
+ # Complex logic
170
+ end
171
+ end
172
+ ```
173
+
174
+ ## Viewing and Analyzing Profiles
175
+
176
+ ### 1. Generate Profile Data
177
+
178
+ Run your action with profiling enabled:
179
+
180
+ ```ruby
181
+ # This will generate a profile file if conditions are met
182
+ result = UserCreation.call(user_params: { name: "John", email: "john@example.com" })
183
+ ```
184
+
185
+ ### 2. Locate Profile Files
186
+
187
+ Profile files are saved as JSON in your configured output directory:
188
+
189
+ ```bash
190
+ # Default location
191
+ ls tmp/profiles/
192
+
193
+ # Example output
194
+ axn_UserCreation_1703123456.json
195
+ axn_DataProcessing_1703123457.json
196
+ ```
197
+
198
+ ### 3. View in Firefox Profiler
199
+
200
+ 1. Open [profiler.firefox.com](https://profiler.firefox.com/)
201
+ 2. Click "Load a profile from file"
202
+ 3. Select your generated JSON file
203
+ 4. Analyze the performance data
204
+
205
+ ### 4. Understanding the Profile
206
+
207
+ The Firefox Profiler provides several views:
208
+
209
+ - **Call Tree**: Shows the complete call stack with timing
210
+ - **Flame Graph**: Visual representation of call stacks
211
+ - **Stack Chart**: Timeline view of function calls
212
+ - **Markers**: Custom markers and events
213
+
214
+ ## Best Practices
215
+
216
+ ### 1. Use Conditional Profiling
217
+
218
+ Avoid profiling all actions in production:
219
+
220
+ ```ruby
221
+ # Good: Conditional profiling
222
+ use :vernier, if: -> { Rails.env.development? || debug_mode }
223
+
224
+ # Avoid: Always profiling in production
225
+ use :vernier # This can impact performance
226
+ ```
227
+
228
+ ### 2. Appropriate Sampling Rates
229
+
230
+ Choose sampling rates based on your environment:
231
+
232
+ ```ruby
233
+ class MyAction
234
+ include Axn
235
+
236
+ # High detail for debugging
237
+ use :vernier, sample_rate: 0.5 if Rails.env.development?
238
+
239
+ # Moderate sampling for staging
240
+ use :vernier, sample_rate: 0.1 if Rails.env.staging?
241
+
242
+ # Minimal overhead for production
243
+ use :vernier, sample_rate: 0.01 if Rails.env.production?
244
+
245
+ def call
246
+ # Action logic
247
+ end
248
+ end
249
+ ```
250
+
251
+ ### 3. Profile Specific Scenarios
252
+
253
+ Focus on performance-critical paths:
254
+
255
+ ```ruby
256
+ class OrderProcessing
257
+ include Axn
258
+
259
+ # Profile only expensive operations
260
+ use :vernier, if: -> { order.total > 1000 }
261
+
262
+ expects :order
263
+
264
+ def call
265
+ process_payment
266
+ send_confirmation
267
+ update_inventory
268
+ end
269
+ end
270
+ ```
271
+
272
+ ### 4. Clean Up Old Profiles
273
+
274
+ Implement profile cleanup to avoid disk space issues:
275
+
276
+ ```ruby
277
+ # Add to a rake task or cron job
278
+ namespace :profiles do
279
+ desc "Clean up old profile files"
280
+ task cleanup: :environment do
281
+ profile_dir = Rails.root.join("tmp", "profiles")
282
+ Dir.glob(File.join(profile_dir, "*.json")).each do |file|
283
+ File.delete(file) if File.mtime(file) < 7.days.ago
284
+ end
285
+ end
286
+ end
287
+ ```
288
+
289
+ ## Troubleshooting
290
+
291
+ ### Vernier Not Available
292
+
293
+ If you see this error:
294
+
295
+ ```
296
+ LoadError: Vernier gem is not loaded. Add `gem 'vernier', '~> 0.1'` to your Gemfile to enable profiling.
297
+ ```
298
+
299
+ Make sure to:
300
+ 1. Add `vernier` to your Gemfile
301
+ 2. Run `bundle install`
302
+ 3. Restart your application
303
+
304
+ ### No Profile Files Generated
305
+
306
+ If profile files aren't being generated:
307
+
308
+ 1. Verify your action has `use :vernier` enabled
309
+ 2. Ensure profiling conditions are met
310
+ 3. Check the output directory exists and is writable
311
+
312
+ ### Performance Impact
313
+
314
+ Profiling adds overhead to your application:
315
+
316
+ - **Sampling overhead**: ~1-5% depending on sample rate
317
+ - **File I/O**: Profile files are written to disk
318
+ - **Memory usage**: Slight increase due to sampling
319
+
320
+ Use appropriate sampling rates and conditional profiling to minimize impact.
321
+
322
+ ## Integration with Other Tools
323
+
324
+ ### OpenTelemetry and Datadog Integration
325
+
326
+ Axn automatically creates OpenTelemetry spans for all actions when OpenTelemetry is available. These spans appear as children of your Rails request traces in APM tools.
327
+
328
+ You can combine profiling with OpenTelemetry tracing:
329
+
330
+ ```ruby
331
+ class MyAction
332
+ include Axn
333
+
334
+ # Profiling with custom options
335
+ use :vernier, sample_rate: 0.1
336
+
337
+ def call
338
+ # Action logic
339
+ # OpenTelemetry spans are automatically created
340
+ end
341
+ end
342
+ ```
343
+
344
+ For detailed setup instructions on sending traces to Datadog (including the required gems and initialization order), see the [OpenTelemetry Tracing section](/reference/configuration#opentelemetry-tracing) in the Configuration reference.
345
+
346
+ ## Resources
347
+
348
+ - [Vernier GitHub Repository](https://github.com/Shopify/vernier)
349
+ - [Firefox Profiler](https://profiler.firefox.com/)
350
+ - [Ruby Performance Optimization Guide](https://ruby-doc.org/core-3.2.1/doc/performance_rdoc.html)
351
+ - [Axn Configuration Reference](/reference/configuration)
@@ -1,14 +1,33 @@
1
- ::: danger ALPHA
2
- * TODO: convert rough notes into actual documentation
3
- :::
1
+ # Internal Notes
4
2
 
5
- ## Rough Notes
3
+ This page contains internal implementation notes for contributors and advanced users.
6
4
 
7
- * General note: the inbound/outbound contexts are views into an underlying shared object (passed down through organize calls) -- modifications of one will affect the other (e.g. preprocessing inbound args implicitly transforms them on the underlying context, which is echoed if you also expose it on outbound).
5
+ ## Context Sharing
8
6
 
9
- * `context_for_logging` (and decent #inspect support)
7
+ The inbound/outbound contexts are views into an underlying shared object. Modifications to one affect the other:
10
8
 
11
- * Configuring logging (will default to Rails.logger if available, else fall back to basic Logger (but can explicitly set via e.g. `Action.config.logger = Logger.new($stdout`))
9
+ - Preprocessing inbound args implicitly transforms them on the underlying context
10
+ - If you also expose a preprocessed field on outbound, it will reflect the transformed value
12
11
 
13
- * Note `context_for_logging` is available (filtered to accessible attrs, filtering out sensitive values). Automatically passed into `on_exception` hook.
12
+ ## Logging and Debugging
14
13
 
14
+ For information about logging configuration, see the [Configuration reference](/reference/configuration):
15
+
16
+ - **Logger configuration**: [logger](/reference/configuration#logger)
17
+ - **Log levels**: [log_level](/reference/configuration#log-level)
18
+ - **Automatic logging**: [Automatic Logging](/reference/configuration#automatic-logging)
19
+
20
+ ### `context_for_logging`
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`)
25
+
26
+ 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
+
28
+ ### `#inspect` Support
29
+
30
+ Action instances provide a readable `#inspect` output that shows:
31
+ - The action class name
32
+ - Field values (with sensitive values filtered)
33
+ - Current execution state
data/docs/index.md CHANGED
@@ -22,11 +22,13 @@ hero:
22
22
 
23
23
  features:
24
24
  - title: Declarative interface
25
- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
25
+ details: Clear, explicit contracts for inputs and outputs with `expects` and `exposes`
26
26
  - title: Exception swallowing
27
- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
27
+ details: Automatic error handling with user-safe error messages and internal logging
28
+ - title: Advanced Patterns
29
+ details: Mountable actions, workflow composition, and background processing capabilities
28
30
  - title: Default Observability
29
- details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
31
+ details: Built-in logging, timing, and error tracking out of the box
30
32
  ---
31
33
 
32
34
  ::: danger ALPHA RELEASE
data/docs/intro/about.md CHANGED
@@ -17,7 +17,7 @@ A simple, declarative core API. Concise enough to pick up quickly, but sufficien
17
17
  - Consistent return interface (including exception swallowing)
18
18
  - Clear distinction between user-facing and internal errors
19
19
  - Minimal boilerplate
20
- - Easy backgrounding (no need for a separate Worker class just to wrap a service call)
20
+ - Easy async execution (no need for a separate Worker class just to wrap a service call)
21
21
 
22
22
  **Additional benefits devs get for free:**
23
23
 
@@ -13,11 +13,11 @@ This library provides a set of conventions for writing business logic in Rails (
13
13
 
14
14
  ### Minimal example
15
15
 
16
- Your logic goes in a <abbr title="Plain Old Ruby Object">PORO</abbr>. The only requirements are to `include Action` and a `call` method, meaning the basic skeleton looks something like this:
16
+ Your logic goes in a <abbr title="Plain Old Ruby Object">PORO</abbr>. The only requirements are to `include Axn` and define a `call` method, meaning the basic skeleton looks something like this:
17
17
 
18
18
  ```ruby
19
19
  class Foo
20
- include Action
20
+ include Axn
21
21
 
22
22
  def call
23
23
  log "Doesn't do much, but this technically works..."
@@ -31,7 +31,7 @@ Most actions require inputs, and many return values to the caller; no need for a
31
31
 
32
32
  * `expects :foo` to declare inputs the class expects to receive.
33
33
 
34
- You pass the `expect`ed keyword arguments to `call`, then reference their values as local `attr_reader`s.
34
+ You pass the `expect`ed keyword arguments to `call`, then reference their values as local `attr_reader`s. Use `optional: true` for fields that may be missing or blank.
35
35
 
36
36
  * `exposes :bar` to declare any outputs the class will expose.
37
37
 
@@ -52,7 +52,7 @@ If any declared expectations or exposures are _not_ met the action will fail, se
52
52
 
53
53
  ```ruby
54
54
  class Actions::Slack::Post
55
- include Action
55
+ include Axn
56
56
  VALID_CHANNELS = [ ... ]
57
57
 
58
58
  expects :channel, default: VALID_CHANNELS.first, inclusion: { in: VALID_CHANNELS } # [!code focus:4]
@@ -76,7 +76,7 @@ end
76
76
  ## Return interface {#return-interface}
77
77
 
78
78
 
79
- The return value of an Action call is always an `Action::Result`, which provides a consistent interface:
79
+ The return value of an Axn call is always an `Axn::Result`, which provides a consistent interface:
80
80
 
81
81
  * `ok?` will return a boolean (false if any errors or exceptions occurred, otherwise true)
82
82
  * if OK, `success` will return a string that is _safe to show end users_
@@ -140,7 +140,7 @@ When configuring custom error and success messages, remember to define your stat
140
140
 
141
141
  ```ruby
142
142
  class MyAction
143
- include Action
143
+ include Axn
144
144
 
145
145
  # Static fallback messages first
146
146
  success "Default success message"
@@ -0,0 +1,186 @@
1
+ # Formatting Context for Error Tracking Systems
2
+
3
+ The `context` hash passed to the global `on_exception` handler may contain complex objects (like ActiveRecord models, `ActionController::Parameters`, or `Axn::FormObject` instances) that aren't easily serialized by error tracking systems. You can format these values to make them more readable.
4
+
5
+ ## Basic Example
6
+
7
+ ```ruby
8
+ Axn.configure do |c|
9
+ c.on_exception = proc do |e, action:, context:|
10
+ formatted_context = format_hash_values(context)
11
+
12
+ Honeybadger.notify(e, context: { axn_context: formatted_context })
13
+ end
14
+ end
15
+
16
+ def format_hash_values(hash)
17
+ hash.transform_values do |v|
18
+ if v.respond_to?(:to_global_id)
19
+ v.to_global_id.to_s
20
+ elsif v.is_a?(ActionController::Parameters)
21
+ v.to_unsafe_h
22
+ elsif v.is_a?(Axn::FormObject)
23
+ v.to_h
24
+ else
25
+ v
26
+ end
27
+ end
28
+ end
29
+ ```
30
+
31
+ ## What This Converts
32
+
33
+ - **ActiveRecord objects** → Their global ID string (via `to_global_id`)
34
+ - **`ActionController::Parameters`** → A plain hash
35
+ - **`Axn::FormObject` instances** → Their hash representation
36
+ - **Other values** → Remain unchanged
37
+
38
+ This ensures that your error tracking system receives serializable, readable context data instead of complex objects that may not serialize properly.
39
+
40
+ ## Recursive Formatting
41
+
42
+ If your context contains nested hashes with complex objects, you may want to recursively format the entire structure:
43
+
44
+ ```ruby
45
+ def format_hash_values(hash)
46
+ hash.transform_values do |v|
47
+ case v
48
+ when Hash
49
+ format_hash_values(v)
50
+ when Array
51
+ v.map { |item| item.is_a?(Hash) ? format_hash_values(item) : format_value(item) }
52
+ else
53
+ format_value(v)
54
+ end
55
+ end
56
+ end
57
+
58
+ def format_value(v)
59
+ if v.respond_to?(:to_global_id)
60
+ v.to_global_id.to_s
61
+ elsif v.is_a?(ActionController::Parameters)
62
+ v.to_unsafe_h
63
+ elsif v.is_a?(Axn::FormObject)
64
+ v.to_h
65
+ else
66
+ v
67
+ end
68
+ end
69
+ ```
70
+
71
+ ## Advanced Example: Production Implementation
72
+
73
+ Here's a comprehensive example that includes additional context, a retry command generator, and proper handling of ActiveRecord models:
74
+
75
+ ```ruby
76
+ Axn.configure do |c|
77
+ def format_hash_values(hash)
78
+ hash.transform_values do |v|
79
+ if v.respond_to?(:to_global_id)
80
+ v.to_global_id.to_s
81
+ elsif v.is_a?(ActionController::Parameters)
82
+ v.to_unsafe_h
83
+ elsif v.is_a?(Axn::FormObject)
84
+ v.to_h
85
+ else
86
+ v
87
+ end
88
+ end
89
+ end
90
+
91
+ # Format values for retry commands - produces copy-pasteable Ruby code
92
+ def format_value_for_retry_command(value)
93
+ # Handle ActiveRecord model instances
94
+ if value.respond_to?(:to_global_id) && value.respond_to?(:id) && !value.is_a?(Class)
95
+ begin
96
+ model_class = value.class.name
97
+ id = value.id
98
+ return "#{model_class}.find(#{id.inspect})"
99
+ rescue StandardError
100
+ # If accessing id fails, fall through to default behavior
101
+ end
102
+ end
103
+
104
+ # Handle GlobalID strings (useful for serialized values)
105
+ if value.is_a?(String) && value.start_with?("gid://")
106
+ begin
107
+ gid = GlobalID.parse(value)
108
+ if gid
109
+ model_class = gid.model_class.name
110
+ id = gid.model_id
111
+ return "#{model_class}.find(#{id.inspect})"
112
+ end
113
+ rescue StandardError
114
+ # If parsing fails, fall through to default behavior
115
+ end
116
+ end
117
+
118
+ # Default: use inspect for other types
119
+ value.inspect
120
+ end
121
+
122
+ def retry_command(action:, context:)
123
+ action_name = action.class.name
124
+ return nil if action_name.nil?
125
+
126
+ expected_fields = action.internal_field_configs.map(&:field)
127
+
128
+ return "#{action_name}.call()" if expected_fields.empty?
129
+
130
+ args = expected_fields.map do |field|
131
+ value = context[field]
132
+ "#{field}: #{format_value_for_retry_command(value)}"
133
+ end.join(", ")
134
+
135
+ "#{action_name}.call(#{args})"
136
+ end
137
+
138
+ c.on_exception = proc do |e, action:, context:|
139
+ axn_name = action.class.name || "AnonymousClass"
140
+ message = "[#{axn_name}] Raised #{e.class.name}: #{e.message}"
141
+
142
+ hb_context = {
143
+ axn: axn_name,
144
+ axn_context: format_hash_values(context),
145
+ current_attributes: format_hash_values(Current.attributes),
146
+ retry_command: retry_command(action:, context:),
147
+ exception: e,
148
+ }
149
+
150
+ fingerprint = [axn_name, e.class.name, e.message].join(" - ")
151
+ Honeybadger.notify(message, context: hb_context, backtrace: e.backtrace, fingerprint:)
152
+ rescue StandardError => rep
153
+ Rails.logger.warn "!! Axn failed to report action failure to honeybadger!\nOriginal exception: #{e}\nReporting exception: #{rep}"
154
+ end
155
+ end
156
+ ```
157
+
158
+ This example includes:
159
+
160
+ - **Formatted context**: Uses `format_hash_values` to serialize complex objects for readable error tracking
161
+ - **Smart retry commands**: Generates copy-pasteable Ruby code, converting ActiveRecord models to `Model.find(id)` calls instead of raw inspect output
162
+ - **GlobalID support**: Handles both live model instances and serialized GlobalID strings
163
+ - **Additional context**: Includes `Current.attributes` (if using a Current pattern) for request-level context
164
+ - **Error fingerprinting**: Creates a fingerprint from action name, exception class, and message to group similar errors
165
+ - **Error handling**: Wraps the Honeybadger notification in a rescue block to prevent reporting failures from masking the original exception
166
+
167
+ ### Example Output
168
+
169
+ For an action like:
170
+
171
+ ```ruby
172
+ class UpdateUser
173
+ include Axn
174
+ expects :user, model: User
175
+ expects :name, type: String
176
+ end
177
+ ```
178
+
179
+ The retry command would generate:
180
+
181
+ ```ruby
182
+ UpdateUser.call(user: User.find(123), name: "Alice")
183
+ ```
184
+
185
+ This can be copied directly from your error tracking system and pasted into a Rails console to reproduce the error.
186
+