Skip to content
Snippets Groups Projects
Unverified Commit 8885acdf authored by Clement Ho's avatar Clement Ho Committed by Peter Leitzen
Browse files

Merge branch 'error_tracking_feature_flag_fe' into 'master'

List Sentry Errors in GitLab - Frontend

Closes #55178

See merge request gitlab-org/gitlab-ce!23770
parent df1e0638
No related branches found
No related tags found
No related merge requests found
Showing
with 483 additions and 0 deletions
<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
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
@@ -227,6 +227,12 @@
%span
= _('Environments')
 
- if project_nav_tab?(:error_tracking) && Feature.enabled?(:error_tracking, @project)
= nav_link(controller: :error_tracking) do
= link_to project_error_tracking_index_path(@project), title: _('Error Tracking'), class: 'shortcuts-tracking qa-operations-tracking-link' do
%span
= _('Error Tracking')
- if project_nav_tab? :serverless
= nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
Loading
Loading
- page_title _('Errors')
#js-error_tracking{ data: error_tracking_data(@project) }
---
title: Display a list of Sentry Issues in GitLab
merge_request: 23770
author:
type: added
Loading
Loading
@@ -2746,6 +2746,9 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
 
msgid "Enable error tracking"
msgstr ""
msgid "Enable for this project"
msgstr ""
 
Loading
Loading
@@ -2977,6 +2980,9 @@ msgstr ""
msgid "EventFilterBy|Filter by team"
msgstr ""
 
msgid "Events"
msgstr ""
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
msgstr ""
 
Loading
Loading
@@ -3064,6 +3070,9 @@ msgstr ""
msgid "Failed to load emoji list."
msgstr ""
 
msgid "Failed to load errors from Sentry"
msgstr ""
msgid "Failed to remove issue from board, please try again."
msgstr ""
 
Loading
Loading
@@ -3247,6 +3256,9 @@ msgstr ""
msgid "Geo"
msgstr ""
 
msgid "Get started with error tracking"
msgstr ""
msgid "Getting started with releases"
msgstr ""
 
Loading
Loading
@@ -3953,6 +3965,9 @@ msgstr ""
msgid "Last reply by"
msgstr ""
 
msgid "Last seen"
msgstr ""
msgid "Last update"
msgstr ""
 
Loading
Loading
@@ -4342,6 +4357,9 @@ msgstr ""
msgid "Modal|Close"
msgstr ""
 
msgid "Monitor your errors by integrating with Sentry"
msgstr ""
msgid "Monitoring"
msgstr ""
 
Loading
Loading
@@ -4506,9 +4524,15 @@ msgstr ""
msgid "No contributions were found"
msgstr ""
 
msgid "No details available"
msgstr ""
msgid "No due date"
msgstr ""
 
msgid "No errors to display"
msgstr ""
msgid "No estimate or time spent"
msgstr ""
 
Loading
Loading
@@ -4727,6 +4751,9 @@ msgstr ""
msgid "Open comment type dropdown"
msgstr ""
 
msgid "Open errors"
msgstr ""
msgid "Open in Xcode"
msgstr ""
 
Loading
Loading
# frozen_string_literal: true
require 'spec_helper'
describe Projects::ErrorTrackingHelper do
include Gitlab::Routing.url_helpers
set(:project) { create(:project) }
describe '#error_tracking_data' do
let(:setting_path) { project_settings_operations_path(project) }
let(:index_path) do
project_error_tracking_index_path(project, format: :json)
end
context 'without error_tracking_setting' do
it 'returns frontend configuration' do
expect(error_tracking_data(project)).to eq(
'index-path' => index_path,
'enable-error-tracking-link' => setting_path,
'error-tracking-enabled' => 'false',
"illustration-path" => "/images/illustrations/cluster_popover.svg"
)
end
end
context 'with error_tracking_setting' do
let(:error_tracking_setting) do
create(:project_error_tracking_setting, project: project)
end
context 'when enabled' do
before do
error_tracking_setting.update!(enabled: true)
end
it 'show error tracking enabled' do
expect(error_tracking_data(project)).to include(
'error-tracking-enabled' => 'true'
)
end
end
context 'when disabled' do
before do
error_tracking_setting.update!(enabled: false)
end
it 'show error tracking not enabled' do
expect(error_tracking_data(project)).to include(
'error-tracking-enabled' => 'false'
)
end
end
end
end
end
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import { GlButton, GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ErrorTrackingList', () => {
let store;
let wrapper;
function mountComponent({ errorTrackingEnabled = true } = {}) {
wrapper = shallowMount(ErrorTrackingList, {
localVue,
store,
propsData: {
indexPath: '/path',
enableErrorTrackingLink: '/link',
errorTrackingEnabled,
illustrationPath: 'illustration/path',
},
});
}
beforeEach(() => {
const actions = {
getErrorList: () => {},
};
const state = {
errors: [],
loading: true,
};
store = new Vuex.Store({
actions,
state,
});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('loading', () => {
beforeEach(() => {
mountComponent();
});
it('shows spinner', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy();
expect(wrapper.find(GlTable).exists()).toBeFalsy();
expect(wrapper.find(GlButton).exists()).toBeFalsy();
});
});
describe('results', () => {
beforeEach(() => {
store.state.loading = false;
mountComponent();
});
it('shows table', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
expect(wrapper.find(GlTable).exists()).toBeTruthy();
expect(wrapper.find(GlButton).exists()).toBeTruthy();
});
});
describe('no results', () => {
beforeEach(() => {
store.state.loading = false;
mountComponent();
});
it('shows empty table', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
expect(wrapper.find(GlTable).exists()).toBeTruthy();
expect(wrapper.find(GlButton).exists()).toBeTruthy();
});
});
describe('error tracking feature disabled', () => {
beforeEach(() => {
mountComponent({ errorTrackingEnabled: false });
});
it('shows empty state', () => {
expect(wrapper.find(GlEmptyState).exists()).toBeTruthy();
expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy();
expect(wrapper.find(GlTable).exists()).toBeFalsy();
expect(wrapper.find(GlButton).exists()).toBeFalsy();
});
});
});
import mutations from '~/error_tracking/store/mutations';
import * as types from '~/error_tracking/store/mutation_types';
describe('Error tracking mutations', () => {
describe('SET_ERRORS', () => {
let state;
beforeEach(() => {
state = { errors: [] };
});
it('camelizes response', () => {
const errors = [
{
title: 'the title',
external_url: 'localhost:3456',
count: 100,
userCount: 10,
},
];
mutations[types.SET_ERRORS](state, errors);
expect(state).toEqual({
errors: [
{
title: 'the title',
externalUrl: 'localhost:3456',
count: 100,
userCount: 10,
},
],
});
});
});
});
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