Skip to content

Commit

Permalink
Show client side exception messages when not in production (#1030)
Browse files Browse the repository at this point in the history
When not in production mode, this causes any exception which happens
in the browser to be shown to the developer so it is clear that
something is wrong.

Client side exceptions are always logged to the browser console,
regardless of the production mode status.
  • Loading branch information
Artur- authored Jun 21, 2016
1 parent 8488f53 commit f92253a
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.vaadin.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.vaadin.client.communication.PollConfigurator;
import com.vaadin.client.communication.Poller;
Expand Down Expand Up @@ -47,6 +48,9 @@ public ApplicationConnection(
ApplicationConfiguration applicationConfiguration) {

registry = new DefaultRegistry(this, applicationConfiguration);
GWT.setUncaughtExceptionHandler(
registry.getSystemErrorHandler()::handleError);

StateNode rootNode = registry.getStateTree().getRootNode();

// Bind UI configuration objects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,8 @@ private static boolean addListener(String url,
}

private void fireError(ResourceLoadEvent event) {
Console.error("Error loading " + event.getResourceUrl());
showLoadingError(event);
registry.getSystemErrorHandler()
.handleError("Error loading " + event.getResourceUrl());
String resource = event.getResourceUrl();

JsArray<ResourceLoadListener> listeners = loadListeners.get(resource);
Expand All @@ -478,22 +478,6 @@ private void fireError(ResourceLoadEvent event) {
}
}

protected void showLoadingError(ResourceLoadEvent event) {
if (registry.getApplicationConfiguration().isProductionMode()) {
// Only show error message when not in production
return;
}
Document document = Browser.getDocument();
Element errorContainer = document.createDivElement();
errorContainer.setClassName("v-system-error");
errorContainer
.setTextContent("Error loading " + event.getResourceUrl());
errorContainer.addEventListener("click", e -> {
errorContainer.getParentElement().removeChild(errorContainer);
});
document.getBody().appendChild(errorContainer);
}

private void fireLoad(ResourceLoadEvent event) {
Console.log("Loaded " + event.getResourceUrl());
String resource = event.getResourceUrl();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package com.vaadin.client;

import java.util.Set;

import com.google.web.bindery.event.shared.UmbrellaException;
import com.vaadin.client.bootstrap.ErrorMessage;

import elemental.client.Browser;
Expand Down Expand Up @@ -46,9 +49,8 @@ public SystemErrorHandler(Registry registry) {
* @param details
* message details or null if there are no details
*/
public void showCommunicationError(String details) {
Console.error("Communication error: " + details);
showError(details,
public void handleCommunicationError(String details) {
handleUnrecoverableError(details,
registry.getApplicationConfiguration().getCommunicationError());
}

Expand All @@ -58,9 +60,8 @@ public void showCommunicationError(String details) {
* @param details
* message details or null if there are no details
*/
public void showAuthenticationError(String details) {
Console.error("Authentication error: " + details);
showError(details,
public void handleAuthenticationError(String details) {
handleUnrecoverableError(details,
registry.getApplicationConfiguration().getAuthorizationError());
}

Expand All @@ -70,27 +71,28 @@ public void showAuthenticationError(String details) {
* @param details
* message details or null if there are no details
*/
public void showSessionExpiredError(String details) {
Console.error("Session expired: " + details);
showError(details, registry.getApplicationConfiguration()
public void handleSessionExpiredError(String details) {
handleUnrecoverableError(details, registry.getApplicationConfiguration()
.getSessionExpiredError());
}

/**
* Shows an error notification.
* Shows an error notification for an error which is unrecoverable.
*
* @param details
* message details or null if there are no details
* @param message
* an ErrorMessage describing the error
*/
protected void showError(String details, ErrorMessage message) {
showError(message.getCaption(), message.getMessage(), details,
message.getUrl());
protected void handleUnrecoverableError(String details,
ErrorMessage message) {
handleUnrecoverableError(message.getCaption(), message.getMessage(),
details, message.getUrl());
}

/**
* Shows a error notification using the given parameters.
* Shows an error notification for an error which is unrecoverable, using
* the given parameters.
*
* @param caption
* the caption of the message
Expand All @@ -100,10 +102,10 @@ protected void showError(String details, ErrorMessage message) {
* message details or null if there are no details
* @param url
* a URL to redirect to when the user clicks the message or null
* if no redirection should take place
* to refresh on click
*/
public void showError(String caption, String message, String details,
String url) {
public void handleUnrecoverableError(String caption, String message,
String details, String url) {
Document document = Browser.getDocument();
Element systemErrorContainer = document.createDivElement();
systemErrorContainer.setClassName("v-system-error");
Expand All @@ -113,18 +115,21 @@ public void showError(String caption, String message, String details,
captionDiv.setClassName("caption");
captionDiv.setInnerHTML(caption);
systemErrorContainer.appendChild(captionDiv);
Console.error(caption);
}
if (message != null) {
Element messageDiv = document.createDivElement();
messageDiv.setClassName("message");
messageDiv.setInnerHTML(message);
systemErrorContainer.appendChild(messageDiv);
Console.error(message);
}
if (details != null) {
Element detailsDiv = document.createDivElement();
detailsDiv.setClassName("details");
detailsDiv.setInnerHTML(details);
systemErrorContainer.appendChild(detailsDiv);
Console.error(details);
}

systemErrorContainer.addEventListener("click",
Expand All @@ -133,4 +138,49 @@ public void showError(String caption, String message, String details,
document.getBody().appendChild(systemErrorContainer);
}

/**
* Shows the given error message if not running in production mode and logs
* it to the console if running in production mode.
*
* @param errorMessage
* the error message to show
*/
public void handleError(String errorMessage) {
Console.error(errorMessage);
if (registry.getApplicationConfiguration().isProductionMode()) {
return;
}

Document document = Browser.getDocument();
Element errorContainer = document.createDivElement();
errorContainer.setClassName("v-system-error");
errorContainer.setTextContent(errorMessage);
errorContainer.addEventListener("click", e -> {
// Allow user to dismiss the error by clicking it.
errorContainer.getParentElement().removeChild(errorContainer);
});
document.getBody().appendChild(errorContainer);
}

/**
* Shows an error message if not running in production mode and logs it to
* the console if running in production mode.
*
* @param throwable
* the throwable which occurred
*/
public void handleError(Throwable throwable) {
handleError(unwrapUmbrellaException(throwable).getMessage());
}

private static Throwable unwrapUmbrellaException(Throwable e) {
if (e instanceof UmbrellaException) {
Set<Throwable> causes = ((UmbrellaException) e).getCauses();
if (causes.size() == 1) {
return unwrapUmbrellaException(causes.iterator().next());
}
}
return e;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public void heartbeatInvalidStatusCode(XMLHttpRequest xhr) {

if (statusCode == Response.SC_GONE) {
// Session expired
registry.getSystemErrorHandler().showSessionExpiredError(null);
registry.getSystemErrorHandler().handleSessionExpiredError(null);
stopApplication();
} else if (statusCode == Response.SC_NOT_FOUND) {
// UI closed, do nothing as the UI will react to this
Expand Down Expand Up @@ -460,7 +460,7 @@ protected void handleUnauthorized(XhrConnectionError xhrConnectionError) {
* Authorization has failed (401). Could be that the session has timed
* out.
*/
registry.getSystemErrorHandler().showAuthenticationError("");
registry.getSystemErrorHandler().handleAuthenticationError("");
stopApplication();
}

Expand Down Expand Up @@ -492,7 +492,8 @@ private void handleUnrecoverableCommunicationError(String details,
* @param statusCode
*/
protected void handleCommunicationError(String details, int statusCode) {
registry.getSystemErrorHandler().showError("", details, "", null);
registry.getSystemErrorHandler().handleUnrecoverableError("", details,
"", null);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -375,13 +375,13 @@ assert getServerId(valueMap) == -1
nextResponseSessionExpiredHandler.execute();
} else {
registry.getSystemErrorHandler()
.showSessionExpiredError(null);
.handleSessionExpiredError(null);
registry.getUILifecycle().setState(UIState.TERMINATED);
}
} else if (meta.containsKey("appError")) {
ValueMap error = meta.getValueMap("appError");

registry.getSystemErrorHandler().showError(
registry.getSystemErrorHandler().handleUnrecoverableError(
error.getString("caption"),
error.getString("message"),
error.getString("details"), error.getString("url"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ private static AnchorElement getRouterLink(Event clickEvent) {

Element target = (Element) clickEvent.getTarget();
EventTarget eventListenerElement = clickEvent.getCurrentTarget();
while (target != eventListenerElement) {
// Target can become null if another click handler detaches the element
while (target != null && target != eventListenerElement) {
if (isRouterLinkAnchorElement(target)) {
return (AnchorElement) target;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ private static void setupDocumentHead(BootstrapContext context,
// Basic system error dialog style just to make it visible and outside
// of normal flow
styles.appendText(".v-system-error {" //
+ "color: red;" //
+ "background: white;" //
+ "position: absolute;" //
+ "top: 1em;" //
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.hummingbird.uitest.ui;

import com.vaadin.hummingbird.html.Button;
import com.vaadin.hummingbird.html.Div;
import com.vaadin.hummingbird.router.View;

public class ClientSideExceptionHandlingView extends Div implements View {

static final String CAUSE_EXCEPTION_ID = "causeException";
private Button causeException;

public ClientSideExceptionHandlingView() {
causeException = new Button("Cause client side exception", e -> {
getUI().get().getPage().executeJavaScript("null.foo");
});
causeException.setId(CAUSE_EXCEPTION_ID);
add(causeException);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.hummingbird.uitest.ui;

import org.junit.Assert;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;

import com.vaadin.hummingbird.testutil.PhantomJSTest;

public class ClientSideExceptionHandlingIT extends PhantomJSTest {

private static final By ERROR_LOCATOR = By.className("v-system-error");

@Test
public void developmentModeExceptions() {
open();
causeException();

WebElement errorMessage = findElement(ERROR_LOCATOR);
Assert.assertTrue(errorMessage.getText(),
errorMessage.getText().contains("null is not an object"));
}

@Test
public void productionModeExceptions() {
openProduction();
causeException();

Assert.assertFalse(isElementPresent(ERROR_LOCATOR));
}

private void causeException() {
findElement(By.id(ClientSideExceptionHandlingView.CAUSE_EXCEPTION_ID))
.click();
}

}

0 comments on commit f92253a

Please sign in to comment.