Skip to content

Commit 40f3206

Browse files
bobbyg603claude
andauthored
feat: user feedback (#8)
* Add postFeedback API for user feedback submission Adds FeedbackClient implementing the 3-step presigned URL upload flow (getCrashUploadUrl -> PUT to S3 -> commitS3CrashUpload) with crashTypeId=36. Creates feedback.json with title and description, zips it, and uploads. Exposes postFeedback and postFeedbackBlocking static methods on BugSplat. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add file attachment support to postFeedback Adds List<File> attachments parameter to postFeedback and postFeedbackBlocking. Files are included in the zip alongside feedback.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add appKey parameter to postFeedback Passes appKey through to the commit API call for restricted databases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace 3-step presigned URL upload with direct POST to /post/feedback/ Simplifies FeedbackClient to a single multipart form POST. Adds a feedback dialog to the example app and documents the API in README. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add "Include Logs" checkbox to feedback dialog When checked, generates a sample log file and attaches it to the feedback report via the postFeedbackBlocking attachments overload. Checkbox is checked by default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix FeedbackClient connection leaks, missing timeouts, and UTF-8 encoding - Add 30s connect/read timeouts to prevent hanging on stalled networks - Wrap HttpURLConnection in try/finally to ensure disconnect on all paths - Close output stream via try-with-resources - Replace DataOutputStream.writeBytes with UTF-8 byte writes to prevent corruption of non-ASCII characters in multipart form fields Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d83c7f5 commit 40f3206

8 files changed

Lines changed: 439 additions & 14 deletions

File tree

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,56 @@ When integrating BugSplat into your Android application, it's crucial to ensure
327327

328328
These configurations ensure that the BugSplat native libraries are properly included in your app and can function correctly to capture and report native crashes.
329329

330+
## User Feedback 💬
331+
332+
BugSplat supports collecting non-crashing user feedback such as bug reports and feature requests. Feedback reports appear in BugSplat alongside crash reports with the "User Feedback" type.
333+
334+
### Posting Feedback
335+
336+
Use `BugSplat.postFeedback` to submit feedback asynchronously, or `BugSplat.postFeedbackBlocking` for synchronous submission:
337+
338+
```java
339+
// Async (returns immediately, runs on background thread)
340+
BugSplat.postFeedback(
341+
"fred", // database
342+
"my-android-crasher", // application
343+
"1.0.0", // version
344+
"Login button broken", // title (required)
345+
"Nothing happens on tap", // description
346+
"Jane", // user
347+
"jane@example.com", // email
348+
null // appKey
349+
);
350+
351+
// Blocking (returns true on success)
352+
boolean success = BugSplat.postFeedbackBlocking(
353+
"fred", "my-android-crasher", "1.0.0",
354+
"Login button broken", "Nothing happens on tap",
355+
"Jane", "jane@example.com", null
356+
);
357+
```
358+
359+
### File Attachments
360+
361+
Pass a list of `File` objects to attach files to the feedback report:
362+
363+
```java
364+
List<File> attachments = new ArrayList<>();
365+
attachments.add(new File(getFilesDir(), "screenshot.png"));
366+
attachments.add(new File(getFilesDir(), "app.log"));
367+
368+
BugSplat.postFeedback(
369+
"fred", "my-android-crasher", "1.0.0",
370+
"Login button broken", "Nothing happens on tap",
371+
"Jane", "jane@example.com", null,
372+
attachments
373+
);
374+
```
375+
376+
### Example Feedback Dialog
377+
378+
The example app includes a simple feedback dialog using Android's `AlertDialog`. See [`MainActivity.java`](example/src/main/java/com/bugsplat/example/MainActivity.java) for the implementation. The dialog collects a subject and optional description, then posts feedback using `BugSplat.postFeedbackBlocking` on a background thread.
379+
330380
## Sample Applications 🧑‍🏫
331381

332382
### Example App
@@ -342,6 +392,7 @@ To run the example app:
342392
The example app demonstrates:
343393
- Automatically initializing the BugSplat SDK at app startup
344394
- Triggering a crash for testing purposes
395+
- Submitting user feedback via a dialog
345396
- Handling errors during initialization
346397

347398
For more information, see the [Example App README](example/README.md).

app/src/main/java/com/bugsplat/android/BugSplat.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import android.app.Activity;
44
import android.content.Context;
5+
import java.io.File;
6+
import java.util.List;
57
import java.util.Map;
68

79
/**
@@ -136,4 +138,88 @@ public static void uploadSymbolsBlocking(Context context, String database, Strin
136138
SymbolUploader uploader = new SymbolUploader(database, application, version, clientId, clientSecret);
137139
uploader.uploadSymbolsBlocking(context, nativeLibsDir);
138140
}
141+
142+
/**
143+
* Post user feedback to BugSplat.
144+
* This runs on a background thread and returns immediately.
145+
*
146+
* @param database The BugSplat database name
147+
* @param application The application name
148+
* @param version The application version
149+
* @param title The feedback title (becomes the stack key for grouping)
150+
* @param description Additional feedback context
151+
* @param user The user's name or id
152+
* @param email The user's email
153+
* @param appKey The application key for authentication
154+
*/
155+
public static void postFeedback(String database, String application, String version,
156+
String title, String description, String user, String email,
157+
String appKey) {
158+
postFeedback(database, application, version, title, description, user, email, appKey, null);
159+
}
160+
161+
/**
162+
* Post user feedback to BugSplat with file attachments.
163+
* This runs on a background thread and returns immediately.
164+
*
165+
* @param database The BugSplat database name
166+
* @param application The application name
167+
* @param version The application version
168+
* @param title The feedback title (becomes the stack key for grouping)
169+
* @param description Additional feedback context
170+
* @param user The user's name or id
171+
* @param email The user's email
172+
* @param appKey The application key for authentication
173+
* @param attachments List of files to attach to the feedback report, or null for none
174+
*/
175+
public static void postFeedback(String database, String application, String version,
176+
String title, String description, String user, String email,
177+
String appKey, List<File> attachments) {
178+
new Thread(() -> {
179+
FeedbackClient client = new FeedbackClient(database, application, version);
180+
client.postFeedback(title, description, user, email, appKey, attachments);
181+
}).start();
182+
}
183+
184+
/**
185+
* Post user feedback to BugSplat.
186+
* This blocks until the upload is complete.
187+
*
188+
* @param database The BugSplat database name
189+
* @param application The application name
190+
* @param version The application version
191+
* @param title The feedback title (becomes the stack key for grouping)
192+
* @param description Additional feedback context
193+
* @param user The user's name or id
194+
* @param email The user's email
195+
* @param appKey The application key for authentication
196+
* @return true if feedback was posted successfully
197+
*/
198+
public static boolean postFeedbackBlocking(String database, String application, String version,
199+
String title, String description, String user, String email,
200+
String appKey) {
201+
return postFeedbackBlocking(database, application, version, title, description, user, email, appKey, null);
202+
}
203+
204+
/**
205+
* Post user feedback to BugSplat with file attachments.
206+
* This blocks until the upload is complete.
207+
*
208+
* @param database The BugSplat database name
209+
* @param application The application name
210+
* @param version The application version
211+
* @param title The feedback title (becomes the stack key for grouping)
212+
* @param description Additional feedback context
213+
* @param user The user's name or id
214+
* @param email The user's email
215+
* @param appKey The application key for authentication
216+
* @param attachments List of files to attach to the feedback report, or null for none
217+
* @return true if feedback was posted successfully
218+
*/
219+
public static boolean postFeedbackBlocking(String database, String application, String version,
220+
String title, String description, String user, String email,
221+
String appKey, List<File> attachments) {
222+
FeedbackClient client = new FeedbackClient(database, application, version);
223+
return client.postFeedback(title, description, user, email, appKey, attachments);
224+
}
139225
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package com.bugsplat.android;
2+
3+
import android.util.Log;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.DataOutputStream;
7+
import java.io.File;
8+
import java.io.FileInputStream;
9+
import java.io.OutputStream;
10+
import java.net.HttpURLConnection;
11+
import java.net.URL;
12+
import java.nio.charset.StandardCharsets;
13+
import java.util.List;
14+
import java.util.UUID;
15+
16+
class FeedbackClient {
17+
private static final String TAG = "BugSplat";
18+
19+
private final String database;
20+
private final String application;
21+
private final String version;
22+
23+
FeedbackClient(String database, String application, String version) {
24+
this.database = database;
25+
this.application = application;
26+
this.version = version;
27+
}
28+
29+
boolean postFeedback(String title, String description, String user, String email, String appKey) {
30+
return postFeedback(title, description, user, email, appKey, null);
31+
}
32+
33+
boolean postFeedback(String title, String description, String user, String email, String appKey, List<File> attachments) {
34+
try {
35+
String url = "https://" + database + ".bugsplat.com/post/feedback/";
36+
String boundary = UUID.randomUUID().toString();
37+
38+
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
39+
try {
40+
conn.setRequestMethod("POST");
41+
conn.setDoOutput(true);
42+
conn.setConnectTimeout(30000);
43+
conn.setReadTimeout(30000);
44+
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
45+
46+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
47+
DataOutputStream dos = new DataOutputStream(baos);
48+
49+
// Required fields
50+
writeFormField(dos, boundary, "database", database);
51+
writeFormField(dos, boundary, "appName", application);
52+
writeFormField(dos, boundary, "appVersion", version);
53+
writeFormField(dos, boundary, "title", title);
54+
55+
// Optional fields
56+
if (description != null && !description.isEmpty()) {
57+
writeFormField(dos, boundary, "description", description);
58+
}
59+
if (user != null && !user.isEmpty()) {
60+
writeFormField(dos, boundary, "user", user);
61+
}
62+
if (email != null && !email.isEmpty()) {
63+
writeFormField(dos, boundary, "email", email);
64+
}
65+
if (appKey != null && !appKey.isEmpty()) {
66+
writeFormField(dos, boundary, "appKey", appKey);
67+
}
68+
69+
// File attachments
70+
if (attachments != null) {
71+
for (File file : attachments) {
72+
if (file == null || !file.exists() || !file.isFile()) {
73+
Log.w(TAG, "Skipping invalid attachment: " + file);
74+
continue;
75+
}
76+
writeFileField(dos, boundary, file.getName(), file);
77+
Log.d(TAG, "Attached file " + file.getName() + " (" + file.length() + " bytes)");
78+
}
79+
}
80+
81+
dos.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
82+
dos.flush();
83+
84+
byte[] payload = baos.toByteArray();
85+
conn.setRequestProperty("Content-Length", String.valueOf(payload.length));
86+
try (OutputStream out = conn.getOutputStream()) {
87+
out.write(payload);
88+
out.flush();
89+
}
90+
91+
int status = conn.getResponseCode();
92+
93+
if (status >= 200 && status < 300) {
94+
Log.i(TAG, "Feedback posted successfully (HTTP " + status + ")");
95+
return true;
96+
} else {
97+
Log.e(TAG, "Failed to post feedback (HTTP " + status + ")");
98+
return false;
99+
}
100+
} finally {
101+
conn.disconnect();
102+
}
103+
104+
} catch (Exception e) {
105+
Log.e(TAG, "Failed to post feedback", e);
106+
return false;
107+
}
108+
}
109+
110+
private void writeFormField(DataOutputStream dos, String boundary, String name, String value) throws Exception {
111+
dos.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
112+
dos.write(("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n").getBytes(StandardCharsets.UTF_8));
113+
dos.write(value.getBytes(StandardCharsets.UTF_8));
114+
dos.write("\r\n".getBytes(StandardCharsets.UTF_8));
115+
}
116+
117+
private void writeFileField(DataOutputStream dos, String boundary, String fieldName, File file) throws Exception {
118+
dos.write(("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8));
119+
dos.write(("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + file.getName() + "\"\r\n").getBytes(StandardCharsets.UTF_8));
120+
dos.write("Content-Type: application/octet-stream\r\n\r\n".getBytes(StandardCharsets.UTF_8));
121+
122+
byte[] buffer = new byte[4096];
123+
try (FileInputStream fis = new FileInputStream(file)) {
124+
int bytesRead;
125+
while ((bytesRead = fis.read(buffer)) != -1) {
126+
dos.write(buffer, 0, bytesRead);
127+
}
128+
}
129+
dos.write("\r\n".getBytes(StandardCharsets.UTF_8));
130+
}
131+
}

0 commit comments

Comments
 (0)