Ejemplo n.º 1
0
    def test_no_commits(self):
        event = self.store_event(
            data={
                "timestamp": iso_format(before_now(seconds=1)),
                "message": "Kaboom!",
                "stacktrace": {
                    "frames": [
                        {
                            "function": "handle_set_commits",
                            "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
                            "module": "sentry.tasks",
                            "in_app": True,
                            "lineno": 30,
                            "filename": "sentry/tasks.py",
                        },
                        {
                            "function": "set_commits",
                            "abs_path":
                            "/usr/src/sentry/src/sentry/models/release.py",
                            "module": "sentry.models.release",
                            "in_app": True,
                            "lineno": 39,
                            "filename": "sentry/models/release.py",
                        },
                    ]
                },
                "tags": {
                    "sentry:release": self.release.version
                },
            },
            project_id=self.project.id,
        )

        with self.assertRaises(Commit.DoesNotExist):
            get_serialized_event_file_committers(self.project, event)
Ejemplo n.º 2
0
    def test_no_commits(self):
        event = self.create_event(
            group=self.group,
            message="Kaboom!",
            platform="python",
            stacktrace={
                "frames": [
                    {
                        "function": "handle_set_commits",
                        "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
                        "module": "sentry.tasks",
                        "in_app": True,
                        "lineno": 30,
                        "filename": "sentry/tasks.py",
                    },
                    {
                        "function": "set_commits",
                        "abs_path": "/usr/src/sentry/src/sentry/models/release.py",
                        "module": "sentry.models.release",
                        "in_app": True,
                        "lineno": 39,
                        "filename": "sentry/models/release.py",
                    },
                ]
            },
        )

        with self.assertRaises(Commit.DoesNotExist):
            get_serialized_event_file_committers(self.project, event)
Ejemplo n.º 3
0
    def test_java_sdk_path_mangling(self):
        event = self.store_event(
            data={
                "message": "Kaboom!",
                "platform": "java",
                "stacktrace": {
                    "frames": [
                        {
                            "function": "invoke0",
                            "abs_path": "NativeMethodAccessorImpl.java",
                            "in_app": False,
                            "module": "jdk.internal.reflect.NativeMethodAccessorImpl",
                            "filename": "NativeMethodAccessorImpl.java",
                        },
                        {
                            "function": "home",
                            "abs_path": "Application.java",
                            "module": "io.sentry.example.Application",
                            "in_app": True,
                            "lineno": 30,
                            "filename": "Application.java",
                        },
                        {
                            "function": "handledError",
                            "abs_path": "Application.java",
                            "module": "io.sentry.example.Application",
                            "in_app": True,
                            "lineno": 39,
                            "filename": "Application.java",
                        },
                    ]
                },
                "tags": {"sentry:release": self.release.version},
            },
            project_id=self.project.id,
        )
        self.release.set_commits(
            [
                {
                    "id": "a" * 40,
                    "repository": self.repo.name,
                    "author_email": "*****@*****.**",
                    "author_name": "Bob",
                    "message": "i fixed a bug",
                    "patch_set": [
                        {"path": "src/main/java/io/sentry/example/Application.java", "type": "M"}
                    ],
                }
            ]
        )
        GroupRelease.objects.create(
            group_id=event.group.id, project_id=self.project.id, release_id=self.release.id
        )

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 1
        assert "commits" in result[0]
        assert len(result[0]["commits"]) == 1
        assert result[0]["commits"][0]["id"] == "a" * 40
