diff --git a/src/sentry/api/serializers/models/organization.py b/src/sentry/api/serializers/models/organization.py
index c398cdc7775e46..c40a6478e52d7f 100644
--- a/src/sentry/api/serializers/models/organization.py
+++ b/src/sentry/api/serializers/models/organization.py
@@ -59,6 +59,8 @@ def serialize(self, obj, attrs, user):
feature_list.append('sso')
if features.has('organizations:callsigns', obj, actor=user):
feature_list.append('callsigns')
+ if features.has('organizations:new-tracebacks', obj, actor=user):
+ feature_list.append('new-tracebacks')
if features.has('organizations:onboarding', obj, actor=user) and \
not OrganizationOption.objects.filter(organization=obj).exists():
feature_list.append('onboarding')
diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py
index 869d509905e07c..94ef3d15acd03a 100644
--- a/src/sentry/conf/server.py
+++ b/src/sentry/conf/server.py
@@ -621,6 +621,7 @@ def create_partitioned_queues(name):
'organizations:create': True,
'organizations:sso': True,
'organizations:callsigns': False,
+ 'organizations:new-tracebacks': False,
'projects:global-events': False,
'projects:quotas': True,
'projects:plugins': True,
diff --git a/src/sentry/features/__init__.py b/src/sentry/features/__init__.py
index f5b9e312b2abfc..95686c9ec53e5e 100644
--- a/src/sentry/features/__init__.py
+++ b/src/sentry/features/__init__.py
@@ -13,6 +13,7 @@
default_manager.add('organizations:sso', OrganizationFeature)
default_manager.add('organizations:onboarding', OrganizationFeature)
default_manager.add('organizations:callsigns', OrganizationFeature)
+default_manager.add('organizations:new-tracebacks', OrganizationFeature)
default_manager.add('projects:global-events', ProjectFeature)
default_manager.add('projects:quotas', ProjectFeature)
default_manager.add('projects:plugins', ProjectPluginFeature)
diff --git a/src/sentry/interfaces/message.py b/src/sentry/interfaces/message.py
index fc2f09569f2763..551e403bc2d2c9 100644
--- a/src/sentry/interfaces/message.py
+++ b/src/sentry/interfaces/message.py
@@ -35,7 +35,7 @@ class Message(Interface):
>>> }
"""
score = 0
- display_score = 1050
+ display_score = 2050
@classmethod
def to_python(cls, data):
diff --git a/src/sentry/static/sentry/app/components/clippedBox.jsx b/src/sentry/static/sentry/app/components/clippedBox.jsx
index 41fc870045c55f..f282667c8953dc 100644
--- a/src/sentry/static/sentry/app/components/clippedBox.jsx
+++ b/src/sentry/static/sentry/app/components/clippedBox.jsx
@@ -35,7 +35,9 @@ const ClippedBox = React.createClass({
}
},
- reveal() {
+ reveal(e) {
+ e.stopPropagation();
+
this.setState({
clipped: false
});
diff --git a/src/sentry/static/sentry/app/components/events/interfaces/frame.jsx b/src/sentry/static/sentry/app/components/events/interfaces/frame.jsx
index b2b9dbc051ecdf..538d978297c35a 100644
--- a/src/sentry/static/sentry/app/components/events/interfaces/frame.jsx
+++ b/src/sentry/static/sentry/app/components/events/interfaces/frame.jsx
@@ -1,13 +1,16 @@
import React from 'react';
import _ from 'underscore';
import classNames from 'classnames';
-import {defined, objectIsEmpty, isUrl} from '../../../utils';
+import ClippedBox from '../../../components/clippedBox';
import TooltipMixin from '../../../mixins/tooltip';
-import FrameVariables from './frameVariables';
-import ContextLine from './contextLine';
import StrictClick from '../../strictClick';
+import Truncate from '../../../components/truncate';
import {t} from '../../../locale';
+import {defined, objectIsEmpty, isUrl} from '../../../utils';
+
+import ContextLine from './contextLine';
+import FrameVariables from './frameVariables';
function trimPackage(pkg) {
let pieces = pkg.split(/\//g);
@@ -33,17 +36,27 @@ const Frame = React.createClass({
})
],
+ getDefaultProps() {
+ return {
+ isExpanded: false
+ };
+ },
+
getInitialState() {
// isExpanded can be initialized to true via parent component;
// data synchronization is not important
// https://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html
return {
- isExpanded: defined(this.props.isExpanded) ? this.props.isExpanded : false
+ isExpanded: this.props.isExpanded
};
},
toggleContext(evt) {
evt && evt.preventDefault();
+ if (!this.isExpandable()) {
+ return null;
+ }
+
this.setState({
isExpanded: !this.state.isExpanded
});
@@ -53,19 +66,14 @@ const Frame = React.createClass({
return defined(this.props.data.context) && this.props.data.context.length;
},
- hasExtendedSource() {
- return this.hasContextSource() && this.props.data.context.length > 1;
- },
-
hasContextVars() {
return !objectIsEmpty(this.props.data.vars);
},
isExpandable() {
- return this.hasExtendedSource() || this.hasContextVars();
+ return this.hasContextSource() || this.hasContextVars();
},
-
renderOriginalSourceInfo() {
let data = this.props.data;
@@ -94,7 +102,11 @@ const Frame = React.createClass({
// lazy to change this up right now. This should be a format string
if (defined(data.filename || data.module)) {
- title.push({data.filename || data.module});
+ title.push((
+
+
+
+ ));
if (isUrl(data.absPath)) {
title.push();
}
@@ -104,26 +116,26 @@ const Frame = React.createClass({
}
if (defined(data.function)) {
- title.push({data.function});
+ title.push({data.function});
}
// we don't want to render out zero line numbers which are used to
// indicate lack of source information for native setups. We could
// TODO(mitsuhiko): only do this for events from native platforms?
- if (defined(data.lineNo) && data.lineNo != 0) {
+ else if (defined(data.lineNo) && data.lineNo != 0) {
// TODO(dcramer): we need to implement source mappings
// title.push( View Code);
title.push( {t('at line')} );
if (defined(data.colNo)) {
- title.push({data.lineNo}:{data.colNo});
+ title.push({data.lineNo}:{data.colNo});
} else {
- title.push({data.lineNo});
+ title.push({data.lineNo});
}
}
if (defined(data.package)) {
title.push( {t('within')} );
- title.push({trimPackage(data.package)});
+ title.push({trimPackage(data.package)});
}
if (defined(data.origAbsPath)) {
@@ -134,35 +146,9 @@ const Frame = React.createClass({
);
}
- if (data.inApp) {
- title.push({t('application')});
- }
return title;
},
- renderContextLine(line, activeLineNo) {
- let liClassName = 'expandable';
- if (line[0] === activeLineNo) {
- liClassName += ' active';
- }
-
- let lineWs;
- let lineCode;
- if (defined(line[1]) && line[1].match) {
- [, lineWs, lineCode] = line[1].match(/^(\s*)(.*?)$/m);
- } else {
- lineWs = '';
- lineCode = '';
- }
- return (
-
- {
- lineWs}{lineCode
- }
-
- );
- },
-
renderContext() {
let data = this.props.data;
let context = '';
@@ -192,11 +178,11 @@ const Frame = React.createClass({
}
{data.context && contextLines.map((line, index) => {
- return ;
+ return ;
})}
{hasContextVars &&
-
+
}
@@ -221,7 +207,7 @@ const Frame = React.createClass({
renderDefaultLine() {
return (
-
+
{this.renderDefaultTitle()}
{this.renderExpander()}
@@ -230,22 +216,21 @@ const Frame = React.createClass({
renderCocoaLine() {
let data = this.props.data;
- let className = 'stacktrace-table';
return (
-
+
{defined(data.package)
? (
-
+
{trimPackage(data.package)}
-
+
) : (
-
+
)
}
-
+
{data.instructionAddr}
-
-
+
+
{data.function || ''}
{data.instructionOffset &&
{' + ' + data.instructionOffset}}
@@ -253,8 +238,8 @@ const Frame = React.createClass({
{data.filename}
{data.lineNo ? ':' + data.lineNo : ''}}
{this.renderExpander()}
-
-
+
+
);
},
@@ -273,6 +258,8 @@ const Frame = React.createClass({
let className = classNames({
'frame': true,
+ 'is-expandable': this.isExpandable(),
+ 'expanded': this.state.isExpanded,
'system-frame': !data.inApp,
'frame-errors': data.errors,
'leads-to-app': !data.inApp && this.props.nextFrameInApp
diff --git a/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx b/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx
new file mode 100644
index 00000000000000..254f5119ae3cca
--- /dev/null
+++ b/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx
@@ -0,0 +1,293 @@
+import React from 'react';
+import _ from 'underscore';
+import classNames from 'classnames';
+import {defined, objectIsEmpty, isUrl} from '../../../utils';
+
+import StrictClick from '../../strictClick';
+import TooltipMixin from '../../../mixins/tooltip';
+import FrameVariables from './frameVariables';
+import ContextLine from './contextLine';
+import {t} from '../../../locale';
+
+function trimPackage(pkg) {
+ let pieces = pkg.split(/\//g);
+ let rv = pieces[pieces.length - 1] || pieces[pieces.length - 2] || pkg;
+ let match = rv.match(/^(.*?)\.(dylib|so|a)$/);
+ return match && match[1] || rv;
+}
+
+
+const OldFrame = React.createClass({
+ propTypes: {
+ data: React.PropTypes.object.isRequired,
+ nextFrameInApp: React.PropTypes.bool,
+ platform: React.PropTypes.string,
+ isExpanded: React.PropTypes.bool,
+ },
+
+ mixins: [
+ TooltipMixin({
+ html: true,
+ selector: '.tip',
+ trigger: 'hover'
+ })
+ ],
+
+ getInitialState() {
+ // isExpanded can be initialized to true via parent component;
+ // data synchronization is not important
+ // https://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html
+ return {
+ isExpanded: defined(this.props.isExpanded) ? this.props.isExpanded : false
+ };
+ },
+
+ toggleContext(evt) {
+ evt && evt.preventDefault();
+ this.setState({
+ isExpanded: !this.state.isExpanded
+ });
+ },
+
+ hasContextSource() {
+ return defined(this.props.data.context) && this.props.data.context.length;
+ },
+
+ hasExtendedSource() {
+ return this.hasContextSource() && this.props.data.context.length > 1;
+ },
+
+ hasContextVars() {
+ return !objectIsEmpty(this.props.data.vars);
+ },
+
+ isExpandable() {
+ return this.hasExtendedSource() || this.hasContextVars();
+ },
+
+
+ renderOriginalSourceInfo() {
+ let data = this.props.data;
+
+ let sourceMapText = t('Source Map');
+
+ let out = `
+
+ ${sourceMapText}
`;
+
+ // mapUrl not always present; e.g. uploaded source maps
+ if (data.mapUrl)
+ out += `${_.escape(data.mapUrl)}
`;
+ else
+ out += `${_.escape(data.map)}
`;
+
+ out += '
';
+
+ return out;
+ },
+
+ renderDefaultTitle() {
+ let data = this.props.data;
+ let title = [];
+
+ // TODO(mitsuhiko): this is terrible for translators but i'm too
+ // lazy to change this up right now. This should be a format string
+
+ if (defined(data.filename || data.module)) {
+ title.push({data.filename || data.module});
+ if (isUrl(data.absPath)) {
+ title.push();
+ }
+ if (defined(data.function)) {
+ title.push( {t('in')} );
+ }
+ }
+
+ if (defined(data.function)) {
+ title.push({data.function});
+ }
+
+ // we don't want to render out zero line numbers which are used to
+ // indicate lack of source information for native setups. We could
+ // TODO(mitsuhiko): only do this for events from native platforms?
+ if (defined(data.lineNo) && data.lineNo != 0) {
+ // TODO(dcramer): we need to implement source mappings
+ // title.push( View Code);
+ title.push( {t('at line')} );
+ if (defined(data.colNo)) {
+ title.push({data.lineNo}:{data.colNo});
+ } else {
+ title.push({data.lineNo});
+ }
+ }
+
+ if (defined(data.package)) {
+ title.push( {t('within')} );
+ title.push({trimPackage(data.package)});
+ }
+
+ if (defined(data.origAbsPath)) {
+ title.push(
+
+
+
+ );
+ }
+
+ if (data.inApp) {
+ title.push({t('application')});
+ }
+ return title;
+ },
+
+ renderContextLine(line, activeLineNo) {
+ let liClassName = 'expandable';
+ if (line[0] === activeLineNo) {
+ liClassName += ' active';
+ }
+
+ let lineWs;
+ let lineCode;
+ if (defined(line[1]) && line[1].match) {
+ [, lineWs, lineCode] = line[1].match(/^(\s*)(.*?)$/m);
+ } else {
+ lineWs = '';
+ lineCode = '';
+ }
+ return (
+
+ {
+ lineWs}{lineCode
+ }
+
+ );
+ },
+
+ renderContext() {
+ let data = this.props.data;
+ let context = '';
+ let {isExpanded} = this.state;
+
+ let outerClassName = 'context';
+ if (isExpanded) {
+ outerClassName += ' expanded';
+ }
+
+ let hasContextSource = this.hasContextSource();
+ let hasContextVars = this.hasContextVars();
+ let expandable = this.isExpandable();
+
+ let contextLines = isExpanded
+ ? data.context
+ : data.context && data.context.filter(l => l[0] === data.lineNo);
+
+ if (hasContextSource || hasContextVars) {
+ let startLineNo = hasContextSource ? data.context[0][0] : '';
+ context = (
+
+
+ {defined(data.errors) &&
+ - {data.errors.join(', ')}
+ }
+
+ {data.context && contextLines.map((line, index) => {
+ return ;
+ })}
+
+ {hasContextVars &&
+
+ }
+
+
+ );
+ }
+ return context;
+ },
+
+ renderExpander() {
+ if (!this.isExpandable()) {
+ return null;
+ }
+ return (
+
+
+
+ );
+ },
+
+ renderDefaultLine() {
+ return (
+
+ {this.renderDefaultTitle()}
+ {this.renderExpander()}
+
+ );
+ },
+
+ renderCocoaLine() {
+ let data = this.props.data;
+ let className = 'stacktrace-table';
+ return (
+
+ {defined(data.package)
+ ? (
+
+ {trimPackage(data.package)}
+
+ ) : (
+
+ )
+ }
+
+ {data.instructionAddr}
+
+
+ {data.function || ''}
+ {data.instructionOffset &&
+ {' + ' + data.instructionOffset}}
+ {data.filename &&
+ {data.filename}
+ {data.lineNo ? ':' + data.lineNo : ''}}
+ {this.renderExpander()}
+
+
+ );
+ },
+
+ renderLine() {
+ switch (this.props.platform) {
+ case 'objc':
+ case 'cocoa':
+ return this.renderCocoaLine();
+ default:
+ return this.renderDefaultLine();
+ }
+ },
+
+ render() {
+ let data = this.props.data;
+
+ let className = classNames({
+ 'frame': true,
+ 'system-frame': !data.inApp,
+ 'frame-errors': data.errors,
+ 'leads-to-app': !data.inApp && this.props.nextFrameInApp
+ });
+ let props = {className: className};
+
+ let context = this.renderContext();
+
+ return (
+
+ {this.renderLine()}
+ {context}
+
+ );
+ }
+});
+
+export default OldFrame;
diff --git a/src/sentry/static/sentry/app/components/events/interfaces/rawStacktraceContent.jsx b/src/sentry/static/sentry/app/components/events/interfaces/rawStacktraceContent.jsx
index 0c51c2488cb11c..9ac806b2ff8d95 100644
--- a/src/sentry/static/sentry/app/components/events/interfaces/rawStacktraceContent.jsx
+++ b/src/sentry/static/sentry/app/components/events/interfaces/rawStacktraceContent.jsx
@@ -43,6 +43,11 @@ function getRubyFrame(frame) {
return result;
}
+export function getPHPFrame(frame, idx) {
+ let funcName = (frame.function === 'null' ? '{main}' : frame.function);
+ return `#${idx} ${frame.filename || frame.module}(${frame.lineNo}): ${funcName}`;
+}
+
export function getPythonFrame(frame) {
let result = '';
if (defined(frame.filename)) {
@@ -132,21 +137,23 @@ function getPreamble(exception, platform) {
}
}
-function getFrame(frame, platform) {
+function getFrame(frame, frameIdx, platform) {
switch (platform) {
case 'javascript':
- return getJavaScriptFrame(frame);
+ return getJavaScriptFrame(frame, frameIdx);
case 'ruby':
- return getRubyFrame(frame);
+ return getRubyFrame(frame, frameIdx);
+ case 'php':
+ return getPHPFrame(frame, frameIdx);
case 'python':
- return getPythonFrame(frame);
+ return getPythonFrame(frame, frameIdx);
case 'java':
- return getJavaFrame(frame);
+ return getJavaFrame(frame, frameIdx);
case 'objc':
case 'cocoa':
- return getCocoaFrame(frame);
+ return getCocoaFrame(frame, frameIdx);
default:
- return getPythonFrame(frame);
+ return getPythonFrame(frame, frameIdx);
}
}
@@ -163,7 +170,7 @@ export default function render (data, platform, exception) {
}
data.frames.forEach((frame, frameIdx) => {
- frames.push(getFrame(frame, platform));
+ frames.push(getFrame(frame, frameIdx, platform));
if (frameIdx === firstFrameOmitted) {
frames.push((
'.. frames ' + firstFrameOmitted + ' until ' + lastFrameOmitted + ' were omitted and not available ..'
diff --git a/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.jsx b/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.jsx
index 4083f3f763ec18..9a0c0a8c034959 100644
--- a/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.jsx
+++ b/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.jsx
@@ -1,7 +1,10 @@
import React from 'react';
//import GroupEventDataSection from "../eventDataSection";
import Frame from './frame';
+import OldFrame from './oldFrame';
import {t} from '../../../locale';
+import OrganizationState from '../../../mixins/organizationState';
+
const StacktraceContent = React.createClass({
propTypes: {
@@ -10,6 +13,7 @@ const StacktraceContent = React.createClass({
platform: React.PropTypes.string,
newestFirst: React.PropTypes.bool
},
+ mixins: [OrganizationState],
getDefaultProps() {
return {
@@ -51,14 +55,29 @@ const StacktraceContent = React.createClass({
lastFrameOmitted = null;
}
+ let lastFrameIdx = null;
+ data.frames.forEach((frame, frameIdx) => {
+ if (frame.inApp) lastFrameIdx = frameIdx;
+ });
+ if (lastFrameIdx === null) {
+ lastFrameIdx = data.frames.length - 1;
+ }
+
+ let oldFrames = !this.getFeatures().has('new-tracebacks');
+ let FrameComponent = Frame;
+ if (oldFrames) {
+ FrameComponent = OldFrame;
+ }
+
let frames = [];
data.frames.forEach((frame, frameIdx) => {
let nextFrame = data.frames[frameIdx + 1];
if (this.frameIsVisible(frame, nextFrame)) {
frames.push(
-
);
@@ -74,7 +93,7 @@ const StacktraceContent = React.createClass({
}
return (
-
+
);
diff --git a/src/sentry/static/sentry/app/components/truncate.jsx b/src/sentry/static/sentry/app/components/truncate.jsx
new file mode 100644
index 00000000000000..47922b464393c6
--- /dev/null
+++ b/src/sentry/static/sentry/app/components/truncate.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+
+const Truncate = React.createClass({
+ propTypes: {
+ value: React.PropTypes.string.isRequired,
+ leftTrim: React.PropTypes.bool,
+ maxLength: React.PropTypes.number,
+ },
+
+ getDefaultProps() {
+ return {
+ leftTrim: false,
+ maxLength: 50,
+ };
+ },
+
+ getInitialState() {
+ return {
+ isExpanded: false,
+ };
+ },
+
+ onFocus(e) {
+ let {value, maxLength} = this.props;
+ if (value.length <= maxLength) return;
+ this.setState({isExpanded: true});
+ },
+
+ onBlur(e) {
+ if (this.state.isExpanded)
+ this.setState({isExpanded: false});
+ },
+
+ render() {
+ let {leftTrim, maxLength, value} = this.props;
+ let isTruncated = (value.length > maxLength);
+ let shortValue = '';
+
+ if (isTruncated) {
+ if (leftTrim) {
+ shortValue =
… {value.slice(value.length - (maxLength - 4), value.length)};
+ } else {
+ shortValue =
{value.slice(0, maxLength - 4)} …;
+ }
+ } else {
+ shortValue = value;
+ }
+
+ let className = this.props.className || '';
+ className += ' truncated';
+ if (this.state.isExpanded)
+ className += ' expanded';
+
+ return (
+
+ {shortValue}
+ {isTruncated &&
+ {value}
+ }
+
+ );
+ }
+});
+
+export default Truncate;
+
diff --git a/src/sentry/static/sentry/less/group-detail.less b/src/sentry/static/sentry/less/group-detail.less
index 49e98586231e33..9d49421b0668b1 100644
--- a/src/sentry/static/sentry/less/group-detail.less
+++ b/src/sentry/static/sentry/less/group-detail.less
@@ -831,51 +831,19 @@
.traceback {
list-style-type: none;
padding-left: 0;
+ margin: 0 -20px;
}
-.traceback > h3 {
- margin-top: 0;
- font-weight: normal;
-
- span {
- font-weight: 600;
- }
-}
-
-.traceback > pre {
- color: @gray-dark;
- font-size: 12px;
- padding: 0;
- background: inherit;
- margin: -10px 0 20px;
+// TODO(dcramer): we probably shouldnt overload these
+pre.traceback {
+ margin: 0 0 20px;
}
-.traceback ul {
+div.traceback > ul {
padding: 0;
-}
-
-.traceback > .traceback {
- padding-bottom: 5px;
-}
-.subtraceback {
- padding-left: 15px;
- padding-right: 15px;
- position: relative;
-
- > h3 {
- line-height: 26px;
- }
-
- > h3:before {
- display: block;
- content: "";
- width: 20px;
- height: 0;
- border-bottom: 1px solid #E9EBEC;
- position: absolute;
- left: -15px;
- top: 11px;
+ &:last-child {
+ margin-bottom: 0;
}
}
@@ -915,342 +883,741 @@
margin-right: -21px;
}
-.frame {
- list-style-type: none;
- position: relative;
- margin-bottom: 0;
+.traceback {
+ .frame {
+ list-style-type: none;
+ position: relative;
+ margin: 0;
+ border-top: 1px solid lighten(@trim, 4);
- &.frame-errors {}
+ &.frame-errors {}
- &.system-frame {}
+ &.system-frame { }
- &.leads-to-app {}
+ &.leads-to-app {}
- h3 {
- font-size: 22px;
- }
+ h3 {
+ font-size: 22px;
+ }
- p {
- font-size: 12px;
- margin-bottom: 12px;
- line-height: 1.4;
+ &.is-expandable p {
+ cursor: pointer;
+ &:hover {
+ background: lighten(@blue-light, 25);
+ }
+ }
- a.annotation {
- &.trigger-popover {
- cursor: pointer;
+ &.system-frame.is-expandable p:hover {
+ background: darken(@white-dark, 5);
+ }
+
+ p {
+ padding: 8px 20px;
+ font-size: 12px;
+ margin: 0;
+ // line-height: 1.4;
+ line-height: 16px;
+ color: @gray-darkest;
+ background: lighten(@blue-light, 30);
+
+ a.annotation {
+ &.trigger-popover {
+ cursor: pointer;
+ }
+ color: inherit;
+ padding: 0 1px;
+ border-bottom: 1px dotted #666;
+ &:hover { text-decoration: none; }
}
- color: inherit;
- padding: 0 1px;
- border-bottom: 1px dotted #666;
- &:hover { text-decoration: none; }
}
- }
- .original-src {
- font-size: 12px;
- padding-left: 3px;
- position: relative;
- top: 1px;
- }
+ &.system-frame p {
+ background: @white-dark;
+ }
- .icon-open {
- font-size: 12px;
- margin-right: 3px;
- margin-left: 3px;
- position: relative;
- top: 1px;
- }
+ p.as-table {
+ display: flex;
+ align-items: baseline;
+ width: 100%;
- .in-at {
- opacity: .8;
- margin: 0 2px;
- }
+ > span {
+ display: block;
+ padding: 0 5px;
+ }
- .blame {
- color: lighten(@gray, 5);
+ .package {
+ width: 15%;
+ font-size: 13px;
+ font-weight: bold;
+ .truncate;
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
- a {
- color: @gray;
+ .address {
+ font-family: @font-family-code;
+ font-size: 11px;
+ color: @gray-dark;
+ letter-spacing: -0.25px;
+ width: 85px;
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
+
+ .symbol {
+ word-break: break-word;
+ flex: 1;
+
+ code {
+ background: transparent;
+ color: @blue-dark;
+ padding-right: 5px;
+ }
+
+ span.offset {
+ font-weight: bold;
+ padding-right: 10px;
+ }
+
+ span.filename {
+ color: @purple;
+
+ &:before {
+ content: "(";
+ }
+ &:after {
+ content: ")";
+ }
+ }
+ }
}
- .icon-mark-github {
+ .original-src {
+ font-size: 12px;
+ padding-left: 3px;
position: relative;
top: 1px;
}
- }
- .tooltip-inner {
- word-wrap: break-word;
- text-align: left;
- max-width: 300px;
- }
+ .icon-open {
+ font-size: 12px;
+ margin-right: 3px;
+ margin-left: 3px;
+ position: relative;
+ top: 1px;
+ }
- .divider {
- border-left: 1px solid @trim;
- display: inline-block;
- width: 1px;
- height: 10px;
- margin: 0 6px;
- position: relative;
- top: 1px;
- }
+ .in-at {
+ opacity: .6;
+ margin: 0 2px;
+ }
- code {
- padding: 0;
- background: #fff;
- font-size: inherit;
- color: inherit;
- }
+ .blame {
+ color: lighten(@gray, 5);
- .context {
- margin-top: -5px;
- margin-bottom: 15px;
+ a {
+ color: @gray;
+ }
- table.key-value {
- margin-top: 20px;
+ .icon-mark-github {
+ position: relative;
+ top: 1px;
+ }
}
- }
- .tag-app {
- color: #aaa;
- font-size: 0.9em;
- margin-left: 10px;
- }
+ .tooltip-inner {
+ word-wrap: break-word;
+ text-align: left;
+ max-width: 300px;
+ }
- > div > table.key-value {
- margin-bottom: 5px;
- > tbody > tr > th {
- color: @gray-dark;
- text-align: right;
- padding-right: 12px !important;
+ .divider {
+ border-left: 1px solid @trim;
+ display: inline-block;
+ width: 1px;
+ height: 10px;
+ margin: 0 6px;
+ position: relative;
+ top: 1px;
}
- }
- .btn-toggle {
- display: block;
- float: right;
- .square(18px);
- padding: 0;
- line-height: 18px;
- font-size: 9px;
- text-align: center;
- }
+ code {
+ padding: 0;
+ background: inherit;
+ font-size: inherit;
+ color: inherit;
+ }
+
+ .context {
+ display: none;
+ background: #fff;
+ margin: 0;
+ padding: 8px 0;
+
+ > li {
+ padding: 0 20px;
+ background: inherit;
+ }
+
+ table.key-value {
+ border-top: 1px solid lighten(@trim, 4);
+ padding: 0 20px;
+ margin: 0 0 -8px;
+
+ td {
+ border-bottom: 1px solid lighten(@trim, 4) !important;
+
+ &.key {
+ width: 125px;
+ max-width: 125px;
+ }
+
+ &.value pre {
+ background: inherit;
+ }
+ }
+
+ tr:last-child {
+ td {
+ border-bottom: 0 !important;
+ }
+ }
+ }
+
+ &.expanded {
+ display: block;
+ }
+ }
+
+ .box-clippable {
+ margin-left: 0;
+ margin-right: 0;
+ &:first-of-type {
+ margin-top: 0;
+ }
+ }
+
+ .tag-app {
+ color: #aaa;
+ font-size: 0.9em;
+ margin-left: 10px;
+ }
+
+ > div > table.key-value {
+ margin-bottom: 5px;
+ > tbody > tr > th {
+ color: @gray-dark;
+ text-align: right;
+ padding-right: 12px !important;
+ }
+ }
+
+ .btn-toggle {
+ display: block;
+ float: right;
+ .square(16px);
+ padding: 0;
+ line-height: 16px;
+ font-size: 9px;
+ text-align: center;
+ }
+
+ .expand-button:hover {
+ cursor: pointer;
+ }
+
+ &.expanded {
+ > p {
+ color: #000;
+ }
+
+ .expandable {
+ height: auto;
+ }
+ }
- .expand-button:hover {
- cursor: pointer;
+ &:last-child {
+ .context {
+ margin-bottom: 0;
+ }
+
+ .toggle-expand .btn {
+ margin-bottom: -13px;
+ }
+ }
}
- &.expanded {
+ .expandable {
+ height: 0;
+ overflow: hidden;
+ position: relative;
+
+ .icon-plus {
+ position: absolute;
+ left: 8px;
+ top: 6px;
+ opacity: .25;
+ .transition(.1s opacity linear);
+ }
+
+ &.key-value {
+ display: none;
+ }
- > p {
- color: #000;
+ .ws {
+ display: none;
}
+ &:hover {
+ .icon-plus {
+ opacity: .5;
+ }
+ }
+ }
+
+ .expanded {
.expandable {
+ overflow: none;
height: auto;
}
+
+ .ws {
+ display: inline;
+ }
}
- &:last-child {
- .context {
- margin-bottom: 0;
+ ol.context {
+ margin: 0;
+ list-style-position: inside;
+ border-radius: 3px;
+ padding-left: 0;
+
+ .key-value {
+ display: none;
+
+ pre {
+ overflow: auto;
+ }
}
- .toggle-expand .btn {
- margin-bottom: -13px;
+ > li {
+ padding-left: 15px;
+ font-family: @font-family-code;
+ color: #222;
+ background-color: #f6f7f8;
+ line-height: 24px;
+ font-size: 12px;
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ min-height: 24px;
}
- }
-}
-.expandable {
- height: 0;
- overflow: hidden;
- position: relative;
+ > li.active {
+ background-color: #f6f7f8;
+ list-style-type: none;
+ border-radius: 2px;
- .icon-plus {
- position: absolute;
- left: 8px;
- top: 6px;
- opacity: .25;
- .transition(.1s opacity linear);
- }
+ &:first-child:last-child {
+ background-color: inherit;
+ color: inherit;
+ border-radius: 0;
+ }
- &.key-value {
- display: none;
- }
+ pre {
+ color: @gray-dark;
+ }
+ }
- .ws {
- display: none;
- }
+ > li:first-child {
+ border-radius: 2px 2px 0 0;
+ }
- &:hover {
- .icon-plus {
- opacity: .5;
+ > li:last-of-type {
+ border-radius: 0 0 2px 2px;
+ }
+
+ li.closed {
+ border-radius: 2px;
+ }
+
+ &.expanded {
+ .key-value {
+ display: table;
+ }
+
+ > li.active {
+ background-color: @purple;
+ color: #fff;
+ list-style-type: inherit;
+ border-radius: 0;
+ }
}
}
-}
-.expanded {
- .expandable {
- overflow: none;
- height: auto;
+ ol.context-line {
+ > li {
+ > span {
+ float: right;
+ }
+ }
}
- .ws {
- display: inline;
+ .exception-mechanism {
+ margin: 15px 0;
}
}
-ol.context {
- margin: 0;
- list-style-position: inside;
- border-radius: 3px;
+//TODO(mitsuhiko): kill us when the A/B test is over
+.old-traceback {
+ list-style-type: none;
padding-left: 0;
+ margin-bottom: 20px;
- .key-value {
- display: none;
-
- pre {
- overflow: auto;
+ > ul {
+ padding: 0;
+ &:last-child {
+ margin-bottom: 0;
}
}
- > li {
- cursor: pointer;
- padding-left: 15px;
- font-family: @font-family-code;
- color: #222;
- background-color: #f6f7f8;
- line-height: 24px;
- font-size: 12px;
- white-space: pre;
- white-space: pre-wrap;
- word-wrap: break-word;
- }
- > li.active {
- background-color: #f6f7f8;
- min-height: 24px;
+ .frame {
list-style-type: none;
- border-radius: 2px;
+ position: relative;
+ margin-bottom: 0;
- pre {
- color: @gray-dark;
+ &.frame-errors {}
+
+ &.system-frame {}
+
+ &.leads-to-app {}
+
+ h3 {
+ font-size: 22px;
}
- }
- > li:first-child {
- border-radius: 2px 2px 0 0;
- }
+ p {
+ font-size: 12px;
+ margin-bottom: 12px;
+ line-height: 1.4;
- > li:last-of-type {
- border-radius: 0 0 2px 2px;
- }
+ a.annotation {
+ &.trigger-popover {
+ cursor: pointer;
+ }
+ color: inherit;
+ padding: 0 1px;
+ border-bottom: 1px dotted #666;
+ &:hover { text-decoration: none; }
+ }
+ }
- li.closed {
- border-radius: 2px;
+ .original-src {
+ font-size: 12px;
+ padding-left: 3px;
+ position: relative;
+ top: 1px;
+ }
+
+ .icon-open {
+ font-size: 12px;
+ margin-right: 3px;
+ margin-left: 3px;
+ position: relative;
+ top: 1px;
+ }
+
+ .in-at {
+ opacity: .8;
+ margin: 0 2px;
+ }
+
+ .blame {
+ color: lighten(@gray, 5);
+
+ a {
+ color: @gray;
+ }
+
+ .icon-mark-github {
+ position: relative;
+ top: 1px;
+ }
+ }
+
+ .tooltip-inner {
+ word-wrap: break-word;
+ text-align: left;
+ max-width: 300px;
+ }
+
+ .divider {
+ border-left: 1px solid @trim;
+ display: inline-block;
+ width: 1px;
+ height: 10px;
+ margin: 0 6px;
+ position: relative;
+ top: 1px;
+ }
+
+ code {
+ padding: 0;
+ background: #fff;
+ font-size: inherit;
+ color: inherit;
+ }
+
+ .context {
+ margin-top: -5px;
+ margin-bottom: 15px;
+
+ table.key-value {
+ margin-top: 20px;
+ }
+ }
+
+ .tag-app {
+ color: #aaa;
+ font-size: 0.9em;
+ margin-left: 10px;
+ }
+
+ > div > table.key-value {
+ margin-bottom: 5px;
+ > tbody > tr > th {
+ color: @gray-dark;
+ text-align: right;
+ padding-right: 12px !important;
+ }
+ }
+
+ .btn-toggle {
+ display: block;
+ float: right;
+ .square(18px);
+ padding: 0;
+ line-height: 18px;
+ font-size: 9px;
+ text-align: center;
+ }
+
+ .expand-button:hover {
+ cursor: pointer;
+ }
+
+ &.expanded {
+
+ > p {
+ color: #000;
+ }
+
+ .expandable {
+ height: auto;
+ }
+ }
+
+ &:last-child {
+ .context {
+ margin-bottom: 0;
+ }
+
+ .toggle-expand .btn {
+ margin-bottom: -13px;
+ }
+ }
}
- &.expanded {
- .key-value {
- display: table;
+ .expandable {
+ height: 0;
+ overflow: hidden;
+ position: relative;
+
+ .icon-plus {
+ position: absolute;
+ left: 8px;
+ top: 6px;
+ opacity: .25;
+ .transition(.1s opacity linear);
}
- > li.active {
- background-color: @purple;
- color: #fff;
- list-style-type: inherit;
- border-radius: 0;
+ &.key-value {
+ display: none;
+ }
+
+ .ws {
+ display: none;
+ }
+
+ &:hover {
+ .icon-plus {
+ opacity: .5;
+ }
}
}
-}
-ol.context-line {
- > li {
- > span {
- float: right;
+ .expanded {
+ .expandable {
+ overflow: none;
+ height: auto;
+ }
+
+ .ws {
+ display: inline;
}
}
-}
-.expanded {
ol.context {
+ margin: 0;
+ list-style-position: inside;
+ border-radius: 3px;
+ padding-left: 0;
+
+ .key-value {
+ display: none;
+
+ pre {
+ overflow: auto;
+ }
+ }
+
> li {
- min-height: 22px;
+ cursor: pointer;
+ padding-left: 15px;
+ font-family: @font-family-code;
+ color: #222;
+ background-color: #f6f7f8;
+ line-height: 24px;
+ font-size: 12px;
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
}
> li.active {
- background-color: @blue;
- color: #fff;
+ background-color: #f6f7f8;
+ min-height: 24px;
+ list-style-type: none;
+ border-radius: 2px;
+
+ pre {
+ color: @gray-dark;
+ }
}
- }
-}
-.stacktrace-table {
- display: flex;
- align-items: baseline;
- line-height: 16px;
- box-shadow: inset 0 1px 0 #E6EEF4;
+ > li:first-child {
+ border-radius: 2px 2px 0 0;
+ }
+
+ > li:last-of-type {
+ border-radius: 0 0 2px 2px;
+ }
- .trace-col {
- padding: 6px 5px 3px;
- font-size: 12px;
+ li.closed {
+ border-radius: 2px;
+ }
+
+ &.expanded {
+ .key-value {
+ display: table;
+ }
+
+ > li.active {
+ background-color: @purple;
+ color: #fff;
+ list-style-type: inherit;
+ border-radius: 0;
+ }
+ }
}
- .package {
- width: 15%;
- font-size: 13px;
- font-weight: bold;
- .truncate;
- flex-grow: 0;
- flex-shrink: 0;
+ ol.context-line {
+ > li {
+ > span {
+ float: right;
+ }
+ }
}
- .address {
- font-family: @font-family-code;
- font-size: 11px;
- color: @gray-dark;
- letter-spacing: -0.25px;
- width: 85px;
- flex-grow: 0;
- flex-shrink: 0;
+ .expanded {
+ ol.context {
+ > li {
+ min-height: 22px;
+ }
+ > li.active {
+ background-color: @blue;
+ color: #fff;
+ }
+ }
}
- .symbol {
- word-break: break-word;
+ .stacktrace-table {
+ display: flex;
+ align-items: baseline;
+ line-height: 16px;
+ box-shadow: inset 0 1px 0 #E6EEF4;
- code {
- background: transparent;
- color: @blue-dark;
- padding-right: 5px;
+ .trace-col {
+ padding: 6px 5px 3px;
+ font-size: 12px;
}
- span.offset {
+ .package {
+ width: 15%;
+ font-size: 13px;
font-weight: bold;
- padding-right: 10px;
+ .truncate;
+ flex-grow: 0;
+ flex-shrink: 0;
}
- span.filename {
- color: @purple;
+ .address {
+ font-family: @font-family-code;
+ font-size: 11px;
+ color: @gray-dark;
+ letter-spacing: -0.25px;
+ width: 85px;
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
- &:before {
- content: "(";
+ .symbol {
+ word-break: break-word;
+
+ code {
+ background: transparent;
+ color: @blue-dark;
+ padding-right: 5px;
}
- &:after {
- content: ")";
+
+ span.offset {
+ font-weight: bold;
+ padding-right: 10px;
+ }
+
+ span.filename {
+ color: @purple;
+
+ &:before {
+ content: "(";
+ }
+ &:after {
+ content: ")";
+ }
}
}
}
-}
-.system-frame .stacktrace-table {
- background-color: @white-dark;
-}
+ .system-frame .stacktrace-table {
+ background-color: @white-dark;
+ }
-.leads-to-app .stacktrace-table {
- background: lighten(@blue-light, 30);
-}
+ .leads-to-app .stacktrace-table {
+ background: lighten(@blue-light, 30);
+ }
-.exception-mechanism {
- margin: 15px 0;
+ .exception-mechanism {
+ margin: 15px 0;
+ }
}
#full-message {
diff --git a/src/sentry/static/sentry/less/shared-components.less b/src/sentry/static/sentry/less/shared-components.less
index 38ccc5b4094ebe..fb3553c22f0333 100644
--- a/src/sentry/static/sentry/less/shared-components.less
+++ b/src/sentry/static/sentry/less/shared-components.less
@@ -3294,6 +3294,28 @@ div.qrcode {
background: @red;
}
+/**
+ * Truncate component.
+ */
+.truncated {
+ position: relative;
+ .full-value {
+ display: none;
+ position: absolute;
+ background: @white;
+ left: -5px;
+ top: -5px;
+ padding: 4px;
+ border: 1px solid @trim;
+ white-space: nowrap;
+ border-radius: 4px;
+ }
+ &.expanded .full-value {
+ z-index: 10;
+ display: block;
+ }
+}
+
/**
* Responsive small screens
* ============================================================================