add bug matching configuration

This commit is contained in:
F43nd1r 2018-01-24 01:11:55 +01:00
parent 6012536fe7
commit 3258a74557
12 changed files with 164 additions and 14 deletions

View file

@ -44,6 +44,7 @@ vaadin {
}
vaadinCompile {
style 'PRETTY'
outputDirectory 'src/main/resources'
strict true
widgetset 'com.faendir.acra.AppWidgetset'

View file

@ -8,7 +8,6 @@ import com.vaadin.client.ServerConnector;
import com.vaadin.client.connectors.grid.GridConnector;
import com.vaadin.client.extensions.AbstractExtensionConnector;
import com.vaadin.client.widget.grid.CellReference;
import com.vaadin.client.widgets.Grid;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.data.DataCommunicatorConstants;
@ -23,11 +22,10 @@ import elemental.json.JsonObject;
public class MiddleClickGridExtensionConnector extends AbstractExtensionConnector {
@Override
protected void extend(ServerConnector target) {
Grid<JsonObject> grid = getParent().getWidget();
grid.addDomHandler(event -> {
getParent().getWidget().addDomHandler(event -> {
if (event.getNativeButton() == NativeEvent.BUTTON_MIDDLE) {
event.preventDefault();
CellReference<JsonObject> cell = grid.getCellReference(event.getRelativeElement());
CellReference<JsonObject> cell = getParent().getWidget().getEventCell();
getRpcProxy(Rpc.class).middleClick(cell.getRow().getString(DataCommunicatorConstants.KEY), getParent().getColumnId(cell.getColumn()),
MouseEventDetailsBuilder.buildMouseEventDetails(event.getNativeEvent(), event.getRelativeElement()));
}

View file

@ -98,7 +98,7 @@ public class ReportService {
private void newReport(@NonNull App app, @NonNull String content, @NonNull List<MultipartFile> attachments) {
JSONObject jsonObject = new JSONObject(content);
String stacktrace = jsonObject.optString(ReportField.STACK_TRACE.name());
String stacktrace = Utils.generifyStacktrace(jsonObject.optString(ReportField.STACK_TRACE.name()), app.getConfiguration());
Date crashDate = Utils.getDateFromString(jsonObject.optString(ReportField.USER_CRASH_DATE.name()));
Bug bug = bugRepository.findBugByAppAndStacktrace(app, stacktrace)
.orElseGet(() -> new Bug(app, stacktrace, jsonObject.optInt(ReportField.APP_VERSION_CODE.name()), crashDate));

View file

@ -5,7 +5,10 @@ import com.faendir.acra.sql.model.Bug;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@ -23,4 +26,9 @@ public interface BugRepository extends JpaRepository<Bug, Integer> {
int countAllByAppAndSolvedFalse(@NonNull App app);
Optional<Bug> findBugByAppAndStacktrace(@NonNull App app, @NonNull String stacktrace);
@Transactional
@Modifying
@Query("delete from Bug bug where bug not in (select report.bug from Report report group by report.bug)")
void deleteOrphans();
}

View file

@ -4,14 +4,19 @@ import com.faendir.acra.sql.model.App;
import com.faendir.acra.sql.model.Bug;
import com.faendir.acra.sql.model.Report;
import com.faendir.acra.sql.util.CountResult;
import com.faendir.acra.util.Utils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
/**
* @author Lukas
@ -22,6 +27,8 @@ public interface ReportRepository extends JpaRepository<Report, String> {
Slice<Report> findAllByBugApp(@NonNull App app, @NonNull Pageable pageable);
Stream<Report> findAllByBugApp(@NonNull App app);
int countAllByBugApp(@NonNull App app);
@SuppressWarnings("SpringDataRepositoryMethodReturnTypeInspection")
@ -41,4 +48,20 @@ public interface ReportRepository extends JpaRepository<Report, String> {
@SuppressWarnings("SpringDataRepositoryMethodReturnTypeInspection")
@Query("select new com.faendir.acra.sql.util.CountResult(bug.id, count(report)) from Report report group by report.bug")
List<CountResult<Integer>> countAllByBug();
@Transactional
default void reassignBugs(App app) {
Map<String, Bug> bugs = new HashMap<>();
try(Stream<Report> stream = findAllByBugApp(app)) {
stream.forEach(report -> {
String stacktrace = Utils.generifyStacktrace(report.getStacktrace(), app.getConfiguration());
Bug bug = bugs.get(stacktrace);
if (bug == null) {
bug = new Bug(app, stacktrace, report.getVersionCode(), report.getDate());
}
report.setBug(bug);
bugs.put(stacktrace, save(report).getBug());
});
}
}
}

View file

@ -6,6 +6,7 @@ import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.lang.NonNull;
import javax.persistence.CascadeType;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
@ -25,6 +26,7 @@ public class App {
@OneToOne(cascade = CascadeType.ALL, optional = false, orphanRemoval = true)
@OnDelete(action = OnDeleteAction.CASCADE)
private User reporter;
private Configuration configuration;
@PersistenceConstructor
App() {
@ -53,6 +55,14 @@ public class App {
this.reporter = reporter;
}
public Configuration getConfiguration() {
return configuration;
}
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -65,4 +75,36 @@ public class App {
public int hashCode() {
return id;
}
@Embeddable
public static class Configuration {
private boolean matchByMessage;
private boolean ignoreInstanceIds;
private boolean ignoreAndroidLineNumbers;
@PersistenceConstructor
Configuration() {
matchByMessage = true;
ignoreInstanceIds = true;
ignoreAndroidLineNumbers = true;
}
public Configuration(boolean matchByMessage, boolean ignoreInstanceIds, boolean ignoreAndroidLineNumbers) {
this.matchByMessage = matchByMessage;
this.ignoreInstanceIds = ignoreInstanceIds;
this.ignoreAndroidLineNumbers = ignoreAndroidLineNumbers;
}
public boolean matchByMessage() {
return matchByMessage;
}
public boolean ignoreInstanceIds() {
return ignoreInstanceIds;
}
public boolean ignoreAndroidLineNumbers() {
return ignoreAndroidLineNumbers;
}
}
}

View file

@ -68,6 +68,10 @@ public class Report {
return bug;
}
public void setBug(Bug bug) {
this.bug = bug;
}
@NonNull
public String getId() {
return id;

View file

@ -58,7 +58,7 @@ public class MyGrid<T> extends Grid<T> {
}
public void setSizeToRows() {
((ObservableDataProvider) getDataProvider()).addSizeListener(this::setHeightByRows);
((ObservableDataProvider) getDataProvider()).addSizeListener(rows -> getUI().access(() -> setHeightByRows(rows)));
}
public void addOnClickNavigation(@NonNull NavigationManager navigationManager, Class<? extends NamedView> namedView, Function<ItemClick<T>, String> parameterGetter) {
@ -71,9 +71,9 @@ public class MyGrid<T> extends Grid<T> {
public static class MiddleClickExtension<T> extends AbstractGridExtension<T> {
private MiddleClickExtension(MyGrid<T> grid) {
super.extend(grid);
grid.registerRpc((rowKey, columnInternalId, details) -> grid.fireEvent(
new ItemClick<>(grid, grid.getColumnByInternalId(columnInternalId), grid.getDataCommunicator().getKeyMapper().get(rowKey), details)),
MiddleClickGridExtensionConnector.Rpc.class);
registerRpc((rowKey, columnInternalId, details) -> grid.fireEvent(
new ItemClick<>(grid, grid.getColumnByInternalId(columnInternalId), grid.getDataCommunicator().getKeyMapper().get(rowKey),
details)), MiddleClickGridExtensionConnector.Rpc.class);
}
public static void extend(MyGrid<?> grid) {

View file

@ -1,6 +1,7 @@
package com.faendir.acra.ui.view.tabs;
import com.faendir.acra.sql.data.AppRepository;
import com.faendir.acra.sql.data.BugRepository;
import com.faendir.acra.sql.data.ReportRepository;
import com.faendir.acra.sql.model.App;
import com.faendir.acra.sql.model.Permission;
@ -11,11 +12,13 @@ import com.faendir.acra.ui.annotation.RequiresAppPermission;
import com.faendir.acra.ui.view.base.ConfigurationLabel;
import com.faendir.acra.ui.view.base.MyTabSheet;
import com.faendir.acra.ui.view.base.Popup;
import com.faendir.acra.ui.view.base.ValidatedField;
import com.vaadin.shared.ui.ContentMode;
import com.vaadin.spring.annotation.SpringComponent;
import com.vaadin.spring.annotation.ViewScope;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
@ -38,12 +41,14 @@ import java.util.Date;
public class PropertiesTab implements MyTabSheet.Tab {
public static final String CAPTION = "Properties";
@NonNull private final AppRepository appRepository;
private final ReportRepository reportRepository;
@NonNull private final BugRepository bugRepository;
@NonNull private final ReportRepository reportRepository;
@NonNull private final UserManager userManager;
@Autowired
public PropertiesTab(@NonNull AppRepository appRepository, @NonNull ReportRepository reportRepository, @NonNull UserManager userManager) {
public PropertiesTab(@NonNull AppRepository appRepository, @NonNull BugRepository bugRepository, @NonNull ReportRepository reportRepository, @NonNull UserManager userManager) {
this.appRepository = appRepository;
this.bugRepository = bugRepository;
this.reportRepository = reportRepository;
this.userManager = userManager;
}
@ -77,6 +82,25 @@ public class PropertiesTab implements MyTabSheet.Tab {
}), new Label("Reports older than "), age, new Label("Days"));
purgeAge.setDefaultComponentAlignment(Alignment.MIDDLE_CENTER);
layout.addComponent(purgeAge);
layout.addComponent(new Button("Configure bug matching", e -> {
App.Configuration configuration = app.getConfiguration();
CheckBox matchByMessage = new CheckBox("Match by exception message", configuration.matchByMessage());
CheckBox ignoreInstanceIds = new CheckBox("Ignore instance ids", configuration.ignoreInstanceIds());
CheckBox ignoreAndroidLineNumbers = new CheckBox("Ignore android SDK line numbers", configuration.ignoreAndroidLineNumbers());
new Popup().addValidatedField(ValidatedField.of(matchByMessage), true)
.addValidatedField(ValidatedField.of(ignoreInstanceIds), true)
.addValidatedField(ValidatedField.of(ignoreAndroidLineNumbers), true)
.addComponent(new Label(
"Are you sure you want to save this configuration? All bugs will be recalculated, which may take some time and will reset the 'solved' status"))
.addYesNoButtons(p -> {
app.setConfiguration(new App.Configuration(matchByMessage.getValue(), ignoreInstanceIds.getValue(), ignoreAndroidLineNumbers.getValue()));
appRepository.save(app);
reportRepository.reassignBugs(app);
bugRepository.deleteOrphans();
p.close();
})
.show();
}));
layout.setSizeUndefined();
return layout;
}

View file

@ -54,6 +54,7 @@ public class UserManagerView extends NamedView {
public void enter(ViewChangeListener.ViewChangeEvent event) {
userGrid = new MyGrid<>("Users", factory.create(UserManager.ROLE_USER, userRepository::findAllByRoles, userRepository::countAllByRoles));
userGrid.setSelectionMode(Grid.SelectionMode.NONE);
userGrid.setBodyRowHeight(42);
userGrid.setSizeToRows();
userGrid.addColumn(User::getUsername, "Username");
userGrid.addComponentColumn(user -> new MyCheckBox(user.getRoles().contains(UserManager.ROLE_ADMIN), !user.getUsername().equals(SecurityUtils.getUsername()),
@ -68,7 +69,6 @@ public class UserManagerView extends NamedView {
return levelComboBox;
}).setCaption("Access Permission for " + app.getName());
}
userGrid.setRowHeight(42);
Button newUser = new Button("New User", e -> newUser());
VerticalLayout layout = new VerticalLayout(userGrid, newUser);
Style.NO_PADDING.apply(layout);

View file

@ -1,5 +1,6 @@
package com.faendir.acra.util;
import com.faendir.acra.sql.model.App;
import com.vaadin.ui.UI;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -7,11 +8,19 @@ import org.springframework.lang.NonNull;
import org.springframework.web.util.UriComponentsBuilder;
import proguard.retrace.ReTrace;
import java.io.*;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author Lukas
@ -31,7 +40,9 @@ public final class Utils {
}
public static String retrace(@NonNull String stacktrace, @NonNull String mappings) {
try (Reader mappingsReader = new StringReader(mappings); Reader stacktraceReader = new StringReader(stacktrace); StringWriter output = new StringWriter()) {
try (Reader mappingsReader = new StringReader(mappings);
Reader stacktraceReader = new StringReader(stacktrace);
StringWriter output = new StringWriter()) {
new ReTrace(ReTrace.STACK_TRACE_EXPRESSION, false, mappingsReader, stacktraceReader, output).execute();
return output.toString();
} catch (IOException e) {
@ -43,4 +54,43 @@ public final class Utils {
public static String getUrlWithFragment(String fragment) {
return UriComponentsBuilder.fromUri(UI.getCurrent().getPage().getLocation()).fragment(fragment).build().encode().toUri().toASCIIString();
}
public static String generifyStacktrace(String stacktrace, App.Configuration configuration) {
List<String> lines = Pattern.compile("\r?\n").splitAsStream(stacktrace).collect(Collectors.toCollection(ArrayList::new));
StringBuilder output = new StringBuilder();
Pattern headLinePattern = Pattern.compile("^([\\w.]+)(:(.*))?$");
Pattern tracePattern = Pattern.compile("^\\s*at\\s+([\\w.$_]+)\\.([\\w$_]+)\\((.*)\\)$");
Pattern sourcePattern = Pattern.compile("^(android\\..*:)(\\d+)$");
Pattern instancePattern = Pattern.compile("(([a-z_$][a-z0-9_$]*\\.)+[a-zA-Z_$][a-zA-Z0-9_$]*@)([a-fA-F0-9]+)");
while (lines.size() > 0) {
String line;
Matcher headLineMatcher = headLinePattern.matcher(line = lines.remove(0));
if (!headLineMatcher.find()) {
output.append(line);
} else if (!configuration.matchByMessage()) {
output.append(headLineMatcher.group(1)).append(":<message>");
} else if (configuration.ignoreInstanceIds()) {
String message = headLineMatcher.group(2);
if(message != null) {
output.append(headLineMatcher.group(1)).append(instancePattern.matcher(message).replaceAll("$1<instance>"));
}else {
output.append(headLineMatcher.group(1));
}
}
Matcher lineMatcher;
while (lines.size() > 0 && (lineMatcher = tracePattern.matcher(line = lines.remove(0))).find()) {
output.append('\n');
Matcher sourceMatcher;
if (configuration.ignoreAndroidLineNumbers() && (sourceMatcher = sourcePattern.matcher(lineMatcher.group(3))).find()) {
output.append(sourceMatcher.group(1)).append("<line>");
} else {
output.append(line);
}
}
while (lines.size() > 0 && !headLinePattern.matcher(line = lines.remove(0)).find()) {
output.append('\n').append(line);
}
}
return output.toString();
}
}