Merge lp:~henninge/launchpad/bug-824435-failure-reporting-4 into lp:launchpad

Proposed by Henning Eggers
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 13837
Proposed branch: lp:~henninge/launchpad/bug-824435-failure-reporting-4
Merge into: lp:launchpad
Diff against target: 424 lines (+154/-149)
3 files modified
lib/lp/code/browser/sourcepackagerecipe.py (+1/-1)
lib/lp/code/javascript/requestbuild_overlay.js (+130/-132)
lib/lp/code/javascript/tests/test_requestbuild_overlay.js (+23/-16)
To merge this branch: bzr merge lp:~henninge/launchpad/bug-824435-failure-reporting-4
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Review via email: mp+73507@code.launchpad.net

Commit message

[r=danilo][bug=824435] Include OOPS-ID in error output of requestbuild overlay.

Description of the change

= Summary =

This branch finally fixes bug 824435 by making sure that the OOPS ID
returned by server failures is displayed in the UI. This makes it
possible for users to report useful information to the LP team.

== Proposed fix ==

Use lp.cient.ErrorHandler as the base for RequestResponseHandler which
provides a getFailureHandler method which in turn produces an io handler
that is able to extract OOPS information from the server response.

"Partial" errors are not "Bad requests" because they were may have been
able to request some builds while others had already been requested.
This is still normal processing and therefore a successful response. So
it should return status 200. The success handler will have the content
type information to figure out which type of response it got.

Factor out the error display into its own functio because it will be
used from the failure handler as well as the success handler.

Improve markup creation by using YUI methods.

== Pre-implementation notes ==

On-going effort.

== Implementation details ==

I refactored the code a bit by creating the get_new_builds_message and
get_info_header functions, to make the code in the success handler more
concise.

== Tests ==

I updated the tests to check for an OOPS id beint displayed with the
error messages for both request daily build and request builds.

firefox lib/lp/code/javascript/tests/test_requestbuild_overlay.html

== Demo and Q/A ==

Verify that the succesful and partially succesful handlers still work.

For QA, create a new recipe on any old branch, using the stand settings.
Open the recipe page in two tabs and click "Request build(s)" in both.
Select one distroseries in the first and two in the second tab, where
the two include the one in the first. Got that? ;-)
Submit the first, it will close successfully.
Submit the second, you will get an informational message that "An
identical build is already pending for ..."

Verify that server errors are displayed.

