action_trace 0.1.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +213 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/action_trace/application.css +15 -0
  6. data/app/controllers/action_trace/activity_logs_controller.rb +44 -0
  7. data/app/controllers/action_trace/application_controller.rb +6 -0
  8. data/app/controllers/concerns/activity_trackable.rb +47 -0
  9. data/app/helpers/action_trace/application_helper.rb +6 -0
  10. data/app/interactors/action_trace/fetch_activity_logs.rb +13 -0
  11. data/app/interactors/action_trace/fetch_data_changes.rb +45 -0
  12. data/app/interactors/action_trace/fetch_page_visits.rb +51 -0
  13. data/app/interactors/action_trace/fetch_session_starts.rb +31 -0
  14. data/app/interactors/action_trace/initialize_context.rb +23 -0
  15. data/app/interactors/action_trace/merge_and_format_results.rb +20 -0
  16. data/app/interactors/concerns/activity_log_fetchable.rb +81 -0
  17. data/app/jobs/action_trace/application_job.rb +6 -0
  18. data/app/jobs/action_trace/purge_activity_log_job.rb +17 -0
  19. data/app/models/action_trace/activity_log.rb +61 -0
  20. data/app/models/action_trace/application_record.rb +7 -0
  21. data/app/models/concerns/action_trace/data_trackable.rb +64 -0
  22. data/app/presenters/action_trace/activity_log_presenter.rb +70 -0
  23. data/app/views/action_trace/activity_logs/_activity_log.html.erb +6 -0
  24. data/app/views/action_trace/activity_logs/_index.html.erb +13 -0
  25. data/app/views/action_trace/activity_logs/index.html.erb +1 -0
  26. data/config/locales/en.yml +21 -0
  27. data/config/locales/it.yml +18 -0
  28. data/config/locales/views/en.yml +24 -0
  29. data/config/locales/views/it.yml +24 -0
  30. data/config/routes.rb +9 -0
  31. data/lib/action_trace/configuration.rb +14 -0
  32. data/lib/action_trace/engine.rb +32 -0
  33. data/lib/action_trace/version.rb +5 -0
  34. data/lib/action_trace.rb +19 -0
  35. data/lib/generators/action_trace/install/POST_INSTALL +29 -0
  36. data/lib/generators/action_trace/install/install_generator.rb +47 -0
  37. data/lib/generators/action_trace/install/templates/initializers/action_trace.rb.tt +31 -0
  38. data/lib/generators/action_trace/install/templates/migrations/add_version_id_to_activities.rb.tt +7 -0
  39. data/lib/generators/action_trace/views/templates/views/action_trace/activity_logs/_index.html.erb +7 -0
  40. data/lib/generators/action_trace/views/templates/views/action_trace/activity_logs/index.html.erb +1 -0
  41. data/lib/generators/action_trace/views/views_generator.rb +31 -0
  42. data/lib/tasks/action_trace_tasks.rake +6 -0
  43. metadata +298 -0
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class ActivityLog
5
+ extend ActiveModel::Translation
6
+ include ActionTrace::ActivityLogPresenter
7
+
8
+ attr_reader :id, :source, :occurred_at, :user, :raw_subject, :details, :url, :paper_trail_version, :trackable,
9
+ :trackable_type
10
+
11
+ SOURCES = {
12
+ data_create: 'data_create',
13
+ data_change: 'data_change',
14
+ data_destroy: 'data_destroy',
15
+ page_visit: 'page_visit',
16
+ session_start: 'session_start',
17
+ session_end: 'session_end'
18
+ }.freeze
19
+
20
+ def initialize(attributes = {})
21
+ @id = attributes[:id]
22
+ @source = attributes[:source]
23
+ @occurred_at = attributes[:occurred_at]
24
+ @user = attributes[:user]
25
+ @raw_subject = attributes[:subject]
26
+ @details = attributes[:details] || {}
27
+ @url = attributes[:url]
28
+ @paper_trail_version = attributes[:paper_trail_version]
29
+ @trackable = attributes[:trackable]
30
+ @trackable_type = attributes[:trackable_type]
31
+ end
32
+
33
+ def data_create?
34
+ source == SOURCES[:data_create]
35
+ end
36
+
37
+ def data_change?
38
+ source == SOURCES[:data_change]
39
+ end
40
+
41
+ def data_destroy?
42
+ source == SOURCES[:data_destroy]
43
+ end
44
+
45
+ def page_visit?
46
+ source == SOURCES[:page_visit]
47
+ end
48
+
49
+ def session_start?
50
+ source == SOURCES[:session_start]
51
+ end
52
+
53
+ def human_trackable_type
54
+ return nil if trackable_type.blank?
55
+
56
+ trackable_type.constantize.model_name.human
57
+ rescue StandardError
58
+ trackable_type
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ module DataTrackable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include PublicActivity::Common
9
+
10
+ after_commit :track_create_activity, on: :create
11
+ after_commit :track_update_activity, on: :update
12
+ before_destroy :track_destroy_activity
13
+ end
14
+
15
+ private
16
+
17
+ def track_create_activity
18
+ track_activity('create')
19
+ end
20
+
21
+ def track_update_activity
22
+ track_activity('update')
23
+ end
24
+
25
+ def track_destroy_activity
26
+ track_activity('destroy')
27
+ end
28
+
29
+ def devise_actions?
30
+ controller = PublicActivity.get_controller
31
+ return false if controller.nil?
32
+ return false unless defined?(DeviseController)
33
+
34
+ controller.is_a?(DeviseController) || controller.is_a?(Devise::SessionsController)
35
+ end
36
+
37
+ def track_activity(action)
38
+ return if devise_actions?
39
+
40
+ current_user = PublicActivity.get_controller&.current_user
41
+
42
+ create_activity(
43
+ key: action_key(action),
44
+ owner: current_user,
45
+ version_id: action_version_id,
46
+ params: action_params
47
+ )
48
+ end
49
+
50
+ def action_key(action)
51
+ "#{self.class.name.downcase}.#{action}"
52
+ end
53
+
54
+ def action_version_id
55
+ return nil unless respond_to?(:versions)
56
+
57
+ versions.reload.last&.id
58
+ end
59
+
60
+ def action_params
61
+ { id: id, display_name: try(:name) || try(:complete_name) || id }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ module ActivityLogPresenter
5
+ include ActionView::Helpers::TranslationHelper
6
+ include ActionView::Helpers::TagHelper
7
+
8
+ VISUAL_CONFIG = {
9
+ 'data_create' => { icon: 'fas fa-plus-circle', color: 'text-success' },
10
+ 'data_change' => { icon: 'fas fa-pencil-alt', color: 'text-primary' },
11
+ 'data_destroy' => { icon: 'fas fa-trash-alt', color: 'text-danger' },
12
+ 'page_visit' => { icon: 'fas fa-globe-pointer', color: 'text-secondary' },
13
+ 'session_start' => { icon: 'fas fa-arrow-left-to-bracket', color: 'text-warning' },
14
+ 'session_end' => { icon: 'fas fa-arrow-right-to-bracket', color: 'text-danger' }
15
+ }.freeze
16
+
17
+ def icon
18
+ visual[:icon]
19
+ end
20
+
21
+ def color
22
+ visual[:color]
23
+ end
24
+
25
+ def subject
26
+ return raw_subject unless %w[page_visit session_end].include?(source)
27
+
28
+ format_subject(details['controller'], details['action'], details['path'])
29
+ end
30
+
31
+ private
32
+
33
+ def visual
34
+ VISUAL_CONFIG[source] || VISUAL_CONFIG['page_visit']
35
+ end
36
+
37
+ def format_subject(controller, action, path)
38
+ return path if controller.blank?
39
+
40
+ singular, plural = model_names_for(controller)
41
+
42
+ case action
43
+ when 'index'
44
+ "#{I18n.t('.list')} #{plural}"
45
+ when 'show'
46
+ "#{I18n.t('.details')} #{singular}"
47
+ when 'new', 'create', 'edit', 'update', 'destroy', 'delete'
48
+ "#{I18n.t(".#{action_key(action)}")} #{singular}"
49
+ else
50
+ path
51
+ end
52
+ end
53
+
54
+ def model_names_for(controller)
55
+ model_class = controller.classify.constantize
56
+ [model_class.model_name.human, model_class.model_name.human(count: 2)]
57
+ rescue NameError
58
+ [controller.humanize.singularize, controller.humanize]
59
+ end
60
+
61
+ def action_key(action)
62
+ case action
63
+ when 'new', 'create' then 'new'
64
+ when 'edit', 'update' then 'edit'
65
+ when 'destroy', 'delete' then 'destroy'
66
+ else 'show'
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,6 @@
1
+ <tr>
2
+ <td><%= activity_log.occurred_at %></td>
3
+ <td><%= activity_log.source %></td>
4
+ <td><%= activity_log.user %></td>
5
+ <td><%= activity_log.subject %></td>
6
+ </tr>
@@ -0,0 +1,13 @@
1
+ <table>
2
+ <thead>
3
+ <tr>
4
+ <th><%= ActionTrace::ActivityLog.human_attribute_name(:occurred_at) %></th>
5
+ <th><%= ActionTrace::ActivityLog.human_attribute_name(:source) %></th>
6
+ <th><%= ActionTrace::ActivityLog.human_attribute_name(:user) %></th>
7
+ <th><%= ActionTrace::ActivityLog.human_attribute_name(:raw_subject) %></th>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <%= render partial: 'activity_log', collection: @activity_logs, as: :activity_log %>
12
+ </tbody>
13
+ </table>
@@ -0,0 +1 @@
1
+ <%= render partial: 'index' %>
@@ -0,0 +1,21 @@
1
+ en:
2
+ activemodel:
3
+ models:
4
+ action_trace/activity_log: "Activity Log"
5
+ attributes:
6
+ action_trace/activity_log:
7
+ occurred_at: "Date and Time"
8
+ source: "Source"
9
+ user: "User"
10
+ raw_subject: "Subject"
11
+ list: "List"
12
+ details: "Details"
13
+ new: "New"
14
+ edit: "Edit"
15
+ destroy: "Delete"
16
+ no_results_found: "There are no items to display at the moment."
17
+ go_to_page: "Go to Page"
18
+ params: "Parameters"
19
+ data: "Data"
20
+ old_value: "Previous value"
21
+ new_value: "New value"
@@ -0,0 +1,18 @@
1
+ it:
2
+ activemodel:
3
+ models:
4
+ action_trace/activity_log: "Registro Attività"
5
+ attributes:
6
+ action_trace/activity_log:
7
+ occurred_at: "Data e Ora"
8
+ source: "Sorgente"
9
+ user: "Utente"
10
+ raw_subject: "Oggetto"
11
+ list: "Elenco"
12
+ details: "Dettaglio"
13
+ no_results_found: "Non ci sono elementi da visualizzare al momento."
14
+ go_to_page: "Visualizza Pagina"
15
+ params: "Parametri"
16
+ data: "Dati"
17
+ old_value: "Valore precedente"
18
+ new_value: "Nuovo valore"
@@ -0,0 +1,24 @@
1
+ en:
2
+ activity_logs:
3
+ index:
4
+ title: "Activity Log"
5
+ history_detail: "Change Details"
6
+ all_sources: "All sources"
7
+ no_activity_logs: "No activity logs..."
8
+ data_create: "Creation"
9
+ data_change: "Change"
10
+ data_destroy: "Deletion"
11
+ page_visit: "Navigation"
12
+ session_start: "Session Start"
13
+ session_end: "Session End"
14
+ session_details: "Session Details"
15
+ system_activity: "System Activity"
16
+ start_date: "Start date"
17
+ end_date: "End date"
18
+ sources:
19
+ data_create: "Data creation"
20
+ data_change: "Data changes"
21
+ data_destroy: "Data deletion"
22
+ page_visit: "Page navigation"
23
+ session_start: "Portal logins"
24
+ session_end: "Portal logouts"
@@ -0,0 +1,24 @@
1
+ it:
2
+ activity_logs:
3
+ index:
4
+ title: "Registro Attività"
5
+ history_detail: "Dettaglio Modifiche"
6
+ all_sources: "Tutte le sorgenti"
7
+ no_activity_logs: "Nessun registro attività..."
8
+ data_create: "Creazione"
9
+ data_change: "Modifica"
10
+ data_destroy: "Cancellazzione"
11
+ page_visit: "Navigazione"
12
+ session_start: "Inizio Sessione"
13
+ session_end: "Fine Sessione"
14
+ session_details: "Dettagli Sessione"
15
+ system_activity: "Attività di Sistema"
16
+ start_date: "Data inizio"
17
+ end_date: "Data fine"
18
+ sources:
19
+ data_create: "Creazione di dati"
20
+ data_change: "Modifiche ai dati"
21
+ data_destroy: "Cancellazzione di dati"
22
+ page_visit: "Navigazione delle pagine"
23
+ session_start: "Accessi al portale"
24
+ session_end: "Logout dal portale"
data/config/routes.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActionTrace::Engine.routes.draw do
4
+ resources :activity_logs, only: [:index] do
5
+ collection do
6
+ post :filter
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class Configuration
5
+ attr_accessor :excluded_actions, :excluded_controllers, :user_class, :log_retention_period
6
+
7
+ def initialize
8
+ @excluded_actions = []
9
+ @excluded_controllers = []
10
+ @user_class = 'User'
11
+ @log_retention_period = 1.year
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace ActionTrace
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ config.after_initialize do
12
+ begin
13
+ user_class = ActionTrace.configuration.user_class.constantize
14
+ rescue NameError
15
+ raise NameError,
16
+ "ActionTrace: user_class '#{ActionTrace.configuration.user_class}' not found. Check config/initializers/action_trace.rb."
17
+ end
18
+
19
+ PublicActivity::Activity.define_singleton_method(:filter_by_company) do |scope, company_id|
20
+ user_ids = user_class.where(company_id: company_id).select(:id)
21
+ scope.where(owner_type: user_class.name, owner_id: user_ids)
22
+ end
23
+
24
+ [Ahoy::Visit, Ahoy::Event].each do |klass|
25
+ klass.define_singleton_method(:filter_by_company) do |scope, company_id|
26
+ user_ids = user_class.where(company_id: company_id).select(:id)
27
+ scope.where(user_id: user_ids)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_trace/version'
4
+ require 'action_trace/configuration'
5
+ require 'action_trace/engine'
6
+
7
+ module ActionTrace
8
+ class << self
9
+ attr_writer :configuration
10
+
11
+ def configuration
12
+ @configuration ||= Configuration.new
13
+ end
14
+
15
+ def configure
16
+ yield configuration
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+
2
+ ===============================================================================
3
+
4
+ ActionTrace installed successfully!
5
+
6
+ Next steps:
7
+
8
+ 1. Run migrations:
9
+
10
+ rails db:migrate
11
+
12
+ 2. Mount the engine in config/routes.rb:
13
+
14
+ mount ActionTrace::Engine, at: "/action_trace"
15
+
16
+ 3. Include ActivityTrackable in your ApplicationController to track page visits:
17
+
18
+ include ActivityTrackable
19
+
20
+ 4. Include DataTrackable in any model you want to track:
21
+
22
+ include ActionTrace::DataTrackable
23
+
24
+ 5. (Optional) Customize views:
25
+
26
+ rails generate action_trace:views
27
+ rails generate action_trace:views --controller
28
+
29
+ ===============================================================================
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module ActionTrace
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ desc 'Install ActionTrace and all its dependencies'
9
+
10
+ class_option :skip_ahoy, type: :boolean, default: false,
11
+ desc: 'Skip ahoy_matey generator (already installed)'
12
+ class_option :skip_paper_trail, type: :boolean, default: false,
13
+ desc: 'Skip paper_trail generator (already installed)'
14
+ class_option :skip_public_activity, type: :boolean, default: false,
15
+ desc: 'Skip public_activity generator (already installed)'
16
+ class_option :skip_discard, type: :boolean, default: false,
17
+ desc: 'Skip discard generator (already installed)'
18
+
19
+ def run_ahoy_install
20
+ generate 'ahoy:install' unless options[:skip_ahoy]
21
+ end
22
+
23
+ def run_paper_trail_install
24
+ generate 'paper_trail:install' unless options[:skip_paper_trail]
25
+ end
26
+
27
+ def run_public_activity_migration
28
+ generate 'public_activity:migration' unless options[:skip_public_activity]
29
+ end
30
+
31
+ def create_add_version_id_migration
32
+ migration_template(
33
+ 'migrations/add_version_id_to_activities.rb.tt',
34
+ 'db/migrate/add_version_id_to_activities.rb'
35
+ )
36
+ end
37
+
38
+ def create_initializer
39
+ template 'initializers/action_trace.rb.tt', 'config/initializers/action_trace.rb'
40
+ end
41
+
42
+ def show_post_install_message
43
+ readme 'POST_INSTALL' if behavior == :invoke
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ ActionTrace.configure do |config|
3
+ # config.excluded_actions = %w[status ping]
4
+ # config.excluded_controllers = %w[home]
5
+ # config.log_retention_period = 1.year # default: 1.year
6
+ end
7
+
8
+ class Ahoy::Store < Ahoy::DatabaseStore
9
+ def track_visit(data)
10
+ super(data)
11
+ end
12
+
13
+ def track_event(data)
14
+ super(data)
15
+ end
16
+ end
17
+
18
+ Ahoy.api = false
19
+ Ahoy.geocode = false
20
+ Ahoy.mask_ips = true
21
+ Ahoy.cookies = true
22
+
23
+ PaperTrail.config do |config|
24
+ config.enabled = true
25
+ config.version_limit = 10
26
+ config.serializer = PaperTrail::Serializers::JSON
27
+ end
28
+
29
+ PublicActivity::Config.set do
30
+ enabled true
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddVersionIdToActivities < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def change
5
+ add_column :activities, :version_id, :integer
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ <div id="activity-logs">
2
+ <% @activity_logs.each do |log| %>
3
+ <div class="activity-log">
4
+ <%= log %>
5
+ </div>
6
+ <% end %>
7
+ </div>
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module ActionTrace
6
+ module Generators
7
+ class ViewsGenerator < Rails::Generators::Base
8
+ desc 'Copy ActionTrace views to your application for customization'
9
+
10
+ class_option :controller, type: :boolean, default: false,
11
+ desc: 'Also copy the ActivityLogsController to your application'
12
+
13
+ def self.source_root
14
+ File.expand_path('templates', __dir__)
15
+ end
16
+
17
+ def copy_views
18
+ directory 'views/action_trace', 'app/views/action_trace'
19
+ end
20
+
21
+ def copy_controller
22
+ return unless options[:controller]
23
+
24
+ copy_file(
25
+ File.expand_path('../../../../../app/controllers/action_trace/activity_logs_controller.rb', __dir__),
26
+ 'app/controllers/action_trace/activity_logs_controller.rb'
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # desc "Explaining what the task does"
4
+ # task :action_trace do
5
+ # # Task goes here
6
+ # end