webhookdb 1.3.1 → 1.5.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 (164) 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/026_undo_integration_backfill_cursor.rb +2 -0
  8. data/db/migrations/032_remove_db_defaults.rb +2 -0
  9. data/db/migrations/043_text_search.rb +2 -0
  10. data/db/migrations/045_system_log.rb +15 -0
  11. data/db/migrations/046_indices.rb +14 -0
  12. data/db/migrations/047_sync_parallelism.rb +9 -0
  13. data/db/migrations/048_sync_stats.rb +9 -0
  14. data/db/migrations/049_error_handlers.rb +18 -0
  15. data/db/migrations/050_logged_webhook_indices.rb +25 -0
  16. data/db/migrations/051_partitioning.rb +9 -0
  17. data/integration/async_spec.rb +0 -2
  18. data/integration/service_integrations_spec.rb +0 -2
  19. data/lib/amigo/durable_job.rb +2 -2
  20. data/lib/amigo/job_in_context.rb +12 -0
  21. data/lib/webhookdb/admin.rb +6 -0
  22. data/lib/webhookdb/admin_api/data_provider.rb +1 -0
  23. data/lib/webhookdb/admin_api/entities.rb +8 -0
  24. data/lib/webhookdb/aggregate_result.rb +1 -1
  25. data/lib/webhookdb/api/entities.rb +6 -2
  26. data/lib/webhookdb/api/error_handlers.rb +104 -0
  27. data/lib/webhookdb/api/helpers.rb +25 -1
  28. data/lib/webhookdb/api/icalproxy.rb +22 -0
  29. data/lib/webhookdb/api/install.rb +2 -1
  30. data/lib/webhookdb/api/organizations.rb +6 -0
  31. data/lib/webhookdb/api/saved_queries.rb +1 -0
  32. data/lib/webhookdb/api/saved_views.rb +1 -0
  33. data/lib/webhookdb/api/service_integrations.rb +2 -1
  34. data/lib/webhookdb/api/sync_targets.rb +1 -1
  35. data/lib/webhookdb/api/system.rb +5 -0
  36. data/lib/webhookdb/api/webhook_subscriptions.rb +1 -0
  37. data/lib/webhookdb/api.rb +4 -1
  38. data/lib/webhookdb/apps.rb +4 -0
  39. data/lib/webhookdb/async/autoscaler.rb +10 -0
  40. data/lib/webhookdb/async/job.rb +4 -0
  41. data/lib/webhookdb/async/scheduled_job.rb +4 -0
  42. data/lib/webhookdb/async.rb +2 -0
  43. data/lib/webhookdb/backfiller.rb +17 -4
  44. data/lib/webhookdb/concurrent.rb +96 -0
  45. data/lib/webhookdb/connection_cache.rb +57 -10
  46. data/lib/webhookdb/console.rb +1 -1
  47. data/lib/webhookdb/customer/reset_code.rb +1 -1
  48. data/lib/webhookdb/customer.rb +5 -4
  49. data/lib/webhookdb/database_document.rb +1 -1
  50. data/lib/webhookdb/db_adapter/default_sql.rb +1 -14
  51. data/lib/webhookdb/db_adapter/partition.rb +14 -0
  52. data/lib/webhookdb/db_adapter/partitioning.rb +8 -0
  53. data/lib/webhookdb/db_adapter/pg.rb +77 -5
  54. data/lib/webhookdb/db_adapter/snowflake.rb +15 -6
  55. data/lib/webhookdb/db_adapter.rb +25 -3
  56. data/lib/webhookdb/dbutil.rb +2 -0
  57. data/lib/webhookdb/errors.rb +34 -0
  58. data/lib/webhookdb/fixtures/logged_webhooks.rb +4 -0
  59. data/lib/webhookdb/fixtures/organization_error_handlers.rb +20 -0
  60. data/lib/webhookdb/http.rb +30 -16
  61. data/lib/webhookdb/icalendar.rb +30 -9
  62. data/lib/webhookdb/jobs/amigo_test_jobs.rb +1 -1
  63. data/lib/webhookdb/jobs/backfill.rb +21 -25
  64. data/lib/webhookdb/jobs/create_mirror_table.rb +3 -4
  65. data/lib/webhookdb/jobs/deprecated_jobs.rb +3 -0
  66. data/lib/webhookdb/jobs/emailer.rb +2 -1
  67. data/lib/webhookdb/jobs/front_signalwire_message_channel_sync_inbound.rb +15 -0
  68. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +7 -2
  69. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +74 -11
  70. data/lib/webhookdb/jobs/icalendar_enqueue_syncs_for_urls.rb +22 -0
  71. data/lib/webhookdb/jobs/icalendar_sync.rb +21 -9
  72. data/lib/webhookdb/jobs/increase_event_handler.rb +3 -2
  73. data/lib/webhookdb/jobs/{logged_webhook_replay.rb → logged_webhooks_replay.rb} +5 -3
  74. data/lib/webhookdb/jobs/message_dispatched.rb +1 -0
  75. data/lib/webhookdb/jobs/model_event_system_log_tracker.rb +112 -0
  76. data/lib/webhookdb/jobs/monitor_metrics.rb +29 -0
  77. data/lib/webhookdb/jobs/organization_database_migration_notify.rb +32 -0
  78. data/lib/webhookdb/jobs/organization_database_migration_run.rb +4 -6
  79. data/lib/webhookdb/jobs/organization_error_handler_dispatch.rb +26 -0
  80. data/lib/webhookdb/jobs/prepare_database_connections.rb +1 -0
  81. data/lib/webhookdb/jobs/process_webhook.rb +11 -12
  82. data/lib/webhookdb/jobs/renew_watch_channel.rb +10 -10
  83. data/lib/webhookdb/jobs/replication_migration.rb +5 -2
  84. data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +1 -2
  85. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -2
  86. data/lib/webhookdb/jobs/send_invite.rb +3 -2
  87. data/lib/webhookdb/jobs/send_test_webhook.rb +1 -3
  88. data/lib/webhookdb/jobs/send_webhook.rb +4 -5
  89. data/lib/webhookdb/jobs/stale_row_deleter.rb +31 -0
  90. data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +3 -0
  91. data/lib/webhookdb/jobs/sync_target_run_sync.rb +9 -15
  92. data/lib/webhookdb/jobs/{webhook_subscription_delivery_attempt.rb → webhook_subscription_delivery_event.rb} +5 -8
  93. data/lib/webhookdb/liquid/expose.rb +1 -1
  94. data/lib/webhookdb/liquid/filters.rb +1 -1
  95. data/lib/webhookdb/liquid/partial.rb +2 -2
  96. data/lib/webhookdb/logged_webhook/resilient.rb +3 -3
  97. data/lib/webhookdb/logged_webhook.rb +16 -2
  98. data/lib/webhookdb/message/email_transport.rb +1 -1
  99. data/lib/webhookdb/message/transport.rb +1 -1
  100. data/lib/webhookdb/message.rb +55 -4
  101. data/lib/webhookdb/messages/error_generic_backfill.rb +47 -0
  102. data/lib/webhookdb/messages/error_icalendar_fetch.rb +5 -0
  103. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +2 -0
  104. data/lib/webhookdb/messages/specs.rb +16 -0
  105. data/lib/webhookdb/organization/alerting.rb +56 -6
  106. data/lib/webhookdb/organization/database_migration.rb +2 -2
  107. data/lib/webhookdb/organization/db_builder.rb +5 -4
  108. data/lib/webhookdb/organization/error_handler.rb +141 -0
  109. data/lib/webhookdb/organization.rb +76 -10
  110. data/lib/webhookdb/postgres/model.rb +1 -0
  111. data/lib/webhookdb/postgres/model_utilities.rb +2 -0
  112. data/lib/webhookdb/postgres.rb +3 -4
  113. data/lib/webhookdb/replicator/base.rb +202 -68
  114. data/lib/webhookdb/replicator/base_stale_row_deleter.rb +165 -0
  115. data/lib/webhookdb/replicator/column.rb +2 -0
  116. data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +0 -1
  117. data/lib/webhookdb/replicator/fake.rb +106 -88
  118. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +131 -61
  119. data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +17 -0
  120. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +197 -32
  121. data/lib/webhookdb/replicator/icalendar_event_v1.rb +20 -44
  122. data/lib/webhookdb/replicator/icalendar_event_v1_partitioned.rb +33 -0
  123. data/lib/webhookdb/replicator/intercom_contact_v1.rb +1 -0
  124. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +1 -0
  125. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +49 -6
  126. data/lib/webhookdb/replicator/partitionable_mixin.rb +116 -0
  127. data/lib/webhookdb/replicator/shopify_v1_mixin.rb +1 -1
  128. data/lib/webhookdb/replicator/signalwire_message_v1.rb +31 -1
  129. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  130. data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +0 -1
  131. data/lib/webhookdb/replicator/transistor_episode_v1.rb +11 -5
  132. data/lib/webhookdb/replicator/webhook_request.rb +8 -0
  133. data/lib/webhookdb/replicator.rb +6 -3
  134. data/lib/webhookdb/service/helpers.rb +4 -0
  135. data/lib/webhookdb/service/middleware.rb +6 -2
  136. data/lib/webhookdb/service/view_api.rb +1 -1
  137. data/lib/webhookdb/service.rb +10 -10
  138. data/lib/webhookdb/service_integration.rb +19 -1
  139. data/lib/webhookdb/signalwire.rb +1 -1
  140. data/lib/webhookdb/spec_helpers/async.rb +0 -4
  141. data/lib/webhookdb/spec_helpers/sentry.rb +32 -0
  142. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +239 -64
  143. data/lib/webhookdb/spec_helpers.rb +1 -0
  144. data/lib/webhookdb/sync_target.rb +202 -34
  145. data/lib/webhookdb/system_log_event.rb +9 -0
  146. data/lib/webhookdb/tasks/admin.rb +1 -1
  147. data/lib/webhookdb/tasks/annotate.rb +1 -1
  148. data/lib/webhookdb/tasks/db.rb +13 -1
  149. data/lib/webhookdb/tasks/docs.rb +1 -1
  150. data/lib/webhookdb/tasks/fixture.rb +1 -1
  151. data/lib/webhookdb/tasks/message.rb +1 -1
  152. data/lib/webhookdb/tasks/regress.rb +1 -1
  153. data/lib/webhookdb/tasks/release.rb +1 -1
  154. data/lib/webhookdb/tasks/sidekiq.rb +1 -1
  155. data/lib/webhookdb/tasks/specs.rb +1 -1
  156. data/lib/webhookdb/version.rb +1 -1
  157. data/lib/webhookdb/webhook_subscription.rb +3 -4
  158. data/lib/webhookdb.rb +34 -8
  159. metadata +114 -64
  160. data/lib/webhookdb/jobs/customer_created_notify_internal.rb +0 -22
  161. data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +0 -21
  162. data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +0 -21
  163. /data/lib/webhookdb/jobs/{logged_webhook_resilient_replay.rb → logged_webhooks_resilient_replay.rb} +0 -0
  164. /data/lib/webhookdb/jobs/{webhook_resource_notify_integrations.rb → webhookdb_resource_notify_integrations.rb} +0 -0
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mixin for replicators that support partitioning.
4
+ # Partitioning is currently in beta,
5
+ # with the following limitations/context:
6
+ #
7
+ # - They cannot be created from the CLI.
8
+ # Because the partitions must be created during the CREATE TABLE call,
9
+ # the partition_value must be set immediately on creation,
10
+ # or CREATE TABLE must be deferred.
11
+ # - CLI support would also require making sure this field isn't edited.
12
+ # This is an annoying change, so we're putting it off for now.
13
+ # - Instead, partitioned replicators must be created in the console.
14
+ # - The number of HASH partitions cannot be changed;
15
+ # there is no good way to handle this in Postgres so we don't bother here.
16
+ # - RANGE partitions are not supported.
17
+ # We need to support creating the partition when the INSERT fails.
18
+ # But creating the partitioned table definition itself does work/has a shared behavior at least.
19
+ # - Existing replicators cannot be converted to partitioned.
20
+ # This is theoretically possible, but it seems easier to just start over
21
+ # with a new replicator.
22
+ # - Instead:
23
+ # - If this is a 'child' replicator, then create a new parent and this child,
24
+ # then copy over the parent data, either directly (for icalendar)
25
+ # or using HTTP requests (like with Plaid or Google) where more logic is required.
26
+ # - Otherwise, it'll depend on the replicator.
27
+ # - Then to switch clients using the old replicator, to the new replicator, you can:
28
+ # - Then turn off all workers.
29
+ # - Rename the new table to the old, and old table to the new.
30
+ # - Update the service integrations, so the old one points to the new table name and opaque id,
31
+ # and the new one points to the old table name and opaque id.
32
+ #
33
+ module Webhookdb::Replicator::PartitionableMixin
34
+ # The partition method, like Webhookdb::DBAdapter::Partitioning::HASH
35
+ def partition_method = raise NotImplementedError
36
+ # The partition column name.
37
+ # Must be present in +_denormalized_columns+.
38
+ # @return [Symbol]
39
+ def partition_column_name = raise NotImplementedError
40
+ # The value for the denormalized column. For HASH partitioning this would be an integer,
41
+ # for RANGE partitioning this could be a timestamp, etc.
42
+ # Takes the resource and returns the value.
43
+ def partition_value(_resource) = raise NotImplementedError
44
+
45
+ def partition? = true
46
+
47
+ def partitioning
48
+ return Webhookdb::DBAdapter::Partitioning.new(by: self.partition_method, column: self.partition_column_name)
49
+ end
50
+
51
+ def _prepare_for_insert(resource, event, request, enrichment)
52
+ h = super
53
+ h[self.partition_column_name] = self.partition_value(resource)
54
+ return h
55
+ end
56
+
57
+ def _upsert_conflict_target
58
+ return [self.partition_column_name, self._remote_key_column.name]
59
+ end
60
+
61
+ # Convert the given string into a stable MD5-derived hash
62
+ # that can be stored in a (signed, 4 bit) INTEGER column.
63
+ def _str2inthash(s)
64
+ # MD5 is 128 bits/16 bytes/32 hex chars (2 chars per byte).
65
+ # Integers are 32 bits/4 bytes/8 hex chars.
66
+ # Grab the first 8 chars and convert it to an integer.
67
+ unsigned_md5int = Digest::MD5.hexdigest(s)[..8].to_i(16)
68
+ # Then AND it with a 32 bit bitmask to make sure it fits in 32 bits
69
+ # (though I'm not entirely sure why the above doesn't result in 32 bits always).
70
+ unsigned_int32 = unsigned_md5int & 0xFFFFFFFF
71
+ # Convert it from unsigned (0 to 4.2B) to signed (-2.1B to 2.1B) by subtracting 2.1B
72
+ # (the max 2 byte integer), as opposed to a 4 byte integer which we're dealing with here.
73
+ signed_md5int = unsigned_int32 - MAX_16BIT_INT
74
+ return signed_md5int
75
+ end
76
+
77
+ MAX_16BIT_INT = 2**31
78
+
79
+ # Return the partitions belonging to the table.
80
+ # @param db The organization connection.
81
+ # @return [Array<Webhookdb::DBAdapter::Partition>]
82
+ def existing_partitions(db)
83
+ # SELECT inhrelid::regclass AS child
84
+ # FROM pg_catalog.pg_inherits
85
+ # WHERE inhparent = 'my_schema.foo'::regclass;
86
+ parent = self.schema_and_table_symbols.map(&:to_s).join(".")
87
+ partnames = db[Sequel[:pg_catalog][:pg_inherits]].
88
+ where(inhparent: Sequel[parent].cast(:regclass)).
89
+ select_map(Sequel[:inhrelid].cast(:regclass))
90
+ parent_table = self.dbadapter_table
91
+ result = partnames.map do |part|
92
+ suffix = self.partition_suffix(part)
93
+ Webhookdb::DBAdapter::Partition.new(parent_table:, partition_name: part.to_sym, suffix:)
94
+ end
95
+ return result
96
+ end
97
+
98
+ def partition_suffix(partname)
99
+ return partname[/_[a-zA-Z\d]+$/].to_sym
100
+ end
101
+
102
+ def partition_align_name
103
+ tblname = self.service_integration.table_name
104
+ self.service_integration.organization.admin_connection do |db|
105
+ partitions = self.existing_partitions(db)
106
+ db.transaction do
107
+ partitions.each do |partition|
108
+ next if partition.partition_name.to_s.start_with?(tblname)
109
+ schema = partition.parent_table.schema.name
110
+ new_partname = "#{tblname}#{partition.suffix}"
111
+ db << "ALTER TABLE #{schema}.#{partition.partition_name} RENAME TO #{new_partname}"
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -45,7 +45,7 @@ module Webhookdb::Replicator::ShopifyV1Mixin
45
45
  field = "api_url"
