action_webhook 0.1.1 → 1.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 +4 -4
- data/lib/action_webhook/base.rb +276 -264
- 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: 2063e93683fe64ce519f241843f17e33da9f29d671b965af97c0f97540a98ecc
|
4
|
+
data.tar.gz: 1f591bf9cfb727d9ab6e750338b708e4e24df2e782ea3f1a5096277f4311474d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14ed8242d705d62f573fd884e83bc70479ed4397c4bb3fffd75fd178bdc929fe5d981c395abb99ab7d647c7f1aec3aeaf1685784b3d521e3e62ed2e073ee8c1c
|
7
|
+
data.tar.gz: 45c7362968926262d79f6dedb16f2391d30a2b64980e3a5d0116558360ee63fe416b54f33a17012b82e5b50d8a465a58d5f91e1b6fa1f05e185959a2ce547230
|
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,101 @@ 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
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
84
|
+
# Separate successful and failed responses
|
85
|
+
successful_responses = response.select { |r| r[:success] }
|
86
|
+
failed_responses = response.reject { |r| r[:success] }
|
87
|
+
|
88
|
+
# Invoke success callback for successful deliveries
|
89
|
+
invoke_callback(self.class.after_deliver_callback, successful_responses) if successful_responses.any?
|
90
|
+
|
91
|
+
# Handle failed responses
|
92
|
+
if failed_responses.any? && @attempts < self.class.max_retries
|
93
|
+
# Extract failed webhook details for retry
|
94
|
+
failed_webhook_details = failed_responses.map { |r| @webhook_details.find { |detail| detail[:url] == r[:url] } }.compact
|
95
|
+
retry_with_backoff(failed_webhook_details)
|
96
|
+
elsif failed_responses.any?
|
97
|
+
# All retries exhausted for failed URLs
|
98
|
+
invoke_callback(self.class.after_retries_exhausted_callback, failed_responses)
|
85
99
|
end
|
86
100
|
|
87
101
|
response
|
88
102
|
end
|
89
103
|
|
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
104
|
def deliver_later(options = {})
|
104
105
|
enqueue_delivery(:deliver_now, options)
|
105
106
|
end
|
106
107
|
|
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
108
|
def deliver(webhook_details, params = {})
|
114
|
-
# Determine action name from the caller
|
115
109
|
@action_name = caller_locations(1, 1)[0].label.to_sym
|
116
110
|
@webhook_details = webhook_details
|
117
111
|
@params = params
|
118
112
|
|
119
|
-
# Return self for chaining with delivery methods
|
120
113
|
DeliveryMessenger.new(self)
|
121
114
|
end
|
122
115
|
|
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
116
|
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
|
117
|
+
assigns = extract_instance_variables
|
142
118
|
assigns.merge!(variables)
|
143
|
-
|
144
|
-
# Render the template
|
145
119
|
generate_json_from_template(@action_name, assigns)
|
146
120
|
end
|
147
121
|
|
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)
|
122
|
+
def post_webhook(webhook_details, payload)
|
154
123
|
responses = []
|
155
124
|
|
156
|
-
webhook_details.
|
157
|
-
# Ensure headers exists
|
125
|
+
webhook_details.each do |detail|
|
158
126
|
detail[:headers] ||= {}
|
127
|
+
headers = build_headers(detail[:headers])
|
159
128
|
|
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
|
-
}
|
129
|
+
response = send_webhook_request(detail[:url], payload, headers)
|
130
|
+
responses << build_response_hash(response, detail[:url])
|
131
|
+
log_webhook_result(response, detail[:url])
|
183
132
|
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})")
|
133
|
+
responses << build_error_response_hash(e, detail[:url])
|
134
|
+
log_webhook_error(e, detail[:url])
|
191
135
|
end
|
192
136
|
|
193
137
|
responses
|
194
138
|
end
|
195
139
|
|
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
140
|
def generate_json_from_template(input_event_name, assigns = {})
|
202
141
|
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
|
218
|
-
|
219
|
-
# Find the first template that exists
|
220
|
-
template_path = possible_paths.find { |path| File.exist?(path) }
|
142
|
+
template_path = find_template_path(event_name)
|
221
143
|
|
222
|
-
unless template_path
|
223
|
-
raise ArgumentError, "Template not found for #{event_name} in paths:\n#{possible_paths.join("\n")}"
|
224
|
-
end
|
144
|
+
raise ArgumentError, "Template not found for #{event_name}" unless template_path
|
225
145
|
|
226
|
-
|
227
|
-
json = template.result_with_hash(assigns)
|
228
|
-
JSON.parse(json)
|
146
|
+
render_json_template(template_path, assigns)
|
229
147
|
rescue JSON::ParserError => e
|
230
|
-
raise "Invalid JSON in template #{event_name}: #{e.message}"
|
148
|
+
raise ArgumentError, "Invalid JSON in template #{event_name}: #{e.message}"
|
231
149
|
end
|
232
150
|
|
233
151
|
def extract_method_name(path)
|
234
152
|
path.include?("#") ? path.split("#").last : path
|
235
153
|
end
|
236
154
|
|
237
|
-
# Process the webhook to generate and send the payload
|
238
155
|
def process_webhook
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
# Post the webhook
|
243
|
-
post_webhook(webhook_details, payloads)
|
156
|
+
payload = build_payload
|
157
|
+
post_webhook(webhook_details, payload)
|
244
158
|
end
|
245
159
|
|
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
|
265
|
-
end
|
266
|
-
|
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
160
|
def serialize
|
289
161
|
{
|
290
162
|
"action_name" => @action_name.to_s,
|
@@ -296,143 +168,283 @@ module ActionWebhook
|
|
296
168
|
}
|
297
169
|
end
|
298
170
|
|
299
|
-
# Restores state from serialized data
|
300
171
|
def deserialize(data)
|
301
172
|
@action_name = data["action_name"].to_sym
|
302
173
|
@webhook_details = data["webhook_details"]
|
303
174
|
@params = data["params"]
|
304
175
|
@attempts = data["attempts"] || 0
|
305
176
|
|
306
|
-
|
307
|
-
|
308
|
-
|
177
|
+
restore_instance_variables(data["instance_variables"])
|
178
|
+
end
|
179
|
+
|
180
|
+
class << self
|
181
|
+
def method_missing(method_name, *args, &block)
|
182
|
+
if public_instance_methods(false).include?(method_name)
|
183
|
+
webhook = new
|
184
|
+
webhook.send(method_name, *args, &block)
|
185
|
+
else
|
186
|
+
super
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def respond_to_missing?(method_name, include_private = false)
|
191
|
+
public_instance_methods(false).include?(method_name) || super
|
192
|
+
end
|
193
|
+
|
194
|
+
def deliveries
|
195
|
+
@deliveries ||= []
|
196
|
+
end
|
197
|
+
|
198
|
+
def clear_deliveries
|
199
|
+
@deliveries = []
|
200
|
+
end
|
201
|
+
|
202
|
+
def after_deliver(method_name = nil, &block)
|
203
|
+
self.after_deliver_callback = block_given? ? block : method_name
|
204
|
+
end
|
205
|
+
|
206
|
+
def after_retries_exhausted(method_name = nil, &block)
|
207
|
+
self.after_retries_exhausted_callback = block_given? ? block : method_name
|
309
208
|
end
|
310
209
|
end
|
311
210
|
|
312
211
|
private
|
313
212
|
|
314
|
-
|
315
|
-
|
316
|
-
|
213
|
+
EXCLUDED_INSTANCE_VARIABLES = %w[@action_name @webhook_details @params @attempts].freeze
|
214
|
+
|
215
|
+
def invoke_callback(callback, response)
|
216
|
+
return unless callback
|
217
|
+
|
218
|
+
case callback
|
219
|
+
when Symbol
|
220
|
+
send(callback, response)
|
221
|
+
when Proc
|
222
|
+
callback.call(self, response)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def extract_instance_variables
|
227
|
+
assigns = {}
|
317
228
|
instance_variables.each do |ivar|
|
318
|
-
# Skip internal instance variables
|
319
229
|
next if ivar.to_s.start_with?("@_")
|
320
|
-
next if
|
230
|
+
next if EXCLUDED_INSTANCE_VARIABLES.include?(ivar.to_s)
|
321
231
|
|
322
|
-
|
323
|
-
result[ivar.to_s[1..]] = instance_variable_get(ivar)
|
232
|
+
assigns[ivar.to_s[1..].to_sym] = instance_variable_get(ivar)
|
324
233
|
end
|
325
|
-
|
234
|
+
assigns
|
326
235
|
end
|
327
236
|
|
328
|
-
|
329
|
-
|
330
|
-
|
237
|
+
def build_headers(detail_headers)
|
238
|
+
headers = default_headers.merge(detail_headers)
|
239
|
+
headers["Content-Type"] = "application/json" unless headers.key?("Content-Type")
|
240
|
+
headers["X-Webhook-Attempt"] = @attempts.to_s if @attempts.positive?
|
241
|
+
headers
|
242
|
+
end
|
331
243
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
const = mod.const_get(c)
|
336
|
-
const.is_a?(Class) && const < Rails::Engine
|
337
|
-
end
|
244
|
+
def send_webhook_request(url, payload, headers)
|
245
|
+
HTTParty.post(url, body: payload.to_json, headers: headers, timeout: 10)
|
246
|
+
end
|
338
247
|
|
339
|
-
|
248
|
+
def build_response_hash(response, url)
|
249
|
+
{
|
250
|
+
success: response.success?,
|
251
|
+
status: response.code,
|
252
|
+
body: response.body,
|
253
|
+
url: url,
|
254
|
+
attempt: @attempts
|
255
|
+
}
|
256
|
+
end
|
340
257
|
|
341
|
-
|
258
|
+
def build_error_response_hash(error, url)
|
259
|
+
{
|
260
|
+
success: false,
|
261
|
+
error: error.message,
|
262
|
+
url: url,
|
263
|
+
attempt: @attempts
|
264
|
+
}
|
265
|
+
end
|
266
|
+
|
267
|
+
def log_webhook_result(response, url)
|
268
|
+
if response.success?
|
269
|
+
logger.info("Webhook delivered successfully: #{url} (Status: #{response.code}, Attempt: #{@attempts})")
|
270
|
+
else
|
271
|
+
logger.warn("Webhook delivery failed with HTTP error: #{url} (Status: #{response.code}, Attempt: #{@attempts})")
|
342
272
|
end
|
273
|
+
end
|
343
274
|
|
344
|
-
|
275
|
+
def log_webhook_error(error, url)
|
276
|
+
logger.error("Webhook delivery failed: #{error.message} for URL: #{url} (Attempt: #{@attempts})")
|
345
277
|
end
|
346
278
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
279
|
+
def find_template_path(event_name)
|
280
|
+
webhook_class_name = self.class.name.underscore
|
281
|
+
possible_paths = build_template_paths(webhook_class_name, event_name)
|
282
|
+
possible_paths.find { |path| File.exist?(path) }
|
283
|
+
end
|
351
284
|
|
352
|
-
|
285
|
+
def build_template_paths(webhook_class_name, event_name)
|
286
|
+
[
|
287
|
+
File.join(Rails.root.to_s, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb"),
|
288
|
+
engine_template_path(webhook_class_name, event_name),
|
289
|
+
namespaced_engine_template_path(webhook_class_name, event_name)
|
290
|
+
].compact
|
291
|
+
end
|
353
292
|
|
354
|
-
|
355
|
-
|
293
|
+
def engine_template_path(webhook_class_name, event_name)
|
294
|
+
return unless engine_root
|
356
295
|
|
357
|
-
|
358
|
-
|
296
|
+
File.join(engine_root, "app/webhooks/#{webhook_class_name}/#{event_name}.json.erb")
|
297
|
+
end
|
359
298
|
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
end
|
299
|
+
def namespaced_engine_template_path(webhook_class_name, event_name)
|
300
|
+
return unless engine_root && self.class.module_parent != Object
|
301
|
+
|
302
|
+
parent_name = self.class.module_parent.name.underscore
|
303
|
+
class_name = webhook_class_name.split("/").last
|
304
|
+
File.join(engine_root, "app/webhooks/#{parent_name}/#{class_name}/#{event_name}.json.erb")
|
367
305
|
end
|
368
306
|
|
369
|
-
|
370
|
-
|
371
|
-
|
307
|
+
def render_json_template(template_path, assigns)
|
308
|
+
template = ERB.new(File.read(template_path))
|
309
|
+
json = template.result_with_hash(assigns)
|
310
|
+
JSON.parse(json)
|
372
311
|
end
|
373
312
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
if
|
379
|
-
# Create a new instance
|
380
|
-
webhook = new
|
313
|
+
def collect_instance_variables
|
314
|
+
result = {}
|
315
|
+
instance_variables.each do |ivar|
|
316
|
+
next if ivar.to_s.start_with?("@_")
|
317
|
+
next if EXCLUDED_INSTANCE_VARIABLES.include?(ivar.to_s)
|
381
318
|
|
382
|
-
|
383
|
-
webhook.send(method_name, *args, &block)
|
384
|
-
else
|
385
|
-
super
|
386
|
-
end
|
319
|
+
result[ivar.to_s[1..]] = instance_variable_get(ivar)
|
387
320
|
end
|
321
|
+
result
|
322
|
+
end
|
388
323
|
|
389
|
-
|
390
|
-
|
324
|
+
def restore_instance_variables(variables)
|
325
|
+
variables.each do |name, value|
|
326
|
+
instance_variable_set("@#{name}", value)
|
391
327
|
end
|
328
|
+
end
|
392
329
|
|
393
|
-
|
394
|
-
|
395
|
-
|
330
|
+
def retry_with_backoff(failed_webhook_details = nil)
|
331
|
+
# Use failed webhook details if provided, otherwise retry all
|
332
|
+
retry_details = failed_webhook_details || @webhook_details
|
333
|
+
|
334
|
+
delay = calculate_backoff_delay
|
335
|
+
logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} for #{retry_details.size} URLs in #{delay} seconds")
|
336
|
+
|
337
|
+
job_class = resolve_job_class
|
338
|
+
serialized_webhook = serialize
|
339
|
+
|
340
|
+
# Update the webhook details to only include failed URLs
|
341
|
+
serialized_webhook["webhook_details"] = retry_details
|
342
|
+
|
343
|
+
enqueue_retry_job(job_class, serialized_webhook, delay)
|
344
|
+
end
|
345
|
+
|
346
|
+
def calculate_backoff_delay
|
347
|
+
base_delay = self.class.retry_delay
|
348
|
+
multiplier = case self.class.retry_backoff
|
349
|
+
when :exponential
|
350
|
+
2**(@attempts - 1)
|
351
|
+
when :linear
|
352
|
+
@attempts
|
353
|
+
else
|
354
|
+
1
|
355
|
+
end
|
356
|
+
|
357
|
+
base_delay * multiplier + rand(self.class.retry_jitter)
|
358
|
+
end
|
359
|
+
|
360
|
+
def resolve_job_class
|
361
|
+
self.class.delivery_job.is_a?(Proc) ? self.class.delivery_job.call : self.class.delivery_job
|
362
|
+
end
|
363
|
+
|
364
|
+
def enqueue_retry_job(job_class, serialized_webhook, delay)
|
365
|
+
if deliver_later_queue_name
|
366
|
+
job_class.set(queue: deliver_later_queue_name, wait: delay).perform_later("deliver_now", serialized_webhook)
|
367
|
+
else
|
368
|
+
job_class.set(wait: delay).perform_later("deliver_now", serialized_webhook)
|
396
369
|
end
|
370
|
+
end
|
397
371
|
|
398
|
-
|
399
|
-
|
400
|
-
|
372
|
+
def engine_root
|
373
|
+
return nil unless defined?(Rails::Engine)
|
374
|
+
|
375
|
+
find_engine_root_for_module(self.class.module_parent)
|
376
|
+
end
|
377
|
+
|
378
|
+
def find_engine_root_for_module(mod)
|
379
|
+
while mod != Object
|
380
|
+
engine_constant = find_engine_constant(mod)
|
381
|
+
return mod.const_get(engine_constant).root.to_s if engine_constant
|
382
|
+
|
383
|
+
mod = mod.module_parent
|
401
384
|
end
|
402
385
|
|
403
|
-
|
404
|
-
|
405
|
-
|
386
|
+
nil
|
387
|
+
end
|
388
|
+
|
389
|
+
def find_engine_constant(mod)
|
390
|
+
mod.constants.find do |c|
|
391
|
+
const = mod.const_get(c)
|
392
|
+
const.is_a?(Class) && const < Rails::Engine
|
406
393
|
end
|
394
|
+
end
|
407
395
|
|
408
|
-
|
409
|
-
|
410
|
-
|
396
|
+
def enqueue_delivery(delivery_method, options = {})
|
397
|
+
options = options.dup
|
398
|
+
queue = options.delete(:queue) || self.class.deliver_later_queue_name
|
399
|
+
job_class = resolve_job_class
|
400
|
+
serialized_webhook = serialize
|
401
|
+
|
402
|
+
enqueue_job(job_class, delivery_method.to_s, serialized_webhook, queue, options)
|
403
|
+
end
|
404
|
+
|
405
|
+
def enqueue_job(job_class, delivery_method, serialized_webhook, queue, options)
|
406
|
+
if queue
|
407
|
+
job_class.set(queue: queue).perform_later(delivery_method, serialized_webhook)
|
408
|
+
elsif options[:wait]
|
409
|
+
job_class.set(wait: options[:wait]).perform_later(delivery_method, serialized_webhook)
|
410
|
+
else
|
411
|
+
job_class.perform_later(delivery_method, serialized_webhook)
|
411
412
|
end
|
412
413
|
end
|
414
|
+
|
415
|
+
def logger
|
416
|
+
Rails.logger
|
417
|
+
end
|
413
418
|
end
|
414
419
|
|
415
|
-
# Delivery messenger for ActionMailer-like API
|
416
420
|
class DeliveryMessenger
|
417
421
|
def initialize(webhook)
|
418
422
|
@webhook = webhook
|
419
423
|
end
|
420
424
|
|
421
425
|
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
|
426
|
+
return nil if skip_delivery?
|
427
|
+
return test_delivery if test_mode?
|
428
|
+
|
429
|
+
@webhook.deliver_now
|
432
430
|
end
|
433
431
|
|
434
432
|
def deliver_later(options = {})
|
435
433
|
@webhook.deliver_later(options)
|
436
434
|
end
|
435
|
+
|
436
|
+
private
|
437
|
+
|
438
|
+
def skip_delivery?
|
439
|
+
@webhook.respond_to?(:perform_deliveries) && !@webhook.perform_deliveries
|
440
|
+
end
|
441
|
+
|
442
|
+
def test_mode?
|
443
|
+
@webhook.class.delivery_method == :test
|
444
|
+
end
|
445
|
+
|
446
|
+
def test_delivery
|
447
|
+
ActionWebhook::Base.deliveries << @webhook
|
448
|
+
end
|
437
449
|
end
|
438
450
|
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:
|
4
|
+
version: 1.1.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
|