@npaw/forge-react-native 9.6.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.
Files changed (74) hide show
  1. package/LICENSE +10 -0
  2. package/NPAWForgeReactNative.podspec +44 -0
  3. package/README.md +51 -0
  4. package/android/build.gradle +77 -0
  5. package/android/src/main/AndroidManifest.xml +14 -0
  6. package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +295 -0
  7. package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +61 -0
  8. package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +735 -0
  9. package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgePackage.java +53 -0
  10. package/android/src/test/java/com/npaw/forge/reactnative/ForgeDtoMapperTest.java +133 -0
  11. package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +82 -0
  12. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/ForgeKernelFFI.h +17 -0
  13. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +873 -0
  14. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/module.modulemap +7 -0
  15. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
  16. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/ForgeKernelFFI.h +17 -0
  17. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +873 -0
  18. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +7 -0
  19. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
  20. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/ForgeKernelFFI.h +17 -0
  21. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +873 -0
  22. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/module.modulemap +7 -0
  23. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
  24. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/ForgeKernelFFI.h +17 -0
  25. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +873 -0
  26. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/module.modulemap +7 -0
  27. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
  28. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +3931 -0
  29. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +873 -0
  30. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.modulemap +7 -0
  31. package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAccessLogFacts.swift +152 -0
  32. package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +877 -0
  33. package/ios/ForgeApple/Sources/NPAWForgeCapture/AttributionCapture.swift +158 -0
  34. package/ios/ForgeApple/Sources/NPAWForgeCapture/CapturePlanGate.swift +189 -0
  35. package/ios/ForgeApple/Sources/NPAWForgeCapture/CrashCapture.swift +120 -0
  36. package/ios/ForgeApple/Sources/NPAWForgeCapture/ForgeAutomaticCapture.swift +639 -0
  37. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsCapture.swift +1176 -0
  38. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsEngine.swift +674 -0
  39. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsMarkers.swift +105 -0
  40. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsSwiftUI.swift +284 -0
  41. package/ios/ForgeApple/Sources/NPAWForgeCapture/SwiftUICapture.swift +44 -0
  42. package/ios/ForgeApple/Sources/NPAWForgeCore/AdAdapter.swift +199 -0
  43. package/ios/ForgeApple/Sources/NPAWForgeCore/AdSemantics.swift +136 -0
  44. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +596 -0
  45. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +297 -0
  46. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeClock.swift +29 -0
  47. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +109 -0
  48. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeOutcome.swift +89 -0
  49. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +291 -0
  50. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeStorage.swift +70 -0
  51. package/ios/ForgeApple/Sources/NPAWForgeCore/ManualAdReporter.swift +180 -0
  52. package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +453 -0
  53. package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +594 -0
  54. package/ios/ForgeApple/Sources/NPAWForgeCore/NetworkRequestRecord.swift +191 -0
  55. package/ios/ForgeApple/Sources/NPAWForgeCore/ObjectiveCWrappers.swift +191 -0
  56. package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +299 -0
  57. package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerSemantics.swift +266 -0
  58. package/ios/ForgeApple/Sources/NPAWForgeCore/PrivacySanitizer.swift +59 -0
  59. package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +478 -0
  60. package/ios/ForgeApple/Sources/NPAWForgeCore/VideoOptions.swift +431 -0
  61. package/ios/ForgeApple/Sources/NPAWForgeNetworkCapture/ForgeNetworkCapture.swift +116 -0
  62. package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/NPAWForgeNetworkSwizzler.m +293 -0
  63. package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/include/NPAWForgeNetworkSwizzler.h +33 -0
  64. package/ios/ForgeModuleImpl.swift +1131 -0
  65. package/ios/NPAWForgeTurboModule.mm +399 -0
  66. package/package.json +33 -0
  67. package/src/ForgeAdReporter.ts +101 -0
  68. package/src/ForgeVideo.ts +169 -0
  69. package/src/NPAWForge.ts +177 -0
  70. package/src/capture.ts +192 -0
  71. package/src/events.ts +40 -0
  72. package/src/index.ts +37 -0
  73. package/src/specs/NativeNPAWForge.ts +168 -0
  74. package/src/types.ts +114 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Copyright (C) NPAW - All Rights Reserved
