feat(webhooks): Add payload validation during dual-write migration#116040
Conversation
…param Move _ParityChecker and _qualify from testutils/helpers/serializer_parity.py to sentry/utils/payload_comparison.py so it can be used by production code. Changes from the original: - Renamed _ParityChecker -> PayloadComparator, _qualify -> qualify - Added format_value parameter (defaults to repr, preserving current behavior) - Added describe_value() for PII-safe value formatting in production serializer_parity.py now imports from the new location; all existing callers of assert_serializer_parity are unaffected.
Move _ParityChecker and _qualify from testutils/helpers/serializer_parity.py to sentry/utils/payload_comparison.py so it can be used by production code. Changes from the original: - Added format_value parameter (defaults to repr, preserving current behavior) - Added describe_value() for PII-safe value formatting in production serializer_parity.py now imports from the new location; all existing callers of assert_serializer_parity are unaffected.
8c1a7d6 to
c3948de
Compare
ParityChecker only recursed into nested structures when known_diffs or unreliable field paths referenced something inside. When known_diffs is empty (production validation case), differing nested dicts were reported as a single flat mismatch instead of drilling into specific fields. Now always recurses into Mapping and list types.
During the dual-write window (new path on, old path not disabled), build both old-path and new-path payloads and compare them using PayloadComparator with PII-safe formatting. Validation runs after both paths have dispatched and never blocks webhook delivery. Logs legacy_webhook.validation.match on parity, or legacy_webhook.validation.payload_mismatch per field difference with structural metadata only (no actual values).
…om_action create_rule_instance_from_action calls build_rule_action_blob which requires an integration_id mapping that webhooks don't have. Use _get_triggering_rule_name directly — it does the same workflow->rule label lookup without the integration_id requirement. Also fixes mypy: dict -> dict[str, Any] in test return types.
TypedDict is not assignable to dict[str, Any] in mypy. Use Mapping[str, Any] for old_payload (from get_group_data) and LegacyWebhookPayload for new_payload (from build_legacy_webhook_payload).
d566f1a to
581d107
Compare
| old_payload = WebHooksPlugin().get_group_data(group, event, [rule_name]) | ||
| new_payload = build_legacy_webhook_payload(invocation) |
There was a problem hiding this comment.
How expensive are these? Are these doing any queries that could add additional latency into our delivery process?
This may be another place we want to have an option to enable/disable these checks.
There was a problem hiding this comment.
I dont think these are expensive at all since we're just extracting fields for old and new payload. The more expensive thing would be the amount of recurring we're doing on the event payload. Yeah option makes sense.
Also this validation would happen after the old and new path webhooks have been sent out so it wouldn't affect webhook delivery
…n log Change payload validation option from bool to float-based rollout rate so we can gradually increase validation coverage. Remove the logger.info call from the dry run path to avoid logging payload contents.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a90e775. Configure here.
leeandher
left a comment
There was a problem hiding this comment.
few comments, but makes sense!
…ndling - Add logging_context variable to avoid redeclaring shared context - Narrow try/except to only wrap compare_payloads call - Log all mismatches in a single warning instead of one per mismatch - Add test for comparison error exception handling

Use the ParityChecker on when dual writing to validate the old and new path are outputting the same payload w/o PII exposed
relies on #116038