From 56ec58d45beec4c5a5e213262a87176c1af26514 Mon Sep 17 00:00:00 2001 From: Leander Rodrigues Date: Fri, 5 Dec 2025 16:02:37 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20publishing=20details?= =?UTF-8?q?=20for=20data-forwarding-index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apidocs/examples/integration_examples.py | 147 ++++++++++++++++++ .../api/endpoints/data_forwarding_index.py | 11 +- .../rest_framework/data_forwarder.py | 28 +++- 3 files changed, 177 insertions(+), 9 deletions(-) diff --git a/src/sentry/apidocs/examples/integration_examples.py b/src/sentry/apidocs/examples/integration_examples.py index 32a712fc4f3b6c..28761729a37efc 100644 --- a/src/sentry/apidocs/examples/integration_examples.py +++ b/src/sentry/apidocs/examples/integration_examples.py @@ -754,3 +754,150 @@ class IntegrationExamples: response_only=True, ) ] + + LIST_DATA_FORWARDERS = [ + OpenApiExample( + "List all data forwarders for an organization", + value=[ + [ + { + "id": "1", + "organizationId": "1", + "isEnabled": True, + "enrollNewProjects": True, + "enrolledProjects": [], + "provider": "sqs", + "config": { + "region": "us-east-1", + "queue_url": "https://sqs.us-east-1.amazonaws.com/01234567890/sentry-errors.fifo", + "s3_bucket": "sentry-errors-bucket", + "access_key": "AKIAIOSFODNN7EXAMPLE", + "secret_key": "wJalrXUtnFEMI1K7MDENGSbPxRfiCYEXAMPLEKEY", + "message_group_id": "sentry-errors", + }, + "projectConfigs": [], + "dateAdded": "2025-11-01T00:00:00.000000Z", + "dateUpdated": "2025-11-01T00:00:00.000000Z", + }, + { + "id": "2", + "organizationId": "1", + "isEnabled": True, + "enrollNewProjects": False, + "enrolledProjects": [ + {"id": "1", "slug": "proj-1", "platform": "javascript-react"}, + {"id": "2", "slug": "proj-2", "platform": "python-flask"}, + ], + "provider": "segment", + "config": {"write_key": "itA5bLOPNxccvZ9ON1NYg9EXAMPLEKEY"}, + "projectConfigs": [ + { + "id": "1", + "isEnabled": True, + "dataForwarderId": "2", + "project": { + "id": "1", + "slug": "proj-1", + "platform": "javascript-react", + }, + "overrides": {}, + "effectiveConfig": { + "write_key": "itA5bLOPNxccvZ9ON1NYg9EXAMPLEKEY" + }, + "dateAdded": "2025-11-01T00:00:00.000000Z", + "dateUpdated": "2025-11-01T00:00:00.000000Z", + }, + { + "id": "2", + "isEnabled": True, + "dataForwarderId": "2", + "project": { + "id": "2", + "slug": "proj-2", + "platform": "python-flask", + }, + "overrides": {}, + "effectiveConfig": { + "write_key": "itA5bLOPNxccvZ9ON1NYg9EXAMPLEKEY" + }, + "dateAdded": "2025-11-01T00:00:00.000000Z", + "dateUpdated": "2025-11-01T00:00:00.000000Z", + }, + ], + "dateAdded": "2025-11-01T00:00:00.000000Z", + "dateUpdated": "2025-11-01T00:00:00.000000Z", + }, + { + "id": "3", + "organizationId": "1", + "isEnabled": True, + "enrollNewProjects": True, + "enrolledProjects": [ + {"id": "1", "slug": "proj-1", "platform": "javascript-react"}, + ], + "provider": "splunk", + "config": { + "index": "main", + "token": "ab13cdef-45aa-1bcd-a123-bcEXAMPLEKEY", + "source": "sentry", + "instance_url": "https://prd-a-abcde.splunkcloud.com:8088", + }, + "projectConfigs": [ + { + "id": "3", + "isEnabled": True, + "dataForwarderId": "3", + "project": { + "id": "1", + "slug": "proj-1", + "platform": "javascript-react", + }, + "overrides": { + "source": "sentry-custom", + }, + "effectiveConfig": { + "index": "main", + "token": "ab13cdef-45aa-1bcd-a123-bcEXAMPLEKEY", + "source": "sentry-custom", + "instance_url": "https://prd-a-abcde.splunkcloud.com:8088", + }, + "dateAdded": "2025-11-01T00:00:00.000000Z", + "dateUpdated": "2025-11-01T00:00:00.000000Z", + } + ], + "dateAdded": "2025-11-01T00:00:00.000000Z", + "dateUpdated": "2025-11-01T00:00:00.000000Z", + }, + ] + ], + status_codes=["200"], + response_only=True, + ) + ] + + CREATE_DATA_FORWARDER = [ + OpenApiExample( + "Create a data forwarder for an organization", + value={ + "id": "1", + "organizationId": "1", + "isEnabled": True, + "enrollNewProjects": True, + "enrolledProjects": [], + "provider": "sqs", + "config": { + "region": "us-east-1", + "queue_url": "https://sqs.us-east-1.amazonaws.com/01234567890/sentry-errors.fifo", + "s3_bucket": "sentry-errors-bucket", + "access_key": "AKIAIOSFODNN7EXAMPLE", + "secret_key": "wJalrXUtnFEMI1K7MDENGSbPxRfiCYEXAMPLEKEY", + "message_group_id": "sentry-errors", + }, + "projectConfigs": [], + "dateAdded": "2025-11-01T00:00:00.000000Z", + "dateUpdated": "2025-11-01T00:00:00.000000Z", + }, + status_codes=["200"], + response_only=True, + ) + ] diff --git a/src/sentry/integrations/api/endpoints/data_forwarding_index.py b/src/sentry/integrations/api/endpoints/data_forwarding_index.py index 44538b9d5e54eb..0c3223cd7f79d3 100644 --- a/src/sentry/integrations/api/endpoints/data_forwarding_index.py +++ b/src/sentry/integrations/api/endpoints/data_forwarding_index.py @@ -13,8 +13,10 @@ from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.paginator import OffsetPaginator from sentry.api.serializers import serialize -from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_CONFLICT, RESPONSE_FORBIDDEN +from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_FORBIDDEN +from sentry.apidocs.examples.integration_examples import IntegrationExamples from sentry.apidocs.parameters import GlobalParams +from sentry.apidocs.utils import inline_sentry_response_serializer from sentry.integrations.api.serializers.models.data_forwarder import ( DataForwarderSerializer as DataForwarderModelSerializer, ) @@ -52,8 +54,11 @@ def convert_args(self, request: Request, *args, **kwargs): operation_id="Retrieve Data Forwarding Configurations for an Organization", parameters=[GlobalParams.ORG_ID_OR_SLUG], responses={ - 200: DataForwarderModelSerializer, + 200: inline_sentry_response_serializer( + "ListDataForwarderResponse", list[DataForwarderSerializer] + ) }, + examples=IntegrationExamples.LIST_DATA_FORWARDERS, ) @set_referrer_policy("strict-origin-when-cross-origin") @method_decorator(never_cache) @@ -75,8 +80,8 @@ def get(self, request: Request, organization) -> Response: 201: DataForwarderModelSerializer, 400: RESPONSE_BAD_REQUEST, 403: RESPONSE_FORBIDDEN, - 409: RESPONSE_CONFLICT, }, + examples=IntegrationExamples.CREATE_DATA_FORWARDER, ) @set_referrer_policy("strict-origin-when-cross-origin") @method_decorator(never_cache) diff --git a/src/sentry/integrations/api/serializers/rest_framework/data_forwarder.py b/src/sentry/integrations/api/serializers/rest_framework/data_forwarder.py index 137ea9165900ba..9ab8a4c84e4c5a 100644 --- a/src/sentry/integrations/api/serializers/rest_framework/data_forwarder.py +++ b/src/sentry/integrations/api/serializers/rest_framework/data_forwarder.py @@ -40,19 +40,35 @@ class SplunkConfig(TypedDict, total=False): class DataForwarderSerializer(Serializer): - organization_id = serializers.IntegerField() - is_enabled = serializers.BooleanField(default=True) - enroll_new_projects = serializers.BooleanField(default=False) + organization_id = serializers.IntegerField( + help_text="The ID of the organization related to the data forwarder." + ) + is_enabled = serializers.BooleanField( + default=True, help_text="Whether the data forwarder is enabled." + ) + enroll_new_projects = serializers.BooleanField( + default=False, + help_text="Whether to enroll new projects automatically, after they're created.", + ) provider = serializers.ChoiceField( choices=[ (DataForwarderProviderSlug.SEGMENT, "Segment"), (DataForwarderProviderSlug.SQS, "Amazon SQS"), (DataForwarderProviderSlug.SPLUNK, "Splunk"), - ] + ], + help_text='The provider of the data forwarder. One of "segment", "sqs", or "splunk".', + ) + config = serializers.DictField( + child=serializers.CharField(allow_blank=True), + default=dict, + help_text="The configuration for the data forwarder.", ) - config = serializers.DictField(child=serializers.CharField(allow_blank=True), default=dict) project_ids = serializers.ListField( - child=serializers.IntegerField(), allow_empty=True, required=False, default=list + child=serializers.IntegerField(), + allow_empty=True, + required=False, + default=list, + help_text="The IDs of the projects to attach the data forwarder to.", ) def validate_config(self, config) -> SQSConfig | SegmentConfig | SplunkConfig: From 44a740b152f48716897bf441f1774a8d40398642 Mon Sep 17 00:00:00 2001 From: Leander Rodrigues Date: Fri, 5 Dec 2025 16:06:03 -0500 Subject: [PATCH 2/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20publishing=20details?= =?UTF-8?q?=20for=20data-forwarding-details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/sentry/apidocs/examples/integration_examples.py | 4 ++-- src/sentry/apidocs/parameters.py | 10 ++++++++++ .../api/endpoints/data_forwarding_details.py | 8 +++++--- .../api/endpoints/data_forwarding_index.py | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/sentry/apidocs/examples/integration_examples.py b/src/sentry/apidocs/examples/integration_examples.py index 28761729a37efc..65151a09526474 100644 --- a/src/sentry/apidocs/examples/integration_examples.py +++ b/src/sentry/apidocs/examples/integration_examples.py @@ -875,9 +875,9 @@ class IntegrationExamples: ) ] - CREATE_DATA_FORWARDER = [ + SINGLE_DATA_FORWARDER = [ OpenApiExample( - "Create a data forwarder for an organization", + "A data forwarder for an organization", value={ "id": "1", "organizationId": "1", diff --git a/src/sentry/apidocs/parameters.py b/src/sentry/apidocs/parameters.py index 47c7b137595c66..832c8ec58f6385 100644 --- a/src/sentry/apidocs/parameters.py +++ b/src/sentry/apidocs/parameters.py @@ -544,6 +544,16 @@ class MetricAlertParams: ) +class DataForwarderParams: + DATA_FORWARDER_ID = OpenApiParameter( + name="data_forwarder_id", + location="path", + required=True, + type=int, + description="The ID of the data forwarder you'd like to query.", + ) + + class SentryAppParams: SENTRY_APP_ID_OR_SLUG = OpenApiParameter( name="sentry_app_id_or_slug", diff --git a/src/sentry/integrations/api/endpoints/data_forwarding_details.py b/src/sentry/integrations/api/endpoints/data_forwarding_details.py index 7b47233cb9b9ab..f3e4539ac777d6 100644 --- a/src/sentry/integrations/api/endpoints/data_forwarding_details.py +++ b/src/sentry/integrations/api/endpoints/data_forwarding_details.py @@ -18,7 +18,8 @@ from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize from sentry.apidocs.constants import RESPONSE_BAD_REQUEST, RESPONSE_FORBIDDEN, RESPONSE_NO_CONTENT -from sentry.apidocs.parameters import GlobalParams +from sentry.apidocs.examples.integration_examples import IntegrationExamples +from sentry.apidocs.parameters import DataForwarderParams, GlobalParams from sentry.integrations.api.serializers.models.data_forwarder import ( DataForwarderSerializer as DataForwarderModelSerializer, ) @@ -290,13 +291,14 @@ def _update_single_project_configuration( @method_decorator(never_cache) @extend_schema( operation_id="Update a Data Forwarding Configuration for an Organization", - parameters=[GlobalParams.ORG_ID_OR_SLUG], + parameters=[GlobalParams.ORG_ID_OR_SLUG, DataForwarderParams.DATA_FORWARDER_ID], request=DataForwarderSerializer, responses={ 200: DataForwarderModelSerializer, 400: RESPONSE_BAD_REQUEST, 403: RESPONSE_FORBIDDEN, }, + examples=IntegrationExamples.SINGLE_DATA_FORWARDER, ) def put( self, request: Request, organization: Organization, data_forwarder: DataForwarder @@ -332,7 +334,7 @@ def put( @extend_schema( operation_id="Delete a Data Forwarding Configuration for an Organization", - parameters=[GlobalParams.ORG_ID_OR_SLUG], + parameters=[GlobalParams.ORG_ID_OR_SLUG, DataForwarderParams.DATA_FORWARDER_ID], responses={ 204: RESPONSE_NO_CONTENT, 403: RESPONSE_FORBIDDEN, diff --git a/src/sentry/integrations/api/endpoints/data_forwarding_index.py b/src/sentry/integrations/api/endpoints/data_forwarding_index.py index 0c3223cd7f79d3..f67b9e83787e0d 100644 --- a/src/sentry/integrations/api/endpoints/data_forwarding_index.py +++ b/src/sentry/integrations/api/endpoints/data_forwarding_index.py @@ -81,7 +81,7 @@ def get(self, request: Request, organization) -> Response: 400: RESPONSE_BAD_REQUEST, 403: RESPONSE_FORBIDDEN, }, - examples=IntegrationExamples.CREATE_DATA_FORWARDER, + examples=IntegrationExamples.SINGLE_DATA_FORWARDER, ) @set_referrer_policy("strict-origin-when-cross-origin") @method_decorator(never_cache) From 5b2a378d51adc47d6ee6ee0569c614e0b8d89a80 Mon Sep 17 00:00:00 2001 From: Leander Rodrigues Date: Fri, 5 Dec 2025 16:27:21 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Allow=20clearing=20empty=20o?= =?UTF-8?q?verrides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/projectOverrideForm.tsx | 16 +++++++++++++++- .../organizationDataForwarding/util/forms.tsx | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/static/app/views/settings/organizationDataForwarding/components/projectOverrideForm.tsx b/static/app/views/settings/organizationDataForwarding/components/projectOverrideForm.tsx index a99dd9797ff50e..756ebc7851484e 100644 --- a/static/app/views/settings/organizationDataForwarding/components/projectOverrideForm.tsx +++ b/static/app/views/settings/organizationDataForwarding/components/projectOverrideForm.tsx @@ -10,6 +10,7 @@ import JsonForm from 'sentry/components/forms/jsonForm'; import FormModel from 'sentry/components/forms/model'; import Panel from 'sentry/components/panels/panel'; import PanelHeader from 'sentry/components/panels/panelHeader'; +import {IconRefresh} from 'sentry/icons'; import {IconInfo} from 'sentry/icons/iconInfo'; import {t} from 'sentry/locale'; import type {AvatarProject} from 'sentry/types/project'; @@ -75,7 +76,20 @@ export function ProjectOverrideForm({ )} renderFooter={() => ( - + + diff --git a/static/app/views/settings/organizationDataForwarding/util/forms.tsx b/static/app/views/settings/organizationDataForwarding/util/forms.tsx index 078a11fe882499..64bf0a47582462 100644 --- a/static/app/views/settings/organizationDataForwarding/util/forms.tsx +++ b/static/app/views/settings/organizationDataForwarding/util/forms.tsx @@ -188,7 +188,7 @@ const SQS_GLOBAL_CONFIGURATION_FORM: JsonFormObject = { label: 'Secret Key', type: 'text', required: true, - help: 'Only visible once when the access key is created..', + help: 'Only visible once when the access key is created.', placeholder: 'e.g. wJalrXUtnFEMI1K7MDENGSbPxRfiCYEXAMPLEKEY', }, { @@ -241,7 +241,7 @@ const SPLUNK_GLOBAL_CONFIGURATION_FORM: JsonFormObject = { type: 'text', required: true, help: 'The token generated for your HTTP Event Collector.', - placeholder: 'e.g. 1234567890abcdef1234567890abcdef', + placeholder: 'e.g. ab13cdef-45aa-1bcd-a123-bcEXAMPLEKEY', }, { name: 'index',