Skip to content

Commit

Permalink
chore: session replay (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Mar 27, 2024
1 parent d127599 commit e24943d
Show file tree
Hide file tree
Showing 53 changed files with 2,365 additions and 115 deletions.
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.

2 changes: 1 addition & 1 deletion PostHog.xcodeproj/xcshareddata/xcschemes/PostHog.xcscheme
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

0 comments on commit e24943d

Please sign in to comment.