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
@@ -19,6 +19,8 @@ class Webhookdb::Messages::ErrorSignalwireSendSms < Webhookdb::Message::Template
19
19
  )
20
20
  end
21
21
 
22
+ attr_accessor :service_integration
23
+
22
24
  def initialize(service_integration, request_url:, request_method:, response_status:, response_body:)
23
25
  @service_integration = service_integration
24
26
  @request_url = request_url
@@ -27,6 +27,22 @@ module Webhookdb::Messages::Testers
27
27
  end
28
28
  end
29
29
 
30
+ class WithFields < Base
31
+ # noinspection RubyInstanceVariableNamingConvention
32
+ def initialize(a: nil, b: nil, c: nil, d: nil, e: nil)
33
+ @a = a
34
+ @b = b
35
+ @c = c
36
+ @d = d
37
+ @e = e
38
+ super()
39
+ end
40
+
41
+ def liquid_drops
42
+ return super.merge(a: @a, b: @b, c: @c, d: @d, e: @e)
43
+ end
44
+ end
45
+
30
46
  class Nonextant < Base
31
47
  end
32
48
 
@@ -3,6 +3,14 @@
3
3
  require "appydays/configurable"
4
4
  require "appydays/loggable"
5
5
 
6
+ require "webhookdb/jobs/organization_error_handler_dispatch"
7
+
8
+ # Alert an organization when errors happen during webhook handling.
9
+ # These errors are explicitly managed in the handler code,
10
+ # and are usually things like outdated credentials.
11
+ # The alerts contain information about the error, and actions to take to fix the problem.
12
+ # If an organization has no +Webhookdb::Postgres::ErrorHandler+ registered,
13
+ # send an email to org admins instead.
6
14
  class Webhookdb::Organization::Alerting
7
15
  include Appydays::Configurable
8
16
 
@@ -13,6 +21,14 @@ class Webhookdb::Organization::Alerting
13
21
  # Each customer can only receive this many alerts for a given template per day.
14
22
  # Avoids spamming a customer when many rows of a replicator have problems.
15
23
  setting :max_alerts_per_customer_per_day, 15
24
+ # Timeout when POSTing to a customer-defined URL on errors.
25
+ # Should be relatively short.
26
+ setting :error_handler_timeout, 7
27
+ # How many times should we call an error handler before giving up?
28
+ # Error handlers are often lossy so it's not a big deal to give up.
29
+ setting :error_handler_retries, 5
30
+ # Wait this long before retrying an error handler.
31
+ setting :error_handler_retry_interval, 60
16
32
  end
17
33
 
18
34
  attr_reader :org
@@ -21,20 +37,54 @@ class Webhookdb::Organization::Alerting
21
37
  @org = org
22
38
  end
23
39
 
24
- # Dispatch the message template to administrators of the org.
40
+ # Dispatch an alert using the given message template.
41
+ # See +Webhookdb::Organization::Alerting+ for details about how alerts are dispatched.
25
42
  # @param message_template [Webhookdb::Message::Template]
26
- def dispatch_alert(message_template)
43
+ # @param separate_connection [true,false] Only relevant if the organization has no error handlers
44
+ # and email alerting (+dispatch_alert_default+) is used. If true, send the alert on a separate connection.
45
+ # See +Webhookdb::Idempotency+. Defaults to true since this is an alert method and we
46
+ # don't want it to error accidentally, if the code is called from an unexpected situation.
47
+ def dispatch_alert(message_template, separate_connection: true)
48
+ self.validate_template(message_template)
49
+ if self.org.error_handlers.empty?
50
+ self.dispatch_alert_default(message_template, separate_connection:)
51
+ return
52
+ end
53
+ self.org.error_handlers.each do |eh|
54
+ payload = eh.payload_for_template(message_template)
55
+ # It's possible that the template includes caller-provided values including improperly-encoded strings.
56
+ # Sidekiq's strict job args will do a dump/parse to check for valid args,
57
+ # which will potentially fail if valid utf-8 bytes are in a string that's encoded as ascii.
58
+ # Really hard to explain, so see the specs, but there's nothing we can do about invalid content
59
+ # other than not error.
60
+ payload = JSON.parse(JSON.dump(payload))
61
+ Webhookdb::Jobs::OrganizationErrorHandlerDispatch.perform_async(eh.id, payload.as_json)
62
+ end
63
+ end
64
+
65
+ private def validate_template(message_template)
27
66
  unless message_template.respond_to?(:signature)
28
- raise Webhookdb::InvalidPrecondition,
29
- "message template #{message_template.template_name} must define a #signature method, " \
67
+ msg = "message template #{message_template.template_name} must define a #signature method, " \
30
68
  "which is a unique identity for this error type, used for grouping and idempotency"
69
+ raise Webhookdb::InvalidPrecondition, msg
70
+
71
+ end
72
+ unless message_template.respond_to?(:service_integration)
73
+ msg = "message template #{message_template.template_name} must return " \
74
+ "its ServiceIntegration from #service_integration"
75
+ raise Webhookdb::InvalidPrecondition, msg
31
76
  end
77
+ return true
78
+ end
79
+
80
+ def dispatch_alert_default(message_template, separate_connection:)
32
81
  signature = message_template.signature
33
82
  max_alerts_per_customer_per_day = Webhookdb::Organization::Alerting.max_alerts_per_customer_per_day
34
83
  yesterday = Time.now - 24.hours
35
84
  self.org.admin_customers.each do |c|
36
- idemkey = "orgalert-#{signature}-#{c.id}"
37
- Webhookdb::Idempotency.every(Webhookdb::Organization::Alerting.interval).under_key(idemkey) do
85
+ idem = Webhookdb::Idempotency.every(Webhookdb::Organization::Alerting.interval)
86
+ idem = idem.using_seperate_connection if separate_connection
87
+ idem.under_key("orgalert-#{signature}-#{c.id}") do
38
88
  sent_last_day = Webhookdb::Message::Delivery.
39
89
  where(template: message_template.full_template_name, recipient: c).
40
90
  where { created_at > yesterday }.
@@ -4,7 +4,7 @@ class Webhookdb::Organization::DatabaseMigration < Webhookdb::Postgres::Model(:o
4
4
  include Webhookdb::Dbutil
5
5
 
6
6
  class MigrationInProgress < Webhookdb::DatabaseLocked; end
7
- class MigrationAlreadyFinished < StandardError; end
7
+ class MigrationAlreadyFinished < Webhookdb::WebhookdbError; end
8
8
 
9
9
  plugin :timestamps
10
10
  plugin :text_searchable, terms: [:organization, :started_by]
@@ -112,7 +112,7 @@ class Webhookdb::Organization::DatabaseMigration < Webhookdb::Postgres::Model(:o
112
112
  tscol = svc.timestamp_column.name
113
113
  dstdb[svc.qualified_table_sequel_identifier].
114
114
  insert_conflict(
115
- target: svc.remote_key_column.name,
115
+ target: svc._upsert_conflict_target,
116
116
  update_where: svc._update_where_expr,
117
117
  ).multi_insert(chunk)
118
118
  self.update(last_migrated_timestamp: chunk.last[tscol])
@@ -22,7 +22,7 @@ class Webhookdb::Organization::DbBuilder
22
22
  include Webhookdb::Dbutil
23
23
  extend Webhookdb::MethodUtilities
24
24
 
25
- class IsolatedOperationError < StandardError; end
25
+ class IsolatedOperationError < Webhookdb::ProgrammingError; end
26
26
 
27
27
  DATABASE = "database"
28
28
  SCHEMA = "schema"
@@ -282,7 +282,7 @@ class Webhookdb::Organization::DbBuilder
282
282
  # (and we probably don't want the admin role trying to delete itself).
283
283
  def remove_related_database
284
284
  return if @org.admin_connection_url_raw.blank?
285
- superuser_str = self._find_superuser_url_str
285
+ superuser_str = self.find_superuser_url_str
286
286
  # Cannot use conn cache since we may be removing ourselves
287
287
  borrow_conn(superuser_str) do |conn|
288
288
  case self.class.isolation_mode
@@ -309,7 +309,8 @@ class Webhookdb::Organization::DbBuilder
309
309
  end
310
310
  end
311
311
 
312
- protected def _find_superuser_url_str
312
+ # Find the superuser connection URL for the organization's database.
313
+ def find_superuser_url_str
313
314
  admin_url = URI.parse(@org.admin_connection_url_raw)
314
315
  superuser_str = self.class.available_server_urls.find do |sstr|
315
316
  surl = URI.parse(sstr)
@@ -325,7 +326,7 @@ class Webhookdb::Organization::DbBuilder
325
326
  def roll_connection_credentials
326
327
  raise IsolatedOperationError, "cannot roll credentials without a user isolation mode" unless
327
328
  self.class.isolate?(USER)
328
- superuser_uri = URI(self._find_superuser_url_str)
329
+ superuser_uri = URI(self.find_superuser_url_str)
329
330
  orig_readonly_user = URI(@org.readonly_connection_url_raw).user
330
331
  ro_user = self.randident("ro")
331
332
  ro_pwd = self.randident
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "premailer"
4
+
5
+ class Webhookdb::Organization::ErrorHandler < Webhookdb::Postgres::Model(:organization_error_handlers)
6
+ include Webhookdb::Dbutil
7
+
8
+ DOCS_URL = "https://docs.webhookdb.com/docs/integrating/error-handlers.html"
9
+
10
+ plugin :timestamps
11
+
12
+ many_to_one :organization, class: "Webhookdb::Organization"
13
+ many_to_one :created_by, class: "Webhookdb::Customer"
14
+
15
+ # @param tmpl [Webhookdb::Message::Template]
16
+ def payload_for_template(tmpl)
17
+ params = {
18
+ error_type: tmpl.class.name.split("::").last.underscore,
19
+ details: tmpl.liquid_drops.to_h,
20
+ signature: tmpl.signature,
21
+ organization_key: self.organization.key,
22
+ service_integration_id: tmpl.service_integration.opaque_id,
23
+ service_integration_name: tmpl.service_integration.service_name,
24
+ service_integration_table: tmpl.service_integration.table_name,
25
+ }
26
+ recipient = Webhookdb::Message::Transport.for(:email).recipient(Webhookdb.support_email)
27
+ message = Webhookdb::Message.render(tmpl, :email, recipient)
28
+ message = message.to_s.strip
29
+ message = Premailer.new(
30
+ message,
31
+ with_html_string: true,
32
+ warn_level: Premailer::Warnings::SAFE,
33
+ )
34
+ message = message.to_plain_text
35
+ params[:message] = message
36
+ return params
37
+ end
38
+
39
+ def dispatch(payload)
40
+ if self.sentry?
41
+ self._handle_sentry(payload)
42
+ return
43
+ end
44
+
45
+ Webhookdb::Http.post(
46
+ self.url,
47
+ payload,
48
+ timeout: Webhookdb::Organization::Alerting.error_handler_timeout,
49
+ logger: self.logger,
50
+ )
51
+ end
52
+
53
+ def sentry?
54
+ u = URI(self.url)
55
+ return u.scheme == "sentry" || u.host&.end_with?("sentry.io")
56
+ end
57
+
58
+ MAX_SENTRY_TAG_CHARS = 200
59
+
60
+ # See https://develop.sentry.dev/sdk/data-model/envelopes/ for directly posting to Sentry.
61
+ # We do NOT want to use the SDK here, since we do not want to leak anything,
62
+ # and anyway, the runtime information is not important.
63
+ def _handle_sentry(payload)
64
+ payload = payload.deep_symbolize_keys
65
+ now = Time.now.utc
66
+ # We can assume the url is the Sentry DSN
67
+ u = URI(self.url)
68
+ key = u.user
69
+ project_id = u.path.delete("/")
70
+ # Give some valid value for this, though it's not accurate.
71
+ client = "sentry-ruby/5.22.1"
72
+ ts = now.to_i
73
+ # Auth headers are done by capturing an actual request. The docs aren't clear about their format.
74
+ # It's possible using the DSN auth would also work but let's use this.
75
+ headers = {
76
+ "Content-Type" => "application/x-sentry-envelope",
77
+ "X-Sentry-Auth" => "Sentry sentry_version=7, sentry_key=#{key}, sentry_client=#{client}, sentry_timestamp=#{ts}",
78
+ }
79
+ event_id = Uuidx.v4
80
+ # The first line will be used as the title.
81
+ message = "WebhookDB Error in #{payload.fetch(:service_integration_name)}\n\n#{payload.fetch(:message)}"
82
+ # Let the caller set the level through query params
83
+ level = URI.decode_www_form(u.query || "").to_h.fetch("level", "warning")
84
+
85
+ # Split structured data into 'extra' (cannot be searched on, just shows in the UI)
86
+ # and 'tags' (can be searched/faceted on, shows in the right bar).
87
+ ignore_tags = Webhookdb::Message::Template.new.liquid_drops.keys.to_set
88
+ tags, extra = payload.fetch(:details).partition do |k, v|
89
+ # Non-strings are always tags
90
+ next true unless v.is_a?(String)
91
+ # Never tag on basic stuff that doesn't change ever
92
+ next false if ignore_tags.include?(k)
93
+ # Unstructured strings may include spaces or braces, and are not tags
94
+ next false if v.include?(" ") || v.include?("{")
95
+ # If it's a small string, treat it as a tag.
96
+ v.size < MAX_SENTRY_TAG_CHARS
97
+ end
98
+
99
+ # Envelope structure is a multiline JSON file, I guess jsonl format
100
+ envelopes = [
101
+ {event_id:, sent_at: now.iso8601},
102
+ {type: "event", content_type: "application/json"},
103
+ {
104
+ event_id:,
105
+ timestamp: now.iso8601,
106
+ platform: "ruby",
107
+ level:,
108
+ transaction: payload.fetch(:service_integration_table),
109
+ release: "webhookdb@#{Webhookdb::RELEASE}",
110
+ environment: Webhookdb::RACK_ENV,
111
+ tags: tags.to_h,
112
+ extra: extra.to_h,
113
+ # We should use the same grouping for these messages as we would for emails
114
+ fingerprint: [payload.fetch(:signature)],
115
+ message: message,
116
+ },
117
+ ]
118
+ body = envelopes.map(&:to_json).join("\n")
119
+ store_url = URI(self.url)
120
+ store_url.scheme = "https" if store_url.scheme == "sentry"
121
+ store_url.user = nil
122
+ store_url.password = nil
123
+ store_url.path = "/api/#{project_id}/envelope/"
124
+ store_url.query = ""
125
+ Webhookdb::Http.post(
126
+ store_url.to_s,
127
+ body,
128
+ headers:,
129
+ timeout: Webhookdb::Organization::Alerting.error_handler_timeout,
130
+ logger: self.logger,
131
+ )
132
+ end
133
+
134
+ #
135
+ # :Sequel Hooks:
136
+ #
137
+
138
+ def before_create
139
+ self[:opaque_id] ||= Webhookdb::Id.new_opaque_id("oeh")
140
+ end
141
+ end
@@ -7,7 +7,9 @@ require "webhookdb/stripe"
7
7
  require "webhookdb/jobs/replication_migration"
8
8
 
9
9
  class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
10
- class SchemaMigrationError < StandardError; end
10
+ include Webhookdb::Admin::Linked
11
+
12
+ class SchemaMigrationError < Webhookdb::ProgrammingError; end
11
13
 
12
14
  plugin :timestamps
13
15
  plugin :soft_deletes
@@ -35,6 +37,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
35
37
  adder: ->(om) { om.update(organization_id: id, verified: false) },
36
38
  order: :id
37
39
  one_to_many :service_integrations, class: "Webhookdb::ServiceIntegration", order: :id
40
+ one_to_many :error_handlers, class: "Webhookdb::Organization::ErrorHandler", order: :id
38
41
  one_to_many :saved_queries, class: "Webhookdb::SavedQuery", order: :id
39
42
  one_to_many :saved_views, class: "Webhookdb::SavedView", order: :id
40
43
  one_to_many :webhook_subscriptions, class: "Webhookdb::WebhookSubscription", order: :id
@@ -156,7 +159,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
156
159
  result = self.execute_readonly_query(sql)
157
160
  return result, nil
158
161
  rescue Sequel::DatabaseError => e
159
- self.logger.error("db_query_database_error", error: e)
162
+ self.logger.error("db_query_database_error", e)
160
163
  # We want to handle InsufficientPrivileges and UndefinedTable explicitly
161
164
  # since we can hint the user at what to do.
162
165
  # Otherwise, we should just return the Postgres exception.
@@ -229,9 +232,10 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
229
232
  return "#{self.name} (#{self.key})"
230
233
  end
231
234
 
232
- def prepare_database_connections?
233
- return self.prepare_database_connections(safe: true)
234
- end
235
+ # @return [Webhookdb::Organization::DbBuilder]
236
+ def db_builder = Webhookdb::Organization::DbBuilder.new(self)
237
+
238
+ def prepare_database_connections? = self.prepare_database_connections(safe: true)
235
239
 
236
240
  # Build the org-specific users, database, and set our connection URLs to it.
237
241
  # @param safe [*] If true, noop if connection urls are set.
@@ -242,7 +246,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
242
246
  return if safe
243
247
  raise Webhookdb::InvalidPrecondition, "connections already set"
244
248
  end
245
- builder = Webhookdb::Organization::DbBuilder.new(self)
249
+ builder = self.db_builder
246
250
  builder.prepare_database_connections
247
251
  self.admin_connection_url_raw = builder.admin_url
248
252
  self.readonly_connection_url_raw = builder.readonly_url
@@ -264,7 +268,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
264
268
  end
265
269
  # Use the raw URL, even though we know at this point
266
270
  # public_host is empty so raw and public host urls are the same.
267
- Webhookdb::Organization::DbBuilder.new(self).create_public_host_cname(self.readonly_connection_url_raw)
271
+ self.db_builder.create_public_host_cname(self.readonly_connection_url_raw)
268
272
  self.save_changes
269
273
  end
270
274
  end
@@ -274,7 +278,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
274
278
  def remove_related_database
275
279
  self.db.transaction do
276
280
  self.lock!
277
- Webhookdb::Organization::DbBuilder.new(self).remove_related_database
281
+ self.db_builder.remove_related_database
278
282
  self.admin_connection_url_raw = ""
279
283
  self.readonly_connection_url_raw = ""
280
284
  self.save_changes
@@ -375,7 +379,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
375
379
  def roll_database_credentials
376
380
  self.db.transaction do
377
381
  self.lock!
378
- builder = Webhookdb::Organization::DbBuilder.new(self)
382
+ builder = self.db_builder
379
383
  builder.roll_connection_credentials
380
384
  self.admin_connection_url_raw = builder.admin_url
381
385
  self.readonly_connection_url_raw = builder.readonly_url
@@ -387,7 +391,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
387
391
  Webhookdb::DBAdapter.validate_identifier!(schema, type: "schema")
388
392
  Webhookdb::Organization::DatabaseMigration.guard_ongoing!(self)
389
393
  raise SchemaMigrationError, "destination and target schema are the same" if schema == self.replication_schema
390
- builder = Webhookdb::Organization::DbBuilder.new(self)
394
+ builder = self.db_builder
391
395
  sql = builder.migration_replication_schema_sql(self.replication_schema, schema)
392
396
  self.admin_connection(transaction: true) do |db|
393
397
  db << sql
@@ -454,6 +458,17 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
454
458
  return self.add_all_membership(opts)
455
459
  end
456
460
 
461
+ def close(confirm:)
462
+ raise Webhookdb::InvalidPrecondition, "confirm must be true to close the org" unless confirm
463
+ unless self.service_integrations_dataset.empty?
464
+ msg = "Organization[#{self.key} cannot close with active service integrations"
465
+ raise Webhookdb::InvalidPrecondition, msg
466
+ end
467
+ memberships = self.all_memberships_dataset.all.each(&:destroy)
468
+ self.destroy
469
+ return [self, memberships]
470
+ end
471
+
457
472
  # SUBSCRIPTION PERMISSIONS
458
473
 
459
474
  def active_subscription?
@@ -499,6 +514,57 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
499
514
 
500
515
  # @!attribute service_integrations
501
516
  # @return [Array<Webhookdb::ServiceIntegration>]
517
+
518
+ # @!attribute created_at
519
+ # @return [Time,nil]
520
+
521
+ # @!attribute updated_at
522
+ # @return [Time,nil]
523
+
524
+ # @!attribute soft_deleted_at
525
+ # @return [Time,nil]
526
+
527
+ # @!attribute name
528
+ # @return [String]
529
+
530
+ # @!attribute key
531
+ # @return [String]
532
+
533
+ # @!attribute billing_email
534
+ # @return [String]
535
+
536
+ # @!attribute stripe_customer_id
537
+ # @return [String]
538
+
539
+ # @!attribute readonly_connection_url_raw
540
+ # @return [String]
541
+
542
+ # @!attribute admin_connection_url_raw
543
+ # @return [String]
544
+
545
+ # @!attribute public_host
546
+ # @return [String]
547
+
548
+ # @!attribute cloudflare_dns_record_json
549
+ # @return [Hash]
550
+
551
+ # @!attribute replication_schema
552
+ # @return [String]
553
+
554
+ # @!attribute job_semaphore_size
555
+ # @return [Integer]
556
+
557
+ # @!attribute minimum_sync_seconds
558
+ # @return [Integer]
559
+
560
+ # @!attribute sync_target_timeout
561
+ # @return [Integer]
562
+
563
+ # @!attribute max_query_rows
564
+ # @return [Integer]
565
+
566
+ # @!attribute priority_backfill
567
+ # @return [true,false]
502
568
  end
503
569
 
504
570
  require "webhookdb/organization/alerting"
@@ -7,6 +7,7 @@ require "sequel"
7
7
  require "tsort"
8
8
 
9
9
  require "webhookdb"
10
+ require "webhookdb/admin"
10
11
  require "webhookdb/postgres"
11
12
  require "webhookdb/postgres/validations"
12
13
  require "webhookdb/postgres/model_utilities"
@@ -208,7 +208,9 @@ module Webhookdb::Postgres::ModelUtilities
208
208
  rescue NoMethodError
209
209
  encrypted = Set.new
210
210
  end
211
+ text_search_col = self.class.respond_to?(:text_search_column) && self.class.text_search_column
211
212
  values = values.map do |(k, v)|
213
+ next "#{k}: {#{v.size}}" if k == text_search_col
212
214
  k = k.to_s
213
215
  v = if v.is_a?(Time)
214
216
  self.inspect_time(v)
@@ -13,7 +13,7 @@ module Webhookdb::Postgres
13
13
  extend Webhookdb::MethodUtilities
14
14
  include Appydays::Loggable
15
15
 
16
- class InTransaction < StandardError; end
16
+ class InTransaction < Webhookdb::ProgrammingError; end
17
17
 
18
18
  singleton_attr_accessor :unsafe_skip_transaction_check
19
19
  @unsafe_skip_transaction_check = false
@@ -59,6 +59,7 @@ module Webhookdb::Postgres
59
59
  "webhookdb/oauth/session",
60
60
  "webhookdb/organization",
61
61
  "webhookdb/organization/database_migration",
62
+ "webhookdb/organization/error_handler",
62
63
  "webhookdb/organization_membership",
63
64
  "webhookdb/role",
64
65
  "webhookdb/saved_query",
@@ -66,6 +67,7 @@ module Webhookdb::Postgres
66
67
  "webhookdb/service_integration",
67
68
  "webhookdb/subscription",
68
69
  "webhookdb/sync_target",
70
+ "webhookdb/system_log_event",
69
71
  "webhookdb/webhook_subscription",
70
72
  "webhookdb/webhook_subscription/delivery",
71
73
  ].freeze
@@ -81,7 +83,6 @@ module Webhookdb::Postgres
81
83
  ### Register the given +superclass+ as a base class for a set of models, for operations
82
84
  ### which should happen on all the current database connections.
83
85
  def self.register_model_superclass(superclass)
84
- self.logger.debug "Registered model superclass: %p" % [superclass]
85
86
  self.model_superclasses << superclass
86
87
  end
87
88
 
@@ -92,8 +93,6 @@ module Webhookdb::Postgres
92
93
 
93
94
  ### Add a +path+ to require once the database connection is set.
94
95
  def self.register_model(path)
95
- self.logger.debug "Registered model for requiring: %s" % [path]
96
-
97
96
  # If the connection's set, require the path immediately.
98
97
  if self.model_superclasses.any?(&:db)
99
98
  Appydays::Loggable[self].silence(:fatal) do