Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: session replay #115

Merged
merged 21 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Next

- iOS session recording - very first alpha release [#115](https://github.com/PostHog/posthog-ios/pull/115)

## 3.2.4 - 2024-03-12

- `maxQueueSize` wasn't respected when capturing events [#116](https://github.com/PostHog/posthog-ios/pull/116)
Expand Down
286 changes: 281 additions & 5 deletions PostHog.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "69F517F72BAC768100F52C14"
BuildableName = "PostHogExampleStoryboard.app"
BlueprintName = "PostHogExampleStoryboard"
ReferencedContainer = "container:PostHog.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "69F517F72BAC768100F52C14"
BuildableName = "PostHogExampleStoryboard.app"
BlueprintName = "PostHogExampleStoryboard"
ReferencedContainer = "container:PostHog.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "69F517F72BAC768100F52C14"
BuildableName = "PostHogExampleStoryboard.app"
BlueprintName = "PostHogExampleStoryboard"
ReferencedContainer = "container:PostHog.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
LastUpgradeVersion = "1530"
version = "2.2">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
19 changes: 16 additions & 3 deletions PostHog/Models/PostHogEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,25 @@ public class PostHogEvent {
public var properties: [String: Any]
public var timestamp: Date
public var uuid: UUID
// Only used for Replay
public var apiKey: String?

enum Key: String {
case event
case distinctId
case properties
case timestamp
case uuid
case apiKey
}

init(event: String, distinctId: String, properties: [String: Any]? = nil, timestamp: Date = Date(), uuid: UUID = .init()) {
init(event: String, distinctId: String, properties: [String: Any]? = nil, timestamp: Date = Date(), uuid: UUID = .init(), apiKey: String? = nil) {
self.event = event
self.distinctId = distinctId
self.properties = properties ?? [:]
self.timestamp = timestamp
self.uuid = uuid
self.apiKey = apiKey
}

// NOTE: Ideally we would use the NSCoding behaviour but it gets needlessly complex
Expand Down Expand Up @@ -60,22 +64,31 @@ public class PostHogEvent {
let uuid = ((json["uuid"] as? String) ?? (json["message_id"] as? String)) ?? UUID().uuidString
let uuidObj = UUID(uuidString: uuid) ?? UUID()

let apiKey = json["api_key"] as? String

return PostHogEvent(
event: event,
distinctId: distinctId,
properties: properties,
timestamp: timestampDate,
uuid: uuidObj
uuid: uuidObj,
apiKey: apiKey
)
}

func toJSON() -> [String: Any] {
[
var json: [String: Any] = [
"event": event,
"distinct_id": distinctId,
"properties": properties,
"timestamp": toISO8601String(timestamp),
"uuid": uuid.uuidString,
]

if let apiKey = apiKey {
json["api_key"] = apiKey
}

return json
}
}
63 changes: 61 additions & 2 deletions PostHog/PostHogApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ class PostHogApi {
let httpResponse = response as! HTTPURLResponse

if !(200 ... 299 ~= httpResponse.statusCode) {
let errorMessage = "Error sending events to batch API: status: \(httpResponse.statusCode), body: \(String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]))."
let jsonBody = String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any])
let errorMessage = "Error sending events to batch API: status: \(jsonBody)."
hedgeLog(errorMessage)
} else {
hedgeLog("Events sent successfully.")
Expand All @@ -91,6 +92,63 @@ class PostHogApi {
}.resume()
}

func snapshot(events: [PostHogEvent], completion: @escaping (PostHogBatchUploadInfo) -> Void) {
guard let url = URL(string: config.snapshotEndpoint, relativeTo: config.host) else {
hedgeLog("Malformed snapshot URL error.")
return completion(PostHogBatchUploadInfo(statusCode: nil, error: nil))
}

for event in events {
event.apiKey = self.config.apiKey
}

let config = sessionConfig()
var headers = config.httpAdditionalHeaders ?? [:]
headers["Accept-Encoding"] = "gzip"
headers["Content-Encoding"] = "gzip"
config.httpAdditionalHeaders = headers

let request = getURL(url)

let toSend = events.map { $0.toJSON() }

var data: Data?

do {
data = try JSONSerialization.data(withJSONObject: toSend)
} catch {
hedgeLog("Error parsing the snapshot body: \(error)")
return completion(PostHogBatchUploadInfo(statusCode: nil, error: error))
}

var gzippedPayload: Data?
do {
gzippedPayload = try data!.gzipped()
} catch {
hedgeLog("Error gzipping the snapshot body: \(error).")
return completion(PostHogBatchUploadInfo(statusCode: nil, error: error))
}

URLSession(configuration: config).uploadTask(with: request, from: gzippedPayload!) { data, response, error in
if error != nil {
hedgeLog("Error calling the snapshot API: \(String(describing: error)).")
return completion(PostHogBatchUploadInfo(statusCode: nil, error: error))
}

let httpResponse = response as! HTTPURLResponse

if !(200 ... 299 ~= httpResponse.statusCode) {
let jsonBody = String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any])
let errorMessage = "Error sending events to snapshot API: status: \(httpResponse.statusCode), body: \(jsonBody)."
hedgeLog(errorMessage)
} else {
hedgeLog("Snapshots sent successfully.")
}

return completion(PostHogBatchUploadInfo(statusCode: httpResponse.statusCode, error: error))
}.resume()
}

