Implement webhook service and scheduling jobs [SCI-5801, SCI-5802]

This commit is contained in:
Martin Artnik 2021-06-17 09:19:04 +02:00
parent 6cf9ea5bc0
commit 4ee50f87d5
8 changed files with 283 additions and 2 deletions

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Activities
class DispatchWebhooksJobs < ApplicationJob
queue_as :high_priority
def perform(activity)
webhooks =
Webhook.where(
activity_filter_id:
Activities::ActivityFilterMatchingService.new(activity).activity_filters.select(:id)
)
webhooks.each do |webhook|
Activities::SendWebhookJob.perform_later(webhook, activity)
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Activities
class SendWebhookJob < ApplicationJob
queue_as :high_priority
def perform(webhook, activity)
Activities::WebhookService.new(webhook, activity).send_webhook
end
end
end

View file

@ -63,6 +63,8 @@ class Activity < ApplicationRecord
breadcrumbs: {}
)
after_create :dispatch_webhooks
def self.activity_types_list
activity_list = type_ofs.map do |key, value|
[
@ -151,4 +153,8 @@ class Activity < ApplicationRecord
def activity_version
errors.add(:activity, 'wrong combination of associations') if (experiment_id || my_module_id) && subject
end
def dispatch_webhooks
Activities::DispatchWebhooksJobs.perform_later(self)
end
end

View file

@ -8,6 +8,8 @@ class Webhook < ApplicationRecord
validates :url, presence: true
validate :valid_url
scope :active, -> { where(active: true) }
private
def valid_url

View file

@ -0,0 +1,56 @@
# frozen_string_literal: true
module Activities
class WebhookService
DISABLE_WEBHOOK_ERROR_THRESHOLD = 10
def initialize(webhook, activity)
@webhook = webhook
@activity = activity
end
def send_webhook
raise "Cannot send inactive webhook." unless @webhook.active?
response = HTTParty.send(
@webhook.method,
@webhook.url,
{
headers: { 'Content-Type' => 'application/json' },
body: activity_payload
}
)
unless response.success?
log_error!("#{response.status}: #{response.message}")
end
rescue Net::ReadTimeout, SocketError => error
log_error!(error)
ensure
disable_webhook_if_broken!
end
private
def activity_payload
@activity.values.merge(
type: @activity.type_of,
created_at: @activity.created_at
)
end
def log_error!(message)
error_count = @webhook.error_count + 1
@webhook.update(
error_count: error_count,
last_error: message
)
end
def disable_webhook_if_broken!
return if @webhook.error_count < DISABLE_WEBHOOK_ERROR_THRESHOLD
@webhook.update(active: false)
end
end
end

View file

@ -0,0 +1,6 @@
class AddErrorInfoToWebhooks < ActiveRecord::Migration[6.1]
def change
add_column :webhooks, :error_count, :integer, default: 0, null: false
add_column :webhooks, :last_error, :text
end
end

View file

@ -2801,7 +2801,9 @@ CREATE TABLE public.webhooks (
url character varying NOT NULL,
method integer NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
updated_at timestamp(6) without time zone NOT NULL,
error_count integer DEFAULT 0 NOT NULL,
last_error text
);
@ -7348,6 +7350,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20210410100006'),
('20210506125657'),
('20210531114633'),
('20210603152345');
('20210603152345'),
('20210616071836');

View file

@ -0,0 +1,178 @@
# frozen_string_literal: true
require 'rails_helper'
describe Activities::ActivityFilterMatchingService do
let(:user) { create :user }
let(:user_2) { create :user }
let(:team) { create :team, :with_members }
let(:team_2) { create :team }
let(:project) do
create :project, team: team, user_projects: []
end
let(:project_2) do
create :project, team: team, user_projects: []
end
let(:activity) { create :activity }
it 'matches activity filters by activity date' do
matching_filter = ActivityFilter.create(
name: "date filter",
filter: {"to_date"=>"2021-1-2", "from_date"=>"2021-1-1"}
)
non_matching_filter = ActivityFilter.create(
name: "date filter",
filter: {"to_date"=>"2021-12-2", "from_date"=>"2021-12-1"}
)
activity.update_column(:created_at, Date.parse("2021-1-1").to_time)
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
expect(matched_activity_filters).to include(matching_filter)
expect(matched_activity_filters).to_not include(non_matching_filter)
end
it 'matches activity filters by activity user' do
matching_filter = ActivityFilter.create(
name: "user filter 1",
filter: {"users" => [user.id.to_s], "from_date" => "", "to_date" => ""}
)
non_matching_filter = ActivityFilter.create(
name: "user filter 2",
filter: {"users" => [user_2.id.to_s], "from_date" => "", "to_date" => ""}
)
activity.update_column(:owner_id, user.id)
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
expect(matched_activity_filters).to include(matching_filter)
expect(matched_activity_filters).to_not include(non_matching_filter)
end
it 'matches activity filters by activity type' do
matching_filter = ActivityFilter.create(
name: "type filter 1",
filter: {"types" => ["163"], "from_date" => "", "to_date" => ""}
)
non_matching_filter = ActivityFilter.create(
name: "type filter 2",
filter: {"types" => ["0"], "from_date" => "", "to_date" => ""}
)
activity.update_column(:type_of, 163)
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
expect(matched_activity_filters).to include(matching_filter)
expect(matched_activity_filters).to_not include(non_matching_filter)
end
it 'matches activity filters by activity team' do
matching_filter = ActivityFilter.create(
name: "team filter 1",
filter: {"teams" => [team.id.to_s], "from_date" => "", "to_date" => ""}
)
non_matching_filter = ActivityFilter.create(
name: "team filter 2",
filter: {"teams" => [team_2.id.to_s], "from_date" => "", "to_date" => ""}
)
activity.update_column(:team_id, team.id)
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
expect(matched_activity_filters).to include(matching_filter)
expect(matched_activity_filters).to_not include(non_matching_filter)
end
it 'matches activity filters by activity subject' do
matching_filter = ActivityFilter.create(
name: "subject filter 1",
filter: {"subjects" => { "Project" => [project.id.to_s] }, "from_date" => "", "to_date" => ""}
)
non_matching_filter = ActivityFilter.create(
name: "subject filter 2",
filter: {"subjects" => { "Project" => [project_2.id.to_s] }, "from_date" => "", "to_date" => ""}
)
activity.update_columns(subject_type: "Project", subject_id: project.id)
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
expect(matched_activity_filters).to include(matching_filter)
expect(matched_activity_filters).to_not include(non_matching_filter)
end
it 'matches activity filters by a combination of filters' do
matching_filter = ActivityFilter.create(
name: "mixed filter 1",
filter: {
"subjects" => { "Project" => [project.id.to_s] },
"to_date"=>"2021-1-2",
"from_date"=>"2021-1-1",
"teams"=>[team.id.to_s],
"users"=>[user.id.to_s],
"types"=>["163"]
}
)
activity.update_columns(
created_at: Date.parse("2021-1-1").to_time,
owner_id: user.id,
type_of: 163,
subject_type: "Project",
subject_id: project.id,
team_id: team.id
)
non_matching_filter_1 = ActivityFilter.create(
name: "mixed filter 1",
filter: {
"subjects" => { "Project" => [project.id.to_s] },
"to_date"=>"2021-10-2",
"from_date"=>"2021-10-1",
"teams"=>[team.id.to_s],
"users"=>[user.id.to_s],
"types"=>["163"]
}
)
non_matching_filter_2 = ActivityFilter.create(
name: "mixed filter 2",
filter: {
"subjects" => { "Project" => [project.id.to_s] },
"to_date"=>"2021-1-2",
"from_date"=>"2021-1-1",
"teams"=>[team_2.id.to_s],
"users"=>[user.id.to_s],
"types"=>["163"]
}
)
non_matching_filter_3 = ActivityFilter.create(
name: "mixed filter 3",
filter: {
"subjects" => { "Project" => [project_2.id.to_s] },
"to_date"=>"2021-1-2",
"from_date"=>"2021-1-1",
"teams"=>[team.id.to_s],
"users"=>[user.id.to_s],
"types"=>["163"]
}
)
matched_activity_filters = Activities::ActivityFilterMatchingService.new(activity).activity_filters
expect(matched_activity_filters).to include(matching_filter)
expect(matched_activity_filters).to_not include(non_matching_filter_1)
expect(matched_activity_filters).to_not include(non_matching_filter_2)
expect(matched_activity_filters).to_not include(non_matching_filter_3)
end
end