From 8845d9e8ed2d06863b866df88dc110039a48a95a Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Tue, 25 Feb 2025 18:05:57 +0100 Subject: [PATCH 01/21] API to open internal issue --- treeherder/webapp/api/internal_issue.py | 14 ++++++++++++ treeherder/webapp/api/serializers.py | 29 +++++++++++++++++++++++++ treeherder/webapp/api/urls.py | 4 ++++ 3 files changed, 47 insertions(+) create mode 100644 treeherder/webapp/api/internal_issue.py diff --git a/treeherder/webapp/api/internal_issue.py b/treeherder/webapp/api/internal_issue.py new file mode 100644 index 00000000000..33cb63d6701 --- /dev/null +++ b/treeherder/webapp/api/internal_issue.py @@ -0,0 +1,14 @@ +from rest_framework import generics + +from treeherder.webapp.api.serializers import InternalIssueSerializer + + +class CreateInternalIssue(generics.CreateAPIView): + """ + Create a Bugscache entry, not necessarilly linked to a real Bugzilla ticket. + In case it already exists, update its occurrences. + + Returns the number of occurrences of this bug in the last week. + """ + + serializer_class = InternalIssueSerializer diff --git a/treeherder/webapp/api/serializers.py b/treeherder/webapp/api/serializers.py index 30ef154e67b..5ebd4c72de9 100644 --- a/treeherder/webapp/api/serializers.py +++ b/treeherder/webapp/api/serializers.py @@ -2,6 +2,8 @@ from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.utils import timezone from rest_framework import serializers from treeherder.changelog.models import Changelog @@ -446,3 +448,30 @@ class InvestigatedTestsSerializers(serializers.ModelSerializer): class Meta: model = models.InvestigatedTests fields = ("id", "test", "job_name", "job_symbol") + + +class InternalIssueSerializer(serializers.Serializer): + job_id = serializers.PrimaryKeyRelatedField( + write_only=True, + source="job", + queryset=models.Job.objects.all(), + ) + + class Meta: + model = models.Bugscache + fields = ["job_id", "summary"] + + @transaction.atomic + def create(self, validated_data): + job = validated_data.pop("job") + + # Build or retrieve a bug already reported for a similar FailureLine + try: + bug, _ = models.Bugscache.get_or_create( + **validated_data, default={"modified": timezone.now()} + ) + except models.Bugscache.MultipleObjectsReturned: + # Take last modified in case a conflict happens + bug = models.Bugscache.filter(**validated_data).order_by("modified").first() + bug.jobmap.get_or_create(job=job, defaults={"user": self.context["request"].user}) + return bug diff --git a/treeherder/webapp/api/urls.py b/treeherder/webapp/api/urls.py index f9e596c61b1..1172be69435 100644 --- a/treeherder/webapp/api/urls.py +++ b/treeherder/webapp/api/urls.py @@ -13,6 +13,7 @@ groups, infra_compare, intermittents_view, + internal_issue, investigated_test, job_log_url, jobs, @@ -175,4 +176,7 @@ ), re_path(r"^csp-report/$", csp_report.csp_report_collector, name="csp-report"), re_path(r"^schema/", get_schema_view(title="Treeherder Rest API"), name="openapi-schema"), + re_path( + r"^internal_issue/", internal_issue.CreateInternalIssue.as_view(), name="internal_issue" + ), ] From 2ce08860af1e142f9859f16a7f2ddd27464ac7b4 Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Thu, 27 Feb 2025 17:12:37 +0100 Subject: [PATCH 02/21] Fix warnings about wrong prop types in PinBoard.jsx --- ui/job-view/details/PinBoard.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/job-view/details/PinBoard.jsx b/ui/job-view/details/PinBoard.jsx index 63a4105b972..741778a83ac 100644 --- a/ui/job-view/details/PinBoard.jsx +++ b/ui/job-view/details/PinBoard.jsx @@ -712,8 +712,8 @@ PinBoard.propTypes = { isStaff: PropTypes.bool.isRequired, isPinBoardVisible: PropTypes.bool.isRequired, pinnedJobs: PropTypes.shape({}).isRequired, - pinnedJobBugs: PropTypes.shape({}).isRequired, - newBug: PropTypes.string.isRequired, + pinnedJobBugs: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + newBug: PropTypes.shape({}).isRequired, addBug: PropTypes.func.isRequired, removeBug: PropTypes.func.isRequired, unPinJob: PropTypes.func.isRequired, From 0d186d84ede399b913bf4700a71998ee57c5e873 Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Thu, 27 Feb 2025 17:40:33 +0100 Subject: [PATCH 03/21] [UI] Move bug summary to generic helpers --- ui/helpers/bug.js | 101 +++++++++++++++++++++++++++++++++++++++++ ui/shared/BugFiler.jsx | 72 +---------------------------- 2 files changed, 102 insertions(+), 71 deletions(-) create mode 100644 ui/helpers/bug.js diff --git a/ui/helpers/bug.js b/ui/helpers/bug.js new file mode 100644 index 00000000000..65bcdc08d00 --- /dev/null +++ b/ui/helpers/bug.js @@ -0,0 +1,101 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { + Button, + Modal, + ModalHeader, + ModalBody, + ModalFooter, + Tooltip, + FormGroup, + Input, + Label, +} from 'reactstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + faChevronCircleDown, + faChevronCircleUp, + faSpinner, + faExclamationTriangle, +} from '@fortawesome/free-solid-svg-icons'; + +import { + bugzillaBugsApi, + bzBaseUrl, + bzComponentEndpoint, + getApiUrl, +} from '../helpers/url'; +import { create } from '../helpers/http'; +import { notify } from '../job-view/redux/stores/notifications'; + +export const omittedLeads = [ + 'TEST-UNEXPECTED-FAIL', + 'PROCESS-CRASH', + 'TEST-UNEXPECTED-ERROR', + 'REFTEST ERROR', +]; +/* + * Find the first thing in the summary line that looks like a filename. + */ +const findFilename = (summary) => { + // Take left side of any reftest comparisons, as the right side is the reference file + // eslint-disable-next-line prefer-destructuring + summary = summary.split('==')[0]; + // Take the leaf node of unix paths + summary = summary.split('/').pop(); + // Take the leaf node of Windows paths + summary = summary.split('\\').pop(); + // Remove leading/trailing whitespace + summary = summary.trim(); + // If there's a space in what's remaining, take the first word + // eslint-disable-next-line prefer-destructuring + summary = summary.split(' ')[0]; + return summary; +}; +/* + * Remove extraneous junk from the start of the summary line + * and try to find the failing test name from what's left + */ +export const parseSummary = (suggestion) => { + let summary = suggestion.search; + const searchTerms = suggestion.search_terms; + // Strip out some extra stuff at the start of some failure paths + let re = /file:\/\/\/.*?\/build\/tests\/reftest\/tests\//gi; + summary = summary.replace(re, ''); + re = /chrome:\/\/mochitests\/content\/a11y\//gi; + summary = summary.replace(re, ''); + re = /http:\/\/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):([0-9]+)\/tests\//gi; + summary = summary.replace(re, ''); + re = /xpcshell([-a-zA-Z0-9]+)?.(ini|toml):/gi; + summary = summary.replace(re, ''); + summary = summary.replace('/_mozilla/', 'mozilla/tests/'); + // We don't want to include "REFTEST" when it's an unexpected pass + summary = summary.replace( + 'REFTEST TEST-UNEXPECTED-PASS', + 'TEST-UNEXPECTED-PASS', + ); + const summaryParts = summary.split(' | '); + + // If the search_terms used for finding bug suggestions + // contains any of the omittedLeads, that lead is needed + // for the full string match, so don't omit it in this case. + // If it's not needed, go ahead and omit it. + if (searchTerms.length && summaryParts.length > 1) { + omittedLeads.forEach((lead) => { + if (!searchTerms[0].includes(lead) && summaryParts[0].includes(lead)) { + summaryParts.shift(); + } + }); + } + + // Some of the TEST-FOO bits aren't removed from the summary, + // so we sometimes end up with them instead of the test path here. + const summaryName = + summaryParts[0].startsWith('TEST-') && summaryParts.length > 1 + ? summaryParts[1] + : summaryParts[0]; + const possibleFilename = findFilename(summaryName); + + return [summaryParts, possibleFilename]; +}; diff --git a/ui/shared/BugFiler.jsx b/ui/shared/BugFiler.jsx index e124d17c10d..2b6cc89f03a 100644 --- a/ui/shared/BugFiler.jsx +++ b/ui/shared/BugFiler.jsx @@ -27,79 +27,9 @@ import { getApiUrl, } from '../helpers/url'; import { create } from '../helpers/http'; +import { omittedLeads, parseSummary } from '../helpers/bug'; import { notify } from '../job-view/redux/stores/notifications'; -const omittedLeads = [ - 'TEST-UNEXPECTED-FAIL', - 'PROCESS-CRASH', - 'TEST-UNEXPECTED-ERROR', - 'REFTEST ERROR', -]; -/* - * Find the first thing in the summary line that looks like a filename. - */ -const findFilename = (summary) => { - // Take left side of any reftest comparisons, as the right side is the reference file - // eslint-disable-next-line prefer-destructuring - summary = summary.split('==')[0]; - // Take the leaf node of unix paths - summary = summary.split('/').pop(); - // Take the leaf node of Windows paths - summary = summary.split('\\').pop(); - // Remove leading/trailing whitespace - summary = summary.trim(); - // If there's a space in what's remaining, take the first word - // eslint-disable-next-line prefer-destructuring - summary = summary.split(' ')[0]; - return summary; -}; -/* - * Remove extraneous junk from the start of the summary line - * and try to find the failing test name from what's left - */ -const parseSummary = (suggestion) => { - let summary = suggestion.search; - const searchTerms = suggestion.search_terms; - // Strip out some extra stuff at the start of some failure paths - let re = /file:\/\/\/.*?\/build\/tests\/reftest\/tests\//gi; - summary = summary.replace(re, ''); - re = /chrome:\/\/mochitests\/content\/a11y\//gi; - summary = summary.replace(re, ''); - re = /http:\/\/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):([0-9]+)\/tests\//gi; - summary = summary.replace(re, ''); - re = /xpcshell([-a-zA-Z0-9]+)?.(ini|toml):/gi; - summary = summary.replace(re, ''); - summary = summary.replace('/_mozilla/', 'mozilla/tests/'); - // We don't want to include "REFTEST" when it's an unexpected pass - summary = summary.replace( - 'REFTEST TEST-UNEXPECTED-PASS', - 'TEST-UNEXPECTED-PASS', - ); - const summaryParts = summary.split(' | '); - - // If the search_terms used for finding bug suggestions - // contains any of the omittedLeads, that lead is needed - // for the full string match, so don't omit it in this case. - // If it's not needed, go ahead and omit it. - if (searchTerms.length && summaryParts.length > 1) { - omittedLeads.forEach((lead) => { - if (!searchTerms[0].includes(lead) && summaryParts[0].includes(lead)) { - summaryParts.shift(); - } - }); - } - - // Some of the TEST-FOO bits aren't removed from the summary, - // so we sometimes end up with them instead of the test path here. - const summaryName = - summaryParts[0].startsWith('TEST-') && summaryParts.length > 1 - ? summaryParts[1] - : summaryParts[0]; - const possibleFilename = findFilename(summaryName); - - return [summaryParts, possibleFilename]; -}; - export class BugFilerClass extends React.Component { constructor(props) { super(props); From 0f4ca1371bc7c249f23ea04a5fafb1fe1d50c039 Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Thu, 27 Feb 2025 16:31:11 +0100 Subject: [PATCH 04/21] [UI] Display a form to file internal issue by default --- ui/shared/tabs/failureSummary/BugListItem.jsx | 24 ++++++++++++++++++- .../tabs/failureSummary/FailureSummaryTab.jsx | 19 +++++++++++++++ .../failureSummary/SuggestionsListItem.jsx | 14 +++++++---- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/ui/shared/tabs/failureSummary/BugListItem.jsx b/ui/shared/tabs/failureSummary/BugListItem.jsx index d7f88c96c51..678c2ad4c6c 100644 --- a/ui/shared/tabs/failureSummary/BugListItem.jsx +++ b/ui/shared/tabs/failureSummary/BugListItem.jsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Highlighter from 'react-highlight-words'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBug } from '@fortawesome/free-solid-svg-icons'; import { faThumbtack } from '@fortawesome/free-solid-svg-icons'; import { Button } from 'reactstrap'; @@ -9,7 +10,15 @@ import { getSearchWords } from '../../../helpers/display'; import { getBugUrl } from '../../../helpers/url'; function BugListItem(props) { - const { bug, suggestion, bugClassName, title, selectedJob, addBug } = props; + const { + bug, + suggestion, + bugClassName, + title, + selectedJob, + addBug, + toggleBugFiler, + } = props; const bugUrl = getBugUrl(bug.id); const duplicateBugUrl = bug.dupe_of ? getBugUrl(bug.dupe_of) : undefined; @@ -31,6 +40,18 @@ function BugListItem(props) { {!bug.id && ( {bug.summary} ({bug.occurrences} occurrences) + {bug.occurrences > -1 && ( + // TODO: Update the condition above to match backend configuration + + )} )} {bug.id && ( @@ -78,6 +99,7 @@ BugListItem.propTypes = { bugClassName: PropTypes.string, title: PropTypes.string, addBug: PropTypes.func, + toggleBugFiler: PropTypes.func.isRequired, }; BugListItem.defaultProps = { diff --git a/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx b/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx index 0a8c09df0d0..5643b011e32 100644 --- a/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx +++ b/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx @@ -8,6 +8,7 @@ import { thBugSuggestionLimit, thEvents } from '../../../helpers/constants'; import { getResultState, isReftest } from '../../../helpers/job'; import { getReftestUrl } from '../../../helpers/url'; import BugFiler from '../../BugFiler'; +import InternalIssueFiler from '../../InternalIssueFiler'; import BugSuggestionsModel from '../../../models/bugSuggestions'; import ErrorsList from './ErrorsList'; @@ -20,6 +21,7 @@ class FailureSummaryTab extends React.Component { this.state = { isBugFilerOpen: false, + isInternalIssueFilerOpen: false, suggestions: [], errors: [], bugSuggestionsLoading: false, @@ -58,6 +60,12 @@ class FailureSummaryTab extends React.Component { })); }; + toggleInternalIssueFiler = () => { + this.setState((prevState) => ({ + isInternalIssueFilerOpen: !prevState.isInternalIssueFilerOpen, + })); + }; + bugFilerCallback = (data) => { const { addBug } = this.props; @@ -154,6 +162,7 @@ class FailureSummaryTab extends React.Component { } = this.props; const { isBugFilerOpen, + isInternalIssueFilerOpen, suggestion, bugSuggestionsLoading, suggestions, @@ -205,6 +214,9 @@ class FailureSummaryTab extends React.Component { index={index} suggestion={suggestion} toggleBugFiler={() => this.fileBug(suggestion)} + toggleInternalIssueFiler={() => + this.setState({ isInternalIssueFilerOpen: true }) + } selectedJob={selectedJob} addBug={addBug} repoName={repoName} @@ -304,6 +316,13 @@ class FailureSummaryTab extends React.Component { platform={selectedJob.platform} /> )} + + {isInternalIssueFilerOpen && ( + + )} ); } diff --git a/ui/shared/tabs/failureSummary/SuggestionsListItem.jsx b/ui/shared/tabs/failureSummary/SuggestionsListItem.jsx index 07d2673c193..ba35bbc4185 100644 --- a/ui/shared/tabs/failureSummary/SuggestionsListItem.jsx +++ b/ui/shared/tabs/failureSummary/SuggestionsListItem.jsx @@ -3,7 +3,10 @@ import PropTypes from 'prop-types'; import { Button } from 'reactstrap'; import { Link } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faBug, faFilter } from '@fortawesome/free-solid-svg-icons'; +import { + faCircleExclamation, + faFilter, +} from '@fortawesome/free-solid-svg-icons'; import Clipboard from '../../Clipboard'; import logviewerIcon from '../../../img/logviewerIcon.png'; @@ -45,6 +48,7 @@ export default class SuggestionsListItem extends React.Component { const { suggestion, toggleBugFiler, + toggleInternalIssueFiler, selectedJob, addBug, repoName, @@ -68,6 +72,7 @@ export default class SuggestionsListItem extends React.Component { suggestion={suggestion} selectedJob={selectedJob} addBug={addBug} + toggleBugFiler={toggleBugFiler} bugClassName={ developerMode ? 'text-darker-secondary small-text' : '' } @@ -164,10 +169,10 @@ export default class SuggestionsListItem extends React.Component { className="bg-light py-1 px-2 mr-2" outline style={{ fontSize: '8px' }} - onClick={() => toggleBugFiler(suggestion)} - title="file a bug for this failure" + onClick={() => toggleInternalIssueFiler(suggestion)} + title="file an internal issue for this failure" > - + {suggestion.showNewButton && ( @@ -230,6 +235,7 @@ SuggestionsListItem.propTypes = { selectedJob: PropTypes.shape({}).isRequired, suggestion: PropTypes.shape({}).isRequired, toggleBugFiler: PropTypes.func.isRequired, + toggleInternalIssueFiler: PropTypes.func.isRequired, developerMode: PropTypes.bool.isRequired, addBug: PropTypes.func, }; From 7770a3dce9eb57ccb36aa4b60c27ba48c7986dac Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Thu, 6 Mar 2025 14:55:23 +0100 Subject: [PATCH 05/21] Suggestions --- ui/shared/tabs/failureSummary/BugListItem.jsx | 23 +++++++++++++------ .../failureSummary/SuggestionsListItem.jsx | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/ui/shared/tabs/failureSummary/BugListItem.jsx b/ui/shared/tabs/failureSummary/BugListItem.jsx index 678c2ad4c6c..6529cb3eec0 100644 --- a/ui/shared/tabs/failureSummary/BugListItem.jsx +++ b/ui/shared/tabs/failureSummary/BugListItem.jsx @@ -31,25 +31,34 @@ function BugListItem(props) { style={{ fontSize: '8px' }} type="button" onClick={() => addBug(bug, selectedJob)} - title="add to list of bugs to associate with all pinned jobs" + title="Add to list of bugs to associate with all pinned jobs" > )} i{bug.internal_id} {!bug.id && ( - - {bug.summary} ({bug.occurrences} occurrences) - {bug.occurrences > -1 && ( - // TODO: Update the condition above to match backend configuration + + + ({bug.occurrences} occurrences{' '} + ) + + {bug.summary} + {!bug.bugzilla_id && ( )} diff --git a/ui/shared/tabs/failureSummary/SuggestionsListItem.jsx b/ui/shared/tabs/failureSummary/SuggestionsListItem.jsx index ba35bbc4185..8937ed4bf00 100644 --- a/ui/shared/tabs/failureSummary/SuggestionsListItem.jsx +++ b/ui/shared/tabs/failureSummary/SuggestionsListItem.jsx @@ -170,9 +170,9 @@ export default class SuggestionsListItem extends React.Component { outline style={{ fontSize: '8px' }} onClick={() => toggleInternalIssueFiler(suggestion)} - title="file an internal issue for this failure" + title="File an internal issue for this failure" > - + {suggestion.showNewButton && ( From 29708e4fd6375eb287a594754d9ada2b0005a41d Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Thu, 6 Mar 2025 15:19:30 +0100 Subject: [PATCH 06/21] Move occurrences setting to the frontend --- treeherder/config/settings.py | 3 +-- ui/shared/tabs/failureSummary/BugListItem.jsx | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/treeherder/config/settings.py b/treeherder/config/settings.py index bdb86b79bf7..de39c2e966b 100644 --- a/treeherder/config/settings.py +++ b/treeherder/config/settings.py @@ -448,9 +448,8 @@ MAX_ERROR_LINES = 40 FAILURE_LINES_CUTOFF = 150 -# Required number of occurrences in a given time window before being prompted to file a bug in Bugzilla +# Count internal issue annotations in a limited time window (before prompting user to file a bug in Bugzilla) INTERNAL_OCCURRENCES_DAYS_WINDOW = 7 -INTERNAL_OCCURRENCES_COUNT = 3 # Perfherder # Default minimum regression threshold for perfherder is 2% (otherwise diff --git a/ui/shared/tabs/failureSummary/BugListItem.jsx b/ui/shared/tabs/failureSummary/BugListItem.jsx index 6529cb3eec0..cd62e90c306 100644 --- a/ui/shared/tabs/failureSummary/BugListItem.jsx +++ b/ui/shared/tabs/failureSummary/BugListItem.jsx @@ -21,6 +21,8 @@ function BugListItem(props) { } = props; const bugUrl = getBugUrl(bug.id); const duplicateBugUrl = bug.dupe_of ? getBugUrl(bug.dupe_of) : undefined; + // Number of internal issue classifications to open a bug in Bugzilla + const requiredInternalOcurrences = 3; return (
  • @@ -46,15 +48,14 @@ function BugListItem(props) { {bug.summary} {!bug.bugzilla_id && ( {' '} + + + + + ); + } +} + +InternalIssueFilerClass.propTypes = { + isOpen: PropTypes.bool.isRequired, + toggle: PropTypes.func.isRequired, + suggestion: PropTypes.shape({}).isRequired, + jobGroupName: PropTypes.string.isRequired, + notify: PropTypes.func.isRequired, +}; + +export default connect(null, { notify })(InternalIssueFilerClass); From 78add0365d23872392ced8f6c81a0f84d9da2dac Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Fri, 7 Mar 2025 14:47:05 +0100 Subject: [PATCH 08/21] [UI] Fix context for the Internal Issue component --- ui/shared/tabs/failureSummary/FailureSummaryTab.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx b/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx index 5643b011e32..055afe42fe5 100644 --- a/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx +++ b/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx @@ -215,7 +215,10 @@ class FailureSummaryTab extends React.Component { suggestion={suggestion} toggleBugFiler={() => this.fileBug(suggestion)} toggleInternalIssueFiler={() => - this.setState({ isInternalIssueFilerOpen: true }) + this.setState({ + isInternalIssueFilerOpen: true, + suggestion: suggestion, + }) } selectedJob={selectedJob} addBug={addBug} @@ -321,6 +324,8 @@ class FailureSummaryTab extends React.Component { )} From ec7642902527f746082947b1d821caa01ba79c91 Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Fri, 7 Mar 2025 16:40:25 +0100 Subject: [PATCH 09/21] [UI] Add the same summary transformation that in BugFiler --- ui/helpers/bug.js | 6 ++ ui/shared/BugFiler.jsx | 12 +--- ui/shared/InternalIssueFiler.jsx | 104 ++++++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/ui/helpers/bug.js b/ui/helpers/bug.js index 65bcdc08d00..eb2b5f10027 100644 --- a/ui/helpers/bug.js +++ b/ui/helpers/bug.js @@ -99,3 +99,9 @@ export const parseSummary = (suggestion) => { return [summaryParts, possibleFilename]; }; + +export const getCrashSignatures = (failureLine) => { + const crashRegex = /(\[@ .+\])/g; + const crashSignatures = failureLine.search.match(crashRegex); + return crashSignatures ? [crashSignatures[0]] : []; +}; diff --git a/ui/shared/BugFiler.jsx b/ui/shared/BugFiler.jsx index 2b6cc89f03a..026c811d4af 100644 --- a/ui/shared/BugFiler.jsx +++ b/ui/shared/BugFiler.jsx @@ -27,7 +27,7 @@ import { getApiUrl, } from '../helpers/url'; import { create } from '../helpers/http'; -import { omittedLeads, parseSummary } from '../helpers/bug'; +import { omittedLeads, parseSummary, getCrashSignatures } from '../helpers/bug'; import { notify } from '../job-view/redux/stores/notifications'; export class BugFilerClass extends React.Component { @@ -63,7 +63,7 @@ export class BugFilerClass extends React.Component { summaryString = summaryString.replace(re, ''); } - const crashSignatures = this.getCrashSignatures(suggestion); + const crashSignatures = getCrashSignatures(suggestion); const keywords = []; let isAssertion = [ @@ -213,12 +213,6 @@ export class BugFilerClass extends React.Component { this.findProductByPath(); } - getCrashSignatures(failureLine) { - const crashRegex = /(\[@ .+\])/g; - const crashSignatures = failureLine.search.match(crashRegex); - return crashSignatures ? [crashSignatures[0]] : []; - } - getUnhelpfulSummaryReason(summary) { const { suggestion } = this.props; const searchTerms = suggestion.search_terms; @@ -611,7 +605,7 @@ export class BugFilerClass extends React.Component { selectedProduct, } = this.state; const searchTerms = suggestion.search_terms; - const crashSignatures = this.getCrashSignatures(suggestion); + const crashSignatures = getCrashSignatures(suggestion); const unhelpfulSummaryReason = this.getUnhelpfulSummaryReason(summary); return ( diff --git a/ui/shared/InternalIssueFiler.jsx b/ui/shared/InternalIssueFiler.jsx index 99454bb307b..a821b1e23ab 100644 --- a/ui/shared/InternalIssueFiler.jsx +++ b/ui/shared/InternalIssueFiler.jsx @@ -11,7 +11,7 @@ import { Label, } from 'reactstrap'; -import { parseSummary } from '../helpers/bug'; +import { parseSummary, getCrashSignatures } from '../helpers/bug'; import { notify } from '../job-view/redux/stores/notifications'; export class InternalIssueFilerClass extends React.Component { @@ -27,6 +27,108 @@ export class InternalIssueFilerClass extends React.Component { summaryString = summaryString.replace(re, ''); } + const crashSignatures = getCrashSignatures(suggestion); + + if (crashSignatures.length > 0) { + isTestPath = false; + const parts = summaryString.split(' | '); + summaryString = `${parts[0]} | single tracking bug`; + keywords.push('intermittent-testcase'); + } + + let isAssertion = [ + /ASSERTION:/, // binary code + /assertion fail/i, // JavaScript + /assertion count \d+ is \w+ than expected \d+ assertion/, // layout + ].some((regexp) => regexp.test(summaryString)); + + const jg = jobGroupName.toLowerCase(); + if ( + jg.includes('xpcshell') || + jg.includes('mochitest') || + jg.includes('web platform tests') || + jg.includes('reftest') || + jg.includes('talos') || + jobTypeName.includes('junit') || + jobTypeName.includes('marionette') + ) { + // simple hack to make sure we have a testcase in the summary + let isTestPath = [ + /.*test_.*\.js/, // xpcshell + /.*test_.*\.html/, // mochitest + /.*test_.*\.xhtml/, // mochitest-chrome + /.*browser_.*\.html/, // b-c + /.*browser_.*\.js/, // b-c + /.*test_.*\.py/, // marionette + /.*\.ini/, // when we have a failure on shutdown (crash/leak/timeout) + /.*\.toml/, // when we have a failure on shutdown (crash/leak/timeout) + /.*org.mozilla.geckoview.test.*/, // junit + ].some((regexp) => regexp.test(summaryString)); + + if (jg.includes('talos')) { + isTestPath = [ + /.*PROCESS-CRASH \| .*/, // crashes + ].some((regexp) => regexp.test(suggestion.search)); + } else if (jg.includes('web platform tests') || jg.includes('reftest')) { + // account for .html?blah... | failure message + isTestPath = [ + /.*\.js(\?.*| )\|/, + /.*\.html(\?.*| )\|/, + /.*\.htm(\?.*| )\|/, + /.*\.xhtml(\?.*| )\|/, + /.*\.xht(\?.*| )\|/, + /.*\.mp4 \|/, // reftest specific + /.*\.webm \|/, // reftest specific + / \| .*\.js(\?.*)?/, // crash format + / \| .*\.html(\?.*)?/, + / \| .*\.htm(\?.*)?/, + / \| .*\.xhtml(\?.*)?/, + / \| .*\.xht(\?.*)?/, + / \| .*.mp4/, // reftest specific + / \| .*\.webm/, // reftest specific + ].some((regexp) => regexp.test(summaryString)); + } + + if (crashSignatures.length > 0) { + isTestPath = false; + const parts = summaryString.split(' | '); + summaryString = `${parts[0]} | single tracking bug`; + keywords.push('intermittent-testcase'); + } + + // trimming params from end of a test case name when filing for stb + let trimParams = false; + + // only handle straight forward reftest pixel/color errors + if ( + isTestPath && + jobGroupName.includes('reftest') && + !/.*image comparison, max difference.*/.test(summaryString) + ) { + isTestPath = false; + } else if ( + jg.includes('web platform tests') || + jobTypeName.includes('marionette') + ) { + trimParams = true; + } + + // If not leak + if (!isAssertion && isTestPath) { + const parts = summaryString.split(' | '); + // split('?') is for removing `?params...` from the test name + if (parts.length === 2 || parts.length === 1) { + summaryString = `${ + trimParams ? parts[0].split('?')[0].split(' ')[0] : parts[0] + } | single tracking bug`; + } else if (parts.length === 3) { + summaryString = `${ + trimParams ? parts[1].split('?')[0].split(' ')[0] : parts[1] + } | single tracking bug`; + } + } + } + this.state = { summary: `Intermittent ${summaryString}`, }; From 2a49f01ec64ac817e47421479aa6217bf5d2199d Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Fri, 7 Mar 2025 17:33:40 +0100 Subject: [PATCH 10/21] [UI] Create initial internal issue --- ui/shared/InternalIssueFiler.jsx | 28 +++++++++++++++---- .../tabs/failureSummary/FailureSummaryTab.jsx | 2 ++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/ui/shared/InternalIssueFiler.jsx b/ui/shared/InternalIssueFiler.jsx index a821b1e23ab..3240fcee3ea 100644 --- a/ui/shared/InternalIssueFiler.jsx +++ b/ui/shared/InternalIssueFiler.jsx @@ -1,6 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { create } from '../helpers/http'; +import { getApiUrl } from '../helpers/url'; import { Button, Modal, @@ -18,7 +20,7 @@ export class InternalIssueFilerClass extends React.Component { constructor(props) { super(props); - const { suggestion, jobGroupName } = props; + const { suggestion, jobGroupName, jobTypeName } = props; const parsedSummary = parseSummary(suggestion); let summaryString = parsedSummary[0].join(' | '); @@ -136,9 +138,18 @@ export class InternalIssueFilerClass extends React.Component { submitInternalIssue = async () => { const { summary } = this.state; - const { notify } = this.props; - - notify(summary, 'danger'); + const { notify, jobId } = this.props; + + const resp = await create(getApiUrl('/internal_issue/'), { + summary, + job_id: jobId, + }); + if ('failureStatus' in resp) { + notify(resp?.data || resp.failureStatus, 'danger'); + } else { + notify('Error line reported as an internal issue', 'success'); + // TODO: Reload failures summary + } }; render() { @@ -161,13 +172,16 @@ export class InternalIssueFilerClass extends React.Component { type="text" placeholder="Intermittent..." pattern=".{0,255}" - defaultValue={summary} + value={summary} + onChange={(evt) => + this.setState({ summary: evt.target.value }) + } /> - {' '} + ); + const bugzillaButton = ( + + ); return (
  • - {!!addBug && ( - - )} + {!!addBug && + bug.occurrences < requiredInternalOccurrences && + internalOccurrenceButton} + {!bug.bugzilla_id && + bug.occurrences >= requiredInternalOccurrences && + bugzillaButton} + {bug.bugzilla_id} i{bug.internal_id} {!bug.id && ( - + ({bug.occurrences} occurrences{' '} ) - {bug.summary} - {!bug.bugzilla_id && ( - - )} + {bug.summary} + {!!addBug && + bug.occurrences >= requiredInternalOccurrences && + internalOccurrenceButton} + {!bug.bugzilla_id && + bug.occurrences < requiredInternalOccurrences && + bugzillaButton} )} {bug.id && ( From f864369fb3caeaa736a2b974e0cf3a7c4c82fdb7 Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Fri, 18 Apr 2025 13:51:12 +0200 Subject: [PATCH 20/21] Increment occurrence on succesful internal issue creation --- ui/shared/tabs/failureSummary/FailureSummaryTab.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx b/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx index b8420a3392b..38c6f2c9d33 100644 --- a/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx +++ b/ui/shared/tabs/failureSummary/FailureSummaryTab.jsx @@ -109,6 +109,7 @@ class FailureSummaryTab extends React.Component { existingBug && existingBug.occurrences >= requiredInternalOccurrences - 1 ) { + existingBug.occurrences += 1; this.fileBug(suggestion); } }; From 09602612575441183a88a4e33c7aba773b00e327 Mon Sep 17 00:00:00 2001 From: Valentin Rigal Date: Fri, 18 Apr 2025 14:26:38 +0200 Subject: [PATCH 21/21] Display summary length help + validation in internal issue filer --- ui/shared/InternalIssueFiler.jsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui/shared/InternalIssueFiler.jsx b/ui/shared/InternalIssueFiler.jsx index b02f254bd24..bd330c0dc3a 100644 --- a/ui/shared/InternalIssueFiler.jsx +++ b/ui/shared/InternalIssueFiler.jsx @@ -137,6 +137,14 @@ export class InternalIssueFilerClass extends React.Component { const { summary } = this.state; const { notify, successCallback, toggle } = this.props; + if (summary.length > 255) { + notify( + 'Please ensure the summary is no more than 255 characters', + 'danger', + ); + return; + } + const resp = await create(getApiUrl('/internal_issue/'), { summary }); if (resp?.failureStatus && resp.failureStatus >= 400) { const msg = @@ -174,6 +182,14 @@ export class InternalIssueFilerClass extends React.Component { this.setState({ summary: evt.target.value }) } /> + 255 ? 'text-danger' : 'text-success' + }`} + > + {summary.length} +