Skip to content

Commit

Permalink
Merge branch 'main' into olegyevik-sentry-datasource-select-project
Browse files Browse the repository at this point in the history
  • Loading branch information
olegpixel authored Oct 13, 2023
2 parents d7fb83b + 263e9d2 commit 369fb4f
Show file tree
Hide file tree
Showing 17 changed files with 486 additions and 201 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/dependabot-reviewer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Dependabot reviewer

on: pull_request_target

permissions:
pull-requests: write
contents: write

jobs:
call-workflow-passing-data:
uses: grafana/security-github-actions/.github/workflows/dependabot-automerge.yaml@main
# with:
# Add this to define production packages that dependabot can auto-update if the bump is minor
# packages-minor-autoupdate: '[]'
secrets: inherit
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The Sentry data source has the following requirements:

## Known limitations

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/).
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/).

## Install the Sentry data source plugin

Expand Down Expand Up @@ -65,7 +65,7 @@ datasources:
## Query the data source
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.
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.
### Sentry issues
Expand All @@ -80,6 +80,19 @@ To get the list of Sentry issues, select **Sentry Issues** as the query type. Is
| Sort By | (optional) Select the order of results you want to display. |
| Limit | (optional) Limit the number of results displayed. |
### Sentry events
To get the list of Sentry events, select **Sentry Events** as the query type. Events are filtered based on Grafana’s selected time range.
| Field | Description |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Query Type | Choose **Events** as query type. |
| Projects | (optional) Select one or more projects to filter the results. |
| Environments | (optional) Select one or more environments to filter the results. |
| Query | (optional) Enter your sentry query to get the relevant results. More on [query syntax](https://docs.sentry.io/product/sentry-basics/search/) |
| Sort By | (optional) Select the order of results you want to display. |
| Limit | (optional) Limit the number of results displayed. Max limit - 100. |
### Sentry Org stats
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.
Expand Down Expand Up @@ -113,4 +126,4 @@ Annotations give you the ability to overlay Sentry issues on graphs. In the anno
- Add [Annotations](https://grafana.com/docs/grafana/latest/dashboards/annotations/).
- Configure and use [Templates and variables](https://grafana.com/docs/grafana/latest/variables/).
- Add [Transformations](https://grafana.com/docs/grafana/latest/panels/transformations/).
- Set up alerting; refer to [Alerts overview](https://grafana.com/docs/grafana/latest/alerting/).
- Set up alerting; refer to [Alerts overview](https://grafana.com/docs/grafana/latest/alerting/).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@swc/core": "1.3.75",
"@swc/helpers": "^0.5.0",
"@swc/jest": "^0.2.26",
"@testing-library/dom": "^7.31.0",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^14.5.1",
Expand Down
26 changes: 26 additions & 0 deletions pkg/plugin/handlers_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type SentryQuery struct {
IssuesQuery string `json:"issuesQuery,omitempty"`
IssuesSort string `json:"issuesSort,omitempty"`
IssuesLimit int64 `json:"issuesLimit,omitempty"`
EventsQuery string `json:"eventsQuery,omitempty"`
EventsSort string `json:"eventsSort,omitempty"`
EventsLimit int64 `json:"eventsLimit,omitempty"`
StatsCategory []string `json:"statsCategory,omitempty"`
StatsFields []string `json:"statsFields,omitempty"`
StatsGroupBy []string `json:"statsGroupBy,omitempty"`
Expand Down Expand Up @@ -73,6 +76,29 @@ func QueryData(ctx context.Context, pCtx backend.PluginContext, backendQuery bac
}
frame = UpdateFrameMeta(frame, executedQueryString, query, client.BaseURL, client.OrgSlug)
response.Frames = append(response.Frames, frame)
case "events":
if client.OrgSlug == "" {
return GetErrorResponse(response, "", ErrorInvalidOrganizationSlug)
}
events, executedQueryString, err := client.GetEvents(sentry.GetEventsInput{
OrganizationSlug: client.OrgSlug,
ProjectIds: query.ProjectIds,
Environments: query.Environments,
Query: query.EventsQuery,
Sort: query.EventsSort,
Limit: query.EventsLimit,
From: backendQuery.TimeRange.From,
To: backendQuery.TimeRange.To,
})
if err != nil {
return GetErrorResponse(response, executedQueryString, err)
}
frame, err := framestruct.ToDataFrame(GetFrameName("Events", backendQuery.RefID), events)
if err != nil {
return GetErrorResponse(response, executedQueryString, err)
}
frame = UpdateFrameMeta(frame, executedQueryString, query, client.BaseURL, client.OrgSlug)
response.Frames = append(response.Frames, frame)
case "statsV2":
if client.OrgSlug == "" {
return GetErrorResponse(response, "", ErrorInvalidOrganizationSlug)
Expand Down
11 changes: 11 additions & 0 deletions pkg/plugin/handlers_query_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ func UpdateFrameMeta(frame *data.Frame, executedQueryString string, query Sentry
},
}
}
if frame.Fields[i].Name == "ID" && query.QueryType == "events" {
frame.Fields[i].Config = &data.FieldConfig{
Links: []data.DataLink{
{
Title: "Open in Sentry",
URL: fmt.Sprintf("https://%s.sentry.io/discover/${__data.fields[\"Project\"]}:${__data.fields[\"ID\"]}/", orgSlug),
TargetBlank: true,
},
},
}
}
}
return frame
}
Expand Down
53 changes: 53 additions & 0 deletions pkg/plugin/handlers_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,58 @@ func TestSentryDatasource_QueryData(t *testing.T) {
require.Equal(t, "Category=foo2, Reason=bar", res.Frames[0].Fields[3].Labels.String())
require.Equal(t, "Category=foo2, Reason=bar", res.Frames[0].Fields[4].Labels.String())
})
t.Run("valid events query should produce correct result", func(t *testing.T) {
sc := NewFakeClient(fakeDoer{Body: `{
"data": [
{
"id": "event_id_1",
"title": "event_title_1",
"message": "event_description",
"project.name": "project_name_1"
},
{
"id": "event_id_2",
"title": "event_title_2",
"message": "event_description",
"project.name": "project_name_1"
},
{
"id": "event_id_3",
"title": "event_title_3",
"message": "event_description",
"project.name": "project_name_2"
}
],
"meta": {
"fields": {
"id": "string",
"title": "string",
"message": "string",
"project.name": "string"
}
}
}`})
query := `{
"queryType" : "events",
"projectIds" : ["project_id"],
"environments" : ["dev"],
"eventsQuery" : "event_query",
"eventsSort" : "event_sort",
"eventsLimit" : 10
}`
res := plugin.QueryData(context.Background(), backend.PluginContext{}, backend.DataQuery{RefID: "A", JSON: []byte(query)}, *sc)

// Assert that there are no errors and the data frame is correctly formed
assert.Nil(t, res.Error)
require.Equal(t, 1, len(res.Frames))
assert.Equal(t, "Events (A)", res.Frames[0].Name)

// Assert the content of the data frame
frame := res.Frames[0]
require.NotNil(t, frame.Fields)
require.Equal(t, 11, 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)
})
}
84 changes: 84 additions & 0 deletions pkg/sentry/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package sentry

import (
"fmt"
"net/url"
"strconv"
"time"
)

var reqFields = [...]string{
"id",
"title",
"project",
"project.id",
"release",
"count()",
"epm()",
"last_seen()",
"level",
"event.type",
"platform",
}

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
Query string
From time.Time
To time.Time
Sort string
Limit int64
}

func (gei *GetEventsInput) ToQuery() string {
urlPath := fmt.Sprintf("/api/0/organizations/%s/events/?", gei.OrganizationSlug)
if gei.Limit < 1 || gei.Limit > 100 {
gei.Limit = 100
}
params := url.Values{}
params.Set("query", gei.Query)
params.Set("start", gei.From.Format("2006-01-02T15:04:05"))
params.Set("end", gei.To.Format("2006-01-02T15:04:05"))
if gei.Sort != "" {
params.Set("sort", gei.Sort)
}
params.Set("per_page", strconv.FormatInt(gei.Limit, 10))
for _, field := range reqFields {
params.Add("field", field)
}
for _, projectId := range gei.ProjectIds {
params.Add("project", projectId)
}
for _, environment := range gei.Environments {
params.Add("environment", environment)
}
return urlPath + params.Encode()
}

func (sc *SentryClient) GetEvents(gei GetEventsInput) ([]SentryEvent, string, error) {
var out SentryEvents
executedQueryString := gei.ToQuery()
err := sc.Fetch(executedQueryString, &out)
return out.Data, sc.BaseURL + executedQueryString, err
}
17 changes: 16 additions & 1 deletion src/app/replace.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ScopedVars } from '@grafana/data';
import * as runtime from '@grafana/runtime';
import { SentryIssuesQuery, SentryStatsV2Query } from 'types';
import { SentryIssuesQuery, SentryEventsQuery, SentryStatsV2Query } from 'types';
import { applyTemplateVariables, replaceProjectIDs } from './replace';

describe('replace', () => {
Expand Down Expand Up @@ -74,6 +74,21 @@ describe('replace', () => {
expect(output.issuesQuery).toStrictEqual('hello bar');
});

it('should interpolate template variables for events', () => {
const query: SentryEventsQuery = {
refId: '',
queryType: 'events',
projectIds: ['${foo}', 'baz'],
environments: ['${foo}', 'baz'],
eventsQuery: 'hello ${foo}',
};

const output = applyTemplateVariables(query, { foo: { value: 'bar', text: 'bar' } }) as SentryEventsQuery;
expect(output.projectIds).toStrictEqual(['bar', 'baz']);
expect(output.environments).toStrictEqual(['bar', 'baz']);
expect(output.eventsQuery).toStrictEqual('hello bar');
});

it('should interpolate template variables for statsV2', () => {
const query: SentryStatsV2Query = {
refId: '',
Expand Down
7 changes: 7 additions & 0 deletions src/app/replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ export const applyTemplateVariables = (query: SentryQuery, scopedVars: ScopedVar
projectIds: interpolateVariableArray(query.projectIds, scopedVars),
environments: interpolateVariableArray(query.environments, scopedVars),
};
case 'events':
return {
...query,
eventsQuery: interpolateVariable(query.eventsQuery || '', scopedVars),
projectIds: interpolateVariableArray(query.projectIds, scopedVars),
environments: interpolateVariableArray(query.environments, scopedVars),
};
case 'statsV2':
return {
...query,
Expand Down
20 changes: 20 additions & 0 deletions src/components/query-editor/EventsEditor.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { render } from '@testing-library/react';
import { EventsEditor } from './EventsEditor';
import type { SentryEventsQuery } from '../../types';

describe('EventsEditor', () => {
it('should render without error', () => {
const query = {
queryType: 'events',
projectIds: [],
environments: [],
eventsQuery: '',
refId: 'A',
} as SentryEventsQuery;
const onChange = jest.fn();
const onRunQuery = jest.fn();
const result = render(<EventsEditor query={query} onChange={onChange} onRunQuery={onRunQuery} />);
expect(result.container.firstChild).not.toBeNull();
});
});
Loading

0 comments on commit 369fb4f

Please sign in to comment.