Skip to content

Commit

Permalink
Release stats (getsentry#4816)
Browse files Browse the repository at this point in the history
  • Loading branch information
Katie Lundsgaard authored Jan 27, 2017
1 parent 4243142 commit ae9051f
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ API Changes
~~~~~~~~~~~

- Added avatar and avatarType to ``/organizations/{org}/`` endpoint.
- Provide commit and author information associated with a given release

Version 8.12
------------
Expand Down
117 changes: 115 additions & 2 deletions src/sentry/api/serializers/models/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,116 @@

from django.db.models import Sum


from collections import Counter, defaultdict

from sentry.api.serializers import Serializer, register, serialize
from sentry.models import Release, ReleaseProject, TagValue
from sentry.db.models.query import in_iexact
from sentry.models import Release, ReleaseCommit, ReleaseProject, TagValue, User, UserEmail


@register(Release)
class ReleaseSerializer(Serializer):
def _get_users_for_commits(self, release_commits, org_id):
"""
Returns a dictionary of author_id => user, if a Sentry
user object exists for that email. If there is no matching
Sentry user, a {user, email} dict representation of that
author is returned.
e.g.
{
1: serialized(<User id=1>),
2: {email: '[email protected]', name: 'dunno'},
...
}
"""
authors = set(rc.commit.author for rc in release_commits if rc.commit.author is not None)
if not len(authors):
return {}

# Filter users based on the emails provided in the commits
user_emails = UserEmail.objects.filter(
in_iexact('email', [a.email for a in authors]),
).order_by('id')

# Filter users belonging to the organization associated with
# the release
users = User.objects.filter(
id__in=[ue.user_id for ue in user_emails],
sentry_orgmember_set__organization_id=org_id
)
users_by_id = dict((user.id, serialize(user)) for user in users)

# Figure out which email address matches to a user
users_by_email = {}
for user_email in user_emails:
if user_email.email in users_by_email:
pass

user = users_by_id.get(user_email.user_id)
if user:
users_by_email[user_email.email] = user

author_objs = {}
for author in authors:
author_objs[author.id] = users_by_email.get(author.email, {
"name": author.name,
"email": author.email
})

return author_objs

def _get_commit_metadata(self, item_list, user):
"""
Returns a dictionary of release_id => commit metadata,
where each commit metadata dict contains commit_count
and an array of authors.
e.g.
{
1: {
'commit_count': 3,
'authors': [<User id=1>, <User id=2>]
},
...
}
If there are no commits, returns None.
"""

release_commits = list(ReleaseCommit.objects.filter(
release__in=item_list).select_related("commit", "commit__author"))

if not len(release_commits):
return None

org_ids = set(item.organization_id for item in item_list)
assert len(org_ids) == 1
org_id = org_ids.pop()

users_by_email = self._get_users_for_commits(release_commits, org_id)
commit_count_by_release_id = Counter()
authors_by_release_id = defaultdict(dict)

for rc in release_commits:
# Accumulate authors per release
author = rc.commit.author
if author:
authors_by_release_id[rc.release_id][author.id] = \
users_by_email[author.id]

# Increment commit count per release
commit_count_by_release_id[rc.release_id] += 1

result = {}
for item in item_list:
result[item] = {
'commit_count': commit_count_by_release_id[item.id],
'authors': authors_by_release_id.get(item.id, {}).values(),
}
return result

def get_attrs(self, item_list, user, *args, **kwargs):
tags = {
tk.value: tk
Expand Down Expand Up @@ -41,13 +145,20 @@ def get_attrs(self, item_list, user, *args, **kwargs):
.values_list('release_id', 'new_groups')
)

release_metadata_attrs = self._get_commit_metadata(item_list, user)

result = {}
for item in item_list:
result[item] = {
'tag': tags.get(item.version),
'owner': owners[six.text_type(item.owner_id)] if item.owner_id else None,
'new_groups': group_counts_by_release.get(item.id) or 0
'new_groups': group_counts_by_release.get(item.id) or 0,
'commit_count': 0,
'authors': [],
}
if release_metadata_attrs:
result[item].update(release_metadata_attrs[item])

return result

def serialize(self, obj, attrs, user, *args, **kwargs):
Expand All @@ -62,6 +173,8 @@ def serialize(self, obj, attrs, user, *args, **kwargs):
'data': obj.data,
'newGroups': attrs['new_groups'],
'owner': attrs['owner'],
'commitCount': attrs.get('commit_count', 0),
'authors': attrs.get('authors', []),
}
if attrs['tag']:
d.update({
Expand Down
40 changes: 40 additions & 0 deletions src/sentry/static/sentry/app/components/releaseStats.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import Avatar from './avatar';
import TooltipMixin from '../mixins/tooltip';
import {t} from '../locale';

const ReleaseStats = React.createClass({
propTypes: {
release: React.PropTypes.object,
},

mixins: [
TooltipMixin({
selector: '.tip'
}),
],

render() {
let release = this.props.release;
let commitCount = release.commitCount;
let authorCount = release.authors.length;
if (commitCount === 0) {
return null;
}
return (
<div className="release-info">
<div><b>{commitCount}{t(' commits by ')}{authorCount}{t(' authors')}</b></div>
{release.authors.map(author => {
return (
<span className="assignee-selector tip"
title={author.name + ' ' + author.email}>
<Avatar user={author}/>
</span>
);
})}
</div>
);
}
});

export default ReleaseStats;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import ReleaseStats from '../../components/releaseStats';
import Count from '../../components/count';
import TimeSince from '../../components/timeSince';
import Version from '../../components/version';
Expand All @@ -19,12 +20,15 @@ const ReleaseList = React.createClass({
return (
<li className="release" key={release.version}>
<div className="row">
<div className="col-sm-8 col-xs-6">
<div className="col-sm-6 col-xs-4">
<h4><Version orgId={orgId} projectId={projectId} version={release.version} /></h4>
<div className="release-meta">
<span className="icon icon-clock"></span> <TimeSince date={release.dateCreated} />
</div>
</div>
<div className="col-sm-2 col-xs-2">
<ReleaseStats release={release}/>
</div>
<div className="col-sm-2 col-xs-3 release-stats stream-count">
<Count className="release-count" value={release.newGroups} />
</div>
Expand Down
7 changes: 5 additions & 2 deletions src/sentry/static/sentry/app/views/releaseDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import DocumentTitle from 'react-document-title';
import ListLink from '../components/listLink';
import LoadingError from '../components/loadingError';
import LoadingIndicator from '../components/loadingIndicator';
import ReleaseStats from '../components/releaseStats';
import ProjectState from '../mixins/projectState';
import TimeSince from '../components/timeSince';
import Version from '../components/version';
Expand Down Expand Up @@ -92,18 +93,20 @@ const ReleaseDetails = React.createClass({

let release = this.state.release;
let {orgId, projectId} = this.props.params;

return (
<DocumentTitle title={this.getTitle()}>
<div>
<div className="release-details">
<div className="row">
<div className="col-sm-6 col-xs-12">
<div className="col-sm-4 col-xs-12">
<h3>{t('Release')} <strong><Version orgId={orgId} projectId={projectId} version={release.version} anchor={false} /></strong></h3>
<div className="release-meta">
<span className="icon icon-clock"></span> <TimeSince date={release.dateCreated} />
</div>
</div>
<div className="col-sm-2 hidden-xs">
<ReleaseStats release={release}/>
</div>
<div className="col-sm-2 hidden-xs">
<div className="release-stats">
<h6 className="nav-header">{t('New Issues')}</h6>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';
import LoadingIndicator from '../../components/loadingIndicator';
import LoadingError from '../../components/loadingError';

import ApiMixin from '../../mixins/apiMixin';

const ReleaseCommit = React.createClass({
Expand Down Expand Up @@ -72,8 +71,9 @@ const ReleaseCommits = React.createClass({
<div className="panel-heading panel-heading-bold">
<div className="row">
<div className="col-sm-2 col-xs-2">{'SHA'}</div>
<div className="col-sm-7 col-xs-7">{'Message'}</div>
<div className="col-sm-3 col-xs-3 align-right">{'Date'}</div>
<div className="col-sm-5 col-xs-5">{'Message'}</div>
<div className="col-sm-3 col-xs-3 align-right actions">{'Date'}</div>
<div className="col-sm-2 col-xs-2 align-right actions">{'Author'}</div>
</div>
</div>
<ul className="list-group commit-list">
Expand Down
Loading

0 comments on commit ae9051f

Please sign in to comment.