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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4bc6e731d0e4f118c4d01db6af5ac7831064772beb3057ab66f6fdbfc0d5b4f8
4
+ data.tar.gz: 72c40aeedbfad2d1d6ff189ff5f5279e2783059f9b79f57c1b338acfa9602a12
5
+ SHA512:
6
+ metadata.gz: 03aef93ad7fbcf10ae77298cbced45c404841dfd425b981f83568c391d4e3ef343257f0390acd37f173aa56a37d15e59b3155c2788f4934f0e48c73eec7b7528
7
+ data.tar.gz: ee0004fbbee480dc067768dbc2804e06e13abb94fa894e98d4b7ee45274d0ed6972bef6a4d60528c5d0aca2ed4c30ffd728b46feb5e20ac209cac91663c2ec77
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright gimbaro
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # ActionTrace
2
+
3
+ ActionTrace is a Rails engine that consolidates user interaction tracking into a single integration point.
4
+ It glues together [public_activity](https://github.com/chaps-io/public_activity), [ahoy_matey](https://github.com/ankane/ahoy), [paper_trail](https://github.com/paper-trail-gem/paper_trail), and [discard](https://github.com/jhawthorn/discard) so you don't have to configure each one individually.
5
+
6
+ ## What it tracks
7
+
8
+ | Source | Description | Backed by |
9
+ |---|---|---|
10
+ | `data_create` | Model created | public_activity |
11
+ | `data_change` | Model updated | public_activity + paper_trail |
12
+ | `data_destroy` | Model destroyed | public_activity |
13
+ | `page_visit` | Controller action visited | ahoy_matey |
14
+ | `session_start` | User session begun | ahoy_matey (visit) |
15
+ | `session_end` | User logged out | ahoy_matey (event) |
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem "action_trace"
23
+ ```
24
+
25
+ Then run:
26
+
27
+ ```bash
28
+ bundle install
29
+ rails generate action_trace:install
30
+ rails db:migrate
31
+ ```
32
+
33
+ The installer runs the setup generators for all four gems and creates `config/initializers/action_trace.rb`.
34
+
35
+ ### Skipping already-installed gems
36
+
37
+ If one or more of the underlying gems is already set up, pass `--skip-*` flags:
38
+
39
+ ```bash
40
+ rails generate action_trace:install --skip-ahoy --skip-paper-trail
41
+ ```
42
+
43
+ Available flags:
44
+
45
+ | Flag | Skips |
46
+ |---|---|
47
+ | `--skip-ahoy` | `ahoy:install` |
48
+ | `--skip-paper-trail` | `paper_trail:install` |
49
+ | `--skip-public-activity` | `public_activity:migration` |
50
+ | `--skip-discard` | discard initializer entry |
51
+
52
+ ### Mount the engine
53
+
54
+ In `config/routes.rb`:
55
+
56
+ ```ruby
57
+ mount ActionTrace::Engine, at: '/action_trace'
58
+ ```
59
+
60
+ This exposes:
61
+
62
+ ```
63
+ GET /action_trace/activity_logs
64
+ POST /action_trace/activity_logs/filter
65
+ ```
66
+
67
+ The controller inherits from the host app's `ApplicationController`. Authentication and authorization are not enforced by default — copy the controller with the generator and uncomment the relevant lines for your setup (e.g. Devise's `authenticate_user!`, CanCanCan's `load_and_authorize_resource`).
68
+
69
+ ## Configuration
70
+
71
+ `config/initializers/action_trace.rb` is generated automatically. Available options:
72
+
73
+ ```ruby
74
+ ActionTrace.configure do |config|
75
+ # Controller names to exclude from page_visit tracking (default: [])
76
+ config.excluded_controllers = %w[health_checks status]
77
+
78
+ # Action names to exclude from page_visit tracking (default: [])
79
+ config.excluded_actions = %w[ping]
80
+
81
+ # The user model class name used to resolve company filtering
82
+ # for PublicActivity::Activity, Ahoy::Visit and Ahoy::Event (default: 'User')
83
+ config.user_class = 'User'
84
+
85
+ # How long to retain activity records before purging (default: 1.year)
86
+ config.log_retention_period = 6.months
87
+ end
88
+ ```
89
+
90
+ > `user_class` must have a `company_id` column. ActionTrace uses it to filter
91
+ > activity records through the user when filtering by company (since those
92
+ > models store the user reference rather than a direct `company_id`).
93
+
94
+ ## Usage
95
+
96
+ ### Track page visits — controller concern
97
+
98
+ Include `ActivityTrackable` in any controller (or `ApplicationController`):
99
+
100
+ ```ruby
101
+ class ApplicationController < ActionController::Base
102
+ include ActivityTrackable
103
+ end
104
+ ```
105
+
106
+ This adds an `after_action :track_action` that records a `page_visit` event via Ahoy for every successful request made by a logged-in user.
107
+
108
+ To record a session end on logout, call `track_session_end` in your sessions controller before clearing the session:
109
+
110
+ ```ruby
111
+ class SessionsController < Devise::SessionsController
112
+ def destroy
113
+ track_session_end
114
+ super
115
+ end
116
+ end
117
+ ```
118
+
119
+ ### Track model changes — model concern
120
+
121
+ Include `ActionTrace::DataTrackable` in any ActiveRecord model:
122
+
123
+ ```ruby
124
+ class Document < ApplicationRecord
125
+ include ActionTrace::DataTrackable
126
+ end
127
+ ```
128
+
129
+ This records a `public_activity` event on every `create`, `update`, and `destroy`, linked to the current user (via `PublicActivity.get_controller`) and, when paper_trail is active, to the corresponding version.
130
+
131
+ ### Query activity logs
132
+
133
+ Use `ActionTrace::FetchActivityLogs` directly to fetch and paginate unified activity:
134
+
135
+ ```ruby
136
+ result = ActionTrace::FetchActivityLogs.call(
137
+ current_user: current_user,
138
+ filters: {
139
+ 'source' => 'data_change', # optional — one of the sources listed above
140
+ 'company_id' => 5, # optional — overrides current_user.company_id
141
+ 'user_id' => 12, # optional
142
+ 'start_date' => '2026-01-01', # optional — YYYY-MM-DD
143
+ 'end_date' => '2026-03-31' # optional — YYYY-MM-DD
144
+ },
145
+ range: 0 # offset for pagination (increments of 50)
146
+ )
147
+
148
+ result.activity_logs # => Array of ActionTrace::ActivityLog
149
+ result.total_count # => Integer
150
+ ```
151
+
152
+ Each `ActionTrace::ActivityLog` exposes:
153
+
154
+ | Attribute | Type | Description |
155
+ |---|---|---|
156
+ | `id` | String | Prefixed ID e.g. `act_42`, `visit_7`, `evt_3` |
157
+ | `source` | String | One of the sources in the table above |
158
+ | `occurred_at` | DateTime | When the event happened |
159
+ | `user` | String | Display name of the user |
160
+ | `subject` | String | Human-readable description |
161
+ | `details` | Hash | Raw event payload |
162
+ | `paper_trail_version` | PaperTrail::Version | Associated version (data events only) |
163
+ | `trackable` | ActiveRecord object | The changed record (data events only) |
164
+ | `trackable_type` | String | Class name of the changed record |
165
+
166
+ #### Presenter helpers
167
+
168
+ `ActionTrace::ActivityLog` also provides:
169
+
170
+ ```ruby
171
+ log.icon # => 'fas fa-pencil-alt'
172
+ log.color # => 'text-primary'
173
+ log.data_change? # => true / false
174
+ log.page_visit? # => true / false
175
+ # … (data_create?, data_destroy?, session_start?)
176
+ ```
177
+
178
+ ## Customizing views and controller
179
+
180
+ ActionTrace ships minimal default views for `activity_logs#index`. These work out of the box but are intentionally bare — copy them into your app to customize the UI.
181
+
182
+ ### Copy views
183
+
184
+ ```bash
185
+ rails generate action_trace:views
186
+ ```
187
+
188
+ This copies the engine views to `app/views/action_trace/activity_logs/` in your application. Rails will use your copies instead of the engine defaults.
189
+
190
+ ### Copy views and controller
191
+
192
+ ```bash
193
+ rails generate action_trace:views --controller
194
+ ```
195
+
196
+ Also copies `ActivityLogsController` to `app/controllers/action_trace/activity_logs_controller.rb`. The file includes commented-out lines for Devise and CanCanCan — uncomment what applies to your setup, or replace with your own auth logic.
197
+
198
+ > After copying the controller, the engine's version is no longer used. Any future updates to the engine's controller will not be applied automatically — keep that in mind when upgrading.
199
+
200
+ ## Maintenance
201
+
202
+ ### Purge old records
203
+
204
+ `ActionTrace::PurgeActivityLogJob` removes all `PublicActivity::Activity`, `Ahoy::Event`, and `Ahoy::Visit` records older than `log_retention_period` (default: 1 year). Schedule it with your preferred job scheduler:
205
+
206
+ ```ruby
207
+ # e.g. with whenever or Sidekiq-cron
208
+ ActionTrace::PurgeActivityLogJob.perform_later
209
+ ```
210
+
211
+ ## License
212
+
213
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ require 'bundler/gem_tasks'
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class ActivityLogsController < ApplicationController
5
+ # Uncomment and adapt to your authentication/authorization setup.
6
+ # Run `rails generate action_trace:views --controller` to copy this file into your app.
7
+ #
8
+ # before_action :authenticate_user! # Devise
9
+ # load_and_authorize_resource # CanCanCan
10
+
11
+ def index
12
+ @filters = session[:activity_logs_filters] || {}
13
+
14
+ result = ActionTrace::FetchActivityLogs.call(
15
+ filters: @filters,
16
+ current_user: current_user,
17
+ range: params[:range] || 0
18
+ )
19
+
20
+ if result.success?
21
+ @activity_logs = result.activity_logs
22
+ @activity_logs_count = result.total_count
23
+ else
24
+ flash.now[:error] = result.message
25
+ @activity_logs = []
26
+ @activity_logs_count = 0
27
+ end
28
+
29
+ return unless request.xhr?
30
+
31
+ response.headers['Cache-Control'] = 'no-store'
32
+ render partial: 'index'
33
+ end
34
+
35
+ def filter
36
+ session[:activity_logs_filters] = if params[:reset].present?
37
+ {}
38
+ else
39
+ params[:filters]
40
+ end
41
+ redirect_to activity_logs_path
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class ApplicationController < ::ApplicationController
5
+ end
6
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActivityTrackable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include PublicActivity::StoreController
8
+
9
+ after_action :track_action
10
+ end
11
+
12
+ private
13
+
14
+ def track_action
15
+ return if should_skip_tracking?
16
+
17
+ properties = {
18
+ path: request.path,
19
+ method: request.method,
20
+ controller: controller_name,
21
+ action: action_name,
22
+ company_id: current_company_id
23
+ }
24
+
25
+ ahoy.track ActionTrace::ActivityLog::SOURCES[:page_visit], properties
26
+ end
27
+
28
+ def track_session_end
29
+ ahoy.track ActionTrace::ActivityLog::SOURCES[:session_end],
30
+ reason: 'logout',
31
+ ip: request.remote_ip,
32
+ user_agent: request.user_agent,
33
+ visit_id: ahoy.visit&.id
34
+ ahoy.reset_visit
35
+ end
36
+
37
+ def current_company_id
38
+ current_user&.company_id
39
+ end
40
+
41
+ def should_skip_tracking?
42
+ !response.successful? ||
43
+ ActionTrace.configuration.excluded_controllers.include?(controller_name) ||
44
+ ActionTrace.configuration.excluded_actions.include?(action_name) ||
45
+ current_user.nil?
46
+ end
47
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ module ApplicationHelper
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class FetchActivityLogs
5
+ include Interactor::Organizer
6
+
7
+ organize ActionTrace::InitializeContext,
8
+ ActionTrace::FetchDataChanges,
9
+ ActionTrace::FetchPageVisits,
10
+ ActionTrace::FetchSessionStarts,
11
+ ActionTrace::MergeAndFormatResults
12
+ end
13
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class FetchDataChanges
5
+ include Interactor
6
+ include ActivityLogFetchable
7
+
8
+ def call
9
+ return unless should_fetch?('data_change')
10
+
11
+ scope = base_scope(PublicActivity::Activity)
12
+ context.total_count += scope.count
13
+
14
+ entries = scope.includes(:owner, :trackable)
15
+ .order(created_at: :desc)
16
+ .offset(context.range).limit(context.per_page)
17
+ .map do |activity|
18
+ {
19
+ id: "act_#{activity.id}",
20
+ source: source_type(activity),
21
+ occurred_at: activity.created_at,
22
+ user: activity.owner&.complete_name,
23
+ trackable_type: activity.trackable_type,
24
+ details: activity.parameters || {},
25
+ paper_trail_version: PaperTrail::Version.find_by(id: activity.version_id),
26
+ trackable: activity.trackable
27
+ }
28
+ end
29
+
30
+ context.raw_collection += entries
31
+ end
32
+
33
+ private
34
+
35
+ def source_type(activity)
36
+ if activity.key.to_s.include?('destroy')
37
+ ActionTrace::ActivityLog::SOURCES[:data_destroy]
38
+ elsif activity.key.to_s.include?('create')
39
+ ActionTrace::ActivityLog::SOURCES[:data_create]
40
+ else
41
+ ActionTrace::ActivityLog::SOURCES[:data_change]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class FetchPageVisits
5
+ include Interactor
6
+ include ActivityLogFetchable
7
+
8
+ def call
9
+ return unless should_fetch_any?(%w[page_visit session_end])
10
+
11
+ scope = base_scope(Ahoy::Event)
12
+ scope = apply_source_filter(scope)
13
+
14
+ context.total_count += scope.count
15
+ context.raw_collection += map_entries(scope)
16
+ end
17
+
18
+ private
19
+
20
+ def should_fetch_any?(sources)
21
+ sources.any? { |s| should_fetch?(s) }
22
+ end
23
+
24
+ def apply_source_filter(scope)
25
+ case context.source
26
+ when ActionTrace::ActivityLog::SOURCES[:session_end] then scope.where(name: ActionTrace::ActivityLog::SOURCES[:session_end])
27
+ when ActionTrace::ActivityLog::SOURCES[:page_visit] then scope.where(name: ActionTrace::ActivityLog::SOURCES[:page_visit])
28
+ else scope
29
+ end
30
+ end
31
+
32
+ def map_entries(scope)
33
+ scope.includes(:user)
34
+ .order(time: :desc)
35
+ .offset(context.range).limit(context.per_page)
36
+ .map { |event| format_entry(event) }
37
+ end
38
+
39
+ def format_entry(event)
40
+ props = event.properties || {}
41
+ {
42
+ id: "ahoy_#{event.id}",
43
+ source: event.name == 'session_end' ? 'session_end' : 'page_visit',
44
+ occurred_at: event.time,
45
+ user: event.user&.complete_name,
46
+ url: props['path'],
47
+ details: props
48
+ }
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class FetchSessionStarts
5
+ include Interactor
6
+ include ActivityLogFetchable
7
+
8
+ def call
9
+ return unless should_fetch?('session_start')
10
+
11
+ scope = base_scope(Ahoy::Visit)
12
+ context.total_count += scope.count
13
+
14
+ entries = scope.includes(:user)
15
+ .order(started_at: :desc)
16
+ .offset(context.range).limit(context.per_page)
17
+ .map do |visit|
18
+ {
19
+ id: "visit_#{visit.id}",
20
+ source: 'session_start',
21
+ occurred_at: visit.started_at,
22
+ user: visit.user&.complete_name,
23
+ subject: "#{visit.browser} on #{visit.os} (#{visit.ip})",
24
+ details: visit.attributes.slice('ip', 'browser', 'os', 'device_type', 'country', 'landing_page', 'user_agent')
25
+ }
26
+ end
27
+
28
+ context.raw_collection += entries
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class InitializeContext
5
+ include Interactor
6
+
7
+ def call
8
+ filters = context.filters || {}
9
+ user = context.current_user
10
+
11
+ context.company_id = filters['company_id'].presence || user.company_id
12
+ context.user_id = filters['user_id']
13
+ context.source = filters['source']
14
+ context.start_date = filters['start_date']
15
+ context.end_date = filters['end_date']
16
+ context.range = context.range.to_i
17
+ context.per_page = 50
18
+
19
+ context.raw_collection = []
20
+ context.total_count = 0
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class MergeAndFormatResults
5
+ include Interactor
6
+ include ActivityLogFetchable
7
+
8
+ def call
9
+ sorted_results = context.raw_collection
10
+ .sort_by { |item| item[:occurred_at] }
11
+ .reverse
12
+ .first(context.per_page)
13
+
14
+ context.activity_logs = sorted_results.map { |attrs| ActionTrace::ActivityLog.new(attrs) }
15
+
16
+ total = context.total_count
17
+ context.activity_logs.define_singleton_method(:total_count) { total }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActivityLogFetchable
4
+ extend ActiveSupport::Concern
5
+
6
+ def should_fetch?(source_type)
7
+ return true if context.source.blank?
8
+
9
+ if %w[data_change data_destroy].include?(source_type)
10
+ return context.source == ActionTrace::ActivityLog::SOURCES[:data_change] ||
11
+ context.source == ActionTrace::ActivityLog::SOURCES[:data_destroy] ||
12
+ context.source == ActionTrace::ActivityLog::SOURCES[:data_create]
13
+ end
14
+
15
+ context.source == source_type
16
+ end
17
+
18
+ def base_scope(model_class)
19
+ scope = model_class.all
20
+ scope = apply_activity_type_filter(scope) if model_class == PublicActivity::Activity
21
+ if context.company_id.present?
22
+ scope = if model_class.respond_to?(:filter_by_company)
23
+ model_class.filter_by_company(scope, context.company_id)
24
+ else
25
+ scope.where(company_id: context.company_id)
26
+ end
27
+ end
28
+
29
+ user_column = model_class == PublicActivity::Activity ? :owner_id : :user_id
30
+ scope = scope.where(user_column => context.user_id) if context.user_id.present?
31
+
32
+ apply_date_filters(scope, model_class)
33
+ end
34
+
35
+ private
36
+
37
+ def apply_activity_type_filter(scope)
38
+ return scope if context.source.blank?
39
+
40
+ case context.source
41
+ when ActionTrace::ActivityLog::SOURCES[:data_destroy]
42
+ scope.where('activities.`key` LIKE ?', '%.destroy')
43
+ when ActionTrace::ActivityLog::SOURCES[:data_change]
44
+ scope.where('activities.`key` LIKE ?', '%.update')
45
+ when ActionTrace::ActivityLog::SOURCES[:data_create]
46
+ scope.where('activities.`key` LIKE ?', '%.create')
47
+ else
48
+ scope
49
+ end
50
+ end
51
+
52
+ def apply_date_filters(scope, model_class)
53
+ date_column = date_column_for(model_class)
54
+ table = model_class.table_name
55
+
56
+ scope = apply_start_date_filter(scope, table, date_column)
57
+ apply_end_date_filter(scope, table, date_column)
58
+ rescue ArgumentError
59
+ scope
60
+ end
61
+
62
+ def date_column_for(model_class)
63
+ case model_class.to_s
64
+ when 'Ahoy::Event' then :time
65
+ when 'Ahoy::Visit' then :started_at
66
+ else :created_at
67
+ end
68
+ end
69
+
70
+ def apply_start_date_filter(scope, table, column)
71
+ return scope if context.start_date.blank?
72
+
73
+ scope.where(table => { column => Date.parse(context.start_date).beginning_of_day.. })
74
+ end
75
+
76
+ def apply_end_date_filter(scope, table, column)
77
+ return scope if context.end_date.blank?
78
+
79
+ scope.where(table => { column => ..Date.parse(context.end_date).end_of_day })
80
+ end
81
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionTrace
4
+ class PurgeActivityLogJob < ApplicationJob
5
+ queue_as :maintenance
6
+
7
+ def perform
8
+ threshold = ActionTrace.configuration.log_retention_period.ago
9
+
10
+ act_count = PublicActivity::Activity.where(created_at: ...threshold).delete_all
11
+ evt_count = Ahoy::Event.where(time: ...threshold).delete_all
12
+ vst_count = Ahoy::Visit.where(started_at: ...threshold).delete_all
13
+
14
+ Rails.logger.info "Activity log: Removed #{act_count} PublicActivities, #{evt_count} AhoyEvents, #{vst_count} AhoyVisits."
15
+ end
16
+ end
17
+ end