Skip to content

Commit

Permalink
fix(grouping): Store original in_app for idempotent regrouping (#13540)
Browse files Browse the repository at this point in the history
Fixes grouping enhancement display to always operate on the original value of the `in_app` flag. Eventually, this will allow to regroup events based on their original payload.
  • Loading branch information
jan-auer authored Jun 12, 2019
1 parent 785851d commit 00d3c85
Show file tree
Hide file tree
Showing 42 changed files with 2,418 additions and 692 deletions.
2 changes: 1 addition & 1 deletion requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ redis>=2.10.3,<2.10.6
requests-oauthlib==0.3.3
requests[security]>=2.20.0,<2.21.0
selenium==3.141.0
semaphore>=0.4.36,<0.5.0
semaphore>=0.4.37,<0.5.0
sentry-sdk>=0.9.0
setproctitle>=1.1.7,<1.2.0
simplejson>=3.2.0,<3.9.0
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/api/endpoints/event_grouping_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ def get(self, request, project, event_id):
hashes = event.get_hashes()

try:
variants = event.get_grouping_variants(config_name)
variants = event.get_grouping_variants(force_config=config_name,
normalize_stacktraces=True)
except GroupingConfigNotFound:
raise ResourceDoesNotExist(detail='Unknown grouping config')

Expand Down
8 changes: 7 additions & 1 deletion src/sentry/grouping/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ def load_grouping_config(config_dict=None):
return CONFIGURATIONS[config_id](**config_dict)


def load_default_grouping_config():
return load_grouping_config(config_dict=None)


def get_fingerprinting_config_for_project(project):
from sentry.grouping.fingerprinting import FingerprintingRules, \
InvalidFingerprintingConfig
Expand Down Expand Up @@ -201,9 +205,11 @@ def get_grouping_variants_for_event(event, config=None):
'custom-fingerprint': CustomFingerprintVariant(fingerprint),
}

if config is None:
config = load_default_grouping_config()

# At this point we need to calculate the default event values. If the
# fingerprint is salted we will wrap it.
config = load_grouping_config(config)
components = _get_calculated_grouping_variants_for_event(event, config)
rv = {}

Expand Down
32 changes: 22 additions & 10 deletions src/sentry/grouping/enhancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
from parsimonious.exceptions import ParseError

from sentry import projectoptions
from sentry.stacktraces.functions import set_in_app
from sentry.stacktraces.platform import get_behavior_family_for_platform
from sentry.grouping.utils import get_rule_bool
from sentry.utils.compat import implements_to_string
from sentry.utils.glob import glob_match
from sentry.utils.safe import get_path


# Grammar is defined in EBNF syntax.
Expand Down Expand Up @@ -166,7 +168,7 @@ class Action(object):
def apply_modifications_to_frame(self, frames, idx):
pass

def update_frame_components_contributions(self, components, idx, rule=None):
def update_frame_components_contributions(self, components, frames, idx, rule=None):
pass

def modify_stack_state(self, state, rule):
Expand Down Expand Up @@ -207,23 +209,35 @@ def _slice_to_range(self, seq, idx):
return seq[idx + 1:]
return []

def _in_app_changed(self, frame, component):
orig_in_app = get_path(frame, 'data', 'orig_in_app')

if orig_in_app is not None:
if orig_in_app == -1:
orig_in_app = None
return orig_in_app != frame.get('in_app')
else:
return self.flag == component.contributes

def apply_modifications_to_frame(self, frames, idx):
# Grouping is not stored on the frame
if self.key == 'group':
return
for frame in self._slice_to_range(frames, idx):
if self.key == 'app':
frame['in_app'] = self.flag
set_in_app(frame, self.flag)

def update_frame_components_contributions(self, components, idx, rule=None):
def update_frame_components_contributions(self, components, frames, idx, rule=None):
rule_hint = 'grouping enhancement rule'
if rule:
rule_hint = '%s (%s)' % (
rule_hint,
rule.matcher_description,
)

for component in self._slice_to_range(components, idx):
sliced_components = self._slice_to_range(components, idx)
sliced_frames = self._slice_to_range(frames, idx)
for component, frame in izip(sliced_components, sliced_frames):
if self.key == 'group' and self.flag != component.contributes:
component.update(
contributes=self.flag,
Expand All @@ -232,9 +246,7 @@ def update_frame_components_contributions(self, components, idx, rule=None):
)
# The in app flag was set by `apply_modifications_to_frame`
# but we want to add a hint if there is none yet.
elif self.key == 'app' and \
self.flag == component.contributes and \
component.hint is None:
elif self.key == 'app' and self._in_app_changed(frame, component):
component.update(
hint='marked %s by %s' % (
self.flag and 'in-app' or 'out of app', rule_hint)
Expand Down Expand Up @@ -311,7 +323,7 @@ def update_frame_components_contributions(self, components, frames, platform):
actions = rule.get_matching_frame_actions(frame, platform)
for action in actions or ():
action.update_frame_components_contributions(
components, idx, rule=rule)
components, frames, idx, rule=rule)
action.modify_stack_state(stack_state, rule)

