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