コード例 #1
0
class AsanaPlugin(CorePluginMixin, IssuePlugin2):
    description = DESCRIPTION
    slug = "asana"
    title = "Asana"
    conf_title = title
    conf_key = "asana"
    auth_provider = "asana"
    required_field = "workspace"
    feature_descriptions = [
        FeatureDescription(
            """
            Create and link Sentry issue groups directly to an Asana ticket in any of your
            projects, providing a quick way to jump from a Sentry bug to tracked ticket!
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
        FeatureDescription(
            """
            Link Sentry issues to existing Asana tickets.
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
    ]

    def get_group_urls(self):
        return super().get_group_urls() + [
            url(
                r"^autocomplete",
                IssueGroupActionEndpoint.as_view(view_method_name="view_autocomplete", plugin=self),
            )
        ]

    def is_configured(self, request, project, **kwargs):
        return bool(self.get_option("workspace", project))

    def has_workspace_access(self, workspace, choices):
        for c, _ in choices:
            if workspace == c:
                return True
        return False

    def get_workspace_choices(self, workspaces):
        return [(w["gid"], w["name"]) for w in workspaces["data"]]

    def get_new_issue_fields(self, request, group, event, **kwargs):
        fields = super().get_new_issue_fields(request, group, event, **kwargs)
        client = self.get_client(request.user)
        workspaces = client.get_workspaces()
        workspace_choices = self.get_workspace_choices(workspaces)
        workspace = self.get_option("workspace", group.project)
        if workspace and not self.has_workspace_access(workspace, workspace_choices):
            workspace_choices.append((workspace, workspace))

        # use labels that are more applicable to asana
        for field in fields:
            if field["name"] == "title":
                field["label"] = "Name"
            if field["name"] == "description":
                field["label"] = "Notes"
                field["required"] = False

        return (
            [
                {
                    "name": "workspace",
                    "label": "Asana Workspace",
                    "default": workspace,
                    "type": "select",
                    "choices": workspace_choices,
                    "readonly": True,
                }
            ]
            + fields
            + [
                {
                    "name": "project",
                    "label": "Project",
                    "type": "select",
                    "has_autocomplete": True,
                    "required": False,
                    "placeholder": "Start typing to search for a project",
                },
                {
                    "name": "assignee",
                    "label": "Assignee",
                    "type": "select",
                    "has_autocomplete": True,
                    "required": False,
                    "placeholder": "Start typing to search for a user",
                },
            ]
        )

    def get_link_existing_issue_fields(self, request, group, event, **kwargs):
        return [
            {
                "name": "issue_id",
                "label": "Task",
                "default": "",
                "type": "select",
                "has_autocomplete": True,
            },
            {
                "name": "comment",
                "label": "Comment",
                "default": absolute_uri(
                    group.get_absolute_url(params={"referrer": "asana_plugin"})
                ),
                "type": "textarea",
                "help": ("Leave blank if you don't want to " "add a comment to the Asana issue."),
                "required": False,
            },
        ]

    def get_client(self, user):
        auth = self.get_auth_for_user(user=user)
        if auth is None:
            raise PluginIdentityRequired(ERR_AUTH_NOT_CONFIGURED)
        return AsanaClient(auth=auth)

    def error_message_from_json(self, data):
        errors = data.get("errors")
        if errors:
            return " ".join(e["message"] for e in errors)
        return "unknown error"

    def create_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(request.user)

        try:
            response = client.create_issue(
                workspace=self.get_option("workspace", group.project), data=form_data
            )
        except Exception as e:
            self.raise_error(e, identity=client.auth)

        return response["data"]["gid"]

    def link_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(request.user)
        try:
            issue = client.get_issue(issue_id=form_data["issue_id"])["data"]
        except Exception as e:
            self.raise_error(e, identity=client.auth)

        comment = form_data.get("comment")
        if comment:
            try:
                client.create_comment(issue["gid"], {"text": comment})
            except Exception as e:
                self.raise_error(e, identity=client.auth)

        return {"title": issue["name"]}

    def get_issue_label(self, group, issue_id, **kwargs):
        return "Asana Issue"

    def get_issue_url(self, group, issue_id, **kwargs):
        return "https://app.asana.com/0/0/%s" % issue_id

    def validate_config(self, project, config, actor):
        """
        ```
        if config['foo'] and not config['bar']:
            raise PluginError('You cannot configure foo with bar')
        return config
        ```
        """
        try:
            int(config["workspace"])
        except ValueError as exc:
            self.logger.exception(str(exc))
            raise PluginError("Non-numeric workspace value")
        return config

    def get_config(self, *args, **kwargs):
        user = kwargs["user"]
        try:
            client = self.get_client(user)
        except PluginIdentityRequired as e:
            self.raise_error(e)
        try:
            workspaces = client.get_workspaces()
        except HTTPError as e:
            if (
                e.response.status_code == 400
                and e.response.url == "https://app.asana.com/-/oauth_token"
            ):
                raise PluginIdentityRequired(ERR_BEARER_EXPIRED)
            raise
        workspace_choices = self.get_workspace_choices(workspaces)
        workspace = self.get_option("workspace", kwargs["project"])
        # check to make sure the current user has access to the workspace
        helptext = None
        if workspace and not self.has_workspace_access(workspace, workspace_choices):
            workspace_choices.append((workspace, workspace))
            helptext = (
                "This plugin has been configured for an Asana workspace "
                "that either you don't have access to or doesn't "
                "exist. You can edit the configuration, but you will not "
                "be able to change it back to the current configuration "
                "unless a teammate grants you access to the workspace in Asana."
            )
        return [
            {
                "name": "workspace",
                "label": "Workspace",
                "type": "select",
                "choices": workspace_choices,
                "default": workspace or workspaces["data"][0]["gid"],
                "help": helptext,
            }
        ]

    def view_autocomplete(self, request, group, **kwargs):
        field = request.GET.get("autocomplete_field")
        query = request.GET.get("autocomplete_query")

        client = self.get_client(request.user)
        workspace = self.get_option("workspace", group.project)
        results = []
        field_name = field
        if field == "issue_id":
            field_name = "task"
        elif field == "assignee":
            field_name = "user"
        try:
            response = client.search(workspace, field_name, query.encode("utf-8"))
        except Exception as e:
            return Response(
                {"error_type": "validation", "errors": [{"__all__": self.message_from_error(e)}]},
                status=400,
            )
        else:
            results = [
                {"text": "(#{}) {}".format(i["gid"], i["name"]), "id": i["gid"]}
                for i in response.get("data", [])
            ]

        return Response({field: results})
コード例 #2
0
ファイル: plugin.py プロジェクト: liang0/sentry-1
class TwilioPlugin(CorePluginMixin, NotificationPlugin):
    author = "Matt Robenolt"
    author_url = "https://github.com/mattrobenolt"
    version = sentry.VERSION
    description = DESCRIPTION
    resource_links = (
        (
            "Documentation",
            "https://github.com/getsentry/sentry/blob/master/src/sentry_plugins/twilio/Twilio_Instructions.md",
        ),
        ("Report Issue", "https://github.com/getsentry/sentry/issues"),
        (
            "View Source",
            "https://github.com/getsentry/sentry/tree/master/src/sentry_plugins/twilio",
        ),
        ("Twilio", "https://www.twilio.com/"),
    )

    slug = "twilio"
    title = _("Twilio (SMS)")
    conf_title = title
    conf_key = "twilio"
    required_field = "account_sid"
    project_conf_form = TwilioConfigurationForm
    feature_descriptions = [
        FeatureDescription(
            """
            Set up SMS notifications to be sent to your mobile device via Twilio.
            """,
            IntegrationFeatures.MOBILE,
        ),
        FeatureDescription(
            """
            Configure Sentry rules to trigger notifications based on conditions you set.
            """,
            IntegrationFeatures.ALERT_RULE,
        ),
    ]

    def is_configured(self, project, **kwargs):
        return all([
            self.get_option(o, project)
            for o in ("account_sid", "auth_token", "sms_from", "sms_to")
        ])

    def get_send_to(self, *args, **kwargs):
        # This doesn't depend on email permission... stuff.
        return True

    def error_message_from_json(self, data):
        code = data.get("code")
        message = data.get("message")
        more_info = data.get("more_info")
        error_message = "%s - %s %s" % (code, message, more_info)
        if message:
            return error_message
        return None

    def notify_users(self, group, event, **kwargs):
        if not self.is_configured(group.project):
            return
        project = group.project

        body = "Sentry [{0}] {1}: {2}".format(
            project.name.encode("utf-8"),
            event.group.get_level_display().upper().encode("utf-8"),
            event.title.encode("utf-8").splitlines()[0],
        )
        body = body[:MAX_SMS_LENGTH]

        client = self.get_client(group.project)

        payload = {"From": client.sms_from, "Body": body}

        errors = []

        for phone in client.sms_to:
            if not phone:
                continue
            try:
                # TODO: Use API client with raise_error
                phone = clean_phone(phone)
                payload = payload.copy()
                payload["To"] = phone
                client.request(payload)
            except Exception as e:
                errors.append(e)

        if errors:
            self.raise_error(errors[0])

    def get_client(self, project):
        account_sid = self.get_option("account_sid", project)
        auth_token = self.get_option("auth_token", project)
        sms_from = clean_phone(self.get_option("sms_from", project))
        sms_to = self.get_option("sms_to", project)
        sms_to = split_sms_to(sms_to)
        return TwilioApiClient(account_sid, auth_token, sms_from, sms_to)
コード例 #3
0
ファイル: plugin.py プロジェクト: pasala91/test
class SplunkPlugin(CorePluginMixin, DataForwardingPlugin):
    title = "Splunk"
    slug = "splunk"
    description = DESCRIPTION
    conf_key = "splunk"
    resource_links = [("Splunk Setup Instructions", SETUP_URL)] + CorePluginMixin.resource_links
    required_field = "instance"
    project_token = None
    project_index = None
    project_source = None
    project_instance = None
    host = None
    feature_descriptions = [
        FeatureDescription(
            """
            Forward Sentry errors and events to Splunk.
            """,
            IntegrationFeatures.DATA_FORWARDING,
        )
    ]

    def get_rate_limit(self):
        # number of requests, number of seconds (window)
        return (1000, 1)

    def get_client(self):
        return SplunkApiClient(self.project_instance, self.project_token)

    def get_config(self, project, **kwargs):
        return [
            {
                "name": "instance",
                "label": "Instance URL",
                "type": "url",
                "required": True,
                "help": "The HTTP Event Collector endpoint for your Splunk instance.",
                "placeholder": "e.g. https://input-foo.cloud.splunk.com:8088",
            },
            {
                "name": "index",
                "label": "Index",
                "type": "string",
                "required": True,
                "default": "main",
            },
            {
                "name": "source",
                "label": "Source",
                "type": "string",
                "required": True,
                "default": "sentry",
            },
            get_secret_field_config(
                name="token", label="Token", secret=self.get_option("token", project)
            ),
        ]

    def get_host_for_splunk(self, event):
        host = event.get_tag("server_name")
        if host:
            return host

        user_interface = event.interfaces.get("sentry.interfaces.User")
        if user_interface:
            host = user_interface.ip_address
            if host:
                return host

        return None

    def get_event_payload_properties(self, event):
        props = {
            "event_id": event.event_id,
            "issue_id": event.group_id,
            "project_id": event.project.slug,
            "transaction": event.get_tag("transaction") or "",
            "release": event.get_tag("sentry:release") or "",
            "environment": event.get_tag("environment") or "",
            "type": event.get_event_type(),
        }
        props["tags"] = [[k.format(tagstore.get_standardized_key(k)), v] for k, v in event.tags]
        for key, value in event.interfaces.items():
            if key == "request":
                headers = value.headers
                if not isinstance(headers, dict):
                    headers = dict(headers or ())

                props.update(
                    {
                        "request_url": value.url,
                        "request_method": value.method,
                        "request_referer": headers.get("Referer", ""),
                    }
                )
            elif key == "exception":
                exc = value.values[0]
                props.update({"exception_type": exc.type, "exception_value": exc.value})
            elif key == "logentry":
                props.update({"message": value.formatted or value.message})
            elif key in ("csp", "expectct", "expectstable", "hpkp"):
                props.update(
                    {
                        "{}_{}".format(key.rsplit(".", 1)[-1].lower(), k): v
                        for k, v in value.to_json().items()
                    }
                )
            elif key == "user":
                user_payload = {}
                if value.id:
                    user_payload["user_id"] = value.id
                if value.email:
                    user_payload["user_email_hash"] = md5_text(value.email).hexdigest()
                if value.ip_address:
                    user_payload["user_ip_trunc"] = anonymize_ip(value.ip_address)
                if user_payload:
                    props.update(user_payload)
        return props

    def initialize_variables(self, event):
        self.project_token = self.get_option("token", event.project)
        self.project_index = self.get_option("index", event.project)
        self.project_instance = self.get_option("instance", event.project)
        self.host = self.get_host_for_splunk(event)

        if self.project_instance and not self.project_instance.endswith("/services/collector"):
            self.project_instance = self.project_instance.rstrip("/") + "/services/collector"

        self.project_source = self.get_option("source", event.project) or "sentry"

    def get_rl_key(self, event):
        return f"{self.conf_key}:{md5_text(self.project_token).hexdigest()}"

    def is_ratelimited(self, event):
        if super().is_ratelimited(event):
            metrics.incr(
                "integrations.splunk.forward-event.rate-limited",
                tags={
                    "project_id": event.project_id,
                    "organization_id": event.project.organization_id,
                    "event_type": event.get_event_type(),
                },
            )
            return True
        return False

    def get_event_payload(self, event):
        payload = {
            "time": int(event.datetime.strftime("%s")),
            "source": self.project_source,
            "index": self.project_index,
            "event": self.get_event_payload_properties(event),
        }
        return payload

    def forward_event(self, event, payload):
        if not (self.project_token and self.project_index and self.project_instance):
            metrics.incr(
                "integrations.splunk.forward-event.unconfigured",
                tags={
                    "project_id": event.project_id,
                    "organization_id": event.project.organization_id,
                    "event_type": event.get_event_type(),
                },
            )
            return False

        if self.host:
            payload["host"] = self.host

        client = self.get_client()

        try:
            # https://docs.splunk.com/Documentation/Splunk/7.2.3/Data/TroubleshootHTTPEventCollector
            client.request(payload)

            metrics.incr(
                "integrations.splunk.forward-event.success",
                tags={
                    "project_id": event.project_id,
                    "organization_id": event.project.organization_id,
                    "event_type": event.get_event_type(),
                },
            )
            return True
        except Exception as exc:
            metric = "integrations.splunk.forward-event.error"
            metrics.incr(
                metric,
                tags={
                    "project_id": event.project_id,
                    "organization_id": event.project.organization_id,
                    "event_type": event.get_event_type(),
                },
            )
            logger.info(
                metric,
                extra={
                    "instance": self.project_instance,
                    "project_id": event.project_id,
                    "organization_id": event.project.organization_id,
                    "error": str(exc),
                },
            )

            if isinstance(
                exc,
                (
                    ApiHostError,
                    ApiTimeoutError,
                ),
            ):
                # The above errors are already handled by the API client.
                # Just log and return.
                return False

            if isinstance(exc, ApiError) and exc.code == 403:
                # 403s are not errors or actionable for us do not re-raise
                return False

            raise
コード例 #4
0
ファイル: integration.py プロジェクト: toanant/sentry
    def dispatch(self, request, pipeline):
        if "name" in request.POST:
            pipeline.bind_state("name", request.POST["name"])
            return pipeline.next_step()

        return HttpResponse(self.TEMPLATE)


DESCRIPTION = """
This is an example integration. Descriptions support _markdown rendering_.
"""

FEATURES = [
    FeatureDescription(
        "This is a feature description. Also *supports markdown*",
        IntegrationFeatures.ISSUE_SYNC)
]

metadata = IntegrationMetadata(
    description=DESCRIPTION.strip(),
    features=FEATURES,
    author="The Sentry Team",
    noun="example",
    issue_url="https://github.com/getsentry/sentry/issues/new",
    source_url="https://github.com/getsentry/sentry",
    aspects={},
)


class ExampleIntegration(IntegrationInstallation, IssueSyncMixin):
コード例 #5
0
ファイル: integration.py プロジェクト: blacknode/sentry
from .client import VstsApiClient
from .repository import VstsRepositoryProvider
from .webhooks import WorkItemWebhook

DESCRIPTION = """
Connect your Sentry organization to one or more of your Azure DevOps
organizations. Get started streamlining your bug squashing workflow by unifying
your Sentry and Azure DevOps organization together.
"""

FEATURES = [
    FeatureDescription(
        """
        Authorize repositories to be added to your Sentry organization to augment
        sentry issues with commit data with [deployment
        tracking](https://docs.sentry.io/learn/releases/).
        """,
        IntegrationFeatures.COMMITS,
    ),
    FeatureDescription(
        """
        Create and link Sentry issue groups directly to a Azure DevOps work item in any of
        your projects, providing a quick way to jump from Sentry bug to tracked
        work item!
        """,
        IntegrationFeatures.ISSUE_BASIC,
    ),
    FeatureDescription(
        """
        Automatically synchronize comments and assignees to and from Azure DevOps. Don't get
        confused who's fixing what, let us handle ensuring your issues and work
コード例 #6
0
ファイル: plugin.py プロジェクト: lizardkinggg/getsentry
class HerokuPlugin(CorePluginMixin, ReleaseTrackingPlugin):
    author = "Sentry Team"
    author_url = "https://github.com/getsentry"
    title = "Heroku"
    slug = "heroku"
    description = "Integrate Heroku release tracking."
    required_field = "repository"
    feature_descriptions = [
        FeatureDescription(
            """
            Integrate Heroku release tracking.
            """,
            IntegrationFeatures.DEPLOYMENT,
        )
    ]

    def configure(self, project, request):
        return react_plugin_config(self, project, request)

    def can_enable_for_projects(self):
        return True

    def can_configure_for_project(self, project):
        return True

    def has_project_conf(self):
        return True

    def get_conf_key(self):
        return "heroku"

    def get_config(self, project, **kwargs):
        repo_list = list(Repository.objects.filter(organization_id=project.organization_id))
        if not ProjectOption.objects.get_value(project=project, key="heroku:repository"):
            choices = [("", "select a repo")]
        else:
            choices = []
        choices.extend([(repo.name, repo.name) for repo in repo_list])
        return [
            {
                "name": "repository",
                "label": "Respository",
                "type": "select",
                "required": True,
                "choices": choices,
                "help": "Select which repository you would like to be associated with this project",
            },
            {
                "name": "environment",
                "label": "Deploy Environment",
                "type": "text",
                "required": False,
                "default": "production",
                "help": "Specify an environment name for your Heroku deploys",
            },
        ]

    def get_release_doc_html(self, hook_url):
        return """
        <p>Add Sentry as a deploy hook to automatically track new releases.</p>
        <pre class="clippy">heroku addons:create deployhooks:http --url={hook_url}</pre>
        """.format(
            hook_url=hook_url
        )

    def get_release_hook(self):
        return HerokuReleaseHook
コード例 #7
0
ファイル: integration.py プロジェクト: uday160386/sentry
from sentry.utils.compat import filter, map
from sentry.utils import json

from .client import gen_aws_client
from .utils import parse_arn, get_index_of_sentry_layer, get_aws_node_arn

logger = logging.getLogger("sentry.integrations.aws_lambda")

DESCRIPTION = """
The AWS Lambda integration will automatically instrument your Lambda functions without any code changes. All you need to do is run a CloudFormation stack that we provide to get started.
"""

FEATURES = [
    FeatureDescription(
        """
        Instrument your serverless code automatically.
        """,
        IntegrationFeatures.SERVERLESS,
    ),
]

metadata = IntegrationMetadata(
    description=DESCRIPTION.strip(),
    features=FEATURES,
    author="The Sentry Team",
    noun=_("Installation"),
    issue_url="https://github.com/getsentry/sentry/issues/new",
    source_url=
    "https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/aws_lambda",
    aspects={},
)
コード例 #8
0
class JiraPlugin(CorePluginMixin, IssuePlugin2):
    description = "Integrate JIRA issues by linking a project."
    slug = "jira"
    title = "JIRA"
    conf_title = title
    conf_key = slug
    required_field = "username"
    feature_descriptions = [
        FeatureDescription(
            """
            Create and link Sentry issue groups directly to a Jira ticket in any of your
            projects, providing a quick way to jump from a Sentry bug to tracked ticket!
            """,
            IntegrationFeatures.ISSUE_BASIC,
        )
    ]

    def get_group_urls(self):
        _patterns = super().get_group_urls()
        _patterns.append(
            url(
                r"^autocomplete",
                IssueGroupActionEndpoint.as_view(
                    view_method_name="view_autocomplete", plugin=self),
            ))
        return _patterns

    def is_configured(self, request: Request, project, **kwargs):
        if not self.get_option("default_project", project):
            return False
        return True

    def get_group_description(self, request: Request, group, event):
        # mostly the same as parent class, but change ``` to {code}
        output = [
            absolute_uri(
                group.get_absolute_url(params={"referrer": "jira_plugin"}))
        ]
        body = self.get_group_body(request, group, event)
        if body:
            output.extend(["", "{code}", body, "{code}"])
        return "\n".join(output)

    def build_dynamic_field(self, group, field_meta):
        """
        Builds a field based on JIRA's meta field information
        """
        schema = field_meta["schema"]

        # set up some defaults for form fields
        fieldtype = "text"
        fkwargs = {
            "label": field_meta["name"],
            "required": field_meta["required"]
        }
        # override defaults based on field configuration
        if (schema["type"] in ["securitylevel", "priority"]
                or schema.get("custom") == JIRA_CUSTOM_FIELD_TYPES["select"]):
            fieldtype = "select"
            fkwargs["choices"] = self.make_choices(
                field_meta.get("allowedValues"))
        elif field_meta.get("autoCompleteUrl") and (
                schema.get("items") == "user" or schema["type"] == "user"):
            fieldtype = "select"
            sentry_url = f"/api/0/issues/{group.id}/plugins/{self.slug}/autocomplete"
            fkwargs["url"] = "{}?jira_url={}".format(
                sentry_url,
                quote_plus(field_meta["autoCompleteUrl"]),
            )
            fkwargs["has_autocomplete"] = True
            fkwargs["placeholder"] = "Start typing to search for a user"
        elif schema["type"] in ["timetracking"]:
            # TODO: Implement timetracking (currently unsupported alltogether)
            return None
        elif schema.get("items") in ["worklog", "attachment"]:
            # TODO: Implement worklogs and attachments someday
            return None
        elif schema["type"] == "array" and schema["items"] != "string":
            fieldtype = "select"
            fkwargs.update({
                "multiple":
                True,
                "choices":
                self.make_choices(field_meta.get("allowedValues")),
                "default": [],
            })

        # break this out, since multiple field types could additionally
        # be configured to use a custom property instead of a default.
        if schema.get("custom"):
            if schema["custom"] == JIRA_CUSTOM_FIELD_TYPES["textarea"]:
                fieldtype = "textarea"

        fkwargs["type"] = fieldtype
        return fkwargs

    def get_issue_type_meta(self, issue_type, meta):
        issue_types = meta["issuetypes"]
        issue_type_meta = None
        if issue_type:
            matching_type = [t for t in issue_types if t["id"] == issue_type]
            issue_type_meta = matching_type[0] if len(
                matching_type) > 0 else None

        # still no issue type? just use the first one.
        if not issue_type_meta:
            issue_type_meta = issue_types[0]

        return issue_type_meta

    def get_new_issue_fields(self, request: Request, group, event, **kwargs):
        fields = super().get_new_issue_fields(request, group, event, **kwargs)

        jira_project_key = self.get_option("default_project", group.project)

        client = self.get_jira_client(group.project)
        try:
            meta = client.get_create_meta_for_project(jira_project_key)
        except ApiError as e:
            raise PluginError(
                f"JIRA responded with an error. We received a status code of {e.code}"
            )
        except ApiUnauthorized:
            raise PluginError(
                "JIRA returned: Unauthorized. "
                "Please check your username, password, "
                "instance and project in your configuration settings.")

        if not meta:
            raise PluginError("Error in JIRA configuration, no projects "
                              "found for user %s." % client.username)

        # check if the issuetype was passed as a GET parameter
        issue_type = None
        if request is not None:
            if request.method == "POST":
                issue_type = request.data.get("issuetype")
            else:
                issue_type = request.GET.get("issuetype")

        if issue_type is None:
            issue_type = self.get_option("default_issue_type", group.project)

        issue_type_meta = self.get_issue_type_meta(issue_type, meta)

        issue_type_choices = self.make_choices(meta["issuetypes"])

        # make sure default issue type is actually
        # one that is allowed for project
        if issue_type:
            if not any(c for c in issue_type_choices if c[0] == issue_type):
                issue_type = issue_type_meta["id"]

        fields = [
            {
                "name": "project",
                "label": "Jira Project",
                "choices": ((meta["id"], jira_project_key), ),
                "default": meta["id"],
                "type": "select",
                "readonly": True,
            },
            *fields,
            {
                "name": "issuetype",
                "label": "Issue Type",
                "default": issue_type or issue_type_meta["id"],
                "type": "select",
                "choices": issue_type_choices,
            },
        ]

        # title is renamed to summary before sending to JIRA
        standard_fields = [f["name"] for f in fields] + ["summary"]
        ignored_fields = (self.get_option("ignored_fields", group.project)
                          or "").split(",")

        # apply ordering to fields based on some known built-in JIRA fields.
        # otherwise weird ordering occurs.
        anti_gravity = {
            "priority": -150,
            "fixVersions": -125,
            "components": -100,
            "security": -50
        }

        dynamic_fields = list(issue_type_meta.get("fields").keys())
        dynamic_fields.sort(key=lambda f: anti_gravity.get(f) or 0)
        # Build up some dynamic fields based on what is required.
        for field in dynamic_fields:
            if field in standard_fields or field in [
                    x.strip() for x in ignored_fields
            ]:
                # don't overwrite the fixed fields for the form.
                continue
            mb_field = self.build_dynamic_field(
                group, issue_type_meta["fields"][field])
            if mb_field:
                mb_field["name"] = field
                fields.append(mb_field)

        for field in fields:
            if field["name"] == "priority":
                # whenever priorities are available, put the available ones in the list.
                # allowedValues for some reason doesn't pass enough info.
                field["choices"] = self.make_choices(client.get_priorities())
                field["default"] = self.get_option("default_priority",
                                                   group.project) or ""
            elif field["name"] == "fixVersions":
                field["choices"] = self.make_choices(
                    client.get_versions(jira_project_key))

        return fields

    def get_link_existing_issue_fields(self, request: Request, group, event,
                                       **kwargs):
        return [
            {
                "name": "issue_id",
                "label": "Issue",
                "default": "",
                "type": "select",
                "has_autocomplete": True,
            },
            {
                "name":
                "comment",
                "label":
                "Comment",
                "default":
                absolute_uri(
                    group.get_absolute_url(
                        params={"referrer": "jira_plugin"})),
                "type":
                "textarea",
                "help": ("Leave blank if you don't want to "
                         "add a comment to the JIRA issue."),
                "required":
                False,
            },
        ]

    def link_issue(self, request: Request, group, form_data, **kwargs):
        client = self.get_jira_client(group.project)
        try:
            issue = client.get_issue(form_data["issue_id"])
        except Exception as e:
            raise self.raise_error(e)

        comment = form_data.get("comment")
        if comment:
            try:
                client.create_comment(issue["key"], comment)
            except Exception as e:
                raise self.raise_error(e)

        return {"title": issue["fields"]["summary"]}

    def get_issue_label(self, group, issue_id, **kwargs):
        return issue_id

    def get_issue_url(self, group, issue_id, **kwargs):
        instance = self.get_option("instance_url", group.project)
        return f"{instance}/browse/{issue_id}"

    def _get_formatted_user(self, user):
        display = "{} {}({})".format(
            user.get("displayName", user["name"]),
            "- %s " %
            user.get("emailAddress") if user.get("emailAddress") else "",
            user["name"],
        )
        return {"id": user["name"], "text": display}

    def view_autocomplete(self, request: Request, group, **kwargs):
        query = request.GET.get("autocomplete_query")
        field = request.GET.get("autocomplete_field")
        project = self.get_option("default_project", group.project)

        if field == "issue_id":
            client = self.get_jira_client(group.project)
            try:
                response = client.search_issues(project, query)
            except ApiError as e:
                return Response(
                    {
                        "error_type": "validation",
                        "errors": [{
                            "__all__": self.message_from_error(e)
                        }],
                    },
                    status=400,
                )
            else:
                issues = [{
                    "text":
                    "({}) {}".format(i["key"], i["fields"]["summary"]),
                    "id":
                    i["key"]
                } for i in response.get("issues", [])]
                return Response({field: issues})

        jira_url = request.GET.get("jira_url")
        if jira_url:
            jira_url = unquote_plus(jira_url)
            parsed = list(urlsplit(jira_url))
            jira_query = parse_qs(parsed[3])

            jira_client = self.get_jira_client(group.project)

            is_user_api = re.search("/rest/api/(latest|[0-9])/user/", jira_url)

            is_user_picker = "/rest/api/1.0/users/picker" in jira_url

            if is_user_api:  # its the JSON version of the autocompleter
                is_xml = False
                jira_query["username"] = query.encode("utf8")
                jira_query.pop(
                    "issueKey", False
                )  # some reason JIRA complains if this key is in the URL.
                jira_query["project"] = project.encode("utf8")
            elif is_user_picker:
                is_xml = False
                # for whatever reason, the create meta api returns an
                # invalid path, so let's just use the correct, documented one here:
                # https://docs.atlassian.com/jira/REST/cloud/#api/2/user
                # also, only pass path so saved instance url will be used
                parsed[0] = ""
                parsed[1] = ""
                parsed[2] = "/rest/api/2/user/picker"
                jira_query["query"] = query.encode("utf8")
            else:  # its the stupid XML version of the API.
                is_xml = True
                jira_query["query"] = query.encode("utf8")
                if jira_query.get("fieldName"):
                    # for some reason its a list.
                    jira_query["fieldName"] = jira_query["fieldName"][0]

            parsed[3] = urlencode(jira_query)
            final_url = urlunsplit(parsed)

            autocomplete_response = jira_client.get_cached(final_url)

            if is_user_picker:
                autocomplete_response = autocomplete_response["users"]

            users = []

            if is_xml:
                for userxml in autocomplete_response.xml.findAll("users"):
                    users.append({
                        "id": userxml.find("name").text,
                        "text": userxml.find("html").text
                    })
            else:
                for user in autocomplete_response:
                    if user.get("name"):
                        users.append(self._get_formatted_user(user))

            # if JIRA user doesn't have proper permission for user api,
            # try the assignee api instead
            if not users and is_user_api:
                try:
                    autocomplete_response = jira_client.search_users_for_project(
                        jira_query.get("project"), jira_query.get("username"))
                except (ApiUnauthorized, ApiError) as e:

                    return Response(
                        {
                            "error_type": "validation",
                            "errors": [{
                                "__all__": self.message_from_error(e)
                            }],
                        },
                        status=400,
                    )

                for user in autocomplete_response:
                    if user.get("name"):
                        users.append(self._get_formatted_user(user))

            return Response({field: users})

    def message_from_error(self, exc):
        if isinstance(exc, ApiUnauthorized):
            return "Unauthorized: either your username and password were invalid or you do not have access"
        return super().message_from_error(exc)

    def error_message_from_json(self, data):
        message = ""
        if data.get("errorMessages"):
            message = " ".join(data["errorMessages"])
        if data.get("errors"):
            if message:
                message += " "
            message += " ".join(f"{k}: {v}"
                                for k, v in data.get("errors").items())
        return message

    def create_issue(self, request: Request, group, form_data, **kwargs):
        cleaned_data = {}

        # protect against mis-configured plugin submitting a form without an
        # issuetype assigned.
        if not form_data.get("issuetype"):
            raise PluginError("Issue Type is required.")

        jira_project_key = self.get_option("default_project", group.project)
        client = self.get_jira_client(group.project)
        meta = client.get_create_meta_for_project(jira_project_key)

        if not meta:
            raise PluginError(
                "Something went wrong. Check your plugin configuration.")

        issue_type_meta = self.get_issue_type_meta(form_data["issuetype"],
                                                   meta)

        fs = issue_type_meta["fields"]
        for field in fs.keys():
            f = fs[field]
            if field == "description":
                cleaned_data[field] = form_data[field]
                continue
            elif field == "summary":
                cleaned_data["summary"] = form_data["title"]
                continue
            if field in form_data.keys():
                v = form_data.get(field)
                if v:
                    schema = f["schema"]
                    if schema.get(
                            "type") == "string" and not schema.get("custom"):
                        cleaned_data[field] = v
                        continue
                    if schema["type"] == "user" or schema.get(
                            "items") == "user":
                        v = {"name": v}
                    elif schema.get("custom") == JIRA_CUSTOM_FIELD_TYPES.get(
                            "multiuserpicker"):
                        # custom multi-picker
                        v = [{"name": v}]
                    elif schema["type"] == "array" and schema.get(
                            "items") != "string":
                        v = [{"id": vx} for vx in v]
                    elif schema["type"] == "array" and schema.get(
                            "items") == "string":
                        v = [v]
                    elif schema.get("custom") == JIRA_CUSTOM_FIELD_TYPES.get(
                            "textarea"):
                        v = v
                    elif (schema["type"] == "number" or schema.get("custom")
                          == JIRA_CUSTOM_FIELD_TYPES["tempo_account"]):
                        try:
                            if "." in v:
                                v = float(v)
                            else:
                                v = int(v)
                        except ValueError:
                            pass
                    elif (schema.get("type") != "string"
                          or (schema.get("items")
                              and schema.get("items") != "string")
                          or schema.get("custom")
                          == JIRA_CUSTOM_FIELD_TYPES.get("select")):
                        v = {"id": v}
                    cleaned_data[field] = v

        if not (isinstance(cleaned_data["issuetype"], dict)
                and "id" in cleaned_data["issuetype"]):
            # something fishy is going on with this field, working on some JIRA
            # instances, and some not.
            # testing against 5.1.5 and 5.1.4 does not convert (perhaps is no longer included
            # in the projectmeta API call, and would normally be converted in the
            # above clean method.)
            cleaned_data["issuetype"] = {"id": cleaned_data["issuetype"]}

        try:
            response = client.create_issue(cleaned_data)
        except Exception as e:
            raise self.raise_error(e)

        return response.get("key")

    def get_jira_client(self, project):
        instance = self.get_option("instance_url", project)
        username = self.get_option("username", project)
        pw = self.get_option("password", project)
        return JiraClient(instance, username, pw)

    def make_choices(self, x):
        return [(y["id"], y["name"] if "name" in y else y["value"])
                for y in x] if x else []

    def validate_config_field(self, project, name, value, actor=None):
        value = super().validate_config_field(project, name, value, actor)
        # Don't make people update password every time
        if name == "password":
            value = value or self.get_option("password", project)
        return value

    def validate_config(self, project, config, actor=None):
        """
        ```
        if config['foo'] and not config['bar']:
            raise PluginError('You cannot configure foo with bar')
        return config
        ```
        """
        client = JiraClient(config["instance_url"], config["username"],
                            config["password"])
        try:
            client.get_projects_list()
        except ApiError as e:
            raise self.raise_error(e)

        return config

    def get_configure_plugin_fields(self, request: Request, project, **kwargs):
        instance = self.get_option("instance_url", project)
        username = self.get_option("username", project)
        pw = self.get_option("password", project)
        jira_project = self.get_option("default_project", project)

        default_priority = self.get_option("default_priority", project)

        default_issue_type = self.get_option("default_issue_type", project)

        project_choices = []
        priority_choices = []
        issue_type_choices = []
        if instance and username and pw:
            client = JiraClient(instance, username, pw)
            try:
                projects = client.get_projects_list()
            except ApiError:
                projects = None
            else:
                if projects:
                    project_choices = [
                        (p.get("key"),
                         "{} ({})".format(p.get("name"), p.get("key")))
                        for p in projects
                    ]
                    jira_project = jira_project or projects[0]["key"]

            if jira_project:
                try:
                    priorities = client.get_priorities()
                except ApiError:
                    priorities = None
                else:
                    if priorities:
                        priority_choices = [(p.get("id"),
                                             "%s" % (p.get("name")))
                                            for p in priorities]

                        default_priority = default_priority or priorities[0][
                            "id"]

                try:
                    meta = client.get_create_meta_for_project(jira_project)
                except ApiError:
                    meta = None
                else:
                    if meta:
                        issue_type_choices = self.make_choices(
                            meta["issuetypes"])
                        if issue_type_choices:
                            default_issue_type = default_issue_type or issue_type_choices[
                                0][0]

        secret_field = get_secret_field_config(pw, "")
        secret_field.update({
            "name": "password",
            "label": "Password/API Token"
        })

        return [
            {
                "name": "instance_url",
                "label": "JIRA Instance URL",
                "default": instance,
                "type": "text",
                "placeholder": 'e.g. "https://jira.atlassian.com"',
                "help": "It must be visible to the Sentry server",
            },
            {
                "name":
                "username",
                "label":
                "Username/Email",
                "default":
                username,
                "type":
                "text",
                "help":
                "Ensure the JIRA user has admin permissions on the project",
            },
            secret_field,
            {
                "name": "default_project",
                "label": "Linked Project",
                "type": "select",
                "choices": project_choices,
                "default": jira_project,
                "required": False,
            },
            {
                "name":
                "ignored_fields",
                "label":
                "Ignored Fields",
                "type":
                "textarea",
                "required":
                False,
                "placeholder":
                'e.g. "components, security, customfield_10006"',
                "default":
                self.get_option("ignored_fields", project),
                "help":
                "Comma-separated list of properties that you don't want to show in the form",
            },
            {
                "name": "default_priority",
                "label": "Default Priority",
                "type": "select",
                "choices": priority_choices,
                "required": False,
                "default": default_priority,
            },
            {
                "name": "default_issue_type",
                "label": "Default Issue Type",
                "type": "select",
                "choices": issue_type_choices,
                "required": False,
                "default": default_issue_type,
            },
            {
                "name": "auto_create",
                "label": "Automatically create JIRA Tickets",
                "default": self.get_option("auto_create", project) or False,
                "type": "bool",
                "required": False,
                "help":
                "Automatically create a JIRA ticket for EVERY new issue",
            },
        ]

    def should_create(self, group, event, is_new):
        if not is_new:
            return False

        if not self.get_option("auto_create", group.project):
            return False

        # XXX(dcramer): Sentry doesn't expect GroupMeta referenced here so we
        # need to populate the cache
        GroupMeta.objects.populate_cache([group])
        if GroupMeta.objects.get_value(group, "%s:tid" % self.get_conf_key(),
                                       None):
            return False

        return True

    def post_process(self, group, event, is_new, **kwargs):
        if not self.should_create(group, event, is_new):
            return

        fields = self.get_new_issue_fields(None, group, event, **kwargs)

        post_data = {}
        included_fields = {
            "priority", "issuetype", "title", "description", "project"
        }
        for field in fields:
            name = field["name"]
            if name in included_fields:
                post_data[name] = field.get("default")

        if not (post_data.get("priority") and post_data.get("issuetype")
                and post_data.get("project")):
            return

        interface = event.interfaces.get("sentry.interfaces.Exception")

        if interface:
            post_data[
                "description"] += "\n{code}%s{code}" % interface.get_stacktrace(
                    event,
                    system_frames=False,
                    max_frames=settings.SENTRY_MAX_STACKTRACE_FRAMES)

        try:
            issue_id = self.create_issue(request={},
                                         group=group,
                                         form_data=post_data)
        except PluginError as e:
            logger.info("post_process.fail", extra={"error": str(e)})
        else:
            prefix = self.get_conf_key()
            GroupMeta.objects.set_value(group, "%s:tid" % prefix, issue_id)
コード例 #9
0
from sentry.pipeline import (PipelineView, NestedPipelineView)
from sentry.utils.http import absolute_uri

import pypd

DESCRIPTION = """
Connect your Sentry organization to your Pagerduty app, and start
getting alerts for errors right in front of you where all the
action happens in your office!
"""

FEATURES = [
    FeatureDescription(
        """
        Unfurls Sentry URLs directly within pagerduty, providing you context and
        actionability on issues right at your fingertips.
        """,
        IntegrationFeatures.CHAT_UNFURL,
    ),
    # to be done - as this requires custom actions from Pagerduty
    # FeatureDescription(
    #     """
    #     Resolve, ignore, and assign issues with minimal context switching.
    #     """,
    #     IntegrationFeatures.ACTION_NOTIFICATION,
    # ),
    FeatureDescription(
        """
        Configure rule based Pagerduty alerts to automatically be posted to a
        specific service/user. Want any error that's happening more than 100 times a
        minute to be posted with critical severity? Setup a rule for it!
コード例 #10
0
class AmazonSQSPlugin(CorePluginMixin, DataForwardingPlugin):
    title = "Amazon SQS"
    slug = "amazon-sqs"
    description = DESCRIPTION
    conf_key = "amazon-sqs"
    required_field = "queue_url"
    feature_descriptions = [
        FeatureDescription(
            """
            Forward Sentry errors and events to Amazon SQS.
            """,
            IntegrationFeatures.DATA_FORWARDING,
        )
    ]

    def get_config(self, project, **kwargs):
        return [
            {
                "name":
                "queue_url",
                "label":
                "Queue URL",
                "type":
                "url",
                "placeholder":
                "https://sqs-us-east-1.amazonaws.com/12345678/myqueue",
            },
            {
                "name": "region",
                "label": "Region",
                "type": "select",
                "choices": tuple((z, z) for z in get_regions()),
            },
            {
                "name": "access_key",
                "label": "Access Key",
                "type": "text",
                "placeholder": "Access Key",
            },
            get_secret_field_config(name="secret_key",
                                    label="Secret Key",
                                    secret=self.get_option(
                                        "secret_key", project)),
            {
                "name":
                "message_group_id",
                "label":
                "Message Group ID",
                "type":
                "text",
                "required":
                False,
                "placeholder":
                "Required for FIFO queues, exclude for standard queues",
            },
            {
                "name":
                "s3_bucket",
                "label":
                "S3 Bucket",
                "type":
                "text",
                "required":
                False,
                "placeholder":
                "s3-bucket",
                "help":
                ("Specify a bucket to store events in S3. The SQS message will contain a reference"
                 " to the payload location in S3. If no S3 bucket is provided, events over the SQS"
                 " limit of 256KB will not be forwarded."),
            },
        ]

    def get_rate_limit(self):
        # no rate limit for SQS
        return (0, 0)

    @track_response_metric
    def forward_event(self, event, payload):
        queue_url = self.get_option("queue_url", event.project)
        access_key = self.get_option("access_key", event.project)
        secret_key = self.get_option("secret_key", event.project)
        region = self.get_option("region", event.project)
        message_group_id = self.get_option("message_group_id", event.project)

        # the metrics tags are a subset of logging params
        metric_tags = {
            "project_id": event.project_id,
            "organization_id": event.project.organization_id,
        }
        logging_params = metric_tags.copy()
        logging_params["event_id"] = event.event_id
        logging_params["issue_id"] = event.group_id

        if not all((queue_url, access_key, secret_key, region)):
            logger.info("sentry_plugins.amazon_sqs.skip_unconfigured",
                        extra=logging_params)
            return

        boto3_args = {
            "aws_access_key_id": access_key,
            "aws_secret_access_key": secret_key,
            "region_name": region,
        }

        def log_and_increment(metrics_name):
            logger.info(
                metrics_name,
                extra=logging_params,
            )
            metrics.incr(
                metrics_name,
                tags=metric_tags,
            )

        def s3_put_object(*args, **kwargs):
            s3_client = boto3.client(service_name="s3",
                                     config=Config(signature_version="s3v4"),
                                     **boto3_args)
            return s3_client.put_object(*args, **kwargs)

        def sqs_send_message(message):
            client = boto3.client(service_name="sqs", **boto3_args)
            send_message_args = {"QueueUrl": queue_url, "MessageBody": message}
            # need a MessageGroupId for FIFO queues
            # note that if MessageGroupId is specified for non-FIFO, this will fail
            if message_group_id:
                from uuid import uuid4

                send_message_args["MessageGroupId"] = message_group_id
                # if content based de-duplication is not enabled, we need to provide a
                # MessageDeduplicationId
                send_message_args["MessageDeduplicationId"] = uuid4().hex
            return client.send_message(**send_message_args)

        # wrap S3 put_object and and SQS send message in one try/except
        s3_bucket = self.get_option("s3_bucket", event.project)
        try:
            # if we have an S3 bucket, upload to S3
            if s3_bucket:
                # we want something like 2020-08-29 so we can store it by the date
                date = event.datetime.strftime("%Y-%m-%d")
                key = "{}/{}/{}".format(event.project.slug, date,
                                        event.event_id)
                logger.info("sentry_plugins.amazon_sqs.s3_put_object",
                            extra=logging_params)
                s3_put_object(Bucket=s3_bucket,
                              Body=json.dumps(payload),
                              Key=key)

                url = "https://{}.s3-{}.amazonaws.com/{}".format(
                    s3_bucket, region, key)
                # just include the s3Url and the event ID in the payload
                payload = {"s3Url": url, "eventID": event.event_id}

            message = json.dumps(payload)

            if len(message) > 256 * 1024:
                logger.info("sentry_plugins.amazon_sqs.skip_oversized",
                            extra=logging_params)
                return False

            sqs_send_message(message)
        except ClientError as e:
            if str(e).startswith(
                    "An error occurred (InvalidClientTokenId)") or str(
                        e).startswith("An error occurred (AccessDenied)"):
                # If there's an issue with the user's token then we can't do
                # anything to recover. Just log and continue.
                log_and_increment(
                    "sentry_plugins.amazon_sqs.access_token_invalid")
                return False
            elif str(e).endswith("must contain the parameter MessageGroupId."):
                log_and_increment(
                    "sentry_plugins.amazon_sqs.missing_message_group_id")
                return False
            elif str(e).startswith("An error occurred (NoSuchBucket)"):
                # If there's an issue with the user's s3 bucket then we can't do
                # anything to recover. Just log and continue.
                log_and_increment(
                    "sentry_plugins.amazon_sqs.s3_bucket_invalid")
                return False
            raise
        return True
コード例 #11
0
ファイル: plugin.py プロジェクト: joan2015/sentry
class TeamworkPlugin(CorePluginMixin, IssuePlugin):
    author = "Sentry Team"
    author_url = "https://github.com/getsentry/sentry"
    title = _("Teamwork")
    description = DESCRIPTION
    slug = "teamwork"
    required_field = "url"
    feature_descriptions = [
        FeatureDescription(
            """
            Create and link Sentry issue groups directly to an Teamwork ticket in any of your
            projects, providing a quick way to jump from a Sentry bug to tracked ticket!
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
        FeatureDescription(
            """
            Link Sentry issues to existing Teamwork tickets.
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
    ]

    conf_title = title
    conf_key = slug

    version = sentry.VERSION
    project_conf_form = TeamworkSettingsForm

    new_issue_form = TeamworkTaskForm
    create_issue_template = "sentry_teamwork/create_issue.html"

    def _get_group_description(self, request, group, event):
        """
        Return group description in markdown-compatible format.

        This overrides an internal method to IssuePlugin.
        """
        output = [absolute_uri(group.get_absolute_url())]
        body = self._get_group_body(request, group, event)
        if body:
            output.extend(
                ["", "\n".join("    " + line for line in body.splitlines())])
        return "\n".join(output)

    def is_configured(self, request, project, **kwargs):
        return all(self.get_option(key, project) for key in ("url", "token"))

    def get_client(self, project):
        return TeamworkClient(base_url=self.get_option("url", project),
                              token=self.get_option("token", project))

    def get_new_issue_form(self, request, group, event, **kwargs):
        """
        Return a Form for the "Create new issue" page.
        """
        return self.new_issue_form(
            client=self.get_client(group.project),
            data=request.POST or None,
            initial=self.get_initial_form_data(request, group, event),
        )

    def get_issue_url(self, group, issue_id, **kwargs):
        url = self.get_option("url", group.project)
        return "%s/tasks/%s" % (url.rstrip("/"), issue_id)

    def get_new_issue_title(self, **kwargs):
        return _("Create Teamwork Task")

    def create_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(group.project)
        try:
            task_id = client.create_task(
                content=form_data["title"],
                description=form_data["description"],
                tasklist_id=form_data["tasklist"],
            )
        except RequestException as e:
            raise forms.ValidationError(
                _("Error creating Teamwork task: %s") % str(e))

        return task_id

    def view(self, request, group, **kwargs):
        op = request.GET.get("op")
        # TODO(dcramer): add caching
        if op == "getTaskLists":
            project_id = request.GET.get("pid")
            if not project_id:
                return HttpResponse(status=400)

            client = self.get_client(group.project)
            task_list = client.list_tasklists(project_id)
            return JSONResponse([{
                "id": i["id"],
                "text": i["name"]
            } for i in task_list])

        return super().view(request, group, **kwargs)
コード例 #12
0
from sentry.utils.http import absolute_uri
from .client import VstsApiClient
from .repository import VstsRepositoryProvider
from .webhooks import WorkItemWebhook

DESCRIPTION = """
Connect your Sentry organization to one or more of your Azure DevOps
organizations. Get started streamlining your bug squashing workflow by unifying
your Sentry and Azure DevOps organization together.
"""

FEATURES = [
    FeatureDescription(
        """
        Create and link Sentry issue groups directly to a Azure DevOps work item in any of
        your projects, providing a quick way to jump from Sentry bug to tracked
        work item!
        """,
        IntegrationFeatures.ISSUE_BASIC,
    ),
    FeatureDescription(
        """
        Automatically synchronize assignees to and from Azure DevOps. Don't get
        confused who's fixing what, let us handle ensuring your issues and work
        items match up to your Sentry and Azure DevOps assignees.
        """,
        IntegrationFeatures.ISSUE_SYNC,
    ),
    FeatureDescription(
        """
        Never forget to close a resolved workitem! Resolving an issue in Sentry
        will resolve your linked workitems and viceversa.
コード例 #13
0
ファイル: plugin.py プロジェクト: liang0/sentry-1
class ClubhousePlugin(CorePluginMixin, IssuePlugin2):
    description = "Create Clubhouse Stories from a project."
    slug = "clubhouse"
    title = "Clubhouse"
    conf_title = title
    conf_key = "clubhouse"
    required_field = "token"
    feature_descriptions = [
        FeatureDescription(
            """
            Create and link Sentry issue groups directly to a Clubhouse story in any of your
            projects, providing a quick way to jump from a Sentry bug to tracked ticket!
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
        FeatureDescription(
            """
            Link Sentry issues to existing Clubhouse stories.
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
    ]

    issue_fields = frozenset(["id", "title", "url"])

    def get_group_urls(self):
        return super(ClubhousePlugin, self).get_group_urls() + [
            url(
                r"^autocomplete",
                IssueGroupActionEndpoint.as_view(
                    view_method_name="view_autocomplete", plugin=self),
            )
        ]

    def get_configure_plugin_fields(self, request, project, **kwargs):
        token = self.get_option("token", project)
        helptext = "Enter your API Token (found on " "your account Settings, under API Tokens)."
        secret_field = get_secret_field_config(token,
                                               helptext,
                                               include_prefix=True)
        secret_field.update({
            "name":
            "token",
            "label":
            "API Token",
            "placeholder":
            "e.g. 12345678-1234-1234-1234-1234567890AB",
        })
        return [
            secret_field,
            {
                "name": "project",
                "label": "Project ID",
                "default": self.get_option("project", project),
                "type": "text",
                "placeholder": "e.g. 639281",
                "help": "Enter your project's numerical ID.",
            },
        ]

    def is_configured(self, request, project, **kwargs):
        return all(self.get_option(k, project) for k in ("token", "project"))

    def get_client(self, project):
        token = self.get_option("token", project)
        return ClubhouseClient(token)

    def create_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(group.project)

        try:
            response = client.create_story(project=self.get_option(
                "project", group.project),
                                           data=form_data)
        except Exception as e:
            self.raise_error(e)

        return {
            "id": response["id"],
            "title": response["name"],
            "url": response["app_url"]
        }

    def get_issue_label(self, group, issue, **kwargs):
        return "Clubhouse Story #%s" % issue["id"]

    def get_issue_url(self, group, issue, **kwargs):
        return issue["url"]

    def validate_config(self, project, config, actor):
        try:
            config["project"] = int(config["project"])
        except ValueError as exc:
            self.logger.exception(six.text_type(exc))
            raise PluginError(
                "Invalid Project ID. "
                "Project IDs are numbers-only, and can be found on the Project's page"
            )
        return config

    # This drives the `Link` UI
    def get_link_existing_issue_fields(self, request, group, event, **kwargs):
        return [
            {
                "name":
                "issue_id",
                "label":
                "Story",
                "default":
                "",
                "type":
                "select",
                "has_autocomplete":
                True,
                "help":
                ("You can use any syntax supported by Clubhouse's "
                 '<a href="https://help.clubhouse.io/hc/en-us/articles/360000046646-Search-Operators" '
                 'target="_blank">search operators.</a>'),
            },
            {
                "name":
                "comment",
                "label":
                "Comment",
                "default":
                absolute_uri(
                    group.get_absolute_url(
                        params={"referrer": "clubhouse_plugin"})),
                "type":
                "textarea",
                "help": ("Leave blank if you don't want to "
                         "add a comment to the Clubhouse story."),
                "required":
                False,
            },
        ]

    # Handle the incoming search terms, make requests and build responses
    def view_autocomplete(self, request, group, **kwargs):
        field = request.GET.get("autocomplete_field")
        query = request.GET.get("autocomplete_query")
        if field != "issue_id" or not query:
            return Response({"issue_id": []})

        project = self.get_option("project", group.project)

        client = self.get_client(group.project)

        # TODO: Something about the search API won't allow an explicit number search.
        # Should it switch the search mechanism from search_stories(text) to get_story(id)?
        try:
            response = client.search_stories(
                query=(u"project:%s %s" % (project, query)).encode("utf-8"))
        except Exception as e:
            return self.handle_api_error(e)

        issues = [{
            "text": "(#%s) %s" % (i["id"], i["name"]),
            "id": i["id"]
        } for i in response.get("data", [])]

        return Response({field: issues})

    def link_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(group.project)

        try:
            story = client.get_story(story_id=form_data["issue_id"])
        except Exception as e:
            self.raise_error(e)

        comment = form_data.get("comment")
        if comment:
            try:
                client.add_comment(story_id=story["id"], comment=comment)
            except Exception as e:
                self.raise_error(e)

        return {
            "id": story["id"],
            "title": story["name"],
            "url": story["app_url"]
        }
コード例 #14
0
ファイル: plugin.py プロジェクト: winter5080/sentry
class GitHubPlugin(GitHubMixin, IssuePlugin2):
    description = "Integrate GitHub issues by linking a repository to a project."
    slug = "github"
    title = "GitHub"
    conf_title = title
    conf_key = "github"
    auth_provider = "github"
    required_field = "repo"
    logger = logging.getLogger("sentry.plugins.github")
    feature_descriptions = [
        FeatureDescription(
            """
            Authorize repositories to be added to your Sentry organization to augment
            sentry issues with commit data with [deployment
            tracking](https://docs.sentry.io/learn/releases/).
            """,
            IntegrationFeatures.COMMITS,
        ),
        FeatureDescription(
            """
            Create and link Sentry issue groups directly to a GitHub issue or pull
            request in any of your repositories, providing a quick way to jump from
            Sentry bug to tracked issue or PR!
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
    ]

    def get_group_urls(self):
        return super(GitHubPlugin, self).get_group_urls() + [
            url(
                r"^autocomplete",
                IssueGroupActionEndpoint.as_view(
                    view_method_name="view_autocomplete", plugin=self),
            )
        ]

    def get_url_module(self):
        return "sentry_plugins.github.urls"

    def is_configured(self, request, project, **kwargs):
        return bool(self.get_option("repo", project))

    def get_new_issue_fields(self, request, group, event, **kwargs):
        fields = super(GitHubPlugin,
                       self).get_new_issue_fields(request, group, event,
                                                  **kwargs)
        return ([{
            "name": "repo",
            "label": "GitHub Repository",
            "default": self.get_option("repo", group.project),
            "type": "text",
            "readonly": True,
        }] + fields + [{
            "name": "assignee",
            "label": "Assignee",
            "default": "",
            "type": "select",
            "required": False,
            "choices": self.get_allowed_assignees(request, group),
        }])

    def get_link_existing_issue_fields(self, request, group, event, **kwargs):
        return [
            {
                "name":
                "issue_id",
                "label":
                "Issue",
                "default":
                "",
                "type":
                "select",
                "has_autocomplete":
                True,
                "help":
                ("You can use any syntax supported by GitHub's "
                 '<a href="https://help.github.com/articles/searching-issues/" '
                 'target="_blank">issue search.</a>'),
            },
            {
                "name":
                "comment",
                "label":
                "Comment",
                "default":
                u"Sentry issue: [{issue_id}]({url})".format(
                    url=absolute_uri(
                        group.get_absolute_url(
                            params={"referrer": "github_plugin"})),
                    issue_id=group.qualified_short_id,
                ),
                "type":
                "textarea",
                "help": ("Leave blank if you don't want to "
                         "add a comment to the GitHub issue."),
                "required":
                False,
            },
        ]

    def get_allowed_assignees(self, request, group):
        client = self.get_client(request.user)
        try:
            response = client.list_assignees(
                repo=self.get_option("repo", group.project))
        except Exception as e:
            self.raise_error(e)

        users = tuple((u["login"], u["login"]) for u in response)

        return (("", "Unassigned"), ) + users

    def create_issue(self, request, group, form_data, **kwargs):
        # TODO: support multiple identities via a selection input in the form?
        client = self.get_client(request.user)

        try:
            response = client.create_issue(
                repo=self.get_option("repo", group.project),
                data={
                    "title": form_data["title"],
                    "body": form_data["description"],
                    "assignee": form_data.get("assignee"),
                },
            )
        except Exception as e:
            self.raise_error(e)

        return response["number"]

    def link_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(request.user)
        repo = self.get_option("repo", group.project)
        try:
            issue = client.get_issue(repo=repo, issue_id=form_data["issue_id"])
        except Exception as e:
            self.raise_error(e)

        comment = form_data.get("comment")
        if comment:
            try:
                client.create_comment(repo=repo,
                                      issue_id=issue["number"],
                                      data={"body": comment})
            except Exception as e:
                self.raise_error(e)

        return {"title": issue["title"]}

    def get_issue_label(self, group, issue_id, **kwargs):
        return "GH-%s" % issue_id

    def get_issue_url(self, group, issue_id, **kwargs):
        # XXX: get_option may need tweaked in Sentry so that it can be pre-fetched in bulk
        repo = self.get_option("repo", group.project)

        return "https://github.com/%s/issues/%s" % (repo, issue_id)

    def view_autocomplete(self, request, group, **kwargs):
        field = request.GET.get("autocomplete_field")
        query = request.GET.get("autocomplete_query")
        if field != "issue_id" or not query:
            return Response({"issue_id": []})

        repo = self.get_option("repo", group.project)
        client = self.get_client(request.user)

        try:
            response = client.search_issues(
                query=(u"repo:%s %s" % (repo, query)).encode("utf-8"))
        except Exception as e:
            return self.handle_api_error(e)

        issues = [{
            "text": "(#%s) %s" % (i["number"], i["title"]),
            "id": i["number"]
        } for i in response.get("items", [])]

        return Response({field: issues})

    def get_configure_plugin_fields(self, request, project, **kwargs):
        return [{
            "name":
            "repo",
            "label":
            "Repository Name",
            "default":
            self.get_option("repo", project),
            "type":
            "text",
            "placeholder":
            "e.g. getsentry/sentry",
            "help":
            ("Enter your repository name, including the owner. "
             "<p><b>Looking to integrate commit data with releases?</b> You'll need to configure this through our"
             '<a href="/organizations/{}/repos/" '
             "> repos page</a>.</p>").format(project.organization.slug),
            "required":
            True,
        }]

    def has_apps_configured(self):
        return bool(
            options.get("github.apps-install-url")
            and options.get("github.integration-app-id")
            and options.get("github.integration-hook-secret")
            and options.get("github.integration-private-key"))

    def setup(self, bindings):
        bindings.add("repository.provider",
                     GitHubRepositoryProvider,
                     id="github")
        if self.has_apps_configured():
            bindings.add("repository.provider",
                         GitHubAppsRepositoryProvider,
                         id="github_apps")
        else:
            self.logger.info("apps-not-configured")
コード例 #15
0
from sentry.tasks.integrations import migrate_repo
from sentry.utils.http import absolute_uri

from .client import BitbucketApiClient
from .issues import BitbucketIssueBasicMixin
from .repository import BitbucketRepositoryProvider

DESCRIPTION = """
Connect your Sentry organization to Bitbucket, enabling the following features:
"""

FEATURES = [
    FeatureDescription(
        """
        Track commits and releases (learn more
        [here](https://docs.sentry.io/learn/releases/))
        """,
        IntegrationFeatures.COMMITS,
    ),
    FeatureDescription(
        """
        Resolve Sentry issues via Bitbucket commits by
        including `Fixes PROJ-ID` in the message
        """,
        IntegrationFeatures.COMMITS,
    ),
    FeatureDescription(
        """
        Create Bitbucket issues from Sentry
        """,
        IntegrationFeatures.ISSUE_BASIC,
コード例 #16
0
ファイル: plugin.py プロジェクト: sugusbs/sentry
class PushoverPlugin(CorePluginMixin, NotifyPlugin):
    description = DESCRIPTION
    slug = "pushover"
    title = "Pushover"
    conf_title = "Pushover"
    conf_key = "pushover"
    required_field = "apikey"
    feature_descriptions = [
        FeatureDescription(
            """
            Have Pushover notifications get sent to your mobile device with the Pushover app.
            """,
            IntegrationFeatures.MOBILE,
        ),
        FeatureDescription(
            """
            Configure Sentry rules to trigger notifications based on conditions you set.
            """,
            IntegrationFeatures.ALERT_RULE,
        ),
    ]

    def is_configured(self, project):
        return all(
            self.get_option(key, project) for key in ("userkey", "apikey"))

    def get_config(self, **kwargs):
        userkey = self.get_option("userkey", kwargs["project"])
        apikey = self.get_option("apikey", kwargs["project"])

        userkey_field = get_secret_field_config(
            userkey,
            "Your user key. See https://pushover.net/",
            include_prefix=True)
        userkey_field.update({"name": "userkey", "label": "User Key"})

        apikey_field = get_secret_field_config(
            apikey,
            "Application API token. See https://pushover.net/apps/",
            include_prefix=True)

        apikey_field.update({"name": "apikey", "label": "API Key"})

        return [
            userkey_field,
            apikey_field,
            {
                "name":
                "priority",
                "label":
                "Message Priority",
                "type":
                "choice",
                "required":
                True,
                "choices": [
                    ("-2", "Lowest"),
                    ("-1", "Low"),
                    ("0", "Normal"),
                    ("1", "High"),
                    ("2", "Emergency"),
                ],
                "default":
                "0",
            },
            {
                "name":
                "retry",
                "label":
                "Retry",
                "type":
                "number",
                "required":
                False,
                "placeholder":
                "e.g. 30",
                "help":
                'How often (in seconds) you will receive the same notification. Minimum of 30 seconds. Only required for "Emergency" level priority.',
            },
            {
                "name":
                "expire",
                "label":
                "Expire",
                "type":
                "number",
                "required":
                False,
                "placeholder":
                "e.g. 9000",
                "help":
                'How many seconds your notification will continue to be retried for. Maximum of 10800 seconds. Only required for "Emergency" level priority.',
            },
        ]

    def validate_config(self, project, config, actor):
        if int(config["priority"]) == 2 and config["retry"] < 30:
            retry = six.text_type(config["retry"])
            self.logger.exception(
                six.text_type(
                    "Retry not 30 or higher. It is {}.".format(retry)))
            raise PluginError(
                "Retry must be 30 or higher. It is {}.".format(retry))
        return config

    def get_client(self, project):
        return PushoverClient(apikey=self.get_option("apikey", project),
                              userkey=self.get_option("userkey", project))

    def error_message_from_json(self, data):
        errors = data.get("errors")
        if errors:
            return " ".join(errors)
        return "unknown error"

    def notify(self, notification, **kwargs):
        event = notification.event
        group = event.group
        project = group.project
        priority = int(self.get_option("priority", project) or 0)
        retry = int(self.get_option("retry", project) or 30)
        expire = int(self.get_option("expire", project) or 90)

        title = "%s: %s" % (project.name, group.title)
        link = group.get_absolute_url(params={"referrer": "pushover_plugin"})

        message = event.title[:256]

        tags = event.tags
        if tags:
            message += "\n\nTags: %s" % (", ".join("%s=%s" % (k, v)
                                                   for (k, v) in tags))

        client = self.get_client(project)
        try:
            response = client.send_message({
                "message": message[:1024],
                "title": title[:250],
                "url": link,
                "url_title": "Issue Details",
                "priority": priority,
                "retry": retry,
                "expire": expire,
            })
        except Exception as e:
            self.raise_error(e)
        assert response["status"]
コード例 #17
0
class AmazonSQSPlugin(CorePluginMixin, DataForwardingPlugin):
    title = "Amazon SQS"
    slug = "amazon-sqs"
    description = DESCRIPTION
    conf_key = "amazon-sqs"
    required_field = "queue_url"
    feature_descriptions = [
        FeatureDescription(
            """
            Forward Sentry errors and events to Amazon SQS.
            """,
            IntegrationFeatures.DATA_FORWARDING,
        )
    ]

    def get_config(self, project, **kwargs):
        return [
            {
                "name":
                "queue_url",
                "label":
                "Queue URL",
                "type":
                "url",
                "placeholder":
                "https://sqs-us-east-1.amazonaws.com/12345678/myqueue",
            },
            {
                "name": "region",
                "label": "Region",
                "type": "select",
                "choices": tuple((z, z) for z in get_regions()),
            },
            get_secret_field_config(name="access_key",
                                    label="Access Key",
                                    secret=self.get_option(
                                        "access_key", project)),
            get_secret_field_config(name="secret_key",
                                    label="Secret Key",
                                    secret=self.get_option(
                                        "secret_key", project)),
            {
                "name":
                "message_group_id",
                "label":
                "Message Group ID",
                "type":
                "text",
                "required":
                False,
                "placeholder":
                "Required for FIFO queues, exclude for standard queues",
            },
        ]

    def forward_event(self, event, payload):
        queue_url = self.get_option("queue_url", event.project)
        access_key = self.get_option("access_key", event.project)
        secret_key = self.get_option("secret_key", event.project)
        region = self.get_option("region", event.project)
        message_group_id = self.get_option("message_group_id", event.project)

        if not all((queue_url, access_key, secret_key, region)):
            return

        # TODO(dcramer): Amazon doesnt support payloads larger than 256kb
        # We could support this by simply trimming it and allowing upload
        # to S3
        message = json.dumps(payload)
        if len(message) > 256 * 1024:
            return False

        try:
            client = boto3.client(
                service_name="sqs",
                aws_access_key_id=access_key,
                aws_secret_access_key=secret_key,
                region_name=region,
            )

            message = {"QueueUrl": queue_url, "MessageBody": message}

            # need a MessageGroupId for FIFO queues
            # note that if MessageGroupId is specified for non-FIFO, this will fail
            if message_group_id:
                from uuid import uuid4

                message["MessageGroupId"] = message_group_id
                # if content based de-duplication is not enabled, we need to provide a
                # MessageDeduplicationId
                message["MessageDeduplicationId"] = uuid4().hex

            client.send_message(**message)
        except ClientError as e:
            if six.text_type(e).startswith("An error occurred (AccessDenied)"):
                # If there's an issue with the user's token then we can't do
                # anything to recover. Just log and continue.
                metrics_name = "sentry_plugins.amazon_sqs.access_token_invalid"
                logger.info(
                    metrics_name,
                    extra={
                        "queue_url": queue_url,
                        "access_key": access_key,
                        "region": region,
                        "project_id": event.project.id,
                        "organization_id": event.project.organization_id,
                    },
                )
                metrics.incr(
                    metrics_name,
                    tags={
                        "project_id": event.project_id,
                        "organization_id": event.project.organization_id,
                    },
                )
                return False
            elif six.text_type(e).endswith(
                    "must contain the parameter MessageGroupId."):
                metrics_name = "sentry_plugins.amazon_sqs.missing_message_group_id"
                logger.info(
                    metrics_name,
                    extra={
                        "queue_url": queue_url,
                        "access_key": access_key,
                        "region": region,
                        "project_id": event.project.id,
                        "organization_id": event.project.organization_id,
                        "message_group_id": message_group_id,
                    },
                )
                metrics.incr(
                    metrics_name,
                    tags={
                        "project_id": event.project_id,
                        "organization_id": event.project.organization_id,
                    },
                )
                return False
            raise

        return True
コード例 #18
0
ファイル: plugin.py プロジェクト: pasala91/test
class OpsGeniePlugin(CorePluginMixin, notify.NotificationPlugin):
    author = "Sentry Team"
    author_url = "https://github.com/getsentry"
    title = "OpsGenie"
    slug = "opsgenie"
    description = DESCRIPTION
    conf_key = "opsgenie"
    version = sentry.VERSION
    project_conf_form = OpsGenieOptionsForm
    required_field = "api_key"
    feature_descriptions = [
        FeatureDescription(
            """
            Manage incidents and outages by sending Sentry notifications to OpsGenie.
            """,
            IntegrationFeatures.INCIDENT_MANAGEMENT,
        ),
        FeatureDescription(
            """
            Configure Sentry rules to trigger notifications based on conditions you set.
            """,
            IntegrationFeatures.ALERT_RULE,
        ),
    ]

    logger = logging.getLogger("sentry.plugins.opsgenie")

    def is_configured(self, project):
        return all(self.get_option(k, project) for k in ("api_key", "alert_url"))

    def get_form_initial(self, project=None):
        return {"alert_url": "https://api.opsgenie.com/v2/alerts"}

    def build_payload(self, group, event, triggering_rules):
        payload = {
            "message": event.message or event.title,
            "alias": "sentry: %d" % group.id,
            "source": "Sentry",
            "details": {
                "Sentry ID": str(group.id),
                "Sentry Group": getattr(group, "title", group.message).encode("utf-8"),
                "Project ID": group.project.slug,
                "Project Name": group.project.name,
                "Logger": group.logger,
                "Level": group.get_level_display(),
                "URL": group.get_absolute_url(),
                "Triggering Rules": json.dumps(triggering_rules),
            },
            "entity": group.culprit,
        }

        payload["tags"] = [
            "{}:{}".format(str(x).replace(",", ""), str(y).replace(",", "")) for x, y in event.tags
        ]

        return payload

    def notify_users(self, group, event, fail_silently=False, triggering_rules=None, **kwargs):
        if not self.is_configured(group.project):
            return

        client = self.get_client(group.project)
        payload = self.build_payload(group, event, triggering_rules)
        try:
            client.trigger_incident(payload)
        except Exception as e:
            self.raise_error(e)

    def get_client(self, project):
        api_key = self.get_option("api_key", project)
        alert_url = self.get_option("alert_url", project)
        recipients = self.get_option("recipients", project)
        return OpsGenieApiClient(api_key, alert_url, recipients)
コード例 #19
0
from .card_builder import build_installation_confirmation_message
from .client import get_token_data, MsTeamsClient

logger = logging.getLogger("sentry.integrations.msteams")

DESCRIPTION = (
    "Microsoft Teams is a hub for teamwork in Office 365. Keep all your team's chats, meetings, files, and apps together in one place."
    "\n\nGet [alerts](https://docs.sentry.io/product/alerts-notifications/alerts/) that let you assign, ignore, and resolve issues"
    " right in your Teams channels with the Sentry integration for Microsoft Teams."
)

FEATURES = [
    FeatureDescription(
        """
        Interact with messages in the chat to assign, ignore, and resolve issues.
        """,
        IntegrationFeatures.
        CHAT_UNFURL,  # not acutally using unfurl but we show this as just "chat"
    ),
    FeatureDescription(
        "Configure rule based Teams alerts to automatically be posted into a specific channel or user.",
        IntegrationFeatures.ALERT_RULE,
    ),
]

INSTALL_NOTICE_TEXT = (
    "Visit the Teams Marketplace to install this integration. After adding the integration"
    " to your team, you will get a welcome message in the General channel to complete installation."
)

external_install = {
コード例 #20
0
class BitbucketPlugin(BitbucketMixin, IssuePlugin2):
    description = "Integrate Bitbucket issues by linking a repository to a project."
    slug = "bitbucket"
    conf_title = BitbucketMixin.title
    conf_key = "bitbucket"
    auth_provider = "bitbucket"
    required_field = "repo"
    feature_descriptions = [
        FeatureDescription(
            """
            Track commits and releases (learn more
            [here](https://docs.sentry.io/learn/releases/))
            """,
            IntegrationFeatures.COMMITS,
        ),
        FeatureDescription(
            """
            Create Bitbucket issues from Sentry
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
        FeatureDescription(
            """
            Link Sentry issues to existing Bitbucket issues
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
    ]

    def get_group_urls(self):
        return super().get_group_urls() + [
            url(
                r"^autocomplete",
                IssueGroupActionEndpoint.as_view(
                    view_method_name="view_autocomplete", plugin=self),
            )
        ]

    def get_url_module(self):
        return "sentry_plugins.bitbucket.urls"

    def is_configured(self, request, project, **kwargs):
        return bool(self.get_option("repo", project))

    def get_new_issue_fields(self, request, group, event, **kwargs):
        fields = super().get_new_issue_fields(request, group, event, **kwargs)
        return ([{
            "name": "repo",
            "label": "Bitbucket Repository",
            "default": self.get_option("repo", group.project),
            "type": "text",
            "readonly": True,
        }] + fields + [
            {
                "name": "issue_type",
                "label": "Issue type",
                "default": ISSUE_TYPES[0][0],
                "type": "select",
                "choices": ISSUE_TYPES,
            },
            {
                "name": "priority",
                "label": "Priority",
                "default": PRIORITIES[0][0],
                "type": "select",
                "choices": PRIORITIES,
            },
        ])

    def get_link_existing_issue_fields(self, request, group, event, **kwargs):
        return [
            {
                "name": "issue_id",
                "label": "Issue",
                "default": "",
                "type": "select",
                "has_autocomplete": True,
            },
            {
                "name":
                "comment",
                "label":
                "Comment",
                "default":
                absolute_uri(
                    group.get_absolute_url(
                        params={"referrer": "bitbucket_plugin"})),
                "type":
                "textarea",
                "help": ("Leave blank if you don't want to "
                         "add a comment to the Bitbucket issue."),
                "required":
                False,
            },
        ]

    def message_from_error(self, exc):
        if isinstance(exc, ApiError) and exc.code == 404:
            return ERR_404
        return super().message_from_error(exc)

    def create_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(request.user)

        try:
            response = client.create_issue(repo=self.get_option(
                "repo", group.project),
                                           data=form_data)
        except Exception as e:
            self.raise_error(e, identity=client.auth)

        return response["local_id"]

    def link_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(request.user)
        repo = self.get_option("repo", group.project)
        try:
            issue = client.get_issue(repo=repo, issue_id=form_data["issue_id"])
        except Exception as e:
            self.raise_error(e, identity=client.auth)

        comment = form_data.get("comment")
        if comment:
            try:
                client.create_comment(repo, issue["local_id"],
                                      {"content": comment})
            except Exception as e:
                self.raise_error(e, identity=client.auth)

        return {"title": issue["title"]}

    def get_issue_label(self, group, issue_id, **kwargs):
        return "Bitbucket-%s" % issue_id

    def get_issue_url(self, group, issue_id, **kwargs):
        repo = self.get_option("repo", group.project)
        return f"https://bitbucket.org/{repo}/issue/{issue_id}/"

    def view_autocomplete(self, request, group, **kwargs):
        field = request.GET.get("autocomplete_field")
        query = request.GET.get("autocomplete_query")
        if field != "issue_id" or not query:
            return Response({"issue_id": []})

        repo = self.get_option("repo", group.project)
        client = self.get_client(request.user)

        try:
            response = client.search_issues(repo, query.encode("utf-8"))
        except Exception as e:
            return Response(
                {
                    "error_type": "validation",
                    "errors": [{
                        "__all__": self.message_from_error(e)
                    }]
                },
                status=400,
            )

        issues = [{
            "text": "(#{}) {}".format(i["local_id"], i["title"]),
            "id": i["local_id"]
        } for i in response.get("issues", [])]

        return Response({field: issues})

    def get_configure_plugin_fields(self, request, project, **kwargs):
        return [{
            "name": "repo",
            "label": "Repository Name",
            "type": "text",
            "placeholder": "e.g. getsentry/sentry",
            "help": "Enter your repository name, including the owner.",
            "required": True,
        }]

    def setup(self, bindings):
        bindings.add("repository.provider",
                     BitbucketRepositoryProvider,
                     id="bitbucket")
コード例 #21
0
ファイル: plugin.py プロジェクト: sugusbs/sentry
class TrelloPlugin(CorePluginMixin, IssuePlugin2):
    description = DESCRIPTION
    slug = "trello"
    title = "Trello"
    conf_title = title
    conf_key = "trello"
    auth_provider = None
    resource_links = [("Trello Setup Instructions", SETUP_URL)
                      ] + CorePluginMixin.resource_links
    required_field = "key"
    feature_descriptions = [
        FeatureDescription(
            """
            Create and link Sentry issue groups directly to an Trello card in any of your
            projects, providing a quick way to jump from a Sentry bug to tracked ticket!
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
        FeatureDescription(
            """
            Link Sentry issues to existing Trello cards
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
    ]

    def get_config(self, project, **kwargs):
        """
        Return the configuration of the plugin.
        Pull the value out of our the arguments to this function or from the DB
        """
        def get_value(field):
            initial_values = kwargs.get("initial", {})
            return initial_values.get(field) or self.get_option(field, project)

        token_config = {
            "name": "token",
            "type": "secret",
            "label": "Trello API Token",
            "default": None,
        }

        token_val = get_value("token")
        if token_val:
            # The token is sensitive so we should mask the value by only sending back the first 5 characters
            token_config["required"] = False
            token_config["prefix"] = token_val[:5]
            token_config["has_saved_value"] = True
        else:
            token_config["required"] = True

        api_key = get_value("key")

        key_config = {
            "name": "key",
            "type": "text",
            "required": True,
            "label": "Trello API Key",
            "default": api_key,
        }

        config = [key_config, token_config]
        org_value = get_value("organization")
        include_org = kwargs.get("add_additial_fields", org_value)
        if api_key and token_val and include_org:
            trello_client = TrelloApiClient(api_key, token_val)
            try:
                org_options = trello_client.get_organization_options()
                config.append({
                    "name": "organization",
                    "label": "Trello Organization",
                    "choices": org_options,
                    "type": "select",
                    "required": False,
                    "default": org_value,
                })
            except Exception as e:
                self.raise_error(e)
        return config

    def validate_config(self, project, config, actor=None):
        """
        Make sure the configuration is valid by trying to query for the organizations with the auth
        """
        trello_client = TrelloApiClient(config["key"], config["token"])
        try:
            trello_client.get_organization_options()
        except Exception as e:
            self.raise_error(e)
        return config

    def get_group_urls(self):
        """
        Return the URLs and the matching views
        """
        return super(TrelloPlugin, self).get_group_urls() + [
            url(
                r"^options",
                IssueGroupActionEndpoint.as_view(
                    view_method_name="view_options", plugin=self),
            ),
            url(
                r"^autocomplete",
                IssueGroupActionEndpoint.as_view(
                    view_method_name="view_autocomplete", plugin=self),
            ),
        ]

    def is_configured(self, request, project, **kwargs):
        return all(self.get_option(key, project) for key in ("token", "key"))

    # used for boards and lists but not cards (shortLink used as ID for cards)
    def map_to_options(self, items):
        return [(item["id"], item["name"]) for item in items]

    def get_new_issue_fields(self, request, group, event, **kwargs):
        """
        Return the fields needed for creating a new issue
        """
        fields = super(TrelloPlugin,
                       self).get_new_issue_fields(request, group, event,
                                                  **kwargs)
        client = self.get_client(group.project)
        organization = self.get_option("organization", group.project)

        boards = client.get_boards(organization)
        board_choices = self.map_to_options(boards)

        return fields + [
            {
                "name": "board",
                "label": "Board",
                "type": "select",
                "choices": board_choices,
                "readonly": False,
                "required": True,
            },
            {
                "name": "list",
                "depends": ["board"],
                "label": "List",
                "type": "select",
                "has_autocomplete": False,
                "required": True,
            },
        ]

    def get_link_existing_issue_fields(self, request, group, event, **kwargs):
        """
        Return the fields needed for linking to an existing issue
        """
        return [
            {
                "name": "issue_id",
                "label": "Card",
                "type": "select",
                "has_autocomplete": True,
                "required": True,
            },
            {
                "name":
                "comment",
                "label":
                "Comment",
                "default":
                absolute_uri(
                    group.get_absolute_url(
                        params={"referrer": "trello_plugin"})),
                "type":
                "textarea",
                "help": ("Leave blank if you don't want to "
                         "add a comment to the Trello card."),
                "required":
                False,
            },
        ]

    def get_client(self, project):
        return TrelloApiClient(self.get_option("key", project),
                               token=self.get_option("token", project))

    def error_message_from_json(self, data):
        errors = data.get("errors")
        if errors:
            return " ".join(e["message"] for e in errors)
        return "unknown error"

    def create_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(group.project)

        try:
            response = client.new_card(id_list=form_data["list"],
                                       name=form_data["title"],
                                       desc=form_data["description"])
        except Exception as e:
            self.raise_error(e)

        return response["shortLink"]

    def link_issue(self, request, group, form_data, **kwargs):
        client = self.get_client(group.project)

        try:
            card = client.get_card(form_data["issue_id"])
        except Exception as e:
            self.raise_error(e)

        comment = form_data.get("comment")
        if comment:
            try:
                client.create_comment(card["shortLink"], comment)
            except Exception as e:
                self.raise_error(e)

        return {"title": card["name"], "id": card["shortLink"]}

    def get_issue_label(self, group, issue, **kwargs):
        """
        Return label of the linked issue we show in the UI from the issue string
        """
        # the old version of the plugin stores the url in the issue
        if LABLEX_REGEX.search(issue):
            short_issue = issue.split("/", 1)[0]
            return "Trello-%s" % short_issue
        return "Trello-%s" % issue

    def get_issue_url(self, group, issue, **kwargs):
        """
        Return label of the url of card in Trello based off the issue object or issue ID
        """
        # TODO(Steve): figure out why we sometimes get a string and sometimes a dict
        if isinstance(issue, dict):
            issue = issue["id"]
        # the old version of the plugin stores the url in the issue
        if LABLEX_REGEX.search(issue):
            return issue.split("/", 1)[1]
        return "https://trello.com/c/%s" % issue

    def view_options(self, request, group, **kwargs):
        """
        Return the lists on a given Trello board
        """
        field = request.GET.get("option_field")
        board = request.GET.get("board")

        results = []
        if field == "list" and board:
            client = self.get_client(group.project)
            try:
                response = client.get_lists_of_board(board)
            except Exception as e:
                return Response(
                    {
                        "error_type": "validation",
                        "errors": [{
                            "__all__": self.message_from_error(e)
                        }],
                    },
                    status=400,
                )
            else:
                results = self.map_to_options(response)

        return Response({field: results})

    def view_autocomplete(self, request, group, **kwargs):
        """
        Return the cards matching a given query and the organization of the configuration
        """
        field = request.GET.get("autocomplete_field")
        query = request.GET.get("autocomplete_query")

        output = []
        if field == "issue_id" and query:
            organization = self.get_option("organization", group.project)

            client = self.get_client(group.project)
            cards = client.get_cards(query, organization)
            output = [{
                "text": "(#%s) %s" % (card["idShort"], card["name"]),
                "id": card["shortLink"]
            } for card in cards]

        return Response({field: output})
コード例 #22
0
ファイル: integration.py プロジェクト: KingDEV95/sentry
from .client import SlackClient
from .utils import get_integration_type, logger

Channel = namedtuple("Channel", ["name", "id"])

DESCRIPTION = """
Connect your Sentry organization to one or more Slack workspaces, and start
getting errors right in front of you where all the action happens in your
office!
"""

FEATURES = [
    FeatureDescription(
        """
        Unfurls Sentry URLs directly within Slack, providing you context and
        actionability on issues right at your fingertips. Resolve, ignore, and assign issues with minimal context switching.
        """,
        IntegrationFeatures.CHAT_UNFURL,
    ),
    FeatureDescription(
        """
        Configure rule based Slack notifications to automatically be posted into a
        specific channel. Want any error that's happening more than 100 times a
        minute to be posted in `#critical-errors`? Setup a rule for it!
        """,
        IntegrationFeatures.ALERT_RULE,
    ),
]

setup_alert = {
    "type":
コード例 #23
0
from sentry.utils.http import absolute_uri
from sentry.web.helpers import render_to_response

from .client import GitLabApiClient, GitLabSetupClient
from .issues import GitlabIssueBasic
from .repository import GitlabRepositoryProvider

DESCRIPTION = """
Connect your Sentry organization to an organization in your GitLab instance or gitlab.com, enabling the following features:
"""

FEATURES = [
    FeatureDescription(
        """
        Track commits and releases (learn more
        [here](https://docs.sentry.io/learn/releases/))
        """,
        IntegrationFeatures.COMMITS,
    ),
    FeatureDescription(
        """
        Resolve Sentry issues via GitLab commits and merge requests by
        including `Fixes PROJ-ID` in the message
        """,
        IntegrationFeatures.COMMITS,
    ),
    FeatureDescription(
        """
        Create GitLab issues from Sentry
        """,
        IntegrationFeatures.ISSUE_BASIC,
コード例 #24
0
class RedminePlugin(CorePluginMixin, IssuePlugin):
    author = "Sentry"
    author_url = "https://github.com/getsentry/sentry"
    version = sentry.VERSION
    description = DESCRIPTION

    slug = "redmine"
    title = _("Redmine")
    conf_title = "Redmine"
    conf_key = "redmine"
    required_field = "host"
    feature_descriptions = [
        FeatureDescription(
            """
            Create and link Sentry issue groups directly to an Redmine issue in any of your
            projects, providing a quick way to jump from a Sentry bug to tracked ticket!
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
        FeatureDescription(
            """
            Link Sentry issues to existing Redmine issue.
            """,
            IntegrationFeatures.ISSUE_BASIC,
        ),
    ]

    new_issue_form = RedmineNewIssueForm

    def __init__(self):
        super(RedminePlugin, self).__init__()
        self.client_errors = []
        self.fields = []

    def has_project_conf(self):
        return True

    def is_configured(self, project, **kwargs):
        return all((self.get_option(k, project)
                    for k in ("host", "key", "project_id")))

    def get_new_issue_title(self, **kwargs):
        return "Create Redmine Task"

    def get_initial_form_data(self, request, group, event, **kwargs):
        return {
            "description": self._get_group_description(request, group, event),
            "title": self._get_group_title(request, group, event),
        }

    def _get_group_description(self, request, group, event):
        output = [absolute_uri(group.get_absolute_url())]
        body = self._get_group_body(request, group, event)
        if body:
            output.extend(["", "<pre>", body, "</pre>"])
        return "\n".join(output)

    def get_client(self, project):
        return RedmineClient(host=self.get_option("host", project),
                             key=self.get_option("key", project))

    def create_issue(self, group, form_data, **kwargs):
        """
        Create a Redmine issue
        """
        client = self.get_client(group.project)
        default_priority = self.get_option("default_priority", group.project)
        if default_priority is None:
            default_priority = 4

        issue_dict = {
            "project_id": self.get_option("project_id", group.project),
            "tracker_id": self.get_option("tracker_id", group.project),
            "priority_id": default_priority,
            "subject": form_data["title"].encode("utf-8"),
            "description": form_data["description"].encode("utf-8"),
        }

        extra_fields_str = self.get_option("extra_fields", group.project)
        if extra_fields_str:
            extra_fields = json.loads(extra_fields_str)
        else:
            extra_fields = {}
        issue_dict.update(extra_fields)

        response = client.create_issue(issue_dict)
        return response["issue"]["id"]

    def get_issue_url(self, group, issue_id, **kwargs):
        host = self.get_option("host", group.project)
        return "{}/issues/{}".format(host.rstrip("/"), issue_id)

    def build_config(self):
        host = {
            "name": "host",
            "label": "Host",
            "type": "text",
            "help": "e.g. http://bugs.redmine.org",
            "required": True,
        }
        key = {
            "name": "key",
            "label": "Key",
            "type": "text",
            "help":
            "Your API key is available on your account page after enabling the Rest API (Administration -> Settings -> Authentication)",
            "required": True,
        }
        project_id = {
            "name": "project_id",
            "label": "Project*",
            "type": "select",
            "choices": [],
            "required": False,
        }
        tracker_id = {
            "name": "tracker_id",
            "label": "Tracker*",
            "type": "select",
            "choices": [],
            "required": False,
        }
        default_priority = {
            "name": "default_priority",
            "label": "Default Priority*",
            "type": "select",
            "choices": [],
            "required": False,
        }
        extra_fields = {
            "name": "extra_fields",
            "label": "Extra Fields",
            "type": "text",
            "help":
            "Extra attributes (custom fields, status id, etc.) in JSON format",
            "required": False,
        }
        return [
            host, key, project_id, tracker_id, default_priority, extra_fields
        ]

    def add_choices(self, field_name, choices, default):
        for field in self.fields:
            if field_name == field["name"]:
                field["choices"] = choices
                field["default"] = default
                return

    def remove_field(self, field_name):
        for field in self.fields:
            if field["name"] == field_name:
                self.fields.remove(field)
                return

    def build_initial(self, initial_args, project):
        initial = {}
        fields = [
            "host", "key", "project_id", "tracker_id", "default_priority",
            "extra_fields"
        ]
        for field in fields:
            value = initial_args.get(field) or self.get_option(field, project)
            if value is not None:
                initial[field] = value
        return initial

    def get_config(self, project, **kwargs):
        self.client_errors = []
        self.fields = self.build_config()
        initial_args = kwargs.get("initial") or {}
        initial = self.build_initial(initial_args, project)

        has_credentials = all(initial.get(k) for k in ("host", "key"))
        if has_credentials:
            client = RedmineClient(initial["host"], initial["key"])
            try:
                projects = client.get_projects()
            except Exception:
                has_credentials = False
                self.client_errors.append(
                    "There was an issue authenticating with Redmine")
            else:
                choices_value = self.get_option("project_id", project)
                project_choices = [("", "--")] if not choices_value else []
                project_choices += [(p["id"],
                                     "%s (%s)" % (p["name"], p["identifier"]))
                                    for p in projects["projects"]]
                self.add_choices("project_id", project_choices, choices_value)

        if has_credentials:
            try:
                trackers = client.get_trackers()
            except Exception:
                self.remove_field("tracker_id")
            else:
                choices_value = self.get_option("tracker_id", project)
                tracker_choices = [("", "--")] if not choices_value else []
                tracker_choices += [(p["id"], p["name"])
                                    for p in trackers["trackers"]]
                self.add_choices("tracker_id", tracker_choices, choices_value)

            try:
                priorities = client.get_priorities()
            except Exception:
                self.remove_field("default_priority")
            else:
                choices_value = self.get_option("default_priority", project)
                tracker_choices = [("", "--")] if not choices_value else []
                tracker_choices += [(p["id"], p["name"])
                                    for p in priorities["issue_priorities"]]
                self.add_choices("default_priority", tracker_choices,
                                 choices_value)

        if not has_credentials:
            for field_name in [
                    "project_id", "tracker_id", "default_priority",
                    "extra_fields"
            ]:
                self.remove_field(field_name)

        return self.fields

    def validate_config(self, project, config, actor):
        super(RedminePlugin, self).validate_config(project, config, actor)
        self.client_errors = []

        for field in self.fields:
            if field["name"] in [
                    "project_id", "tracker_id", "default_priority"
            ]:
                if not config[field["name"]]:
                    self.logger.exception(
                        six.text_type("{} required.".format(field["name"])))
                    self.client_errors.append(field["name"])

        if self.client_errors:
            raise PluginError(", ".join(self.client_errors) + " required.")
        return config
コード例 #25
0
from .issues import GitHubIssueBasic
from .repository import GitHubRepositoryProvider
from .utils import get_jwt

DESCRIPTION = """
Connect your Sentry organization into your GitHub organization or user account.
Take a step towards augmenting your sentry issues with commits from your
repositories ([using releases](https://docs.sentry.io/learn/releases/)) and
linking up your GitHub issues and pull requests directly to issues in Sentry.
"""

FEATURES = [
    FeatureDescription(
        """
        Authorize repositories to be added to your Sentry organization to augment
        sentry issues with commit data with [deployment
        tracking](https://docs.sentry.io/learn/releases/).
        """,
        IntegrationFeatures.COMMITS,
    ),
    FeatureDescription(
        """
        Create and link Sentry issue groups directly to a GitHub issue or pull
        request in any of your repositories, providing a quick way to jump from
        Sentry bug to tracked issue or PR!
        """,
        IntegrationFeatures.ISSUE_BASIC,
    ),
]

metadata = IntegrationMetadata(
    description=DESCRIPTION.strip(),
コード例 #26
0
)
from sentry.integrations.repositories import RepositoryMixin
from sentry.models.repository import Repository
from sentry.pipeline import PipelineView
from sentry.web.helpers import render_to_response

from .repository import CustomSCMRepositoryProvider

DESCRIPTION = """
Custom Source Control Management (SCM)
"""

FEATURES = [
    FeatureDescription(
        """
        Send your own commits
        """,
        IntegrationFeatures.COMMITS,
    ),
    FeatureDescription(
        """
        Stack trace linky dink
        """,
        IntegrationFeatures.STACKTRACE_LINK,
    ),
]

metadata = IntegrationMetadata(
    description=DESCRIPTION.strip(),
    features=FEATURES,
    author="The Sentry Team",
    noun=_("Installation"),
コード例 #27
0
from sentry.mediators.sentry_apps import InternalCreator

from .client import VercelClient

logger = logging.getLogger("sentry.integrations.vercel")

DESCRIPTION = _(
    """
Vercel is an all-in-one platform with Global CDN supporting static & JAMstack deployment and Serverless Functions.
"""
)

FEATURES = [
    FeatureDescription(
        """
        Connect your Sentry and Vercel projects to automatically upload source maps and notify Sentry of new releases being deployed.
        """,
        IntegrationFeatures.DEPLOYMENT,
    )
]

INSTALL_NOTICE_TEXT = _(
    "Visit the Vercel Marketplace to install this integration. After installing the"
    " Sentry integration, you'll be redirected back to Sentry to finish syncing Vercel and Sentry projects."
)


external_install = {
    "url": "https://vercel.com/integrations/%s/add" % options.get("vercel.integration-slug"),
    "buttonText": _("Vercel Marketplace"),
    "noticeText": _(INSTALL_NOTICE_TEXT),
}
コード例 #28
0
from sentry.web.helpers import render_to_response

from .client import BitbucketServer, BitbucketServerSetupClient
from .repository import BitbucketServerRepositoryProvider

logger = logging.getLogger("sentry.integrations.bitbucket_server")

DESCRIPTION = """
Connect your Sentry organization to Bitbucket Server, enabling the following features:
"""

FEATURES = [
    FeatureDescription(
        """
        Track commits and releases (learn more
        [here](https://docs.sentry.io/learn/releases/))
        """,
        IntegrationFeatures.COMMITS,
    ),
    FeatureDescription(
        """
        Resolve Sentry issues via Bitbucket Server commits by
        including `Fixes PROJ-ID` in the message
        """,
        IntegrationFeatures.COMMITS,
    ),
]

setup_alert = {
    "type": "warning",
    "icon": "icon-warning-sm",
コード例 #29
0
from sentry.web.helpers import render_to_response
from .client import JiraServerClient, JiraServerSetupClient

logger = logging.getLogger('sentry.integrations.jira_server')

DESCRIPTION = """
Connect your Sentry organization into one or more of your Jira Server instances.
Get started streamlining your bug squashing workflow by unifying your Sentry and
Jira instances together.
"""

FEATURE_DESCRIPTIONS = [
    FeatureDescription(
        """
        Create and link Sentry issue groups directly to a Jira ticket in any of your
        projects, providing a quick way to jump from Sentry bug to tracked ticket!
        """,
        IntegrationFeatures.ISSUE_BASIC,
    ),
    FeatureDescription(
        """
        Automatically synchronize assignees to and from Jira. Don't get confused
        who's fixing what, let us handle ensuring your issues and tickets match up
        to your Sentry and Jira assignees.
        """,
        IntegrationFeatures.ISSUE_SYNC,
    ),
    FeatureDescription(
        """
        Synchronize Comments on Sentry Issues directly to the linked Jira ticket.
        """,
コード例 #30
0
ファイル: plugin.py プロジェクト: wux6533/sentry
class SlackPlugin(CorePluginMixin, notify.NotificationPlugin):
    title = "Slack"
    slug = "slack"
    description = "Post notifications to a Slack channel."
    conf_key = "slack"
    required_field = "webhook"
    feature_descriptions = [
        FeatureDescription(
            """
            Configure rule based Slack notifications to automatically be posted into a
            specific channel. Want any error that's happening more than 100 times a
            minute to be posted in `#critical-errors`? Setup a rule for it!
            """,
            IntegrationFeatures.ALERT_RULE,
        )
    ]

    def is_configured(self, project):
        return bool(self.get_option("webhook", project))

    def get_config(self, project, **kwargs):
        return [
            {
                "name": "webhook",
                "label": "Webhook URL",
                "type": "url",
                "placeholder": "e.g. https://hooks.slack.com/services/000000000/000000000/00000000000000000",
                "required": True,
                "help": "Your custom Slack webhook URL.",
            },
            {
                "name": "username",
                "label": "Bot Name",
                "type": "string",
                "placeholder": "e.g. Sentry",
                "default": "Sentry",
                "required": False,
                "help": "The name used when publishing messages.",
            },
            {
                "name": "icon_url",
                "label": "Icon URL",
                "type": "url",
                "required": False,
                "help": (
                    "The url of the icon to appear beside your bot (32px png), "
                    "leave empty for none.<br />You may use "
                    "http://myovchev.github.io/sentry-slack/images/logo32.png"
                ),
            },
            {
                "name": "channel",
                "label": "Destination",
                "type": "string",
                "placeholder": "e.g. #engineering",
                "required": False,
                "help": "Optional #channel name or @user",
            },
            {
                "name": "custom_message",
                "label": "Custom Message",
                "type": "string",
                "placeholder": "e.g. Hey <!everyone> there is something wrong",
                "required": False,
                "help": "Optional - Slack message formatting can be used",
            },
            {
                "name": "include_tags",
                "label": "Include Tags",
                "type": "bool",
                "required": False,
                "help": "Include tags with notifications",
            },
            {
                "name": "included_tag_keys",
                "label": "Included Tags",
                "type": "string",
                "required": False,
                "help": (
                    "Only include these tags (comma separated list). " "Leave empty to include all."
                ),
            },
            {
                "name": "excluded_tag_keys",
                "label": "Excluded Tags",
                "type": "string",
                "required": False,
                "help": "Exclude these tags (comma separated list).",
            },
            {
                "name": "include_rules",
                "label": "Include Rules",
                "type": "bool",
                "required": False,
                "help": "Include triggering rules with notifications.",
            },
            {
                "name": "exclude_project",
                "label": "Exclude Project Name",
                "type": "bool",
                "default": False,
                "required": False,
                "help": "Exclude project name with notifications.",
            },
            {
                "name": "exclude_culprit",
                "label": "Exclude Culprit",
                "type": "bool",
                "default": False,
                "required": False,
                "help": "Exclude culprit with notifications.",
            },
        ]

    def color_for_event(self, event):
        return "#" + LEVEL_TO_COLOR.get(event.get_tag("level"), "error")

    def _get_tags(self, event):
        tag_list = event.tags
        if not tag_list:
            return ()

        return (
            (tagstore.get_tag_key_label(k), tagstore.get_tag_value_label(k, v)) for k, v in tag_list
        )

    def get_tag_list(self, name, project):
        option = self.get_option(name, project)
        if not option:
            return None
        return set(tag.strip().lower() for tag in option.split(","))

    def notify(self, notification, raise_exception=False):
        event = notification.event
        group = event.group
        project = group.project

        if not self.is_configured(project):
            return

        webhook = self.get_option("webhook", project)
        username = (self.get_option("username", project) or "Sentry").strip()
        icon_url = self.get_option("icon_url", project)
        channel = (self.get_option("channel", project) or "").strip()

        title = event.title.encode("utf-8")
        # TODO(dcramer): we'd like this to be the event culprit, but Sentry
        # does not currently retain it
        if group.culprit:
            culprit = group.culprit.encode("utf-8")
        else:
            culprit = None
        project_name = project.get_full_name().encode("utf-8")

        fields = []

        # They can be the same if there is no culprit
        # So we set culprit to an empty string instead of duplicating the text
        if not self.get_option("exclude_culprit", project) and culprit and title != culprit:
            fields.append({"title": "Culprit", "value": culprit, "short": False})
        if not self.get_option("exclude_project", project):
            fields.append({"title": "Project", "value": project_name, "short": True})

        if self.get_option("custom_message", project):
            fields.append(
                {
                    "title": "Custom message",
                    "value": self.get_option("custom_message", project),
                    "short": False,
                }
            )

        if self.get_option("include_rules", project):
            rules = []
            for rule in notification.rules:
                rule_link = "/%s/%s/settings/alerts/rules/%s/" % (
                    group.organization.slug,
                    project.slug,
                    rule.id,
                )

                # Make sure it's an absolute uri since we're sending this
                # outside of Sentry into Slack
                rule_link = absolute_uri(rule_link)
                rules.append((rule_link, rule.label))

            if rules:
                value = u", ".join(u"<{} | {}>".format(*r) for r in rules)

                fields.append(
                    {"title": "Triggered By", "value": value.encode("utf-8"), "short": False}
                )

        if self.get_option("include_tags", project):
            included_tags = set(self.get_tag_list("included_tag_keys", project) or [])
            excluded_tags = set(self.get_tag_list("excluded_tag_keys", project) or [])
            for tag_key, tag_value in self._get_tags(event):
                key = tag_key.lower()
                std_key = tagstore.get_standardized_key(key)
                if included_tags and key not in included_tags and std_key not in included_tags:
                    continue
                if excluded_tags and (key in excluded_tags or std_key in excluded_tags):
                    continue
                fields.append(
                    {
                        "title": tag_key.encode("utf-8"),
                        "value": tag_value.encode("utf-8"),
                        "short": True,
                    }
                )

        payload = {
            "attachments": [
                {
                    "fallback": "[%s] %s" % (project_name, title),
                    "title": title,
                    "title_link": group.get_absolute_url(params={"referrer": "slack"}),
                    "color": self.color_for_event(event),
                    "fields": fields,
                }
            ]
        }

        if username:
            payload["username"] = username.encode("utf-8")

        if channel:
            payload["channel"] = channel

        if icon_url:
            payload["icon_url"] = icon_url

        values = {"payload": json.dumps(payload)}

        # Apparently we've stored some bad data from before we used `URLField`.
        webhook = webhook.strip(" ")
        return http.safe_urlopen(webhook, method="POST", data=values, timeout=5)