Repository navigation
Conversation
There was a problem hiding this comment.
PostHog Review · Standard · reviewed a4fd695: 4 Must fix, 2 Should fix. This pull request is large, so the review ran in 4 parts with less depth than usual.
This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.
See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.
posthog-ios Compliance ReportDate: 2026-10-30T14:31:44.371717+00:00
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 3446ms |
| Format Validation.Event Has Uuid | ✅ | 351ms |
| Format Validation.Event Has Lib Properties | ✅ | 2893ms |
| Format Validation.Distinct Id Is String | ✅ | 2755ms |
| Format Validation.Token Is Present | ✅ | 2750ms |
| Format Validation.Custom Properties Preserved | ✅ | 2722ms |
| Format Validation.Event Has Timestamp | ✅ | 2804ms |
| Format Validation.Non Utc Event Timestamp Is Converted To Utc | ✅ | 2883ms |
| Retry Behavior.Retries On 503 | ✅ | 5587ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 4769ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 4831ms |
| Retry Behavior.Respects Retry After Header | ✅ | 7881ms |
| Retry Behavior.Implements Backoff | ✅ | 21826ms |
| Retry Behavior.Retries On 500 | ✅ | 5518ms |
| Retry Behavior.Retries On 502 | ✅ | 9346ms |
| Retry Behavior.Retries On 504 | ✅ | 8810ms |
| Retry Behavior.Max Retries Respected | ❌ | 21734ms |
| Deduplication.Generates Unique Uuids | ✅ | 2922ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 9283ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 17105ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 8516ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 2935ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 2845ms |
| Compression.Sends Gzip When Enabled | ✅ | 2791ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 2747ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 441ms |
| Batch Format.Multiple Events Batched Together | ✅ | 2923ms |
| Error Handling.Does Not Retry On 403 | ✅ | 4701ms |
| Error Handling.Does Not Retry On 413 | ✅ | 4749ms |
| Error Handling.Retries On 408 | ✅ | 9224ms |
Failures
retry_behavior.max_retries_respected
Expected 4 requests, got 6
Feature_Flags Tests
✅ 17/17 tests passed
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 2952ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 2843ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 3055ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 2844ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 2884ms |
| Request Payload.Groups Round Trip | ✅ | 2967ms |
| Request Payload.Groups Default To Empty Object | ✅ | 2907ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 2876ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 2838ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 2807ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 86ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 2830ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 5667ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 2663ms |
| Retry Behavior.Retries Flags On 502 | ✅ | 2855ms |
| Retry Behavior.Retries Flags On 504 | ✅ | 2906ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 2970ms |
9649f7a to
16517c5
Compare
|
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 3 · PR risk: 0/10 |
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 94ce0c1: 2 Must fix. This pull request is large, so the review ran in 4 parts with less depth than usual.
This is review round 3 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.
…nder postHogMask extents
There was a problem hiding this comment.
PostHog Review · Standard · reviewed f58d153: 1 Should fix. This pull request is large, so the review ran in 4 parts with less depth than usual.
This is review round 4 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.
| case .atPortal: | ||
| let elsewhere = PostHogPortalSource.elsewhere(source: source, portal: portal, root: walk.root) | ||
| walk.portalSources.append(PostHogPortalSource(view: view, elsewhere: elsewhere)) | ||
| walk.portalSources.append(PostHogPortalSource(view: view, elsewhere: elsewhere, hasInPlaceCopy: walk.viewCopies[id] != nil)) |
There was a problem hiding this comment.
P2 · Determine in-place copy availability after the mirror walk
When a portal precedes its source with hidesSourceLayer = false, viewCopies[id] is still nil here even though the walk subsequently creates the in-place copy. wholePortals then adds a full-portal rectangle despite successful inline masking, blacking out unrelated content and controls painted above it. Resolve hasInPlaceCopy after the walk finishes.
💡 Motivation and Context
Stacked on #911, so review that one first. This PR only changes what masked content looks like on the experimental GPU capture path (
screenshotModeGPUCapture). The default path is unchanged, byte for byte.On #911 the GPU path masks the same way the default path does: the frame is read back, then each masked region is painted over as a black rect. This PR masks the mirror itself before the render instead, so masked content is redacted in place, at its own z-order. A button or toolbar on top of masked content stays visible instead of disappearing under a black rect.
How each kind of content is masked:
*in the view's own font, colour and position. Labels, buttons and text views keep whitespace (John Smith→**** *****, like rrweb'smaskText), and text fields and secure fields get one*per character (like rrweb'smaskInputValue). Ruby annotations are dropped. The*bitmaps are drawn off the main thread and cached, and until one is ready that text shows as per-line bars. Text over 4096 UTF-16 units, or whose bitmap would go over 4 MB, stays as bars.CATextLayer, React Native) shows as rows of*sized from its bounds, in the label colour..drawingGroup()andCanvascontent is black, like on the default path.ph-no-captureviews are one black block over their bounds, no chrome.postHogMask()on the GPU pathpostHogMask()only reports a rect, so there's no view to redact. On this path everything painted under that rect before the modifier's own overlay is masked, chrome included: text as above, images and colour layers black. That includes shapes and contents drawn past their layer's bounds and shadows cast into the rect from outside it. A layer with filters is replaced whole, never covered in part. UI painted after it (a button over masked text, a sheet over a masked card) stays visible. When a non-zerozPositioncould change the paint order on the reporter's branch, every intersecting layer is masked.Portals
A portal on the GPU path draws a second, separate copy of its source. Every inline redaction is applied to all copies of the same source, so a
postHogMask()overlay next to a source also masks the copy the portal shows somewhere else. A copy that doesn't line up with its source is blacked out whole.Behaviour changes from the default path
*text gives away text length, and for labels word lengths and line breaks too. Web masking gives away the same.ph-no-captureviews are still blacked out whole.Over-masking
postHogMask()go black as a square block, and so doph-no-captureviews.*remain.Spec note
session-replay-privacysays screenshot recorders paint opaque masks over the screenshot. Images and blocked views are opaque here, but masked text is drawn into the screenshot as*instead. No masked text reaches the upload, but the wording doesn't cover it. If inline masking stays past the experiment, I'll propose a spec amendment.Not in this PR
.drawingGroup()content isn't drawn by the GPU mirror at all (Session replay doesn't mask SwiftUI text inside .drawingGroup() or SwiftUI Canvas #935), so a Swift Chart underpostHogNoMask()comes out blank on this path.💚 How did you test it?
PostHogGPUInlineMaskTestandPostHogGPUSwiftUIMaskTestare new, andPostHogGPUCaptureTestis updated. They cover text (ruby annotations, the length and bitmap caps), images,ph-no-captureblocks, backdrops,postHogMask()extents (chrome, paint order,zPosition, shapes drawn past their bounds, sheets over an extent), portal copies and first-frame behaviour. Leaks are checked by pixel colour: magenta content must not survive masking.The golden image suite also gets a GPU set (
make gpuMaskSnapshots,make recordGPUMaskSnapshots): the existing 28 scenarios through the GPU path, plus 7 GPU-only ones (postHogMask()chrome, paint order, a sheet over an extent, aph-no-capturecard, photos under a blur panel, portal copies, partial covers). CI runs it in the same job as the existing goldens, on the same pins.make lintshows no errors in the changed files.postHogMask()extent pass adds about 2.0 to 2.4 ms to the first main-thread turn.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
DRI: @ioannisj
Autonomy: Human-driven (agent-assisted)