# Use the stack state to update frame contributions again
Expand Down Expand Up @@ -406,8 +418,8 @@ def __init__(self, matchers, actions):
@property
def matcher_description(self):
rv = ' '.join(x.description for x in self.matchers)
if any(x.range is not None for x in self.actions):
rv += ' - ranged'
for action in self.actions:
rv = '%s %s' % (rv, action)
return rv

def as_dict(self):
Expand Down
15 changes: 13 additions & 2 deletions src/sentry/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,19 @@ def get_hashes(self, force_config=None):
return filter(None, [
x.get_hash() for x in self.get_grouping_variants(force_config).values()])

def get_grouping_variants(self, force_config=None):
def get_grouping_variants(self, force_config=None, normalize_stacktraces=False):
"""
This is similar to `get_hashes` but will instead return the
grouping components for each variant in a dictionary.
If `normalize_stacktraces` is set to `True` then the event data will be
modified for `in_app` in addition to event variants being created. This
means that after calling that function the event data has been modified
in place.
"""
from sentry.grouping.api import get_grouping_variants_for_event
from sentry.grouping.api import get_grouping_variants_for_event, \
load_grouping_config
from sentry.stacktraces.processing import normalize_stacktraces_for_grouping

# Forcing configs has two separate modes. One is where just the
# config ID is given in which case it's merged with the stored or
Expand All @@ -208,6 +215,10 @@ def get_grouping_variants(self, force_config=None):
else:
config = self.data.get('grouping_config')

config = load_grouping_config(config)
if normalize_stacktraces:
normalize_stacktraces_for_grouping(self.data, config)

return get_grouping_variants_for_event(self, config)

def get_primary_hash(self):
Expand Down
13 changes: 12 additions & 1 deletion src/sentry/stacktraces/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re

from sentry.stacktraces.platform import get_behavior_family_for_platform
from sentry.utils.safe import setdefault_path


_windecl_hash = re.compile(r'^@?(.*?)@[0-9]+$')
Expand Down Expand Up @@ -109,7 +110,7 @@ def trim_function_name(function, platform, normalize_lambdas=True):
return function

# Chop off C++ trailers
while 1:
while True:
match = _cpp_trailer_re.search(function)
if match is None:
break
Expand Down Expand Up @@ -199,3 +200,13 @@ def get_function_name_for_frame(frame, platform=None):
rv = frame.get('function')
if rv:
return trim_function_name(rv, frame.get('platform') or platform)


def set_in_app(frame, value):
orig_in_app = frame.get('in_app')
if orig_in_app == value:
return

orig_in_app = int(orig_in_app) if orig_in_app is not None else -1
setdefault_path(frame, 'data', 'orig_in_app', value=orig_in_app)
frame['in_app'] = value
13 changes: 10 additions & 3 deletions src/sentry/stacktraces/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from sentry.utils.cache import cache
from sentry.utils.hashlib import hash_values
from sentry.utils.safe import get_path, safe_execute
from sentry.stacktraces.functions import trim_function_name
from sentry.stacktraces.functions import set_in_app, trim_function_name


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -218,12 +218,12 @@ def _normalize_in_app(stacktrace, platform=None, sdk_info=None):
for frame in stacktrace:
# If all frames are in_app, flip all of them. This is expected by the UI
if not has_system_frames:
frame['in_app'] = False
set_in_app(frame, False)

# Default to false in all cases where processors or grouping enhancers
# have not yet set in_app.
elif frame.get('in_app') is None:
frame['in_app'] = False
set_in_app(frame, False)


def normalize_stacktraces_for_grouping(data, grouping_config=None):
Expand All @@ -250,6 +250,13 @@ def normalize_stacktraces_for_grouping(data, grouping_config=None):
# unnecessarily.
for frames in stacktraces:
for frame in frames:
# Restore the original in_app value before the first grouping
# enhancers have been run. This allows to re-apply grouping
# enhancers on the original frame data.
orig_in_app = get_path(frame, 'data', 'orig_in_app')
if orig_in_app is not None:
frame['in_app'] = None if orig_in_app == -1 else bool(orig_in_app)