Ejemplo n.º 4
0
    def test_matching(self):
        event = self.store_event(
            data={
                "message": "Kaboom!",
                "platform": "python",
                "timestamp": iso_format(before_now(seconds=1)),
                "stacktrace": {
                    "frames": [
                        {
                            "function": "handle_set_commits",
                            "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
                            "module": "sentry.tasks",
                            "in_app": True,
                            "lineno": 30,
                            "filename": "sentry/tasks.py",
                        },
                        {
                            "function": "set_commits",
                            "abs_path":
                            "/usr/src/sentry/src/sentry/models/release.py",
                            "module": "sentry.models.release",
                            "in_app": True,
                            "lineno": 39,
                            "filename": "sentry/models/release.py",
                        },
                    ]
                },
                "tags": {
                    "sentry:release": self.release.version
                },
            },
            project_id=self.project.id,
        )
        self.release.set_commits([{
            "id":
            "a" * 40,
            "repository":
            self.repo.name,
            "author_email":
            "*****@*****.**",
            "author_name":
            "Bob",
            "message":
            "i fixed a bug",
            "patch_set": [{
                "path": "src/sentry/models/release.py",
                "type": "M"
            }],
        }])
        GroupRelease.objects.create(group_id=event.group.id,
                                    project_id=self.project.id,
                                    release_id=self.release.id)

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 1
        assert "commits" in result[0]
        assert len(result[0]["commits"]) == 1
        assert result[0]["commits"][0]["id"] == "a" * 40
Ejemplo n.º 5
0
    def test_no_matching_user(self):
        self.set_release_commits("*****@*****.**")

        result = get_serialized_event_file_committers(self.project, self.event)

        assert len(result) == 1
        assert "commits" in result[0]
        assert len(result[0]["commits"]) == 1
        assert result[0]["commits"][0]["id"] == "a" * 40
        assert not GroupOwner.objects.filter(group=self.event.group).exists()
        process_suspect_commits(self.event)
        assert not GroupOwner.objects.filter(group=self.event.group).exists()
Ejemplo n.º 6
0
    def test_java_sdk_path_mangling(self):
        event = self.create_event(
            group=self.group,
            message='Kaboom!',
            platform='java',
            stacktrace={
                'frames': [{
                    "function": "invoke0",
                    "abs_path": "NativeMethodAccessorImpl.java",
                    "in_app": False,
                    "module": "jdk.internal.reflect.NativeMethodAccessorImpl",
                    "filename": "NativeMethodAccessorImpl.java",
                }, {
                    "function": "home",
                    "abs_path": "Application.java",
                    "module": "io.sentry.example.Application",
                    "in_app": True,
                    "lineno": 30,
                    "filename": "Application.java",
                }, {
                    "function": "handledError",
                    "abs_path": "Application.java",
                    "module": "io.sentry.example.Application",
                    "in_app": True,
                    "lineno": 39,
                    "filename": "Application.java",
                }]
            })
        self.release.set_commits([{
            'id':
            'a' * 40,
            'repository':
            self.repo.name,
            'author_email':
            '*****@*****.**',
            'author_name':
            'Bob',
            'message':
            'i fixed a bug',
            'patch_set': [
                {
                    'path': 'src/main/java/io/sentry/example/Application.java',
                    'type': 'M',
                },
            ]
        }])

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 1
        assert 'commits' in result[0]
        assert len(result[0]['commits']) == 1
        assert result[0]['commits'][0]['id'] == 'a' * 40
Ejemplo n.º 7
0
    def test_not_matching(self):
        event = self.store_event(
            data={
                "message": "Kaboom!",
                "platform": "python",
                "stacktrace": {
                    "frames": [
                        {
                            "function": "handle_set_commits",
                            "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
                            "module": "sentry.tasks",
                            "in_app": True,
                            "lineno": 30,
                            "filename": "sentry/tasks.py",
                        },
                        {
                            "function": "set_commits",
                            "abs_path":
                            "/usr/src/sentry/src/sentry/models/release.py",
                            "module": "sentry.models.release",
                            "in_app": True,
                            "lineno": 39,
                            "filename": "sentry/models/release.py",
                        },
                    ]
                },
                "tags": {
                    "sentry:release": self.release.version
                },
            },
            project_id=self.project.id,
        )
        self.release.set_commits([{
            "id":
            "a" * 40,
            "repository":
            self.repo.name,
            "author_email":
            "*****@*****.**",
            "author_name":
            "Bob",
            "message":
            "i fixed a bug",
            "patch_set": [{
                "path": "some/other/path.py",
                "type": "M"
            }],
        }])

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 0
Ejemplo n.º 8
0
    def get(self, request, project, event_id):
        """
        Retrieve Committer information for an event
        ```````````````````````````````````````````

        Return commiters on an individual event, plus a per-frame breakdown.

        :pparam string project_slug: the slug of the project the event
                                     belongs to.
        :pparam string event_id: the hexadecimal ID of the event to
                                 retrieve (as reported by the raven client).
        :auth: required
        """

        use_snuba = options.get('snuba.events-queries.enabled')

        event_cls = event_cls = SnubaEvent if use_snuba else Event

        event = event_cls.objects.from_event_id(event_id, project.id)
        if event is None:
            return Response({'detail': 'Event not found'}, status=404)

        # populate event data
        Event.objects.bind_nodes([event], 'data')

        try:
            committers = get_serialized_event_file_committers(
                project,
                event,
                frame_limit=int(request.GET.get('frameLimit', 25)),
            )
        except Release.DoesNotExist:
            return Response({'detail': 'Release not found'}, status=404)
        except Commit.DoesNotExist:
            return Response({'detail': 'No Commits found for Release'},
                            status=404)

        # XXX(dcramer): this data is unused, so lets not bother returning it for now
        # serialize the commit objects
        # serialized_annotated_frames = [
        #     {
        #         'frame': frame['frame'],
        #         'commits': serialize(frame['commits'])
        #     } for frame in annotated_frames
        # ]

        data = {
            'committers': committers,
            # 'annotatedFrames': serialized_annotated_frames
        }
        return Response(data)