3
+ *
4
+ * This source code is protected under international copyright law. All rights
5
+ * reserved and protected by the copyright holders.
6
+ * This file is confidential and only available to authorized individuals with the
7
+ * permission of the copyright holders. If you encounter this file and do not have
8
+ * permission, please contact the copyright holders and delete this file.
9
+ *
10
+ *
11
+ * @author: Victor Soto <victor.soto@nicepeopleatwork.com>
12
+ * @description: Clean-room Objective-C swizzling implementation for NSURLSession and NSURLConnection capture.
13
+ */
14
+
15
+ #import "NPAWForgeNetworkSwizzler.h"
16
+ #import <objc/runtime.h>
17
+ #import <os/lock.h>
18
+
19
+ // Shared configuration written by +installWithCallback:blockedHosts: and read
20
+ // from arbitrary URLSession/NSURLConnection completion threads. All access goes
21
+ // through NPAWStateLock so a re-install never races ARC release of the previous
22
+ // callback block or blocked-hosts set against an in-flight completion.
23
+ static NPAWForgeNetworkTraceCallback NPAWNetworkCallback;
24
+ static NSSet<NSString *> *NPAWBlockedHosts;
25
+ static BOOL NPAWInstalled;
26
+ static os_unfair_lock NPAWStateLock = OS_UNFAIR_LOCK_INIT;
27
+
28
+ // Thread-local guard so a convenience dataTask variant that funnels into another
29
+ // swizzled variant on the same thread wraps (and therefore emits) exactly once.
30
+ static _Thread_local BOOL NPAWWrappingInProgress;
31
+
32
+ static NPAWForgeNetworkTraceCallback NPAWCurrentCallback(void) {
33
+ os_unfair_lock_lock(&NPAWStateLock);
34
+ NPAWForgeNetworkTraceCallback callback = NPAWNetworkCallback;
35
+ os_unfair_lock_unlock(&NPAWStateLock);
36
+ return callback;
37
+ }
38
+
39
+ static NSSet<NSString *> *NPAWCurrentBlockedHosts(void) {
40
+ os_unfair_lock_lock(&NPAWStateLock);
41
+ NSSet<NSString *> *blockedHosts = NPAWBlockedHosts;
42
+ os_unfair_lock_unlock(&NPAWStateLock);
43
+ return blockedHosts;
44
+ }
45
+
46
+ static long long NPAWNowMs(void) {
47
+ return (long long)([[NSDate date] timeIntervalSince1970] * 1000.0);
48
+ }
49
+
50
+ static NSString *NPAWTrimmedURL(NSURL *url) {
51
+ if (url == nil) {
52
+ return @"unknown";
53
+ }
54
+ NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
55
+ components.user = nil;
56
+ components.password = nil;
57
+ components.query = nil;
58
+ components.fragment = nil;
59
+ return components.URL.absoluteString ?: url.absoluteString ?: @"unknown";
60
+ }
61
+
62
+ static BOOL NPAWShouldBlock(NSURLRequest *request) {
63
+ NSString *host = request.URL.host.lowercaseString;
64
+ if (host.length == 0) {
65
+ return NO;
66
+ }
67
+ return [NPAWCurrentBlockedHosts() containsObject:host];
68
+ }
69
+
70
+ static NSString *NPAWOutcome(NSHTTPURLResponse *response, NSError *error) {
71
+ if (error != nil) {
72
+ if (error.code == NSURLErrorCancelled) {
73
+ return @"cancelled";
74
+ }
75
+ if (error.code == NSURLErrorTimedOut) {
76
+ return @"timeout";
77
+ }
78
+ return @"network_error";
79
+ }
80
+ NSInteger status = response.statusCode;
81
+ if (status >= 200 && status < 400) {
82
+ return @"success";
83
+ }
84
+ if (status > 0) {
85
+ return @"http_error";
86
+ }
87
+ return @"unknown";
88
+ }
89
+
90
+ static void NPAWEmit(NSURLRequest *request, NSURLResponse *response, NSData *data, NSError *error, long long startMs, NSString *source) {
91
+ NPAWForgeNetworkTraceCallback callback = NPAWCurrentCallback();
92
+ if (callback == nil || NPAWShouldBlock(request)) {
93
+ return;
94
+ }
95
+ long long endMs = NPAWNowMs();
96
+ NSHTTPURLResponse *http = [response isKindOfClass:NSHTTPURLResponse.class] ? (NSHTTPURLResponse *)response : nil;
97
+ NSMutableDictionary<NSString *, NSString *> *payload = [NSMutableDictionary dictionary];
98
+ payload[@"eventId"] = NSUUID.UUID.UUIDString;
99
+ payload[@"source"] = source;
100
+ payload[@"targetUrl"] = NPAWTrimmedURL(request.URL);
101
+ payload[@"method"] = request.HTTPMethod ?: @"GET";
102
+ payload[@"timestampMs"] = @"0";
103
+ payload[@"durationMs"] = [NSString stringWithFormat:@"%lld", MAX(0, endMs - startMs)];
104
+ payload[@"responseStatusCode"] = [NSString stringWithFormat:@"%ld", (long)(http != nil ? http.statusCode : 0)];
105
+ payload[@"outcome"] = NPAWOutcome(http, error);
106
+ if (error.localizedDescription.length > 0) {
107
+ payload[@"errorMessage"] = error.localizedDescription;
108
+ }
109
+ // Body SIZES only, never body content: measured byte counts first, the
110
+ // Content-Length header as fallback (kernel size contract feeding the
111
+ // requestBytes/responseBytes wire keys and net.bytes_out/net.bytes_in).
112
+ if (data.length > 0) {
113
+ payload[@"responseBytes"] = [NSString stringWithFormat:@"%lu", (unsigned long)data.length];
114
+ } else if (response.expectedContentLength > 0) {
115
+ payload[@"responseBytes"] = [NSString stringWithFormat:@"%lld", response.expectedContentLength];
116
+ }
117
+ if (request.HTTPBody.length > 0) {
118
+ payload[@"requestBytes"] = [NSString stringWithFormat:@"%lu", (unsigned long)request.HTTPBody.length];
119
+ } else {
120
+ long long declaredRequestLength = [request valueForHTTPHeaderField:@"Content-Length"].longLongValue;
121
+ if (declaredRequestLength > 0) {
122
+ payload[@"requestBytes"] = [NSString stringWithFormat:@"%lld", declaredRequestLength];
123
+ }
124
+ }
125
+ if (http.MIMEType.length > 0) {
126
+ payload[@"responseHeader.Content-Type"] = http.MIMEType;
127
+ }
128
+ callback(payload);
129
+ }
130
+
131
+ static void NPAWSwizzleInstanceMethod(Class cls, SEL original, SEL replacement) {
132
+ Method originalMethod = class_getInstanceMethod(cls, original);
133
+ Method replacementMethod = class_getInstanceMethod(cls, replacement);
134
+ if (originalMethod == NULL || replacementMethod == NULL) {
135
+ return;
136
+ }
137
+ class_addMethod(
138
+ cls,
139
+ replacement,
140
+ method_getImplementation(replacementMethod),
141
+ method_getTypeEncoding(replacementMethod)
142
+ );
143
+ replacementMethod = class_getInstanceMethod(cls, replacement);
144
+ if (replacementMethod == NULL) {
145
+ return;
146
+ }
147
+ if (class_addMethod(
148
+ cls,
149
+ original,
150
+ method_getImplementation(replacementMethod),
151
+ method_getTypeEncoding(replacementMethod)
152
+ )) {
153
+ class_replaceMethod(
154
+ cls,
155
+ replacement,
156
+ method_getImplementation(originalMethod),
157
+ method_getTypeEncoding(originalMethod)
158
+ );
159
+ return;
160
+ }
161
+ method_exchangeImplementations(originalMethod, replacementMethod);
162
+ }
163
+
164
+ static void NPAWSwizzleClassMethod(Class cls, SEL original, SEL replacement) {
165
+ Class metaClass = object_getClass(cls);
166
+ Method originalMethod = class_getClassMethod(cls, original);
167
+ Method replacementMethod = class_getClassMethod(cls, replacement);
168
+ if (metaClass == Nil || originalMethod == NULL || replacementMethod == NULL) {
169
+ return;
170
+ }
171
+ method_exchangeImplementations(originalMethod, replacementMethod);
172
+ }
173
+
174
+ @interface NSURLSession (NPAWForgeNetworkCapture)
175
+ - (NSURLSessionDataTask *)npaw_dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error))completionHandler;
176
+ - (NSURLSessionDataTask *)npaw_dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error))completionHandler;
177
+ @end
178
+
179
+ @implementation NSURLSession (NPAWForgeNetworkCapture)
180
+
181
+ - (NSURLSessionDataTask *)npaw_dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error))completionHandler {
182
+ if (NPAWWrappingInProgress) {
183
+ // Funneled here from another swizzled convenience variant on this thread:
184
+ // that variant already wrapped the completion, so pass through untouched
185
+ // to guarantee a single emission per request.
186
+ return [self npaw_dataTaskWithRequest:request completionHandler:completionHandler];
187
+ }
188
+ long long startMs = NPAWNowMs();
189
+ void (^wrapped)(NSData *_Nullable, NSURLResponse *_Nullable, NSError *_Nullable) = ^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
190
+ NPAWEmit(request, response, data, error, startMs, @"automatic_urlsession");
191
+ if (completionHandler != nil) {
192
+ completionHandler(data, response, error);
193
+ }
194
+ };
195
+ NPAWWrappingInProgress = YES;
196
+ NSURLSessionDataTask *task = [self npaw_dataTaskWithRequest:request completionHandler:wrapped];
197
+ NPAWWrappingInProgress = NO;
198
+ return task;
199
+ }
200
+
201
+ - (NSURLSessionDataTask *)npaw_dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error))completionHandler {
202
+ if (NPAWWrappingInProgress) {
203
+ return [self npaw_dataTaskWithURL:url completionHandler:completionHandler];
204
+ }
205
+ NSURLRequest *request = [NSURLRequest requestWithURL:url];
206
+ long long startMs = NPAWNowMs();
207
+ void (^wrapped)(NSData *_Nullable, NSURLResponse *_Nullable, NSError *_Nullable) = ^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) {
208
+ NPAWEmit(request, response, data, error, startMs, @"automatic_urlsession");
209
+ if (completionHandler != nil) {
210
+ completionHandler(data, response, error);
211
+ }
212
+ };
213
+ NPAWWrappingInProgress = YES;
214
+ NSURLSessionDataTask *task = [self npaw_dataTaskWithURL:url completionHandler:wrapped];
215
+ NPAWWrappingInProgress = NO;
216
+ return task;
217
+ }
218
+
219
+ @end
220
+
221
+ @interface NSURLConnection (NPAWForgeNetworkCapture)
222
+ + (void)npaw_sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse *_Nullable response, NSData *_Nullable data, NSError *_Nullable connectionError))handler;
223
+ @end
224
+
225
+ @implementation NSURLConnection (NPAWForgeNetworkCapture)
226
+
227
+ + (void)npaw_sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse *_Nullable response, NSData *_Nullable data, NSError *_Nullable connectionError))handler {
228
+ long long startMs = NPAWNowMs();
229
+ [self npaw_sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *_Nullable response, NSData *_Nullable data, NSError *_Nullable connectionError) {
230
+ NPAWEmit(request, response, data, connectionError, startMs, @"automatic_nsurlconnection");
231
+ if (handler != nil) {
232
+ handler(response, data, connectionError);
233
+ }
234
+ }];
235
+ }
236
+
237
+ @end
238
+
239
+ @implementation NPAWForgeNetworkSwizzler
240
+
241
+ + (BOOL)installWithCallback:(NPAWForgeNetworkTraceCallback)callback
242
+ blockedHosts:(NSSet<NSString *> *)blockedHosts {
243
+ @synchronized (self) {
244
+ os_unfair_lock_lock(&NPAWStateLock);
245
+ NPAWNetworkCallback = [callback copy];
246
+ NPAWBlockedHosts = [blockedHosts copy] ?: [NSSet set];
247
+ os_unfair_lock_unlock(&NPAWStateLock);
248
+ if (NPAWInstalled) {
249
+ return YES;
250
+ }
251
+ Class sharedSessionClass = object_getClass(NSURLSession.sharedSession);
252
+ Class sessionClasses[] = { sharedSessionClass, NSURLSession.class };
253
+ for (NSUInteger index = 0; index < 2; index++) {
254
+ Class cls = sessionClasses[index];
255
+ if (cls == Nil || (index == 1 && cls == sharedSessionClass)) {
256
+ continue;
257
+ }
258
+ NPAWSwizzleInstanceMethod(
259
+ cls,
260
+ @selector(dataTaskWithRequest:completionHandler:),
261
+ @selector(npaw_dataTaskWithRequest:completionHandler:)
262
+ );
263
+ NPAWSwizzleInstanceMethod(
264
+ cls,
265
+ @selector(dataTaskWithURL:completionHandler:),
266
+ @selector(npaw_dataTaskWithURL:completionHandler:)
267
+ );
268
+ }
269
+ NPAWSwizzleClassMethod(
270
+ NSURLConnection.class,
271
+ @selector(sendAsynchronousRequest:queue:completionHandler:),
272
+ @selector(npaw_sendAsynchronousRequest:queue:completionHandler:)
273
+ );
274
+ NPAWInstalled = YES;
275
+ return YES;
276
+ }
277
+ }
278
+
279
+ + (void)uninstall {
280
+ @synchronized (self) {
281
+ os_unfair_lock_lock(&NPAWStateLock);
282
+ NPAWNetworkCallback = nil;
283
+ NPAWBlockedHosts = nil;
284
+ os_unfair_lock_unlock(&NPAWStateLock);
285
+ // NPAWInstalled deliberately stays YES and the exchanged IMPs stay in
286
+ // place: re-exchanging implementations is unsafe when another library
287
+ // swizzled the same selectors after us (we would splice their IMP out
288
+ // of the chain, not restore the original). With the callback cleared,
289
+ // NPAWEmit no-ops until the next install re-sets the state.
290
+ }
291
+ }
292
+
293
+ @end
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Copyright (C) NPAW - All Rights Reserved
3
+ *
4
+ * This source code is protected under international copyright law. All rights
5
+ * reserved and protected by the copyright holders.
6
+ * This file is confidential and only available to authorized individuals with the
7
+ * permission of the copyright holders. If you encounter this file and do not have
8
+ * permission, please contact the copyright holders and delete this file.
9
+ *
10
+ *
11
+ * @author: Victor Soto <victor.soto@nicepeopleatwork.com>
12
+ * @description: Objective-C runtime swizzler entrypoint for Apple network capture.
13
+ */
14
+
15
+ #import <Foundation/Foundation.h>
16
+
17
+ NS_ASSUME_NONNULL_BEGIN
18
+
19
+ typedef void (^NPAWForgeNetworkTraceCallback)(NSDictionary<NSString *, NSString *> *payload);
20
+
21
+ @interface NPAWForgeNetworkSwizzler : NSObject
22
+
23
+ + (BOOL)installWithCallback:(NPAWForgeNetworkTraceCallback)callback
24
+ blockedHosts:(NSSet<NSString *> *)blockedHosts;
25
+
26
+ // Clears the stored callback and blocked-hosts state so no further payloads are
27
+ // emitted. The swizzled implementations stay installed for the process lifetime;
28
+ // a later install resumes capture by re-setting the callback.
29
+ + (void)uninstall;
30
+
31
+ @end
32
+
33
+ NS_ASSUME_NONNULL_END