@plushanalytics/react-native-session-replay 3.0.2 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -112,10 +112,14 @@ Legacy flat replay keys are still supported in per-session overrides for backwar
|
|
|
112
112
|
|
|
113
113
|
## Replay Authentication Headers
|
|
114
114
|
|
|
115
|
-
Replay uploads include
|
|
115
|
+
Replay uploads include these headers from the configured `apiKey`:
|
|
116
116
|
|
|
117
117
|
- `x-api-key`
|
|
118
118
|
- `x-plush-api-key`
|
|
119
|
+
- `x-plushanalytics-key`
|
|
120
|
+
- `Authorization: Bearer <apiKey>`
|
|
121
|
+
|
|
122
|
+
Headers are injected natively on outgoing replay batch requests so they remain present even if underlying SDK session defaults are rewritten.
|
|
119
123
|
|
|
120
124
|
## Common Usage
|
|
121
125
|
|
|
@@ -103,6 +103,7 @@ class PosthogReactNativeSessionReplayModule(
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
PostHogAndroid.setup(context, config)
|
|
106
|
+
ensureReplayClientsAuthHeaders(apiKey)
|
|
106
107
|
setIdentify(config.cachePreferences, distinctId, anonymousId)
|
|
107
108
|
} catch (e: Throwable) {
|
|
108
109
|
logError("start", e)
|
|
@@ -128,6 +129,7 @@ class PosthogReactNativeSessionReplayModule(
|
|
|
128
129
|
val uuid = UUID.fromString(sessionId)
|
|
129
130
|
PostHogSessionManager.setSessionId(uuid)
|
|
130
131
|
PostHog.startSession()
|
|
132
|
+
PostHog.startSessionReplay(false)
|
|
131
133
|
} catch (e: Throwable) {
|
|
132
134
|
logError("startSession", e)
|
|
133
135
|
} finally {
|
|
@@ -207,12 +209,67 @@ class PosthogReactNativeSessionReplayModule(
|
|
|
207
209
|
.newBuilder()
|
|
208
210
|
.header("x-api-key", apiKey)
|
|
209
211
|
.header("x-plush-api-key", apiKey)
|
|
212
|
+
.header("x-plushanalytics-key", apiKey)
|
|
213
|
+
.header("authorization", "Bearer $apiKey")
|
|
210
214
|
.build()
|
|
211
215
|
chain.proceed(updated)
|
|
212
216
|
}
|
|
213
217
|
).build()
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
private fun ensureReplayClientsAuthHeaders(apiKey: String) {
|
|
221
|
+
try {
|
|
222
|
+
val sharedMethod = PostHog::class.java.getDeclaredMethod("access\$getShared\$cp")
|
|
223
|
+
sharedMethod.isAccessible = true
|
|
224
|
+
val sharedInstance = sharedMethod.invoke(null) ?: return
|
|
225
|
+
|
|
226
|
+
patchQueueApiClient(sharedInstance, "queue", apiKey)
|
|
227
|
+
patchQueueApiClient(sharedInstance, "replayQueue", apiKey)
|
|
228
|
+
} catch (e: Throwable) {
|
|
229
|
+
logError("ensureReplayClientsAuthHeaders", e)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private fun patchQueueApiClient(
|
|
234
|
+
sharedInstance: Any,
|
|
235
|
+
queueFieldName: String,
|
|
236
|
+
apiKey: String,
|
|
237
|
+
) {
|
|
238
|
+
try {
|
|
239
|
+
val queueField = sharedInstance.javaClass.getDeclaredField(queueFieldName)
|
|
240
|
+
queueField.isAccessible = true
|
|
241
|
+
val queue = queueField.get(sharedInstance) ?: return
|
|
242
|
+
|
|
243
|
+
val apiField = queue.javaClass.getDeclaredField("api")
|
|
244
|
+
apiField.isAccessible = true
|
|
245
|
+
val api = apiField.get(queue) ?: return
|
|
246
|
+
|
|
247
|
+
val clientField = api.javaClass.getDeclaredField("client")
|
|
248
|
+
clientField.isAccessible = true
|
|
249
|
+
val originalClient = clientField.get(api) as? OkHttpClient ?: return
|
|
250
|
+
|
|
251
|
+
val patchedClient =
|
|
252
|
+
originalClient.newBuilder()
|
|
253
|
+
.addInterceptor(
|
|
254
|
+
Interceptor { chain ->
|
|
255
|
+
val updatedRequest =
|
|
256
|
+
chain.request()
|
|
257
|
+
.newBuilder()
|
|
258
|
+
.header("x-api-key", apiKey)
|
|
259
|
+
.header("x-plush-api-key", apiKey)
|
|
260
|
+
.header("x-plushanalytics-key", apiKey)
|
|
261
|
+
.header("authorization", "Bearer $apiKey")
|
|
262
|
+
.build()
|
|
263
|
+
chain.proceed(updatedRequest)
|
|
264
|
+
}
|
|
265
|
+
).build()
|
|
266
|
+
|
|
267
|
+
clientField.set(api, patchedClient)
|
|
268
|
+
} catch (e: Throwable) {
|
|
269
|
+
logError("patchQueueApiClient", e)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
216
273
|
private fun logError(
|
|
217
274
|
method: String,
|
|
218
275
|
error: Throwable,
|
|
@@ -1,14 +1,129 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import PostHog
|
|
3
3
|
|
|
4
|
-
private func
|
|
5
|
-
let normalizedHost: String
|
|
4
|
+
private func normalizedHost(from value: String) -> String {
|
|
6
5
|
if let url = URL(string: value), let host = url.host {
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
6
|
+
return host.lowercased()
|
|
7
|
+
}
|
|
8
|
+
return value.lowercased()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
private final class PlushReplayAuthURLProtocol: URLProtocol, URLSessionDataDelegate {
|
|
12
|
+
private static let handledKey = "PlushReplayAuthHandled"
|
|
13
|
+
private static var apiKey: String = ""
|
|
14
|
+
private static var allowedHost: String = ""
|
|
15
|
+
private static var allowedPathSuffixes: [String] = ["/v1/events/batch", "/batch"]
|
|
16
|
+
|
|
17
|
+
private var outboundDataTask: URLSessionDataTask?
|
|
18
|
+
private var outboundSession: URLSession?
|
|
19
|
+
|
|
20
|
+
static func configure(apiKey: String, host: String, snapshotPath: String) {
|
|
21
|
+
Self.apiKey = apiKey
|
|
22
|
+
Self.allowedHost = normalizedHost(from: host)
|
|
23
|
+
|
|
24
|
+
let trimmedPath = snapshotPath.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
25
|
+
let normalizedPath: String
|
|
26
|
+
if trimmedPath.isEmpty {
|
|
27
|
+
normalizedPath = "/v1/events/batch"
|
|
28
|
+
} else {
|
|
29
|
+
normalizedPath = trimmedPath.hasPrefix("/") ? trimmedPath : "/\(trimmedPath)"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var suffixes = ["/v1/events/batch", "/batch"]
|
|
33
|
+
if !suffixes.contains(normalizedPath) {
|
|
34
|
+
suffixes.insert(normalizedPath, at: 0)
|
|
35
|
+
}
|
|
36
|
+
Self.allowedPathSuffixes = suffixes
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override class func canInit(with request: URLRequest) -> Bool {
|
|
40
|
+
guard URLProtocol.property(forKey: handledKey, in: request) == nil else {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
guard let url = request.url else {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let requestHost = url.host?.lowercased() ?? ""
|
|
49
|
+
if !Self.allowedHost.isEmpty && requestHost != Self.allowedHost {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Self.allowedPathSuffixes.contains { suffix in
|
|
54
|
+
url.path.hasSuffix(suffix)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
|
59
|
+
request
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override func startLoading() {
|
|
63
|
+
guard let mutableRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {
|
|
64
|
+
client?.urlProtocol(self, didFailWithError: NSError(domain: "PlushReplayAuthURLProtocol", code: -1))
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
URLProtocol.setProperty(true, forKey: Self.handledKey, in: mutableRequest)
|
|
69
|
+
|
|
70
|
+
if !Self.apiKey.isEmpty {
|
|
71
|
+
mutableRequest.setValue(Self.apiKey, forHTTPHeaderField: "x-api-key")
|
|
72
|
+
mutableRequest.setValue(Self.apiKey, forHTTPHeaderField: "x-plush-api-key")
|
|
73
|
+
mutableRequest.setValue(Self.apiKey, forHTTPHeaderField: "x-plushanalytics-key")
|
|
74
|
+
mutableRequest.setValue("Bearer \(Self.apiKey)", forHTTPHeaderField: "authorization")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let outboundRequest = mutableRequest as URLRequest
|
|
78
|
+
let configuration = URLSessionConfiguration.default
|
|
79
|
+
configuration.protocolClasses = []
|
|
80
|
+
|
|
81
|
+
outboundSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
|
|
82
|
+
outboundDataTask = outboundSession?.dataTask(with: outboundRequest)
|
|
83
|
+
outboundDataTask?.resume()
|
|
10
84
|
}
|
|
11
85
|
|
|
86
|
+
override func stopLoading() {
|
|
87
|
+
outboundDataTask?.cancel()
|
|
88
|
+
outboundDataTask = nil
|
|
89
|
+
outboundSession?.invalidateAndCancel()
|
|
90
|
+
outboundSession = nil
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func urlSession(
|
|
94
|
+
_ session: URLSession,
|
|
95
|
+
dataTask: URLSessionDataTask,
|
|
96
|
+
didReceive response: URLResponse,
|
|
97
|
+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
|
98
|
+
) {
|
|
99
|
+
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
|
100
|
+
completionHandler(.allow)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func urlSession(
|
|
104
|
+
_ session: URLSession,
|
|
105
|
+
dataTask: URLSessionDataTask,
|
|
106
|
+
didReceive data: Data
|
|
107
|
+
) {
|
|
108
|
+
client?.urlProtocol(self, didLoad: data)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func urlSession(
|
|
112
|
+
_ session: URLSession,
|
|
113
|
+
task: URLSessionTask,
|
|
114
|
+
didCompleteWithError error: Error?
|
|
115
|
+
) {
|
|
116
|
+
if let error {
|
|
117
|
+
client?.urlProtocol(self, didFailWithError: error)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
client?.urlProtocolDidFinishLoading(self)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func isPosthogHost(_ value: String) -> Bool {
|
|
126
|
+
let normalizedHost = normalizedHost(from: value)
|
|
12
127
|
return normalizedHost == "posthog.com" || normalizedHost.hasSuffix(".posthog.com")
|
|
13
128
|
}
|
|
14
129
|
|
|
@@ -56,11 +171,25 @@ class PosthogReactNativeSessionReplay: NSObject {
|
|
|
56
171
|
config.debug = debug
|
|
57
172
|
config.sessionReplayConfig.screenshotMode = true
|
|
58
173
|
|
|
174
|
+
let endpoint = decideReplayConfig["endpoint"] as? String ?? ""
|
|
175
|
+
if !endpoint.isEmpty {
|
|
176
|
+
config.snapshotEndpoint = endpoint
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
PlushReplayAuthURLProtocol.configure(
|
|
180
|
+
apiKey: apiKey,
|
|
181
|
+
host: host,
|
|
182
|
+
snapshotPath: config.snapshotEndpoint
|
|
183
|
+
)
|
|
184
|
+
|
|
59
185
|
let sessionConfiguration = URLSessionConfiguration.default
|
|
60
186
|
sessionConfiguration.httpAdditionalHeaders = [
|
|
61
187
|
"x-api-key": apiKey,
|
|
62
188
|
"x-plush-api-key": apiKey,
|
|
189
|
+
"x-plushanalytics-key": apiKey,
|
|
190
|
+
"authorization": "Bearer \(apiKey)",
|
|
63
191
|
]
|
|
192
|
+
sessionConfiguration.protocolClasses = [PlushReplayAuthURLProtocol.self]
|
|
64
193
|
config.urlSessionConfiguration = sessionConfiguration
|
|
65
194
|
|
|
66
195
|
if #available(iOS 15.0, *) {
|
|
@@ -88,11 +217,6 @@ class PosthogReactNativeSessionReplay: NSObject {
|
|
|
88
217
|
let captureNetworkTelemetry = sdkReplayConfig["captureNetworkTelemetry"] as? Bool ?? true
|
|
89
218
|
config.sessionReplayConfig.captureNetworkTelemetry = captureNetworkTelemetry
|
|
90
219
|
|
|
91
|
-
let endpoint = decideReplayConfig["endpoint"] as? String ?? ""
|
|
92
|
-
if !endpoint.isEmpty {
|
|
93
|
-
config.snapshotEndpoint = endpoint
|
|
94
|
-
}
|
|
95
|
-
|
|
96
220
|
let distinctId = sdkOptions["distinctId"] as? String ?? ""
|
|
97
221
|
let anonymousId = sdkOptions["anonymousId"] as? String ?? ""
|
|
98
222
|
|
package/package.json
CHANGED