Skip to content

Commit ddd7084

Browse files
authored
feat: forward all post options through to bugsplat-js (#17)
1 parent 43f1ac3 commit ddd7084

14 files changed

Lines changed: 270 additions & 904 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,8 @@ build/
7070
plugin/build/
7171
plugin/tsconfig.tsbuildinfo
7272

73+
# Tarballs
74+
*.tgz
75+
7376
# Playwright
7477
.playwright-mcp/

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,58 @@ The fallback prop accepts a React node or a render function:
177177
</ErrorBoundary>
178178
```
179179

180+
### Collecting user input before posting
181+
182+
By default, `<ErrorBoundary>` posts to BugSplat the moment it catches an error. If you'd rather give the user a chance to describe what they were doing first — and bundle that into a single report instead of two — set `disablePost` on the boundary and post manually from your fallback:
183+
184+
`<ErrorBoundary>` is a class component, so hooks can't live directly inside its `fallback` render prop — extract the fallback into its own functional component:
185+
186+
```tsx
187+
import { useRef, useState } from 'react';
188+
import { ErrorBoundary, post, type FallbackProps } from '@bugsplat/expo';
189+
import { Text, TextInput, Button, View } from 'react-native';
190+
191+
function ErrorFallback({ error, componentStack, resetErrorBoundary }: FallbackProps) {
192+
const [description, setDescription] = useState('');
193+
const posted = useRef(false);
194+
195+
const submit = async () => {
196+
if (posted.current) return;
197+
posted.current = true;
198+
await post(error, {
199+
description,
200+
attributes: { route: 'tasks/123' },
201+
attachments: componentStack
202+
? [{
203+
filename: 'componentStack.txt',
204+
data: new Blob([componentStack], { type: 'text/plain' }),
205+
}]
206+
: undefined,
207+
});
208+
};
209+
210+
return (
211+
<View>
212+
<Text>Something went wrong: {error.message}</Text>
213+
<TextInput value={description} onChangeText={setDescription} />
214+
<Button title="Submit" onPress={submit} />
215+
<Button title="Dismiss" onPress={() => { submit(); resetErrorBoundary(); }} />
216+
</View>
217+
);
218+
}
219+
220+
<ErrorBoundary disablePost fallback={(props) => <ErrorFallback {...props} />}>
221+
<App />
222+
</ErrorBoundary>
223+
```
224+
225+
A few notes on this pattern:
226+
227+
- `post()` is **not** idempotent. The `useRef` guard is the consumer's responsibility — without it, a fast double-tap (or "Submit then Dismiss") would fire two reports. `useRef` updates synchronously, so it guards taps that land in the same render window; `useState` would not.
228+
- `componentStack` is wrapped in a `Blob`. This works cross-platform because `@bugsplat/expo` includes `expo-blob`, which polyfills the web-standard `Blob` API on native.
229+
- `attributes` becomes a queryable column in the BugSplat dashboard — useful for filtering crashes by route, feature flag, build channel, etc.
230+
- If posting fails and you want retry, check the `success` property of the value returned by `post()` and reset `posted.current` accordingly. The recipe doesn't show this to keep it minimal.
231+
180232
### User Feedback
181233

182234
Submit user feedback tied to your BugSplat database. Works on iOS, Android, and Web.

android/src/main/java/expo/modules/bugsplatexpo/BugsplatExpoModule.kt

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import expo.modules.kotlin.modules.Module
66
import expo.modules.kotlin.modules.ModuleDefinition
77
import expo.modules.kotlin.exception.Exceptions
88
import com.bugsplat.android.BugSplatBridge
9-
import java.io.OutputStreamWriter
10-
import java.net.HttpURLConnection
11-
import java.net.URL
12-
import java.util.UUID
139

1410
class BugsplatExpoModule : Module() {
1511
private var database: String = ""
@@ -71,69 +67,6 @@ class BugsplatExpoModule : Module() {
7167
initialized = true
7268
}
7369

74-
AsyncFunction("post") { message: String, callstack: String, options: Map<String, Any>? ->
75-
val postDatabase = database
76-
val postApp = applicationName
77-
val postVersion = applicationVersion
78-
var postAppKey = appKey
79-
var postUser = userName
80-
var postEmail = userEmail
81-
var postDescription = ""
82-
83-
options?.let { opts ->
84-
(opts["appKey"] as? String)?.let { postAppKey = it }
85-
(opts["user"] as? String)?.let { postUser = it }
86-
(opts["email"] as? String)?.let { postEmail = it }
87-
(opts["description"] as? String)?.let { postDescription = it }
88-
}
89-
90-
try {
91-
val url = URL("https://$postDatabase.bugsplat.com/post/js/")
92-
val boundary = UUID.randomUUID().toString()
93-
val connection = url.openConnection() as HttpURLConnection
94-
connection.requestMethod = "POST"
95-
connection.doOutput = true
96-
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=$boundary")
97-
98-
val writer = OutputStreamWriter(connection.outputStream)
99-
100-
fun writeField(name: String, value: String) {
101-
writer.write("--$boundary\r\n")
102-
writer.write("Content-Disposition: form-data; name=\"$name\"\r\n\r\n")
103-
writer.write("$value\r\n")
104-
}
105-
106-
writeField("database", postDatabase)
107-
writeField("appName", postApp)
108-
writeField("appVersion", postVersion)
109-
writeField("appKey", postAppKey)
110-
writeField("user", postUser)
111-
writeField("email", postEmail)
112-
writeField("description", postDescription)
113-
writeField("callstack", callstack)
114-
115-
if (attributes.isNotEmpty()) {
116-
val json = org.json.JSONObject(attributes as Map<*, *>).toString()
117-
writeField("attributes", json)
118-
}
119-
120-
writer.write("--$boundary--\r\n")
121-
writer.flush()
122-
writer.close()
123-
124-
val responseCode = connection.responseCode
125-
connection.disconnect()
126-
127-
if (responseCode == 200) {
128-
mapOf("success" to true)
129-
} else {
130-
mapOf("success" to false, "error" to "HTTP $responseCode")
131-
}
132-
} catch (e: Exception) {
133-
mapOf("success" to false, "error" to (e.message ?: "Unknown error"))
134-
}
135-
}
136-
13770
Function("setUser") { name: String, email: String ->
13871
userName = name
13972
userEmail = email

ios/BugsplatExpoModule.swift

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -74,68 +74,6 @@ public class BugsplatExpoModule: Module {
7474
bs.start()
7575
}
7676

77-
AsyncFunction("post") { (message: String, callstack: String, options: [String: Any]?) -> [String: Any] in
78-
let postDatabase = self.database
79-
let postApp = self.applicationName
80-
let postVersion = self.applicationVersion
81-
var postAppKey = self.appKey
82-
var postUser = self.userName
83-
var postEmail = self.userEmail
84-
var postDescription = ""
85-
86-
if let opts = options {
87-
if let key = opts["appKey"] as? String { postAppKey = key }
88-
if let user = opts["user"] as? String { postUser = user }
89-
if let email = opts["email"] as? String { postEmail = email }
90-
if let desc = opts["description"] as? String { postDescription = desc }
91-
}
92-
93-
let url = URL(string: "https://\(postDatabase).bugsplat.com/post/js/")!
94-
let boundary = UUID().uuidString
95-
var request = URLRequest(url: url)
96-
request.httpMethod = "POST"
97-
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
98-
99-
var body = Data()
100-
101-
func appendField(_ name: String, _ value: String) {
102-
body.append("--\(boundary)\r\n".data(using: .utf8)!)
103-
body.append("Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n".data(using: .utf8)!)
104-
body.append("\(value)\r\n".data(using: .utf8)!)
105-
}
106-
107-
appendField("database", postDatabase)
108-
appendField("appName", postApp)
109-
appendField("appVersion", postVersion)
110-
appendField("appKey", postAppKey)
111-
appendField("user", postUser)
112-
appendField("email", postEmail)
113-
appendField("description", postDescription)
114-
appendField("callstack", callstack)
115-
116-
if !self.attributes.isEmpty {
117-
if let json = try? JSONSerialization.data(withJSONObject: self.attributes),
118-
let jsonString = String(data: json, encoding: .utf8) {
119-
appendField("attributes", jsonString)
120-
}
121-
}
122-
123-
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
124-
request.httpBody = body
125-
126-
do {
127-
let (_, response) = try await URLSession.shared.data(for: request)
128-
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
129-
return ["success": true]
130-
} else {
131-
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
132-
return ["success": false, "error": "HTTP \(statusCode)"]
133-
}
134-
} catch {
135-
return ["success": false, "error": error.localizedDescription]
136-
}
137-
}
138-
13977
Function("setUser") { (name: String, email: String) in
14078
self.userName = name
14179
self.userEmail = email

0 commit comments

Comments
 (0)