Screenshot with Annotation and Context: In-App Feedback Module for Mobile Applications
A user reports a bug: "the button doesn't work." The developer wonders: which button? On which screen? In-App Feedback removes all questions — the user literally shows the problem on a screenshot, highlights it with a marker, and adds a comment. We implement such a module turnkey: from screen capture to sending to Jira or Sentry. On one project with an audience of a million users, implementing this module halved the time for initial bug analysis, and the number of reproducible issues increased by 70%.
Typical challenges: lack of context (a screenshot gives 10 times more information than text), incomplete metadata (version, OS, device), inability to reproduce the bug (screenshot captures the state). Our solution automatically attaches all metadata and saves the screenshot to the tracking system. We use proven approaches and guarantee performance — the screenshot is created in less than 0.5 seconds. Based on our experience, implementing such a module reduces debugging time by 60% and saves up to 40 hours of engineer work per month.
Why In-App Feedback Is Critical for Mobile Applications
Without a screenshot with context, a developer spends an average of 20 minutes clarifying bug details. In-App Feedback reduces this time to zero: all information (image, annotation, metadata) arrives in the ticketing system in one click. This is especially important for applications with high dynamics — for example, fintech or logistics, where every hour of downtime is costly.
How to Capture a Screenshot on iOS and Android
Each platform requires its own approach. We combine methods for maximum coverage.
iOS — UIGraphicsImageRenderer + WKWebView
func captureScreenshot() -> UIImage? {
let renderer = UIGraphicsImageRenderer(bounds: UIScreen.main.bounds)
return renderer.image { ctx in
UIApplication.shared.windows.first?.layer.render(in: ctx.cgContext)
}
}
// For WKWebView:
webView.takeSnapshot(with: nil) { image, error in
// Insert into final screenshot via Core Graphics
}
For more details: UIGraphicsImageRenderer. Important nuance: layer.render does not capture content from WKWebView and ARSCNView — they render through a separate GPU context. Therefore, for WebView we use the takeSnapshot(with:) method.
Android — PixelCopy API
fun captureScreenshot(activity: Activity, callback: (Bitmap?) -> Unit) {
val bitmap = Bitmap.createBitmap(
activity.window.decorView.width,
activity.window.decorView.height,
Bitmap.Config.ARGB_8888
)
PixelCopy.request(activity.window, bitmap, { result ->
callback(if (result == PixelCopy.SUCCESS) bitmap else null)
}, Handler(Looper.getMainLooper()))
}
The Android Developer Guide recommends PixelCopy starting from Android 8.0 (API 26). For older devices, we use View.getDrawingCache(), but it does not capture SurfaceView.
| Platform | Method | Limitations |
|---|---|---|
| iOS | UIGraphicsImageRenderer | Does not capture WKWebView and ARSCNView |
| Android | PixelCopy (API 26+) | Does not capture SurfaceView |
| Flutter | RenderRepaintBoundary | Works only within Flutter widgets |
Why a Custom Annotator Is Better Than Off-the-Shelf Solutions
Off-the-shelf libraries (PSPDFKit, Pen) save time but limit control. A custom annotator provides 40% more flexibility and has no licensing restrictions. Developing your own annotator takes 2–3 days and gives full control over the UX. In one project, we replaced PSPDFKit with a custom canvas and achieved:
- Custom arrow shapes (double line, curved)
- Pixelation by area (hiding personal data)
- Instant touch response (no UIKit delays)
Example of a Custom Canvas
class AnnotationCanvasView: UIView {
private var paths: [UIBezierPath] = []
private var currentPath: UIBezierPath?
var strokeColor: UIColor = .red
var strokeWidth: CGFloat = 3.0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let path = UIBezierPath()
path.move(to: touches.first!.location(in: self))
currentPath = path
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
currentPath?.addLine(to: touches.first!.location(in: self))
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let path = currentPath { paths.append(path) }
currentPath = nil
}
override func draw(_ rect: CGRect) {
for path in paths {
strokeColor.setStroke()
path.lineWidth = strokeWidth
path.stroke()
}
strokeColor.setStroke()
currentPath?.stroke()
}
}
| Criterion | Off-the-Shelf SDK | Custom Implementation |
|---|---|---|
| Time to integrate | 1–2 days | 2–3 days |
| Control over UX | Limited | Full |
| Licensing | Paid (SDK) | Free |
| Customization | Via API | Any |
Collecting Metadata
We automatically attach the following to the screenshot:
struct FeedbackPayload: Encodable {
let screenshot: Data // JPEG, quality 0.7
let description: String
let appVersion: String
let osVersion: String
let deviceModel: String
let screenName: String // current screen (router/NavigationStack)
let userId: String?
let sessionId: String // UUID session for log correlation
let timestamp: Date
}
screenName is especially important — it allows developers to immediately know on which screen the problem occurred without questioning the user.
How to Send Feedback to the Ticketing System
We send the screenshot as multipart/form-data. For backend storage, we use S3 or similar with pre-signed URLs. In the ticketing system (Jira, Linear, Sentry), we attach a link to the image.
Example via Sentry:
let attachment = Attachment(
data: screenshotData,
filename: "screenshot.jpg",
contentType: "image/jpeg"
)
SentrySDK.capture(message: feedback.description) { scope in
scope.addAttachment(attachment)
scope.setTag(value: feedback.screenName, key: "screen")
}
Example Sentry configuration for iOS
```swift SentrySDK.start { options in options.dsn = "https://[email protected]/0" options.enableAutoPerformanceTracking = true } ```Work Process
- Analysis — we test the current bug collection process, identify bottlenecks.
- Design — we choose the capture method, design the annotation UX.
- Development — we implement the module, integrate with your CRM/ticketing system.
- Testing — load testing (100+ screenshots per minute), privacy verification (pixelation).
- Deployment — documentation, team training, monitoring.
What's Included
- Architectural document describing the module
- SDK with no conflicts with existing dependencies
- Integration with Jira, Sentry, or your CRM
- User instructions
- Performance guarantee (screenshot < 1 sec)
- Team training
Time and Cost Estimates
Our team has extensive experience in mobile development, having implemented over 15 projects with in-app feedback. A custom implementation takes 1–1.5 weeks. Integration of an off-the-shelf library takes 3–5 days. The cost is calculated individually and depends on the complexity of the integration. Contact us for a project evaluation — we will suggest the optimal solution. Order a consultation on integrating in-app feedback into your application.