func decide(
distinctId: String,
anonymousId: String,
Expand Down Expand Up @@ -135,7 +193,8 @@ class PostHogApi {
let httpResponse = response as! HTTPURLResponse

if !(200 ... 299 ~= httpResponse.statusCode) {
let errorMessage = "Error calling decide API: status: \(httpResponse.statusCode), body: \(String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]))."
let jsonBody = String(describing: try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any])
let errorMessage = "Error calling decide API: status: \(httpResponse.statusCode), body: \(jsonBody)."
hedgeLog(errorMessage)

return completion(nil,
Expand Down
13 changes: 13 additions & 0 deletions PostHog/PostHogConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,21 @@ import Foundation
@objc public var captureScreenViews: Bool = true
@objc public var debug: Bool = false
@objc public var optOut: Bool = false
/// Internal
var snapshotEndpoint: String = "/s/"

public static let defaultHost: String = "https://app.posthog.com"

#if os(iOS)
/// Enable Recording of Session Replays for iOS
/// Experimental support
/// Default: false
@objc public var sessionReplay: Bool = false
/// Session Replay configuration
/// Experimental support
@objc public let sessionReplayConfig: PostHogSessionReplayConfig = .init()
#endif

// only internal
var disableReachabilityForTesting: Bool = false
var disableQueueTimerForTesting: Bool = false
Expand Down
14 changes: 14 additions & 0 deletions PostHog/PostHogFeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ class PostHogFeatureFlags {
}
let errorsWhileComputingFlags = data?["errorsWhileComputingFlags"] as? Bool ?? false

#if os(iOS)
if let sessionRecording = data?["sessionRecording"] as? Bool {
self.config.sessionReplay = self.config.sessionReplay && sessionRecording
} else if let sessionRecording = data?["sessionRecording"] as? [String: Any] {
// keeps the value from config.sessionReplay since having sessionRecording
// means its enabled on the project settings, but its only enabled
// when local config.sessionReplay is also enabled
if let endpoint = sessionRecording["endpoint"] as? String {
self.config.snapshotEndpoint = endpoint
}
// TODO: handle sessionRecording config such as consoleLogRecordingEnabled, networkPayloadCapture, sampleRate, etc
}
#endif

self.featureFlagsLock.withLock {
if errorsWhileComputingFlags {
let cachedFeatureFlags = self.storage.getDictionary(forKey: .enabledFeatureFlags) as? [String: Any] ?? [:]
Expand Down
2 changes: 1 addition & 1 deletion PostHog/PostHogFileBackedQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class PostHogFileBackedQueue {
items.count
}

init(queue: URL, oldQueue: URL) {
init(queue: URL, oldQueue: URL? = nil) {
self.queue = queue
setup(oldQueue: oldQueue)
}
Expand Down
Loading