From 2ff7b113a1fb004db26522d6509155742773255c Mon Sep 17 00:00:00 2001 From: David Cramer Date: Fri, 10 Jun 2016 16:47:58 -0700 Subject: [PATCH 1/4] Initial pass at new stacktrace rendering - New cell-based stack rendering - Add PHP specific rendering - Remove legacy CSS - Left-truncate filenames (expand on hover) - Move message above exception - Add clipping to stack locals - Prevent propagation of clicks on ClippedBox - Dont collapse context on click (require header click) @getsentry/ui --- src/sentry/interfaces/message.py | 2 +- .../sentry/app/components/clippedBox.jsx | 4 +- .../components/events/interfaces/frame.jsx | 78 ++--- .../components/events/interfaces/oldFrame.jsx | 295 ++++++++++++++++++ .../interfaces/rawStacktraceContent.jsx | 23 +- .../events/interfaces/stacktraceContent.jsx | 9 + .../static/sentry/app/components/truncate.jsx | 71 +++++ .../static/sentry/less/group-detail.less | 148 +++++---- .../static/sentry/less/shared-components.less | 22 ++ 9 files changed, 533 insertions(+), 119 deletions(-) create mode 100644 src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx create mode 100644 src/sentry/static/sentry/app/components/truncate.jsx 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..218869da607b8e 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()}

    @@ -273,6 +259,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..934708ae753c49 --- /dev/null +++ b/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx @@ -0,0 +1,295 @@ +import React from 'react'; +import _ from 'underscore'; +import classNames from 'classnames'; +import PureRenderMixin from 'react-addons-pure-render-mixin'; +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) && +
    1. {data.errors.join(', ')}
    2. + } + + {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..daf0a03344eaf2 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.jsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/stacktraceContent.jsx @@ -51,6 +51,14 @@ 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 frames = []; data.frames.forEach((frame, frameIdx) => { let nextFrame = data.frames[frameIdx + 1]; @@ -59,6 +67,7 @@ const StacktraceContent = React.createClass({ ); 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..c208a58d8117f3 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; } } @@ -918,11 +886,12 @@ .frame { list-style-type: none; position: relative; - margin-bottom: 0; + margin: 0; + border-top: 1px solid lighten(@trim, 4); &.frame-errors {} - &.system-frame {} + &.system-frame { } &.leads-to-app {} @@ -930,10 +899,24 @@ font-size: 22px; } + &.is-expandable p { + cursor: pointer; + &:hover { + background: lighten(@blue-light, 25); + } + } + + &.system-frame.is-expandable p:hover { + background: darken(@white-dark, 5); + } + p { + padding: 8px 20px; font-size: 12px; - margin-bottom: 12px; + margin: 0; line-height: 1.4; + color: @gray-darkest; + background: lighten(@blue-light, 30); a.annotation { &.trigger-popover { @@ -946,6 +929,10 @@ } } + &.system-frame p { + background: @white-dark; + } + .original-src { font-size: 12px; padding-left: 3px; @@ -962,7 +949,7 @@ } .in-at { - opacity: .8; + opacity: .6; margin: 0 2px; } @@ -997,17 +984,57 @@ code { padding: 0; - background: #fff; + background: inherit; font-size: inherit; color: inherit; } .context { - margin-top: -5px; - margin-bottom: 15px; + display: none; + background: #fff; + margin: 0; + padding: 8px 0; + + > li { + padding: 0 20px; + background: inherit; + } table.key-value { - margin-top: 20px; + 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; } } @@ -1029,9 +1056,9 @@ .btn-toggle { display: block; float: right; - .square(18px); + .square(16px); padding: 0; - line-height: 18px; + line-height: 16px; font-size: 9px; text-align: center; } @@ -1041,7 +1068,6 @@ } &.expanded { - > p { color: #000; } @@ -1116,7 +1142,6 @@ ol.context { } > li { - cursor: pointer; padding-left: 15px; font-family: @font-family-code; color: #222; @@ -1126,13 +1151,20 @@ ol.context { white-space: pre; white-space: pre-wrap; word-wrap: break-word; + min-height: 24px; } + > li.active { background-color: #f6f7f8; - min-height: 24px; list-style-type: none; border-radius: 2px; + &:first-child:last-child { + background-color: inherit; + color: inherit; + border-radius: 0; + } + pre { color: @gray-dark; } @@ -1172,18 +1204,6 @@ ol.context-line { } } -.expanded { - ol.context { - > li { - min-height: 22px; - } - > li.active { - background-color: @blue; - color: #fff; - } - } -} - .stacktrace-table { display: flex; align-items: baseline; 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 * ============================================================================ From 67c814eaa20e1302c25cb393586f29ad9c0c4283 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 14 Jun 2016 23:22:33 +0200 Subject: [PATCH 2/4] Updated cocoa stacks to look more like the new style stacks now --- .../components/events/interfaces/frame.jsx | 19 ++- .../static/sentry/less/group-detail.less | 125 +++++++++--------- 2 files changed, 68 insertions(+), 76 deletions(-) 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 218869da607b8e..538d978297c35a 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/frame.jsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/frame.jsx @@ -216,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}} @@ -239,8 +238,8 @@ const Frame = React.createClass({ {data.filename} {data.lineNo ? ':' + data.lineNo : ''}} {this.renderExpander()} -
    -
    + +

    ); }, diff --git a/src/sentry/static/sentry/less/group-detail.less b/src/sentry/static/sentry/less/group-detail.less index c208a58d8117f3..9a66ba0aa14938 100644 --- a/src/sentry/static/sentry/less/group-detail.less +++ b/src/sentry/static/sentry/less/group-detail.less @@ -914,7 +914,8 @@ div.traceback > ul { padding: 8px 20px; font-size: 12px; margin: 0; - line-height: 1.4; + // line-height: 1.4; + line-height: 16px; color: @gray-darkest; background: lighten(@blue-light, 30); @@ -933,6 +934,63 @@ div.traceback > ul { background: @white-dark; } + p.as-table { + display: flex; + align-items: baseline; + width: 100%; + + > span { + display: block; + padding: 0 5px; + } + + .package { + width: 15%; + font-size: 13px; + font-weight: bold; + .truncate; + flex-grow: 0; + flex-shrink: 0; + } + + .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: ")"; + } + } + } + } + .original-src { font-size: 12px; padding-left: 3px; @@ -1204,71 +1262,6 @@ ol.context-line { } } -.stacktrace-table { - display: flex; - align-items: baseline; - line-height: 16px; - box-shadow: inset 0 1px 0 #E6EEF4; - - .trace-col { - padding: 6px 5px 3px; - font-size: 12px; - } - - .package { - width: 15%; - font-size: 13px; - font-weight: bold; - .truncate; - flex-grow: 0; - flex-shrink: 0; - } - - .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; - - 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: ")"; - } - } - } -} - -.system-frame .stacktrace-table { - background-color: @white-dark; -} - -.leads-to-app .stacktrace-table { - background: lighten(@blue-light, 30); -} - .exception-mechanism { margin: 15px 0; } From 51a7a3c3da3f78680a71399139f13107f8f2993d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Wed, 15 Jun 2016 00:31:06 +0200 Subject: [PATCH 3/4] Added feature flag for new and old frame rendering --- .../api/serializers/models/organization.py | 2 + src/sentry/conf/server.py | 1 + src/sentry/features/__init__.py | 1 + .../components/events/interfaces/oldFrame.jsx | 1 - .../events/interfaces/stacktraceContent.jsx | 14 +- .../static/sentry/less/group-detail.less | 888 ++++++++++++------ 6 files changed, 637 insertions(+), 270 deletions(-) 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/static/sentry/app/components/events/interfaces/oldFrame.jsx b/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx index 934708ae753c49..b1c679e9dc6101 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx @@ -10,7 +10,6 @@ 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; 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 daf0a03344eaf2..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 { @@ -59,12 +63,18 @@ const StacktraceContent = React.createClass({ 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( - +
      {frames}
    ); diff --git a/src/sentry/static/sentry/less/group-detail.less b/src/sentry/static/sentry/less/group-detail.less index 9a66ba0aa14938..9d49421b0668b1 100644 --- a/src/sentry/static/sentry/less/group-detail.less +++ b/src/sentry/static/sentry/less/group-detail.less @@ -883,387 +883,741 @@ div.traceback > ul { margin-right: -21px; } -.frame { - list-style-type: none; - position: relative; - margin: 0; - border-top: 1px solid lighten(@trim, 4); +.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; - } - - &.is-expandable p { - cursor: pointer; - &:hover { - background: lighten(@blue-light, 25); + h3 { + font-size: 22px; } - } - &.system-frame.is-expandable p:hover { - background: darken(@white-dark, 5); - } + &.is-expandable p { + cursor: pointer; + &:hover { + background: lighten(@blue-light, 25); + } + } - p { - padding: 8px 20px; - font-size: 12px; - margin: 0; - // line-height: 1.4; - line-height: 16px; - color: @gray-darkest; - background: lighten(@blue-light, 30); + &.system-frame.is-expandable p:hover { + background: darken(@white-dark, 5); + } - a.annotation { - &.trigger-popover { - cursor: pointer; + 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; } } - } - &.system-frame p { - background: @white-dark; - } + &.system-frame p { + background: @white-dark; + } - p.as-table { - display: flex; - align-items: baseline; - width: 100%; + p.as-table { + display: flex; + align-items: baseline; + width: 100%; - > span { - display: block; - padding: 0 5px; + > span { + display: block; + padding: 0 5px; + } + + .package { + width: 15%; + font-size: 13px; + font-weight: bold; + .truncate; + flex-grow: 0; + flex-shrink: 0; + } + + .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: ")"; + } + } + } } - .package { - width: 15%; - font-size: 13px; - font-weight: bold; - .truncate; - flex-grow: 0; - flex-shrink: 0; + .original-src { + font-size: 12px; + padding-left: 3px; + position: relative; + top: 1px; } - .address { - font-family: @font-family-code; - font-size: 11px; - color: @gray-dark; - letter-spacing: -0.25px; - width: 85px; - flex-grow: 0; - flex-shrink: 0; + .icon-open { + font-size: 12px; + margin-right: 3px; + margin-left: 3px; + position: relative; + top: 1px; } - .symbol { - word-break: break-word; - flex: 1; + .in-at { + opacity: .6; + margin: 0 2px; + } - code { - background: transparent; - color: @blue-dark; - padding-right: 5px; + .blame { + color: lighten(@gray, 5); + + a { + color: @gray; } - span.offset { - font-weight: bold; - padding-right: 10px; + .icon-mark-github { + position: relative; + top: 1px; } + } - span.filename { - color: @purple; + .tooltip-inner { + word-wrap: break-word; + text-align: left; + max-width: 300px; + } - &:before { - content: "("; + .divider { + border-left: 1px solid @trim; + display: inline-block; + width: 1px; + height: 10px; + margin: 0 6px; + position: relative; + top: 1px; + } + + 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; + } } - &:after { - content: ")"; + + tr:last-child { + td { + border-bottom: 0 !important; + } } } + + &.expanded { + display: block; + } } - } - .original-src { - font-size: 12px; - padding-left: 3px; - position: relative; - top: 1px; - } + .box-clippable { + margin-left: 0; + margin-right: 0; + &:first-of-type { + margin-top: 0; + } + } - .icon-open { - font-size: 12px; - margin-right: 3px; - margin-left: 3px; - position: relative; - top: 1px; - } + .tag-app { + color: #aaa; + font-size: 0.9em; + margin-left: 10px; + } - .in-at { - opacity: .6; - margin: 0 2px; - } + > div > table.key-value { + margin-bottom: 5px; + > tbody > tr > th { + color: @gray-dark; + text-align: right; + padding-right: 12px !important; + } + } - .blame { - color: lighten(@gray, 5); + .btn-toggle { + display: block; + float: right; + .square(16px); + padding: 0; + line-height: 16px; + font-size: 9px; + text-align: center; + } - a { - color: @gray; + .expand-button:hover { + cursor: pointer; } - .icon-mark-github { - position: relative; - top: 1px; + &.expanded { + > p { + color: #000; + } + + .expandable { + height: auto; + } } - } - .tooltip-inner { - word-wrap: break-word; - text-align: left; - max-width: 300px; + &:last-child { + .context { + margin-bottom: 0; + } + + .toggle-expand .btn { + margin-bottom: -13px; + } + } } - .divider { - border-left: 1px solid @trim; - display: inline-block; - width: 1px; - height: 10px; - margin: 0 6px; + .expandable { + height: 0; + overflow: hidden; position: relative; - top: 1px; + + .icon-plus { + position: absolute; + left: 8px; + top: 6px; + opacity: .25; + .transition(.1s opacity linear); + } + + &.key-value { + display: none; + } + + .ws { + display: none; + } + + &:hover { + .icon-plus { + opacity: .5; + } + } } - code { - padding: 0; - background: inherit; - font-size: inherit; - color: inherit; + .expanded { + .expandable { + overflow: none; + height: auto; + } + + .ws { + display: inline; + } } - .context { - display: none; - background: #fff; + ol.context { margin: 0; - padding: 8px 0; + list-style-position: inside; + border-radius: 3px; + padding-left: 0; - > li { - padding: 0 20px; - background: inherit; - } + .key-value { + display: none; - table.key-value { - border-top: 1px solid lighten(@trim, 4); - padding: 0 20px; - margin: 0 0 -8px; + pre { + overflow: auto; + } + } - td { - border-bottom: 1px solid lighten(@trim, 4) !important; + > 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; + } - &.key { - width: 125px; - max-width: 125px; - } + > li.active { + background-color: #f6f7f8; + list-style-type: none; + border-radius: 2px; - &.value pre { - background: inherit; - } + &:first-child:last-child { + background-color: inherit; + color: inherit; + border-radius: 0; } - tr:last-child { - td { - border-bottom: 0 !important; - } + pre { + color: @gray-dark; } } + > li:first-child { + border-radius: 2px 2px 0 0; + } + + > li:last-of-type { + border-radius: 0 0 2px 2px; + } + + li.closed { + border-radius: 2px; + } + &.expanded { - display: block; + .key-value { + display: table; + } + + > li.active { + background-color: @purple; + color: #fff; + list-style-type: inherit; + border-radius: 0; + } } } - .box-clippable { - margin-left: 0; - margin-right: 0; - &:first-of-type { - margin-top: 0; + ol.context-line { + > li { + > span { + float: right; + } } } - .tag-app { - color: #aaa; - font-size: 0.9em; - margin-left: 10px; + .exception-mechanism { + margin: 15px 0; } +} - > div > table.key-value { - margin-bottom: 5px; - > tbody > tr > th { - color: @gray-dark; - text-align: right; - padding-right: 12px !important; - } - } +//TODO(mitsuhiko): kill us when the A/B test is over +.old-traceback { + list-style-type: none; + padding-left: 0; + margin-bottom: 20px; - .btn-toggle { - display: block; - float: right; - .square(16px); + > ul { padding: 0; - line-height: 16px; - font-size: 9px; - text-align: center; + &:last-child { + margin-bottom: 0; + } } - .expand-button:hover { - cursor: pointer; - } + .frame { + list-style-type: none; + position: relative; + margin-bottom: 0; + + &.frame-errors {} + + &.system-frame {} - &.expanded { - > p { - color: #000; + &.leads-to-app {} + + h3 { + font-size: 22px; } - .expandable { - height: auto; + p { + font-size: 12px; + margin-bottom: 12px; + line-height: 1.4; + + a.annotation { + &.trigger-popover { + cursor: pointer; + } + 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; + } + + .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; } - } - &:last-child { .context { - margin-bottom: 0; + margin-top: -5px; + margin-bottom: 15px; + + table.key-value { + margin-top: 20px; + } } - .toggle-expand .btn { - margin-bottom: -13px; + .tag-app { + color: #aaa; + font-size: 0.9em; + margin-left: 10px; } - } -} -.expandable { - height: 0; - overflow: hidden; - position: relative; + > div > table.key-value { + margin-bottom: 5px; + > tbody > tr > th { + color: @gray-dark; + text-align: right; + padding-right: 12px !important; + } + } - .icon-plus { - position: absolute; - left: 8px; - top: 6px; - opacity: .25; - .transition(.1s opacity linear); - } + .btn-toggle { + display: block; + float: right; + .square(18px); + padding: 0; + line-height: 18px; + font-size: 9px; + text-align: center; + } - &.key-value { - display: none; - } + .expand-button:hover { + cursor: pointer; + } - .ws { - display: none; - } + &.expanded { - &:hover { - .icon-plus { - opacity: .5; + > p { + color: #000; + } + + .expandable { + height: auto; + } + } + + &:last-child { + .context { + margin-bottom: 0; + } + + .toggle-expand .btn { + margin-bottom: -13px; + } } } -} -.expanded { .expandable { - overflow: none; - height: auto; - } + height: 0; + overflow: hidden; + position: relative; - .ws { - display: inline; - } -} + .icon-plus { + position: absolute; + left: 8px; + top: 6px; + opacity: .25; + .transition(.1s opacity linear); + } -ol.context { - margin: 0; - list-style-position: inside; - border-radius: 3px; - padding-left: 0; + &.key-value { + display: none; + } - .key-value { - display: none; + .ws { + display: none; + } - pre { - overflow: auto; + &:hover { + .icon-plus { + opacity: .5; + } } } - > 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; + .expanded { + .expandable { + overflow: none; + height: auto; + } + + .ws { + display: inline; + } } - > li.active { - background-color: #f6f7f8; - list-style-type: none; - border-radius: 2px; + ol.context { + margin: 0; + list-style-position: inside; + border-radius: 3px; + padding-left: 0; - &:first-child:last-child { - background-color: inherit; - color: inherit; - border-radius: 0; + .key-value { + display: none; + + pre { + overflow: auto; + } } - pre { - color: @gray-dark; + > 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; + list-style-type: none; + border-radius: 2px; - > li:first-child { - border-radius: 2px 2px 0 0; + pre { + color: @gray-dark; + } + } + + > li:first-child { + border-radius: 2px 2px 0 0; + } + + > 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; + } + } } - > li:last-of-type { - border-radius: 0 0 2px 2px; + ol.context-line { + > li { + > span { + float: right; + } + } } - li.closed { - border-radius: 2px; + .expanded { + ol.context { + > li { + min-height: 22px; + } + > li.active { + background-color: @blue; + color: #fff; + } + } } - &.expanded { - .key-value { - display: table; + .stacktrace-table { + display: flex; + align-items: baseline; + line-height: 16px; + box-shadow: inset 0 1px 0 #E6EEF4; + + .trace-col { + padding: 6px 5px 3px; + font-size: 12px; } - > 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; + } + + .symbol { + word-break: break-word; + + 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: ")"; + } + } } } -} -.exception-mechanism { - margin: 15px 0; + .system-frame .stacktrace-table { + background-color: @white-dark; + } + + .leads-to-app .stacktrace-table { + background: lighten(@blue-light, 30); + } + + .exception-mechanism { + margin: 15px 0; + } } #full-message { From 924021f23bb8325d3b0afbeea7234e594b367743 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 16 Jun 2016 00:31:39 +0200 Subject: [PATCH 4/4] Fixed a js lint error --- .../static/sentry/app/components/events/interfaces/oldFrame.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx b/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx index b1c679e9dc6101..254f5119ae3cca 100644 --- a/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx +++ b/src/sentry/static/sentry/app/components/events/interfaces/oldFrame.jsx @@ -1,7 +1,6 @@ import React from 'react'; import _ from 'underscore'; import classNames from 'classnames'; -import PureRenderMixin from 'react-addons-pure-render-mixin'; import {defined, objectIsEmpty, isUrl} from '../../../utils'; import StrictClick from '../../strictClick';