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.
- checksums.yaml +4 -4
- data/.cursor/commands/pr.md +36 -0
- data/.cursor/rules/axn-framework-patterns.mdc +43 -0
- data/.cursor/rules/general-coding-standards.mdc +27 -0
- data/.cursor/rules/spec/testing-patterns.mdc +40 -0
- data/CHANGELOG.md +57 -0
- data/Rakefile +114 -4
- data/docs/.vitepress/config.mjs +19 -10
- data/docs/advanced/conventions.md +3 -3
- data/docs/advanced/mountable.md +476 -0
- data/docs/advanced/profiling.md +351 -0
- data/docs/advanced/rough.md +27 -8
- data/docs/index.md +5 -3
- data/docs/intro/about.md +1 -1
- data/docs/intro/overview.md +6 -6
- data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
- data/docs/recipes/memoization.md +103 -18
- data/docs/recipes/rubocop-integration.md +38 -284
- data/docs/recipes/testing.md +14 -14
- data/docs/recipes/validating-user-input.md +1 -1
- data/docs/reference/async.md +429 -0
- data/docs/reference/axn-result.md +107 -0
- data/docs/reference/class.md +225 -64
- data/docs/reference/configuration.md +366 -34
- data/docs/reference/form-object.md +252 -0
- data/docs/reference/instance.md +14 -29
- data/docs/strategies/client.md +212 -0
- data/docs/strategies/form.md +235 -0
- data/docs/strategies/index.md +21 -21
- data/docs/strategies/transaction.md +1 -1
- data/docs/usage/setup.md +16 -2
- data/docs/usage/steps.md +7 -7
- data/docs/usage/using.md +23 -12
- data/docs/usage/writing.md +191 -12
- data/lib/axn/async/adapters/active_job.rb +74 -0
- data/lib/axn/async/adapters/disabled.rb +41 -0
- data/lib/axn/async/adapters/sidekiq.rb +67 -0
- data/lib/axn/async/adapters.rb +26 -0
- data/lib/axn/async/batch_enqueue/config.rb +38 -0
- data/lib/axn/async/batch_enqueue.rb +99 -0
- data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
- data/lib/axn/async.rb +178 -0
- data/lib/axn/configuration.rb +113 -0
- data/lib/{action → axn}/context.rb +22 -4
- data/lib/axn/core/automatic_logging.rb +89 -0
- data/lib/axn/core/context/facade.rb +69 -0
- data/lib/{action → axn}/core/context/facade_inspector.rb +32 -5
- data/lib/{action → axn}/core/context/internal.rb +5 -5
- data/lib/{action → axn}/core/contract.rb +111 -73
- data/lib/{action → axn}/core/contract_for_subfields.rb +30 -35
- data/lib/{action → axn}/core/contract_validation.rb +27 -12
- data/lib/axn/core/contract_validation_for_subfields.rb +165 -0
- data/lib/axn/core/default_call.rb +63 -0
- data/lib/axn/core/field_resolvers/extract.rb +32 -0
- data/lib/axn/core/field_resolvers/model.rb +63 -0
- data/lib/axn/core/field_resolvers.rb +24 -0
- data/lib/{action → axn}/core/flow/callbacks.rb +7 -7
- data/lib/{action → axn}/core/flow/exception_execution.rb +9 -13
- data/lib/{action → axn}/core/flow/handlers/base_descriptor.rb +3 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/callback_descriptor.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/descriptors/message_descriptor.rb +23 -11
- data/lib/axn/core/flow/handlers/invoker.rb +47 -0
- data/lib/{action → axn}/core/flow/handlers/matcher.rb +9 -19
- data/lib/{action → axn}/core/flow/handlers/registry.rb +3 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/base_resolver.rb +1 -1
- data/lib/{action → axn}/core/flow/handlers/resolvers/callback_resolver.rb +2 -2
- data/lib/{action → axn}/core/flow/handlers/resolvers/message_resolver.rb +12 -3
- data/lib/axn/core/flow/handlers.rb +20 -0
- data/lib/{action → axn}/core/flow/messages.rb +8 -8
- data/lib/{action → axn}/core/flow.rb +4 -4
- data/lib/{action → axn}/core/hooks.rb +17 -5
- data/lib/axn/core/logging.rb +48 -0
- data/lib/axn/core/memoization.rb +53 -0
- data/lib/{action → axn}/core/nesting_tracking.rb +1 -1
- data/lib/{action → axn}/core/timing.rb +1 -1
- data/lib/axn/core/tracing.rb +90 -0
- data/lib/axn/core/use_strategy.rb +29 -0
- data/lib/{action → axn}/core/validation/fields.rb +26 -2
- data/lib/{action → axn}/core/validation/subfields.rb +14 -12
- data/lib/axn/core/validation/validators/model_validator.rb +36 -0
- data/lib/axn/core/validation/validators/type_validator.rb +80 -0
- data/lib/{action → axn}/core/validation/validators/validate_validator.rb +12 -2
- data/lib/{action → axn}/core.rb +55 -55
- data/lib/{action → axn}/exceptions.rb +12 -2
- data/lib/axn/extras/strategies/client.rb +150 -0
- data/lib/axn/extras/strategies/vernier.rb +121 -0
- data/lib/axn/extras.rb +4 -0
- data/lib/axn/factory.rb +122 -34
- data/lib/axn/form_object.rb +90 -0
- data/lib/axn/internal/logging.rb +30 -0
- data/lib/axn/internal/registry.rb +87 -0
- data/lib/axn/mountable/descriptor.rb +76 -0
- data/lib/axn/mountable/helpers/class_builder.rb +193 -0
- data/lib/axn/mountable/helpers/mounter.rb +33 -0
- data/lib/axn/mountable/helpers/namespace_manager.rb +38 -0
- data/lib/axn/mountable/helpers/validator.rb +112 -0
- data/lib/axn/mountable/inherit_profiles.rb +72 -0
- data/lib/axn/mountable/mounting_strategies/_base.rb +87 -0
- data/lib/axn/mountable/mounting_strategies/axn.rb +48 -0
- data/lib/axn/mountable/mounting_strategies/method.rb +95 -0
- data/lib/axn/mountable/mounting_strategies/step.rb +69 -0
- data/lib/axn/mountable/mounting_strategies.rb +32 -0
- data/lib/axn/mountable.rb +119 -0
- data/lib/axn/rails/engine.rb +51 -0
- data/lib/axn/rails/generators/axn_generator.rb +86 -0
- data/lib/axn/rails/generators/templates/action.rb.erb +17 -0
- data/lib/axn/rails/generators/templates/action_spec.rb.erb +25 -0
- data/lib/{action → axn}/result.rb +32 -13
- data/lib/axn/strategies/form.rb +98 -0
- data/lib/axn/strategies/transaction.rb +26 -0
- data/lib/axn/strategies.rb +20 -0
- data/lib/axn/testing/spec_helpers.rb +6 -8
- data/lib/axn/util/callable.rb +120 -0
- data/lib/axn/util/contract_error_handling.rb +32 -0
- data/lib/axn/util/execution_context.rb +34 -0
- data/lib/axn/util/global_id_serialization.rb +52 -0
- data/lib/axn/util/logging.rb +87 -0
- data/lib/axn/util/memoization.rb +20 -0
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +26 -16
- data/lib/rubocop/cop/axn/README.md +23 -23
- data/lib/rubocop/cop/axn/unchecked_result.rb +138 -17
- metadata +106 -64
- data/.rspec +0 -3
- data/.rubocop.yml +0 -76
- data/.tool-versions +0 -1
- data/docs/reference/action-result.md +0 -37
- data/lib/action/attachable/base.rb +0 -43
- data/lib/action/attachable/steps.rb +0 -63
- data/lib/action/attachable/subactions.rb +0 -70
- data/lib/action/attachable.rb +0 -17
- data/lib/action/configuration.rb +0 -55
- data/lib/action/core/automatic_logging.rb +0 -93
- data/lib/action/core/context/facade.rb +0 -48
- data/lib/action/core/flow/handlers/invoker.rb +0 -73
- data/lib/action/core/flow/handlers.rb +0 -20
- data/lib/action/core/logging.rb +0 -37
- data/lib/action/core/tracing.rb +0 -17
- data/lib/action/core/use_strategy.rb +0 -30
- data/lib/action/core/validation/validators/model_validator.rb +0 -34
- data/lib/action/core/validation/validators/type_validator.rb +0 -30
- data/lib/action/enqueueable/via_sidekiq.rb +0 -76
- data/lib/action/enqueueable.rb +0 -13
- data/lib/action/strategies/transaction.rb +0 -19
- data/lib/action/strategies.rb +0 -48
- data/lib/axn/util.rb +0 -24
- data/package.json +0 -10
- 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)
|
data/docs/advanced/rough.md
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
* TODO: convert rough notes into actual documentation
|
|
3
|
-
:::
|
|
1
|
+
# Internal Notes
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
This page contains internal implementation notes for contributors and advanced users.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
## Context Sharing
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
The inbound/outbound contexts are views into an underlying shared object. Modifications to one affect the other:
|
|
10
8
|
|
|
11
|
-
|
|
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
|
-
|
|
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:
|
|
25
|
+
details: Clear, explicit contracts for inputs and outputs with `expects` and `exposes`
|
|
26
26
|
- title: Exception swallowing
|
|
27
|
-
details:
|
|
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:
|
|
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
|
|
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
|
|
data/docs/intro/overview.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
|