Beispiel #1
0
    def _get_events_snuba(self, request, group, environments, query, tags, start, end):
        default_end = timezone.now()
        default_start = default_end - timedelta(days=90)
        params = {
            "group_ids": [group.id],
            "project_id": [group.project_id],
            "organization_id": group.project.organization_id,
            "start": start if start else default_start,
            "end": end if end else default_end,
        }
        direct_hit_resp = get_direct_hit_response(request, query, params, "api.group-events")
        if direct_hit_resp:
            return direct_hit_resp

        if environments:
            params["environment"] = [env.name for env in environments]

        full = request.GET.get("full", False)
        try:
            snuba_filter = get_filter(request.GET.get("query", None), params)
        except InvalidSearchQuery as e:
            raise ParseError(detail=str(e))

        snuba_filter.conditions.append(["event.type", "!=", "transaction"])

        data_fn = partial(eventstore.get_events, referrer="api.group-events", filter=snuba_filter)
        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(results, request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn),
        )
Beispiel #2
0
    def __handle_result(self, user, project_id, group_id, result):
        event = eventstore.get_event_by_id(project_id, result["event_id"])

        return {
            "id": result["primary_hash"],
            "latestEvent": serialize(event, user, EventSerializer()),
        }
Beispiel #3
0
    def get(self, request: Request, group) -> Response:
        """
        Retrieve the Latest Event for an Issue
        ``````````````````````````````````````

        Retrieves the details of the latest event for an issue.

        :pparam string group_id: the ID of the issue
        """
        environments = [
            e.name
            for e in get_environments(request, group.project.organization)
        ]

        event = group.get_latest_event_for_environments(environments)

        if not event:
            return Response({"detail": "No events found for group"},
                            status=404)

        collapse = request.GET.getlist("collapse", [])
        if "stacktraceOnly" in collapse:
            return Response(serialize(event, request.user, EventSerializer()))

        try:
            return client.get(
                f"/projects/{event.organization.slug}/{event.project.slug}/events/{event.event_id}/",
                request=request,
                data={
                    "environment": environments,
                    "group_id": event.group_id
                },
            )
        except client.ApiError as e:
            return Response(e.body, status=e.status_code)
def _process_snuba_results(query_res, group: Group, user):
    event_ids = {
        row["latest_event_id"]: Event.generate_node_id(group.project_id,
                                                       row["latest_event_id"])
        for row in query_res
    }

    node_data = nodestore.get_multi(list(event_ids.values()))

    response = []

    for row in query_res:
        response_item = {
            "hash": row["new_materialized_hash"],
            "eventCount": row["event_count"],
        }
        event_id = row["latest_event_id"]
        event_data = node_data.get(event_ids[event_id], None)

        if event_data is not None:
            event = Event(group.project_id,
                          event_id,
                          group_id=group.id,
                          data=event_data)
            response_item["latestEvent"] = serialize(event, user,
                                                     EventSerializer())

        response.append(response_item)

    return response
Beispiel #5
0
    def _get_events_snuba(self, request, group, environments, query, tags, start, end):
        default_end = timezone.now()
        default_start = default_end - timedelta(days=90)
        params = {
            "issue.id": [group.id],
            "project_id": [group.project_id],
            "start": start if start else default_start,
            "end": end if end else default_end,
        }
        direct_hit_resp = get_direct_hit_response(request, query, params, "api.group-events")
        if direct_hit_resp:
            return direct_hit_resp

        if environments:
            params["environment"] = [env.name for env in environments]

        full = request.GET.get("full", False)
        snuba_filter = get_filter(request.GET.get("query", None), params)

        snuba_cols = None if full else eventstore.full_columns

        data_fn = partial(
            eventstore.get_events,
            additional_columns=snuba_cols,
            referrer="api.group-events",
            filter=snuba_filter,
        )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(results, request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn),
        )