46
46
  value = "https://#{value}.myshopify.com"
47
47
  end
48
- return super(field, value)
48
+ return super
49
49
  end
50
50
 
51
51
  def calculate_webhook_state_machine
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "webhookdb/errors"
3
4
  require "webhookdb/signalwire"
5
+ require "webhookdb/messages/error_generic_backfill"
4
6
 
5
7
  class Webhookdb::Replicator::SignalwireMessageV1 < Webhookdb::Replicator::Base
6
8
  include Appydays::Loggable
@@ -50,7 +52,7 @@ class Webhookdb::Replicator::SignalwireMessageV1 < Webhookdb::Replicator::Base
50
52
  h = u.host.gsub(/\.signalwire\.com$/, "")
51
53
  value = h
52
54
  end
53
- return super(field, value, attr:)
55
+ return super
54
56
  end
55
57
 
56
58
  def calculate_backfill_state_machine
@@ -180,4 +182,32 @@ Press 'Show' next to the newly-created API token, and copy it.)
180
182
 
181
183
  return messages, data["next_page_uri"]
182
184
  end
185
+
186
+ def on_backfill_error(be)
187
+ e = Webhookdb::Errors.find_cause(be) do |ex|
188
+ next true if ex.is_a?(Webhookdb::Http::Error) && ex.status == 401
189
+ next true if ex.is_a?(::SocketError)
190
+ end
191
+ return unless e
192
+ if e.is_a?(::SocketError)
193
+ response_status = 0
194
+ response_body = e.message
195
+ request_url = "<unknown>"
196
+ request_method = "<unknown>"
197
+ else
198
+ response_status = e.status
199
+ response_body = e.body
200
+ request_url = e.uri.to_s
201
+ request_method = e.http_method
202
+ end
203
+ message = Webhookdb::Messages::ErrorGenericBackfill.new(
204
+ self.service_integration,
205
+ response_status:,
206
+ response_body:,
207
+ request_url:,
208
+ request_method:,
209
+ )
210
+ self.service_integration.organization.alerting.dispatch_alert(message)
211
+ return true
212
+ end
183
213
  end
