lesli_audit 1.1.0 → 1.1.2

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +6 -1
  3. data/app/assets/stylesheets/lesli_audit/application.css +366 -35
  4. data/app/controllers/lesli_audit/accounts_controller.rb +7 -3
  5. data/app/controllers/lesli_audit/logs_controller.rb +3 -8
  6. data/app/controllers/lesli_audit/visitors_controller.rb +1 -1
  7. data/app/interfaces/lesli_audit/logger_interface.rb +56 -59
  8. data/app/jobs/lesli_audit/retention_cleanup_job.rb +9 -0
  9. data/app/models/concerns/lesli_audit/append_only.rb +12 -0
  10. data/app/models/lesli_audit/account.rb +26 -10
  11. data/app/models/lesli_audit/account_device.rb +3 -1
  12. data/app/models/lesli_audit/account_log.rb +12 -7
  13. data/app/models/lesli_audit/account_request.rb +3 -1
  14. data/app/models/lesli_audit/user_journal.rb +16 -3
  15. data/app/models/lesli_audit/user_log.rb +12 -3
  16. data/app/models/lesli_audit/user_request.rb +7 -3
  17. data/app/services/lesli_audit/activity_audit.rb +130 -0
  18. data/app/services/lesli_audit/request_service.rb +2 -35
  19. data/app/services/lesli_audit/retention_cleanup.rb +146 -0
  20. data/app/services/lesli_audit/user_agent_parser.rb +51 -0
  21. data/app/services/lesli_audit/user_service.rb +18 -16
  22. data/app/services/lesli_audit/visitor_service.rb +22 -49
  23. data/app/views/lesli_audit/logs/show.html.erb +14 -8
  24. data/app/views/lesli_audit/partials/_navigation.html.erb +1 -1
  25. data/app/views/lesli_audit/users/index.html.erb +67 -64
  26. data/app/views/lesli_audit/visitors/index.html.erb +126 -80
  27. data/config/routes.rb +3 -2
  28. data/db/migrate/v1.0/0501000110_create_lesli_audit_accounts.rb +14 -1
  29. data/db/migrate/v1.0/0501110110_create_lesli_audit_account_logs.rb +10 -6
  30. data/db/migrate/v1.0/0501110210_create_lesli_audit_account_devices.rb +10 -7
  31. data/db/migrate/v1.0/0501110310_create_lesli_audit_account_requests.rb +10 -7
  32. data/db/migrate/v1.0/0501120110_create_lesli_audit_user_logs.rb +11 -7
  33. data/db/migrate/v1.0/0501120210_create_lesli_audit_user_journals.rb +19 -8
  34. data/db/migrate/v1.0/0501120310_create_lesli_audit_user_requests.rb +13 -10
  35. data/db/seed/devices.rb +1 -1
  36. data/db/seed/requests.rb +3 -3
  37. data/lib/lesli_audit/engine.rb +1 -6
  38. data/lib/lesli_audit/version.rb +2 -2
  39. data/lib/tasks/lesli_audit_tasks.rake +12 -4
  40. data/readme.md +127 -50
  41. metadata +14 -4
@@ -0,0 +1,12 @@
1
+ module LesliAudit
2
+ module AppendOnly
3
+ extend ActiveSupport::Concern
4
+
5
+ # New records remain writable until their first INSERT. Once persisted,
6
+ # Active Record blocks ordinary updates and destroys. Retention cleanup
7
+ # and tenant hard-purge foreign keys intentionally operate below models.
8
+ def readonly?
9
+ persisted? || super
10
+ end
11
+ end
12
+ end
@@ -1,19 +1,35 @@
1
1
  module LesliAudit
2
2
  class Account < ApplicationRecord
3
- belongs_to :account, class_name: "Lesli::Account"
3
+ belongs_to :account,
4
+ class_name: "Lesli::Account",
5
+ inverse_of: :audit
6
+
7
+ validates :account_id, uniqueness: true
8
+
4
9
  has_many :dashboards
