def test_comparison_delta_below(self): params = self.valid_params.copy() params["threshold_type"] = AlertRuleThresholdType.BELOW.value params["comparison_delta"] = 60 params["resolveThreshold"] = 10 params["triggers"][0]["alertThreshold"] = 50 params["triggers"][1]["alertThreshold"] = 40 serializer = AlertRuleSerializer(context=self.context, data=params, partial=True) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() assert alert_rule.comparison_delta == 60 * 60 assert alert_rule.resolve_threshold == 90 triggers = { trigger.label: trigger for trigger in alert_rule.alertruletrigger_set.all() } assert triggers["critical"].alert_threshold == 50 assert triggers["warning"].alert_threshold == 60 assert alert_rule.snuba_query.resolution == DEFAULT_CMP_ALERT_RULE_RESOLUTION * 60 params["comparison_delta"] = None params["resolveThreshold"] = 100 params["triggers"][0]["alertThreshold"] = 40 params["triggers"][1]["alertThreshold"] = 50 serializer = AlertRuleSerializer(context=self.context, instance=alert_rule, data=params, partial=True) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() assert alert_rule.comparison_delta is None assert alert_rule.snuba_query.resolution == DEFAULT_ALERT_RULE_RESOLUTION * 60
def test_channel_timeout(self, mock_get_channel_id): self.setup_slack_integration() trigger = { "label": "critical", "alertThreshold": 200, "actions": [{ "type": "slack", "targetIdentifier": "my-channel", "targetType": "specific", "integration": self.integration.id, }], } base_params = self.valid_params.copy() base_params.update({"triggers": [trigger]}) serializer = AlertRuleSerializer(context=self.context, data=base_params) # error is raised during save assert serializer.is_valid() with self.assertRaises(ChannelLookupTimeoutError) as err: serializer.save() assert ( str(err.exception) == "Could not find channel my-channel. We have timed out trying to look for it." )
def test_owner_validation(self): self.run_fail_validation_test( {"owner": f"meow:{self.user.id}"}, { "owner": [ "Could not parse actor. Format should be `type:id` where type is `team` or `user`." ] }, ) self.run_fail_validation_test( {"owner": "user:1234567"}, {"owner": ["User does not exist"]}, ) self.run_fail_validation_test( {"owner": "team:1234567"}, {"owner": ["Team does not exist"]}, ) other_org = self.create_organization() other_team = self.create_team(organization=other_org) other_user = self.create_user() self.create_member(user=other_user, organization=other_org, teams=[other_team]) self.run_fail_validation_test( {"owner": f"user:{other_user.id}"}, {"owner": ["User is not a member of this organization"]}, ) self.run_fail_validation_test( {"owner": f"team:{other_team.id}"}, {"owner": ["Team is not a member of this organization"]}, ) base_params = self.valid_params.copy() base_params.update({"owner": f"team:{self.team.id}"}) serializer = AlertRuleSerializer(context=self.context, data=base_params) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() assert alert_rule.owner == self.team.actor assert alert_rule.owner.type == ACTOR_TYPES["team"] base_params.update({ "name": "another_test", "owner": f"user:{self.user.id}" }) serializer = AlertRuleSerializer(context=self.context, data=base_params) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() assert alert_rule.owner == self.user.actor assert alert_rule.owner.type == ACTOR_TYPES["user"]
def test_valid_metric_field(self): base_params = self.valid_params.copy() base_params.update({ "name": "Aun1qu3n4m3", "aggregate": "count_unique(user)" }) serializer = AlertRuleSerializer(context=self.context, data=base_params) assert serializer.is_valid() serializer.save() assert len(list(AlertRule.objects.filter(name="Aun1qu3n4m3"))) == 1 alert_rule = AlertRule.objects.filter(name="Aun1qu3n4m3").first() assert alert_rule.snuba_query.aggregate == "count_unique(tags[sentry:user])"
def test_aggregate(self): self.run_fail_validation_test( {"aggregate": "what()"}, {"aggregate": ["Invalid Metric: what() is not a valid function"]}, ) self.run_fail_validation_test( {"aggregate": "what"}, { "nonFieldErrors": [ "Invalid Metric: Please pass a valid function for aggregation" ] }, ) self.run_fail_validation_test( {"aggregate": "123"}, { "nonFieldErrors": [ "Invalid Metric: Please pass a valid function for aggregation" ] }, ) self.run_fail_validation_test( {"aggregate": "count_unique(123, hello)"}, { "aggregate": [ "Invalid Metric: count_unique(123, hello): expected at most 1 argument(s)" ] }, ) self.run_fail_validation_test( {"aggregate": "max()"}, {"aggregate": ["Invalid Metric: max(): expected 1 argument(s)"]}) aggregate = "count_unique(tags[sentry:user])" base_params = self.valid_params.copy() base_params["aggregate"] = aggregate serializer = AlertRuleSerializer(context=self.context, data=base_params) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() assert alert_rule.snuba_query.aggregate == aggregate aggregate = "sum(measurements.fp)" base_params = self.valid_transaction_params.copy() base_params["name"] = "measurement test" base_params["aggregate"] = aggregate serializer = AlertRuleSerializer(context=self.context, data=base_params) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() assert alert_rule.snuba_query.aggregate == aggregate
def test_transaction_dataset(self): serializer = AlertRuleSerializer(context=self.context, data=self.valid_transaction_params) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() assert alert_rule.snuba_query.dataset == QueryDatasets.TRANSACTIONS.value assert alert_rule.snuba_query.aggregate == "count()"
def put(self, request: Request, project, alert_rule) -> Response: data = request.data serializer = DrfAlertRuleSerializer( context={ "organization": project.organization, "access": request.access, "user": request.user, }, instance=alert_rule, data=data, partial=True, ) if serializer.is_valid(): if get_slack_actions_with_async_lookups(project.organization, request.user, data): # need to kick off an async job for Slack client = tasks.RedisRuleStatus() task_args = { "organization_id": project.organization_id, "uuid": client.uuid, "data": data, "alert_rule_id": alert_rule.id, "user_id": request.user.id, } tasks.find_channel_id_for_alert_rule.apply_async(kwargs=task_args) return Response({"uuid": client.uuid}, status=202) else: alert_rule = serializer.save() return Response(serialize(alert_rule, request.user), status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def test_environment_non_list(self): base_params = self.valid_params.copy() env_1 = Environment.objects.create( organization_id=self.organization.id, name="test_env_1") base_params.update({"environment": env_1.name}) serializer = AlertRuleSerializer(context=self.context, data=base_params) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() assert alert_rule.snuba_query.environment == env_1
def test_invalid_slack_channel(self): # We had an error where an invalid slack channel was spitting out unclear # error for the user, and CREATING THE RULE. So the next save (after fixing slack action) # says "Name already in use". This test makes sure that is not happening anymore. # We save a rule with an invalid slack, make sure we get back a useful error # and that the rule is not created. base_params = self.valid_params.copy() base_params["name"] = "Aun1qu3n4m3" integration = Integration.objects.create( external_id="1", provider="slack", metadata={ "access_token": "xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx" }, ) integration.add_organization(self.organization, self.user) base_params["triggers"][0]["actions"].append({ "type": AlertRuleTriggerAction.get_registered_type( AlertRuleTriggerAction.Type.SLACK).slug, "targetType": ACTION_TARGET_TYPE_TO_STRING[ AlertRuleTriggerAction.TargetType.SPECIFIC], "targetIdentifier": "123", "integration": str(integration.id), }) serializer = AlertRuleSerializer(context=self.context, data=base_params) assert serializer.is_valid() with self.assertRaises(serializers.ValidationError): serializer.save() # Make sure the rule was not created. assert len(list(AlertRule.objects.filter(name="Aun1qu3n4m3"))) == 0 # Make sure the action was not created. alert_rule_trigger_actions = list( AlertRuleTriggerAction.objects.filter(integration=integration)) assert len(alert_rule_trigger_actions) == 0
def post(self, request: Request, project) -> Response: """ Create an alert rule """ if not features.has("organizations:incidents", project.organization, actor=request.user): raise ResourceDoesNotExist data = deepcopy(request.data) data["projects"] = [project.slug] serializer = AlertRuleSerializer( context={ "organization": project.organization, "access": request.access, "user": request.user, }, data=data, ) if serializer.is_valid(): if get_slack_actions_with_async_lookups(project.organization, request.user, data): # need to kick off an async job for Slack client = tasks.RedisRuleStatus() task_args = { "organization_id": project.organization_id, "uuid": client.uuid, "data": data, "user_id": request.user.id, } tasks.find_channel_id_for_alert_rule.apply_async( kwargs=task_args) return Response({"uuid": client.uuid}, status=202) else: alert_rule = serializer.save() referrer = request.query_params.get("referrer") session_id = request.query_params.get("sessionId") alert_rule_created.send_robust( user=request.user, project=project, rule=alert_rule, rule_type="metric", sender=self, referrer=referrer, session_id=session_id, is_api_token=request.auth is not None, ) return Response(serialize(alert_rule, request.user), status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def test_event_types(self): invalid_values = [ "Invalid event_type, valid values are %s" % [item.name.lower() for item in SnubaQueryEventType.EventType] ] self.run_fail_validation_test({"event_types": ["a"]}, {"eventTypes": invalid_values}) self.run_fail_validation_test({"event_types": [1]}, {"eventTypes": invalid_values}) self.run_fail_validation_test( {"event_types": ["transaction"]}, { "nonFieldErrors": [ "Invalid event types for this dataset. Valid event types are ['default', 'error']" ] }, ) params = self.valid_params.copy() serializer = AlertRuleSerializer(context=self.context, data=params, partial=True) assert serializer.is_valid() alert_rule = serializer.save() assert set(alert_rule.snuba_query.event_types) == { SnubaQueryEventType.EventType.DEFAULT } params["event_types"] = [ SnubaQueryEventType.EventType.ERROR.name.lower() ] serializer = AlertRuleSerializer(context=self.context, instance=alert_rule, data=params, partial=True) assert serializer.is_valid() alert_rule = serializer.save() assert set(alert_rule.snuba_query.event_types) == { SnubaQueryEventType.EventType.ERROR }
def test_decimal(self): params = self.valid_transaction_params.copy() alert_threshold = 0.8 resolve_threshold = 0.7 params["triggers"][0]["alertThreshold"] = alert_threshold params["resolve_threshold"] = resolve_threshold # Drop off the warning trigger params["triggers"].pop() serializer = AlertRuleSerializer(context=self.context, data=params) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() trigger = alert_rule.alertruletrigger_set.filter( label="critical").get() assert trigger.alert_threshold == alert_threshold
def test_invalid_team_with_channel_timeout(self, mock_get_channel_id): self.setup_slack_integration() other_org = self.create_organization() new_team = self.create_team(organization=other_org) trigger = { "label": "critical", "alertThreshold": 200, "actions": [ { "type": "slack", "targetIdentifier": "my-channel", "targetType": "specific", "integration": self.integration.id, }, { "type": "email", "targetType": "team", "targetIdentifier": new_team.id }, ], } base_params = self.valid_params.copy() base_params.update({"triggers": [trigger]}) serializer = AlertRuleSerializer(context=self.context, data=base_params) # error is raised during save assert serializer.is_valid() with self.assertRaises(serializers.ValidationError) as err: serializer.save() assert err.exception.detail == { "nonFieldErrors": ["Team does not exist"] } mock_get_channel_id.assert_called_with(self.integration, "my-channel", 10)
def new_alert_rule(self, data=None): if data is None: data = deepcopy(self.alert_rule_dict) serializer = AlertRuleSerializer( context={ "organization": self.organization, "access": OrganizationGlobalAccess(self.organization, settings.SENTRY_SCOPES), "user": self.user, }, data=data, ) assert serializer.is_valid(), serializer.errors alert_rule = serializer.save() return alert_rule
def put(self, request: Request, organization, alert_rule) -> Response: serializer = DrfAlertRuleSerializer( context={"organization": organization, "access": request.access, "user": request.user}, instance=alert_rule, data=request.data, ) if serializer.is_valid(): if not self._verify_user_has_permission(request, alert_rule): return Response( { "detail": [ "You do not have permission to edit this alert rule because you are not a member of the assigned team." ] }, status=403, ) alert_rule = serializer.save() return Response(serialize(alert_rule, request.user), status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def post(self, request: Request, organization) -> Response: """ Create an alert rule """ if not features.has( "organizations:incidents", organization, actor=request.user): raise ResourceDoesNotExist serializer = AlertRuleSerializer( context={ "organization": organization, "access": request.access, "user": request.user }, data=request.data, ) if serializer.is_valid(): alert_rule = serializer.save() return Response(serialize(alert_rule, request.user), status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def find_channel_id_for_alert_rule( organization_id: int, uuid: str, data: Any, alert_rule_id: Optional[int] = None, user_id: Optional[int] = None, ) -> None: redis_rule_status = RedisRuleStatus(uuid) try: organization = Organization.objects.get(id=organization_id) except Organization.DoesNotExist: redis_rule_status.set_value("failed") return user = None if user_id: try: user = User.objects.get(id=user_id) except User.DoesNotExist: pass alert_rule = None if alert_rule_id: try: alert_rule = AlertRule.objects.get(organization_id=organization_id, id=alert_rule_id) except AlertRule.DoesNotExist: redis_rule_status.set_value("failed") return try: mapped_ids = get_slack_channel_ids(organization, user, data) except (serializers.ValidationError, ChannelLookupTimeoutError, InvalidTriggerActionError) as e: # channel doesn't exist error or validation error logger.info( "get_slack_channel_ids.failed", extra={ "exception": e, }, ) redis_rule_status.set_value("failed") return except ApiRateLimitedError as e: logger.info( "get_slack_channel_ids.rate_limited", extra={ "exception": e, }, ) redis_rule_status.set_value("failed", None, SLACK_RATE_LIMITED_MESSAGE) return for trigger in data["triggers"]: for action in trigger["actions"]: if action["type"] == "slack": if action["targetIdentifier"] in mapped_ids: action["input_channel_id"] = mapped_ids[action["targetIdentifier"]] else: # We can early exit because we couldn't map this action's slack channel name to a slack id # This is a fail safe, but I think we shouldn't really hit this. redis_rule_status.set_value("failed") return # we use SystemAccess here because we can't pass the access instance from the request into the task # this means at this point we won't raise any validation errors associated with permissions # however, we should only be calling this task after we tried saving the alert rule first # which will catch those kinds of validation errors serializer = AlertRuleSerializer( context={ "organization": organization, "access": SystemAccess(), "user": user, "use_async_lookup": True, "validate_channel_id": False, }, data=data, instance=alert_rule, ) if serializer.is_valid(): try: alert_rule = serializer.save() redis_rule_status.set_value("success", alert_rule.id) return # we can still get a validation error for the channel not existing except (serializers.ValidationError, ChannelLookupTimeoutError): # channel doesn't exist error or validation error redis_rule_status.set_value("failed") return # some other error redis_rule_status.set_value("failed") return