Skip to content
Snippets Groups Projects
Commit d7db6236 authored by Robert Speicher's avatar Robert Speicher
Browse files

Merge branch '11-7-stable-prepare-rc7' into '11-7-stable'

Prepare 11.7.0-rc7 release

See merge request gitlab-org/gitlab-ce!24442
parents bbf5e923 6d2c02a0
No related branches found
No related tags found
No related merge requests found
Showing
with 390 additions and 6 deletions
1.12.0
1.12.1
<script>
import { mapActions, mapState } from 'vuex';
import { GlEmptyState, GlButton, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
export default {
fields: [
{ key: 'error', label: __('Open errors') },
{ key: 'events', label: __('Events') },
{ key: 'users', label: __('Users') },
{ key: 'lastSeen', label: __('Last seen') },
],
components: {
GlEmptyState,
GlButton,
GlLink,
GlLoadingIcon,
GlTable,
Icon,
TimeAgo,
},
props: {
indexPath: {
type: String,
required: true,
},
enableErrorTrackingLink: {
type: String,
required: true,
},
errorTrackingEnabled: {
type: Boolean,
required: true,
},
illustrationPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['errors', 'externalUrl', 'loading']),
},
created() {
if (this.errorTrackingEnabled) {
this.startPolling(this.indexPath);
}
},
methods: {
...mapActions(['startPolling']),
},
};
</script>
<template>
<div>
<div v-if="errorTrackingEnabled">
<div v-if="loading" class="py-3"><gl-loading-icon :size="3" /></div>
<div v-else>
<div class="d-flex justify-content-end">
<gl-button class="my-3 ml-auto" variant="primary" :href="externalUrl" target="_blank"
>View in Sentry <icon name="external-link" />
</gl-button>
</div>
<gl-table
:items="errors"
:fields="$options.fields"
:show-empty="true"
:empty-text="__('No errors to display')"
>
<template slot="HEAD_events" slot-scope="data">
<div class="text-right">{{ data.label }}</div>
</template>
<template slot="HEAD_users" slot-scope="data">
<div class="text-right">{{ data.label }}</div>
</template>
<template slot="error" slot-scope="errors">
<div class="d-flex flex-column">
<div class="d-flex">
<gl-link :href="errors.item.externalUrl" class="d-flex text-dark" target="_blank">
<strong>{{ errors.item.title.trim() }}</strong>
<icon name="external-link" class="ml-1" />
</gl-link>
<span class="text-secondary ml-2">{{ errors.item.culprit }}</span>
</div>
{{ errors.item.message || __('No details available') }}
</div>
</template>
<template slot="events" slot-scope="errors">
<div class="text-right">{{ errors.item.count }}</div>
</template>
<template slot="users" slot-scope="errors">
<div class="text-right">{{ errors.item.userCount }}</div>
</template>
<template slot="lastSeen" slot-scope="errors">
<div class="d-flex align-items-center">
<icon name="calendar" css-classes="text-secondary mr-1" />
<time-ago :time="errors.item.lastSeen" class="text-secondary" />
</div>
</template>
</gl-table>
</div>
</div>
<div v-else>
<gl-empty-state
:title="__('Get started with error tracking')"
:description="__('Monitor your errors by integrating with Sentry')"
:primary-button-text="__('Enable error tracking')"
:primary-button-link="enableErrorTrackingLink"
:svg-path="illustrationPath"
/>
</div>
</div>
</template>
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import store from './store';
import ErrorTrackingList from './components/error_tracking_list.vue';
export default () => {
if (!gon.features.errorTracking) {
return;
}
// eslint-disable-next-line no-new
new Vue({
el: '#js-error_tracking',
components: {
ErrorTrackingList,
},
store,
render(createElement) {
const domEl = document.querySelector(this.$options.el);
const { indexPath, enableErrorTrackingLink, illustrationPath } = domEl.dataset;
let { errorTrackingEnabled } = domEl.dataset;
errorTrackingEnabled = parseBoolean(errorTrackingEnabled);
return createElement('error-tracking-list', {
props: {
indexPath,
enableErrorTrackingLink,
errorTrackingEnabled,
illustrationPath,
},
});
},
});
};
import axios from '~/lib/utils/axios_utils';
export default {
getErrorList({ endpoint }) {
return axios.get(endpoint);
},
};
import Service from '../services';
import * as types from './mutation_types';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
let eTagPoll;
export function startPolling({ commit }, endpoint) {
eTagPoll = new Poll({
resource: Service,
method: 'getErrorList',
data: { endpoint },
successCallback: ({ data }) => {
if (!data) {
return;
}
commit(types.SET_ERRORS, data.errors);
commit(types.SET_EXTERNAL_URL, data.external_url);
commit(types.SET_LOADING, false);
},
errorCallback: () => {
commit(types.SET_LOADING, false);
createFlash(__('Failed to load errors from Sentry'));
},
});
eTagPoll.makeRequest();
}
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
state: {
errors: [],
externalUrl: '',
loading: true,
},
actions,
mutations,
});
export default createStore();
export const SET_ERRORS = 'SET_ERRORS';
export const SET_EXTERNAL_URL = 'SET_EXTERNAL_URL';
export const SET_LOADING = 'SET_LOADING';
import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default {
[types.SET_ERRORS](state, data) {
state.errors = convertObjectPropsToCamelCase(data, { deep: true });
},
[types.SET_EXTERNAL_URL](state, url) {
state.externalUrl = url;
},
[types.SET_LOADING](state, loading) {
state.loading = loading;
},
};
import ErrorTracking from '~/error_tracking';
document.addEventListener('DOMContentLoaded', () => {
ErrorTracking();
});
# frozen_string_literal: true
class Projects::ErrorTrackingController < Projects::ApplicationController
before_action :check_feature_flag!
before_action :authorize_read_sentry_issue!
before_action :push_feature_flag_to_frontend
POLLING_INTERVAL = 10_000
def index
respond_to do |format|
format.html
format.json do
set_polling_interval
render_index_json
end
end
end
private
def render_index_json
service = ErrorTracking::ListIssuesService.new(project, current_user)
result = service.execute
unless result[:status] == :success
return render json: { message: result[:message] },
status: result[:http_status] || :bad_request
end
render json: {
errors: serialize_errors(result[:issues]),
external_url: service.external_url
}
end
def set_polling_interval
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
end
def serialize_errors(errors)
ErrorTracking::ErrorSerializer
.new(project: project, user: current_user)
.represent(errors)
end
def check_feature_flag!
render_404 unless Feature.enabled?(:error_tracking, project)
end
def push_feature_flag_to_frontend
push_frontend_feature_flag(:error_tracking, current_user)
end
end
Loading
Loading
@@ -178,8 +178,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
 
