Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 59 additions & 10 deletions bin/load-mocks
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,50 @@ def create_system_time_series():
now = now - timedelta(hours=1)


def create_sample_time_series(event):
def create_sample_time_series(event, release=None):
group = event.group
project = group.project

now = datetime.utcnow().replace(tzinfo=utc)

for _ in xrange(60):
count = randint(1, 10)
tsdb.incr_multi((
(tsdb.models.project, group.project.id),
(tsdb.models.project, project.id),
(tsdb.models.group, group.id),
), now, count)
tsdb.incr_multi((
(tsdb.models.organization_total_received, group.project.organization_id),
(tsdb.models.project_total_received, group.project.id),
(tsdb.models.organization_total_received, project.organization_id),
(tsdb.models.project_total_received, project.id),
), now, int(count * 1.1))
tsdb.incr_multi((
(tsdb.models.organization_total_rejected, group.project.organization_id),
(tsdb.models.project_total_rejected, group.project.id),
(tsdb.models.organization_total_rejected, project.organization_id),
(tsdb.models.project_total_rejected, project.id),
), now, int(count * 0.1))

frequencies = [
(tsdb.models.frequent_projects_by_organization, {
project.organization_id: {
project.id: count,
},
}),
(tsdb.models.frequent_issues_by_project, {
project.id: {
group.id: count,
},
}),
]
if release:
frequencies.append(
(tsdb.models.frequent_releases_by_groups, {
group.id: {
release.id: count,
},
})
)

tsdb.record_frequency_multi(frequencies, now)

now = now - timedelta(seconds=1)

for _ in xrange(24 * 30):
Expand All @@ -105,6 +130,30 @@ def create_sample_time_series(event):
(tsdb.models.organization_total_rejected, group.project.organization_id),
(tsdb.models.project_total_rejected, group.project.id),
), now, int(count * 0.1))

frequencies = [
(tsdb.models.frequent_projects_by_organization, {
project.organization_id: {
project.id: count,
},
}),
(tsdb.models.frequent_issues_by_project, {
project.id: {
group.id: count,
},
}),
]
if release:
frequencies.append(
(tsdb.models.frequent_releases_by_groups, {
group.id: {
release.id: count,
},
})
)

tsdb.record_frequency_multi(frequencies, now)

now = now - timedelta(hours=1)


Expand Down Expand Up @@ -371,11 +420,11 @@ def main(num_events=1):

print(' > Loading time series data'.format(project_name))

create_sample_time_series(event1)
create_sample_time_series(event2)
create_sample_time_series(event1, release=release)
create_sample_time_series(event2, release=release)
create_sample_time_series(event3)
create_sample_time_series(event4)
create_sample_time_series(event5)
create_sample_time_series(event4, release=release)
create_sample_time_series(event5, release=release)

if hasattr(buffer, 'process_pending'):
print(' > Processing pending buffers')
Expand Down
48 changes: 48 additions & 0 deletions src/sentry/api/endpoints/group_release_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import absolute_import

from rest_framework.response import Response

from sentry.app import tsdb
from sentry.api.base import StatsMixin
from sentry.api.bases.group import GroupEndpoint
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.serializers import serialize
from sentry.models import Release


class GroupReleaseDetailsEndpoint(GroupEndpoint, StatsMixin):
def get(self, request, group, version):
try:
if version == ':latest':
release = Release.objects.filter(
project=group.project_id,
).order_by('-date_added')[0:1].get()
else:
release = Release.objects.get(
project=group.project_id,
version=version,
)
except Release.DoesNotExist:
raise ResourceDoesNotExist

try:
data = tsdb.get_frequency_series(
model=tsdb.models.frequent_releases_by_groups,
items={
group.id: [release.id],
},
**self._parse_args(request)
)[group.id]
except NotImplementedError:
# TODO(dcramer): probably should log this, but not worth
# erring out
data = []
else:
data = [
(k, v[release.id])
for k, v in data
]

context = serialize(release, request.user)
context['stats'] = data
return Response(context)
4 changes: 4 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .endpoints.group_notes import GroupNotesEndpoint
from .endpoints.group_notes_details import GroupNotesDetailsEndpoint
from .endpoints.group_participants import GroupParticipantsEndpoint
from .endpoints.group_release_details import GroupReleaseDetailsEndpoint
from .endpoints.group_stats import GroupStatsEndpoint
from .endpoints.group_tags import GroupTagsEndpoint
from .endpoints.group_tagkey_details import GroupTagKeyDetailsEndpoint
Expand Down Expand Up @@ -307,6 +308,9 @@
url(r'^(?:issues|groups)/(?P<issue_id>\d+)/stats/$',
GroupStatsEndpoint.as_view(),
name='sentry-api-0-group-stats'),
url(r'^(?:issues|groups)/(?P<issue_id>\d+)/releases/(?P<version>[^/]+)/$',
GroupReleaseDetailsEndpoint.as_view(),
name='sentry-api-0-group-release-details'),
url(r'^(?:issues|groups)/(?P<issue_id>\d+)/tags/$',
GroupTagsEndpoint.as_view(),
name='sentry-api-0-group-tags'),
Expand Down
2 changes: 0 additions & 2 deletions src/sentry/static/sentry/app/components/group/chart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import PropTypes from '../../proptypes';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import {t} from '../../locale';


