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
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
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
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)
def __init__(self, options=None): self.options = options if options and options["dsn"]: self.parsed_dsn = Dsn(options["dsn"]) else: self.parsed_dsn = None
def test_parse_invalid_dsn(dsn): with pytest.raises(BadDsn): dsn = Dsn(dsn)
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