Beispiel #6
0
    def _get_events_snuba(self, request, project):
        from sentry.api.paginator import GenericOffsetPaginator
        from sentry.models import SnubaEvent
        from sentry.utils.snuba import raw_query

        query = request.GET.get('query')
        conditions = []
        if query:
            conditions.append(
                [['positionCaseInsensitive', ['message',
                                              "'%s'" % (query, )]], '!=', 0])

        full = request.GET.get('full', False)
        snuba_cols = SnubaEvent.minimal_columns if full else SnubaEvent.selected_columns
        now = timezone.now()
        data_fn = partial(
            # extract 'data' from raw_query result
            lambda *args, **kwargs: raw_query(*args, **kwargs)['data'],
            start=now - timedelta(days=90),
            end=now,
            conditions=conditions,
            filter_keys={'project_id': [project.id]},
            selected_columns=snuba_cols,
            orderby='-timestamp',
            referrer='api.project-events',
        )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(
                [SnubaEvent(row)
                 for row in results], request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn))
Beispiel #7
0
    def __handle_result(self, user, project_id, group_id, result):
        event = {
            'timestamp': result['latest_event_timestamp'],
            'event_id': result['event_id'],
            'group_id': group_id,
            'project_id': project_id
        }

        return {
            'id': result['primary_hash'],
            'latestEvent': serialize(SnubaEvent(event), user, EventSerializer())
        }
Beispiel #8
0
    def __handle_result(self, user, project_id, group_id, result):
        event = {
            "timestamp": result["latest_event_timestamp"],
            "event_id": result["event_id"],
            "group_id": group_id,
            "project_id": project_id,
        }

        return {
            "id": result["primary_hash"],
            "latestEvent": serialize(SnubaEvent(event), user,
                                     EventSerializer()),
        }
Beispiel #9
0
    def get(self, request, organization):
        # Check for a direct hit on event ID
        query = request.GET.get("query", "").strip()

        try:
            direct_hit_resp = get_direct_hit_response(
                request,
                query,
                self.get_filter_params(request, organization),
                "api.organization-events-direct-hit",
            )
        except (OrganizationEventsError, NoProjects):
            pass
        else:
            if direct_hit_resp:
                return direct_hit_resp

        full = request.GET.get("full", False)
        try:
            snuba_args = self.get_snuba_query_args_legacy(
                request, organization)
        except OrganizationEventsError as exc:
            return Response({"detail": exc.message}, status=400)
        except NoProjects:
            # return empty result if org doesn't have projects
            # or user doesn't have access to projects in org
            data_fn = lambda *args, **kwargs: []
        else:
            cols = None if full else eventstore.full_columns

            data_fn = partial(
                eventstore.get_events,
                additional_columns=cols,
                referrer="api.organization-events",
                filter=eventstore.Filter(
                    start=snuba_args["start"],
                    end=snuba_args["end"],
                    conditions=snuba_args["conditions"],
                    project_ids=snuba_args["filter_keys"].get(
                        "project_id", None),
                    group_ids=snuba_args["filter_keys"].get("group_id", None),
                ),
            )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(results, request.user,
                                                 serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn),
        )
Beispiel #10
0
    def _get_events_snuba(self, request, group, environments, query, tags,
                          start, end):
        default_end = timezone.now()
        default_start = default_end - timedelta(days=90)
        params = {
            'issue.id': [group.id],
            'project_id': [group.project_id],
            'start': start if start else default_start,
            'end': end if end else default_end
        }
        direct_hit_resp = get_direct_hit_response(request, query, params,
                                                  'api.group-events')
        if direct_hit_resp:
            return direct_hit_resp

        if environments:
            params['environment'] = [env.name for env in environments]

        full = request.GET.get('full', False)
        snuba_args = get_snuba_query_args(request.GET.get('query', None),
                                          params)

        # TODO(lb): remove once boolean search is fully functional
        if snuba_args:
            has_boolean_op_flag = features.has('organizations:boolean-search',
                                               group.project.organization,
                                               actor=request.user)
            if snuba_args.pop('has_boolean_terms',
                              False) and not has_boolean_op_flag:
                raise GroupEventsError(
                    'Boolean search operator OR and AND not allowed in this search.'
                )

        snuba_cols = SnubaEvent.minimal_columns if full else SnubaEvent.selected_columns

        data_fn = partial(
            # extract 'data' from raw_query result
            lambda *args, **kwargs: raw_query(*args, **kwargs)['data'],
            selected_columns=snuba_cols,
            orderby='-timestamp',
            referrer='api.group-events',
            **snuba_args)

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(
                [SnubaEvent(row)
                 for row in results], request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn))
