Skip to content
15 changes: 13 additions & 2 deletions src/sentry/buffer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
"""

from django.db.models import F
from sentry.utils.queue import maybe_async
from sentry.signals import buffer_incr_complete
from sentry.tasks.process_buffer import process_incr
from sentry.utils.queue import maybe_async


class Buffer(object):
Expand Down Expand Up @@ -42,7 +43,17 @@ def process(self, model, columns, filters, extra=None):
update_kwargs = dict((c, F(c) + v) for c, v in columns.iteritems())
if extra:
update_kwargs.update(extra)
model.objects.create_or_update(

_, created = model.objects.create_or_update(
defaults=update_kwargs,
**filters
)

buffer_incr_complete.send_robust(
model=model,
columns=columns,
filters=filters,
extra=extra,
created=created,
sender=model,
)
1 change: 0 additions & 1 deletion src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@
'sentry.plugins.sentry_mail',
'sentry.plugins.sentry_servers',
'sentry.plugins.sentry_urls',
'sentry.plugins.sentry_user_emails',
'sentry.plugins.sentry_useragents',
'social_auth',
'south',
Expand Down
14 changes: 8 additions & 6 deletions src/sentry/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,9 +697,11 @@ class User(Interface):
"""
An interface which describes the authenticated User for a request.

All data is arbitrary and optional other than the ``is_authenticated``
field which should be a boolean value indiciating whether the user
is logged in or not.
All data is arbitrary and optional other than the ``email``
field which should be a string representing the user's email
address.

The email will automatically be tagged and used to determine unique users.

>>> {
>>> "is_authenticated": true,
Expand All @@ -709,11 +711,11 @@ class User(Interface):
>>> }
"""

def __init__(self, is_authenticated, **kwargs):
self.is_authenticated = is_authenticated
def __init__(self, email=None, **kwargs):
self.id = kwargs.pop('id', None)
self.email = email
self.username = kwargs.pop('username', None)
self.email = kwargs.pop('email', None)
self.is_authenticated = kwargs.get('is_authenticated', None)
self.data = kwargs

def serialize(self):
Expand Down
8 changes: 6 additions & 2 deletions src/sentry/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ def from_kwargs(self, project, **kwargs):

if is_new:
try:
regression_signal.send(sender=self.model, instance=group)
regression_signal.send_robust(sender=self.model, instance=group)
except Exception, e:
transaction.rollback_unless_managed(using=group._state.db)
logger.exception(u'Error sending regression signal: %s', e)
Expand Down Expand Up @@ -594,7 +594,7 @@ def _create_group(self, event, tags=None, **kwargs):
silence = silence_timedelta.days * 86400 + silence_timedelta.seconds

app.buffer.incr(self.model, update_kwargs, {
'pk': group.pk,
'id': group.id,
}, extra)
else:
# TODO: this update should actually happen as part of create
Expand Down Expand Up @@ -641,6 +641,10 @@ def _create_group(self, event, tags=None, **kwargs):
('level', event.get_level_display()),
]

user_data = event.user_data
if user_data.get('email'):
tags.append(('user_email', user_data['email']))

self.add_tags(group, tags)

return group, is_new, is_sample
Expand Down
257 changes: 257 additions & 0 deletions src/sentry/migrations/0071_auto__add_field_group_users_seen.py

Large diffs are not rendered by default.

31 changes: 30 additions & 1 deletion src/sentry/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from sentry.manager import GroupManager, ProjectManager, \
MetaManager, InstanceMetaManager, SearchDocumentManager, BaseManager, \
UserOptionManager, FilterKeyManager, TeamManager
from sentry.signals import buffer_incr_complete
from sentry.utils import cached_property, \
MockDjangoRequest
from sentry.utils.models import Model, GzippedDictField, update
Expand Down Expand Up @@ -389,13 +390,18 @@ def message_top(self):
return self.culprit
return truncatechars(self.message.splitlines()[0], 100)

@property
def user_data(self):
return self.data.get('sentry.interfaces.User', {})


class Group(MessageBase):
"""
Aggregated message which summarizes a set of Events.
"""
status = models.PositiveIntegerField(default=0, choices=STATUS_LEVELS, db_index=True)
times_seen = models.PositiveIntegerField(default=1, db_index=True)
users_seen = models.PositiveIntegerField(default=0, db_index=True)
last_seen = models.DateTimeField(default=timezone.now, db_index=True)
first_seen = models.DateTimeField(default=timezone.now, db_index=True)
resolved_at = models.DateTimeField(null=True, db_index=True)
Expand Down Expand Up @@ -936,6 +942,25 @@ def set_language_on_logon(request, user, **kwargs):
if language and hasattr(request, 'session'):
request.session['django_language'] = language


@buffer_incr_complete.connect(sender=MessageFilterValue, weak=False)
def record_user_count(filters, created, **kwargs):
from sentry import app

if not created:
# if it's not a new row, it's not a unique user
return

if filters.get('key') != 'user_email':
return

app.buffer.incr(Group, {
'users_seen': 1,
}, {
'id': filters['group'].id,
})


# Signal registration
post_syncdb.connect(
create_default_project,
Expand Down Expand Up @@ -972,6 +997,10 @@ def set_language_on_logon(request, user, **kwargs):
dispatch_uid="remove_key_for_team_member",
weak=False,
)
user_logged_in.connect(set_language_on_logon)
user_logged_in.connect(
set_language_on_logon,
dispatch_uid="set_language_on_logon",
weak=False
)

add_introspection_rules([], ["^social_auth\.fields\.JSONField"])
7 changes: 0 additions & 7 deletions src/sentry/plugins/sentry_user_emails/__init__.py

This file was deleted.

39 changes: 0 additions & 39 deletions src/sentry/plugins/sentry_user_emails/models.py

This file was deleted.

25 changes: 23 additions & 2 deletions src/sentry/signals.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
import django
from functools import wraps
from django.dispatch import Signal

regression_signal = django.dispatch.Signal(providing_args=["instance"])

class BetterSignal(Signal):
def connect(self, receiver=None, **kwargs):
"""
Support decorator syntax:

