@rejourneyco/react-native 1.0.7

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 (105) hide show
  1. package/README.md +29 -0
  2. package/android/build.gradle.kts +135 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/proguard-rules.pro +1 -0
  5. package/android/src/main/AndroidManifest.xml +15 -0
  6. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
  7. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
  8. package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
  9. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
  10. package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
  11. package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
  12. package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
  13. package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
  14. package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
  15. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
  16. package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
  17. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
  18. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
  19. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
  20. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
  21. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
  22. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
  23. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
  24. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
  25. package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
  26. package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
  27. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
  28. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  29. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
  30. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  31. package/ios/Engine/DeviceRegistrar.swift +288 -0
  32. package/ios/Engine/DiagnosticLog.swift +387 -0
  33. package/ios/Engine/RejourneyImpl.swift +719 -0
  34. package/ios/Recording/AnrSentinel.swift +142 -0
  35. package/ios/Recording/EventBuffer.swift +326 -0
  36. package/ios/Recording/InteractionRecorder.swift +428 -0
  37. package/ios/Recording/ReplayOrchestrator.swift +624 -0
  38. package/ios/Recording/SegmentDispatcher.swift +492 -0
  39. package/ios/Recording/StabilityMonitor.swift +223 -0
  40. package/ios/Recording/TelemetryPipeline.swift +547 -0
  41. package/ios/Recording/ViewHierarchyScanner.swift +156 -0
  42. package/ios/Recording/VisualCapture.swift +675 -0
  43. package/ios/Rejourney.h +38 -0
  44. package/ios/Rejourney.mm +375 -0
  45. package/ios/Utility/DataCompression.swift +55 -0
  46. package/ios/Utility/ImageBlur.swift +89 -0
  47. package/ios/Utility/RuntimeMethodSwap.swift +41 -0
  48. package/ios/Utility/ViewIdentifier.swift +37 -0
  49. package/lib/commonjs/NativeRejourney.js +40 -0
  50. package/lib/commonjs/components/Mask.js +88 -0
  51. package/lib/commonjs/index.js +1443 -0
  52. package/lib/commonjs/sdk/autoTracking.js +1087 -0
  53. package/lib/commonjs/sdk/constants.js +166 -0
  54. package/lib/commonjs/sdk/errorTracking.js +187 -0
  55. package/lib/commonjs/sdk/index.js +50 -0
  56. package/lib/commonjs/sdk/metricsTracking.js +205 -0
  57. package/lib/commonjs/sdk/navigation.js +128 -0
  58. package/lib/commonjs/sdk/networkInterceptor.js +375 -0
  59. package/lib/commonjs/sdk/utils.js +433 -0
  60. package/lib/commonjs/sdk/version.js +13 -0
  61. package/lib/commonjs/types/expo-router.d.js +2 -0
  62. package/lib/commonjs/types/index.js +2 -0
  63. package/lib/module/NativeRejourney.js +38 -0
  64. package/lib/module/components/Mask.js +83 -0
  65. package/lib/module/index.js +1341 -0
  66. package/lib/module/sdk/autoTracking.js +1059 -0
  67. package/lib/module/sdk/constants.js +154 -0
  68. package/lib/module/sdk/errorTracking.js +177 -0
  69. package/lib/module/sdk/index.js +26 -0
  70. package/lib/module/sdk/metricsTracking.js +187 -0
  71. package/lib/module/sdk/navigation.js +120 -0
  72. package/lib/module/sdk/networkInterceptor.js +364 -0
  73. package/lib/module/sdk/utils.js +412 -0
  74. package/lib/module/sdk/version.js +7 -0
  75. package/lib/module/types/expo-router.d.js +2 -0
  76. package/lib/module/types/index.js +2 -0
  77. package/lib/typescript/NativeRejourney.d.ts +160 -0
  78. package/lib/typescript/components/Mask.d.ts +54 -0
  79. package/lib/typescript/index.d.ts +117 -0
  80. package/lib/typescript/sdk/autoTracking.d.ts +226 -0
  81. package/lib/typescript/sdk/constants.d.ts +138 -0
  82. package/lib/typescript/sdk/errorTracking.d.ts +47 -0
  83. package/lib/typescript/sdk/index.d.ts +24 -0
  84. package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
  85. package/lib/typescript/sdk/navigation.d.ts +48 -0
  86. package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
  87. package/lib/typescript/sdk/utils.d.ts +193 -0
  88. package/lib/typescript/sdk/version.d.ts +6 -0
  89. package/lib/typescript/types/index.d.ts +618 -0
  90. package/package.json +122 -0
  91. package/rejourney.podspec +23 -0
  92. package/src/NativeRejourney.ts +185 -0
  93. package/src/components/Mask.tsx +93 -0
  94. package/src/index.ts +1555 -0
  95. package/src/sdk/autoTracking.ts +1245 -0
  96. package/src/sdk/constants.ts +155 -0
  97. package/src/sdk/errorTracking.ts +231 -0
  98. package/src/sdk/index.ts +25 -0
  99. package/src/sdk/metricsTracking.ts +227 -0
  100. package/src/sdk/navigation.ts +152 -0
  101. package/src/sdk/networkInterceptor.ts +423 -0
  102. package/src/sdk/utils.ts +442 -0
  103. package/src/sdk/version.ts +6 -0
  104. package/src/types/expo-router.d.ts +7 -0
  105. package/src/types/index.ts +709 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Copyright 2026 Rejourney
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import Foundation
18
+
19
+ @objc(AnrSentinel)
20
+ public final class AnrSentinel: NSObject {
21
+
22
+ @objc public static let shared = AnrSentinel()
23
+
24
+ private let _freezeThreshold: TimeInterval = 5.0
25
+ private let _pollFrequency: TimeInterval = 2.0
26
+
27
+ private var _watchThread: Thread?
28
+ private var _volatile = VolatileState()
29
+ private let _stateLock = os_unfair_lock_t.allocate(capacity: 1)
30
+
31
+ private override init() {
32
+ _stateLock.initialize(to: os_unfair_lock())
33
+ super.init()
34
+ }
35
+
36
+ deinit {
37
+ _stateLock.deallocate()
38
+ }
39
+
40
+ @objc public func activate() {
41
+ os_unfair_lock_lock(_stateLock)
42
+ guard _watchThread == nil else {
43
+ os_unfair_lock_unlock(_stateLock)
44
+ return
45
+ }
46
+
47
+ _volatile.running = true
48
+ _volatile.lastResponse = Date().timeIntervalSince1970
49
+
50
+ let t = Thread { [weak self] in self?._watchLoop() }
51
+ t.name = "co.rejourney.anr"
52
+ t.qualityOfService = .utility
53
+ _watchThread = t
54
+ os_unfair_lock_unlock(_stateLock)
55
+
56
+ t.start()
57
+ }
58
+
59
+ @objc public func halt() {
60
+ os_unfair_lock_lock(_stateLock)
61
+ _volatile.running = false
62
+ _watchThread = nil
63
+ os_unfair_lock_unlock(_stateLock)
64
+ }
65
+
66
+ private func _watchLoop() {
67
+ while true {
68
+ os_unfair_lock_lock(_stateLock)
69
+ let running = _volatile.running
70
+ os_unfair_lock_unlock(_stateLock)
71
+ guard running else { break }
72
+
73
+ _sendPing()
74
+ Thread.sleep(forTimeInterval: _pollFrequency)
75
+ _checkPong()
76
+ }
77
+ }
78
+
79
+ private func _sendPing() {
80
+ os_unfair_lock_lock(_stateLock)
81
+ if _volatile.awaitingPong {
82
+ os_unfair_lock_unlock(_stateLock)
83
+ return
84
+ }
85
+ _volatile.awaitingPong = true
86
+ os_unfair_lock_unlock(_stateLock)
87
+
88
+ DispatchQueue.main.async { [weak self] in
89
+ guard let self else { return }
90
+ os_unfair_lock_lock(self._stateLock)
91
+ self._volatile.lastResponse = Date().timeIntervalSince1970
92
+ self._volatile.awaitingPong = false
93
+ os_unfair_lock_unlock(self._stateLock)
94
+ }
95
+ }
96
+
97
+ private func _checkPong() {
98
+ os_unfair_lock_lock(_stateLock)
99
+ let awaiting = _volatile.awaitingPong
100
+ let last = _volatile.lastResponse
101
+ os_unfair_lock_unlock(_stateLock)
102
+
103
+ guard awaiting else { return }
104
+
105
+ let delta = Date().timeIntervalSince1970 - last
106
+ if delta >= _freezeThreshold {
107
+ _reportFreeze(duration: delta)
108
+ }
109
+ }
110
+
111
+ private func _reportFreeze(duration: TimeInterval) {
112
+ DiagnosticLog.emit(.caution, "Main thread frozen for \(String(format: "%.1f", duration))s")
113
+
114
+ ReplayOrchestrator.shared.incrementStalledTally()
115
+
116
+ let trace = Thread.callStackSymbols.joined(separator: "\n")
117
+ let ms = Int(duration * 1000)
118
+
119
+ TelemetryPipeline.shared.recordAnrEvent(durationMs: ms, stack: trace)
120
+ }
121
+ }
122
+
123
+ private struct VolatileState {
124
+ var running = false
125
+ var awaitingPong = false
126
+ var lastResponse: TimeInterval = 0
127
+ }
128
+
129
+ @objc(ResponsivenessWatcher)
130
+ public final class ResponsivenessWatcher: NSObject {
131
+ @objc public static let shared = ResponsivenessWatcher()
132
+
133
+ private override init() { super.init() }
134
+
135
+ @objc public func activate() {
136
+ AnrSentinel.shared.activate()
137
+ }
138
+
139
+ @objc public func halt() {
140
+ AnrSentinel.shared.halt()
141
+ }
142
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Copyright 2026 Rejourney
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import Foundation
18
+
19
+ /// Write-first event buffer for crash-safe event persistence.
20
+ /// Events are written to disk on append for crash safety.
21
+ /// JSONL format (one JSON object per line).
22
+ @objc(RJEventBuffer)
23
+ public final class EventBuffer: NSObject {
24
+
25
+ @objc public static let shared = EventBuffer()
26
+
27
+ private let _lock = NSLock()
28
+ private var _sessionId: String?
29
+ private var _eventsFile: URL?
30
+ private var _metaFile: URL?
31
+ private var _fileHandle: FileHandle?
32
+ private var _eventCount: Int = 0
33
+ private var _lastEventTimestamp: Int64 = 0
34
+ private var _pendingRootPath: URL?
35
+ private var _isShutdown = false
36
+
37
+ @objc public var eventCount: Int {
38
+ _lock.lock()
39
+ defer { _lock.unlock() }
40
+ return _eventCount
41
+ }
42
+
43
+ @objc public var lastEventTimestamp: Int64 {
44
+ _lock.lock()
45
+ defer { _lock.unlock() }
46
+ return _lastEventTimestamp
47
+ }
48
+
49
+ private override init() {
50
+ super.init()
51
+ }
52
+
53
+ // MARK: - Public API
54
+
55
+ @objc public func configure(sessionId: String) {
56
+ _lock.lock()
57
+ defer { _lock.unlock() }
58
+
59
+ _closeFileHandle()
60
+
61
+ _sessionId = sessionId
62
+ _isShutdown = false
63
+
64
+ guard let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
65
+ DiagnosticLog.debugStorage(op: "CONFIGURE", key: sessionId, success: false, detail: "No cache directory")
66
+ return
67
+ }
68
+
69
+ _pendingRootPath = cacheDir.appendingPathComponent("rj_pending")
70
+ let sessionDir = _pendingRootPath!.appendingPathComponent(sessionId)
71
+
72
+ do {
73
+ try FileManager.default.createDirectory(at: sessionDir, withIntermediateDirectories: true)
74
+ } catch {
75
+ DiagnosticLog.debugStorage(op: "CONFIGURE", key: sessionId, success: false, detail: "Failed to create directory: \(error)")
76
+ return
77
+ }
78
+
79
+ _eventsFile = sessionDir.appendingPathComponent("events.jsonl")
80
+ _metaFile = sessionDir.appendingPathComponent("buffer_meta.json")
81
+
82
+ _countExistingEvents()
83
+ _openFileHandle()
84
+
85
+ DiagnosticLog.debugStorage(op: "CONFIGURE", key: sessionId, success: true, detail: "Ready with \(_eventCount) existing events")
86
+ }
87
+
88
+ @objc public func appendEvent(_ event: [String: Any]) -> Bool {
89
+ _lock.lock()
90
+ defer { _lock.unlock() }
91
+
92
+ guard !_isShutdown else {
93
+ DiagnosticLog.debugStorage(op: "APPEND", key: event["type"] as? String ?? "unknown", success: false, detail: "Buffer is shutdown")
94
+ return false
95
+ }
96
+
97
+ return _writeEventToDisk(event)
98
+ }
99
+
100
+ @objc public func flush() -> Bool {
101
+ _lock.lock()
102
+ defer { _lock.unlock() }
103
+
104
+ guard let handle = _fileHandle else { return false }
105
+
106
+ do {
107
+ try handle.synchronize()
108
+ _saveMeta()
109
+ return true
110
+ } catch {
111
+ DiagnosticLog.debugStorage(op: "FLUSH", key: _sessionId ?? "", success: false, detail: "\(error)")
112
+ return false
113
+ }
114
+ }
115
+
116
+ @objc public func shutdown() {
117
+ _lock.lock()
118
+ defer { _lock.unlock() }
119
+
120
+ _isShutdown = true
121
+ _saveMeta()
122
+ _closeFileHandle()
123
+ }
124
+
125
+ @objc public func readPendingEvents() -> [[String: Any]] {
126
+ _lock.lock()
127
+ defer { _lock.unlock() }
128
+
129
+ guard let eventsFile = _eventsFile, FileManager.default.fileExists(atPath: eventsFile.path) else {
130
+ return []
131
+ }
132
+
133
+ var events: [[String: Any]] = []
134
+
135
+ do {
136
+ let content = try String(contentsOf: eventsFile, encoding: .utf8)
137
+ let lines = content.components(separatedBy: .newlines)
138
+
139
+ for line in lines where !line.isEmpty {
140
+ if let data = line.data(using: .utf8),
141
+ let event = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
142
+ events.append(event)
143
+ }
144
+ }
145
+ } catch {
146
+ DiagnosticLog.debugStorage(op: "READ", key: _sessionId ?? "", success: false, detail: "\(error)")
147
+ }
148
+
149
+ return events
150
+ }
151
+
152
+ @objc public func clearEvents() {
153
+ _lock.lock()
154
+ defer { _lock.unlock() }
155
+
156
+ _closeFileHandle()
157
+
158
+ if let eventsFile = _eventsFile {
159
+ try? FileManager.default.removeItem(at: eventsFile)
160
+ }
161
+ if let metaFile = _metaFile {
162
+ try? FileManager.default.removeItem(at: metaFile)
163
+ }
164
+
165
+ _eventCount = 0
166
+ _lastEventTimestamp = 0
167
+
168
+ _openFileHandle()
169
+ }
170
+
171
+ @objc public func clearSession(_ sessionId: String) {
172
+ guard let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { return }
173
+ let sessionDir = cacheDir.appendingPathComponent("rj_pending").appendingPathComponent(sessionId)
174
+ try? FileManager.default.removeItem(at: sessionDir)
175
+ }
176
+
177
+ /// Returns list of session IDs that have pending data on disk
178
+ @objc public func getPendingSessions() -> [String] {
179
+ guard let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { return [] }
180
+ let pendingRoot = cacheDir.appendingPathComponent("rj_pending")
181
+
182
+ guard let contents = try? FileManager.default.contentsOfDirectory(at: pendingRoot, includingPropertiesForKeys: nil) else {
183
+ return []
184
+ }
185
+
186
+ return contents.compactMap { url in
187
+ let eventsFile = url.appendingPathComponent("events.jsonl")
188
+ if FileManager.default.fileExists(atPath: eventsFile.path) {
189
+ return url.lastPathComponent
190
+ }
191
+ return nil
192
+ }
193
+ }
194
+
195
+ /// Read events from a specific session's pending data
196
+ @objc public func readEventsForSession(_ sessionId: String) -> [[String: Any]] {
197
+ guard let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { return [] }
198
+ let eventsFile = cacheDir.appendingPathComponent("rj_pending").appendingPathComponent(sessionId).appendingPathComponent("events.jsonl")
199
+
200
+ guard FileManager.default.fileExists(atPath: eventsFile.path) else { return [] }
201
+
202
+ var events: [[String: Any]] = []
203
+
204
+ do {
205
+ let content = try String(contentsOf: eventsFile, encoding: .utf8)
206
+ let lines = content.components(separatedBy: .newlines)
207
+
208
+ for line in lines where !line.isEmpty {
209
+ if let data = line.data(using: .utf8),
210
+ let event = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
211
+ events.append(event)
212
+ }
213
+ }
214
+ } catch {
215
+ DiagnosticLog.debugStorage(op: "READ_SESSION", key: sessionId, success: false, detail: "\(error)")
216
+ }
217
+
218
+ return events
219
+ }
220
+
221
+ // MARK: - Private Methods
222
+
223
+ private func _countExistingEvents() {
224
+ guard let eventsFile = _eventsFile, FileManager.default.fileExists(atPath: eventsFile.path) else {
225
+ _eventCount = 0
226
+ _lastEventTimestamp = 0
227
+ return
228
+ }
229
+
230
+ do {
231
+ let content = try String(contentsOf: eventsFile, encoding: .utf8)
232
+ let lines = content.components(separatedBy: .newlines)
233
+
234
+ var count = 0
235
+ var lastTs: Int64 = 0
236
+
237
+ for line in lines where !line.isEmpty {
238
+ if let data = line.data(using: .utf8),
239
+ let event = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
240
+ count += 1
241
+ if let ts = event["timestamp"] as? Int64, ts > lastTs {
242
+ lastTs = ts
243
+ } else if let ts = event["timestamp"] as? Int, Int64(ts) > lastTs {
244
+ lastTs = Int64(ts)
245
+ }
246
+ }
247
+ }
248
+
249
+ _eventCount = count
250
+ _lastEventTimestamp = lastTs
251
+
252
+ // Load meta if exists
253
+ if let metaFile = _metaFile, FileManager.default.fileExists(atPath: metaFile.path) {
254
+ if let metaData = try? Data(contentsOf: metaFile),
255
+ let meta = try? JSONSerialization.jsonObject(with: metaData) as? [String: Any] {
256
+ // Could track uploadedEventCount here
257
+ }
258
+ }
259
+ } catch {
260
+ _eventCount = 0
261
+ _lastEventTimestamp = 0
262
+ }
263
+ }
264
+
265
+ private func _openFileHandle() {
266
+ guard let eventsFile = _eventsFile else { return }
267
+
268
+ if !FileManager.default.fileExists(atPath: eventsFile.path) {
269
+ FileManager.default.createFile(atPath: eventsFile.path, contents: nil)
270
+ }
271
+
272
+ do {
273
+ _fileHandle = try FileHandle(forWritingTo: eventsFile)
274
+ try _fileHandle?.seekToEnd()
275
+ } catch {
276
+ DiagnosticLog.debugStorage(op: "OPEN_HANDLE", key: _sessionId ?? "", success: false, detail: "\(error)")
277
+ }
278
+ }
279
+
280
+ private func _closeFileHandle() {
281
+ try? _fileHandle?.close()
282
+ _fileHandle = nil
283
+ }
284
+
285
+ private func _writeEventToDisk(_ event: [String: Any]) -> Bool {
286
+ guard let handle = _fileHandle else {
287
+ _openFileHandle()
288
+ guard _fileHandle != nil else { return false }
289
+ return _writeEventToDisk(event)
290
+ }
291
+
292
+ do {
293
+ let data = try JSONSerialization.data(withJSONObject: event)
294
+ var line = data
295
+ line.append(0x0A) // newline
296
+
297
+ try handle.write(contentsOf: line)
298
+
299
+ _eventCount += 1
300
+ if let ts = event["timestamp"] as? Int64 {
301
+ _lastEventTimestamp = ts
302
+ } else if let ts = event["timestamp"] as? Int {
303
+ _lastEventTimestamp = Int64(ts)
304
+ }
305
+
306
+ return true
307
+ } catch {
308
+ DiagnosticLog.debugStorage(op: "WRITE", key: event["type"] as? String ?? "unknown", success: false, detail: "\(error)")
309
+ return false
310
+ }
311
+ }
312
+
313
+ private func _saveMeta() {
314
+ guard let metaFile = _metaFile else { return }
315
+
316
+ let meta: [String: Any] = [
317
+ "eventCount": _eventCount,
318
+ "lastEventTimestamp": _lastEventTimestamp,
319
+ "savedAt": Date().timeIntervalSince1970 * 1000
320
+ ]
321
+
322
+ if let data = try? JSONSerialization.data(withJSONObject: meta) {
323
+ try? data.write(to: metaFile)
324
+ }
325
+ }
326
+ }