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
@@ -115,7 +115,7 @@
115
115
  </style>
116
116
  <link rel="preconnect" href="https://fonts.gstatic.com" />
117
117
  <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;500;600;700&display=swap" rel="stylesheet">
118
- <script type="module" crossorigin src="/admin/assets/index-6aebf805.js"></script>
118
+ <script type="module" crossorigin src="/admin/assets/index-9306dd28.js"></script>
119
119
  </head>
120
120
 
121
121
  <body>
@@ -0,0 +1,30 @@
1
+ {% expose subject %}WebhookDB: {{ friendly_name }} Error{% endexpose %}
2
+
3
+ {% partial 'greeting' %}
4
+
5
+ <p>
6
+ WebhookDB encountered an error trying to sync {{ friendly_name }} data using your configured credentials. We have the following information about the failure:
7
+ </p>
8
+ <ul>
9
+ <li>Service Integration Name: {{ service_name }}, ID: <code>{{ opaque_id }}</code></li>
10
+ <li>Request: <code>{{ request_method }} {{ request_url }}</code></li>
11
+ <li>Response Status: <code>{{ response_status }}</code></li>
12
+ <li>Body: <code>{{ response_body }}</code></li>
13
+ </ul>
14
+ <p>
15
+ Usually this indicates an API key has been revoked or some other misconfiguration, rather than being an error in WebhookDB itself.
16
+ </p>
17
+ <p>To fix this integration, head over to <a href="{{ app_url }}">{{ app_url }}</a>, log in, and run:</p>
18
+ <pre>
19
+ webhookdb integrations reset {{ opaque_id }}
20
+ </pre>
21
+ <p>
22
+ If you no longer wish to use this integration, it can be deleted using:
23
+ </p>
24
+ <pre>
25
+ webhookdb integrations delete {{ opaque_id }}
26
+ </pre>
27
+ <p>We'll continue to send daily emails when this happens, so please do fix this up when you get a chance.</p>
28
+ <p>Please file an issue at {{ oss_repo }} or email <a href="mailto:{{ support_email }}">{{ support_email }}</a> if you need any help.</p>
29
+
30
+ {% partial 'signoff' %}
@@ -1,4 +1,4 @@
1
- {% expose subject %}WebhookDB: ICalendar Error{% endexpose %}
1
+ {% expose subject %}WebhookDB: ICalendar Error{% endexpose %}
2
2
 
3
3
  {% partial 'greeting' %}
4
4
 
@@ -6,8 +6,9 @@
6
6
  WebhookDB encountered an error syncing an ICalendar feed. We have the following information about the failure:
7
7
  </p>
8
8
  <ul>
9
+ <li>Organization: {{ org_name }} (key: <code>{{ org_key }}</code>)</li>
9
10
  <li>Service Integration Name: {{ service_name }}, ID: <code>{{ opaque_id }}</code></li>
10
- <li>Calendar ID (you send this to us when adding the calendar): <code>{{ external_calendar_id }}</code></li>
11
+ <li>Calendar ID (you sent this when adding the calendar): <code>{{ external_calendar_id }}</code></li>
11
12
  <li>Request: <code>{{ request_method }} {{ request_url }}</code></li>
12
13
  <li>Response Status: <code>{{ response_status }}</code></li>
13
14
  <li>Body: <code>{{ response_body }}</code></li>
@@ -22,7 +23,12 @@
22
23
  <p>
23
24
  If this calendar is no longer shared, it should be removed.
24
25
  Use a DELETE request, as per <a href="{{ docs_url }}/guides/icalendar/#delete">{{ docs_url }}/guides/icalendar/#delete</a>.
26
+ For example, here is the cURL to run to delete this from the shell:
25
27
  </p>
28
+ <pre>
29
+ $ export WHDB_WEBHOOK_SECRET=`webhookdb integration info --org={{ org_key }} --field=webhook_secret {{ opaque_id }}`
30
+ $ curl -X POST -d '{"type":"DELETE","external_id":"{{ external_calendar_id }}"}' -H "Content-Type: application/json" -H "Whdb-Webhook-Secret: ${WHDB_WEBHOOK_SECRET}" "{{ webhook_endpoint }}"
31
+ </pre>
26
32
  <p>We'll continue to send daily emails when this happens, so please do fix this up when you get a chance.</p>
27
33
  <p>Please file an issue at {{ oss_repo }} if you need any help.</p>
28
34
 
