@@ -16,8 +16,6 @@ import com.demonwav.mcdev.util.fromJson
16
16
import com.google.gson.Gson
17
17
import com.intellij.ide.plugins.PluginManagerCore
18
18
import com.intellij.openapi.diagnostic.Attachment
19
- import com.intellij.util.io.readCharSequence
20
- import java.io.InputStreamReader
21
19
import java.net.HttpURLConnection
22
20
import java.nio.ByteBuffer
23
21
import java.nio.charset.CodingErrorAction
@@ -29,7 +27,8 @@ object AnonymousFeedback {
29
27
30
28
data class FeedbackData (val url : String , val token : Int , val isDuplicate : Boolean )
31
29
32
- const val url = " https://www.denwav.dev/errorReport"
30
+ private const val authedUrl = " https://www.denwav.dev/errorReport"
31
+ private const val baseUrl = " https://api.github.com/repos/minecraft-dev/MinecraftDev/issues"
33
32
34
33
fun sendFeedback (
35
34
factory : HttpConnectionFactory ,
@@ -39,8 +38,8 @@ object AnonymousFeedback {
39
38
val duplicateId = findDuplicateIssue(envDetails, factory)
40
39
if (duplicateId != null ) {
41
40
// This is a duplicate
42
- val commentUrl =
43
- sendCommentOnDuplicateIssue(duplicateId, factory, convertToGitHubIssueFormat(envDetails, attachments) )
41
+ val issueContent = convertToGitHubIssueFormat(envDetails, attachments)
42
+ val commentUrl = sendCommentOnDuplicateIssue(duplicateId, factory, issueContent )
44
43
return FeedbackData (commentUrl, duplicateId, true )
45
44
}
46
45
@@ -67,21 +66,31 @@ object AnonymousFeedback {
67
66
}
68
67
69
68
var stackTrace = body.remove(" error.stacktrace" )
70
- if (stackTrace.isNullOrEmpty()) {
71
- stackTrace = " no stacktrace"
69
+ stackTrace = if (stackTrace.isNullOrEmpty()) {
70
+ " no stacktrace"
71
+ } else {
72
+ linkStacktrace(stackTrace)
72
73
}
73
74
74
75
val sb = StringBuilder ()
75
76
76
- if (! errorDescription.isEmpty ()) {
77
+ if (errorDescription.isNotEmpty ()) {
77
78
sb.append(errorDescription).append(" \n\n " )
78
79
}
79
80
80
- for ((key, value) in body) {
81
- sb.append(key).append(" : " ).append(value).append(" \n " )
81
+ sb.append(" <table><tr><td><table>\n " )
82
+ for ((i, entry) in body.entries.withIndex()) {
83
+ if (i == 6 ) {
84
+ sb.append(" </table></td><td><table>\n " )
85
+ }
86
+ val (key, value) = entry
87
+ sb.append(" <tr><td><b>" ).append(key).append(" </b></td><td><code>" ).append(value).append(
88
+ " </code></td></tr>\n "
89
+ )
82
90
}
91
+ sb.append(" </table></td></tr></table>\n " )
83
92
84
- sb.append(" \n ``` \n " ).append(stackTrace).append(" \n ``` \n " )
93
+ sb.append(" \n <pre> \n " ).append(stackTrace).append(" \n </pre> \n " )
85
94
sb.append(" \n ```\n " ).append(errorMessage).append(" \n ```\n " )
86
95
87
96
if (attachments.isNotEmpty()) {
@@ -118,7 +127,7 @@ object AnonymousFeedback {
118
127
}
119
128
120
129
private fun sendFeedback (factory : HttpConnectionFactory , payload : ByteArray ): Pair <String , Int > {
121
- val connection = getConnection(factory, url )
130
+ val connection = getConnection(factory, authedUrl )
122
131
connection.connect()
123
132
val json = executeCall(connection, payload)
124
133
return json[" html_url" ] as String to (json[" number" ] as Double ).toInt()
@@ -131,17 +140,15 @@ object AnonymousFeedback {
131
140
return connection
132
141
}
133
142
134
- private val numberRegex = Regex (" \\ d+" )
135
- private val newLineRegex = Regex (" [\r\n ]+" )
136
-
137
- private const val openIssueUrl = " https://api.github.com/repos/minecraft-dev/MinecraftDev/issues" +
138
- " ?state=open&creator=minecraft-dev-autoreporter&per_page=100"
139
- private const val closedIssueUrl = " https://api.github.com/repos/minecraft-dev/MinecraftDev/issues" +
140
- " ?state=closed&creator=minecraft-dev-autoreporter&per_page=100"
143
+ private const val openIssueUrl = " $baseUrl ?state=open&creator=minecraft-dev-autoreporter&per_page=100"
144
+ private const val closedIssueUrl = " $baseUrl ?state=closed&creator=minecraft-dev-autoreporter&per_page=100"
141
145
142
146
private const val packagePrefix = " \t at com.demonwav.mcdev"
143
147
144
148
private fun findDuplicateIssue (envDetails : LinkedHashMap <String , String ?>, factory : HttpConnectionFactory ): Int? {
149
+ val numberRegex = Regex (" \\ d+" )
150
+ val newLineRegex = Regex (" [\r\n ]+" )
151
+
145
152
val stack = envDetails[" error.stacktrace" ]?.replace(numberRegex, " " ) ? : return null
146
153
147
154
val stackMcdevParts = stack.lineSequence()
@@ -177,55 +184,50 @@ object AnonymousFeedback {
177
184
}
178
185
179
186
private fun getAllIssues (url : String , factory : HttpConnectionFactory ): List <Map <* , * >>? {
180
- var connection = connect(factory, url)
181
- connection.requestMethod = " GET"
182
- connection.setRequestProperty(" User-Agent" , userAgent)
183
-
184
- connection.connect()
185
- if (connection.responseCode != 200 ) {
186
- connection.disconnect()
187
- return null
188
- }
187
+ var useAuthed = false
189
188
189
+ var next: String? = url
190
190
val list = mutableListOf<Map <* , * >>()
191
- var data = connection.inputStream.reader().use(InputStreamReader ::readCharSequence).toString()
192
191
193
- var response = Gson ().fromJson<List <Map <* , * >>>(data)
194
- list.addAll(response)
192
+ while (next != null ) {
193
+ val connection: HttpURLConnection = connect(factory, next)
194
+ try {
195
+ connection.requestMethod = " GET"
196
+ connection.setRequestProperty(" User-Agent" , userAgent)
195
197
196
- var link = connection.getHeaderField(" Link" )
197
- connection.disconnect()
198
+ connection.connect()
198
199
199
- var next = getNextLink(link)
200
- while (next != null ) {
201
- connection = connect(factory, next)
202
- connection.requestMethod = " GET "
203
- connection.setRequestProperty( " User-Agent " , userAgent)
200
+ if (connection.responseCode == 403 && ! useAuthed) {
201
+ useAuthed = true
202
+ next = replaceWithAuth( next)
203
+ continue
204
+ }
204
205
205
- connection.connect()
206
- if (connection.responseCode != 200 ) {
207
- connection.disconnect()
208
- continue
209
- }
206
+ if (connection.responseCode != 200 ) {
207
+ return null
208
+ }
209
+
210
+ val charset = connection.getHeaderField(HttpHeaders .CONTENT_TYPE )?.let {
211
+ ContentType .parse(it).charset
212
+ } ? : Charsets .UTF_8
210
213
211
- val charset = connection.getHeaderField(HttpHeaders .CONTENT_TYPE )?.let {
212
- ContentType .parse(it).charset
213
- } ? : Charsets .UTF_8
214
+ val data = connection.inputStream.reader(charset).readText()
214
215
215
- data = connection.inputStream.reader(charset).readText()
216
+ val response = Gson ().fromJson<List <Map <* , * >>>(data)
217
+ list.addAll(response)
216
218
217
- response = Gson ().fromJson(data)
218
- list.addAll(response)
219
+ val link = connection.getHeaderField(" Link" )
219
220
220
- link = connection.getHeaderField(" Link" )
221
- connection.disconnect()
222
- next = getNextLink(link)
221
+ next = getNextLink(link, useAuthed)
222
+ } finally {
223
+ connection.disconnect()
224
+ }
223
225
}
224
226
225
227
return list
226
228
}
227
229
228
- private fun getNextLink (linkHeader : String? ): String? {
230
+ private fun getNextLink (linkHeader : String? , useAuthed : Boolean ): String? {
229
231
if (linkHeader == null ) {
230
232
return null
231
233
}
@@ -239,14 +241,31 @@ object AnonymousFeedback {
239
241
if (parts.isEmpty()) {
240
242
continue
241
243
}
242
- return parts[0 ].trim().removePrefix(" <" ).removeSuffix(" >" )
244
+ val nextUrl = parts[0 ].trim().removePrefix(" <" ).removeSuffix(" >" )
245
+ if (! useAuthed) {
246
+ return nextUrl
247
+ }
248
+
249
+ return replaceWithAuth(nextUrl)
243
250
}
244
251
245
252
return null
246
253
}
247
254
255
+ private fun replaceWithAuth (url : String ): String? {
256
+ // non-authed-API requests are rate limited at 60 / hour / IP
257
+ // authed requests have a rate limit of 5000 / hour / account
258
+ // We don't want to use the authed URL by default since all users would use the same rate limit
259
+ // but it's a good fallback when the non-authed API stops working.
260
+ val index = url.indexOf(' ?' )
261
+ if (index == - 1 ) {
262
+ return null
263
+ }
264
+ return authedUrl + url.substring(index)
265
+ }
266
+
248
267
private fun sendCommentOnDuplicateIssue (id : Int , factory : HttpConnectionFactory , payload : ByteArray ): String {
249
- val commentUrl = " $url /$id /comments"
268
+ val commentUrl = " $authedUrl /$id /comments"
250
269
val connection = getConnection(factory, commentUrl)
251
270
val json = executeCall(connection, payload)
252
271
return json[" html_url" ] as String
@@ -281,6 +300,74 @@ object AnonymousFeedback {
281
300
return connection
282
301
}
283
302
303
+ private fun linkStacktrace (stacktrace : String ): String {
304
+ val versionRegex = Regex (""" (?<intellijVersion>\d{4}\.\d)-(?<pluginVersion>\d+\.\d+\.\d+)""" )
305
+
306
+ val version = PluginUtil .pluginVersion
307
+ val match = versionRegex.matchEntire(version) ? : return stacktrace
308
+
309
+ val intellijVersion = match.groups[" intellijVersion" ]?.value ? : return stacktrace
310
+ val pluginVersion = match.groups[" pluginVersion" ]?.value ? : return stacktrace
311
+
312
+ val tag = " $pluginVersion -$intellijVersion "
313
+
314
+ // v stack element text v
315
+ // at com.demonwav.mcdev.facet.MinecraftFacet.shouldShowPluginIcon(MinecraftFacet.kt:185)
316
+ // prefix ^ class path ^ ^ file name ^ ^ ^ line number
317
+ val stackElementRegex = Regex (
318
+ """ (?<prefix>\s+at\s+)""" +
319
+ """ (?<stackElementText>""" +
320
+ """ (?<className>com\.demonwav\.mcdev(?:\.\p{javaJavaIdentifierStart}\p{javaJavaIdentifierPart}*)+)""" +
321
+ """ (?:\.\p{javaJavaIdentifierStart}\p{javaJavaIdentifierPart}*|<(?:cl)?init>)""" +
322
+ """ \((?<fileName>.*\.\w+):(?<lineNumber>\d+)\)""" +
323
+ """ )\s*"""
324
+ )
325
+
326
+ val baseTagUrl = " https://github.com/minecraft-dev/MinecraftDev/blob/$tag /src/main/kotlin/"
327
+
328
+ val sb = StringBuilder (stacktrace.length * 2 )
329
+
330
+ for (line in stacktrace.lineSequence()) {
331
+ val lineMatch = stackElementRegex.matchEntire(line)
332
+ if (lineMatch == null ) {
333
+ sb.append(line).append(' \n ' )
334
+ continue
335
+ }
336
+
337
+ val prefix = lineMatch.groups[" prefix" ]?.value
338
+ val className = lineMatch.groups[" className" ]?.value
339
+ val fileName = lineMatch.groups[" fileName" ]?.value
340
+ val lineNumber = lineMatch.groups[" lineNumber" ]?.value
341
+ val stackElementText = lineMatch.groups[" stackElementText" ]?.value
342
+
343
+ if (prefix == null || className == null || fileName == null ||
344
+ lineNumber == null || stackElementText == null
345
+ ) {
346
+ sb.append(line).append(' \n ' )
347
+ continue
348
+ }
349
+
350
+ val path = className.substringAfter(" com.demonwav.mcdev." )
351
+ .substringBeforeLast(' .' )
352
+ .replace(' .' , ' /' )
353
+ sb.apply {
354
+ append(prefix)
355
+ append(" <a href=\" " )
356
+ append(baseTagUrl)
357
+ append(path)
358
+ append(' /' )
359
+ append(fileName)
360
+ append(" #L" )
361
+ append(lineNumber)
362
+ append(" \" >" )
363
+ append(stackElementText)
364
+ append(" </a>\n " )
365
+ }
366
+ }
367
+
368
+ return sb.toString()
369
+ }
370
+
284
371
private val userAgent by lazy {
285
372
var agent = " Minecraft Development IntelliJ IDEA plugin"
286
373
0 commit comments