add bug matching configuration
This commit is contained in:
parent
6012536fe7
commit
3258a74557
12 changed files with 164 additions and 14 deletions
|
@ -44,6 +44,7 @@ vaadin {
|
|||
}
|
||||
|
||||
vaadinCompile {
|
||||
style 'PRETTY'
|
||||
outputDirectory 'src/main/resources'
|
||||
strict true
|
||||
widgetset 'com.faendir.acra.AppWidgetset'
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,10 @@ public class Report {
|
|||
return bug;
|
||||
}
|
||||
|
||||
public void setBug(Bug bug) {
|
||||
this.bug = bug;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getId() {
|
||||
return id;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue