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.
- checksums.yaml +4 -4
- data/admin-dist/assets/{index-6aebf805.js → index-9306dd28.js} +39 -39
- data/admin-dist/index.html +1 -1
- data/data/messages/templates/errors/generic_backfill.email.liquid +30 -0
- data/data/messages/templates/errors/icalendar_fetch.email.liquid +8 -2
- data/data/messages/templates/specs/with_fields.email.liquid +6 -0
- data/db/migrations/026_undo_integration_backfill_cursor.rb +2 -0
- data/db/migrations/032_remove_db_defaults.rb +2 -0
- data/db/migrations/043_text_search.rb +2 -0
- data/db/migrations/045_system_log.rb +15 -0
- data/db/migrations/046_indices.rb +14 -0
- data/db/migrations/047_sync_parallelism.rb +9 -0
- data/db/migrations/048_sync_stats.rb +9 -0
- data/db/migrations/049_error_handlers.rb +18 -0
- data/db/migrations/050_logged_webhook_indices.rb +25 -0
- data/db/migrations/051_partitioning.rb +9 -0
- data/integration/async_spec.rb +0 -2
- data/integration/service_integrations_spec.rb +0 -2
- data/lib/amigo/durable_job.rb +2 -2
- data/lib/amigo/job_in_context.rb +12 -0
- data/lib/webhookdb/admin.rb +6 -0
- data/lib/webhookdb/admin_api/data_provider.rb +1 -0
- data/lib/webhookdb/admin_api/entities.rb +8 -0
- data/lib/webhookdb/aggregate_result.rb +1 -1
- data/lib/webhookdb/api/entities.rb +6 -2
- data/lib/webhookdb/api/error_handlers.rb +104 -0
- data/lib/webhookdb/api/helpers.rb +25 -1
- data/lib/webhookdb/api/icalproxy.rb +22 -0
- data/lib/webhookdb/api/install.rb +2 -1
- data/lib/webhookdb/api/organizations.rb +6 -0
- data/lib/webhookdb/api/saved_queries.rb +1 -0
- data/lib/webhookdb/api/saved_views.rb +1 -0
- data/lib/webhookdb/api/service_integrations.rb +2 -1
- data/lib/webhookdb/api/sync_targets.rb +1 -1
- data/lib/webhookdb/api/system.rb +5 -0
- data/lib/webhookdb/api/webhook_subscriptions.rb +1 -0
- data/lib/webhookdb/api.rb +4 -1
- data/lib/webhookdb/apps.rb +4 -0
- data/lib/webhookdb/async/autoscaler.rb +10 -0
- data/lib/webhookdb/async/job.rb +4 -0
- data/lib/webhookdb/async/scheduled_job.rb +4 -0
- data/lib/webhookdb/async.rb +2 -0
- data/lib/webhookdb/backfiller.rb +17 -4
- data/lib/webhookdb/concurrent.rb +96 -0
- data/lib/webhookdb/connection_cache.rb +57 -10
- data/lib/webhookdb/console.rb +1 -1
- data/lib/webhookdb/customer/reset_code.rb +1 -1
- data/lib/webhookdb/customer.rb +5 -4
- data/lib/webhookdb/database_document.rb +1 -1
- data/lib/webhookdb/db_adapter/default_sql.rb +1 -14
- data/lib/webhookdb/db_adapter/partition.rb +14 -0
- data/lib/webhookdb/db_adapter/partitioning.rb +8 -0
- data/lib/webhookdb/db_adapter/pg.rb +77 -5
- data/lib/webhookdb/db_adapter/snowflake.rb +15 -6
- data/lib/webhookdb/db_adapter.rb +25 -3
- data/lib/webhookdb/dbutil.rb +2 -0
- data/lib/webhookdb/errors.rb +34 -0
- data/lib/webhookdb/fixtures/logged_webhooks.rb +4 -0
- data/lib/webhookdb/fixtures/organization_error_handlers.rb +20 -0
- data/lib/webhookdb/http.rb +30 -16
- data/lib/webhookdb/icalendar.rb +30 -9
- data/lib/webhookdb/jobs/amigo_test_jobs.rb +1 -1
- data/lib/webhookdb/jobs/backfill.rb +21 -25
- data/lib/webhookdb/jobs/create_mirror_table.rb +3 -4
- data/lib/webhookdb/jobs/deprecated_jobs.rb +3 -0
- data/lib/webhookdb/jobs/emailer.rb +2 -1
- data/lib/webhookdb/jobs/front_signalwire_message_channel_sync_inbound.rb +15 -0
- data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +7 -2
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +74 -11
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs_for_urls.rb +22 -0
- data/lib/webhookdb/jobs/icalendar_sync.rb +21 -9
- data/lib/webhookdb/jobs/increase_event_handler.rb +3 -2
- data/lib/webhookdb/jobs/{logged_webhook_replay.rb → logged_webhooks_replay.rb} +5 -3
- data/lib/webhookdb/jobs/message_dispatched.rb +1 -0
- data/lib/webhookdb/jobs/model_event_system_log_tracker.rb +112 -0
- data/lib/webhookdb/jobs/monitor_metrics.rb +29 -0
- data/lib/webhookdb/jobs/organization_database_migration_notify.rb +32 -0
- data/lib/webhookdb/jobs/organization_database_migration_run.rb +4 -6
- data/lib/webhookdb/jobs/organization_error_handler_dispatch.rb +26 -0
- data/lib/webhookdb/jobs/prepare_database_connections.rb +1 -0
- data/lib/webhookdb/jobs/process_webhook.rb +11 -12
- data/lib/webhookdb/jobs/renew_watch_channel.rb +10 -10
- data/lib/webhookdb/jobs/replication_migration.rb +5 -2
- data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +1 -2
- data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -2
- data/lib/webhookdb/jobs/send_invite.rb +3 -2
- data/lib/webhookdb/jobs/send_test_webhook.rb +1 -3
- data/lib/webhookdb/jobs/send_webhook.rb +4 -5
- data/lib/webhookdb/jobs/stale_row_deleter.rb +31 -0
- data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +3 -0
- data/lib/webhookdb/jobs/sync_target_run_sync.rb +9 -15
- data/lib/webhookdb/jobs/{webhook_subscription_delivery_attempt.rb → webhook_subscription_delivery_event.rb} +5 -8
- data/lib/webhookdb/liquid/expose.rb +1 -1
- data/lib/webhookdb/liquid/filters.rb +1 -1
- data/lib/webhookdb/liquid/partial.rb +2 -2
- data/lib/webhookdb/logged_webhook/resilient.rb +3 -3
- data/lib/webhookdb/logged_webhook.rb +16 -2
- data/lib/webhookdb/message/email_transport.rb +1 -1
- data/lib/webhookdb/message/transport.rb +1 -1
- data/lib/webhookdb/message.rb +55 -4
- data/lib/webhookdb/messages/error_generic_backfill.rb +47 -0
- data/lib/webhookdb/messages/error_icalendar_fetch.rb +5 -0
- data/lib/webhookdb/messages/error_signalwire_send_sms.rb +2 -0
- data/lib/webhookdb/messages/specs.rb +16 -0
- data/lib/webhookdb/organization/alerting.rb +56 -6
- data/lib/webhookdb/organization/database_migration.rb +2 -2
- data/lib/webhookdb/organization/db_builder.rb +5 -4
- data/lib/webhookdb/organization/error_handler.rb +141 -0
- data/lib/webhookdb/organization.rb +76 -10
- data/lib/webhookdb/postgres/model.rb +1 -0
- data/lib/webhookdb/postgres/model_utilities.rb +2 -0
- data/lib/webhookdb/postgres.rb +3 -4
- data/lib/webhookdb/replicator/base.rb +202 -68
- data/lib/webhookdb/replicator/base_stale_row_deleter.rb +165 -0
- data/lib/webhookdb/replicator/column.rb +2 -0
- data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +0 -1
- data/lib/webhookdb/replicator/fake.rb +106 -88
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +131 -61
- data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +17 -0
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +197 -32
- data/lib/webhookdb/replicator/icalendar_event_v1.rb +20 -44
- data/lib/webhookdb/replicator/icalendar_event_v1_partitioned.rb +33 -0
- data/lib/webhookdb/replicator/intercom_contact_v1.rb +1 -0
- data/lib/webhookdb/replicator/intercom_conversation_v1.rb +1 -0
- data/lib/webhookdb/replicator/intercom_v1_mixin.rb +49 -6
- data/lib/webhookdb/replicator/partitionable_mixin.rb +116 -0
- data/lib/webhookdb/replicator/shopify_v1_mixin.rb +1 -1
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +31 -1
- data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
- data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +0 -1
- data/lib/webhookdb/replicator/transistor_episode_v1.rb +11 -5
- data/lib/webhookdb/replicator/webhook_request.rb +8 -0
- data/lib/webhookdb/replicator.rb +6 -3
- data/lib/webhookdb/service/helpers.rb +4 -0
- data/lib/webhookdb/service/middleware.rb +6 -2
- data/lib/webhookdb/service/view_api.rb +1 -1
- data/lib/webhookdb/service.rb +10 -10
- data/lib/webhookdb/service_integration.rb +19 -1
- data/lib/webhookdb/signalwire.rb +1 -1
- data/lib/webhookdb/spec_helpers/async.rb +0 -4
- data/lib/webhookdb/spec_helpers/sentry.rb +32 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +239 -64
- data/lib/webhookdb/spec_helpers.rb +1 -0
- data/lib/webhookdb/sync_target.rb +202 -34
- data/lib/webhookdb/system_log_event.rb +9 -0
- data/lib/webhookdb/tasks/admin.rb +1 -1
- data/lib/webhookdb/tasks/annotate.rb +1 -1
- data/lib/webhookdb/tasks/db.rb +13 -1
- data/lib/webhookdb/tasks/docs.rb +1 -1
- data/lib/webhookdb/tasks/fixture.rb +1 -1
- data/lib/webhookdb/tasks/message.rb +1 -1
- data/lib/webhookdb/tasks/regress.rb +1 -1
- data/lib/webhookdb/tasks/release.rb +1 -1
- data/lib/webhookdb/tasks/sidekiq.rb +1 -1
- data/lib/webhookdb/tasks/specs.rb +1 -1
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb/webhook_subscription.rb +3 -4
- data/lib/webhookdb.rb +34 -8
- metadata +114 -64
- data/lib/webhookdb/jobs/customer_created_notify_internal.rb +0 -22
- data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +0 -21
- data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +0 -21
- /data/lib/webhookdb/jobs/{logged_webhook_resilient_replay.rb → logged_webhooks_resilient_replay.rb} +0 -0
- /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
|
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
|
-
|
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
|
-
|
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
|
-
|
37
|
-
|
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 <
|
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.
|
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 <
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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",
|
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
|
-
|
233
|
-
|
234
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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 =
|
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 =
|
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"
|
@@ -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)
|
data/lib/webhookdb/postgres.rb
CHANGED
@@ -13,7 +13,7 @@ module Webhookdb::Postgres
|
|
13
13
|
extend Webhookdb::MethodUtilities
|
14
14
|
include Appydays::Loggable
|
15
15
|
|
16
|
-
class InTransaction <
|
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
|