Browse Source

Move user tracking into standard tag infrastructure

David Cramer 12 years ago
parent
commit
4e1820445f

+ 7 - 1
src/sentry/manager.py

@@ -689,7 +689,12 @@ class GroupManager(BaseManager, ChartMixin):
         project = group.project
         date = group.last_seen
 
-        for key, value in itertools.ifilter(lambda x: bool(x[1]), tags):
+        for tag_item in tags:
+            if len(tag_item) == 2:
+                (key, value), data = tag_item, None
+            else:
+                key, value, data = tag_item
+
             if not value:
                 continue
 
@@ -705,6 +710,7 @@ class GroupManager(BaseManager, ChartMixin):
                 'value': value,
             }, {
                 'last_seen': date,
+                'data': data,
             })
 
             app.buffer.incr(GroupTag, {

+ 324 - 0
src/sentry/migrations/0097_auto__del_affecteduserbygroup__del_unique_affecteduserbygroup_project_.py

@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Removing unique constraint on 'TrackedUser', fields ['project', 'ident']
+        db.delete_unique('sentry_trackeduser', ['project_id', 'ident'])
+
+        # Removing unique constraint on 'AffectedUserByGroup', fields ['project', 'tuser', 'group']
+        db.delete_unique('sentry_affecteduserbygroup', ['project_id', 'tuser_id', 'group_id'])
+
+        # Deleting model 'AffectedUserByGroup'
+        db.delete_table('sentry_affecteduserbygroup')
+
+        # Deleting model 'TrackedUser'
+        db.delete_table('sentry_trackeduser')
+
+        # Deleting field 'Group.users_seen'
+        db.delete_column('sentry_groupedmessage', 'users_seen')
+
+    def backwards(self, orm):
+        raise NotImplementedError("This is no time machine bro")
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'sentry.accessgroup': {
+            'Meta': {'unique_together': "(('team', 'name'),)", 'object_name': 'AccessGroup'},
+            'data': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'managed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'projects': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['sentry.Project']", 'symmetrical': 'False'}),
+            'team': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Team']"}),
+            'type': ('django.db.models.fields.IntegerField', [], {'default': '50'})
+        },
+        'sentry.activity': {
+            'Meta': {'object_name': 'Activity'},
+            'data': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'datetime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Event']", 'null': 'True'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']", 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ident': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']"}),
+            'type': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
+        },
+        'sentry.alert': {
+            'Meta': {'object_name': 'Alert'},
+            'data': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'datetime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']", 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']"}),
+            'related_groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'related_alerts'", 'symmetrical': 'False', 'through': "orm['sentry.AlertRelatedGroup']", 'to': "orm['sentry.Group']"}),
+            'status': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'})
+        },
+        'sentry.alertrelatedgroup': {
+            'Meta': {'unique_together': "(('group', 'alert'),)", 'object_name': 'AlertRelatedGroup'},
+            'alert': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Alert']"}),
+            'data': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        'sentry.event': {
+            'Meta': {'unique_together': "(('project', 'event_id'),)", 'object_name': 'Event', 'db_table': "'sentry_message'"},
+            'checksum': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
+            'culprit': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'db_column': "'view'", 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'datetime': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'event_id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'db_column': "'message_id'"}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'event_set'", 'null': 'True', 'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '40', 'db_index': 'True', 'blank': 'True'}),
+            'logger': ('django.db.models.fields.CharField', [], {'default': "'root'", 'max_length': '64', 'db_index': 'True', 'blank': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'num_comments': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True'}),
+            'platform': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']", 'null': 'True'}),
+            'server_name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}),
+            'site': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'db_index': 'True'}),
+            'time_spent': ('django.db.models.fields.FloatField', [], {'null': 'True'})
+        },
+        'sentry.eventmapping': {
+            'Meta': {'unique_together': "(('project', 'event_id'),)", 'object_name': 'EventMapping'},
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'event_id': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']"})
+        },
+        'sentry.group': {
+            'Meta': {'unique_together': "(('project', 'checksum'),)", 'object_name': 'Group', 'db_table': "'sentry_groupedmessage'"},
+            'active_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
+            'checksum': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
+            'culprit': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'db_column': "'view'", 'blank': 'True'}),
+            'data': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'first_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_public': ('django.db.models.fields.NullBooleanField', [], {'default': 'False', 'null': 'True', 'blank': 'True'}),
+            'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'default': '40', 'db_index': 'True', 'blank': 'True'}),
+            'logger': ('django.db.models.fields.CharField', [], {'default': "'root'", 'max_length': '64', 'db_index': 'True', 'blank': 'True'}),
+            'message': ('django.db.models.fields.TextField', [], {}),
+            'num_comments': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'null': 'True'}),
+            'platform': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']", 'null': 'True'}),
+            'resolved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'status': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
+            'time_spent_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'time_spent_total': ('django.db.models.fields.FloatField', [], {'default': '0'}),
+            'times_seen': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1', 'db_index': 'True'})
+        },
+        'sentry.groupbookmark': {
+            'Meta': {'unique_together': "(('project', 'user', 'group'),)", 'object_name': 'GroupBookmark'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bookmark_set'", 'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'bookmark_set'", 'to': "orm['sentry.Project']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sentry_bookmark_set'", 'to': "orm['auth.User']"})
+        },
+        'sentry.groupcountbyminute': {
+            'Meta': {'unique_together': "(('project', 'group', 'date'),)", 'object_name': 'GroupCountByMinute', 'db_table': "'sentry_messagecountbyminute'"},
+            'date': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']", 'null': 'True'}),
+            'time_spent_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'time_spent_total': ('django.db.models.fields.FloatField', [], {'default': '0'}),
+            'times_seen': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'sentry.groupmeta': {
+            'Meta': {'unique_together': "(('group', 'key'),)", 'object_name': 'GroupMeta'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'value': ('django.db.models.fields.TextField', [], {})
+        },
+        'sentry.grouptag': {
+            'Meta': {'unique_together': "(('project', 'key', 'value', 'group'),)", 'object_name': 'GroupTag', 'db_table': "'sentry_messagefiltervalue'"},
+            'first_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'db_index': 'True'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'db_index': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']", 'null': 'True'}),
+            'times_seen': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'sentry.grouptagkey': {
+            'Meta': {'unique_together': "(('project', 'group', 'key'),)", 'object_name': 'GroupTagKey'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']", 'null': 'True'}),
+            'values_seen': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'sentry.lostpasswordhash': {
+            'Meta': {'object_name': 'LostPasswordHash'},
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'hash': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
+        },
+        'sentry.option': {
+            'Meta': {'object_name': 'Option'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
+            'value': ('picklefield.fields.PickledObjectField', [], {})
+        },
+        'sentry.pendingteammember': {
+            'Meta': {'unique_together': "(('team', 'email'),)", 'object_name': 'PendingTeamMember'},
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pending_member_set'", 'to': "orm['sentry.Team']"}),
+            'type': ('django.db.models.fields.IntegerField', [], {'default': '50'})
+        },
+        'sentry.project': {
+            'Meta': {'unique_together': "(('team', 'slug'),)", 'object_name': 'Project'},
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sentry_owned_project_set'", 'null': 'True', 'to': "orm['auth.User']"}),
+            'platform': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+            'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'null': 'True'}),
+            'status': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'db_index': 'True'}),
+            'team': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Team']", 'null': 'True'})
+        },
+        'sentry.projectcountbyminute': {
+            'Meta': {'unique_together': "(('project', 'date'),)", 'object_name': 'ProjectCountByMinute'},
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']", 'null': 'True'}),
+            'time_spent_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'time_spent_total': ('django.db.models.fields.FloatField', [], {'default': '0'}),
+            'times_seen': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'sentry.projectkey': {
+            'Meta': {'object_name': 'ProjectKey'},
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'key_set'", 'to': "orm['sentry.Project']"}),
+            'public_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}),
+            'secret_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+            'user_added': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'keys_added_set'", 'null': 'True', 'to': "orm['auth.User']"})
+        },
+        'sentry.projectoption': {
+            'Meta': {'unique_together': "(('project', 'key'),)", 'object_name': 'ProjectOption', 'db_table': "'sentry_projectoptions'"},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']"}),
+            'value': ('picklefield.fields.PickledObjectField', [], {})
+        },
+        'sentry.searchdocument': {
+            'Meta': {'unique_together': "(('project', 'group'),)", 'object_name': 'SearchDocument'},
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'date_changed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Group']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']"}),
+            'status': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'total_events': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'})
+        },
+        'sentry.searchtoken': {
+            'Meta': {'unique_together': "(('document', 'field', 'token'),)", 'object_name': 'SearchToken'},
+            'document': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'token_set'", 'to': "orm['sentry.SearchDocument']"}),
+            'field': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '64'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'times_seen': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        },
+        'sentry.tagkey': {
+            'Meta': {'unique_together': "(('project', 'key'),)", 'object_name': 'TagKey', 'db_table': "'sentry_filterkey'"},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']"}),
+            'values_seen': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        'sentry.tagvalue': {
+            'Meta': {'unique_together': "(('project', 'key', 'value'),)", 'object_name': 'TagValue', 'db_table': "'sentry_filtervalue'"},
+            'data': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'first_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+            'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'db_index': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']", 'null': 'True'}),
+            'times_seen': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'value': ('django.db.models.fields.CharField', [], {'max_length': '200'})
+        },
+        'sentry.team': {
+            'Meta': {'object_name': 'Team'},
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'members': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'team_memberships'", 'symmetrical': 'False', 'through': "orm['sentry.TeamMember']", 'to': "orm['auth.User']"}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+        },
+        'sentry.teammember': {
+            'Meta': {'unique_together': "(('team', 'user'),)", 'object_name': 'TeamMember'},
+            'date_added': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'team': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'member_set'", 'to': "orm['sentry.Team']"}),
+            'type': ('django.db.models.fields.IntegerField', [], {'default': '50'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sentry_teammember_set'", 'to': "orm['auth.User']"})
+        },
+        'sentry.useroption': {
+            'Meta': {'unique_together': "(('user', 'project', 'key'),)", 'object_name': 'UserOption'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sentry.Project']", 'null': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'value': ('picklefield.fields.PickledObjectField', [], {})
+        }
+    }
+
+    complete_apps = ['sentry']

+ 0 - 55
src/sentry/models.py

@@ -518,7 +518,6 @@ class Group(EventBase):
     """
     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)
@@ -979,45 +978,6 @@ class LostPasswordHash(Model):
             logger.exception(e)
 
 
-class TrackedUser(Model):
-    project = models.ForeignKey(Project)
-    ident = models.CharField(max_length=200)
-    email = models.EmailField(null=True)
-    data = GzippedDictField(blank=True, null=True)
-    last_seen = models.DateTimeField(default=timezone.now, db_index=True)
-    first_seen = models.DateTimeField(default=timezone.now, db_index=True)
-    num_events = models.PositiveIntegerField(default=0)
-    groups = models.ManyToManyField(Group, through='sentry.AffectedUserByGroup')
-
-    objects = BaseManager()
-
-    class Meta:
-        unique_together = (('project', 'ident'),)
-
-    __repr__ = sane_repr('project_id', 'ident', 'email')
-
-
-class AffectedUserByGroup(Model):
-    """
-    Stores a count of how many times a ``Group`` has affected
-    a user.
-    """
-    project = models.ForeignKey(Project)
-    tuser = models.ForeignKey(TrackedUser, null=True)
-    group = models.ForeignKey(Group)
-    ident = models.CharField(max_length=200, null=True)
-    times_seen = models.PositiveIntegerField(default=0)
-    last_seen = models.DateTimeField(default=timezone.now, db_index=True)
-    first_seen = models.DateTimeField(default=timezone.now, db_index=True)
-
-    objects = BaseManager()
-
-    class Meta:
-        unique_together = (('project', 'tuser', 'group'),)
-
-    __repr__ = sane_repr('project_id', 'group_id', 'tuser_id')
-
-
 class Activity(Model):
     COMMENT = 0
     SET_RESOLVED = 1
@@ -1296,21 +1256,6 @@ def record_group_tag_count(filters, created, **kwargs):
     })
 
 
-@buffer_incr_complete.connect(sender=AffectedUserByGroup, 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
-
-    app.buffer.incr(Group, {
-        'users_seen': 1,
-    }, {
-        'id': filters['group'].id,
-    })
-
-
 @regression_signal.connect(weak=False)
 def create_regression_activity(instance, **kwargs):
     if instance.times_seen == 1:

+ 1 - 2
src/sentry/tasks/cleanup.py

@@ -27,7 +27,7 @@ def cleanup(days=30, project=None, chunk_size=1000, **kwargs):
     from sentry.models import (
         Group, Event, GroupCountByMinute,
         GroupTag, TagValue, ProjectCountByMinute, Alert,
-        SearchDocument, Activity, AffectedUserByGroup, LostPasswordHash)
+        SearchDocument, Activity, LostPasswordHash)
 
     GENERIC_DELETES = (
         (SearchDocument, 'date_changed'),
@@ -36,7 +36,6 @@ def cleanup(days=30, project=None, chunk_size=1000, **kwargs):
         (GroupTag, 'last_seen'),
         (Event, 'datetime'),
         (Activity, 'datetime'),
-        (AffectedUserByGroup, 'last_seen'),
         (TagValue, 'last_seen'),
         (Alert, 'datetime'),
 

+ 8 - 42
src/sentry/tasks/post_process.py

@@ -38,8 +38,7 @@ def plugin_post_process_group(plugin_slug, group, **kwargs):
     name='sentry.tasks.post_process.record_affected_user',
     queue='triggers')
 def record_affected_user(group, event, **kwargs):
-    from sentry import app
-    from sentry.models import TrackedUser, AffectedUserByGroup
+    from sentry.models import Group
 
     user_ident = event.user_ident
     if not user_ident:
@@ -47,44 +46,11 @@ def record_affected_user(group, event, **kwargs):
 
     data = event.data.get('sentry.interfaces.User')
 
-    project = group.project
-    date = group.last_seen
-
-    if data:
-        email = data.get('email')
-    else:
-        email = None
-
-    # TODO: we should be able to chain the affected user update so that tracked
-    # user gets updated serially
-    tuser, created = TrackedUser.objects.get_or_create(
-        project=project,
-        ident=user_ident,
-        defaults={
-            'email': email,
-            'data': data,
-            'num_events': 1,
-            'last_seen': date,
-        }
-    )
-
-    if not created:
-        app.buffer.incr(TrackedUser, {
-            'num_events': 1,
-        }, {
-            'id': tuser.id,
-        }, {
-            'last_seen': date,
-            'email': email,
-            'data': data,
+    Group.objects.add_tags(group, [
+        ('sentry:user', user_ident, {
+            'id': data.get('id'),
+            'email': data.get('email'),
+            'username': data.get('username'),
+            'data': data.get('data'),
         })
-
-    app.buffer.incr(AffectedUserByGroup, {
-        'times_seen': 1,
-    }, {
-        'group': group,
-        'project': project,
-        'tuser': tuser,
-    }, {
-        'last_seen': date,
-    })
+    ])

+ 7 - 7
src/sentry/templates/sentry/users/details.html

@@ -10,7 +10,7 @@
 {% block breadcrumb %}
     {{ block.super }}
     <li class="divider"></li>
-    <li><a href="{% url 'sentry-user-details' team.slug tuser.id %}">{{ tuser.email }}</a></li>
+    <li><a href="{% url 'sentry-user-details' team.slug tag.id %}">{{ tag.data.email }}</a></li>
 {% endblock %}
 
 {% block bodyclass %} with-sidebar{% endblock %}
@@ -19,18 +19,18 @@
     <h6>User Details</h6>
     <dl>
         <dt>Email:</dt>
-        <dd>{{ tuser.email }}</dd>
+        <dd>{{ tag.data.email }}</dd>
         <dt>First Seen:</dt>
-        <dd>{{ tuser.first_seen|timesince }}</dd>
+        <dd>{{ tag.first_seen|timesince }}</dd>
         <dt>Last Seen:</dt>
-        <dd>{{ tuser.last_seen|timesince }}</dd>
+        <dd>{{ tag.last_seen|timesince }}</dd>
         <dt>Number of Events:</dt>
-        <dd>{{ tuser.num_events|small_count }}</dd>
+        <dd>{{ tag.times_seen|small_count }}</dd>
     </dl>
-    {% if tuser.data %}
+    {% if tag.data.data %}
         <h6>Metadata</h6>
         <dl>
-            {% for key, value in tuser.data.iteritems %}
+            {% for key, value in tag.data.data.iteritems %}
                 <dt>{{ key|titlize }}</dt>
                 <dd>{{ value }}</dd>
             {% endfor %}

+ 10 - 10
src/sentry/templates/sentry/users/list.html

@@ -16,7 +16,7 @@
 
 {% block main %}
     <section class="body">
-        {% paginator tuser_list from request as tuser_list %}
+        {% paginator tag_list from request as tag_list %}
         {% querystring from request without sort as sort_querystring %}
 
         <div class="btn-toolbar">
@@ -29,12 +29,12 @@
                 </ul>
             </div>
             <div class="btn-group pull-right">
-                <a class="btn prev{% if not tuser_list.paginator.has_previous %} disabled{% endif %}" href="?{{ tuser_list.query_string|escape }}&amp;p={{ tuser_list.paginator.previous_page }}"><span>{% trans "Previous" %}</span></a>
-                <a class="btn next{% if not tuser_list.paginator.has_next %} disabled{% endif %}" href="?{{ tuser_list.query_string|escape }}&amp;p={{ tuser_list.paginator.next_page }}"><span>{% trans "Next" %}</span></a>
+                <a class="btn prev{% if not tag_list.paginator.has_previous %} disabled{% endif %}" href="?{{ tag_list.query_string|escape }}&amp;p={{ tag_list.paginator.previous_page }}"><span>{% trans "Previous" %}</span></a>
+                <a class="btn next{% if not tag_list.paginator.has_next %} disabled{% endif %}" href="?{{ tag_list.query_string|escape }}&amp;p={{ tag_list.paginator.next_page }}"><span>{% trans "Next" %}</span></a>
             </div>
         </div>
 
-        {% if not tuser_list.paginator.objects %}
+        {% if not tag_list.paginator.objects %}
             <p>{% blocktrans %}You dont seem to have any user data recorded. For more information on how to send this information consult your client's documentation.{% endblocktrans %}</p>
         {% else %}
             <table class="table table-bordered table-striped">
@@ -46,11 +46,11 @@
                     </tr>
                 </thead>
                 <tbody>
-                    {% for tuser in tuser_list.paginator.objects %}
+                    {% for tag in tag_list.paginator.objects %}
                         <tr>
-                            <td><a href="{% url 'sentry-user-details' team.slug tuser.id %}">{% if tuser.email %}{{ tuser.email }}{% else %}{{ tuser.ident }}{% endif %}</a></td>
-                            <td style="text-align:center">{{ tuser.last_seen|timesince }}</td>
-                            <td style="text-align:center">{{ tuser.num_events|small_count }}</td>
+                            <td><a href="{% url 'sentry-user-details' team.slug tag.id %}">{% if tag.data.email %}{{ tag.data.email }}{% else %}{{ tag.value }}{% endif %}</a></td>
+                            <td style="text-align:center">{{ tag.last_seen|timesince }}</td>
+                            <td style="text-align:center">{{ tag.times_seen|small_count }}</td>
                         </tr>
                     {% endfor %}
                 </tbody>
@@ -58,8 +58,8 @@
 
             <div class="btn-toolbar">
                 <div class="btn-group pull-right">
-                    <a class="btn prev{% if not tuser_list.paginator.has_previous %} disabled{% endif %}" href="?{{ tuser_list.query_string|escape }}&amp;p={{ tuser_list.paginator.previous_page }}"><span>{% trans "Previous" %}</span></a>
-                    <a class="btn next{% if not tuser_list.paginator.has_next %} disabled{% endif %}" href="?{{ tuser_list.query_string|escape }}&amp;p={{ tuser_list.paginator.next_page }}"><span>{% trans "Next" %}</span></a>
+                    <a class="btn prev{% if not tag_list.paginator.has_previous %} disabled{% endif %}" href="?{{ tag_list.query_string|escape }}&amp;p={{ tag_list.paginator.previous_page }}"><span>{% trans "Previous" %}</span></a>
+                    <a class="btn next{% if not tag_list.paginator.has_next %} disabled{% endif %}" href="?{{ tag_list.query_string|escape }}&amp;p={{ tag_list.paginator.next_page }}"><span>{% trans "Next" %}</span></a>
                 </div>
             </div>
         {% endif %}

+ 0 - 1
src/sentry/utils/javascript.py

@@ -93,7 +93,6 @@ class GroupTransformer(Transformer):
         d = {
             'id': str(obj.id),
             'count': str(obj.times_seen),
-            'userCount': str(obj.users_seen),
             'title': escape(obj.message_top()),
             'message': escape(obj.error()),
             'level': obj.level,

+ 17 - 13
src/sentry/web/frontend/users.py

@@ -5,7 +5,7 @@ sentry.web.frontend.users
 :copyright: (c) 2012 by the Sentry Team, see AUTHORS for more details.
 :license: BSD, see LICENSE for more details.
 """
-from sentry.models import TrackedUser
+from sentry.models import TagValue, GroupTag
 from sentry.web.decorators import login_required, has_access
 from sentry.web.helpers import render_to_response
 
@@ -24,21 +24,17 @@ def user_list(request, team):
     if sort not in SORT_OPTIONS:
         sort = DEFAULT_SORT_OPTION
 
-    # TODO: TrackedUser needs to be team-bound before we can launch it
-    user_list = TrackedUser.objects.filter(project__team=team)
+    tag_list = TagValue.objects.filter(project__team=team, key='sentry:user')
 
     if sort == 'recent':
-        user_list = user_list.order_by('-last_seen')
+        tag_list = tag_list.order_by('-last_seen')
     elif sort == 'newest':
-        user_list = user_list.order_by('-first_seen')
+        tag_list = tag_list.order_by('-first_seen')
     elif sort == 'events':
-        user_list = user_list.order_by('-num_events')
-
-    # TODO: add separate pain for unbound users (e.g. missing email addresses)
-    user_list = user_list.filter(email__isnull=False)
+        tag_list = tag_list.order_by('-num_events')
 
     return render_to_response('sentry/users/list.html', {
-        'tuser_list': user_list,
+        'tag_list': tag_list,
         'team': team,
         'sort_label': SORT_OPTIONS[sort],
         'SECTION': 'users',
@@ -49,13 +45,21 @@ def user_list(request, team):
 @has_access
 @login_required
 def user_details(request, team, user_id):
-    user = TrackedUser.objects.get(project__team=team, id=user_id)
+    tag = TagValue.objects.get(
+        project__team=team,
+        key='sentry:user',
+        id=user_id,
+    )
 
-    event_list = user.groups.all()
+    event_list = GroupTag.objects.filter(
+        project__team=team,
+        key='sentry:user',
+        value=tag.value,
+    )
 
     return render_to_response('sentry/users/details.html', {
         'team': team,
-        'tuser': user,
+        'tag': tag,
         'event_list': event_list,
         'SECTION': 'users',
     }, request)

+ 14 - 22
tests/sentry/tasks/post_process/tests.py

@@ -2,37 +2,29 @@
 
 from __future__ import absolute_import
 
+import mock
+
 from sentry.models import Group
 from sentry.testutils import TestCase
 from sentry.tasks.post_process import record_affected_user
 
 
-class SentryManagerTest(TestCase):
+class RecordAffectedUserTest(TestCase):
     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',
             },
         })
 
-        record_affected_user(group=event.group, event=event)
-
-        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
+        with mock.patch.object(Group.objects, 'add_tags') as add_tags:
+            record_affected_user(group=event.group, event=event)
+
+            add_tags.assert_called_once(event.group, [
+                ('sentry:user', 'email:foo@example.com', {
+                    'id': None,
+                    'email': 'foo@example.com',
+                    'username': None,
+                    'data': None,
+                })
+            ])

Some files were not shown because too many files changed in this diff