Skip to content

Commit c9c0e4d

Browse files
authored
fix: captureTouches errors with reanimated styles (#2763)
1 parent 024e5ea commit c9c0e4d

File tree

3 files changed

+100
-3
lines changed

3 files changed

+100
-3
lines changed

.changeset/empty-pillows-wait.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'posthog-react-native': patch
3+
---
4+
5+
fix: captureTouches errors with reanimated styles

packages/react-native/src/autocapture.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,33 @@ interface Element {
1111
return?: Element
1212
}
1313

14+
const isAnimatedValue = (value: any): boolean => {
15+
// Check if it's a Reanimated shared value or animated style
16+
// _isReanimatedSharedValue is the official internal marker for SharedValues
17+
// Also check for _value property which is present in SharedValues
18+
return value?._isReanimatedSharedValue === true || (typeof value === 'object' && value !== null && '_value' in value)
19+
}
20+
1421
const flattenStyles = (styles: any): any => {
1522
const flattened: any = {}
1623

24+
// Skip if the entire style object is an animated value
25+
if (isAnimatedValue(styles)) {
26+
return {}
27+
}
28+
1729
if (Array.isArray(styles)) {
1830
for (const style of styles) {
1931
Object.assign(flattened, flattenStyles(style))
2032
}
21-
} else {
22-
Object.assign(flattened, styles)
33+
} else if (styles && typeof styles === 'object') {
34+
// Filter out individual animated properties within a regular style object
35+
// This handles cases like { opacity: animatedValue, backgroundColor: 'red' }
36+
for (const key in styles) {
37+
if (!isAnimatedValue(styles[key])) {
38+
flattened[key] = styles[key]
39+
}
40+
}
2341
}
2442

2543
return flattened
@@ -81,7 +99,12 @@ export const autocaptureFromTouchEvent = (e: any, posthog: PostHog, options: Pos
8199
}
82100
const value = props[key]
83101
if (key === 'style') {
84-
el.attr__style = stringifyStyle(value)
102+
// Safely handle style prop, especially for animated styles
103+
try {
104+
el.attr__style = stringifyStyle(value)
105+
} catch (error) {
106+
// Skip style capturing if it fails (e.g., animated styles)
107+
}
85108
} else if (['string', 'number', 'boolean'].includes(typeof value)) {
86109
if (key === 'children') {
87110
el.$el_text = typeof value === 'string' ? value : JSON.stringify(value)

packages/react-native/test/autocapture.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,74 @@ describe('PostHog React Native', () => {
1919
autocaptureFromTouchEvent({ _targetInst: ignoreEvent, nativeEvent }, mockPostHog)
2020
expect(mockPostHog.autocapture).toHaveBeenCalledTimes(0)
2121
})
22+
23+
it('should handle animated styles without errors', () => {
24+
const mockPostHog = { autocapture: jest.fn() } as any
25+
26+
// Mock a Reanimated animated style
27+
const animatedStyle = {
28+
_isReanimatedSharedValue: true,
29+
_value: { opacity: 1 },
30+
__reanimatedHostObjectRef: {},
31+
}
32+
33+
const eventWithAnimatedStyle = {
34+
_targetInst: {
35+
elementType: { name: 'TouchableOpacity' },
36+
memoizedProps: {
37+
style: animatedStyle,
38+
children: 'Test Button',
39+
},
40+
return: null,
41+
},
42+
nativeEvent,
43+
}
44+
45+
// Should not throw error when processing animated styles
46+
expect(() => {
47+
autocaptureFromTouchEvent(eventWithAnimatedStyle, mockPostHog)
48+
}).not.toThrow()
49+
50+
// Should still capture the event, just with empty style
51+
expect(mockPostHog.autocapture).toHaveBeenCalledTimes(1)
52+
const capturedElement = mockPostHog.autocapture.mock.calls[0][1][0]
53+
expect(capturedElement.attr__style).toBe('')
54+
expect(capturedElement.$el_text).toBe('Test Button')
55+
})
56+
57+
it('should handle mixed animated and regular styles', () => {
58+
const mockPostHog = { autocapture: jest.fn() } as any
59+
60+
const mixedStyle = [
61+
{ backgroundColor: 'red', padding: 10 },
62+
{
63+
opacity: {
64+
_isReanimatedSharedValue: true,
65+
_value: 1,
66+
},
67+
},
68+
]
69+
70+
const eventWithMixedStyle = {
71+
_targetInst: {
72+
elementType: { name: 'View' },
73+
memoizedProps: {
74+
style: mixedStyle,
75+
testID: 'test-view',
76+
},
77+
return: null,
78+
},
79+
nativeEvent,
80+
}
81+
82+
autocaptureFromTouchEvent(eventWithMixedStyle, mockPostHog)
83+
84+
expect(mockPostHog.autocapture).toHaveBeenCalledTimes(1)
85+
const capturedElement = mockPostHog.autocapture.mock.calls[0][1][0]
86+
// Should capture regular styles but skip animated values
87+
expect(capturedElement.attr__style).toContain('backgroundColor:red')
88+
expect(capturedElement.attr__style).toContain('padding:10')
89+
expect(capturedElement.attr__style).not.toContain('opacity')
90+
})
2291
})
2392
})

0 commit comments

Comments
 (0)