Skip to content

Commit 08373d8

Browse files
authored
Merge branch 'main' into dependabot/github_actions/actions/checkout-6
2 parents 6ea591d + fac9fc7 commit 08373d8

File tree

7 files changed

+105
-34
lines changed

7 files changed

+105
-34
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Next
22

3+
## 3.35.1 - 2025-12-02
4+
5+
- fix: avoid memory leaks on foat conversions ([#401](https://github.com/PostHog/posthog-ios/pull/401))
6+
- fix: app group migration now skips identity-related keys from extensions ([#402](https://github.com/PostHog/posthog-ios/pull/402))
7+
38
## 3.35.0 - 2025-11-07
49

510
- fix: call the flags api with the correct groups key name (the api has a back compatible fix already) ([#389](https://github.com/PostHog/posthog-ios/pull/389))

PostHog.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = "PostHog"
3-
s.version = "3.35.0"
3+
s.version = "3.35.1"
44
s.summary = "The hassle-free way to add posthog to your iOS app."
55

66
s.description = <<-DESC

PostHog/PostHogStorage.swift

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ func getBundleIdentifier() -> String {
7676
#endif
7777
}
7878

79+
/**
80+
Determines if the current process is an extension target.
81+
82+
App extensions have bundle paths ending in ".appex"
83+
*/
84+
func isExtension() -> Bool {
85+
Bundle.main.bundlePath.hasSuffix(".appex")
86+
}
87+
7988
/**
8089
Merges content from a legacy container directory into the current app group container.
8190

@@ -84,7 +93,7 @@ func getBundleIdentifier() -> String {
8493

8594
Migration rules:
8695
- Files that already exist at the destination are skipped (no overwrite)
87-
- The anonymousId from the first processed container (legacy or current) is preserved to maintain user identity
96+
- Identity-related keys (distinctId, anonymousId, etc.) are only migrated from the main app target
8897
- Successfully migrated files are deleted from the source
8998
- Empty directories are cleaned up after migration
9099
- The entire folder structure is preserved during migration
@@ -99,10 +108,26 @@ func mergeLegacyContainerIfNeeded(within libraryUrl: URL?, to destinationUrl: UR
99108
return
100109
}
101110

102-
hedgeLog("Legacy folder found at \(sourceUrl), merging...")
111+
let skipKeys: [PostHogStorage.StorageKey]
112+
if isExtension() {
113+
// Extensions should skip migrating identity-related keys to ensure consistent user identity with main app target
114+
skipKeys = [
115+
.distinctId,
116+
.anonymousId,
117+
.isIdentified,
118+
.groups,
119+
.registerProperties,
120+
.personPropertiesForFlags,
121+
.groupPropertiesForFlags,
122+
]
123+
hedgeLog("Legacy folder found at \(sourceUrl), merging from extension... (skipping \(skipKeys.count) identity keys)")
124+
} else {
125+
skipKeys = []
126+
hedgeLog("Legacy folder found at \(sourceUrl), merging from main app... (migrating all keys)")
127+
}
103128

104-
// Migrate all contents from the legacy container
105-
migrateDirectoryContents(from: sourceUrl, to: destinationUrl)
129+
// Migrate contents from the legacy container
130+
migrateDirectoryContents(from: sourceUrl, to: destinationUrl, skipKeys: skipKeys)
106131

107132
// Try to remove the source directory if it's empty
108133
if removeIfEmpty(sourceUrl) {
@@ -141,10 +166,11 @@ func removeIfEmpty(_ url: URL) -> Bool {
141166
- Parameters:
142167
- sourceFile: The source file URL
143168
- destinationFile: The destination file URL
169+
- skipCopy: Wether to skip copying file to desitnation
144170
- Throws: Any errors that occur during file operations
145171
*/
146-
func migrateFile(from sourceFile: URL, to destinationFile: URL) throws {
147-
if !FileManager.default.fileExists(atPath: destinationFile.path) {
172+
func migrateFile(from sourceFile: URL, to destinationFile: URL, skipCopy: Bool) throws {
173+
if !skipCopy, !FileManager.default.fileExists(atPath: destinationFile.path) {
148174
try FileManager.default.copyItem(at: sourceFile, to: destinationFile)
149175
}
150176
// Always delete source file after processing (whether copied or skipped)
@@ -157,8 +183,9 @@ func migrateFile(from sourceFile: URL, to destinationFile: URL) throws {
157183
- Parameters:
158184
- sourceDir: The source directory URL
159185
- destinationDir: The destination directory URL
186+
- skipKeys: Array of storage keys that should be skipped during migration
160187
*/
161-
func migrateDirectoryContents(from sourceDir: URL, to destinationDir: URL) {
188+
func migrateDirectoryContents(from sourceDir: URL, to destinationDir: URL, skipKeys: [PostHogStorage.StorageKey] = []) {
162189
do {
163190
// Create destination directory if it doesn't exist (we need to call this here again as the function is recursive)
164191
createDirectoryAtURLIfNeeded(url: destinationDir)
@@ -174,13 +201,16 @@ func migrateDirectoryContents(from sourceDir: URL, to destinationDir: URL) {
174201
if FileManager.default.fileExists(atPath: item.path, isDirectory: &isDirectory) {
175202
if isDirectory.boolValue {
176203
// Recursively migrate subdirectory (preserving the folder structure)
177-
migrateDirectoryContents(from: item, to: destinationItem)
204+
migrateDirectoryContents(from: item, to: destinationItem, skipKeys: skipKeys)
178205
// Remove empty directory after migration
179206
removeIfEmpty(item)
180207
} else {
208+
let fileName = item.lastPathComponent
209+
let shouldSkip = skipKeys.contains(where: { $0.rawValue == fileName })
210+
181211
// Migrate file
182212
do {
183-
try migrateFile(from: item, to: destinationItem)
213+
try migrateFile(from: item, to: destinationItem, skipCopy: shouldSkip)
184214
} catch {
185215
hedgeLog("Failed to migrate file from \(item.path) to \(destinationItem.path): \(error)")
186216
}

PostHog/PostHogVersion.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010
// if you change this, make sure to also change it in the podspec and check if the script scripts/bump-version.sh still works
1111
// This property is internal only
12-
public var postHogVersion = "3.35.0"
12+
public var postHogVersion = "3.35.1"
1313

1414
public let postHogiOSSdkName = "posthog-ios"
1515
// This property is internal only

PostHog/Replay/Float+Util.swift

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@
77

88
import Foundation
99

10-
extension CGFloat {
11-
func toInt() -> Int {
12-
NSNumber(value: rounded()).intValue
13-
}
14-
}
15-
16-
extension Double {
17-
func toInt() -> Int {
18-
NSNumber(value: rounded()).intValue
10+
extension BinaryFloatingPoint {
11+
func toInt() -> Int? {
12+
guard isFinite else { return nil }
13+
guard self >= Self(Int.min), self <= Self(Int.max) else { return nil }
14+
// Since this is used primarily for UI size & position, rounding vs truncating makes sense
15+
return Int(rounded())
1916
}
2017
}

PostHog/Replay/PostHogReplayIntegration.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@
276276
continue
277277
}
278278

279-
let posX = touch.location.x.toInt()
280-
let posY = touch.location.y.toInt()
279+
let posX = touch.location.x.toInt() ?? 0
280+
let posY = touch.location.y.toInt() ?? 0
281281

282282
// if the id is 0, BE transformer will set it to the virtual bodyId
283283
let touchData: [String: Any] = ["id": 0, "pointerType": 2, "source": 2, "type": type, "x": posX, "y": posY]
@@ -318,8 +318,8 @@
318318

319319
if !snapshotStatus.sentMetaEvent {
320320
let size = window.bounds.size
321-
let width = size.width.toInt()
322-
let height = size.height.toInt()
321+
let width = size.width.toInt() ?? 0
322+
let height = size.height.toInt() ?? 0
323323

324324
var data: [String: Any] = ["width": width, "height": height]
325325

@@ -393,10 +393,10 @@
393393
let frame = view.toAbsoluteRect(view.window)
394394

395395
wireframe.id = view.hash
396-
wireframe.posX = frame.origin.x.toInt()
397-
wireframe.posY = frame.origin.y.toInt()
398-
wireframe.width = frame.size.width.toInt()
399-
wireframe.height = frame.size.height.toInt()
396+
wireframe.posX = frame.origin.x.toInt() ?? 0
397+
wireframe.posY = frame.origin.y.toInt() ?? 0
398+
wireframe.width = frame.size.width.toInt() ?? 0
399+
wireframe.height = frame.size.height.toInt() ?? 0
400400

401401
return wireframe
402402
}

PostHogTests/UtilsTest.swift

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,22 @@ import Testing
1414
struct UtilsTest {
1515
@Suite("CGFloat Tests")
1616
struct CGFloatTests {
17-
@Test("safely converts NaN to Int")
17+
@Test("safely handles NaN value")
1818
func safelyConvertsNanToInt() {
1919
let nanNumber = CGFloat.nan
20-
#expect(nanNumber.toInt() == 0)
20+
#expect(nanNumber.toInt() == nil)
2121
}
2222

23-
@Test("safely converts Max to Int and deals with overflow")
23+
@Test("safely handles Max values")
2424
func safelyConvertsMaxToIntAndDealsWithOverflow() {
2525
let gfmNumber = CGFloat.greatestFiniteMagnitude
26-
#expect(gfmNumber.toInt() == Int.max)
26+
#expect(gfmNumber.toInt() == nil)
27+
}
28+
29+
@Test("safely handles infinity")
30+
func safelyHandlesInfinity() {
31+
let infNumber = CGFloat.infinity
32+
#expect(infNumber.toInt() == nil)
2733
}
2834

2935
@Test("safely converts to Int and rounds value")
@@ -38,13 +44,19 @@ struct UtilsTest {
3844
@Test("safely converts NaN to Int")
3945
func safelyConvertsNanToInt() {
4046
let nanNumber = Double.nan
41-
#expect(nanNumber.toInt() == 0)
47+
#expect(nanNumber.toInt() == nil)
4248
}
4349

4450
@Test("safely converts Max to Int and deals with overflow")
4551
func safelyConvertsMaxToIntAndDealsWithOverflow() {
4652
let gfmNumber = Double.greatestFiniteMagnitude
47-
#expect(gfmNumber.toInt() == Int.max)
53+
#expect(gfmNumber.toInt() == nil)
54+
}
55+
56+
@Test("safely handles infinity")
57+
func safelyHandlesInfinity() {
58+
let infNumber = Double.infinity
59+
#expect(infNumber.toInt() == nil)
4860
}
4961

5062
@Test("safely converts to Int and rounds value")
@@ -54,6 +66,33 @@ struct UtilsTest {
5466
}
5567
}
5668

69+
@Suite("Float Tests")
70+
struct FloatTests {
71+
@Test("safely converts NaN to Int")
72+
func safelyConvertsNanToInt() {
73+
let nanNumber = Float.nan
74+
#expect(nanNumber.toInt() == nil)
75+
}
76+
77+
@Test("safely converts Max to Int and deals with overflow")
78+
func safelyConvertsMaxToIntAndDealsWithOverflow() {
79+
let gfmNumber = Float.greatestFiniteMagnitude
80+
#expect(gfmNumber.toInt() == nil)
81+
}
82+
83+
@Test("safely handles infinity")
84+
func safelyHandlesInfinity() {
85+
let infNumber = Float.infinity
86+
#expect(infNumber.toInt() == nil)
87+
}
88+
89+
@Test("safely converts to Int and rounds value")
90+
func safelyConvertsToIntAndRoundsValue() {
91+
let frNumber: Float = 123456.5
92+
#expect(frNumber.toInt() == 123457)
93+
}
94+
}
95+
5796
@Suite("Date format tests")
5897
struct DateTests {
5998
@Test("can parse ISO8601 date with microsecond precision")

0 commit comments

Comments
 (0)