Beispiel #11
0
    def get(self, request: Request, project) -> Response:
        """
        List a Project's Events
        ```````````````````````

        Return a list of events bound to a project.

        Note: This endpoint is experimental and may be removed without notice.

        :qparam bool full: if this is set to true then the event payload will
                           include the full event body, including the stacktrace.
                           Set to 1 to enable.

        :pparam string organization_slug: the slug of the organization the
                                          groups belong to.
        :pparam string project_slug: the slug of the project the groups
                                     belong to.
        """
        from sentry.api.paginator import GenericOffsetPaginator

        query = request.GET.get("query")
        conditions = []
        if query:
            conditions.append(
                [["positionCaseInsensitive", ["message", f"'{query}'"]], "!=",
                 0])

        event_filter = eventstore.Filter(conditions=conditions,
                                         project_ids=[project.id])
        if features.has("organizations:project-event-date-limit",
                        project.organization,
                        actor=request.user):
            event_filter.start = timezone.now() - timedelta(days=7)

        full = request.GET.get("full", False)

        data_fn = partial(
            eventstore.get_events,
            filter=event_filter,
            referrer="api.project-events",
        )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(results, request.user,
                                                 serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn),
        )
Beispiel #12
0
    def _get_events_snuba(self, request, group, environments, query, tags,
                          start, end):
        default_end = timezone.now()
        default_start = default_end - timedelta(days=90)
        params = {
            "issue.id": [group.id],
            "project_id": [group.project_id],
            "start": start if start else default_start,
            "end": end if end else default_end,
        }
        direct_hit_resp = get_direct_hit_response(request, query, params,
                                                  "api.group-events")
        if direct_hit_resp:
            return direct_hit_resp

        if environments:
            params["environment"] = [env.name for env in environments]

        full = request.GET.get("full", False)
        snuba_args = get_snuba_query_args(request.GET.get("query", None),
                                          params)

        # TODO(lb): remove once boolean search is fully functional
        if snuba_args:
            has_boolean_op_flag = features.has("organizations:boolean-search",
                                               group.project.organization,
                                               actor=request.user)
            if snuba_args.pop("has_boolean_terms",
                              False) and not has_boolean_op_flag:
                raise GroupEventsError(
                    "Boolean search operator OR and AND not allowed in this search."
                )

        snuba_cols = None if full else eventstore.full_columns

        data_fn = partial(eventstore.get_events,
                          additional_columns=snuba_cols,
                          referrer="api.group-events",
                          **snuba_args)

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(results, request.user,
                                                 serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn),
        )
Beispiel #13
0
    def get(self, request, project):
        """
        List a Project's Events
        ```````````````````````

        Return a list of events bound to a project.

        Note: This endpoint is experimental and may be removed without notice.

        :pparam string organization_slug: the slug of the organization the
                                          groups belong to.
        :pparam string project_slug: the slug of the project the groups
                                     belong to.
        """
        from sentry.api.paginator import GenericOffsetPaginator
        from sentry.models import SnubaEvent
        from sentry.utils.snuba import raw_query

        query = request.GET.get('query')
        conditions = []
        if query:
            conditions.append(
                [['positionCaseInsensitive', ['message',
                                              "'%s'" % (query, )]], '!=', 0])

        full = request.GET.get('full', False)
        snuba_cols = SnubaEvent.minimal_columns if full else SnubaEvent.selected_columns
        now = timezone.now()
        data_fn = partial(
            # extract 'data' from raw_query result
            lambda *args, **kwargs: raw_query(*args, **kwargs)['data'],
            start=now - timedelta(days=90),
            end=now,
            conditions=conditions,
            filter_keys={'project_id': [project.id]},
            selected_columns=snuba_cols,
            orderby='-timestamp',
            referrer='api.project-events',
        )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(
                [SnubaEvent(row)
                 for row in results], request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn))
