Skip to content

Commit 6473390

Browse files
authored
Merge pull request #139 from ml-in-programming/errorReporting
Implemented error reporting
2 parents a07e286 + d1ad348 commit 6473390

File tree

12 files changed

+538
-2
lines changed

12 files changed

+538
-2
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ repositories {
2525
apply plugin: 'idea'
2626

2727
dependencies {
28+
compile group: 'org.eclipse.mylyn.github', name: 'org.eclipse.egit.github.core', version: '2.1.5'
2829
runtime 'com.google.code.gson:gson:2.8.5'
2930

3031
compile project(':MetricsReloaded')
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package org.jetbrains.research.groups.ml_methods.error_reporting;
2+
3+
import com.intellij.openapi.diagnostic.SubmittedReportInfo;
4+
import com.intellij.openapi.diagnostic.SubmittedReportInfo.SubmissionStatus;
5+
import org.eclipse.egit.github.core.Issue;
6+
import org.eclipse.egit.github.core.Label;
7+
import org.eclipse.egit.github.core.RepositoryId;
8+
import org.eclipse.egit.github.core.client.GitHubClient;
9+
import org.eclipse.egit.github.core.client.PageIterator;
10+
import org.eclipse.egit.github.core.service.IssueService;
11+
import org.jetbrains.research.groups.ml_methods.error_reporting.ErrorReportInformation.InformationType;
12+
import org.jetbrains.research.groups.ml_methods.utils.ArchitectureReloadedBundle;
13+
14+
import javax.annotation.Nullable;
15+
import java.util.*;
16+
17+
import static org.jetbrains.research.groups.ml_methods.error_reporting.ErrorReportInformation.InformationType.*;
18+
19+
/**
20+
* Provides functionality to create and send GitHub issues when an exception is thrown by a plugin.
21+
*/
22+
class AnonymousFeedback {
23+
private final static String TOKEN_FILE = "errorReporterToken";
24+
private final static String GIT_REPO_USER = "ml-in-programming";
25+
private final static String GIT_REPO = "ArchitectureReloaded";
26+
private final static String ISSUE_LABEL_BUG = "bug";
27+
private final static String ISSUE_LABEL_AUTO_GENERATED = "auto-generated";
28+
private final static String GIT_ISSUE_TITLE = "[auto-generated:%s] %s";
29+
private final static String HTML_URL_TO_CREATE_NEW_ISSUE = "https://github.com/ml-in-programming/ArchitectureReloaded/issues/new";
30+
private final static EnumMap<InformationType, String> usersInformationToPresentableForm
31+
= new EnumMap<>(InformationType.class);
32+
33+
static {
34+
usersInformationToPresentableForm.put(PLUGIN_NAME, "Plugin Name");
35+
usersInformationToPresentableForm.put(PLUGIN_VERSION, "Plugin Version");
36+
usersInformationToPresentableForm.put(OS_NAME, "OS Name");
37+
usersInformationToPresentableForm.put(JAVA_VERSION, "Java Version");
38+
usersInformationToPresentableForm.put(JAVA_VM_VENDOR, "Java VM Vendor");
39+
usersInformationToPresentableForm.put(APP_NAME, "App Name");
40+
usersInformationToPresentableForm.put(APP_FULL_NAME, "App Full Name");
41+
usersInformationToPresentableForm.put(APP_VERSION_NAME, "App Version Name");
42+
usersInformationToPresentableForm.put(IS_EAP, "Is EAP");
43+
usersInformationToPresentableForm.put(APP_BUILD, "App Build");
44+
usersInformationToPresentableForm.put(APP_VERSION, "App Version");
45+
usersInformationToPresentableForm.put(LAST_ACTION, "Last Action");
46+
usersInformationToPresentableForm.put(PERMANENT_INSTALLATION_ID, "User's Permanent Installation ID");
47+
}
48+
49+
private AnonymousFeedback() {
50+
}
51+
52+
/**
53+
* Makes a connection to GitHub. Checks if there is an issue that is a duplicate and based on this, creates either a
54+
* new issue or comments on the duplicate (if the user provided additional information).
55+
*
56+
* @param errorReportInformation Information collected by {@link ErrorReportInformation}
57+
* @return The report info that is then used in {@link GitHubErrorReporter} to show the user a balloon with the link
58+
* of the created issue.
59+
*/
60+
static SubmittedReportInfo sendFeedback(ErrorReportInformation errorReportInformation) {
61+
62+
final SubmittedReportInfo result;
63+
try {
64+
final String gitAccessToken = GitHubAccessTokenScrambler.decrypt(AnonymousFeedback.class.getResourceAsStream(TOKEN_FILE));
65+
66+
GitHubClient client = new GitHubClient();
67+
client.setOAuth2Token(gitAccessToken);
68+
RepositoryId repoID = new RepositoryId(GIT_REPO_USER, GIT_REPO);
69+
IssueService issueService = new IssueService(client);
70+
71+
Issue newGibHubIssue = createNewGibHubIssue(errorReportInformation);
72+
Issue duplicate = findFirstDuplicate(newGibHubIssue.getTitle(), issueService, repoID);
73+
boolean isNewIssue = true;
74+
if (duplicate != null) {
75+
String newErrorComment = generateGitHubIssueBody(errorReportInformation, false);
76+
issueService.createComment(repoID, duplicate.getNumber(), newErrorComment);
77+
newGibHubIssue = duplicate;
78+
isNewIssue = false;
79+
} else {
80+
newGibHubIssue = issueService.createIssue(repoID, newGibHubIssue);
81+
}
82+
83+
final long id = newGibHubIssue.getNumber();
84+
final String htmlUrl = newGibHubIssue.getHtmlUrl();
85+
final String message = ArchitectureReloadedBundle.message(isNewIssue ? "git.issue.text" : "git.issue.duplicate.text", htmlUrl, id);
86+
result = new SubmittedReportInfo(htmlUrl, message, isNewIssue ? SubmissionStatus.NEW_ISSUE : SubmissionStatus.DUPLICATE);
87+
return result;
88+
} catch (Exception e) {
89+
return new SubmittedReportInfo(HTML_URL_TO_CREATE_NEW_ISSUE,
90+
ArchitectureReloadedBundle.message("report.error.connection.failure",
91+
HTML_URL_TO_CREATE_NEW_ISSUE),
92+
SubmissionStatus.FAILED);
93+
}
94+
}
95+
96+
/**
97+
* Collects all issues on the repo and finds the first duplicate that has the same title. For this to work, the title
98+
* contains the hash of the stack trace.
99+
*
100+
* @param uniqueTitle Title of the newly created issue. Since for auto-reported issues the title is always the same,
101+
* it includes the hash of the stack trace. The title is used so that I don't have to match
102+
* something in the whole body of the issue.
103+
* @param service Issue-service of the GitHub lib that lets you access all issues
104+
* @param repo The repository that should be used
105+
* @return The duplicate if one is found or null
106+
*/
107+
@Nullable
108+
private static Issue findFirstDuplicate(String uniqueTitle, final IssueService service, RepositoryId repo) {
109+
Map<String, String> searchParameters = new HashMap<>(2);
110+
searchParameters.put(IssueService.FILTER_STATE, IssueService.STATE_OPEN);
111+
final PageIterator<Issue> pages = service.pageIssues(repo, searchParameters);
112+
for (Collection<Issue> page : pages) {
113+
for (Issue issue : page) {
114+
if (issue.getTitle().equals(uniqueTitle)) {
115+
return issue;
116+
}
117+
}
118+
}
119+
return null;
120+
}
121+
122+
/**
123+
* Turns collected information of an error into a new (offline) GitHub issue
124+
*
125+
* @param errorReportInformation A map of the information. Note that I remove items from there when they should not go in the issue
126+
* body as well. When creating the body, all remaining items are iterated.
127+
* @return The new issue
128+
*/
129+
private static Issue createNewGibHubIssue(ErrorReportInformation errorReportInformation) {
130+
String errorMessage = errorReportInformation.get(ERROR_MESSAGE);
131+
if (errorMessage == null || errorMessage.isEmpty()) {
132+
errorMessage = "Unspecified error";
133+
}
134+
String errorHash = errorReportInformation.get(ERROR_HASH);
135+
if (errorHash == null) {
136+
errorHash = "";
137+
}
138+
139+
final Issue gitHubIssue = new Issue();
140+
final String body = generateGitHubIssueBody(errorReportInformation, true);
141+
gitHubIssue.setTitle(String.format(GIT_ISSUE_TITLE, errorHash, errorMessage));
142+
gitHubIssue.setBody(body);
143+
Label bugLabel = new Label();
144+
bugLabel.setName(ISSUE_LABEL_BUG);
145+
Label autoGeneratedLabel = new Label();
146+
autoGeneratedLabel.setName(ISSUE_LABEL_AUTO_GENERATED);
147+
gitHubIssue.setLabels(Arrays.asList(autoGeneratedLabel, bugLabel));
148+
return gitHubIssue;
149+
}
150+
151+
/**
152+
* Creates the body of the GitHub issue. It will contain information about the system, error report information
153+
* provided by the user and the full stack trace. Everything is formatted using markdown.
154+
*
155+
* @param errorReportInformation Details provided by {@link ErrorReportInformation}
156+
* @return A markdown string representing the GitHub issue body.
157+
*/
158+
private static String generateGitHubIssueBody(ErrorReportInformation errorReportInformation, boolean addStacktrace) {
159+
String errorDescription = errorReportInformation.get(ERROR_DESCRIPTION);
160+
if (errorDescription == null) {
161+
errorDescription = "";
162+
}
163+
String stackTrace = errorReportInformation.get(ERROR_STACKTRACE);
164+
if (stackTrace == null || stackTrace.isEmpty()) {
165+
stackTrace = "invalid stacktrace";
166+
}
167+
168+
StringBuilder result = new StringBuilder();
169+
if (!errorDescription.isEmpty()) {
170+
result.append(errorDescription);
171+
result.append("\n\n----------------------\n\n");
172+
}
173+
for (Map.Entry<InformationType, String> usersInformationEntry : usersInformationToPresentableForm.entrySet()) {
174+
result.append("- ");
175+
result.append(usersInformationEntry.getValue());
176+
result.append(": ");
177+
result.append(errorReportInformation.get(usersInformationEntry.getKey()));
178+
result.append("\n");
179+
}
180+
181+
if (addStacktrace) {
182+
result.append("\n```\n");
183+
result.append(stackTrace);
184+
result.append("\n```\n");
185+
}
186+
187+
return result.toString();
188+
}
189+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.jetbrains.research.groups.ml_methods.error_reporting;
2+
3+
import com.intellij.openapi.diagnostic.SubmittedReportInfo;
4+
import com.intellij.openapi.progress.ProgressIndicator;
5+
import com.intellij.openapi.progress.Task.Backgroundable;
6+
import com.intellij.openapi.project.Project;
7+
import com.intellij.util.Consumer;
8+
import org.jetbrains.annotations.NotNull;
9+
import org.jetbrains.annotations.Nullable;
10+
11+
12+
/**
13+
* Encapsulates the sending of feedback into a background task that is run by {@link GitHubErrorReporter}
14+
*/
15+
public class AnonymousFeedbackTask extends Backgroundable {
16+
private final Consumer<SubmittedReportInfo> myCallback;
17+
private final ErrorReportInformation errorReportInformation;
18+
19+
AnonymousFeedbackTask(@Nullable Project project,
20+
@NotNull String title,
21+
boolean canBeCancelled,
22+
ErrorReportInformation errorReportInformation,
23+
final Consumer<SubmittedReportInfo> callback) {
24+
super(project, title, canBeCancelled);
25+
26+
this.errorReportInformation = errorReportInformation;
27+
myCallback = callback;
28+
}
29+
30+
@Override
31+
public void run(@NotNull ProgressIndicator indicator) {
32+
indicator.setIndeterminate(true);
33+
myCallback.consume(AnonymousFeedback.sendFeedback(errorReportInformation));
34+
}
35+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.jetbrains.research.groups.ml_methods.error_reporting;
2+
3+
import com.intellij.openapi.application.ApplicationNamesInfo;
4+
import com.intellij.openapi.application.PermanentInstallationID;
5+
import com.intellij.openapi.application.ex.ApplicationInfoEx;
6+
import com.intellij.openapi.diagnostic.Attachment;
7+
import com.intellij.util.SystemProperties;
8+
9+
import java.util.EnumMap;
10+
11+
import static org.jetbrains.research.groups.ml_methods.error_reporting.ErrorReportInformation.InformationType.*;
12+
13+
/**
14+
* Collects information about the running IDEA and the error
15+
*/
16+
class ErrorReportInformation {
17+
public enum InformationType {
18+
ERROR_DESCRIPTION, PLUGIN_NAME, PLUGIN_VERSION, OS_NAME, JAVA_VERSION, JAVA_VM_VENDOR,
19+
APP_NAME, APP_FULL_NAME, APP_VERSION_NAME, IS_EAP, APP_BUILD, APP_VERSION, LAST_ACTION,
20+
PERMANENT_INSTALLATION_ID, ERROR_MESSAGE, ERROR_STACKTRACE, ERROR_HASH, ATTACHMENT_NAME, ATTACHMENT_VALUE
21+
}
22+
23+
private final EnumMap<InformationType, String> information = new EnumMap<>(InformationType.class);
24+
25+
private ErrorReportInformation(GitHubErrorBean error,
26+
ApplicationInfoEx appInfo,
27+
ApplicationNamesInfo namesInfo) {
28+
information.put(ERROR_DESCRIPTION, error.getDescription());
29+
30+
information.put(PLUGIN_NAME, error.getPluginName());
31+
information.put(PLUGIN_VERSION, error.getPluginVersion());
32+
information.put(OS_NAME, SystemProperties.getOsName());
33+
information.put(JAVA_VERSION, SystemProperties.getJavaVersion());
34+
information.put(JAVA_VM_VENDOR, SystemProperties.getJavaVmVendor());
35+
information.put(APP_NAME, namesInfo.getProductName());
36+
information.put(APP_FULL_NAME, namesInfo.getFullProductName());
37+
information.put(APP_VERSION_NAME, appInfo.getVersionName());
38+
information.put(IS_EAP, Boolean.toString(appInfo.isEAP()));
39+
information.put(APP_BUILD, appInfo.getBuild().asString());
40+
information.put(APP_VERSION, appInfo.getFullVersion());
41+
information.put(PERMANENT_INSTALLATION_ID, PermanentInstallationID.get());
42+
information.put(LAST_ACTION, error.getLastAction());
43+
44+
information.put(ERROR_MESSAGE, error.getMessage());
45+
information.put(ERROR_STACKTRACE, error.getStackTrace());
46+
information.put(ERROR_HASH, error.getExceptionHash());
47+
48+
for (Attachment attachment : error.getAttachments()) {
49+
information.put(ATTACHMENT_NAME, attachment.getName());
50+
information.put(ATTACHMENT_VALUE, attachment.getEncodedBytes());
51+
}
52+
53+
}
54+
55+
static ErrorReportInformation getUsersInformation(GitHubErrorBean error,
56+
ApplicationInfoEx appInfo,
57+
ApplicationNamesInfo namesInfo) {
58+
return new ErrorReportInformation(error, appInfo, namesInfo);
59+
}
60+
61+
public String get(InformationType informationType) {
62+
return information.get(informationType);
63+
}
64+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.jetbrains.research.groups.ml_methods.error_reporting;
2+
3+
4+
import org.apache.commons.codec.binary.Base64;
5+
6+
import javax.crypto.Cipher;
7+
import javax.crypto.spec.IvParameterSpec;
8+
import javax.crypto.spec.SecretKeySpec;
9+
import java.io.FileOutputStream;
10+
import java.io.InputStream;
11+
import java.io.ObjectInputStream;
12+
import java.io.ObjectOutputStream;
13+
14+
/**
15+
* Provides functionality to encode and decode secret tokens to make them not directly readable. Let me be clear:
16+
* THIS IS THE OPPOSITE OF SECURITY!
17+
*/
18+
public class GitHubAccessTokenScrambler {
19+
private static final String myInitVector = "RandomInitVector";
20+
private static final String myKey = "GitHubErrorToken";
21+
22+
public static void main(String[] args) {
23+
if (args.length != 2) {
24+
return;
25+
}
26+
String horse = args[0];
27+
String outputFile = args[1];
28+
try {
29+
final String e = encrypt(horse);
30+
final ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(outputFile));
31+
o.writeObject(e);
32+
o.close();
33+
} catch (Exception e) {
34+
e.printStackTrace();
35+
}
36+
}
37+
38+
private static String encrypt(String value) {
39+
try {
40+
IvParameterSpec iv = new IvParameterSpec(myInitVector.getBytes("UTF-8"));
41+
SecretKeySpec keySpec = new SecretKeySpec(myKey.getBytes("UTF-8"), "AES");
42+
43+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
44+
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
45+
46+
byte[] encrypted = cipher.doFinal(value.getBytes());
47+
return Base64.encodeBase64String(encrypted);
48+
} catch (Exception ex) {
49+
ex.printStackTrace();
50+
}
51+
return null;
52+
}
53+
54+
static String decrypt(InputStream inputStream) throws Exception {
55+
String in;
56+
final ObjectInputStream o = new ObjectInputStream(inputStream);
57+
in = (String) o.readObject();
58+
IvParameterSpec iv = new IvParameterSpec(myInitVector.getBytes("UTF-8"));
59+
SecretKeySpec keySpec = new SecretKeySpec(myKey.getBytes("UTF-8"), "AES");
60+
61+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
62+
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
63+
64+
byte[] original = cipher.doFinal(Base64.decodeBase64(in));
65+
return new String(original);
66+
}
67+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.jetbrains.research.groups.ml_methods.error_reporting;
2+
3+
import com.intellij.errorreport.bean.ErrorBean;
4+
5+
import java.util.Arrays;
6+
7+
/**
8+
* Extends the standard class to provide the hash of the thrown exception stack trace.
9+
*/
10+
class GitHubErrorBean extends ErrorBean {
11+
12+
private String myExceptionHash;
13+
14+
GitHubErrorBean(Throwable throwable, String lastAction) {
15+
super(throwable, lastAction);
16+
final long hashCode = Integer.toUnsignedLong(Arrays.hashCode(throwable.getStackTrace()));
17+
myExceptionHash = Long.toHexString(hashCode);
18+
}
19+
20+
String getExceptionHash() {
21+
return myExceptionHash;
22+
}
23+
}

0 commit comments

Comments
 (0)