@@ -0,0 +1,6 @@
1
+ {% expose subject %}subject with multiple fields{% endexpose %}
2
+ a: {{ a }}
3
+ b: {{ b }}
4
+ c: {{ c }}
5
+ d: {{ d }}
6
+ e: {{ e }}
@@ -2,8 +2,10 @@
2
2
 
3
3
  Sequel.migration do
4
4
  change do
5
+ # rubocop:disable Sequel/IrreversibleMigration
5
6
  alter_table(:service_integrations) do
6
7
  drop_column :backfill_cursor
7
8
  end
9
+ # rubocop:enable Sequel/IrreversibleMigration
8
10
  end
9
11
  end
@@ -2,11 +2,13 @@
2
2
 
3
3
  Sequel.migration do
4
4
  change do
5
+ # rubocop:disable Sequel/IrreversibleMigration
5
6
  alter_table(:sync_targets) do
6
7
  set_column_default :page_size, nil
7
8
  end
8
9
  alter_table(:organizations) do
9
10
  set_column_default :minimum_sync_seconds, nil
10
11
  end
12
+ # rubocop:enable Sequel/IrreversibleMigration
11
13
  end
12
14
  end
@@ -19,10 +19,12 @@ Sequel.migration do
19
19
  :webhook_subscription_deliveries,
20
20
  :webhook_subscriptions,
21
21
  ]
22
+ # rubocop:disable Sequel/IrreversibleMigration
22
23
  searchable.each do |tbl|
23
24
  alter_table(tbl) do
24
25
  add_column :text_search, :tsvector
25
26
  end
26
27
  end
28
+ # rubocop:enable Sequel/IrreversibleMigration
27
29
  end
28
30
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:system_log_events) do
6
+ primary_key :id, type: :bigserial
7
+ timestamptz :at, null: false, default: Sequel.function(:now), index: true
8
+ text :title, null: false, default: ""
9
+ text :body, null: false, default: ""
10
+ text :link, null: false, default: ""
11
+ foreign_key :actor_id, :customers
12
+ column :text_search, :tsvector
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ alter_table(:service_integrations) do
6
+ add_index :depends_on_id
7
+ add_index :service_name
8
+ end
9
+
10
+ alter_table(:message_deliveries) do
11
+ add_index :soft_deleted_at
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ alter_table(:sync_targets) do
6
+ add_column :parallelism, :integer, null: false, default: 0
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ alter_table(:sync_targets) do
6
+ add_column :sync_stats, :jsonb, null: false, default: "[]"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:organization_error_handlers) do
6
+ primary_key :id
7
+ timestamptz :created_at, null: false, default: Sequel.function(:now)
8
+ timestamptz :updated_at
9
+
10
+ text :opaque_id, null: false, unique: true
11
+
12
+ foreign_key :organization_id, :organizations, null: false
13
+ foreign_key :created_by_id, :customers, null: true, on_delete: :set_null
14
+
15
+ text :url, null: false
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ no_transaction
5
+ up do
6
+ # These are optimized for Webhookdb::LoggedWebhook.trim. See code for details/coupling.
7
+ run "CREATE INDEX CONCURRENTLY logged_webhooks_trim_unowned_idx ON logged_webhooks(inserted_at) " \
8
+ "WHERE organization_id IS NULL"
9
+ run "CREATE INDEX CONCURRENTLY logged_webhooks_trim_success_idx ON logged_webhooks(inserted_at) " \
10
+ "WHERE organization_id IS NOT NULL AND response_status < 400 AND truncated_at IS NULL"
11
+ run "CREATE INDEX CONCURRENTLY logged_webhooks_delete_success_idx ON logged_webhooks(inserted_at) " \
12
+ "WHERE organization_id IS NOT NULL AND response_status < 400 AND truncated_at IS NOT NULL"
13
+ run "CREATE INDEX CONCURRENTLY logged_webhooks_trim_failures_idx ON logged_webhooks(inserted_at) " \
14
+ "WHERE organization_id IS NOT NULL AND response_status >= 400 AND truncated_at IS NULL"
15
+ run "CREATE INDEX CONCURRENTLY logged_webhooks_delete_failures_idx ON logged_webhooks(inserted_at) " \
16
+ "WHERE organization_id IS NOT NULL AND response_status >= 400 AND truncated_at IS NOT NULL"
17
+ end
18
+ down do
19
+ run "DROP INDEX logged_webhooks_trim_unowned_idx"
20
+ run "DROP INDEX logged_webhooks_trim_success_idx"
21
+ run "DROP INDEX logged_webhooks_delete_success_idx"
22
+ run "DROP INDEX logged_webhooks_trim_failures_idx"
23
+ run "DROP INDEX logged_webhooks_delete_failures_idx"
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ alter_table(:service_integrations) do
6
+ add_column :partition_value, :int, default: 0, null: false
7
+ end
8
+ end
9
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rspec/eventually"
4
-
5
3
  require "webhookdb/async"
