class DashboardWidget(Model): """ A dashboard widget. """ __include_in_export__ = True dashboard = FlexibleForeignKey("sentry.Dashboard") order = BoundedPositiveIntegerField() title = models.CharField(max_length=255) interval = models.CharField(max_length=10, null=True) display_type = BoundedPositiveIntegerField( choices=DashboardWidgetDisplayTypes.as_choices()) date_added = models.DateTimeField(default=timezone.now) widget_type = BoundedPositiveIntegerField( choices=DashboardWidgetTypes.as_choices(), null=True) limit = models.IntegerField(null=True) detail = JSONField(null=True) class Meta: app_label = "sentry" db_table = "sentry_dashboardwidget" unique_together = (("dashboard", "order"), ) __repr__ = sane_repr("dashboard", "title")
class DiscoverSavedQuery(Model): """ A saved Discover query """ __include_in_export__ = False projects = models.ManyToManyField("sentry.Project", through=DiscoverSavedQueryProject) organization = FlexibleForeignKey("sentry.Organization") created_by = FlexibleForeignKey("sentry.User", null=True, on_delete=models.SET_NULL) name = models.CharField(max_length=255) query = JSONField() version = models.IntegerField(null=True) date_created = models.DateTimeField(auto_now_add=True) date_updated = models.DateTimeField(auto_now=True) class Meta: app_label = "sentry" db_table = "sentry_discoversavedquery" __repr__ = sane_repr("organization_id", "created_by", "name") def set_projects(self, project_ids): with transaction.atomic(): DiscoverSavedQueryProject.objects.filter( discover_saved_query=self).exclude( project__in=project_ids).delete() existing_project_ids = DiscoverSavedQueryProject.objects.filter( discover_saved_query=self).values_list("project", flat=True) new_project_ids = list( set(project_ids) - set(existing_project_ids)) DiscoverSavedQueryProject.objects.bulk_create([ DiscoverSavedQueryProject(project_id=project_id, discover_saved_query=self) for project_id in new_project_ids ])
class ProjectOwnership(Model): __core__ = True project = FlexibleForeignKey("sentry.Project", unique=True) raw = models.TextField(null=True) schema = JSONField(null=True) fallthrough = models.BooleanField(default=True) auto_assignment = models.BooleanField(default=False) date_created = models.DateTimeField(default=timezone.now) last_updated = models.DateTimeField(default=timezone.now) is_active = models.BooleanField(default=True) # An object to indicate ownership is implicitly everyone Everyone = object() class Meta: app_label = "sentry" db_table = "sentry_projectownership" __repr__ = sane_repr("project_id", "is_active") @classmethod def get_cache_key(self, project_id): return "projectownership_project_id:1:{}".format(project_id) @classmethod def get_ownership_cached(cls, project_id): """ Cached read access to projectownership. This method implements a negative cache which saves us a pile of read queries in post_processing as most projects don't have ownership rules. See the post_save and post_delete signals below for additional cache updates. """ cache_key = cls.get_cache_key(project_id) ownership = cache.get(cache_key) if ownership is None: try: ownership = cls.objects.get(project_id=project_id) except cls.DoesNotExist: ownership = False cache.set(cache_key, ownership, READ_CACHE_DURATION) return ownership or None @classmethod def get_owners(cls, project_id, data): """ For a given project_id, and event data blob. If Everyone is returned, this means we implicitly are falling through our rules and everyone is responsible. If an empty list is returned, this means there are explicitly no owners. """ ownership = cls.get_ownership_cached(project_id) if not ownership: ownership = cls(project_id=project_id) rules = cls._matching_ownership_rules(ownership, project_id, data) if not rules: return cls.Everyone if ownership.fallthrough else [], None owners = {o for rule in rules for o in rule.owners} owners_to_actors = resolve_actors(owners, project_id) ordered_actors = [] for rule in rules: for o in rule.owners: if o in owners and owners_to_actors.get(o) is not None: ordered_actors.append(owners_to_actors[o]) owners.remove(o) return ordered_actors, rules @classmethod 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, [] owners = [] # Automatic assignment prefers the owner with the longest # matching pattern as the match is more specific. for rule in rules: candidate = len(rule.matcher.pattern) for i, owner in enumerate(rule.owners): owners.append((candidate, -i, owner)) owners.sort(reverse=True) actors = { key: val for key, val in resolve_actors( set([owner[2] for owner in owners]), project_id).items() if val } actors = [ actors[owner[2]] for owner in owners if owner[2] 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, [] return ownership.auto_assignment, actors[0].resolve_many(actors) @classmethod def _matching_ownership_rules(cls, ownership, project_id, data): rules = [] if ownership.schema is not None: for rule in load_schema(ownership.schema): if rule.test(data): rules.append(rule) return rules
class ProjectOwnership(Model): __core__ = True project = FlexibleForeignKey('sentry.Project', unique=True) raw = models.TextField(null=True) schema = JSONField(null=True) fallthrough = models.BooleanField(default=True) auto_assignment = models.BooleanField(default=False) date_created = models.DateTimeField(default=timezone.now) last_updated = models.DateTimeField(default=timezone.now) is_active = models.BooleanField(default=True) # An object to indicate ownership is implicitly everyone Everyone = object() class Meta: app_label = 'sentry' db_table = 'sentry_projectownership' __repr__ = sane_repr('project_id', 'is_active') @classmethod def get_owners(cls, project_id, data): """ For a given project_id, and event data blob. If Everyone is returned, this means we implicitly are falling through our rules and everyone is responsible. If an empty list is returned, this means there are explicitly no owners. """ try: ownership = cls.objects.get(project_id=project_id) except cls.DoesNotExist: ownership = cls( project_id=project_id, ) rules = cls._matching_ownership_rules(ownership, project_id, data) if not rules: return cls.Everyone if ownership.fallthrough else [], None owners = {o for rule in rules for o in rule.owners} return filter(None, resolve_actors(owners, project_id).values()), rules @classmethod def get_autoassign_owner(cls, project_id, data): """ Get the auto-assign owner for a project if there are any. Will return None if there are no owners, or a list of owners. """ try: ownership = cls.objects.get(project_id=project_id) except cls.DoesNotExist: return None if not ownership.auto_assignment: return None rules = cls._matching_ownership_rules(ownership, project_id, data) if not rules: return None score = 0 owners = None # Automatic assignment prefers the owner with the longest # matching pattern as the match is more specific. for rule in rules: candidate = len(rule.matcher.pattern) if candidate > score: score = candidate owners = rule.owners actors = filter(None, resolve_actors(owners, project_id).values()) # 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 None return actors[0].resolve() @classmethod def _matching_ownership_rules(cls, ownership, project_id, data): rules = [] if ownership.schema is not None: for rule in load_schema(ownership.schema): if rule.test(data): rules.append(rule) return rules
class ProjectOwnership(Model): __core__ = True project = FlexibleForeignKey("sentry.Project", unique=True) raw = models.TextField(null=True) schema = JSONField(null=True) fallthrough = models.BooleanField(default=True) auto_assignment = models.BooleanField(default=False) date_created = models.DateTimeField(default=timezone.now) last_updated = models.DateTimeField(default=timezone.now) is_active = models.BooleanField(default=True) # An object to indicate ownership is implicitly everyone Everyone = object() class Meta: app_label = "sentry" db_table = "sentry_projectownership" __repr__ = sane_repr("project_id", "is_active") @classmethod def get_cache_key(self, project_id): return f"projectownership_project_id:1:{project_id}" @classmethod def get_combined_schema(self, ownership, codeowners): if codeowners and codeowners.schema: ownership.schema = (codeowners.schema if not ownership.schema else { **ownership.schema, "rules": [ *codeowners.schema["rules"], *ownership.schema["rules"], ], }) return ownership.schema @classmethod def get_ownership_cached(cls, project_id): """ Cached read access to projectownership. This method implements a negative cache which saves us a pile of read queries in post_processing as most projects don't have ownership rules. See the post_save and post_delete signals below for additional cache updates. """ cache_key = cls.get_cache_key(project_id) ownership = cache.get(cache_key) if ownership is None: try: ownership = cls.objects.get(project_id=project_id) except cls.DoesNotExist: ownership = False cache.set(cache_key, ownership, READ_CACHE_DURATION) return ownership or None @classmethod def get_owners(cls, project_id, data): """ For a given project_id, and event data blob. We combine the schemas from IssueOwners and CodeOwners. If Everyone is returned, this means we implicitly are falling through our rules and everyone is responsible. If an empty list is returned, this means there are explicitly no owners. """ from sentry.models import ProjectCodeOwners ownership = cls.get_ownership_cached(project_id) if not ownership: ownership = cls(project_id=project_id) codeowners = ProjectCodeOwners.get_codeowners_cached(project_id) ownership.schema = cls.get_combined_schema(ownership, codeowners) rules = cls._matching_ownership_rules(ownership, project_id, data) if not rules: return cls.Everyone if ownership.fallthrough else [], None owners = {o for rule in rules for o in rule.owners} owners_to_actors = resolve_actors(owners, project_id) ordered_actors = [] for rule in rules: for o in rule.owners: if o in owners and owners_to_actors.get(o) is not None: ordered_actors.append(owners_to_actors[o]) owners.remove(o) return ordered_actors, rules @classmethod def _find_actors(cls, project_id, rules, limit): """ Get 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] return actors @classmethod 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, ) @classmethod def _matching_ownership_rules(cls, ownership, project_id, data): rules = [] if ownership.schema is not None: for rule in load_schema(ownership.schema): if rule.test(data): rules.append(rule) return rules
class ProjectOwnership(Model): __core__ = True project = FlexibleForeignKey("sentry.Project", unique=True) raw = models.TextField(null=True) schema = JSONField(null=True) fallthrough = models.BooleanField(default=True) auto_assignment = models.BooleanField(default=False) date_created = models.DateTimeField(default=timezone.now) last_updated = models.DateTimeField(default=timezone.now) is_active = models.BooleanField(default=True) # An object to indicate ownership is implicitly everyone Everyone = object() class Meta: app_label = "sentry" db_table = "sentry_projectownership" __repr__ = sane_repr("project_id", "is_active") @classmethod def get_cache_key(self, project_id): return u"projectownership_project_id:1:{}".format(project_id) @classmethod def get_ownership_cached(cls, project_id): """ Cached read access to projectownership. This method implements a negative cache which saves us a pile of read queries in post_processing as most projects don't have ownership rules. See the post_save and post_delete signals below for additional cache updates. """ cache_key = cls.get_cache_key(project_id) ownership = cache.get(cache_key) if ownership is None: try: ownership = cls.objects.get(project_id=project_id) except cls.DoesNotExist: ownership = False cache.set(cache_key, ownership, READ_CACHE_DURATION) return ownership or None @classmethod def get_owners(cls, project_id, data): """ For a given project_id, and event data blob. If Everyone is returned, this means we implicitly are falling through our rules and everyone is responsible. If an empty list is returned, this means there are explicitly no owners. """ ownership = cls.get_ownership_cached(project_id) if not ownership: ownership = cls(project_id=project_id) rules = cls._matching_ownership_rules(ownership, project_id, data) if not rules: return cls.Everyone if ownership.fallthrough else [], None owners = {o for rule in rules for o in rule.owners} return filter(None, resolve_actors(owners, project_id).values()), rules @classmethod def get_autoassign_owner(cls, project_id, data): """ Get the auto-assign owner for a project if there are any. Will return None if there are no owners, or a list of owners. """ ownership = cls.get_ownership_cached(project_id) if not ownership or not ownership.auto_assignment: return None rules = cls._matching_ownership_rules(ownership, project_id, data) if not rules: return None score = 0 owners = None # Automatic assignment prefers the owner with the longest # matching pattern as the match is more specific. for rule in rules: candidate = len(rule.matcher.pattern) if candidate > score: score = candidate owners = rule.owners actors = filter(None, resolve_actors(owners, project_id).values()) # 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 None return actors[0].resolve() @classmethod def _matching_ownership_rules(cls, ownership, project_id, data): rules = [] if ownership.schema is not None: for rule in load_schema(ownership.schema): if rule.test(data): rules.append(rule) return rules