Commit 1d8d4661 authored by Nick Thomas's avatar Nick Thomas

Merge branch '35897-grapghql-error-tracking-list-errors' into 'master'

Resolve "Add a GraphQL query for Error Tracking - List Errors"

Closes #35897

See merge request gitlab-org/gitlab!21802
parents aba6ac92 9435047f
......@@ -8,7 +8,6 @@ module Resolvers
description: 'ID of the Sentry issue'
def resolve(**args)
project = object
current_user = context[:current_user]
issue_id = GlobalID.parse(args[:id]).model_id
......@@ -23,6 +22,14 @@ module Resolvers
issue
end
private
def project
return object.gitlab_project if object.respond_to?(:gitlab_project)
object
end
end
end
end
# frozen_string_literal: true
module Resolvers
module ErrorTracking
class SentryErrorCollectionResolver < BaseResolver
def resolve(**args)
project = object
service = ::ErrorTracking::ListIssuesService.new(
project,
context[:current_user]
)
Gitlab::ErrorTracking::ErrorCollection.new(
external_url: service.external_url,
project: project
)
end
end
end
end
# frozen_string_literal: true
module Resolvers
module ErrorTracking
class SentryErrorsResolver < BaseResolver
def resolve(**args)
args[:cursor] = args.delete(:after)
project = object.project
result = ::ErrorTracking::ListIssuesService.new(
project,
context[:current_user],
args
).execute
next_cursor = result[:pagination]&.dig('next', 'cursor')
previous_cursor = result[:pagination]&.dig('previous', 'cursor')
issues = result[:issues]
# ReactiveCache is still fetching data
return if issues.nil?
Gitlab::Graphql::ExternallyPaginatedArray.new(previous_cursor, next_cursor, *issues)
end
end
end
end
......@@ -4,8 +4,9 @@ module Types
module ErrorTracking
class SentryDetailedErrorType < ::Types::BaseObject
graphql_name 'SentryDetailedError'
description 'A Sentry error.'
present_using SentryDetailedErrorPresenter
present_using SentryErrorPresenter
authorize :read_sentry_issue
......@@ -92,18 +93,6 @@ module Types
field :tags, Types::ErrorTracking::SentryErrorTagsType,
null: false,
description: 'Tags associated with the Sentry Error'
def first_seen
DateTime.parse(object.first_seen)
end
def last_seen
DateTime.parse(object.last_seen)
end
def project_id
Gitlab::GlobalId.build(model_name: 'Project', id: object.project_id).to_s
end
end
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
class SentryErrorCollectionType < ::Types::BaseObject
graphql_name 'SentryErrorCollection'
description 'An object containing a collection of Sentry errors, and a detailed error.'
authorize :read_sentry_issue
field :errors,
Types::ErrorTracking::SentryErrorType.connection_type,
connection: false,
null: true,
description: "Collection of Sentry Errors",
extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension],
resolver: Resolvers::ErrorTracking::SentryErrorsResolver do
argument :search_term,
String,
description: 'Search term for the Sentry error.',
required: false
argument :sort,
String,
description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.',
required: false
end
field :detailed_error, Types::ErrorTracking::SentryDetailedErrorType,
null: true,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
field :external_url,
GraphQL::STRING_TYPE,
null: true,
description: "External URL for Sentry"
end
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
# rubocop: disable Graphql/AuthorizeTypes
class SentryErrorType < ::Types::BaseObject
graphql_name 'SentryError'
description 'A Sentry error. A simplified version of SentryDetailedError.'
present_using SentryErrorPresenter
field :id, GraphQL::ID_TYPE,
null: false,
description: 'ID (global ID) of the error'
field :sentry_id, GraphQL::STRING_TYPE,
method: :id,
null: false,
description: 'ID (Sentry ID) of the error'
field :first_seen, Types::TimeType,
null: false,
description: 'Timestamp when the error was first seen'
field :last_seen, Types::TimeType,
null: false,
description: 'Timestamp when the error was last seen'
field :title, GraphQL::STRING_TYPE,
null: false,
description: 'Title of the error'
field :type, GraphQL::STRING_TYPE,
null: false,
description: 'Type of the error'
field :user_count, GraphQL::INT_TYPE,
null: false,
description: 'Count of users affected by the error'
field :count, GraphQL::INT_TYPE,
null: false,
description: 'Count of occurrences'
field :message, GraphQL::STRING_TYPE,
null: true,
description: 'Sentry metadata message of the error'
field :culprit, GraphQL::STRING_TYPE,
null: false,
description: 'Culprit of the error'
field :external_url, GraphQL::STRING_TYPE,
null: false,
description: 'External URL of the error'
field :short_id, GraphQL::STRING_TYPE,
null: false,
description: 'Short ID (Sentry ID) of the error'
field :status, Types::ErrorTracking::SentryErrorStatusEnum,
null: false,
description: 'Status of the error'
field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType],
null: false,
description: 'Last 24hr stats of the error'
field :sentry_project_id, GraphQL::ID_TYPE,
method: :project_id,
null: false,
description: 'ID of the project (Sentry project)'
field :sentry_project_name, GraphQL::STRING_TYPE,
method: :project_name,
null: false,
description: 'Name of the project affected by the error'
field :sentry_project_slug, GraphQL::STRING_TYPE,
method: :project_slug,
null: false,
description: 'Slug of the project affected by the error'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
......@@ -173,6 +173,12 @@ module Types
null: true,
description: 'Snippets of the project',
resolver: Resolvers::Projects::SnippetsResolver
field :sentry_errors,
Types::ErrorTracking::SentryErrorCollectionType,
null: true,
description: 'Paginated collection of Sentry errors on the project',
resolver: Resolvers::ErrorTracking::SentryErrorCollectionResolver
end
end
......
# frozen_string_literal: true
module ErrorTracking
class DetailedErrorPolicy < BasePolicy
class BasePolicy < ::BasePolicy
delegate { @subject.gitlab_project }
end
end
# frozen_string_literal: true
class SentryDetailedErrorPresenter < Gitlab::View::Presenter::Delegated
class SentryErrorPresenter < Gitlab::View::Presenter::Delegated
presents :error
FrequencyStruct = Struct.new(:time, :count, keyword_init: true)
def first_seen
DateTime.parse(error.first_seen)
end
def last_seen
DateTime.parse(error.last_seen)
end
def project_id
Gitlab::GlobalId.build(model_name: 'Project', id: error.project_id).to_s
end
def frequency
utc_offset = Time.zone_offset('UTC')
......
---
title: Add querying of Sentry errors to Graphql
merge_request: 21802
author:
type: added
......@@ -5453,6 +5453,11 @@ type Project {
id: ID!
): SentryDetailedError
"""
Paginated collection of Sentry errors on the project
"""
sentryErrors: SentryErrorCollection
"""
E-mail address of the service desk.
"""
......@@ -6054,6 +6059,9 @@ type RootStorageStatistics {
wikiSize: Int!
}
"""
A Sentry error.
"""
type SentryDetailedError {
"""
Count of occurrences
......@@ -6186,6 +6194,186 @@ type SentryDetailedError {
userCount: Int!
}
"""
A Sentry error. A simplified version of SentryDetailedError.
"""
type SentryError {
"""
Count of occurrences
"""
count: Int!
"""
Culprit of the error
"""
culprit: String!
"""
External URL of the error
"""
externalUrl: String!
"""
Timestamp when the error was first seen
"""
firstSeen: Time!
"""
Last 24hr stats of the error
"""
frequency: [SentryErrorFrequency!]!
"""
ID (global ID) of the error
"""
id: ID!
"""
Timestamp when the error was last seen
"""
lastSeen: Time!
"""
Sentry metadata message of the error
"""
message: String
"""
ID (Sentry ID) of the error
"""
sentryId: String!
"""
ID of the project (Sentry project)
"""
sentryProjectId: ID!
"""
Name of the project affected by the error
"""
sentryProjectName: String!
"""
Slug of the project affected by the error
"""
sentryProjectSlug: String!
"""
Short ID (Sentry ID) of the error
"""
shortId: String!
"""
Status of the error
"""
status: SentryErrorStatus!
"""
Title of the error
"""
title: String!
"""
Type of the error
"""
type: String!
"""
Count of users affected by the error
"""
userCount: Int!
}
"""
An object containing a collection of Sentry errors, and a detailed error.
"""
type SentryErrorCollection {
"""
Detailed version of a Sentry error on the project
"""
detailedError(
"""
ID of the Sentry issue
"""
id: ID!
): SentryDetailedError
"""
Collection of Sentry Errors
"""
errors(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Search term for the Sentry error.
"""
searchTerm: String
"""
Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.
"""
sort: String
): SentryErrorConnection
"""
External URL for Sentry
"""
externalUrl: String
}
"""
The connection type for SentryError.
"""
type SentryErrorConnection {
"""
A list of edges.
"""
edges: [SentryErrorEdge]
"""
A list of nodes.
"""
nodes: [SentryError]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type SentryErrorEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: SentryError
}
type SentryErrorFrequency {
"""
Count of errors received since the previously recorded time
......
......@@ -1433,6 +1433,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryErrors",
"description": "Paginated collection of Sentry errors on the project",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "SentryErrorCollection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "serviceDeskAddress",
"description": "E-mail address of the service desk.",
......@@ -16708,7 +16722,7 @@
{
"kind": "OBJECT",
"name": "SentryDetailedError",
"description": null,
"description": "A Sentry error.",
"fields": [
{
"name": "count",
......@@ -17408,6 +17422,568 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SentryErrorCollection",
"description": "An object containing a collection of Sentry errors, and a detailed error.",
"fields": [
{
"name": "detailedError",
"description": "Detailed version of a Sentry error on the project",
"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": "SentryDetailedError",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Collection of Sentry Errors",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "searchTerm",
"description": "Search term for the Sentry error.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "sort",
"description": "Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "SentryErrorConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "externalUrl",
"description": "External URL for Sentry",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SentryErrorConnection",
"description": "The connection type for SentryError.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "SentryErrorEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "SentryError",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SentryErrorEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "SentryError",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SentryError",
"description": "A Sentry error. A simplified version of SentryDetailedError.",
"fields": [
{
"name": "count",
"description": "Count of occurrences",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "culprit",
"description": "Culprit of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "externalUrl",
"description": "External URL of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "firstSeen",
"description": "Timestamp when the error was first seen",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "frequency",
"description": "Last 24hr stats of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "SentryErrorFrequency",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID (global ID) of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lastSeen",
"description": "Timestamp when the error was last seen",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "message",
"description": "Sentry metadata message of the error",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryId",
"description": "ID (Sentry ID) of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryProjectId",
"description": "ID of the project (Sentry project)",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryProjectName",
"description": "Name of the project affected by the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sentryProjectSlug",
"description": "Slug of the project affected by the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "shortId",
"description": "Short ID (Sentry ID) of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "status",
"description": "Status of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SentryErrorStatus",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "title",
"description": "Title of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "Type of the error",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "userCount",
"description": "Count of users affected by the error",
"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",
......
......@@ -815,6 +815,7 @@ Information about pagination in a connection.
| `repository` | Repository | Git repository of the project |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
| `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project |
| `serviceDeskAddress` | String | E-mail address of the service desk. |
| `serviceDeskEnabled` | Boolean | Indicates if the project has service desk enabled. |
| `sharedRunnersEnabled` | Boolean | Indicates if shared runners are enabled on the project |
......@@ -919,6 +920,8 @@ Autogenerated return type of RemoveAwardEmoji
## SentryDetailedError
A Sentry error.
| Name | Type | Description |
| --- | ---- | ---------- |
| `count` | Int! | Count of occurrences |
......@@ -948,6 +951,40 @@ Autogenerated return type of RemoveAwardEmoji
| `type` | String! | Type of the error |
| `userCount` | Int! | Count of users affected by the error |
## SentryError
A Sentry error. A simplified version of SentryDetailedError.
| Name | Type | Description |
| --- | ---- | ---------- |
| `count` | Int! | Count of occurrences |
| `culprit` | String! | Culprit of the error |
| `externalUrl` | String! | External URL of the error |
| `firstSeen` | Time! | Timestamp when the error was first seen |
| `frequency` | SentryErrorFrequency! => Array | Last 24hr stats of the error |
| `id` | ID! | ID (global ID) of the error |
| `lastSeen` | Time! | Timestamp when the error was last seen |
| `message` | String | Sentry metadata message of the error |
| `sentryId` | String! | ID (Sentry ID) of the error |
| `sentryProjectId` | ID! | ID of the project (Sentry project) |
| `sentryProjectName` | String! | Name of the project affected by the error |
| `sentryProjectSlug` | String! | Slug of the project affected by the error |
| `shortId` | String! | Short ID (Sentry ID) of the error |
| `status` | SentryErrorStatus! | Status of the error |
| `title` | String! | Title of the error |
| `type` | String! | Type of the error |
| `userCount` | Int! | Count of users affected by the error |
## SentryErrorCollection
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 |
| `errors` | SentryErrorConnection | Collection of Sentry Errors |
| `externalUrl` | String | External URL for Sentry |
## SentryErrorFrequency
| Name | Type | Description |
......
......@@ -35,7 +35,7 @@ module Gitlab
:user_count
def self.declarative_policy_class
'ErrorTracking::DetailedErrorPolicy'
'ErrorTracking::BasePolicy'
end
end
end
......
......@@ -4,11 +4,16 @@ module Gitlab
module ErrorTracking
class Error
include ActiveModel::Model
include GlobalID::Identification
attr_accessor :id, :title, :type, :user_count, :count,
:first_seen, :last_seen, :message, :culprit,
:external_url, :project_id, :project_name, :project_slug,
:short_id, :status, :frequency
def self.declarative_policy_class
'ErrorTracking::BasePolicy'
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class ErrorCollection
include GlobalID::Identification
attr_accessor :issues, :external_url, :project
alias_attribute :gitlab_project, :project
def initialize(project:, external_url: nil, issues: [])
@project = project
@external_url = external_url
@issues = issues
end
def self.declarative_policy_class
'ErrorTracking::BasePolicy'
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Graphql
module Extensions
class ExternallyPaginatedArrayExtension < GraphQL::Schema::Field::ConnectionExtension
def resolve(object:, arguments:, context:)
yield(object, arguments)
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :detailed_error_tracking_error, class: 'Gitlab::ErrorTracking::DetailedError' do
id { '1' }
title { 'title' }
type { 'error' }
user_count { 1 }
count { 2 }
first_seen { Time.now.iso8601 }
last_seen { Time.now.iso8601 }
message { 'message' }
culprit { 'culprit' }
external_url { 'http://example.com/id' }
factory :detailed_error_tracking_error, parent: :error_tracking_error, class: 'Gitlab::ErrorTracking::DetailedError' do
gitlab_issue { 'http://gitlab.example.com/issues/1' }
external_base_url { 'http://example.com' }
project_id { 'project1' }
project_name { 'project name' }
project_slug { 'project_name' }
short_id { 'ID' }
status { 'unresolved' }
first_release_last_commit { '68c914da9' }
last_release_last_commit { '9ad419c86' }
first_release_short_version { 'abc123' }
last_release_short_version { 'abc123' }
first_release_version { '12345678' }
tags do
{
level: 'error',
logger: 'rails'
}
end
frequency do
[
[Time.now.to_i, 10]
]
end
gitlab_issue { 'http://gitlab.example.com/issues/1' }
first_release_last_commit { '68c914da9' }
last_release_last_commit { '9ad419c86' }
first_release_short_version { 'abc123' }
last_release_short_version { 'abc123' }
first_release_version { '12345678' }
skip_create
end
end
......@@ -2,13 +2,13 @@
FactoryBot.define do
factory :error_tracking_error, class: 'Gitlab::ErrorTracking::Error' do
id { 'id' }
id { '1' }
title { 'title' }
type { 'error' }
user_count { 1 }
count { 2 }
first_seen { Time.now }
last_seen { Time.now }
first_seen { Time.now.iso8601 }
last_seen { Time.now.iso8601 }
message { 'message' }
culprit { 'culprit' }
external_url { 'http://example.com/id' }
......@@ -17,7 +17,11 @@ FactoryBot.define do
project_slug { 'project_name' }
short_id { 'ID' }
status { 'unresolved' }
frequency { [] }
frequency do
[
[Time.now.to_i, 10]
]
end
skip_create
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::ErrorTracking::SentryErrorCollectionResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') }
before do
project.add_developer(current_user)
allow(ErrorTracking::ListIssuesService)
.to receive(:new)
.and_return list_issues_service
end
describe '#resolve' do
it 'returns an error collection object' do
expect(resolve_error_collection).to be_a Gitlab::ErrorTracking::ErrorCollection
end
it 'provides the service url' do
fake_url = 'http://test.com'
expect(list_issues_service)
.to receive(:external_url)
.and_return(fake_url)
result = resolve_error_collection
expect(result.external_url).to eq fake_url
end
it 'provides the project' do
expect(resolve_error_collection.project).to eq project
end
end
private
def resolve_error_collection(context = { current_user: current_user })
resolve(described_class, obj: project, args: {}, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::ErrorTracking::SentryErrorsResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:error_collection) { Gitlab::ErrorTracking::ErrorCollection.new(project: project) }
let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') }
let(:issues) { nil }
let(:pagination) { nil }
describe '#resolve' do
context 'insufficient user permission' do
let(:user) { create(:user) }
it 'returns nil' do
context = { current_user: user }
expect(resolve_errors({}, context)).to eq nil
end
end
context 'user with permission' do
before do
project.add_developer(current_user)
allow(ErrorTracking::ListIssuesService)
.to receive(:new)
.and_return list_issues_service
end
context 'when after arg given' do
let(:after) { "1576029072000:0:0" }
it 'gives the cursor arg' do
expect(ErrorTracking::ListIssuesService)
.to receive(:new)
.with(project, current_user, { cursor: after })
.and_return list_issues_service
resolve_errors({ after: after })
end
end
context 'when no issues fetched' do
before do
allow(list_issues_service)
.to receive(:execute)
.and_return(
issues: nil
)
end
it 'returns nil' do
expect(resolve_errors).to eq nil
end
end
context 'when issues returned' do
let(:issues) { [:issue_1, :issue_2] }
let(:pagination) do
{
'next' => { 'cursor' => 'next' },
'previous' => { 'cursor' => 'prev' }
}
end
before do
allow(list_issues_service)
.to receive(:execute)
.and_return(
issues: issues,
pagination: pagination
)
end
it 'sets the issues' do
expect(resolve_errors).to contain_exactly(*issues)
end
it 'sets the pagination variables' do
result = resolve_errors
expect(result.next_cursor).to eq 'next'
expect(result.previous_cursor).to eq 'prev'
end
it 'returns an externally paginated array' do
expect(resolve_errors).to be_a Gitlab::Graphql::ExternallyPaginatedArray
end
end
end
end
private
def resolve_errors(args = {}, context = { current_user: current_user })
resolve(described_class, obj: error_collection, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['SentryErrorCollection'] do
it { expect(described_class.graphql_name).to eq('SentryErrorCollection') }
it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
it 'exposes the expected fields' do
expected_fields = %i[
errors
detailed_error
external_url
]
is_expected.to have_graphql_fields(*expected_fields)
end
describe 'errors field' do
subject { described_class.fields['errors'] }
it 'returns errors' do
aggregate_failures 'testing the correct types are returned' do
is_expected.to have_graphql_type(Types::ErrorTracking::SentryErrorType.connection_type)
is_expected.to have_graphql_extension(Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension)
is_expected.to have_graphql_resolver(Resolvers::ErrorTracking::SentryErrorsResolver)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['SentryError'] do
it { expect(described_class.graphql_name).to eq('SentryError') }
it 'exposes the expected fields' do
expected_fields = %i[
id
sentryId
title
type
userCount
count
firstSeen
lastSeen
message
culprit
externalUrl
sentryProjectId
sentryProjectName
sentryProjectSlug
shortId
status
frequency
]
is_expected.to have_graphql_fields(*expected_fields)
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe SentryDetailedErrorPresenter do
describe SentryErrorPresenter do
let(:error) { build(:detailed_error_tracking_error) }
let(:presenter) { described_class.new(error) }
......@@ -10,7 +10,7 @@ describe SentryDetailedErrorPresenter do
subject { presenter.frequency }
it 'returns an array of frequency structs' do
expect(subject).to include(a_kind_of(SentryDetailedErrorPresenter::FrequencyStruct))
expect(subject).to include(a_kind_of(SentryErrorPresenter::FrequencyStruct))
end
it 'converts the times into UTC time objects' do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'sentry errors requests' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) }
let_it_be(:current_user) { project.owner }
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('sentryErrors', {}, fields)
)
end
describe 'getting a detailed sentry error' do
let_it_be(:sentry_detailed_error) { build(:detailed_error_tracking_error) }
let(:sentry_gid) { sentry_detailed_error.to_global_id.to_s }
let(:detailed_fields) do
all_graphql_fields_for('SentryDetailedError'.classify)
end
let(:fields) do
query_graphql_field('detailedError', { id: sentry_gid }, detailed_fields)
end
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'detailedError') }
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(error_data).to eq nil
end
end
context 'reactive cache returns data' do
before do
allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details)
.and_return({ issue: sentry_detailed_error })
post_graphql(query, current_user: current_user)
end
let(:sentry_error) { sentry_detailed_error }
let(:error) { error_data }
it_behaves_like 'setting sentry error data'
it 'is expected to return the frequency correctly' do
aggregate_failures 'it returns the frequency correctly' do
expect(error_data['frequency'].count).to eql sentry_detailed_error.frequency.count
first_frequency = error_data['frequency'].first
expect(Time.parse(first_frequency['time'])).to eql Time.at(sentry_detailed_error.frequency[0][0], in: 0)
expect(first_frequency['count']).to eql sentry_detailed_error.frequency[0][1]
end
end
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
end
end
end
context 'sentry api returns an error' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details)
.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
end
end
end
describe 'getting an errors list' do
let_it_be(:sentry_error) { build(:error_tracking_error) }
let_it_be(:pagination) do
{
'next' => { 'cursor' => '2222' },
'previous' => { 'cursor' => '1111' }
}
end
let(:fields) do
<<~QUERY
errors {
nodes {
#{all_graphql_fields_for('SentryError'.classify)}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
QUERY
end
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'nodes') }
let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') }
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 nil" do
expect(error_data).to eq nil
end
end
context 'reactive cache returns data' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:list_sentry_issues)
.and_return({ issues: [sentry_error], pagination: pagination })
post_graphql(query, current_user: current_user)
end
let(:error) { error_data.first }
it 'is expected to return an array of data' do
expect(error_data).to be_a Array
expect(error_data.count).to eq 1
end
it_behaves_like 'setting sentry error data'
it 'sets the pagination correctly' do
expect(pagination_data['startCursor']).to eq(pagination['previous']['cursor'])
expect(pagination_data['endCursor']).to eq(pagination['next']['cursor'])
end
it 'is expected to return the frequency correctly' do
aggregate_failures 'it returns the frequency correctly' do
error = error_data.first
expect(error['frequency'].count).to eql sentry_error.frequency.count
first_frequency = error['frequency'].first
expect(Time.parse(first_frequency['time'])).to eql Time.at(sentry_error.frequency[0][0], in: 0)
expect(first_frequency['count']).to eql sentry_error.frequency[0][1]
end
end
end
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' })
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
end
end
end
end
......@@ -108,6 +108,12 @@ RSpec::Matchers.define :have_graphql_resolver do |expected|
end
end
RSpec::Matchers.define :have_graphql_extension do |expected|
match do |field|
expect(field.metadata[:type_class].extensions).to include(expected)
end
end
RSpec::Matchers.define :expose_permissions_using do |expected|
match do |type|
permission_field = type.fields['userPermissions']
......
# frozen_string_literal: true
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
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