feat: capture requests - screenshots, screen recordings and logs on the web - #163
Merged
Merged
Conversation
…not uploaded twice - "Stop sharing" in the browser ended only the screen track: the timer kept running and the microphone stayed open (the recorder went on recording audio). The track's end now stops the recording like the Stop button, releasing the timer and every track. - The recording stayed in the global data after the report that carried it was sent, so a later silent crash report uploaded it again. It is cleared once a report with it was sent, and silent reports never take the feedback form's recording along. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rrweb records one session per page. A capture request's page recording (phones and tablets, where there is no screen capture) pauses the running replay, keeping its buffer, and resumes it in a new checkpoint afterwards. A config apply meanwhile doesn't restart it; stop() ends the pause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sts on the web The host side of the capture-requests contract (Messenger bridge §7, web capture §8, logs §9, APIs §10): - capture-capabilities after every Messenger ping (screenshot, recording via getDisplayMedia or rrweb, microphone per flowConfig.capture.allowMicrophone); everything false with Gleap.setCaptureEnabled(false). The SDK websocket announces &caps=capture.screenshot, capture.recording,capture.logs (only what is enabled). - capture-start hides the widget without closing it and shows a capture bar in the page: shadow DOM under one 0x0 rr-block/gl-block host, max z-index, draggable on desktop, a bottom sheet with safe-area padding on phones, keyboard accessible, reduced-motion aware, labels from the Messenger with English fallbacks. capture-state reports every step; Cancel releases the request. - Screenshots: desktop Chromium grabs one frame of the current tab (preferCurrentTab; ImageCapture, MediaStreamTrackProcessor, <video> fallback; Gleap UI hidden, tracks stopped right away); otherwise, or when the tab capture is declined or fails, the visible viewport is rendered in the browser (modern-screenshot 4.7.0, ~8.6 kB gzip). Both paint black boxes over blocked and masked elements, private fields (password, OTP, card), payment frames and capture.maskSelectors, also in shadow roots and same-origin frames; the DOM render also hides them in the clone. JPEG 0.85, long edge <= 2560, sent as capture-image; capture-editor opens the widget full screen. - Recordings: getDisplayMedia + MediaRecorder (MP4 avc3/avc1, then WebM; 2.5 Mbps; 1 s chunks; optional microphone; auto-stop at maxDurationSec; "Stop sharing" = Stop) or, without getDisplayMedia, an rrweb page recording with every input masked, uploaded as a webReplay-shaped page-recording.json. Preview (Send / Retake / Cancel), upload with progress, /complete with a timeline digest (clicks, inputs without values, navigations, console errors, failed requests). - attachLogs: the log bundle (screenshot: at capture time; recording: its window) goes to /logs, gzipped with CompressionStream when available. - Log requests (capture-request on the SDK websocket): claim, bundle from the existing collectors (replay only when asked and web replays are on), gzip, POST /logs; once per request id; answered "unsupported" with Gleap.setRemoteLogCollectionEnabled(false) or backgroundLogs off. - Multi-page sites: the bar (or "Recording stopped because the page changed") comes back after a page load in the same tab; the result then opens the conversation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… kill switches - Screenshot boxes cover private fields, rr-/gl- mask and block markers, the site's replay options, capture.maskSelectors, payment frames, open shadow roots and same-origin frames, and nothing else; an invalid selector doesn't switch the others off. - Page recordings mask every input and leave masked, blocked and maskSelectors content and the capture bar out; timeline labels never carry field values or masked text. - Log bundles carry only what the request includes (replay only when asked and enabled); log requests are answered "unsupported" when the app or the project switched them off, posted once per request id, and left to the device that claimed them first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…them MediaRecorder writes WebM as a live stream without a Duration in the Segment Info (Firefox, and Chrome before MP4 recording), so players showed no length and couldn't seek. The finished recording now gets the measured duration before the preview and the upload: an existing Duration is overwritten in place; otherwise one is added to the Info, with the Info size, a known Segment size and SeekHead positions after it adjusted (files whose SeekHead points at Cues are left alone). Anything unexpected leaves the file as recorded; MP4 is untouched. No dependency (~1.2 kB gzip). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a desktop recording's getDisplayMedia is denied, blocked by a policy, unavailable or the picker is cancelled, the capture bar stays open with "Screen sharing didn't start. You can record this page instead." and "Record this page" (rrweb page recording, method 'rrweb') / "Cancel". Only a Cancel there reports the outcome: capture-state + /event 'declined' (NotAllowedError/SecurityError) or 'failed' with the error as reason. Labels come from capture-start.labels (recordPage / recordPageHint) with English fallbacks. The choice survives page loads, so "Record again" after a navigation goes straight to a page recording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o covers attached logs A getDisplayMedia video can't be masked afterwards, so while a screen (tab/window/screen) recording runs the live page now carries a privacy veil: one constructed stylesheet (CSP-safe; nonce'd <style> fallback), adopted by the document, its open shadow roots and same-origin frames, that blurs (filter: blur(12px), plus -webkit-text-security on fields) everything screenshots mask: rr-/gl- block and mask markers, the replay options' classes (string or RegExp) and selectors, gleap-ignore="value" fields, password / one-time-code / card fields, masked input kinds, payment frames and flowConfig.capture.maskSelectors. Only the outermost matches are blurred and never <html>/<body> (their children instead), so the capture bar stays fixed and crisp. Content added later is covered. It goes up inside the Start click (before the picker) and the recorder starts only once it is painted; it comes down on stop, "Stop sharing", max duration, cancel, fallback, errors, pagehide and destroy. Page recordings (rrweb) and screenshots don't use it. Gleap.setRemoteLogCollectionEnabled(false) now also keeps logs off screenshots and recordings (attachLogs), checked at capture time and again before posting, as iOS and Android do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DOM screenshots - <video> elements no longer go to modern-screenshot (its cloneVideo waits for a seek that never comes for camera streams, missing sources or preload=none): a stand-in with the video's computed style shows the current frame when readable, else the poster, else a dark box. - The render has a 10 s deadline and a 10 000-element budget; Cancel stays available while it runs. Giving up (or a cancel) cleans up at once: network logging back on, sandbox removed, widget restored; the request fails with the upload offered. - The style sandbox gets its srcdoc before insertion and only accepts the about:srcdoc load (a synchronous about:blank load used to discard it, so the render never yielded). Under Trusted Types the about:blank document serves; failing that a stub keeps modern-screenshot from creating (and leaking) its own frames. - Boxes away from the viewport are cloned empty (only where their size and place can't change) and their images and CSS backgrounds aren't downloaded; modern-screenshot's per-image redraw delay (Safari/Firefox) is capped at two redraws. - Network logging pauses are counted, so overlapping renders can't leave it off. Tab frames: at most 15 s with the bar hidden; Escape cancels meanwhile and stops sharing at once; mask rects follow pinch zoom (visualViewport); no tab capture from inside a frame. Logs and page recordings: replay events are packed in slices (large events through CompressionStream off the main thread) and bundles are serialized in slices into a Blob; the /logs timeout grows with the body. Also: Gleap banners, modals, tooltips, tours, checklists and the admin UI stay out of captures, and their actions open nothing while the widget is hidden for one; restored bars keep their original start time and are dropped when the request is final or expired (GET on restore); a fallback to page recording stops whatever screen recording started; a MediaRecorder error keeps what was recorded and offers it; standard rr-/gl- markers stay private when a site sets its own classes; timeline labels respect flowConfig.capture.maskSelectors; the capture bar is built before the widget hides; timer and event callbacks are guarded. The capture modules compile for the browsers the bundle already needs (ES2020). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 1, 2026
Merged
The bar was hidden only once sharing had started, and a tab stream can still hand out frames painted before that, so a screenshot could show the bar with its spinner. It is now hidden as the browser's share dialog opens (Escape still cancels), shown again with its busy state if sharing is declined, and the first frame is taken 120 ms after the hide is painted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter its label Matches the native bars: the bar and its buttons are fully rounded, Stop is a round icon-only button (its label stays the accessible name), the recording bar shows only the dot and the timer, the start button reads "Start", the grip shows the translated drag hint (barDragHint) as its tooltip, and the preview's Send carries its icon after the label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ests # Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
Web support for capture requests. When a workflow, an AI agent or a teammate asks for a screenshot or a screen recording, the customer answers from the widget card and the SDK captures the page. Logs requests are collected in the background.
Screenshots (
GleapCaptureScreenshot)webScreenshotMethod: 'tab-first'): one-click "Share this tab" (getDisplayMediawithpreferCurrentTab) gives exact pixels. Gleap's UI is hidden; one frame is grabbed, the tracks stop at once, and masked boxes are painted black. If the customer cancels, denies or anything fails, the SDK falls back to the in-browser render without a second prompt.dom-only): in-browser DOM render with modern-screenshot 4.7.0 (pinned).gl-mask/gl-block/rr-mask/rr-block;setReplayOptionsmasks; payment iframes; and the project'smaskSelectors. It reaches same-origin frames and open shadow roots.Recordings (
GleapCaptureRecorder)getDisplayMediaand MediaRecorder: tab-first on Chromium, the browser's picker elsewhere. MP4 (avc3 → avc1) is preferred, WebM otherwise.GleapWebmDuration) so players can seek.getDisplayMedia): rrweb page recording, uploaded in thewebReplayshape the dashboard already plays. Every input value is masked, and a timeline digest goes to the AI.Capture bar (
GleapCaptureUI)capture-start.labels.Logs requests (
GleapCaptureManager)capture-request), which announces&caps=on connect. Collected from the existing collectors (same shapes as bug reports), gzipped, posted to/logsonce per request id.Public API
Classic recorder fixes found on the way
Bundle size
build/browser/index.js: 166.4 → 200.2 KB gzip (+34 KB, +20 %), mostly capture code and modern-screenshot (~8.6 KB).The capture modules are compiled for the ES2020 browsers the bundle already needs, because rrweb ships
??untranspiled (babel.config.jsoverride). The minimum browser is unchanged. Lazy-loading the capture code would need CDN/publishing changes and is left for later.Release notes
build/andpublished/are not in this PR. They are regenerated at release, andpublished/latestdeploys to the CDN on push to master.media-srcneedsblob:for the recording preview (docs PR).Verification
npm test480/480 (masking stays private, log kill switches, bundle serialization).gl-mask);Not verified
getDisplayMedia. It needs a manual pass in Chrome/Edge (tab screenshot and recording), Firefox/Safari (DOM screenshot, window recording), and iOS Safari / Android Chrome (DOM screenshot, page recording).about:blanksandbox).The whole program: capture requests ("Show me the issue")
Workflows, AI agents (Kai, Kai Resolve, custom agents) and teammates can now ask a customer for a screenshot (with annotation) or a screen recording in the middle of a conversation, and can collect logs (console, network, custom data, environment data, events) from the customer's app in the background.
One server object, the capture request, backs all three. The customer sees a card in the widget; the SDK in their app does the capture; the result lands in the thread and resumes the workflow or wakes the agent. Background log requests travel over the SDK websocket every SDK already keeps open.
How each platform captures:
Design: https://claude.ai/artifact/5i88BeWfcsWUFhhJmPwK5V (private)
PRs in this program (one per repo):
🤖 Generated with Claude Code