• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Camelcade / Perl5-IDEA / #525521770

03 Mar 2026 03:30PM UTC coverage: 75.982% (+0.01%) from 75.972%
#525521770

push

github

hurricup
[qodana] Hardcoded literal

14770 of 22632 branches covered (65.26%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

13 existing lines in 6 files now uncovered.

31095 of 37731 relevant lines covered (82.41%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.75
/plugin/common/src/main/java/com/perl5/errorHandler/YoutrackErrorHandler.java
1
/*
2
 * Copyright 2015-2026 Alexandr Evstigneev
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 * http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package com.perl5.errorHandler;
18

19
import com.google.gson.Gson;
20
import com.google.gson.JsonSyntaxException;
21
import com.intellij.diagnostic.DiagnosticBundle;
22
import com.intellij.diagnostic.IdeErrorsDialog;
23
import com.intellij.ide.BrowserUtil;
24
import com.intellij.ide.DataManager;
25
import com.intellij.notification.NotificationGroupManager;
26
import com.intellij.notification.NotificationType;
27
import com.intellij.openapi.actionSystem.AnAction;
28
import com.intellij.openapi.actionSystem.AnActionEvent;
29
import com.intellij.openapi.actionSystem.CommonDataKeys;
30
import com.intellij.openapi.actionSystem.DataContext;
31
import com.intellij.openapi.application.ApplicationInfo;
32
import com.intellij.openapi.application.ApplicationManager;
33
import com.intellij.openapi.diagnostic.*;
34
import com.intellij.openapi.progress.ProgressIndicator;
35
import com.intellij.openapi.progress.Task;
36
import com.intellij.openapi.project.Project;
37
import com.intellij.openapi.util.Pair;
38
import com.intellij.openapi.util.SystemInfo;
39
import com.intellij.openapi.util.text.StringUtil;
40
import com.intellij.util.Consumer;
41
import com.intellij.util.containers.ContainerUtil;
42
import com.perl5.PerlBundle;
43
import com.perl5.lang.perl.idea.actions.PerlDumbAwareAction;
44
import com.perl5.lang.perl.util.PerlPluginUtil;
45
import org.apache.http.Consts;
46
import org.apache.http.client.entity.EntityBuilder;
47
import org.apache.http.client.methods.CloseableHttpResponse;
48
import org.apache.http.client.methods.HttpDelete;
49
import org.apache.http.client.methods.HttpPost;
50
import org.apache.http.entity.ContentType;
51
import org.apache.http.entity.mime.MultipartEntityBuilder;
52
import org.apache.http.impl.client.CloseableHttpClient;
53
import org.apache.http.impl.client.HttpClients;
54
import org.apache.http.util.EntityUtils;
55
import org.jetbrains.annotations.*;
56

57
import java.awt.*;
58
import java.io.IOException;
59
import java.util.ArrayList;
60
import java.util.List;
61

62
import static com.intellij.openapi.diagnostic.SubmittedReportInfo.SubmissionStatus.FAILED;
63
import static com.intellij.openapi.diagnostic.SubmittedReportInfo.SubmissionStatus.NEW_ISSUE;
64
import static com.intellij.openapi.util.text.StringUtil.isEmpty;
65
import static com.perl5.errorHandler.YoutrackApi.YoutrackIssue;
66
import static com.perl5.errorHandler.YoutrackApi.YoutrackIssueResponse;
67

68
public class YoutrackErrorHandler extends ErrorReportSubmitter {
1✔
69
  private static final Logger LOGGER = Logger.getInstance(YoutrackErrorHandler.class);
1✔
70
  private static final @NonNls String SERVER_URL = "https://camelcade.myjetbrains.com/youtrack";
71
  private static final String SERVER_REST_URL = SERVER_URL + "/api";
72
  private static final String ISSUES_REST_URL = SERVER_REST_URL + "/issues";
73
  private static final String SERVER_ISSUE_URL = ISSUES_REST_URL + "?fields=idReadable,id";
74
  @VisibleForTesting
75
  public static final String YOUTRACK_PROPERTY_KEY = "youtrack.token";
76
  public static final String YOUTRACK_PROPERTY_VALUE = System.getProperty(YOUTRACK_PROPERTY_KEY);
1✔
77
  private static final String ADMIN_TOKEN = "Bearer " + YOUTRACK_PROPERTY_VALUE;
1✔
78
  private static final String ACCESS_TOKEN = "Bearer perm-YXV0b3JlcG9ydGVy.NjEtMjc=.yx1WGp5YfO5cMymM5kmT4kR31HiOck";
79

80
  @Override
81
  public @NotNull String getReportActionText() {
82
    return PerlBundle.message("perl.issue.report");
×
83
  }
84

85
  @Override
86
  public boolean submit(IdeaLoggingEvent @NotNull [] events,
87
                        @Nullable String additionalInfo,
88
                        @NotNull Component parentComponent,
89
                        @NotNull Consumer<? super SubmittedReportInfo> consumer) {
90
    final DataContext dataContext = DataManager.getInstance().getDataContext(parentComponent);
×
91
    final Project project = CommonDataKeys.PROJECT.getData(dataContext);
×
92

93
    Task.Backgroundable task = new Task.Backgroundable(project, DiagnosticBundle.message("title.submitting.error.report")) {
×
94
      @Override
95
      public void run(@NotNull ProgressIndicator indicator) {
96
        consumer.consume(doSubmit(events, additionalInfo, project).first);
×
97
      }
×
98
    };
99
    task.queue();
×
100
    return true;
×
101
  }
102

103
  @VisibleForTesting
104
  public @NotNull Pair<SubmittedReportInfo, YoutrackIssueResponse> doSubmit(IdeaLoggingEvent @NotNull [] ideaLoggingEvents,
105
                                                                            @Nullable String addInfo,
106
                                                                            @Nullable Project project) {
107
    final IdeaLoggingEvent ideaLoggingEvent = ideaLoggingEvents[0];
1✔
108
    final String throwableText = ideaLoggingEvent.getThrowableText();
1✔
109
    String description = throwableText.substring(0, Math.min(80, throwableText.length()));
1✔
110

111
    StringBuilder descBuilder = new StringBuilder();
1✔
112

113
    descBuilder.append("Build: ").append(ApplicationInfo.getInstance().getBuild()).append('\n');
1✔
114
    descBuilder.append("OS: ").append(SystemInfo.OS_NAME).append(" ").append(SystemInfo.OS_ARCH).append(" ").append(SystemInfo.OS_VERSION)
1✔
115
      .append('\n');
1✔
116
    descBuilder.append("Java Vendor: ").append(SystemInfo.JAVA_VENDOR).append('\n');
1✔
117
    descBuilder.append("Java Version: ").append(SystemInfo.JAVA_VERSION).append('\n');
1✔
118
    descBuilder.append("Java Runtime Version: ").append(SystemInfo.JAVA_RUNTIME_VERSION).append('\n');
1✔
119
    descBuilder.append("Perl Plugin Version: ").append(PerlPluginUtil.getPluginVersion()).append('\n');
1✔
120
    descBuilder.append("Description: ").append(StringUtil.notNullize(addInfo, "<none>"));
1✔
121

122
    List<Attachment> attachments = new ArrayList<>();
1✔
123
    for (IdeaLoggingEvent e : ideaLoggingEvents) {
1✔
124
      descBuilder
1✔
125
        .append("\n").append("Message: ").append(StringUtil.notNullize(e.getMessage(), "none"))
1✔
126
        .append("\n").append("```\n").append(e.getThrowableText().trim())
1✔
127
        .append("\n```")
1✔
128
      ;
129

130
      Throwable throwable = e.getThrowable();
1✔
131

132
      while (throwable != null) {
1✔
133
        if (throwable instanceof ExceptionWithAttachments exceptionWithAttachments) {
1!
134
          ContainerUtil.addAll(attachments, exceptionWithAttachments.getAttachments());
1✔
135
        }
136
        throwable = throwable.getCause();
1✔
137
      }
138
    }
139

140
    var issueResponse = submit(description, descBuilder.toString(), attachments);
1✔
141
    LOGGER.info("Error submitted, response: " + issueResponse);
1✔
142
    if (issueResponse == null) {
1!
143
      return Pair.create(new SubmittedReportInfo(SERVER_ISSUE_URL, "", FAILED), null);
×
144
    }
145
    var issueNumber = issueResponse.idReadable;
1✔
146

147
    final SubmittedReportInfo reportInfo = new SubmittedReportInfo(SERVER_URL + "/issue/" + issueNumber, issueNumber, NEW_ISSUE);
1✔
148

149
    popupResultInfo(reportInfo, project);
1✔
150

151
    return Pair.create(reportInfo, issueResponse);
1!
152
  }
153

154
  /**
155
   * @return human-readable issue number or null if failed to create one
156
   */
157
  private @Nullable YoutrackIssueResponse submit(@Nullable String desc,
158
                                                 @NotNull String body,
159
                                                 @NotNull List<Attachment> attachments) {
160
    if (isEmpty(desc)) {
1!
161
      LOGGER.warn("Won't submit empty issue");
×
162
      return null;
×
163
    }
164

165
    var issueResponse = createIssue(desc, body);
1✔
166
    if (issueResponse == null) {
1!
167
      return null;
×
168
    }
169
    attachFiles(issueResponse, attachments);
1✔
170

171
    return issueResponse;
1✔
172
  }
173

174
  private void attachFiles(@NotNull YoutrackIssueResponse issueResponse, @NotNull List<Attachment> attachments) {
175
    if (attachments.isEmpty()) {
1!
176
      return;
×
177
    }
178

179
    MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
1✔
180

181
    ContentType contentType = ContentType.create("text/plain", Consts.UTF_8);
1✔
182
    for (Attachment it : attachments) {
1✔
183
      entityBuilder.addBinaryBody("attachments[]", it.getBytes(), contentType, it.getName());
1✔
184
    }
1✔
185

186
    try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
1✔
187
      HttpPost issuePost = new HttpPost(String.join("/", ISSUES_REST_URL, issueResponse.id, "attachments"));
1✔
188
      issuePost.setEntity(entityBuilder.build());
1✔
189
      issuePost.addHeader("Authorization", ACCESS_TOKEN);
1✔
190

191
      CloseableHttpResponse response;
192
      try {
193
        response = httpClient.execute(issuePost);
1✔
194
      }
195
      catch (IOException ex) {
×
196
        LOGGER.warn("Error attaching files to the issue: " + ex.getMessage() + "; issue id: " + issueResponse.idReadable);
×
197
        return;
×
198
      }
1✔
199

200
      var statusLine = response.getStatusLine();
1✔
201
      var responsePayload = EntityUtils.toString(response.getEntity());
1✔
202
      if (statusLine.getStatusCode() != 200) {
1!
203
        LOGGER.warn("Error attaching files: status=" + statusLine +
×
204
                    "; response: " + responsePayload +
205
                    "; issue id: " + issueResponse.idReadable
206
        );
207
      }
208
      else {
209
        issueResponse.attachmentsAdded = attachments.size();
1✔
210
      }
211
    }
×
212
    catch (IOException e) {
×
213
      LOGGER.warn(e.getMessage());
×
214
    }
1✔
215
  }
1✔
216

217
  @TestOnly
218
  public CloseableHttpResponse deleteIssue(@NotNull YoutrackIssueResponse issueResponse) throws IOException {
219
    try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
1✔
220
      var deleteRequest = new HttpDelete(String.join("/", ISSUES_REST_URL, issueResponse.id));
1✔
221
      deleteRequest.addHeader("Authorization", ADMIN_TOKEN);
1✔
222
      return httpClient.execute(deleteRequest);
1✔
223
    }
224
    catch (IOException e) {
×
225
      throw new IOException(e);
×
226
    }
227
  }