Beispiel #14
0
    def get(self, request, organization):
        if features.has('organizations:events-v2', organization, actor=request.user):
            return self.get_v2(request, organization)

        # Check for a direct hit on event ID
        query = request.GET.get('query', '').strip()

        try:
            direct_hit_resp = get_direct_hit_response(
                request,
                query,
                self.get_filter_params(request, organization),
                'api.organization-events'
            )
        except (OrganizationEventsError, NoProjects):
            pass
        else:
            if direct_hit_resp:
                return direct_hit_resp

        full = request.GET.get('full', False)
        try:
            snuba_args = self.get_snuba_query_args(request, organization)
        except OrganizationEventsError as exc:
            return Response({'detail': exc.message}, status=400)
        except NoProjects:
            # return empty result if org doesn't have projects
            # or user doesn't have access to projects in org
            data_fn = lambda *args, **kwargs: []
        else:
            snuba_cols = SnubaEvent.minimal_columns if full else SnubaEvent.selected_columns
            data_fn = partial(
                # extract 'data' from raw_query result
                lambda *args, **kwargs: raw_query(*args, **kwargs)['data'],
                selected_columns=snuba_cols,
                orderby='-timestamp',
                referrer='api.organization-events',
                **snuba_args
            )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(
                [SnubaEvent(row) for row in results], request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn)
        )
Beispiel #15
0
    def get(self, request, organization):
        logger.info("eventsv1.request", extra={"organization_id": organization.id})

        # Check for a direct hit on event ID
        query = request.GET.get("query", "").strip()

        try:
            direct_hit_resp = get_direct_hit_response(
                request,
                query,
                self.get_filter_params(request, organization),
                "api.organization-events-direct-hit",
            )
        except NoProjects:
            pass
        else:
            if direct_hit_resp:
                return direct_hit_resp

        full = request.GET.get("full", False)
        try:
            snuba_args = self.get_snuba_query_args_legacy(request, organization)
        except NoProjects:
            # return empty result if org doesn't have projects
            # or user doesn't have access to projects in org
            data_fn = lambda *args, **kwargs: []
        else:
            data_fn = partial(
                eventstore.get_events,
                referrer="api.organization-events",
                filter=eventstore.Filter(
                    start=snuba_args["start"],
                    end=snuba_args["end"],
                    conditions=snuba_args["conditions"],
                    project_ids=snuba_args["filter_keys"].get("project_id", None),
                    group_ids=snuba_args["filter_keys"].get("group_id", None),
                ),
            )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(results, request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn),
        )
Beispiel #16
0
def _process_snuba_results(query_res, group: Group, id: int, user):
    event_ids = {
        row["latest_event_id"]: Event.generate_node_id(group.project_id,
                                                       row["latest_event_id"])
        for row in query_res
    }

    node_data = nodestore.get_multi(list(event_ids.values()))

    response = []

    for row in query_res:
        response_item = {
            "hash": row["new_materialized_hash"],
            "eventCount": row["event_count"],
        }
        event_id = row["latest_event_id"]
        event_data = node_data.get(event_ids[event_id], None)

        if event_data is not None:
            event = Event(group.project_id,
                          event_id,
                          group_id=group.id,
                          data=event_data)
            response_item["latestEvent"] = serialize(event, user,
                                                     EventSerializer())

            tree_label = get_path(
                event_data, "hierarchical_tree_labels", id) or get_path(
                    event_data, "hierarchical_tree_labels", -1)

            # Rough approximation of what happens with Group title
            event_type = get_event_type(event.data)
            metadata = dict(event.get_event_metadata())
            metadata["current_tree_label"] = tree_label
            # Force rendering of grouping tree labels irrespective of platform
            metadata["display_title_with_tree_label"] = True
            title = event_type.get_title(metadata)
            response_item["title"] = title or event.title
            response_item["metadata"] = metadata

        response.append(response_item)

    return response
