Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6c4491e936
commit
082a21ddd4
4
Gemfile
4
Gemfile
|
@ -549,3 +549,7 @@ gem 'parslet', '~> 1.8'
|
||||||
gem 'ipynbdiff', path: 'vendor/gems/ipynbdiff'
|
gem 'ipynbdiff', path: 'vendor/gems/ipynbdiff'
|
||||||
|
|
||||||
gem 'ed25519', '~> 1.3.0'
|
gem 'ed25519', '~> 1.3.0'
|
||||||
|
|
||||||
|
# Error Tracking OpenAPI client
|
||||||
|
# See https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/rake_tasks.md#update-openapi-client-for-error-tracking-feature
|
||||||
|
gem 'error_tracking_open_api', path: 'vendor/gems/error_tracking_open_api'
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
|
PATH
|
||||||
|
remote: vendor/gems/error_tracking_open_api
|
||||||
|
specs:
|
||||||
|
error_tracking_open_api (1.0.0)
|
||||||
|
typhoeus (~> 1.0, >= 1.0.1)
|
||||||
|
|
||||||
PATH
|
PATH
|
||||||
remote: vendor/gems/ipynbdiff
|
remote: vendor/gems/ipynbdiff
|
||||||
specs:
|
specs:
|
||||||
|
@ -1520,6 +1526,7 @@ DEPENDENCIES
|
||||||
elasticsearch-rails (~> 7.2)
|
elasticsearch-rails (~> 7.2)
|
||||||
email_reply_trimmer (~> 0.1)
|
email_reply_trimmer (~> 0.1)
|
||||||
email_spec (~> 2.2.0)
|
email_spec (~> 2.2.0)
|
||||||
|
error_tracking_open_api!
|
||||||
erubi (~> 1.9.0)
|
erubi (~> 1.9.0)
|
||||||
escape_utils (~> 1.1)
|
escape_utils (~> 1.1)
|
||||||
factory_bot_rails (~> 6.2.0)
|
factory_bot_rails (~> 6.2.0)
|
||||||
|
|
|
@ -150,6 +150,12 @@ export default {
|
||||||
paginationRequired() {
|
paginationRequired() {
|
||||||
return !isEmpty(this.pagination);
|
return !isEmpty(this.pagination);
|
||||||
},
|
},
|
||||||
|
previousPage() {
|
||||||
|
return this.pagination.previous ? this.$options.PREV_PAGE : null;
|
||||||
|
},
|
||||||
|
nextPage() {
|
||||||
|
return this.pagination.next ? this.$options.NEXT_PAGE : null;
|
||||||
|
},
|
||||||
errorTrackingHelpUrl() {
|
errorTrackingHelpUrl() {
|
||||||
return helpPagePath('operations/error_tracking');
|
return helpPagePath('operations/error_tracking');
|
||||||
},
|
},
|
||||||
|
@ -430,8 +436,8 @@ export default {
|
||||||
<gl-pagination
|
<gl-pagination
|
||||||
v-show="!loading"
|
v-show="!loading"
|
||||||
v-if="paginationRequired"
|
v-if="paginationRequired"
|
||||||
:prev-page="$options.PREV_PAGE"
|
:prev-page="previousPage"
|
||||||
:next-page="$options.NEXT_PAGE"
|
:next-page="nextPage"
|
||||||
:value="pageValue"
|
:value="pageValue"
|
||||||
align="center"
|
align="center"
|
||||||
@input="goToPage"
|
@input="goToPage"
|
||||||
|
|
|
@ -16,7 +16,7 @@ class ErrorTracking::ClientKey < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def sentry_dsn
|
def sentry_dsn
|
||||||
@sentry_dsn ||= ErrorTracking::Collector::Dsn.build_url(public_key, project_id)
|
@sentry_dsn ||= ::Gitlab::ErrorTracking::ErrorRepository.build(project).dsn_url(public_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
module ErrorTracking
|
module ErrorTracking
|
||||||
class ErrorEntity < Grape::Entity
|
class ErrorEntity < Grape::Entity
|
||||||
expose :id, :title, :type, :user_count, :count,
|
expose :id do |error|
|
||||||
|
error.id.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
expose :title, :type, :user_count, :count,
|
||||||
:first_seen, :last_seen, :message, :culprit,
|
:first_seen, :last_seen, :message, :culprit,
|
||||||
:external_url, :project_id, :project_name, :project_slug,
|
:external_url, :project_id, :project_name, :project_slug,
|
||||||
:short_id, :status, :frequency
|
:short_id, :status, :frequency
|
||||||
|
|
|
@ -75,6 +75,7 @@ module ErrorTracking
|
||||||
# For now we implement the bare minimum for rendering the list in UI.
|
# For now we implement the bare minimum for rendering the list in UI.
|
||||||
list_opts = {
|
list_opts = {
|
||||||
filters: { status: opts[:issue_status] },
|
filters: { status: opts[:issue_status] },
|
||||||
|
query: opts[:search_term],
|
||||||
sort: opts[:sort],
|
sort: opts[:sort],
|
||||||
limit: opts[:limit],
|
limit: opts[:limit],
|
||||||
cursor: opts[:cursor]
|
cursor: opts[:cursor]
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module ErrorTracking
|
|
||||||
module Collector
|
|
||||||
class Dsn
|
|
||||||
# Build a sentry compatible DSN URL for GitLab collector.
|
|
||||||
#
|
|
||||||
# The expected URL looks like that:
|
|
||||||
# https://PUBLIC_KEY@gitlab.example.com/api/v4/error_tracking/collector/PROJECT_ID
|
|
||||||
#
|
|
||||||
def self.build_url(public_key, project_id)
|
|
||||||
gitlab = Settings.gitlab
|
|
||||||
|
|
||||||
custom_port = Settings.gitlab_on_standard_port? ? nil : ":#{gitlab.port}"
|
|
||||||
|
|
||||||
base_url = [
|
|
||||||
gitlab.protocol,
|
|
||||||
"://",
|
|
||||||
public_key,
|
|
||||||
'@',
|
|
||||||
gitlab.host,
|
|
||||||
custom_port,
|
|
||||||
gitlab.relative_url_root
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
"#{base_url}/api/v4/error_tracking/collector/#{project_id}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -15,7 +15,12 @@ module Gitlab
|
||||||
#
|
#
|
||||||
# @return [self]
|
# @return [self]
|
||||||
def self.build(project)
|
def self.build(project)
|
||||||
strategy = ActiveRecordStrategy.new(project)
|
strategy =
|
||||||
|
if Feature.enabled?(:use_click_house_database_for_error_tracking, project)
|
||||||
|
OpenApiStrategy.new(project)
|
||||||
|
else
|
||||||
|
ActiveRecordStrategy.new(project)
|
||||||
|
end
|
||||||
|
|
||||||
new(strategy)
|
new(strategy)
|
||||||
end
|
end
|
||||||
|
@ -72,14 +77,15 @@ module Gitlab
|
||||||
# @param sort [String] order list by 'first_seen', 'last_seen', or 'frequency'
|
# @param sort [String] order list by 'first_seen', 'last_seen', or 'frequency'
|
||||||
# @param filters [Hash<Symbol, String>] filter list by
|
# @param filters [Hash<Symbol, String>] filter list by
|
||||||
# @option filters [String] :status error status
|
# @option filters [String] :status error status
|
||||||
|
# @params query [String, nil] free text search
|
||||||
# @param limit [Integer, String] limit result
|
# @param limit [Integer, String] limit result
|
||||||
# @param cursor [Hash] pagination information
|
# @param cursor [Hash] pagination information
|
||||||
#
|
#
|
||||||
# @return [Array<Array<Gitlab::ErrorTracking::Error>, Pagination>]
|
# @return [Array<Array<Gitlab::ErrorTracking::Error>, Pagination>]
|
||||||
def list_errors(sort: 'last_seen', filters: {}, limit: 20, cursor: {})
|
def list_errors(sort: 'last_seen', filters: {}, query: nil, limit: 20, cursor: {})
|
||||||
limit = [limit.to_i, 100].min
|
limit = [limit.to_i, 100].min
|
||||||
|
|
||||||
strategy.list_errors(filters: filters, sort: sort, limit: limit, cursor: cursor)
|
strategy.list_errors(filters: filters, query: query, sort: sort, limit: limit, cursor: cursor)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Fetches last event for error +id+.
|
# Fetches last event for error +id+.
|
||||||
|
@ -105,6 +111,10 @@ module Gitlab
|
||||||
strategy.update_error(id, status: status)
|
strategy.update_error(id, status: status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dsn_url(public_key)
|
||||||
|
strategy.dsn_url(public_key)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :strategy
|
attr_reader :strategy
|
||||||
|
|
|
@ -39,11 +39,12 @@ module Gitlab
|
||||||
handle_exceptions(e)
|
handle_exceptions(e)
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_errors(filters:, sort:, limit:, cursor:)
|
def list_errors(filters:, query:, sort:, limit:, cursor:)
|
||||||
errors = project_errors
|
errors = project_errors
|
||||||
errors = filter_by_status(errors, filters[:status])
|
errors = filter_by_status(errors, filters[:status])
|
||||||
errors = sort(errors, sort)
|
errors = sort(errors, sort)
|
||||||
errors = errors.keyset_paginate(cursor: cursor, per_page: limit)
|
errors = errors.keyset_paginate(cursor: cursor, per_page: limit)
|
||||||
|
# query is not supported
|
||||||
|
|
||||||
pagination = ErrorRepository::Pagination.new(errors.cursor_for_next_page, errors.cursor_for_previous_page)
|
pagination = ErrorRepository::Pagination.new(errors.cursor_for_next_page, errors.cursor_for_previous_page)
|
||||||
|
|
||||||
|
@ -60,6 +61,24 @@ module Gitlab
|
||||||
project_error(id).update(attributes)
|
project_error(id).update(attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dsn_url(public_key)
|
||||||
|
gitlab = Settings.gitlab
|
||||||
|
|
||||||
|
custom_port = Settings.gitlab_on_standard_port? ? nil : ":#{gitlab.port}"
|
||||||
|
|
||||||
|
base_url = [
|
||||||
|
gitlab.protocol,
|
||||||
|
"://",
|
||||||
|
public_key,
|
||||||
|
'@',
|
||||||
|
gitlab.host,
|
||||||
|
custom_port,
|
||||||
|
gitlab.relative_url_root
|
||||||
|
].join('')
|
||||||
|
|
||||||
|
"#{base_url}/api/v4/error_tracking/collector/#{project.id}"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :project
|
attr_reader :project
|
||||||
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
module ErrorTracking
|
||||||
|
class ErrorRepository
|
||||||
|
class OpenApiStrategy
|
||||||
|
def initialize(project)
|
||||||
|
@project = project
|
||||||
|
|
||||||
|
api_url = configured_api_url
|
||||||
|
|
||||||
|
open_api.configure do |config|
|
||||||
|
config.scheme = api_url.scheme
|
||||||
|
config.host = [api_url.host, api_url.port].compact.join(':')
|
||||||
|
config.server_index = nil
|
||||||
|
config.logger = Gitlab::AppLogger
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def report_error(
|
||||||
|
name:, description:, actor:, platform:,
|
||||||
|
environment:, level:, occurred_at:, payload:
|
||||||
|
)
|
||||||
|
raise NotImplementedError, 'Use ingestion endpoint'
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_error(id)
|
||||||
|
api = open_api::ErrorsApi.new
|
||||||
|
error = api.get_error(project_id, id)
|
||||||
|
|
||||||
|
to_sentry_detailed_error(error)
|
||||||
|
rescue ErrorTrackingOpenAPI::ApiError => e
|
||||||
|
log_exception(e)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_errors(filters:, query:, sort:, limit:, cursor:)
|
||||||
|
opts = {
|
||||||
|
sort: "#{sort}_desc",
|
||||||
|
status: filters[:status],
|
||||||
|
query: query,
|
||||||
|
cursor: cursor,
|
||||||
|
limit: limit
|
||||||
|
}.compact
|
||||||
|
|
||||||
|
api = open_api::ErrorsApi.new
|
||||||
|
errors, _status, headers = api.list_errors_with_http_info(project_id, opts)
|
||||||
|
pagination = pagination_from_headers(headers)
|
||||||
|
|
||||||
|
if errors.size < limit
|
||||||
|
# Don't show next link if amount of errors is less then requested.
|
||||||
|
# This a workaround until the Golang backend returns link cursor
|
||||||
|
# only if there is a next page.
|
||||||
|
pagination.next = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
[errors.map { to_sentry_error(_1) }, pagination]
|
||||||
|
rescue ErrorTrackingOpenAPI::ApiError => e
|
||||||
|
log_exception(e)
|
||||||
|
[[], ErrorRepository::Pagination.new]
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_event_for(id)
|
||||||
|
event = newest_event_for(id)
|
||||||
|
return unless event
|
||||||
|
|
||||||
|
api = open_api::ErrorsApi.new
|
||||||
|
error = api.get_error(project_id, id)
|
||||||
|
return unless error
|
||||||
|
|
||||||
|
to_sentry_error_event(event, error)
|
||||||
|
rescue ErrorTrackingOpenAPI::ApiError => e
|
||||||
|
log_exception(e)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_error(id, **attributes)
|
||||||
|
opts = attributes.slice(:status)
|
||||||
|
|
||||||
|
body = open_api::ErrorUpdatePayload.new(opts)
|
||||||
|
|
||||||
|
api = open_api::ErrorsApi.new
|
||||||
|
api.update_error(project_id, id, body)
|
||||||
|
|
||||||
|
true
|
||||||
|
rescue ErrorTrackingOpenAPI::ApiError => e
|
||||||
|
log_exception(e)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def dsn_url(public_key)
|
||||||
|
config = open_api::Configuration.default
|
||||||
|
|
||||||
|
base_url = [
|
||||||
|
config.scheme,
|
||||||
|
"://",
|
||||||
|
public_key,
|
||||||
|
'@',
|
||||||
|
config.host,
|
||||||
|
config.base_path
|
||||||
|
].join('')
|
||||||
|
|
||||||
|
"#{base_url}/projects/api/#{project_id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def event_for(id, sort:)
|
||||||
|
opts = { sort: sort, limit: 1 }
|
||||||
|
|
||||||
|
api = open_api::ErrorsApi.new
|
||||||
|
api.list_events(project_id, id, opts).first
|
||||||
|
rescue ErrorTrackingOpenAPI::ApiError => e
|
||||||
|
log_exception(e)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def newest_event_for(id)
|
||||||
|
event_for(id, sort: 'occurred_at_desc')
|
||||||
|
end
|
||||||
|
|
||||||
|
def oldest_event_for(id)
|
||||||
|
event_for(id, sort: 'occurred_at_asc')
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_sentry_error(error)
|
||||||
|
Gitlab::ErrorTracking::Error.new(
|
||||||
|
id: error.fingerprint.to_s,
|
||||||
|
title: error.name,
|
||||||
|
message: error.description,
|
||||||
|
culprit: error.actor,
|
||||||
|
first_seen: error.first_seen_at,
|
||||||
|
last_seen: error.last_seen_at,
|
||||||
|
status: error.status,
|
||||||
|
count: error.event_count,
|
||||||
|
user_count: error.approximated_user_count
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_sentry_detailed_error(error)
|
||||||
|
Gitlab::ErrorTracking::DetailedError.new(
|
||||||
|
id: error.fingerprint.to_s,
|
||||||
|
title: error.name,
|
||||||
|
message: error.description,
|
||||||
|
culprit: error.actor,
|
||||||
|
first_seen: error.first_seen_at.to_s,
|
||||||
|
last_seen: error.last_seen_at.to_s,
|
||||||
|
count: error.event_count,
|
||||||
|
user_count: error.approximated_user_count,
|
||||||
|
project_id: error.project_id,
|
||||||
|
status: error.status,
|
||||||
|
tags: { level: nil, logger: nil },
|
||||||
|
external_url: external_url(error.fingerprint),
|
||||||
|
external_base_url: external_base_url,
|
||||||
|
integrated: true,
|
||||||
|
first_release_version: release_from(oldest_event_for(error.fingerprint)),
|
||||||
|
last_release_version: release_from(newest_event_for(error.fingerprint))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_sentry_error_event(event, error)
|
||||||
|
Gitlab::ErrorTracking::ErrorEvent.new(
|
||||||
|
issue_id: event.fingerprint.to_s,
|
||||||
|
date_received: error.last_seen_at,
|
||||||
|
stack_trace_entries: build_stacktrace(event)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_from_headers(headers)
|
||||||
|
links = headers['link'].to_s.split(', ')
|
||||||
|
|
||||||
|
pagination_hash = links.map { parse_pagination_link(_1) }.compact.to_h
|
||||||
|
|
||||||
|
ErrorRepository::Pagination.new(pagination_hash['next'], pagination_hash['prev'])
|
||||||
|
end
|
||||||
|
|
||||||
|
LINK_PATTERN = %r{cursor=(?<cursor>[^&]+).*; rel="(?<direction>\w+)"}.freeze
|
||||||
|
|
||||||
|
def parse_pagination_link(content)
|
||||||
|
match = LINK_PATTERN.match(content)
|
||||||
|
return unless match
|
||||||
|
|
||||||
|
[match['direction'], CGI.unescape(match['cursor'])]
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_stacktrace(event)
|
||||||
|
payload = parse_json(event.payload)
|
||||||
|
return [] unless payload
|
||||||
|
|
||||||
|
::ErrorTracking::StacktraceBuilder.new(payload).stacktrace
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_json(payload)
|
||||||
|
Gitlab::Json.parse(payload)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_from(event)
|
||||||
|
return unless event
|
||||||
|
|
||||||
|
payload = parse_json(event.payload)
|
||||||
|
return unless payload
|
||||||
|
|
||||||
|
payload['release']
|
||||||
|
end
|
||||||
|
|
||||||
|
def project_id
|
||||||
|
@project.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def open_api
|
||||||
|
ErrorTrackingOpenAPI
|
||||||
|
end
|
||||||
|
|
||||||
|
# For compatibility with sentry integration
|
||||||
|
def external_url(id)
|
||||||
|
Gitlab::Routing.url_helpers.details_namespace_project_error_tracking_index_url(
|
||||||
|
namespace_id: @project.namespace,
|
||||||
|
project_id: @project,
|
||||||
|
issue_id: id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# For compatibility with sentry integration
|
||||||
|
def external_base_url
|
||||||
|
Gitlab::Routing.url_helpers.project_url(@project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def configured_api_url
|
||||||
|
url = ENV.fetch('ERROR_TRACKING_API_URL', 'http://localhost:8080')
|
||||||
|
|
||||||
|
Gitlab::UrlBlocker.validate!(url, schemes: %w[http https], allow_localhost: true)
|
||||||
|
|
||||||
|
URI(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_exception(exception)
|
||||||
|
params = {
|
||||||
|
http_code: exception.code,
|
||||||
|
response_body: exception.response_body&.truncate(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
Gitlab::AppLogger.error(Gitlab::Utils::InlineHash.merge_keys(params, prefix: 'open_api'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -55,6 +55,8 @@ namespace :gems do
|
||||||
write_file(gem_dir / 'LICENSE', license)
|
write_file(gem_dir / 'LICENSE', license)
|
||||||
write_file(gem_dir / "#{gem_name}.gemspec") do |content|
|
write_file(gem_dir / "#{gem_name}.gemspec") do |content|
|
||||||
replace_string(content, 'Unlicense', 'MIT')
|
replace_string(content, 'Unlicense', 'MIT')
|
||||||
|
replace_string(content, /(\.files\s*=).*/, '\1 Dir.glob("lib/**/*")')
|
||||||
|
replace_string(content, /(\.test_files\s*=).*/, '\1 []')
|
||||||
end
|
end
|
||||||
|
|
||||||
remove_entry_secure(gem_dir / '.rubocop.yml')
|
remove_entry_secure(gem_dir / '.rubocop.yml')
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :error_tracking_open_api_error, class: 'ErrorTrackingOpenAPI::Error' do
|
||||||
|
fingerprint { 1 }
|
||||||
|
project_id { 2 }
|
||||||
|
name { 'ActionView::MissingTemplate' }
|
||||||
|
description { 'Missing template posts/edit' }
|
||||||
|
actor { 'PostsController#edit' }
|
||||||
|
event_count { 3 }
|
||||||
|
approximated_user_count { 4 }
|
||||||
|
first_seen_at { Time.now.iso8601 }
|
||||||
|
last_seen_at { Time.now.iso8601 }
|
||||||
|
status { 'unresolved' }
|
||||||
|
|
||||||
|
skip_create
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :error_tracking_open_api_error_event, class: 'ErrorTrackingOpenAPI::ErrorEvent' do
|
||||||
|
fingerprint { 1 }
|
||||||
|
project_id { 2 }
|
||||||
|
payload { File.read(Rails.root.join('spec/fixtures/error_tracking/parsed_event.json')) }
|
||||||
|
name { 'ActionView::MissingTemplate' }
|
||||||
|
description { 'Missing template posts/edit' }
|
||||||
|
actor { 'PostsController#edit' }
|
||||||
|
environment { 'development' }
|
||||||
|
platform { 'ruby' }
|
||||||
|
|
||||||
|
trait :golang do
|
||||||
|
payload { File.read(Rails.root.join('spec/fixtures/error_tracking/go_parsed_event.json')) }
|
||||||
|
platform { 'go' }
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :browser do
|
||||||
|
payload { File.read(Rails.root.join('spec/fixtures/error_tracking/browser_event.json')) }
|
||||||
|
platform { 'javascript' }
|
||||||
|
end
|
||||||
|
|
||||||
|
skip_create
|
||||||
|
end
|
||||||
|
end
|
|
@ -398,6 +398,30 @@ describe('ErrorTrackingList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When pagination is required', () => {
|
describe('When pagination is required', () => {
|
||||||
|
describe('and previous cursor is not available', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
store.state.list.loading = false;
|
||||||
|
delete store.state.list.pagination.previous;
|
||||||
|
mountComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables Prev button in the pagination', async () => {
|
||||||
|
expect(findPagination().props('prevPage')).toBe(null);
|
||||||
|
expect(findPagination().props('nextPage')).not.toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('and next cursor is not available', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
store.state.list.loading = false;
|
||||||
|
delete store.state.list.pagination.next;
|
||||||
|
mountComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables Next button in the pagination', async () => {
|
||||||
|
expect(findPagination().props('prevPage')).not.toBe(null);
|
||||||
|
expect(findPagination().props('nextPage')).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe('and the user is not on the first page', () => {
|
describe('and the user is not on the first page', () => {
|
||||||
describe('and the previous button is clicked', () => {
|
describe('and the previous button is clicked', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
RSpec.describe ErrorTracking::Collector::Dsn do
|
|
||||||
describe '.build_url' do
|
|
||||||
let(:setting) do
|
|
||||||
{
|
|
||||||
protocol: 'https',
|
|
||||||
https: true,
|
|
||||||
port: 443,
|
|
||||||
host: 'gitlab.example.com',
|
|
||||||
relative_url_root: nil
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
subject { described_class.build_url('abcdef1234567890', 778) }
|
|
||||||
|
|
||||||
it 'returns a valid URL without explicit port' do
|
|
||||||
stub_config_setting(setting)
|
|
||||||
|
|
||||||
is_expected.to eq('https://abcdef1234567890@gitlab.example.com/api/v4/error_tracking/collector/778')
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with non-standard port' do
|
|
||||||
it 'returns a valid URL with custom port' do
|
|
||||||
setting[:port] = 4567
|
|
||||||
stub_config_setting(setting)
|
|
||||||
|
|
||||||
is_expected.to eq('https://abcdef1234567890@gitlab.example.com:4567/api/v4/error_tracking/collector/778')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,436 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Gitlab::ErrorTracking::ErrorRepository::OpenApiStrategy do
|
||||||
|
include AfterNextHelpers
|
||||||
|
|
||||||
|
let(:project) { build_stubbed(:project) }
|
||||||
|
let(:api_exception) { ErrorTrackingOpenAPI::ApiError.new(code: 500, response_body: 'b' * 101) }
|
||||||
|
|
||||||
|
subject(:repository) { Gitlab::ErrorTracking::ErrorRepository.build(project) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Disabled in spec_helper by default thus we need to enable it here.
|
||||||
|
stub_feature_flags(use_click_house_database_for_error_tracking: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'exception logging' do
|
||||||
|
it 'logs error' do
|
||||||
|
expect(Gitlab::AppLogger).to receive(:error).with(
|
||||||
|
'open_api.http_code' => api_exception.code,
|
||||||
|
'open_api.response_body' => api_exception.response_body.truncate(100)
|
||||||
|
)
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'no logging' do
|
||||||
|
it 'does not log anything' do
|
||||||
|
expect(Gitlab::AppLogger).not_to receive(:debug)
|
||||||
|
expect(Gitlab::AppLogger).not_to receive(:info)
|
||||||
|
expect(Gitlab::AppLogger).not_to receive(:error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#report_error' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
name: 'anything',
|
||||||
|
description: 'anything',
|
||||||
|
actor: 'anything',
|
||||||
|
platform: 'anything',
|
||||||
|
environment: 'anything',
|
||||||
|
level: 'anything',
|
||||||
|
occurred_at: Time.zone.now,
|
||||||
|
payload: {}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { repository.report_error(**params) }
|
||||||
|
|
||||||
|
it 'is not implemented' do
|
||||||
|
expect { subject }.to raise_error(NotImplementedError, 'Use ingestion endpoint')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#find_error' do
|
||||||
|
let(:error) { build(:error_tracking_open_api_error, project_id: project.id) }
|
||||||
|
|
||||||
|
subject { repository.find_error(error.fingerprint) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api|
|
||||||
|
allow(open_api).to receive(:get_error).with(project.id, error.fingerprint)
|
||||||
|
.and_return(error)
|
||||||
|
|
||||||
|
allow(open_api).to receive(:list_events)
|
||||||
|
.with(project.id, error.fingerprint, sort: 'occurred_at_asc', limit: 1)
|
||||||
|
.and_return(list_events_asc)
|
||||||
|
|
||||||
|
allow(open_api).to receive(:list_events)
|
||||||
|
.with(project.id, error.fingerprint, sort: 'occurred_at_desc', limit: 1)
|
||||||
|
.and_return(list_events_desc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when request succeeds' do
|
||||||
|
context 'without events returned' do
|
||||||
|
let(:list_events_asc) { [] }
|
||||||
|
let(:list_events_desc) { [] }
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it 'returns detailed error' do
|
||||||
|
is_expected.to have_attributes(
|
||||||
|
id: error.fingerprint.to_s,
|
||||||
|
title: error.name,
|
||||||
|
message: error.description,
|
||||||
|
culprit: error.actor,
|
||||||
|
first_seen: error.first_seen_at.to_s,
|
||||||
|
last_seen: error.last_seen_at.to_s,
|
||||||
|
count: error.event_count,
|
||||||
|
user_count: error.approximated_user_count,
|
||||||
|
project_id: error.project_id,
|
||||||
|
status: error.status,
|
||||||
|
tags: { level: nil, logger: nil },
|
||||||
|
external_url: "http://localhost/#{project.full_path}/-/error_tracking/#{error.fingerprint}/details",
|
||||||
|
external_base_url: "http://localhost/#{project.full_path}",
|
||||||
|
integrated: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns no first and last release version' do
|
||||||
|
is_expected.to have_attributes(
|
||||||
|
first_release_version: nil,
|
||||||
|
last_release_version: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with events returned' do
|
||||||
|
let(:first_event) { build(:error_tracking_open_api_error_event, project_id: project.id) }
|
||||||
|
let(:first_release) { parse_json(first_event.payload).fetch('release') }
|
||||||
|
let(:last_event) { build(:error_tracking_open_api_error_event, :golang, project_id: project.id) }
|
||||||
|
let(:last_release) { parse_json(last_event.payload).fetch('release') }
|
||||||
|
|
||||||
|
let(:list_events_asc) { [first_event] }
|
||||||
|
let(:list_events_desc) { [last_event] }
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it 'returns first and last release version' do
|
||||||
|
expect(first_release).to be_present
|
||||||
|
expect(last_release).to be_present
|
||||||
|
|
||||||
|
is_expected.to have_attributes(
|
||||||
|
first_release_version: first_release,
|
||||||
|
last_release_version: last_release
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_json(content)
|
||||||
|
Gitlab::Json.parse(content)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when request fails' do
|
||||||
|
before do
|
||||||
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:get_error)
|
||||||
|
.with(project.id, error.fingerprint)
|
||||||
|
.and_raise(api_exception)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'exception logging'
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#list_errors' do
|
||||||
|
let(:errors) { [] }
|
||||||
|
let(:response_with_info) { [errors, 200, headers] }
|
||||||
|
let(:result_errors) { result.first }
|
||||||
|
let(:result_pagination) { result.last }
|
||||||
|
|
||||||
|
let(:headers) do
|
||||||
|
{
|
||||||
|
'link' => [
|
||||||
|
'<url?cursor=next_cursor¶m>; rel="next"',
|
||||||
|
'<url?cursor=prev_cursor¶m>; rel="prev"'
|
||||||
|
].join(', ')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:result) { repository.list_errors(**params) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info)
|
||||||
|
.with(project.id, kind_of(Hash))
|
||||||
|
.and_return(response_with_info)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with errors' do
|
||||||
|
let(:limit) { 3 }
|
||||||
|
let(:params) { { limit: limit } }
|
||||||
|
let(:errors_size) { limit }
|
||||||
|
let(:errors) { build_list(:error_tracking_open_api_error, errors_size, project_id: project.id) }
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it 'maps errors to models' do
|
||||||
|
# All errors are identical
|
||||||
|
error = errors.first
|
||||||
|
|
||||||
|
expect(result_errors).to all(
|
||||||
|
have_attributes(
|
||||||
|
id: error.fingerprint.to_s,
|
||||||
|
title: error.name,
|
||||||
|
message: error.description,
|
||||||
|
culprit: error.actor,
|
||||||
|
first_seen: error.first_seen_at,
|
||||||
|
last_seen: error.last_seen_at,
|
||||||
|
status: error.status,
|
||||||
|
count: error.event_count,
|
||||||
|
user_count: error.approximated_user_count
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when n errors are returned' do
|
||||||
|
let(:errors_size) { limit }
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it 'returns the amount of errors' do
|
||||||
|
expect(result_errors.size).to eq(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'cursor links are preserved' do
|
||||||
|
expect(result_pagination).to have_attributes(
|
||||||
|
prev: 'prev_cursor',
|
||||||
|
next: 'next_cursor'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when less errors than requested are returned' do
|
||||||
|
let(:errors_size) { limit - 1 }
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it 'returns the amount of errors' do
|
||||||
|
expect(result_errors.size).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'cursor link for next is removed' do
|
||||||
|
expect(result_pagination).to have_attributes(
|
||||||
|
prev: 'prev_cursor',
|
||||||
|
next: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with params' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
filters: { status: 'resolved', something: 'different' },
|
||||||
|
query: 'search term',
|
||||||
|
sort: 'first_seen',
|
||||||
|
limit: 2,
|
||||||
|
cursor: 'abc'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it 'passes provided params to client' do
|
||||||
|
passed_params = {
|
||||||
|
sort: 'first_seen_desc',
|
||||||
|
status: 'resolved',
|
||||||
|
query: 'search term',
|
||||||
|
cursor: 'abc',
|
||||||
|
limit: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info)
|
||||||
|
.with(project.id, passed_params)
|
||||||
|
.and_return(response_with_info)
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without explicit params' do
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it 'passes default params to client' do
|
||||||
|
passed_params = {
|
||||||
|
sort: 'last_seen_desc',
|
||||||
|
limit: 20,
|
||||||
|
cursor: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info)
|
||||||
|
.with(project.id, passed_params)
|
||||||
|
.and_return(response_with_info)
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when request fails' do
|
||||||
|
let(:params) { {} }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_errors_with_http_info)
|
||||||
|
.with(project.id, kind_of(Hash))
|
||||||
|
.and_raise(api_exception)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'exception logging'
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect(result_errors).to eq([])
|
||||||
|
expect(result_pagination).to have_attributes(
|
||||||
|
next: nil,
|
||||||
|
prev: nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#last_event_for' do
|
||||||
|
let(:params) { { sort: 'occurred_at_desc', limit: 1 } }
|
||||||
|
let(:event) { build(:error_tracking_open_api_error_event, project_id: project.id) }
|
||||||
|
let(:error) { build(:error_tracking_open_api_error, project_id: project.id, fingerprint: event.fingerprint) }
|
||||||
|
|
||||||
|
subject { repository.last_event_for(error.fingerprint) }
|
||||||
|
|
||||||
|
context 'when both event and error is returned' do
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api|
|
||||||
|
allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params)
|
||||||
|
.and_return([event])
|
||||||
|
|
||||||
|
allow(open_api).to receive(:get_error).with(project.id, error.fingerprint)
|
||||||
|
.and_return(error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it 'returns mapped error event' do
|
||||||
|
is_expected.to have_attributes(
|
||||||
|
issue_id: event.fingerprint.to_s,
|
||||||
|
date_received: error.last_seen_at,
|
||||||
|
stack_trace_entries: kind_of(Array)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when event is not returned' do
|
||||||
|
before do
|
||||||
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_events)
|
||||||
|
.with(project.id, event.fingerprint, params)
|
||||||
|
.and_return([])
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when list_events request fails' do
|
||||||
|
before do
|
||||||
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:list_events)
|
||||||
|
.with(project.id, event.fingerprint, params)
|
||||||
|
.and_raise(api_exception)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'exception logging'
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when error is not returned' do
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api|
|
||||||
|
allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params)
|
||||||
|
.and_return([event])
|
||||||
|
|
||||||
|
allow(open_api).to receive(:get_error).with(project.id, error.fingerprint)
|
||||||
|
.and_return(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when get_error request fails' do
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(ErrorTrackingOpenAPI::ErrorsApi) do |open_api|
|
||||||
|
allow(open_api).to receive(:list_events).with(project.id, error.fingerprint, params)
|
||||||
|
.and_return([event])
|
||||||
|
|
||||||
|
allow(open_api).to receive(:get_error).with(project.id, error.fingerprint)
|
||||||
|
.and_raise(api_exception)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'exception logging'
|
||||||
|
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update_error' do
|
||||||
|
let(:error) { build(:error_tracking_open_api_error, project_id: project.id) }
|
||||||
|
let(:update_params) { { status: 'resolved' } }
|
||||||
|
let(:passed_body) { ErrorTrackingOpenAPI::ErrorUpdatePayload.new(update_params) }
|
||||||
|
|
||||||
|
subject { repository.update_error(error.fingerprint, **update_params) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:update_error)
|
||||||
|
.with(project.id, error.fingerprint, passed_body)
|
||||||
|
.and_return(:anything)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when update succeeds' do
|
||||||
|
include_examples 'no logging'
|
||||||
|
|
||||||
|
it { is_expected.to eq(true) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when update fails' do
|
||||||
|
before do
|
||||||
|
allow_next(ErrorTrackingOpenAPI::ErrorsApi).to receive(:update_error)
|
||||||
|
.with(project.id, error.fingerprint, passed_body)
|
||||||
|
.and_raise(api_exception)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'exception logging'
|
||||||
|
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#dsn_url' do
|
||||||
|
let(:public_key) { 'abc' }
|
||||||
|
let(:config) { ErrorTrackingOpenAPI::Configuration.default }
|
||||||
|
|
||||||
|
subject { repository.dsn_url(public_key) }
|
||||||
|
|
||||||
|
it do
|
||||||
|
is_expected
|
||||||
|
.to eq("#{config.scheme}://#{public_key}@#{config.host}/errortracking/api/v1/projects/api/#{project.id}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -63,6 +63,11 @@ RSpec.describe API::Internal::Base do
|
||||||
post api('/internal/error_tracking/allowed'), params: params, headers: headers
|
post api('/internal/error_tracking/allowed'), params: params, headers: headers
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Because the feature flag is disabled in specs we have to enable it explicitly.
|
||||||
|
stub_feature_flags(use_click_house_database_for_error_tracking: true)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when the secret header is missing' do
|
context 'when the secret header is missing' do
|
||||||
let(:headers) { {} }
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
|
|
@ -289,6 +289,10 @@ RSpec.configure do |config|
|
||||||
stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: false)
|
stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: false)
|
||||||
stub_feature_flags(ci_queueing_disaster_recovery_disable_quota: false)
|
stub_feature_flags(ci_queueing_disaster_recovery_disable_quota: false)
|
||||||
|
|
||||||
|
# It's disabled in specs because we don't support certain features which
|
||||||
|
# cause spec failures.
|
||||||
|
stub_feature_flags(use_click_house_database_for_error_tracking: false)
|
||||||
|
|
||||||
enable_rugged = example.metadata[:enable_rugged].present?
|
enable_rugged = example.metadata[:enable_rugged].present?
|
||||||
|
|
||||||
# Disable Rugged features by default
|
# Disable Rugged features by default
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by `rake gems:error_tracking_open_api:generate` at 2022-07-01
|
# Generated by `rake gems:error_tracking_open_api:generate` on 2022-07-02
|
||||||
|
|
||||||
See https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/rake_tasks.md#update-openapi-client-for-error-tracking-feature
|
See https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/rake_tasks.md#update-openapi-client-for-error-tracking-feature
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ Gem::Specification.new do |s|
|
||||||
|
|
||||||
s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'
|
s.add_development_dependency 'rspec', '~> 3.6', '>= 3.6.0'
|
||||||
|
|
||||||
s.files = `find *`.split("\n").uniq.sort.select { |f| !f.empty? }
|
s.files = Dir.glob("lib/**/*")
|
||||||
s.test_files = `find spec/*`.split("\n")
|
s.test_files = []
|
||||||
s.executables = []
|
s.executables = []
|
||||||
s.require_paths = ["lib"]
|
s.require_paths = ["lib"]
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue