@@ -16,8 +16,6 @@ import com.demonwav.mcdev.util.fromJson
1616import com.google.gson.Gson
1717import com.intellij.ide.plugins.PluginManagerCore
1818import com.intellij.openapi.diagnostic.Attachment
19- import com.intellij.util.io.readCharSequence
20- import java.io.InputStreamReader
2119import java.net.HttpURLConnection
2220import java.nio.ByteBuffer
2321import java.nio.charset.CodingErrorAction
@@ -29,7 +27,8 @@ object AnonymousFeedback {
2927
3028 data class FeedbackData (val url : String , val token : Int , val isDuplicate : Boolean )
3129
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"
3332
3433 fun sendFeedback (
3534 factory : HttpConnectionFactory ,
@@ -39,8 +38,8 @@ object AnonymousFeedback {
3938 val duplicateId = findDuplicateIssue(envDetails, factory)
4039 if (duplicateId != null ) {
4140 // 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 )
4443 return FeedbackData (commentUrl, duplicateId, true )
4544 }
4645
@@ -67,21 +66,31 @@ object AnonymousFeedback {
6766 }
6867
6968 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)
7273 }
7374
7475 val sb = StringBuilder ()
7576
76- if (! errorDescription.isEmpty ()) {
77+ if (errorDescription.isNotEmpty ()) {
7778 sb.append(errorDescription).append(" \n\n " )
7879 }
7980
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+ )
8290 }
91+ sb.append(" </table></td></tr></table>\n " )
8392
84- sb.append(" \n ``` \n " ).append(stackTrace).append(" \n ``` \n " )
93+ sb.append(" \n <pre> \n " ).append(stackTrace).append(" \n </pre> \n " )
8594 sb.append(" \n ```\n " ).append(errorMessage).append(" \n ```\n " )
8695
8796 if (attachments.isNotEmpty()) {
@@ -118,7 +127,7 @@ object AnonymousFeedback {
118127 }
119128
120129 private fun sendFeedback (factory : HttpConnectionFactory , payload : ByteArray ): Pair <String , Int > {
121- val connection = getConnection(factory, url )
130+ val connection = getConnection(factory, authedUrl )
122131 connection.connect()
123132 val json = executeCall(connection, payload)
124133 return json[" html_url" ] as String to (json[" number" ] as Double ).toInt()
@@ -131,17 +140,15 @@ object AnonymousFeedback {
131140 return connection
132141 }
133142
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"
141145
142146 private const val packagePrefix = " \t at com.demonwav.mcdev"
143147
144148 private fun findDuplicateIssue (envDetails : LinkedHashMap <String , String ?>, factory : HttpConnectionFactory ): Int? {
149+ val numberRegex = Regex (" \\ d+" )
150+ val newLineRegex = Regex (" [\r\n ]+" )
151+
145152 val stack = envDetails[" error.stacktrace" ]?.replace(numberRegex, " " ) ? : return null
146153
147154 val stackMcdevParts = stack.lineSequence()
@@ -177,55 +184,50 @@ object AnonymousFeedback {
177184 }
178185
179186 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
189188
189+ var next: String? = url
190190 val list = mutableListOf<Map <* , * >>()
191- var data = connection.inputStream.reader().use(InputStreamReader ::readCharSequence).toString()
192191
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)
195197
196- var link = connection.getHeaderField(" Link" )
197- connection.disconnect()
198+ connection.connect()
198199
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+ }
204205
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
210213
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()
214215
215- data = connection.inputStream.reader(charset).readText()
216+ val response = Gson ().fromJson<List <Map <* , * >>>(data)
217+ list.addAll(response)
216218
217- response = Gson ().fromJson(data)
218- list.addAll(response)
219+ val link = connection.getHeaderField(" Link" )
219220
220- link = connection.getHeaderField(" Link" )
221- connection.disconnect()
222- next = getNextLink(link)
221+ next = getNextLink(link, useAuthed)
222+ } finally {
223+ connection.disconnect()
224+ }
223225 }
224226
225227 return list
226228 }
227229
228- private fun getNextLink (linkHeader : String? ): String? {
230+ private fun getNextLink (linkHeader : String? , useAuthed : Boolean ): String? {
229231 if (linkHeader == null ) {
230232 return null
231233 }
@@ -239,14 +241,31 @@ object AnonymousFeedback {
239241 if (parts.isEmpty()) {
240242 continue
241243 }
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)
243250 }
244251
245252 return null
246253 }
247254
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+
248267 private fun sendCommentOnDuplicateIssue (id : Int , factory : HttpConnectionFactory , payload : ByteArray ): String {
249- val commentUrl = " $url /$id /comments"
268+ val commentUrl = " $authedUrl /$id /comments"
250269 val connection = getConnection(factory, commentUrl)
251270 val json = executeCall(connection, payload)
252271 return json[" html_url" ] as String
@@ -281,6 +300,74 @@ object AnonymousFeedback {
281300 return connection
282301 }
283302
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+
284371 private val userAgent by lazy {
285372 var agent = " Minecraft Development IntelliJ IDEA plugin"
286373
0 commit comments