upkeep-rails 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.

Potentially problematic release.


This version of upkeep-rails might be problematic. Click here for more details.

Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +424 -0
  4. data/docs/architecture/ambient-inputs-roadmap.md +306 -0
  5. data/docs/architecture/herb-roadmap.md +324 -0
  6. data/docs/architecture/identity-and-sharing.md +187 -0
  7. data/docs/architecture/query-dependencies.md +230 -0
  8. data/docs/cost-model-roadmap.md +703 -0
  9. data/docs/guides/getting-started.md +282 -0
  10. data/docs/handoff-2026-05-15.md +230 -0
  11. data/docs/production_roadmap.md +372 -0
  12. data/docs/shared-warm-scale-roadmap.md +214 -0
  13. data/docs/single-subscriber-cold-roadmap.md +192 -0
  14. data/docs/testing.md +113 -0
  15. data/lib/generators/upkeep/install/install_generator.rb +90 -0
  16. data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +31 -0
  17. data/lib/generators/upkeep/install/templates/subscription.js +107 -0
  18. data/lib/generators/upkeep/install/templates/upkeep.rb +6 -0
  19. data/lib/upkeep/active_record_query.rb +294 -0
  20. data/lib/upkeep/capture/request.rb +150 -0
  21. data/lib/upkeep/dag/subscription_shape.rb +244 -0
  22. data/lib/upkeep/dag.rb +370 -0
  23. data/lib/upkeep/delivery/action_cable_adapter.rb +43 -0
  24. data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
  25. data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
  26. data/lib/upkeep/delivery/transport.rb +194 -0
  27. data/lib/upkeep/delivery/turbo_streams.rb +275 -0
  28. data/lib/upkeep/delivery.rb +7 -0
  29. data/lib/upkeep/dependencies.rb +466 -0
  30. data/lib/upkeep/herb/developer_report.rb +116 -0
  31. data/lib/upkeep/herb/manifest_cache.rb +83 -0
  32. data/lib/upkeep/herb/manifest_diff.rb +183 -0
  33. data/lib/upkeep/herb/source_instrumenter.rb +84 -0
  34. data/lib/upkeep/herb/template_manifest.rb +377 -0
  35. data/lib/upkeep/invalidation/collection_append.rb +84 -0
  36. data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
  37. data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
  38. data/lib/upkeep/invalidation/collection_remove.rb +57 -0
  39. data/lib/upkeep/invalidation/planner.rb +341 -0
  40. data/lib/upkeep/invalidation.rb +7 -0
  41. data/lib/upkeep/rails/action_view_capture.rb +765 -0
  42. data/lib/upkeep/rails/cable/channel.rb +108 -0
  43. data/lib/upkeep/rails/cable/subscriber_identity.rb +214 -0
  44. data/lib/upkeep/rails/cable.rb +4 -0
  45. data/lib/upkeep/rails/client_subscription.rb +37 -0
  46. data/lib/upkeep/rails/configuration.rb +57 -0
  47. data/lib/upkeep/rails/controller_runtime.rb +137 -0
  48. data/lib/upkeep/rails/install.rb +28 -0
  49. data/lib/upkeep/rails/railtie.rb +36 -0
  50. data/lib/upkeep/rails/replay.rb +176 -0
  51. data/lib/upkeep/rails/testing.rb +36 -0
  52. data/lib/upkeep/rails.rb +276 -0
  53. data/lib/upkeep/replay.rb +408 -0
  54. data/lib/upkeep/runtime.rb +1075 -0
  55. data/lib/upkeep/shared_streams.rb +72 -0
  56. data/lib/upkeep/subscriptions/active_record_store.rb +292 -0
  57. data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +291 -0
  58. data/lib/upkeep/subscriptions/active_registry.rb +93 -0
  59. data/lib/upkeep/subscriptions/async_durable_writer.rb +136 -0
  60. data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
  61. data/lib/upkeep/subscriptions/layered_reverse_index.rb +122 -0
  62. data/lib/upkeep/subscriptions/persistent_reverse_index.rb +144 -0
  63. data/lib/upkeep/subscriptions/registrar.rb +36 -0
  64. data/lib/upkeep/subscriptions/reverse_index.rb +294 -0
  65. data/lib/upkeep/subscriptions/shape.rb +116 -0
  66. data/lib/upkeep/subscriptions/store.rb +159 -0
  67. data/lib/upkeep/subscriptions.rb +7 -0
  68. data/lib/upkeep/targeting.rb +135 -0
  69. data/lib/upkeep/version.rb +5 -0
  70. data/lib/upkeep-rails.rb +3 -0
  71. data/lib/upkeep.rb +14 -0
  72. data/upkeep-rails.gemspec +53 -0
  73. metadata +296 -0
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable"
4
+ require "active_support/notifications"
5
+
6
+ module Upkeep
7
+ module Rails
8
+ module Cable
9
+ class Channel < ::ActionCable::Channel::Base
10
+ SUBSCRIBE_NOTIFICATION = "subscribe_channel.upkeep"
11
+
12
+ def subscribed
13
+ if ActiveSupport::Notifications.notifier.listening?(SUBSCRIBE_NOTIFICATION)
14
+ instrumented_subscribe
15
+ else
16
+ subscribe_without_instrumentation
17
+ end
18
+ end
19
+
20
+ def unsubscribed
21
+ Upkeep::Rails.subscriptions.unregister(subscription_id)
22
+ rescue KeyError, ActiveRecord::RecordNotFound
23
+ nil
24
+ end
25
+
26
+ private
27
+
28
+ def instrumented_subscribe
29
+ payload = { subscription_id: safe_subscription_id }
30
+ ActiveSupport::Notifications.instrument(SUBSCRIBE_NOTIFICATION, payload) do
31
+ subscribe_without_instrumentation(payload: payload)
32
+ end
33
+ end
34
+
35
+ def subscribe_without_instrumentation(payload: nil)
36
+ id = subscription_id
37
+ payload[:subscription_id] = id if payload
38
+ subscription = measure(payload, :fetch_ms) { Upkeep::Rails.subscriptions.fetch(id) }
39
+ authorized = measure(payload, :authorization_ms) { authorized_subscription?(subscription) }
40
+ unless authorized
41
+ payload[:rejected] = true if payload
42
+ return reject
43
+ end
44
+
45
+ measure(payload, :activation_ms) { Upkeep::Rails.subscriptions.activate(id) }
46
+ stream_count = measure(payload, :stream_attach_ms) { attach_streams(subscription) }
47
+ payload[:stream_count] = stream_count if payload
48
+ rescue KeyError, ActiveRecord::RecordNotFound, UnidentifiedSubscriber
49
+ payload[:rejected] = true if payload
50
+ reject
51
+ end
52
+
53
+ def attach_streams(subscription)
54
+ stream_from stream_name_for(subscription)
55
+ count = 1
56
+ shared_stream_names_for(subscription).each do |stream_name|
57
+ stream_from stream_name
58
+ count += 1
59
+ end
60
+ count
61
+ end
62
+
63
+ def measure(payload, key)
64
+ return yield unless payload
65
+
66
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
67
+ yield
68
+ ensure
69
+ payload[key] = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0).round(3) if payload && started_at
70
+ end
71
+
72
+ def safe_subscription_id
73
+ subscription_id
74
+ rescue KeyError
75
+ nil
76
+ end
77
+
78
+ def subscription_id
79
+ params.fetch(:subscription_id)
80
+ end
81
+
82
+ def authorized_subscription?(subscription)
83
+ return true if anonymous_public_subscription?(subscription)
84
+ return true unless metadata_value(subscription, :identity_mode)
85
+
86
+ SubscriberIdentity.derive_all(connection)
87
+ .any? { |identity| identity.subscriber_id == subscription.subscriber_id }
88
+ end
89
+
90
+ def anonymous_public_subscription?(subscription)
91
+ metadata_value(subscription, :identity_mode) == SubscriberIdentity::ANONYMOUS_PUBLIC_MODE
92
+ end
93
+
94
+ def stream_name_for(subscription)
95
+ metadata_value(subscription, :stream_name) || subscription.metadata.fetch(:stream_name)
96
+ end
97
+
98
+ def shared_stream_names_for(subscription)
99
+ metadata_value(subscription, :shared_stream_names) || []
100
+ end
101
+
102
+ def metadata_value(subscription, key)
103
+ subscription.metadata[key] || subscription.metadata[key.to_s]
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "securerandom"
6
+
7
+ module Upkeep
8
+ module Rails
9
+ module Cable
10
+ class UnidentifiedSubscriber < StandardError; end
11
+
12
+ Identity = Data.define(:subscriber_id, :stream_name, :components)
13
+ Decision = Data.define(:mode, :anonymous, :deopt_reason, :identity_sources)
14
+
15
+ module SubscriberIdentity
16
+ ANONYMOUS_PUBLIC_MODE = "anonymous_public"
17
+ IDENTIFIED_MODE = "identified"
18
+ CONNECTION_IDENTITY_SOURCES = %w[Current.user cookie current_attribute session warden_user].freeze
19
+
20
+ module_function
21
+
22
+ def derive(connection)
23
+ derive_all(connection).last
24
+ end
25
+
26
+ def derive_all(connection)
27
+ identities = []
28
+ request_components = request_components(connection.request) if connection.respond_to?(:request)
29
+ identifier_components = identifier_components(connection)
30
+
31
+ identities << for_components(request_components) if request_components&.any?
32
+ identities << for_components(Array(request_components) + identifier_components) if identifier_components.any?
33
+ identities = identities.uniq(&:subscriber_id)
34
+
35
+ raise UnidentifiedSubscriber, "ActionCable connection has no server identifiers" if identities.empty?
36
+
37
+ identities
38
+ end
39
+
40
+ def derive_from_request(request, recorder:, decision: decision_for(request, recorder: recorder))
41
+ components = if decision.anonymous
42
+ anonymous_components
43
+ else
44
+ request_components(request) + recorder_components(recorder)
45
+ end
46
+
47
+ if components.empty?
48
+ raise UnidentifiedSubscriber,
49
+ "subscription has identity dependencies but no canonical request or recorder identity"
50
+ end
51
+
52
+ for_components(components)
53
+ end
54
+
55
+ def decision_for(_request = nil, recorder:)
56
+ dependencies = identity_dependencies(recorder)
57
+ if dependencies.empty?
58
+ Decision.new(ANONYMOUS_PUBLIC_MODE, true, nil, [])
59
+ else
60
+ Decision.new(
61
+ IDENTIFIED_MODE,
62
+ false,
63
+ "identity_dependencies_present",
64
+ dependencies.map { |dependency| dependency.source.to_s }.uniq.sort
65
+ )
66
+ end
67
+ end
68
+
69
+ def identifier_components(connection)
70
+ identifiers = Array(connection.identifiers)
71
+ identifiers.map { |name| component_for(name, connection.public_send(name)) }
72
+ end
73
+
74
+ def request_components(request)
75
+ session_id = session_id_for(request)
76
+ return [] unless session_id
77
+
78
+ [scalar_component(:rails_session, session_id)]
79
+ end
80
+
81
+ def for_identifiers(identifiers)
82
+ for_components(identifiers.map { |name, value| component_for(name, value) })
83
+ end
84
+
85
+ def for_components(components)
86
+ canonical_bytes = JSON.generate(components.sort_by { |component| component.fetch(:name) })
87
+ subscriber_id = "action_cable:#{Digest::SHA256.hexdigest(canonical_bytes)}"
88
+
89
+ Identity.new(
90
+ subscriber_id,
91
+ Delivery::ActionCableAdapter.stream_name_for(subscriber_id),
92
+ components
93
+ )
94
+ end
95
+
96
+ def recorder_components(recorder)
97
+ identity_dependencies(recorder)
98
+ .filter_map { |dependency| component_for_dependency(dependency) }
99
+ .uniq
100
+ end
101
+
102
+ def anonymous_components
103
+ [ scalar_component(:anonymous_public_subscription, SecureRandom.uuid) ]
104
+ end
105
+
106
+ def identity_dependencies(recorder)
107
+ return [] unless recorder
108
+
109
+ recorder.graph.dependency_nodes
110
+ .map(&:payload)
111
+ .select(&:identity?)
112
+ .select { |dependency| connection_identity_dependency?(dependency) }
113
+ .uniq(&:cache_key)
114
+ end
115
+
116
+ def connection_identity_dependency?(dependency)
117
+ CONNECTION_IDENTITY_SOURCES.include?(dependency.source.to_s)
118
+ end
119
+
120
+ def component_for_dependency(dependency)
121
+ if dependency.source == :current_attribute && current_user_dependency?(dependency)
122
+ model_component(:current_user, dependency.key.fetch(:value))
123
+ elsif dependency.source == "Current.user"
124
+ model_component(:current_user, dependency.metadata)
125
+ elsif dependency.source == :warden_user
126
+ model_component(:"warden_#{dependency.metadata.fetch(:scope)}", dependency.key.fetch(:value))
127
+ end
128
+ end
129
+
130
+ def current_user_dependency?(dependency)
131
+ dependency.metadata.fetch(:name) == "user"
132
+ end
133
+
134
+ def model_component(name, identity)
135
+ return unless identity.is_a?(Hash) && identity[:model] && identity[:id]
136
+
137
+ {
138
+ name: name.to_s,
139
+ kind: "model",
140
+ model: identity.fetch(:model),
141
+ id: identity.fetch(:id).to_s
142
+ }
143
+ end
144
+
145
+ def component_for(name, value)
146
+ raise UnidentifiedSubscriber, "ActionCable identifier #{name} is nil" if value.nil?
147
+
148
+ if active_record?(value)
149
+ active_record_component(name, value)
150
+ elsif scalar?(value)
151
+ scalar_component(name, value)
152
+ elsif value.respond_to?(:to_gid_param)
153
+ global_id_component(name, value)
154
+ else
155
+ raise UnidentifiedSubscriber, "ActionCable identifier #{name} has no canonical identity"
156
+ end
157
+ end
158
+
159
+ def active_record?(value)
160
+ defined?(::ActiveRecord::Base) && value.is_a?(::ActiveRecord::Base)
161
+ end
162
+
163
+ def active_record_component(name, record)
164
+ raise UnidentifiedSubscriber, "ActionCable identifier #{name} is an unsaved record" unless record.id
165
+
166
+ model_component(name, model: record.class.name, id: record.id)
167
+ end
168
+
169
+ def scalar?(value)
170
+ value.is_a?(String) ||
171
+ value.is_a?(Symbol) ||
172
+ value.is_a?(Integer) ||
173
+ value == true ||
174
+ value == false
175
+ end
176
+
177
+ def scalar_component(name, value)
178
+ {
179
+ name: name.to_s,
180
+ kind: "scalar",
181
+ class: value.class.name,
182
+ value: value.to_s
183
+ }
184
+ end
185
+
186
+ def global_id_component(name, value)
187
+ {
188
+ name: name.to_s,
189
+ kind: "global_id",
190
+ value: value.to_gid_param
191
+ }
192
+ end
193
+
194
+ def session_id_for(request)
195
+ return unless request&.respond_to?(:session)
196
+
197
+ session = request.session
198
+ session_id = session.id if session.respond_to?(:id)
199
+ session_id = session_id.public_id if session_id.respond_to?(:public_id)
200
+ session_id = session_id.private_id if session_id.respond_to?(:private_id)
201
+ session_id = session[:session_id] if blank?(session_id) && session.respond_to?(:[])
202
+
203
+ session_id.to_s unless blank?(session_id)
204
+ rescue StandardError
205
+ nil
206
+ end
207
+
208
+ def blank?(value)
209
+ value.nil? || value == ""
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cable/subscriber_identity"
4
+ require_relative "cable/channel"
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Upkeep
6
+ module Rails
7
+ module ClientSubscription
8
+ CHANNEL = "Upkeep::Rails::Cable::Channel"
9
+
10
+ module_function
11
+
12
+ def inject(html, identity:, subscription:)
13
+ marker = marker_for(identity: identity, subscription: subscription)
14
+ insert_before_closing("head", html, marker) ||
15
+ insert_before_closing("body", html, marker) ||
16
+ "#{html}#{marker}"
17
+ end
18
+
19
+ def marker_for(identity:, subscription:)
20
+ payload = JSON.generate(
21
+ channel: CHANNEL,
22
+ subscription_id: subscription.id,
23
+ stream_name: identity.stream_name
24
+ ).gsub("</", '<\/')
25
+
26
+ %(<script type="application/json" data-upkeep-subscription>#{payload}</script>)
27
+ end
28
+
29
+ def insert_before_closing(tag, html, marker)
30
+ index = html.rindex(%(</#{tag}>)) || html.rindex(%(</#{tag.upcase}>))
31
+ return unless index
32
+
33
+ "#{html[0...index]}#{marker}#{html[index..]}"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ module Rails
5
+ class ConfigurationError < StandardError; end
6
+
7
+ class Configuration
8
+ SUBSCRIPTION_STORES = [:active_record, :memory].freeze
9
+ REFUSED_BOUNDARY_BEHAVIORS = [:raise, :warn].freeze
10
+
11
+ attr_accessor :enabled
12
+ attr_reader :subscription_store
13
+
14
+ def initialize
15
+ @enabled = true
16
+ @subscription_store = :active_record
17
+ @refused_boundary_behavior = nil
18
+ end
19
+
20
+ def subscription_store=(value)
21
+ value = value.to_sym if value.respond_to?(:to_sym)
22
+
23
+ unless SUBSCRIPTION_STORES.include?(value)
24
+ raise ConfigurationError,
25
+ "Unknown Upkeep subscription_store #{value.inspect}; expected one of #{SUBSCRIPTION_STORES.join(", ")}"
26
+ end
27
+
28
+ @subscription_store = value
29
+ end
30
+
31
+ def refused_boundary_behavior
32
+ @refused_boundary_behavior || default_refused_boundary_behavior
33
+ end
34
+
35
+ def refused_boundary_behavior=(value)
36
+ value = value.to_sym if value.respond_to?(:to_sym)
37
+
38
+ unless REFUSED_BOUNDARY_BEHAVIORS.include?(value)
39
+ raise ConfigurationError,
40
+ "Unknown Upkeep refused_boundary_behavior #{value.inspect}; expected one of #{REFUSED_BOUNDARY_BEHAVIORS.join(", ")}"
41
+ end
42
+
43
+ @refused_boundary_behavior = value
44
+ end
45
+
46
+ private
47
+
48
+ def default_refused_boundary_behavior
49
+ if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.to_s == "production"
50
+ :warn
51
+ else
52
+ :raise
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Upkeep
6
+ module Rails
7
+ module ControllerRuntime
8
+ extend ActiveSupport::Concern
9
+
10
+ SUPPRESS_KEY = :upkeep_rails_controller_runtime_suppressed
11
+
12
+ included do
13
+ prepend_around_action :upkeep_capture_request
14
+ end
15
+
16
+ module_function
17
+
18
+ def install
19
+ return if @installed
20
+ return unless defined?(::ActionController::Base)
21
+
22
+ ::ActionController::Base.include(self)
23
+ @installed = true
24
+ end
25
+
26
+ def installed?
27
+ !!@installed
28
+ end
29
+
30
+ def reset!
31
+ @installed = false
32
+ end
33
+
34
+ def suppress
35
+ previous = Thread.current[SUPPRESS_KEY]
36
+ Thread.current[SUPPRESS_KEY] = true
37
+ yield
38
+ ensure
39
+ Thread.current[SUPPRESS_KEY] = previous
40
+ end
41
+
42
+ def suppressed?
43
+ Thread.current[SUPPRESS_KEY]
44
+ end
45
+
46
+ private
47
+
48
+ def upkeep_capture_request(&action)
49
+ return action.call if ControllerRuntime.suppressed?
50
+ return action.call if Upkeep::Runtime::Observation.recorder
51
+
52
+ payload = {
53
+ controller: self.class.name,
54
+ action: action_name,
55
+ method: request.request_method,
56
+ path: request.fullpath,
57
+ subscription_request: upkeep_subscription_request?
58
+ }
59
+ ActiveSupport::Notifications.instrument(Upkeep::Rails::REQUEST_CAPTURE, payload) do
60
+ upkeep_capture_request_with_timing(action, payload)
61
+ end
62
+ end
63
+
64
+ def upkeep_capture_request_with_timing(action, payload)
65
+ measure_phase(payload, :deliver_pending_ms) { Upkeep::Rails.deliver_changes_now! }
66
+
67
+ result = nil
68
+ capture = nil
69
+ changes = []
70
+ measure_phase(payload, :change_capture_ms) do
71
+ _captured, changes = Upkeep::Runtime::ChangeLog.capture do
72
+ if payload.fetch(:subscription_request)
73
+ capture = Upkeep::Capture::Request.call(self, profile: request_capture_profile?) { action.call }
74
+ result = capture.action_result
75
+ else
76
+ measure_phase(payload, :action_ms) { result = action.call }
77
+ end
78
+ end
79
+ end
80
+ record_capture_payload(payload, capture) if capture
81
+
82
+ registration = nil
83
+ if capture
84
+ measure_phase(payload, :register_ms) do
85
+ registration = Upkeep::Rails.register_controller_subscription(self, capture)
86
+ end
87
+ end
88
+ payload[:registered] = !!registration
89
+ if capture && registration
90
+ measure_phase(payload, :inject_ms) do
91
+ response.body = Upkeep::Rails::ClientSubscription.inject(
92
+ capture.html,
93
+ identity: registration.identity,
94
+ subscription: registration.subscription
95
+ )
96
+ end
97
+ payload[:subscription_id] = registration.subscription.id
98
+ end
99
+ measure_phase(payload, :deliver_changes_ms) { Upkeep::Rails.deliver_changes!(changes) }
100
+
101
+ result
102
+ end
103
+
104
+ def record_capture_payload(payload, capture)
105
+ payload[:response_status] = capture.response_status
106
+ payload[:response_content_type] = capture.response_content_type
107
+ payload[:response_media_type] = capture.response_media_type
108
+ payload[:html_response] = capture.html_response?
109
+ payload[:response_successful] = capture.successful?
110
+ payload[:html_bytes] = capture.html.bytesize
111
+ payload[:graph_frames] = capture.recorder.graph.frame_nodes.size
112
+ payload[:graph_dependencies] = capture.recorder.graph.dependency_nodes.size
113
+ capture.timings.each do |phase, ms|
114
+ payload[:"capture_#{phase}"] = ms
115
+ end
116
+ capture.counters.each do |counter, value|
117
+ payload[:"capture_#{counter}"] = value
118
+ end
119
+ end
120
+
121
+ def measure_phase(payload, key)
122
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
123
+ yield
124
+ ensure
125
+ payload[key] = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0).round(3)
126
+ end
127
+
128
+ def upkeep_subscription_request?
129
+ request.get? || request.head?
130
+ end
131
+
132
+ def request_capture_profile?
133
+ ActiveSupport::Notifications.notifier.listening?(Upkeep::Rails::REQUEST_CAPTURE)
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ module Rails
5
+ module Install
6
+ module_function
7
+
8
+ def call
9
+ return unless Upkeep::Rails.configuration.enabled
10
+ return if @installed
11
+
12
+ Runtime::Install.call if defined?(::ActiveRecord::Base)
13
+ ActionViewCapture.install if defined?(::ActionView::Template)
14
+ ControllerRuntime.install if defined?(::ActionController::Base)
15
+
16
+ @installed = true
17
+ end
18
+
19
+ def installed?
20
+ !!@installed
21
+ end
22
+
23
+ def reset!
24
+ @installed = false
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ config.upkeep = ActiveSupport::OrderedOptions.new
7
+
8
+ initializer "upkeep_rails.configure" do |app|
9
+ Upkeep::Rails.configure do |config|
10
+ config.enabled = app.config.upkeep.fetch(:enabled, true)
11
+ config.subscription_store = app.config.upkeep.fetch(:subscription_store, config.subscription_store)
12
+ config.refused_boundary_behavior =
13
+ app.config.upkeep.fetch(:refused_boundary_behavior, config.refused_boundary_behavior)
14
+ end
15
+ end
16
+
17
+ initializer "upkeep_rails.install" do
18
+ ActiveSupport.on_load(:active_record) { Upkeep::Rails::Install.call }
19
+ ActiveSupport.on_load(:action_controller_base) { Upkeep::Rails::Install.call }
20
+ ActiveSupport.on_load(:action_view) { Upkeep::Rails::Install.call }
21
+ end
22
+
23
+ initializer "upkeep_rails.validate_configuration", after: "upkeep_rails.install" do |app|
24
+ app.config.after_initialize do
25
+ Upkeep::Rails.validate_configuration! unless Railtie.rake_task?
26
+ end
27
+ end
28
+
29
+ def self.rake_task?
30
+ defined?(::Rake) &&
31
+ ::Rake.respond_to?(:application) &&
32
+ ::Rake.application.top_level_tasks.any?
33
+ end
34
+ end
35
+ end
36
+ end