6
4
 
7
5
  RSpec.describe "async workers", :integration do
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rspec/eventually"
4
-
5
3
  RSpec.describe "service integrations", :integration do
6
4
  def catch_missing_db(default)
7
5
  yield
@@ -351,8 +351,8 @@ module Amigo::DurableJob
351
351
  # Space-separate multiple env vars.
352
352
  setting :server_env_vars, ["DATABASE_URL"], convert: ->(s) { s.split.map(&:strip) }
353
353
 
354
- setting :schema_name, :public, convert: ->(s) { s.to_sym }
355
- setting :table_name, :durable_jobs, convert: ->(s) { s.to_sym }
354
+ setting :schema_name, :public, convert: lambda(&:to_sym)
355
+ setting :table_name, :durable_jobs, convert: lambda(&:to_sym)
356
356
 
357
357
  after_configured do
358
358
  self.storage_database_urls = self.server_urls.dup
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Amigo
4
+ end
5
+
6
+ module Amigo::JobInContext
7
+ class ServerMiddleware
8
+ def call(worker, job, queue, &)
9
+ Sidekiq::Context.with(worker:, queue:, job_hash: job, &)
10
+ end
11
+ end
12
+ end
@@ -1,4 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webhookdb::Admin
4
+ module Linked
5
+ protected def _admin_datatype = self.class.dataset.first_source_table
6
+ protected def _admin_id = self.pk
7
+ protected def _admin_display = "show"
8
+ def admin_link = "#{Webhookdb.admin_url}/admin#/#{_admin_datatype}/#{_admin_id}/#{_admin_display}"
9
+ end
4
10
  end
@@ -29,6 +29,7 @@ class Webhookdb::AdminAPI::DataProvider < Webhookdb::AdminAPI::V1
29
29
  service_integrations: [Webhookdb::ServiceIntegration, ServiceIntegration],
30
30
  subscriptions: [Webhookdb::Subscription, Subscription],
31
31
  sync_targets: [Webhookdb::SyncTarget, SyncTarget],
32
+ system_log_events: [Webhookdb::SystemLogEvent, SystemLogEvent],
32
33
  webhook_subscriptions: [Webhookdb::WebhookSubscription, WebhookSubscription],
33
34
  webhook_subscription_deliveries: [Webhookdb::WebhookSubscription::Delivery, WebhookSubscriptionDelivery],
34
35
  }.freeze
@@ -163,6 +163,14 @@ module Webhookdb::AdminAPI::Entities
163
163
  expose :page_size
164
164
  end
165
165
 
166
+ class SystemLogEvent < Base
167
+ expose :at
168
+ expose :title
169
+ expose :body
170
+ expose :link
171
+ expose :actor_id
172
+ end
173
+
166
174
  class WebhookSubscription < Base
167
175
  expose :organization, with: Related
168
176
  expose :service_integration, with: Related
@@ -18,7 +18,7 @@ require "webhookdb" unless defined?(Webhookdb)
18
18
  # end
19
19
  # return ag.finish
20
20
  #
21
- class Webhookdb::AggregateResult < StandardError
21
+ class Webhookdb::AggregateResult < Webhookdb::WebhookdbError
22
22
  attr_reader :successes, :failures, :errors
23
23
 
24
24
  def initialize(existing=nil)
@@ -136,10 +136,12 @@ module Webhookdb::API
136
136
  expose :opaque_id
137
137
  expose :service_integration, with: ServiceIntegrationEntity
138
138
  expose :period_seconds
139
+ expose :page_size
139
140
  expose :displaysafe_connection_url, as: :connection_url
140
141
  expose :table
141
142
  expose :schema
142
143
  expose :last_synced_at
144
+ expose :latency, &self.delegate_to(:latency, :to_i)
143
145
  expose :associated_type
144
146
  expose :associated_id
145
147
  expose :associated_object_display
@@ -155,8 +157,9 @@ module Webhookdb::API
155
157
  [:associated_object_display, "Associated"],
156
158
  [:schema_and_table_string, "Table"],
157
159
  [:last_synced_at, "Last Synced"],
