diff --git a/CHANGES b/CHANGES index 89782b39119607..6ff7b4abb3a8c6 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,35 @@ +Version 9.1.2 +------------- + +- de95702691 Matt Robenolt Wed Apr 10 13:35:52 2019 -0700 build: Switch to psycopg2-binary (#12717) +- fff834413f Sasha Case Tue Jul 16 21:36:47 2019 +1000 fix: Docs not built in fresh bdist_wheel (#12220) +- d44a6d7860 Jan Michael Auer Wed May 22 12:17:18 2019 +0200 fix: Renormalize snuba events (#13329) +- 08ac2c6efc Markus Unterwaditzer Tue Apr 30 15:00:36 2019 +0200 fix: Renormalize once (#12991) +- 0a9d43d50b Jan Michael Auer Thu Apr 25 01:02:10 2019 +0200 fix(assemble): Update outdated test, change status (#12928) +- ca155eadb6 Jan Michael Auer Thu Apr 25 00:14:18 2019 +0200 fix(assemble): Avoid increasing backlogs in assemble (#12923) +- 896d4c33e7 Jan Michael Auer Wed Apr 17 14:15:30 2019 +0200 fix(unreal): Extract better portable callstacks (#12812) +- daa5ccbd94 Markus Unterwaditzer Thu Apr 4 10:06:46 2019 +0200 fix: Fix a bunch of null references (#12637) +- ba0d389bac ganzevoort Thu Jul 11 16:40:32 2019 +0200 fix issue 13968: silent cleanup honors is_filtered (#13969) +- b5b4b9089a Burak Yigit Kaya Thu Jun 20 19:01:32 2019 +0300 fix(packaging): Include all of `src/sentry` (#13743) +- 00dc2d34e9 Burak Yigit Kaya Wed Jun 19 22:48:54 2019 +0300 test(installWizard): Remove obsolete test case and rename the remaining (#13736) +- 7822ea389d Sam Hausmann Wed Jun 19 13:55:34 2019 -0400 ref(slack): Remove message title limit (#13687) +- c762e3a60d Burak Yigit Kaya Wed Jun 19 00:22:12 2019 +0300 fix(installWizard): Fix client default values not set (#13726) +- ceffe91379 Burak Yigit Kaya Mon Jun 17 16:51:17 2019 +0300 feat(upgrade): Default to superuser for first user (#13706) +- c8d139d18c Mark Story Mon Jun 3 15:55:48 2019 -0400 fix(api) Update serializer max length to match database (#13499) +- ae5eb0344b Evan Purkhiser Tue May 21 11:42:08 2019 -0700 fix(slack): use oauth.token over oauth.access for legacy WST app +- e9570f068e Evan Purkhiser Mon May 20 15:13:02 2019 -0700 ref(slack): Implement bot app switch (#13184) +- 3cd4d5ca03 Roach Mon May 13 10:37:58 2019 -0700 Updated Slack integration logo (#13174) +- 3626966f5f Mark Story Mon Apr 29 10:08:40 2019 -0400 chore: Bump moment-timezone (#12966) +- b604a2deb1 Mark Story Wed Apr 24 09:41:21 2019 -0400 fix(ui) Fix zindex woes in hovercards. (#12887) +- 0f6675a46c Mark Story Tue Apr 23 18:10:38 2019 -0400 fix(jira) Don't 500 when request token requests timeout (#12884) +- a9cb023208 Dan Fuller Tue Apr 23 12:57:55 2019 -0700 fix(api): Fix bug where serialization fails for a pull request that is related to a removed repo (SEN-516) +- 6146169ba4 Mark Story Tue Apr 23 16:00:27 2019 -0400 fix(jira) Don't 500 when oauth tokens are revoked (#12886) +- 15bf6bd777 Evan Purkhiser Tue Apr 23 09:48:11 2019 -0700 fix(slack): Use event message /type when available (#12881) +- fcaadcbc86 Billy Vong Tue Apr 23 08:57:44 2019 -0700 fix(sidebar): Fix using wrong pathname (#12870) +- 46bbe21130 Billy Vong Tue Apr 23 08:57:21 2019 -0700 fix(js): Fix cleanup API client (#12872) +- 0c8553133b Lauryn Brown Mon Apr 15 12:26:36 2019 -0700 ref(releases): Refactored Releases Serializers (#12535) +- db41bf89b0 Dena Mwangi Fri Apr 5 10:41:48 2019 -0700 fix field name to match serializer (#12532) + Version 9.1.1 ------------- diff --git a/MANIFEST.in b/MANIFEST.in index b3f28ccf01bcf0..dd96a0ff8a735b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,4 @@ -include setup.py src/sentry/assets.json README.rst MANIFEST.in LICENSE AUTHORS -include src/sentry/loader/_registry.json +include setup.py README.rst MANIFEST.in LICENSE AUTHORS recursive-include ./ requirements*.txt -recursive-include src/sentry/templates * -recursive-include src/sentry/locale * -recursive-include src/sentry/data * -recursive-include src/sentry/static/sentry * -recursive-include src/sentry/scripts *.lua -recursive-include src/sentry/integration-docs *.json +graft src/sentry global-exclude *~ diff --git a/package.json b/package.json index bdbd5265046f20..35bbc83370cf95 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "mobx": "^3.2.2", "mobx-react": "^4.2.2", "moment": "2.23.0", - "moment-timezone": "0.5.23", + "moment-timezone": "0.5.25", "node-libs-browser": "0.5.3", "optimize-css-assets-webpack-plugin": "^5.0.1", "papaparse": "^4.6.0", diff --git a/requirements-base.txt b/requirements-base.txt index b84c8418be6be0..cd8be01a92a84e 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -38,7 +38,7 @@ percy>=1.1.2 petname>=2.0,<2.1 Pillow>=3.2.0,<=4.2.1 progressbar2>=3.10,<3.11 -psycopg2>=2.6.0,<2.8.0 +psycopg2-binary>=2.6.0,<2.8.0 PyJWT>=1.5.0,<1.6.0 pytest-django>=2.9.1,<2.10.0 pytest-html>=1.9.0,<1.10.0 diff --git a/setup.py b/setup.py index 927ae4f1cbb448..987481b8582423 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ ) # The version of sentry -VERSION = '9.1.1' +VERSION = '9.1.2' # Hack to prevent stupid "TypeError: 'NoneType' object is not callable" error # in multiprocessing/util.py _exit_function when running `python @@ -97,20 +97,20 @@ class SentrySDistCommand(SDistCommand): class SentryBuildCommand(BuildCommand): def run(self): - BuildCommand.run(self) if not IS_LIGHT_BUILD: self.run_command('build_integration_docs') self.run_command('build_assets') self.run_command('build_js_sdk_registry') + BuildCommand.run(self) class SentryDevelopCommand(DevelopCommand): def run(self): - DevelopCommand.run(self) if not IS_LIGHT_BUILD: self.run_command('build_integration_docs') self.run_command('build_assets') self.run_command('build_js_sdk_registry') + DevelopCommand.run(self) cmdclass = { diff --git a/src/sentry/api/endpoints/broadcast_index.py b/src/sentry/api/endpoints/broadcast_index.py index 5235331228730c..44317831682c54 100644 --- a/src/sentry/api/endpoints/broadcast_index.py +++ b/src/sentry/api/endpoints/broadcast_index.py @@ -152,7 +152,7 @@ def post(self, request): message=result['message'], link=result['link'], is_active=result.get('isActive') or False, - date_expires=result.get('expiresAt'), + date_expires=result.get('dateExpires'), ) logger.info('broadcasts.create', extra={ 'ip_address': request.META['REMOTE_ADDR'], diff --git a/src/sentry/api/endpoints/debug_files.py b/src/sentry/api/endpoints/debug_files.py index 9a97d90111fbec..52a3fd208f93f1 100644 --- a/src/sentry/api/endpoints/debug_files.py +++ b/src/sentry/api/endpoints/debug_files.py @@ -337,7 +337,7 @@ def post(self, request, project): # We don't have a state yet, this means we can now start # an assemble job in the background. - set_assemble_status(project, checksum, state) + set_assemble_status(project, checksum, ChunkFileState.CREATED) assemble_dif.apply_async( kwargs={ 'project_id': project.id, diff --git a/src/sentry/api/endpoints/organization_release_details.py b/src/sentry/api/endpoints/organization_release_details.py index ab41522b04fa61..06253a7cdd7a29 100644 --- a/src/sentry/api/endpoints/organization_release_details.py +++ b/src/sentry/api/endpoints/organization_release_details.py @@ -1,21 +1,14 @@ from __future__ import absolute_import -from rest_framework import serializers from rest_framework.response import Response from sentry.api.base import DocSection from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import InvalidRepository, ResourceDoesNotExist from sentry.api.serializers import serialize -from sentry.api.serializers.rest_framework import ( - CommitSerializer, - ListField, - ReleaseHeadCommitSerializerDeprecated, - ReleaseHeadCommitSerializer, -) +from sentry.api.serializers.rest_framework import ListField, ReleaseSerializer, ReleaseHeadCommitSerializer, ReleaseHeadCommitSerializerDeprecated from sentry.models import Activity, Group, Release, ReleaseFile from sentry.utils.apidocs import scenario, attach_scenarios -from sentry.constants import VERSION_LENGTH ERR_RELEASE_REFERENCED = "This release is referenced by active issues and cannot be removed." @@ -41,13 +34,11 @@ def update_organization_release_scenario(runner): ) -class ReleaseSerializer(serializers.Serializer): - ref = serializers.CharField(max_length=VERSION_LENGTH, required=False) - url = serializers.URLField(required=False) - dateReleased = serializers.DateTimeField(required=False) - commits = ListField(child=CommitSerializer(), required=False, allow_null=False) +class OrganizationReleaseSerializer(ReleaseSerializer): headCommits = ListField( - child=ReleaseHeadCommitSerializerDeprecated(), required=False, allow_null=False + child=ReleaseHeadCommitSerializerDeprecated(), + required=False, + allow_null=False ) refs = ListField( child=ReleaseHeadCommitSerializer(), @@ -129,7 +120,7 @@ def put(self, request, organization, version): if not self.has_release_permission(request, organization, release): raise ResourceDoesNotExist - serializer = ReleaseSerializer(data=request.DATA) + serializer = OrganizationReleaseSerializer(data=request.DATA) if not serializer.is_valid(): return Response(serializer.errors, status=400) diff --git a/src/sentry/api/endpoints/organization_releases.py b/src/sentry/api/endpoints/organization_releases.py index c1adc1bf0e13dd..0f7915ea19282e 100644 --- a/src/sentry/api/endpoints/organization_releases.py +++ b/src/sentry/api/endpoints/organization_releases.py @@ -5,14 +5,13 @@ from rest_framework.response import Response from sentry.api.bases import NoProjects, OrganizationEventsError -from .project_releases import ReleaseSerializer from sentry.api.base import DocSection, EnvironmentMixin from sentry.api.bases.organization import OrganizationReleasesBaseEndpoint from sentry.api.exceptions import InvalidRepository from sentry.api.paginator import OffsetPaginator from sentry.api.serializers import serialize from sentry.api.serializers.rest_framework import ( - ReleaseHeadCommitSerializer, ReleaseHeadCommitSerializerDeprecated, ListField + ReleaseHeadCommitSerializer, ReleaseHeadCommitSerializerDeprecated, ReleaseWithVersionSerializer, ListField ) from sentry.models import Activity, Release from sentry.signals import release_created @@ -64,12 +63,12 @@ def list_org_releases_scenario(runner): runner.request(method='GET', path='/organizations/%s/releases/' % (runner.org.slug, )) -class ReleaseSerializerWithProjects(ReleaseSerializer): +class ReleaseSerializerWithProjects(ReleaseWithVersionSerializer): projects = ListField() headCommits = ListField( child=ReleaseHeadCommitSerializerDeprecated(), required=False, - allow_null=False, + allow_null=False ) refs = ListField( child=ReleaseHeadCommitSerializer(), diff --git a/src/sentry/api/endpoints/project_release_details.py b/src/sentry/api/endpoints/project_release_details.py index 721bce10a8ebfd..8055a088bf593e 100644 --- a/src/sentry/api/endpoints/project_release_details.py +++ b/src/sentry/api/endpoints/project_release_details.py @@ -1,24 +1,17 @@ from __future__ import absolute_import -from rest_framework import serializers from rest_framework.response import Response from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize -from sentry.api.serializers.rest_framework import CommitSerializer, ListField +from sentry.api.serializers.rest_framework import ReleaseSerializer + from sentry.models import Activity, Group, Release, ReleaseFile from sentry.plugins.interfaces.releasehook import ReleaseHook -from sentry.constants import VERSION_LENGTH - -ERR_RELEASE_REFERENCED = "This release is referenced by active issues and cannot be removed." -class ReleaseSerializer(serializers.Serializer): - ref = serializers.CharField(max_length=VERSION_LENGTH, required=False) - url = serializers.URLField(required=False) - dateReleased = serializers.DateTimeField(required=False) - commits = ListField(child=CommitSerializer(), required=False, allow_null=False) +ERR_RELEASE_REFERENCED = "This release is referenced by active issues and cannot be removed." class ProjectReleaseDetailsEndpoint(ProjectEndpoint): diff --git a/src/sentry/api/endpoints/project_releases.py b/src/sentry/api/endpoints/project_releases.py index 107c660a71be21..74545319efb668 100644 --- a/src/sentry/api/endpoints/project_releases.py +++ b/src/sentry/api/endpoints/project_releases.py @@ -2,64 +2,22 @@ from django.db import IntegrityError, transaction -from rest_framework import serializers from rest_framework.response import Response from sentry.api.base import EnvironmentMixin from sentry.api.bases.project import ProjectEndpoint, ProjectReleasePermission from sentry.api.paginator import OffsetPaginator -from sentry.api.fields.user import UserField from sentry.api.serializers import serialize -from sentry.api.serializers.rest_framework import CommitSerializer, ListField +from sentry.api.serializers.rest_framework import ReleaseWithVersionSerializer from sentry.models import ( Activity, - CommitFileChange, Environment, Release, ) from sentry.plugins.interfaces.releasehook import ReleaseHook -from sentry.constants import VERSION_LENGTH from sentry.signals import release_created -class CommitPatchSetSerializer(serializers.Serializer): - path = serializers.CharField(max_length=255) - type = serializers.CharField(max_length=1) - - def validate_type(self, attrs, source): - value = attrs[source] - if not CommitFileChange.is_valid_type(value): - raise serializers.ValidationError('Commit patch_set type %s is not supported.' % value) - return attrs - - -class CommitSerializerWithPatchSet(CommitSerializer): - patch_set = ListField( - child=CommitPatchSetSerializer( - required=False), - required=False, - allow_null=True) - - -class ReleaseSerializer(serializers.Serializer): - version = serializers.CharField(max_length=VERSION_LENGTH, required=True) - ref = serializers.CharField(max_length=VERSION_LENGTH, required=False) - url = serializers.URLField(required=False) - owner = UserField(required=False) - dateReleased = serializers.DateTimeField(required=False) - commits = ListField( - child=CommitSerializerWithPatchSet( - required=False), - required=False, - allow_null=True) - - def validate_version(self, attrs, source): - value = attrs[source] - if not Release.is_valid_version(value): - raise serializers.ValidationError('Invalid value for release') - return attrs - - class ProjectReleasesEndpoint(ProjectEndpoint, EnvironmentMixin): permission_classes = (ProjectReleasePermission, ) @@ -146,7 +104,7 @@ def post(self, request, project): the current time is assumed. :auth: required """ - serializer = ReleaseSerializer(data=request.DATA) + serializer = ReleaseWithVersionSerializer(data=request.DATA) if serializer.is_valid(): result = serializer.object diff --git a/src/sentry/api/serializers/models/pullrequest.py b/src/sentry/api/serializers/models/pullrequest.py index 2cb3f9693536ea..e8453f247a15e7 100644 --- a/src/sentry/api/serializers/models/pullrequest.py +++ b/src/sentry/api/serializers/models/pullrequest.py @@ -36,9 +36,12 @@ def get_attrs(self, item_list, user): result = {} for item in item_list: repository_id = six.text_type(item.repository_id) + external_url = '' + if item.repository_id in repository_map: + external_url = self._external_url(repository_map[item.repository_id], item) result[item] = { 'repository': serialized_repos.get(repository_id, {}), - 'external_url': self._external_url(repository_map[item.repository_id], item), + 'external_url': external_url, 'user': users_by_author.get(six.text_type(item.author_id), {}) if item.author_id else {}, } diff --git a/src/sentry/api/serializers/rest_framework/commit.py b/src/sentry/api/serializers/rest_framework/commit.py index 93699c55b63127..2d78c21b38783e 100644 --- a/src/sentry/api/serializers/rest_framework/commit.py +++ b/src/sentry/api/serializers/rest_framework/commit.py @@ -1,6 +1,19 @@ from __future__ import absolute_import from rest_framework import serializers +from sentry.api.serializers.rest_framework.list import ListField +from sentry.models import CommitFileChange + + +class CommitPatchSetSerializer(serializers.Serializer): + path = serializers.CharField(max_length=255) + type = serializers.CharField(max_length=1) + + def validate_type(self, attrs, source): + value = attrs[source] + if not CommitFileChange.is_valid_type(value): + raise serializers.ValidationError('Commit patch_set type %s is not supported.' % value) + return attrs class CommitSerializer(serializers.Serializer): @@ -10,3 +23,8 @@ class CommitSerializer(serializers.Serializer): author_name = serializers.CharField(max_length=128, required=False) author_email = serializers.EmailField(max_length=75, required=False) timestamp = serializers.DateTimeField(required=False) + patch_set = ListField( + child=CommitPatchSetSerializer(required=False), + required=False, + allow_null=True, + ) diff --git a/src/sentry/api/serializers/rest_framework/release.py b/src/sentry/api/serializers/rest_framework/release.py new file mode 100644 index 00000000000000..c9f4afa8f705ac --- /dev/null +++ b/src/sentry/api/serializers/rest_framework/release.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import + +from rest_framework import serializers + +from sentry.api.serializers.rest_framework import CommitSerializer, ListField +from sentry.api.fields.user import UserField +from sentry.constants import VERSION_LENGTH +from sentry.models import Release + + +class ReleaseHeadCommitSerializerDeprecated(serializers.Serializer): + currentId = serializers.CharField(max_length=64) + repository = serializers.CharField(max_length=64) + previousId = serializers.CharField(max_length=64, required=False) + + +class ReleaseHeadCommitSerializer(serializers.Serializer): + commit = serializers.CharField(max_length=64) + repository = serializers.CharField(max_length=200) + previousCommit = serializers.CharField(max_length=64, required=False) + + +class ReleaseSerializer(serializers.Serializer): + ref = serializers.CharField(max_length=VERSION_LENGTH, required=False) + url = serializers.URLField(required=False) + dateReleased = serializers.DateTimeField(required=False) + commits = ListField(child=CommitSerializer(), required=False, allow_null=False) + + +class ReleaseWithVersionSerializer(ReleaseSerializer): + version = serializers.CharField(max_length=VERSION_LENGTH, required=True) + owner = UserField(required=False) + + def validate_version(self, attrs, source): + value = attrs[source] + if not Release.is_valid_version(value): + raise serializers.ValidationError('Release with name %s is not allowed' % value) + return attrs diff --git a/src/sentry/api/serializers/rest_framework/release_head_commit.py b/src/sentry/api/serializers/rest_framework/release_head_commit.py deleted file mode 100644 index 26c436b2a48f36..00000000000000 --- a/src/sentry/api/serializers/rest_framework/release_head_commit.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import absolute_import - -from rest_framework import serializers - - -class ReleaseHeadCommitSerializerDeprecated(serializers.Serializer): - currentId = serializers.CharField(max_length=64) - repository = serializers.CharField(max_length=64) - previousId = serializers.CharField(max_length=64, required=False) - - -class ReleaseHeadCommitSerializer(serializers.Serializer): - commit = serializers.CharField(max_length=64) - repository = serializers.CharField(max_length=64) - previousCommit = serializers.CharField(max_length=64, required=False) diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 7c897860ce8880..a09c936de398b3 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -1606,3 +1606,8 @@ def get_sentry_sdk_config(): 'topic': KAFKA_EVENTS, }, } + +# Enable this to use the legacy Slack Workspace Token apps. You will likely +# never need to switch this unless you created a workspace app before slack +# disabled them. +SLACK_INTEGRATION_USE_WST = False diff --git a/src/sentry/db/models/fields/node.py b/src/sentry/db/models/fields/node.py index 1111f85e7a6bbf..fd40eb5047ffc0 100644 --- a/src/sentry/db/models/fields/node.py +++ b/src/sentry/db/models/fields/node.py @@ -46,7 +46,7 @@ class NodeData(collections.MutableMapping): data={...} means, this is an object that should be saved to nodestore. """ - def __init__(self, field, id, data=None): + def __init__(self, field, id, data=None, wrapper=None): self.field = field self.id = id self.ref = None @@ -54,6 +54,9 @@ def __init__(self, field, id, data=None): # (this does not mean the Event is mutable, it just removes ref checking # in the case of something changing on the data model) self.ref_version = None + self.wrapper = wrapper + if data is not None and self.wrapper is not None: + data = self.wrapper(data) self._node_data = data def __getstate__(self): @@ -130,8 +133,8 @@ def bind_data(self, data, ref=None): raise NodeIntegrityFailure( 'Node reference for %s is invalid: %s != %s' % (self.id, ref, self.ref, ) ) - if self.field is not None and self.field.wrapper is not None: - data = self.field.wrapper(data) + if self.wrapper is not None: + data = self.wrapper(data) self._node_data = data def bind_ref(self, instance): @@ -216,10 +219,7 @@ def to_python(self, value): # to load data from, and no data to save. value = None - if value is not None and self.wrapper is not None: - value = self.wrapper(value) - - return NodeData(self, node_id, value) + return NodeData(self, node_id, value, wrapper=self.wrapper) def get_prep_value(self, value): """ diff --git a/src/sentry/event_manager.py b/src/sentry/event_manager.py index 6051be6fcbe5c3..804d405ba7122f 100644 --- a/src/sentry/event_manager.py +++ b/src/sentry/event_manager.py @@ -32,7 +32,7 @@ ) from sentry.interfaces.base import get_interface from sentry.models import ( - Activity, Environment, Event, EventError, EventMapping, EventUser, Group, + Activity, Environment, Event, EventDict, EventError, EventMapping, EventUser, Group, GroupEnvironment, GroupHash, GroupLink, GroupRelease, GroupResolution, GroupStatus, Project, Release, ReleaseEnvironment, ReleaseProject, ReleaseProjectEnvironment, UserReport, Organization, @@ -515,7 +515,7 @@ def _get_event_instance(self, project_id=None): return Event( project_id=project_id or self._project.id, event_id=event_id, - data=data, + data=EventDict(data, skip_renormalization=True), time_spent=time_spent, datetime=date, platform=platform diff --git a/src/sentry/filters/legacy_browsers.py b/src/sentry/filters/legacy_browsers.py index 288e5cf0639772..531fbbaa4a68dd 100644 --- a/src/sentry/filters/legacy_browsers.py +++ b/src/sentry/filters/legacy_browsers.py @@ -7,6 +7,7 @@ from sentry.models import ProjectOption from sentry.api.fields import MultipleChoiceField from sentry.utils.data_filters import FilterStatKeys +from sentry.utils.safe import get_path """ For default (legacy) filter @@ -81,7 +82,7 @@ def enable(self, value=None): def get_user_agent(self, data): try: - for key, value in data['request']['headers']: + for key, value in get_path(data, 'request', 'headers', filter=True): if key.lower() == 'user-agent': return value except LookupError: diff --git a/src/sentry/identity/slack/provider.py b/src/sentry/identity/slack/provider.py index c989ae973a2d55..03cadd6fb8c310 100644 --- a/src/sentry/identity/slack/provider.py +++ b/src/sentry/identity/slack/provider.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +from django.conf import settings + from sentry import options from sentry.identity.oauth2 import OAuth2Provider @@ -8,10 +10,8 @@ class SlackIdentityProvider(OAuth2Provider): key = 'slack' name = 'Slack' - # TODO(epurkhiser): This identity provider is actually used for authorizing - # the Slack application through their Workspace Token OAuth flow, not a - # standard user access token flow. - oauth_access_token_url = 'https://slack.com/api/oauth.token' + # This identity provider is used for authorizing the Slack application + # through their Bot token (or legacy Workspace Token if enabled) flow. oauth_authorize_url = 'https://slack.com/oauth/authorize' oauth_scopes = ( @@ -19,17 +19,31 @@ class SlackIdentityProvider(OAuth2Provider): 'identity.email', ) + # XXX(epurkhiser): While workspace tokens _do_ support the oauth.access + # endpoint, it will no include the authorizing_user, so we continue to use + # the deprecated oauth.token endpoint until we are able to migrate to a bot + # app which uses oauth.access. + def get_oauth_access_token_url(self): + return 'https://slack.com/api/oauth.token' if settings.SLACK_INTEGRATION_USE_WST else 'https://slack.com/api/oauth.access' + def get_oauth_client_id(self): return options.get('slack.client-id') def get_oauth_client_secret(self): return options.get('slack.client-secret') + def get_oauth_data(self, payload): + # TODO(epurkhiser): This flow isn't actually used right now in sentry. + # In slack-bot world we would need to make an API call to the 'me' + # endpoint to get their user ID here. + return super(SlackIdentityProvider, self).get_oauth_data(self, payload) + def build_identity(self, data): data = data['data'] return { 'type': 'slack', + # TODO(epurkhiser): See note above 'id': data['user']['id'], 'email': data['user']['email'], 'scopes': sorted(data['scope'].split(',')), diff --git a/src/sentry/integrations/client.py b/src/sentry/integrations/client.py index c024dead9ddfd3..ba27d6d4470a19 100644 --- a/src/sentry/integrations/client.py +++ b/src/sentry/integrations/client.py @@ -9,13 +9,19 @@ from BeautifulSoup import BeautifulStoneSoup from django.utils.functional import cached_property -from requests.exceptions import ConnectionError, HTTPError +from requests.exceptions import ConnectionError, Timeout, HTTPError from sentry.exceptions import InvalidIdentity from sentry.http import build_session from sentry.utils import metrics from six.moves.urllib.parse import urlparse -from .exceptions import ApiHostError, ApiError, ApiUnauthorized, UnsupportedResponseType +from .exceptions import ( + ApiHostError, + ApiTimeoutError, + ApiError, + ApiUnauthorized, + UnsupportedResponseType +) class BaseApiResponse(object): @@ -173,6 +179,12 @@ def _request(self, method, path, headers=None, data=None, params=None, 'status': 'connection_error' }) raise ApiHostError.from_exception(e) + except Timeout as e: + metrics.incr('integrations.http_response', tags={ + 'host': host, + 'status': 'timeout' + }) + raise ApiTimeoutError.from_exception(e) except HTTPError as e: resp = e.response if resp is None: diff --git a/src/sentry/integrations/exceptions.py b/src/sentry/integrations/exceptions.py index 1c7d93dda250c8..fd64b8ed1265bf 100644 --- a/src/sentry/integrations/exceptions.py +++ b/src/sentry/integrations/exceptions.py @@ -43,7 +43,7 @@ class ApiHostError(ApiError): @classmethod def from_exception(cls, exception): - if hasattr(exception, 'request'): + if getattr(exception, 'request'): return cls.from_request(exception.request) return cls('Unable to reach host') @@ -53,6 +53,21 @@ def from_request(cls, request): return cls(u'Unable to reach host: {}'.format(host)) +class ApiTimeoutError(ApiError): + code = 504 + + @classmethod + def from_exception(cls, exception): + if getattr(exception, 'request'): + return cls.from_request(exception.request) + return cls('Timed out reaching host') + + @classmethod + def from_request(cls, request): + host = urlparse(request.url).netloc + return cls(u'Timed out attempting to reach host: {}'.format(host)) + + class ApiUnauthorized(ApiError): code = 401 diff --git a/src/sentry/integrations/jira/integration.py b/src/sentry/integrations/jira/integration.py index fe0dd4ddd24928..7e04db3175e063 100644 --- a/src/sentry/integrations/jira/integration.py +++ b/src/sentry/integrations/jira/integration.py @@ -762,7 +762,7 @@ def get_pipeline_views(self): return [] def build_integration(self, state): - # Most information is not availabe during integration install time, + # Most information is not available during integration installation, # since the integration won't have been fully configired on JIRA's side # yet, we can't make API calls for more details like the server name or # Icon. diff --git a/src/sentry/integrations/jira_server/integration.py b/src/sentry/integrations/jira_server/integration.py index 6b21813ebd0cf7..dd7d21aefa920a 100644 --- a/src/sentry/integrations/jira_server/integration.py +++ b/src/sentry/integrations/jira_server/integration.py @@ -179,8 +179,11 @@ def dispatch(self, request, pipeline): return self.redirect(authorize_url) except ApiError as error: - logger.info('identity.jira-server.request-token', extra={'error': error}) - return pipeline.error('Could not fetch a request token from Jira') + logger.info('identity.jira-server.request-token', extra={ + 'url': config.get('url'), + 'error': error + }) + return pipeline.error('Could not fetch a request token from Jira. %s' % error) class OAuthCallbackView(PipelineView): diff --git a/src/sentry/integrations/jira_server/webhooks.py b/src/sentry/integrations/jira_server/webhooks.py index 11be08a14630fa..c1f0845f6d79d9 100644 --- a/src/sentry/integrations/jira_server/webhooks.py +++ b/src/sentry/integrations/jira_server/webhooks.py @@ -6,6 +6,7 @@ from django.views.decorators.csrf import csrf_exempt from sentry.api.base import Endpoint +from sentry.integrations.exceptions import ApiError from sentry.integrations.jira.webhooks import ( handle_assignee_change, handle_status_change @@ -69,7 +70,14 @@ def post(self, request, token, *args, **kwargs): logger.info('missing-changelog', extra={'integration_id': integration.id}) return self.respond() - handle_assignee_change(integration, data) - handle_status_change(integration, data) - - return self.respond() + try: + handle_assignee_change(integration, data) + handle_status_change(integration, data) + except ApiError as err: + logger.info('sync-failed', extra={ + 'token': token, + 'error': six.text_type(err) + }) + return self.respond(status=400) + else: + return self.respond() diff --git a/src/sentry/integrations/slack/event_endpoint.py b/src/sentry/integrations/slack/event_endpoint.py index 6d57f7234440bf..8c20b7464a58de 100644 --- a/src/sentry/integrations/slack/event_endpoint.py +++ b/src/sentry/integrations/slack/event_endpoint.py @@ -4,6 +4,8 @@ import re import six +from django.conf import settings + from sentry import http from sentry.api.base import Endpoint from sentry.models import Group, Project @@ -58,8 +60,13 @@ def on_link_shared(self, request, integration, token, data): if not results: return + if settings.SLACK_INTEGRATION_USE_WST: + access_token = integration.metadata['access_token'], + else: + access_token = integration.metadata['user_access_token'], + payload = { - 'token': integration.metadata['access_token'], + 'token': access_token, 'channel': data['channel'], 'ts': data['message_ts'], 'unfurls': json.dumps({ diff --git a/src/sentry/integrations/slack/integration.py b/src/sentry/integrations/slack/integration.py index 7646c81656ee4e..0926a1132dd7fe 100644 --- a/src/sentry/integrations/slack/integration.py +++ b/src/sentry/integrations/slack/integration.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from django.utils.translation import ugettext_lazy as _ +from django.conf import settings from sentry import http from sentry.identity.pipeline import IdentityProviderPipeline @@ -68,7 +69,12 @@ class SlackIntegrationProvider(IntegrationProvider): IntegrationFeatures.ALERT_RULE, ]) + # Scopes differ depending on if it's a workspace app identity_oauth_scopes = frozenset([ + 'bot', + 'links:read', + 'links:write', + ]) if not settings.SLACK_INTEGRATION_USE_WST else frozenset([ 'channels:read', 'groups:read', 'users:read', @@ -110,25 +116,51 @@ def get_team_info(self, access_token): return resp['team'] + def get_identity(self, user_token): + payload = { + 'token': user_token, + } + + session = http.build_session() + resp = session.get('https://slack.com/api/auth.test', params=payload) + resp.raise_for_status() + resp = resp.json() + + return resp['user_id'] + def build_integration(self, state): data = state['identity']['data'] assert data['ok'] + if settings.SLACK_INTEGRATION_USE_WST: + access_token = data['access_token'] + user_id_slack = data['authorizing_user_id'] + else: + access_token = data['bot']['bot_access_token'] + user_id_slack = self.get_identity(data['access_token']) + scopes = sorted(self.identity_oauth_scopes) - team_data = self.get_team_info(data['access_token']) + team_data = self.get_team_info(access_token) + + metadata = { + 'access_token': access_token, + 'scopes': scopes, + 'icon': team_data['icon']['image_132'], + 'domain_name': team_data['domain'] + '.slack.com', + } + + # When using bot tokens, we must use the user auth token for URL + # unfurling + if not settings.SLACK_INTEGRATION_USE_WST: + metadata['user_access_token'] = data['access_token'] return { 'name': data['team_name'], 'external_id': data['team_id'], - 'metadata': { - 'access_token': data['access_token'], - 'scopes': scopes, - 'icon': team_data['icon']['image_132'], - 'domain_name': team_data['domain'] + '.slack.com', - }, + 'metadata': metadata, 'user_identity': { 'type': 'slack', - 'external_id': data['authorizing_user_id'], + 'external_id': user_id_slack, 'scopes': [], 'data': {}, }, diff --git a/src/sentry/integrations/slack/utils.py b/src/sentry/integrations/slack/utils.py index 9999588ce6f78b..90d702fbe67072 100644 --- a/src/sentry/integrations/slack/utils.py +++ b/src/sentry/integrations/slack/utils.py @@ -80,13 +80,16 @@ def build_attachment_title(group, event=None): return u'{} - {}'.format(ev_metadata['directive'], ev_metadata['uri']) else: if group.culprit: - return u'{} - {}'.format(group.title[:40], group.culprit) + return u'{} - {}'.format(group.title, group.culprit) return group.title def build_attachment_text(group, event=None): - ev_metadata = group.get_event_metadata() - ev_type = group.get_event_type() + # Group and Event both implement get_event_{type,metadata} + obj = event if event is not None else group + ev_metadata = obj.get_event_metadata() + ev_type = obj.get_event_type() + if ev_type == 'error': return ev_metadata.get('value') or ev_metadata.get('function') else: diff --git a/src/sentry/interfaces/exception.py b/src/sentry/interfaces/exception.py index 8421a00d2d3291..b625774a714e47 100644 --- a/src/sentry/interfaces/exception.py +++ b/src/sentry/interfaces/exception.py @@ -533,6 +533,9 @@ def to_string(self, event, is_public=False, **kwargs): output = [] for exc in self.values: + if not exc: + continue + output.append(u'{0}: {1}\n'.format(exc.type, exc.value)) if exc.stacktrace: output.append( diff --git a/src/sentry/interfaces/http.py b/src/sentry/interfaces/http.py index 2c3692d2c8bc11..e26bd5ad0d6582 100644 --- a/src/sentry/interfaces/http.py +++ b/src/sentry/interfaces/http.py @@ -264,7 +264,7 @@ def full_url(self): url = self.url if url: if self.query_string: - url = url + '?' + urlencode(self.query_string) + url = url + '?' + urlencode(get_path(self.query_string, filter=True)) if self.fragment: url = url + '#' + self.fragment return url @@ -276,7 +276,7 @@ def to_email_html(self, event, **kwargs): 'url': self.full_url, 'short_url': self.url, 'method': self.method, - 'query_string': urlencode(self.query_string), + 'query_string': urlencode(get_path(self.query_string, filter=True)), 'fragment': self.fragment, } ) diff --git a/src/sentry/lang/native/unreal.py b/src/sentry/lang/native/unreal.py index 0a10f19b3dda31..b28143ca15fa04 100644 --- a/src/sentry/lang/native/unreal.py +++ b/src/sentry/lang/native/unreal.py @@ -6,8 +6,14 @@ import re -_portable_callstack_regexp = re.compile( - r'(?P[\w]+) (?P0x[\da-fA-F]+) \+ (?P[\da-fA-F]+)') +_portable_callstack_regexp = re.compile(r'''(?x) + (?:^|\s) + (?P[^\s]+) + \s + (?P0x[\da-fA-F]+) + \s\+\s + (?P[\da-fA-F]+) +''') def process_unreal_crash(payload, user_id, environment, event): @@ -84,6 +90,40 @@ def merge_apple_crash_report(apple_crash_report, event): event.setdefault('debug_meta', {})['images'] = images +def parse_portable_callstack(portable_callstack, images): + frames = [] + for match in _portable_callstack_regexp.finditer(portable_callstack): + baseaddr = int(match.group('baseaddr'), 16) + offset = int(match.group('offset'), 16) + # Crashes without PDB in the client report: 0x00000000ffffffff + ffffffff + if baseaddr == 0xffffffff and offset == 0xffffffff: + continue + + package_re = re.escape(match.group('package')) + r"(\.dll|\.exe)?$" + image = next(( + image for image in images + if image.get('code_file') + and re.search(package_re, image['code_file'], re.IGNORECASE) + ), {}) + + # baseaddr reported in the pcallstack missing most relevant bits: + # i.e: 0x0000000080db0000 + # The image address should be used instead with the offset: + image_addr = image.get('image_addr') + if image_addr: + # Rebase with the image address if available. + baseaddr = int(image_addr, 16) + + frames.append({ + 'package': image.get('code_file') or match.group('package'), + 'instruction_addr': hex(baseaddr + offset), + 'trust': 'prewalked', + }) + + frames.reverse() + return frames + + def merge_unreal_context_event(unreal_context, event, project): """Merges the context from an Unreal Engine 4 crash with the given event.""" @@ -130,35 +170,8 @@ def merge_unreal_context_event(unreal_context, event, project): for thread in event.get('threads', [])): portable_callstack = runtime_prop.pop('portable_call_stack', None) if portable_callstack is not None: - frames = [] images = get_path(event, 'debug_meta', 'images', filter=True, default=()) - for match in _portable_callstack_regexp.finditer(portable_callstack): - baseaddr = int(match.group('baseaddr'), 16) - offset = int(match.group('offset'), 16) - # Crashes without PDB in the client report: 0x00000000ffffffff + ffffffff - if baseaddr == 0xffffffff and offset == 0xffffffff: - continue - - my_regex = re.escape(match.group('package')) + r"(\.dll|\.exe)?$" - - # baseaddr reported in the pcallstack missing most relevant bits: - # i.e: 0x0000000080db0000 - # The image address should be used instead with the offset: - it = next( - (image for image in images if image.get('code_file') is not None and re.search( - my_regex, image.get('code_file'), re.IGNORECASE)), {}) - - image_addr = it.get('image_addr') - if image_addr: - # Rebase with the image address if available. - baseaddr = int(image_addr, 16) - - frames.append({ - 'package': match.group('package'), - 'instruction_addr': hex(baseaddr + offset), - }) - - frames.reverse() + frames = parse_portable_callstack(portable_callstack, images) if len(frames) > 0: event['stacktrace'] = { diff --git a/src/sentry/models/event.py b/src/sentry/models/event.py index 25f4ddcb1dd5ad..f2950d077123cb 100644 --- a/src/sentry/models/event.py +++ b/src/sentry/models/event.py @@ -64,7 +64,17 @@ class EventDict(CanonicalKeyDict): """ def __init__(self, data, skip_renormalization=False, **kwargs): - if not skip_renormalization and not isinstance(data, EventDict): + is_renormalized = ( + isinstance(data, EventDict) or + (isinstance(data, NodeData) and isinstance(data.data, EventDict)) + ) + + with configure_scope() as scope: + scope.set_tag("rust.is_renormalized", is_renormalized) + scope.set_tag("rust.skip_renormalization", skip_renormalization) + scope.set_tag("rust.renormalized", "null") + + if not skip_renormalization and not is_renormalized: rust_renormalized = _should_skip_to_python(data.get('event_id')) if rust_renormalized: normalizer = StoreNormalizer(is_renormalize=True) @@ -472,10 +482,11 @@ def __init__(self, snuba_values): self.__dict__ = snuba_values - # This should be lazy loaded and will only be accessed if we access any - # properties on self.data - node_id = SnubaEvent.generate_node_id(self.project_id, self.event_id) - self.data = NodeData(None, node_id, data=None) + # self.data is a (lazy) dict of everything we got from nodestore + node_id = SnubaEvent.generate_node_id( + self.snuba_data['project_id'], + self.snuba_data['event_id']) + self.data = NodeData(None, node_id, data=None, wrapper=EventDict) # ============================================ # Snuba-only implementations of properties that diff --git a/src/sentry/models/release.py b/src/sentry/models/release.py index 0d7296e1afa838..b4ad3eddb4918f 100644 --- a/src/sentry/models/release.py +++ b/src/sentry/models/release.py @@ -97,7 +97,8 @@ class Meta: @staticmethod def is_valid_version(value): return not (any(c in value for c in BAD_RELEASE_CHARS) - or value in ('.', '..') or not value) + or value in ('.', '..') or not value + or value.lower() == 'latest') @classmethod def get_cache_key(cls, organization_id, version): diff --git a/src/sentry/ownership/grammar.py b/src/sentry/ownership/grammar.py index b4b9bc0dfb2814..31d4cf8b6559b1 100644 --- a/src/sentry/ownership/grammar.py +++ b/src/sentry/ownership/grammar.py @@ -104,13 +104,10 @@ def test_url(self, data): def test_path(self, data): for frame in _iter_frames(data): - try: - filename = frame['filename'] - except KeyError: - try: - filename = frame['abs_path'] - except KeyError: - continue + filename = frame.get('filename') or frame.get('abs_path') + + if not filename: + continue # fnmatch keeps it's own internal cache, so # there isn't any optimization we can do here diff --git a/src/sentry/receivers/users.py b/src/sentry/receivers/users.py index a344a27638971a..684512f9d7cedb 100644 --- a/src/sentry/receivers/users.py +++ b/src/sentry/receivers/users.py @@ -30,7 +30,7 @@ def create_first_user(created_models, verbosity, db, app=None, **kwargs): return from sentry.runner import call_command - call_command('sentry.runner.commands.createuser.createuser') + call_command('sentry.runner.commands.createuser.createuser', superuser=True) post_syncdb.connect( diff --git a/src/sentry/runner/commands/cleanup.py b/src/sentry/runner/commands/cleanup.py index f7e7aa0c568ed8..937bfce8cdae58 100644 --- a/src/sentry/runner/commands/cleanup.py +++ b/src/sentry/runner/commands/cleanup.py @@ -206,10 +206,13 @@ def is_filtered(model): date_added__lte=timezone.now() - timedelta(hours=48) ).delete() - if is_filtered(models.OrganizationMember) and not silent: - click.echo('>> Skipping OrganizationMember') - else: + if not silent: click.echo('Removing expired values for OrganizationMember') + + if is_filtered(models.OrganizationMember): + if not silent: + click.echo('>> Skipping OrganizationMember') + else: expired_threshold = timezone.now() - timedelta(days=days) models.OrganizationMember.delete_expired(expired_threshold) diff --git a/src/sentry/static/sentry/app/api.jsx b/src/sentry/static/sentry/app/api.jsx index 4de31c94fbe45a..678db6288cadee 100644 --- a/src/sentry/static/sentry/app/api.jsx +++ b/src/sentry/static/sentry/app/api.jsx @@ -87,16 +87,12 @@ export class Client { } wrapCallback(id, func, cleanup) { - /*eslint consistent-return:0*/ - if (isUndefined(func)) { - return; - } - return (...args) => { const req = this.activeRequests[id]; if (cleanup === true) { delete this.activeRequests[id]; } + if (req && req.alive) { // Check if API response is a 302 -- means project slug was renamed and user // needs to be redirected @@ -104,8 +100,12 @@ export class Client { return; } + if (isUndefined(func)) { + return; + } + // Call success callback - return func.apply(req, args); + return func.apply(req, args); // eslint-disable-line } }; } diff --git a/src/sentry/static/sentry/app/components/hovercard.jsx b/src/sentry/static/sentry/app/components/hovercard.jsx index affd063634d170..ab3ba73b38119a 100644 --- a/src/sentry/static/sentry/app/components/hovercard.jsx +++ b/src/sentry/static/sentry/app/components/hovercard.jsx @@ -151,7 +151,8 @@ const StyledHovercard = styled('div')` text-align: left; padding: 0; line-height: 1; - z-index: 1000; + /* Some hovercards overlap the toplevel header, so we need the same zindex to appear on top */ + z-index: ${p => p.theme.zIndex.globalSelectionHeader}; white-space: initial; color: ${p => p.theme.gray5}; border: 1px solid ${p => p.theme.borderLight}; diff --git a/src/sentry/static/sentry/app/components/sidebar/index.jsx b/src/sentry/static/sentry/app/components/sidebar/index.jsx index 99ef6d74808362..0c0c0be6c6784c 100644 --- a/src/sentry/static/sentry/app/components/sidebar/index.jsx +++ b/src/sentry/static/sentry/app/components/sidebar/index.jsx @@ -175,7 +175,7 @@ class Sidebar extends React.Component { ].map(route => `/organizations/${this.props.organization.slug}/${route}/`); // Only keep the querystring if the current route matches one of the above - if (globalSelectionRoutes.includes(this.props.location.pathname)) { + if (globalSelectionRoutes.includes(pathname)) { const query = extractSelectionParameters(this.props.location.query); // Handle cmd-click (mac) and meta-click (linux) diff --git a/src/sentry/static/sentry/app/options.jsx b/src/sentry/static/sentry/app/options.jsx index 7c0309de8ee207..f6c6253b0cce09 100644 --- a/src/sentry/static/sentry/app/options.jsx +++ b/src/sentry/static/sentry/app/options.jsx @@ -171,6 +171,11 @@ export function getOption(option) { return definitionsMap[option]; } +export function getOptionDefault(option) { + const meta = getOption(option); + return meta.defaultValue ? meta.defaultValue() : undefined; +} + function optionsForSection(section) { return definitions.filter(option => option.key.split('.')[0] === section.key); } @@ -183,7 +188,7 @@ export function getOptionField(option, field) { {...meta} name={option} key={option} - defaultValue={meta.defaultValue ? meta.defaultValue() : undefined} + defaultValue={getOptionDefault(option)} required={meta.required && !meta.allowEmpty} disabledReason={meta.disabledReason && disabledReasons[meta.disabledReason]} /> diff --git a/src/sentry/static/sentry/app/views/installWizard.jsx b/src/sentry/static/sentry/app/views/installWizard.jsx index 7a556b40dbff52..5db9b8666ead92 100644 --- a/src/sentry/static/sentry/app/views/installWizard.jsx +++ b/src/sentry/static/sentry/app/views/installWizard.jsx @@ -6,7 +6,7 @@ import AsyncView from 'app/views/asyncView'; import {t} from 'app/locale'; import ConfigStore from 'app/stores/configStore'; import {ApiForm} from 'app/components/forms'; -import {getOptionField, getForm} from 'app/options'; +import {getOptionDefault, getOptionField, getForm} from 'app/options'; export default class InstallWizard extends AsyncView { static propTypes = { @@ -64,16 +64,21 @@ export default class InstallWizard extends AsyncView { if (option.field.disabled) { return; } - // XXX(dcramer): we need the user to explicitly choose beacon.anonymous - // vs using an implied default so effectively this is binding - // all values to their server-defaults (as client-side defaults dont really work) + // TODO(dcramer): we need to rethink this logic as doing multiple "is this value actually set" // is problematic + // all values to their server-defaults (as client-side defaults dont really work) + const displayValue = option.value || getOptionDefault(optionName); if ( - option.value !== undefined && option.value !== "" && option.value !== null && - (option.field.isSet || optionName != 'beacon.anonymous') + // XXX(dcramer): we need the user to explicitly choose beacon.anonymous + // vs using an implied default so effectively this is binding + optionName != 'beacon.anonymous' && + // XXX(byk): if we don't have a set value but have a default value filled + // instead, from the client, set it on the data so it is sent to the server + !option.field.isSet && + displayValue !== undefined ) { - data[optionName] = option.value; + data[optionName] = displayValue; } }); return data; diff --git a/src/sentry/static/sentry/images/integrations/slack-logo.png b/src/sentry/static/sentry/images/integrations/slack-logo.png index b7f08a930db851..805e43f0f34fa9 100644 Binary files a/src/sentry/static/sentry/images/integrations/slack-logo.png and b/src/sentry/static/sentry/images/integrations/slack-logo.png differ diff --git a/src/sentry/tasks/post_process.py b/src/sentry/tasks/post_process.py index dd5e3b1287e039..ec2d067d40462c 100644 --- a/src/sentry/tasks/post_process.py +++ b/src/sentry/tasks/post_process.py @@ -14,6 +14,7 @@ from django.conf import settings from sentry import features +from sentry.models import EventDict from sentry.utils import snuba from sentry.utils.cache import cache from sentry.plugins import plugins @@ -100,6 +101,10 @@ def post_process_group(event, is_new, is_regression, is_sample, is_new_group_env from sentry.rules.processor import RuleProcessor from sentry.tasks.servicehooks import process_service_hook + # Re-bind node data to avoid renormalization. We only want to + # renormalize when loading old data from the database. + event.data = EventDict(event.data, skip_renormalization=True) + # Re-bind Group since we're pickling the whole Event object # which may contain a stale Group. event.group, _ = get_group_with_redirect(event.group_id) diff --git a/src/sentry/utils/committers.py b/src/sentry/utils/committers.py index b9b66e8af83811..3f90a0886a5fb9 100644 --- a/src/sentry/utils/committers.py +++ b/src/sentry/utils/committers.py @@ -7,6 +7,7 @@ from sentry.models import (Release, ReleaseCommit, Commit, CommitFileChange, Event, Group) from sentry.api.serializers.models.commit import CommitSerializer, get_users_for_commits from sentry.utils import metrics +from sentry.utils.safe import get_path from django.db.models import Q @@ -38,15 +39,11 @@ def score_path_match_length(path_a, path_b): def _get_frame_paths(event): data = event.data - try: - frames = data['stacktrace']['frames'] - except KeyError: - try: - frames = data['exception']['values'][0]['stacktrace']['frames'] - except (KeyError, TypeError): - return [] # can't find stacktrace information + frames = get_path(data, 'stacktrace', 'frames', filter=True) + if frames: + return frames - return frames + return get_path(data, 'exception', 'values', 0, 'stacktrace', 'frames', filter=True) or [] def _get_commits(releases): @@ -186,7 +183,7 @@ def get_event_file_committers(project, event, frame_limit=25): if not commits: raise Commit.DoesNotExist - frames = _get_frame_paths(event) + frames = _get_frame_paths(event) or () app_frames = [frame for frame in frames if frame['in_app']][-frame_limit:] if not app_frames: app_frames = [frame for frame in frames][-frame_limit:] diff --git a/src/sentry/utils/pytest/fixtures.py b/src/sentry/utils/pytest/fixtures.py index 4c3e1ffbc9f29a..ec838ce9fb10fe 100644 --- a/src/sentry/utils/pytest/fixtures.py +++ b/src/sentry/utils/pytest/fixtures.py @@ -149,6 +149,12 @@ def factories(): return Factories +@pytest.fixture +def task_runner(): + from sentry.testutils.helpers.task_runner import TaskRunner + return TaskRunner + + @pytest.fixture(scope='function') def session(): return factories.create_session() diff --git a/tests/js/spec/views/installWizard.spec.jsx b/tests/js/spec/views/installWizard.spec.jsx index 07e3ed38d6ad86..99d304566bee80 100644 --- a/tests/js/spec/views/installWizard.spec.jsx +++ b/tests/js/spec/views/installWizard.spec.jsx @@ -59,7 +59,7 @@ describe('InstallWizard', function() { ).toBe(false); }); - it('has "Send my contact information..." when beacon.anonymous is false', function() { + it('has no option selected even when beacon.anonymous is set', function() { MockApiClient.addMockResponse({ url: '/internal/options/?query=is:required', body: TestStubs.InstallWizard({ @@ -78,40 +78,12 @@ describe('InstallWizard', function() { }); const wrapper = mount(); - expect( - wrapper.find('input[name="beacon.anonymous"][value="false"]').prop('checked') - ).toBe(true); - - expect( - wrapper.find('input[name="beacon.anonymous"][value="true"]').prop('checked') - ).toBe(false); - }); - - it('has "Please keep my usage anonymous" when beacon.anonymous is true', function() { - MockApiClient.addMockResponse({ - url: '/internal/options/?query=is:required', - body: TestStubs.InstallWizard({ - 'beacon.anonymous': { - field: { - disabledReason: null, - default: false, - required: true, - disabled: false, - allowEmpty: true, - isSet: true, - }, - value: true, - }, - }), - }); - const wrapper = mount(); - expect( wrapper.find('input[name="beacon.anonymous"][value="false"]').prop('checked') ).toBe(false); expect( wrapper.find('input[name="beacon.anonymous"][value="true"]').prop('checked') - ).toBe(true); + ).toBe(false); }); }); diff --git a/tests/sentry/api/endpoints/test_dif_assemble.py b/tests/sentry/api/endpoints/test_dif_assemble.py index b0b8a03a61d689..20ce0dcf09b412 100644 --- a/tests/sentry/api/endpoints/test_dif_assemble.py +++ b/tests/sentry/api/endpoints/test_dif_assemble.py @@ -8,7 +8,7 @@ from sentry.models import ApiToken, FileBlob, File, FileBlobIndex, FileBlobOwner from sentry.models.file import ChunkFileState -from sentry.models.debugfile import get_assemble_status, ProjectDebugFile +from sentry.models.debugfile import get_assemble_status, set_assemble_status, ProjectDebugFile from sentry.testutils import APITestCase from sentry.tasks.assemble import assemble_dif, assemble_file @@ -141,6 +141,7 @@ def test_assemble_check(self): debug_id='df449af8-0dcd-4320-9943-ec192134d593', code_id='DF449AF80DCD43209943EC192134D593', ) + set_assemble_status(self.project, checksum, None) # Request now tells us that everything is alright response = self.client.post( diff --git a/tests/sentry/api/endpoints/test_organization_release_details.py b/tests/sentry/api/endpoints/test_organization_release_details.py index 81924a1e30f182..0b72d84fe32591 100644 --- a/tests/sentry/api/endpoints/test_organization_release_details.py +++ b/tests/sentry/api/endpoints/test_organization_release_details.py @@ -9,7 +9,7 @@ Activity, Environment, File, Release, ReleaseCommit, ReleaseFile, ReleaseProject, ReleaseProjectEnvironment, Repository ) from sentry.testutils import APITestCase -from sentry.api.endpoints.organization_release_details import ReleaseSerializer +from sentry.api.endpoints.organization_release_details import OrganizationReleaseSerializer class ReleaseDetailsTest(APITestCase): @@ -715,7 +715,7 @@ def setUp(self): ] def test_simple(self): - serializer = ReleaseSerializer(data={ + serializer = OrganizationReleaseSerializer(data={ 'ref': self.ref, 'url': self.url, 'dateReleased': self.dateReleased, @@ -737,33 +737,33 @@ def test_simple(self): assert result['refs'] == self.refs def test_fields_not_required(self): - serializer = ReleaseSerializer(data={}) + serializer = OrganizationReleaseSerializer(data={}) assert serializer.is_valid() def test_do_not_allow_null_commits(self): - serializer = ReleaseSerializer(data={ + serializer = OrganizationReleaseSerializer(data={ 'commits': None, }) assert not serializer.is_valid() def test_do_not_allow_null_head_commits(self): - serializer = ReleaseSerializer(data={ + serializer = OrganizationReleaseSerializer(data={ 'headCommits': None, }) assert not serializer.is_valid() def test_do_not_allow_null_refs(self): - serializer = ReleaseSerializer(data={ + serializer = OrganizationReleaseSerializer(data={ 'refs': None, }) assert not serializer.is_valid() def test_ref_limited_by_max_version_length(self): - serializer = ReleaseSerializer(data={ + serializer = OrganizationReleaseSerializer(data={ 'ref': 'a' * VERSION_LENGTH, }) assert serializer.is_valid() - serializer = ReleaseSerializer(data={ + serializer = OrganizationReleaseSerializer(data={ 'ref': 'a' * (VERSION_LENGTH + 1), }) assert not serializer.is_valid() diff --git a/tests/sentry/api/endpoints/test_organization_releases.py b/tests/sentry/api/endpoints/test_organization_releases.py index 01057863c4d366..88b9ec522a7c7e 100644 --- a/tests/sentry/api/endpoints/test_organization_releases.py +++ b/tests/sentry/api/endpoints/test_organization_releases.py @@ -1500,3 +1500,10 @@ def test_version_does_not_allow_null_or_empty_value(self): 'projects': self.projects, }) assert not serializer.is_valid() + + def test_version_cannot_be_latest(self): + serializer = ReleaseSerializerWithProjects(data={ + 'version': 'Latest', + 'projects': self.projects, + }) + assert not serializer.is_valid() diff --git a/tests/sentry/api/endpoints/test_project_releases.py b/tests/sentry/api/endpoints/test_project_releases.py index a1e55a1cd7f978..d8f5ee2fbb0cfc 100644 --- a/tests/sentry/api/endpoints/test_project_releases.py +++ b/tests/sentry/api/endpoints/test_project_releases.py @@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse from exam import fixture -from sentry.api.endpoints.project_releases import ReleaseSerializer +from sentry.api.endpoints.project_releases import ReleaseWithVersionSerializer from sentry.constants import VERSION_LENGTH from sentry.models import ( BAD_RELEASE_CHARS, @@ -707,7 +707,7 @@ def setUp(self): self.dateReleased = '1000-10-10T06:06' def test_simple(self): - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': self.version, 'owner': self.user.username, 'ref': self.ref, @@ -730,61 +730,67 @@ def test_simple(self): assert result['commits'] == self.commits def test_fields_not_required(self): - serializer = ReleaseSerializer(data={'version': self.version}) + serializer = ReleaseWithVersionSerializer(data={'version': self.version}) assert serializer.is_valid() def test_do_not_allow_null_commits(self): - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': self.version, 'commits': None, }) assert not serializer.is_valid() def test_ref_limited_by_max_version_length(self): - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': self.version, 'ref': 'a' * VERSION_LENGTH, }) assert serializer.is_valid() - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': self.version, 'ref': 'a' * (VERSION_LENGTH + 1), }) assert not serializer.is_valid() def test_version_limited_by_max_version_length(self): - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': 'a' * VERSION_LENGTH, }) assert serializer.is_valid() - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': 'a' * (VERSION_LENGTH + 1), }) assert not serializer.is_valid() def test_version_does_not_allow_whitespace(self): for char in BAD_RELEASE_CHARS: - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': char, }) assert not serializer.is_valid() def test_version_does_not_allow_current_dir_path(self): - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': '.', }) assert not serializer.is_valid() - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': '..', }) assert not serializer.is_valid() def test_version_does_not_allow_null_or_empty_value(self): - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': None, }) assert not serializer.is_valid() - serializer = ReleaseSerializer(data={ + serializer = ReleaseWithVersionSerializer(data={ 'version': '', }) assert not serializer.is_valid() + + def test_version_cannot_be_latest(self): + serializer = ReleaseWithVersionSerializer(data={ + 'version': 'Latest', + }) + assert not serializer.is_valid() diff --git a/tests/sentry/api/serializers/test_pull_request.py b/tests/sentry/api/serializers/test_pull_request.py index 89eb22426ffa66..ba30a4387580d5 100644 --- a/tests/sentry/api/serializers/test_pull_request.py +++ b/tests/sentry/api/serializers/test_pull_request.py @@ -110,3 +110,25 @@ def test_integration_repository(self): assert result['title'] == 'cool pr' assert result['repository']['name'] == 'test/test' assert result['author'] == {'name': 'stebe', 'email': 'stebe@sentry.io'} + + def test_deleted_repository(self): + commit_author = CommitAuthor.objects.create( + name='stebe', + email='stebe@sentry.io', + organization_id=self.project.organization_id, + ) + pull_request = PullRequest.objects.create( + organization_id=self.project.organization_id, + repository_id=12345, + key='9', + author=commit_author, + message='waddap', + title="cool pr" + ) + result = serialize(pull_request, self.user) + + assert result['message'] == pull_request.message + assert result['title'] == pull_request.title + assert result['repository'] == {} + assert result['author'] == {'name': commit_author.name, 'email': commit_author.email} + assert result['externalUrl'] == '' diff --git a/tests/sentry/integrations/jira_server/test_integration.py b/tests/sentry/integrations/jira_server/test_integration.py index 910b030f30040c..bed10fcb7a6f0a 100644 --- a/tests/sentry/integrations/jira_server/test_integration.py +++ b/tests/sentry/integrations/jira_server/test_integration.py @@ -1,7 +1,9 @@ from __future__ import absolute_import import jwt +import responses +from requests.exceptions import ReadTimeout from sentry.integrations.jira_server import JiraServerIntegrationProvider from sentry.models import ( Identity, @@ -13,8 +15,6 @@ from sentry.utils import json from .testutils import EXAMPLE_PRIVATE_KEY -import responses - class JiraServerIntegrationTest(IntegrationTestCase): provider = JiraServerIntegrationProvider @@ -66,6 +66,30 @@ def test_validate_private_key(self): self.assertContains( resp, 'Private key must be a valid SSH private key encoded in a PEM format.') + @responses.activate + def test_authentication_request_token_timeout(self): + timeout = ReadTimeout('Read timed out. (read timeout=30)') + responses.add( + responses.POST, + 'https://jira.example.com/plugins/servlet/oauth/request-token', + body=timeout) + + # Start pipeline and go to setup page. + self.client.get(self.setup_path) + + # Submit credentials + data = { + 'url': 'https://jira.example.com/', + 'verify_ssl': False, + 'consumer_key': 'sentry-bot', + 'private_key': EXAMPLE_PRIVATE_KEY + } + resp = self.client.post(self.setup_path, data=data) + assert resp.status_code == 200 + self.assertContains(resp, 'Setup Error') + self.assertContains(resp, 'request token from Jira') + self.assertContains(resp, 'Timed out') + @responses.activate def test_authentication_request_token_fails(self): responses.add( diff --git a/tests/sentry/integrations/jira_server/test_webhooks.py b/tests/sentry/integrations/jira_server/test_webhooks.py index a874ab103327b9..67e3f4b1c157a1 100644 --- a/tests/sentry/integrations/jira_server/test_webhooks.py +++ b/tests/sentry/integrations/jira_server/test_webhooks.py @@ -1,10 +1,12 @@ from __future__ import absolute_import import jwt +import responses from django.core.urlresolvers import reverse from exam import fixture from mock import patch +from requests.exceptions import ConnectionError from sentry.integrations.jira_server.integration import JiraServerIntegration from sentry.models import ( @@ -17,7 +19,7 @@ from .testutils import EXAMPLE_PRIVATE_KEY -class JiraWebhookEndpointTest(APITestCase): +class JiraServerWebhookEndpointTest(APITestCase): @fixture def integration(self): @@ -172,3 +174,45 @@ def test_post_update_status(self, mock_sync): 'issue': payload['issue'], } ) + + @responses.activate + def test_post_update_status_token_error(self): + responses.add( + method=responses.GET, + url='https://jira.example.org/rest/api/2/status', + body=ConnectionError() + ) + project = self.create_project() + self.create_group(project=project) + integration = self.integration + installation = integration.get_installation(self.organization.id) + installation.update_organization_config({'sync_status_reverse': True}) + + payload = { + 'changelog': { + 'items': [ + { + 'from': '10101', + 'field': 'status', + 'fromString': 'In Progress', + 'to': '10102', + 'toString': 'Done', + 'fieldtype': 'jira', + 'fieldId': 'status' + } + ], + 'id': 12345 + }, + 'issue': { + 'project': { + 'key': 'APP', + 'id': '10000', + }, + 'key': 'APP-1' + } + } + token = self.jwt_token + path = reverse('sentry-extensions-jiraserver-issue-updated', args=[token]) + resp = self.client.post(path, data=payload) + + assert resp.status_code == 400 diff --git a/tests/sentry/integrations/slack/test_event_endpoint.py b/tests/sentry/integrations/slack/test_event_endpoint.py index f68cfd93cb22dd..fc7a7f1b04e8e1 100644 --- a/tests/sentry/integrations/slack/test_event_endpoint.py +++ b/tests/sentry/integrations/slack/test_event_endpoint.py @@ -2,6 +2,7 @@ import json import responses +from django.test.utils import override_settings from sentry import options from sentry.models import Integration, OrganizationIntegration @@ -40,6 +41,7 @@ }""" +@override_settings(SLACK_INTEGRATION_USE_WST=True) class BaseEventTest(APITestCase): def setUp(self): super(BaseEventTest, self).setUp() @@ -50,7 +52,6 @@ def setUp(self): external_id='TXXXXXXX1', metadata={ 'access_token': 'xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', - 'bot_access_token': 'xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', } ) OrganizationIntegration.objects.create( diff --git a/tests/sentry/integrations/slack/test_integration.py b/tests/sentry/integrations/slack/test_integration.py index 52a077fa3bc578..0b3470053af255 100644 --- a/tests/sentry/integrations/slack/test_integration.py +++ b/tests/sentry/integrations/slack/test_integration.py @@ -3,6 +3,7 @@ import responses import six +from django.test.utils import override_settings from six.moves.urllib.parse import parse_qs, urlencode, urlparse from sentry.integrations.slack import SlackIntegrationProvider @@ -13,7 +14,9 @@ class SlackIntegrationTest(IntegrationTestCase): provider = SlackIntegrationProvider - def assert_setup_flow(self, team_id='TXXXXXXX1', authorizing_user_id='UXXXXXXX1'): + def assert_setup_flow(self, team_id='TXXXXXXX1', authorizing_user_id='UXXXXXXX1', + access_token='xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', access_extras=None, + use_oauth_token_endpoint=True): responses.reset() resp = self.client.get(self.init_path) @@ -32,14 +35,35 @@ def assert_setup_flow(self, team_id='TXXXXXXX1', authorizing_user_id='UXXXXXXX1' # easier authorize_params = {k: v[0] for k, v in six.iteritems(params)} + access_json = { + 'ok': True, + 'access_token': access_token, + 'team_id': team_id, + 'team_name': 'Example', + 'authorizing_user_id': authorizing_user_id, + } + + if access_extras is not None: + access_json.update(access_extras) + + # XXX(epurkhiser): The slack workspace token app uses oauth.token, the + # slack bot app uses oauth.access. + if use_oauth_token_endpoint: + responses.add( + responses.POST, 'https://slack.com/api/oauth.token', + json=access_json, + ) + else: + responses.add( + responses.POST, 'https://slack.com/api/oauth.access', + json=access_json, + ) + responses.add( - responses.POST, 'https://slack.com/api/oauth.token', + responses.GET, 'https://slack.com/api/auth.test', json={ 'ok': True, - 'access_token': 'xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', - 'team_id': team_id, - 'team_name': 'Example', - 'authorizing_user_id': authorizing_user_id, + 'user_id': authorizing_user_id, } ) @@ -74,8 +98,51 @@ def assert_setup_flow(self, team_id='TXXXXXXX1', authorizing_user_id='UXXXXXXX1' self.assertDialogSuccess(resp) @responses.activate - def test_basic_flow(self): - self.assert_setup_flow() + def test_bot_flow(self): + self.assert_setup_flow( + use_oauth_token_endpoint=False, + access_token='xoxa-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', + access_extras={ + 'bot': { + 'bot_access_token': 'xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', + } + }) + + integration = Integration.objects.get(provider=self.provider.key) + assert integration.external_id == 'TXXXXXXX1' + assert integration.name == 'Example' + assert integration.metadata == { + 'access_token': 'xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', + 'user_access_token': 'xoxa-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', + 'scopes': sorted(self.provider.identity_oauth_scopes), + 'icon': 'http://example.com/ws_icon.jpg', + 'domain_name': 'test-slack-workspace.slack.com', + } + oi = OrganizationIntegration.objects.get( + integration=integration, + organization=self.organization, + ) + assert oi.config == {} + + idp = IdentityProvider.objects.get( + type='slack', + external_id='TXXXXXXX1', + ) + identity = Identity.objects.get( + idp=idp, + user=self.user, + external_id='UXXXXXXX1', + ) + assert identity.status == IdentityStatus.VALID + + @override_settings(SLACK_INTEGRATION_USE_WST=True) + def assert_wst_setup_flow(self, *args, **kwargs): + self.assert_setup_flow(*args, **kwargs) + + @responses.activate + @override_settings(SLACK_INTEGRATION_USE_WST=True) + def test_wst_flow(self): + self.assert_wst_setup_flow() integration = Integration.objects.get(provider=self.provider.key) assert integration.external_id == 'TXXXXXXX1' @@ -104,9 +171,10 @@ def test_basic_flow(self): assert identity.status == IdentityStatus.VALID @responses.activate + @override_settings(SLACK_INTEGRATION_USE_WST=True) def test_multiple_integrations(self): - self.assert_setup_flow() - self.assert_setup_flow(team_id='TXXXXXXX2', authorizing_user_id='UXXXXXXX2') + self.assert_wst_setup_flow() + self.assert_wst_setup_flow(team_id='TXXXXXXX2', authorizing_user_id='UXXXXXXX2') integrations = Integration.objects.filter(provider=self.provider.key) @@ -131,11 +199,12 @@ def test_multiple_integrations(self): assert identities[0].idp != identities[1].idp @responses.activate + @override_settings(SLACK_INTEGRATION_USE_WST=True) def test_reassign_user(self): - self.assert_setup_flow() + self.assert_wst_setup_flow() identity = Identity.objects.get() assert identity.external_id == 'UXXXXXXX1' - self.assert_setup_flow(authorizing_user_id='UXXXXXXX2') + self.assert_wst_setup_flow(authorizing_user_id='UXXXXXXX2') identity = Identity.objects.get() assert identity.external_id == 'UXXXXXXX2' diff --git a/tests/sentry/integrations/slack/test_notify_action.py b/tests/sentry/integrations/slack/test_notify_action.py index a879cb6ed04f46..127ffa5861239b 100644 --- a/tests/sentry/integrations/slack/test_notify_action.py +++ b/tests/sentry/integrations/slack/test_notify_action.py @@ -22,7 +22,6 @@ def setUp(self): external_id='TXXXXXXX1', metadata={ 'access_token': 'xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', - 'bot_access_token': 'xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx', } ) self.integration.add_organization(event.project.organization, self.user) diff --git a/tests/sentry/lang/native/snapshots/UnrealIntegrationTest/test_merge_unreal_context_event.pysnap b/tests/sentry/lang/native/snapshots/UnrealIntegrationTest/test_merge_unreal_context_event.pysnap index 5827a917af09b9..a4697670e62d50 100644 --- a/tests/sentry/lang/native/snapshots/UnrealIntegrationTest/test_merge_unreal_context_event.pysnap +++ b/tests/sentry/lang/native/snapshots/UnrealIntegrationTest/test_merge_unreal_context_event.pysnap @@ -1,5 +1,5 @@ --- -created: '2019-03-21T12:20:37.810590Z' +created: '2019-04-17T10:59:28.715639Z' creator: sentry source: tests/sentry/lang/native/test_unreal.py --- @@ -92,44 +92,64 @@ stacktrace: frames: - instruction_addr: '0x100d1471' package: ntdll + trust: prewalked - instruction_addr: '0xfd53034' package: KERNEL32 + trust: prewalked - instruction_addr: '0x589c73c6' package: YetAnother + trust: prewalked - instruction_addr: '0x548229e6' package: YetAnother + trust: prewalked - instruction_addr: '0x54814eaa' package: YetAnother + trust: prewalked - instruction_addr: '0x54814e4c' package: YetAnother + trust: prewalked - instruction_addr: '0x54805258' package: YetAnother + trust: prewalked - instruction_addr: '0x571fcd39' package: YetAnother + trust: prewalked - instruction_addr: '0x5739984f' package: YetAnother + trust: prewalked - instruction_addr: '0x5739082f' package: YetAnother + trust: prewalked - instruction_addr: '0x57aafb58' package: YetAnother + trust: prewalked - instruction_addr: '0x57aa121d' package: YetAnother + trust: prewalked - instruction_addr: '0x54d8cf00' package: YetAnother + trust: prewalked - instruction_addr: '0x54d8cc56' package: YetAnother + trust: prewalked - instruction_addr: '0x57a56186' package: YetAnother + trust: prewalked - instruction_addr: '0x57a3e77e' package: YetAnother + trust: prewalked - instruction_addr: '0x56f2f984' package: YetAnother + trust: prewalked - instruction_addr: '0x56f06dd3' package: YetAnother + trust: prewalked - instruction_addr: '0x56cff2ee' package: YetAnother + trust: prewalked - instruction_addr: '0x54be3394' package: YetAnother + trust: prewalked tags: epic_account_id: 2e7d369327054a448be6c8d3601213cb machine_id: C52DC39D-DAF3-5E36-A8D3-BF5F53A5D38F diff --git a/tests/sentry/lang/native/snapshots/test_unreal/test_parse_portable_callstack.pysnap b/tests/sentry/lang/native/snapshots/test_unreal/test_parse_portable_callstack.pysnap new file mode 100644 index 00000000000000..675cb21c4aa6d6 --- /dev/null +++ b/tests/sentry/lang/native/snapshots/test_unreal/test_parse_portable_callstack.pysnap @@ -0,0 +1,20 @@ +--- +created: '2019-04-17T11:13:04.279532Z' +creator: sentry +source: tests/sentry/lang/native/test_unreal.py +--- +- instruction_addr: '0x7ff8b7293691' + package: C:\Windows\System32\ntdll.dll + trust: prewalked +- instruction_addr: '0x7ff8b5aa3034' + package: C:\Windows\System32\kernel32.dll + trust: prewalked +- instruction_addr: '0x7ff84e1e0a28' + package: C:\Unreal\UE4Editor-Renderer.dll + trust: prewalked +- instruction_addr: '0x7ff84e1e3ee2' + package: C:\Unreal\UE4Editor-Renderer.dll + trust: prewalked +- instruction_addr: '0x7ff881116998' + package: C:\Unreal\UE4Editor-ShaderCore.dll + trust: prewalked diff --git a/tests/sentry/lang/native/test_unreal.py b/tests/sentry/lang/native/test_unreal.py index d5a9545f3b5e5f..015fafff5f7c6f 100644 --- a/tests/sentry/lang/native/test_unreal.py +++ b/tests/sentry/lang/native/test_unreal.py @@ -8,7 +8,9 @@ from sentry.testutils import TestCase from sentry.lang.native.minidump import MINIDUMP_ATTACHMENT_TYPE -from sentry.lang.native.unreal import process_unreal_crash, unreal_attachment_type, merge_unreal_context_event, merge_unreal_logs_event, merge_apple_crash_report +from sentry.lang.native.unreal import process_unreal_crash, unreal_attachment_type, \ + merge_unreal_context_event, merge_unreal_logs_event, merge_apple_crash_report, \ + parse_portable_callstack from sentry.models import Event, EventAttachment, UserReport @@ -212,3 +214,55 @@ def test_unreal_crash_with_attachments(self): assert log.name == 'YetAnother.log' # Log file is named after the project assert log.file.type == 'event.attachment' assert log.file.checksum == '24d1c5f75334cd0912cc2670168d593d5fe6c081' + + +def test_parse_portable_callstack(insta_snapshot): + portable_callstack = ( + 'UE4Editor-ShaderCore 0x0000000081060000 + b6998 ' + 'UE4Editor-Renderer 0x000000004da80000 + 763ee2 ' + 'UE4Editor-Renderer 0x000000004da80000 + 760a28 ' + 'KERNEL32 0x00000000b5a90000 + 13034 ' + 'ntdll 0x00000000b7220000 + 73691' + ) + + images = [ + { + "code_file": "C:\\Unreal\\UE4Editor-ShaderCore.dll", + "code_id": "5CB4A59512a000", + "image_addr": "0x7ff881060000", + "debug_file": "UE4Editor-ShaderCore.pdb", + "image_size": 1220608, + "type": "pe", + "debug_id": "19978799-526a-4d94-a18d-4a18ea7e989f-1" + }, + { + "code_file": "C:\\Unreal\\UE4Editor-Renderer.dll", + "code_id": "5CB4A5A6e77000", + "image_addr": "0x7ff84da80000", + "debug_file": "UE4Editor-Renderer.pdb", + "image_size": 15167488, + "type": "pe", + "debug_id": "70bad0d5-0da7-459c-b854-0bb41a753eac-1" + }, + { + "code_file": "C:\\Windows\\System32\\kernel32.dll", + "code_id": "5F488A51b2000", + "image_addr": "0x7ff8b5a90000", + "debug_file": "kernel32.pdb", + "image_size": 729088, + "type": "pe", + "debug_id": "63816243-ec70-4dc0-91bc-31470bac48a3-1" + }, + { + "code_file": "C:\\Windows\\System32\\ntdll.dll", + "code_id": "7E614C221e1000", + "image_addr": "0x7ff8b7220000", + "debug_file": "ntdll.pdb", + "image_size": 1970176, + "type": "pe", + "debug_id": "338c83b3-1707-66b1-728d-0b2ff2f39588-1" + }, + ] + + frames = parse_portable_callstack(portable_callstack, images) + insta_snapshot(frames) diff --git a/tests/sentry/models/test_event.py b/tests/sentry/models/test_event.py index ec106bea97e9da..607b37cb548615 100644 --- a/tests/sentry/models/test_event.py +++ b/tests/sentry/models/test_event.py @@ -1,11 +1,12 @@ from __future__ import absolute_import +import pytest import pickle from sentry.models import Environment -from sentry.testutils import TestCase from sentry.db.models.fields.node import NodeData from sentry.event_manager import EventManager +from sentry.testutils import TestCase class EventTest(TestCase): @@ -162,6 +163,42 @@ def test_ip_address(self): assert event.ip_address is None +@pytest.mark.django_db +def test_renormalization(monkeypatch, factories, task_runner, default_project): + from semaphore.processing import StoreNormalizer + + old_normalize = StoreNormalizer.normalize_event + normalize_mock_calls = [] + + def normalize(*args, **kwargs): + normalize_mock_calls.append(1) + return old_normalize(*args, **kwargs) + + monkeypatch.setattr('semaphore.processing.StoreNormalizer.normalize_event', + normalize) + + sample_mock_calls = [] + + def sample(*args, **kwargs): + sample_mock_calls.append(1) + return False + + with task_runner(): + factories.store_event( + data={ + 'event_id': 'a' * 32, + 'environment': 'production', + }, + project_id=default_project.id + ) + + # Assert we only renormalize this once. If this assertion fails it's likely + # that you will encounter severe performance issues during event processing + # or postprocessing. + assert len(normalize_mock_calls) == 1 + assert len(sample_mock_calls) == 0 + + class EventGetLegacyMessageTest(TestCase): def test_message(self): event = self.create_event(message='foo bar') diff --git a/yarn.lock b/yarn.lock index d1e3ee517c04db..913cbbac70d04a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9079,10 +9079,10 @@ mockdate@2.0.2: resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-2.0.2.tgz#5ae0c0eaf8fe23e009cd01f9889b42c4f634af12" integrity sha1-WuDA6vj+I+AJzQH5iJtCxPY0rxI= -moment-timezone@0.5.23: - version "0.5.23" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.23.tgz#7cbb00db2c14c71b19303cb47b0fb0a6d8651463" - integrity sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w== +moment-timezone@0.5.25: + version "0.5.25" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.25.tgz#a11bfa2f74e088327f2cd4c08b3e7bdf55957810" + integrity sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw== dependencies: moment ">= 2.9.0"