228

229
  @TestOnly
230
  public static boolean hasAdminToken() {
231
    return StringUtil.isNotEmpty(ADMIN_TOKEN);
1✔
232
  }
233

234
  private @Nullable YoutrackIssueResponse createIssue(@NotNull String desc, @NotNull String body) {
235
    try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
1✔
236
      // posting an issue
237
      var issue = new YoutrackIssue(
1✔
238
        desc.replaceAll("[\r\n]", ""),
1✔
239
        body
240
      );
241

242
      var gson = new Gson();
1✔
243
      var requestContent = gson.toJson(issue);
1✔
244

245
      HttpPost issuePost = new HttpPost(SERVER_ISSUE_URL);
1✔
246
      issuePost.setEntity(EntityBuilder.create()
1✔
247
                            .setContentType(ContentType.create("application/json", Consts.UTF_8))
1✔
248
                            .setText(requestContent).build());
1✔
249
      issuePost.addHeader("Authorization", ACCESS_TOKEN);
1✔
250

251
      CloseableHttpResponse response;
252
      try {
253
        response = httpClient.execute(issuePost);
1✔
254
      }
255
      catch (IOException ex) {
×
256
        LOGGER.warn("Error posting an issue: " + ex.getMessage() + "; request: " + requestContent);
×
257
        return null;
×
258
      }
1✔
259

260
      var statusLine = response.getStatusLine();
1✔
261
      var responsePayload = EntityUtils.toString(response.getEntity());
1✔
262
      if (statusLine.getStatusCode() != 200) {
1!
263
        LOGGER.warn("Error submitting report: status=" + statusLine +
×
264
                    "; response: " + responsePayload +
265
                    "; request: " + requestContent
266
        );
267
        return null;
×
268
      }
269

270
      try {
271
        var issueResponse = gson.fromJson(responsePayload, YoutrackIssueResponse.class);
1✔
272
        issueResponse.issue = issue;
1✔
273
        return issueResponse;
1✔
274
      }
275
      catch (JsonSyntaxException e) {
×
276
        LOGGER.warn("Error decoding server response: " + responsePayload + "; request: " + requestContent);
×
277
      }
278
    }