Beispiel #17
0
    def get(self, request, project):
        """
        List a Project's Events
        ```````````````````````

        Return a list of events bound to a project.

        Note: This endpoint is experimental and may be removed without notice.

        :qparam bool full: if this is set to true then the event payload will
                           include the full event body, including the stacktrace.
                           Set to 1 to enable.

        :pparam string organization_slug: the slug of the organization the
                                          groups belong to.
        :pparam string project_slug: the slug of the project the groups
                                     belong to.
        """
        from sentry.api.paginator import GenericOffsetPaginator

        query = request.GET.get("query")
        conditions = []
        if query:
            conditions.append(
                [["positionCaseInsensitive", ["message", "'%s'" % (query,)]], "!=", 0]
            )

        full = request.GET.get("full", False)

        cols = None if full else eventstore.full_columns

        data_fn = partial(
            eventstore.get_events,
            additional_columns=cols,
            filter=eventstore.Filter(conditions=conditions, project_ids=[project.id]),
            referrer="api.project-events",
        )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(results, request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn),
        )
Beispiel #18
0
def _add_hash(
    trees: List[Dict[str, Any]],
    group: Group,
    user,
    parent_hash: Optional[str],
    hash: str,
    child_hash: Optional[str],
    event_count: int,
    last_seen,
    latest_event_id,
):
    event = eventstore.get_event_by_id(group.project_id,
                                       latest_event_id,
                                       group_id=group.id)

    tree = {
        "parentId": parent_hash,
        "id": hash,
        "childId": child_hash,
        "eventCount": event_count,
        "latestEvent": serialize(event, user, EventSerializer()),
    }

    trees.append(tree)

    try:
        for variant in event.get_grouping_variants().values():
            if not isinstance(variant, ComponentVariant):
                continue

            if variant.get_hash() == tree["parentId"]:
                tree["parentLabel"] = variant.component.tree_label

            if variant.get_hash() == tree["id"]:
                tree["label"] = variant.component.tree_label

            if variant.get_hash() == tree["childId"]:
                tree["childLabel"] = variant.component.tree_label

    except Exception:
        sentry_sdk.capture_exception()
Beispiel #19
0
    def get(self, request, project):
        """
        List a Project's Events
        ```````````````````````

        Return a list of events bound to a project.

        Note: This endpoint is experimental and may be removed without notice.

        :pparam string organization_slug: the slug of the organization the
                                          groups belong to.
        :pparam string project_slug: the slug of the project the groups
                                     belong to.
        """
        from sentry.api.paginator import GenericOffsetPaginator

        query = request.GET.get('query')
        conditions = []
        if query:
            conditions.append(
                [['positionCaseInsensitive', ['message',
                                              "'%s'" % (query, )]], '!=', 0])

        full = request.GET.get('full', False)
        cols = None if full else eventstore.full_columns

        data_fn = partial(
            eventstore.get_events,
            conditions=conditions,
            filter_keys={'project_id': [project.id]},
            additional_columns=cols,
            referrer='api.project-events',
        )

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(request=request,
                             on_results=lambda results: serialize(
                                 results, request.user, serializer),
                             paginator=GenericOffsetPaginator(data_fn=data_fn))
