PostHog / posthog-ios

PostHog iOS SDK
https://posthog.com/docs/libraries/ios
MIT License
38 stars 42 forks source link

EXC_BAD_ACCESS during autocapture of layer borderColor #226

Closed jadar closed 4 days ago

jadar commented 1 week ago

Version

3.13.3

Steps to Reproduce

  1. Create an interface with a Storyboard. Add a UIButton and properties to set the layer.borderColor in the Storyboard file.
  2. Enable PostHog session replay
  3. Navigate to screen and observe crash

Expected Result

No crash

Actual Result

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x194b96cec)
  * frame #0: 0x0000000194b96cec libswiftCore.dylib`swift::hashable_support::findHashableBaseType(swift::TargetMetadata<swift::InProcess> const*) (.cold.1) + 16
    frame #1: 0x0000000194adfaa8 libswiftCore.dylib`swift_slowAlloc + 100
    frame #2: 0x0000000194adfd54 libswiftCore.dylib`swift_allocObject + 52
    frame #3: 0x000000018b208af8 CoreGraphics`___lldb_unnamed_symbol19785 + 64
    frame #4: 0x000000018b206a28 CoreGraphics`__C.CGColorRef.components.getter : Swift.Optional<Swift.Array<CoreGraphics.CGFloat>> + 48
    frame #5: 0x00000001065aca4c App.debug.dylib`CGColorRef.toRGBString() at CGColor+Util.swift:15:36
    frame #6: 0x00000001065b7afc App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x0000000107741870) at PostHogReplayIntegration.swift:461:52
    frame #7: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x0000000107741090) at PostHogReplayIntegration.swift:468:36
    frame #8: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x0000000107742cb0) at PostHogReplayIntegration.swift:468:36
    frame #9: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x0000000107742b20) at PostHogReplayIntegration.swift:468:36
    frame #10: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x00000001079af950) at PostHogReplayIntegration.swift:468:36
    frame #11: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x00000001079a80c0) at PostHogReplayIntegration.swift:468:36
    frame #12: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x00000001079becf0) at PostHogReplayIntegration.swift:468:36
    frame #13: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x00000001079b6c20) at PostHogReplayIntegration.swift:468:36
    frame #14: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x00000001079386e0) at PostHogReplayIntegration.swift:468:36
    frame #15: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x0000000107999d70) at PostHogReplayIntegration.swift:468:36
    frame #16: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x000000010792fe30) at PostHogReplayIntegration.swift:468:36
    frame #17: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x000000010790cb30) at PostHogReplayIntegration.swift:468:36
    frame #18: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x000000010770f6b0) at PostHogReplayIntegration.swift:468:36
    frame #19: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x0000000107908b40) at PostHogReplayIntegration.swift:468:36
    frame #20: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x0000000107909750) at PostHogReplayIntegration.swift:468:36
    frame #21: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x0000000107909000) at PostHogReplayIntegration.swift:468:36
    frame #22: 0x00000001065b7cb0 App.debug.dylib`PostHogReplayIntegration.toWireframe(view=0x00000001076127c0) at PostHogReplayIntegration.swift:468:36
    frame #23: 0x00000001065b27b8 App.debug.dylib`PostHogReplayIntegration.generateSnapshot(view=0x00000001076127c0, screenName="Initial") at PostHogReplayIntegration.swift:87:109
Screenshot 2024-10-31 at 5 09 08 PM Screenshot 2024-10-31 at 5 09 41 PM
ioannisj commented 1 week ago

Hey @jadar! Thanx for reporting this.

I'll take a look. From the screenshot, it seems you are assigning a named color from an asset catalog or color set. Could you confirm if there are any specific settings adjusted in the Attribute Inspector? My initial thought is that the color may is not RGB scale (grayscale?) and we may not handle that case correctly.

In the meantime, you might try enabling screenshot mode to bypass this issue temporarily. Screenshot mode can be enabled with config.sessionReplayConfig.screenshotMode = true

marandaneto commented 1 week ago

@ioannisj would a simple try/catch here works as a quick win?

ioannisj commented 1 week ago

@ioannisj would a simple try/catch here works as a quick win?

No, I don't think so since there are no throwing functions involved here.

marandaneto commented 5 days ago

@ioannisj I think you can do this:

        // Ensure the color is in an RGB color space
        guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
              let rgbColor = self.converted(to: colorSpace, intent: .defaultIntent, options: nil),
              let components = rgbColor.components, components.count >= 3 else {
            return nil
        }

Can you validate if that would help?

ioannisj commented 5 days ago

Managed to recreate this with the following. I'll see if there is a safe way to convert to sRGB, otherwise I'll just add the check there

CleanShot 2024-11-04 at 13 07 38@2x CleanShot 2024-11-04 at 13 07 50@2x CleanShot 2024-11-04 at 13 09 51@2x

ioannisj commented 5 days ago

Digging a bit deeper on this one, it seems that the real underlying issue is not the color space (since we were safe with the components.count >= 3 part) but the fact that all named/dynamic colors from the interface builder get their true values set after viewDidLoad. I imagine this is because the view needs to be fully loaded to check the traits before assigning a dark or light variant.

Adding the RGB check will only handle the cases where the dynamic color happens to be gray scale (and it's an additional check other that components.count which won't crash), but that's not the ultimate reason for the crash 🤔 Changing the dynamic color to RGB range will again crash the app when components is accessed.

Printing out layer.borderColor.numberOfComponents prints out some random values like 105553118884896 which is an indication that we are dealing with an uninitialized instance of CGColor from unmanaged code that points to a random memory address

jadar commented 5 days ago

@ioannisj

I'll take a look. From the screenshot, it seems you are assigning a named color from an asset catalog or color set. Could you confirm if there are any specific settings adjusted in the Attribute Inspector?

You're correct that the color comes from an asset catalog. The settings are sRGB with a hexadecimal value of #676767, with 100% opacity. Here's the full contents:

ButtonTitleGray.colorset/Contents.json ```javascript { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x67", "green" : "0x67", "red" : "0x67" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "1.000", "green" : "1.000", "red" : "1.000" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ```

In the meantime, you might try enabling screenshot mode to bypass this issue temporarily. Screenshot mode can be enabled with config.sessionReplayConfig.screenshotMode = true

I did end up using this workaround as well. I removed references to the custom color in the project, which allowed it to function without crashing. However, the default session capture did not function properly at all, as nothing was distinguishable in the capture.

ioannisj commented 5 days ago

However, the default session capture did not function properly at all, as nothing was distinguishable in the capture.

Are you referring here to the session recording having some blacked out content. You can try experimenting with maskAllTextInputs and maskAllImage to see if this here. docs

A temporary fix on the crash should be out soon btw, I'll let you know once it's out. Hope that helps

jadar commented 5 days ago

@ioannisj

Are you referring here to the session recording having some blacked out content. You can try experimenting with maskAllTextInputs and maskAllImage to see if this here. docs

It's a little out of the scope of this issue, and I don't know what exactly was the issue, but it appeared all the UI elements were clustered in the top left part of the screen.

ioannisj commented 4 days ago

Hey @jadar, fyi fix for this bug has been release with 3.14.0

Regarding the session recording issue, can you please open a bug report when you get a chance with some screenshots? I have a feeling you are describing an issue with wireframe mode and not screenshot mode.

jadar commented 4 days ago

Thank you!