def __init__(self, options=None):
     # type: (Optional[ClientOptions]) -> None
     self.options = options
     if options and options["dsn"]:
         self.parsed_dsn = Dsn(options["dsn"])
     else:
         self.parsed_dsn = None  # type: ignore
        def processor(event, hint):
            # type: (Event, Optional[Hint]) -> Optional[Event]
            if Hub.current.get_integration(PostHogIntegration) is not None:
                if event.get("level") != "error":
                    return event

                if event.get("tags", {}).get(POSTHOG_ID_TAG):
                    posthog_distinct_id = event["tags"][POSTHOG_ID_TAG]
                    event["tags"][
                        "PostHog URL"] = f"{posthog.host or DEFAULT_HOST}/person/{posthog_distinct_id}"

                    properties = {
                        "$sentry_event_id": event["event_id"],
                        "$sentry_exception": event["exception"],
                    }

                    if PostHogIntegration.organization:
                        project_id = PostHogIntegration.project_id or (
                            not not Hub.current.client.dsn
                            and Dsn(Hub.current.client.dsn).project_id)
                        if project_id:
                            properties[
                                "$sentry_url"] = f"{PostHogIntegration.prefix}{PostHogIntegration.organization}/issues/?project={project_id}&query={event['event_id']}"

                    posthog.capture(posthog_distinct_id, "$exception",
                                    properties)

            return event
Esempio n. 3
0
 def __init__(self, options=None):
     # type: (Optional[Dict[str, Any]]) -> None
     self.options = options
     if options and options["dsn"] is not None and options["dsn"]:
         self.parsed_dsn = Dsn(options["dsn"])
     else:
         self.parsed_dsn = None
Esempio n. 4
0
async def validate_input(hass: core.HomeAssistant, data):
    """Validate the DSN input allows us to connect.

    Data has the keys from DATA_SCHEMA with values provided by the user.
    """
    # validate the dsn
    Dsn(data["dsn"])

    return {"title": "Sentry"}
def compute_tracestate_entry(span):
    # type: (Span) -> Optional[str]
    """
    Computes a new sentry tracestate for the span. Includes the `sentry=`.

    Will return `None` if there's no client and/or no DSN.
    """
    data = {}

    hub = span.hub or sentry_sdk.Hub.current

    client = hub.client
    scope = hub.scope

    if client and client.options.get("dsn"):
        options = client.options
        user = scope._user

        data = {
            "trace_id": span.trace_id,
            "environment": options["environment"],
            "release": options.get("release"),
            "public_key": Dsn(options["dsn"]).public_key,
        }

        if user and (user.get("id") or user.get("segment")):
            user_data = {}

            if user.get("id"):
                user_data["id"] = user["id"]

            if user.get("segment"):
                user_data["segment"] = user["segment"]

            data["user"] = user_data

        if span.containing_transaction:
            data["transaction"] = span.containing_transaction.name

        return "sentry=" + compute_tracestate_value(data)

    return None
Esempio n. 6
0
    async def async_step_user(self,
                              user_input: dict[str, Any] | None = None
                              ) -> dict[str, Any]:
        """Handle a user config flow."""
        if self._async_current_entries():
            return self.async_abort(reason="single_instance_allowed")

        errors = {}
        if user_input is not None:
            try:
                Dsn(user_input["dsn"])
            except BadDsn:
                errors["base"] = "bad_dsn"
            except Exception:  # pylint: disable=broad-except
                _LOGGER.exception("Unexpected exception")
                errors["base"] = "unknown"
            else:
                return self.async_create_entry(title="Sentry", data=user_input)

        return self.async_show_form(step_id="user",
                                    data_schema=DATA_SCHEMA,
                                    errors=errors)
Esempio n. 7
0
 def __init__(self, options=None):
     self.options = options
     if options and options["dsn"]:
         self.parsed_dsn = Dsn(options["dsn"])
     else:
         self.parsed_dsn = None
Esempio n. 8
0
def test_parse_invalid_dsn(dsn):
    with pytest.raises(BadDsn):
        dsn = Dsn(dsn)
Esempio n. 9
0
def test_parse_dsn_paths(given, expected):
    dsn = Dsn(given)
    auth = dsn.to_auth()
    assert auth.store_api_url == expected
 def __init__(self, dsn: str, package: str):
     self.sentry_auth = Dsn(dsn).to_auth()
     self.package = package
