webhookdb 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/admin-dist/assets/{index-6aebf805.js → index-9306dd28.js} +39 -39
- data/admin-dist/index.html +1 -1
- data/data/messages/templates/errors/generic_backfill.email.liquid +30 -0
- data/data/messages/templates/errors/icalendar_fetch.email.liquid +8 -2
- data/data/messages/templates/specs/with_fields.email.liquid +6 -0
- data/db/migrations/045_system_log.rb +15 -0
- data/db/migrations/046_indices.rb +14 -0
- data/lib/webhookdb/admin.rb +6 -0
- data/lib/webhookdb/admin_api/data_provider.rb +1 -0
- data/lib/webhookdb/admin_api/entities.rb +8 -0
- data/lib/webhookdb/aggregate_result.rb +1 -1
- data/lib/webhookdb/api/helpers.rb +17 -0
- data/lib/webhookdb/api/organizations.rb +6 -0
- data/lib/webhookdb/api/service_integrations.rb +1 -0
- data/lib/webhookdb/connection_cache.rb +29 -3
- data/lib/webhookdb/console.rb +1 -1
- data/lib/webhookdb/customer/reset_code.rb +1 -1
- data/lib/webhookdb/customer.rb +3 -2
- data/lib/webhookdb/db_adapter.rb +1 -1
- data/lib/webhookdb/dbutil.rb +2 -0
- data/lib/webhookdb/errors.rb +34 -0
- data/lib/webhookdb/http.rb +1 -1
- data/lib/webhookdb/jobs/deprecated_jobs.rb +1 -0
- data/lib/webhookdb/jobs/model_event_system_log_tracker.rb +105 -0
- data/lib/webhookdb/jobs/monitor_metrics.rb +29 -0
- data/lib/webhookdb/jobs/renew_watch_channel.rb +3 -0
- data/lib/webhookdb/message/transport.rb +1 -1
- data/lib/webhookdb/message.rb +53 -2
- data/lib/webhookdb/messages/error_generic_backfill.rb +45 -0
- data/lib/webhookdb/messages/error_icalendar_fetch.rb +3 -0
- data/lib/webhookdb/messages/specs.rb +16 -0
- data/lib/webhookdb/organization/alerting.rb +7 -3
- data/lib/webhookdb/organization/database_migration.rb +1 -1
- data/lib/webhookdb/organization/db_builder.rb +1 -1
- data/lib/webhookdb/organization.rb +14 -1
- data/lib/webhookdb/postgres/model.rb +1 -0
- data/lib/webhookdb/postgres.rb +2 -1
- data/lib/webhookdb/replicator/base.rb +66 -39
- data/lib/webhookdb/replicator/column.rb +2 -0
- data/lib/webhookdb/replicator/fake.rb +6 -0
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +28 -19
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +55 -11
- data/lib/webhookdb/replicator/intercom_v1_mixin.rb +25 -4
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +31 -0
- data/lib/webhookdb/replicator/transistor_episode_v1.rb +11 -5
- data/lib/webhookdb/replicator/webhook_request.rb +8 -0
- data/lib/webhookdb/replicator.rb +2 -2
- data/lib/webhookdb/service/view_api.rb +1 -1
- data/lib/webhookdb/service.rb +10 -10
- data/lib/webhookdb/service_integration.rb +14 -1
- data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +153 -64
- data/lib/webhookdb/sync_target.rb +7 -5
- data/lib/webhookdb/system_log_event.rb +9 -0
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb/webhook_subscription.rb +1 -1
- data/lib/webhookdb.rb +31 -7
- metadata +32 -16
- data/lib/webhookdb/jobs/customer_created_notify_internal.rb +0 -22
- /data/lib/webhookdb/jobs/{logged_webhook_replay.rb → logged_webhooks_replay.rb} +0 -0
- /data/lib/webhookdb/jobs/{logged_webhook_resilient_replay.rb → logged_webhooks_resilient_replay.rb} +0 -0
- /data/lib/webhookdb/jobs/{webhook_subscription_delivery_attempt.rb → webhook_subscription_delivery_event.rb} +0 -0
- /data/lib/webhookdb/jobs/{webhook_resource_notify_integrations.rb → webhookdb_resource_notify_integrations.rb} +0 -0
@@ -141,11 +141,17 @@ class Webhookdb::Replicator::TransistorEpisodeV1 < Webhookdb::Replicator::Base
|
|
141
141
|
transcript_url = resource.fetch("attributes").fetch("transcript_url", nil)
|
142
142
|
return nil if transcript_url.blank?
|
143
143
|
(transcript_url += ".txt") unless transcript_url.end_with?(".txt")
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
144
|
+
begin
|
145
|
+
resp = Webhookdb::Http.get(
|
146
|
+
transcript_url,
|
147
|
+
logger: self.logger,
|
148
|
+
timeout: Webhookdb::Transistor.http_timeout,
|
149
|
+
)
|
150
|
+
rescue Webhookdb::Http::Error => e
|
151
|
+
# Not sure why this happens, but nothing we can do if it does.
|
152
|
+
return nil if e.status == 404
|
153
|
+
raise e
|
154
|
+
end
|
149
155
|
transcript_text = resp.body
|
150
156
|
return {transcript_text:}
|
151
157
|
end
|
@@ -6,4 +6,12 @@ class Webhookdb::Replicator::WebhookRequest < Webhookdb::TypedStruct
|
|
6
6
|
# When a webhook is processed synchronously, this will be set to the Rack::Request.
|
7
7
|
# Normal (async) webhook processing does not have this available.
|
8
8
|
attr_accessor :rack_request
|
9
|
+
|
10
|
+
JSON_KEYS = ["body", "headers", "path", "method"].freeze
|
11
|
+
def as_json
|
12
|
+
return JSON_KEYS.each_with_object({}) do |k, h|
|
13
|
+
v = self.send(k)
|
14
|
+
h[k] = v.as_json unless v.nil?
|
15
|
+
end
|
16
|
+
end
|
9
17
|
end
|
data/lib/webhookdb/replicator.rb
CHANGED
@@ -15,11 +15,11 @@ class Webhookdb::Replicator
|
|
15
15
|
PLUGIN_DIR = Pathname(__FILE__).dirname + PLUGIN_DIRNAME
|
16
16
|
|
17
17
|
# Raised when there is no service registered for a name.
|
18
|
-
class Invalid <
|
18
|
+
class Invalid < Webhookdb::WebhookdbError; end
|
19
19
|
|
20
20
|
# Raised when credentials to interact with a service are not set up.
|
21
21
|
# Usually this is due to a missing dependency.
|
22
|
-
class CredentialsMissing <
|
22
|
+
class CredentialsMissing < Webhookdb::WebhookdbError; end
|
23
23
|
|
24
24
|
# Statically describe a replicator.
|
25
25
|
class Descriptor < Webhookdb::TypedStruct
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# Mixin for Grape API endpoints that use HTML rendering.
|
4
4
|
# This isn't tested well enough.
|
5
5
|
module Webhookdb::Service::ViewApi
|
6
|
-
class FormError <
|
6
|
+
class FormError < Webhookdb::WebhookdbError
|
7
7
|
attr_reader :status
|
8
8
|
|
9
9
|
def initialize(msg, status=400)
|
data/lib/webhookdb/service.rb
CHANGED
@@ -163,6 +163,10 @@ class Webhookdb::Service < Grape::API
|
|
163
163
|
error!(e.message, 405)
|
164
164
|
end
|
165
165
|
|
166
|
+
rescue_from Webhookdb::ExceptionCarrier do |e|
|
167
|
+
merror!(e.status, e.message, code: e.code, more: e.more, headers: e.headers)
|
168
|
+
end
|
169
|
+
|
166
170
|
rescue_from Webhookdb::LockFailed do |_e|
|
167
171
|
merror!(
|
168
172
|
409,
|
@@ -191,17 +195,7 @@ class Webhookdb::Service < Grape::API
|
|
191
195
|
status = e.respond_to?(:status) ? e.status : 500
|
192
196
|
error_id = SecureRandom.uuid
|
193
197
|
error_signature = Digest::MD5.hexdigest("#{e.class}: #{e.message}")
|
194
|
-
|
195
|
-
Webhookdb::Service.logger.error "[%s] Uncaught %p in service: %s" %
|
196
|
-
[error_id, e.class, e.message]
|
197
|
-
Webhookdb::Service.logger.debug { e.backtrace.join("\n") }
|
198
|
-
if ENV["PRINT_API_ERROR"]
|
199
|
-
puts e
|
200
|
-
puts e.backtrace
|
201
|
-
end
|
202
|
-
|
203
198
|
more = {error_id:, error_signature:}
|
204
|
-
|
205
199
|
if Webhookdb::Service.devmode
|
206
200
|
msg = e.message
|
207
201
|
more[:backtrace] = e.backtrace.join("\n")
|
@@ -210,6 +204,12 @@ class Webhookdb::Service < Grape::API
|
|
210
204
|
msg = "An internal error occurred of type #{error_signature}. Error ID: #{error_id}"
|
211
205
|
end
|
212
206
|
Webhookdb::Service.logger.error("api_exception", {error_id:, error_signature:}, e)
|
207
|
+
Webhookdb::Service.logger.debug { e.backtrace.join("\n") }
|
208
|
+
if ENV["PRINT_API_ERROR"]
|
209
|
+
puts e
|
210
|
+
puts e.backtrace
|
211
|
+
end
|
212
|
+
|
213
213
|
merror!(status, msg, code: "api_error", more:)
|
214
214
|
end
|
215
215
|
|
@@ -6,6 +6,8 @@ require "webhookdb/postgres/model"
|
|
6
6
|
require "sequel/plugins/soft_deletes"
|
7
7
|
|
8
8
|
class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integrations)
|
9
|
+
include Webhookdb::Admin::Linked
|
10
|
+
|
9
11
|
class TableRenameError < Webhookdb::InvalidInput; end
|
10
12
|
|
11
13
|
# We limit the information that a user can access through the CLI to these fields.
|
@@ -70,7 +72,7 @@ class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integr
|
|
70
72
|
end)
|
71
73
|
|
72
74
|
many_to_one :depends_on, class: self
|
73
|
-
one_to_many :dependents, key: :depends_on_id, class: self
|
75
|
+
one_to_many :dependents, key: :depends_on_id, class: self, order: :id
|
74
76
|
one_to_many :sync_targets, class: "Webhookdb::SyncTarget"
|
75
77
|
|
76
78
|
# @return [Webhookdb::ServiceIntegration]
|
@@ -139,10 +141,21 @@ class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integr
|
|
139
141
|
select { |si| si.service_name == dep_descr.name }
|
140
142
|
end
|
141
143
|
|
144
|
+
# Return all dependents (integrations that depend on this one), breadth-first
|
145
|
+
# (that is, all children before grandchildren).
|
146
|
+
# @return [Array<Webhookdb::ServiceIntegration>]
|
142
147
|
def recursive_dependents
|
143
148
|
return self.dependents + self.dependents.flat_map(&:recursive_dependents)
|
144
149
|
end
|
145
150
|
|
151
|
+
# Return all service integrations this one depends on,
|
152
|
+
# in closest-ancestor order (that is, parent before grandparent).
|
153
|
+
# @return [Array<Webhookdb::ServiceIntegration>]
|
154
|
+
def recursive_dependencies
|
155
|
+
return [] if self.depends_on.nil?
|
156
|
+
return [self.depends_on].concat(self.depends_on.recursive_dependencies)
|
157
|
+
end
|
158
|
+
|
146
159
|
def destroy_self_and_all_dependents
|
147
160
|
self.dependents.each(&:destroy_self_and_all_dependents)
|
148
161
|
|
@@ -4,12 +4,18 @@ require "webhookdb/spec_helpers/whdb"
|
|
4
4
|
|
5
5
|
# The basics: these shared examples are among the most commonly used.
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
# @param supports_rowupsert: If true, this replicator will emit the rowupsert event when rows change.
|
8
|
+
# Nearly all replicators support this, but in some cases, replicators may not want to,
|
9
|
+
# especially when they otherwise do not adhere to normal replicator design.
|
10
|
+
# Usually this is only the case in enterprise intergrations.
|
11
|
+
#
|
12
|
+
# @param supports_row_diff: If true, test that the rowupsert event is not emitted when the row has not changed.
|
13
|
+
RSpec.shared_examples "a replicator" do |supports_rowupsert: true, supports_row_diff: true|
|
14
|
+
let(:service_name) { described_class.descriptor.name }
|
15
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
9
16
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
10
17
|
let(:body) { raise NotImplementedError }
|
11
18
|
let(:expected_data) { body }
|
12
|
-
let(:supports_row_diff) { true }
|
13
19
|
let(:expected_row) { nil }
|
14
20
|
Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
|
15
21
|
|
@@ -70,48 +76,50 @@ RSpec.shared_examples "a replicator" do |name|
|
|
70
76
|
end
|
71
77
|
end
|
72
78
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
79
|
+
if supports_rowupsert
|
80
|
+
it "emits the rowupsert event if the row has changed", :async, :do_not_defer_events, sidekiq: :fake do
|
81
|
+
Webhookdb::Fixtures.webhook_subscription(service_integration: sint).create
|
82
|
+
svc.create_table
|
83
|
+
upsert_webhook(svc, body:)
|
84
|
+
expect(Sidekiq).to have_queue.consisting_of(
|
85
|
+
job_hash(
|
86
|
+
Webhookdb::Jobs::SendWebhook,
|
87
|
+
args: contain_exactly(
|
88
|
+
hash_including(
|
89
|
+
"id",
|
90
|
+
"name" => "webhookdb.serviceintegration.rowupsert",
|
91
|
+
"payload" => [
|
92
|
+
sint.id,
|
93
|
+
hash_including("external_id", "external_id_column", "row" => hash_including("data")),
|
94
|
+
],
|
95
|
+
),
|
88
96
|
),
|
89
97
|
),
|
90
|
-
)
|
91
|
-
|
92
|
-
end
|
98
|
+
)
|
99
|
+
end
|
93
100
|
|
94
|
-
it "does not emit the rowupsert event if the row has not changed", :async, :do_not_defer_events, sidekiq: :fake do
|
95
101
|
if supports_row_diff
|
96
|
-
|
97
|
-
|
102
|
+
it "does not emit the rowupsert event if the row has not changed", :async, :do_not_defer_events, sidekiq: :fake do
|
103
|
+
Webhookdb::Fixtures.webhook_subscription(service_integration: sint).create
|
104
|
+
expect(Webhookdb::Jobs::SendWebhook).to receive(:perform_async).once
|
105
|
+
svc.create_table
|
106
|
+
upsert_webhook(svc, body:) # Upsert and make sure the next does not run
|
107
|
+
expect do
|
108
|
+
upsert_webhook(svc, body:)
|
109
|
+
end.to_not publish("webhookdb.serviceintegration.rowupsert")
|
110
|
+
expect(Sidekiq).to have_empty_queues
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it "does not emit the rowupsert event if there are no subscriptions", :async, :do_not_defer_events do
|
115
|
+
# No subscription is created so should not publish
|
98
116
|
svc.create_table
|
99
|
-
upsert_webhook(svc, body:) # Upsert and make sure the next does not run
|
100
117
|
expect do
|
101
118
|
upsert_webhook(svc, body:)
|
102
119
|
end.to_not publish("webhookdb.serviceintegration.rowupsert")
|
103
|
-
expect(Sidekiq).to have_empty_queues
|
104
120
|
end
|
105
121
|
end
|
106
122
|
|
107
|
-
it "does not emit the rowupsert event if there are no subscriptions", :async, :do_not_defer_events do
|
108
|
-
# No subscription is created so should not publish
|
109
|
-
svc.create_table
|
110
|
-
expect do
|
111
|
-
upsert_webhook(svc, body:)
|
112
|
-
end.to_not publish("webhookdb.serviceintegration.rowupsert")
|
113
|
-
end
|
114
|
-
|
115
123
|
it "can serve a webhook response" do
|
116
124
|
create_all_dependencies(sint)
|
117
125
|
request = fake_request
|
@@ -174,7 +182,8 @@ RSpec.shared_examples "a replicator" do |name|
|
|
174
182
|
end
|
175
183
|
end
|
176
184
|
|
177
|
-
RSpec.shared_examples "a replicator with dependents" do |
|
185
|
+
RSpec.shared_examples "a replicator with dependents" do |dependent_service_name|
|
186
|
+
let(:service_name) { described_class.descriptor.name }
|
178
187
|
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
179
188
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
180
189
|
let(:body) { raise NotImplementedError }
|
@@ -218,7 +227,8 @@ RSpec.shared_examples "a replicator with dependents" do |service_name, dependent
|
|
218
227
|
end
|
219
228
|
end
|
220
229
|
|
221
|
-
RSpec.shared_examples "a replicator dependent on another" do |
|
230
|
+
RSpec.shared_examples "a replicator dependent on another" do |dependency_service_name|
|
231
|
+
let(:service_name) { described_class.descriptor.name }
|
222
232
|
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
223
233
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
224
234
|
|
@@ -250,8 +260,9 @@ RSpec.shared_examples "a replicator dependent on another" do |service_name, depe
|
|
250
260
|
end
|
251
261
|
end
|
252
262
|
|
253
|
-
RSpec.shared_examples "a replicator that prevents overwriting new data with old" do
|
254
|
-
let(:
|
263
|
+
RSpec.shared_examples "a replicator that prevents overwriting new data with old" do
|
264
|
+
let(:service_name) { described_class.descriptor.name }
|
265
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
255
266
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
256
267
|
let(:old_body) { raise NotImplementedError }
|
257
268
|
let(:new_body) { raise NotImplementedError }
|
@@ -313,11 +324,12 @@ RSpec.shared_examples "a replicator that prevents overwriting new data with old"
|
|
313
324
|
end
|
314
325
|
end
|
315
326
|
|
316
|
-
RSpec.shared_examples "a replicator that can backfill" do
|
327
|
+
RSpec.shared_examples "a replicator that can backfill" do
|
317
328
|
let(:api_url) { "https://fake-url.com" }
|
329
|
+
let(:service_name) { described_class.descriptor.name }
|
318
330
|
let(:sint) do
|
319
331
|
Webhookdb::Fixtures.service_integration.create(
|
320
|
-
service_name
|
332
|
+
service_name:,
|
321
333
|
backfill_key: "bfkey",
|
322
334
|
backfill_secret: "bfsek",
|
323
335
|
api_url:,
|
@@ -427,8 +439,9 @@ end
|
|
427
439
|
|
428
440
|
# These shared examples test the way a replicator synthesizes and retrieves information from the API.
|
429
441
|
|
430
|
-
RSpec.shared_examples "a replicator that may have a minimal body" do
|
431
|
-
let(:
|
442
|
+
RSpec.shared_examples "a replicator that may have a minimal body" do
|
443
|
+
let(:service_name) { described_class.descriptor.name }
|
444
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
432
445
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
433
446
|
let(:body) { raise NotImplementedError }
|
434
447
|
let(:other_bodies) { [] }
|
@@ -453,8 +466,9 @@ RSpec.shared_examples "a replicator that may have a minimal body" do |name|
|
|
453
466
|
end
|
454
467
|
end
|
455
468
|
|
456
|
-
RSpec.shared_examples "a replicator that deals with resources and wrapped events" do
|
457
|
-
let(:
|
469
|
+
RSpec.shared_examples "a replicator that deals with resources and wrapped events" do
|
470
|
+
let(:service_name) { described_class.descriptor.name }
|
471
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
458
472
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
459
473
|
let(:resource_json) { raise NotImplementedError }
|
460
474
|
let(:resource_in_envelope_json) { raise NotImplementedError }
|
@@ -489,8 +503,9 @@ RSpec.shared_examples "a replicator that deals with resources and wrapped events
|
|
489
503
|
end
|
490
504
|
end
|
491
505
|
|
492
|
-
RSpec.shared_examples "a replicator that uses enrichments" do |
|
493
|
-
let(:
|
506
|
+
RSpec.shared_examples "a replicator that uses enrichments" do |stores_enrichment_column: true|
|
507
|
+
let(:service_name) { described_class.descriptor.name }
|
508
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
494
509
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
495
510
|
let(:body) { raise NotImplementedError }
|
496
511
|
# Needed if stores_enrichment_column is true
|
@@ -549,8 +564,9 @@ RSpec.shared_examples "a replicator that uses enrichments" do |name, stores_enri
|
|
549
564
|
end
|
550
565
|
end
|
551
566
|
|
552
|
-
RSpec.shared_examples "a replicator that upserts webhooks only under specific conditions" do
|
553
|
-
let(:
|
567
|
+
RSpec.shared_examples "a replicator that upserts webhooks only under specific conditions" do
|
568
|
+
let(:service_name) { described_class.descriptor.name }
|
569
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
554
570
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
555
571
|
let(:incorrect_webhook) { raise NotImplementedError }
|
556
572
|
Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
|
@@ -574,8 +590,9 @@ end
|
|
574
590
|
|
575
591
|
# These shared examples can be used to test replicators that support webhooks.
|
576
592
|
|
577
|
-
RSpec.shared_examples "a webhook validating replicator that uses credentials from a dependency" do
|
578
|
-
let(:
|
593
|
+
RSpec.shared_examples "a webhook validating replicator that uses credentials from a dependency" do
|
594
|
+
let(:service_name) { described_class.descriptor.name }
|
595
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
579
596
|
|
580
597
|
before(:each) do
|
581
598
|
create_all_dependencies(sint)
|
@@ -597,8 +614,9 @@ RSpec.shared_examples "a webhook validating replicator that uses credentials fro
|
|
597
614
|
end
|
598
615
|
end
|
599
616
|
|
600
|
-
RSpec.shared_examples "a replicator that processes webhooks synchronously" do
|
601
|
-
let(:
|
617
|
+
RSpec.shared_examples "a replicator that processes webhooks synchronously" do
|
618
|
+
let(:service_name) { described_class.descriptor.name }
|
619
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
602
620
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
603
621
|
let(:expected_synchronous_response) { raise NotImplementedError }
|
604
622
|
Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
|
@@ -620,8 +638,9 @@ end
|
|
620
638
|
|
621
639
|
# These shared examples test the intricacies of backfill logic.
|
622
640
|
|
623
|
-
RSpec.shared_examples "a backfill replicator that requires credentials from a dependency" do
|
624
|
-
let(:
|
641
|
+
RSpec.shared_examples "a backfill replicator that requires credentials from a dependency" do
|
642
|
+
let(:service_name) { described_class.descriptor.name }
|
643
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
625
644
|
let(:error_message) { raise NotImplementedError }
|
626
645
|
|
627
646
|
before(:each) do
|
@@ -640,12 +659,13 @@ RSpec.shared_examples "a backfill replicator that requires credentials from a de
|
|
640
659
|
end
|
641
660
|
end
|
642
661
|
|
643
|
-
RSpec.shared_examples "a replicator that can backfill incrementally" do
|
662
|
+
RSpec.shared_examples "a replicator that can backfill incrementally" do
|
663
|
+
let(:service_name) { described_class.descriptor.name }
|
644
664
|
let(:last_backfilled) { raise NotImplementedError, "what should the last_backfilled_at value be to start?" }
|
645
665
|
let(:api_url) { "https://fake-url.com" }
|
646
666
|
let(:sint) do
|
647
667
|
Webhookdb::Fixtures.service_integration.create(
|
648
|
-
service_name
|
668
|
+
service_name:,
|
649
669
|
backfill_key: "bfkey",
|
650
670
|
backfill_secret: "bfsek",
|
651
671
|
api_url:,
|
@@ -707,6 +727,71 @@ RSpec.shared_examples "a replicator that can backfill incrementally" do |name|
|
|
707
727
|
end
|
708
728
|
end
|
709
729
|
|
730
|
+
RSpec.shared_examples "a replicator that alerts on backfill auth errors" do
|
731
|
+
let(:service_name) { described_class.descriptor.name }
|
732
|
+
let(:sint_params) { {} }
|
733
|
+
let(:sint) do
|
734
|
+
Webhookdb::Fixtures.service_integration.create(
|
735
|
+
service_name:,
|
736
|
+
backfill_key: "bfkey",
|
737
|
+
backfill_secret: "bfsek",
|
738
|
+
api_url: "https://fake-url.com",
|
739
|
+
**sint_params,
|
740
|
+
)
|
741
|
+
end
|
742
|
+
let(:svc) { Webhookdb::Replicator.create(sint) }
|
743
|
+
let(:template_name) { raise NotImplementedError }
|
744
|
+
|
745
|
+
def stub_service_request
|
746
|
+
raise NotImplementedError, "stub the request without setting the return response"
|
747
|
+
end
|
748
|
+
|
749
|
+
def handled_responses
|
750
|
+
raise NotImplementedError, "Something like: [[:and_return, {status: 401}], [:and_raise, SocketError.new('hi')]]"
|
751
|
+
end
|
752
|
+
|
753
|
+
def unhandled_response
|
754
|
+
raise NotImplementedError, "Something like: [:and_return, {status: 500}]"
|
755
|
+
end
|
756
|
+
|
757
|
+
def insert_required_data_callback
|
758
|
+
# See backfiller example
|
759
|
+
return ->(*) { return }
|
760
|
+
end
|
761
|
+
|
762
|
+
before(:each) do
|
763
|
+
sint.organization.prepare_database_connections
|
764
|
+
end
|
765
|
+
|
766
|
+
after(:each) do
|
767
|
+
sint.organization.remove_related_database
|
768
|
+
end
|
769
|
+
|
770
|
+
it "dispatches an alert and returns true for handled errors" do
|
771
|
+
create_all_dependencies(sint)
|
772
|
+
setup_dependencies(sint, insert_required_data_callback)
|
773
|
+
Webhookdb::Fixtures.organization_membership.org(sint.organization).verified.admin.create
|
774
|
+
req = stub_service_request
|
775
|
+
handled_responses.each { |(m, arg)| req.send(m, arg) }
|
776
|
+
handled_responses.count.times do
|
777
|
+
backfill(sint)
|
778
|
+
end
|
779
|
+
expect(req).to have_been_made.times(handled_responses.count)
|
780
|
+
expect(Webhookdb::Message::Delivery.all).to contain_exactly(
|
781
|
+
have_attributes(template: template_name),
|
782
|
+
)
|
783
|
+
end
|
784
|
+
|
785
|
+
it "does not dispatch an alert, and raises the original error, if unhandled" do
|
786
|
+
create_all_dependencies(sint)
|
787
|
+
setup_dependencies(sint, insert_required_data_callback)
|
788
|
+
Webhookdb::Fixtures.organization_membership.org(sint.organization).verified.admin.create
|
789
|
+
req = stub_service_request.send(*unhandled_response)
|
790
|
+
expect { backfill(sint) }.to raise_error(Amigo::Retry::OrDie)
|
791
|
+
expect(req).to have_been_made
|
792
|
+
end
|
793
|
+
end
|
794
|
+
|
710
795
|
RSpec.shared_examples "a replicator that verifies backfill secrets" do
|
711
796
|
let(:correct_creds_sint) { raise NotImplementedError, "what sint should we use to test correct creds?" }
|
712
797
|
let(:incorrect_creds_sint) { raise NotImplementedError, "what sint should we use to test incorrect creds?" }
|
@@ -748,19 +833,21 @@ RSpec.shared_examples "a replicator that verifies backfill secrets" do
|
|
748
833
|
end
|
749
834
|
end
|
750
835
|
|
751
|
-
RSpec.shared_examples "a replicator with a custom backfill not supported message" do
|
836
|
+
RSpec.shared_examples "a replicator with a custom backfill not supported message" do
|
837
|
+
let(:service_name) { described_class.descriptor.name }
|
752
838
|
it "has a custom message" do
|
753
|
-
sint = Webhookdb::Fixtures.service_integration.create(service_name:
|
839
|
+
sint = Webhookdb::Fixtures.service_integration.create(service_name:)
|
754
840
|
expect(sint.replicator.backfill_not_supported_message).to_not include("You may be looking for one of the following")
|
755
841
|
end
|
756
842
|
end
|
757
843
|
|
758
|
-
RSpec.shared_examples "a backfill replicator that marks missing rows as deleted" do
|
844
|
+
RSpec.shared_examples "a backfill replicator that marks missing rows as deleted" do
|
845
|
+
let(:service_name) { described_class.descriptor.name }
|
759
846
|
let(:deleted_column_name) { raise NotImplementedError }
|
760
847
|
let(:api_url) { "https://fake-url.com" }
|
761
848
|
let(:sint) do
|
762
849
|
Webhookdb::Fixtures.service_integration.create(
|
763
|
-
service_name
|
850
|
+
service_name:,
|
764
851
|
backfill_key: "bfkey",
|
765
852
|
backfill_secret: "bfsek",
|
766
853
|
api_url:,
|
@@ -813,11 +900,12 @@ RSpec.shared_examples "a backfill replicator that marks missing rows as deleted"
|
|
813
900
|
end
|
814
901
|
end
|
815
902
|
|
816
|
-
RSpec.shared_examples "a replicator that ignores HTTP errors during backfill" do
|
903
|
+
RSpec.shared_examples "a replicator that ignores HTTP errors during backfill" do
|
904
|
+
let(:service_name) { described_class.descriptor.name }
|
817
905
|
let(:api_url) { "https://fake-url.com" }
|
818
906
|
let(:sint) do
|
819
907
|
Webhookdb::Fixtures.service_integration.create(
|
820
|
-
service_name
|
908
|
+
service_name:,
|
821
909
|
backfill_key: "bfkey",
|
822
910
|
backfill_secret: "bfsek",
|
823
911
|
api_url:,
|
@@ -856,8 +944,9 @@ RSpec.shared_examples "a replicator that ignores HTTP errors during backfill" do
|
|
856
944
|
end
|
857
945
|
end
|
858
946
|
|
859
|
-
RSpec.shared_examples "a replicator backfilling against the table of its dependency" do
|
860
|
-
let(:
|
947
|
+
RSpec.shared_examples "a replicator backfilling against the table of its dependency" do
|
948
|
+
let(:service_name) { described_class.descriptor.name }
|
949
|
+
let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
|
861
950
|
let(:svc) { Webhookdb::Replicator.create(sint) }
|
862
951
|
let(:dep_svc) { @dep_svc }
|
863
952
|
let(:external_id_col) { raise NotImplementedError }
|
@@ -21,9 +21,9 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
21
21
|
include Appydays::Configurable
|
22
22
|
include Webhookdb::Dbutil
|
23
23
|
|
24
|
-
class Deleted <
|
25
|
-
class InvalidConnection <
|
26
|
-
class SyncInProgress <
|
24
|
+
class Deleted < Webhookdb::WebhookdbError; end
|
25
|
+
class InvalidConnection < Webhookdb::WebhookdbError; end
|
26
|
+
class SyncInProgress < Webhookdb::WebhookdbError; end
|
27
27
|
|
28
28
|
# Advisory locks for sync targets use this as the first int, and the id as the second.
|
29
29
|
ADVISORY_LOCK_KEYSPACE = 2_000_000_000
|
@@ -87,8 +87,10 @@ class Webhookdb::SyncTarget < Webhookdb::Postgres::Model(:sync_targets)
|
|
87
87
|
dataset_module do
|
88
88
|
def due_for_sync(as_of:)
|
89
89
|
never_synced = Sequel[last_synced_at: nil]
|
90
|
-
|
91
|
-
|
90
|
+
# Use 'last_synced_at <= (now - internal)' rather than 'last_synced_at + interval <= now'
|
91
|
+
# so we can use the last_synced_at index.
|
92
|
+
cutoff = (Sequel[as_of].cast("TIMESTAMPTZ") - (Sequel.lit("INTERVAL '1 second'") * Sequel[:period_seconds]))
|
93
|
+
due_before_now = Sequel[:last_synced_at] <= cutoff
|
92
94
|
return self.where(never_synced | due_before_now)
|
93
95
|
end
|
94
96
|
end
|
data/lib/webhookdb/version.rb
CHANGED
data/lib/webhookdb.rb
CHANGED
@@ -35,27 +35,33 @@ module Webhookdb
|
|
35
35
|
include Appydays::Configurable
|
36
36
|
extend Webhookdb::MethodUtilities
|
37
37
|
|
38
|
+
# Base class for all WebhookDB errors.
|
39
|
+
class WebhookdbError < StandardError; end
|
40
|
+
|
41
|
+
# Class for errors that usually should not be rescued from.
|
42
|
+
class ProgrammingError < WebhookdbError; end
|
43
|
+
|
38
44
|
# Error raised when we cannot take an action
|
39
45
|
# because some condition has not been set up right.
|
40
|
-
class InvalidPrecondition <
|
46
|
+
class InvalidPrecondition < ProgrammingError; end
|
41
47
|
|
42
48
|
# Error raised when, after we take an action,
|
43
49
|
# something we expect to have changed has not changed.
|
44
|
-
class InvalidPostcondition <
|
50
|
+
class InvalidPostcondition < ProgrammingError; end
|
45
51
|
|
46
52
|
# Some invariant has been violated, which we never expect to see.
|
47
|
-
class InvariantViolation <
|
53
|
+
class InvariantViolation < ProgrammingError; end
|
48
54
|
|
49
55
|
# Error raised when a customer gives us some invalid input.
|
50
56
|
# Allows the library to raise the error with the message,
|
51
57
|
# and is caught automatically by the service as a 400.
|
52
|
-
class InvalidInput <
|
58
|
+
class InvalidInput < WebhookdbError; end
|
53
59
|
|
54
60
|
# Raised when an organization's database cannot be modified.
|
55
|
-
class DatabaseLocked <
|
61
|
+
class DatabaseLocked < WebhookdbError; end
|
56
62
|
|
57
63
|
# Used in various places that need to short-circuit code in regression mode.
|
58
|
-
class RegressionModeSkip <
|
64
|
+
class RegressionModeSkip < WebhookdbError; end
|
59
65
|
|
60
66
|
APPLICATION_NAME = "Webhookdb"
|
61
67
|
RACK_ENV = Appydays::Configurable.fetch_env(["RACK_ENV", "RUBY_ENV"], "development")
|
@@ -93,6 +99,8 @@ module Webhookdb
|
|
93
99
|
end
|
94
100
|
end
|
95
101
|
|
102
|
+
def self.admin_url = self.api_url
|
103
|
+
|
96
104
|
# Regression mode is true when we re replaying webhooks locally,
|
97
105
|
# or for some other reason, want to disable certain checks we use in production.
|
98
106
|
# For example, we may want to ignore certain errors (like if integrations are missing dependency rows),
|
@@ -134,7 +142,23 @@ module Webhookdb
|
|
134
142
|
# :section: Errors
|
135
143
|
#
|
136
144
|
|
137
|
-
class LockFailed <
|
145
|
+
class LockFailed < WebhookdbError; end
|
146
|
+
|
147
|
+
# This exception is rescued in the API and returned to the caller via +merror!+.
|
148
|
+
# This is mostly used in synchronously-processed replicators that need to return
|
149
|
+
# very specific types of errors during processing.
|
150
|
+
# This is very rare.
|
151
|
+
class ExceptionCarrier < WebhookdbError
|
152
|
+
attr_reader :status, :code, :more, :headers
|
153
|
+
|
154
|
+
def initialize(status, message, code: nil, more: {}, headers: {})
|
155
|
+
super(message)
|
156
|
+
@status = status
|
157
|
+
@code = code
|
158
|
+
@more = more
|
159
|
+
@headers = headers
|
160
|
+
end
|
161
|
+
end
|
138
162
|
|
139
163
|
### Generate a key for the specified Sequel model +instance+ and
|
140
164
|
### any additional +parts+ that can be used for idempotent requests.
|