const GroupChart = React.createClass({
propTypes: {
group: PropTypes.Group.isRequired,
Expand Down Expand Up @@ -57,4 +56,3 @@ const GroupChart = React.createClass({
});

export default GroupChart;

164 changes: 164 additions & 0 deletions src/sentry/static/sentry/app/components/group/releaseChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React from 'react';

import ApiMixin from '../../mixins/apiMixin';
import LoadingError from '../loadingError';
import StackedBarChart from '../stackedBarChart';
import PropTypes from '../../proptypes';
import {t} from '../../locale';
import {intcomma} from '../../utils';

const GroupReleaseChart = React.createClass({
propTypes: {
group: PropTypes.Group.isRequired,
release: React.PropTypes.string,
statsPeriod: React.PropTypes.string.isRequired,
firstSeen: React.PropTypes.string.isRequired,
lastSeen: React.PropTypes.string.isRequired,
title: React.PropTypes.string
},

mixins: [ApiMixin],

getInitialState() {
let stats = this.props.group.stats[this.props.statsPeriod];
return {
loading: stats && stats.length,
error: false,
release: null,
};
},

componentWillMount() {
if (this.state.loading) {
this.fetchData();
}
},

shouldComponentUpdate(nextProps, nextState) {
return (
this.state.loading !== nextState.loading ||
this.state.error !== nextState.error
);
},

fetchData() {
let group = this.props.group;
let stats = this.props.group.stats[this.props.statsPeriod];

let since = stats[0][0];
let until = stats[stats.length - 1][0];
let resolution = this.props.statsPeriod === '24h' ? '1h' : '1d';

this.api.request(`/issues/${group.id}/releases/:latest/`, {
query: {
resolution: resolution,
since: since,
until: until,
},
success: (data) => {
this.setState({
release: data,
loading: false,
error: false,
});
},
error: () => {
this.setState({
release: null,
loading: false,
error: true,
});
}
});
},

renderTooltip(point, pointIdx, chart) {
let timeLabel = chart.getTimeLabel(point);
let totalY = 0;
for (let i = 0; i < point.y.length; i++) {
totalY += point.y[i];
}
let title = (
'<div style="width:130px">' +
intcomma(totalY) + ' events<br/>' +
'<small>' + intcomma(point.y[0]) + ' in latest release</small><br/>' +
timeLabel +
'</div>'
);
return title;
},

render() {
let className = 'bar-chart group-chart ' + (this.props.className || '');

if (this.state.error) {
return (
<div className={className}>
<h6><span>{this.props.title}</span></h6>
<LoadingError />
</div>
);
}

if (this.state.loading) {
return null;
}

let group = this.props.group;
let stats = group.stats[this.props.statsPeriod];
if (!stats || !stats.length) return null;

let release = this.state.release;
let releasePoints = {};
if (release) {
release.stats.forEach((point) => {
releasePoints[point[0]] = point[1];
});
}

let points = stats.map((point) => {
let rData = releasePoints[point[0]] || 0;
let remaining = point[1] - rData;
return {
x: point[0],
y: [
rData,
remaining >= 0 ? remaining : 0,
],
};
});

let markers = [];
let firstSeenX = new Date(this.props.firstSeen).getTime() / 1000;
let lastSeenX = new Date(this.props.lastSeen).getTime() / 1000;
if (firstSeenX >= points[0].x) {
markers.push({
label: t('First seen'),
x: firstSeenX,
className: 'first-seen'
});
}
if (lastSeenX >= points[0].x) {
markers.push({
label: t('Last seen'),
x: lastSeenX,
className: 'last-seen'
});
}

return (
<div className={className}>
<h6><span>{this.props.title}</span></h6>
<StackedBarChart
points={points}
height={150}
className="sparkline"
markers={markers}
barClasses={['active', 'inactive']}
tooltip={this.renderTooltip} />
</div>
);
}
});

export default GroupReleaseChart;
24 changes: 14 additions & 10 deletions src/sentry/static/sentry/app/components/group/sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';

import ApiMixin from '../../mixins/apiMixin';
import Avatar from '../avatar';
import GroupChart from './chart';
import GroupReleaseChart from './releaseChart';
import GroupState from '../../mixins/groupState';
import IndicatorStore from '../../stores/indicatorStore';
import SeenInfo from './seenInfo';
Expand Down Expand Up @@ -83,15 +83,19 @@ const GroupSidebar = React.createClass({

return (
<div className="group-stats">
<GroupChart statsPeriod="24h" group={group}
title={t('Last 24 Hours')}
firstSeen={group.firstSeen}
lastSeen={group.lastSeen} />
<GroupChart statsPeriod="30d" group={group}
title={t('Last 30 Days')}
className="bar-chart-small"
firstSeen={group.firstSeen}
lastSeen={group.lastSeen} />
<GroupReleaseChart
statsPeriod="24h"
group={group}
title={t('Last 24 Hours')}
firstSeen={group.firstSeen}
lastSeen={group.lastSeen} />
<GroupReleaseChart
statsPeriod="30d"
group={group}
title={t('Last 30 Days')}
className="bar-chart-small"
firstSeen={group.firstSeen}
lastSeen={group.lastSeen} />

<h6 className="first-seen"><span>{t('First seen')}</span></h6>
<SeenInfo
Expand Down
Loading