Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ repositories {
apply plugin: 'idea'

dependencies {
compile group: 'org.eclipse.mylyn.github', name: 'org.eclipse.egit.github.core', version: '2.1.5'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

может быть следовать одному стандарту описания зависимостей?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

какому?
В features-extraction есть такое:
testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'

Там тогда тоже надо менять

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ну хотя бы в пределах одного файла

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

так там остальное это jar и модули, они по другому подключаются же.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не понял, как ты хочешь написать.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ну через colon
'org.eclipse.mylyn.github:org.eclipse.egit.github.core:2.1.5'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maven при копировании для gradle предлагает тот вариант, который сейчас.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ну да и ладно, пусть так будет

runtime 'com.google.code.gson:gson:2.8.5'

compile project(':MetricsReloaded')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package org.jetbrains.research.groups.ml_methods.error_reporting;

import com.intellij.openapi.diagnostic.SubmittedReportInfo;
import com.intellij.openapi.diagnostic.SubmittedReportInfo.SubmissionStatus;
import org.eclipse.egit.github.core.Issue;
import org.eclipse.egit.github.core.Label;
import org.eclipse.egit.github.core.RepositoryId;
import org.eclipse.egit.github.core.client.GitHubClient;
import org.eclipse.egit.github.core.client.PageIterator;
import org.eclipse.egit.github.core.service.IssueService;
import org.jetbrains.research.groups.ml_methods.error_reporting.ErrorReportInformation.InformationType;
import org.jetbrains.research.groups.ml_methods.utils.ArchitectureReloadedBundle;

import javax.annotation.Nullable;
import java.util.*;

import static org.jetbrains.research.groups.ml_methods.error_reporting.ErrorReportInformation.InformationType.*;

/**
* Provides functionality to create and send GitHub issues when an exception is thrown by a plugin.
*/
class AnonymousFeedback {
private final static String TOKEN_FILE = "errorReporterToken";
private final static String GIT_REPO_USER = "ml-in-programming";
private final static String GIT_REPO = "ArchitectureReloaded";
private final static String ISSUE_LABEL_BUG = "bug";
private final static String ISSUE_LABEL_AUTO_GENERATED = "auto-generated";
private final static String GIT_ISSUE_TITLE = "[auto-generated:%s] %s";
private final static String HTML_URL_TO_CREATE_NEW_ISSUE = "https://github.com/ml-in-programming/ArchitectureReloaded/issues/new";
private final static EnumMap<InformationType, String> usersInformationToPresentableForm
= new EnumMap<>(InformationType.class);

static {
usersInformationToPresentableForm.put(PLUGIN_NAME, "Plugin Name");
usersInformationToPresentableForm.put(PLUGIN_VERSION, "Plugin Version");
usersInformationToPresentableForm.put(OS_NAME, "OS Name");
usersInformationToPresentableForm.put(JAVA_VERSION, "Java Version");
usersInformationToPresentableForm.put(JAVA_VM_VENDOR, "Java VM Vendor");
usersInformationToPresentableForm.put(APP_NAME, "App Name");
usersInformationToPresentableForm.put(APP_FULL_NAME, "App Full Name");
usersInformationToPresentableForm.put(APP_VERSION_NAME, "App Version Name");
usersInformationToPresentableForm.put(IS_EAP, "Is EAP");
usersInformationToPresentableForm.put(APP_BUILD, "App Build");
usersInformationToPresentableForm.put(APP_VERSION, "App Version");
usersInformationToPresentableForm.put(LAST_ACTION, "Last Action");
usersInformationToPresentableForm.put(PERMANENT_INSTALLATION_ID, "User's Permanent Installation ID");
}

private AnonymousFeedback() {
}

/**
* Makes a connection to GitHub. Checks if there is an issue that is a duplicate and based on this, creates either a
* new issue or comments on the duplicate (if the user provided additional information).
*
* @param errorReportInformation Information collected by {@link ErrorReportInformation}
* @return The report info that is then used in {@link GitHubErrorReporter} to show the user a balloon with the link
* of the created issue.
*/
static SubmittedReportInfo sendFeedback(ErrorReportInformation errorReportInformation) {

final SubmittedReportInfo result;
try {
final String gitAccessToken = GitHubAccessTokenScrambler.decrypt(AnonymousFeedback.class.getResourceAsStream(TOKEN_FILE));

GitHubClient client = new GitHubClient();
client.setOAuth2Token(gitAccessToken);
RepositoryId repoID = new RepositoryId(GIT_REPO_USER, GIT_REPO);
IssueService issueService = new IssueService(client);

Issue newGibHubIssue = createNewGibHubIssue(errorReportInformation);
Issue duplicate = findFirstDuplicate(newGibHubIssue.getTitle(), issueService, repoID);
boolean isNewIssue = true;
if (duplicate != null) {
String newErrorComment = generateGitHubIssueBody(errorReportInformation, false);
issueService.createComment(repoID, duplicate.getNumber(), newErrorComment);
newGibHubIssue = duplicate;
isNewIssue = false;
} else {
newGibHubIssue = issueService.createIssue(repoID, newGibHubIssue);
}

final long id = newGibHubIssue.getNumber();
final String htmlUrl = newGibHubIssue.getHtmlUrl();
final String message = ArchitectureReloadedBundle.message(isNewIssue ? "git.issue.text" : "git.issue.duplicate.text", htmlUrl, id);
result = new SubmittedReportInfo(htmlUrl, message, isNewIssue ? SubmissionStatus.NEW_ISSUE : SubmissionStatus.DUPLICATE);
return result;
} catch (Exception e) {
return new SubmittedReportInfo(HTML_URL_TO_CREATE_NEW_ISSUE,
ArchitectureReloadedBundle.message("report.error.connection.failure",
HTML_URL_TO_CREATE_NEW_ISSUE),
SubmissionStatus.FAILED);
}
}

/**
* Collects all issues on the repo and finds the first duplicate that has the same title. For this to work, the title
* contains the hash of the stack trace.
*
* @param uniqueTitle Title of the newly created issue. Since for auto-reported issues the title is always the same,
* it includes the hash of the stack trace. The title is used so that I don't have to match
* something in the whole body of the issue.
* @param service Issue-service of the GitHub lib that lets you access all issues
* @param repo The repository that should be used
* @return The duplicate if one is found or null
*/
@Nullable
private static Issue findFirstDuplicate(String uniqueTitle, final IssueService service, RepositoryId repo) {
Map<String, String> searchParameters = new HashMap<>(2);
searchParameters.put(IssueService.FILTER_STATE, IssueService.STATE_OPEN);
final PageIterator<Issue> pages = service.pageIssues(repo, searchParameters);
for (Collection<Issue> page : pages) {
for (Issue issue : page) {
if (issue.getTitle().equals(uniqueTitle)) {
return issue;
}
}
}
return null;
}

/**
* Turns collected information of an error into a new (offline) GitHub issue
*
* @param errorReportInformation A map of the information. Note that I remove items from there when they should not go in the issue
* body as well. When creating the body, all remaining items are iterated.
* @return The new issue
*/
private static Issue createNewGibHubIssue(ErrorReportInformation errorReportInformation) {
String errorMessage = errorReportInformation.get(ERROR_MESSAGE);
if (errorMessage == null || errorMessage.isEmpty()) {
errorMessage = "Unspecified error";
}
String errorHash = errorReportInformation.get(ERROR_HASH);
if (errorHash == null) {
errorHash = "";
}

final Issue gitHubIssue = new Issue();
final String body = generateGitHubIssueBody(errorReportInformation, true);
gitHubIssue.setTitle(String.format(GIT_ISSUE_TITLE, errorHash, errorMessage));
gitHubIssue.setBody(body);
Label bugLabel = new Label();
bugLabel.setName(ISSUE_LABEL_BUG);
Label autoGeneratedLabel = new Label();
autoGeneratedLabel.setName(ISSUE_LABEL_AUTO_GENERATED);
gitHubIssue.setLabels(Arrays.asList(autoGeneratedLabel, bugLabel));
return gitHubIssue;
}

/**
* Creates the body of the GitHub issue. It will contain information about the system, error report information
* provided by the user and the full stack trace. Everything is formatted using markdown.
*
* @param errorReportInformation Details provided by {@link ErrorReportInformation}
* @return A markdown string representing the GitHub issue body.
*/
private static String generateGitHubIssueBody(ErrorReportInformation errorReportInformation, boolean addStacktrace) {
String errorDescription = errorReportInformation.get(ERROR_DESCRIPTION);
if (errorDescription == null) {
errorDescription = "";
}
String stackTrace = errorReportInformation.get(ERROR_STACKTRACE);
if (stackTrace == null || stackTrace.isEmpty()) {
stackTrace = "invalid stacktrace";
}

StringBuilder result = new StringBuilder();
if (!errorDescription.isEmpty()) {
result.append(errorDescription);
result.append("\n\n----------------------\n\n");
}
for (Map.Entry<InformationType, String> usersInformationEntry : usersInformationToPresentableForm.entrySet()) {
result.append("- ");
result.append(usersInformationEntry.getValue());
result.append(": ");
result.append(errorReportInformation.get(usersInformationEntry.getKey()));
result.append("\n");
}

if (addStacktrace) {
result.append("\n```\n");
result.append(stackTrace);
result.append("\n```\n");
}

return result.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.jetbrains.research.groups.ml_methods.error_reporting;

import com.intellij.openapi.diagnostic.SubmittedReportInfo;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task.Backgroundable;
import com.intellij.openapi.project.Project;
import com.intellij.util.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;


/**
* Encapsulates the sending of feedback into a background task that is run by {@link GitHubErrorReporter}
*/
public class AnonymousFeedbackTask extends Backgroundable {
private final Consumer<SubmittedReportInfo> myCallback;
private final ErrorReportInformation errorReportInformation;

AnonymousFeedbackTask(@Nullable Project project,
@NotNull String title,
boolean canBeCancelled,
ErrorReportInformation errorReportInformation,
final Consumer<SubmittedReportInfo> callback) {
super(project, title, canBeCancelled);

this.errorReportInformation = errorReportInformation;
myCallback = callback;
}

@Override
public void run(@NotNull ProgressIndicator indicator) {
indicator.setIndeterminate(true);
myCallback.consume(AnonymousFeedback.sendFeedback(errorReportInformation));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.jetbrains.research.groups.ml_methods.error_reporting;

import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.PermanentInstallationID;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.util.SystemProperties;

import java.util.EnumMap;

import static org.jetbrains.research.groups.ml_methods.error_reporting.ErrorReportInformation.InformationType.*;

/**
* Collects information about the running IDEA and the error
*/
class ErrorReportInformation {
public enum InformationType {
ERROR_DESCRIPTION, PLUGIN_NAME, PLUGIN_VERSION, OS_NAME, JAVA_VERSION, JAVA_VM_VENDOR,
APP_NAME, APP_FULL_NAME, APP_VERSION_NAME, IS_EAP, APP_BUILD, APP_VERSION, LAST_ACTION,
PERMANENT_INSTALLATION_ID, ERROR_MESSAGE, ERROR_STACKTRACE, ERROR_HASH, ATTACHMENT_NAME, ATTACHMENT_VALUE
}

private final EnumMap<InformationType, String> information = new EnumMap<>(InformationType.class);

private ErrorReportInformation(GitHubErrorBean error,
ApplicationInfoEx appInfo,
ApplicationNamesInfo namesInfo) {
information.put(ERROR_DESCRIPTION, error.getDescription());

information.put(PLUGIN_NAME, error.getPluginName());
information.put(PLUGIN_VERSION, error.getPluginVersion());
information.put(OS_NAME, SystemProperties.getOsName());
information.put(JAVA_VERSION, SystemProperties.getJavaVersion());
information.put(JAVA_VM_VENDOR, SystemProperties.getJavaVmVendor());
information.put(APP_NAME, namesInfo.getProductName());
information.put(APP_FULL_NAME, namesInfo.getFullProductName());
information.put(APP_VERSION_NAME, appInfo.getVersionName());
information.put(IS_EAP, Boolean.toString(appInfo.isEAP()));
information.put(APP_BUILD, appInfo.getBuild().asString());
information.put(APP_VERSION, appInfo.getFullVersion());
information.put(PERMANENT_INSTALLATION_ID, PermanentInstallationID.get());
information.put(LAST_ACTION, error.getLastAction());

information.put(ERROR_MESSAGE, error.getMessage());
information.put(ERROR_STACKTRACE, error.getStackTrace());
information.put(ERROR_HASH, error.getExceptionHash());

for (Attachment attachment : error.getAttachments()) {
information.put(ATTACHMENT_NAME, attachment.getName());
information.put(ATTACHMENT_VALUE, attachment.getEncodedBytes());
}

}

static ErrorReportInformation getUsersInformation(GitHubErrorBean error,
ApplicationInfoEx appInfo,
ApplicationNamesInfo namesInfo) {
return new ErrorReportInformation(error, appInfo, namesInfo);
}

public String get(InformationType informationType) {
return information.get(informationType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.jetbrains.research.groups.ml_methods.error_reporting;


import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
* Provides functionality to encode and decode secret tokens to make them not directly readable. Let me be clear:
* THIS IS THE OPPOSITE OF SECURITY!
*/
public class GitHubAccessTokenScrambler {
private static final String myInitVector = "RandomInitVector";
private static final String myKey = "GitHubErrorToken";

public static void main(String[] args) {
if (args.length != 2) {
return;
}
String horse = args[0];
String outputFile = args[1];
try {
final String e = encrypt(horse);
final ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(outputFile));
o.writeObject(e);
o.close();
} catch (Exception e) {
e.printStackTrace();
}
}

private static String encrypt(String value) {
try {
IvParameterSpec iv = new IvParameterSpec(myInitVector.getBytes("UTF-8"));
SecretKeySpec keySpec = new SecretKeySpec(myKey.getBytes("UTF-8"), "AES");

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);

byte[] encrypted = cipher.doFinal(value.getBytes());
return Base64.encodeBase64String(encrypted);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}

static String decrypt(InputStream inputStream) throws Exception {
String in;
final ObjectInputStream o = new ObjectInputStream(inputStream);
in = (String) o.readObject();
IvParameterSpec iv = new IvParameterSpec(myInitVector.getBytes("UTF-8"));
SecretKeySpec keySpec = new SecretKeySpec(myKey.getBytes("UTF-8"), "AES");

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);

byte[] original = cipher.doFinal(Base64.decodeBase64(in));
return new String(original);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.jetbrains.research.groups.ml_methods.error_reporting;

import com.intellij.errorreport.bean.ErrorBean;

import java.util.Arrays;

/**
* Extends the standard class to provide the hash of the thrown exception stack trace.
*/
class GitHubErrorBean extends ErrorBean {

private String myExceptionHash;

GitHubErrorBean(Throwable throwable, String lastAction) {
super(throwable, lastAction);
final long hashCode = Integer.toUnsignedLong(Arrays.hashCode(throwable.getStackTrace()));
myExceptionHash = Long.toHexString(hashCode);
}

String getExceptionHash() {
return myExceptionHash;
}
}
Loading