def import_csv
return render_404 unless Feature.enabled?(:issues_import_csv)
if uploader = UploadService.new(project, params[:file]).execute
ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id)
 
Loading
Loading
# frozen_string_literal: true
module Projects::ErrorTrackingHelper
def error_tracking_data(project)
error_tracking_enabled = !!project.error_tracking_setting&.enabled?
{
'index-path' => project_error_tracking_index_path(project,
format: :json),
'enable-error-tracking-link' => project_settings_operations_path(project),
'error-tracking-enabled' => error_tracking_enabled.to_s,
'illustration-path' => image_path('illustrations/cluster_popover.svg')
}
end
end
Loading
Loading
@@ -335,6 +335,7 @@ module ProjectsHelper
builds: :read_build,
clusters: :read_cluster,
serverless: :read_cluster,
error_tracking: :read_sentry_issue,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
Loading
Loading
@@ -579,6 +580,7 @@ module ProjectsHelper
environments
clusters
functions
error_tracking
user
gcp
]
Loading
Loading
Loading
Loading
@@ -224,8 +224,15 @@ module Ci
 
before_transition any => [:failed] do |build|
next unless build.project
next unless build.deployment
 
build.deployment&.drop
begin
build.deployment.drop!
rescue => e
Gitlab::Sentry.track_exception(e, extra: { build_id: build.id })
end
true
end
 
after_transition any => [:failed] do |build|
Loading
Loading
Loading
Loading
@@ -2,13 +2,58 @@
 
module ErrorTracking
class ProjectErrorTrackingSetting < ActiveRecord::Base
include ReactiveCaching
self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] }
belongs_to :project
 
validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true }
 
validate :validate_api_url_path
attr_encrypted :token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-gcm'
after_save :clear_reactive_cache!
def sentry_client
Sentry::Client.new(api_url, token)
end
def sentry_external_url
self.class.extract_sentry_external_url(api_url)
end
def list_sentry_issues(opts = {})
with_reactive_cache('list_issues', opts.stringify_keys) do |result|
{ issues: result }
end
end
def calculate_reactive_cache(request, opts)
case request
when 'list_issues'
sentry_client.list_issues(**opts.symbolize_keys)
end
end
# http://HOST/api/0/projects/ORG/PROJECT
# ->
# http://HOST/ORG/PROJECT
def self.extract_sentry_external_url(url)
url.sub('api/0/projects/', '')
end
private
def validate_api_url_path
unless URI(api_url).path.starts_with?('/api/0/projects')
errors.add(:api_url, 'path needs to start with /api/0/projects')
end
rescue URI::InvalidURIError
end
end
end
Loading
Loading
@@ -61,7 +61,10 @@ class RemoteMirror < ActiveRecord::Base
 
timestamp = Time.now
remote_mirror.update!(
last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
last_update_at: timestamp,
last_successful_update_at: timestamp,
last_error: nil,
error_notification_sent: false
)
end
 
Loading
Loading
@@ -179,6 +182,10 @@ class RemoteMirror < ActiveRecord::Base
project.repository.add_remote(remote_name, remote_url)
end
 
def after_sent_notification
update_column(:error_notification_sent, true)
end
private
 
def store_credentials
Loading
Loading
@@ -221,7 +228,8 @@ class RemoteMirror < ActiveRecord::Base
last_error: nil,
last_update_at: nil,
last_successful_update_at: nil,
update_status: 'finished'
update_status: 'finished',
error_notification_sent: false
)
end
 
Loading
Loading
Loading
Loading
@@ -52,6 +52,11 @@ class SshHostKey
@compare_host_keys = compare_host_keys
end
 
# Needed for reactive caching
def self.primary_key
'id'
end
def id
[project.id, url].join(':')
end
Loading
Loading
Loading
Loading
@@ -200,6 +200,7 @@ class ProjectPolicy < BasePolicy
enable :read_environment
enable :read_deployment
enable :read_merge_request
enable :read_sentry_issue
end
 
# We define `:public_user_access` separately because there are cases in gitlab-ee
Loading
Loading
# frozen_string_literal: true
module ErrorTracking
class ErrorEntity < Grape::Entity
expose :id, :title, :type, :user_count, :count,
:first_seen, :last_seen, :message, :culprit,
:external_url, :project_id, :project_name, :project_slug,
:short_id, :status, :frequency
end
end
# frozen_string_literal: true
module ErrorTracking
class ErrorSerializer < BaseSerializer
entity ErrorEntity
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment