Publishing an API
In this document, we explain when and how to publish API documentation to public Sentry docs.
Currently, Sentry's API is v0
and considered to be in draft phase. While we don't expect our public endpoints to change greatly, keep in mind that our API is still under development.
A public API is an API that is stable and well documented at https://docs.sentry.io/api/. When you make an endpoint public, you're committing to always returning at least the set of defined attributes when that endpoint is called. As a result, the attribute(s) cannot be removed from an endpoint after they are public. Additionally, the type of the attribute cannot be changed once the attribute is public. The reason the attribute and its type cannot be changed is because both external and managed clients are relying on the public attribute to be returned from that endpoint.
You can add attributes to a public endpoint.
Should I publish my API? The answer in most cases is Yes. As a developer facing company it is critical to make Sentry features available for developers to use in their various automations. Do not try to predict if an automation finds the API useful because we can't predict all the automations our clients might want to build. With that being said, there are 3 exceptions where we don't publish APIs:
- Experimental APIs: APIs that are either in development or undergoing frequent iterations, which may result in breaking changes. These are expected to be published or removed after a while.
- Private APIs: APIs that control UI features in Sentry, such as the visibility of a button.
- APIs in getsentry: If you're a Sentry engineer building APIs in the getsentry repo, they will not be published. This is the expected behavior for all the getsentry APIs, but if you have a case that you think it should be published let owners-api know and we can help with that.
Note
Always keep in mind that your APIs may still be used even if they're private
. All the APIs in the Sentry repo are accessible due to our open-source nature.
Remember, making an API public means that you can only add to it: You cannot make any breaking changes.
As a guide, use these questions:
- Is the feature for which you're making the public endpoint stable?
- Will the API change substantially in the future?
If your answers are Yes and No, you're in business - make the endpoint public. Head over to the public API checklist and ensure that your endpoint conforms to the checklist.
We use Open API Spec 3 and the drf-spectacular library to document our APIs.
API Documentation consists of:
- Owner
- Publish Status
- Sidebar Tab
- Endpoint Description
- Method Decorator
- Title
- Path and Query Parameters
- Body Parameters
- Success Responses
- Error Responses
- Sample Response Body
We utilize the drf-spectacular's extend_schema
decorator to perform the documentation. The sections below outline each step to use this decorator.
Specify an owner for the endpoint. This would be the team at Sentry responsible for maintaining and owning the endpoint. You can see the full list of teams here.
class OrganizationTeamsEndpoint(...):
owner = ApiOwner.ENTERPRISE
Declare endpoint methods public by setting their publish_status to PUBLIC
.
class OrganizationTeamsEndpoint(...):
owner = ApiOwner.ENTERPRISE
publish_status = {
'GET': ApiPublishStatus.PUBLIC,
'POST': ApiPublishStatus.PUBLIC,
'PUT': ApiPublishStatus.EXPERIMENTAL,
}
Specify the endpoint's sidebar tab by using the extend_schema
decorator on the endpoint class. You can see the current list of tags or add tags here. In the example below the endpoint is tagged in the Teams
sidebar tab.
from drf_spectacular.utils import extend_schema
@extend_schema(tags=["Teams"])
class OrganizationTeamsEndpoint(...):
owner = ApiOwner.ENTERPRISE
publish_status = {
'GET': ApiPublishStatus.PUBLIC,
'POST': ApiPublishStatus.PUBLIC,
'PUT': ApiPublishStatus.EXPERIMENTAL,
}
Specify the endpoint description in the documentation using the endpoint's docstring. Please include a description for all resources if they are not immediately clear. For example, team
and organization
don't require a description, but other vague terms like external-issue
or integration
would require a description.
def post(self, request, organization, **kwargs):
"""
Create a new team bound to an organization.
"""
We utilize another @extend_schema
decorator on the endpoint method to perform the majority of the documentation. The code below provides an example of a fully documented POST API.
@extend_schema(tags=["Teams"])
class OrganizationTeamsEndpoint(...):
owner = ApiOwner.ENTERPRISE
publish_status = {
'GET': ApiPublishStatus.PUBLIC,
'POST': ApiPublishStatus.PUBLIC,
'PUT': ApiPublishStatus.EXPERIMENTAL,
}
@extend_schema(
operation_id="Create a New Team",
parameters=[GlobalParams.ORG_ID_OR_SLUG],
request=TeamPostSerializer,
responses={
201: TeamSerializer,
400: RESPONSE_BAD_REQUEST,
403: RESPONSE_FORBIDDEN,
404: OpenApiResponse(description="A team with this slug already exists."),
},
examples=TeamExamples.CREATE_TEAM,
)
def post(self, request, organization, **kwargs):
"""
Create a new team bound to an organization.
"""
Here's description of each argument in the decorator:
operation_id: will be shown as the title of the endpoint's page in the documentation.
parameters: is a list of path and query parameters.
- You can find existing parameters in this file. Note that the
description
field inOpenApiParameter
populates the parameter's description in the documentation. - DRF serializers conveniently translate into parameters. See here for an example of this.
- Note that the ordering of the list determines the order of the parameters in the documentation. Required parameters will automatically be placed at the top of the section.
- You can find existing parameters in this file. Note that the
request: is a serializer that generates body parameters.
- It can generally just be the DRF serializer of the endpoint itself.
- The
help_text
argument of each serializer field populates the body parameter's description in the documentation.- For nested serializers, use the class docstring instead to populate the parameter's description. See here for an example of this.
- To exclude certain body parameters, you can pass
exclude_fields
to the extend_schema_serializer decorator. See here for an example of this. - The ordering of the serializer fields determines the order of the body parameters. Required parameters will automatically be placed at the top of the section.
- Note that for Model serializers, the ordering is determined by the ordering of
fields
array within the Meta class. See here for an example of this.
- Note that for Model serializers, the ordering is determined by the ordering of
- The
- If you need more customization or the endpoint doesn't use a serializer, you can use an inline_serializer to create a one-off serializer. See here for an example of this.
- For custom serializer fields, you must explicitly type them using the extend_schema_field decorator. See here for an example of this.
- If there is no request body, you can omit the
request
field entirely.
- It can generally just be the DRF serializer of the endpoint itself.
responses: include all possible success and failure HTTP response cases. You can learn more about them in the next section.
examples: specify the endpoint's sample response. We keep all our examples in this folder sorted by sidebar tags.
Note that the statement
response_only=True
is required for all examples.We currently only show one response example per endpoint but plan to support this in the future, so feel free to add multiple examples here.
Copied@extend_schema( ... examples=TeamExamples.CREATE_TEAM, ) from drf_spectacular.types import OpenApiExample class TeamExamples: CREATE_TEAM = [ OpenApiExample( # description of example, not used for anything "Create a new team", # actual response body value={"slug": "my-team", "name": "My Team"}, # the status code(s) this example applies to status_codes=["201"], # You MUST INCLUDE this for all examples response_only=True, ) ]
Specify the return type of success responses, which are used to generate the response schema and validate any included example responses. There are three ways to do this:
- If the success response is a single object instead of a list, you can pass a DRF serializer as the response. In order for this serializer to generate a schema, its
serialize
method must be typed to return a TypedDict*
For example, this sample code has the 200
status code returning an OrganizationMemberSCIMSerializer
. It's serialize
method is typed to return an OrganizationMemberSCIMSerializerResponse
TypedDict which specifies the typing of the response body.
In order to indicate optional fields that may or may not be set in the response, you must use totality and separate your TypedDict into two classes. The first class should only contain optional fields and inherit from a TypedDict with total=False
in it's class header. The second class should only contain required fields, and inherit from the first class. See here for an example of this.
For fields that may be null, use Optional
followed by the field's type. Note that a field may be both optional and null.
class ExampleResponse(TypedDict):
potentiallyNullStringField: Optional[str]
*
Similar to the request serializer, use the extend_schema_serializer directly on the TypedDict to exclude any private fields from the response schema. See here for an example of this.
- To return a list of objects or for more customization, use the inline_sentry_response_serializer. Note that the name of each one must be unique or the docs will fail testing.
from sentry.apidocs.utils import inline_sentry_response_serializer
@extend_schema(
responses={
200: inline_sentry_response_serializer(
"ListOrgTeamResponse", List[TeamSerializerResponse]
),
}
)
Note that we HIGHLY discourage returning a single TypedDict object if your endpoint utilizes an existing serializer and returns a single object. Instead, please use the first method as this ensures the documentation stays up-to-date as the endpoint evolves.
- You can also provide OpenAPI JSON if you are running into issues, although we recommend avoiding this if possible.
Specify error responses using the existing OpenApiResponse
constants in this file. You can also define your own for more detailed messages like the example below.
responses={
201: TeamSerializer,
400: RESPONSE_BAD_REQUEST,
403: RESPONSE_FORBIDDEN,
404: OpenApiResponse(description="A team with this slug already exists."),
},
You can have private attributes within a public endpoint.
As an example: https://docs.sentry.io/api/teams/retrieve-a-team/. The response has multiple attributes:
{
"id": "2",
"slug": "the-interstellar-jurisdiction"
...
}
Let's say we also return a nickname
for a team. If the data returned is not documented as a response in our public documentation, that attribute is a private attribute within a public endpoint.
If the endpoint you're modifying had previous JSON documentation, you must delete the old documentation path in this file and its corresponding JSON build in this folder.
Additionally if there are multiple request types in the same endpoint using the old JSON documentation, you must update both of them in the same PR. Updating only one request and deleting the old documentation will cause all other requests to disappear from the docs.
Commands:
make test-api-docs
builds the OpenAPI JSON, validates the schema for all examples, and runs all API docs tests.make build-api-docs
builds the OpenAPI JSON. The build will fail if there are any warnings.make diff-api-docs
produces a diff of your local OpenAPI JSON with production.make watch-api-docs
automatically rebuilds the OpenAPI JSON on change.
To see your changes in the docs locally:
In sentry
:
- Use
make watch-api-docs
to continuously build the intermediate assettests/apidocs/openapi-derefed.json
locally. - Copy the full path to
{YOUR_SYSTEM_FOLDER}/tests/apidocs/openapi-derefed.json
,
e.g. /Users/yourname/code/sentry/tests/apidocs/openapi-derefed.json
.
In sentry-docs
:
Run
OPENAPI_LOCAL_PATH=<COPIED_FULL_PATH> DISABLE_THUMBNAILS=1 yarn dev
and substitute<COPIED_FULL_PATH>
with the path to your local openapi-derefed.json.Unfortunately changes do not automatically reflect in your local server, so you will need to rerun this command on every change. We hope to add this feature in the future.
See here for detailed doc build instructions.
When you open the pull request, please add a screenshot of the page or pages you're adding.
The openapi-diff test will fail when CI runs on your pull request, this is expected and meant to highlight the diff. It is not required to merge.
Once you make changes to an endpoint and merge the change into Sentry, a series of GitHub Actions will be triggered to make your changes automatically go live:
- The openapi workflow in sentry updates the schema in sentry-api-schema with the OpenAPI build artifact.
- The cascade-to-sentry-docs workflow in sentry-api-schema reacts to the push to
main
in (1) by triggering the bump-api-schema-sha workflow in sentry-docs. - The bump-api-schema-sha workflow in sentry-docs fetches the latest commit SHA from
sentry-api-schema
and writes it into the correct file, then makes and merges a PR insentry-docs
, which kicks off a deploy via Vercel to https://docs.sentry.io/api/.
NOTE: The openapi-diff
test is supposed to fail when CI runs on your pull request as it is meant to highlight the schema changes. It is not required to merge.
Problem: TypeError: Cannot read properties of null (reading 'type')
Solution: If you have a ChoiceField
that can be null, make sure you're using the right option. allow_blank
should be used for textual choices, while allow_null
should be used for numeric and other non-textual choices.
Problem: must have required property '<property_name>'
{
"type": "Validation",
"message": "must have required property '<property_name>'",
"instancePath": "",
"schemaPath": "#/required",
"keyword": "required",
"params": {
"missingProperty": "<property_name>"
},
"examplePath": "<endpoint_path>"
}
Solution: Your example response is missing the required property <property_name>
which is specified by the response class. <property_name>
should either be made optional in the response class or added to the example response.
Problem: must NOT have additional properties
{
"type": "Validation",
"message": "must NOT have additional properties",
"instancePath": "",
"schemaPath": "#/additionalProperties",
"keyword": "additionalProperties",
"params": {
"additionalProperty": "<property_name>"
},
"examplePath": "<endpoint_path>"
}
Solution: Your example response includes a property with <property_name>
that is not included in the response class. This either needs to be added to the response class or removed from the example.
Problem: must be <type>
{
"type": "Validation",
"message": "must be <type>",
"instancePath": "/0/<property_name>",
"schemaPath": "#/items/properties/<property_name>/type",
"keyword": "type",
"params": {
"type": "<type>"
},
"examplePath": "<endpoint_path>"
}
Solution: The type of the value for a property in your example response does not match the type for the property specified by the response class. The example value or the type of the property in the response class should be fixed.
Problem: sentry.apidocs.utils.SentryApiBuildError: projects is not defined by OPENAPI_TAGS in src/sentry/apidocs/build.py for <endpoint title>
Solution: You need to add the endpoint to a sidebar tab with the decorator @extend_schema(tags=['<section title>'])
.
Problem: drf_spectacular.plumbing.UnableToProceedError' <class 'serializer_path.FooSerializer'> ... raise UnableToProceedError(hint)
Solution: Check that the response of your API documentation is using a TypedDict rather than a serializer.
If the schema looks something like this:
...
200: inline_sentry_response_serializer(
"ListDocIntegrationResponse", list[FooSerializer]
),
Then you need to change it to use a TypedDict by first typing the serializer, then updating the schema to use the TypedDict:
...
200: inline_sentry_response_serializer(
"ListDocIntegrationResponse", list[FooSerializerResponse]
),
Refer to the section above on Success Responses for more information.
Are you a Sentry user who wants an endpoint to be public?
Look at the issues on sentry with the Component: API
label. If a request has already been made to make the endpoint public, give it a thumbs up. If not, create a feature request on Sentry and add the Component: API
label.
The team responsible for the endpoint will review the stability of the endpoint. If the endpoint will not have breaking changes in future, they can determine whether to make it public.
When should an attribute be required
?
An attribute is required
if it will always be returned by the API.
What does it mean when a response doesn't have a schema?
Some endpoints have no response schema. This means that while the endpoint is public, the attributes within that endpoint can change at any time. This is a relic from migrating the documentation from our prior approach. Note that, going forward, we recommend new endpoints always provide response schema.
Can customers use private endpoints?
Yes, if they wish. However private endpoints are liable to change without notice at any time, potentially breaking the customer's code. Please be careful using them.
I have a question and it has not been answered.
No problem. Send us an email so we can answer your question.
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").