@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.
- package/CrumbReactNative.podspec +38 -0
- package/LICENSE +201 -0
- package/README.md +109 -0
- package/android/CMakeLists.txt +20 -0
- package/android/build.gradle +118 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +10 -0
- package/android/src/main/java/com/margelo/nitro/crumbsdk/reactnative/CrumbReactNative.kt +228 -0
- package/android/src/main/java/com/margelo/nitro/crumbsdk/reactnative/ReactNativePackage.kt +22 -0
- package/ios/CrumbReactNative.swift +283 -0
- package/lib/module/CrumbReactNative.nitro.js +4 -0
- package/lib/module/CrumbReactNative.nitro.js.map +1 -0
- package/lib/module/NativeCrumbReactNative.js +5 -0
- package/lib/module/NativeCrumbReactNative.js.map +1 -0
- package/lib/module/NativeCrumbReactNative.web.js +18 -0
- package/lib/module/NativeCrumbReactNative.web.js.map +1 -0
- package/lib/module/index.js +60 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/log-buffer.js +200 -0
- package/lib/module/log-buffer.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/CrumbReactNative.nitro.d.ts +12 -0
- package/lib/typescript/src/CrumbReactNative.nitro.d.ts.map +1 -0
- package/lib/typescript/src/NativeCrumbReactNative.d.ts +4 -0
- package/lib/typescript/src/NativeCrumbReactNative.d.ts.map +1 -0
- package/lib/typescript/src/NativeCrumbReactNative.web.d.ts +4 -0
- package/lib/typescript/src/NativeCrumbReactNative.web.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +19 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/log-buffer.d.ts +8 -0
- package/lib/typescript/src/log-buffer.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +55 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/nitro.json +24 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/c++/JHybridCrumbReactNativeSpec.cpp +94 -0
- package/nitrogen/generated/android/c++/JHybridCrumbReactNativeSpec.hpp +67 -0
- package/nitrogen/generated/android/crumbsdk_reactnative+autolinking.cmake +81 -0
- package/nitrogen/generated/android/crumbsdk_reactnative+autolinking.gradle +27 -0
- package/nitrogen/generated/android/crumbsdk_reactnativeOnLoad.cpp +54 -0
- package/nitrogen/generated/android/crumbsdk_reactnativeOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/crumbsdk/reactnative/HybridCrumbReactNativeSpec.kt +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/crumbsdk/reactnative/crumbsdk_reactnativeOnLoad.kt +35 -0
- package/nitrogen/generated/ios/CrumbReactNative+autolinking.rb +62 -0
- package/nitrogen/generated/ios/CrumbReactNative-Swift-Cxx-Bridge.cpp +49 -0
- package/nitrogen/generated/ios/CrumbReactNative-Swift-Cxx-Bridge.hpp +119 -0
- package/nitrogen/generated/ios/CrumbReactNative-Swift-Cxx-Umbrella.hpp +45 -0
- package/nitrogen/generated/ios/CrumbReactNativeAutolinking.mm +33 -0
- package/nitrogen/generated/ios/CrumbReactNativeAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridCrumbReactNativeSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridCrumbReactNativeSpecSwift.hpp +109 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridCrumbReactNativeSpec.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridCrumbReactNativeSpec_cxx.swift +197 -0
- package/nitrogen/generated/shared/c++/HybridCrumbReactNativeSpec.cpp +25 -0
- package/nitrogen/generated/shared/c++/HybridCrumbReactNativeSpec.hpp +67 -0
- package/package.json +138 -0
- package/src/CrumbReactNative.nitro.ts +12 -0
- package/src/NativeCrumbReactNative.ts +6 -0
- package/src/NativeCrumbReactNative.web.ts +20 -0
- package/src/index.ts +115 -0
- package/src/log-buffer.ts +265 -0
- 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 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["CrumbReactNative.nitro.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -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":[]}
|