class GlitchtipStorage(Storage):
    """
    Used to store incoming mails on a GlitchTip server.
    https://app.glitchtip.com/docs/

    Remembers already sent mail reports by putting their hash IDs in a file
    in the application's working directory.
    """
    def __init__(self, dsn: str, package: str):
        self.sentry_auth = Dsn(dsn).to_auth()
        self.package = package

    def make_sentry_payload(self, entry: DatabaseEntry):
        newpipe_exc_info = entry.newpipe_exception_info

        frames: List[SentryFrame] = []

        try:
            raw_data = "".join(newpipe_exc_info["exceptions"])
        except KeyError:
            raise StorageError("'exceptions' key missing in JSON body")

        raw_frames = raw_data.replace("\n", " ").replace("\r",
                                                         " ").split("\tat")

        # pretty ugly, but that's what we receive from NewPipe
        # both message and exception name are contained in the first item in the frames
        message = raw_frames[0]

        for raw_frame in raw_frames[1:]:
            # some very basic sanitation, as e-mail clients all suck
            raw_frame = raw_frame.strip()

            # _very_ basic but gets the job done well enough
            frame_match = re.search(r"(.+)\(([a-zA-Z0-9:.\s]+)\)", raw_frame)

            if frame_match:
                module_path = frame_match.group(1).split(".")
                filename_and_lineno = frame_match.group(2)

                if ":" in filename_and_lineno:
                    # "unknown source" is shown for lambda functions
                    filename_and_lineno_match = re.search(
                        r"(Unknown\s+Source|(?:[a-zA-Z]+\.(?:kt|java)+)):([0-9]+)",
                        filename_and_lineno,
                    )

                    if not filename_and_lineno_match:
                        raise ValueError(
                            f"could not find filename and line number in string {frame_match.group(2)}"
                        )

                    # we want just two matches, anything else would be an error in the regex
                    assert len(filename_and_lineno_match.groups()) == 2

                    frame = SentryFrame(
                        filename_and_lineno_match.group(1),
                        module_path[-1],
                        ".".join(module_path[:-1]),
                        lineno=int(filename_and_lineno_match.group(2)),
                    )

                    frames.append(frame)

                else:
                    # apparently a native exception, so we don't have a line number
                    frame = SentryFrame(
                        frame_match.group(2),
                        module_path[-1],
                        ".".join(module_path[:-1]),
                    )

                    frames.append(frame)

            else:
                raise ParserError(
                    "Could not parse frame: '{}'".format(raw_frame))

        try:
            type = message.split(":")[0].split(".")[-1]
            value = message.split(":")[1]
            module = ".".join(message.split(":")[0].split(".")[:-1])

        except IndexError:
            type = value = module = "<none>"

        timestamp = entry.date.timestamp()

        # set up the payload, with all intermediary value objects
        stacktrace = SentryStacktrace(frames)
        exception = SentryException(type, value, module, stacktrace)

        # TODO: support multiple exceptions to support "Caused by:"
        payload = SentryPayload(entry.hash_id(), timestamp, message, exception)

        # try to fill in as much optional data as possible

        try:
            # in Sentry, releases are now supposed to be unique organization wide
            # in GlitchTip, however, they seem to be regarded as tags, so this should work well enough
            payload.release = entry.newpipe_exception_info["version"]
        except KeyError:
            pass

        for key in [
                "user_comment",
                "request",
                "user_action",
                "content_country",
                "app_language",
        ]:
            try:
                payload.extra[key] = newpipe_exc_info[key]
            except KeyError:
                pass

        for key in ["os", "service", "content_language"]:
            try:
                payload.tags[key] = newpipe_exc_info[key]
            except KeyError:
                pass

        try:
            package = newpipe_exc_info["package"]
        except KeyError:
            package = None

        if package is not None:
            if package != self.package:
                raise ValueError("Package name not allowed: %s" % package)
            else:
                payload.tags["package"] = newpipe_exc_info["package"]

        return payload

    async def save(self, entry: DatabaseEntry):
        exception = self.make_sentry_payload(entry)
        data = exception.to_dict()

        # we use Sentry SDK's auth helper object to calculate both the required auth header as well as the URL from the
        # DSN string we already created a Dsn object for
        url = self.sentry_auth.store_api_url

        # it would be great if the Auth object just had a method to create/update a headers dict
        headers = {
            "X-Sentry-Auth": str(self.sentry_auth.to_header()),
            # user agent isn't really necessary, but sentry-sdk sets it, too, so... why not
            "User-Agent": "NewPipe Crash Report Importer",
            # it's recommended by the Sentry docs to send a valid MIME type
            "Content-Type": "application/json",
        }

        async with aiohttp.ClientSession() as session:
            async with session.post(url,
                                    data=json.dumps(data),
                                    headers=headers) as response:
                # pretty crude way to recognize this issue, but it works well enough
                if response.status == 403:
                    if "An event with the same ID already exists" in (
                            await response.text()):
                        raise AlreadyStoredError()

                if response.status != 200:
                    raise GlitchtipError(response.status, await
                                         response.text())
def test_parse_dsn_paths(given, expected_store, expected_envelope):
    dsn = Dsn(given)
    auth = dsn.to_auth()
    assert auth.store_api_url == expected_store
    assert auth.get_api_url("store") == expected_store
    assert auth.get_api_url("envelope") == expected_envelope