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

Camelcade / Perl5-IDEA / #525521921

28 Aug 2026 10:55AM UTC coverage: 76.07% (+0.001%) from 76.069%
#525521921

push

github

hurricup
#3198 test data update for the debugger

14750 of 22558 branches covered (65.39%)

Branch coverage included in aggregate %.

31109 of 37727 relevant lines covered (82.46%)

0.82 hits per line

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

62.5
/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.NjEtMjg=.BWowi0S8c2EeRgJx3kfsdPkIScJyFC";
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
      if( it == null){
1 !
184
        continue;
×
185
      }
186
      entityBuilder.addBinaryBody("attachments[]", it.getBytes(), contentType, it.getName());
1 ✔
187
    }
1 ✔
188

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

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

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

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

232
  @TestOnly
233
  public static boolean hasAdminToken() {
234
    return StringUtil.isNotEmpty(ADMIN_TOKEN);
1 ✔
235
  }
236

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

245
      var gson = new Gson();
1 ✔
246
      var requestContent = gson.toJson(issue);
1 ✔
247

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

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

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

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

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

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

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

© 2026 Coveralls, Inc