Repository navigation
feat: add experimental screenshotScale option for default replay screenshots - #994
Conversation
There was a problem hiding this comment.
PostHog Review · Standard · reviewed c554f80: 1 Must fix.
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-30T06:33:45.910143+00:00
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 2816ms |
| Format Validation.Event Has Uuid | ✅ | 2760ms |
| Format Validation.Event Has Lib Properties | ✅ | 2680ms |
| Format Validation.Distinct Id Is String | ✅ | 2621ms |
| Format Validation.Token Is Present | ✅ | 417ms |
| Format Validation.Custom Properties Preserved | ✅ | 2629ms |
| Format Validation.Event Has Timestamp | ✅ | 2702ms |
| Format Validation.Non Utc Event Timestamp Is Converted To Utc | ✅ | 2785ms |
| Retry Behavior.Retries On 503 | ✅ | 11747ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 4739ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 2350ms |
| Retry Behavior.Respects Retry After Header | ✅ | 7680ms |
| Retry Behavior.Implements Backoff | ✅ | 15404ms |
| Retry Behavior.Retries On 500 | ✅ | 9543ms |
| Retry Behavior.Retries On 502 | ✅ | 9280ms |
| Retry Behavior.Retries On 504 | ✅ | 5406ms |
| Retry Behavior.Max Retries Respected | ❌ | 21785ms |
| Deduplication.Generates Unique Uuids | ✅ | 2991ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 9259ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 16344ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 7830ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 2985ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 2888ms |
| Compression.Sends Gzip When Enabled | ✅ | 2655ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 2832ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 253ms |
| Batch Format.Multiple Events Batched Together | ✅ | 2901ms |
| Error Handling.Does Not Retry On 403 | ✅ | 4729ms |
| Error Handling.Does Not Retry On 413 | ✅ | 4784ms |
| Error Handling.Retries On 408 | ✅ | 5355ms |
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 | ✅ | 2799ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 2815ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 2740ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 2751ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 2765ms |
| Request Payload.Groups Round Trip | ✅ | 2795ms |
| Request Payload.Groups Default To Empty Object | ✅ | 2665ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 2701ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 2792ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 2837ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 75ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 2829ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 5373ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 2792ms |
| Retry Behavior.Retries Flags On 502 | ✅ | 2823ms |
| Retry Behavior.Retries Flags On 504 | ✅ | 2801ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 2961ms |
|
@PostHog review |
|
@ioannisj PostHog Review is reviewing this pull request. |
dustinbyrne
left a comment
There was a problem hiding this comment.
this is awesome, love to see how dramatic the performance characteristics are depending on the size of the output buffer and depth of filtering. i feel like we have a really good handle on snapshot performance at this point.
some comments similar to PostHog/posthog-android#906 (review)
looking at the reported performance characteristics, 1x/no filtering is roughly 3x faster than the default decided in this PR. i'd personally consider that as the default given it's less likely to affect downstream applications, though, i suppose the iOS case is different because we're not consuming render thread time in this case. just food for thought.
This is my thinking around keeping this:
On Android side this also hits UI thread, which is a different story, but for iOS I would trade a couple of ms for smaller snapshot size and better quality at 1x. If we go with the presets idea from PostHog/posthog-android#906 (review) that could be a good place to add this? |
…cated Keeps screenshotScale from #994 without the removed screenshotMode options and stays on SPM only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
💡 Motivation and Context
Follow-up to #967, which uploads every default-path screenshot at one pixel per point. Adds an experimental
sessionReplayConfig.screenshotScaleso an app can pick a different resolution, and makes the resize smooth instead of nearest-neighbour.Public API change: a new
@objc public var screenshotScale: NSNumber?onPostHogSessionReplayConfig.It means the same thing as posthog-android's
screenshotScale: a multiplier of the screen's native resolution, clamped to 0.1 to 1.0. On a 3x iPhone,1.0uploads native 3x and0.5uploads 1.5 pixels per point. One pixel per point isn't a fixed multiplier (it's1 / screen.scale), so the option is nullable, andnil(the default) keeps the #967 behaviour. NaN and infinite values reset it tonil. It's anNSNumber?because Objective-C can't representCGFloat?, same assampleRate.How it works:
drawHierarchystill renders at native scale on main, and the resize stays in the encode-time redraw on the replay queue, off main. It now targetsceil(points × native × screenshotScale)pixels instead of always one pixel per point.1.0with nothing to mask, the native frame is uploaded as is, with no redraw.The second commit switches the redraw from
.noneto.mediuminterpolation. Nearest-neighbour drops thin strokes when downsampling 3:1 ("Settings" comes out as "Sellings" at 9pt), and any 1 px line that falls on a skipped source column disappears.Redraw time on an iPhone 17 Pro (iOS 26.7.1), a 402×874 pt screen of 9 to 20pt labels at 3x, 200 redraws on the replay queue per row, median / p95 over two runs:
lowis the same asnoneat 3:1.mediumreads about as well ashighfor a bit over half the cost, and uploads 7 to 8% smaller thannone. At the default 1x it costs about 3 ms more per frame, on a background queue, at most once a second.Cross-SDK:
screenshotScale, documented as Android only. iOS could be forwarded later.session-replay-privacyasks for masks to be painted into the bitmap before encoding, and they still are.Heads up for #911: it adds its own
screenshotScale(in pixels per point) and amaskScalein the same places. When it's rebased it should read this option instead, and setwireframe.screenshotPixelScaleto the scale its GPU frame was rendered at.💚 How did you test it?
New tests in
PostHogReplayScreenshotSizeTest(Swift Testing, iOS simulator):screenshotScaleuploads one pixel per point, masked or not0.5and1.0upload that fraction of native resolution, masked or notnilI checked that each one fails with its piece of the change reverted (no outward rounding, truncating size, no native skip, scale ignored,
.nonefilter, masks painted after the resize with a wider filter).PostHogReplayScreenshotSizeTest,PostHogMaskFailClosedTestandRRWireframeTestpass (41 tests), including #969's corner tests.PostHogMaskFailClosedTestforced a failed render with an image 1/8 pt wide, which only failed because the renderer truncated it to zero pixels. With rounding up it renders one pixel, so the fixture is now 1/10,000 pt wide. The fail-closed behaviour itself didn't change.Masking goldens: I re-recorded 24 of them. In each one, the only changed pixels are in the masked panel and every one got darker: mask edges going from a half-blended row to solid black. The original panels didn't change, and 4 cases were identical. Painting masks before the resize leaves all 28 pixel-identical, since the snapshot suite masks 1x images and never resizes. The pinned Xcode 26.3 isn't installed locally, so I recorded under Xcode 27.1 on the pinned iOS 26.2 runtime. Main's own goldens verify unchanged under that setup, so CI's
make maskSnapshotsshould agree.Before rebasing onto main, I ran the full iOS simulator suite. It was green apart from the fixture above (since fixed) and one existing known-issue marker in
PostHogMaskFlattenedContentTest. Also before the rebase, the SDK built for iOS, macOS, Mac Catalyst, tvOS, watchOS and visionOS.make lintpasses on the rebased branch.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
DRI: @ioannisj
Autonomy: Human-driven (agent-assisted)