@txlemetry/react-native-plugin 2.5.1
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/LICENSE +353 -0
- package/README.md +9 -0
- package/android/build.gradle +91 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/posthogreactnativeplugin/PosthogReactNativePluginModule.kt +638 -0
- package/android/src/main/java/com/posthogreactnativeplugin/PosthogReactNativePluginPackage.kt +13 -0
- package/ios/Package.swift +70 -0
- package/ios/PosthogReactNativePlugin-Bridging-Header.h +3 -0
- package/ios/PosthogReactNativePlugin.mm +77 -0
- package/ios/PosthogReactNativePlugin.swift +474 -0
- package/lib/commonjs/index.js +143 -0
- package/lib/module/index.js +124 -0
- package/lib/typescript/commonjs/package.json +3 -0
- package/lib/typescript/commonjs/src/index.d.ts +92 -0
- package/lib/typescript/module/package.json +3 -0
- package/lib/typescript/module/src/index.d.ts +92 -0
- package/package.json +121 -0
- package/src/index.tsx +238 -0
- package/txlemetry-react-native-plugin.podspec +69 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// swift-tools-version: 6.0
|
|
2
|
+
|
|
3
|
+
import PackageDescription
|
|
4
|
+
|
|
5
|
+
// React Native's SwiftPM autolinker references this package through
|
|
6
|
+
// build/generated/autolinking/libs/ReactNativePlugin. These paths are relative
|
|
7
|
+
// to that stable alias, not to node_modules.
|
|
8
|
+
let reactNativeDependencies: [Package.Dependency] = [
|
|
9
|
+
.package(name: "ReactNative", path: "../../../../xcframeworks"),
|
|
10
|
+
.package(name: "React-GeneratedCode", path: "../../../ios"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
let reactHeaderDependencies: [Target.Dependency] = [
|
|
14
|
+
.product(name: "ReactHeaders", package: "ReactNative"),
|
|
15
|
+
.product(name: "ReactNativeHeaders", package: "ReactNative"),
|
|
16
|
+
.product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"),
|
|
17
|
+
.product(name: "ReactAppHeaders", package: "React-GeneratedCode"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
let package = Package(
|
|
21
|
+
// This name must match React Native's Swift-name derivation for
|
|
22
|
+
// @posthog/react-native-plugin.
|
|
23
|
+
name: "ReactNativePlugin",
|
|
24
|
+
platforms: [.iOS(.v15)],
|
|
25
|
+
products: [
|
|
26
|
+
.library(
|
|
27
|
+
name: "ReactNativePlugin",
|
|
28
|
+
targets: ["PosthogReactNativePlugin", "PosthogReactNativePluginBridge"]
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
dependencies: reactNativeDependencies + [
|
|
32
|
+
.package(
|
|
33
|
+
url: "https://github.com/PostHog/posthog-ios.git",
|
|
34
|
+
.upToNextMinor(from: "3.69.10")
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
targets: [
|
|
38
|
+
.target(
|
|
39
|
+
name: "PosthogReactNativePlugin",
|
|
40
|
+
dependencies: reactHeaderDependencies + [
|
|
41
|
+
.product(name: "PostHog", package: "posthog-ios"),
|
|
42
|
+
],
|
|
43
|
+
path: ".",
|
|
44
|
+
exclude: [
|
|
45
|
+
"Package.swift",
|
|
46
|
+
"PosthogReactNativePlugin-Bridging-Header.h",
|
|
47
|
+
"PosthogReactNativePlugin.mm",
|
|
48
|
+
],
|
|
49
|
+
sources: ["PosthogReactNativePlugin.swift"]
|
|
50
|
+
),
|
|
51
|
+
// SwiftPM cannot compile Swift and Objective-C++ in one target. The
|
|
52
|
+
// RCT_EXTERN_MODULE registration bridge is independent and is linked
|
|
53
|
+
// into the same library product as the Swift implementation.
|
|
54
|
+
.target(
|
|
55
|
+
name: "PosthogReactNativePluginBridge",
|
|
56
|
+
dependencies: reactHeaderDependencies,
|
|
57
|
+
path: ".",
|
|
58
|
+
exclude: [
|
|
59
|
+
"Package.swift",
|
|
60
|
+
"PosthogReactNativePlugin.swift",
|
|
61
|
+
],
|
|
62
|
+
sources: ["PosthogReactNativePlugin.mm"],
|
|
63
|
+
publicHeadersPath: "."
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
// Match the CocoaPods integration's Swift 5 language mode. The plugin has
|
|
67
|
+
// main-thread-confined Objective-C bridge state that is not yet annotated
|
|
68
|
+
// for Swift 6's strict concurrency checks.
|
|
69
|
+
swiftLanguageModes: [.v5]
|
|
70
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(PosthogReactNativePlugin, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(setup:(NSString)sessionId
|
|
7
|
+
withSdkOptions:(NSDictionary)sdkOptions
|
|
8
|
+
withPluginConfig:(NSDictionary)pluginConfig
|
|
9
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
10
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
|
|
12
|
+
RCT_EXTERN_METHOD(start:(NSString)sessionId
|
|
13
|
+
withSdkOptions:(NSDictionary)sdkOptions
|
|
14
|
+
withSdkReplayConfig:(NSDictionary)sdkReplayConfig
|
|
15
|
+
withDecideReplayConfig:(NSDictionary)decideReplayConfig
|
|
16
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
17
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
18
|
+
|
|
19
|
+
RCT_EXTERN_METHOD(startSession:(NSString)sessionId
|
|
20
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
21
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
22
|
+
|
|
23
|
+
RCT_EXTERN_METHOD(isEnabled:(RCTPromiseResolveBlock)resolve
|
|
24
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
25
|
+
|
|
26
|
+
RCT_EXTERN_METHOD(endSession:(RCTPromiseResolveBlock)resolve
|
|
27
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
28
|
+
|
|
29
|
+
RCT_EXTERN_METHOD(identify:(NSString)distinctId
|
|
30
|
+
withAnonymousId:(NSString)anonymousId
|
|
31
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
32
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
33
|
+
|
|
34
|
+
RCT_EXTERN_METHOD(startRecording:(BOOL)resumeCurrent
|
|
35
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
36
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
37
|
+
|
|
38
|
+
RCT_EXTERN_METHOD(stopRecording:(RCTPromiseResolveBlock)resolve
|
|
39
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
40
|
+
|
|
41
|
+
RCT_EXTERN_METHOD(addExceptionStep:(NSString)message
|
|
42
|
+
withProperties:(NSDictionary)properties
|
|
43
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
44
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
45
|
+
|
|
46
|
+
RCT_EXTERN_METHOD(reset:(NSString)distinctId
|
|
47
|
+
withAnonymousId:(NSString)anonymousId
|
|
48
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
49
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
50
|
+
|
|
51
|
+
RCT_EXTERN_METHOD(registerPushNotificationToken:(NSString)deviceToken
|
|
52
|
+
withAppId:(NSString)appId
|
|
53
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
54
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
55
|
+
|
|
56
|
+
RCT_EXTERN_METHOD(unregisterPushNotificationToken:(RCTPromiseResolveBlock)resolve
|
|
57
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
58
|
+
|
|
59
|
+
RCT_EXTERN_METHOD(setOptOut:(BOOL)optOut
|
|
60
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
61
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
62
|
+
|
|
63
|
+
RCT_EXTERN_METHOD(capturePushNotificationOpened:(NSDictionary)properties
|
|
64
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
65
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
66
|
+
|
|
67
|
+
RCT_EXTERN_METHOD(providePushIdentityToken:(NSString)requestId
|
|
68
|
+
withToken:(NSString)token
|
|
69
|
+
withResolver:(RCTPromiseResolveBlock)resolve
|
|
70
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
71
|
+
|
|
72
|
+
+ (BOOL)requiresMainQueueSetup
|
|
73
|
+
{
|
|
74
|
+
return NO;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@end
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import PostHog
|
|
2
|
+
import React
|
|
3
|
+
|
|
4
|
+
/// Meant for internally logging PostHog related things
|
|
5
|
+
private func hedgeLog(_ message: String) {
|
|
6
|
+
print("[PostHog] \(message)")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#if !os(iOS)
|
|
10
|
+
// Session replay is part of posthog-ios's iOS-only surface, so recording is a no-op on macOS.
|
|
11
|
+
// Log once so a caller isn't left wondering why recording "started" but nothing arrives.
|
|
12
|
+
private var didLogSessionReplayUnsupported = false
|
|
13
|
+
private func logSessionReplayUnsupportedOnMacOS() {
|
|
14
|
+
guard !didLogSessionReplayUnsupported else { return }
|
|
15
|
+
didLogSessionReplayUnsupported = true
|
|
16
|
+
hedgeLog("Session replay is not supported on macOS")
|
|
17
|
+
}
|
|
18
|
+
#endif
|
|
19
|
+
|
|
20
|
+
/// Deduplication works on Android (both architectures), iOS (old architecture only), and macOS.
|
|
21
|
+
/// On the iOS new architecture, fatal JS exception events surface as a generic SIGABRT
|
|
22
|
+
/// crash event with no JS-error text in any field, so they currently cannot be filtered.
|
|
23
|
+
private let fatalJsErrorMarkers = ["Unhandled JS Exception", "ExceptionsManager.reportException", "facebook::jsi::JSError"]
|
|
24
|
+
|
|
25
|
+
private func containsFatalJsErrorMarker(_ text: String?) -> Bool {
|
|
26
|
+
guard let text else { return false }
|
|
27
|
+
return fatalJsErrorMarkers.contains { text.contains($0) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private func isReactNativeFatalJsError(_ event: PostHogEvent) -> Bool {
|
|
31
|
+
guard event.event == "$exception",
|
|
32
|
+
let exceptionList = event.properties["$exception_list"] as? [[String: Any]]
|
|
33
|
+
else { return false }
|
|
34
|
+
return exceptionList.contains { exception in
|
|
35
|
+
if containsFatalJsErrorMarker(exception["type"] as? String) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
if containsFatalJsErrorMarker(exception["value"] as? String) {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
// New-architecture RN rethrows fatal JS errors as a C++ jsi::JSError (SIGABRT);
|
|
42
|
+
// the JS-error text only survives in the signal's crash-info message.
|
|
43
|
+
let mechanism = exception["mechanism"] as? [String: Any]
|
|
44
|
+
let meta = mechanism?["meta"] as? [String: Any]
|
|
45
|
+
let signal = meta?["signal"] as? [String: Any]
|
|
46
|
+
return containsFatalJsErrorMarker(signal?["crash_info_message"] as? String)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A nil identity token sends the request unauthenticated, which a project requiring
|
|
51
|
+
// identity verification rejects server-side. Log the reason so that failure is greppable
|
|
52
|
+
// and distinct from a host that deliberately returned nil.
|
|
53
|
+
private func declinePushIdentity(_ completion: (String?) -> Void, _ reason: String) {
|
|
54
|
+
hedgeLog("Push subscription will be sent unauthenticated: \(reason)")
|
|
55
|
+
completion(nil)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@objc(PosthogReactNativePlugin)
|
|
59
|
+
public class PosthogReactNativePlugin: RCTEventEmitter {
|
|
60
|
+
private var config: PostHogConfig?
|
|
61
|
+
|
|
62
|
+
private static let pushIdentityEvent = "PostHogPushIdentityRequest"
|
|
63
|
+
|
|
64
|
+
// This module dies on every bridge reload, so the provider closure resolves the live
|
|
65
|
+
// module through this static weak reference at call time, not a captured setup-time one.
|
|
66
|
+
private static weak var pushInstance: PosthogReactNativePlugin?
|
|
67
|
+
|
|
68
|
+
// Main-thread confined, like the rest of the identity-request bookkeeping below.
|
|
69
|
+
private var hasPushListeners = false
|
|
70
|
+
private var pushIdentityCompletions: [String: (String?) -> Void] = [:]
|
|
71
|
+
|
|
72
|
+
public override func supportedEvents() -> [String]! {
|
|
73
|
+
[PosthogReactNativePlugin.pushIdentityEvent]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public override func startObserving() {
|
|
77
|
+
DispatchQueue.main.async { self.hasPushListeners = true }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public override func stopObserving() {
|
|
81
|
+
DispatchQueue.main.async { self.hasPushListeners = false }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@objc(setup:withSdkOptions:withPluginConfig:withResolver:withRejecter:)
|
|
85
|
+
func setup(
|
|
86
|
+
sessionId: String, sdkOptions: [String: Any], pluginConfig: [String: Any],
|
|
87
|
+
resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
|
|
88
|
+
) {
|
|
89
|
+
let sessionReplayConfig = pluginConfig["sessionReplay"] as? [String: Any] ?? [:]
|
|
90
|
+
let errorTrackingConfig = pluginConfig["errorTracking"] as? [String: Any] ?? [:]
|
|
91
|
+
let exceptionStepsConfig = errorTrackingConfig["exceptionSteps"] as? [String: Any] ?? [:]
|
|
92
|
+
|
|
93
|
+
setupNativeSdk(
|
|
94
|
+
method: "setup",
|
|
95
|
+
sessionId: sessionId,
|
|
96
|
+
sdkOptions: sdkOptions,
|
|
97
|
+
sessionReplayEnabled: sessionReplayConfig["enabled"] as? Bool ?? false,
|
|
98
|
+
sdkReplayConfig: sessionReplayConfig["sdkReplayConfig"] as? [String: Any] ?? [:],
|
|
99
|
+
decideReplayConfig: sessionReplayConfig["decideReplayConfig"] as? [String: Any] ?? [:],
|
|
100
|
+
nativeErrorTrackingAutocapture: errorTrackingConfig["nativeAutocapture"] as? Bool ?? false,
|
|
101
|
+
exceptionStepsConfig: exceptionStepsConfig,
|
|
102
|
+
pushConfig: pluginConfig["push"] as? [String: Any] ?? [:],
|
|
103
|
+
resolve: resolve
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@objc(start:withSdkOptions:withSdkReplayConfig:withDecideReplayConfig:withResolver:withRejecter:)
|
|
108
|
+
func start(
|
|
109
|
+
sessionId: String, sdkOptions: [String: Any], sdkReplayConfig: [String: Any],
|
|
110
|
+
decideReplayConfig: [String: Any], resolve: RCTPromiseResolveBlock,
|
|
111
|
+
reject _: RCTPromiseRejectBlock
|
|
112
|
+
) {
|
|
113
|
+
setupNativeSdk(
|
|
114
|
+
method: "start",
|
|
115
|
+
sessionId: sessionId,
|
|
116
|
+
sdkOptions: sdkOptions,
|
|
117
|
+
sessionReplayEnabled: true,
|
|
118
|
+
sdkReplayConfig: sdkReplayConfig,
|
|
119
|
+
decideReplayConfig: decideReplayConfig,
|
|
120
|
+
nativeErrorTrackingAutocapture: false,
|
|
121
|
+
exceptionStepsConfig: [:],
|
|
122
|
+
pushConfig: [:],
|
|
123
|
+
resolve: resolve
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private func setupNativeSdk(
|
|
128
|
+
method _: String,
|
|
129
|
+
sessionId: String,
|
|
130
|
+
sdkOptions: [String: Any],
|
|
131
|
+
sessionReplayEnabled: Bool,
|
|
132
|
+
sdkReplayConfig: [String: Any],
|
|
133
|
+
decideReplayConfig: [String: Any],
|
|
134
|
+
nativeErrorTrackingAutocapture: Bool,
|
|
135
|
+
exceptionStepsConfig: [String: Any],
|
|
136
|
+
pushConfig: [String: Any],
|
|
137
|
+
resolve: RCTPromiseResolveBlock
|
|
138
|
+
) {
|
|
139
|
+
if sessionId.isEmpty {
|
|
140
|
+
hedgeLog("Invalid empty sessionId provided.")
|
|
141
|
+
resolve(nil)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let projectToken =
|
|
146
|
+
(sdkOptions["projectToken"] as? String)
|
|
147
|
+
?? (sdkOptions["apiKey"] as? String)
|
|
148
|
+
?? ""
|
|
149
|
+
let host = sdkOptions["host"] as? String ?? PostHogConfig.defaultHost
|
|
150
|
+
let debug = sdkOptions["debug"] as? Bool ?? false
|
|
151
|
+
|
|
152
|
+
PostHogSessionManager.shared.setSessionId(sessionId)
|
|
153
|
+
|
|
154
|
+
let config = PostHogConfig(projectToken: projectToken, host: host)
|
|
155
|
+
config.captureApplicationLifecycleEvents = false
|
|
156
|
+
config.captureScreenViews = false
|
|
157
|
+
config.debug = debug
|
|
158
|
+
config.errorTrackingConfig.autoCapture = nativeErrorTrackingAutocapture
|
|
159
|
+
|
|
160
|
+
// Keep the native exception-steps buffer aligned with the JS layer (one logical buffer).
|
|
161
|
+
if let enabled = exceptionStepsConfig["enabled"] as? Bool {
|
|
162
|
+
config.errorTrackingConfig.exceptionSteps.enabled = enabled
|
|
163
|
+
}
|
|
164
|
+
if let maxBytes = exceptionStepsConfig["maxBytes"] as? Int {
|
|
165
|
+
config.errorTrackingConfig.exceptionSteps.maxBytes = maxBytes
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// React Native rethrows fatal JS errors natively (RCTFatalException / ExceptionsManager).
|
|
169
|
+
// The JS layer already captured them, so drop the native duplicate.
|
|
170
|
+
config.setBeforeSend { event in
|
|
171
|
+
isReactNativeFatalJsError(event) ? nil : event
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Surveys and session replay are iOS-only in posthog-ios, so the APIs below
|
|
175
|
+
// don't exist on macOS. macOS gets error tracking only.
|
|
176
|
+
#if os(iOS)
|
|
177
|
+
if #available(iOS 15.0, *) {
|
|
178
|
+
config.surveys = false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Always apply the session replay configuration so that recording started later
|
|
182
|
+
// (e.g. startRecording or a linked feature flag) uses the right mode and masking;
|
|
183
|
+
// sessionReplayEnabled only controls whether recording starts at setup.
|
|
184
|
+
config.sessionReplay = sessionReplayEnabled
|
|
185
|
+
config.sessionReplayConfig.screenshotMode = true
|
|
186
|
+
|
|
187
|
+
let maskAllTextInputs = sdkReplayConfig["maskAllTextInputs"] as? Bool ?? true
|
|
188
|
+
config.sessionReplayConfig.maskAllTextInputs = maskAllTextInputs
|
|
189
|
+
|
|
190
|
+
let maskAllImages = sdkReplayConfig["maskAllImages"] as? Bool ?? true
|
|
191
|
+
config.sessionReplayConfig.maskAllImages = maskAllImages
|
|
192
|
+
|
|
193
|
+
let maskAllSandboxedViews = sdkReplayConfig["maskAllSandboxedViews"] as? Bool ?? true
|
|
194
|
+
config.sessionReplayConfig.maskAllSandboxedViews = maskAllSandboxedViews
|
|
195
|
+
|
|
196
|
+
// read throttleDelayMs and use iOSdebouncerDelayMs as a fallback for back compatibility
|
|
197
|
+
let throttleDelayMs =
|
|
198
|
+
(sdkReplayConfig["throttleDelayMs"] as? Int)
|
|
199
|
+
?? (sdkReplayConfig["iOSdebouncerDelayMs"] as? Int)
|
|
200
|
+
?? 1000
|
|
201
|
+
|
|
202
|
+
let timeInterval: TimeInterval = Double(throttleDelayMs) / 1000.0
|
|
203
|
+
config.sessionReplayConfig.throttleDelay = timeInterval
|
|
204
|
+
|
|
205
|
+
let captureNetworkTelemetry = sdkReplayConfig["captureNetworkTelemetry"] as? Bool ?? true
|
|
206
|
+
config.sessionReplayConfig.captureNetworkTelemetry = captureNetworkTelemetry
|
|
207
|
+
|
|
208
|
+
let captureLog = sdkReplayConfig["captureLog"] as? Bool ?? true
|
|
209
|
+
config.sessionReplayConfig.captureLogs = captureLog
|
|
210
|
+
|
|
211
|
+
config.sessionReplayConfig.sampleRate = sdkReplayConfig["sampleRate"] as? NSNumber
|
|
212
|
+
|
|
213
|
+
let screenshotModeBackgroundCapture = sdkReplayConfig["screenshotModeBackgroundCapture"] as? Bool ?? false
|
|
214
|
+
config.sessionReplayConfig.screenshotModeBackgroundCapture = screenshotModeBackgroundCapture
|
|
215
|
+
|
|
216
|
+
let endpoint = decideReplayConfig["endpoint"] as? String ?? ""
|
|
217
|
+
if !endpoint.isEmpty {
|
|
218
|
+
config.snapshotEndpoint = endpoint
|
|
219
|
+
}
|
|
220
|
+
#endif
|
|
221
|
+
|
|
222
|
+
let distinctId = sdkOptions["distinctId"] as? String ?? ""
|
|
223
|
+
let anonymousId = sdkOptions["anonymousId"] as? String ?? ""
|
|
224
|
+
|
|
225
|
+
let sdkVersion = sdkOptions["sdkVersion"] as? String ?? ""
|
|
226
|
+
|
|
227
|
+
let flushAt = sdkOptions["flushAt"] as? Int ?? 20
|
|
228
|
+
config.flushAt = flushAt
|
|
229
|
+
|
|
230
|
+
config.optOut = sdkOptions["optOut"] as? Bool ?? false
|
|
231
|
+
// JS owns flags; it tells us when the native preload would be a duplicate. posthog-ios
|
|
232
|
+
// has no remoteConfig switch to mirror — it deprecated the option and always loads.
|
|
233
|
+
config.preloadFeatureFlags = sdkOptions["preloadFeatureFlags"] as? Bool ?? true
|
|
234
|
+
|
|
235
|
+
// Forward custom headers (e.g. Authorization for a reverse proxy) so the native SDK
|
|
236
|
+
// attaches them to the requests it sends directly (session replay, crash uploads).
|
|
237
|
+
// Keep only string values so a stray non-string doesn't drop every header (matches Android).
|
|
238
|
+
if let rawHeaders = sdkOptions["requestHeaders"] as? [String: Any] {
|
|
239
|
+
config.requestHeaders = rawHeaders.compactMapValues { $0 as? String }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if !sdkVersion.isEmpty {
|
|
243
|
+
postHogSdkName = "posthog-react-native"
|
|
244
|
+
postHogVersion = sdkVersion
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Only set when present: the legacy start() path predates push, and there the
|
|
248
|
+
// native defaults (both true) must win, matching posthog-ios on its own.
|
|
249
|
+
if let capturePushSubscriptions = pushConfig["capturePushNotificationSubscriptions"] as? Bool {
|
|
250
|
+
config.capturePushNotificationSubscriptions = capturePushSubscriptions
|
|
251
|
+
}
|
|
252
|
+
if let capturePushOpened = pushConfig["capturePushNotificationOpened"] as? Bool {
|
|
253
|
+
config.capturePushNotificationOpened = capturePushOpened
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Installed only when JS asked for it: an uninvited bridging provider would change
|
|
257
|
+
// how the native SDK handles a 401 on the subscription call.
|
|
258
|
+
if pushConfig["pushIdentityProviderEnabled"] as? Bool == true {
|
|
259
|
+
PosthogReactNativePlugin.pushInstance = self
|
|
260
|
+
config.pushIdentityProvider = { distinctId, appId, completion in
|
|
261
|
+
DispatchQueue.main.async {
|
|
262
|
+
guard let instance = PosthogReactNativePlugin.pushInstance, instance.hasPushListeners else {
|
|
263
|
+
declinePushIdentity(completion, "no JS listener attached")
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
let requestId = UUID().uuidString
|
|
267
|
+
instance.pushIdentityCompletions[requestId] = completion
|
|
268
|
+
instance.sendEvent(
|
|
269
|
+
withName: PosthogReactNativePlugin.pushIdentityEvent,
|
|
270
|
+
body: ["requestId": requestId, "distinctId": distinctId, "appId": appId]
|
|
271
|
+
)
|
|
272
|
+
// The native SDK's own 10s mint watchdog handles the fallback; this only
|
|
273
|
+
// drops the entry so a late JS reply is ignored and the closure doesn't leak.
|
|
274
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 15) { [weak instance] in
|
|
275
|
+
instance?.pushIdentityCompletions.removeValue(forKey: requestId)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
PostHogSDK.shared.setup(config)
|
|
282
|
+
|
|
283
|
+
self.config = config
|
|
284
|
+
|
|
285
|
+
guard let storageManager = self.config?.storageManager else {
|
|
286
|
+
hedgeLog("Storage manager is not available in the config.")
|
|
287
|
+
resolve(nil)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
setIdentify(storageManager, distinctId: distinctId, anonymousId: anonymousId)
|
|
292
|
+
|
|
293
|
+
resolve(nil)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@objc(startSession:withResolver:withRejecter:)
|
|
297
|
+
func startSession(
|
|
298
|
+
sessionId: String, resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
|
|
299
|
+
) {
|
|
300
|
+
if sessionId.isEmpty {
|
|
301
|
+
hedgeLog("Invalid empty sessionId provided.")
|
|
302
|
+
resolve(nil)
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
PostHogSessionManager.shared.setSessionId(sessionId)
|
|
306
|
+
PostHogSDK.shared.startSession()
|
|
307
|
+
resolve(nil)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@objc(isEnabled:withRejecter:)
|
|
311
|
+
func isEnabled(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
|
|
312
|
+
#if os(iOS)
|
|
313
|
+
resolve(PostHogSDK.shared.isSessionReplayActive())
|
|
314
|
+
#else
|
|
315
|
+
// Session replay is unsupported on macOS.
|
|
316
|
+
resolve(false)
|
|
317
|
+
#endif
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@objc(endSession:withRejecter:)
|
|
321
|
+
func endSession(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
|
|
322
|
+
PostHogSDK.shared.endSession()
|
|
323
|
+
resolve(nil)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@objc(identify:withAnonymousId:withResolver:withRejecter:)
|
|
327
|
+
func identify(
|
|
328
|
+
distinctId: String, anonymousId: String, resolve: RCTPromiseResolveBlock,
|
|
329
|
+
reject _: RCTPromiseRejectBlock
|
|
330
|
+
) {
|
|
331
|
+
guard let storageManager = config?.storageManager else {
|
|
332
|
+
hedgeLog("Storage manager is not available in the config.")
|
|
333
|
+
resolve(nil)
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
setIdentify(storageManager, distinctId: distinctId, anonymousId: anonymousId)
|
|
337
|
+
|
|
338
|
+
resolve(nil)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Calls the native SDK rather than writing storage like identify() does: reset() is what
|
|
342
|
+
// unregisters the logged-out user's push subscription and re-registers under the new identity.
|
|
343
|
+
@objc(reset:withAnonymousId:withResolver:withRejecter:)
|
|
344
|
+
func reset(
|
|
345
|
+
distinctId: String, anonymousId: String, resolve: RCTPromiseResolveBlock,
|
|
346
|
+
reject _: RCTPromiseRejectBlock
|
|
347
|
+
) {
|
|
348
|
+
PostHogSDK.shared.reset()
|
|
349
|
+
// Native reset() mints its own anonymous id; overwrite it with the JS one so the two SDKs
|
|
350
|
+
// stay on the same identity. Must run after reset(), which needs the pre-reset distinctId
|
|
351
|
+
// to know which subscription to unregister.
|
|
352
|
+
if let storageManager = config?.storageManager {
|
|
353
|
+
setIdentify(storageManager, distinctId: distinctId, anonymousId: anonymousId)
|
|
354
|
+
}
|
|
355
|
+
resolve(nil)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private func setIdentify(
|
|
359
|
+
_ storageManager: PostHogStorageManager, distinctId: String, anonymousId: String
|
|
360
|
+
) {
|
|
361
|
+
if !anonymousId.isEmpty {
|
|
362
|
+
storageManager.setAnonymousId(anonymousId)
|
|
363
|
+
}
|
|
364
|
+
if !distinctId.isEmpty {
|
|
365
|
+
storageManager.setDistinctId(distinctId)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Runtime consent changes must reach native: it persists its own opt-out flag and only
|
|
370
|
+
// reads the JS value at setup(), so a refreshed APNs token could otherwise auto-register
|
|
371
|
+
// after the user opted out. optIn() also reinstalls the integrations opt-out removed.
|
|
372
|
+
@objc(setOptOut:withResolver:withRejecter:)
|
|
373
|
+
func setOptOut(
|
|
374
|
+
optOut: Bool, resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
|
|
375
|
+
) {
|
|
376
|
+
if optOut {
|
|
377
|
+
PostHogSDK.shared.optOut()
|
|
378
|
+
} else {
|
|
379
|
+
PostHogSDK.shared.optIn()
|
|
380
|
+
}
|
|
381
|
+
resolve(nil)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@objc(startRecording:withResolver:withRejecter:)
|
|
385
|
+
func startRecording(
|
|
386
|
+
resumeCurrent: Bool, resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
|
|
387
|
+
) {
|
|
388
|
+
#if os(iOS)
|
|
389
|
+
PostHogSDK.shared.startSessionRecording(resumeCurrent: resumeCurrent)
|
|
390
|
+
#else
|
|
391
|
+
logSessionReplayUnsupportedOnMacOS()
|
|
392
|
+
#endif
|
|
393
|
+
resolve(nil)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@objc(stopRecording:withRejecter:)
|
|
397
|
+
func stopRecording(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
|
|
398
|
+
#if os(iOS)
|
|
399
|
+
PostHogSDK.shared.stopSessionRecording()
|
|
400
|
+
#else
|
|
401
|
+
logSessionReplayUnsupportedOnMacOS()
|
|
402
|
+
#endif
|
|
403
|
+
resolve(nil)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@objc(addExceptionStep:withProperties:withResolver:withRejecter:)
|
|
407
|
+
func addExceptionStep(
|
|
408
|
+
message: String, properties: [String: Any]?, resolve: RCTPromiseResolveBlock,
|
|
409
|
+
reject _: RCTPromiseRejectBlock
|
|
410
|
+
) {
|
|
411
|
+
PostHogSDK.shared.addExceptionStep(message, properties: properties)
|
|
412
|
+
resolve(nil)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
@objc(registerPushNotificationToken:withAppId:withResolver:withRejecter:)
|
|
416
|
+
func registerPushNotificationToken(
|
|
417
|
+
deviceToken: String, appId: String?, resolve: RCTPromiseResolveBlock,
|
|
418
|
+
reject: RCTPromiseRejectBlock
|
|
419
|
+
) {
|
|
420
|
+
#if os(iOS)
|
|
421
|
+
// A blank token is dropped silently by the native SDK, so surface it here
|
|
422
|
+
// instead of reporting false success.
|
|
423
|
+
if deviceToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
424
|
+
reject("PosthogReactNativePluginError", "registerPushNotificationToken: deviceToken is blank; token not registered.", nil)
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
PostHogSDK.shared.registerPushNotificationToken(deviceToken, appId: appId)
|
|
428
|
+
resolve(nil)
|
|
429
|
+
#else
|
|
430
|
+
// posthog-ios push registration is iOS-only (the backend rejects the macos platform).
|
|
431
|
+
_ = reject
|
|
432
|
+
hedgeLog("registerPushNotificationToken is not supported on macOS; token not registered.")
|
|
433
|
+
resolve(nil)
|
|
434
|
+
#endif
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
@objc(unregisterPushNotificationToken:withRejecter:)
|
|
438
|
+
func unregisterPushNotificationToken(resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock) {
|
|
439
|
+
#if os(iOS)
|
|
440
|
+
PostHogSDK.shared.unregisterPushNotificationToken()
|
|
441
|
+
#else
|
|
442
|
+
hedgeLog("unregisterPushNotificationToken is not supported on macOS; nothing to unregister.")
|
|
443
|
+
#endif
|
|
444
|
+
resolve(nil)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@objc(capturePushNotificationOpened:withResolver:withRejecter:)
|
|
448
|
+
func capturePushNotificationOpened(
|
|
449
|
+
properties: [String: Any], resolve: RCTPromiseResolveBlock, reject _: RCTPromiseRejectBlock
|
|
450
|
+
) {
|
|
451
|
+
PostHogSDK.shared.capturePushNotificationOpened(
|
|
452
|
+
title: properties["title"] as? String,
|
|
453
|
+
subtitle: properties["subtitle"] as? String,
|
|
454
|
+
body: properties["body"] as? String,
|
|
455
|
+
payload: properties["payload"] as? [String: Any],
|
|
456
|
+
action: properties["action"] as? String
|
|
457
|
+
)
|
|
458
|
+
resolve(nil)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
@objc(providePushIdentityToken:withToken:withResolver:withRejecter:)
|
|
462
|
+
func providePushIdentityToken(
|
|
463
|
+
requestId: String, token: String?, resolve: RCTPromiseResolveBlock,
|
|
464
|
+
reject _: RCTPromiseRejectBlock
|
|
465
|
+
) {
|
|
466
|
+
DispatchQueue.main.async { [weak self] in
|
|
467
|
+
guard let completion = self?.pushIdentityCompletions.removeValue(forKey: requestId) else {
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
completion(token)
|
|
471
|
+
}
|
|
472
|
+
resolve(nil)
|
|
473
|
+
}
|
|
474
|
+
}
|