160
+ [:latency, "Latency"],
158
161
  [:period_seconds, "Period"],
159
- [:page_size, "Page"],
162
+ [:page_size, "Page Size"],
160
163
  ]
161
164
  end
162
165
  end
@@ -168,8 +171,9 @@ module Webhookdb::API
168
171
  [:connection_url, "URL"],
169
172
  [:associated_object_display, "Associated"],
170
173
  [:last_synced_at, "Last Synced"],
174
+ [:latency, "Latency"],
171
175
  [:period_seconds, "Period"],
172
- [:page_size, "Page"],
176
+ [:page_size, "Page Size"],
173
177
  ]
174
178
  end
175
179
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/api"
4
+
5
+ class Webhookdb::API::ErrorHandlers < Webhookdb::API::V1
6
+ include Webhookdb::Service::Types
7
+
8
+ CREATE_PROMPT = "What is the URL that WebhookDB should POST to when a replicator fails? " \
9
+ "See #{Webhookdb::Organization::ErrorHandler::DOCS_URL} for more information:".freeze
10
+
11
+ resource :organizations do
12
+ route_param :org_identifier do
13
+ resource :error_handlers do
14
+ helpers do
15
+ def lookup!
16
+ org = lookup_org!
17
+ eh = org.error_handlers_dataset[opaque_id: params[:opaque_id].strip]
18
+ merror!(403, "There is no error handler with that id.") if eh.nil?
19
+ set_request_tags(error_handler_opaque_id: eh.opaque_id)
20
+ return eh
21
+ end
22
+
23
+ def guard_editable!(customer, org)
24
+ return if has_admin?(org, customer:)
25
+ permission_error!("You must be an org admin to modify error handlers.")
26
+ end
27
+
28
+ def url_valid?(u)
29
+ begin
30
+ uri = URI(u)
31
+ rescue URI::InvalidURIError
32
+ return false
33
+ end
34
+ return true if uri.scheme == "sentry"
35
+ return false unless uri.scheme&.start_with?("http")
36
+ return true
37
+ end
38
+ end
39
+
40
+ desc "Returns a list of all error handlers associated with the org."
41
+ get do
42
+ handlers = lookup_org!.error_handlers
43
+ message = ""
44
+ if handlers.empty?
45
+ message = "This organization doesn't have any error handlers yet.\n" \
46
+ "Use `webhookdb error-handler create` to set one up."
47
+ end
48
+ present_collection handlers, with: ErrorHandlerEntity, message:
49
+ end
50
+
51
+ desc "Creates a new error handler."
52
+ params do
53
+ optional :url, type: TrimmedString, prompt: CREATE_PROMPT
54
+ end
55
+ post :create do
56
+ cust = current_customer
57
+ org = lookup_org!
58
+ guard_editable!(cust, org)
59
+ unless url_valid?(params[:url])
60
+ msg = "URL is malformed. It should be a URL like https://foo.bar/path, http://foo.bar:123, etc. " \
61
+ "See #{Webhookdb::Organization::ErrorHandler::DOCS_URL} for more info."
62
+ merror!(400, msg)
63
+ end
64
+ eh = Webhookdb::Organization::ErrorHandler.create(
65
+ organization: org,
66
+ url: params[:url],
67
+ created_by: cust,
68
+ )
69
+ message = "Whenever one of your replicators errors, WebhookDB will alert the given URL. " \
70
+ "See #{Webhookdb::Organization::ErrorHandler::DOCS_URL} for more information."
71
+ status 200
72
+ present eh, with: ErrorHandlerEntity, message:
73
+ end
74
+
75
+ route_param :opaque_id, type: String do
76
+ get do
77
+ eh = lookup!
78
+ status 200
79
+ present eh, with: ErrorHandlerEntity
80
+ end
81
+
82
+ post :delete do
83
+ customer = current_customer
84
+ eh = lookup!
85
+ guard_editable!(customer, eh.organization)
86
+ eh.destroy
87
+ status 200
88
+ present eh, with: ErrorHandlerEntity,
89
+ message: "You have successfully deleted the error handler '#{eh.opaque_id}'."
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ class ErrorHandlerEntity < Webhookdb::API::BaseEntity
97
+ expose :opaque_id, as: :id
98
+ expose :url
99
+
100
+ def self.display_headers
101
+ return [[:id, "ID"], [:url, "Url"]]
102
+ end
103
+ end
104
+ end
@@ -122,7 +122,14 @@ module Webhookdb::API::Helpers
122
122
  sints = org.service_integrations_dataset.