Ejemplo n.º 9
0
    def test_matching(self):
        event = self.create_event(
            group=self.group,
            message='Kaboom!',
            platform='python',
            stacktrace={
                'frames': [{
                    "function": "handle_set_commits",
                    "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
                    "module": "sentry.tasks",
                    "in_app": True,
                    "lineno": 30,
                    "filename": "sentry/tasks.py",
                }, {
                    "function": "set_commits",
                    "abs_path": "/usr/src/sentry/src/sentry/models/release.py",
                    "module": "sentry.models.release",
                    "in_app": True,
                    "lineno": 39,
                    "filename": "sentry/models/release.py",
                }]
            })
        self.release.set_commits([{
            'id':
            'a' * 40,
            'repository':
            self.repo.name,
            'author_email':
            '*****@*****.**',
            'author_name':
            'Bob',
            'message':
            'i fixed a bug',
            'patch_set': [
                {
                    'path': 'src/sentry/models/release.py',
                    'type': 'M',
                },
            ]
        }])

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 1
        assert 'commits' in result[0]
        assert len(result[0]['commits']) == 1
        assert result[0]['commits'][0]['id'] == 'a' * 40
Ejemplo n.º 10
0
    def get(self, request, project, event_id):
        """
        Retrieve Committer information for an event
        ```````````````````````````````````````````

        Return commiters on an individual event, plus a per-frame breakdown.

        :pparam string project_slug: the slug of the project the event
                                     belongs to.
        :pparam string event_id: the hexadecimal ID of the event to
                                 retrieve (as reported by the raven client).
        :auth: required
        """
        event = eventstore.get_event_by_id(project.id, event_id)
        if event is None:
            return Response({"detail": "Event not found"}, status=404)

        # populate event data
        if not options.get("eventstore.use-nodestore"):
            event.bind_node_data()

        try:
            committers = get_serialized_event_file_committers(
                project,
                event,
                frame_limit=int(request.GET.get("frameLimit", 25)))
        except Release.DoesNotExist:
            return Response({"detail": "Release not found"}, status=404)
        except Commit.DoesNotExist:
            return Response({"detail": "No Commits found for Release"},
                            status=404)

        # XXX(dcramer): this data is unused, so lets not bother returning it for now
        # serialize the commit objects
        # serialized_annotated_frames = [
        #     {
        #         'frame': frame['frame'],
        #         'commits': serialize(frame['commits'])
        #     } for frame in annotated_frames
        # ]

        data = {
            "committers": committers,
            # 'annotatedFrames': serialized_annotated_frames
        }
        return Response(data)