>>> @signal.connect(sender=type)
>>> def my_receiver(**kwargs):
>>> pass

"""
def wrapped(func):
return super(BetterSignal, self).connect(func, **kwargs)

if receiver is None:
return wrapped
return wraps(receiver)(wrapped(receiver))


regression_signal = BetterSignal(providing_args=["instance"])
buffer_incr_complete = BetterSignal(providing_args=["model", "columns", "extra", "result"])
37 changes: 37 additions & 0 deletions tests/sentry/manager/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,32 @@ def test_valid_only_message(self):
self.assertEquals(event.message, 'foo')
self.assertEquals(event.project_id, 1)

def test_records_users_seen(self):
# TODO: we could lower the level of this test by just testing our signal receiver's logic
event = Group.objects.from_kwargs(1, message='foo', **{
'sentry.interfaces.User': {
'email': 'foo@example.com',
},
})
group = Group.objects.get(id=event.group_id)
assert group.users_seen == 1

event = Group.objects.from_kwargs(1, message='foo', **{
'sentry.interfaces.User': {
'email': 'foo@example.com',
},
})
group = Group.objects.get(id=event.group_id)
assert group.users_seen == 1

event = Group.objects.from_kwargs(1, message='foo', **{
'sentry.interfaces.User': {
'email': 'bar@example.com',
},
})
group = Group.objects.get(id=event.group_id)
assert group.users_seen == 2

def test_valid_timestamp_without_tz(self):
# TODO: this doesnt error, but it will throw a warning. What should we do?
with self.Settings(USE_TZ=True):
Expand Down Expand Up @@ -134,6 +160,17 @@ def test_tags_as_list(self, add_tags):
group = event.group
add_tags.assert_called_once_with(group, [('foo', 'bar'), ('logger', 'root'), ('level', 'error')])

@mock.patch('sentry.manager.send_group_processors', mock.Mock())
@mock.patch('sentry.manager.GroupManager.add_tags')
def test_does_tag_user_email(self, add_tags):
event = Group.objects.from_kwargs(1, message='foo', **{
'sentry.interfaces.User': {
'email': 'foo@example.com',
},
})
group = event.group
add_tags.assert_called_once_with(group, [('logger', 'root'), ('level', 'error'), ('user_email', 'foo@example.com')])

@mock.patch('sentry.manager.send_group_processors', mock.Mock())
@mock.patch('sentry.manager.GroupManager.add_tags')
def test_tags_as_dict(self, add_tags):
Expand Down