Skip to content

Commit 369fb4f

Browse files
authored
Merge branch 'main' into olegyevik-sentry-datasource-select-project
2 parents d7fb83b + 263e9d2 commit 369fb4f

17 files changed

+486
-201
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Dependabot reviewer
2+
3+
on: pull_request_target
4+
5+
permissions:
6+
pull-requests: write
7+
contents: write
8+
9+
jobs:
10+
call-workflow-passing-data:
11+
uses: grafana/security-github-actions/.github/workflows/dependabot-automerge.yaml@main
12+
# with:
13+
# Add this to define production packages that dependabot can auto-update if the bump is minor
14+
# packages-minor-autoupdate: '[]'
15+
secrets: inherit

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The Sentry data source has the following requirements:
1212

1313
## Known limitations
1414

15-
With the Grafana Sentry data source plugin, you are able to visualize issues or usage statistics within an organization. For more information, see [Issues](https://docs.sentry.io/product/issues/) and [Org Stats](https://docs.sentry.io/product/accounts/quotas/org-stats/).
15+
With the Grafana Sentry data source plugin, you are able to visualize issues, events or usage statistics within an organization. For more information, see [Issues](https://docs.sentry.io/product/issues/), [Events](https://docs.sentry.io/product/discover-queries/) and [Org Stats](https://docs.sentry.io/product/accounts/quotas/org-stats/).
1616

1717
## Install the Sentry data source plugin
1818

@@ -65,7 +65,7 @@ datasources:
6565
6666
## Query the data source
6767
68-
The query editor allows you to query Sentry, get sentry issues and stats and display them in Grafana dashboard panels. You can choose one of the following query types, to get the relevant data.
68+
The query editor allows you to query Sentry, get sentry issues, events and stats and display them in Grafana dashboard panels. You can choose one of the following query types, to get the relevant data.
6969
7070
### Sentry issues
7171
@@ -80,6 +80,19 @@ To get the list of Sentry issues, select **Sentry Issues** as the query type. Is
8080
| Sort By | (optional) Select the order of results you want to display. |
8181
| Limit | (optional) Limit the number of results displayed. |
8282
83+
### Sentry events
84+
85+
To get the list of Sentry events, select **Sentry Events** as the query type. Events are filtered based on Grafana’s selected time range.
86+
87+
| Field | Description |
88+
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
89+
| Query Type | Choose **Events** as query type. |
90+
| Projects | (optional) Select one or more projects to filter the results. |
91+
| Environments | (optional) Select one or more environments to filter the results. |
92+
| Query | (optional) Enter your sentry query to get the relevant results. More on [query syntax](https://docs.sentry.io/product/sentry-basics/search/) |
93+
| Sort By | (optional) Select the order of results you want to display. |
94+
| Limit | (optional) Limit the number of results displayed. Max limit - 100. |
95+
8396
### Sentry Org stats
8497
8598
To get the trend of Sentry Org stats, select **Stats** as the query type. Org stats are filtered based on Grafana’s selected time range.
@@ -113,4 +126,4 @@ Annotations give you the ability to overlay Sentry issues on graphs. In the anno
113126
- Add [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/).
114127
- Configure and use [Templates and variables](https://grafana.com/docs/grafana/latest/variables/).
115128
- Add [Transformations](https://grafana.com/docs/grafana/latest/panels/transformations/).
116-
- Set up alerting; refer to [Alerts overview](https://grafana.com/docs/grafana/latest/alerting/).
129+
- Set up alerting; refer to [Alerts overview](https://grafana.com/docs/grafana/latest/alerting/).

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"@swc/core": "1.3.75",
4545
"@swc/helpers": "^0.5.0",
4646
"@swc/jest": "^0.2.26",
47-
"@testing-library/dom": "^7.31.0",
47+
"@testing-library/dom": "^9.3.3",
4848
"@testing-library/jest-dom": "^5.16.5",
4949
"@testing-library/react": "^12.1.4",
5050
"@testing-library/user-event": "^14.5.1",

pkg/plugin/handlers_query.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ type SentryQuery struct {
1616
IssuesQuery string `json:"issuesQuery,omitempty"`
1717
IssuesSort string `json:"issuesSort,omitempty"`
1818
IssuesLimit int64 `json:"issuesLimit,omitempty"`
19+
EventsQuery string `json:"eventsQuery,omitempty"`
20+
EventsSort string `json:"eventsSort,omitempty"`
21+
EventsLimit int64 `json:"eventsLimit,omitempty"`
1922
StatsCategory []string `json:"statsCategory,omitempty"`
2023
StatsFields []string `json:"statsFields,omitempty"`
2124
StatsGroupBy []string `json:"statsGroupBy,omitempty"`
@@ -73,6 +76,29 @@ func QueryData(ctx context.Context, pCtx backend.PluginContext, backendQuery bac
7376
}
7477
frame = UpdateFrameMeta(frame, executedQueryString, query, client.BaseURL, client.OrgSlug)
7578
response.Frames = append(response.Frames, frame)
79+
case "events":
80+
if client.OrgSlug == "" {
81+
return GetErrorResponse(response, "", ErrorInvalidOrganizationSlug)
82+
}
83+
events, executedQueryString, err := client.GetEvents(sentry.GetEventsInput{
84+
OrganizationSlug: client.OrgSlug,
85+
ProjectIds: query.ProjectIds,
86+
Environments: query.Environments,
87+
Query: query.EventsQuery,
88+
Sort: query.EventsSort,
89+
Limit: query.EventsLimit,
90+
From: backendQuery.TimeRange.From,
91+
To: backendQuery.TimeRange.To,
92+
})
93+
if err != nil {
94+
return GetErrorResponse(response, executedQueryString, err)
95+
}
96+
frame, err := framestruct.ToDataFrame(GetFrameName("Events", backendQuery.RefID), events)
97+
if err != nil {
98+
return GetErrorResponse(response, executedQueryString, err)
99+
}
100+
frame = UpdateFrameMeta(frame, executedQueryString, query, client.BaseURL, client.OrgSlug)
101+
response.Frames = append(response.Frames, frame)
76102
case "statsV2":
77103
if client.OrgSlug == "" {
78104
return GetErrorResponse(response, "", ErrorInvalidOrganizationSlug)

pkg/plugin/handlers_query_response.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ func UpdateFrameMeta(frame *data.Frame, executedQueryString string, query Sentry
2727
},
2828
}
2929
}
30+
if frame.Fields[i].Name == "ID" && query.QueryType == "events" {
31+
frame.Fields[i].Config = &data.FieldConfig{
32+
Links: []data.DataLink{
33+
{
34+
Title: "Open in Sentry",
35+
URL: fmt.Sprintf("https://%s.sentry.io/discover/${__data.fields[\"Project\"]}:${__data.fields[\"ID\"]}/", orgSlug),
36+
TargetBlank: true,
37+
},
38+
},
39+
}
40+
}
3041
}
3142
return frame
3243
}

pkg/plugin/handlers_query_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,58 @@ func TestSentryDatasource_QueryData(t *testing.T) {
135135
require.Equal(t, "Category=foo2, Reason=bar", res.Frames[0].Fields[3].Labels.String())
136136
require.Equal(t, "Category=foo2, Reason=bar", res.Frames[0].Fields[4].Labels.String())
137137
})
138+
t.Run("valid events query should produce correct result", func(t *testing.T) {
139+
sc := NewFakeClient(fakeDoer{Body: `{
140+
"data": [
141+
{
142+
"id": "event_id_1",
143+
"title": "event_title_1",
144+
"message": "event_description",
145+
"project.name": "project_name_1"
146+
},
147+
{
148+
"id": "event_id_2",
149+
"title": "event_title_2",
150+
"message": "event_description",
151+
"project.name": "project_name_1"
152+
},
153+
{
154+
"id": "event_id_3",
155+
"title": "event_title_3",
156+
"message": "event_description",
157+
"project.name": "project_name_2"
158+
}
159+
],
160+
"meta": {
161+
"fields": {
162+
"id": "string",
163+
"title": "string",
164+
"message": "string",
165+
"project.name": "string"
166+
}
167+
}
168+
}`})
169+
query := `{
170+
"queryType" : "events",
171+
"projectIds" : ["project_id"],
172+
"environments" : ["dev"],
173+
"eventsQuery" : "event_query",
174+
"eventsSort" : "event_sort",
175+
"eventsLimit" : 10
176+
}`
177+
res := plugin.QueryData(context.Background(), backend.PluginContext{}, backend.DataQuery{RefID: "A", JSON: []byte(query)}, *sc)
178+
179+
// Assert that there are no errors and the data frame is correctly formed
180+
assert.Nil(t, res.Error)
181+
require.Equal(t, 1, len(res.Frames))
182+
assert.Equal(t, "Events (A)", res.Frames[0].Name)
138183

184+
// Assert the content of the data frame
185+
frame := res.Frames[0]
186+
require.NotNil(t, frame.Fields)
187+
require.Equal(t, 11, len(frame.Fields))
188+
assert.Equal(t, 3, frame.Fields[0].Len())
189+
require.Equal(t, "ID", frame.Fields[0].Name)
190+
require.Equal(t, "Title", frame.Fields[1].Name)
191+
})
139192
}

pkg/sentry/events.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package sentry
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"strconv"
7+
"time"
8+
)
9+
10+
var reqFields = [...]string{
11+
"id",
12+
"title",
13+
"project",
14+
"project.id",
15+
"release",
16+
"count()",
17+
"epm()",
18+
"last_seen()",
19+
"level",
20+
"event.type",
21+
"platform",
22+
}
23+
24+
type SentryEvents struct {
25+
Data []SentryEvent `json:"data"`
26+
Meta map[string]interface{} `json:"meta"`
27+
}
28+
29+
type SentryEvent struct {
30+
ID string `json:"id"`
31+
Title string `json:"title"`
32+
Project string `json:"project"`
33+
ProjectId int64 `json:"project.id"`
34+
Release string `json:"release"`
35+
Count int64 `json:"count()"`
36+
EventsPerMinute float64 `json:"epm()"`
37+
LastSeen time.Time `json:"last_seen()"`
38+
Level string `json:"level"`
39+
EventType string `json:"event.type"`
40+
Platform string `json:"platform"`
41+
}
42+
43+
type GetEventsInput struct {
44+
OrganizationSlug string
45+
ProjectIds []string
46+
Environments []string
47+
Query string
48+
From time.Time
49+
To time.Time
50+
Sort string
51+
Limit int64
52+
}
53+
54+
func (gei *GetEventsInput) ToQuery() string {
55+
urlPath := fmt.Sprintf("/api/0/organizations/%s/events/?", gei.OrganizationSlug)
56+
if gei.Limit < 1 || gei.Limit > 100 {
57+
gei.Limit = 100
58+
}
59+
params := url.Values{}
60+
params.Set("query", gei.Query)
61+
params.Set("start", gei.From.Format("2006-01-02T15:04:05"))
62+
params.Set("end", gei.To.Format("2006-01-02T15:04:05"))
63+
if gei.Sort != "" {
64+
params.Set("sort", gei.Sort)
65+
}
66+
params.Set("per_page", strconv.FormatInt(gei.Limit, 10))
67+
for _, field := range reqFields {
68+
params.Add("field", field)
69+
}
70+
for _, projectId := range gei.ProjectIds {
71+
params.Add("project", projectId)
72+
}
73+
for _, environment := range gei.Environments {
74+
params.Add("environment", environment)
75+
}
76+
return urlPath + params.Encode()
77+
}
78+
79+
func (sc *SentryClient) GetEvents(gei GetEventsInput) ([]SentryEvent, string, error) {
80+
var out SentryEvents
81+
executedQueryString := gei.ToQuery()
82+
err := sc.Fetch(executedQueryString, &out)
83+
return out.Data, sc.BaseURL + executedQueryString, err
84+
}

src/app/replace.spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ScopedVars } from '@grafana/data';
22
import * as runtime from '@grafana/runtime';
3-
import { SentryIssuesQuery, SentryStatsV2Query } from 'types';
3+
import { SentryIssuesQuery, SentryEventsQuery, SentryStatsV2Query } from 'types';
44
import { applyTemplateVariables, replaceProjectIDs } from './replace';
55

66
describe('replace', () => {
@@ -74,6 +74,21 @@ describe('replace', () => {
7474
expect(output.issuesQuery).toStrictEqual('hello bar');
7575
});
7676

77+
it('should interpolate template variables for events', () => {
78+
const query: SentryEventsQuery = {
79+
refId: '',
80+
queryType: 'events',
81+
projectIds: ['${foo}', 'baz'],
82+
environments: ['${foo}', 'baz'],
83+
eventsQuery: 'hello ${foo}',
84+
};
85+
86+
const output = applyTemplateVariables(query, { foo: { value: 'bar', text: 'bar' } }) as SentryEventsQuery;
87+
expect(output.projectIds).toStrictEqual(['bar', 'baz']);
88+
expect(output.environments).toStrictEqual(['bar', 'baz']);
89+
expect(output.eventsQuery).toStrictEqual('hello bar');
90+
});
91+
7792
it('should interpolate template variables for statsV2', () => {
7893
const query: SentryStatsV2Query = {
7994
refId: '',

src/app/replace.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ export const applyTemplateVariables = (query: SentryQuery, scopedVars: ScopedVar
2626
projectIds: interpolateVariableArray(query.projectIds, scopedVars),
2727
environments: interpolateVariableArray(query.environments, scopedVars),
2828
};
29+
case 'events':
30+
return {
31+
...query,
32+
eventsQuery: interpolateVariable(query.eventsQuery || '', scopedVars),
33+
projectIds: interpolateVariableArray(query.projectIds, scopedVars),
34+
environments: interpolateVariableArray(query.environments, scopedVars),
35+
};
2936
case 'statsV2':
3037
return {
3138
...query,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { EventsEditor } from './EventsEditor';
4+
import type { SentryEventsQuery } from '../../types';
5+
6+
describe('EventsEditor', () => {
7+
it('should render without error', () => {
8+
const query = {
9+
queryType: 'events',
10+
projectIds: [],
11+
environments: [],
12+
eventsQuery: '',
13+
refId: 'A',
14+
} as SentryEventsQuery;
15+
const onChange = jest.fn();
16+
const onRunQuery = jest.fn();
17+
const result = render(<EventsEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />);
18+
expect(result.container.firstChild).not.toBeNull();
19+
});
20+
});

0 commit comments

Comments
 (0)