Ejemplo n.º 11
0
    def test_matching_case_insensitive(self):
        event = self.store_event(
            data={
                "message": "Kaboom!",
                "platform": "csp",
                "stacktrace": {
                    "frames": [{
                        "function": "roar",
                        "abs_path": "/usr/src/app/TigerMachine.cpp",
                        "module": "",
                        "in_app": True,
                        "lineno": 30,
                        "filename": "app/TigerMachine.cpp",
                    }]
                },
                "tags": {
                    "sentry:release": self.release.version
                },
            },
            project_id=self.project.id,
        )
        self.release.set_commits([{
            "id":
            "a" * 40,
            "repository":
            self.repo.name,
            "author_email":
            "*****@*****.**",
            "author_name":
            "Bob",
            "message":
            "i fixed a bug",
            "patch_set": [{
                "path": "app/tigermachine.cpp",
                "type": "M"
            }],
        }])
        GroupRelease.objects.create(group_id=event.group.id,
                                    project_id=self.project.id,
                                    release_id=self.release.id)

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 1
        assert "commits" in result[0]
        assert len(result[0]["commits"]) == 1
        assert result[0]["commits"][0]["id"] == "a" * 40
Ejemplo n.º 12
0
    def test_matching_case_insensitive(self):
        event = self.create_event(group=self.group,
                                  message='Kaboom!',
                                  platform='cpp',
                                  stacktrace={
                                      'frames': [{
                                          "function":
                                          "roar",
                                          "abs_path":
                                          "/usr/src/app/TigerMachine.cpp",
                                          "module":
                                          "",
                                          "in_app":
                                          True,
                                          "lineno":
                                          30,
                                          "filename":
                                          "app/TigerMachine.cpp",
                                      }]
                                  })
        self.release.set_commits([{
            'id':
            'a' * 40,
            'repository':
            self.repo.name,
            'author_email':
            '*****@*****.**',
            'author_name':
            'Bob',
            'message':
            'i fixed a bug',
            'patch_set': [
                {
                    'path': 'app/tigermachine.cpp',
                    'type': 'M',
                },
            ]
        }])

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 1
        assert 'commits' in result[0]
        assert len(result[0]['commits']) == 1
        assert result[0]['commits'][0]['id'] == 'a' * 40
Ejemplo n.º 13
0
    def test_matching(self):
        event = self.create_event(
            group=self.group,
            message="Kaboom!",
            platform="python",
            stacktrace={
                "frames": [
                    {
                        "function": "handle_set_commits",
                        "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
                        "module": "sentry.tasks",
                        "in_app": True,
                        "lineno": 30,
                        "filename": "sentry/tasks.py",
                    },
                    {
                        "function": "set_commits",
                        "abs_path": "/usr/src/sentry/src/sentry/models/release.py",
                        "module": "sentry.models.release",
                        "in_app": True,
                        "lineno": 39,
                        "filename": "sentry/models/release.py",
                    },
                ]
            },
        )
        self.release.set_commits(
            [
                {
                    "id": "a" * 40,
                    "repository": self.repo.name,
                    "author_email": "*****@*****.**",
                    "author_name": "Bob",
                    "message": "i fixed a bug",
                    "patch_set": [{"path": "src/sentry/models/release.py", "type": "M"}],
                }
            ]
        )

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 1
        assert "commits" in result[0]
        assert len(result[0]["commits"]) == 1
        assert result[0]["commits"][0]["id"] == "a" * 40
Ejemplo n.º 14
0
    def test_no_matching_user(self):
        self.set_release_commits("*****@*****.**")

        result = get_serialized_event_file_committers(self.project, self.event)

        assert len(result) == 1
        assert "commits" in result[0]
        assert len(result[0]["commits"]) == 1
        assert result[0]["commits"][0]["id"] == "a" * 40
        assert not GroupOwner.objects.filter(group=self.event.group).exists()
        event_frames = get_frame_paths(self.event.data)
        process_suspect_commits(
            event_id=self.event.event_id,
            event_platform=self.event.platform,
            event_frames=event_frames,
            group_id=self.event.group_id,
            project_id=self.event.project_id,
        )
        assert not GroupOwner.objects.filter(group=self.event.group).exists()