@@ -99,7 +99,7 @@ module Webhookdb::Replicator::SponsyV1Mixin
99
99
  )
100
100
  rescue Webhookdb::Http::Error => e
101
101
  raise e unless e.status == 404
102
- self.logger.warn("sponsy_404", error: e)
102
+ self.logger.warn("sponsy_404", e)
103
103
  return [], nil
104
104
  end
105
105
 
@@ -40,7 +40,6 @@ class Webhookdb::Replicator::TransistorEpisodeStatsV1 < Webhookdb::Replicator::B
40
40
  :compound_identity,
41
41
  TEXT,
42
42
  data_key: "<compound key, see converter>",
43
- index: true,
44
43
  optional: true,
45
44
  converter: CONV_REMOTE_KEY,
46
45
  )
@@ -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,14 @@ 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
+
24
+ # Raised when the columns or indices for a replicator are invalid.
25
+ class BrokenSpecification < Webhookdb::WebhookdbError; end
23
26
 
24
27
  # Statically describe a replicator.
25
28
  class Descriptor < Webhookdb::TypedStruct
@@ -142,7 +145,7 @@ class Webhookdb::Replicator
142
145
  end
143
146
 
144
147
  class IndexSpec < Webhookdb::TypedStruct
145
- attr_reader :columns, :where
148
+ attr_reader :columns, :where, :identifier
146
149
  end