5
10
 
6
- has_many :account_logs
7
- has_many :account_devices
8
- has_many :account_requests
11
+ has_many :account_logs,
12
+ inverse_of: :account,
13
+ dependent: :delete_all
14
+
15
+ has_many :account_devices,
16
+ inverse_of: :account,
17
+ dependent: :delete_all
18
+
19
+ has_many :account_requests,
20
+ inverse_of: :account,
21
+ dependent: :delete_all
9
22
 
10
- has_many :user_logs
11
- has_many :user_journals
12
- has_many :user_requests
23
+ has_many :user_logs,
24
+ inverse_of: :account,
25
+ dependent: :delete_all
13
26
 
14
- after_create :initialize_account
27
+ has_many :user_journals,
28
+ inverse_of: :account,
29
+ dependent: :delete_all
15
30
 
16
- def initialize_account
17
- end
31
+ has_many :user_requests,
32
+ inverse_of: :account,
33
+ dependent: :delete_all
18
34
  end
19
35
  end
@@ -1,5 +1,7 @@
1
1
  module LesliAudit
2
2
  class AccountDevice < ApplicationRecord
3
- belongs_to :account, class_name: 'Lesli::Account'
3
+ belongs_to :account,
4
+ class_name: "LesliAudit::Account",
5
+ inverse_of: :account_devices
4
6
  end
5
7
  end
@@ -1,11 +1,16 @@
1
1
  module LesliAudit
2
2
  class AccountLog < ApplicationRecord
3
- belongs_to :account, optional: true
4
- belongs_to :user, class_name: "Lesli::User", optional: true
5
-
6
- enum :operation, {
7
- :account_creation => 'account_creation',
8
- :account_initialization => 'account_initialization'
9
- }
3
+ include LesliAudit::AppendOnly
4
+
5
+ belongs_to :account,
6
+ class_name: "LesliAudit::Account",
7
+ inverse_of: :account_logs
8
+
9
+ belongs_to :user,
10
+ class_name: "Lesli::User",
11
+ inverse_of: :account_logs,
12
+ optional: true
13
+
14
+ validates :operation, presence: true
10
15
  end
11
16
  end
@@ -1,5 +1,7 @@
1
1
  module LesliAudit
2
2
  class AccountRequest < ApplicationRecord
3
- belongs_to :account
3
+ belongs_to :account,
4
+ class_name: "LesliAudit::Account",
5
+ inverse_of: :account_requests
4
6
  end
5
7
  end
@@ -1,7 +1,20 @@
1
1
  module LesliAudit
2
2
  class UserJournal < ApplicationRecord
3
- belongs_to :account
4
- belongs_to :user, class_name: "Lesli::User"
5
- belongs_to :session, class_name: "LesliShield::User::Session"
3
+ belongs_to :account,
4
+ class_name: "LesliAudit::Account",
5
+ inverse_of: :user_journals
6
+
7
+ belongs_to :user,
8
+ class_name: "Lesli::User",
9
+ inverse_of: :journals,
10
+ optional: true
11
+
12
+ validates :event_uid, presence: true
13
+ validates :request_controller, :request_action, presence: true, if: :retain?
14
+
15
+ scope :pending, -> { where(processed_at: nil) }
16
+ scope :processed, -> { where.not(processed_at: nil) }
17
+ scope :retained, -> { where(retain: true) }
18
+ scope :transient, -> { where(retain: false) }
6
19
  end
7
20
  end
@@ -1,7 +1,16 @@
1
1
  module LesliAudit
2
2
  class UserLog < ApplicationRecord
3
- belongs_to :account, optional: true
4
- belongs_to :user, class_name: "Lesli::User"
5
- belongs_to :session, class_name: "LesliShield::User::Session", optional: true
3
+ include LesliAudit::AppendOnly
4
+
5
+ belongs_to :account,
6
+ class_name: "LesliAudit::Account",
7
+ inverse_of: :user_logs
8
+
9
+ belongs_to :user,
10
+ class_name: "Lesli::User",
11
+ inverse_of: :logs,
12
+ optional: true
13
+
14
+ validates :operation, presence: true
6
15
  end