1!
279
    catch (IOException e) {
×
280
      LOGGER.warn(e);
×
281
    }
×
282
    return null;
×
283
  }
284

285
  private static void popupResultInfo(@NotNull SubmittedReportInfo reportInfo, final @Nullable Project project) {
286
    ApplicationManager.getApplication().invokeLater(() -> {
1✔
287
      StringBuilder text = new StringBuilder("<html>");
1✔
288
      var urlOpener = appendSubmissionInformationAndGetAction(reportInfo, text);
1✔
289
      final SubmittedReportInfo.SubmissionStatus status = reportInfo.getStatus();
1✔
290

291
      var notificationTitle = DiagnosticBundle.message("error.report.submitted");
1✔
292
      if (status == SubmittedReportInfo.SubmissionStatus.NEW_ISSUE) {
1!
293
        text.append(DiagnosticBundle.message("error.report.gratitude"));
1✔
294
      }
295
      else if (status == SubmittedReportInfo.SubmissionStatus.DUPLICATE) {
×
296
        text.append("Possible duplicate report");
×
297
      }
298
      text.append("</html>");
1✔
299
      NotificationType type;
300
      if (status == SubmittedReportInfo.SubmissionStatus.FAILED) {
1!
301
        //noinspection DialogTitleCapitalization
302
        notificationTitle = DiagnosticBundle.message("error.report.failed.title");
×
UNCOV
303
        type = NotificationType.ERROR;
×
304
      }
305
      else if (status == SubmittedReportInfo.SubmissionStatus.DUPLICATE) {
1!
UNCOV
306
        type = NotificationType.WARNING;
×
307
      }
308
      else {
309
        type = NotificationType.INFORMATION;
1✔
310
      }
311
      @NonNls var notificationText = text.toString();
1✔
312
      var notification = NotificationGroupManager.getInstance()
1✔
313
        .getNotificationGroup("Error Report")
1✔
314
        .createNotification(notificationTitle, notificationText, type);
1✔
315
      if (urlOpener != null) {
1!
316
        notification.addAction(new PerlDumbAwareAction(urlOpener.getTemplateText()) {
1✔
317
          @Override
318
          public void actionPerformed(@NotNull AnActionEvent e) {
319
            urlOpener.actionPerformed(e);
×
320
            notification.expire();
×
UNCOV
321
          }
×
322
        });
323
      }
324
      notification.notify(project);
1✔
325
    });
1✔
326
  }
