webhookdb 1.3.1 → 1.4.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/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.
|