Skip to content

feat(discover): Add docs for organization_events #34768

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

Merged
merged 11 commits into from
Jun 2, 2022
77 changes: 75 additions & 2 deletions src/sentry/api/endpoints/organization_events.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

import sentry_sdk
from drf_spectacular.utils import OpenApiExample, OpenApiResponse, extend_schema
from rest_framework.exceptions import ParseError
from rest_framework.request import Request
from rest_framework.response import Response
Expand All @@ -9,6 +10,9 @@
from sentry.api.bases import NoProjects, OrganizationEventsV2EndpointBase
from sentry.api.paginator import GenericOffsetPaginator
from sentry.api.utils import InvalidParams
from sentry.apidocs import constants as api_constants
from sentry.apidocs.parameters import GLOBAL_PARAMS, VISIBILITY_PARAMS
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.search.events.fields import is_function
from sentry.snuba import discover, metrics_enhanced_performance

Expand Down Expand Up @@ -47,6 +51,8 @@


class OrganizationEventsV2Endpoint(OrganizationEventsV2EndpointBase):
"""Deprecated in favour of OrganizationEventsEndpoint"""

def get(self, request: Request, organization) -> Response:
if not self.has_feature(organization, request):
return Response(status=404)
Expand Down Expand Up @@ -127,10 +133,77 @@ def data_fn(offset, limit):
)


@extend_schema(tags=["Discover"])
class OrganizationEventsEndpoint(OrganizationEventsV2EndpointBase):
private = True

public = {"GET"}

@extend_schema(
operation_id="Query Discover Events in Table Format",
parameters=[
VISIBILITY_PARAMS.QUERY,
VISIBILITY_PARAMS.FIELD,
VISIBILITY_PARAMS.SORT,
VISIBILITY_PARAMS.PER_PAGE,
GLOBAL_PARAMS.STATS_PERIOD,
GLOBAL_PARAMS.START,
GLOBAL_PARAMS.END,
GLOBAL_PARAMS.PROJECT,
GLOBAL_PARAMS.ENVIRONMENT,
],
responses={
200: inline_sentry_response_serializer(
"OrganizationEventsResponseDict", discover.EventsResponse
),
400: OpenApiResponse(description="Invalid Query"),
404: api_constants.RESPONSE_NOTFOUND,
},
examples=[
OpenApiExample(
"Success",
value={
"data": [
{
"count_if(transaction.duration,greater,300)": 5,
"count()": 10,
"equation|count_if(transaction.duration,greater,300) / count() * 100": 50,
"transaction": "foo",
},
{
"count_if(transaction.duration,greater,300)": 3,
"count()": 20,
"equation|count_if(transaction.duration,greater,300) / count() * 100": 15,
"transaction": "bar",
},
{
"count_if(transaction.duration,greater,300)": 8,
"count()": 40,
"equation|count_if(transaction.duration,greater,300) / count() * 100": 20,
"transaction": "baz",
},
],
"meta": {
"fields": {
"count_if(transaction.duration,greater,300)": "integer",
"count()": "integer",
"equation|count_if(transaction.duration,greater,300) / count() * 100": "number",
"transaction": "string",
},
},
},
)
],
)
def get(self, request: Request, organization) -> Response:
"""
Retrieves discover (also known as events) data for a given organization.

**Note**: This endpoint is intended to get a table of results, and is not for doing a full export of data sent to
Sentry.

The `field` query parameter determines what fields will be selected in the `data` and `meta` keys of the endpoint response.
- The `data` key contains a list of results row by row that match the `query` made
- The `meta` key contains information about the response, including the unit or type of the fields requested
"""
if not self.has_feature(organization, request):
return Response(status=404)

Expand Down
12 changes: 12 additions & 0 deletions src/sentry/apidocs/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,16 @@ def get_old_json_paths(filename: str) -> json.JSONData:
"url": "https://github.com/getsentry/sentry-docs/issues/new/?title=API%20Documentation%20Error:%20/api/integration-platform/&template=api_error_template.md",
},
},
{
# Not using visibility here since users won't be aware what that is, this "name" is only used in the URL so not
# a big deal that its missing Performance
"name": "Discover",
"x-sidebar-name": "Discover & Performance",
"description": "Discover and Performance allow you to slice and dice your Error and Transaction events",
"x-display-description": True,
"externalDocs": {
"description": "Found an error? Let us know.",
"url": "https://github.com/getsentry/sentry-docs/issues/new/?title=API%20Documentation%20Error:%20/api/integration-platform/&template=api_error_template.md",
},
},
]
90 changes: 90 additions & 0 deletions src/sentry/apidocs/parameters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from rest_framework import serializers

Expand All @@ -17,6 +18,50 @@ class GLOBAL_PARAMS:
type=str,
location="path",
)
STATS_PERIOD = OpenApiParameter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think OpenApiParameter maps to URL parameters, not query parameters

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drf serializers map to query parameters

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, its what the location param on the OpenApiParameter is for so you can define whether the parameter is a part of the path or the query

Based on docs you can do header, cookie or form parameters too:
https://drf-spectacular.readthedocs.io/en/latest/drf_yasg.html?highlight=location#parameter-location

name="statsPeriod",
location="query",
required=False,
type=str,
description="""The period of time for the query, will override the start & end parameters, a number followed by one of:
- `d` for days
- `h` for hours
- `m` for minutes
- `s` for seconds
- `w` for weeks

For example `24h`, to mean query data starting from 24 hours ago to now.""",
)
START = OpenApiParameter(
name="start",
location="query",
required=False,
type=OpenApiTypes.DATETIME,
description="The start of the period of time for the query, expected in ISO-8601 format. For example `2001-12-14T12:34:56.7890`",
)
END = OpenApiParameter(
name="end",
location="query",
required=False,
type=OpenApiTypes.DATETIME,
description="The end of the period of time for the query, expected in ISO-8601 format. For example `2001-12-14T12:34:56.7890`",
)
PROJECT = OpenApiParameter(
name="project",
location="query",
required=False,
many=True,
type=int,
description="The ids of projects to filter by. `-1` means all available projects. If this parameter is omitted, the request will default to using 'My Projects'",
)
ENVIRONMENT = OpenApiParameter(
name="environment",
location="query",
required=False,
many=True,
type=str,
description="The name of environments to filter by.",
)


class SCIM_PARAMS:
Expand Down Expand Up @@ -46,6 +91,51 @@ class ISSUE_ALERT_PARAMS:
)


class VISIBILITY_PARAMS:
QUERY = OpenApiParameter(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a visibility-only param? I think we use it in several places outside of visibility.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a specific Query to Visibility (see the bit about the search syntax)

name="query",
location="query",
required=False,
type=str,
description="""The search filter for your query, read more about query syntax [here](https://docs.sentry.io/product/sentry-basics/search/)

example: `query=(transaction:foo AND release:abc) OR (transaction:[bar,baz] AND release:def)`
""",
)
FIELD = OpenApiParameter(
name="field",
location="query",
required=True,
type=str,
many=True,
description="""The fields, functions, or equations to request for the query. At most 20 fields can be selected per request. Each field can be one of the following types:
- A built-in key field. See possible fields in the [properties table](/product/sentry-basics/search/searchable-properties/#properties-table), under any field that is an event property
- example: `field=transaction`
- A tag. Tags should use the `tag[]` formatting to avoid ambiguity with any fields
- example: `field=tag[isEnterprise]`
- A function which will be in the format of `function_name(parameters,...)`. See possible functions in the [query builder documentation](/product/discover-queries/query-builder/#stacking-functions)
- when a function is included, Discover will group by any tags or fields
- example: `field=count_if(transaction.duration,greater,300)`
- An equation when prefixed with `equation|`. Read more about [equations here](https://docs.sentry.io/product/discover-queries/query-builder/query-equations/)
- example: `field=equation|count_if(transaction.duration,greater,300) / count() * 100`
""",
)
SORT = OpenApiParameter(
name="sort",
location="query",
required=False,
type=str,
description="What to order the results of the query by. Must be something in the `field` list, excluding equations.",
)
PER_PAGE = OpenApiParameter(
name="per_page",
location="query",
required=False,
type=int,
description="Limit the number of rows to return in the result. Default and maximum allowed is 100.",
)


class CURSOR_QUERY_PARAM(serializers.Serializer): # type: ignore
cursor = serializers.CharField(
help_text="A pointer to the last object fetched and its' sort order; used to retrieve the next or previous results.",
Expand Down
42 changes: 30 additions & 12 deletions src/sentry/snuba/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
from collections import namedtuple
from copy import deepcopy
from datetime import timedelta
from typing import Dict, Optional, Sequence
from typing import Any, Dict, List, Optional, Sequence

import sentry_sdk
from dateutil.parser import parse as parse_datetime
from snuba_sdk.conditions import Condition, Op
from snuba_sdk.function import Function
from typing_extensions import TypedDict

from sentry.discover.arithmetic import categorize_columns
from sentry.models import Group
Expand Down Expand Up @@ -67,6 +68,16 @@
PaginationResult = namedtuple("PaginationResult", ["next", "previous", "oldest", "latest"])
FacetResult = namedtuple("FacetResult", ["key", "value", "count"])


class EventsMeta(TypedDict):
fields: Dict[str, str]


class EventsResponse(TypedDict):
data: List[Dict[str, Any]]
meta: EventsMeta


resolve_discover_column = resolve_column(Dataset.Discover)

OTHER_KEY = "Other"
Expand Down Expand Up @@ -128,14 +139,16 @@ def zerofill(data, start, end, rollup, orderby):
return rv


def transform_results(results, function_alias_map, translated_columns, snuba_filter):
def transform_results(
results, function_alias_map, translated_columns, snuba_filter
) -> EventsResponse:
results = transform_data(results, translated_columns, snuba_filter)
results["meta"] = transform_meta(results, function_alias_map)
return results


def transform_meta(results, function_alias_map):
meta = {
def transform_meta(results: EventsResponse, function_alias_map) -> Dict[str, str]:
meta: Dict[str, str] = {
value["name"]: get_json_meta_type(
value["name"], value.get("type"), function_alias_map.get(value["name"])
)
Expand All @@ -149,14 +162,15 @@ def transform_meta(results, function_alias_map):
return meta


def transform_data(result, translated_columns, snuba_filter):
def transform_data(result, translated_columns, snuba_filter) -> EventsResponse:
"""
Transform internal names back to the public schema ones.

When getting timeseries results via rollup, this function will
zerofill the output results.
"""
for col in result["meta"]:
final_result: EventsResponse = {"data": result["data"], "meta": result["meta"]}
for col in final_result["meta"]:
# Translate back column names that were converted to snuba format
col["name"] = translated_columns.get(col["name"], col["name"])

Expand All @@ -174,19 +188,23 @@ def get_row(row):

return transformed

result["data"] = [get_row(row) for row in result["data"]]
final_result["data"] = [get_row(row) for row in final_result["data"]]

if snuba_filter and snuba_filter.rollup and snuba_filter.rollup > 0:
rollup = snuba_filter.rollup
with sentry_sdk.start_span(
op="discover.discover", description="transform_results.zerofill"
) as span:
span.set_data("result_count", len(result.get("data", [])))
result["data"] = zerofill(
result["data"], snuba_filter.start, snuba_filter.end, rollup, snuba_filter.orderby
span.set_data("result_count", len(final_result.get("data", [])))
final_result["data"] = zerofill(
final_result["data"],
snuba_filter.start,
snuba_filter.end,
rollup,
snuba_filter.orderby,
)

return result
return final_result


def transform_tips(tips):
Expand All @@ -213,7 +231,7 @@ def query(
conditions=None,
functions_acl=None,
transform_alias_to_input_format=False,
):
) -> EventsResponse:
"""
High-level API for doing arbitrary user queries against events.

Expand Down