chronicle-rails 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 +28 -0
- data/Rakefile +6 -0
- data/app/controllers/chronicle/api_logs_controller.rb +61 -0
- data/app/controllers/chronicle/api_routes_controller.rb +35 -0
- data/app/controllers/chronicle/application_controller.rb +23 -0
- data/app/controllers/chronicle/auth_controller.rb +17 -0
- data/app/controllers/chronicle/error_groups_controller.rb +58 -0
- data/app/controllers/chronicle/error_logs_controller.rb +47 -0
- data/app/controllers/chronicle/resource_controller.rb +41 -0
- data/app/controllers/concerns/chronicle/filterable.rb +57 -0
- data/app/controllers/concerns/chronicle/pagination.rb +41 -0
- data/app/errors/chronicle/authentication_error.rb +7 -0
- data/app/errors/chronicle/bad_request_error.rb +7 -0
- data/app/errors/chronicle/base_error.rb +10 -0
- data/app/errors/chronicle/forbidden_error.rb +7 -0
- data/app/errors/chronicle/not_acceptable_error.rb +7 -0
- data/app/errors/chronicle/not_found_error.rb +7 -0
- data/app/errors/chronicle/resource_busy_error.rb +7 -0
- data/app/errors/chronicle/validation_error.rb +7 -0
- data/app/jobs/chronicle/application_job.rb +4 -0
- data/app/jobs/chronicle/flush_api_logs_job.rb +9 -0
- data/app/mailers/chronicle/application_mailer.rb +6 -0
- data/app/models/chronicle/admin_user.rb +16 -0
- data/app/models/chronicle/api_log.rb +25 -0
- data/app/models/chronicle/api_route.rb +5 -0
- data/app/models/chronicle/application_record.rb +5 -0
- data/app/models/chronicle/error_group.rb +53 -0
- data/app/models/chronicle/error_log.rb +69 -0
- data/app/services/chronicle/api_logs/buffer.rb +62 -0
- data/app/services/chronicle/api_logs/flusher.rb +98 -0
- data/app/services/chronicle/api_logs/metrics.rb +285 -0
- data/app/services/chronicle/api_logs/updater.rb +19 -0
- data/app/services/chronicle/api_routes/stats.rb +131 -0
- data/app/services/chronicle/error_logs/group_resolver.rb +66 -0
- data/config/routes.rb +28 -0
- data/db/migrate/20260101000001_create_chronicle_admin_users.rb +16 -0
- data/db/migrate/20260101000002_create_chronicle_api_logs.rb +32 -0
- data/db/migrate/20260101000003_create_chronicle_api_routes.rb +13 -0
- data/db/migrate/20260101000004_create_chronicle_error_groups.rb +26 -0
- data/db/migrate/20260101000005_create_chronicle_error_logs.rb +19 -0
- data/lib/chronicle/configuration.rb +56 -0
- data/lib/chronicle/engine.rb +12 -0
- data/lib/chronicle/util.rb +26 -0
- data/lib/chronicle/version.rb +3 -0
- data/lib/chronicle-rails.rb +1 -0
- data/lib/chronicle.rb +70 -0
- data/lib/tasks/chronicle_tasks.rake +4 -0
- metadata +127 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 01df3cacea5d88d244620c36366cb20ddfd054c6ea59ee4894bb3e1fbd5f25e5
|
|
4
|
+
data.tar.gz: 5029310077b4c2e31e5da5bbea76881805ad601858586067ccc4dfed50718e9e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ace523c09d4cd700f40f58656bde01c84ee0f750c21a256c6aa376fb124e9547d1dbd1a41e103d577ed8cab8b32fbffd886203e113e5ffb001a288da6f5e46f1
|
|
7
|
+
data.tar.gz: a2e3007b8a1d5e6cfc4621acf9b42fd1bee6d1f1efba4bfc3f958ca78aacaeee6102cec904e05ec732e3f4db93d5889c791961b12dffd01ec85a8c92566f2662
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright TODO: Write your name
|
|
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,28 @@
|
|
|
1
|
+
# Chronicle
|
|
2
|
+
Short description and motivation.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
How to use my plugin.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
Add this line to your application's Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "chronicle"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
```bash
|
|
16
|
+
$ bundle
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
```bash
|
|
21
|
+
$ gem install chronicle
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Contributing
|
|
25
|
+
Contribution directions go here.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
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,61 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class ApiLogsController < ResourceController
|
|
3
|
+
FILTER_DEFINITION = {
|
|
4
|
+
user_id: :exact,
|
|
5
|
+
request_id: :exact,
|
|
6
|
+
device_os: :exact,
|
|
7
|
+
device_id: :exact,
|
|
8
|
+
device_type: :exact,
|
|
9
|
+
ip_address: :exact,
|
|
10
|
+
http_method: :exact,
|
|
11
|
+
api_endpoint: :like,
|
|
12
|
+
http_status_code: :exact,
|
|
13
|
+
brand: :exact,
|
|
14
|
+
device_model_name: :like,
|
|
15
|
+
os_version: :exact,
|
|
16
|
+
backend_version: :exact,
|
|
17
|
+
client_version: :exact,
|
|
18
|
+
time_zone: :exact,
|
|
19
|
+
start_date: :date_range,
|
|
20
|
+
end_date: :date_range,
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def update
|
|
24
|
+
api_log = ApiLogs::Updater.new(params[:request_id], params.require(:frontend_response_time_ms)).call
|
|
25
|
+
render json: { status: 'success', id: api_log.id }, status: :ok
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def kpi_cards
|
|
29
|
+
render json: ApiLogs::Metrics.kpi_cards(filters: metrics_filters), status: :ok
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def distribution_metrics
|
|
33
|
+
render json: ApiLogs::Metrics.distribution_metrics(filters: metrics_filters), status: :ok
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def model
|
|
39
|
+
ApiLog
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def filter_definition
|
|
43
|
+
FILTER_DEFINITION
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def date_column
|
|
47
|
+
:timestamp
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def eager_load_associations
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def metrics_filters
|
|
55
|
+
params.fetch(:filters, {}).permit(
|
|
56
|
+
:start_date, :end_date, :client_version, :backend_version,
|
|
57
|
+
:device_os, :time_zone, :api_endpoint, :http_method
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class ApiRoutesController < ResourceController
|
|
3
|
+
FILTER_DEFINITION = {
|
|
4
|
+
http_method: :exact,
|
|
5
|
+
path: :like,
|
|
6
|
+
}.freeze
|
|
7
|
+
|
|
8
|
+
before_action :authenticate_admin_user!
|
|
9
|
+
|
|
10
|
+
def stats
|
|
11
|
+
result = ApiRoutes::Stats.new(
|
|
12
|
+
filters: stats_filters,
|
|
13
|
+
sort_by: params[:sort_by],
|
|
14
|
+
sort_direction: params[:sort_direction],
|
|
15
|
+
page: params[:page],
|
|
16
|
+
per_page: params[:per_page]
|
|
17
|
+
).call
|
|
18
|
+
render json: result, status: :ok
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def model
|
|
24
|
+
ApiRoute
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def filter_definition
|
|
28
|
+
FILTER_DEFINITION
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stats_filters
|
|
32
|
+
params.fetch(:filters, {}).permit(:start_date, :end_date, :http_method, :api_endpoint)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class ApplicationController < ActionController::API
|
|
3
|
+
rescue_from BaseError do |e|
|
|
4
|
+
render json: { error: e.message }, status: e.status_code
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
rescue_from ActionController::ParameterMissing do |e|
|
|
8
|
+
render json: { error: e.message }, status: :bad_request
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def authenticate_admin_user!
|
|
14
|
+
auth_token = request.headers['X-Admin-Auth-Token']
|
|
15
|
+
raise ForbiddenError, 'Missing admin auth token' unless auth_token
|
|
16
|
+
|
|
17
|
+
admin_user = AdminUser.find_by(auth_token: auth_token)
|
|
18
|
+
raise ForbiddenError, 'Invalid admin auth token' unless admin_user
|
|
19
|
+
|
|
20
|
+
@current_admin_user = admin_user
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class AuthController < ApplicationController
|
|
3
|
+
def login
|
|
4
|
+
email = params.require(:email)
|
|
5
|
+
password = params.require(:password)
|
|
6
|
+
|
|
7
|
+
admin_user = AdminUser.find_by(email: email)
|
|
8
|
+
raise NotFoundError, 'User not found' unless admin_user
|
|
9
|
+
|
|
10
|
+
if admin_user.authenticate(password)
|
|
11
|
+
render json: { admin_user: admin_user.slice(:id, :name, :email, :auth_token) }, status: :ok
|
|
12
|
+
else
|
|
13
|
+
render json: { error: 'Invalid email or password' }, status: :unauthorized
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class ErrorGroupsController < ResourceController
|
|
3
|
+
before_action :authenticate_admin_user!, only: [:index, :show, :update] # rubocop:disable Rails/LexicallyScopedActionFilter
|
|
4
|
+
before_action :set_error_group, only: [:update]
|
|
5
|
+
|
|
6
|
+
FILTER_DEFINITION = {
|
|
7
|
+
project: :exact,
|
|
8
|
+
source_type: :exact,
|
|
9
|
+
source_name: :like,
|
|
10
|
+
error_message: :like,
|
|
11
|
+
status: :exact,
|
|
12
|
+
fingerprint: :exact,
|
|
13
|
+
backend_version: :exact,
|
|
14
|
+
client_version: :exact,
|
|
15
|
+
start_date: :date_range,
|
|
16
|
+
end_date: :date_range,
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def show
|
|
20
|
+
record = ErrorGroup.find_by(id: params[:id])
|
|
21
|
+
raise NotFoundError, 'Error group not found' unless record
|
|
22
|
+
|
|
23
|
+
render json: record.get_hash, status: :ok
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update
|
|
27
|
+
if params[:status].present? && ErrorGroup::VALID_STATUSES.exclude?(params[:status])
|
|
28
|
+
raise BadRequestError, 'Invalid status'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@error_group.update!(update_params)
|
|
32
|
+
render json: @error_group.get_hash, status: :ok
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def model
|
|
38
|
+
ErrorGroup
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def filter_definition
|
|
42
|
+
FILTER_DEFINITION
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def date_column
|
|
46
|
+
:last_seen_at
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set_error_group
|
|
50
|
+
@error_group = ErrorGroup.find_by(id: params[:id])
|
|
51
|
+
raise NotFoundError, 'Error group not found' unless @error_group
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def update_params
|
|
55
|
+
params.permit(:status, :jira_link)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class ErrorLogsController < ResourceController
|
|
3
|
+
before_action :authenticate_admin_user!, only: [:index, :show, :destroy] # rubocop:disable Rails/LexicallyScopedActionFilter
|
|
4
|
+
before_action :set_error_log, only: [:destroy]
|
|
5
|
+
|
|
6
|
+
FILTER_DEFINITION = {
|
|
7
|
+
request_id: :exact,
|
|
8
|
+
backend_version: :exact,
|
|
9
|
+
client_version: :exact,
|
|
10
|
+
user_id: :exact,
|
|
11
|
+
error_group_id: :exact,
|
|
12
|
+
start_date: :date_range,
|
|
13
|
+
end_date: :date_range,
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def show
|
|
17
|
+
record = ErrorLog.find_by(id: params[:id])
|
|
18
|
+
raise NotFoundError, 'Error log not found' unless record
|
|
19
|
+
|
|
20
|
+
render json: record.get_hash, status: :ok
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def destroy
|
|
24
|
+
@error_log.destroy
|
|
25
|
+
render status: :no_content
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def model
|
|
31
|
+
ErrorLog
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def filter_definition
|
|
35
|
+
FILTER_DEFINITION
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set_error_log
|
|
39
|
+
@error_log = ErrorLog.find_by(id: params[:id])
|
|
40
|
+
raise NotFoundError, 'Error log not found' unless @error_log
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def eager_load_associations
|
|
44
|
+
[:error_group]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class ResourceController < ApplicationController
|
|
3
|
+
include Pagination
|
|
4
|
+
include Filterable
|
|
5
|
+
|
|
6
|
+
before_action :authenticate_admin_user!, only: [:index, :show]
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
query = build_query(model, filter_definition: filter_definition, filters: params[:filters],
|
|
10
|
+
date_column: date_column)
|
|
11
|
+
query = query.includes(eager_load_associations) if eager_load_associations.any?
|
|
12
|
+
render json: paginate(query), status: :ok
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
record = model.find_by(id: params[:id])
|
|
17
|
+
raise NotFoundError, 'Record not found' unless record
|
|
18
|
+
|
|
19
|
+
data = record.respond_to?(:get_hash) ? record.get_hash : record.attributes
|
|
20
|
+
render json: data, status: :ok
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def model
|
|
26
|
+
raise NotImplementedError, "#{self.class} must define #model"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def filter_definition
|
|
30
|
+
raise NotImplementedError, "#{self.class} must define #filter_definition"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def date_column
|
|
34
|
+
:created_at
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def eager_load_associations
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
module Filterable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
def build_query(model, filter_definition: {}, filters: {}, group_by: nil, date_column: :created_at)
|
|
6
|
+
scope = model.all
|
|
7
|
+
params_to_hash = filters.present? ? Util.coerce_to_hash(filters).symbolize_keys : {}
|
|
8
|
+
|
|
9
|
+
filter_definition.each do |key, filter_type|
|
|
10
|
+
param_value = params_to_hash[key]
|
|
11
|
+
next if param_value.blank?
|
|
12
|
+
|
|
13
|
+
scope = apply_filter(scope, key, filter_type, param_value, date_column)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
scope = scope.group(group_by) if group_by.present?
|
|
17
|
+
|
|
18
|
+
scope
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def apply_filter(scope, key, filter_type, value, date_column)
|
|
24
|
+
case filter_type
|
|
25
|
+
when :like
|
|
26
|
+
sanitized_value = "%#{ActiveRecord::Base.sanitize_sql_like(value)}%"
|
|
27
|
+
scope.where(scope.arel_table[key].matches(sanitized_value))
|
|
28
|
+
when :date_range
|
|
29
|
+
apply_date_range_filter(scope, key, value, date_column)
|
|
30
|
+
when :exact
|
|
31
|
+
scope.where(key => value)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def apply_date_range_filter(scope, key, value, date_column)
|
|
36
|
+
date = Util.parse_date(value)
|
|
37
|
+
raise BadRequestError, 'Invalid Date Format' if date.nil?
|
|
38
|
+
|
|
39
|
+
case key
|
|
40
|
+
when :start_date
|
|
41
|
+
apply_date_range(scope, { from: date.beginning_of_day }, date_key: date_column)
|
|
42
|
+
when :end_date
|
|
43
|
+
apply_date_range(scope, { to: date.end_of_day }, date_key: date_column)
|
|
44
|
+
else
|
|
45
|
+
scope.where(date_column => date.all_day)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def apply_date_range(scope, range, date_key: :datetime)
|
|
50
|
+
return scope if range.nil?
|
|
51
|
+
|
|
52
|
+
scope = scope.where(date_key => (range[:from])..) if range[:from].present?
|
|
53
|
+
scope = scope.where(date_key => ..(range[:to])) if range[:to].present?
|
|
54
|
+
scope
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
module Pagination
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
DEFAULT_LIMIT = 20
|
|
6
|
+
MAX_LIMIT = 100
|
|
7
|
+
|
|
8
|
+
def paginate(scope)
|
|
9
|
+
limit = validate_limit
|
|
10
|
+
last_id = params[:last_id]&.to_i
|
|
11
|
+
|
|
12
|
+
scope = scope.where(id: ...last_id) if last_id.present?
|
|
13
|
+
scope = scope.order(id: order_key).limit(limit)
|
|
14
|
+
batch_count = scope.size
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
data: scope.map { |record| item_data(record) },
|
|
18
|
+
pagination: {
|
|
19
|
+
has_more: batch_count == limit,
|
|
20
|
+
last_id: scope.last&.id,
|
|
21
|
+
batch_count: batch_count,
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def validate_limit
|
|
29
|
+
limit = params[:limit].present? ? [params[:limit].to_i, 1].max : DEFAULT_LIMIT
|
|
30
|
+
[limit, MAX_LIMIT].min
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def order_key
|
|
34
|
+
params[:order_by] == 'asc' ? :asc : :desc
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def item_data(record)
|
|
38
|
+
record.respond_to?(:get_hash) ? record.get_hash : record.attributes
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class AdminUser < ApplicationRecord
|
|
3
|
+
enum :role, a1: 0, a2: 1
|
|
4
|
+
validates :password, presence: true, length: { minimum: 6 }
|
|
5
|
+
validates :email, :auth_token, :name, presence: true
|
|
6
|
+
|
|
7
|
+
has_secure_password validations: false
|
|
8
|
+
|
|
9
|
+
A1 = :a1
|
|
10
|
+
A2 = :a2
|
|
11
|
+
|
|
12
|
+
def basic_info
|
|
13
|
+
self.slice(:id, :name, :email)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class ApiLog < ApplicationRecord
|
|
3
|
+
after_commit :sync_api_route, on: :create
|
|
4
|
+
|
|
5
|
+
def get_hash
|
|
6
|
+
hash = attributes
|
|
7
|
+
|
|
8
|
+
klass = Chronicle.config.user_model
|
|
9
|
+
|
|
10
|
+
hash['user'] = klass.find_by(id: user_id)&.basic_info if user_id.present? && klass.present?
|
|
11
|
+
|
|
12
|
+
hash
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def sync_api_route
|
|
18
|
+
return if api_endpoint.blank? || http_method.blank?
|
|
19
|
+
return if ApiRoute.exists?(path: api_endpoint, http_method: http_method)
|
|
20
|
+
|
|
21
|
+
ApiRoute.create!(path: api_endpoint, http_method: http_method, first_seen_at: created_at,
|
|
22
|
+
created_at: created_at, updated_at: created_at)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Chronicle
|
|
2
|
+
class ErrorGroup < ApplicationRecord
|
|
3
|
+
has_many :error_logs, dependent: :destroy
|
|
4
|
+
|
|
5
|
+
OPEN = 'open'.freeze
|
|
6
|
+
RESOLVED = 'resolved'.freeze
|
|
7
|
+
IGNORED = 'ignored'.freeze
|
|
8
|
+
VALID_STATUSES = [OPEN, RESOLVED, IGNORED].freeze
|
|
9
|
+
|
|
10
|
+
validates :project, presence: true
|
|
11
|
+
validates :fingerprint, presence: true
|
|
12
|
+
validates :source_type,
|
|
13
|
+
:source_name,
|
|
14
|
+
:error_message, presence: true
|
|
15
|
+
validates :status, inclusion: { in: VALID_STATUSES }, presence: true
|
|
16
|
+
validates :first_seen_at,
|
|
17
|
+
:last_seen_at, presence: true
|
|
18
|
+
validates :occurrence_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
|
19
|
+
|
|
20
|
+
# Derives a deterministic fingerprint from the structural identity of an error.
|
|
21
|
+
# Same source location + message + cleaned backtrace always yields the same hash,
|
|
22
|
+
# so two occurrences of the same bug map to the same group.
|
|
23
|
+
def self.compute_fingerprint(error_log)
|
|
24
|
+
payload = [
|
|
25
|
+
error_log.source_type,
|
|
26
|
+
error_log.source_name,
|
|
27
|
+
error_log.error_message,
|
|
28
|
+
error_log.cleaned_backtrace.to_s.first(2000),
|
|
29
|
+
].join("\xff")
|
|
30
|
+
Digest::SHA256.hexdigest(payload)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Atomically records a new occurrence of this error.
|
|
34
|
+
# Uses a row-level lock so concurrent increments never collide.
|
|
35
|
+
def record_occurrence!(backend_version: nil, client_version: nil, at: Time.current)
|
|
36
|
+
with_lock do
|
|
37
|
+
self.last_seen_at = [last_seen_at, at].compact.max
|
|
38
|
+
self.backend_version = backend_version if backend_version.present?
|
|
39
|
+
self.client_version = client_version if client_version.present?
|
|
40
|
+
self.occurrence_count += 1
|
|
41
|
+
save!
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def get_hash
|
|
46
|
+
attributes.symbolize_keys.merge(error_fingerprint: fingerprint)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def as_json(_options = {})
|
|
50
|
+
get_hash
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|