Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow choosing fields to fetch for sentry events #400

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func HandleEvents(client sentry.SentryClient, query query.SentryQuery, backendQu
ProjectIds: query.ProjectIds,
Environments: query.Environments,
Query: query.EventsQuery,
Fields: query.EventsFields,
Sort: query.EventsSort,
Limit: query.EventsLimit,
From: backendQuery.TimeRange.From,
Expand Down
8 changes: 5 additions & 3 deletions pkg/handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func TestSentryDatasource_Events(t *testing.T) {
"projectIds" : ["project_id"],
"environments" : ["dev"],
"eventsQuery" : "event_query",
"eventsFields" : ["id","title","message","project.name","release"],
"eventsSort" : "event_sort",
"eventsLimit" : 10
}`
Expand All @@ -185,10 +186,11 @@ func TestSentryDatasource_Events(t *testing.T) {
// Assert the content of the data frame
frame := res.Responses["A"].Frames[0]
require.NotNil(t, frame.Fields)
require.Equal(t, 11, len(frame.Fields))
require.Equal(t, 4, len(frame.Fields))
assert.Equal(t, 3, frame.Fields[0].Len())
require.Equal(t, "ID", frame.Fields[0].Name)
require.Equal(t, "Title", frame.Fields[1].Name)
require.Equal(t, "id", frame.Fields[0].Name)
require.Equal(t, "message", frame.Fields[1].Name)
require.Contains(t, frame.Meta.ExecutedQueryString, "release")
})

t.Run("valid events stats query should produce correct result", func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions pkg/query/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type SentryQuery struct {
IssuesSort string `json:"issuesSort,omitempty"`
IssuesLimit int64 `json:"issuesLimit,omitempty"`
EventsQuery string `json:"eventsQuery,omitempty"`
EventsFields []string `json:"eventsFields,omitempty"`
EventsSort string `json:"eventsSort,omitempty"`
EventsLimit int64 `json:"eventsLimit,omitempty"`
EventsStatsQuery string `json:"eventsStatsQuery,omitempty"`
Expand Down
46 changes: 6 additions & 40 deletions pkg/sentry/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,54 +7,18 @@ import (
"time"
)

type SentryEvents struct {
Data []SentryEvent `json:"data"`
Meta map[string]interface{} `json:"meta"`
}

type SentryEvent struct {
ID string `json:"id"`
Title string `json:"title"`
Project string `json:"project"`
ProjectId int64 `json:"project.id"`
Release string `json:"release"`
Count int64 `json:"count()"`
EventsPerMinute float64 `json:"epm()"`
LastSeen time.Time `json:"last_seen()"`
Level string `json:"level"`
EventType string `json:"event.type"`
Platform string `json:"platform"`
}

type GetEventsInput struct {
OrganizationSlug string
ProjectIds []string
Environments []string
Fields []string
Query string
From time.Time
To time.Time
Sort string
Limit int64
}

// getRequiredFields returns the list of fields that are required to be fetched
// from the sentry API. This is used to build the query string.
func getRequiredFields() []string {
return []string{
"id",
"title",
"project",
"project.id",
"release",
"count()",
"epm()",
"last_seen()",
"level",
"event.type",
"platform",
}
}

func (gei *GetEventsInput) ToQuery() string {
urlPath := fmt.Sprintf("/api/0/organizations/%s/events/?", gei.OrganizationSlug)
if gei.Limit < 1 || gei.Limit > 100 {
Expand All @@ -68,7 +32,7 @@ func (gei *GetEventsInput) ToQuery() string {
params.Set("sort", gei.Sort)
}
params.Set("per_page", strconv.FormatInt(gei.Limit, 10))
for _, field := range getRequiredFields() {
for _, field := range gei.Fields {
params.Add("field", field)
}
for _, projectId := range gei.ProjectIds {
Expand All @@ -80,8 +44,10 @@ func (gei *GetEventsInput) ToQuery() string {
return urlPath + params.Encode()
}

func (sc *SentryClient) GetEvents(gei GetEventsInput) ([]SentryEvent, string, error) {
var out SentryEvents
func (sc *SentryClient) GetEvents(gei GetEventsInput) ([]map[string]interface{}, string, error) {
var out struct {
Data []map[string]interface{} `json:"data"`
}
executedQueryString := gei.ToQuery()
err := sc.Fetch(executedQueryString, &out)
return out.Data, sc.BaseURL + executedQueryString, err
Expand Down
64 changes: 64 additions & 0 deletions src/components/query-editor/EventsEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,44 @@ interface EventsEditorProps {
onRunQuery: () => void;
}

// A list of fields that are to be fetched from the sentry API.
// This is used to build the default query string.
const DEFAULT_FIELDS = [
'id',
'title',
'project',
'project.id',
'release',
'count()',
'epm()',
'last_seen()',
'level',
'event.type',
'platform'
];

const fieldOptions = DEFAULT_FIELDS.map(field => ({
label: field,
value: field
}));

export const EventsEditor = ({ query, onChange, onRunQuery }: EventsEditorProps) => {
const [customOptions, setCustomOptions] = React.useState<Array<{ label: string, value: string }>>([]);
if (!query.eventsFields) {
onChange({
...query,
eventsFields: DEFAULT_FIELDS
});
onRunQuery();
}

const onEventsQueryChange = (eventsQuery: string) => {
onChange({ ...query, eventsQuery });
};
const onEventsFieldsChange = (eventsFields: string[]) => {
onChange({ ...query, eventsFields: eventsFields });
onRunQuery();
};
const onEventsSortChange = (eventsSort: SentryEventSort) => {
onChange({ ...query, eventsSort: eventsSort });
onRunQuery();
Expand All @@ -39,6 +73,36 @@ export const EventsEditor = ({ query, onChange, onRunQuery }: EventsEditorProps)
/>
</EditorField>
</EditorRow>
<EditorRow>
<EditorField
tooltip={selectors.components.QueryEditor.Events.Fields.tooltip}
label={selectors.components.QueryEditor.Events.Fields.label}
width={'100%'}
>
<Select
isMulti={true}
options={[...fieldOptions, ...customOptions]}
value={query.eventsFields?.map(field => ({ label: field, value: field })) || []}
onChange={(values) => onEventsFieldsChange(
(values || []).map((v: { value: string }) => v.value)
.filter(Boolean)
)}
allowCustomValue={true}
onCreateOption={(v) => {
const customValue = { label: v, value: v };
setCustomOptions([...customOptions, customValue]);
onEventsFieldsChange([
...(query.eventsFields || []),
v
]);
}}
placeholder={selectors.components.QueryEditor.Events.Fields.placeholder}
width={'auto'}
maxVisibleValues={20}
showAllSelectedWhenOpen={true}
/>
</EditorField>
</EditorRow>
<EditorRow>
<EditorFieldGroup>
<EditorField
Expand Down
5 changes: 5 additions & 0 deletions src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export const Components = {
},
},
Events: {
Fields: {
label: 'Fields',
tooltip: 'Sentry field names to fetch',
placeholder: 'Enter a Sentry field name',
},
Query: {
label: 'Query',
tooltip: 'Sentry query to filter the results',
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type SentryEventsQuery = {
projectIds: string[];
environments: string[];
eventsQuery: string;
eventsFields?: string[];
eventsSort?: SentryEventSort;
eventsLimit?: number;
} & SentryQueryBase<'events'>;
Expand Down
Loading