@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 both headers from the configured `apiKey`:
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 isPosthogHost(_ value: String) -> Bool {
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
- normalizedHost = host.lowercased()
8
- } else {
9
- normalizedHost = value.lowercased()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plushanalytics/react-native-session-replay",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "React Native SDK for Plush Analytics events + session replay ingestion.",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",