click_session 0.0.1

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +286 -0
  7. data/Rakefile +1 -0
  8. data/click_session.gemspec +36 -0
  9. data/lib/click_session/async.rb +45 -0
  10. data/lib/click_session/click_session_processor.rb +64 -0
  11. data/lib/click_session/configuration.rb +142 -0
  12. data/lib/click_session/exceptions.rb +12 -0
  13. data/lib/click_session/failure_status_reporter.rb +15 -0
  14. data/lib/click_session/notifier.rb +23 -0
  15. data/lib/click_session/response_serializer.rb +34 -0
  16. data/lib/click_session/s3_connection.rb +34 -0
  17. data/lib/click_session/s3_file_uploader.rb +24 -0
  18. data/lib/click_session/session_state.rb +64 -0
  19. data/lib/click_session/status_reporter.rb +81 -0
  20. data/lib/click_session/successful_status_reporter.rb +15 -0
  21. data/lib/click_session/sync.rb +76 -0
  22. data/lib/click_session/version.rb +3 -0
  23. data/lib/click_session/web_runner.rb +60 -0
  24. data/lib/click_session/web_runner_processor.rb +65 -0
  25. data/lib/click_session/webhook.rb +24 -0
  26. data/lib/click_session/webhook_model_serializer.rb +7 -0
  27. data/lib/click_session.rb +34 -0
  28. data/lib/generators/click_session/db/migration/create_session_states.rb +13 -0
  29. data/lib/generators/click_session/initializers/click_session.rb +4 -0
  30. data/lib/generators/click_session/install_generator.rb +54 -0
  31. data/lib/tasks/click_session.rake +52 -0
  32. data/spec/click_session/async_spec.rb +66 -0
  33. data/spec/click_session/click_session_processor_spec.rb +292 -0
  34. data/spec/click_session/configuration_spec.rb +168 -0
  35. data/spec/click_session/failure_status_reporter_spec.rb +87 -0
  36. data/spec/click_session/notifier_spec.rb +72 -0
  37. data/spec/click_session/response_serializer_spec.rb +50 -0
  38. data/spec/click_session/s3_file_uploader_spec.rb +24 -0
  39. data/spec/click_session/session_state_spec.rb +54 -0
  40. data/spec/click_session/status_reporter_spec.rb +199 -0
  41. data/spec/click_session/successful_status_reporter_spec.rb +85 -0
  42. data/spec/click_session/sync_spec.rb +259 -0
  43. data/spec/click_session/web_runner_processor_spec.rb +143 -0
  44. data/spec/click_session/web_runner_spec.rb +77 -0
  45. data/spec/click_session/webhook_spec.rb +75 -0
  46. data/spec/factories/test_unit_model_factory.rb +5 -0
  47. data/spec/spec_helper.rb +42 -0
  48. data/spec/support/click_session_runner.rb +5 -0
  49. data/spec/support/dummy_web_runner.rb +2 -0
  50. data/spec/support/schema.rb +16 -0
  51. data/spec/support/test_unit_model.rb +3 -0
  52. metadata +310 -0
