def edit_user(request, user_id): if not request.is_superuser(): return HttpResponseRedirect(auth.get_login_url()) try: user = User.objects.get(pk=user_id) except User.DoesNotExist: return HttpResponseRedirect(absolute_uri('/manage/users/')) form = ChangeUserForm(request.POST or None, instance=user) if form.is_valid(): user = form.save() return HttpResponseRedirect(absolute_uri('/manage/users/')) project_list = Project.objects.filter( status=0, organization__member_set__user=user, ).order_by('-date_added') context = { 'form': form, 'the_user': user, 'project_list': project_list, } context.update(csrf(request)) return render_to_response('sentry/admin/users/edit.html', context, request)
def remove_user(request, user_id): if six.text_type(user_id) == six.text_type(request.user.id): return HttpResponseRedirect(absolute_uri('/manage/users/')) try: user = User.objects.get(pk=user_id) except User.DoesNotExist: return HttpResponseRedirect(absolute_uri('/manage/users/')) form = RemoveUserForm(request.POST or None) if form.is_valid(): if form.cleaned_data['removal_type'] == '2': user.delete() else: User.objects.filter(pk=user.pk).update(is_active=False) return HttpResponseRedirect(absolute_uri('/manage/users/')) context = csrf(request) context.update({ 'form': form, 'the_user': user, }) return render_to_response('sentry/admin/users/remove.html', context, request)
def build_saml_config(provider_config, org): """ Construct the SAML configuration dict to be passed into the OneLogin SAML library. For more details about the structure of this object see the SAML2Provider.build_config method. """ avd = provider_config.get('advanced', {}) security_config = { 'authnRequestsSigned': avd.get('authn_request_signed', False), 'logoutRequestSigned': avd.get('logout_request_signed', False), 'logoutResponseSigned': avd.get('logout_response_signed', False), 'signMetadata': avd.get('metadata_signed', False), 'wantMessagesSigned': avd.get('want_message_signed', False), 'wantAssertionsSigned': avd.get('want_assertion_signed', False), 'wantAssertionsEncrypted': avd.get('want_assertion_encrypted', False), 'signatureAlgorithm': avd.get('signature_algorithm', OneLogin_Saml2_Constants.RSA_SHA256), 'digestAlgorithm': avd.get('digest_algorithm', OneLogin_Saml2_Constants.SHA256), 'wantNameId': False, } idp = provider_config['idp'] # TODO(epurkhiser): This is also available in the helper and should probably come from there. acs_url = absolute_uri(reverse('sentry-auth-organization-saml-acs', args=[org])) sls_url = absolute_uri(reverse('sentry-auth-organization-saml-sls', args=[org])) metadata_url = absolute_uri(reverse('sentry-auth-organization-saml-metadata', args=[org])) saml_config = { 'strict': True, 'idp': { 'entityId': idp['entity_id'], 'x509cert': idp['x509cert'], 'singleSignOnService': {'url': idp['sso_url']}, 'singleLogoutService': {'url': idp['slo_url']}, }, 'sp': { 'entityId': metadata_url, 'assertionConsumerService': { 'url': acs_url, 'binding': OneLogin_Saml2_Constants.BINDING_HTTP_POST, }, 'singleLogoutService': { 'url': sls_url, 'binding': OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT, }, }, 'security': security_config, } if avd.get('x509cert') is not None: saml_config['sp']['x509cert'] = avd['x509cert'] if avd.get('private_key') is not None: saml_config['sp']['privateKey'] = avd['private_key'] return saml_config
def handle(self, request, organization, team, project): token = None if request.method == "POST": op = request.POST.get("op") if op == "regenerate-token": token = self._regenerate_token(project) messages.add_message(request, messages.SUCCESS, OK_TOKEN_REGENERATED) elif op == "enable": self._handle_enable_plugin(request, project) elif op == "disable": self._handle_disable_plugin(request, project) return HttpResponseRedirect(request.path) if token is None: token = ProjectOption.objects.get_value(project, "sentry:release-token") if token is None: token = self._regenerate_token(project) enabled_plugins = [] other_plugins = [] for plugin in self._iter_plugins(): if plugin.is_enabled(project): hook_url = absolute_uri( reverse( "sentry-release-hook", kwargs={ "plugin_id": plugin.slug, "project_id": project.id, "signature": self._get_signature(project.id, plugin.slug, token), }, ) ) content = plugin.get_release_doc_html(hook_url=hook_url) enabled_plugins.append((plugin, mark_safe(content))) elif plugin.can_configure_for_project(project): other_plugins.append(plugin) context = { "page": "release-tracking", "token": token, "enabled_plugins": enabled_plugins, "other_plugins": other_plugins, "webhook_url": absolute_uri( reverse( "sentry-release-hook", kwargs={ "plugin_id": "builtin", "project_id": project.id, "signature": self._get_signature(project.id, "builtin", token), }, ) ), } return self.respond("sentry/project-release-tracking.html", context)
def dispatch(self, request, pipeline): client_key = request.GET.get('clientKey') if client_key is None: return self.redirect( 'https://bitbucket.org/site/addons/authorize?descriptor_uri=%s&redirect_uri=%s' % ( absolute_uri('/extensions/bitbucket/descriptor/'), absolute_uri('/extensions/bitbucket/setup/'), )) pipeline.bind_state('bitbucket_client_key', client_key) return pipeline.next_step()
def get_absolute_url(self): # HACK(dcramer): quick and dirty way to support code/users try: url_name = self.URL_NAMES[self.key] except KeyError: url_name = self.DEFAULT_URL_NAME return absolute_uri(reverse(url_name, args=[ self.project.organization.slug, self.project.slug, self.key])) return absolute_uri(reverse(url_name, args=[ self.project.organization.slug, self.project.slug]))
def test_send_alert_event(self, safe_urlopen): group = self.create_group(project=self.project) event = self.create_event(group=group) rule_future = RuleFuture( rule=self.rule, kwargs={'sentry_app': self.sentry_app}, ) with self.feature('organizations:sentry10'): with self.tasks(): notify_sentry_app(event, [rule_future]) data = json.loads(faux(safe_urlopen).kwargs['data']) assert data == { 'action': 'triggered', 'installation': { 'uuid': self.install.uuid, }, 'data': { 'event': DictContaining( event_id=event.event_id, url=absolute_uri(reverse('sentry-api-0-project-event-details', args=[ self.organization.slug, self.project.slug, event.id, ])), web_url=absolute_uri(reverse('sentry-organization-event-detail', args=[ self.organization.slug, group.id, event.id, ])), issue_url=absolute_uri( '/api/0/issues/{}/'.format(group.id), ), ), 'triggered_rule': self.rule.label, }, 'actor': { 'type': 'application', 'id': 'sentry', 'name': 'Sentry', } } assert faux(safe_urlopen).kwarg_equals('headers', DictContaining( 'Content-Type', 'Request-ID', 'Sentry-Hook-Resource', 'Sentry-Hook-Timestamp', 'Sentry-Hook-Signature', ))
def handle(self, request, organization, team, project): token = None if request.method == 'POST': op = request.POST.get('op') if op == 'regenerate-token': token = self._regenerate_token(project) messages.add_message( request, messages.SUCCESS, OK_TOKEN_REGENERATED, ) elif op == 'enable': self._handle_enable_plugin(request, project) elif op == 'disable': self._handle_disable_plugin(request, project) return HttpResponseRedirect(request.path) if token is None: token = ProjectOption.objects.get_value(project, 'sentry:release-token') if token is None: token = self._regenerate_token(project) enabled_plugins = [] other_plugins = [] for plugin in self._iter_plugins(): if plugin.is_enabled(project): hook_url = absolute_uri(reverse('sentry-release-hook', kwargs={ 'plugin_id': plugin.slug, 'project_id': project.id, 'signature': self._get_signature(project.id, plugin.slug, token), })) content = plugin.get_release_doc_html(hook_url=hook_url) enabled_plugins.append((plugin, mark_safe(content))) elif plugin.can_configure_for_project(project): other_plugins.append(plugin) context = { 'page': 'release-tracking', 'token': token, 'enabled_plugins': enabled_plugins, 'other_plugins': other_plugins, 'webhook_url': absolute_uri(reverse('sentry-release-hook', kwargs={ 'plugin_id': 'builtin', 'project_id': project.id, 'signature': self._get_signature(project.id, 'builtin', token), })) } return self.respond('sentry/project-release-tracking.html', context)
def get(self, request): org = Organization( id=1, slug='organization', name='My Company', ) team = Team( id=1, slug='team', name='My Team', organization=org, ) project = Project( id=1, organization=org, team=team, slug='project', name='My Project', ) release = Release( project=project, version=sha1(uuid4().hex).hexdigest(), ) release_link = absolute_uri(reverse('sentry-release-details', kwargs={ 'organization_slug': org.slug, 'project_id': project.slug, 'version': release.version, })) project_link = absolute_uri(reverse('sentry-stream', kwargs={ 'organization_slug': org.slug, 'project_id': project.slug, })) preview = MailPreview( html_template='sentry/emails/activity/release.html', text_template='sentry/emails/activity/release.txt', context={ 'release': release, 'project': project, 'release_link': release_link, 'project_link': project_link, }, ) return render_to_response('sentry/debug/mail/preview.html', { 'preview': preview, })
def get_absolute_url(self): # HACK(dcramer): quick and dirty way to support code/users if self.key == 'sentry:user': url_name = 'sentry-user-details' elif self.key == 'sentry:filename': url_name = 'sentry-explore-code-details' elif self.key == 'sentry:function': url_name = 'sentry-explore-code-details-by-function' else: url_name = 'sentry-explore-tag-value' return absolute_uri(reverse(url_name, args=[ self.project.organization.slug, self.project.slug, self.key, self.id])) return absolute_uri(reverse(url_name, args=[ self.project.organization.slug, self.project.slug, self.id]))
def dispatch(self, request, pipeline): if 'completed_installation_guide' in request.GET: return pipeline.next_step() return render_to_response( template='sentry/integrations/gitlab-config.html', context={ 'next_url': '%s%s' % (absolute_uri('extensions/gitlab/setup/'), '?completed_installation_guide'), 'setup_values': [ {'label': 'Name', 'value': 'Sentry'}, {'label': 'Redirect URI', 'value': absolute_uri('/extensions/gitlab/setup/')}, {'label': 'Scopes', 'value': 'api'} ] }, request=request, )
def get_invite_link(self): if not self.is_pending: return None return absolute_uri(reverse('sentry-accept-invite', kwargs={ 'member_id': self.id, 'token': self.token or self.legacy_token, }))
def check(self): # There is no queue, and celery is not running, so never show error if settings.CELERY_ALWAYS_EAGER: return [] last_ping = options.get('sentry:last_worker_ping') or 0 if last_ping >= time() - 300: return [] backlogged, size = None, 0 from sentry.monitoring.queues import backend if backend is not None: size = backend.get_size('default') backlogged = size > 0 message = "Background workers haven't checked in recently. " if backlogged: message += "It seems that you have a backlog of %d tasks. Either your workers aren't running or you need more capacity." % size else: message += "This is likely an issue with your configuration or the workers aren't running." return [ Problem( message, url=absolute_uri('/manage/queue/'), ), ]
def test_inbound_status_sync_unresolve(self): responses.add( responses.GET, 'https://instance.visualstudio.com/c0bf429a-c03c-4a99-9336-d45be74db5a6/_apis/wit/workitemtypes/Bug/states', json=WORK_ITEM_STATES, ) work_item_id = 33 num_groups = 5 external_issue = ExternalIssue.objects.create( organization_id=self.organization.id, integration_id=self.model.id, key=work_item_id, ) groups = [ self.create_linked_group( external_issue, self.project, GroupStatus.RESOLVED) for _ in range(num_groups)] # Change so that state is changing from resolved to unresolved work_item = self.set_workitem_state('Resolved', 'Active') with self.feature('organizations:integrations-issue-sync'): resp = self.client.post( absolute_uri('/extensions/vsts/issue-updated/'), data=work_item, HTTP_SHARED_SECRET=self.shared_secret, ) assert resp.status_code == 200 group_ids = [g.id for g in groups] assert len( Group.objects.filter( id__in=group_ids, status=GroupStatus.UNRESOLVED)) == num_groups assert len(Activity.objects.filter(group_id__in=group_ids)) == num_groups
def get_payload_v0(event): from sentry.api.serializers import serialize group = event.group project = group.project project_url_base = absolute_uri(u'/{}/{}'.format( project.organization.slug, project.slug, )) group_context = serialize(group) group_context['url'] = u'{}/issues/{}/'.format( project_url_base, group.id, ) event_context = serialize(event) event_context['url'] = u'{}/issues/{}/events/{}/'.format( project_url_base, group.id, event.id, ) data = { 'project': { 'slug': project.slug, 'name': project.name, }, 'group': group_context, 'event': event_context, } return data
def send_invite_email(self): from sentry.utils.email import MessageBuilder context = { 'email': self.email, 'organization': self.organization, 'url': absolute_uri(reverse('sentry-accept-invite', kwargs={ 'member_id': self.id, 'token': self.token, })), } msg = MessageBuilder( subject='Join %s in using Sentry' % self.organization.name, template='sentry/emails/member-invite.txt', html_template='sentry/emails/member-invite.html', type='organization.invite', context=context, ) try: msg.send_async([self.get_email()]) except Exception as e: logger = get_logger(name='sentry.mail') logger.exception(e)
def get(self, request): return self.respond( { 'key': BITBUCKET_KEY, 'name': 'Sentry for Bitbucket', 'description': 'A Sentry integration', 'vendor': { 'name': 'Sentry.io', 'url': 'https://sentry.io/' }, 'baseUrl': absolute_uri(), 'authentication': { 'type': 'JWT', }, 'lifecycle': { 'installed': '/extensions/bitbucket/installed/', 'uninstalled': '/extensions/bitbucket/uninstalled/' }, 'scopes': scopes, 'contexts': ['account'], # When the user is redirected the URL will become: # https://sentry.io/extensions/bitbucket/setup/?jwt=1212121212 'modules': { 'postInstallRedirect': { 'url': '/extensions/bitbucket/setup/', 'key': 'redirect' } } } )
def send_invite_email(self): from django.core.mail import send_mail from sentry.web.helpers import render_to_string context = { 'email': self.email, 'team': self.team, 'url': absolute_uri( reverse( 'sentry-accept-invite', kwargs={ 'member_id': self.id, 'token': self.token, })), } body = render_to_string('sentry/emails/member_invite.txt', context) try: send_mail( '%sInvite to join team: %s' % (settings.EMAIL_SUBJECT_PREFIX, self.team.name), body, settings.SERVER_EMAIL, [self.email], fail_silently=False) except Exception, e: logger = logging.getLogger('sentry.mail.errors') logger.exception(e)
def serialize(self, obj, attrs, user): from sentry.api.endpoints.project_releases_token import _get_webhook_url doc = '' if self.project is not None: release_token = ProjectOption.objects.get_value(self.project, 'sentry:release-token') if release_token is not None: webhook_url = _get_webhook_url(self.project, obj.slug, release_token) if hasattr(obj, 'get_release_doc_html'): try: doc = obj.get_release_doc_html(webhook_url) except NotImplementedError: pass contexts = [] if hasattr(obj, 'get_custom_contexts'): contexts.extend(x.type for x in obj.get_custom_contexts() or ()) d = { 'id': obj.slug, 'name': six.text_type(obj.get_title()), 'slug': obj.slug or slugify(six.text_type(obj.get_title())), 'shortName': six.text_type(obj.get_short_title()), 'type': obj.get_plugin_type(), 'canDisable': obj.can_disable, 'isTestable': hasattr(obj, 'is_testable') and obj.is_testable(), 'hasConfiguration': obj.has_project_conf(), 'metadata': obj.get_metadata(), 'contexts': contexts, 'status': obj.get_status(), 'assets': [ { 'url': absolute_uri(get_asset_url(obj.asset_key or obj.slug, asset)), } for asset in obj.get_assets() ], 'doc': doc, } if self.project: d['enabled'] = obj.is_enabled(self.project) if obj.version: d['version'] = six.text_type(obj.version) if obj.author: d['author'] = { 'name': six.text_type(obj.author), 'url': six.text_type(obj.author_url) } d['isHidden'] = d.get('enabled', False) is False and obj.is_hidden() if obj.description: d['description'] = six.text_type(obj.description) if obj.resource_links: d['resourceLinks'] = [ {'title': title, 'url': url} for [title, url] in obj.resource_links ] return d
def get(self, request, *args, **kwargs): try: # make sure this exists and is valid jira_auth = self.get_jira_auth() except (ApiError, JiraTenant.DoesNotExist, ExpiredSignatureError): return self.get_response('error.html') if request.user.is_anonymous(): return self.get_response('signin.html') org = jira_auth.organization context = self.get_context() if org is None: context.update( { 'error_message': ( 'You still need to configure this plugin, which ' 'can be done from the Manage Add-ons page.' ) } ) return self.get_response('error.html', context) context.update( { 'sentry_api_url': absolute_uri('/api/0/organizations/%s/users/issues/' % (org.slug, )), 'issue_key': self.request.GET.get('issueKey') } ) return self.get_response('widget.html', context)
def request_access(request): org = Organization( id=1, slug='example', name='Example', ) team = Team( id=1, slug='example', name='Example', organization=org, ) return MailPreview( html_template='sentry/emails/request-team-access.html', text_template='sentry/emails/request-team-access.txt', context={ 'email': '*****@*****.**', 'name': 'George Bush', 'organization': org, 'team': team, 'url': absolute_uri(reverse('sentry-organization-members', kwargs={ 'organization_slug': org.slug, }) + '?ref=access-requests'), }, ).render()
def send_sso_unlink_email(self, actor, provider): from sentry.utils.email import MessageBuilder from sentry.models import LostPasswordHash email = self.get_email() recover_uri = '{path}?{query}'.format( path=reverse('sentry-account-recover'), query=urlencode({'email': email}), ) context = { 'email': email, 'recover_url': absolute_uri(recover_uri), 'has_password': self.user.password, 'organization': self.organization, 'actor': actor, 'provider': provider, } if not self.user.password: password_hash = LostPasswordHash.for_user(self.user) context['set_password_url'] = password_hash.get_absolute_url(mode='set_password') msg = MessageBuilder( subject='Action Required for %s' % (self.organization.name, ), template='sentry/emails/auth-sso-disabled.txt', html_template='sentry/emails/auth-sso-disabled.html', type='organization.auth_sso_disabled', context=context, ) msg.send_async([email])
def exchange_token(self, request, pipeline, code): # TODO: this needs the auth yet data = self.get_token_params( code=code, redirect_uri=absolute_uri(pipeline.redirect_url()), ) verify_ssl = pipeline.config.get('verify_ssl', True) try: req = safe_urlopen(self.access_token_url, data=data, verify_ssl=verify_ssl) body = safe_urlread(req) if req.headers.get('Content-Type', '').startswith('application/x-www-form-urlencoded'): return dict(parse_qsl(body)) return json.loads(body) except SSLError: logger.info('identity.oauth2.ssl-error', extra={ 'url': self.access_token_url, 'verify_ssl': verify_ssl, }) url = self.access_token_url return { 'error': 'Could not verify SSL certificate', 'error_description': u'Ensure that {} has a valid SSL certificate'.format(url) } except JSONDecodeError: logger.info('identity.oauth2.json-error', extra={ 'url': self.access_token_url, }) return { 'error': 'Could not decode a JSON Response', 'error_description': u'We were not able to parse a JSON response, please try again.' }
def send_recover_mail(self): from django.core.mail import send_mail from sentry.web.helpers import render_to_string context = { 'user': self.user, 'domain': urlparse.urlparse(settings.SENTRY_URL_PREFIX).hostname, 'url': absolute_uri( reverse( 'sentry-account-recover-confirm', args=[self.user.id, self.hash])), } body = render_to_string('sentry/emails/recover_account.txt', context) try: send_mail( '%sPassword Recovery' % (settings.EMAIL_SUBJECT_PREFIX, ), body, settings.SERVER_EMAIL, [self.user.email], fail_silently=False) except Exception, e: logger = logging.getLogger('sentry.mail.errors') logger.exception(e)
def send_confirm_email_singular(self, email, is_new_user=False): from sentry import options from sentry.utils.email import MessageBuilder if not email.hash_is_valid(): email.set_hash() email.save() context = { 'user': self, 'url': absolute_uri( reverse('sentry-account-confirm-email', args=[self.id, email.validation_hash]) ), 'confirm_email': email.email, 'is_new_user': is_new_user, } msg = MessageBuilder( subject='%sConfirm Email' % (options.get('mail.subject-prefix'), ), template='sentry/emails/confirm_email.txt', html_template='sentry/emails/confirm_email.html', type='user.confirm_email', context=context, ) msg.send_async([email.email])
def render(self, context): from sentry.utils.http import absolute_uri # Attempt to resolve all arguments into actual variables. # This converts a value such as `"foo"` into the string `foo` # and will look up a value such as `foo` from the context as # the variable `foo`. If the variable does not exist, silently # resolve as an empty string, which matches the behavior # `SimpleTagNode` args = [] for arg in self.args: try: arg = template.Variable(arg).resolve(context) except template.VariableDoesNotExist: arg = '' args.append(arg) # No args is just fine if not args: rv = '' # If there's only 1 argument, there's nothing to format elif len(args) == 1: rv = args[0] else: rv = args[0].format(*args[1:]) rv = absolute_uri(rv) # We're doing an `as foo` and we want to assign the result # to a variable instead of actually returning. if self.target_var is not None: context[self.target_var] = rv rv = '' return rv
def get_description(self): data = self.activity.data if features.has('organizations:sentry10', self.organization): url = u'/organizations/{}/releases/{}/?project={}'.format( self.organization.slug, data['version'], self.project.id, ) else: url = u'/{}/{}/releases/{}/'.format( self.organization.slug, self.project.slug, data['version'], ) if data.get('version'): return u'{author} marked {an issue} as resolved in {version}', { 'version': data['version'], }, { 'version': u'<a href="{}">{}</a>'.format( absolute_uri(url), escape(data['version']), ) } return u'{author} marked {an issue} as resolved in an upcoming release'
def test_inbound_status_sync_new_workitem(self): responses.add( responses.GET, 'https://instance.visualstudio.com/c0bf429a-c03c-4a99-9336-d45be74db5a6/_apis/wit/workitemtypes/Bug/states', json=WORK_ITEM_STATES, ) work_item_id = 33 external_issue = ExternalIssue.objects.create( organization_id=self.organization.id, integration_id=self.model.id, key=work_item_id, ) group = self.create_linked_group( external_issue, self.project, GroupStatus.UNRESOLVED) # Change so that it is a new workitem work_item = self.set_workitem_state(None, 'New') assert 'oldValue' not in work_item['resource']['fields']['System.State'] with self.feature('organizations:integrations-issue-sync'): resp = self.client.post( absolute_uri('/extensions/vsts/issue-updated/'), data=work_item, HTTP_SHARED_SECRET=self.shared_secret, ) assert resp.status_code == 200 assert Group.objects.get(id=group.id).status == GroupStatus.UNRESOLVED # no change happened. no activity should be created here assert len(Activity.objects.filter(group_id=group.id)) == 0
def handle(self, request, organization): team_list = [ t for t in Team.objects.get_for_user( organization=organization, user=request.user, ) if request.access.has_team_scope(t, self.required_scope) ] if not team_list: messages.error(request, ERR_NO_TEAMS) return self.redirect(reverse('sentry-organization-home', args=[organization.slug])) form = self.get_form(request, organization, team_list) if form.is_valid(): project = form.save(request.user, request.META['REMOTE_ADDR']) install_uri = absolute_uri('/{}/{}/settings/install/'.format( organization.slug, project.slug, )) if 'signup' in request.GET: install_uri += '?signup' return self.redirect(install_uri) context = { 'form': form, } return self.respond('sentry/create-project.html', context)
def get_context(self): file_count = CommitFileChange.objects.filter( commit__in=self.commit_list, organization_id=self.organization.id, ).values('filename').distinct().count() return { 'commit_count': len(self.commit_list), 'author_count': len(self.email_list), 'file_count': file_count, 'repos': self.repos, 'release': self.release, 'deploy': self.deploy, 'environment': self.environment, 'setup_repo_link': absolute_uri('/organizations/{}/repos/'.format( self.organization.slug, )), }
def get_absolute_url(self): return absolute_uri(reverse('sentry', args=[self.slug]))
def build_group_footer(group, rules, project, event): # TODO: implement with event as well image_column = { "type": "Column", "items": [{ "type": "Image", "url": absolute_uri( get_asset_url("sentry", "images/sentry-glyph-black.png")), "height": "20px", }], "width": "auto", } text = f"{group.qualified_short_id}" if rules: rule_url = build_rule_url(rules[0], group, project) text += f" via [{rules[0].label}]({rule_url})" if len(rules) > 1: text += f" (+{len(rules) - 1} other)" text_column = { "type": "Column", "items": [{ "type": "TextBlock", "size": "Small", "weight": "Lighter", "text": text }], "isSubtle": True, "width": "auto", "spacing": "none", } date_ts = group.last_seen if event: event_ts = event.datetime date_ts = max(date_ts, event_ts) date = date_ts.replace(microsecond=0).isoformat() date_text = f"{{{{DATE({date}, SHORT)}}}} at {{{{TIME({date})}}}}" date_column = { "type": "Column", "items": [{ "type": "TextBlock", "size": "Small", "weight": "Lighter", "horizontalAlignment": "Center", "text": date_text, }], "width": "auto", } return { "type": "ColumnSet", "columns": [image_column, text_column, date_column] }
def send_welcome_message(self, conversation_id, signed_params): url = u"%s?signed_params=%s" % ( absolute_uri("/extensions/msteams/configure/"), signed_params, ) # TODO: Refactor message creation logo = { "type": "Image", "url": "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png", "size": "Medium", } welcome = { "type": "TextBlock", "weight": "Bolder", "size": "Large", "text": "Welcome to Sentry for Microsoft Teams", "wrap": True, } description = { "type": "TextBlock", "text": "You can use the Sentry app for Microsoft Teams to get notifications that allow you to assign, ignore, or resolve directly in your chat.", "wrap": True, } instruction = { "type": "TextBlock", "text": "If that sounds good to you, finish the setup process.", "wrap": True, } button = { "type": "Action.OpenUrl", "title": "Complete Setup", "url": url, } card = { "type": "AdaptiveCard", "body": [ { "type": "ColumnSet", "columns": [ {"type": "Column", "items": [logo], "width": "auto"}, { "type": "Column", "items": [welcome], "width": "stretch", "verticalContentAlignment": "Center", }, ], }, description, instruction, ], "actions": [button], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.2", } payload = { "type": "message", "attachments": [ {"contentType": "application/vnd.microsoft.card.adaptive", "content": card} ], } self.send_message(conversation_id, payload)
def build_attachment(group, event=None, tags=None, identity=None, actions=None, rules=None): # XXX(dcramer): options are limited to 100 choices, even when nested status = group.get_status() members = get_member_assignees(group) teams = get_team_assignees(group) logo_url = absolute_uri( get_asset_url('sentry', 'images/sentry-email-avatar.png')) color = LEVEL_TO_COLOR.get(event.get_tag('level'), 'error') if event else LEVEL_TO_COLOR['error'] text = build_attachment_text(group, event) or '' if actions is None: actions = [] assignee = get_assignee(group) resolve_button = { 'name': 'resolve_dialog', 'value': 'resolve_dialog', 'type': 'button', 'text': 'Resolve...', } ignore_button = { 'name': 'status', 'value': 'ignored', 'type': 'button', 'text': 'Ignore', } has_releases = Release.objects.filter( projects=group.project, organization_id=group.project.organization_id).exists() if not has_releases: resolve_button.update({ 'name': 'status', 'text': 'Resolve', 'value': 'resolved', }) if status == GroupStatus.RESOLVED: resolve_button.update({ 'name': 'status', 'text': 'Unresolve', 'value': 'unresolved', }) if status == GroupStatus.IGNORED: ignore_button.update({ 'text': 'Stop Ignoring', 'value': 'unresolved', }) option_groups = [] if teams: option_groups.append({ 'text': 'Teams', 'options': teams, }) if members: option_groups.append({ 'text': 'People', 'options': members, }) payload_actions = [ resolve_button, ignore_button, { 'name': 'assign', 'text': 'Select Assignee...', 'type': 'select', 'selected_options': [assignee], 'option_groups': option_groups, }, ] fields = [] if tags: event_tags = event.tags if event else group.get_latest_event().tags for key, value in event_tags: std_key = tagstore.get_standardized_key(key) if std_key not in tags: continue labeled_value = tagstore.get_tag_value_label(key, value) fields.append({ 'title': std_key.encode('utf-8'), 'value': labeled_value.encode('utf-8'), 'short': True, }) if actions: action_texts = filter( None, [build_action_text(group, identity, a) for a in actions]) text += '\n' + '\n'.join(action_texts) color = ACTIONED_ISSUE_COLOR payload_actions = [] ts = group.last_seen if event: event_ts = event.datetime ts = max(ts, event_ts) footer = u'{}'.format(group.qualified_short_id) if rules: footer += u' via {}'.format(rules[0].label) if len(rules) > 1: footer += u' (+{} other)'.format(len(rules) - 1) return { 'fallback': u'[{}] {}'.format(group.project.slug, group.title), 'title': build_attachment_title(group, event), 'title_link': group.get_absolute_url(params={'referrer': 'slack'}), 'text': text, 'fields': fields, 'mrkdwn_in': ['text'], 'callback_id': json.dumps({'issue': group.id}), 'footer_icon': logo_url, 'footer': footer, 'ts': to_timestamp(ts), 'color': color, 'actions': payload_actions, }
def get_absolute_url(self): return absolute_uri( reverse('sentry-group', args=[self.team.slug, self.project.slug, self.id]))
def build_attachment(group, event=None, tags=None, identity=None, actions=None, rules=None): # XXX(dcramer): options are limited to 100 choices, even when nested status = group.get_status() assignees = get_assignees(group) logo_url = absolute_uri( get_asset_url('sentry', 'images/sentry-email-avatar.png')) color = NEW_ISSUE_COLOR text = build_attachment_text(group, event) or '' if actions is None: actions = [] try: assignee = GroupAssignee.objects.get(group=group).user assignee = { 'text': assignee.get_display_name(), 'value': assignee.username, } # Add unassign option to the top of the list assignees.insert(0, UNASSIGN_OPTION) except GroupAssignee.DoesNotExist: assignee = None resolve_button = { 'name': 'resolve_dialog', 'value': 'resolve_dialog', 'type': 'button', 'text': 'Resolve...', } ignore_button = { 'name': 'status', 'value': 'ignored', 'type': 'button', 'text': 'Ignore', } if status == GroupStatus.RESOLVED: resolve_button.update({ 'name': 'status', 'text': 'Unresolve', 'value': 'unresolved', }) if status == GroupStatus.IGNORED: ignore_button.update({ 'text': 'Stop Ignoring', 'value': 'unresolved', }) payload_actions = [ resolve_button, ignore_button, { 'name': 'assign', 'text': 'Select Assignee...', 'type': 'select', 'options': assignees, 'selected_options': [assignee], }, ] fields = [] if tags: event_tags = event.tags if event else group.get_latest_event().tags for tag_key, tag_value in event_tags: if tag_key in tags: fields.append({ 'title': tag_key.encode('utf-8'), 'value': tag_value.encode('utf-8'), 'short': True, }) if actions: action_texts = filter( None, [build_action_text(identity, a) for a in actions]) text += '\n' + '\n'.join(action_texts) color = ACTIONED_ISSUE_COLOR payload_actions = [] ts = group.last_seen if event: event_ts = event.datetime ts = max(ts, event_ts) footer = u'{}'.format(group.qualified_short_id) if rules: footer += u' via {}'.format(rules[0].label) if len(rules) > 1: footer += u' (+{} other)'.format(len(rules) - 1) return { 'fallback': u'[{}] {}'.format(group.project.slug, group.title), 'title': build_attachment_title(group, event), 'title_link': add_notification_referrer_param(group.get_absolute_url(), 'slack'), 'text': text, 'fields': fields, 'mrkdwn_in': ['text'], 'callback_id': json.dumps({'issue': group.id}), 'footer_icon': logo_url, 'footer': footer, 'ts': to_timestamp(ts), 'color': color, 'actions': payload_actions, }
def post(self, request): if not request.META.get("HTTP_X_ZEIT_SIGNATURE"): logger.error("vercel.webhook.missing-signature") self.respond(status=401) is_valid = verify_signature(request) if not is_valid: logger.error("vercel.webhook.invalid-signature") return self.respond(status=401) data = request.data payload = data["payload"] external_id = data.get("teamId") or data["userId"] vercel_project_id = payload["projectId"] logging_params = { "external_id": external_id, "vercel_project_id": vercel_project_id } if payload["target"] != "production": logger.info("Ignoring deployment for environment: %s" % payload["target"], extra=logging_params) return self.respond(status=204) # Steps: # 1. Find all org integrations that match the external id # 2. Search the configs to find one that matches the vercel project of the webhook # 3. Look up the Sentry project that matches # 4. Look up the connected internal integration # 5. Find the token associated with that installation # 6. Determine the commit sha and repo based on what provider is used # 7. Create the release using the token WITHOUT refs # 8. Update the release with refs # find all org integrations that match the external id org_integrations = OrganizationIntegration.objects.select_related( "organization").filter(integration__external_id=external_id, integration__provider=self.provider) if not org_integrations: logger.info("Integration not found", extra=logging_params) return self.respond({"detail": "Integration not found"}, status=404) # for each org integration, search the configs to find one that matches the vercel project of the webhook for org_integration in org_integrations: project_mappings = org_integration.config.get( "project_mappings") or [] matched_mappings = filter(lambda x: x[1] == vercel_project_id, project_mappings) if matched_mappings: organization = org_integration.organization sentry_project_id = matched_mappings[0][0] logging_params["organization_id"] = organization.id logging_params["project_id"] = sentry_project_id try: [release_payload, token ] = self.get_payload_and_token(payload, organization.id, sentry_project_id) except Project.DoesNotExist: logger.info("Project not found", extra=logging_params) return self.respond({"detail": "Project not found"}, status=404) except SentryAppInstallationForProvider.DoesNotExist: logger.info("Installation not found", extra=logging_params) return self.respond({"detail": "Installation not found"}, status=404) except SentryAppInstallationToken.DoesNotExist: logger.info("Token not found", extra=logging_params) return self.respond({"detail": "Token not found"}, status=404) except NoCommitFoundError: logger.info("No commit found", extra=logging_params) return self.respond({"detail": "No commit found"}, status=404) session = http.build_session() url = absolute_uri("/api/0/organizations/%s/releases/" % organization.slug) headers = { "Accept": "application/json", "Authorization": "Bearer %s" % token, "User-Agent": "sentry_vercel/{}".format(VERSION), } json_error = None # create the basic release payload without refs no_ref_payload = release_payload.copy() del no_ref_payload["refs"] try: resp = session.post(url, json=no_ref_payload, headers=headers) json_error = safe_json_parse(resp) resp.raise_for_status() except RequestException as e: # errors here should be uncommon but we should be aware of them logger.error( "Error creating release: %s - %s" % (e, json_error), extra=logging_params, exc_info=True, ) # 400 probably isn't the right status code but oh well return self.respond( {"detail": "Error creating release: %s" % e}, status=400) # set the refs try: resp = session.post( url, json=release_payload, headers=headers, ) json_error = safe_json_parse(resp) resp.raise_for_status() except RequestException as e: # errors will probably be common if the user doesn't have repos set up logger.info( "Error setting refs: %s - %s" % (e, json_error), extra=logging_params, exc_info=True, ) # 400 probably isn't the right status code but oh well return self.respond( {"detail": "Error setting refs: %s" % e}, status=400) # we are going to quit after the first project match as there shouldn't be multiple matches return self.respond(status=201) return self.respond(status=204)
def handle(self, request, organization, team, project): token = None if request.method == 'POST': op = request.POST.get('op') if op == 'regenerate-token': token = self._regenerate_token(project) messages.add_message( request, messages.SUCCESS, OK_TOKEN_REGENERATED, ) elif op == 'enable': self._handle_enable_plugin(request, project) elif op == 'disable': self._handle_disable_plugin(request, project) return HttpResponseRedirect(request.path) if token is None: token = ProjectOption.objects.get_value(project, 'sentry:release-token') if token is None: token = self._regenerate_token(project) enabled_plugins = [] other_plugins = [] for plugin in self._iter_plugins(): if plugin.is_enabled(project): hook_url = absolute_uri( reverse('sentry-release-hook', kwargs={ 'plugin_id': plugin.slug, 'project_id': project.id, 'signature': self._get_signature(project.id, plugin.slug, token), })) content = plugin.get_release_doc_html(hook_url=hook_url) enabled_plugins.append((plugin, mark_safe(content))) elif plugin.can_configure_for_project(project): other_plugins.append(plugin) context = { 'page': 'release-tracking', 'token': token, 'enabled_plugins': enabled_plugins, 'other_plugins': other_plugins, 'webhook_url': absolute_uri( reverse('sentry-release-hook', kwargs={ 'plugin_id': 'builtin', 'project_id': project.id, 'signature': self._get_signature(project.id, 'builtin', token), })) } return self.respond('sentry/project-release-tracking.html', context)
def notify(self, notification, raise_exception=False): event = notification.event group = event.group project = group.project if not self.is_configured(project): return 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 = ", ".join("<{} | {}>".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": b"[%s] %s" % (project_name, title), "title": title, "title_link": group.get_absolute_url(params={"referrer": "slack"}), "color": self.color_for_event(event), "fields": fields, }] } client = self.get_client(project) if client.username: payload["username"] = client.username.encode("utf-8") if client.channel: payload["channel"] = client.channel if client.icon_url: payload["icon_url"] = client.icon_url values = {"payload": json.dumps(payload)} client.request(values)
def get_absolute_url(self): return absolute_uri(reverse('sentry-alert-details', args=[ self.team.slug, self.project.slug, self.id]))
def get(self, request): sentry_logo = absolute_uri( get_asset_url("sentry", "images/logos/logo-sentry.svg")) return self.respond({ "name": "Sentry", "description": "Sentry", "key": JIRA_KEY, "baseUrl": absolute_uri(), "vendor": { "name": "Sentry", "url": "https://sentry.io" }, "authentication": { "type": "jwt" }, "lifecycle": { "installed": "/extensions/jira/installed/", "uninstalled": "/extensions/jira/uninstalled/", }, "apiVersion": 1, "modules": { "postInstallPage": { "url": "/extensions/jira/ui-hook", "name": { "value": "Configure Sentry Add-on" }, "key": "post-install-sentry", }, "configurePage": { "url": "/extensions/jira/ui-hook", "name": { "value": "Configure Sentry Add-on" }, "key": "configure-sentry", }, "jiraIssueGlances": [{ "icon": { "width": 24, "height": 24, "url": sentry_logo }, "content": { "type": "label", "label": { "value": "Linked Issues" } }, "target": { "type": "web_panel", "url": "/extensions/jira/issue/{issue.key}/", }, "name": { "value": "Sentry " }, "key": "sentry-issues-glance", }], "webhooks": [{ "event": "jira:issue_updated", "url": reverse("sentry-extensions-jira-issue-updated"), "excludeBody": False, }], }, "apiMigrations": { "gdpr": True }, "scopes": scopes, })
def get_settings_url(notification: BaseNotification) -> str: return urljoin(absolute_uri("/settings/account/notifications/"), get_referrer_qstring(notification))
def build_rule_url(rule, group, project): org_slug = group.organization.slug project_slug = project.slug rule_url = f"/organizations/{org_slug}/alerts/rules/{project_slug}/{rule.id}/" return absolute_uri(rule_url)
def get_absolute_url(self): return absolute_uri( reverse('sentry-stream', args=[self.team.slug, self.slug]))
def u2f_app_id(cls): rv = options.get('u2f.app-id') return rv or absolute_uri(reverse('sentry-u2f-app-id'))
def get_absolute_url(self): return absolute_uri('/{}/{}/'.format(self.organization.slug, self.slug))
def build_welcome_card(signed_params): url = "{}?signed_params={}".format( absolute_uri("/extensions/msteams/configure/"), signed_params, ) welcome = { "type": "TextBlock", "weight": "Bolder", "size": "Large", "text": "Welcome to Sentry for Microsoft Teams", "wrap": True, } description = { "type": "TextBlock", "text": "You can use Sentry for Microsoft Teams to get notifications that allow you to assign, ignore, or resolve directly in your chat.", "wrap": True, } instruction = { "type": "TextBlock", "text": ("Please click **Complete Setup** to finish the setup process." " Don't have a Sentry account? [Sign Up](https://sentry.io/signup/)." ), "wrap": True, } button = { "type": "Action.OpenUrl", "title": "Complete Setup", "url": url, } return { "type": "AdaptiveCard", "body": [ { "type": "ColumnSet", "columns": [ { "type": "Column", "items": [logo], "width": "auto" }, { "type": "Column", "items": [welcome], "width": "stretch", "verticalContentAlignment": "Center", }, ], }, description, instruction, ], "actions": [button], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.2", }
def serialize(self, obj, attrs, user): status = obj.status status_details = {} if attrs['ignore_until']: snooze = attrs['ignore_until'] if snooze.is_valid(group=obj): # counts return the delta remaining when window is not set status_details.update({ 'ignoreCount': (snooze.count - (obj.times_seen - snooze.state['times_seen']) if snooze.count and not snooze.window else snooze.count), 'ignoreUntil': snooze.until, 'ignoreUserCount': (snooze.user_count - (attrs['user_count'] - snooze.state['users_seen']) if snooze.user_count and not snooze.user_window else snooze.user_count), 'ignoreUserWindow': snooze.user_window, 'ignoreWindow': snooze.window, 'actor': attrs['ignore_actor'], }) else: status = GroupStatus.UNRESOLVED if status == GroupStatus.UNRESOLVED and obj.is_over_resolve_age(): status = GroupStatus.RESOLVED status_details['autoResolved'] = True if status == GroupStatus.RESOLVED: status_label = 'resolved' if attrs['resolution_type'] == 'release': res_type, res_version, _ = attrs['resolution'] if res_type in (GroupResolution.Type.in_next_release, None): status_details['inNextRelease'] = True elif res_type == GroupResolution.Type.in_release: status_details['inRelease'] = res_version status_details['actor'] = attrs['resolution_actor'] elif attrs['resolution_type'] == 'commit': status_details['inCommit'] = attrs['resolution'] elif status == GroupStatus.IGNORED: status_label = 'ignored' elif status in [ GroupStatus.PENDING_DELETION, GroupStatus.DELETION_IN_PROGRESS ]: status_label = 'pending_deletion' elif status == GroupStatus.PENDING_MERGE: status_label = 'pending_merge' else: status_label = 'unresolved' # If user is not logged in and member of the organization, # do not return the permalink which contains private information i.e. org name. if user.is_authenticated() and user.get_orgs().filter( id=obj.organization.id).exists(): permalink = absolute_uri( reverse('sentry-group', args=[obj.organization.slug, obj.project.slug, obj.id])) else: permalink = None subscription_details = None if attrs['subscription'] is not disabled: is_subscribed, subscription = attrs['subscription'] if subscription is not None and subscription.is_active: subscription_details = { 'reason': SUBSCRIPTION_REASON_MAP.get( subscription.reason, 'unknown', ), } else: is_subscribed = False subscription_details = { 'disabled': True, } share_id = attrs['share_id'] return { 'id': six.text_type(obj.id), 'shareId': share_id, 'shortId': obj.qualified_short_id, 'count': six.text_type(attrs['times_seen']), 'userCount': attrs['user_count'], 'title': obj.title, 'culprit': obj.culprit, 'permalink': permalink, 'firstSeen': attrs['first_seen'], 'lastSeen': attrs['last_seen'], 'logger': obj.logger or None, 'level': LOG_LEVELS.get(obj.level, 'unknown'), 'status': status_label, 'statusDetails': status_details, 'isPublic': share_id is not None, 'project': { 'id': six.text_type(obj.project.id), 'name': obj.project.name, 'slug': obj.project.slug, }, 'type': obj.get_event_type(), 'metadata': obj.get_event_metadata(), 'numComments': obj.num_comments, 'assignedTo': serialize(attrs['assigned_to'], user, ActorSerializer()), 'isBookmarked': attrs['is_bookmarked'], 'isSubscribed': is_subscribed, 'subscriptionDetails': subscription_details, 'hasSeen': attrs['has_seen'], 'annotations': attrs['annotations'], }
from typing import Optional from sentry.integrations.metric_alerts import incident_attachment_info from sentry.models import Group, GroupStatus, Project, Team, User from sentry.utils.assets import get_asset_url from sentry.utils.compat import map from sentry.utils.http import absolute_uri from .utils import ACTION_TYPE ME = "ME" logo = { "type": "Image", "url": absolute_uri(get_asset_url("sentry", "images/sentry-glyph-black.png")), "size": "Medium", } def generate_action_payload(action_type, event, rules, integration): rule_ids = map(lambda x: x.id, rules) # we need nested data or else Teams won't handle the payload correctly return { "payload": { "actionType": action_type, "groupId": event.group.id, "eventId": event.event_id, "rules": rule_ids, "integrationId": integration.id, } }
def test_without_path(self): assert absolute_uri() == options.get('system.url-prefix')
def get_settings_url(notification: BaseNotification) -> str: url_str = f"/settings/account/notifications/{notification.fine_tuning_key}" return str( urljoin(absolute_uri(url_str), get_referrer_qstring(notification)))
def get(self, request: Request) -> Response: sentry_logo = absolute_uri( get_asset_url("sentry", "images/logos/logo-sentry.svg")) return self.respond({ "name": "Sentry", "description": "Connect your Sentry organization into one or more of your Jira cloud instances. Get started streamlining your bug squashing workflow by unifying your Sentry and Jira instances together.", "key": JIRA_KEY, "baseUrl": absolute_uri(), "vendor": { "name": "Sentry", "url": "https://sentry.io" }, "authentication": { "type": "jwt" }, "lifecycle": { "installed": "/extensions/jira/installed/", "uninstalled": "/extensions/jira/uninstalled/", }, "apiVersion": 1, "modules": { "postInstallPage": { "url": "/extensions/jira/ui-hook", "name": { "value": "Configure Sentry Add-on" }, "key": "post-install-sentry", }, "configurePage": { "url": "/extensions/jira/ui-hook", "name": { "value": "Configure Sentry Add-on" }, "key": "configure-sentry", }, "jiraIssueGlances": [{ "icon": { "width": 24, "height": 24, "url": sentry_logo }, "content": { "type": "label", "label": { "value": "Linked Issues" } }, "target": { "type": "web_panel", "url": "/extensions/jira/issue/{issue.key}/", }, "name": { "value": "Sentry " }, "key": "sentry-issues-glance", }], "webhooks": [{ "event": "jira:issue_updated", "url": reverse("sentry-extensions-jira-issue-updated"), "excludeBody": False, }], }, "apiMigrations": { "gdpr": True, "context-qsh": True, "signed-install": True }, "scopes": scopes, })
def test_with_path(self): assert absolute_uri( '/foo/bar') == '%s/foo/bar' % (options.get('system.url-prefix'), )
def get_project_url(self, project): return absolute_uri(reverse('sentry-stream', args=[ project.organization.slug, project.slug, ]))
def notify_about_activity(self, activity): if activity.type not in (Activity.NOTE, Activity.ASSIGNED, Activity.RELEASE): return candidate_ids = set(self.get_send_to(activity.project)) # Never send a notification to the user that performed the action. candidate_ids.discard(activity.user_id) if activity.type == Activity.ASSIGNED: # Only notify the assignee, and only if they are in the candidate set. recipient_ids = candidate_ids & set(map(int, (activity.data['assignee'],))) elif activity.type == Activity.NOTE: recipient_ids = candidate_ids - set( UserOption.objects.filter( user__in=candidate_ids, key='subscribe_notes', value=u'0', ).values_list('user', flat=True) ) else: recipient_ids = candidate_ids if not recipient_ids: return project = activity.project org = project.organization group = activity.group headers = {} context = { 'data': activity.data, 'author': activity.user, 'project': project, 'project_link': absolute_uri(reverse('sentry-stream', kwargs={ 'organization_slug': org.slug, 'project_id': project.slug, })), } if group: group_link = absolute_uri('/{}/{}/issues/{}/'.format( org.slug, project.slug, group.id )) activity_link = '{}activity/'.format(group_link) headers.update({ 'X-Sentry-Reply-To': group_id_to_email(group.id), }) context.update({ 'group': group, 'link': group_link, 'activity_link': activity_link, }) # TODO(dcramer): abstract each activity email into its own helper class if activity.type == Activity.RELEASE: context.update({ 'release': Release.objects.get( version=activity.data['version'], project=project, ), 'release_link': absolute_uri('/{}/{}/releases/{}/'.format( org.slug, project.slug, activity.data['version'], )), }) template_name = activity.get_type_display() # TODO: Everything below should instead use `_send_mail` for consistency. subject_prefix = project.get_option('subject_prefix', settings.EMAIL_SUBJECT_PREFIX) if subject_prefix: subject_prefix = subject_prefix.rstrip() + ' ' if group: subject = '%s%s' % (subject_prefix, group.get_email_subject()) elif activity.type == Activity.RELEASE: subject = '%sRelease %s' % (subject_prefix, activity.data['version']) else: raise NotImplementedError msg = MessageBuilder( subject=subject, context=context, template='sentry/emails/activity/{}.txt'.format(template_name), html_template='sentry/emails/activity/{}.html'.format(template_name), headers=headers, reference=activity, reply_reference=group, ) msg.add_users(recipient_ids, project=project) msg.send()
def test_simple_get_create(self): self.login_as(user=self.user) org = self.organization group = self.create_group() self.create_event(group=group) integration = Integration.objects.create( provider='example', name='Example', ) integration.add_organization(org.id) path = '/api/0/issues/{}/integrations/{}/?action=create'.format( group.id, integration.id) response = self.client.get(path) provider = integration.get_provider() assert response.data == { 'id': six.text_type(integration.id), 'name': integration.name, 'icon': integration.metadata.get('icon'), 'domainName': integration.metadata.get('domain_name'), 'accountType': integration.metadata.get('account_type'), 'status': integration.get_status_display(), 'provider': { 'key': provider.key, 'name': provider.name, 'canAdd': provider.can_add, 'canDisable': provider.can_disable, 'features': [f.value for f in provider.features], 'aspects': provider.metadata.aspects, }, 'createIssueConfig': [{ 'default': 'message', 'type': 'string', 'name': 'title', 'label': 'Title', 'required': True, }, { 'default': ('Sentry Issue: [%s](%s)\n\n```\n' 'Stacktrace (most recent call last):\n\n ' 'File "sentry/models/foo.py", line 29, in build_msg\n ' 'string_max_length=self.string_max_length)\n\nmessage\n```') % (group.qualified_short_id, absolute_uri(group.get_absolute_url())), 'type': 'textarea', 'name': 'description', 'label': 'Description', 'autosize': True, 'maxRows': 10, }] }
def get_project_url(self, project): return absolute_uri('/{}/{}/'.format(project.organization.slug, project.slug))
def get_notification_settings_url(self): return absolute_uri(reverse('sentry-account-settings-notifications'))
def transform(self, obj, request=None): status = obj.get_status() if status == GroupStatus.RESOLVED: status_label = 'resolved' elif status == GroupStatus.IGNORED: status_label = 'ignored' else: status_label = 'unresolved' version = obj.last_seen if obj.resolved_at: version = max(obj.resolved_at, obj.last_seen) version = int(version.strftime('%s')) d = { 'id': six.text_type(obj.id), 'count': six.text_type(obj.times_seen), 'title': escape(obj.title), 'message': escape(obj.get_legacy_message()), 'level': obj.level, 'levelName': escape(obj.get_level_display()), 'logger': escape(obj.logger), 'permalink': absolute_uri( reverse('sentry-group', args=[obj.organization.slug, obj.project.slug, obj.id])), 'firstSeen': self.localize_datetime(obj.first_seen, request=request), 'lastSeen': self.localize_datetime(obj.last_seen, request=request), 'canResolve': request and request.user.is_authenticated(), 'status': status_label, 'isResolved': obj.get_status() == GroupStatus.RESOLVED, 'isPublic': obj.is_public, 'score': getattr(obj, 'sort_value', 0), 'project': { 'name': escape(obj.project.name), 'slug': obj.project.slug, }, 'version': version, } if hasattr(obj, 'is_bookmarked'): d['isBookmarked'] = obj.is_bookmarked if hasattr(obj, 'has_seen'): d['hasSeen'] = obj.has_seen if hasattr(obj, 'historical_data'): d['historicalData'] = obj.historical_data if hasattr(obj, 'annotations'): d['annotations'] = obj.annotations # TODO(dcramer): these aren't tags, and annotations aren't annotations if request: d['tags'] = get_legacy_annotations(obj, request) return d
def generate_footer(self, rule_url): return "\nThis work item was automatically created by Sentry via [{}]({})".format( self.rule.label, absolute_uri(rule_url), )