147
150
 
148
151
  class << self
@@ -16,6 +16,10 @@ module Webhookdb::Service::Helpers
16
16
  return Webhookdb::Service.logger
17
17
  end
18
18
 
19
+ def set_request_tags(tags)
20
+ Webhookdb::Service::Middleware::RequestLogger.set_request_tags(tags)
21
+ end
22
+
19
23
  # Return the currently-authenticated user,
20
24
  # or respond with a 401 if there is no authenticated user.
21
25
  def current_customer
@@ -128,12 +128,16 @@ module Webhookdb::Service::Middleware
128
128
  def request_tags(env)
129
129
  tags = super
130
130
  begin
131
- tags[:customer_id] = env["warden"].user(:customer)&.id || 0
131
+ c = env["warden"].user(:customer)
132
132
  rescue Sequel::DatabaseError
133
133
  # If we cant hit the database, ignore this for now.
134
134
  # We run this code on all code paths, including those that don't need the customer,
135
135
  # and we want those to run even if the DB is down (like health checks, for example).
136
- nil
136
+ c = nil
137
+ end
138
+ if c
139
+ tags[:customer_id] = c.id || 0
140
+ tags[:customer] = c.email
137
141
  end
138
142
  return tags
139
143
  end
@@ -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
 
@@ -329,6 +342,11 @@ class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integr
329
342
  # @!attribute skip_webhook_verification
