From ff05a7a9835eb2a03966a43af63d936716d464e2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Date: Thu, 28 Nov 2024 08:29:10 +0100 Subject: [PATCH] fix: flutter session replay (#266) --- CHANGELOG.md | 2 ++ PostHog.xcodeproj/project.pbxproj | 4 +++ PostHog/PostHogSessionManager.swift | 15 ++++------ PostHog/Replay/Date+Util.swift | 4 +++ PostHog/Replay/PostHogReplayIntegration.swift | 18 ++++++++---- PostHog/Replay/RRWireframe.swift | 15 ++-------- PostHog/Replay/UIImage+Util.swift | 28 +++++++++++++++++++ PostHog/Utils/Data+Gzip.swift | 2 +- PostHog/Utils/Reachability.swift | 4 +-- PostHog/Utils/UUIDUtils.swift | 2 +- 10 files changed, 63 insertions(+), 31 deletions(-) create mode 100644 PostHog/Replay/UIImage+Util.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 1791cb0c4..8c2b62dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Next +- no user facing changes + ## 3.15.7 - 2024-11-25 - fix: detect and mask out system photo library and user photos ([#261](https://github.com/PostHog/posthog-ios/pull/261)) diff --git a/PostHog.xcodeproj/project.pbxproj b/PostHog.xcodeproj/project.pbxproj index 25eed903c..c61a367ed 100644 --- a/PostHog.xcodeproj/project.pbxproj +++ b/PostHog.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ 6999919A2AFE1BAB000DCB78 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699991992AFE1BAB000DCB78 /* AppDelegate.swift */; }; 699C5FE62C20178E007DB818 /* UUIDUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699C5FE52C20178E007DB818 /* UUIDUtils.swift */; }; 699C5FEF2C20242A007DB818 /* UUIDTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 699C5FEE2C20242A007DB818 /* UUIDTest.swift */; }; + 69B7F60C2CF7703400A48BCC /* UIImage+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B7F6062CF7702D00A48BCC /* UIImage+Util.swift */; }; 69BA38D72B888E8500AA69D6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */; }; 69ED1A5C2C7F15F300FE7A91 /* PostHogSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ED1A5B2C7F15F300FE7A91 /* PostHogSessionManager.swift */; }; 69ED1A882C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69ED1A872C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift */; }; @@ -358,6 +359,7 @@ 699991992AFE1BAB000DCB78 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 699C5FE52C20178E007DB818 /* UUIDUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDUtils.swift; sourceTree = ""; }; 699C5FEE2C20242A007DB818 /* UUIDTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDTest.swift; sourceTree = ""; }; + 69B7F6062CF7702D00A48BCC /* UIImage+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Util.swift"; sourceTree = ""; }; 69BA38D62B888E8500AA69D6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 69ED1A5B2C7F15F300FE7A91 /* PostHogSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSessionManager.swift; sourceTree = ""; }; 69ED1A872C89B73100FE7A91 /* PostHogSwiftUIViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSwiftUIViewModifiers.swift; sourceTree = ""; }; @@ -723,6 +725,7 @@ 69EE82B82BA9C4DA00EB9542 /* Replay */ = { isa = PBXGroup; children = ( + 69B7F6062CF7702D00A48BCC /* UIImage+Util.swift */, 69EE82B92BA9C50400EB9542 /* PostHogReplayIntegration.swift */, 69EE82BB2BA9C53000EB9542 /* PostHogSessionReplayConfig.swift */, 69EE82BD2BA9C8AA00EB9542 /* ViewLayoutTracker.swift */, @@ -1185,6 +1188,7 @@ 69ED1A9F2C8F451B00FE7A91 /* PostHogPersonProfiles.swift in Sources */, 69EE82BA2BA9C50400EB9542 /* PostHogReplayIntegration.swift in Sources */, 3AE3FB472992AB0000AFFC18 /* Hedgelog.swift in Sources */, + 69B7F60C2CF7703400A48BCC /* UIImage+Util.swift in Sources */, 69261D132AD5685B00232EC7 /* PostHogFeatureFlags.swift in Sources */, 699C5FE62C20178E007DB818 /* UUIDUtils.swift in Sources */, 690B2DF32C205B5600AE3B45 /* TimeBasedEpochGenerator.swift in Sources */, diff --git a/PostHog/PostHogSessionManager.swift b/PostHog/PostHogSessionManager.swift index c85e04e27..1d4ef2e10 100644 --- a/PostHog/PostHogSessionManager.swift +++ b/PostHog/PostHogSessionManager.swift @@ -46,14 +46,13 @@ import Foundation timeNow - sessionLastTimestamp > sessionChangeThreshold } - private func isiOSNativeSdk() -> Bool { - // postHogSdkName will be set to eg posthog-react-native if not - postHogSdkName == postHogiOSSdkName + private func isNotReactNative() -> Bool { + // for the RN SDK, the session is handled by the RN SDK itself + postHogSdkName != "posthog-react-native" } func resetSessionIfExpired(_ completion: () -> Void) { - // for hybrid SDKs, the session is handled by the hybrid SDK - guard isiOSNativeSdk() else { + guard isNotReactNative() else { return } @@ -89,8 +88,7 @@ import Foundation } func rotateSessionIdIfRequired(_ completion: @escaping (() -> Void)) { - // for hybrid SDKs, the session is handled by the hybrid SDK - guard isiOSNativeSdk() else { + guard isNotReactNative() else { return } @@ -109,8 +107,7 @@ import Foundation } func updateSessionLastTime() { - // for hybrid SDKs, the session is handled by the hybrid SDK - guard isiOSNativeSdk() else { + guard isNotReactNative() else { return } diff --git a/PostHog/Replay/Date+Util.swift b/PostHog/Replay/Date+Util.swift index dedfdc906..4cb98d9dd 100644 --- a/PostHog/Replay/Date+Util.swift +++ b/PostHog/Replay/Date+Util.swift @@ -12,3 +12,7 @@ extension Date { Int64(timeIntervalSince1970 * 1000) } } + +public func dateToMillis(_ date: Date) -> Int64 { + date.toMillis() +} diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index 0ce10b0b9..095dd820f 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -99,14 +99,22 @@ } } + private func isNotFlutter() -> Bool { + postHogSdkName != "posthog-flutter" + } + func start() { stopTimer() - DispatchQueue.main.async { - self.timer = Timer.scheduledTimer(withTimeInterval: self.config.sessionReplayConfig.debouncerDelay, repeats: true, block: { _ in - self.snapshot() - }) + + // flutter captures snapshots, so we don't need to capture them here + if isNotFlutter() { + DispatchQueue.main.async { + self.timer = Timer.scheduledTimer(withTimeInterval: self.config.sessionReplayConfig.debouncerDelay, repeats: true, block: { _ in + self.snapshot() + }) + } + ViewLayoutTracker.swizzleLayoutSubviews() } - ViewLayoutTracker.swizzleLayoutSubviews() UIApplicationTracker.swizzleSendEvent() diff --git a/PostHog/Replay/RRWireframe.swift b/PostHog/Replay/RRWireframe.swift index ef5adfe32..bd6553aa1 100644 --- a/PostHog/Replay/RRWireframe.swift +++ b/PostHog/Replay/RRWireframe.swift @@ -38,17 +38,6 @@ class RRWireframe { var parentId: Int? #if os(iOS) - private func imageToBase64(_ image: UIImage) -> String? { - let jpegData = image.jpegData(compressionQuality: 0.3) - let base64 = jpegData?.base64EncodedString() - - if let base64 = base64 { - return "data:image/jpeg;base64,\(base64)" - } - - return nil - } - private func maskImage() -> UIImage? { if let image = image { // the scale also affects the image size/resolution, from usually 100kb to 15kb each @@ -106,9 +95,9 @@ class RRWireframe { #if os(iOS) if let image = image { if let maskedImage = maskImage() { - base64 = imageToBase64(maskedImage) + base64 = maskedImage.toBase64() } else { - base64 = imageToBase64(image) + base64 = image.toBase64() } } #endif diff --git a/PostHog/Replay/UIImage+Util.swift b/PostHog/Replay/UIImage+Util.swift new file mode 100644 index 000000000..d511f0447 --- /dev/null +++ b/PostHog/Replay/UIImage+Util.swift @@ -0,0 +1,28 @@ +// +// UIImage+Util.swift +// PostHog +// +// Created by Manoel Aranda Neto on 27.11.24. +// + +#if os(iOS) + import Foundation + import UIKit + + extension UIImage { + func toBase64(_ compressionQuality: CGFloat = 0.3) -> String? { + let jpegData = jpegData(compressionQuality: compressionQuality) + let base64 = jpegData?.base64EncodedString() + + if let base64 = base64 { + return "data:image/jpeg;base64,\(base64)" + } + + return nil + } + } + + public func imageToBase64(_ image: UIImage, _ compressionQuality: CGFloat = 0.3) -> String? { + image.toBase64(compressionQuality) + } +#endif diff --git a/PostHog/Utils/Data+Gzip.swift b/PostHog/Utils/Data+Gzip.swift index 37a76f34f..09498a64c 100644 --- a/PostHog/Utils/Data+Gzip.swift +++ b/PostHog/Utils/Data+Gzip.swift @@ -134,7 +134,7 @@ private extension GzipError.Kind { } } -public extension Data { +extension Data { /// Whether the receiver is compressed in gzip format. var isGzipped: Bool { starts(with: [0x1F, 0x8B]) // check magic number diff --git a/PostHog/Utils/Reachability.swift b/PostHog/Utils/Reachability.swift index 15ca1bf38..b405edcab 100644 --- a/PostHog/Utils/Reachability.swift +++ b/PostHog/Utils/Reachability.swift @@ -41,7 +41,7 @@ import Foundation @available(*, unavailable, renamed: "Notification.Name.reachabilityChanged") public let ReachabilityChangedNotification = NSNotification.Name("ReachabilityChangedNotification") - public extension Notification.Name { + extension Notification.Name { static let reachabilityChanged = Notification.Name("reachabilityChanged") } @@ -162,7 +162,7 @@ import Foundation } } - public extension Reachability { + extension Reachability { // MARK: - *** Notifier methods *** func startNotifier() throws { diff --git a/PostHog/Utils/UUIDUtils.swift b/PostHog/Utils/UUIDUtils.swift index 437ece52f..66c3dd268 100644 --- a/PostHog/Utils/UUIDUtils.swift +++ b/PostHog/Utils/UUIDUtils.swift @@ -10,7 +10,7 @@ import Foundation -public extension UUID { +extension UUID { static func v7() -> Self { TimeBasedEpochGenerator.shared.v7() }