@rejourneyco/react-native 1.0.0
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/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- package/src/types/index.ts +739 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJEventBuffer.h
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Write-first event buffer that persists events to disk immediately.
|
|
6
|
+
// Industry-standard approach: events are never lost even on force-kill.
|
|
7
|
+
//
|
|
8
|
+
// Copyright (c) 2026 Rejourney
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
#import <Foundation/Foundation.h>
|
|
12
|
+
|
|
13
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Write-first event buffer for crash-safe event persistence.
|
|
17
|
+
*
|
|
18
|
+
* Events are written to disk immediately when logged, ensuring no data loss
|
|
19
|
+
* even if the app is force-killed. Events are read back for upload on next
|
|
20
|
+
* app launch if not uploaded during the session.
|
|
21
|
+
*
|
|
22
|
+
* File format: JSONL (one JSON object per line) for efficient append
|
|
23
|
+
* operations.
|
|
24
|
+
*/
|
|
25
|
+
@interface RJEventBuffer : NSObject
|
|
26
|
+
|
|
27
|
+
/// Session ID this buffer is associated with
|
|
28
|
+
@property(nonatomic, copy, readonly) NSString *sessionId;
|
|
29
|
+
|
|
30
|
+
/// Base directory for pending session data
|
|
31
|
+
@property(nonatomic, copy, readonly) NSString *pendingRootPath;
|
|
32
|
+
|
|
33
|
+
/// Number of events currently buffered on disk
|
|
34
|
+
@property(nonatomic, readonly) NSInteger eventCount;
|
|
35
|
+
|
|
36
|
+
/// Timestamp of the last event written (milliseconds since epoch)
|
|
37
|
+
@property(nonatomic, readonly) NSTimeInterval lastEventTimestamp;
|
|
38
|
+
|
|
39
|
+
#pragma mark - Initialization
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates an event buffer for the specified session.
|
|
43
|
+
*
|
|
44
|
+
* @param sessionId The session ID to buffer events for.
|
|
45
|
+
* @param pendingRootPath Base directory for pending session data.
|
|
46
|
+
* @return A new event buffer instance.
|
|
47
|
+
*/
|
|
48
|
+
- (instancetype)initWithSessionId:(NSString *)sessionId
|
|
49
|
+
pendingRootPath:(NSString *)pendingRootPath;
|
|
50
|
+
|
|
51
|
+
/// Unavailable. Use initWithSessionId:pendingRootPath: instead.
|
|
52
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
53
|
+
|
|
54
|
+
#pragma mark - Event Operations
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Appends an event to the buffer, writing immediately to disk.
|
|
58
|
+
* This operation is synchronous and thread-safe.
|
|
59
|
+
*
|
|
60
|
+
* @param event The event dictionary to persist.
|
|
61
|
+
* @return YES if the event was successfully written, NO otherwise.
|
|
62
|
+
*/
|
|
63
|
+
- (BOOL)appendEvent:(NSDictionary *)event;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Appends multiple events to the buffer atomically.
|
|
67
|
+
*
|
|
68
|
+
* @param events Array of event dictionaries to persist.
|
|
69
|
+
* @return YES if all events were successfully written, NO otherwise.
|
|
70
|
+
*/
|
|
71
|
+
- (BOOL)appendEvents:(NSArray<NSDictionary *> *)events;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reads all buffered events from disk.
|
|
75
|
+
* Used for upload or recovery.
|
|
76
|
+
*
|
|
77
|
+
* @return Array of event dictionaries, or empty array if no events.
|
|
78
|
+
*/
|
|
79
|
+
- (NSArray<NSDictionary *> *)readAllEvents;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reads events that haven't been uploaded yet (after the given batch number).
|
|
83
|
+
*
|
|
84
|
+
* @param afterBatchNumber Only return events logged after this batch was
|
|
85
|
+
* uploaded.
|
|
86
|
+
* @return Array of event dictionaries.
|
|
87
|
+
*/
|
|
88
|
+
- (NSArray<NSDictionary *> *)readEventsAfterBatchNumber:
|
|
89
|
+
(NSInteger)afterBatchNumber;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Marks events up to the given index as uploaded.
|
|
93
|
+
* This allows incremental uploads without losing state.
|
|
94
|
+
*
|
|
95
|
+
* @param eventIndex The index of the last successfully uploaded event.
|
|
96
|
+
*/
|
|
97
|
+
- (void)markEventsUploadedUpToIndex:(NSInteger)eventIndex;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clears all buffered events from disk.
|
|
101
|
+
* Call after session is successfully closed.
|
|
102
|
+
*/
|
|
103
|
+
- (void)clearAllEvents;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Returns the timestamp of the last event, useful for session end time.
|
|
107
|
+
*/
|
|
108
|
+
- (NSTimeInterval)lastEventTimestampMs;
|
|
109
|
+
|
|
110
|
+
@end
|
|
111
|
+
|
|
112
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJEventBuffer.m
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Write-first event buffer implementation.
|
|
6
|
+
// Events are persisted to disk immediately in JSONL format.
|
|
7
|
+
//
|
|
8
|
+
// Copyright (c) 2026 Rejourney
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
#import "RJEventBuffer.h"
|
|
12
|
+
#import "../Core/RJLogger.h"
|
|
13
|
+
|
|
14
|
+
static void *kRJEventBufferQueueKey = &kRJEventBufferQueueKey;
|
|
15
|
+
|
|
16
|
+
@interface RJEventBuffer ()
|
|
17
|
+
|
|
18
|
+
@property(nonatomic, copy, readwrite) NSString *sessionId;
|
|
19
|
+
@property(nonatomic, copy, readwrite) NSString *pendingRootPath;
|
|
20
|
+
@property(nonatomic, strong) NSFileHandle *fileHandle;
|
|
21
|
+
@property(nonatomic, copy) NSString *eventsFilePath;
|
|
22
|
+
@property(nonatomic, assign, readwrite) NSInteger eventCount;
|
|
23
|
+
@property(nonatomic, assign, readwrite) NSTimeInterval lastEventTimestamp;
|
|
24
|
+
@property(nonatomic, strong) dispatch_queue_t writeQueue;
|
|
25
|
+
@property(nonatomic, assign) NSInteger uploadedEventCount;
|
|
26
|
+
|
|
27
|
+
@end
|
|
28
|
+
|
|
29
|
+
@implementation RJEventBuffer
|
|
30
|
+
|
|
31
|
+
#pragma mark - Initialization
|
|
32
|
+
|
|
33
|
+
- (instancetype)initWithSessionId:(NSString *)sessionId
|
|
34
|
+
pendingRootPath:(NSString *)pendingRootPath {
|
|
35
|
+
self = [super init];
|
|
36
|
+
if (self) {
|
|
37
|
+
_sessionId = [sessionId copy];
|
|
38
|
+
if (pendingRootPath.length == 0) {
|
|
39
|
+
NSString *defaultPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
|
|
40
|
+
NSUserDomainMask, YES)
|
|
41
|
+
.firstObject stringByAppendingPathComponent:@"rj_pending"];
|
|
42
|
+
_pendingRootPath = [defaultPath copy];
|
|
43
|
+
} else {
|
|
44
|
+
_pendingRootPath = [pendingRootPath copy];
|
|
45
|
+
}
|
|
46
|
+
_eventCount = 0;
|
|
47
|
+
_lastEventTimestamp = 0;
|
|
48
|
+
_uploadedEventCount = 0;
|
|
49
|
+
_writeQueue = dispatch_queue_create("com.rejourney.eventbuffer",
|
|
50
|
+
DISPATCH_QUEUE_SERIAL);
|
|
51
|
+
dispatch_queue_set_specific(_writeQueue, kRJEventBufferQueueKey,
|
|
52
|
+
kRJEventBufferQueueKey, NULL);
|
|
53
|
+
|
|
54
|
+
[self setupEventsFile];
|
|
55
|
+
}
|
|
56
|
+
return self;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (void)dealloc {
|
|
60
|
+
[self performWriteSync:^{
|
|
61
|
+
[self closeFileHandle];
|
|
62
|
+
}];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#pragma mark - File Setup
|
|
66
|
+
|
|
67
|
+
- (void)setupEventsFile {
|
|
68
|
+
NSString *sessionDir =
|
|
69
|
+
[self.pendingRootPath stringByAppendingPathComponent:self.sessionId];
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
73
|
+
if (![fm fileExistsAtPath:sessionDir]) {
|
|
74
|
+
NSError *error = nil;
|
|
75
|
+
[fm createDirectoryAtPath:sessionDir
|
|
76
|
+
withIntermediateDirectories:YES
|
|
77
|
+
attributes:nil
|
|
78
|
+
error:&error];
|
|
79
|
+
if (error) {
|
|
80
|
+
RJLogError(@"Failed to create session directory: %@", error);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
self.eventsFilePath =
|
|
86
|
+
[sessionDir stringByAppendingPathComponent:@"events.jsonl"];
|
|
87
|
+
|
|
88
|
+
if (![fm fileExistsAtPath:self.eventsFilePath]) {
|
|
89
|
+
NSDictionary *attrs = @{
|
|
90
|
+
NSFileProtectionKey: NSFileProtectionCompleteUntilFirstUserAuthentication
|
|
91
|
+
};
|
|
92
|
+
[fm createFileAtPath:self.eventsFilePath contents:nil attributes:attrs];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
NSError *error = nil;
|
|
97
|
+
self.fileHandle =
|
|
98
|
+
[NSFileHandle fileHandleForWritingAtPath:self.eventsFilePath];
|
|
99
|
+
if (!self.fileHandle) {
|
|
100
|
+
RJLogError(@"Failed to open events file for writing: %@",
|
|
101
|
+
self.eventsFilePath);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if (@available(iOS 13.0, *)) {
|
|
107
|
+
[self.fileHandle seekToEndReturningOffset:nil error:&error];
|
|
108
|
+
} else {
|
|
109
|
+
[self.fileHandle seekToEndOfFile];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
[self countExistingEvents];
|
|
114
|
+
|
|
115
|
+
RJLogDebug(@"Event buffer ready: %@ (%ld existing events)",
|
|
116
|
+
self.eventsFilePath, (long)self.eventCount);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
- (void)countExistingEvents {
|
|
120
|
+
NSError *error = nil;
|
|
121
|
+
NSString *content = [NSString stringWithContentsOfFile:self.eventsFilePath
|
|
122
|
+
encoding:NSUTF8StringEncoding
|
|
123
|
+
error:&error];
|
|
124
|
+
if (error || !content) {
|
|
125
|
+
self.eventCount = 0;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
NSArray<NSString *> *lines = [content componentsSeparatedByString:@"\n"];
|
|
130
|
+
NSInteger count = 0;
|
|
131
|
+
NSTimeInterval lastTs = 0;
|
|
132
|
+
|
|
133
|
+
for (NSString *line in lines) {
|
|
134
|
+
if (line.length == 0)
|
|
135
|
+
continue;
|
|
136
|
+
|
|
137
|
+
NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding];
|
|
138
|
+
NSDictionary *event = [NSJSONSerialization JSONObjectWithData:data
|
|
139
|
+
options:0
|
|
140
|
+
error:nil];
|
|
141
|
+
if (event) {
|
|
142
|
+
count++;
|
|
143
|
+
NSNumber *ts = event[@"timestamp"];
|
|
144
|
+
if (ts && [ts doubleValue] > lastTs) {
|
|
145
|
+
lastTs = [ts doubleValue];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
self.eventCount = count;
|
|
151
|
+
self.lastEventTimestamp = lastTs;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
- (void)closeFileHandle {
|
|
155
|
+
if (self.fileHandle) {
|
|
156
|
+
if (@available(iOS 13.0, *)) {
|
|
157
|
+
[self.fileHandle closeAndReturnError:nil];
|
|
158
|
+
} else {
|
|
159
|
+
[self.fileHandle closeFile];
|
|
160
|
+
}
|
|
161
|
+
self.fileHandle = nil;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#pragma mark - Event Operations
|
|
166
|
+
|
|
167
|
+
- (void)performWriteSync:(dispatch_block_t)block {
|
|
168
|
+
if (!block || !self.writeQueue) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (dispatch_get_specific(kRJEventBufferQueueKey)) {
|
|
173
|
+
block();
|
|
174
|
+
} else {
|
|
175
|
+
dispatch_sync(self.writeQueue, block);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
- (BOOL)appendEvent:(NSDictionary *)event {
|
|
180
|
+
if (!event)
|
|
181
|
+
return NO;
|
|
182
|
+
|
|
183
|
+
__block BOOL success = NO;
|
|
184
|
+
|
|
185
|
+
[self performWriteSync:^{
|
|
186
|
+
success = [self writeEventToDisk:event];
|
|
187
|
+
}];
|
|
188
|
+
|
|
189
|
+
return success;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
- (BOOL)appendEvents:(NSArray<NSDictionary *> *)events {
|
|
193
|
+
if (!events || events.count == 0)
|
|
194
|
+
return YES;
|
|
195
|
+
|
|
196
|
+
__block BOOL success = YES;
|
|
197
|
+
|
|
198
|
+
[self performWriteSync:^{
|
|
199
|
+
for (NSDictionary *event in events) {
|
|
200
|
+
if (![self writeEventToDisk:event]) {
|
|
201
|
+
success = NO;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}];
|
|
205
|
+
|
|
206
|
+
return success;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
- (BOOL)writeEventToDisk:(NSDictionary *)event {
|
|
210
|
+
if (!self.fileHandle) {
|
|
211
|
+
RJLogWarning(@"Event buffer file handle not available");
|
|
212
|
+
return NO;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@try {
|
|
216
|
+
NSError *jsonError = nil;
|
|
217
|
+
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event
|
|
218
|
+
options:0
|
|
219
|
+
error:&jsonError];
|
|
220
|
+
if (jsonError || !jsonData) {
|
|
221
|
+
RJLogWarning(@"Failed to serialize event: %@", jsonError);
|
|
222
|
+
return NO;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
NSMutableData *lineData = [jsonData mutableCopy];
|
|
227
|
+
[lineData appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if (@available(iOS 13.4, *)) {
|
|
231
|
+
NSError *writeError = nil;
|
|
232
|
+
[self.fileHandle writeData:lineData error:&writeError];
|
|
233
|
+
if (writeError) {
|
|
234
|
+
RJLogWarning(@"Failed to write event: %@", writeError);
|
|
235
|
+
return NO;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
@try {
|
|
239
|
+
[self.fileHandle writeData:lineData];
|
|
240
|
+
} @catch (NSException *e) {
|
|
241
|
+
RJLogWarning(@"Failed to write event: %@", e);
|
|
242
|
+
return NO;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if (@available(iOS 13.0, *)) {
|
|
248
|
+
[self.fileHandle synchronizeAndReturnError:nil];
|
|
249
|
+
} else {
|
|
250
|
+
[self.fileHandle synchronizeFile];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
self.eventCount++;
|
|
255
|
+
NSNumber *ts = event[@"timestamp"];
|
|
256
|
+
if (ts) {
|
|
257
|
+
self.lastEventTimestamp = [ts doubleValue];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return YES;
|
|
261
|
+
|
|
262
|
+
} @catch (NSException *exception) {
|
|
263
|
+
RJLogError(@"Exception writing event: %@", exception);
|
|
264
|
+
return NO;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
- (NSArray<NSDictionary *> *)readAllEvents {
|
|
269
|
+
__block NSMutableArray<NSDictionary *> *events = [NSMutableArray new];
|
|
270
|
+
|
|
271
|
+
[self performWriteSync:^{
|
|
272
|
+
NSError *error = nil;
|
|
273
|
+
NSString *content = [NSString stringWithContentsOfFile:self.eventsFilePath
|
|
274
|
+
encoding:NSUTF8StringEncoding
|
|
275
|
+
error:&error];
|
|
276
|
+
if (error || !content) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
NSArray<NSString *> *lines = [content componentsSeparatedByString:@"\n"];
|
|
281
|
+
for (NSString *line in lines) {
|
|
282
|
+
if (line.length == 0)
|
|
283
|
+
continue;
|
|
284
|
+
|
|
285
|
+
NSData *data = [line dataUsingEncoding:NSUTF8StringEncoding];
|
|
286
|
+
NSDictionary *event = [NSJSONSerialization JSONObjectWithData:data
|
|
287
|
+
options:0
|
|
288
|
+
error:nil];
|
|
289
|
+
if (event) {
|
|
290
|
+
[events addObject:event];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}];
|
|
294
|
+
|
|
295
|
+
return events;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
- (NSArray<NSDictionary *> *)readEventsAfterBatchNumber:
|
|
299
|
+
(NSInteger)afterBatchNumber {
|
|
300
|
+
|
|
301
|
+
NSArray<NSDictionary *> *allEvents = [self readAllEvents];
|
|
302
|
+
|
|
303
|
+
__block NSInteger uploadedCount = 0;
|
|
304
|
+
[self performWriteSync:^{
|
|
305
|
+
uploadedCount = self.uploadedEventCount;
|
|
306
|
+
}];
|
|
307
|
+
|
|
308
|
+
NSInteger startIndex = MAX(uploadedCount, MAX(0, afterBatchNumber));
|
|
309
|
+
if (startIndex >= allEvents.count) {
|
|
310
|
+
return @[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return [allEvents subarrayWithRange:NSMakeRange(
|
|
314
|
+
startIndex,
|
|
315
|
+
allEvents.count - startIndex)];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
- (void)markEventsUploadedUpToIndex:(NSInteger)eventIndex {
|
|
319
|
+
[self performWriteSync:^{
|
|
320
|
+
self.uploadedEventCount = eventIndex;
|
|
321
|
+
|
|
322
|
+
NSString *metaPath =
|
|
323
|
+
[[self.eventsFilePath stringByDeletingLastPathComponent]
|
|
324
|
+
stringByAppendingPathComponent:@"buffer_meta.json"];
|
|
325
|
+
NSDictionary *meta = @{
|
|
326
|
+
@"uploadedEventCount" : @(self.uploadedEventCount),
|
|
327
|
+
@"lastEventTimestamp" : @(self.lastEventTimestamp)
|
|
328
|
+
};
|
|
329
|
+
NSData *data = [NSJSONSerialization dataWithJSONObject:meta
|
|
330
|
+
options:0
|
|
331
|
+
error:nil];
|
|
332
|
+
[data writeToFile:metaPath atomically:YES];
|
|
333
|
+
}];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
- (void)clearAllEvents {
|
|
337
|
+
[self performWriteSync:^{
|
|
338
|
+
[self closeFileHandle];
|
|
339
|
+
|
|
340
|
+
NSFileManager *fm = [NSFileManager defaultManager];
|
|
341
|
+
[fm removeItemAtPath:self.eventsFilePath error:nil];
|
|
342
|
+
|
|
343
|
+
NSString *metaPath =
|
|
344
|
+
[[self.eventsFilePath stringByDeletingLastPathComponent]
|
|
345
|
+
stringByAppendingPathComponent:@"buffer_meta.json"];
|
|
346
|
+
[fm removeItemAtPath:metaPath error:nil];
|
|
347
|
+
|
|
348
|
+
self.eventCount = 0;
|
|
349
|
+
self.uploadedEventCount = 0;
|
|
350
|
+
self.lastEventTimestamp = 0;
|
|
351
|
+
}];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
- (NSTimeInterval)lastEventTimestampMs {
|
|
355
|
+
return self.lastEventTimestamp;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJGzipUtils.h
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Gzip compression and Base64 decoding utilities.
|
|
6
|
+
//
|
|
7
|
+
// Copyright (c) 2026 Rejourney
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
#import <Foundation/Foundation.h>
|
|
11
|
+
|
|
12
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gzip compress data using zlib (with gzip headers).
|
|
16
|
+
*
|
|
17
|
+
* @param input The data to compress.
|
|
18
|
+
* @param error Optional pointer to receive error information.
|
|
19
|
+
* @return The compressed data, or nil on failure.
|
|
20
|
+
*/
|
|
21
|
+
NSData *_Nullable RJGzipData(NSData *input, NSError **error);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Decode base64 data from a data URI or plain base64 string.
|
|
25
|
+
* Handles data URIs (removes "data:...;base64," prefix) and
|
|
26
|
+
* delta prefixes ("delta:...").
|
|
27
|
+
*
|
|
28
|
+
* @param dataString The base64 string to decode.
|
|
29
|
+
* @return The decoded data, or nil on failure.
|
|
30
|
+
*/
|
|
31
|
+
NSData *_Nullable RJDecodeBase64Data(NSString *dataString);
|
|
32
|
+
|
|
33
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJGzipUtils.m
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Gzip compression and Base64 decoding utilities implementation.
|
|
6
|
+
//
|
|
7
|
+
// Copyright (c) 2026 Rejourney
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
#import "RJGzipUtils.h"
|
|
11
|
+
#import <zlib.h>
|
|
12
|
+
|
|
13
|
+
NSData *_Nullable RJGzipData(NSData *input, NSError **error) {
|
|
14
|
+
if (!input || input.length == 0)
|
|
15
|
+
return input;
|
|
16
|
+
|
|
17
|
+
z_stream stream;
|
|
18
|
+
stream.zalloc = Z_NULL;
|
|
19
|
+
stream.zfree = Z_NULL;
|
|
20
|
+
stream.opaque = Z_NULL;
|
|
21
|
+
stream.next_in = (Bytef *)input.bytes;
|
|
22
|
+
stream.avail_in = (uInt)input.length;
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8,
|
|
26
|
+
Z_DEFAULT_STRATEGY) != Z_OK) {
|
|
27
|
+
if (error) {
|
|
28
|
+
*error =
|
|
29
|
+
[NSError errorWithDomain:@"com.rejourney.gzip"
|
|
30
|
+
code:-1
|
|
31
|
+
userInfo:@{
|
|
32
|
+
NSLocalizedDescriptionKey : @"Failed to init gzip"
|
|
33
|
+
}];
|
|
34
|
+
}
|
|
35
|
+
return nil;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
NSMutableData *compressed =
|
|
39
|
+
[NSMutableData dataWithLength:input.length * 1.1 + 32];
|
|
40
|
+
|
|
41
|
+
int status;
|
|
42
|
+
do {
|
|
43
|
+
if (stream.total_out >= compressed.length) {
|
|
44
|
+
[compressed increaseLengthBy:input.length / 2];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
stream.next_out = (Bytef *)compressed.mutableBytes + stream.total_out;
|
|
48
|
+
stream.avail_out = (uInt)(compressed.length - stream.total_out);
|
|
49
|
+
|
|
50
|
+
status = deflate(&stream, Z_FINISH);
|
|
51
|
+
} while (status == Z_OK);
|
|
52
|
+
|
|
53
|
+
if (status != Z_STREAM_END) {
|
|
54
|
+
deflateEnd(&stream);
|
|
55
|
+
if (error) {
|
|
56
|
+
*error =
|
|
57
|
+
[NSError errorWithDomain:@"com.rejourney.gzip"
|
|
58
|
+
code:status
|
|
59
|
+
userInfo:@{
|
|
60
|
+
NSLocalizedDescriptionKey : @"Failed to gzip data"
|
|
61
|
+
}];
|
|
62
|
+
}
|
|
63
|
+
return nil;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
deflateEnd(&stream);
|
|
67
|
+
[compressed setLength:stream.total_out];
|
|
68
|
+
return compressed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
NSData *_Nullable RJDecodeBase64Data(NSString *dataString) {
|
|
72
|
+
if (!dataString || dataString.length == 0)
|
|
73
|
+
return nil;
|
|
74
|
+
|
|
75
|
+
NSString *clean = dataString;
|
|
76
|
+
NSRange comma = [dataString rangeOfString:@","];
|
|
77
|
+
if (comma.location != NSNotFound) {
|
|
78
|
+
clean = [dataString substringFromIndex:comma.location + 1];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if ([clean hasPrefix:@"delta:"]) {
|
|
83
|
+
clean = [clean substringFromIndex:6];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [[NSData alloc]
|
|
87
|
+
initWithBase64EncodedString:clean
|
|
88
|
+
options:NSDataBase64DecodingIgnoreUnknownCharacters];
|
|
89
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJKeychainManager.h
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Secure Keychain storage for sensitive credentials.
|
|
6
|
+
//
|
|
7
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
// you may not use this file except in compliance with the License.
|
|
9
|
+
// You may obtain a copy of the License at
|
|
10
|
+
//
|
|
11
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
//
|
|
13
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
// See the License for the specific language governing permissions and
|
|
17
|
+
// limitations under the License.
|
|
18
|
+
//
|
|
19
|
+
// Copyright (c) 2026 Rejourney
|
|
20
|
+
//
|
|
21
|
+
|
|
22
|
+
#import <Foundation/Foundation.h>
|
|
23
|
+
|
|
24
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Manages secure storage of sensitive data in iOS Keychain.
|
|
28
|
+
*/
|
|
29
|
+
@interface RJKeychainManager : NSObject
|
|
30
|
+
|
|
31
|
+
/// Shared instance
|
|
32
|
+
+ (instancetype)sharedManager;
|
|
33
|
+
|
|
34
|
+
/// Store a string value securely
|
|
35
|
+
- (BOOL)setString:(NSString *)value forKey:(NSString *)key;
|
|
36
|
+
|
|
37
|
+
/// Retrieve a string value
|
|
38
|
+
- (nullable NSString *)stringForKey:(NSString *)key;
|
|
39
|
+
|
|
40
|
+
/// Delete a value
|
|
41
|
+
- (BOOL)deleteValueForKey:(NSString *)key;
|
|
42
|
+
|
|
43
|
+
/// Clear all Rejourney keychain items
|
|
44
|
+
- (void)clearAll;
|
|
45
|
+
|
|
46
|
+
@end
|
|
47
|
+
|
|
48
|
+
NS_ASSUME_NONNULL_END
|