react-native-notify-kit 10.4.7 → 10.4.9
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/README.md +4 -0
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeReactUtils.kt +679 -7
- package/android/src/test/java/io/invertase/notifee/NotifeeReactUtilsPendingEventsTest.java +1025 -0
- package/dist/NotifeeApiModule.d.ts +7 -2
- package/dist/NotifeeApiModule.js +34 -15
- package/dist/NotifeeApiModule.js.map +1 -1
- package/dist/fcm/reconstructNotification.js +3 -2
- package/dist/fcm/reconstructNotification.js.map +1 -1
- package/dist/fcm/types.d.ts +5 -4
- package/dist/types/Module.d.ts +11 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/NotifeeApiModule.ts +44 -17
- package/src/fcm/reconstructNotification.ts +5 -3
- package/src/fcm/types.ts +5 -4
- package/src/types/Module.ts +12 -2
- package/src/version.ts +1 -1
- package/server/dist/android.d.ts +0 -8
- package/server/dist/android.d.ts.map +0 -1
- package/server/dist/android.js +0 -20
- package/server/dist/android.js.map +0 -1
- package/server/dist/buildPayload.d.ts +0 -3
- package/server/dist/buildPayload.d.ts.map +0 -1
- package/server/dist/buildPayload.js +0 -82
- package/server/dist/buildPayload.js.map +0 -1
- package/server/dist/index.d.ts +0 -6
- package/server/dist/index.d.ts.map +0 -1
- package/server/dist/index.js +0 -12
- package/server/dist/index.js.map +0 -1
- package/server/dist/ios.d.ts +0 -11
- package/server/dist/ios.d.ts.map +0 -1
- package/server/dist/ios.js +0 -61
- package/server/dist/ios.js.map +0 -1
- package/server/dist/serialize.d.ts +0 -11
- package/server/dist/serialize.d.ts.map +0 -1
- package/server/dist/serialize.js +0 -32
- package/server/dist/serialize.js.map +0 -1
- package/server/dist/types.d.ts +0 -2
- package/server/dist/types.d.ts.map +0 -1
- package/server/dist/types.js +0 -3
- package/server/dist/types.js.map +0 -1
- package/server/dist/validation.d.ts +0 -3
- package/server/dist/validation.d.ts.map +0 -1
- package/server/dist/validation.js +0 -101
- package/server/dist/validation.js.map +0 -1
|
@@ -5,42 +5,350 @@
|
|
|
5
5
|
package io.invertase.notifee
|
|
6
6
|
|
|
7
7
|
import android.annotation.SuppressLint
|
|
8
|
+
import android.content.Context
|
|
9
|
+
import android.content.pm.ApplicationInfo
|
|
10
|
+
import android.content.pm.PackageManager
|
|
8
11
|
import android.os.Bundle
|
|
12
|
+
import android.os.SystemClock
|
|
9
13
|
import android.util.Log
|
|
10
14
|
import androidx.lifecycle.Lifecycle
|
|
11
15
|
import androidx.lifecycle.ProcessLifecycleOwner
|
|
12
16
|
import app.notifee.core.EventSubscriber
|
|
13
17
|
import com.facebook.react.bridge.Arguments
|
|
14
18
|
import com.facebook.react.bridge.Promise
|
|
19
|
+
import com.facebook.react.bridge.ReadableMap
|
|
20
|
+
import com.facebook.react.bridge.ReadableType
|
|
21
|
+
import com.facebook.react.bridge.ReactContext
|
|
15
22
|
import com.facebook.react.bridge.WritableArray
|
|
23
|
+
import com.facebook.react.bridge.WritableMap
|
|
16
24
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
17
25
|
import java.lang.reflect.Method
|
|
26
|
+
import java.util.IdentityHashMap
|
|
27
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
18
28
|
|
|
19
29
|
object NotifeeReactUtils {
|
|
20
30
|
|
|
21
31
|
val headlessTaskManager = HeadlessTask()
|
|
22
32
|
|
|
23
33
|
private const val MAX_PENDING_EVENTS = 10
|
|
24
|
-
private val
|
|
34
|
+
private const val PENDING_DIAGNOSTICS_TAG = "NOTIFEE_PENDING_DIAG"
|
|
35
|
+
private const val PENDING_DIAGNOSTICS_ENABLED_METADATA =
|
|
36
|
+
"notifee_pending_events_diagnostics_enabled"
|
|
37
|
+
private const val PENDING_DIAGNOSTICS_DELAY_METADATA =
|
|
38
|
+
"notifee_pending_events_delay_after_snapshot_ms"
|
|
39
|
+
private const val MAX_PENDING_DIAGNOSTIC_DELAY_MS = 10_000
|
|
40
|
+
private const val PENDING_DIAGNOSTIC_SCENARIO_ID_KEY = "__notifeePendingDiagScenarioId"
|
|
41
|
+
private const val PENDING_DIAGNOSTIC_EVENT_ID_KEY = "__notifeePendingDiagEventId"
|
|
42
|
+
private const val MAX_PENDING_DIAGNOSTIC_SCENARIO_ID_LENGTH = 48
|
|
43
|
+
private const val MAX_PENDING_DIAGNOSTIC_EVENT_ID_LENGTH = 80
|
|
44
|
+
|
|
45
|
+
@Volatile
|
|
46
|
+
private var pendingDiagnosticsState: PendingDiagnosticsState? = null
|
|
47
|
+
|
|
48
|
+
private val pendingEvents = mutableListOf<Pair<String, WritableMap>>()
|
|
49
|
+
|
|
50
|
+
private data class PendingDiagnosticIdentity(
|
|
51
|
+
val scenarioId: String,
|
|
52
|
+
val eventId: String,
|
|
53
|
+
val eventType: Int?,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
private data class QueuedPendingEventDiagnostic(
|
|
57
|
+
val eventSeq: Long,
|
|
58
|
+
val identity: PendingDiagnosticIdentity?,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
private data class PendingDiagnosticEvent(
|
|
62
|
+
val eventSeq: Long,
|
|
63
|
+
val pendingEvent: Pair<String, WritableMap>,
|
|
64
|
+
val identity: PendingDiagnosticIdentity?,
|
|
65
|
+
) {
|
|
66
|
+
val eventName: String
|
|
67
|
+
get() = pendingEvent.first
|
|
68
|
+
|
|
69
|
+
val eventBody: WritableMap
|
|
70
|
+
get() = pendingEvent.second
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private data class PendingEventToRequeue(
|
|
74
|
+
val pendingEvent: Pair<String, WritableMap>,
|
|
75
|
+
val diagnostic: QueuedPendingEventDiagnostic? = null,
|
|
76
|
+
val reason: String? = null,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
private data class PendingEventMergeResult(
|
|
80
|
+
val queueSizeBefore: Int,
|
|
81
|
+
val requeuedCount: Int,
|
|
82
|
+
val newArrivalsCount: Int,
|
|
83
|
+
val queueSizeAfter: Int,
|
|
84
|
+
val droppedEvents: List<PendingEventToRequeue>,
|
|
85
|
+
val survivingRequeuedEvents: List<PendingEventToRequeue>,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
private data class PendingDiagnosticsConfig(
|
|
89
|
+
val enabled: Boolean,
|
|
90
|
+
val delayAfterSnapshotMs: Long,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
private class PendingDiagnosticsState(val delayAfterSnapshotMs: Long) {
|
|
94
|
+
val eventSequence = AtomicLong(0)
|
|
95
|
+
val flushSequence = AtomicLong(0)
|
|
96
|
+
val queuedEventDiagnostics =
|
|
97
|
+
IdentityHashMap<Pair<String, WritableMap>, QueuedPendingEventDiagnostic>()
|
|
98
|
+
}
|
|
25
99
|
|
|
26
100
|
fun flushPendingEvents() {
|
|
27
|
-
val
|
|
101
|
+
val diagnosticsState = pendingDiagnosticsState
|
|
102
|
+
if (diagnosticsState != null) {
|
|
103
|
+
flushPendingEventsDiagnostics(diagnosticsState)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
val eventsToSend: List<Pair<String, WritableMap>>
|
|
28
108
|
synchronized(pendingEvents) {
|
|
29
109
|
eventsToSend = pendingEvents.toList()
|
|
30
110
|
pendingEvents.clear()
|
|
31
111
|
}
|
|
32
|
-
|
|
112
|
+
val eventsToRequeue = mutableListOf<PendingEventToRequeue>()
|
|
113
|
+
for (pendingEvent in eventsToSend) {
|
|
114
|
+
val (name, map) = pendingEvent
|
|
33
115
|
try {
|
|
34
116
|
val reactContext = HeadlessTask.getReactContext(EventSubscriber.getContext())
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
117
|
+
val hasActiveReactInstance = reactContext?.hasActiveReactInstance()
|
|
118
|
+
if (reactContext == null || hasActiveReactInstance != true) {
|
|
119
|
+
eventsToRequeue.add(
|
|
120
|
+
PendingEventToRequeue(
|
|
121
|
+
pendingEvent = pendingEvent,
|
|
122
|
+
reason = if (reactContext == null) "context_null" else "context_inactive",
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
reactContext
|
|
128
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
129
|
+
.emit(name, map)
|
|
130
|
+
} catch (e: Exception) {
|
|
131
|
+
Log.e("SEND_EVENT", "flush failed", e)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
mergeUndeliveredPendingEvents(eventsToRequeue, null)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun flushPendingEventsDiagnostics(diagnosticsState: PendingDiagnosticsState) {
|
|
138
|
+
val flushId = diagnosticsState.flushSequence.incrementAndGet()
|
|
139
|
+
val queueSizeAtRequest = currentPendingEventCount()
|
|
140
|
+
logPendingDiagnostic(
|
|
141
|
+
marker = "FLUSH_REQUESTED",
|
|
142
|
+
flushId = flushId,
|
|
143
|
+
queueSizeBefore = queueSizeAtRequest,
|
|
144
|
+
queueSizeAfter = queueSizeAtRequest,
|
|
145
|
+
result = "requested",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
val eventsToSend = snapshotAndClearPendingEventsDiagnostics(diagnosticsState)
|
|
149
|
+
logPendingDiagnostic(
|
|
150
|
+
marker = "FLUSH_SNAPSHOT",
|
|
151
|
+
flushId = flushId,
|
|
152
|
+
queueSizeBefore = eventsToSend.size,
|
|
153
|
+
queueSizeAfter = eventsToSend.size,
|
|
154
|
+
result = "snapshot_captured",
|
|
155
|
+
)
|
|
156
|
+
eventsToSend.forEach { event ->
|
|
157
|
+
logPendingDiagnostic(
|
|
158
|
+
marker = "FLUSH_SNAPSHOT_ITEM",
|
|
159
|
+
eventSeq = event.eventSeq,
|
|
160
|
+
flushId = flushId,
|
|
161
|
+
eventName = event.eventName,
|
|
162
|
+
queueSizeBefore = eventsToSend.size,
|
|
163
|
+
queueSizeAfter = 0,
|
|
164
|
+
diagnosticIdentity = event.identity,
|
|
165
|
+
result = "snapshot_item",
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
logPendingDiagnostic(
|
|
169
|
+
marker = "FLUSH_QUEUE_CLEARED",
|
|
170
|
+
flushId = flushId,
|
|
171
|
+
queueSizeBefore = eventsToSend.size,
|
|
172
|
+
queueSizeAfter = 0,
|
|
173
|
+
result = "queue_cleared",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
eventsToSend.isNotEmpty() &&
|
|
178
|
+
diagnosticsState.delayAfterSnapshotMs > 0
|
|
179
|
+
) {
|
|
180
|
+
val queueSizeBeforeDelay = currentPendingEventCount()
|
|
181
|
+
logPendingDiagnostic(
|
|
182
|
+
marker = "FLUSH_DIAGNOSTIC_DELAY_BEGIN",
|
|
183
|
+
flushId = flushId,
|
|
184
|
+
queueSizeBefore = queueSizeBeforeDelay,
|
|
185
|
+
queueSizeAfter = queueSizeBeforeDelay,
|
|
186
|
+
result = "sleep_${diagnosticsState.delayAfterSnapshotMs}_ms",
|
|
187
|
+
)
|
|
188
|
+
SystemClock.sleep(diagnosticsState.delayAfterSnapshotMs)
|
|
189
|
+
val queueSizeAfterDelay = currentPendingEventCount()
|
|
190
|
+
logPendingDiagnostic(
|
|
191
|
+
marker = "FLUSH_DIAGNOSTIC_DELAY_END",
|
|
192
|
+
flushId = flushId,
|
|
193
|
+
queueSizeBefore = queueSizeBeforeDelay,
|
|
194
|
+
queueSizeAfter = queueSizeAfterDelay,
|
|
195
|
+
result = "delay_complete",
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
var nativeReturns = 0
|
|
200
|
+
var skippedNoContext = 0
|
|
201
|
+
var exceptions = 0
|
|
202
|
+
val eventsToRequeue = mutableListOf<PendingEventToRequeue>()
|
|
203
|
+
for (event in eventsToSend) {
|
|
204
|
+
var reactContext: ReactContext? = null
|
|
205
|
+
var hasActiveReactInstance: Boolean? = null
|
|
206
|
+
try {
|
|
207
|
+
reactContext = HeadlessTask.getReactContext(EventSubscriber.getContext())
|
|
208
|
+
hasActiveReactInstance = reactContext?.hasActiveReactInstance()
|
|
209
|
+
logPendingDiagnostic(
|
|
210
|
+
marker = "FLUSH_EVENT_CONTEXT_CHECK",
|
|
211
|
+
eventSeq = event.eventSeq,
|
|
212
|
+
flushId = flushId,
|
|
213
|
+
eventName = event.eventName,
|
|
214
|
+
diagnosticIdentity = event.identity,
|
|
215
|
+
reactContext = reactContext,
|
|
216
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
217
|
+
result = when {
|
|
218
|
+
reactContext == null -> "context_null"
|
|
219
|
+
hasActiveReactInstance != true -> "context_inactive"
|
|
220
|
+
else -> "context_active"
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
val activeReactContext = reactContext
|
|
225
|
+
if (activeReactContext == null || hasActiveReactInstance != true) {
|
|
226
|
+
skippedNoContext += 1
|
|
227
|
+
val requeueReason =
|
|
228
|
+
if (activeReactContext == null) "context_null" else "context_inactive"
|
|
229
|
+
logPendingDiagnostic(
|
|
230
|
+
marker = "FLUSH_EVENT_SKIPPED_NO_CONTEXT",
|
|
231
|
+
eventSeq = event.eventSeq,
|
|
232
|
+
flushId = flushId,
|
|
233
|
+
eventName = event.eventName,
|
|
234
|
+
diagnosticIdentity = event.identity,
|
|
235
|
+
reactContext = reactContext,
|
|
236
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
237
|
+
result = if (activeReactContext == null) {
|
|
238
|
+
"skipped_context_null"
|
|
239
|
+
} else {
|
|
240
|
+
"skipped_context_inactive"
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
eventsToRequeue.add(
|
|
244
|
+
PendingEventToRequeue(
|
|
245
|
+
pendingEvent = event.pendingEvent,
|
|
246
|
+
diagnostic =
|
|
247
|
+
QueuedPendingEventDiagnostic(event.eventSeq, event.identity),
|
|
248
|
+
reason = requeueReason,
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
continue
|
|
39
252
|
}
|
|
253
|
+
|
|
254
|
+
logPendingDiagnostic(
|
|
255
|
+
marker = "FLUSH_EVENT_EMIT_BEGIN",
|
|
256
|
+
eventSeq = event.eventSeq,
|
|
257
|
+
flushId = flushId,
|
|
258
|
+
eventName = event.eventName,
|
|
259
|
+
diagnosticIdentity = event.identity,
|
|
260
|
+
reactContext = activeReactContext,
|
|
261
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
262
|
+
result = "native_call_begin",
|
|
263
|
+
)
|
|
264
|
+
activeReactContext
|
|
265
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
266
|
+
.emit(event.eventName, event.eventBody)
|
|
267
|
+
nativeReturns += 1
|
|
268
|
+
logPendingDiagnostic(
|
|
269
|
+
marker = "FLUSH_EVENT_EMIT_RETURN",
|
|
270
|
+
eventSeq = event.eventSeq,
|
|
271
|
+
flushId = flushId,
|
|
272
|
+
eventName = event.eventName,
|
|
273
|
+
diagnosticIdentity = event.identity,
|
|
274
|
+
reactContext = activeReactContext,
|
|
275
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
276
|
+
result = "native_call_returned_no_js_ack",
|
|
277
|
+
)
|
|
40
278
|
} catch (e: Exception) {
|
|
279
|
+
exceptions += 1
|
|
280
|
+
logPendingDiagnostic(
|
|
281
|
+
marker = "FLUSH_EVENT_EMIT_EXCEPTION",
|
|
282
|
+
eventSeq = event.eventSeq,
|
|
283
|
+
flushId = flushId,
|
|
284
|
+
eventName = event.eventName,
|
|
285
|
+
diagnosticIdentity = event.identity,
|
|
286
|
+
reactContext = reactContext,
|
|
287
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
288
|
+
result = "exception_${e.javaClass.simpleName}",
|
|
289
|
+
exception = e,
|
|
290
|
+
)
|
|
41
291
|
Log.e("SEND_EVENT", "flush failed", e)
|
|
42
292
|
}
|
|
43
293
|
}
|
|
294
|
+
|
|
295
|
+
val mergeResult =
|
|
296
|
+
mergeUndeliveredPendingEvents(eventsToRequeue, diagnosticsState)
|
|
297
|
+
mergeResult.survivingRequeuedEvents.forEach { event ->
|
|
298
|
+
logPendingDiagnostic(
|
|
299
|
+
marker = "FLUSH_EVENT_REQUEUED",
|
|
300
|
+
eventSeq = event.diagnostic?.eventSeq ?: -1,
|
|
301
|
+
flushId = flushId,
|
|
302
|
+
eventName = event.pendingEvent.first,
|
|
303
|
+
diagnosticIdentity = event.diagnostic?.identity,
|
|
304
|
+
queueSizeBefore = mergeResult.queueSizeBefore,
|
|
305
|
+
queueSizeAfter = mergeResult.queueSizeAfter,
|
|
306
|
+
reason = event.reason ?: "context_unavailable",
|
|
307
|
+
requeuedCount = mergeResult.requeuedCount,
|
|
308
|
+
newArrivalsCount = mergeResult.newArrivalsCount,
|
|
309
|
+
droppedCount = mergeResult.droppedEvents.size,
|
|
310
|
+
result = "requeued_${event.reason ?: "context_unavailable"}",
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
mergeResult.droppedEvents.forEach { event ->
|
|
314
|
+
logPendingDiagnostic(
|
|
315
|
+
marker = "FLUSH_REQUEUE_DROP",
|
|
316
|
+
eventSeq = event.diagnostic?.eventSeq ?: -1,
|
|
317
|
+
flushId = flushId,
|
|
318
|
+
eventName = event.pendingEvent.first,
|
|
319
|
+
diagnosticIdentity = event.diagnostic?.identity,
|
|
320
|
+
queueSizeBefore = mergeResult.queueSizeBefore,
|
|
321
|
+
queueSizeAfter = mergeResult.queueSizeAfter,
|
|
322
|
+
reason = event.reason ?: "new_arrival",
|
|
323
|
+
requeuedCount = mergeResult.requeuedCount,
|
|
324
|
+
newArrivalsCount = mergeResult.newArrivalsCount,
|
|
325
|
+
droppedCount = mergeResult.droppedEvents.size,
|
|
326
|
+
result = "drop_oldest_after_requeue_merge",
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
if (eventsToRequeue.isNotEmpty()) {
|
|
330
|
+
logPendingDiagnostic(
|
|
331
|
+
marker = "FLUSH_REQUEUE_MERGE",
|
|
332
|
+
flushId = flushId,
|
|
333
|
+
queueSizeBefore = mergeResult.queueSizeBefore,
|
|
334
|
+
queueSizeAfter = mergeResult.queueSizeAfter,
|
|
335
|
+
reason = "context_unavailable",
|
|
336
|
+
requeuedCount = mergeResult.requeuedCount,
|
|
337
|
+
newArrivalsCount = mergeResult.newArrivalsCount,
|
|
338
|
+
droppedCount = mergeResult.droppedEvents.size,
|
|
339
|
+
result = "requeued_before_new_arrivals_cap_applied",
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
val queueSizeAtCompletion = currentPendingEventCount()
|
|
344
|
+
logPendingDiagnostic(
|
|
345
|
+
marker = "FLUSH_COMPLETED",
|
|
346
|
+
flushId = flushId,
|
|
347
|
+
queueSizeBefore = eventsToSend.size,
|
|
348
|
+
queueSizeAfter = queueSizeAtCompletion,
|
|
349
|
+
result =
|
|
350
|
+
"native_returns_${nativeReturns}_skipped_${skippedNoContext}_exceptions_$exceptions",
|
|
351
|
+
)
|
|
44
352
|
}
|
|
45
353
|
|
|
46
354
|
fun promiseResolver(promise: Promise, e: Exception?, bundle: Bundle?) {
|
|
@@ -104,6 +412,12 @@ object NotifeeReactUtils {
|
|
|
104
412
|
}
|
|
105
413
|
|
|
106
414
|
fun sendEvent(eventName: String, eventMap: com.facebook.react.bridge.WritableMap) {
|
|
415
|
+
val diagnosticsState = pendingDiagnosticsState
|
|
416
|
+
if (diagnosticsState != null) {
|
|
417
|
+
sendEventDiagnostics(diagnosticsState, eventName, eventMap)
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
107
421
|
try {
|
|
108
422
|
val reactContext = HeadlessTask.getReactContext(EventSubscriber.getContext())
|
|
109
423
|
if (reactContext == null || !reactContext.hasActiveReactInstance()) {
|
|
@@ -124,6 +438,364 @@ object NotifeeReactUtils {
|
|
|
124
438
|
}
|
|
125
439
|
}
|
|
126
440
|
|
|
441
|
+
private fun sendEventDiagnostics(
|
|
442
|
+
diagnosticsState: PendingDiagnosticsState,
|
|
443
|
+
eventName: String,
|
|
444
|
+
eventMap: WritableMap,
|
|
445
|
+
) {
|
|
446
|
+
val eventSeq = diagnosticsState.eventSequence.incrementAndGet()
|
|
447
|
+
val diagnosticIdentity = extractPendingDiagnosticIdentity(eventMap)
|
|
448
|
+
val queueSizeAtReceipt = currentPendingEventCount()
|
|
449
|
+
logPendingDiagnostic(
|
|
450
|
+
marker = "EVENT_RECEIVED",
|
|
451
|
+
eventSeq = eventSeq,
|
|
452
|
+
eventName = eventName,
|
|
453
|
+
diagnosticIdentity = diagnosticIdentity,
|
|
454
|
+
queueSizeBefore = queueSizeAtReceipt,
|
|
455
|
+
queueSizeAfter = queueSizeAtReceipt,
|
|
456
|
+
result = "received",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
var reactContext: ReactContext? = null
|
|
460
|
+
var hasActiveReactInstance: Boolean? = null
|
|
461
|
+
try {
|
|
462
|
+
reactContext = HeadlessTask.getReactContext(EventSubscriber.getContext())
|
|
463
|
+
hasActiveReactInstance = reactContext?.hasActiveReactInstance()
|
|
464
|
+
val activeReactContext = reactContext
|
|
465
|
+
if (activeReactContext == null || hasActiveReactInstance != true) {
|
|
466
|
+
val pendingEvent = Pair(eventName, eventMap)
|
|
467
|
+
val queueSizeBefore: Int
|
|
468
|
+
val queueSizeAfterDrop: Int
|
|
469
|
+
val queueSizeAfter: Int
|
|
470
|
+
var droppedEvent: Pair<String, WritableMap>? = null
|
|
471
|
+
var droppedEventDiagnostic: QueuedPendingEventDiagnostic? = null
|
|
472
|
+
synchronized(pendingEvents) {
|
|
473
|
+
queueSizeBefore = pendingEvents.size
|
|
474
|
+
if (pendingEvents.size >= MAX_PENDING_EVENTS) {
|
|
475
|
+
droppedEvent = pendingEvents.removeAt(0)
|
|
476
|
+
droppedEventDiagnostic =
|
|
477
|
+
diagnosticsState.queuedEventDiagnostics.remove(droppedEvent)
|
|
478
|
+
}
|
|
479
|
+
queueSizeAfterDrop = pendingEvents.size
|
|
480
|
+
pendingEvents.add(pendingEvent)
|
|
481
|
+
diagnosticsState.queuedEventDiagnostics[pendingEvent] =
|
|
482
|
+
QueuedPendingEventDiagnostic(eventSeq, diagnosticIdentity)
|
|
483
|
+
queueSizeAfter = pendingEvents.size
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
droppedEvent?.let { dropped ->
|
|
487
|
+
logPendingDiagnostic(
|
|
488
|
+
marker = "EVENT_OVERFLOW_DROPPED",
|
|
489
|
+
eventSeq = droppedEventDiagnostic?.eventSeq ?: -1,
|
|
490
|
+
eventName = dropped.first,
|
|
491
|
+
diagnosticIdentity = droppedEventDiagnostic?.identity,
|
|
492
|
+
queueSizeBefore = queueSizeBefore,
|
|
493
|
+
queueSizeAfter = queueSizeAfterDrop,
|
|
494
|
+
reactContext = reactContext,
|
|
495
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
496
|
+
result = "drop_oldest",
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
logPendingDiagnostic(
|
|
500
|
+
marker = "EVENT_ENQUEUED",
|
|
501
|
+
eventSeq = eventSeq,
|
|
502
|
+
eventName = eventName,
|
|
503
|
+
diagnosticIdentity = diagnosticIdentity,
|
|
504
|
+
queueSizeBefore = queueSizeAfterDrop,
|
|
505
|
+
queueSizeAfter = queueSizeAfter,
|
|
506
|
+
reactContext = reactContext,
|
|
507
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
508
|
+
result = if (activeReactContext == null) {
|
|
509
|
+
"enqueued_context_null"
|
|
510
|
+
} else {
|
|
511
|
+
"enqueued_context_inactive"
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
val directQueueSize = currentPendingEventCount()
|
|
518
|
+
logPendingDiagnostic(
|
|
519
|
+
marker = "EVENT_EMIT_DIRECT_BEGIN",
|
|
520
|
+
eventSeq = eventSeq,
|
|
521
|
+
eventName = eventName,
|
|
522
|
+
diagnosticIdentity = diagnosticIdentity,
|
|
523
|
+
queueSizeBefore = directQueueSize,
|
|
524
|
+
queueSizeAfter = directQueueSize,
|
|
525
|
+
reactContext = activeReactContext,
|
|
526
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
527
|
+
result = "native_call_begin",
|
|
528
|
+
)
|
|
529
|
+
activeReactContext
|
|
530
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
531
|
+
.emit(eventName, eventMap)
|
|
532
|
+
logPendingDiagnostic(
|
|
533
|
+
marker = "EVENT_EMIT_DIRECT_RETURN",
|
|
534
|
+
eventSeq = eventSeq,
|
|
535
|
+
eventName = eventName,
|
|
536
|
+
diagnosticIdentity = diagnosticIdentity,
|
|
537
|
+
reactContext = activeReactContext,
|
|
538
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
539
|
+
result = "native_call_returned_no_js_ack",
|
|
540
|
+
)
|
|
541
|
+
} catch (e: Exception) {
|
|
542
|
+
logPendingDiagnostic(
|
|
543
|
+
marker = "EVENT_EMIT_DIRECT_EXCEPTION",
|
|
544
|
+
eventSeq = eventSeq,
|
|
545
|
+
eventName = eventName,
|
|
546
|
+
diagnosticIdentity = diagnosticIdentity,
|
|
547
|
+
reactContext = reactContext,
|
|
548
|
+
hasActiveReactInstance = hasActiveReactInstance,
|
|
549
|
+
result = "exception_${e.javaClass.simpleName}",
|
|
550
|
+
exception = e,
|
|
551
|
+
)
|
|
552
|
+
Log.e("SEND_EVENT", "", e)
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private fun snapshotAndClearPendingEventsDiagnostics(
|
|
557
|
+
diagnosticsState: PendingDiagnosticsState,
|
|
558
|
+
): List<PendingDiagnosticEvent> {
|
|
559
|
+
synchronized(pendingEvents) {
|
|
560
|
+
val eventsToSend =
|
|
561
|
+
pendingEvents.map { event ->
|
|
562
|
+
val eventDiagnostic =
|
|
563
|
+
diagnosticsState.queuedEventDiagnostics.remove(event)
|
|
564
|
+
PendingDiagnosticEvent(
|
|
565
|
+
eventDiagnostic?.eventSeq ?: -1,
|
|
566
|
+
event,
|
|
567
|
+
eventDiagnostic?.identity,
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
pendingEvents.clear()
|
|
571
|
+
return eventsToSend
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private fun mergeUndeliveredPendingEvents(
|
|
576
|
+
eventsToRequeue: List<PendingEventToRequeue>,
|
|
577
|
+
diagnosticsState: PendingDiagnosticsState?,
|
|
578
|
+
): PendingEventMergeResult {
|
|
579
|
+
synchronized(pendingEvents) {
|
|
580
|
+
val newArrivals =
|
|
581
|
+
pendingEvents.map { pendingEvent ->
|
|
582
|
+
PendingEventToRequeue(
|
|
583
|
+
pendingEvent = pendingEvent,
|
|
584
|
+
diagnostic = diagnosticsState?.queuedEventDiagnostics?.get(pendingEvent),
|
|
585
|
+
)
|
|
586
|
+
}
|
|
587
|
+
val mergedEvents = eventsToRequeue + newArrivals
|
|
588
|
+
val droppedCount = (mergedEvents.size - MAX_PENDING_EVENTS).coerceAtLeast(0)
|
|
589
|
+
val droppedEvents = mergedEvents.take(droppedCount)
|
|
590
|
+
val survivingEvents = mergedEvents.drop(droppedCount)
|
|
591
|
+
val droppedRequeuedCount = droppedCount.coerceAtMost(eventsToRequeue.size)
|
|
592
|
+
val survivingRequeuedEvents = eventsToRequeue.drop(droppedRequeuedCount)
|
|
593
|
+
|
|
594
|
+
pendingEvents.clear()
|
|
595
|
+
pendingEvents.addAll(survivingEvents.map { it.pendingEvent })
|
|
596
|
+
diagnosticsState?.queuedEventDiagnostics?.apply {
|
|
597
|
+
clear()
|
|
598
|
+
survivingEvents.forEach { event ->
|
|
599
|
+
event.diagnostic?.let { diagnostic -> put(event.pendingEvent, diagnostic) }
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return PendingEventMergeResult(
|
|
604
|
+
queueSizeBefore = mergedEvents.size,
|
|
605
|
+
requeuedCount = eventsToRequeue.size,
|
|
606
|
+
newArrivalsCount = newArrivals.size,
|
|
607
|
+
queueSizeAfter = survivingEvents.size,
|
|
608
|
+
droppedEvents = droppedEvents,
|
|
609
|
+
survivingRequeuedEvents = survivingRequeuedEvents,
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private fun currentPendingEventCount(): Int = synchronized(pendingEvents) { pendingEvents.size }
|
|
615
|
+
|
|
616
|
+
private fun extractPendingDiagnosticIdentity(
|
|
617
|
+
eventMap: WritableMap,
|
|
618
|
+
): PendingDiagnosticIdentity? {
|
|
619
|
+
return try {
|
|
620
|
+
val detail = readPendingDiagnosticMap(eventMap, "detail") ?: return null
|
|
621
|
+
val notification = readPendingDiagnosticMap(detail, "notification") ?: return null
|
|
622
|
+
val data = readPendingDiagnosticMap(notification, "data") ?: return null
|
|
623
|
+
val scenarioId =
|
|
624
|
+
readPendingDiagnosticString(data, PENDING_DIAGNOSTIC_SCENARIO_ID_KEY)
|
|
625
|
+
?: return null
|
|
626
|
+
val eventId =
|
|
627
|
+
readPendingDiagnosticString(data, PENDING_DIAGNOSTIC_EVENT_ID_KEY) ?: return null
|
|
628
|
+
|
|
629
|
+
if (
|
|
630
|
+
!isValidPendingDiagnosticScenarioId(scenarioId) ||
|
|
631
|
+
!isValidPendingDiagnosticEventId(scenarioId, eventId)
|
|
632
|
+
) {
|
|
633
|
+
null
|
|
634
|
+
} else {
|
|
635
|
+
PendingDiagnosticIdentity(
|
|
636
|
+
scenarioId,
|
|
637
|
+
eventId,
|
|
638
|
+
readPendingDiagnosticInt(eventMap, "type"),
|
|
639
|
+
)
|
|
640
|
+
}
|
|
641
|
+
} catch (_: Exception) {
|
|
642
|
+
null
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private fun readPendingDiagnosticMap(map: ReadableMap, key: String): ReadableMap? {
|
|
647
|
+
if (!map.hasKey(key) || map.isNull(key) || map.getType(key) != ReadableType.Map) {
|
|
648
|
+
return null
|
|
649
|
+
}
|
|
650
|
+
return map.getMap(key)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private fun readPendingDiagnosticString(map: ReadableMap, key: String): String? {
|
|
654
|
+
if (!map.hasKey(key) || map.isNull(key) || map.getType(key) != ReadableType.String) {
|
|
655
|
+
return null
|
|
656
|
+
}
|
|
657
|
+
return map.getString(key)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private fun readPendingDiagnosticInt(map: ReadableMap, key: String): Int? {
|
|
661
|
+
if (!map.hasKey(key) || map.isNull(key) || map.getType(key) != ReadableType.Number) {
|
|
662
|
+
return null
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
val numericValue =
|
|
666
|
+
try {
|
|
667
|
+
map.getDouble(key)
|
|
668
|
+
} catch (_: Exception) {
|
|
669
|
+
return try {
|
|
670
|
+
map.getInt(key)
|
|
671
|
+
} catch (_: Exception) {
|
|
672
|
+
null
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
val integerValue = numericValue.toInt()
|
|
676
|
+
return integerValue.takeIf { it.toDouble() == numericValue }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
private fun isValidPendingDiagnosticScenarioId(value: String): Boolean =
|
|
680
|
+
value.length in 1..MAX_PENDING_DIAGNOSTIC_SCENARIO_ID_LENGTH &&
|
|
681
|
+
value.all { character ->
|
|
682
|
+
character in 'A'..'Z' ||
|
|
683
|
+
character in 'a'..'z' ||
|
|
684
|
+
character in '0'..'9' ||
|
|
685
|
+
character == '.' ||
|
|
686
|
+
character == '_' ||
|
|
687
|
+
character == '-'
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private fun isValidPendingDiagnosticEventId(
|
|
691
|
+
scenarioId: String,
|
|
692
|
+
eventId: String,
|
|
693
|
+
): Boolean {
|
|
694
|
+
if (eventId.length !in 1..MAX_PENDING_DIAGNOSTIC_EVENT_ID_LENGTH) return false
|
|
695
|
+
|
|
696
|
+
val prefix = "pending-diag:$scenarioId:"
|
|
697
|
+
if (!eventId.startsWith(prefix)) return false
|
|
698
|
+
|
|
699
|
+
val suffix = eventId.substring(prefix.length)
|
|
700
|
+
if (suffix == "A" || suffix == "B") return true
|
|
701
|
+
if (!suffix.startsWith("overflow:")) return false
|
|
702
|
+
|
|
703
|
+
val overflowIndex = suffix.substring("overflow:".length)
|
|
704
|
+
if (overflowIndex.length != 2 || overflowIndex.any { it !in '0'..'9' }) return false
|
|
705
|
+
return overflowIndex.toIntOrNull()?.let { it in 1..11 } == true
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Invoked reflectively only by the smoke app's debug-only reload receiver. Keeping this method
|
|
709
|
+
// private avoids adding a library API while ensuring metadata work never runs in an event path.
|
|
710
|
+
@Synchronized
|
|
711
|
+
private fun initializePendingDiagnostics(context: Context): Boolean {
|
|
712
|
+
if (pendingDiagnosticsState != null) return true
|
|
713
|
+
|
|
714
|
+
val config = loadPendingDiagnosticsConfig(context)
|
|
715
|
+
if (!config.enabled) return false
|
|
716
|
+
|
|
717
|
+
pendingDiagnosticsState = PendingDiagnosticsState(config.delayAfterSnapshotMs)
|
|
718
|
+
return true
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
@Suppress("DEPRECATION")
|
|
722
|
+
private fun loadPendingDiagnosticsConfig(context: Context): PendingDiagnosticsConfig {
|
|
723
|
+
return try {
|
|
724
|
+
val applicationInfo =
|
|
725
|
+
context.packageManager.getApplicationInfo(
|
|
726
|
+
context.packageName,
|
|
727
|
+
PackageManager.GET_META_DATA,
|
|
728
|
+
)
|
|
729
|
+
val metadata = applicationInfo.metaData
|
|
730
|
+
val enabled =
|
|
731
|
+
metadata?.getBoolean(PENDING_DIAGNOSTICS_ENABLED_METADATA, false) == true &&
|
|
732
|
+
applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0
|
|
733
|
+
val delayAfterSnapshotMs =
|
|
734
|
+
if (enabled) {
|
|
735
|
+
metadata
|
|
736
|
+
.getInt(PENDING_DIAGNOSTICS_DELAY_METADATA, 0)
|
|
737
|
+
.coerceIn(0, MAX_PENDING_DIAGNOSTIC_DELAY_MS)
|
|
738
|
+
.toLong()
|
|
739
|
+
} else {
|
|
740
|
+
0
|
|
741
|
+
}
|
|
742
|
+
PendingDiagnosticsConfig(enabled, delayAfterSnapshotMs)
|
|
743
|
+
} catch (_: Exception) {
|
|
744
|
+
PendingDiagnosticsConfig(false, 0)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private fun logPendingDiagnostic(
|
|
749
|
+
marker: String,
|
|
750
|
+
eventSeq: Long = -1,
|
|
751
|
+
flushId: Long = -1,
|
|
752
|
+
eventName: String? = null,
|
|
753
|
+
diagnosticIdentity: PendingDiagnosticIdentity? = null,
|
|
754
|
+
queueSizeBefore: Int = -1,
|
|
755
|
+
queueSizeAfter: Int = -1,
|
|
756
|
+
reason: String = "not_applicable",
|
|
757
|
+
requeuedCount: Int = -1,
|
|
758
|
+
newArrivalsCount: Int = -1,
|
|
759
|
+
droppedCount: Int = -1,
|
|
760
|
+
reactContext: ReactContext? = null,
|
|
761
|
+
hasActiveReactInstance: Boolean? = null,
|
|
762
|
+
result: String,
|
|
763
|
+
exception: Exception? = null,
|
|
764
|
+
) {
|
|
765
|
+
val contextIdentity =
|
|
766
|
+
reactContext?.let { "0x${Integer.toHexString(System.identityHashCode(it))}" } ?: "null"
|
|
767
|
+
val message =
|
|
768
|
+
"$PENDING_DIAGNOSTICS_TAG marker=$marker " +
|
|
769
|
+
"eventSeq=$eventSeq flushId=$flushId " +
|
|
770
|
+
"eventName=${sanitizePendingDiagnosticValue(eventName)} " +
|
|
771
|
+
"diagScenarioId=${diagnosticIdentity?.scenarioId ?: "unavailable"} " +
|
|
772
|
+
"diagEventId=${diagnosticIdentity?.eventId ?: "unavailable"} " +
|
|
773
|
+
"eventType=${diagnosticIdentity?.eventType ?: "unavailable"} " +
|
|
774
|
+
"thread=${sanitizePendingDiagnosticValue(Thread.currentThread().name)} " +
|
|
775
|
+
"elapsedRealtime=${SystemClock.elapsedRealtime()} " +
|
|
776
|
+
"queueSizeBefore=$queueSizeBefore queueSizeAfter=$queueSizeAfter " +
|
|
777
|
+
"reason=${sanitizePendingDiagnosticValue(reason)} " +
|
|
778
|
+
"requeuedCount=$requeuedCount newArrivalsCount=$newArrivalsCount " +
|
|
779
|
+
"droppedCount=$droppedCount " +
|
|
780
|
+
"contextIdentity=$contextIdentity reactHostIdentity=unavailable " +
|
|
781
|
+
"hasActiveReactInstance=${hasActiveReactInstance ?: "not_checked"} " +
|
|
782
|
+
"result=${sanitizePendingDiagnosticValue(result)}"
|
|
783
|
+
if (exception == null) {
|
|
784
|
+
Log.i(PENDING_DIAGNOSTICS_TAG, message)
|
|
785
|
+
} else {
|
|
786
|
+
Log.e(PENDING_DIAGNOSTICS_TAG, message, exception)
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
private fun sanitizePendingDiagnosticValue(value: String?): String =
|
|
791
|
+
value
|
|
792
|
+
?.replace(' ', '_')
|
|
793
|
+
?.replace('\t', '_')
|
|
794
|
+
?.replace('\r', '_')
|
|
795
|
+
?.replace('\n', '_')
|
|
796
|
+
?.take(120)
|
|
797
|
+
?: "null"
|
|
798
|
+
|
|
127
799
|
fun isAppInForeground(): Boolean {
|
|
128
800
|
return ProcessLifecycleOwner.get()
|
|
129
801
|
.lifecycle
|