7
16
  end
@@ -1,7 +1,11 @@
1
1
  module LesliAudit
2
2
  class UserRequest < ApplicationRecord
3
- belongs_to :account
4
- belongs_to :user, class_name: "Lesli::User"
5
- belongs_to :session, class_name: "LesliShield::User::Session"
3
+ belongs_to :account,
4
+ class_name: "LesliAudit::Account",
5
+ inverse_of: :user_requests
6
+
7
+ belongs_to :user,
8
+ class_name: "Lesli::User",
9
+ inverse_of: :requests
6
10
  end
7
11
  end
@@ -0,0 +1,130 @@
1
+ =begin
2
+
3
+ Lesli
4
+
5
+ Copyright (c) 2026, Lesli Technologies, S. A.
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program. If not, see http://www.gnu.org/licenses/.
19
+
20
+ Lesli · Ruby on Rails SaaS Development Framework.
21
+
22
+ Made with ♥ by LesliTech
23
+ Building a better future, one line of code at a time.
24
+
25
+ @contact hello@lesli.tech
26
+ @website https://www.lesli.tech
27
+ @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
28
+
29
+ // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
30
+ // ·
31
+ =end
32
+
33
+ module LesliAudit
34
+ # Writes durable business and security events. Unlike request analytics,
35
+ # this entry point never rescues persistence errors: an auditable operation
36
+ # must fail when its event cannot be stored.
37
+ class ActivityAudit
38
+ class MissingAccount < StandardError; end
39
+
40
+ def self.record_account!(account:, actor: nil, **attributes)
41
+ return unless enabled?
42
+
43
+ new(account: account, actor: actor, **attributes).record_account!
44
+ end
45
+
46
+ def self.record_user!(user:, **attributes)
47
+ return unless enabled?
48
+
49
+ new(account: user&.account, actor: user, **attributes).record_user!
50
+ end
51
+
52
+ def self.enabled?
53
+ Lesli.config.audit&.dig(:enable_logs) == true
54
+ end
55
+
56
+ def initialize(
57
+ account:,
58
+ actor: nil,
59
+ engine: nil,
60
+ source: nil,
61
+ action: nil,
62
+ operation: nil,
63
+ description: nil,
64
+ session_id: nil,
65
+ subject: nil
66
+ )
67
+ @audit_account = resolve_audit_account(account)
68
+ @actor = actor
69
+ @engine = identifier(engine)
70
+ @source = identifier(source)
71
+ @action = identifier(action)
72
+ @operation = identifier(operation.presence || action.presence)
73
+ @description = description
74
+ @session_id = session_id.presence
75
+ @subject = subject
76
+
77
+ raise ArgumentError, "operation is required" if @operation.blank?
78
+ end
79
+
80
+ def record_account!
81
+ audit_account.account_logs.create!(event_attributes.merge(user: actor))
82
+ end
83
+
84
+ def record_user!
85
+ raise ArgumentError, "user is required" unless actor
86
+
87
+ audit_account.user_logs.create!(event_attributes.merge(user: actor))
88
+ end
89
+
90
+ private
91
+
92
+ attr_reader :actor, :audit_account, :subject
93
+
94
+ def resolve_audit_account(account)
95
+ resolved = account if account.is_a?(LesliAudit::Account)
96
+ resolved ||= account&.audit if account&.respond_to?(:audit)
97
+
98
+ return resolved if resolved
99
+
100
+ raise MissingAccount, "LesliAudit account is required"
101
+ end
102
+
103
+ # Keep event metadata deliberately small. Request parameters and other
104
+ # unfiltered payloads are not accepted by this API.
105
+ def event_attributes
106
+ {
107
+ engine: @engine,
108
+ source: @source,
109
+ action: @action,
110
+ operation: @operation,
111
+ description: @description,
112
+ session_id: @session_id,
113
+ actor_uid: actor_uid,
114
+ subject_type: subject&.class&.name,
115
+ subject_id: subject&.id
116
+ }
117
+ end
118
+
119
+ def actor_uid
120
+ actor.uid if actor&.respond_to?(:uid)
121
+ end
122
+
123
+ def identifier(value)
124
+ return if value.nil?
125
+ return value.name if value.respond_to?(:name) && value.name.present?
126
+
127
+ value.to_s
128
+ end
129
+ end
130
+ end
@@ -38,44 +38,11 @@ module LesliAudit
38
38
  # @param {query} Has of the formated queries/filters that will be applied to filter data
