Commit 5a521254 authored by Simon Knox's avatar Simon Knox Committed by Clement Ho

List Sentry Errors in GitLab - Frontend

parent 6d6c2e95
<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
...@@ -335,6 +335,7 @@ module ProjectsHelper ...@@ -335,6 +335,7 @@ module ProjectsHelper
builds: :read_build, builds: :read_build,
clusters: :read_cluster, clusters: :read_cluster,
serverless: :read_cluster, serverless: :read_cluster,
error_tracking: :read_sentry_issue,
labels: :read_label, labels: :read_label,
issues: :read_issue, issues: :read_issue,
project_members: :read_project_member, project_members: :read_project_member,
...@@ -579,6 +580,7 @@ module ProjectsHelper ...@@ -579,6 +580,7 @@ module ProjectsHelper
environments environments
clusters clusters
functions functions
error_tracking
user user
gcp gcp
] ]
......
...@@ -227,6 +227,12 @@ ...@@ -227,6 +227,12 @@
%span %span
= _('Environments') = _('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 - if project_nav_tab? :serverless
= nav_link(controller: :functions) do = nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do = link_to project_serverless_functions_path(@project), title: _('Serverless') do
......
- page_title _('Errors') - 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
...@@ -2749,6 +2749,9 @@ msgstr "" ...@@ -2749,6 +2749,9 @@ msgstr ""
msgid "Enable and configure Prometheus metrics." msgid "Enable and configure Prometheus metrics."
msgstr "" msgstr ""
msgid "Enable error tracking"
msgstr ""
msgid "Enable for this project" msgid "Enable for this project"
msgstr "" msgstr ""
...@@ -2980,6 +2983,9 @@ msgstr "" ...@@ -2980,6 +2983,9 @@ msgstr ""
msgid "EventFilterBy|Filter by team" msgid "EventFilterBy|Filter by team"
msgstr "" msgstr ""
msgid "Events"
msgstr ""
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again." msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
msgstr "" msgstr ""
...@@ -3067,6 +3073,9 @@ msgstr "" ...@@ -3067,6 +3073,9 @@ msgstr ""
msgid "Failed to load emoji list." msgid "Failed to load emoji list."
msgstr "" msgstr ""
msgid "Failed to load errors from Sentry"
msgstr ""
msgid "Failed to remove issue from board, please try again." msgid "Failed to remove issue from board, please try again."
msgstr "" msgstr ""
...@@ -3250,6 +3259,9 @@ msgstr "" ...@@ -3250,6 +3259,9 @@ msgstr ""
msgid "Geo" msgid "Geo"
msgstr "" msgstr ""
msgid "Get started with error tracking"
msgstr ""
msgid "Getting started with releases" msgid "Getting started with releases"
msgstr "" msgstr ""
...@@ -3956,6 +3968,9 @@ msgstr "" ...@@ -3956,6 +3968,9 @@ msgstr ""
msgid "Last reply by" msgid "Last reply by"
msgstr "" msgstr ""
msgid "Last seen"
msgstr ""
msgid "Last update" msgid "Last update"
msgstr "" msgstr ""
...@@ -4345,6 +4360,9 @@ msgstr "" ...@@ -4345,6 +4360,9 @@ msgstr ""
msgid "Modal|Close" msgid "Modal|Close"
msgstr "" msgstr ""
msgid "Monitor your errors by integrating with Sentry"
msgstr ""
msgid "Monitoring" msgid "Monitoring"
msgstr "" msgstr ""
...@@ -4509,9 +4527,15 @@ msgstr "" ...@@ -4509,9 +4527,15 @@ msgstr ""
msgid "No contributions were found" msgid "No contributions were found"
msgstr "" msgstr ""
msgid "No details available"
msgstr ""
msgid "No due date" msgid "No due date"
msgstr "" msgstr ""
msgid "No errors to display"
msgstr ""
msgid "No estimate or time spent" msgid "No estimate or time spent"
msgstr "" msgstr ""
...@@ -4730,6 +4754,9 @@ msgstr "" ...@@ -4730,6 +4754,9 @@ msgstr ""
msgid "Open comment type dropdown" msgid "Open comment type dropdown"
msgstr "" msgstr ""
msgid "Open errors"
msgstr ""
msgid "Open in Xcode" msgid "Open in Xcode"
msgstr "" msgstr ""
......
# 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,
},
],
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment