@crumbsdk/react-native 0.0.1-rc.3

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 (67) hide show
  1. package/CrumbReactNative.podspec +38 -0
  2. package/LICENSE +201 -0
  3. package/README.md +109 -0
  4. package/android/CMakeLists.txt +20 -0
  5. package/android/build.gradle +118 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +10 -0
  8. package/android/src/main/java/com/margelo/nitro/crumbsdk/reactnative/CrumbReactNative.kt +228 -0
  9. package/android/src/main/java/com/margelo/nitro/crumbsdk/reactnative/ReactNativePackage.kt +22 -0
  10. package/ios/CrumbReactNative.swift +283 -0
  11. package/lib/module/CrumbReactNative.nitro.js +4 -0
  12. package/lib/module/CrumbReactNative.nitro.js.map +1 -0
  13. package/lib/module/NativeCrumbReactNative.js +5 -0
  14. package/lib/module/NativeCrumbReactNative.js.map +1 -0
  15. package/lib/module/NativeCrumbReactNative.web.js +18 -0
  16. package/lib/module/NativeCrumbReactNative.web.js.map +1 -0
  17. package/lib/module/index.js +60 -0
  18. package/lib/module/index.js.map +1 -0
  19. package/lib/module/log-buffer.js +200 -0
  20. package/lib/module/log-buffer.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/module/types.js +2 -0
  23. package/lib/module/types.js.map +1 -0
  24. package/lib/typescript/package.json +1 -0
  25. package/lib/typescript/src/CrumbReactNative.nitro.d.ts +12 -0
  26. package/lib/typescript/src/CrumbReactNative.nitro.d.ts.map +1 -0
  27. package/lib/typescript/src/NativeCrumbReactNative.d.ts +4 -0
  28. package/lib/typescript/src/NativeCrumbReactNative.d.ts.map +1 -0
  29. package/lib/typescript/src/NativeCrumbReactNative.web.d.ts +4 -0
  30. package/lib/typescript/src/NativeCrumbReactNative.web.d.ts.map +1 -0
  31. package/lib/typescript/src/index.d.ts +19 -0
  32. package/lib/typescript/src/index.d.ts.map +1 -0
  33. package/lib/typescript/src/log-buffer.d.ts +8 -0
  34. package/lib/typescript/src/log-buffer.d.ts.map +1 -0
  35. package/lib/typescript/src/types.d.ts +55 -0
  36. package/lib/typescript/src/types.d.ts.map +1 -0
  37. package/nitro.json +24 -0
  38. package/nitrogen/generated/.gitattributes +1 -0
  39. package/nitrogen/generated/android/c++/JHybridCrumbReactNativeSpec.cpp +94 -0
  40. package/nitrogen/generated/android/c++/JHybridCrumbReactNativeSpec.hpp +67 -0
  41. package/nitrogen/generated/android/crumbsdk_reactnative+autolinking.cmake +81 -0
  42. package/nitrogen/generated/android/crumbsdk_reactnative+autolinking.gradle +27 -0
  43. package/nitrogen/generated/android/crumbsdk_reactnativeOnLoad.cpp +54 -0
  44. package/nitrogen/generated/android/crumbsdk_reactnativeOnLoad.hpp +34 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/crumbsdk/reactnative/HybridCrumbReactNativeSpec.kt +73 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/crumbsdk/reactnative/crumbsdk_reactnativeOnLoad.kt +35 -0
  47. package/nitrogen/generated/ios/CrumbReactNative+autolinking.rb +62 -0
  48. package/nitrogen/generated/ios/CrumbReactNative-Swift-Cxx-Bridge.cpp +49 -0
  49. package/nitrogen/generated/ios/CrumbReactNative-Swift-Cxx-Bridge.hpp +119 -0
  50. package/nitrogen/generated/ios/CrumbReactNative-Swift-Cxx-Umbrella.hpp +45 -0
  51. package/nitrogen/generated/ios/CrumbReactNativeAutolinking.mm +33 -0
  52. package/nitrogen/generated/ios/CrumbReactNativeAutolinking.swift +26 -0
  53. package/nitrogen/generated/ios/c++/HybridCrumbReactNativeSpecSwift.cpp +11 -0
  54. package/nitrogen/generated/ios/c++/HybridCrumbReactNativeSpecSwift.hpp +109 -0
  55. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  56. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  57. package/nitrogen/generated/ios/swift/HybridCrumbReactNativeSpec.swift +59 -0
  58. package/nitrogen/generated/ios/swift/HybridCrumbReactNativeSpec_cxx.swift +197 -0
  59. package/nitrogen/generated/shared/c++/HybridCrumbReactNativeSpec.cpp +25 -0
  60. package/nitrogen/generated/shared/c++/HybridCrumbReactNativeSpec.hpp +67 -0
  61. package/package.json +138 -0
  62. package/src/CrumbReactNative.nitro.ts +12 -0
  63. package/src/NativeCrumbReactNative.ts +6 -0
  64. package/src/NativeCrumbReactNative.web.ts +20 -0
  65. package/src/index.ts +115 -0
  66. package/src/log-buffer.ts +265 -0
  67. package/src/types.ts +70 -0