39
39
  # @description
40
40
  # @example
41
- def index
42
-
43
-
44
- requests = current_user.requests
41
+ def index
42
+ current_user.requests
45
43
  .order(created_at: :desc)
46
44
  .page(query[:pagination][:page])
47
45
  .per(query[:pagination][:perPage])
48
-
49
- return requests
50
-
51
-
52
- search_string = nil # query[:search].downcase.gsub(" ","%") unless query[:search].blank?
53
- order_string = "user_requests.created_at"
54
-
55
- user_requests = current_user.account.users.joins(:requests)
56
- #user_requests = user_requests.where("users.email = ?", query[:filters][:user]) unless query[:filters][:user].blank?
57
-
58
- if !search_string.blank?
59
- # user_requests = user_requests.where([
60
- # "lower(users.email) like :search_string",
61
- # "lower(request_controller) like :search_string",
62
- # "lower(request_action) like :search_string"
63
- # ].join(" or "), {
64
- # search_string: "%#{search_string}%"
65
- # })
66
- end
67
-
68
- user_requests.select(
69
- :id,
70
- "users.id as user_id",
71
- "users.email",
72
- :request_controller,
73
- :request_action,
74
- :request_method,
75
- :user_session_id,
76
- Date2.new.date_time.db_timestamps("user_requests")
77
- ).page(query[:pagination][:page])
78
- .per(query[:pagination][:perPage])
79
46
  end
80
47
  end
81
48
  end