@@ -0,0 +1,34 @@
1
+ module ClickSession
2
+ class ResponseSerializer
3
+ def serialize_success(click_session)
4
+ {
5
+ id: click_session.id,
6
+ status: {
7
+ success: true
8
+ },
9
+ data: serializer.serialize(click_session.model)
10
+ }
11
+ end
12
+
13
+ def serialize_failure(click_session)
14
+ {
15
+ id: click_session.id,
16
+ status: {
17
+ success: false
18
+ }
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ delegate :serializer_class, :notifier_class, to: :clicksession_configuration
25
+
26
+ def serializer
27
+ @serializer ||= serializer_class.new
28
+ end
29
+
30
+ def clicksession_configuration
31
+ ClickSession.configuration
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ module ClickSession
2
+ class S3Connection
3
+ attr_reader :bucket_name
4
+
5
+ def initialize(
6
+ key_id = ClickSession.configuration.screenshot[:s3_key_id],
7
+ access_key = ClickSession.configuration.screenshot[:s3_access_key],
8
+ bucket_name = ClickSession.configuration.screenshot[:s3_bucket]
9
+ )
10
+ @key_id = key_id
11
+ @access_key = access_key
12
+ @bucket_name = bucket_name
13
+ end
14
+
15
+ def upload_from_filesystem_to_bucket(file_name, file_path)
16
+ bucket.objects[file_name].write(
17
+ Pathname.new(file_path),
18
+ acl: :public_read
19
+ )
20
+ end
21
+
22
+ private
23
+ def bucket
24
+ @bucket ||= s3.buckets[@bucket_name]
25
+ end
26
+
27
+ def s3
28
+ @s3 ||= AWS::S3.new(
29
+ :access_key_id => @key_id,
30
+ :secret_access_key => @access_key
31
+ )
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ module ClickSession
2
+ class S3FileUploader
3
+ def initialize(s3_connection = S3Connection.new)
4
+ @s3_connection = s3_connection
5
+ end
6
+
7
+ def upload_file(file_name)
8
+ @s3_connection.upload_from_filesystem_to_bucket(
9
+ file_name,
10
+ file_path_for(file_name)
11
+ )
12
+ uploaded_file_path_for(file_name)
13
+ end
14
+
15
+ private
16
+ def file_path_for(file_name)
17
+ "#{Rails.root}/tmp/#{file_name}"
18
+ end
19
+
20
+ def uploaded_file_path_for(file_name)
21
+ "https://s3.amazonaws.com/#{@s3_connection.bucket_name}/#{file_name}"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,64 @@
1
+ require 'state_machine'
2
+ require 'active_record'
3
+
4
+ module ClickSession
5
+ class SessionState < ActiveRecord::Base
6
+ validates :model_record, presence: true
7
+
8
+ state_machine initial: :active do
9
+ state :active, value: 0
10
+ state :processed, value: 1
11
+ state :success_reported, value: 10
12
+ state :failed_to_process, value: 2
13
+ state :failure_reported, value: 20
14
+
15
+ event :success do
16
+ transition active: :processed
17
+ end
18
+
19
+ event :failure do
20
+ transition active: :failed_to_process
21
+ end
22
+
23
+ event :reported_back do
24
+ transition processed: :success_reported, failed_to_process: :failure_reported
25
+ end
26
+ end
27
+
28
+ def model=(model)
29
+ set_model_record_for(model)
30
+ end
31
+
32
+ def model
33
+ @model ||= model_class.find_by_id(self.model_record)
34
+ end
35
+
36
+ def webhook_attempt_failed
37
+ self.webhook_attempts += 1
38
+ end
39
+
40
+ private
41
+
42
+ delegate :model_class, to: :clicksession_configuration
43
+
44
+ def set_model_record_for(model)
45
+ if model.is_a? Integer
46
+ self.model_record = model
47
+ else
48
+ set_active_record_model_id_for(model)
49
+ end
50
+ end
51
+
52
+ def set_active_record_model_id_for(model)
53
+ if model.new_record?
54
+ model.save!
55
+ end
56
+
57
+ self.model_record = model.id
58
+ end
59
+
60
+ def clicksession_configuration
61
+ ClickSession.configuration
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,81 @@
1
+ module ClickSession
2
+ class StatusReporter
3
+
4
+ MAX_WEBHOOK_ATTEMPTS = 5
5
+
6
+ def initialize(webhook)
7
+ @webhook = webhook
8
+ end
9
+
10
+ delegate :serializer_class, :notifier_class, to: :clicksession_configuration
11
+
12
+ def report(click_session)
13
+ @click_session = click_session
14
+
15
+ begin
16
+ webhook.call(
17
+ serialized_webhook_message
18
+ )
19
+
20
+ @click_session.reported_back!
21
+ notifier.session_reported(@click_session)
22
+ rescue StandardError => e
23
+ notifier.rescued_error(e)
24
+ handle_webhook_failure
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :webhook
31
+
32
+ def serialized_webhook_message
33
+ if @click_session.processed?
34
+ serialize_success_message
35
+ else
36
+ serialize_error_message
37
+ end
38
+ end
39
+
40
+ def serialize_success_message
41
+ {
42
+ id: @click_session.id,
43
+ status: {
44
+ success: true
45
+ },
46
+ data: serializer.serialize(@click_session.model)
47
+ }
48
+ end
49
+
50
+ def serialize_error_message
51
+ {
52
+ id: @click_session.id,
53
+ status: {
54
+ success: false,
55
+ message: "See error logs"
56
+ }
57
+ }
58
+ end
59
+
60
+ def handle_webhook_failure
61
+ @click_session.webhook_attempt_failed
62
+ @click_session.save!
63
+
64
+ if @click_session.webhook_attempts >= MAX_WEBHOOK_ATTEMPTS
65
+ notifier.session_failed_to_report(@click_session)
66
+ end
67
+ end
68
+
69
+ def notifier
70
+ @notifier ||= notifier_class.new
71
+ end
72
+
73
+ def serializer
74
+ @serializer ||= serializer_class.new
75
+ end
76
+
77
+ def clicksession_configuration
78
+ ClickSession.configuration
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,15 @@
1
+ module ClickSession
2
+ class SuccessfulStatusReporter < StatusReporter
3
+ def initialize(
4
+ webhook = Webhook.new(ClickSession.configuration.success_callback_url)
5
+ )
6
+ super(webhook)
7
+ end
8
+
9
+ def report(click_session)
10
+ raise ArgumentError unless click_session.processed?
11
+
12
+ super(click_session)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,76 @@
1
+ module ClickSession
2
+
3
+ # Executes the click session and retuns a serialized response
4
+ class Sync
5
+ attr_reader :model
6
+ attr_accessor :click_session
7
+
8
+ def initialize(model)
9
+ @model = model
10
+ end
11
+
12
+ def run
13
+ @click_session = SessionState.create!(model: model)
14
+
15
+ begin
16
+ click_session_processor = ClickSessionProcessor.new(
17
+ click_session,
18
+ processor,
19
+ configured_notifier,
20
+ options
21
+ )
22
+
23
+ click_session_processor.process
24
+
25
+ click_session.reported_back!
26
+ serialize_success_response
27
+ rescue TooManyRetriesError => e
28
+ click_session.reported_back!
29
+ serialize_failure_response
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ delegate :processor_class, :notifier_class, to: :clicksession_configuration
36
+
37
+ def serialize_success_response
38
+ serializer.serialize_success(click_session)
39
+ end
40
+
41
+ def serialize_failure_response
42
+ serializer.serialize_failure(click_session)
43
+ end
44
+
45
+ def processor
46
+ @processor ||= ClickSession::WebRunnerProcessor.new(configured_web_runner)
47
+ end
48
+
49
+ def configured_web_runner
50
+ @web_runner ||= processor_class.new
51
+ end
52
+
53
+ def configured_notifier
54
+ @notifier ||= notifier_class.new
55
+ end
56
+
57
+ def options
58
+ if clicksession_configuration.screenshot_enabled?
59
+ {
60
+ screenshot_enabled: true,
61
+ screenshot_options: clicksession_configuration.screenshot
62
+ }
63
+ else
64
+ {}
65
+ end
66
+ end
67
+
68
+ def clicksession_configuration
69
+ ClickSession.configuration
70
+ end
71
+
72
+ def serializer
73
+ @serializer ||= ClickSession::ResponseSerializer.new
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module ClickSession
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,60 @@
1
+ require 'capybara'
2
+ require 'capybara/poltergeist'
3
+
4
+ CAPYBARA_ERRORS = [
5
+ Capybara::ElementNotFound
6
+ ]
7
+
8
+ module ClickSession
9
+ class WebRunner
10
+ include Capybara::DSL
11
+
12
+ def initialize
13
+ Capybara.default_driver = ClickSession.configuration.driver_client
14
+ Capybara.javascript_driver = ClickSession.configuration.driver_client
15
+ Capybara.run_server = false
16
+ page = Capybara::Session.new(ClickSession.configuration.driver_client)
17
+ end
18
+
19
+ def run(model)
20
+ raise NotImplementedError("You need to override the #steps method by sub classing WebRunner")
21
+ end
22
+
23
+ def reset
24
+ clear_cookies
25
+ end
26
+
27
+ def save_screenshot(file_name_identity = "")
28
+ @file_name = build_file_name_for(file_name_identity)
29
+ page.save_screenshot(screenshot_save_path, full: true)
30
+
31
+ S3FileUploader.new.upload_file(@file_name)
32
+ end
33
+
34
+ private
35
+ def clear_cookies
36
+ browser = Capybara.current_session.driver.browser
37
+
38
+ if browser.respond_to?(:manage) and browser.manage.respond_to?(:delete_all_cookies)
39
+ browser.manage.delete_all_cookies
40
+ else
41
+ page.driver.cookies.keys.each do |cookie|
42
+ page.driver.remove_cookie(cookie)
43
+ end
44
+ end
45
+ end
46
+
47
+ def screenshot_save_path
48
+ path_for(@file_name)
49
+ end
50
+
51
+ def build_file_name_for(file_name_identity)
52
+ readable_date = Time.now.strftime("%F_%T")
53
+ "screenshot-#{file_name_identity}-#{readable_date}.png"
54
+ end
55
+
56
+ def path_for(file_name)
57
+ "#{Rails.root}/tmp/#{file_name}"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "./exceptions"
2
+
3
+ module ClickSession
4
+ class WebRunnerProcessor
5
+ def initialize(web_runner)
6
+ @web_runner = web_runner
7
+ @retries_made = 0
8
+ @making_requests = true
9
+ end
10
+
11
+ delegate :notifier_class, to: :clicksession_configuration
12
+
13
+ def process(model)
14
+ while can_make_requests?
15
+ begin
16
+ run_steps_in_browser_with(model)
17
+ rescue StandardError => e
18
+ make_note_of_error(e)
19
+
20
+ if too_many_retries?
21
+ raise TooManyRetriesError.new
22
+ end
23
+ end
24
+ end
25
+
26
+ model
27
+ end
28
+
29
+ delegate :save_screenshot, to: :web_runner
30
+
31
+ private
32
+ attr_reader :web_runner
33
+
34
+ def can_make_requests?
35
+ @making_requests
36
+ end
37
+
38
+ def stop_making_requests
39
+ @making_requests = false
40
+ end
41
+
42
+ def run_steps_in_browser_with(model)
43
+ web_runner.reset
44
+ web_runner.run(model)
45
+ stop_making_requests
46
+ end
47
+
48
+ def make_note_of_error(error)
49
+ @retries_made += 1
50
+ notifier.rescued_error(error)
51
+ end
52
+
53
+ def too_many_retries?
54
+ @retries_made > 2
55
+ end
56
+
57
+ def notifier
58
+ @notifier ||= notifier_class.new
59
+ end
60
+
61
+ def clicksession_configuration
62
+ ClickSession.configuration
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,24 @@
1
+ require "rest-client"
2
+
3
+ module ClickSession
4
+ class Webhook
5
+ def initialize(url)
6
+ @url = url
7
+ end
8
+
9
+ def call(message)
10
+ RestClient.post(
11
+ url,
12
+ message.to_json,
13
+ {
14
+ content_type: :json,
15
+ accept: :json,
16
+ }
17
+ )
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :url
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ module ClickSession
2
+ class WebhookModelSerializer
3
+ def serialize(model)
4
+ model.as_json
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ require "click_session/version"
2
+ require "click_session/session_state"
3
+ require "click_session/configuration"
4
+ require "click_session/exceptions"
5
+ require "click_session/notifier"
6
+
7
+ require "click_session/async"
8
+ require "click_session/sync"
9
+
10
+ require "click_session/click_session_processor"
11
+ require "click_session/web_runner"
12
+ require "click_session/web_runner_processor"
13
+
14
+ require "click_session/status_reporter"
15
+ require "click_session/failure_status_reporter"
16
+ require "click_session/successful_status_reporter"
17
+
18
+
19
+ require "click_session/response_serializer"
20
+ require "click_session/webhook_model_serializer"
21
+
22
+ require "click_session/s3_connection"
23
+ require "click_session/s3_file_uploader"
24
+
25
+ require "click_session/webhook"
26
+
27
+
28
+ module StateMachine
29
+ module Integrations
30
+ module ActiveModel
31
+ public :around_validation
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ class CreateSessionStates < ActiveRecord::Migration
2
+ def change
3
+ create_table :session_states do |t|
4
+ t.integer "webhook_attempts", default: 0, null: false
5
+ t.integer "state", default: 0, null: false
6
+ t.integer "model_record"
7
+ t.string "screenshot_url"
8
+ t.timestamps null: false
9
+ end
10
+
11
+ add_index :session_states, :model_record, unique: true
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ ClickSession.configure do |config|
2
+ config.model_class = RenameThisToYourOwnActiveModelClass
3
+ config.processor_class = ClickSessionRunner
4
+ end
@@ -0,0 +1,54 @@
1
+ require 'rails/generators/base'
2
+ require 'rails/generators/active_record'
3
+
4
+ module ClickSession
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ source_root File.expand_path('../', __FILE__)
9
+
10
+ def create_click_session_migration
11
+ unless session_states_table_exists?
12
+ copy_migration 'create_session_states.rb'
13
+ end
14
+ end
15
+
16
+ def copy_initializer
17
+ copy_file 'initializers/click_session.rb', 'config/initializers/click_session.rb'
18
+ end
19
+
20
+ private
21
+
22
+ def copy_migration(migration_name, config = {})
23
+ unless migration_exists?(migration_name)
24
+ migration_template(
25
+ "db/migration/#{migration_name}",
26
+ "db/migrate/#{migration_name}",
27
+ config
28
+ )
29
+ end
30
+ end
31
+
32
+ def migration_exists?(name)
33
+ existing_migrations.include?(name)
34
+ end
35
+
36
+ def existing_migrations
37
+ @existing_migrations ||= Dir.glob("db/migrate/*.rb").map do |file|
38
+ migration_name_without_timestamp(file)
39
+ end
40
+ end
41
+
42
+ def migration_name_without_timestamp(file)
43
+ file.sub(%r{^.*(db/migrate/)(?:\d+_)?}, '')
44
+ end
45
+
46
+ def session_states_table_exists?
47
+ ActiveRecord::Base.connection.table_exists?(:session_states)
48
+ end
49
+
50
+ def self.next_migration_number(dir)
51
+ ActiveRecord::Generators::Base.next_migration_number(dir)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,52 @@
1
+ require "click_session/configuration"
2
+
3
+ namespace :click_session do
4
+ desc "Processes all click_sessions in the 'active' state"
5
+ task process_active: :environment do
6
+ def processor_for(session_state)
7
+ ClickSession::ClickSessionProcessor.new(
8
+ session_state,
9
+ ClickSession::WebRunnerProcessor.new(configured_web_runner),
10
+ ClickSession.configuration.notifier_class.new,
11
+ processor_options
12
+ )
13
+ end
14
+
15
+ def configured_web_runner
16
+ ClickSession.configuration.processor_class.new
17
+ end
18
+
19
+ def processor_options
20
+ if ClickSession.configuration.screenshot_enabled?
21
+ {
22
+ screenshot_enabled: true,
23
+ screenshot_options: ClickSession.configuration.screenshot
24
+ }
25
+ else
26
+ {}
27
+ end
28
+ end
29
+
30
+ ClickSession::SessionState.with_state(:active).each do | session_state |
31
+ processor_for(session_state).process
32
+ end
33
+ end
34
+
35
+ desc "reports click_sessions in 'processed' state to the webhook"
36
+ task report_processed: :environment do
37
+ reporter = ClickSession::SuccessfulStatusReporter.new
38
+
39
+ ClickSession::SessionState.with_state(:processed).each do | session_state |
40
+ reporter.report(session_state)
41
+ end
42
+ end
43
+
44
+ desc "reports click_sessions in 'failed' state to the webhook"
45
+ task report_failed: :environment do
46
+ reporter = ClickSession::FailureStatusReporter.new
47
+
48
+ ClickSession::SessionState.with_state(:failed_to_process).each do | session_state |
49
+ reporter.report(session_state)
50
+ end
51
+ end
52
+ end