Beispiel #20
0
    def _get_events_snuba(self, request, group, environments, query, tags,
                          start, end):
        default_end = timezone.now()
        default_start = default_end - timedelta(days=90)
        params = {
            'issue.id': [group.id],
            'project_id': [group.project_id],
            'start': start if start else default_start,
            'end': end if end else default_end
        }
        direct_hit_resp = get_direct_hit_response(request, query, params,
                                                  'api.group-events')
        if direct_hit_resp:
            return direct_hit_resp

        if environments:
            params['environment'] = [env.name for env in environments]

        full = request.GET.get('full', False)
        snuba_args = get_snuba_query_args(request.GET.get('query', None),
                                          params)
        snuba_cols = SnubaEvent.minimal_columns if full else SnubaEvent.selected_columns

        data_fn = partial(
            # extract 'data' from raw_query result
            lambda *args, **kwargs: raw_query(*args, **kwargs)['data'],
            selected_columns=snuba_cols,
            orderby='-timestamp',
            referrer='api.group-events',
            **snuba_args)

        serializer = EventSerializer() if full else SimpleEventSerializer()
        return self.paginate(
            request=request,
            on_results=lambda results: serialize(
                [SnubaEvent(row)
                 for row in results], request.user, serializer),
            paginator=GenericOffsetPaginator(data_fn=data_fn))
Beispiel #21
0
def _render_trees(group: Group, user):
    materialized_hashes = {
        gh.hash
        for gh in GroupHash.objects.filter(project=group.project, group=group)
    }

    rv = []

    common_where = _get_group_filters(group)

    for materialized_hash in materialized_hashes:
        # For every materialized hash we want to render parent and child
        # hashes, a limited view of the entire tree. We fetch one sample event
        # so we know how we need to slice hierarchical_hashes.
        hierarchical_hashes = _get_full_hierarchical_hashes(
            group, materialized_hash)

        if not hierarchical_hashes:
            # No hierarchical_hashes found, the materialized hash is probably
            # from flat grouping.
            parent_pos = None
            hash_pos = None
            child_pos = None
            slice_start = 0
        else:
            materialized_pos = hierarchical_hashes.index(materialized_hash)

            if materialized_pos == 0:
                parent_pos = None
                hash_pos = 0
                child_pos = 1
                slice_start = 1
            else:
                parent_pos = 0
                hash_pos = 1
                child_pos = 2
                slice_start = materialized_pos

        # Select sub-views of the trees that contain materialized_hash.
        query = (Query("events", Entity("events")).set_select([
            Function("count", [], "event_count"),
            Function(
                "argMax",
                [Column("event_id"), Column("timestamp")], "event_id"),
            Function("max", [Column("timestamp")], "latest_event_timestamp"),
            Function("arraySlice",
                     [Column("hierarchical_hashes"), slice_start, 3],
                     "hashes"),
        ]).set_where(common_where + [
            Condition(
                Function(
                    "has",
                    [
                        Column("hierarchical_hashes"),
                        materialized_hash,
                    ],
                ),
                Op.EQ,
                1,
            ),
        ]).set_groupby([Column("hashes")]).set_orderby(
            [OrderBy(Column("latest_event_timestamp"), Direction.DESC)]))

        for row in snuba.raw_snql_query(query)["data"]:
            assert not row["hashes"] or row["hashes"][
                hash_pos] == materialized_hash

            event_id = row["event_id"]
            event = eventstore.get_event_by_id(group.project_id, event_id)

            tree = {
                "parentId": _get_checked(row["hashes"], parent_pos),
                "id": materialized_hash,
                "childId": _get_checked(row["hashes"], child_pos),
                "eventCount": row["event_count"],
                "latestEvent": serialize(event, user, EventSerializer()),
            }

            rv.append(tree)

            if not row["hashes"]:
                continue

            try:
                for variant in event.get_grouping_variants().values():
                    if not isinstance(variant, ComponentVariant):
                        continue

                    if variant.get_hash() == tree["parentId"]:
                        tree["parentLabel"] = variant.component.tree_label

                    if variant.get_hash() == tree["childId"]:
                        tree["childLabel"] = variant.component.tree_label

                    if variant.get_hash() == tree["id"]:
                        tree["label"] = variant.component.tree_label
            except Exception:
                sentry_sdk.capture_exception()

    rv.sort(key=lambda tree:
            (tree["parentId"] or "", tree["id"] or "", tree["childId"] or ""))

    return rv