if frame.get('raw_function') is not None:
continue
raw_func = frame.get('function')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
created: '2019-04-30T08:25:08.407188Z'
created: '2019-06-05T20:07:56.986283Z'
creator: sentry
source: tests/sentry/event_manager/interfaces/test_exception.py
---
Expand Down Expand Up @@ -70,6 +70,8 @@ to_json:
stacktrace:
frames:
- abs_path: foo/baz.py
data:
orig_in_app: 1
filename: foo/baz.py
in_app: false
lineno: 1
Expand All @@ -79,6 +81,8 @@ to_json:
stacktrace:
frames:
- abs_path: foo/baz.py
data:
orig_in_app: 1
filename: foo/baz.py
in_app: false
lineno: 1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
created: '2019-05-16T11:55:53.361870Z'
created: '2019-06-05T11:45:12.934622Z'
creator: sentry
source: tests/sentry/grouping/test_variants.py
---
Expand Down Expand Up @@ -27,10 +27,10 @@ system:
system*
exception*
stacktrace*
frame (ignored because only 1 frame is considered by grouping enhancement rule (family:native))
frame (ignored because only 1 frame is considered by grouping enhancement rule (family:native max-frames=1))
function* (isolated function)
u'Scaleform::GFx::IME::GImeNamesManagerVista::OnActivated'
frame (ignored because only 1 frame is considered by grouping enhancement rule (family:native))
frame (ignored because only 1 frame is considered by grouping enhancement rule (family:native max-frames=1))
function* (isolated function)
u'Scaleform::GFx::AS3::IMEManager::DispatchEvent'
frame*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
created: '2019-05-10T11:49:03.468450Z'
created: '2019-06-05T11:45:13.045473Z'
creator: sentry
source: tests/sentry/grouping/test_variants.py
---
Expand Down Expand Up @@ -64,7 +64,7 @@ app:
u'bound_func'
lineno (line number is used only if module or filename are available)
25
frame (ignored by grouping enhancement rule (path:**/release_webhook.py - ranged))
frame (ignored by grouping enhancement rule (path:**/release_webhook.py v-group))
module*
u'sentry.web.frontend.release_webhook'
filename (module takes precedence)
Expand Down Expand Up @@ -130,7 +130,7 @@ system:
system*
exception*
stacktrace*
frame (ignored by grouping enhancement rule (path:**/release_webhook.py - ranged))
frame (ignored by grouping enhancement rule (path:**/release_webhook.py v-group))
module*
u'django.core.handlers.base'
filename (module takes precedence)
Expand All @@ -141,7 +141,7 @@ system:
u'get_response'
lineno (line number is used only if module or filename are available)
112
frame (ignored by grouping enhancement rule (path:**/release_webhook.py - ranged))
frame (ignored by grouping enhancement rule (path:**/release_webhook.py v-group))
module*
u'django.views.generic.base'
filename (module takes precedence)
Expand All @@ -152,7 +152,7 @@ system:
u'view'
lineno (line number is used only if module or filename are available)
69
frame (ignored by grouping enhancement rule (path:**/release_webhook.py - ranged))
frame (ignored by grouping enhancement rule (path:**/release_webhook.py v-group))
module*
u'django.utils.decorators'
filename (module takes precedence)
Expand All @@ -163,7 +163,7 @@ system:
u'_wrapper'
lineno (line number is used only if module or filename are available)
29
frame (ignored by grouping enhancement rule (path:**/release_webhook.py - ranged))
frame (ignored by grouping enhancement rule (path:**/release_webhook.py v-group))
module*
u'django.views.decorators.csrf'
filename (module takes precedence)
Expand All @@ -174,7 +174,7 @@ system:
u'wrapped_view'
lineno (line number is used only if module or filename are available)
57
frame (ignored by grouping enhancement rule (path:**/release_webhook.py - ranged))
frame (ignored by grouping enhancement rule (path:**/release_webhook.py v-group))
module*
u'django.utils.decorators'
filename (module takes precedence)
Expand All @@ -185,7 +185,7 @@ system:
u'bound_func'
lineno (line number is used only if module or filename are available)
25
frame (ignored by grouping enhancement rule (path:**/release_webhook.py - ranged))
frame (ignored by grouping enhancement rule (path:**/release_webhook.py v-group))
module*
u'sentry.web.frontend.release_webhook'
filename (module takes precedence)
Expand All @@ -196,7 +196,7 @@ system:
u'dispatch'
lineno (line number is used only if module or filename are available)
37
frame (ignored by grouping enhancement rule (path:**/release_webhook.py - ranged))
frame (ignored by grouping enhancement rule (path:**/release_webhook.py v-group))
module*
u'django.views.generic.base'
filename (module takes precedence)
Expand Down
Loading

0 comments on commit 00d3c85

Please sign in to comment.