The only idea I have for that is to have a LOSA cowboy a patch onto
qastaging that places and odd "assert fail" in
SourcePackageRecipeRequestBuildsAjaxView.request_action.
Request builds using the "Bulid now" button and the "Request build(s)"
overlay and you should get an error message that includes an OOPS-ID.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/javascript/requestbuild_overlay.js
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/javascript/tests/test_requestbuild_overlay.js

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
2--- lib/lp/code/browser/sourcepackagerecipe.py 2011-07-15 03:13:14 +0000
3+++ lib/lp/code/browser/sourcepackagerecipe.py 2011-08-31 11:10:17 +0000
4@@ -479,7 +479,7 @@
5 def _process_error(self, data=None, builds=None, informational=None,
6 errors=None, reason="Validation"):
7 """Set up the response and json data to return to the caller."""
8- self.request.response.setStatus(400, reason)
9+ self.request.response.setStatus(200, reason)
10 self.request.response.setHeader('Content-type', 'application/json')
11 return_data = dict(builds=builds, errors=errors)
12 if informational:
13
14=== modified file 'lib/lp/code/javascript/requestbuild_overlay.js'
15--- lib/lp/code/javascript/requestbuild_overlay.js 2011-08-25 13:19:31 +0000
16+++ lib/lp/code/javascript/requestbuild_overlay.js 2011-08-31 11:10:17 +0000
17@@ -34,35 +34,16 @@
18 // developer to specify onSuccess and onError hooks. It is quite generic and
19 // perhaps could be moved to an infrastructure class.
20
21-RequestResponseHandler = function () {};
22-RequestResponseHandler.prototype = {
23- clearProgressUI: function () {},
24- showError: function (header, error_msg) {},
25- getErrorHandler: function (errorCallback) {
26- var self = this;
27- return function (id, response) {
28- self.clearProgressUI();
29- // If it was a timeout...
30- if (response.status === 503) {
31- self.showError(
32- 'Timeout error, please try again in a few minutes.',
33- null);
34- } else {
35- if (errorCallback !== null) {
36- errorCallback(self, id, response);
37- } else {
38- self.showError(response.responseText, null);
39- }
40- }
41- };
42- },
43- getSuccessHandler: function (successCallback) {
44- var self = this;
45- return function (id, response) {
46- self.clearProgressUI();
47- successCallback(self, id, response);
48- };
49- }
50+var RequestResponseHandler = function () {};
51+
52+RequestResponseHandler.prototype = new Y.lp.client.ErrorHandler();
53+
54+RequestResponseHandler.prototype.getSuccessHandler = function(callback) {
55+ var self = this;
56+ return function (id, response) {
57+ self.clearProgressUI();
58+ callback(self, id, response);
59+ };
60 };
61
62 namespace.RequestResponseHandler = RequestResponseHandler;
63@@ -115,28 +96,8 @@
64 request_build_response_handler.clearProgressUI = function() {
65 destroy_temporary_spinner();
66 };
67- request_build_response_handler.showError = function(header, error_msgs) {
68- if (header === null) {
69- if (error_msgs === null) {
70- header = "An error occurred, please contact an " +
71- "administrator.";
72- } else {
73- header = "The following errors occurred:";
74- }
75- }
76- var error_html = header;
77- if (error_msgs !== null) {
78- error_html += "<ul>";
79- if (typeof(error_msgs) === "string"){
80- error_msgs = [error_msgs];
81- }
82- Y.each(error_msgs, function(error_msg){
83- error_html += "<li>" + error_msg.replace(/<([^>]+)>/g,'') +
84- "</li>";
85- });
86- error_html += "</ul><p/>";
87- }
88- request_build_overlay.error_node.set('innerHTML', error_html);
89+ request_build_response_handler.showError = function(errormessage) {
90+ display_errors(null, errormessage);
91 };
92 };
93
94@@ -191,12 +152,8 @@
95 method: "POST",
96 headers: {'Accept': 'application/xhtml'},
97 on: {
98- failure: request_daily_build_response_handler.getErrorHandler(
99- function(handler, id, response) {
100- var server_error = 'Server error, ' +
101- 'please contact an administrator.';
102- handler.showError(server_error, null);
103- }),
104+ failure:
105+ request_daily_build_response_handler.getFailureHandler(),
106 success:
107 request_daily_build_response_handler.getSuccessHandler(
108 function(handler, id, response) {
109@@ -232,13 +189,9 @@
110 request_daily_build_response_handler.clearProgressUI = function() {
111 destroy_temporary_spinner();
112 };
113- request_daily_build_response_handler.showError = function(header, error) {
114- var error_msg = header;
115- if (error !== null) {
116- error_msg += " " + error;
117- }
118- display_message(error_msg, 'build-error');
119- Y.log(error_msg);
120+ request_daily_build_response_handler.showError = function(message) {
121+ display_message(message, 'build-error');
122+ Y.log(message);
123 };
124 };
125
126@@ -294,6 +247,35 @@
127 return true;
128 };
129
130+var get_new_builds_message = function(build_html, current_builds) {
131+ var nr_new;
132+ if (build_html === null) {
133+ return null;
134+ }
135+ nr_new = display_build_records(build_html, current_builds);
136+ if (nr_new > 1) {
137+ return Y.Lang.substitute(MANY_BUILDS_MESSAGE, {nr_new: nr_new});
138+ }
139+ return ONE_BUILD_MESSAGE;
140+};
141+
142+var get_info_header = function(new_builds_message, pending_build_info) {
143+ var info_header = Y.Node.create('<div></div>')
144+ .addClass('popup-build-informational');
145+ if (new_builds_message !== null) {
146+ info_header.append(Y.Node.create('<p></p>')
147+ .set('text', new_builds_message));
148+ }
149+ if (pending_build_info !== null) {
150+ info_header.append(Y.Node.create('<p></p>')
151+ .set('text', pending_build_info));
152+ }
153+ if (info_header.hasChildNodes()) {
154+ return info_header;
155+ }
156+ return null;
157+};
158+
159 /*
160 * The form submit function
161 */
162@@ -312,79 +294,70 @@
163 method: "POST",
164 headers: {'Accept': 'application/json; application/xhtml'},
165 on: {
166- failure: request_build_response_handler.getErrorHandler(
167- function(handler, id, response) {
168- var errors = [],
169- error_header = null,
170- error_header_text = "",
171- build_info, build_html, error_info,
172- nr_new, new_builds_message, field_name;
173- if( response.status >= 500 ) {
174- // There's some error content we need to display.
175- errors = [
176- response.status + ' ' + response.statusText];
177- } else {
178- // The response will be a json data object containing
179- // info about what builds there are, informational
180- // text about any builds already pending, plus any
181- // errors. The FormOverlay infrastructure only
182- // supports displaying errors so we will construct
183- // our own HTML where the informational text will be
184- // appropriately displayed.
185- build_info = Y.JSON.parse(response.responseText);
186-
187- // The result of rendering the +builds view
188- build_html = build_info.builds;
189- // Any builds already pending (informational only)
190- pending_build_info = build_info.already_pending;
191- // Other more critical errors
192- error_info = build_info.errors;
193-
194- if (build_html !== null) {
195- nr_new = display_build_records(
196- build_html, current_builds);
197- new_builds_message = ONE_BUILD_MESSAGE;
198- if (nr_new > 1) {
199- new_builds_message =
200- Y.Lang.substitute(
201- MANY_BUILDS_MESSAGE,
202- {nr_new: nr_new});
203- }
204- error_header_text = new_builds_message;
205- }
206- if (pending_build_info !== null) {
207- if (error_header_text !== "" ) {
208- error_header_text += "<p/>" +
209- pending_build_info;
210- } else {
211- error_header_text = pending_build_info;
212- }
213- }
214- for (field_name in error_info) {
215- if (error_info.hasOwnProperty(field_name)) {
216- errors.push(error_info[field_name]);
217- }
218- }
219- if (error_header_text !== "") {
220- error_header =
221- "<p><div class='popup-build-informational'>"
222- + error_header_text + "</p></div>";
223- if (Y.Object.size(errors)>0) {
224- error_header +=
225- "<p/>" + "There were also some errors:";
226- }
227- } else {
228- error_header = "There were some errors:";
229- }
230- }
231- handler.showError(error_header, errors);
232- }),
233+ failure: request_build_response_handler.getFailureHandler(),
234 success: request_build_response_handler.getSuccessHandler(
235 function(handler, id, response) {
236+ var errors = [],
237+ error_header = null,
238+ error_header_text = "",
239+ build_info, build_html, error_info,
240+ nr_new, info_header, field_name;
241+ // The content type is used to tell a fully successful
242+ // request from a partially successful one. Successful
243+ // responses simply return the HTML snippet for the builds
244+ // table. If this ever causes problems, the view should
245+ // be changed to always return JSON and to provide an
246+ // attribute that identifies the type of response.
247+ content_type = response.getResponseHeader('Content-type');
248+ if( content_type !== 'application/json' ) {
249+ // We got the HTML for the builds back, we're done.
250 request_build_overlay.hide();
251 display_build_records(
252 response.responseText, current_builds);
253- })
254+ } else {
255+ // The response will be a json data object containing
256+ // info about what builds there are, informational
257+ // text about any builds already pending, plus any
258+ // errors. The FormOverlay infrastructure only
259+ // supports displaying errors so we will construct
260+ // our own HTML where the informational text will be
261+ // appropriately displayed.
262+ build_info = Y.JSON.parse(response.responseText);
263+
264+ // The result of rendering the +builds view
265+ build_html = build_info.builds;
266+ // Any builds already pending (informational only)
267+ pending_build_info = build_info.already_pending;
268+ // Other more critical errors
269+ error_info = build_info.errors;
270+
271+ info_header = get_info_header(
272+ get_new_builds_message(
273+ build_html, current_builds),
274+ pending_build_info);
275+
276+ for (field_name in error_info) {
277+ if (error_info.hasOwnProperty(field_name)) {
278+ errors.push(error_info[field_name]);
279+ }
280+ }
281+ error_container = Y.Node.create('<div></div>');
282+ if (info_header !== null) {
283+ error_container.append(info_header);
284+ if (errors.length > 0) {
285+ error_container.append(
286+ Y.Node.create('<p></p>')
287+ .set(
288+ 'text',
289+ "There were also some errors:"));
290+ }
291+ } else {
292+ error_container.set(
293+ 'text', "There were some errors:");
294+ }
295+ display_errors(error_container, errors);
296+ }
297+ })
298 },
299 form: {
300 id: request_build_overlay.form_node,
301@@ -432,6 +405,31 @@
302 "label[for^='field.distroseries.']");
303 };
304
305+var display_errors = function(container, error_msgs) {
306+ var errors_list, header;
307+ if (container === null) {
308+ if (error_msgs === null) {
309+ header = "An error occurred, please contact an administrator.";
310+ } else {
311+ header = "The following errors occurred:";
312+ }
313+ container = Y.Node.create('<div></div>')
314+ .set('text', header);
315+ }
316+ if (error_msgs !== null) {
317+ errors_list = Y.Node.create('<ul></ul>');
318+ if (Y.Lang.isString(error_msgs)){
319+ error_msgs = [error_msgs];
320+ }
321+ Y.each(error_msgs, function(error_msg){
322+ errors_list.append(Y.Node.create('<li></li>')
323+ .set('text', error_msg));
324+ });
325+ container.append(errors_list);
326+ }
327+ request_build_overlay.error_node.setContent(container);
328+};
329+
330 /*
331 * Callback used to enable/disable distro series selection when a different
332 * ppa is selected.
333
334=== modified file 'lib/lp/code/javascript/tests/test_requestbuild_overlay.js'
335--- lib/lp/code/javascript/tests/test_requestbuild_overlay.js 2011-08-26 07:41:59 +0000
336+++ lib/lp/code/javascript/tests/test_requestbuild_overlay.js 2011-08-31 11:10:17 +0000
337@@ -66,15 +66,20 @@
338 },
339
340
341- _testRequestbuildFailure: function(status, expected_message) {
342- var mockio = this._makeRequest();
343- mockio.respond({status: status});
344+ _testRequestbuildFailure: function(status, expected_message, oops) {
345+ var mockio = this._makeRequest(),
346+ response = {status: status},
347+ error;
348+ if (oops !== undefined) {
349+ response.responseHeaders = {'X-Lazr-OopsId': oops};
350+ }
351+ mockio.respond(response);
352
353 // No build targets.
354 Y.Assert.areSame(
355 0, Y.one("#builds-target").all('.package-build').size());
356 // The message is being displayed as an error.
357- var error = Y.one("#new-builds-info");
358+ error = Y.one("#new-builds-info");
359 Y.Assert.isTrue(error.hasClass('build-error'));
360 Y.Assert.areSame(expected_message + "Dismiss", error.get('text'));
361 // The message can be dismissed.
362@@ -90,10 +95,11 @@
363 503, "Timeout error, please try again in a few minutes.");
364 },
365
366-
367 test_requestdailybuild_failure_500: function() {
368- this._testRequestbuildFailure(
369- 500, "Server error, please contact an administrator.");
370+ var oops_id = "OOPS-TESTING",
371+ message = "Server error, please contact an administrator. " +
372+ "OOPS ID:" + oops_id;
373+ this._testRequestbuildFailure(500, message, oops_id);
374 }
375
376 }));
377@@ -171,10 +177,12 @@
378
379 test_requestbuilds_failure_500: function() {
380 var mockio = this._makeRequest(),
381- status = 500,
382- status_text = "Internal Server Error",
383- expected_error = status + ' ' + status_text;
384- mockio.failure({status: status, statusText: status_text});
385+ oops_id = "OOPS-TESTING",
386+ message = "Server error, please contact an administrator. " +
387+ "OOPS ID:" + oops_id;
388+ mockio.failure({
389+ 'responseHeaders': {'X-Lazr-OopsId': oops_id}
390+ });
391
392 // The form stays visible and no builds are displayed.
393 Y.Assert.isFalse(
394@@ -184,14 +192,14 @@
395 0, Y.one("#builds-target").all('.package-build').size());
396 // The error message is displayed.
397 Y.Assert.areSame(
398- expected_error,
399+ message,
400 Y.one(".yui3-lazr-formoverlay-errors li").get('text'));
401 // The submit button is disabled.
402 Y.Assert.isTrue(
403 Y.one("[name='field.actions.request']").get('disabled'));
404 },
405
406- test_requestbuilds_failure_400: function() {
407+ test_requestbuilds_build_collision: function() {
408 var mockio = this._makeRequest(),
409 success_message = "2 new recipe builds have been queued.",
410 informational_message = "An identical build ...",
411@@ -201,11 +209,10 @@
412 already_pending: informational_message,
413 errors: [error_message]
414 };
415- mockio.failure({
416- status: 400,
417+ mockio.success({
418 statusText: "Request Build",
419 responseText: Y.JSON.stringify(response_text),
420- responseHeaders: {'Content-type:': 'application/json'}
421+ responseHeaders: {'Content-type': 'application/json'}
422 });
423
424 // The form stays visible and the builds are displayed.