flipper_trail 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/CODE_OF_CONDUCT.md +89 -0
- data/CONTRIBUTING.md +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +139 -0
- data/SECURITY.md +29 -0
- data/lib/flipper_trail/actor.rb +51 -0
- data/lib/flipper_trail/adapter.rb +108 -0
- data/lib/flipper_trail/configuration.rb +96 -0
- data/lib/flipper_trail/current.rb +13 -0
- data/lib/flipper_trail/entry.rb +31 -0
- data/lib/flipper_trail/generators/flipper_trail/install_generator.rb +31 -0
- data/lib/flipper_trail/generators/flipper_trail/templates/initializer.rb.tt +28 -0
- data/lib/flipper_trail/generators/flipper_trail/templates/migration.rb.tt +21 -0
- data/lib/flipper_trail/middleware.rb +43 -0
- data/lib/flipper_trail/railtie.rb +11 -0
- data/lib/flipper_trail/recorder.rb +65 -0
- data/lib/flipper_trail/storage/active_record.rb +57 -0
- data/lib/flipper_trail/storage/mongoid.rb +70 -0
- data/lib/flipper_trail/version.rb +5 -0
- data/lib/flipper_trail.rb +98 -0
- data/sig/flipper_trail.rbs +68 -0
- metadata +281 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlipperTrail
|
|
4
|
+
# An immutable record of a single feature-flag change: which feature and gate,
|
|
5
|
+
# the operation, the before/after state, the {Actor}, and when it happened.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [rw] feature_name
|
|
8
|
+
# @return [String] the feature key
|
|
9
|
+
# @!attribute [rw] operation
|
|
10
|
+
# @return [Symbol] the write performed (`:add`, `:remove`, `:clear`,
|
|
11
|
+
# `:enable`, `:disable`)
|
|
12
|
+
# @!attribute [rw] gate_name
|
|
13
|
+
# @return [String, nil] the gate affected, if any
|
|
14
|
+
# @!attribute [rw] before
|
|
15
|
+
# @return [Object] the gate state before the change
|
|
16
|
+
# @!attribute [rw] after
|
|
17
|
+
# @return [Object] the gate state after the change
|
|
18
|
+
# @!attribute [rw] actor
|
|
19
|
+
# @return [Actor, nil] who made the change
|
|
20
|
+
# @!attribute [rw] created_at
|
|
21
|
+
# @return [Time] when the change was recorded
|
|
22
|
+
Entry = Struct.new(
|
|
23
|
+
:feature_name, :operation, :gate_name, :before, :after, :actor, :created_at,
|
|
24
|
+
keyword_init: true
|
|
25
|
+
) do
|
|
26
|
+
# @return [Boolean] whether the change altered the feature's state
|
|
27
|
+
def changed?
|
|
28
|
+
before != after
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/migration'
|
|
5
|
+
require 'active_record'
|
|
6
|
+
|
|
7
|
+
module FlipperTrail
|
|
8
|
+
module Generators
|
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
10
|
+
include ::Rails::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path('templates', __dir__)
|
|
13
|
+
|
|
14
|
+
def self.next_migration_number(dirname)
|
|
15
|
+
next_number = current_migration_number(dirname) + 1
|
|
16
|
+
::ActiveRecord::Migration.next_migration_number(next_number)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_initializer
|
|
20
|
+
template 'initializer.rb.tt', 'config/initializers/flipper_trail.rb'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# NOTE: must NOT be named `create_migration` — Rails::Generators::Migration already defines an
|
|
24
|
+
# instance method `create_migration(destination, data, config)` that migration_template calls internally;
|
|
25
|
+
# a zero-arg Thor command of the same name shadows it and raises ArgumentError (given 3, expected 0).
|
|
26
|
+
def create_migration_file
|
|
27
|
+
migration_template 'migration.rb.tt', 'db/migrate/create_flipper_trail_entries.rb'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FlipperTrail.configure do |config|
|
|
4
|
+
# config.storage = :active_record # optional — inferred from the Flipper adapter you wrap; set to override
|
|
5
|
+
# Mongoid users: indexes are declared on the audit document but not auto-built. Create them once:
|
|
6
|
+
# bin/rails runner 'require "flipper_trail/storage/mongoid"; FlipperTrail::Storage::Mongoid::Entry.create_indexes'
|
|
7
|
+
# config.actor_resolver = -> { Current.user } # MUST be zero-arity (runs off-request too)
|
|
8
|
+
# config.system_actor = { type: "system", id: nil, label: "system" }
|
|
9
|
+
# config.ignored_features = []
|
|
10
|
+
# config.raise_on_audit_error = false # true = a failing audit write aborts the flag write
|
|
11
|
+
# config.on_error = ->(error, entry) { Rails.logger.error("[flipper_trail] #{error.message}") }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Wrap the FlipperTrail decorator around your real Flipper adapter, e.g.:
|
|
15
|
+
#
|
|
16
|
+
# require "flipper/adapters/active_record"
|
|
17
|
+
#
|
|
18
|
+
# Flipper.configure do |config|
|
|
19
|
+
# config.adapter { FlipperTrail.wrap(Flipper::Adapters::ActiveRecord.new) }
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Capture the acting user per request (e.g. in ApplicationController):
|
|
23
|
+
#
|
|
24
|
+
# before_action { FlipperTrail::Current.actor = current_user }
|
|
25
|
+
#
|
|
26
|
+
# In front of a mounted Flipper::UI, insert the middleware so UI toggles are attributed:
|
|
27
|
+
#
|
|
28
|
+
# config.middleware.use FlipperTrail::Middleware, resolver: ->(env) { ... }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateFlipperTrailEntries < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :flipper_trail_entries do |t|
|
|
6
|
+
t.string :feature_name, null: false
|
|
7
|
+
t.string :operation, null: false
|
|
8
|
+
t.string :gate_name
|
|
9
|
+
t.json :before_state
|
|
10
|
+
t.json :after_state
|
|
11
|
+
t.string :actor_type
|
|
12
|
+
t.string :actor_id
|
|
13
|
+
t.string :actor_label
|
|
14
|
+
t.datetime :created_at, null: false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :flipper_trail_entries, %i[feature_name created_at]
|
|
18
|
+
add_index :flipper_trail_entries, :created_at
|
|
19
|
+
add_index :flipper_trail_entries, %i[actor_id created_at]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'flipper_trail/current'
|
|
4
|
+
|
|
5
|
+
module FlipperTrail
|
|
6
|
+
# Rack middleware that resolves the current actor for the duration of a request
|
|
7
|
+
# and stores it on {Current}, so flag changes during that request are attributed
|
|
8
|
+
# to it.
|
|
9
|
+
#
|
|
10
|
+
# @example Insert into a Rails app
|
|
11
|
+
# config.middleware.use FlipperTrail::Middleware, resolver: ->(env) { env["warden"].user }
|
|
12
|
+
class Middleware
|
|
13
|
+
# @param app [#call] the next Rack app in the stack
|
|
14
|
+
# @param resolver [#call, nil] resolves the request's actor; may take the
|
|
15
|
+
# Rack env (called with it) or take no arguments (called zero-arity). When
|
|
16
|
+
# nil, falls back to the configured `actor_resolver`.
|
|
17
|
+
def initialize(app, resolver: nil)
|
|
18
|
+
@app = app
|
|
19
|
+
@resolver = resolver
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Sets the resolved actor on {Current} for the wrapped request.
|
|
23
|
+
#
|
|
24
|
+
# @param env [Hash] the Rack environment
|
|
25
|
+
# @return [Array(Integer, Hash, #each)] the Rack response
|
|
26
|
+
def call(env)
|
|
27
|
+
Current.set(actor: resolve(env)) { @app.call(env) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# The injected `resolver:` MAY take env (it runs inside a request). The shared
|
|
33
|
+
# config.actor_resolver is ALWAYS called zero-arity — it's the same fallback the Recorder
|
|
34
|
+
# uses off-request (console/job), so keeping one contract avoids an arity-mismatch crash there.
|
|
35
|
+
def resolve(env)
|
|
36
|
+
if @resolver
|
|
37
|
+
@resolver.arity.zero? ? @resolver.call : @resolver.call(env)
|
|
38
|
+
else
|
|
39
|
+
FlipperTrail.configuration.actor_resolver&.call
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'flipper_trail/actor'
|
|
4
|
+
require 'flipper_trail/entry'
|
|
5
|
+
|
|
6
|
+
module FlipperTrail
|
|
7
|
+
# Builds an {Entry} from a flag change and persists it through the configured
|
|
8
|
+
# storage backend. No-op changes (`before == after`) and ignored features are
|
|
9
|
+
# dropped.
|
|
10
|
+
class Recorder
|
|
11
|
+
# Records a single flag change as an audit entry.
|
|
12
|
+
#
|
|
13
|
+
# @param feature_name [String, Symbol] the feature key
|
|
14
|
+
# @param operation [Symbol] the write performed (`:add`, `:remove`,
|
|
15
|
+
# `:clear`, `:enable`, `:disable`)
|
|
16
|
+
# @param gate_name [String, Symbol, nil] the gate affected, if any
|
|
17
|
+
# @param before [Object] the feature's gate state before the change
|
|
18
|
+
# @param after [Object] the feature's gate state after the change
|
|
19
|
+
# @return [Entry, nil] the persisted entry, or nil when the change was a
|
|
20
|
+
# no-op or the feature is ignored
|
|
21
|
+
def record(feature_name:, operation:, gate_name:, before:, after:)
|
|
22
|
+
feature_name = feature_name.to_s
|
|
23
|
+
return if config.ignored_features.map(&:to_s).include?(feature_name)
|
|
24
|
+
return if before == after
|
|
25
|
+
|
|
26
|
+
entry = Entry.new(
|
|
27
|
+
feature_name: feature_name,
|
|
28
|
+
operation: operation,
|
|
29
|
+
gate_name: gate_name&.to_s,
|
|
30
|
+
before: before,
|
|
31
|
+
after: after,
|
|
32
|
+
actor: current_actor,
|
|
33
|
+
created_at: Time.now
|
|
34
|
+
).freeze
|
|
35
|
+
persist(entry)
|
|
36
|
+
entry
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Resolves the actor to attribute a change to, trying the per-request
|
|
40
|
+
# {Current} actor, then the configured `actor_resolver`, then the system actor.
|
|
41
|
+
#
|
|
42
|
+
# @return [Actor, nil] the wrapped actor, or nil when none resolves
|
|
43
|
+
def current_actor
|
|
44
|
+
raw = Current.actor || config.actor_resolver&.call || config.system_actor
|
|
45
|
+
Actor.wrap(raw)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Audit-write failures must never break the flag write that already succeeded.
|
|
51
|
+
# Swallow + route to on_error by default; opt into fail-closed via raise_on_audit_error.
|
|
52
|
+
def persist(entry)
|
|
53
|
+
config.storage_backend.record(entry)
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
raise if config.raise_on_audit_error
|
|
56
|
+
|
|
57
|
+
handler = config.on_error
|
|
58
|
+
handler ? handler.call(e, entry) : warn("[flipper_trail] audit write failed: #{e.class}: #{e.message}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def config
|
|
62
|
+
FlipperTrail.configuration
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_record'
|
|
4
|
+
require 'flipper_trail/entry'
|
|
5
|
+
require 'flipper_trail/actor'
|
|
6
|
+
|
|
7
|
+
module FlipperTrail
|
|
8
|
+
module Storage
|
|
9
|
+
class ActiveRecord
|
|
10
|
+
class Entry < ::ActiveRecord::Base
|
|
11
|
+
self.table_name = 'flipper_trail_entries'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def record(entry)
|
|
15
|
+
Entry.create!(
|
|
16
|
+
feature_name: entry.feature_name,
|
|
17
|
+
operation: entry.operation.to_s,
|
|
18
|
+
gate_name: entry.gate_name,
|
|
19
|
+
before_state: entry.before,
|
|
20
|
+
after_state: entry.after,
|
|
21
|
+
actor_type: entry.actor&.type,
|
|
22
|
+
actor_id: entry.actor&.id,
|
|
23
|
+
actor_label: entry.actor&.label,
|
|
24
|
+
created_at: entry.created_at
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def query(feature: nil, actor_id: nil, since: nil, until_time: nil, limit: 100)
|
|
29
|
+
scope = Entry.all
|
|
30
|
+
scope = scope.where(feature_name: feature.to_s) if feature
|
|
31
|
+
scope = scope.where(actor_id: actor_id.to_s) if actor_id
|
|
32
|
+
scope = scope.where(arel_table[:created_at].gteq(since)) if since
|
|
33
|
+
scope = scope.where(arel_table[:created_at].lteq(until_time)) if until_time
|
|
34
|
+
# id DESC is a deterministic tiebreaker so bursts with equal created_at stay newest-first.
|
|
35
|
+
scope.order(created_at: :desc, id: :desc).limit(limit).map { |row| to_entry(row) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def arel_table
|
|
41
|
+
Entry.arel_table
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_entry(row)
|
|
45
|
+
FlipperTrail::Entry.new(
|
|
46
|
+
feature_name: row.feature_name,
|
|
47
|
+
operation: row.operation.to_sym,
|
|
48
|
+
gate_name: row.gate_name,
|
|
49
|
+
before: row.before_state,
|
|
50
|
+
after: row.after_state,
|
|
51
|
+
actor: FlipperTrail::Actor.new(type: row.actor_type, id: row.actor_id, label: row.actor_label),
|
|
52
|
+
created_at: row.created_at
|
|
53
|
+
).freeze
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mongoid'
|
|
4
|
+
require 'flipper_trail/entry'
|
|
5
|
+
require 'flipper_trail/actor'
|
|
6
|
+
|
|
7
|
+
module FlipperTrail
|
|
8
|
+
module Storage
|
|
9
|
+
class Mongoid
|
|
10
|
+
class Entry
|
|
11
|
+
include ::Mongoid::Document
|
|
12
|
+
|
|
13
|
+
store_in collection: 'flipper_trail_entries'
|
|
14
|
+
|
|
15
|
+
field :feature_name, type: String
|
|
16
|
+
field :operation, type: String
|
|
17
|
+
field :gate_name, type: String
|
|
18
|
+
field :before_state, type: Hash
|
|
19
|
+
field :after_state, type: Hash
|
|
20
|
+
field :actor_type, type: String
|
|
21
|
+
field :actor_id, type: String
|
|
22
|
+
field :actor_label, type: String
|
|
23
|
+
field :created_at, type: Time
|
|
24
|
+
|
|
25
|
+
index({ feature_name: 1, created_at: -1 })
|
|
26
|
+
index({ created_at: -1 })
|
|
27
|
+
index({ actor_id: 1, created_at: -1 })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def record(entry)
|
|
31
|
+
Entry.create!(
|
|
32
|
+
feature_name: entry.feature_name,
|
|
33
|
+
operation: entry.operation.to_s,
|
|
34
|
+
gate_name: entry.gate_name,
|
|
35
|
+
before_state: entry.before,
|
|
36
|
+
after_state: entry.after,
|
|
37
|
+
actor_type: entry.actor&.type,
|
|
38
|
+
actor_id: entry.actor&.id,
|
|
39
|
+
actor_label: entry.actor&.label,
|
|
40
|
+
created_at: entry.created_at
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def query(feature: nil, actor_id: nil, since: nil, until_time: nil, limit: 100)
|
|
45
|
+
scope = Entry.all
|
|
46
|
+
scope = scope.where(feature_name: feature.to_s) if feature
|
|
47
|
+
scope = scope.where(actor_id: actor_id.to_s) if actor_id
|
|
48
|
+
scope = scope.gte(created_at: since) if since
|
|
49
|
+
scope = scope.lte(created_at: until_time) if until_time
|
|
50
|
+
# _id DESC is a deterministic tiebreaker (BSON ObjectId is creation-ordered)
|
|
51
|
+
# so equal-timestamp bursts stay newest-first.
|
|
52
|
+
scope.order_by(created_at: :desc, _id: :desc).limit(limit).map { |doc| to_entry(doc) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def to_entry(doc)
|
|
58
|
+
FlipperTrail::Entry.new(
|
|
59
|
+
feature_name: doc.feature_name,
|
|
60
|
+
operation: doc.operation.to_sym,
|
|
61
|
+
gate_name: doc.gate_name,
|
|
62
|
+
before: doc.before_state,
|
|
63
|
+
after: doc.after_state,
|
|
64
|
+
actor: FlipperTrail::Actor.new(type: doc.actor_type, id: doc.actor_id, label: doc.actor_label),
|
|
65
|
+
created_at: doc.created_at
|
|
66
|
+
).freeze
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'flipper_trail/version'
|
|
4
|
+
require 'flipper_trail/configuration'
|
|
5
|
+
require 'flipper_trail/current'
|
|
6
|
+
require 'flipper_trail/actor'
|
|
7
|
+
require 'flipper_trail/entry'
|
|
8
|
+
require 'flipper_trail/recorder'
|
|
9
|
+
require 'flipper_trail/adapter'
|
|
10
|
+
require 'flipper_trail/middleware'
|
|
11
|
+
|
|
12
|
+
# Records an append-only audit trail of Flipper feature-flag changes — who
|
|
13
|
+
# changed what, when, and the before/after state — via an adapter decorator.
|
|
14
|
+
# {configure} it once, then query it with {history}.
|
|
15
|
+
module FlipperTrail
|
|
16
|
+
class << self
|
|
17
|
+
# The memoized global configuration.
|
|
18
|
+
#
|
|
19
|
+
# @return [Configuration] the shared configuration instance
|
|
20
|
+
def configuration
|
|
21
|
+
@configuration ||= Configuration.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The memoized global recorder that builds and persists audit entries.
|
|
25
|
+
#
|
|
26
|
+
# @return [Recorder] the shared recorder instance
|
|
27
|
+
def recorder
|
|
28
|
+
@recorder ||= Recorder.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Wrap a Flipper adapter so every write through it is audited.
|
|
32
|
+
#
|
|
33
|
+
# Shorthand for FlipperTrail::Adapter.new(adapter); pass the result to Flipper's config.adapter.
|
|
34
|
+
#
|
|
35
|
+
# @param adapter [Object] the real Flipper adapter to decorate
|
|
36
|
+
# @param recorder [FlipperTrail::Recorder, nil] optional recorder override (mainly for tests)
|
|
37
|
+
# @return [FlipperTrail::Adapter] the audit decorator to give to +config.adapter+
|
|
38
|
+
# @example
|
|
39
|
+
# Flipper.configure { |c| c.adapter { FlipperTrail.wrap(Flipper::Adapters::ActiveRecord.new) } }
|
|
40
|
+
def wrap(adapter, recorder: nil)
|
|
41
|
+
Adapter.new(adapter, recorder: recorder)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Configures the gem by yielding the global {Configuration}.
|
|
45
|
+
#
|
|
46
|
+
# @yieldparam config [Configuration] the configuration to mutate
|
|
47
|
+
# @return [void]
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# FlipperTrail.configure do |config|
|
|
51
|
+
# config.storage = :active_record
|
|
52
|
+
# config.actor_resolver = -> { Current.user }
|
|
53
|
+
# end
|
|
54
|
+
def configure
|
|
55
|
+
yield configuration
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Queries the audit trail through the configured storage backend.
|
|
59
|
+
#
|
|
60
|
+
# @param filters [Hash] optional filters
|
|
61
|
+
# @option filters [String] :feature only entries for this feature key
|
|
62
|
+
# @option filters [String] :actor_id only entries by this actor id
|
|
63
|
+
# @option filters [Time] :since only entries created at or after this time
|
|
64
|
+
# @option filters [Time] :until only entries created at or before this time
|
|
65
|
+
# @option filters [Integer] :limit maximum number of entries (default 100)
|
|
66
|
+
# @return [Array<Entry>] the matching audit entries, newest first
|
|
67
|
+
#
|
|
68
|
+
# @example
|
|
69
|
+
# FlipperTrail.history(feature: "search", limit: 10)
|
|
70
|
+
def history(**filters)
|
|
71
|
+
configuration.storage_backend.query(
|
|
72
|
+
feature: filters[:feature],
|
|
73
|
+
actor_id: filters[:actor_id],
|
|
74
|
+
since: filters[:since],
|
|
75
|
+
until_time: filters[:until],
|
|
76
|
+
limit: filters[:limit] || 100
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The actor the recorder would attribute a change to right now.
|
|
81
|
+
#
|
|
82
|
+
# @return [Actor, nil] the resolved actor, or nil when none is set
|
|
83
|
+
def current_actor
|
|
84
|
+
recorder.current_actor
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Resets the memoized configuration, recorder, and per-request actor.
|
|
88
|
+
#
|
|
89
|
+
# @return [void]
|
|
90
|
+
def reset!
|
|
91
|
+
@configuration = nil
|
|
92
|
+
@recorder = nil
|
|
93
|
+
Current.reset if defined?(Current)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
require 'flipper_trail/railtie' if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# RBS type signatures for the public API of FlipperTrail.
|
|
2
|
+
#
|
|
3
|
+
# Only the public surface is described here. Everything that touches an external
|
|
4
|
+
# library (flipper, activerecord, mongoid, rack env, ActiveSupport::CurrentAttributes)
|
|
5
|
+
# is intentionally left as `untyped` — we do not chase typing the dependency surface.
|
|
6
|
+
|
|
7
|
+
module FlipperTrail
|
|
8
|
+
VERSION: String
|
|
9
|
+
|
|
10
|
+
def self.configure: () { (Configuration) -> void } -> void
|
|
11
|
+
def self.configuration: () -> Configuration
|
|
12
|
+
def self.history: (**untyped) -> Array[Entry]
|
|
13
|
+
def self.current_actor: () -> Actor
|
|
14
|
+
def self.recorder: () -> Recorder
|
|
15
|
+
def self.wrap: (untyped adapter, ?recorder: untyped) -> Adapter
|
|
16
|
+
def self.reset!: () -> void
|
|
17
|
+
|
|
18
|
+
class Configuration
|
|
19
|
+
attr_accessor storage: untyped
|
|
20
|
+
attr_accessor actor_resolver: untyped
|
|
21
|
+
attr_accessor system_actor: untyped
|
|
22
|
+
attr_accessor ignored_features: untyped
|
|
23
|
+
attr_accessor on_error: untyped
|
|
24
|
+
attr_accessor raise_on_audit_error: bool
|
|
25
|
+
|
|
26
|
+
def initialize: () -> void
|
|
27
|
+
def storage_backend: () -> untyped
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Entry
|
|
31
|
+
attr_reader feature_name: untyped
|
|
32
|
+
attr_reader operation: untyped
|
|
33
|
+
attr_reader gate_name: untyped
|
|
34
|
+
attr_reader before: untyped
|
|
35
|
+
attr_reader after: untyped
|
|
36
|
+
attr_reader actor: untyped
|
|
37
|
+
attr_reader created_at: untyped
|
|
38
|
+
|
|
39
|
+
def changed?: () -> bool
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class Actor
|
|
43
|
+
attr_reader type: untyped
|
|
44
|
+
attr_reader id: String?
|
|
45
|
+
attr_reader label: untyped
|
|
46
|
+
|
|
47
|
+
def initialize: (type: untyped, id: untyped, label: untyped) -> void
|
|
48
|
+
def self.wrap: (untyped) -> Actor?
|
|
49
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Current
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Adapter
|
|
56
|
+
def initialize: (untyped, ?recorder: untyped) -> void
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Middleware
|
|
60
|
+
def initialize: (untyped, ?resolver: untyped) -> void
|
|
61
|
+
def call: (untyped) -> untyped
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class Recorder
|
|
65
|
+
def record: (**untyped) -> untyped
|
|
66
|
+
def current_actor: () -> Actor
|
|
67
|
+
end
|
|
68
|
+
end
|