Ejemplo n.º 15
0
def get_commits(project: Project, event: Event) -> Sequence[Mapping[str, Any]]:
    # lets identify possibly suspect commits and owners
    commits: MutableMapping[int, Mapping[str, Any]] = {}
    try:
        committers = get_serialized_event_file_committers(project, event)
    except (Commit.DoesNotExist, Release.DoesNotExist):
        pass
    except Exception as exc:
        logging.exception(str(exc))
    else:
        for committer in committers:
            for commit in committer["commits"]:
                if commit["id"] not in commits:
                    commit_data = commit.copy()
                    commit_data["shortId"] = commit_data["id"][:7]
                    commit_data["author"] = committer["author"]
                    commit_data["subject"] = commit_data["message"].split(
                        "\n", 1)[0]
                    commits[commit["id"]] = commit_data

    return sorted(commits.values(),
                  key=lambda x: float(x["score"]),
                  reverse=True)
Ejemplo n.º 16
0
    def test_matching_case_insensitive(self):
        event = self.create_event(
            group=self.group,
            message="Kaboom!",
            platform="cpp",
            stacktrace={
                "frames": [
                    {
                        "function": "roar",
                        "abs_path": "/usr/src/app/TigerMachine.cpp",
                        "module": "",
                        "in_app": True,
                        "lineno": 30,
                        "filename": "app/TigerMachine.cpp",
                    }
                ]
            },
        )
        self.release.set_commits(
            [
                {
                    "id": "a" * 40,
                    "repository": self.repo.name,
                    "author_email": "*****@*****.**",
                    "author_name": "Bob",
                    "message": "i fixed a bug",
                    "patch_set": [{"path": "app/tigermachine.cpp", "type": "M"}],
                }
            ]
        )

        result = get_serialized_event_file_committers(self.project, event)
        assert len(result) == 1
        assert "commits" in result[0]
        assert len(result[0]["commits"]) == 1
        assert result[0]["commits"][0]["id"] == "a" * 40
Ejemplo n.º 17
0
    def notify(self,
               notification,
               target_type,
               target_identifier=None,
               **kwargs):
        metrics.incr("mail_adapter.notify")
        event = notification.event
        environment = event.get_tag("environment")
        group = event.group
        project = group.project
        org = group.organization
        logger.info(
            "mail.adapter.notify",
            extra={
                "target_type": target_type.value,
                "target_identifier": target_identifier,
                "group": group.id,
                "project_id": project.id,
            },
        )

        subject = event.get_email_subject()

        query_params = {"referrer": "alert_email"}
        if environment:
            query_params["environment"] = environment
        link = group.get_absolute_url(params=query_params)

        template = "sentry/emails/error.txt"
        html_template = "sentry/emails/error.html"

        rules = []
        for rule in notification.rules:
            rule_link = "/settings/%s/projects/%s/alerts/rules/%s/" % (
                org.slug,
                project.slug,
                rule.id,
            )

            rules.append((rule.label, rule_link))

        enhanced_privacy = org.flags.enhanced_privacy

        # lets identify possibly suspect commits and owners
        commits = {}
        try:
            committers = get_serialized_event_file_committers(project, event)
        except (Commit.DoesNotExist, Release.DoesNotExist):
            pass
        except Exception as exc:
            logging.exception(six.text_type(exc))
        else:
            for committer in committers:
                for commit in committer["commits"]:
                    if commit["id"] not in commits:
                        commit_data = commit.copy()
                        commit_data["shortId"] = commit_data["id"][:7]
                        commit_data["author"] = committer["author"]
                        commit_data["subject"] = commit_data["message"].split(
                            "\n", 1)[0]
                        commits[commit["id"]] = commit_data

        project_plugins = plugins.for_project(project, version=1)
        organization_integrations = Integration.objects.filter(
            organizations=org).first()
        has_integrations = bool(project_plugins or organization_integrations)

        context = {
            "project_label":
            project.get_full_name(),
            "group":
            group,
            "event":
            event,
            "link":
            link,
            "rules":
            rules,
            "has_integrations":
            has_integrations,
            "enhanced_privacy":
            enhanced_privacy,
            "commits":
            sorted(commits.values(), key=lambda x: x["score"], reverse=True),
            "environment":
            environment,
        }

        # if the organization has enabled enhanced privacy controls we dont send
        # data which may show PII or source code
        if not enhanced_privacy:
            interface_list = []
            for interface in six.itervalues(event.interfaces):
                body = interface.to_email_html(event)
                if not body:
                    continue
                text_body = interface.to_string(event)
                interface_list.append(
                    (interface.get_title(), mark_safe(body), text_body))

            context.update({"tags": event.tags, "interfaces": interface_list})

        headers = {
            "X-Sentry-Logger": group.logger,
            "X-Sentry-Logger-Level": group.get_level_display(),
            "X-Sentry-Project": project.slug,
            "X-Sentry-Reply-To": group_id_to_email(group.id),
        }

        for user_id in self.get_send_to(
                project=project,
                target_type=target_type,
                target_identifier=target_identifier,
                event=event,
        ):
            logger.info(
                "mail.adapter.notify.mail_user",
                extra={
                    "target_type": target_type,
                    "target_identifier": target_identifier,
                    "group": group.id,
                    "project_id": project.id,
                    "user_id": user_id,
                },
            )

            self.add_unsubscribe_link(context, user_id, project, "alert_email")
            self._send_mail(
                subject=subject,
                template=template,
                html_template=html_template,
                project=project,
                reference=group,
                headers=headers,
                type="notify.error",
                context=context,
                send_to=[user_id],
            )
