Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
# Project Management Integrations Feature Parity Flags
manager.add("organizations:integrations-github_enterprise-project-management", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
manager.add("organizations:integrations-gitlab-project-management", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True)
# Temporary: log full Jira Cloud `issue.updated` webhook payloads so we can design project-change link rewriting.
manager.add("organizations:jira-issue-updated-payload-logging", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable inviting billing members to organizations at the member limit.
manager.add("organizations:invite-billing", OrganizationFeature, FeatureHandlerStrategy.INTERNAL, default=False, api_expose=False)
# Enable inviting members to organizations.
Expand Down
53 changes: 53 additions & 0 deletions src/sentry/integrations/jira/webhooks/issue_updated.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
from rest_framework.response import Response
from sentry_sdk import Scope

from sentry import features
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import cell_silo_endpoint
from sentry.integrations.services.integration import integration_service
from sentry.integrations.utils.atlassian_connect import get_integration_from_jwt
from sentry.integrations.utils.scope import bind_org_context_from_integration
from sentry.organizations.services.organization import organization_service
from sentry.ratelimits.config import RateLimitConfig
from sentry.shared_integrations.exceptions import ApiError
from sentry.types.ratelimit import RateLimit, RateLimitCategory
Expand All @@ -24,6 +27,26 @@

logger = logging.getLogger(__name__)

PAYLOAD_LOGGING_FEATURE = "organizations:jira-issue-updated-payload-logging"


def _payload_logging_enabled(integration_id: int) -> bool:
"""True if any org linked to this Jira integration has the
`jira-issue-updated-payload-logging` feature enabled.

A Jira integration can be shared by multiple Sentry orgs, and
`features.has` needs an `Organization`, so we have to walk the linked
`OrganizationIntegration` rows and look each org up.
"""
contexts = integration_service.organization_contexts(integration_id=integration_id)
for oi in contexts.organization_integrations:
Comment on lines +33 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The function _payload_logging_enabled introduces an N+1 database query pattern in the Jira issue.updated webhook handler, causing unnecessary latency on every request.
Severity: MEDIUM

Suggested Fix

The organization_id values are already available in the contexts.organization_integrations object. Use these IDs directly for the feature flag check. Alternatively, fetch all required organization objects in a single batch query instead of making individual database calls in a loop.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: src/sentry/integrations/jira/webhooks/issue_updated.py#L33-L42

Potential issue: The function `_payload_logging_enabled` is called unconditionally on
every `issue.updated` webhook request, introducing an N+1 database query pattern. It
first fetches all `OrganizationIntegration` rows for the integration, then issues a
separate `organization_service.get_organization_by_id()` call for each linked
organization. This overhead occurs on every webhook even when the associated feature
flag is disabled for all linked organizations, adding unnecessary latency to a
high-frequency webhook handler.

Did we get this right? 👍 / 👎 to inform future reviews.

org = organization_service.get_organization_by_id(
id=oi.organization_id, include_teams=False, include_projects=False
)
if org and features.has(PAYLOAD_LOGGING_FEATURE, org.organization):
return True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundant RPC calls on every webhook request

Medium Severity

_payload_logging_enabled calls integration_service.organization_contexts on every webhook request, duplicating the same RPC call already made by bind_org_context_from_integration a few lines earlier. It then makes N additional organization_service.get_organization_by_id calls (one per linked org). All of this runs on every issue.updated webhook regardless of whether the feature is enabled for any org, adding unnecessary latency and load to a high-throughput endpoint.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7bfa989. Configure here.

return False


@cell_silo_endpoint
class JiraIssueUpdatedWebhook(JiraWebhookBase):
Expand Down Expand Up @@ -75,6 +98,36 @@
sentry_sdk.set_tag("integration_id", rpc_integration.id)

data = request.data

# Temporary: when a linked org has the
# `jira-issue-updated-payload-logging` feature enabled, log the full
# webhook payload so we can see exactly what Jira sends us (especially
# for `project` changes, which we want to use to update the linked
# Jira issue link in Sentry).
if _payload_logging_enabled(rpc_integration.id):
issue = data.get("issue") or {}
fields = issue.get("fields") or {}
changelog_items = (data.get("changelog") or {}).get("items") or []
payload_extra = {
"integration_id": rpc_integration.id,
"issue_key": issue.get("key"),
"issue_id": issue.get("id"),
"webhook_event": data.get("webhookEvent"),
"changed_fields": [item.get("field") for item in changelog_items],

Check failure on line 116 in src/sentry/integrations/jira/webhooks/issue_updated.py

View check run for this annotation

@sentry/warden / warden: wrdn-pii

Full Jira webhook payload logged to durable operational sink

The handler logs the entire Jira `issue.updated` webhook body (`"payload": data`) plus assignee/reporter fields nested in `fields` to the application logger. Jira issue payloads routinely contain real user PII (assignee/reporter `emailAddress`, `displayName`, `accountId`), customer org identifiers, and free-form issue summaries/descriptions that may contain customer-confidential content. Sending the raw payload to logs (a durable, vendor-visible sink) exposes user and customer data well beyond what the feature requires, which is project-change metadata.
Comment on lines +114 to +116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Full Jira webhook payload logged to durable operational sink

The handler logs the entire Jira issue.updated webhook body ("payload": data) plus assignee/reporter fields nested in fields to the application logger. Jira issue payloads routinely contain real user PII (assignee/reporter emailAddress, displayName, accountId), customer org identifiers, and free-form issue summaries/descriptions that may contain customer-confidential content. Sending the raw payload to logs (a durable, vendor-visible sink) exposes user and customer data well beyond what the feature requires, which is project-change metadata.

Verification

Traced the data flow: data = request.data is the parsed Jira webhook body. Jira Cloud jira:issue_updated webhooks include issue.fields.assignee, issue.fields.reporter, issue.fields.creator with emailAddress/displayName/accountId, plus summary/description free text and user (the actor) with email. The new extra dict embeds the entire data under payload and additionally embeds fields.project (which together with the issue context links to a named customer's Jira instance). The logger is the standard module logger, which routes to durable operational logging. The skill explicitly calls out logging request.body/webhook payloads/identity-provider payloads as a high-severity privacy sink.

Identified by Warden wrdn-pii · JEH-S4Q

"project": fields.get("project"),
"payload": data,
}
logger.info("jira.issue-updated.payload", extra=payload_extra)
project_change = next(
(item for item in changelog_items if item.get("field") == "project"),
None,
)
if project_change is not None:
logger.info(
"jira.issue-updated.project-changed",
extra={**payload_extra, "project_change": project_change},
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unprotected diagnostic code can break webhook processing

Medium Severity

The call to _payload_logging_enabled on every webhook request makes multiple RPC calls (integration_service.organization_contexts + N × organization_service.get_organization_by_id) that are not wrapped in a try/except. If any of these calls raises an exception (e.g., transient database/network error), it propagates up and the dispatch method returns a 500, causing handle_assignee_change and handle_status_change to be skipped entirely. Temporary diagnostic code can thus break real webhook processing.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7bfa989. Configure here.


if not data.get("changelog"):
logger.info("jira.missing-changelog", extra={"integration_id": rpc_integration.id})
return self.respond()
Expand Down
Loading