@@ -0,0 +1,146 @@
1
+ =begin
2
+
3
+ Lesli
4
+
5
+ Copyright (c) 2026, Lesli Technologies, S. A.
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program. If not, see http://www.gnu.org/licenses/.
19
+
20
+ Lesli · Ruby on Rails SaaS Development Framework.
21
+
22
+ Made with ♥ by LesliTech
23
+ Building a better future, one line of code at a time.
24
+
25
+ @contact hello@lesli.tech
26
+ @website https://www.lesli.tech
27
+ @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
28
+
29
+ // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
30
+ // ·
31
+ =end
32
+
33
+ module LesliAudit
34
+ # Applies the retention policy configured by the host application.
35
+ #
36
+ # Cleanup uses delete_all deliberately: analytics has no destroy callbacks,
37
+ # while activity events are append-only during their retained lifetime. Each
38
+ # relation is deleted in bounded primary-key batches and can be retried safely.
39
+ class RetentionCleanup
40
+ DEFAULT_BATCH_SIZE = 1_000
41
+
42
+ ANALYTICS_MODELS = [
43
+ LesliAudit::AccountDevice,
44
+ LesliAudit::AccountRequest,
45
+ LesliAudit::UserRequest
46
+ ].freeze
47
+
48
+ ACTIVITY_MODELS = [
49
+ LesliAudit::AccountLog,
50
+ LesliAudit::UserLog
51
+ ].freeze
52
+
53
+ def self.call(batch_size: DEFAULT_BATCH_SIZE, today: Date.current, now: Time.current)
54
+ new(batch_size: batch_size, today: today, now: now).call
55
+ end
56
+
57
+ def initialize(batch_size:, today:, now:)
58
+ @batch_size = positive_integer!(batch_size, :batch_size)
59
+ @today = today.to_date
60
+ @now = now
61
+ end
62
+
63
+ def call
64
+ deleted = {}
65
+
66
+ cleanup_journals(deleted)
67
+ cleanup_group(
68
+ deleted,
69
+ ANALYTICS_MODELS,
70
+ column: :date,
71
+ cutoff: date_cutoff(:analytics_retention_days)
72
+ )
73
+ cleanup_group(
74
+ deleted,
75
+ ACTIVITY_MODELS,
76
+ column: :created_at,
77
+ cutoff: time_cutoff(:audit_retention_days)
78
+ )
79
+
80
+ deleted
81
+ end
82
+
83
+ private
84
+
85
+ attr_reader :batch_size, :now, :today
86
+
87
+ def cleanup_journals(deleted)
88
+ retained_count = delete_with_cutoff(
89
+ LesliAudit::UserJournal.retained.processed,
90
+ :date,
91
+ date_cutoff(:journal_retention_days)
92
+ )
93
+ transient_count = delete_with_cutoff(
94
+ LesliAudit::UserJournal.transient.processed,
95
+ :date,
96
+ date_cutoff(:analytics_retention_days)
97
+ )
98
+
99
+ deleted[LesliAudit::UserJournal.table_name] = retained_count + transient_count
100
+ end
101
+
102
+ def cleanup_group(deleted, models, column:, cutoff:)
103
+ models.each do |model|
104
+ deleted[model.table_name] = delete_with_cutoff(model.all, column, cutoff)
105
+ end
106
+ end
107
+
108
+ def delete_with_cutoff(relation, column, cutoff)
109
+ return 0 unless cutoff
110
+
111
+ relation
112
+ .where(relation.klass.arel_table[column].lt(cutoff))
113
+ .in_batches(of: batch_size)
114
+ .delete_all
115
+ end
116
+
117
+ def date_cutoff(setting)
118
+ days = retention_days(setting)
119
+ today - days if days
120
+ end
121
+
122
+ def time_cutoff(setting)
123
+ days = retention_days(setting)
124
+ now - days.days if days
125
+ end
126
+
127
+ def retention_days(setting)
128
+ value = Lesli.config.audit&.dig(setting)
129
+ return if value.nil?
130
+
131
+ positive_integer!(value, setting)
132
+ end
133
+
134
+ def positive_integer!(value, setting)
135
+ valid_value = value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A[1-9]\d*\z/))
136
+ raise ArgumentError unless valid_value
137
+
138
+ integer = value.to_i
139
+ return integer if integer.positive?
140
+
141
+ raise ArgumentError, "#{setting} must be a positive integer or nil"
142
+ rescue ArgumentError, TypeError
143
+ raise ArgumentError, "#{setting} must be a positive integer or nil"
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,51 @@
1
+ =begin
2
+
3
+ Lesli
4
+
5
+ Copyright (c) 2026, Lesli Technologies, S. A.
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program. If not, see http://www.gnu.org/licenses/.
19
+
20
+ Lesli · Ruby on Rails SaaS Development Framework.
21
+
22
+ Made with ♥ by LesliTech
23
+ Building a better future, one line of code at a time.
24
+
25
+ @contact hello@lesli.tech
26
+ @website https://www.lesli.tech
27
+ @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
28
+
29
+ // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
30
+ // ·
31
+ =end
32
+
33
+ require "device_detector"
34
+
35
+ module LesliAudit
36
+ # Provides one normalized user-agent contract for the engines that need to
37
+ # persist browser and operating-system metadata.
38
+ class UserAgentParser
39
+ def self.parse(user_agent)
40
+ detector = DeviceDetector.new(user_agent.to_s)
41
+
42
+ {
43
+ platform: detector.os_name,
44
+ os: detector.os_full_version,
45
+ browser: detector.name,
46
+ version: detector.full_version,
47
+ device: detector.device_type
48
+ }
49
+ end
50
+ end
51
+ end
@@ -35,22 +35,24 @@ module LesliAudit
35
35
 