Ejemplo n.º 18
0
    def notify(self, notification):
        from sentry.models import Commit, Release

        event = notification.event

        environment = event.get_tag('environment')

        group = event.group
        project = group.project
        org = group.organization

        subject = event.get_email_subject()

        query_params = {'referrer': 'alert_email'}
        if environment:
            query_params['environment'] = environment
        link = group.get_absolute_url(params=query_params)

        template = 'sentry/emails/error.txt'
        html_template = 'sentry/emails/error.html'

        rules = []
        for rule in notification.rules:
            rule_link = '/settings/%s/projects/%s/alerts/rules/%s/' % (
                org.slug, project.slug, rule.id)

            rules.append((rule.label, rule_link))

        enhanced_privacy = org.flags.enhanced_privacy

        # lets identify possibly suspect commits and owners
        commits = {}
        try:
            committers = get_serialized_event_file_committers(project, event)
        except (Commit.DoesNotExist, Release.DoesNotExist):
            pass
        except Exception as exc:
            logging.exception(six.text_type(exc))
        else:
            for committer in committers:
                for commit in committer['commits']:
                    if commit['id'] not in commits:
                        commit_data = commit.copy()
                        commit_data['shortId'] = commit_data['id'][:7]
                        commit_data['author'] = committer['author']
                        commit_data['subject'] = commit_data['message'].split(
                            '\n', 1)[0]
                        commits[commit['id']] = commit_data

        context = {
            'project_label':
            project.get_full_name(),
            'group':
            group,
            'event':
            event,
            'link':
            link,
            'rules':
            rules,
            'enhanced_privacy':
            enhanced_privacy,
            'commits':
            sorted(commits.values(), key=lambda x: x['score'], reverse=True),
            'environment':
            environment
        }

        # if the organization has enabled enhanced privacy controls we dont send
        # data which may show PII or source code
        if not enhanced_privacy:
            interface_list = []
            for interface in six.itervalues(event.interfaces):
                body = interface.to_email_html(event)
                if not body:
                    continue
                text_body = interface.to_string(event)
                interface_list.append(
                    (interface.get_title(), mark_safe(body), text_body))

            context.update({
                'tags': event.tags,
                'interfaces': interface_list,
            })

        headers = {
            'X-Sentry-Logger': group.logger,
            'X-Sentry-Logger-Level': group.get_level_display(),
            'X-Sentry-Project': project.slug,
            'X-Sentry-Reply-To': group_id_to_email(group.id),
        }

        for user_id in self.get_send_to(project=project, event=event):
            self.add_unsubscribe_link(context, user_id, project, 'alert_email')

            self._send_mail(
                subject=subject,
                template=template,
                html_template=html_template,
                project=project,
                reference=group,
                headers=headers,
                type='notify.error',
                context=context,
                send_to=[user_id],
            )