330
343
  # @return [Boolean] Set this to disable webhook verification on this integration.
331
344
  # Useful when replaying logged webhooks.
345
+
346
+ # @!attribute partition_value
347
+ # @return [Integer] Value to control partitioning. For replicators that use hash partitioning,
348
+ # this defines the number of partitions. For other partition types, like range,
349
+ # the meaning of this value depends on the replicator itself.
332
350
  end
333
351
 
334
352
  # Table: service_integrations
@@ -9,7 +9,7 @@ module Webhookdb::Signalwire
9
9
 
10
10
  configurable(:signalwire) do
11
11
  setting :http_timeout, 30
12
- setting :sms_allowlist, [], convert: ->(s) { s.split }
12
+ setting :sms_allowlist, [], convert: lambda(&:split)
13
13
  end
14
14
 
15
15
  def self.send_sms(from:, to:, body:, project_id:, **kw)
@@ -22,10 +22,6 @@ module Webhookdb::SpecHelpers::Async
22
22
  Webhookdb::Slack.http_client = Webhookdb::Slack::NoOpHttpClient.new
23
23
  Webhookdb::Slack.suppress_all = false
24
24
  end
25
- if example.metadata[:sentry]
26
- Webhookdb::Sentry.dsn = "http://public:secret@not-really-sentry.nope/someproject"
27
- Webhookdb::Sentry.run_after_configured_hooks
28
- end
29
25
  end
30
26
 
31
27
  context.after(:each) do |example|
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/spec_helpers"
4
+ require "webhookdb/sentry"
5
+
6
+ module Webhookdb::SpecHelpers::Sentry
7
+ def self.included(context)
8
+ context.before(:each) do |example|
9
+ if example.metadata[:sentry]
10
+ # We need to fake doing what Sentry would be doing for initialization,
11
+ # so we can assert it has the right data in its scope.
12
+ Webhookdb::Sentry.dsn = "https://public:secret@test-sentry.webhookdb.com/whdb"
13
+ hub = Sentry::Hub.new(
14
+ Sentry::Client.new(Sentry::Configuration.new),
15
+ Sentry::Scope.new,
16
+ )
17
+ expect(Sentry).to_not be_initialized
18
+ Sentry.instance_variable_set(:@main_hub, hub)
19
+ expect(Sentry).to be_initialized
20
+ end
21
+ end
22
+
23
+ context.after(:each) do |example|
24
+ if example.metadata[:sentry]
25
+ Webhookdb::Sentry.reset_configuration
26
+ expect(Sentry).to_not be_initialized
27
+ end
28
+ end
29
+
30
+ super
31
+ end
32
+ end