1✔
327

328
  /**
329
   * Inspired by {@link IdeErrorsDialog#appendSubmissionInformation(SubmittedReportInfo, StringBuilder)}
330
   *
331
   * @return action to open url in browser if applicable
332
   * @implSpec the main difference is that method does not append url, but returns opening action instead.
333
   */
334
  private static @Nullable AnAction appendSubmissionInformationAndGetAction(@NotNull SubmittedReportInfo info, @NotNull StringBuilder out) {
335
    @NonNls var linkText = info.getLinkText();
1✔
336
    var linkUrl = info.getURL();
1✔
337
    if (info.getStatus() == SubmittedReportInfo.SubmissionStatus.FAILED) {
1!
338
      out.append(linkText != null ? DiagnosticBundle.message("error.list.message.submission.failed.details", linkText)
×
UNCOV
339
                                  : DiagnosticBundle.message("error.list.message.submission.failed"));
×
340
    }
341
    else if (linkUrl != null && linkText != null) {
1!
342
      return new PerlDumbAwareAction(linkText) {
1✔
343
        @Override
344
        public void actionPerformed(@NotNull AnActionEvent e) {
345
          BrowserUtil.browse(linkUrl);
×
UNCOV
346
        }
×
347
      };
348
    }
UNCOV
349
    return null;
×
350
  }
351
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc