Commit 9d9ab266 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch '35896-graphql-error-stack-trace' into 'master'

Add Sentry error stack trace to GraphQL API

Closes #35896

See merge request gitlab-org/gitlab!23750
parents 21d7eadb 93849981
# frozen_string_literal: true
module Resolvers
module ErrorTracking
class SentryErrorStackTraceResolver < BaseResolver
argument :id, GraphQL::ID_TYPE,
required: true,
description: 'ID of the Sentry issue'
def resolve(**args)
issue_id = GlobalID.parse(args[:id]).model_id
# Get data from Sentry
response = ::ErrorTracking::IssueLatestEventService.new(
project,
current_user,
{ issue_id: issue_id }
).execute
event = response[:latest_event]
event.gitlab_project = project if event
event
end
private
def project
return object.gitlab_project if object.respond_to?(:gitlab_project)
object
end
end
end
end
......@@ -28,6 +28,10 @@ module Types
null: true,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
field :error_stack_trace, Types::ErrorTracking::SentryErrorStackTraceType,
null: true,
description: 'Stack Trace of Sentry Error',
resolver: Resolvers::ErrorTracking::SentryErrorStackTraceResolver
field :external_url,
GraphQL::STRING_TYPE,
null: true,
......
# frozen_string_literal: true
module Types
module ErrorTracking
# rubocop: disable Graphql/AuthorizeTypes
class SentryErrorStackTraceContextType < ::Types::BaseObject
graphql_name 'SentryErrorStackTraceContext'
description 'An object context for a Sentry error stack trace'
field :line,
GraphQL::INT_TYPE,
null: false,
description: 'Line number of the context'
field :code,
GraphQL::STRING_TYPE,
null: false,
description: 'Code number of the context'
def line
object[0]
end
def code
object[1]
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
# rubocop: disable Graphql/AuthorizeTypes
class SentryErrorStackTraceEntryType < ::Types::BaseObject
graphql_name 'SentryErrorStackTraceEntry'
description 'An object containing a stack trace entry for a Sentry error.'
field :function, GraphQL::STRING_TYPE,
null: true,
description: 'Function in which the Sentry error occurred'
field :col, GraphQL::STRING_TYPE,
null: true,
description: 'Function in which the Sentry error occurred'
field :line, GraphQL::STRING_TYPE,
null: true,
description: 'Function in which the Sentry error occurred'
field :file_name, GraphQL::STRING_TYPE,
null: true,
description: 'File in which the Sentry error occurred'
field :trace_context, [Types::ErrorTracking::SentryErrorStackTraceContextType],
null: true,
description: 'Context of the Sentry error'
def function
object['function']
end
def col
object['colNo']
end
def line
object['lineNo']
end
def file_name
object['filename']
end
def trace_context
object['context']
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
class SentryErrorStackTraceType < ::Types::BaseObject
graphql_name 'SentryErrorStackTrace'
description 'An object containing a stack trace entry for a Sentry error.'
authorize :read_sentry_issue
field :issue_id, GraphQL::STRING_TYPE,
null: false,
description: 'ID of the Sentry error'
field :date_received, GraphQL::STRING_TYPE,
null: false,
description: 'Time the stack trace was received by Sentry'
field :stack_trace_entries, [Types::ErrorTracking::SentryErrorStackTraceEntryType],
null: false,
description: 'Stack trace entries for the Sentry error'
end
end
end
---
title: Add Sentry error stack trace to GraphQL API
merge_request: 23750
author:
type: added
......@@ -6298,6 +6298,16 @@ type SentryErrorCollection {
id: ID!
): SentryDetailedError
"""
Stack Trace of Sentry Error
"""
errorStackTrace(
"""
ID of the Sentry issue
"""
id: ID!
): SentryErrorStackTrace
"""
Collection of Sentry Errors
"""
......@@ -6386,6 +6396,71 @@ type SentryErrorFrequency {
time: Time!
}
"""
An object containing a stack trace entry for a Sentry error.
"""
type SentryErrorStackTrace {
"""
Time the stack trace was received by Sentry
"""
dateReceived: String!
"""
ID of the Sentry error
"""
issueId: String!
"""
Stack trace entries for the Sentry error
"""
stackTraceEntries: [SentryErrorStackTraceEntry!]!
}
"""
An object context for a Sentry error stack trace
"""
type SentryErrorStackTraceContext {
"""
Code number of the context
"""
code: String!
"""
Line number of the context
"""
line: Int!
}
"""
An object containing a stack trace entry for a Sentry error.
"""
type SentryErrorStackTraceEntry {
"""
Function in which the Sentry error occurred
"""
col: String
"""
File in which the Sentry error occurred
"""
fileName: String
"""
Function in which the Sentry error occurred
"""
function: String
"""
Function in which the Sentry error occurred
"""
line: String
"""
Context of the Sentry error
"""
traceContext: [SentryErrorStackTraceContext!]
}
"""
State of a Sentry error
"""
......
......@@ -17454,6 +17454,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errorStackTrace",
"description": "Stack Trace of Sentry Error",
"args": [
{
"name": "id",
"description": "ID of the Sentry issue",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "SentryErrorStackTrace",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Collection of Sentry Errors",
......@@ -17984,6 +18011,221 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SentryErrorStackTrace",
"description": "An object containing a stack trace entry for a Sentry error.",
"fields": [
{
"name": "dateReceived",
"description": "Time the stack trace was received by Sentry",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issueId",
"description": "ID of the Sentry error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "stackTraceEntries",
"description": "Stack trace entries for the Sentry error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "SentryErrorStackTraceEntry",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SentryErrorStackTraceEntry",
"description": "An object containing a stack trace entry for a Sentry error.",
"fields": [
{
"name": "col",
"description": "Function in which the Sentry error occurred",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fileName",
"description": "File in which the Sentry error occurred",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "function",
"description": "Function in which the Sentry error occurred",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "line",
"description": "Function in which the Sentry error occurred",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "traceContext",
"description": "Context of the Sentry error",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "SentryErrorStackTraceContext",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SentryErrorStackTraceContext",
"description": "An object context for a Sentry error stack trace",
"fields": [
{
"name": "code",
"description": "Code number of the context",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "line",
"description": "Line number of the context",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Metadata",
......
......@@ -983,6 +983,7 @@ An object containing a collection of Sentry errors, and a detailed error.
| Name | Type | Description |
| --- | ---- | ---------- |
| `detailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
| `errorStackTrace` | SentryErrorStackTrace | Stack Trace of Sentry Error |
| `errors` | SentryErrorConnection | Collection of Sentry Errors |
| `externalUrl` | String | External URL for Sentry |
......@@ -993,6 +994,37 @@ An object containing a collection of Sentry errors, and a detailed error.
| `count` | Int! | Count of errors received since the previously recorded time |
| `time` | Time! | Time the error frequency stats were recorded |
## SentryErrorStackTrace
An object containing a stack trace entry for a Sentry error.
| Name | Type | Description |
| --- | ---- | ---------- |
| `dateReceived` | String! | Time the stack trace was received by Sentry |
| `issueId` | String! | ID of the Sentry error |
| `stackTraceEntries` | SentryErrorStackTraceEntry! => Array | Stack trace entries for the Sentry error |
## SentryErrorStackTraceContext
An object context for a Sentry error stack trace
| Name | Type | Description |
| --- | ---- | ---------- |
| `code` | String! | Code number of the context |
| `line` | Int! | Line number of the context |
## SentryErrorStackTraceEntry
An object containing a stack trace entry for a Sentry error.
| Name | Type | Description |
| --- | ---- | ---------- |
| `col` | String | Function in which the Sentry error occurred |
| `fileName` | String | File in which the Sentry error occurred |
| `function` | String | Function in which the Sentry error occurred |
| `line` | String | Function in which the Sentry error occurred |
| `traceContext` | SentryErrorStackTraceContext! => Array | Context of the Sentry error |
## SentryErrorTags
State of a Sentry error
......
......@@ -5,7 +5,11 @@ module Gitlab
class ErrorEvent
include ActiveModel::Model
attr_accessor :issue_id, :date_received, :stack_trace_entries
attr_accessor :issue_id, :date_received, :stack_trace_entries, :gitlab_project
def self.declarative_policy_class
'ErrorTracking::BasePolicy'
end
end
end
end
......@@ -12,6 +12,7 @@ describe GitlabSchema.types['SentryErrorCollection'] do
errors
detailed_error
external_url
error_stack_trace
]
is_expected.to have_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['SentryErrorStackTraceEntry'] do
it { expect(described_class.graphql_name).to eq('SentryErrorStackTraceEntry') }
it 'exposes the expected fields' do
expected_fields = %i[
function
col
line
file_name
trace_context
]
is_expected.to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['SentryErrorStackTrace'] do
it { expect(described_class.graphql_name).to eq('SentryErrorStackTrace') }
it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
it 'exposes the expected fields' do
expected_fields = %i[
issue_id
date_received
stack_trace_entries
]
is_expected.to have_graphql_fields(*expected_fields)
end
end
......@@ -40,8 +40,8 @@ describe 'sentry errors requests' do
post_graphql(query, current_user: current_user)
end
it "is expected to return an empty error" do
expect(error_data).to eq nil
it 'is expected to return an empty error' do
expect(error_data).to be_nil
end
end
......@@ -49,7 +49,7 @@ describe 'sentry errors requests' do
before do
allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details)
.and_return({ issue: sentry_detailed_error })
.and_return(issue: sentry_detailed_error)
post_graphql(query, current_user: current_user)
end
......@@ -72,8 +72,8 @@ describe 'sentry errors requests' do
context 'user does not have permission' do
let(:current_user) { create(:user) }
it "is expected to return an empty error" do
expect(error_data).to eq nil
it 'is expected to return an empty error' do
expect(error_data).to be_nil
end
end
end
......@@ -82,13 +82,13 @@ describe 'sentry errors requests' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details)
.and_return({ error: 'error message' })
.and_return(error: 'error message')
post_graphql(query, current_user: current_user)
end
it 'is expected to handle the error and return nil' do
expect(error_data).to eq nil
expect(error_data).to be_nil
end
end
end
......@@ -132,8 +132,8 @@ describe 'sentry errors requests' do
post_graphql(query, current_user: current_user)
end
it "is expected to return nil" do
expect(error_data).to eq nil
it 'is expected to return nil' do
expect(error_data).to be_nil
end
end
......@@ -141,7 +141,7 @@ describe 'sentry errors requests' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:list_sentry_issues)
.and_return({ issues: [sentry_error], pagination: pagination })
.and_return(issues: [sentry_error], pagination: pagination)
post_graphql(query, current_user: current_user)
end
......@@ -174,17 +174,82 @@ describe 'sentry errors requests' do
end
end
context "sentry api itself errors out" do
context 'sentry api itself errors out' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:list_sentry_issues)
.and_return({ error: 'error message' })
.and_return(error: 'error message')
post_graphql(query, current_user: current_user)
end
it 'is expected to handle the error and return nil' do
expect(error_data).to eq nil
expect(error_data).to be_nil
end
end
end
describe 'getting a stack trace' do
let_it_be(:sentry_stack_trace) { build(:error_tracking_error_event) }
let(:sentry_gid) { Gitlab::ErrorTracking::DetailedError.new(id: 1).to_global_id.to_s }
let(:stack_trace_fields) do
all_graphql_fields_for('SentryErrorStackTrace'.classify)
end
let(:fields) do
query_graphql_field('errorStackTrace', { id: sentry_gid }, stack_trace_fields)
end
let(:stack_trace_data) { graphql_data.dig('project', 'sentryErrors', 'errorStackTrace') }
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when data is loading via reactive cache' do
before do
post_graphql(query, current_user: current_user)
end
it 'is expected to return an empty error' do
expect(stack_trace_data).to be_nil
end
end
context 'reactive cache returns data' do
before do
allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_latest_event)
.and_return(latest_event: sentry_stack_trace)
post_graphql(query, current_user: current_user)
end
it_behaves_like 'setting stack trace error'
context 'user does not have permission' do
let(:current_user) { create(:user) }
it 'is expected to return an empty error' do
expect(stack_trace_data).to be_nil
end
end
end
context 'sentry api returns an error' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_latest_event)
.and_return(error: 'error message')
post_graphql(query, current_user: current_user)
end
it 'is expected to handle the error and return nil' do
expect(stack_trace_data).to be_nil
end
end
end
......
......@@ -3,11 +3,34 @@
RSpec.shared_examples 'setting sentry error data' do
it 'sets the sentry error data correctly' do
aggregate_failures 'testing the sentry error is correct' do
expect(error['id']).to eql sentry_error.to_global_id.to_s
expect(error['sentryId']).to eql sentry_error.id.to_s
expect(error['status']).to eql sentry_error.status.upcase
expect(error['firstSeen']).to eql sentry_error.first_seen
expect(error['lastSeen']).to eql sentry_error.last_seen
expect(error['id']).to eq sentry_error.to_global_id.to_s
expect(error['sentryId']).to eq sentry_error.id.to_s
expect(error['status']).to eq sentry_error.status.upcase
expect(error['firstSeen']).to eq sentry_error.first_seen
expect(error['lastSeen']).to eq sentry_error.last_seen
end
end
end
RSpec.shared_examples 'setting stack trace error' do
it 'sets the stack trace data correctly' do
aggregate_failures 'testing the stack trace is correct' do
expect(stack_trace_data['dateReceived']).to eq(sentry_stack_trace.date_received)
expect(stack_trace_data['issueId']).to eq(sentry_stack_trace.issue_id)
expect(stack_trace_data['stackTraceEntries']).to be_an_instance_of(Array)
expect(stack_trace_data['stackTraceEntries'].size).to eq(sentry_stack_trace.stack_trace_entries.size)
end
end
it 'sets the stack trace entry data correctly' do
aggregate_failures 'testing the stack trace entry is correct' do
stack_trace_entry = stack_trace_data['stackTraceEntries'].first
model_entry = sentry_stack_trace.stack_trace_entries.first
expect(stack_trace_entry['function']).to eq model_entry['function']
expect(stack_trace_entry['col']).to eq model_entry['colNo']
expect(stack_trace_entry['line']).to eq model_entry['lineNo'].to_s
expect(stack_trace_entry['fileName']).to eq model_entry['filename']
end
end
end
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