123
123
  where(Sequel[service_name: identifier] | Sequel[table_name: identifier] | Sequel[opaque_id: identifier]).
124
124
  limit(2).all
125
- return sints.first if sints.size == 1
125
+ if sints.size == 1
126
+ sint = sints.first
127
+ set_request_tags(
128
+ service_integration_service_name: sint.service_name,
129
+ service_integration_table_name: sint.table_name,
130
+ )
131
+ return sint
132
+ end
126
133
  merror!(403, "There is no service integration with that identifier.") if sints.empty?
127
134
  dupe_attr = nil
128
135
  alternative = nil
@@ -171,6 +178,23 @@ module Webhookdb::API::Helpers
171
178
  sint = yield
172
179
  return if sint == :pass
173
180
  raise "error instead of return nil if there is no service integration" if sint.nil?
181
+ # If this is a valid GET request (ie, the replicator doesn't do useful auth),
182
+ # and it's from a bot, we want to block it, otherwise we'd process invalid webhooks.
183
+ #
184
+ # This is almost never a problem, because almost all services perform some validation,
185
+ # and the auth info isn't in a GET URL; but in some cases, that won't be the case,
186
+ # and we protect against it here.
187
+ #
188
+ # An example would be a bot getting a link preview.
189
+ if request.get? && !request.GET["skipbotcheck"] && Browser.new(request.user_agent).bot?
190
+ # IMPORTANT: Run the Browser code last since it has to run a bunch of regexes to detect a bot.
191
+ env["api.format"] = :binary
192
+ header "Content-Type", "text/plain"
193
+ body("This route is for receiving webhooks and HTTP calls, not for bots. " \
194
+ "Call this endpoint with the query param 'skipbotcheck=true' to bypass this check.")
195
+ status 403
196
+ return
197
+ end
174
198
  opaque_id = sint.opaque_id
175
199
  organization_id = sint.organization_id
176
200
  svc = Webhookdb::Replicator.create(sint).dispatch_request_to(request)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/api"
4
+ require "webhookdb/jobs/icalendar_enqueue_syncs_for_urls"
5
+
6
+ class Webhookdb::API::Icalproxy < Webhookdb::API::V1
7
+ resource :icalproxy do
8
+ resource :webhook do
9
+ post do
10
+ merror!(402, "Api key not configured") unless Webhookdb::Icalendar.proxy_api_key.present?
11
+ request_key = request.env["HTTP_AUTHORIZATION"] || ""
12
+ configured_key = "Apikey #{Webhookdb::Icalendar.proxy_api_key}"
13
+ verified = request_key && ActiveSupport::SecurityUtils.secure_compare(request_key, configured_key)
14
+ merror!(401, "Invalid api key") unless verified
15
+ urls = params.fetch(:urls)
16
+ Webhookdb::Jobs::IcalendarEnqueueSyncsForUrls.perform_async(urls)
17
+ status 202
18
+ present({o: "k"})
19
+ end
20
+ end
21
+ end
22
+ end
@@ -13,6 +13,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
13
13
  def lookup_session!
14
14
  session = Webhookdb::Oauth::Session.usable.where(oauth_state: params[:state]).first
15
15
  error!("Forbidden", 302, {"Location" => "/v1/install/#{oauth_provider.key}/forbidden"}) if session.nil?
16
+ set_request_tags(session_id: session.id, session_state: session.oauth_state)
16
17
  return session
17
18
  end
18
19
 
@@ -73,7 +74,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
73
74
  def exchange_authorization_code(code)
74
75
  return oauth_provider.exchange_authorization_code(code:)
75
76
  rescue Webhookdb::Http::Error => e
76
- logger.warn "oauth_exchange_error", exception: e
77
+ logger.warn "oauth_exchange_error", e
77
78
  url = "#{Webhookdb.api_url}/v1/install/#{oauth_provider.key}"
78
79
  raise FormError.new(
79
80
  "Something went wrong getting your access token from #{oauth_provider.app_name}. " \
@@ -206,6 +206,12 @@ class Webhookdb::API::Organizations < Webhookdb::API::V1
206
206
  {title: "Customer", value: "(#{c.id}) #{c.email}", short: false},
207
207
  ],
208
208
  ).emit
209
+ Webhookdb::SystemLogEvent.create(
210
+ title: "Organization Closure Requested",
211
+ body: "#{org.name} (#{org.key})",
212
+ link: org.admin_link,
213
+ actor: c,
214
+ )
209
215
  step = Webhookdb::Replicator::StateMachineStep.new.completed
210
216
  step.output = "Thanks! We've received the request to close your #{org.name} organization. " \
211
217
  "We'll be in touch within 2 business days confirming removal."
@@ -15,6 +15,7 @@ class Webhookdb::API::SavedQueries < Webhookdb::API::V1
15
15
  # We can add other identifiers in the future
16
16
  cq = org.saved_queries_dataset[opaque_id: params[:query_identifier]]
17
17
  merror!(403, "There is no saved query with that identifier.") if cq.nil?
18
+ set_request_tags(saved_query_opaque_id: cq.opaque_id)
18
19
  return cq
19
20
  end
20
21
 
@@ -13,6 +13,7 @@ class Webhookdb::API::SavedViews < Webhookdb::API::V1
13
13
  org = lookup_org!
14
14
  cq = org.saved_views_dataset[name: params[:name].strip]
15
15
  merror!(403, "There is no view with that name.") if cq.nil?
16
+ set_request_tags(saved_view_name: cq.name)
16
17
  return cq
17
18
  end
18
19
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "browser"
3
4
  require "grape"
4
5
  require "oj"
5
6
 
@@ -178,7 +179,7 @@ If the list does not look correct, you can contact support at #{Webhookdb.suppor
178
179
  begin
179
180
  svc.upsert_webhook_body(body)
180
181
  rescue KeyError, TypeError => e
181
- self.logger.error "immediate_upsert", error: e
182
+ self.logger.error "immediate_upsert", e
182
183
  err_msg = "Sorry! Looks like something has gone wrong. " \
183
184
  "Check your schema or contact support at #{Webhookdb.support_email}."
184
185
  merror!(400, err_msg)
@@ -151,6 +151,7 @@ class Webhookdb::API::SyncTargets < Webhookdb::API::V1
151
151
  (stgt = org.all_sync_targets_dataset[Sequel[:sync_targets][:opaque_id] => params[:opaque_id]])
152
152
  merror!(403, "There is no #{fullname} sync target with that id.") if
153
153
  stgt.nil? || !stgt.send(predicate)
154
+ set_request_tags(stgt.log_tags)
154
155
  return stgt
155
156
  end
156
157
  end
@@ -204,7 +205,6 @@ class Webhookdb::API::SyncTargets < Webhookdb::API::V1
204
205
  )
205
206
  end
206
207
 
207
- stgt.logger.warn("destroying_sync_target", sync_target_id: stgt.id, customer_id: current_customer.id)
208
208
  stgt.destroy
209
209
  status 200
210
210
  message = "#{fullname.capitalize} sync target has been removed and will no longer sync."
@@ -37,6 +37,11 @@ class Webhookdb::API::System < Webhookdb::Service
37
37
  }
38
38
  end
39
39
 
40
+ post :sink do
41
+ status 204
42
+ body ""
43
+ end
44
+
40
45
  if ["development", "test"].include?(Webhookdb::RACK_ENV)
41
46
  resource :debug do
42
47
  resource :echo do
@@ -52,6 +52,7 @@ class Webhookdb::API::WebhookSubscriptions < Webhookdb::API::V1
52
52
  org = lookup_org!
53
53
  whsub = org.all_webhook_subscriptions_dataset[opaque_id: params[:opaque_id]]
54
54
  merror!(403, "No webhook subscription with that ID exists in that organization.") if whsub.nil?
55
+ set_request_tags(webhook_subscription_id: whsub.id)
55
56
  return whsub
56
57
  end
57
58
  end
data/lib/webhookdb/api.rb CHANGED
@@ -52,11 +52,14 @@ module Webhookdb::API
52
52
  memberships = customer.verified_memberships_dataset.where(organization: orgs).limit(2).all
53
53
  permission_error!("You don't have permissions with that organization.") if memberships.empty?
54
54
  merror!(500, "ambiguous", alert: true) if memberships.size > 1 # TODO: better message, tests
55
- return memberships.first.organization
55
+ org = memberships.first.organization
56
+ set_request_tags(organization: org.key)
57
+ return org
56
58
  end
57
59
  raise "something went wrong" unless allow_connstr_auth
58
60
  org = Webhookdb::API::ConnstrAuth.find_authed(orgs, request)
59
61
  unauthenticated! if org.nil?
62
+ set_request_tags(organization: org.key)
60
63
  return org
61
64
  end
62
65