Skip to content

chore(integrations): register integration domains in initializer #80212

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
OrganizationIntegrationBaseEndpoint,
)
from sentry.integrations.api.serializers.models.integration import OrganizationIntegrationResponse
from sentry.integrations.base import INTEGRATION_TYPE_TO_PROVIDER, IntegrationDomain
from sentry.integrations.base import IntegrationDomain
from sentry.integrations.manager import default_manager as integrations
from sentry.integrations.models.integration import Integration
from sentry.integrations.models.organization_integration import OrganizationIntegration
from sentry.organizations.services.organization.model import (
Expand Down Expand Up @@ -117,7 +118,8 @@ def get(
except ValueError:
return Response({"detail": "Invalid integration type"}, status=400)
provider_slugs = [
provider for provider in INTEGRATION_TYPE_TO_PROVIDER.get(integration_domain, [])
provider
for provider in integrations.get_integrations_for_domain(integration_domain)
]
queryset = queryset.filter(integration__provider__in=provider_slugs)

Expand Down
49 changes: 5 additions & 44 deletions src/sentry/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,49 +135,6 @@ class IntegrationDomain(StrEnum):
IDENTITY = "identity" # for identity pipelines


class IntegrationProviderSlug(StrEnum):
SLACK = "slack"
DISCORD = "discord"
MSTeams = "msteams"
JIRA = "jira"
JIRA_SERVER = "jira_server"
AZURE_DEVOPS = "vsts"
GITHUB = "github"
GITHUB_ENTERPRISE = "github_enterprise"
GITLAB = "gitlab"
BITBUCKET = "bitbucket"
PAGERDUTY = "pagerduty"
OPSGENIE = "opsgenie"


INTEGRATION_TYPE_TO_PROVIDER = {
IntegrationDomain.MESSAGING: [
IntegrationProviderSlug.SLACK,
IntegrationProviderSlug.DISCORD,
IntegrationProviderSlug.MSTeams,
],
IntegrationDomain.PROJECT_MANAGEMENT: [
IntegrationProviderSlug.JIRA,
IntegrationProviderSlug.JIRA_SERVER,
IntegrationProviderSlug.GITHUB,
IntegrationProviderSlug.GITHUB_ENTERPRISE,
IntegrationProviderSlug.GITLAB,
IntegrationProviderSlug.AZURE_DEVOPS,
],
IntegrationDomain.SOURCE_CODE_MANAGEMENT: [
IntegrationProviderSlug.GITHUB,
IntegrationProviderSlug.GITHUB_ENTERPRISE,
IntegrationProviderSlug.GITLAB,
IntegrationProviderSlug.BITBUCKET,
IntegrationProviderSlug.AZURE_DEVOPS,
],
IntegrationDomain.ON_CALL_SCHEDULING: [
IntegrationProviderSlug.PAGERDUTY,
IntegrationProviderSlug.OPSGENIE,
],
}


class IntegrationProvider(PipelineProvider, abc.ABC):
"""
An integration provider describes a third party that can be registered within Sentry.
Expand Down Expand Up @@ -245,6 +202,8 @@ class is just a descriptor for how that object functions, and what behavior
requires_feature_flag = False
"""if this is hidden without the feature flag"""

domain: IntegrationDomain | None = None

@classmethod
def get_installation(
cls, model: RpcIntegration | Integration, organization_id: int, **kwargs: Any
Expand Down Expand Up @@ -593,8 +552,10 @@ def disable_integration(


def get_integration_types(provider: str):
from sentry.integrations.manager import default_manager as integrations

types = []
for integration_type, providers in INTEGRATION_TYPE_TO_PROVIDER.items():
for integration_type, providers in integrations.get_integrations_by_domain().items():
if provider in providers:
types.append(integration_type)
return types
1 change: 1 addition & 0 deletions src/sentry/integrations/bitbucket/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def username(self):
class BitbucketIntegrationProvider(IntegrationProvider):
key = "bitbucket"
name = "Bitbucket"
domain = IntegrationDomain.SOURCE_CODE_MANAGEMENT
metadata = metadata
scopes = scopes
integration_cls = BitbucketIntegration
Expand Down
1 change: 1 addition & 0 deletions src/sentry/integrations/bitbucket_server/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ def username(self):
class BitbucketServerIntegrationProvider(IntegrationProvider):
key = "bitbucket_server"
name = "Bitbucket Server"
domain = IntegrationDomain.SOURCE_CODE_MANAGEMENT
metadata = metadata
integration_cls = BitbucketServerIntegration
needs_default_identity = True
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/discord/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from sentry.constants import ObjectStatus
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationInstallation,
IntegrationMetadata,
Expand Down Expand Up @@ -114,6 +115,7 @@ def uninstall(self) -> None:
class DiscordIntegrationProvider(IntegrationProvider):
key = "discord"
name = "Discord"
domain = IntegrationDomain.MESSAGING
metadata = metadata
integration_cls = DiscordIntegration
features = frozenset([IntegrationFeatures.CHAT_UNFURL, IntegrationFeatures.ALERT_RULE])
Expand Down
1 change: 1 addition & 0 deletions src/sentry/integrations/github/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ def search_issues(self, query: str | None, **kwargs) -> dict[str, Any]:
class GitHubIntegrationProvider(IntegrationProvider):
key = "github"
name = "GitHub"
domain = IntegrationDomain.SOURCE_CODE_MANAGEMENT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely a bit nicer. Tying the domain to the provider class really solidifies what an integration's responsibility is at instantiation time. I also remember @ameliahsu mentioning turning this into an array, which should work for the multiple domains on a single integration problem, but is the issue that we need a single "primary" domain per integration?

metadata = metadata
integration_cls = GitHubIntegration
features = frozenset(
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/github_enterprise/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sentry.identity.pipeline import IdentityProviderPipeline
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatureNotImplementedError,
IntegrationFeatures,
IntegrationMetadata,
Expand Down Expand Up @@ -318,6 +319,7 @@ def dispatch(self, request: Request, pipeline) -> HttpResponse:
class GitHubEnterpriseIntegrationProvider(GitHubIntegrationProvider):
key = "github_enterprise"
name = "GitHub Enterprise"
domain = IntegrationDomain.SOURCE_CODE_MANAGEMENT
metadata = metadata
integration_cls = GitHubEnterpriseIntegration
features = frozenset(
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/gitlab/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sentry.identity.pipeline import IdentityProviderPipeline
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationMetadata,
IntegrationProvider,
Expand Down Expand Up @@ -313,6 +314,7 @@ def dispatch(self, request: Request, pipeline) -> HttpResponse:
class GitlabIntegrationProvider(IntegrationProvider):
key = "gitlab"
name = "GitLab"
domain = IntegrationDomain.SOURCE_CODE_MANAGEMENT
metadata = metadata
integration_cls = GitlabIntegration

Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/jira/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from sentry.eventstore.models import GroupEvent
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationMetadata,
IntegrationProvider,
Expand Down Expand Up @@ -995,6 +996,7 @@ def parse_jira_issue_metadata(self, meta: dict[str, Any]) -> list[JiraIssueTypeM
class JiraIntegrationProvider(IntegrationProvider):
key = "jira"
name = "Jira"
domain = IntegrationDomain.PROJECT_MANAGEMENT
metadata = metadata
integration_cls = JiraIntegration

Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/jira_server/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sentry import features
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationMetadata,
IntegrationProvider,
Expand Down Expand Up @@ -1167,6 +1168,7 @@ def migrate_issues(self):
class JiraServerIntegrationProvider(IntegrationProvider):
key = "jira_server"
name = "Jira Server"
domain = IntegrationDomain.PROJECT_MANAGEMENT
metadata = metadata
integration_cls = JiraServerIntegration

Expand Down
25 changes: 24 additions & 1 deletion src/sentry/integrations/manager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

from collections import defaultdict
from collections.abc import Iterable, Iterator
from typing import Any

from sentry.exceptions import NotRegistered
from sentry.integrations.base import IntegrationProvider
from sentry.integrations.base import IntegrationDomain, IntegrationProvider

__all__ = ["IntegrationManager"]

Expand All @@ -14,6 +15,12 @@
class IntegrationManager:
def __init__(self) -> None:
self.__values: dict[str, type[IntegrationProvider]] = {}
self.__domain_integrations: dict[IntegrationDomain, list[str]] = defaultdict(
list
) # integrations by domain
self.__integration_domains: dict[str, IntegrationDomain] = (
{}
) # map of integration to domain
Comment on lines +21 to +23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: I think it's fine to generate this on the fly from the list of integration providers instead of storing it on the class. I don't think it'll add too much overhead and reduces our need to remove them when something unregisters.


def __iter__(self) -> Iterator[IntegrationProvider]:
return iter(self.all())
Expand All @@ -36,6 +43,9 @@ def exists(self, key: str) -> bool:

def register(self, cls: type[IntegrationProvider]) -> None:
self.__values[cls.key] = cls
if cls.domain:
self.__domain_integrations[cls.domain].append(cls.key)
self.__integration_domains[cls.key] = cls.domain

def unregister(self, cls: type[IntegrationProvider]) -> None:
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do decide to keep the domain dictionaries around, you'll need to remove integrations from them here.

Expand All @@ -47,6 +57,19 @@ def unregister(self, cls: type[IntegrationProvider]) -> None:
return
del self.__values[cls.key]

def get_integrations_for_domain(self, domain: IntegrationDomain) -> list[str]:
if domain not in self.__domain_integrations:
return []
return self.__domain_integrations[domain]

def get_integrations_by_domain(self) -> dict[IntegrationDomain, list[str]]:
return self.__domain_integrations

def get_integration_domain(self, key: str) -> IntegrationDomain:
if key not in self.__integration_domains:
raise NotRegistered(key)
return self.__integration_domains[key]


default_manager = IntegrationManager()
all = default_manager.all
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/msteams/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from sentry import options
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationInstallation,
IntegrationMetadata,
Expand Down Expand Up @@ -79,6 +80,7 @@ def get_client(self) -> MsTeamsClient:
class MsTeamsIntegrationProvider(IntegrationProvider):
key = "msteams"
name = "Microsoft Teams"
domain = IntegrationDomain.MESSAGING
can_add = False
metadata = metadata
integration_cls = MsTeamsIntegration
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/opsgenie/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sentry.constants import ObjectStatus
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationInstallation,
IntegrationMetadata,
Expand Down Expand Up @@ -229,6 +230,7 @@ def schedule_migrate_opsgenie_plugin(self):
class OpsgenieIntegrationProvider(IntegrationProvider):
key = "opsgenie"
name = "Opsgenie"
domain = IntegrationDomain.ON_CALL_SCHEDULING
metadata = metadata
integration_cls = OpsgenieIntegration
features = frozenset([IntegrationFeatures.INCIDENT_MANAGEMENT, IntegrationFeatures.ALERT_RULE])
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/pagerduty/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from sentry import options
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationInstallation,
IntegrationMetadata,
Expand Down Expand Up @@ -166,6 +167,7 @@ def services(self) -> list[PagerDutyServiceDict]:
class PagerDutyIntegrationProvider(IntegrationProvider):
key = "pagerduty"
name = "PagerDuty"
domain = IntegrationDomain.ON_CALL_SCHEDULING
metadata = metadata
features = frozenset([IntegrationFeatures.ALERT_RULE, IntegrationFeatures.INCIDENT_MANAGEMENT])
integration_cls = PagerDutyIntegration
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/slack/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sentry.identity.pipeline import IdentityProviderPipeline
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationInstallation,
IntegrationMetadata,
Expand Down Expand Up @@ -88,6 +89,7 @@ def get_config_data(self) -> Mapping[str, str]:
class SlackIntegrationProvider(IntegrationProvider):
key = "slack"
name = "Slack"
domain = IntegrationDomain.MESSAGING
metadata = metadata
features = frozenset([IntegrationFeatures.CHAT_UNFURL, IntegrationFeatures.ALERT_RULE])
integration_cls = SlackIntegration
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/vsts/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sentry.identity.vsts.provider import get_user_info
from sentry.integrations.base import (
FeatureDescription,
IntegrationDomain,
IntegrationFeatures,
IntegrationMetadata,
IntegrationProvider,
Expand Down Expand Up @@ -381,6 +382,7 @@ def default_project(self) -> str | None:
class VstsIntegrationProvider(IntegrationProvider):
key = "vsts"
name = "Azure DevOps"
domain = IntegrationDomain.SOURCE_CODE_MANAGEMENT
metadata = metadata
api_version = "4.1"
oauth_redirect_url = "/extensions/vsts/setup/"
Expand Down
Loading