36
36
  def users
37
37
  users = current_user.account.users
38
+ users = users.with_deleted if users.respond_to?(:with_deleted)
38
39
 
39
40
  now = Time.current
40
41
  last_30_days = 30.days.ago
42
+ quoted_false = ActiveRecord::Base.connection.quote(false)
41
43
 
42
44
  stats = users.pick(
43
45
  # total users
44
46
  Arel.sql("COUNT(*)"),
45
47
 
46
48
  # total deactivated users
47
- Arel.sql("SUM(CASE WHEN active = 0 AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
49
+ Arel.sql("SUM(CASE WHEN active = #{quoted_false} AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
48
50
 
49
51
  # total locked users
50
52
  Arel.sql(
51
53
  "SUM(CASE WHEN (
52
54
  locked_at IS NOT NULL
53
- OR locked_until > #{ActiveRecord::Base.connection.quote(Time.current)}
55
+ OR locked_until > #{ActiveRecord::Base.connection.quote(now)}
54
56
  ) AND deleted_at IS NULL THEN 1 ELSE 0 END)"
55
57
  ),
56
58
 
@@ -137,37 +139,37 @@ module LesliAudit
137
139
 
138
140
  group = 'day'
139
141
 
140
- group_by = "DATE_TRUNC('month', lesli_audit_user_requests.date)" if group == 'month'
141
- group_by = "DATE_TRUNC('week', lesli_audit_user_requests.date)" if group == 'week'
142
- group_by = "DATE_TRUNC('day', lesli_audit_user_requests.date)" if group == 'day'
142
+ group_by = "DATE_TRUNC('month', lesli_audit_user_journals.date)" if group == 'month'
143
+ group_by = "DATE_TRUNC('week', lesli_audit_user_journals.date)" if group == 'week'
144
+ group_by = "DATE_TRUNC('day', lesli_audit_user_journals.date)" if group == 'day'
143
145
 
144
146
  # compatibility for SQLite
145
147
  if ActiveRecord::Base.connection.adapter_name == "SQLite"
146
- group_by = "strftime('%Y-%m-%d', lesli_audit_user_requests.date)"
148
+ group_by = "strftime('%Y-%m-%d', lesli_audit_user_journals.date)"
147
149
  end
148
150
 
149
- user_requests = current_user.account.users.joins(:requests)
151
+ user_journals = current_user.account.users
152
+ .joins(:journals)
153
+ .merge(LesliAudit::UserJournal.retained.processed)
150
154
 
151
- user_requests = user_requests.select(
152
- :id,
155
+ user_journals = user_journals.select(
153
156
  "lesli_users.id as user_id",
154
157
  "lesli_users.email as user_email",
155
158
  "#{group_by} date",
156
- "min(lesli_audit_user_requests.created_at) as first_activity",
157
- "max(lesli_audit_user_requests.updated_at) as last_activity",
158
- "count(lesli_audit_user_requests.id) resources",
159
- "sum(lesli_audit_user_requests.request_count) requests"
159
+ "min(lesli_audit_user_journals.created_at) as first_activity",
160
+ "max(lesli_audit_user_journals.created_at) as last_activity",
161
+ "count(lesli_audit_user_journals.id) requests"
160
162
  )
161
163
 
162
- user_requests = user_requests.group(:user_id, group_by)
164
+ user_journals = user_journals.group("lesli_users.id", group_by)
163
165
 
164
- user_requests.map do |request|
166
+ user_journals.map do |request|
165
167
  request[:first_activity] = Date2.new(request[:first_activity]).time
166
168
  request[:last_activity] = Date2.new(request[:last_activity]).time
167
169
  request
168
170
  end
169
171
 
170
- user_requests.reverse
172
+ user_journals.reverse
171
173
  end
172
174
 
173
175
  def registrations