def test_team_actors_to_user_ids(self): team1 = self.create_team() team2 = self.create_team() team3 = self.create_team() # team with no active members users = [self.create_user() for i in range(0, 8)] self.create_member(user=users[0], organization=self.organization, teams=[team1]) self.create_member(user=users[1], organization=self.organization, teams=[team1]) self.create_member(user=users[2], organization=self.organization, teams=[team1]) self.create_member(user=users[3], organization=self.organization, teams=[team1, team2]) self.create_member(user=users[4], organization=self.organization, teams=[team2, self.team]) self.create_member(user=users[5], organization=self.organization, teams=[team2]) # Inactive member member6 = self.create_member( user=users[6], organization=self.organization, teams=[team2, team3] ) team_member6 = OrganizationMemberTeam.objects.filter(organizationmember_id=member6.id) for team_member in team_member6: team_member.update(is_active=False) # Member without teams self.create_member(user=users[7], organization=self.organization, teams=[]) team_actors = [ ActorTuple(team1.id, Team), ActorTuple(team2.id, Team), ActorTuple(team3.id, Team), ] user_ids = [user.id for user in users] assert team_actors_to_user_ids(team_actors, user_ids) == { team1.id: {users[0].id, users[1].id, users[2].id, users[3].id}, team2.id: {users[3].id, users[4].id, users[5].id}, }
def test_get_owners_when_codeowners_and_issueowners_exists(self): self.team = self.create_team( organization=self.organization, slug="tiger-team", members=[self.user] ) self.team2 = self.create_team( organization=self.organization, slug="dolphin-team", members=[self.user] ) self.project = self.create_project( organization=self.organization, teams=[self.team, self.team2] ) self.code_mapping = self.create_code_mapping(project=self.project) rule_a = Rule(Matcher("path", "*.py"), [Owner("team", self.team.slug)]) rule_b = Rule(Matcher("path", "src/*"), [Owner("user", self.user.email)]) rule_c = Rule(Matcher("path", "*.py"), [Owner("team", self.team2.slug)]) ProjectOwnership.objects.create( project_id=self.project.id, schema=dump_schema([rule_a, rule_b]), fallthrough=True ) self.create_codeowners( self.project, self.code_mapping, raw="*.py @tiger-team", schema=dump_schema([rule_c]) ) self.assert_ownership_equals( ProjectOwnership.get_owners( self.project.id, {"stacktrace": {"frames": [{"filename": "api/foo.py"}]}} ), ( [ActorTuple(self.team.id, Team), ActorTuple(self.team2.id, Team)], [rule_a, rule_c], ), )
def test_basic(self): owners = [ Owner("user", self.user.email), Owner("team", self.team.slug) ] assert resolve_actors(owners, self.project.id) == { owners[0]: ActorTuple(self.user.id, User), owners[1]: ActorTuple(self.team.id, Team), }
def test_build_events_by_actor(self): events = self.team1_events + self.team2_events + self.user4_events events_by_actor = { ActorTuple(self.team1.id, Team): set(self.team1_events), ActorTuple(self.team2.id, Team): set(self.team2_events), ActorTuple(self.user3.id, User): set(self.team1_events), ActorTuple(self.user4.id, User): set(self.user4_events), } assert build_events_by_actor(self.project.id, events, self.user_ids) == events_by_actor
def test_users(self): actor = ActorTuple(self.user.id, User) result = extract_user_ids_from_mentions(self.organization.id, [actor]) assert result["users"] == {self.user.id} assert result["team_users"] == set() other_user = self.create_user() result = extract_user_ids_from_mentions( self.organization.id, [actor, ActorTuple(other_user.id, User)]) assert result["users"] == {self.user.id, other_user.id} assert result["team_users"] == set()
def test_convert_actors_to_user_set(self): user1 = self.create_user() user2 = self.create_user() user3 = self.create_user() user4 = self.create_user() team1 = self.create_team() team2 = self.create_team() self.create_member(user=user1, organization=self.organization, teams=[team1]) self.create_member(user=user2, organization=self.organization, teams=[team2]) self.create_member(user=user3, organization=self.organization, teams=[team1, team2]) self.create_member(user=user4, organization=self.organization, teams=[]) team1_events = { self.create_event(self.project.id), self.create_event(self.project.id), self.create_event(self.project.id), self.create_event(self.project.id), } team2_events = { self.create_event(self.project.id), self.create_event(self.project.id), self.create_event(self.project.id), self.create_event(self.project.id), } user4_events = { self.create_event(self.project.id), self.create_event(self.project.id) } events_by_actor = { ActorTuple(team1.id, Team): team1_events, ActorTuple(team2.id, Team): team2_events, ActorTuple(user3.id, User): team1_events.union(team2_events), ActorTuple(user4.id, User): user4_events, } user_by_events = { user1.id: team1_events, user2.id: team2_events, user3.id: team1_events.union(team2_events), user4.id: user4_events, } assert convert_actors_to_users(events_by_actor, user_by_events.keys()) == user_by_events
def resolve_actors(owners, project_id): """Convert a list of Owner objects into a dictionary of {Owner: Actor} pairs. Actors not identified are returned as None.""" from sentry.models import ActorTuple from sentry.models import User, Team if not owners: return {} users, teams = [], [] owners_lookup = {} for owner in owners: # teams aren't technical case insensitive, but teams also # aren't allowed to have non-lowercase in slugs, so # this kinda works itself out correctly since they won't match owners_lookup[(owner.type, owner.identifier.lower())] = owner if owner.type == "user": users.append(owner) elif owner.type == "team": teams.append(owner) actors = {} if users: actors.update( { ("user", email.lower()): ActorTuple(u_id, User) for u_id, email in User.objects.filter( reduce(operator.or_, [Q(emails__email__iexact=o.identifier) for o in users]), # We don't require verified emails # emails__is_verified=True, is_active=True, sentry_orgmember_set__organizationmemberteam__team__projectteam__project_id=project_id, ) .distinct() .values_list("id", "emails__email") } ) if teams: actors.update( { ("team", slug): ActorTuple(t_id, Team) for t_id, slug in Team.objects.filter( slug__in=[o.identifier for o in teams], projectteam__project_id=project_id ).values_list("id", "slug") } ) return {o: actors.get((o.type, o.identifier.lower())) for o in owners}
def test_get_owners_when_codeowners_exists_and_no_issueowners(self): # This case will never exist bc we create a ProjectOwnership record if none exists when creating a ProjectCodeOwner record. # We have this testcase for potential corrupt data. self.team = self.create_team(organization=self.organization, slug="tiger-team", members=[self.user]) self.code_mapping = self.create_code_mapping(project=self.project) rule_a = Rule(Matcher("path", "*.js"), [Owner("team", self.team.slug)]) self.create_codeowners( self.project, self.code_mapping, raw="*.js @tiger-team", schema=dump_schema([rule_a]), ) self.assert_ownership_equals( ProjectOwnership.get_owners( self.project.id, {"stacktrace": { "frames": [{ "filename": "src/foo.js" }] }}), ( [ActorTuple(self.team.id, Team)], [rule_a], ), )
def get_owners( project: Project, event: Optional["Event"] = None ) -> Iterable[Union["Team", "User"]]: """Given a project and an event, decide which users and teams are the owners.""" if event: owners, _ = ProjectOwnership.get_owners(project.id, event.data) else: owners = ProjectOwnership.Everyone if not owners: outcome = "empty" recipients = set() elif owners == ProjectOwnership.Everyone: outcome = "everyone" recipients = User.objects.filter(id__in=project.member_set.values_list("user", flat=True)) else: outcome = "match" recipients = ActorTuple.resolve_many(owners) metrics.incr( "features.owners.send_to", tags={"organization": project.organization_id, "outcome": outcome}, skip_internal=True, ) return recipients
def to_internal_value(self, data): if not data: return None try: actor = ActorTuple.from_actor_identifier(data) except Exception: raise serializers.ValidationError( "Could not parse actor. Format should be `type:id` where type is `team` or `user`." ) try: obj = actor.resolve() except (Team.DoesNotExist, User.DoesNotExist): raise serializers.ValidationError( f"{actor.type.__name__} does not exist") if actor.type == Team: if obj.organization != self.context["organization"]: raise serializers.ValidationError( "Team is not a member of this organization") elif actor.type == User: if not OrganizationMember.objects.filter( organization=self.context["organization"], user=obj).exists(): raise serializers.ValidationError( "User is not a member of this organization") return actor
def test_teams(self): member_user = self.create_user() self.create_member(user=member_user, organization=self.organization, role="member", teams=[self.team]) actor = ActorTuple(self.team.id, Team) result = extract_user_ids_from_mentions(self.organization.id, [actor]) assert result["users"] == set() assert result["team_users"] == {self.user.id, member_user.id} # Explicitly mentioned users shouldn't be included in team_users result = extract_user_ids_from_mentions( self.organization.id, [ActorTuple(member_user.id, User), actor]) assert result["users"] == {member_user.id} assert result["team_users"] == {self.user.id}
def get_autoassign_owners(cls, project_id, data, limit=2): """ Get the auto-assign owner for a project if there are any. Returns a tuple of (auto_assignment_enabled, list_of_owners). """ with metrics.timer("projectownership.get_autoassign_owners"): ownership = cls.get_ownership_cached(project_id) if not ownership: return False, [] rules = cls._matching_ownership_rules(ownership, project_id, data) if not rules: return ownership.auto_assignment, [] # We want the last matching rule to take the most precedence. owners = [owner for rule in rules for owner in rule.owners] owners.reverse() actors = { key: val for key, val in resolve_actors({owner for owner in owners}, project_id).items() if val } actors = [actors[owner] for owner in owners if owner in actors][:limit] # Can happen if the ownership rule references a user/team that no longer # is assigned to the project or has been removed from the org. if not actors: return ownership.auto_assignment, [] from sentry.models import ActorTuple return ownership.auto_assignment, ActorTuple.resolve_many(actors)
def to_internal_value(self, data): if not data: return None try: return ActorTuple.from_actor_identifier(data) except Exception: raise serializers.ValidationError("Unknown actor input")
def get_autoassign_owners(cls, project_id, data, limit=2): """ Get the auto-assign owner for a project if there are any. We combine the schemas from IssueOwners and CodeOwners. Returns a tuple of (auto_assignment_enabled, list_of_owners, assigned_by_codeowners: boolean). """ from sentry.models import ProjectCodeOwners with metrics.timer("projectownership.get_autoassign_owners"): ownership = cls.get_ownership_cached(project_id) codeowners = ProjectCodeOwners.get_codeowners_cached(project_id) assigned_by_codeowners = False if not (ownership or codeowners): return False, [], assigned_by_codeowners if not ownership: ownership = cls(project_id=project_id) ownership_rules = cls._matching_ownership_rules( ownership, project_id, data) codeowners_rules = (cls._matching_ownership_rules( codeowners, project_id, data) if codeowners else []) if not (codeowners_rules or ownership_rules): return ownership.auto_assignment, [], assigned_by_codeowners ownership_actors = cls._find_actors(project_id, ownership_rules, limit) codeowners_actors = cls._find_actors(project_id, codeowners_rules, limit) # Can happen if the ownership rule references a user/team that no longer # is assigned to the project or has been removed from the org. if not (ownership_actors or codeowners_actors): return ownership.auto_assignment, [], assigned_by_codeowners # Ownership rules take precedence over codeowner rules. actors = [*ownership_actors, *codeowners_actors][:limit] # Only the first item in the list is used for assignment, the rest are just used to suggest suspect owners. # So if ownership_actors is empty, it will be assigned by codeowners_actors if len(ownership_actors) == 0: assigned_by_codeowners = True from sentry.models import ActorTuple return ( ownership.auto_assignment, ActorTuple.resolve_many(actors), assigned_by_codeowners, )
def validate_owner(self, owner): # owner should be team:id or user:id try: actor = ActorTuple.from_actor_identifier(owner) except serializers.ValidationError: raise serializers.ValidationError( "Could not parse owner. Format should be `type:id` where type is `team` or `user`." ) try: if actor.resolve(): return actor except (User.DoesNotExist, Team.DoesNotExist): raise serializers.ValidationError( "Could not resolve owner to existing team or user.")
def test_abs_path_when_filename_present(self): frame = { "filename": "computer.cpp", "abs_path": "C:\\My\\Path\\computer.cpp", } rule = Rule(Matcher("path", "*My\\Path*"), [Owner("team", self.team.slug)]) ProjectOwnership.objects.create(project_id=self.project.id, schema=dump_schema([rule]), fallthrough=True) assert ProjectOwnership.get_owners( self.project.id, {"stacktrace": { "frames": [frame] }}) == ([ActorTuple(self.team.id, Team)], [rule])
def self_subscribe_and_assign_issue(acting_user, group): # Used during issue resolution to assign to acting user # returns None if the user didn't elect to self assign on resolution # or the group is assigned already, otherwise returns Actor # representation of current user if acting_user: GroupSubscription.objects.subscribe( user=acting_user, group=group, reason=GroupSubscriptionReason.status_change) self_assign_issue = UserOption.objects.get_value( user=acting_user, key="self_assign_issue", default="0") if self_assign_issue == "1" and not group.assignee_set.exists(): return ActorTuple(type=User, id=acting_user.id)
def test_transfer_to_organization_alert_rules(self): from_org = self.create_organization() from_user = self.create_user() self.create_member(user=from_user, role="member", organization=from_org) team = self.create_team(organization=from_org) to_org = self.create_organization() to_team = self.create_team(organization=to_org) to_user = self.create_user() self.create_member(user=to_user, role="member", organization=to_org) project = self.create_project(teams=[team]) # should lose their owners alert_rule = self.create_alert_rule( organization=self.organization, projects=[project], owner=ActorTuple.from_actor_identifier(f"team:{team.id}"), ) rule1 = Rule.objects.create(label="another test rule", project=project, owner=team.actor) rule2 = Rule.objects.create(label="rule4", project=project, owner=from_user.actor) # should keep their owners rule3 = Rule.objects.create(label="rule2", project=project, owner=to_team.actor) rule4 = Rule.objects.create(label="rule3", project=project, owner=to_user.actor) project.transfer_to(organization=to_org) alert_rule.refresh_from_db() rule1.refresh_from_db() rule2.refresh_from_db() rule3.refresh_from_db() rule4.refresh_from_db() assert alert_rule.organization_id == to_org.id assert alert_rule.owner is None assert rule1.owner is None assert rule2.owner is None assert rule3.owner is not None assert rule4.owner is not None
def build_events_by_actor(project_id, events, user_ids): """ build_events_by_actor(project_id: Int, events: Set(Events), user_ids: Set[Int]) -> Map[Actor, Set(Events)] """ events_by_actor = defaultdict(set) for event in events: # TODO(LB): I Know this is inefficient. # ProjectOwnership.get_owners is O(n) queries and I'm doing that O(len(events)) times # I will create a follow-up PR to address this method's efficiency problem # Just wanted to make as few changes as possible for now. actors, __ = ProjectOwnership.get_owners(project_id, event.data) if actors == ProjectOwnership.Everyone: actors = [ActorTuple(user_id, User) for user_id in user_ids] for actor in actors: events_by_actor[actor].add(event) return events_by_actor
def test_get_owners_basic(self): rule_a = Rule(Matcher("path", "*.py"), [Owner("team", self.team.slug)]) rule_b = Rule(Matcher("path", "src/*"), [Owner("user", self.user.email)]) ProjectOwnership.objects.create( project_id=self.project.id, schema=dump_schema([rule_a, rule_b]), fallthrough=True ) # No data matches assert ProjectOwnership.get_owners(self.project.id, {}) == (ProjectOwnership.Everyone, None) # Match only rule_a self.assert_ownership_equals( ProjectOwnership.get_owners( self.project.id, {"stacktrace": {"frames": [{"filename": "foo.py"}]}} ), ([ActorTuple(self.team.id, Team)], [rule_a]), ) # Match only rule_b self.assert_ownership_equals( ProjectOwnership.get_owners( self.project.id, {"stacktrace": {"frames": [{"filename": "src/thing.txt"}]}} ), ([ActorTuple(self.user.id, User)], [rule_b]), ) # Matches both rule_a and rule_b self.assert_ownership_equals( ProjectOwnership.get_owners( self.project.id, {"stacktrace": {"frames": [{"filename": "src/foo.py"}]}} ), ([ActorTuple(self.team.id, Team), ActorTuple(self.user.id, User)], [rule_a, rule_b]), ) assert ProjectOwnership.get_owners( self.project.id, {"stacktrace": {"frames": [{"filename": "xxxx"}]}} ) == (ProjectOwnership.Everyone, None) # When fallthrough = False, we don't implicitly assign to Everyone owner = ProjectOwnership.objects.get(project_id=self.project.id) owner.fallthrough = False owner.save() assert ProjectOwnership.get_owners( self.project.id, {"stacktrace": {"frames": [{"filename": "xxxx"}]}} ) == ([], None) self.assert_ownership_equals( ProjectOwnership.get_owners( self.project.id, {"stacktrace": {"frames": [{"filename": "src/foo.py"}]}} ), ([ActorTuple(self.team.id, Team), ActorTuple(self.user.id, User)], [rule_a, rule_b]), )
def build_events_by_actor( project_id: int, events: Iterable[Event], user_ids: Iterable[int]) -> Mapping[ActorTuple, Iterable[Event]]: """ TODO(mgaeta): I know this is inefficient. ProjectOwnership.get_owners is O(n) queries and I'm doing that O(len(events)) times. I "will" create a follow-up PR to address this method's efficiency problem. Just wanted to make as few changes as possible for now. """ events_by_actor: MutableMapping[ActorTuple, Set[Event]] = defaultdict(set) for event in events: actors, __ = ProjectOwnership.get_owners(project_id, event.data) if actors == ProjectOwnership.Everyone: actors = [ActorTuple(user_id, User) for user_id in user_ids] for actor in actors: events_by_actor[actor].add(event) return events_by_actor
def test_teams(self): # Normal team owner1 = Owner("team", self.team.slug) actor1 = ActorTuple(self.team.id, Team) # Team that doesn't exist owner2 = Owner("team", "nope") actor2 = None # A team that's not ours otherteam = Team.objects.exclude(projectteam__project_id=self.project.id)[0] owner3 = Owner("team", otherteam.slug) actor3 = None assert resolve_actors([owner1, owner2, owner3], self.project.id) == { owner1: actor1, owner2: actor2, owner3: actor3, }
def test_users(self): # Normal user owner1 = Owner("user", self.user.email) actor1 = ActorTuple(self.user.id, User) # An extra secondary email email1 = self.create_useremail(self.user, None, is_verified=True).email owner2 = Owner("user", email1) actor2 = actor1 # They map to the same user since it's just a secondary email # Another secondary email, that isn't verified email2 = self.create_useremail(self.user, None, is_verified=False).email owner3 = Owner("user", email2) # Intentionally allow unverified emails # actor3 = None actor3 = actor1 # An entirely unknown user owner4 = Owner("user", "nope") actor4 = None # A user that doesn't belong with us otheruser = self.create_user() owner5 = Owner("user", otheruser.email) actor5 = None # Case-insensitive for user owner6 = Owner("user", self.user.email.upper()) actor6 = actor1 assert resolve_actors([owner1, owner2, owner3, owner4, owner5, owner6], self.project.id) == { owner1: actor1, owner2: actor2, owner3: actor3, owner4: actor4, owner5: actor5, owner6: actor6, }
def get_owners(project: Project, event: Event | None = None) -> Sequence[Team | User]: """ Given a project and an event, decide which users and teams are the owners. If when checking owners, there is a rule match we only notify the last owner (would-be auto-assignee) unless the organization passes the feature-flag """ if event: owners, _ = ProjectOwnership.get_owners(project.id, event.data) else: owners = ProjectOwnership.Everyone if not owners: outcome = "empty" recipients = list() elif owners == ProjectOwnership.Everyone: outcome = "everyone" recipients = User.objects.filter( id__in=project.member_set.values_list("user", flat=True)) else: outcome = "match" recipients = ActorTuple.resolve_many(owners) # Used to suppress extra notifications to all matched owners, only notify the would-be auto-assignee if not features.has("organizations:notification-all-recipients", project.organization): recipients = recipients[-1:] metrics.incr( "features.owners.send_to", tags={ "organization": project.organization_id, "outcome": outcome }, skip_internal=True, ) return recipients
def build_assigned_text(identity: Identity, assignee: str): actor = ActorTuple.from_actor_identifier(assignee) try: assigned_actor = actor.resolve() except actor.type.DoesNotExist: return if actor.type == Team: assignee_text = f"#{assigned_actor.slug}" elif actor.type == User: try: assignee_ident = Identity.objects.get( user=assigned_actor, idp__type="slack", idp__external_id=identity.idp.external_id ) assignee_text = f"<@{assignee_ident.external_id}>" except Identity.DoesNotExist: assignee_text = assigned_actor.get_display_name() else: raise NotImplementedError return f"*Issue assigned to {assignee_text} by <@{identity.external_id}>*"
def get(self, request: Request, project, event_id) -> Response: """ Retrieve suggested owners information for an event `````````````````````````````````````````````````` :pparam string project_slug: the slug of the project the event belongs to. :pparam string event_id: the id of the event. :auth: required """ event = eventstore.get_event_by_id(project.id, event_id) if event is None: return Response({"detail": "Event not found"}, status=404) owners, rules = ProjectOwnership.get_owners(project.id, event.data) # For sake of the API, we don't differentiate between # the implicit "everyone" and no owners if owners == ProjectOwnership.Everyone: owners = [] serialized_owners = serialize(ActorTuple.resolve_many(owners), request.user, ActorSerializer()) # Make sure the serialized owners are in the correct order ordered_owners = [] owner_by_id = {(o["id"], o["type"]): o for o in serialized_owners} for o in owners: key = (str(o.id), "team" if o.type == Team else "user") if owner_by_id.get(key): ordered_owners.append(owner_by_id[key]) return Response({ "owners": ordered_owners, # TODO(mattrobenolt): We need to change the API here to return # all rules, just keeping this way currently for API compat "rule": rules[0].matcher if rules else None, "rules": rules or [], })
def extract_user_ids_from_mentions(organization_id, mentions): """ Extracts user ids from a set of mentions. Mentions should be a list of `ActorTuple` instances. Returns a dictionary with 'users' and 'team_users' keys. 'users' is the user ids for all explicitly mentioned users, and 'team_users' is all user ids from explicitly mentioned teams, excluding any already mentioned users. """ actors = ActorTuple.resolve_many(mentions) actor_mentions = separate_resolved_actors(actors) mentioned_team_users = list( User.objects.get_from_teams( organization_id, actor_mentions["teams"]).exclude( id__in={u.id for u in actor_mentions["users"]}).values_list( "id", flat=True)) return { "users": {user.id for user in actor_mentions["users"]}, "team_users": set(mentioned_team_users), }
def assigned_actor(self): from sentry.models import ActorTuple return ActorTuple.from_actor_identifier(self.assigned_actor_id())
def owner(self): from sentry.models import ActorTuple return ActorTuple.from_actor_identifier(self.owner_id())
def test_simple(self): project1 = self.create_project( teams=[self.team], slug="foo" ) # This project will return counts for this team user_owned_rule = self.create_alert_rule( organization=self.organization, projects=[project1], name="user owned rule", query="", aggregate="count()", time_window=1, threshold_type=AlertRuleThresholdType.ABOVE, resolve_threshold=10, threshold_period=1, owner=ActorTuple.from_actor_identifier(self.user.id), ) user_owned_incident = self.create_incident(status=20, alert_rule=user_owned_rule) activities = [] for i in range(1, 9): activities.append( IncidentActivity( incident=user_owned_incident, type=IncidentActivityType.CREATED.value, value=IncidentStatus.OPEN, date_added=before_now(days=i), ) ) IncidentActivity.objects.bulk_create(activities) self.login_as(user=self.user) response = self.get_success_response(self.team.organization.slug, self.team.slug) assert len(response.data) == 90 for i in range(1, 9): assert ( response.data[ str( before_now(days=i) .replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc) .isoformat() ) ] == 1 ) for i in range(10, 90): assert ( response.data[ str( before_now(days=i) .replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc) .isoformat() ) ] == 0 ) response = self.get_success_response( self.team.organization.slug, self.team.slug, statsPeriod="7d" ) assert len(response.data) == 7 assert ( response.data[ str( before_now(days=0) .replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc) .isoformat() ) ] == 0 ) for i in range(1, 6): assert ( response.data[ str( before_now(days=i) .replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=timezone.utc) .isoformat() ) ] == 1 )