optiaudit-rails 0.1.1 → 0.2.0
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/CHANGELOG.md +23 -1
- data/README.md +44 -1
- data/lib/optiaudit/controller.rb +26 -1
- data/lib/optiaudit/railtie.rb +17 -0
- data/lib/optiaudit/registry.rb +121 -0
- data/lib/optiaudit/testing.rb +8 -0
- data/lib/optiaudit/version.rb +1 -1
- data/lib/optiaudit.rb +15 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ab9fc2af772216e10bb2376e7eb5c7dab31393f41500bf9375564b94c848128
|
|
4
|
+
data.tar.gz: ad4ba3df9b4b5b86cba423aaf50d2f9c87f832f78dde26873ecb3a4c2939f5f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 454b2763272e3483aab0f1fb6f8c5dddf647e60dedda80929df8cdb30c0801810b52b146eed5bfea5794f337428e6429443d91c847470002cdfbca295a5e104f
|
|
7
|
+
data.tar.gz: e2f094abd10c8e528f7fcd316c1f9f208c6e53143c6316b57b223774e2cc507bddfa3e1a33211e05216947d290655fb90cf3506939ed53a39011f9b6bd588987
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Registro central de acciones auditadas** en `config/optiaudit.rb`, que la gema carga sola al arrancar:
|
|
15
|
+
`Optiaudit.audit { page_view "o/pax/itineraries#index", module: :itineraries, formats: :html, if: ... }` y
|
|
16
|
+
`event :create, "controller#action", module: ...`. Auditar una pantalla nueva es una línea en ese archivo, sin tocar
|
|
17
|
+
controllers. Las macros siguen funcionando.
|
|
18
|
+
- **Validación al arrancar**: cada entrada se compara con las rutas de la app, cuando Rails termina de dibujarlas
|
|
19
|
+
(un bloque `routes.append`, que corre en `RouteSet#finalize!`): sin reordenar initializers ni cargar las rutas dos
|
|
20
|
+
veces, y con los initializers de la app ya aplicados. En development y test una entrada sin
|
|
21
|
+
ruta detiene el arranque (`Optiaudit::InvalidRegistry`); en production se registra un warning y la app arranca.
|
|
22
|
+
`Optiaudit::Testing.verify_registry!` para tenerlo como test.
|
|
23
|
+
- Opción `formats:` (por ejemplo, solo `:html`).
|
|
24
|
+
|
|
25
|
+
### Performance
|
|
26
|
+
|
|
27
|
+
Un único `after_action` global busca la acción en un hash de dos niveles congelado. Para acciones que no están en el
|
|
28
|
+
registro suma entre 0,6 y 3,7 µs por request (300 entradas, Ruby 2.7.5 + Rails 5.2.8, benchmark intercalado con y sin
|
|
29
|
+
el hook, GC desactivado). La búsqueda en sí son ~117 ns.
|
|
30
|
+
|
|
10
31
|
## [0.1.1] - 2026-10-22
|
|
11
32
|
|
|
12
33
|
Correcciones de la verificación del README contra OptiAudit real, con la 0.1.0 publicada.
|
|
@@ -37,6 +58,7 @@ Primera versión: el equivalente para Rails de `@theoptimalpartner/nestjs-optiau
|
|
|
37
58
|
- **`Optiaudit::Testing`** para que las apps verifiquen sus eventos en tests sin HTTP.
|
|
38
59
|
- Compatibilidad probada en CI: Ruby 2.7 + Rails 5.2 + Sidekiq 6.3, Ruby 3.0 + Rails 6.1 + Sidekiq 6.5 y Ruby 3.2 + Rails 7.0 + Sidekiq 7.2.
|
|
39
60
|
|
|
40
|
-
[Unreleased]: https://bitbucket.org/RoutingUC/optiaudit-rails/branches/compare/main%0Dv0.
|
|
61
|
+
[Unreleased]: https://bitbucket.org/RoutingUC/optiaudit-rails/branches/compare/main%0Dv0.2.0
|
|
62
|
+
[0.2.0]: https://bitbucket.org/RoutingUC/optiaudit-rails/src/v0.2.0/
|
|
41
63
|
[0.1.1]: https://bitbucket.org/RoutingUC/optiaudit-rails/src/v0.1.1/
|
|
42
64
|
[0.1.0]: https://bitbucket.org/RoutingUC/optiaudit-rails/src/v0.1.0/
|
data/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Integrating an app is adding the gem, two environment variables and one line per
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Zero-config**: Reads `OPTIAUDIT_URL` and `OPTIAUDIT_WRITE_KEY` from the environment
|
|
12
|
+
- **One list of audited actions**: `config/optiaudit.rb` declares what the app audits, without touching controllers, checked against the routes at boot
|
|
12
13
|
- **Controller macros**: `optiaudit_page_view` and `optiaudit_event`, the equivalent of `@TrackPageView()` and `@TrackEvent()`
|
|
13
14
|
- **Auto context extraction**: actor from `current_user`, client IP, user agent, path, request id and a hashed session id
|
|
14
15
|
- **Never breaks the request**: delivery runs in Sidekiq or a background thread, and every error is logged as a warning
|
|
@@ -56,7 +57,44 @@ If the app runs Sidekiq, events go through the `optiaudit` queue. Add it to the
|
|
|
56
57
|
|
|
57
58
|
Apps without Sidekiq need nothing: the gem uses a background thread (see [Delivery](#delivery)).
|
|
58
59
|
|
|
59
|
-
### 3.
|
|
60
|
+
### 3. Declare what to audit
|
|
61
|
+
|
|
62
|
+
List the audited actions in `config/optiaudit.rb`. The gem loads it at boot: auditing a new screen is
|
|
63
|
+
one line in this file, and no controller changes.
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# config/optiaudit.rb
|
|
67
|
+
Optiaudit.audit do
|
|
68
|
+
page_view "routes#index", module: :routes, formats: :html
|
|
69
|
+
page_view "routes#show", module: :routes, formats: :html
|
|
70
|
+
event :update, "routes#update", module: :routes
|
|
71
|
+
event :delete, "routes#destroy", module: :routes,
|
|
72
|
+
metadata: ->(controller) { { action: "delete_route", route_id: controller.params[:id] } }
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **Key:** `"<controller_path>#<action>"` of the controller that serves the request (`Admin::RoutesController`
|
|
77
|
+
is `"admin/routes"`). A parent controller does not cover its subclasses: an action served by two
|
|
78
|
+
controllers needs two lines.
|
|
79
|
+
- **Options:** `module:` (required), `formats:` (one or more, e.g. `:html`; default all), `if:` / `unless:`
|
|
80
|
+
(a controller method name or a lambda that receives the controller) and `metadata:` (a hash or a lambda
|
|
81
|
+
that receives the controller).
|
|
82
|
+
- **Checked at boot:** an entry without a route (a renamed controller, a typo) stops the boot in
|
|
83
|
+
development and test, so it never reaches production unnoticed. In production it is logged
|
|
84
|
+
(`<key> has no route: that action is not being audited`) and the app boots.
|
|
85
|
+
- The same rules as the macros apply: only 2xx and 3xx, only with an actor, never raises.
|
|
86
|
+
- **Cost:** one global `after_action` looks the action up in a frozen hash. For actions not in the list
|
|
87
|
+
it adds about 1–4 µs per request (measured with 300 entries on Ruby 2.7 / Rails 5.2).
|
|
88
|
+
- **Keep metadata in memory:** lambdas run inside the request; use params and instance variables the
|
|
89
|
+
action already loaded, never database queries.
|
|
90
|
+
|
|
91
|
+
Changes to this file go through code review and a deploy, so the audit trail of what is audited is the
|
|
92
|
+
repository history.
|
|
93
|
+
|
|
94
|
+
### Or use the Macros
|
|
95
|
+
|
|
96
|
+
The registry is the recommended way. The macros remain for apps that prefer to declare tracking in
|
|
97
|
+
the controller:
|
|
60
98
|
|
|
61
99
|
```ruby
|
|
62
100
|
class RoutesController < ApplicationController
|
|
@@ -258,6 +296,9 @@ it "audits the update" do
|
|
|
258
296
|
"metadata" => include("module" => "routes")
|
|
259
297
|
)
|
|
260
298
|
end
|
|
299
|
+
|
|
300
|
+
# Fails when an entry of config/optiaudit.rb has no route
|
|
301
|
+
it("audits only routed actions") { Optiaudit::Testing.verify_registry! }
|
|
261
302
|
```
|
|
262
303
|
|
|
263
304
|
## Track Params
|
|
@@ -299,6 +340,8 @@ the gem truncates or omits fields instead of sending them as they are.
|
|
|
299
340
|
| HTTP 4xx (other) | Logs warning, doesn't raise, no retry |
|
|
300
341
|
| HTTP 207 on a batch | Logs warning with the rejected events |
|
|
301
342
|
| Redis down when enqueueing | Logs warning, doesn't raise |
|
|
343
|
+
| `config/optiaudit.rb` entry without a route | Raises at boot in development and test; logs a warning in production |
|
|
344
|
+
| `if:` / `metadata:` lambda raises | Event not sent, logs warning, doesn't raise |
|
|
302
345
|
|
|
303
346
|
## Contributing
|
|
304
347
|
|
data/lib/optiaudit/controller.rb
CHANGED
|
@@ -33,6 +33,31 @@ module Optiaudit
|
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
+
# Global after_action (added by the Railtie): tracks the action if it is in
|
|
37
|
+
# the registry. Unregistered actions cost one hash lookup.
|
|
38
|
+
def optiaudit_track_registered
|
|
39
|
+
entry = Optiaudit.registry.lookup(controller_path, action_name)
|
|
40
|
+
return unless entry && optiaudit_trackable_response?
|
|
41
|
+
return unless entry.formats.nil? || entry.formats.include?(request.format.to_sym)
|
|
42
|
+
return unless optiaudit_condition(entry.if, true) && !optiaudit_condition(entry.unless, false)
|
|
43
|
+
|
|
44
|
+
optiaudit_track(entry.event_type, entry.module_name, metadata: entry.metadata)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Optiaudit.log_warning("could not track #{entry&.key}: #{e.class}: #{e.message}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def optiaudit_condition(condition, default)
|
|
50
|
+
case condition
|
|
51
|
+
when nil then default
|
|
52
|
+
when Symbol, String then send(condition)
|
|
53
|
+
else condition.call(self)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def optiaudit_trackable_response?
|
|
58
|
+
response.status >= 200 && response.status < 400
|
|
59
|
+
end
|
|
60
|
+
|
|
36
61
|
# Tracks an event with the actor and request data of this controller.
|
|
37
62
|
# `params` accepts every field of Optiaudit.track and overrides the defaults;
|
|
38
63
|
# `metadata` may also be a lambda that receives the controller.
|
|
@@ -55,7 +80,7 @@ module Optiaudit
|
|
|
55
80
|
end
|
|
56
81
|
|
|
57
82
|
def optiaudit_track_action(event_type, module_name, metadata)
|
|
58
|
-
return unless
|
|
83
|
+
return unless optiaudit_trackable_response?
|
|
59
84
|
|
|
60
85
|
optiaudit_track(event_type, module_name, metadata: metadata)
|
|
61
86
|
end
|
data/lib/optiaudit/railtie.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Optiaudit
|
|
|
8
8
|
ActiveSupport.on_load(:action_controller) do
|
|
9
9
|
require "optiaudit/controller"
|
|
10
10
|
include Optiaudit::Controller
|
|
11
|
+
after_action :optiaudit_track_registered
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
|
|
@@ -16,5 +17,21 @@ module Optiaudit
|
|
|
16
17
|
config.after_initialize do
|
|
17
18
|
require "optiaudit/sidekiq_job" if defined?(::Sidekiq)
|
|
18
19
|
end
|
|
20
|
+
|
|
21
|
+
# The app's list of audited actions (config/optiaudit.rb), loaded and
|
|
22
|
+
# verified against the routes when Rails finishes drawing them: an entry
|
|
23
|
+
# without a route stops the boot in development and test, and is logged in
|
|
24
|
+
# production. A routes.append block runs at that point (RouteSet#finalize!)
|
|
25
|
+
# in every supported Rails, without reordering initializers or loading the
|
|
26
|
+
# routes twice. By then the app initializers ran, so its config applies.
|
|
27
|
+
initializer "optiaudit.registry" do |app|
|
|
28
|
+
path = app.root.join("config", "optiaudit.rb")
|
|
29
|
+
next unless File.exist?(path)
|
|
30
|
+
|
|
31
|
+
app.routes.append do
|
|
32
|
+
Optiaudit::Registry.load_file(path)
|
|
33
|
+
Optiaudit.registry.verify!(app.routes, strict: !::Rails.env.production?)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
19
36
|
end
|
|
20
37
|
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Optiaudit
|
|
4
|
+
# Raised by Registry#verify! in strict mode when entries have no route.
|
|
5
|
+
class InvalidRegistry < StandardError; end
|
|
6
|
+
|
|
7
|
+
# The actions an app audits, declared in one place (config/optiaudit.rb)
|
|
8
|
+
# instead of with macros in each controller:
|
|
9
|
+
#
|
|
10
|
+
# Optiaudit.audit do
|
|
11
|
+
# page_view "o/pax/itineraries#index", module: :itineraries, formats: :html, if: :sky?
|
|
12
|
+
# event :create, "o/sky/demand_set_imports#confirm", module: :demand_sets,
|
|
13
|
+
# metadata: ->(controller) { { import_id: controller.params[:id] } }
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Keys are "<controller_path>#<action>" of the controller that serves the
|
|
17
|
+
# request: a parent controller does not cover its subclasses.
|
|
18
|
+
#
|
|
19
|
+
# Every request looks itself up here, so the registry is a frozen two-level
|
|
20
|
+
# hash ({ "o/pax/itineraries" => { "index" => entry } }) and the lookup
|
|
21
|
+
# allocates nothing.
|
|
22
|
+
class Registry
|
|
23
|
+
Entry = Struct.new(:key, :event_type, :module_name, :formats, :if, :unless, :metadata, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
# The DSL of Optiaudit.audit.
|
|
26
|
+
class Builder
|
|
27
|
+
def initialize(config)
|
|
28
|
+
@config = config
|
|
29
|
+
@entries = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def page_view(key, **options)
|
|
33
|
+
event(:page_view, key, **options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Options: module (required), formats, if, unless, metadata. if/unless
|
|
37
|
+
# take a controller method name or a lambda that receives the controller.
|
|
38
|
+
def event(event_type, key, module: nil, formats: nil, if: nil, unless: nil, metadata: nil)
|
|
39
|
+
module_name = binding.local_variable_get(:module)
|
|
40
|
+
controller_path, action = parse_key(key)
|
|
41
|
+
validate!(key, event_type.to_s, module_name)
|
|
42
|
+
|
|
43
|
+
actions = (@entries[controller_path] ||= {})
|
|
44
|
+
raise ArgumentError, "#{key} declared twice in the Optiaudit registry" if actions.key?(action)
|
|
45
|
+
|
|
46
|
+
actions[action] = Entry.new(
|
|
47
|
+
key: key, event_type: event_type.to_s, module_name: module_name.to_s,
|
|
48
|
+
formats: formats && Array(formats).map(&:to_sym).freeze,
|
|
49
|
+
if: binding.local_variable_get(:if), unless: binding.local_variable_get(:unless), metadata: metadata
|
|
50
|
+
).freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build
|
|
54
|
+
Registry.new(@entries.transform_values(&:freeze).freeze)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def parse_key(key)
|
|
60
|
+
controller_path, action = key.to_s.split("#", 2)
|
|
61
|
+
if Util.blank?(controller_path) || Util.blank?(action)
|
|
62
|
+
raise ArgumentError, %(#{key.inspect} is not "controller_path#action")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
[controller_path.freeze, action.freeze]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate!(key, event_type, module_name)
|
|
69
|
+
raise ArgumentError, "#{key}: module is required" if Util.blank?(module_name)
|
|
70
|
+
return if @config.event_types.map(&:to_s).include?(event_type)
|
|
71
|
+
|
|
72
|
+
raise ArgumentError, "#{key}: unknown event_type #{event_type.inspect}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Loads the app's list (config/optiaudit.rb). Returns false when there is none.
|
|
77
|
+
def self.load_file(path)
|
|
78
|
+
return false unless File.exist?(path)
|
|
79
|
+
|
|
80
|
+
load path.to_s
|
|
81
|
+
true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def initialize(entries)
|
|
85
|
+
@entries = entries
|
|
86
|
+
freeze
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def lookup(controller_path, action_name)
|
|
90
|
+
actions = @entries[controller_path]
|
|
91
|
+
actions && actions[action_name]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def empty?
|
|
95
|
+
@entries.empty?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def keys
|
|
99
|
+
@entries.flat_map { |_, actions| actions.values.map(&:key) }.sort
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Checks every entry against the app routes, so that renaming a controller
|
|
103
|
+
# does not silently stop auditing it. Strict mode raises; otherwise each
|
|
104
|
+
# missing action is logged. Returns the keys without a route.
|
|
105
|
+
def verify!(routes, strict:)
|
|
106
|
+
routed = routes.routes.each_with_object({}) do |route, set|
|
|
107
|
+
controller, action = route.defaults.values_at(:controller, :action)
|
|
108
|
+
set["#{controller}##{action}"] = true if controller && action
|
|
109
|
+
end
|
|
110
|
+
missing = keys.reject { |key| routed.key?(key) }
|
|
111
|
+
return missing if missing.empty?
|
|
112
|
+
|
|
113
|
+
raise InvalidRegistry, "Optiaudit registry entries without a route: #{missing.join(', ')}" if strict
|
|
114
|
+
|
|
115
|
+
missing.each { |key| Optiaudit.log_warning("#{key} has no route: that action is not being audited") }
|
|
116
|
+
missing
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
EMPTY = new({}.freeze)
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/optiaudit/testing.rb
CHANGED
|
@@ -49,5 +49,13 @@ module Optiaudit
|
|
|
49
49
|
def clear!
|
|
50
50
|
events.clear
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
# For the app's test suite: fails when an entry of config/optiaudit.rb has
|
|
54
|
+
# no route.
|
|
55
|
+
#
|
|
56
|
+
# it("audits only routed actions") { Optiaudit::Testing.verify_registry! }
|
|
57
|
+
def verify_registry!
|
|
58
|
+
Optiaudit.registry.verify!(::Rails.application.routes, strict: true)
|
|
59
|
+
end
|
|
52
60
|
end
|
|
53
61
|
end
|
data/lib/optiaudit/version.rb
CHANGED
data/lib/optiaudit.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "optiaudit/event"
|
|
|
9
9
|
require "optiaudit/client"
|
|
10
10
|
require "optiaudit/actor"
|
|
11
11
|
require "optiaudit/request_data"
|
|
12
|
+
require "optiaudit/registry"
|
|
12
13
|
require "optiaudit/testing"
|
|
13
14
|
|
|
14
15
|
# Sends audit events to OptiAudit. See README.md.
|
|
@@ -35,6 +36,19 @@ module Optiaudit
|
|
|
35
36
|
Testing.fake? || configuration.enabled?
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# Declares the actions the app audits (see Optiaudit::Registry). Called from
|
|
40
|
+
# config/optiaudit.rb, which the Railtie loads at boot. Replaces any previous
|
|
41
|
+
# declaration.
|
|
42
|
+
def audit(&block)
|
|
43
|
+
builder = Registry::Builder.new(configuration)
|
|
44
|
+
builder.instance_exec(&block)
|
|
45
|
+
@registry = builder.build
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def registry
|
|
49
|
+
@registry || Registry::EMPTY
|
|
50
|
+
end
|
|
51
|
+
|
|
38
52
|
# Tracks an event. Returns true when it was handed to the adapter, false when
|
|
39
53
|
# it was skipped (disabled, invalid) or failed. Never raises.
|
|
40
54
|
#
|
|
@@ -80,6 +94,7 @@ module Optiaudit
|
|
|
80
94
|
def reset!
|
|
81
95
|
discard_adapter
|
|
82
96
|
@configuration = nil
|
|
97
|
+
@registry = nil
|
|
83
98
|
end
|
|
84
99
|
|
|
85
100
|
private
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: optiaudit-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Optimal Partner
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- lib/optiaudit/event.rb
|
|
51
51
|
- lib/optiaudit/rails.rb
|
|
52
52
|
- lib/optiaudit/railtie.rb
|
|
53
|
+
- lib/optiaudit/registry.rb
|
|
53
54
|
- lib/optiaudit/request_data.rb
|
|
54
55
|
- lib/optiaudit/sidekiq_job.rb
|
|
55
56
|
- lib/optiaudit/testing.rb
|