webhookdb 1.3.0 → 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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/admin-dist/assets/{index-6aebf805.js → index-9306dd28.js} +39 -39
  3. data/admin-dist/index.html +1 -1
  4. data/data/messages/templates/errors/generic_backfill.email.liquid +30 -0
  5. data/data/messages/templates/errors/icalendar_fetch.email.liquid +8 -2
  6. data/data/messages/templates/specs/with_fields.email.liquid +6 -0
  7. data/db/migrations/045_system_log.rb +15 -0
  8. data/db/migrations/046_indices.rb +14 -0
  9. data/lib/webhookdb/admin.rb +6 -0
  10. data/lib/webhookdb/admin_api/data_provider.rb +1 -0
  11. data/lib/webhookdb/admin_api/entities.rb +8 -0
  12. data/lib/webhookdb/aggregate_result.rb +1 -1
  13. data/lib/webhookdb/api/helpers.rb +17 -0
  14. data/lib/webhookdb/api/organizations.rb +6 -0
  15. data/lib/webhookdb/api/service_integrations.rb +1 -0
  16. data/lib/webhookdb/connection_cache.rb +29 -3
  17. data/lib/webhookdb/console.rb +1 -1
  18. data/lib/webhookdb/customer/reset_code.rb +1 -1
  19. data/lib/webhookdb/customer.rb +3 -2
  20. data/lib/webhookdb/db_adapter.rb +1 -1
  21. data/lib/webhookdb/dbutil.rb +2 -0
  22. data/lib/webhookdb/errors.rb +34 -0
  23. data/lib/webhookdb/http.rb +1 -1
  24. data/lib/webhookdb/jobs/deprecated_jobs.rb +1 -0
  25. data/lib/webhookdb/jobs/model_event_system_log_tracker.rb +105 -0
  26. data/lib/webhookdb/jobs/monitor_metrics.rb +29 -0
  27. data/lib/webhookdb/jobs/renew_watch_channel.rb +3 -0
  28. data/lib/webhookdb/message/transport.rb +1 -1
  29. data/lib/webhookdb/message.rb +53 -2
  30. data/lib/webhookdb/messages/error_generic_backfill.rb +45 -0
  31. data/lib/webhookdb/messages/error_icalendar_fetch.rb +3 -0
  32. data/lib/webhookdb/messages/specs.rb +16 -0
  33. data/lib/webhookdb/organization/alerting.rb +7 -3
  34. data/lib/webhookdb/organization/database_migration.rb +1 -1
  35. data/lib/webhookdb/organization/db_builder.rb +1 -1
  36. data/lib/webhookdb/organization.rb +14 -1
  37. data/lib/webhookdb/postgres/model.rb +1 -0
  38. data/lib/webhookdb/postgres.rb +2 -1
  39. data/lib/webhookdb/replicator/base.rb +66 -39
  40. data/lib/webhookdb/replicator/column.rb +2 -0
  41. data/lib/webhookdb/replicator/fake.rb +6 -0
  42. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +28 -19
  43. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +55 -11
  44. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +25 -4
  45. data/lib/webhookdb/replicator/signalwire_message_v1.rb +31 -0
  46. data/lib/webhookdb/replicator/transistor_episode_v1.rb +11 -5
  47. data/lib/webhookdb/replicator/webhook_request.rb +8 -0
  48. data/lib/webhookdb/replicator.rb +2 -2
  49. data/lib/webhookdb/service/view_api.rb +1 -1
  50. data/lib/webhookdb/service.rb +10 -10
  51. data/lib/webhookdb/service_integration.rb +14 -1
  52. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +153 -64
  53. data/lib/webhookdb/sync_target.rb +7 -5
  54. data/lib/webhookdb/system_log_event.rb +9 -0
  55. data/lib/webhookdb/version.rb +1 -1
  56. data/lib/webhookdb/webhook_subscription.rb +1 -1
  57. data/lib/webhookdb.rb +31 -7
  58. metadata +32 -16
  59. data/lib/webhookdb/jobs/customer_created_notify_internal.rb +0 -22
  60. /data/lib/webhookdb/jobs/{logged_webhook_replay.rb → logged_webhooks_replay.rb} +0 -0
  61. /data/lib/webhookdb/jobs/{logged_webhook_resilient_replay.rb → logged_webhooks_resilient_replay.rb} +0 -0
  62. /data/lib/webhookdb/jobs/{webhook_subscription_delivery_attempt.rb → webhook_subscription_delivery_event.rb} +0 -0
  63. /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
- resp = Webhookdb::Http.get(
145
- transcript_url,
146
- logger: self.logger,
147
- timeout: Webhookdb::Transistor.http_timeout,
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
@@ -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 < StandardError; end
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 < StandardError; end
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 < StandardError
6
+ class FormError < Webhookdb::WebhookdbError
7
7
  attr_reader :status
8
8
 
9
9
  def initialize(msg, status=400)
@@ -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
- RSpec.shared_examples "a replicator" do |name|
8
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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
- it "emits the rowupsert event if the row has changed", :async, :do_not_defer_events, sidekiq: :fake do
74
- Webhookdb::Fixtures.webhook_subscription(service_integration: sint).create
75
- svc.create_table
76
- upsert_webhook(svc, body:)
77
- expect(Sidekiq).to have_queue.consisting_of(
78
- job_hash(
79
- Webhookdb::Jobs::SendWebhook,
80
- args: contain_exactly(
81
- hash_including(
82
- "id",
83
- "name" => "webhookdb.serviceintegration.rowupsert",
84
- "payload" => [
85
- sint.id,
86
- hash_including("external_id", "external_id_column", "row" => hash_including("data")),
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
- Webhookdb::Fixtures.webhook_subscription(service_integration: sint).create
97
- expect(Webhookdb::Jobs::SendWebhook).to receive(:perform_async).once
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 |service_name, dependent_service_name|
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 |service_name, dependency_service_name|
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 |name|
254
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 |name|
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: 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 |name|
431
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 |name|
457
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 |name, stores_enrichment_column: true|
493
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 |name|
553
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 |name|
578
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 |name|
601
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 |name|
624
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 |name|
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: 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 |name|
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: 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 |name|
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: 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 |name|
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: 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 |name|
860
- let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
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 < StandardError; end
25
- class InvalidConnection < StandardError; end
26
- class SyncInProgress < StandardError; end
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
- next_due_at = Sequel[:last_synced_at] + (Sequel.lit("INTERVAL '1 second'") * Sequel[:period_seconds])
91
- due_before_now = next_due_at <= as_of
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/postgres/model"
4
+
5
+ class Webhookdb::SystemLogEvent < Webhookdb::Postgres::Model(:system_log_events)
6
+ plugin :text_searchable, terms: [:title, :body]
7
+
8
+ many_to_one :actor, class: "Webhookdb::Customer"
9
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webhookdb
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "webhookdb/jobs/webhook_subscription_delivery_attempt"
3
+ require "webhookdb/jobs/webhook_subscription_delivery_event"
4
4
 
5
5
  # Webhook subscriptions have a few parts:
6
6
  #
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 < StandardError; end
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 < StandardError; end
50
+ class InvalidPostcondition < ProgrammingError; end
45
51
 
46
52
  # Some invariant has been violated, which we never expect to see.
47
- class InvariantViolation < StandardError; end
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 < StandardError; end
58
+ class InvalidInput < WebhookdbError; end
53
59
 
54
60
  # Raised when an organization's database cannot be modified.
55
- class DatabaseLocked < StandardError; end
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 < StandardError; end
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 < StandardError; end
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.