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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +213 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/action_trace/application.css +15 -0
- data/app/controllers/action_trace/activity_logs_controller.rb +44 -0
- data/app/controllers/action_trace/application_controller.rb +6 -0
- data/app/controllers/concerns/activity_trackable.rb +47 -0
- data/app/helpers/action_trace/application_helper.rb +6 -0
- data/app/interactors/action_trace/fetch_activity_logs.rb +13 -0
- data/app/interactors/action_trace/fetch_data_changes.rb +45 -0
- data/app/interactors/action_trace/fetch_page_visits.rb +51 -0
- data/app/interactors/action_trace/fetch_session_starts.rb +31 -0
- data/app/interactors/action_trace/initialize_context.rb +23 -0
- data/app/interactors/action_trace/merge_and_format_results.rb +20 -0
- data/app/interactors/concerns/activity_log_fetchable.rb +81 -0
- data/app/jobs/action_trace/application_job.rb +6 -0
- data/app/jobs/action_trace/purge_activity_log_job.rb +17 -0
- data/app/models/action_trace/activity_log.rb +61 -0
- data/app/models/action_trace/application_record.rb +7 -0
- data/app/models/concerns/action_trace/data_trackable.rb +64 -0
- data/app/presenters/action_trace/activity_log_presenter.rb +70 -0
- data/app/views/action_trace/activity_logs/_activity_log.html.erb +6 -0
- data/app/views/action_trace/activity_logs/_index.html.erb +13 -0
- data/app/views/action_trace/activity_logs/index.html.erb +1 -0
- data/config/locales/en.yml +21 -0
- data/config/locales/it.yml +18 -0
- data/config/locales/views/en.yml +24 -0
- data/config/locales/views/it.yml +24 -0
- data/config/routes.rb +9 -0
- data/lib/action_trace/configuration.rb +14 -0
- data/lib/action_trace/engine.rb +32 -0
- data/lib/action_trace/version.rb +5 -0
- data/lib/action_trace.rb +19 -0
- data/lib/generators/action_trace/install/POST_INSTALL +29 -0
- data/lib/generators/action_trace/install/install_generator.rb +47 -0
- data/lib/generators/action_trace/install/templates/initializers/action_trace.rb.tt +31 -0
- data/lib/generators/action_trace/install/templates/migrations/add_version_id_to_activities.rb.tt +7 -0
- data/lib/generators/action_trace/views/templates/views/action_trace/activity_logs/_index.html.erb +7 -0
- data/lib/generators/action_trace/views/templates/views/action_trace/activity_logs/index.html.erb +1 -0
- data/lib/generators/action_trace/views/views_generator.rb +31 -0
- data/lib/tasks/action_trace_tasks.rake +6 -0
- 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,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,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,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,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
|