action_webhook 0.1.0 → 1.0.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/lib/action_webhook/base.rb +305 -301
- data/lib/action_webhook/delivery_job.rb +6 -4
- data/lib/action_webhook/version.rb +1 -1
- data/lib/action_webhook.rb +3 -1
- metadata +36 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7077d23d4f22c68b2a33a9cc9ee6e6ad911d9c9ee23a2a861bb93837c69910e7
|
4
|
+
data.tar.gz: 124749c8b109502308684d10e5a05791f99f35f26a0d01bdc1b3d11fd3ff43d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3be33f1a7a4528062882ea98e05caa9d61ef413bc72e4fba03788a7862e52b891f91897389e513f712fddaab7f7b5b68f4367b091f148df897a0548b25db556
|
7
|
+
data.tar.gz: 41d39d74c5ce2854375b1397552b687d64d9f3ad991eade8b92627b2f5efed8744de3fa092fe72926a244e50244a4363174b1ba91ad537bc0f485c94552d8a5c
|
data/lib/action_webhook/base.rb
CHANGED
@@ -22,410 +22,414 @@ module ActionWebhook
|
|
22
22
|
# # Send immediately
|
23
23
|
# UserWebhook.created(user).deliver_now
|
24
24
|
#
|
25
|
-
# # Send in background
|
25
|
+
# # Send in background (uses default queue)
|
26
26
|
# UserWebhook.created(user).deliver_later
|
27
27
|
#
|
28
|
+
# # Send in background with specific queue
|
29
|
+
# UserWebhook.created(user).deliver_later(queue: 'webhooks')
|
30
|
+
#
|
31
|
+
# # Send in background with delay
|
32
|
+
# UserWebhook.created(user).deliver_later(wait: 5.minutes)
|
33
|
+
#
|
34
|
+
# # Send in background with specific queue and delay
|
35
|
+
# UserWebhook.created(user).deliver_later(queue: 'webhooks', wait: 10.minutes)
|
36
|
+
#
|
37
|
+
# You can also configure the default queue at the class level:
|
38
|
+
#
|
39
|
+
# class UserWebhook < ActionWebhook::Base
|
40
|
+
# self.deliver_later_queue_name = 'webhooks'
|
41
|
+
#
|
42
|
+
# def created(user)
|
43
|
+
# @user = user
|
44
|
+
# endpoints = WebhookSubscription.where(event: 'user.created').map do |sub|
|
45
|
+
# { url: sub.url, headers: { 'Authorization' => "Bearer #{sub.token}" } }
|
46
|
+
# end
|
47
|
+
# deliver(endpoints)
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
28
51
|
class Base
|
29
|
-
# Add these lines near the top of your class
|
30
52
|
include GlobalID::Identification if defined?(GlobalID)
|
31
53
|
include ActiveJob::SerializationAdapter::ObjectSerializer if defined?(ActiveJob::SerializationAdapter)
|
32
54
|
|
33
55
|
# Delivery configuration
|
34
|
-
|
56
|
+
class_attribute :delivery_job, instance_writer: false, default: -> { "ActionWebhook::DeliveryJob".constantize }
|
35
57
|
class_attribute :deliver_later_queue_name, instance_writer: false
|
36
58
|
class_attribute :default_headers, instance_writer: false, default: {}
|
37
59
|
class_attribute :delivery_method, instance_writer: false, default: :deliver_now
|
38
60
|
class_attribute :perform_deliveries, instance_writer: false, default: true
|
39
|
-
|
61
|
+
|
40
62
|
# Retry configuration
|
41
63
|
class_attribute :max_retries, instance_writer: false, default: 3
|
42
64
|
class_attribute :retry_delay, instance_writer: false, default: 30.seconds
|
43
|
-
class_attribute :retry_backoff, instance_writer: false, default: :exponential
|
65
|
+
class_attribute :retry_backoff, instance_writer: false, default: :exponential
|
44
66
|
class_attribute :retry_jitter, instance_writer: false, default: 5.seconds
|
45
|
-
|
67
|
+
|
46
68
|
# Callbacks
|
47
69
|
class_attribute :after_deliver_callback, instance_writer: false
|
48
70
|
class_attribute :after_retries_exhausted_callback, instance_writer: false
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
# The webhook details (URLs and headers) that will be used
|
54
|
-
attr_accessor :webhook_details
|
55
|
-
|
56
|
-
# Stores the execution params for later delivery
|
57
|
-
attr_accessor :params
|
58
|
-
|
59
|
-
# Current attempt number for retries
|
60
|
-
attr_accessor :attempts
|
61
|
-
|
62
|
-
# Creates a new webhook and initializes its settings
|
71
|
+
|
72
|
+
attr_accessor :action_name, :webhook_details, :params, :attempts
|
73
|
+
|
63
74
|
def initialize
|
64
75
|
@_webhook_message = {}
|
65
76
|
@_webhook_defaults = {}
|
66
77
|
@attempts = 0
|
67
78
|
end
|
68
79
|
|
69
|
-
# Synchronously delivers the webhook
|
70
80
|
def deliver_now
|
71
81
|
@attempts += 1
|
72
82
|
response = process_webhook
|
73
|
-
|
74
|
-
# Call success callback if defined
|
83
|
+
|
75
84
|
if response.all? { |r| r[:success] }
|
76
85
|
invoke_callback(self.class.after_deliver_callback, response)
|
77
86
|
elsif @attempts < self.class.max_retries
|
78
|
-
# Schedule a retry with backoff
|
79
87
|
retry_with_backoff
|
80
88
|
else
|
81
|
-
# We've exhausted all retries
|
82
89
|
invoke_callback(self.class.after_retries_exhausted_callback, response)
|
83
90
|
end
|
84
|
-
|
85
|
-
response
|
86
|
-
end
|
87
91
|
|
88
|
-
|
89
|
-
def invoke_callback(callback, response)
|
90
|
-
return unless callback
|
91
|
-
|
92
|
-
case callback
|
93
|
-
when Symbol
|
94
|
-
send(callback, response)
|
95
|
-
when Proc
|
96
|
-
callback.call(self, response)
|
97
|
-
end
|
92
|
+
response
|
98
93
|
end
|
99
94
|
|
100
|
-
# Enqueues the webhook delivery via ActiveJob
|
101
95
|
def deliver_later(options = {})
|
102
96
|
enqueue_delivery(:deliver_now, options)
|
103
97
|
end
|
104
98
|
|
105
|
-
# Prepares the webhook for delivery using the current method as template name
|
106
|
-
# and instance variables as template data
|
107
|
-
#
|
108
|
-
# @param webhook_details [Array<Hash>] Array of hashes with :url and :headers keys
|
109
|
-
# @param params [Hash] Optional parameters to store for later delivery
|
110
|
-
# @return [DeliveryMessenger] A messenger object for further delivery options
|
111
99
|
def deliver(webhook_details, params = {})
|
112
|
-
# Determine action name from the caller
|
113
100
|
@action_name = caller_locations(1, 1)[0].label.to_sym
|
114
101
|
@webhook_details = webhook_details
|
115
102
|
@params = params
|
116
|
-
|
117
|
-
# Return self for chaining with delivery methods
|
103
|
+
|
118
104
|
DeliveryMessenger.new(self)
|
119
105
|
end
|
120
106
|
|
121
|
-
# Renders a template based on the action name
|
122
|
-
#
|
123
|
-
# @param variables [Hash] Optional variables to add to template context
|
124
|
-
# @return [Hash] The JSON payload
|
125
107
|
def build_payload(variables = {})
|
126
|
-
|
127
|
-
assigns = {}
|
128
|
-
|
129
|
-
# Extract instance variables
|
130
|
-
instance_variables.each do |ivar|
|
131
|
-
# Skip internal instance variables
|
132
|
-
next if ivar.to_s.start_with?("@_")
|
133
|
-
next if [:@action_name, :@webhook_details, :@params, :@attempts].include?(ivar)
|
134
|
-
|
135
|
-
# Add to assigns with symbol key (without @)
|
136
|
-
assigns[ivar.to_s[1..-1].to_sym] = instance_variable_get(ivar)
|
137
|
-
end
|
138
|
-
|
139
|
-
# Add passed variables
|
108
|
+
assigns = extract_instance_variables
|
140
109
|
assigns.merge!(variables)
|
141
|
-
|
142
|
-
# Render the template
|
143
110
|
generate_json_from_template(@action_name, assigns)
|
144
111
|
end
|
145
112
|
|
146
|
-
|
147
|
-
#
|
148
|
-
# @param webhook_details [Array<Hash>] An array of hashes containing `url` and `headers`
|
149
|
-
# @param payloads [Array<Hash>] One payload for each webhook
|
150
|
-
# @return [Array<Hash>] Array of response objects with status and body
|
151
|
-
def post_webhook(webhook_details, payloads)
|
113
|
+
def post_webhook(webhook_details, payload)
|
152
114
|
responses = []
|
153
|
-
|
154
|
-
webhook_details.
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
# Add attempt tracking in headers
|
166
|
-
headers["X-Webhook-Attempt"] = @attempts.to_s if @attempts > 0
|
167
|
-
|
168
|
-
response = HTTParty.post(
|
169
|
-
detail[:url],
|
170
|
-
body: payloads[idx].to_json,
|
171
|
-
headers: headers,
|
172
|
-
timeout: 10 # Add reasonable timeout
|
173
|
-
)
|
174
|
-
|
175
|
-
responses << {
|
176
|
-
success: response.success?,
|
177
|
-
status: response.code,
|
178
|
-
body: response.body,
|
179
|
-
url: detail[:url],
|
180
|
-
attempt: @attempts
|
181
|
-
}
|
182
|
-
rescue => e
|
183
|
-
responses << {
|
184
|
-
success: false,
|
185
|
-
error: e.message,
|
186
|
-
url: detail[:url],
|
187
|
-
attempt: @attempts
|
188
|
-
}
|
189
|
-
logger.error("Webhook delivery failed: #{e.message} for URL: #{detail[:url]} (Attempt #{@attempts})")
|
190
|
-
end
|
115
|
+
|
116
|
+
webhook_details.each do |detail|
|
117
|
+
detail[:headers] ||= {}
|
118
|
+
headers = build_headers(detail[:headers])
|
119
|
+
|
120
|
+
response = send_webhook_request(detail[:url], payload, headers)
|
121
|
+
responses << build_response_hash(response, detail[:url])
|
122
|
+
log_webhook_result(response, detail[:url])
|
123
|
+
rescue StandardError => e
|
124
|
+
responses << build_error_response_hash(e, detail[:url])
|
125
|
+
log_webhook_error(e, detail[:url])
|
191
126
|
end
|
192
|
-
|
127
|
+
|
193
128
|
responses
|
194
129
|
end
|
195
130
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
# Possible template locations
|
205
|
-
possible_paths = [
|
206
|
-
# Main app templates
|
207
|
-
File.join(Rails.root.to_s, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
|
208
|
-
|
209
|
-
# Engine templates
|
210
|
-
engine_root && File.join(engine_root, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
|
211
|
-
|
212
|
-
# Namespaced templates in engine
|
213
|
-
engine_root && self.class.module_parent != Object &&
|
214
|
-
File.join(engine_root, "app/webhooks/#{self.class.module_parent.name.underscore}/#{webhook_class_name.split('/').last}/#{event_name}.json.erb")
|
215
|
-
].compact
|
216
|
-
|
217
|
-
# Find the first template that exists
|
218
|
-
template_path = possible_paths.find { |path| File.exist?(path) }
|
219
|
-
|
220
|
-
unless template_path
|
221
|
-
raise ArgumentError, "Template not found for #{event_name} in paths:\n#{possible_paths.join("\n")}"
|
222
|
-
end
|
223
|
-
|
224
|
-
template = ERB.new(File.read(template_path))
|
225
|
-
json = template.result_with_hash(assigns)
|
226
|
-
JSON.parse(json)
|
131
|
+
def generate_json_from_template(input_event_name, assigns = {})
|
132
|
+
event_name = extract_method_name(input_event_name.to_s)
|
133
|
+
template_path = find_template_path(event_name)
|
134
|
+
|
135
|
+
raise ArgumentError, "Template not found for #{event_name}" unless template_path
|
136
|
+
|
137
|
+
render_json_template(template_path, assigns)
|
227
138
|
rescue JSON::ParserError => e
|
228
|
-
raise "Invalid JSON in template #{event_name}: #{e.message}"
|
139
|
+
raise ArgumentError, "Invalid JSON in template #{event_name}: #{e.message}"
|
229
140
|
end
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
# Render the message
|
234
|
-
payloads = [build_payload]
|
235
|
-
|
236
|
-
# Post the webhook
|
237
|
-
post_webhook(webhook_details, payloads)
|
238
|
-
end
|
239
|
-
|
240
|
-
# Schedule a retry with appropriate backoff delay
|
241
|
-
# Modify the retry_with_backoff method:
|
242
|
-
def retry_with_backoff
|
243
|
-
delay = calculate_backoff_delay
|
244
|
-
|
245
|
-
logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} in #{delay} seconds")
|
246
|
-
|
247
|
-
# Get the actual job class by evaluating the proc
|
248
|
-
job_class = self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
|
249
|
-
|
250
|
-
# Serialize the webhook and pass the serialized data instead of the object
|
251
|
-
serialized_webhook = serialize
|
252
|
-
|
253
|
-
# Re-enqueue with the calculated delay
|
254
|
-
if deliver_later_queue_name
|
255
|
-
job_class.set(queue: deliver_later_queue_name, wait: delay).perform_later('deliver_now', serialized_webhook)
|
256
|
-
else
|
257
|
-
job_class.set(wait: delay).perform_later('deliver_now', serialized_webhook)
|
258
|
-
end
|
141
|
+
|
142
|
+
def extract_method_name(path)
|
143
|
+
path.include?("#") ? path.split("#").last : path
|
259
144
|
end
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
case self.class.retry_backoff
|
266
|
-
when :exponential
|
267
|
-
# 30s, 60s, 120s, etc.
|
268
|
-
delay = base_delay * (2 ** (@attempts - 1))
|
269
|
-
when :linear
|
270
|
-
# 30s, 60s, 90s, etc.
|
271
|
-
delay = base_delay * @attempts
|
272
|
-
else
|
273
|
-
delay = base_delay
|
274
|
-
end
|
275
|
-
|
276
|
-
# Add jitter to prevent thundering herd problem
|
277
|
-
jitter = rand(self.class.retry_jitter)
|
278
|
-
delay + jitter
|
145
|
+
|
146
|
+
def process_webhook
|
147
|
+
payload = build_payload
|
148
|
+
post_webhook(webhook_details, payload)
|
279
149
|
end
|
280
|
-
|
281
|
-
# For ActiveJob serialization
|
150
|
+
|
282
151
|
def serialize
|
283
152
|
{
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
153
|
+
"action_name" => @action_name.to_s,
|
154
|
+
"webhook_details" => @webhook_details,
|
155
|
+
"params" => @params,
|
156
|
+
"attempts" => @attempts,
|
157
|
+
"instance_variables" => collect_instance_variables,
|
158
|
+
"webhook_class" => self.class.name
|
290
159
|
}
|
291
160
|
end
|
292
|
-
|
293
|
-
# Restores state from serialized data
|
161
|
+
|
294
162
|
def deserialize(data)
|
295
|
-
@action_name = data[
|
296
|
-
@webhook_details = data[
|
297
|
-
@params = data[
|
298
|
-
@attempts = data[
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
163
|
+
@action_name = data["action_name"].to_sym
|
164
|
+
@webhook_details = data["webhook_details"]
|
165
|
+
@params = data["params"]
|
166
|
+
@attempts = data["attempts"] || 0
|
167
|
+
|
168
|
+
restore_instance_variables(data["instance_variables"])
|
169
|
+
end
|
170
|
+
|
171
|
+
class << self
|
172
|
+
def method_missing(method_name, *args, &block)
|
173
|
+
if public_instance_methods(false).include?(method_name)
|
174
|
+
webhook = new
|
175
|
+
webhook.send(method_name, *args, &block)
|
176
|
+
else
|
177
|
+
super
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def respond_to_missing?(method_name, include_private = false)
|
182
|
+
public_instance_methods(false).include?(method_name) || super
|
183
|
+
end
|
184
|
+
|
185
|
+
def deliveries
|
186
|
+
@deliveries ||= []
|
187
|
+
end
|
188
|
+
|
189
|
+
def clear_deliveries
|
190
|
+
@deliveries = []
|
191
|
+
end
|
192
|
+
|
193
|
+
def after_deliver(method_name = nil, &block)
|
194
|
+
self.after_deliver_callback = block_given? ? block : method_name
|
195
|
+
end
|
196
|
+
|
197
|
+
def after_retries_exhausted(method_name = nil, &block)
|
198
|
+
self.after_retries_exhausted_callback = block_given? ? block : method_name
|
303
199
|
end
|
304
200
|
end
|
305
|
-
|
201
|
+
|
306
202
|
private
|
307
|
-
|
308
|
-
|
203
|
+
|
204
|
+
EXCLUDED_INSTANCE_VARIABLES = %w[@action_name @webhook_details @params @attempts].freeze
|
205
|
+
|
206
|
+
def invoke_callback(callback, response)
|
207
|
+
return unless callback
|
208
|
+
|
209
|
+
case callback
|
210
|
+
when Symbol
|
211
|
+
send(callback, response)
|
212
|
+
when Proc
|
213
|
+
callback.call(self, response)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def extract_instance_variables
|
218
|
+
assigns = {}
|
219
|
+
instance_variables.each do |ivar|
|
220
|
+
next if ivar.to_s.start_with?("@_")
|
221
|
+
next if EXCLUDED_INSTANCE_VARIABLES.include?(ivar.to_s)
|
222
|
+
|
223
|
+
assigns[ivar.to_s[1..].to_sym] = instance_variable_get(ivar)
|
224
|
+
end
|
225
|
+
assigns
|
226
|
+
end
|
227
|
+
|
228
|
+
def build_headers(detail_headers)
|
229
|
+
headers = default_headers.merge(detail_headers)
|
230
|
+
headers["Content-Type"] = "application/json" unless headers.key?("Content-Type")
|
231
|
+
headers["X-Webhook-Attempt"] = @attempts.to_s if @attempts.positive?
|
232
|
+
headers
|
233
|
+
end
|
234
|
+
|
235
|
+
def send_webhook_request(url, payload, headers)
|
236
|
+
HTTParty.post(url, body: payload.to_json, headers: headers, timeout: 10)
|
237
|
+
end
|
238
|
+
|
239
|
+
def build_response_hash(response, url)
|
240
|
+
{
|
241
|
+
success: response.success?,
|
242
|
+
status: response.code,
|
243
|
+
body: response.body,
|
244
|
+
url: url,
|
245
|
+
attempt: @attempts
|
246
|
+
}
|
247
|
+
end
|
248
|
+
|
249
|
+
def build_error_response_hash(error, url)
|
250
|
+
{
|
251
|
+
success: false,
|
252
|
+
error: error.message,
|
253
|
+
url: url,
|
254
|
+
attempt: @attempts
|
255
|
+
}
|
256
|
+
end
|
257
|
+
|
258
|
+
def log_webhook_result(response, url)
|
259
|
+
if response.success?
|
260
|
+
logger.info("Webhook delivered successfully: #{url} (Status: #{response.code}, Attempt: #{@attempts})")
|
261
|
+
else
|
262
|
+
logger.warn("Webhook delivery failed with HTTP error: #{url} (Status: #{response.code}, Attempt: #{@attempts})")
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def log_webhook_error(error, url)
|
267
|
+
logger.error("Webhook delivery failed: #{error.message} for URL: #{url} (Attempt: #{@attempts})")
|
268
|
+
end
|
269
|
+
|
270
|
+
def find_template_path(event_name)
|
271
|
+
webhook_class_name = self.class.name.underscore
|
272
|
+
possible_paths = build_template_paths(webhook_class_name, event_name)
|
273
|
+
possible_paths.find { |path| File.exist?(path) }
|
274
|
+
end
|
275
|
+
|
276
|
+
def build_template_paths(webhook_class_name, event_name)
|
277
|
+
[
|
278
|
+
File.join(Rails.root.to_s, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
|
279
|
+
engine_template_path(webhook_class_name, event_name),
|
280
|
+
namespaced_engine_template_path(webhook_class_name, event_name)
|
281
|
+
].compact
|
282
|
+
end
|
283
|
+
|
284
|
+
def engine_template_path(webhook_class_name, event_name)
|
285
|
+
return unless engine_root
|
286
|
+
|
287
|
+
File.join(engine_root, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb")
|
288
|
+
end
|
289
|
+
|
290
|
+
def namespaced_engine_template_path(webhook_class_name, event_name)
|
291
|
+
return unless engine_root && self.class.module_parent != Object
|
292
|
+
|
293
|
+
parent_name = self.class.module_parent.name.underscore
|
294
|
+
class_name = webhook_class_name.split("/").last
|
295
|
+
File.join(engine_root, "app/webhooks/#{parent_name}/#{class_name}/#{event_name}.json.erb")
|
296
|
+
end
|
297
|
+
|
298
|
+
def render_json_template(template_path, assigns)
|
299
|
+
template = ERB.new(File.read(template_path))
|
300
|
+
json = template.result_with_hash(assigns)
|
301
|
+
JSON.parse(json)
|
302
|
+
end
|
303
|
+
|
309
304
|
def collect_instance_variables
|
310
305
|
result = {}
|
311
306
|
instance_variables.each do |ivar|
|
312
|
-
# Skip internal instance variables
|
313
307
|
next if ivar.to_s.start_with?("@_")
|
314
|
-
next if
|
315
|
-
|
316
|
-
|
317
|
-
result[ivar.to_s[1..-1]] = instance_variable_get(ivar)
|
308
|
+
next if EXCLUDED_INSTANCE_VARIABLES.include?(ivar.to_s)
|
309
|
+
|
310
|
+
result[ivar.to_s[1..]] = instance_variable_get(ivar)
|
318
311
|
end
|
319
312
|
result
|
320
313
|
end
|
321
|
-
|
322
|
-
|
314
|
+
|
315
|
+
def restore_instance_variables(variables)
|
316
|
+
variables.each do |name, value|
|
317
|
+
instance_variable_set("@#{name}", value)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def retry_with_backoff
|
322
|
+
delay = calculate_backoff_delay
|
323
|
+
logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} in #{delay} seconds")
|
324
|
+
|
325
|
+
job_class = resolve_job_class
|
326
|
+
serialized_webhook = serialize
|
327
|
+
|
328
|
+
enqueue_retry_job(job_class, serialized_webhook, delay)
|
329
|
+
end
|
330
|
+
|
331
|
+
def calculate_backoff_delay
|
332
|
+
base_delay = self.class.retry_delay
|
333
|
+
multiplier = case self.class.retry_backoff
|
334
|
+
when :exponential
|
335
|
+
2**(@attempts - 1)
|
336
|
+
when :linear
|
337
|
+
@attempts
|
338
|
+
else
|
339
|
+
1
|
340
|
+
end
|
341
|
+
|
342
|
+
base_delay * multiplier + rand(self.class.retry_jitter)
|
343
|
+
end
|
344
|
+
|
345
|
+
def resolve_job_class
|
346
|
+
self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
|
347
|
+
end
|
348
|
+
|
349
|
+
def enqueue_retry_job(job_class, serialized_webhook, delay)
|
350
|
+
if deliver_later_queue_name
|
351
|
+
job_class.set(queue: deliver_later_queue_name, wait: delay).perform_later("deliver_now", serialized_webhook)
|
352
|
+
else
|
353
|
+
job_class.set(wait: delay).perform_later("deliver_now", serialized_webhook)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
323
357
|
def engine_root
|
324
358
|
return nil unless defined?(Rails::Engine)
|
325
|
-
|
326
|
-
|
359
|
+
|
360
|
+
find_engine_root_for_module(self.class.module_parent)
|
361
|
+
end
|
362
|
+
|
363
|
+
def find_engine_root_for_module(mod)
|
327
364
|
while mod != Object
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
}
|
332
|
-
|
333
|
-
return mod.const_get(constants.first).root.to_s if constants.any?
|
365
|
+
engine_constant = find_engine_constant(mod)
|
366
|
+
return mod.const_get(engine_constant).root.to_s if engine_constant
|
367
|
+
|
334
368
|
mod = mod.module_parent
|
335
369
|
end
|
336
|
-
|
370
|
+
|
337
371
|
nil
|
338
372
|
end
|
339
|
-
|
340
|
-
|
373
|
+
|
374
|
+
def find_engine_constant(mod)
|
375
|
+
mod.constants.find do |c|
|
376
|
+
const = mod.const_get(c)
|
377
|
+
const.is_a?(Class) && const < Rails::Engine
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
341
381
|
def enqueue_delivery(delivery_method, options = {})
|
342
382
|
options = options.dup
|
343
383
|
queue = options.delete(:queue) || self.class.deliver_later_queue_name
|
344
|
-
|
345
|
-
args = [delivery_method.to_s]
|
346
|
-
|
347
|
-
# Get the actual job class by evaluating the proc
|
348
|
-
job_class = self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
|
349
|
-
|
350
|
-
# Serialize the webhook
|
384
|
+
job_class = resolve_job_class
|
351
385
|
serialized_webhook = serialize
|
352
|
-
|
353
|
-
|
386
|
+
|
387
|
+
enqueue_job(job_class, delivery_method.to_s, serialized_webhook, queue, options)
|
388
|
+
end
|
389
|
+
|
390
|
+
def enqueue_job(job_class, delivery_method, serialized_webhook, queue, options)
|
354
391
|
if queue
|
355
|
-
job_class.set(queue: queue).perform_later(
|
392
|
+
job_class.set(queue: queue).perform_later(delivery_method, serialized_webhook)
|
393
|
+
elsif options[:wait]
|
394
|
+
job_class.set(wait: options[:wait]).perform_later(delivery_method, serialized_webhook)
|
356
395
|
else
|
357
|
-
job_class.
|
358
|
-
job_class.perform_later(*args, serialized_webhook) unless options[:wait]
|
396
|
+
job_class.perform_later(delivery_method, serialized_webhook)
|
359
397
|
end
|
360
398
|
end
|
361
|
-
|
362
|
-
# Logger for webhook errors
|
399
|
+
|
363
400
|
def logger
|
364
401
|
Rails.logger
|
365
402
|
end
|
366
|
-
|
367
|
-
# Class methods
|
368
|
-
class << self
|
369
|
-
# Handle method calls on the class
|
370
|
-
def method_missing(method_name, *args, &block)
|
371
|
-
if public_instance_methods(false).include?(method_name)
|
372
|
-
# Create a new instance
|
373
|
-
webhook = new
|
374
|
-
|
375
|
-
# Call the instance method
|
376
|
-
webhook.send(method_name, *args, &block)
|
377
|
-
else
|
378
|
-
super
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
|
-
def respond_to_missing?(method_name, include_private = false)
|
383
|
-
public_instance_methods(false).include?(method_name) || super
|
384
|
-
end
|
385
|
-
|
386
|
-
# Test delivery collection
|
387
|
-
def deliveries
|
388
|
-
@deliveries ||= []
|
389
|
-
end
|
390
|
-
|
391
|
-
# Reset the test delivery collection
|
392
|
-
def clear_deliveries
|
393
|
-
@deliveries = []
|
394
|
-
end
|
395
|
-
|
396
|
-
# Register callback for successful delivery
|
397
|
-
def after_deliver(method_name = nil, &block)
|
398
|
-
self.after_deliver_callback = block_given? ? block : method_name
|
399
|
-
end
|
400
|
-
|
401
|
-
# Register callback for when retries are exhausted
|
402
|
-
def after_retries_exhausted(method_name = nil, &block)
|
403
|
-
self.after_retries_exhausted_callback = block_given? ? block : method_name
|
404
|
-
end
|
405
|
-
end
|
406
403
|
end
|
407
|
-
|
408
|
-
# Delivery messenger for ActionMailer-like API
|
404
|
+
|
409
405
|
class DeliveryMessenger
|
410
406
|
def initialize(webhook)
|
411
407
|
@webhook = webhook
|
412
408
|
end
|
413
|
-
|
409
|
+
|
414
410
|
def deliver_now
|
415
|
-
if
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
# Test delivery
|
420
|
-
ActionWebhook::Base.deliveries << @webhook
|
421
|
-
else
|
422
|
-
# Normal delivery
|
423
|
-
@webhook.deliver_now
|
424
|
-
end
|
411
|
+
return nil if skip_delivery?
|
412
|
+
return test_delivery if test_mode?
|
413
|
+
|
414
|
+
@webhook.deliver_now
|
425
415
|
end
|
426
|
-
|
416
|
+
|
427
417
|
def deliver_later(options = {})
|
428
418
|
@webhook.deliver_later(options)
|
429
419
|
end
|
420
|
+
|
421
|
+
private
|
422
|
+
|
423
|
+
def skip_delivery?
|
424
|
+
@webhook.respond_to?(:perform_deliveries) && !@webhook.perform_deliveries
|
425
|
+
end
|
426
|
+
|
427
|
+
def test_mode?
|
428
|
+
@webhook.class.delivery_method == :test
|
429
|
+
end
|
430
|
+
|
431
|
+
def test_delivery
|
432
|
+
ActionWebhook::Base.deliveries << @webhook
|
433
|
+
end
|
430
434
|
end
|
431
|
-
end
|
435
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionWebhook
|
2
4
|
# Job responsible for delivering webhooks in the background
|
3
5
|
class DeliveryJob < ActiveJob::Base
|
@@ -9,10 +11,10 @@ module ActionWebhook
|
|
9
11
|
# @param serialized_webhook [Hash] The serialized webhook data
|
10
12
|
def perform(delivery_method, serialized_webhook)
|
11
13
|
# Reconstruct the webhook from serialized data
|
12
|
-
webhook_class = serialized_webhook[
|
14
|
+
webhook_class = serialized_webhook["webhook_class"].constantize
|
13
15
|
webhook = webhook_class.new
|
14
16
|
webhook.deserialize(serialized_webhook)
|
15
|
-
|
17
|
+
|
16
18
|
# Invoke the specified delivery method
|
17
19
|
webhook.send(delivery_method)
|
18
20
|
end
|
@@ -22,9 +24,9 @@ module ActionWebhook
|
|
22
24
|
# Log the error
|
23
25
|
Rails.logger.error("ActionWebhook delivery failed: #{exception.message}")
|
24
26
|
Rails.logger.error(exception.backtrace.join("\n"))
|
25
|
-
|
27
|
+
|
26
28
|
# Re-raise the exception if in development or test
|
27
29
|
raise exception if Rails.env.development? || Rails.env.test?
|
28
30
|
end
|
29
31
|
end
|
30
|
-
end
|
32
|
+
end
|
data/lib/action_webhook.rb
CHANGED
metadata
CHANGED
@@ -1,56 +1,76 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_webhook
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vinay Uttam Vemparala
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-06-06 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
|
-
name:
|
13
|
+
name: httparty
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version:
|
18
|
+
version: 0.18.1
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version:
|
25
|
+
version: 0.18.1
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
27
|
+
name: activejob
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
33
|
+
- - "<"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '8.0'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '6.0'
|
43
|
+
- - "<"
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '8.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: globalid
|
28
48
|
requirement: !ruby/object:Gem::Requirement
|
29
49
|
requirements:
|
30
50
|
- - "~>"
|
31
51
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0
|
52
|
+
version: '1.0'
|
33
53
|
type: :runtime
|
34
54
|
prerelease: false
|
35
55
|
version_requirements: !ruby/object:Gem::Requirement
|
36
56
|
requirements:
|
37
57
|
- - "~>"
|
38
58
|
- !ruby/object:Gem::Version
|
39
|
-
version: 0
|
59
|
+
version: '1.0'
|
40
60
|
- !ruby/object:Gem::Dependency
|
41
61
|
name: yard
|
42
62
|
requirement: !ruby/object:Gem::Requirement
|
43
63
|
requirements:
|
44
|
-
- - "
|
64
|
+
- - "~>"
|
45
65
|
- !ruby/object:Gem::Version
|
46
|
-
version: '0'
|
66
|
+
version: '0.9'
|
47
67
|
type: :development
|
48
68
|
prerelease: false
|
49
69
|
version_requirements: !ruby/object:Gem::Requirement
|
50
70
|
requirements:
|
51
|
-
- - "
|
71
|
+
- - "~>"
|
52
72
|
- !ruby/object:Gem::Version
|
53
|
-
version: '0'
|
73
|
+
version: '0.9'
|
54
74
|
description: A Rails library for triggering webhooks. Inspired by ActionMailer from
|
55
75
|
Rails
|
56
76
|
email:
|
@@ -63,13 +83,13 @@ files:
|
|
63
83
|
- lib/action_webhook/base.rb
|
64
84
|
- lib/action_webhook/delivery_job.rb
|
65
85
|
- lib/action_webhook/version.rb
|
66
|
-
homepage: https://github.com/vinayuttam/action_webhook
|
86
|
+
homepage: https://github.com/vinayuttam/action_webhook
|
67
87
|
licenses:
|
68
88
|
- MIT
|
69
89
|
metadata:
|
70
|
-
homepage_uri: https://github.com/vinayuttam/action_webhook
|
71
|
-
source_code_uri: https://github.com/vinayuttam/action_webhook
|
72
|
-
changelog_uri: https://github.com/vinayuttam/action_webhook
|
90
|
+
homepage_uri: https://github.com/vinayuttam/action_webhook
|
91
|
+
source_code_uri: https://github.com/vinayuttam/action_webhook/tree/main
|
92
|
+
changelog_uri: https://github.com/vinayuttam/action_webhook/blob/main/CHANGELOG.md
|
73
93
|
rdoc_options: []
|
74
94
|
require_paths:
|
75
95
|
- lib
|