summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClement Ho <clemmakesapps@gmail.com>2019-01-11 22:52:26 +0000
committerClement Ho <clemmakesapps@gmail.com>2019-01-11 22:52:26 +0000
commit8a7f40b1e2719fec7d666cc1b7193a705c7b7faf (patch)
tree9c92bbb6f3c619c69dd225ec32350f59883bba32
parent6d6c2e95dd8c813a9646c6289589957749bd4b0c (diff)
parent5a5212542aeac8c3b2e05e853e2e709c7141a999 (diff)
downloadgitlab-ce-8a7f40b1e2719fec7d666cc1b7193a705c7b7faf.tar.gz
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
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue118
-rw-r--r--app/assets/javascripts/error_tracking/index.js35
-rw-r--r--app/assets/javascripts/error_tracking/services/index.js7
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js31
-rw-r--r--app/assets/javascripts/error_tracking/store/index.js19
-rw-r--r--app/assets/javascripts/error_tracking/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/error_tracking/store/mutations.js14
-rw-r--r--app/assets/javascripts/pages/projects/error_tracking/index.js5
-rw-r--r--app/helpers/projects/error_tracking_helper.rb15
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-rw-r--r--app/views/projects/error_tracking/index.html.haml2
-rw-r--r--changelogs/unreleased/error_tracking_feature_flag_fe.yml5
-rw-r--r--locale/gitlab.pot27
-rw-r--r--spec/helpers/projects/error_tracking_helper_spec.rb58
-rw-r--r--spec/javascripts/error_tracking/components/error_tracking_list_spec.js100
-rw-r--r--spec/javascripts/error_tracking/store/mutation_spec.js36
17 files changed, 483 insertions, 0 deletions
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
new file mode 100644
index 00000000000..6981afe1ead
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -0,0 +1,118 @@
+<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>
diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/index.js
new file mode 100644
index 00000000000..808ae2c9a41
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/index.js
@@ -0,0 +1,35 @@
+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,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/error_tracking/services/index.js b/app/assets/javascripts/error_tracking/services/index.js
new file mode 100644
index 00000000000..ab89521dc46
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/services/index.js
@@ -0,0 +1,7 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ getErrorList({ endpoint }) {
+ return axios.get(endpoint);
+ },
+};
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js
new file mode 100644
index 00000000000..2e192c958ba
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/actions.js
@@ -0,0 +1,31 @@
+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 () => {};
diff --git a/app/assets/javascripts/error_tracking/store/index.js b/app/assets/javascripts/error_tracking/store/index.js
new file mode 100644
index 00000000000..3136682fb64
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/index.js
@@ -0,0 +1,19 @@
+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();
diff --git a/app/assets/javascripts/error_tracking/store/mutation_types.js b/app/assets/javascripts/error_tracking/store/mutation_types.js
new file mode 100644
index 00000000000..f9d77a6b08e
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/mutation_types.js
@@ -0,0 +1,3 @@
+export const SET_ERRORS = 'SET_ERRORS';
+export const SET_EXTERNAL_URL = 'SET_EXTERNAL_URL';
+export const SET_LOADING = 'SET_LOADING';
diff --git a/app/assets/javascripts/error_tracking/store/mutations.js b/app/assets/javascripts/error_tracking/store/mutations.js
new file mode 100644
index 00000000000..e4bd81db9c9
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/mutations.js
@@ -0,0 +1,14 @@
+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;
+ },
+};
diff --git a/app/assets/javascripts/pages/projects/error_tracking/index.js b/app/assets/javascripts/pages/projects/error_tracking/index.js
new file mode 100644
index 00000000000..5a8fe137e9a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/error_tracking/index.js
@@ -0,0 +1,5 @@
+import ErrorTracking from '~/error_tracking';
+
+document.addEventListener('DOMContentLoaded', () => {
+ ErrorTracking();
+});
diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb
new file mode 100644
index 00000000000..6daf2e21ca2
--- /dev/null
+++ b/app/helpers/projects/error_tracking_helper.rb
@@ -0,0 +1,15 @@
+# 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
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index e67c327f7f8..ebbed08f78a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -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,
@@ -579,6 +580,7 @@ module ProjectsHelper
environments
clusters
functions
+ error_tracking
user
gcp
]
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index c8fdc0112b4..d62cbc1684b 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -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
diff --git a/app/views/projects/error_tracking/index.html.haml b/app/views/projects/error_tracking/index.html.haml
index a3e0dc75f6f..bc02c5f0e5a 100644
--- a/app/views/projects/error_tracking/index.html.haml
+++ b/app/views/projects/error_tracking/index.html.haml
@@ -1 +1,3 @@
- page_title _('Errors')
+
+#js-error_tracking{ data: error_tracking_data(@project) }
diff --git a/changelogs/unreleased/error_tracking_feature_flag_fe.yml b/changelogs/unreleased/error_tracking_feature_flag_fe.yml
new file mode 100644
index 00000000000..607929eb6b8
--- /dev/null
+++ b/changelogs/unreleased/error_tracking_feature_flag_fe.yml
@@ -0,0 +1,5 @@
+---
+title: Display a list of Sentry Issues in GitLab
+merge_request: 23770
+author:
+type: added
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d3f751c7796..d3f20a4620f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2749,6 +2749,9 @@ msgstr ""
msgid "Enable and configure Prometheus metrics."
msgstr ""
+msgid "Enable error tracking"
+msgstr ""
+
msgid "Enable for this project"
msgstr ""
@@ -2980,6 +2983,9 @@ msgstr ""
msgid "EventFilterBy|Filter by team"
msgstr ""
+msgid "Events"
+msgstr ""
+
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
msgstr ""
@@ -3067,6 +3073,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 ""
@@ -3250,6 +3259,9 @@ msgstr ""
msgid "Geo"
msgstr ""
+msgid "Get started with error tracking"
+msgstr ""
+
msgid "Getting started with releases"
msgstr ""
@@ -3956,6 +3968,9 @@ msgstr ""
msgid "Last reply by"
msgstr ""
+msgid "Last seen"
+msgstr ""
+
msgid "Last update"
msgstr ""
@@ -4345,6 +4360,9 @@ msgstr ""
msgid "Modal|Close"
msgstr ""
+msgid "Monitor your errors by integrating with Sentry"
+msgstr ""
+
msgid "Monitoring"
msgstr ""
@@ -4509,9 +4527,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 ""
@@ -4730,6 +4754,9 @@ msgstr ""
msgid "Open comment type dropdown"
msgstr ""
+msgid "Open errors"
+msgstr ""
+
msgid "Open in Xcode"
msgstr ""
diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb
new file mode 100644
index 00000000000..7516a636c93
--- /dev/null
+++ b/spec/helpers/projects/error_tracking_helper_spec.rb
@@ -0,0 +1,58 @@
+# 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
diff --git a/spec/javascripts/error_tracking/components/error_tracking_list_spec.js b/spec/javascripts/error_tracking/components/error_tracking_list_spec.js
new file mode 100644
index 00000000000..08bbb390993
--- /dev/null
+++ b/spec/javascripts/error_tracking/components/error_tracking_list_spec.js
@@ -0,0 +1,100 @@
+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();
+ });
+ });
+});
diff --git a/spec/javascripts/error_tracking/store/mutation_spec.js b/spec/javascripts/error_tracking/store/mutation_spec.js
new file mode 100644
index 00000000000..8117104bdbc
--- /dev/null
+++ b/spec/javascripts/error_tracking/store/mutation_spec.js
@@ -0,0 +1,36 @@
+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,
+ },
+ ],
+ });
+ });
+ });
+});