Skip to content

Commit 4827f21

Browse files
grichaclaude
andauthored
feat(integrations): Propagate ViewerContext in VSTS webhook (#112413)
## Summary - Wraps the per-org iteration in `handle_status_change` with `webhook_viewer_context` so downstream code has access to the current organization identity via the ViewerContext contextvar. - Gated behind the `viewer-context.enabled` option (no-op when disabled). - Part of the ViewerContext RFC rollout for webhook handlers. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 69b6217 commit 4827f21

File tree

2 files changed

+67
-17
lines changed

2 files changed

+67
-17
lines changed

src/sentry/integrations/vsts/webhooks.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from sentry.integrations.types import IntegrationProviderSlug
2525
from sentry.integrations.utils.metrics import IntegrationWebhookEvent, IntegrationWebhookEventType
2626
from sentry.integrations.utils.sync import sync_group_assignee_inbound
27+
from sentry.integrations.utils.webhook_viewer_context import webhook_viewer_context
2728
from sentry.ratelimits.config import RateLimitConfig
2829
from sentry.types.ratelimit import RateLimit, RateLimitCategory
2930
from sentry.utils.email import parse_email
@@ -173,24 +174,25 @@ def handle_status_change(
173174
"status_change": status_change,
174175
}
175176
for org_integration in org_integrations:
176-
installation = integration.get_installation(
177-
organization_id=org_integration.organization_id
178-
)
179-
if isinstance(installation, IssueSyncIntegration):
180-
installation.sync_status_inbound(
181-
external_issue_key,
182-
{
183-
"new_state": status_change["newValue"],
184-
# old_state is None when the issue is New
185-
"old_state": status_change.get("oldValue"),
186-
"project": project,
187-
},
188-
)
189-
else:
190-
lifecycle.record_halt(
191-
ProjectManagementHaltReason.SYNC_NON_SYNC_INTEGRATION_PROVIDED,
192-
extra=logging_context,
177+
with webhook_viewer_context(org_integration.organization_id):
178+
installation = integration.get_installation(
179+
organization_id=org_integration.organization_id
193180
)
181+
if isinstance(installation, IssueSyncIntegration):
182+
installation.sync_status_inbound(
183+
external_issue_key,
184+
{
185+
"new_state": status_change["newValue"],
186+
# old_state is None when the issue is New
187+
"old_state": status_change.get("oldValue"),
188+
"project": project,
189+
},
190+
)
191+
else:
192+
lifecycle.record_halt(
193+
ProjectManagementHaltReason.SYNC_NON_SYNC_INTEGRATION_PROVIDED,
194+
extra=logging_context,
195+
)
194196

195197

196198
def handle_updated_workitem(data: Mapping[str, Any], integration: RpcIntegration) -> None:

tests/sentry/integrations/vsts/test_webhooks.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
from sentry.silo.base import SiloMode
2121
from sentry.testutils.asserts import assert_failure_metric, assert_success_metric
2222
from sentry.testutils.cases import APITestCase
23+
from sentry.testutils.helpers.options import override_options
2324
from sentry.testutils.silo import assume_test_silo_mode
2425
from sentry.users.models.identity import Identity
2526
from sentry.utils.http import absolute_uri
27+
from sentry.viewer_context import ActorType, get_viewer_context
2628

2729

2830
class VstsWebhookWorkItemTest(APITestCase):
@@ -295,3 +297,49 @@ def test_inbound_status_sync_new_workitem(self) -> None:
295297
assert Group.objects.get(id=group.id).status == GroupStatus.UNRESOLVED
296298
# no change happened. no activity should be created here
297299
assert len(Activity.objects.filter(group_id=group.id)) == 0
300+
301+
@responses.activate
302+
@override_options({"viewer-context.enabled": True})
303+
def test_status_change_sets_viewer_context(self) -> None:
304+
"""ViewerContext is set with correct org_id and actor_type during status sync."""
305+
captured_contexts: list = []
306+
307+
original_sync = VstsIntegration.sync_status_inbound
308+
309+
def capturing_sync(self_integration, *args, **kwargs):
310+
captured_contexts.append(get_viewer_context())
311+
return original_sync(self_integration, *args, **kwargs)
312+
313+
work_item_id = 33
314+
ExternalIssue.objects.create(
315+
organization_id=self.organization.id,
316+
integration_id=self.model.id,
317+
key=work_item_id,
318+
)
319+
320+
responses.add(
321+
responses.GET,
322+
"https://instance.visualstudio.com/c0bf429a-c03c-4a99-9336-d45be74db5a6/_apis/wit/workitemtypes/Bug/states",
323+
json=WORK_ITEM_STATES,
324+
)
325+
326+
work_item = self.set_workitem_state("Active", "Resolved")
327+
328+
with (
329+
patch.object(VstsIntegration, "sync_status_inbound", capturing_sync),
330+
self.feature("organizations:integrations-issue-sync"),
331+
self.tasks(),
332+
):
333+
resp = self.client.post(
334+
absolute_uri("/extensions/vsts/issue-updated/"),
335+
data=work_item,
336+
HTTP_SHARED_SECRET=self.shared_secret,
337+
)
338+
339+
assert resp.status_code == 200
340+
assert len(captured_contexts) == 1
341+
ctx = captured_contexts[0]
342+
assert ctx is not None
343+
assert ctx.organization_id == self.organization.id
344+
assert ctx.actor_type == ActorType.INTEGRATION
345+
assert ctx.user_id is None

0 commit comments

Comments
 (0)