@@ -0,0 +1,228 @@
1
+ package com.margelo.nitro.crumbsdk.reactnative
2
+
3
+ import android.app.Application
4
+ import android.os.Build
5
+ import com.facebook.react.bridge.UiThreadUtil
6
+ import com.margelo.nitro.NitroModules
7
+ import com.margelo.nitro.core.Promise
8
+ import dev.crumb.core.Crumb
9
+ import dev.crumb.core.CrumbCaptureOptions
10
+ import dev.crumb.core.CrumbConfiguration
11
+ import dev.crumb.core.CrumbDiagnosticsOptions
12
+ import dev.crumb.core.CrumbInvocation
13
+ import dev.crumb.core.CrumbLogEntry
14
+ import dev.crumb.core.CrumbLogLevel
15
+ import dev.crumb.core.CrumbLogOptions
16
+ import dev.crumb.core.CrumbLogProvider
17
+ import dev.crumb.core.CrumbPrivacyOptions
18
+ import dev.crumb.core.CrumbRelease
19
+ import dev.crumb.core.CrumbUploadOptions
20
+ import dev.crumb.ui.CrumbReporter
21
+ import org.json.JSONObject
22
+
23
+ class CrumbReactNative : HybridCrumbReactNativeSpec() {
24
+ private val logBuffer = ReactNativeLogBuffer()
25
+
26
+ override fun start(configurationJson: String) {
27
+ val context = requireNotNull(NitroModules.applicationContext) {
28
+ "React Native is not ready. Call Crumb.start after the app has mounted."
29
+ }
30
+ val payload = JSONObject(configurationJson)
31
+ val release = payload.optJSONObject("release") ?: JSONObject()
32
+ val diagnostics = payload.optJSONObject("diagnostics")
33
+ val logs = diagnostics?.optJSONObject("logs")
34
+ val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
35
+ val nativeBuild = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
36
+ packageInfo.longVersionCode.toString()
37
+ } else {
38
+ @Suppress("DEPRECATION")
39
+ packageInfo.versionCode.toString()
40
+ }
41
+ val maximumEntries = logs?.optionalInt("maximumEntries") ?: 200
42
+ val maximumBytes = logs?.optionalInt("maximumBytes") ?: 65_536
43
+ val lookbackMillis = logs?.optionalLong("lookbackMs") ?: 60_000L
44
+
45
+ logBuffer.configure(
46
+ lookbackMillis = lookbackMillis,
47
+ maximumEntries = maximumEntries,
48
+ maximumBytes = maximumBytes,
49
+ )
50
+
51
+ Crumb.start(
52
+ CrumbConfiguration(
53
+ projectKey = payload.getString("projectKey"),
54
+ environment = payload.getString("environment"),
55
+ release = CrumbRelease(
56
+ appVersion = release.optionalString("appVersion")
57
+ ?: packageInfo.versionName
58
+ ?: "0",
59
+ nativeBuild = release.optionalString("nativeBuild") ?: nativeBuild,
60
+ bundleVersion = release.optionalString("bundleVersion"),
61
+ ),
62
+ invocation = payload.invocations(),
63
+ capture = payload.captureOptions(),
64
+ diagnostics = CrumbDiagnosticsOptions(
65
+ healthCheckUrl = diagnostics?.optionalString("healthCheckUrl"),
66
+ timeoutMillis = diagnostics?.optionalLong("timeoutMs") ?: 2_000,
67
+ logs = CrumbLogOptions(
68
+ enabled = logs?.optionalBoolean("enabled") ?: true,
69
+ lookbackMillis = lookbackMillis,
70
+ maximumEntries = maximumEntries,
71
+ maximumBytes = maximumBytes,
72
+ provider = logBuffer,
73
+ ),
74
+ ),
75
+ privacy = payload.privacyOptions(),
76
+ upload = CrumbUploadOptions(
77
+ ingestionUrl = payload.optJSONObject("upload")
78
+ ?.optionalString("ingestionUrl"),
79
+ ),
80
+ ),
81
+ )
82
+ }
83
+
84
+ override fun installReporter(): Promise<Boolean> {
85
+ val promise = Promise<Boolean>()
86
+ UiThreadUtil.runOnUiThread {
87
+ runCatching {
88
+ val context = requireNotNull(NitroModules.applicationContext)
89
+ val application = context.applicationContext as Application
90
+ CrumbReporter.install(application)
91
+ }.onSuccess(promise::resolve)
92
+ .onFailure(promise::reject)
93
+ }
94
+ return promise
95
+ }
96
+
97
+ override fun show(): Promise<Boolean> {
98
+ val promise = Promise<Boolean>()
99
+ UiThreadUtil.runOnUiThread {
100
+ runCatching {
101
+ val context = requireNotNull(NitroModules.applicationContext)
102
+ val activity = context.getCurrentActivity() ?: return@runCatching false
103
+ CrumbReporter.show(activity)
104
+ }.onSuccess(promise::resolve)
105
+ .onFailure(promise::reject)
106
+ }
107
+ return promise
108
+ }
109
+
110
+ override fun addLog(entryJson: String) {
111
+ val payload = JSONObject(entryJson)
112
+ logBuffer.append(
113
+ CrumbLogEntry(
114
+ timestampMillis = payload.getLong("timestampMs"),
115
+ level = payload.getString("level").toNativeLogLevel(),
116
+ source = payload.optString("source", "react-native"),
117
+ category = payload.optString("category", "javascript"),
118
+ message = payload.getString("message"),
119
+ ),
120
+ )
121
+ }
122
+
123
+ override fun clearLogs() {
124
+ logBuffer.clear()
125
+ }
126
+ }
127
+
128
+ private fun JSONObject.invocations(): Set<CrumbInvocation> {
129
+ val values = optJSONArray("invocation")
130
+ ?: return setOf(CrumbInvocation.SHAKE, CrumbInvocation.PROGRAMMATIC)
131
+ return buildSet {
132
+ for (index in 0 until values.length()) {
133
+ when (values.getString(index)) {
134
+ "shake" -> add(CrumbInvocation.SHAKE)
135
+ "programmatic" -> add(CrumbInvocation.PROGRAMMATIC)
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ private fun JSONObject.captureOptions(): CrumbCaptureOptions {
142
+ val capture = optJSONObject("capture") ?: return CrumbCaptureOptions()
143
+ return CrumbCaptureOptions(
144
+ screenshot = capture.optionalBoolean("screenshot") ?: true,
145
+ maximumScreenshotDimension = capture.optionalInt("maximumScreenshotDimension") ?: 2_048,
146
+ maximumScreenshotBytes = capture.optionalInt("maximumScreenshotBytes") ?: 5_242_880,
147
+ )
148
+ }
149
+
150
+ private fun JSONObject.privacyOptions(): CrumbPrivacyOptions {
151
+ val privacy = optJSONObject("privacy") ?: return CrumbPrivacyOptions()
152
+ return CrumbPrivacyOptions(
153
+ maskAllTextInputs = privacy.optionalBoolean("maskAllTextInputs") ?: true,
154
+ maskScreenshotsBeforeUpload = privacy.optionalBoolean("maskScreenshotsBeforeUpload") ?: true,
155
+ )
156
+ }
157
+
158
+ private fun JSONObject.optionalString(name: String): String? =
159
+ if (has(name) && !isNull(name)) getString(name) else null
160
+
161
+ private fun JSONObject.optionalBoolean(name: String): Boolean? =
162
+ if (has(name) && !isNull(name)) getBoolean(name) else null
163
+
164
+ private fun JSONObject.optionalInt(name: String): Int? =
165
+ if (has(name) && !isNull(name)) getInt(name) else null
166
+
167
+ private fun JSONObject.optionalLong(name: String): Long? =
168
+ if (has(name) && !isNull(name)) getLong(name) else null
169
+
170
+ private fun String.toNativeLogLevel(): CrumbLogLevel = when (this) {
171
+ "debug" -> CrumbLogLevel.DEBUG
172
+ "info" -> CrumbLogLevel.INFO
173
+ "notice" -> CrumbLogLevel.NOTICE
174
+ "warning" -> CrumbLogLevel.WARNING
175
+ "error" -> CrumbLogLevel.ERROR
176
+ "fault" -> CrumbLogLevel.FAULT
177
+ else -> CrumbLogLevel.INFO
178
+ }
179
+
180
+ private class ReactNativeLogBuffer : CrumbLogProvider {
181
+ private val lock = Any()
182
+ private val entries = ArrayDeque<CrumbLogEntry>()
183
+ private var lookbackMillis = 60_000L
184
+ private var maximumEntries = 200
185
+ private var maximumBytes = 65_536
186
+
187
+ fun configure(
188
+ lookbackMillis: Long,
189
+ maximumEntries: Int,
190
+ maximumBytes: Int,
191
+ ) = synchronized(lock) {
192
+ this.lookbackMillis = lookbackMillis
193
+ this.maximumEntries = maximumEntries
194
+ this.maximumBytes = maximumBytes
195
+ prune(System.currentTimeMillis())
196
+ }
197
+
198
+ fun append(entry: CrumbLogEntry) = synchronized(lock) {
199
+ entries.addLast(entry)
200
+ prune(System.currentTimeMillis())
201
+ }
202
+
203
+ fun clear() = synchronized(lock) {
204
+ entries.clear()
205
+ }
206
+
207
+ override fun recentLogs(): List<CrumbLogEntry> = synchronized(lock) {
208
+ prune(System.currentTimeMillis())
209
+ entries.toList()
210
+ }
211
+
212
+ private fun prune(now: Long) {
213
+ val earliest = now - lookbackMillis
214
+ while (entries.firstOrNull()?.timestampMillis?.let { it < earliest } == true) {
215
+ entries.removeFirst()
216
+ }
217
+ while (entries.size > maximumEntries || byteCount() > maximumBytes) {
218
+ entries.removeFirstOrNull()
219
+ }
220
+ }
221
+
222
+ private fun byteCount(): Int = entries.sumOf { entry ->
223
+ entry.source.toByteArray().size +
224
+ entry.category.toByteArray().size +
225
+ entry.message.toByteArray().size +
226
+ 32
227
+ }
228
+ }
@@ -0,0 +1,22 @@
1
+ package com.margelo.nitro.crumbsdk.reactnative
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+
8
+ class ReactNativePackage : BaseReactPackage() {
9
+ override fun getModule(
10
+ name: String,
11
+ reactContext: ReactApplicationContext,
12
+ ): NativeModule? = null
13
+
14
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
15
+ ReactModuleInfoProvider { HashMap() }
16
+
17
+ companion object {
18
+ init {
19
+ System.loadLibrary("crumbsdk_reactnative")
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,283 @@
1
+ import CrumbCore
2
+ import CrumbUI
3
+ import Foundation
4
+ import NitroModules
5
+
6
+ final class CrumbReactNative: HybridCrumbReactNativeSpec {
7
+ private let decoder = JSONDecoder()
8
+ private let logBuffer = ReactNativeLogBuffer()
9
+
10
+ func start(configurationJson: String) throws {
11
+ let data = Data(configurationJson.utf8)
12
+ let payload = try decoder.decode(ConfigurationPayload.self, from: data)
13
+ let bundle = Bundle.main
14
+ let appVersion = payload.release?.appVersion
15
+ ?? bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
16
+ ?? "0"
17
+ let nativeBuild = payload.release?.nativeBuild
18
+ ?? bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String
19
+ ?? "0"
20
+ let logOptions = payload.diagnostics?.logs ?? .init()
21
+
22
+ logBuffer.configure(
23
+ lookback: logOptions.lookbackMs.map { TimeInterval(milliseconds: $0) } ?? 60,
24
+ maximumEntries: logOptions.maximumEntries ?? 200,
25
+ maximumBytes: logOptions.maximumBytes ?? 65_536
26
+ )
27
+
28
+ try Crumb.start(
29
+ CrumbConfiguration(
30
+ projectKey: payload.projectKey,
31
+ environment: payload.environment,
32
+ release: CrumbRelease(
33
+ appVersion: appVersion,
34
+ nativeBuild: nativeBuild,
35
+ bundleVersion: payload.release?.bundleVersion
36
+ ),
37
+ invocation: Set(
38
+ (payload.invocation ?? [.shake, .programmatic]).map(\.native)
39
+ ),
40
+ capture: CrumbCaptureOptions(
41
+ screenshot: payload.capture?.screenshot ?? true,
42
+ maximumScreenshotDimension:
43
+ payload.capture?.maximumScreenshotDimension ?? 2_048,
44
+ maximumScreenshotBytes:
45
+ payload.capture?.maximumScreenshotBytes ?? 5_242_880
46
+ ),
47
+ diagnostics: CrumbDiagnosticsOptions(
48
+ healthCheckURL: try payload.diagnostics?.healthCheckURL(),
49
+ timeout: payload.diagnostics?.timeoutMs
50
+ .map { TimeInterval(milliseconds: $0) } ?? 2,
51
+ logs: CrumbLogOptions(
52
+ enabled: logOptions.enabled ?? true,
53
+ lookback: logOptions.lookbackMs
54
+ .map { TimeInterval(milliseconds: $0) } ?? 60,
55
+ maximumEntries: logOptions.maximumEntries ?? 200,
56
+ maximumBytes: logOptions.maximumBytes ?? 65_536,
57
+ provider: logBuffer
58
+ )
59
+ ),
60
+ privacy: CrumbPrivacyOptions(
61
+ maskAllTextInputs: payload.privacy?.maskAllTextInputs ?? true,
62
+ maskScreenshotsBeforeUpload:
63
+ payload.privacy?.maskScreenshotsBeforeUpload ?? true
64
+ ),
65
+ upload: CrumbUploadOptions(
66
+ ingestionURL: try payload.upload?.ingestionURL()
67
+ )
68
+ )
69
+ )
70
+ }
71
+
72
+ func installReporter() throws -> Promise<Bool> {
73
+ Promise.async { @MainActor in
74
+ Crumb.installReporter()
75
+ }
76
+ }
77
+
78
+ func show() throws -> Promise<Bool> {
79
+ Promise.async { @MainActor in
80
+ Crumb.show()
81
+ }
82
+ }
83
+
84
+ func addLog(entryJson: String) throws {
85
+ let data = Data(entryJson.utf8)
86
+ let entry = try decoder.decode(LogEntryPayload.self, from: data)
87
+ logBuffer.append(entry.native)
88
+ }
89
+
90
+ func clearLogs() throws {
91
+ logBuffer.clear()
92
+ }
93
+ }
94
+
95
+ private struct ConfigurationPayload: Decodable {
96
+ let projectKey: String
97
+ let environment: String
98
+ let release: ReleasePayload?
99
+ let invocation: [InvocationPayload]?
100
+ let capture: CapturePayload?
101
+ let diagnostics: DiagnosticsPayload?
102
+ let privacy: PrivacyPayload?
103
+ let upload: UploadPayload?
104
+ }
105
+
106
+ private struct ReleasePayload: Decodable {
107
+ let appVersion: String?
108
+ let nativeBuild: String?
109
+ let bundleVersion: String?
110
+ }
111
+
112
+ private enum InvocationPayload: String, Decodable {
113
+ case shake
114
+ case programmatic
115
+
116
+ var native: CrumbInvocation {
117
+ switch self {
118
+ case .shake: .shake
119
+ case .programmatic: .programmatic
120
+ }
121
+ }
122
+ }
123
+
124
+ private struct CapturePayload: Decodable {
125
+ let screenshot: Bool?
126
+ let maximumScreenshotDimension: Int?
127
+ let maximumScreenshotBytes: Int?
128
+ }
129
+
130
+ private struct DiagnosticsPayload: Decodable {
131
+ let healthCheckUrl: String?
132
+ let timeoutMs: Int?
133
+ let logs: LogOptionsPayload?
134
+
135
+ func healthCheckURL() throws -> URL? {
136
+ guard let healthCheckUrl else { return nil }
137
+ guard let url = URL(string: healthCheckUrl) else {
138
+ throw BridgeConfigurationError.invalidURL("healthCheckUrl")
139
+ }
140
+ return url
141
+ }
142
+ }
143
+
144
+ private struct LogOptionsPayload: Decodable {
145
+ let enabled: Bool?
146
+ let lookbackMs: Int?
147
+ let maximumEntries: Int?
148
+ let maximumBytes: Int?
149
+
150
+ init(
151
+ enabled: Bool? = nil,
152
+ lookbackMs: Int? = nil,
153
+ maximumEntries: Int? = nil,
154
+ maximumBytes: Int? = nil
155
+ ) {
156
+ self.enabled = enabled
157
+ self.lookbackMs = lookbackMs
158
+ self.maximumEntries = maximumEntries
159
+ self.maximumBytes = maximumBytes
160
+ }
161
+ }
162
+
163
+ private struct PrivacyPayload: Decodable {
164
+ let maskAllTextInputs: Bool?
165
+ let maskScreenshotsBeforeUpload: Bool?
166
+ }
167
+
168
+ private struct UploadPayload: Decodable {
169
+ let ingestionUrl: String?
170
+
171
+ func ingestionURL() throws -> URL? {
172
+ guard let ingestionUrl else { return nil }
173
+ guard let url = URL(string: ingestionUrl) else {
174
+ throw BridgeConfigurationError.invalidURL("ingestionUrl")
175
+ }
176
+ return url
177
+ }
178
+ }
179
+
180
+ private enum BridgeConfigurationError: Error {
181
+ case invalidURL(String)
182
+ }
183
+
184
+ private struct LogEntryPayload: Decodable {
185
+ let timestampMs: Int64
186
+ let level: String
187
+ let source: String
188
+ let category: String
189
+ let message: String
190
+
191
+ var native: CrumbLogEntry {
192
+ CrumbLogEntry(
193
+ timestamp: Date(timeIntervalSince1970: TimeInterval(timestampMs) / 1_000),
194
+ level: level.nativeLogLevel,
195
+ source: source,
196
+ category: category,
197
+ message: message
198
+ )
199
+ }
200
+ }
201
+
202
+ private extension String {
203
+ var nativeLogLevel: CrumbLogLevel {
204
+ switch self {
205
+ case "debug": .debug
206
+ case "notice": .notice
207
+ case "warning": .warning
208
+ case "error": .error
209
+ case "fault": .fault
210
+ default: .info
211
+ }
212
+ }
213
+ }
214
+
215
+ private final class ReactNativeLogBuffer: CrumbLogProvider, @unchecked Sendable {
216
+ private let lock = NSLock()
217
+ private var entries: [CrumbLogEntry] = []
218
+ private var lookback: TimeInterval = 60
219
+ private var maximumEntries = 200
220
+ private var maximumBytes = 65_536
221
+
222
+ func configure(
223
+ lookback: TimeInterval,
224
+ maximumEntries: Int,
225
+ maximumBytes: Int
226
+ ) {
227
+ withLock {
228
+ self.lookback = lookback
229
+ self.maximumEntries = maximumEntries
230
+ self.maximumBytes = maximumBytes
231
+ prune(now: Date())
232
+ }
233
+ }
234
+
235
+ func append(_ entry: CrumbLogEntry) {
236
+ withLock {
237
+ entries.append(entry)
238
+ prune(now: Date())
239
+ }
240
+ }
241
+
242
+ func clear() {
243
+ withLock {
244
+ entries.removeAll(keepingCapacity: false)
245
+ }
246
+ }
247
+
248
+ func recentLogs() throws -> [CrumbLogEntry] {
249
+ withLock {
250
+ prune(now: Date())
251
+ return entries
252
+ }
253
+ }
254
+
255
+ private func prune(now: Date) {
256
+ let earliest = now.addingTimeInterval(-lookback)
257
+ entries.removeAll { $0.timestamp < earliest }
258
+ while entries.count > maximumEntries || byteCount(entries) > maximumBytes {
259
+ entries.removeFirst()
260
+ }
261
+ }
262
+
263
+ private func byteCount(_ entries: [CrumbLogEntry]) -> Int {
264
+ entries.reduce(into: 0) { total, entry in
265
+ total += entry.source.utf8.count
266
+ + entry.category.utf8.count
267
+ + entry.message.utf8.count
268
+ + 32
269
+ }
270
+ }
271
+
272
+ private func withLock<Result>(_ action: () throws -> Result) rethrows -> Result {
273
+ lock.lock()
274
+ defer { lock.unlock() }
275
+ return try action()
276
+ }
277
+ }
278
+
279
+ private extension TimeInterval {
280
+ init(milliseconds: Int) {
281
+ self = TimeInterval(milliseconds) / 1_000
282
+ }
283
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=CrumbReactNative.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["CrumbReactNative.nitro.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+ export default NitroModules.createHybridObject('CrumbReactNative');
5
+ //# sourceMappingURL=NativeCrumbReactNative.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["NitroModules","createHybridObject"],"sourceRoot":"../../src","sources":["NativeCrumbReactNative.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAGzD,eAAeA,YAAY,CAACC,kBAAkB,CAC5C,kBACF,CAAC","ignoreList":[]}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ const unsupportedMessage = 'Crumb requires an iOS or Android native build and is unavailable on web or in Expo Go.';
4
+ const unsupportedModule = {
5
+ name: 'CrumbReactNative',
6
+ dispose: () => undefined,
7
+ equals: () => false,
8
+ toString: () => '[HybridObject CrumbReactNative unavailable on web]',
9
+ start: () => {
10
+ throw new Error(unsupportedMessage);
11
+ },
12
+ installReporter: () => Promise.resolve(false),
13
+ show: () => Promise.resolve(false),
14
+ addLog: () => undefined,
15
+ clearLogs: () => undefined
16
+ };
17
+ export default unsupportedModule;
18
+ //# sourceMappingURL=NativeCrumbReactNative.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["unsupportedMessage","unsupportedModule","name","dispose","undefined","equals","toString","start","Error","installReporter","Promise","resolve","show","addLog","clearLogs"],"sourceRoot":"../../src","sources":["NativeCrumbReactNative.web.ts"],"mappings":";;AAEA,MAAMA,kBAAkB,GACtB,wFAAwF;AAE1F,MAAMC,iBAAmC,GAAG;EAC1CC,IAAI,EAAE,kBAAkB;EACxBC,OAAO,EAAEA,CAAA,KAAMC,SAAS;EACxBC,MAAM,EAAEA,CAAA,KAAM,KAAK;EACnBC,QAAQ,EAAEA,CAAA,KAAM,oDAAoD;EACpEC,KAAK,EAAEA,CAAA,KAAM;IACX,MAAM,IAAIC,KAAK,CAACR,kBAAkB,CAAC;EACrC,CAAC;EACDS,eAAe,EAAEA,CAAA,KAAMC,OAAO,CAACC,OAAO,CAAC,KAAK,CAAC;EAC7CC,IAAI,EAAEA,CAAA,KAAMF,OAAO,CAACC,OAAO,CAAC,KAAK,CAAC;EAClCE,MAAM,EAAEA,CAAA,KAAMT,SAAS;EACvBU,SAAS,EAAEA,CAAA,KAAMV;AACnB,CAAC;AAED,eAAeH,iBAAiB","ignoreList":[]}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ import NativeCrumbReactNative from './NativeCrumbReactNative';
4
+ import { clearLogs, configureLogBuffer, disableConsoleCapture, enableConsoleCapture, markNativeStarted, writeLog } from "./log-buffer.js";
5
+ export async function start(configuration) {
6
+ validateConfiguration(configuration);
7
+ const logOptions = configuration.diagnostics?.logs;
8
+ configureLogBuffer(logOptions);
9
+ NativeCrumbReactNative.start(JSON.stringify(configuration));
10
+ markNativeStarted();
11
+ if (logOptions?.captureConsole) {
12
+ enableConsoleCapture();
13
+ } else {
14
+ disableConsoleCapture();
15
+ }
16
+ }
17
+ export function installReporter() {
18
+ return NativeCrumbReactNative.installReporter();
19
+ }
20
+ export function show() {
21
+ return NativeCrumbReactNative.show();
22
+ }
23
+ export function log(level, message, metadata) {
24
+ if (!message.trim()) {
25
+ throw new TypeError('Crumb.log message must not be empty.');
26
+ }
27
+ writeLog(level, message, metadata);
28
+ }
29
+ export { clearLogs, disableConsoleCapture, enableConsoleCapture };
30
+ function validateConfiguration(configuration) {
31
+ if (!configuration.projectKey?.trim()) {
32
+ throw new TypeError('Crumb projectKey must not be empty.');
33
+ }
34
+ if (!configuration.environment?.trim()) {
35
+ throw new TypeError('Crumb environment must not be empty.');
36
+ }
37
+ assertPositiveInteger(configuration.capture?.maximumScreenshotDimension, 'capture.maximumScreenshotDimension');
38
+ assertPositiveInteger(configuration.capture?.maximumScreenshotBytes, 'capture.maximumScreenshotBytes');
39
+ assertPositiveInteger(configuration.diagnostics?.timeoutMs, 'diagnostics.timeoutMs');
40
+ assertPositiveInteger(configuration.diagnostics?.logs?.lookbackMs, 'diagnostics.logs.lookbackMs');
41
+ assertPositiveInteger(configuration.diagnostics?.logs?.maximumEntries, 'diagnostics.logs.maximumEntries');
42
+ assertPositiveInteger(configuration.diagnostics?.logs?.maximumBytes, 'diagnostics.logs.maximumBytes');
43
+ }
44
+ function assertPositiveInteger(value, field) {
45
+ if (value === undefined) return;
46
+ if (!Number.isSafeInteger(value) || value <= 0) {
47
+ throw new TypeError(`Crumb ${field} must be a positive integer.`);
48
+ }
49
+ }
50
+ const Crumb = Object.freeze({
51
+ start,
52
+ installReporter,
53
+ show,
54
+ log,
55
+ clearLogs,
56
+ enableConsoleCapture,
57
+ disableConsoleCapture
58
+ });
59
+ export default Crumb;
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["NativeCrumbReactNative","clearLogs","configureLogBuffer","disableConsoleCapture","enableConsoleCapture","markNativeStarted","writeLog","start","configuration","validateConfiguration","logOptions","diagnostics","logs","JSON","stringify","captureConsole","installReporter","show","log","level","message","metadata","trim","TypeError","projectKey","environment","assertPositiveInteger","capture","maximumScreenshotDimension","maximumScreenshotBytes","timeoutMs","lookbackMs","maximumEntries","maximumBytes","value","field","undefined","Number","isSafeInteger","Crumb","Object","freeze"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,OAAOA,sBAAsB,MAAM,0BAA0B;AAC7D,SACEC,SAAS,EACTC,kBAAkB,EAClBC,qBAAqB,EACrBC,oBAAoB,EACpBC,iBAAiB,EACjBC,QAAQ,QACH,iBAAc;AAoBrB,OAAO,eAAeC,KAAKA,CAACC,aAAiC,EAAiB;EAC5EC,qBAAqB,CAACD,aAAa,CAAC;EACpC,MAAME,UAAU,GAAGF,aAAa,CAACG,WAAW,EAAEC,IAAI;EAClDV,kBAAkB,CAACQ,UAAU,CAAC;EAC9BV,sBAAsB,CAACO,KAAK,CAACM,IAAI,CAACC,SAAS,CAACN,aAAa,CAAC,CAAC;EAC3DH,iBAAiB,CAAC,CAAC;EAEnB,IAAIK,UAAU,EAAEK,cAAc,EAAE;IAC9BX,oBAAoB,CAAC,CAAC;EACxB,CAAC,MAAM;IACLD,qBAAqB,CAAC,CAAC;EACzB;AACF;AAEA,OAAO,SAASa,eAAeA,CAAA,EAAqB;EAClD,OAAOhB,sBAAsB,CAACgB,eAAe,CAAC,CAAC;AACjD;AAEA,OAAO,SAASC,IAAIA,CAAA,EAAqB;EACvC,OAAOjB,sBAAsB,CAACiB,IAAI,CAAC,CAAC;AACtC;AAEA,OAAO,SAASC,GAAGA,CACjBC,KAAoB,EACpBC,OAAe,EACfC,QAA2B,EACrB;EACN,IAAI,CAACD,OAAO,CAACE,IAAI,CAAC,CAAC,EAAE;IACnB,MAAM,IAAIC,SAAS,CAAC,sCAAsC,CAAC;EAC7D;EACAjB,QAAQ,CAACa,KAAK,EAAEC,OAAO,EAAEC,QAAQ,CAAC;AACpC;AAEA,SAASpB,SAAS,EAAEE,qBAAqB,EAAEC,oBAAoB;AAE/D,SAASK,qBAAqBA,CAACD,aAAiC,EAAQ;EACtE,IAAI,CAACA,aAAa,CAACgB,UAAU,EAAEF,IAAI,CAAC,CAAC,EAAE;IACrC,MAAM,IAAIC,SAAS,CAAC,qCAAqC,CAAC;EAC5D;EACA,IAAI,CAACf,aAAa,CAACiB,WAAW,EAAEH,IAAI,CAAC,CAAC,EAAE;IACtC,MAAM,IAAIC,SAAS,CAAC,sCAAsC,CAAC;EAC7D;EAEAG,qBAAqB,CACnBlB,aAAa,CAACmB,OAAO,EAAEC,0BAA0B,EACjD,oCACF,CAAC;EACDF,qBAAqB,CACnBlB,aAAa,CAACmB,OAAO,EAAEE,sBAAsB,EAC7C,gCACF,CAAC;EACDH,qBAAqB,CACnBlB,aAAa,CAACG,WAAW,EAAEmB,SAAS,EACpC,uBACF,CAAC;EACDJ,qBAAqB,CACnBlB,aAAa,CAACG,WAAW,EAAEC,IAAI,EAAEmB,UAAU,EAC3C,6BACF,CAAC;EACDL,qBAAqB,CACnBlB,aAAa,CAACG,WAAW,EAAEC,IAAI,EAAEoB,cAAc,EAC/C,iCACF,CAAC;EACDN,qBAAqB,CACnBlB,aAAa,CAACG,WAAW,EAAEC,IAAI,EAAEqB,YAAY,EAC7C,+BACF,CAAC;AACH;AAEA,SAASP,qBAAqBA,CAACQ,KAAyB,EAAEC,KAAa,EAAQ;EAC7E,IAAID,KAAK,KAAKE,SAAS,EAAE;EACzB,IAAI,CAACC,MAAM,CAACC,aAAa,CAACJ,KAAK,CAAC,IAAIA,KAAK,IAAI,CAAC,EAAE;IAC9C,MAAM,IAAIX,SAAS,CAAC,SAASY,KAAK,8BAA8B,CAAC;EACnE;AACF;AAEA,MAAMI,KAAK,GAAGC,MAAM,CAACC,MAAM,CAAC;EAC1BlC,KAAK;EACLS,eAAe;EACfC,IAAI;EACJC,GAAG;EACHjB,SAAS;EACTG,oBAAoB;EACpBD;AACF,CAAC,CAAC;AAEF,eAAeoC,KAAK","ignoreList":[]}