@dynatrace/react-native-plugin 2.341.1 → 2.345.1
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 +674 -668
- package/android/build.gradle +16 -3
- package/android/src/main/java/com/dynatrace/android/agent/DynatraceAppStartModule.kt +9 -31
- package/android/src/main/java/com/dynatrace/android/agent/DynatraceConfigurationModule.kt +1 -0
- package/android/src/main/java/com/dynatrace/android/agent/DynatraceRNBridgeImpl.kt +41 -0
- package/android/src/main/java/com/dynatrace/android/agent/DynatraceUtils.kt +1 -0
- package/android/src/main/java/com/dynatrace/android/agent/ScreenshotSelfMonitor.kt +175 -0
- package/android/src/main/java/com/dynatrace/android/agent/UIChangeScreenshotListener.kt +124 -0
- package/android/src/new/java/com/dynatrace/android/agent/DynatraceRNBridge.kt +33 -0
- package/android/src/old/java/com/dynatrace/android/agent/DynatraceRNBridge.kt +40 -0
- package/files/plugin-runtime.gradle +17 -33
- package/files/plugin.gradle +1 -1
- package/instrumentation/BabelPluginDynatrace.js +1 -1
- package/instrumentation/libs/UserInteraction.js +21 -7
- package/instrumentation/libs/react-navigation/ReactNavigation.js +14 -58
- package/instrumentation/libs/withOnPressMonitoring.js +3 -54
- package/internal.d.ts +1 -0
- package/internal.js +3 -1
- package/ios/DTXScreenshotSelfMonitor.h +29 -0
- package/ios/DTXScreenshotSelfMonitor.mm +213 -0
- package/ios/DTXScreenshotSelfMonitorSwizzler.h +15 -0
- package/ios/DTXScreenshotSelfMonitorSwizzler.mm +68 -0
- package/ios/DynatraceRNBridge.h +6 -0
- package/ios/DynatraceRNBridge.mm +62 -0
- package/lib/core/Dynatrace.js +2 -0
- package/lib/core/configuration/ConfigurationHandler.js +0 -21
- package/lib/core/logging/LogMessages.js +42 -0
- package/lib/dynatrace-reporter.js +1 -30
- package/lib/dynatrace-transformer.js +6 -34
- package/lib/next/Dynatrace.js +26 -1
- package/lib/next/configuration/INativeRuntimeConfiguration.js +0 -1
- package/lib/next/configuration/RuntimeConfigurationObserver.js +1 -6
- package/lib/next/events/EventPipeline.js +48 -31
- package/lib/next/events/HttpRequestEventData.js +1 -1
- package/lib/next/events/spec/EventSpecContstants.js +1 -1
- package/lib/next/navigation/NavigationTracker.js +93 -0
- package/lib/next/navigation/useNavigationTracking.js +54 -0
- package/lib/next/userAction/NullUserAction.js +15 -0
- package/lib/next/userAction/UserActionConfiguration.js +10 -0
- package/lib/next/userAction/UserActionImpl.js +70 -0
- package/lib/next/util/TraceContextUtils.js +8 -14
- package/lib/next/util/Utils.js +13 -0
- package/package.json +10 -18
- package/public.js +5 -1
- package/react-native-dynatrace.podspec +10 -3
- package/scripts/Android.js +1 -20
- package/scripts/Config.js +17 -7
- package/scripts/Deprecations.js +18 -0
- package/scripts/Install.js +13 -0
- package/scripts/PathsConstants.js +0 -4
- package/scripts/Uninstall.js +0 -15
- package/scripts/core/InstrumentCall.js +14 -7
- package/scripts/util/DeprecationReport.js +24 -0
- package/src/lib/core/interface/NativeDynatraceBridge.ts +34 -1
- package/types.d.ts +236 -7
- package/instrumentation/DynatraceInstrumentation.js +0 -1
- package/instrumentation/libs/community/Picker.InstrInfo.js +0 -20
- package/instrumentation/libs/community/gesture-handler/Touchables.InstrInfo.js +0 -25
- package/instrumentation/libs/react-native/RefreshControl.InstrInfo.js +0 -12
- package/instrumentation/libs/react-native/Switch.InstrInfo.js +0 -12
- package/instrumentation/libs/react-native/Touchables.InstrInfo.js +0 -25
- package/instrumentation/parser/Babel.js +0 -60
- package/instrumentation/parser/ParserUtil.js +0 -11
- package/lib/features/ui-interaction/Config.js +0 -42
- package/lib/features/ui-interaction/IUserInteractionEvent.js +0 -16
- package/lib/features/ui-interaction/Plugin.Fragment.Test.js +0 -170
- package/lib/features/ui-interaction/Plugin.js +0 -289
- package/lib/features/ui-interaction/RootDetection.js +0 -51
- package/lib/features/ui-interaction/RootWrapper.js +0 -236
- package/lib/features/ui-interaction/Run.js +0 -38
- package/lib/features/ui-interaction/Runtime.js +0 -843
- package/lib/features/ui-interaction/TouchMetaResolver.js +0 -493
- package/lib/features/ui-interaction/Types.js +0 -14
- package/lib/metro/getSourceMapInfo.js +0 -26
- package/scripts/LineOffsetAnalyze.js +0 -25
- package/scripts/core/LineOffsetAnalyzeCall.js +0 -165
- package/scripts/util/SourceMapUtil.js +0 -68
- /package/{instrumentation/model/Reference.js → lib/next/userAction/UserAction.js} +0 -0
package/android/build.gradle
CHANGED
|
@@ -8,6 +8,14 @@ def isNewArchitectureEnabled() {
|
|
|
8
8
|
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Build-time master switch for the screenshot self-monitoring (replay metrics) feature.
|
|
12
|
+
// Defaults to enabled; disable for a build with -PdynatraceReplayMetricsEnabled=false
|
|
13
|
+
// (or set dynatraceReplayMetricsEnabled=false in gradle.properties). When false, the monitor is
|
|
14
|
+
// never registered, regardless of the remote `replay_metrics_enabled` flag.
|
|
15
|
+
def isReplayMetricsBuildEnabled() {
|
|
16
|
+
return !project.hasProperty("dynatraceReplayMetricsEnabled") || project.dynatraceReplayMetricsEnabled == "true"
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
buildscript {
|
|
12
20
|
repositories {
|
|
13
21
|
google()
|
|
@@ -21,7 +29,9 @@ buildscript {
|
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
apply plugin: 'com.android.library'
|
|
24
|
-
|
|
32
|
+
if (!project.extensions.findByName('kotlin')) {
|
|
33
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
34
|
+
}
|
|
25
35
|
|
|
26
36
|
if (isNewArchitectureEnabled()) {
|
|
27
37
|
apply plugin: 'com.facebook.react'
|
|
@@ -43,12 +53,13 @@ android {
|
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
defaultConfig {
|
|
46
|
-
minSdkVersion safeExtGet('minSdkVersion',
|
|
56
|
+
minSdkVersion safeExtGet('minSdkVersion', 23)
|
|
47
57
|
targetSdkVersion safeExtGet('targetSdkVersion', 31)
|
|
48
58
|
versionCode 1
|
|
49
59
|
versionName "1.0"
|
|
50
60
|
|
|
51
61
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
62
|
+
buildConfigField "boolean", "REPLAY_METRICS_ENABLED", isReplayMetricsBuildEnabled().toString()
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
lintOptions {
|
|
@@ -59,8 +70,10 @@ android {
|
|
|
59
70
|
main {
|
|
60
71
|
if (isNewArchitectureEnabled()) {
|
|
61
72
|
java.srcDirs += ['src/new']
|
|
73
|
+
kotlin.srcDirs += ['src/new']
|
|
62
74
|
} else {
|
|
63
75
|
java.srcDirs += ['src/old']
|
|
76
|
+
kotlin.srcDirs += ['src/old']
|
|
64
77
|
}
|
|
65
78
|
}
|
|
66
79
|
}
|
|
@@ -72,7 +85,7 @@ repositories {
|
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
dependencies {
|
|
75
|
-
implementation 'com.dynatrace.agent:agent-android:8.
|
|
88
|
+
implementation 'com.dynatrace.agent:agent-android:8.345.1.1003'
|
|
76
89
|
implementation "com.facebook.react:react-native:${safeExtGet('reactNative', '+')}"
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package com.dynatrace.android.agent
|
|
2
2
|
|
|
3
|
-
import android.os.SystemClock
|
|
4
3
|
import com.dynatrace.android.agent.model.AppStartMeasurement
|
|
5
4
|
import com.dynatrace.android.agent.model.AppStartMeasurementType
|
|
6
5
|
import com.facebook.react.bridge.Arguments
|
|
@@ -31,52 +30,31 @@ private val buffer: Queue<AppStartMeasurement> = ConcurrentLinkedQueue()
|
|
|
31
30
|
* Setup to register all time points which we want to use to report correct app startup time.
|
|
32
31
|
*/
|
|
33
32
|
fun setupAppStartListener() {
|
|
34
|
-
val initTimestamp = System.currentTimeMillis() - SystemClock.elapsedRealtime();
|
|
35
33
|
ReactMarker.addListener { name: ReactMarkerConstants?, _: String?, _: Int ->
|
|
36
34
|
when (name) {
|
|
37
35
|
ReactMarkerConstants.RELOAD -> {
|
|
38
36
|
buffer.clear()
|
|
39
|
-
buffer.add(
|
|
40
|
-
AppStartMeasurement(
|
|
41
|
-
AppStartMeasurementType.RELOAD.value,
|
|
42
|
-
initTimestamp + SystemClock.elapsedRealtime()
|
|
43
|
-
)
|
|
44
|
-
)
|
|
37
|
+
buffer.add(AppStartMeasurement(AppStartMeasurementType.RELOAD.value, TimeLineProvider.getSystemTime()))
|
|
45
38
|
}
|
|
46
39
|
|
|
47
|
-
ReactMarkerConstants.CONTENT_APPEARED ->
|
|
48
|
-
AppStartMeasurement(
|
|
49
|
-
|
|
50
|
-
initTimestamp + SystemClock.elapsedRealtime()
|
|
51
|
-
)
|
|
52
|
-
)
|
|
40
|
+
ReactMarkerConstants.CONTENT_APPEARED -> {
|
|
41
|
+
buffer.add(AppStartMeasurement(AppStartMeasurementType.CONTENT_APPEARED.value, TimeLineProvider.getSystemTime()))
|
|
42
|
+
}
|
|
53
43
|
|
|
54
44
|
ReactMarkerConstants.DOWNLOAD_END -> buffer.add(
|
|
55
|
-
AppStartMeasurement(
|
|
56
|
-
AppStartMeasurementType.DOWNLOAD_END.value,
|
|
57
|
-
initTimestamp + SystemClock.elapsedRealtime()
|
|
58
|
-
)
|
|
45
|
+
AppStartMeasurement(AppStartMeasurementType.DOWNLOAD_END.value, TimeLineProvider.getSystemTime())
|
|
59
46
|
)
|
|
60
47
|
|
|
61
48
|
ReactMarkerConstants.DOWNLOAD_START -> buffer.add(
|
|
62
|
-
AppStartMeasurement(
|
|
63
|
-
AppStartMeasurementType.DOWNLOAD_START.value,
|
|
64
|
-
initTimestamp + SystemClock.elapsedRealtime()
|
|
65
|
-
)
|
|
49
|
+
AppStartMeasurement(AppStartMeasurementType.DOWNLOAD_START.value, TimeLineProvider.getSystemTime())
|
|
66
50
|
)
|
|
67
51
|
|
|
68
52
|
ReactMarkerConstants.RUN_JS_BUNDLE_END -> buffer.add(
|
|
69
|
-
AppStartMeasurement(
|
|
70
|
-
AppStartMeasurementType.RUN_JS_BUNDLE_END.value,
|
|
71
|
-
initTimestamp + SystemClock.elapsedRealtime()
|
|
72
|
-
)
|
|
53
|
+
AppStartMeasurement(AppStartMeasurementType.RUN_JS_BUNDLE_END.value, TimeLineProvider.getSystemTime())
|
|
73
54
|
)
|
|
74
55
|
|
|
75
56
|
ReactMarkerConstants.RUN_JS_BUNDLE_START -> buffer.add(
|
|
76
|
-
AppStartMeasurement(
|
|
77
|
-
AppStartMeasurementType.RUN_JS_BUNDLE_START.value,
|
|
78
|
-
initTimestamp + SystemClock.elapsedRealtime()
|
|
79
|
-
)
|
|
57
|
+
AppStartMeasurement(AppStartMeasurementType.RUN_JS_BUNDLE_START.value, TimeLineProvider.getSystemTime())
|
|
80
58
|
)
|
|
81
59
|
|
|
82
60
|
else -> {
|
|
@@ -126,4 +104,4 @@ internal class DynatraceAppStartModule(reactContext: ReactApplicationContext?) :
|
|
|
126
104
|
override fun getName(): String {
|
|
127
105
|
return APP_START_MODULE
|
|
128
106
|
}
|
|
129
|
-
}
|
|
107
|
+
}
|
|
@@ -308,6 +308,47 @@ class DynatraceRNBridgeImpl(
|
|
|
308
308
|
HybridBridge.forwardAppStartEvent(JSONObject(DynatraceUtils.toHashMap(attributes)), appStartKeys.toArrayList() as ArrayList<String>)
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
fun setAutomaticUserActionDetection(enabled: Boolean) {
|
|
312
|
+
HybridBridge.setAutomaticUserActionDetection(enabled)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fun createUserAction(
|
|
316
|
+
actionId: String,
|
|
317
|
+
name: String,
|
|
318
|
+
completeAutomatically: Boolean,
|
|
319
|
+
properties: ReadableMap?
|
|
320
|
+
) {
|
|
321
|
+
val jsonProperties = properties?.let {
|
|
322
|
+
JSONObject(DynatraceUtils.toHashMap(it))
|
|
323
|
+
}
|
|
324
|
+
HybridBridge.createUserAction(
|
|
325
|
+
actionId,
|
|
326
|
+
name,
|
|
327
|
+
completeAutomatically,
|
|
328
|
+
jsonProperties
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
fun addEventStringPropertyToUserAction(actionId: String, key: String, value: String) {
|
|
333
|
+
HybridBridge.addEventPropertyToUserAction(actionId, key, value)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
fun addEventDoublePropertyToUserAction(actionId: String, key: String, value: Double) {
|
|
337
|
+
HybridBridge.addEventPropertyToUserAction(actionId, key, value)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fun addEventBooleanPropertyToUserAction(actionId: String, key: String, value: Boolean) {
|
|
341
|
+
HybridBridge.addEventPropertyToUserAction(actionId, key, value)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fun completeUserAction(actionId: String) {
|
|
345
|
+
HybridBridge.completeUserAction(actionId)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fun setCompleteUserActionAutomatically(actionId: String, enabled: Boolean) {
|
|
349
|
+
HybridBridge.setCompleteUserActionAutomatically(actionId, enabled)
|
|
350
|
+
}
|
|
351
|
+
|
|
311
352
|
fun startView(name: String) {
|
|
312
353
|
Dynatrace.startView(name)
|
|
313
354
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
package com.dynatrace.android.agent
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.dynatrace.android.agent.conf.ConfigurationSubscriber
|
|
5
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
6
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
7
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
8
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
9
|
+
|
|
10
|
+
private const val TAG = "dtxScreenshotSelfMonitor"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Remote feature-flag key in the cross-platform configuration map that turns screenshot
|
|
14
|
+
* self-monitoring on. Monitoring is off unless the server delivers this flag as `true`, which keeps
|
|
15
|
+
* the per-frame UI-change observation dormant for apps where it isn't enabled.
|
|
16
|
+
*/
|
|
17
|
+
private const val CONFIG_KEY_REPLAY_METRICS = "replay_metrics_enabled"
|
|
18
|
+
|
|
19
|
+
// DEM-26735: replay metrics experiment sunsets on 2026-10-15T00:00:00Z; remove this class after that date.
|
|
20
|
+
private const val EXPERIMENT_EXPIRY_EPOCH_MS = 1_792_022_400_000L
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Drives the screenshot self-monitoring feature.
|
|
24
|
+
*
|
|
25
|
+
* Responsibilities:
|
|
26
|
+
* - Subscribes to the agent configuration and toggles monitoring based on
|
|
27
|
+
* [CONFIG_KEY_REPLAY_METRICS].
|
|
28
|
+
* - When enabled, registers a [UIChangeScreenshotListener] on the current activity and keeps it
|
|
29
|
+
* bound to the foreground activity across the React host lifecycle.
|
|
30
|
+
*
|
|
31
|
+
* Threading: configuration is delivered on a background thread, but [ViewTreeObserver][android.view.ViewTreeObserver]
|
|
32
|
+
* registration must happen on the main thread. All monitoring state ([monitoringEnabled],
|
|
33
|
+
* [uiChangeListener], [lifecycleListenerRegistered]) is therefore confined to the UI thread —
|
|
34
|
+
* config toggles are posted there and the host lifecycle callbacks already run there — so no locking
|
|
35
|
+
* is required.
|
|
36
|
+
*
|
|
37
|
+
* A single instance is registered for the lifetime of the React instance via [initialize].
|
|
38
|
+
*/
|
|
39
|
+
internal class ScreenshotSelfMonitor private constructor(
|
|
40
|
+
private val reactApplicationContext: ReactApplicationContext
|
|
41
|
+
) : LifecycleEventListener, ConfigurationSubscriber {
|
|
42
|
+
|
|
43
|
+
private var monitoringEnabled = false
|
|
44
|
+
private var uiChangeListener: UIChangeScreenshotListener? = null
|
|
45
|
+
private var lifecycleListenerRegistered = false
|
|
46
|
+
|
|
47
|
+
private fun register() {
|
|
48
|
+
// The publisher immediately notifies us with the current configuration on registration.
|
|
49
|
+
HybridBridge.addConfigurationSubscriber(this)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun notify(configuration: Map<String, Any>) {
|
|
53
|
+
val flagEnabled = configuration[CONFIG_KEY_REPLAY_METRICS] as? Boolean ?: false
|
|
54
|
+
val enabled = flagEnabled && !isExperimentExpired()
|
|
55
|
+
if (flagEnabled && !enabled) {
|
|
56
|
+
Log.d(TAG, "notify: $CONFIG_KEY_REPLAY_METRICS=true but experiment expired — treating as disabled")
|
|
57
|
+
} else {
|
|
58
|
+
Log.d(TAG, "notify: $CONFIG_KEY_REPLAY_METRICS = $flagEnabled")
|
|
59
|
+
}
|
|
60
|
+
UiThreadUtil.runOnUiThread { setMonitoringEnabled(enabled) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun isExperimentExpired(): Boolean =
|
|
64
|
+
System.currentTimeMillis() >= EXPERIMENT_EXPIRY_EPOCH_MS
|
|
65
|
+
|
|
66
|
+
private fun setMonitoringEnabled(enabled: Boolean) {
|
|
67
|
+
if (enabled == monitoringEnabled) {
|
|
68
|
+
Log.d(TAG, "setMonitoringEnabled: no change (still $enabled)")
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
monitoringEnabled = enabled
|
|
72
|
+
|
|
73
|
+
if (enabled) {
|
|
74
|
+
Log.d(TAG, "$CONFIG_KEY_REPLAY_METRICS enabled — starting screenshot self-monitoring")
|
|
75
|
+
registerUIChangeListener()
|
|
76
|
+
} else {
|
|
77
|
+
Log.d(TAG, "$CONFIG_KEY_REPLAY_METRICS disabled — stopping screenshot self-monitoring")
|
|
78
|
+
deregisterUIChangeListener()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun registerUIChangeListener() {
|
|
83
|
+
if (uiChangeListener != null) return
|
|
84
|
+
|
|
85
|
+
if (!lifecycleListenerRegistered) {
|
|
86
|
+
reactApplicationContext.addLifecycleEventListener(this)
|
|
87
|
+
lifecycleListenerRegistered = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
val activity = reactApplicationContext.currentActivity
|
|
91
|
+
if (activity == null) {
|
|
92
|
+
Log.w(TAG, "No current activity — UIChangeScreenshotListener registration deferred to onHostResume")
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
val listener = UIChangeScreenshotListener()
|
|
97
|
+
if (!listener.registerOn(activity)) {
|
|
98
|
+
Log.w(TAG, "Failed to register OnDrawListener")
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
uiChangeListener = listener
|
|
102
|
+
Log.d(TAG, "UIChangeScreenshotListener registered")
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private fun deregisterUIChangeListener() {
|
|
106
|
+
if (lifecycleListenerRegistered) {
|
|
107
|
+
reactApplicationContext.removeLifecycleEventListener(this)
|
|
108
|
+
lifecycleListenerRegistered = false
|
|
109
|
+
}
|
|
110
|
+
uiChangeListener?.unregister()
|
|
111
|
+
uiChangeListener = null
|
|
112
|
+
Log.d(TAG, "UIChangeScreenshotListener deregistered")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Host lifecycle callbacks are delivered on the UI thread.
|
|
116
|
+
override fun onHostResume() {
|
|
117
|
+
if (!monitoringEnabled) return
|
|
118
|
+
|
|
119
|
+
val activity = reactApplicationContext.currentActivity
|
|
120
|
+
if (activity == null) {
|
|
121
|
+
if (uiChangeListener != null) {
|
|
122
|
+
Log.d(TAG, "onHostResume: no activity, tearing down UIChangeScreenshotListener")
|
|
123
|
+
deregisterUIChangeListener()
|
|
124
|
+
}
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Deferred registration: monitoring was enabled before an activity was available
|
|
129
|
+
if (uiChangeListener == null) {
|
|
130
|
+
Log.d(TAG, "onHostResume: attempting deferred UIChangeScreenshotListener registration")
|
|
131
|
+
registerUIChangeListener()
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
val listener = uiChangeListener ?: return
|
|
136
|
+
Log.d(TAG, "onHostResume: re-registering UIChangeScreenshotListener on current activity")
|
|
137
|
+
listener.unregister()
|
|
138
|
+
if (!listener.registerOn(activity)) {
|
|
139
|
+
Log.w(TAG, "onHostResume: failed to re-register UIChangeScreenshotListener, tearing down")
|
|
140
|
+
deregisterUIChangeListener()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
override fun onHostPause() {
|
|
145
|
+
Log.d(TAG, "onHostPause: unregistering UIChangeScreenshotListener")
|
|
146
|
+
uiChangeListener?.unregister()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override fun onHostDestroy() {
|
|
150
|
+
Log.d(TAG, "onHostDestroy: deregistering UIChangeScreenshotListener")
|
|
151
|
+
deregisterUIChangeListener()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
companion object {
|
|
155
|
+
private val initialized = AtomicBoolean(false)
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates and registers the single [ScreenshotSelfMonitor] for the current React instance.
|
|
159
|
+
* Safe to call multiple times (e.g. from different module init paths / architectures); only
|
|
160
|
+
* the first call takes effect.
|
|
161
|
+
*/
|
|
162
|
+
@JvmStatic
|
|
163
|
+
fun initialize(reactApplicationContext: ReactApplicationContext) {
|
|
164
|
+
// Build-time master switch (see android/build.gradle isReplayMetricsBuildEnabled()).
|
|
165
|
+
// When disabled the monitor is never registered, so the remote flag can't turn it on.
|
|
166
|
+
if (!BuildConfig.REPLAY_METRICS_ENABLED) {
|
|
167
|
+
Log.d(TAG, "screenshot self-monitoring disabled at build time — not registering")
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
if (initialized.compareAndSet(false, true)) {
|
|
171
|
+
ScreenshotSelfMonitor(reactApplicationContext).register()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
package com.dynatrace.android.agent
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import android.os.SystemClock
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.view.ViewTreeObserver
|
|
9
|
+
import java.lang.ref.WeakReference
|
|
10
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
|
+
|
|
12
|
+
private const val TAG = "dtxScreenshotSelfMonitor"
|
|
13
|
+
private const val DEBOUNCE_MS = 500L
|
|
14
|
+
private const val MAX_DEBOUNCE_MS = 2000L
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Self-monitoring listener that mirrors the Session Replay UI-change detection, but instead of
|
|
18
|
+
* capturing a screenshot it only increments the cross-platform screenshot counter via
|
|
19
|
+
* [HybridBridge.trackCrossPlatformScreenshot].
|
|
20
|
+
*
|
|
21
|
+
* The purpose is to estimate how many screenshots the Session Replay UI-change listener *would*
|
|
22
|
+
* take in production, without shipping the (not yet published) Session Replay package.
|
|
23
|
+
*
|
|
24
|
+
* It observes [ViewTreeObserver.OnDrawListener] on the current activity's decor view and debounces
|
|
25
|
+
* bursts of draws (a settled UI is reported once). Both [lastDrawTime] and [firstDrawTime] are only
|
|
26
|
+
* touched on the main thread (`onDraw` and the Handler-posted [captureRunnable]).
|
|
27
|
+
*/
|
|
28
|
+
internal class UIChangeScreenshotListener : ViewTreeObserver.OnDrawListener {
|
|
29
|
+
|
|
30
|
+
private val pending = AtomicBoolean(false)
|
|
31
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
32
|
+
private var registeredActivity: WeakReference<Activity>? = null
|
|
33
|
+
|
|
34
|
+
private var lastDrawTime = 0L
|
|
35
|
+
private var firstDrawTime = 0L
|
|
36
|
+
|
|
37
|
+
private val captureRunnable = Runnable { performCapture() }
|
|
38
|
+
|
|
39
|
+
override fun onDraw() {
|
|
40
|
+
val now = SystemClock.uptimeMillis()
|
|
41
|
+
lastDrawTime = now
|
|
42
|
+
if (pending.compareAndSet(false, true)) {
|
|
43
|
+
firstDrawTime = now
|
|
44
|
+
handler.postDelayed(captureRunnable, DEBOUNCE_MS)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun registerOn(activity: Activity): Boolean {
|
|
49
|
+
if (registeredActivity?.get() === activity) return true
|
|
50
|
+
|
|
51
|
+
// Clean up previous registration if switching activities
|
|
52
|
+
unregister()
|
|
53
|
+
|
|
54
|
+
val decorView = activity.window?.decorView
|
|
55
|
+
if (decorView == null) {
|
|
56
|
+
Log.w(TAG, "Cannot register: decorView is null")
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
val observer = decorView.viewTreeObserver
|
|
60
|
+
if (!observer.isAlive) {
|
|
61
|
+
Log.w(TAG, "Cannot register: ViewTreeObserver is not alive")
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
observer.addOnDrawListener(this)
|
|
66
|
+
registeredActivity = WeakReference(activity)
|
|
67
|
+
Log.d(TAG, "Registered OnDrawListener on DecorView")
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun unregister() {
|
|
72
|
+
val activity = registeredActivity?.get()
|
|
73
|
+
if (activity != null) {
|
|
74
|
+
try {
|
|
75
|
+
val decorView = activity.window?.decorView
|
|
76
|
+
val observer = decorView?.viewTreeObserver
|
|
77
|
+
if (observer != null && observer.isAlive) {
|
|
78
|
+
observer.removeOnDrawListener(this)
|
|
79
|
+
}
|
|
80
|
+
} catch (e: IllegalStateException) {
|
|
81
|
+
Log.w(TAG, "Expected cleanup issue removing OnDrawListener", e)
|
|
82
|
+
} catch (e: Exception) {
|
|
83
|
+
Log.e(TAG, "Unexpected error removing OnDrawListener", e)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
registeredActivity = null
|
|
87
|
+
dispose()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private fun performCapture() {
|
|
91
|
+
if (registeredActivity?.get() == null) {
|
|
92
|
+
// Leave pending=true so onDraw won't schedule further captures
|
|
93
|
+
Log.w(TAG, "Activity reference lost (GC'd) — capture abandoned")
|
|
94
|
+
handler.removeCallbacks(captureRunnable)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
val now = SystemClock.uptimeMillis()
|
|
99
|
+
val elapsed = now - lastDrawTime
|
|
100
|
+
val sinceFirst = now - firstDrawTime
|
|
101
|
+
|
|
102
|
+
// Reschedule if UI still active, unless max wait exceeded
|
|
103
|
+
if (elapsed < DEBOUNCE_MS && sinceFirst < MAX_DEBOUNCE_MS) {
|
|
104
|
+
val remaining = DEBOUNCE_MS - elapsed
|
|
105
|
+
Log.d(TAG, "UI still active, rescheduling in ${remaining}ms")
|
|
106
|
+
handler.postDelayed(captureRunnable, remaining)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pending.set(false)
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
Log.d(TAG, "Tracking cross-platform screenshot (self-monitoring)")
|
|
114
|
+
HybridBridge.trackCrossPlatformScreenshot()
|
|
115
|
+
} catch (e: Exception) {
|
|
116
|
+
Log.e(TAG, "trackCrossPlatformScreenshot failed during UI change capture", e)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fun dispose() {
|
|
121
|
+
handler.removeCallbacks(captureRunnable)
|
|
122
|
+
pending.set(false)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -179,6 +179,39 @@ class DynatraceRNBridge(
|
|
|
179
179
|
impl.forwardAppStartEvent(attributes, appStartKeys)
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
override fun setAutomaticUserActionDetection(enabled: Boolean) {
|
|
183
|
+
impl.setAutomaticUserActionDetection(enabled)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
override fun createUserAction(
|
|
187
|
+
actionId: String,
|
|
188
|
+
name: String,
|
|
189
|
+
completeAutomatically: Boolean,
|
|
190
|
+
properties: ReadableMap?
|
|
191
|
+
) {
|
|
192
|
+
impl.createUserAction(actionId, name, completeAutomatically, properties)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
override fun addEventStringPropertyToUserAction(actionId: String, key: String, value: String) {
|
|
196
|
+
impl.addEventStringPropertyToUserAction(actionId, key, value)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
override fun addEventDoublePropertyToUserAction(actionId: String, key: String, value: Double) {
|
|
200
|
+
impl.addEventDoublePropertyToUserAction(actionId, key, value)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override fun addEventBooleanPropertyToUserAction(actionId: String, key: String, value: Boolean) {
|
|
204
|
+
impl.addEventBooleanPropertyToUserAction(actionId, key, value)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override fun completeUserAction(actionId: String) {
|
|
208
|
+
impl.completeUserAction(actionId)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
override fun setCompleteUserActionAutomatically(actionId: String, enabled: Boolean) {
|
|
212
|
+
impl.setCompleteUserActionAutomatically(actionId, enabled)
|
|
213
|
+
}
|
|
214
|
+
|
|
182
215
|
override fun startView(name: String) {
|
|
183
216
|
impl.startView(name)
|
|
184
217
|
}
|
|
@@ -197,6 +197,46 @@ class DynatraceRNBridge(
|
|
|
197
197
|
impl.forwardAppStartEvent(attributes, appStartKeys)
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
@ReactMethod
|
|
201
|
+
fun setAutomaticUserActionDetection(enabled: Boolean) {
|
|
202
|
+
impl.setAutomaticUserActionDetection(enabled)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@ReactMethod
|
|
206
|
+
fun createUserAction(
|
|
207
|
+
actionId: String,
|
|
208
|
+
name: String,
|
|
209
|
+
completeAutomatically: Boolean,
|
|
210
|
+
properties: ReadableMap?
|
|
211
|
+
) {
|
|
212
|
+
impl.createUserAction(actionId, name, completeAutomatically, properties)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@ReactMethod
|
|
216
|
+
fun addEventStringPropertyToUserAction(actionId: String, key: String, value: String) {
|
|
217
|
+
impl.addEventStringPropertyToUserAction(actionId, key, value)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@ReactMethod
|
|
221
|
+
fun addEventDoublePropertyToUserAction(actionId: String, key: String, value: Double) {
|
|
222
|
+
impl.addEventDoublePropertyToUserAction(actionId, key, value)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@ReactMethod
|
|
226
|
+
fun addEventBooleanPropertyToUserAction(actionId: String, key: String, value: Boolean) {
|
|
227
|
+
impl.addEventBooleanPropertyToUserAction(actionId, key, value)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@ReactMethod
|
|
231
|
+
fun completeUserAction(actionId: String) {
|
|
232
|
+
impl.completeUserAction(actionId)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@ReactMethod
|
|
236
|
+
fun setCompleteUserActionAutomatically(actionId: String, enabled: Boolean) {
|
|
237
|
+
impl.setCompleteUserActionAutomatically(actionId, enabled)
|
|
238
|
+
}
|
|
239
|
+
|
|
200
240
|
@ReactMethod
|
|
201
241
|
fun startView(name: String) {
|
|
202
242
|
impl.startView(name)
|
|
@@ -1,33 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
commandLine 'npx', 'lineOffsetDynatrace'
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
android.applicationVariants.all { variant ->
|
|
25
|
-
if (variant.buildType.name == "release") {
|
|
26
|
-
def lineOffsetTask = tasks.register("runLineOffset${variant.name.capitalize()}", RunLineOffsetTask) {
|
|
27
|
-
workingDirectory = layout.projectDirectory
|
|
28
|
-
}
|
|
29
|
-
variant.mergeAssetsProvider.configure { task ->
|
|
30
|
-
task.dependsOn(lineOffsetTask)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
// Intentionally left empty. DO NOT DELETE THIS FILE.
|
|
2
|
+
//
|
|
3
|
+
// Apps instrumented with an older version of this plugin have the following
|
|
4
|
+
// line hard-coded in their android/app/build.gradle:
|
|
5
|
+
//
|
|
6
|
+
// apply from: "../../node_modules/@dynatrace/react-native-plugin/files/plugin-runtime.gradle"
|
|
7
|
+
//
|
|
8
|
+
// That line lives in the customer's own build.gradle, so we cannot reliably
|
|
9
|
+
// remove it on upgrade. If this file were missing, the `apply from:` above
|
|
10
|
+
// would fail the Gradle build for every app that still has that line.
|
|
11
|
+
//
|
|
12
|
+
// Previously this script registered a release-build Gradle task that ran
|
|
13
|
+
// `npx lineOffsetDynatrace` for line-offset analysis. That is no longer needed:
|
|
14
|
+
// auto-instrumentation moved to a Babel plugin and capturing moved to native,
|
|
15
|
+
// so the script is now a no-op kept purely for backward compatibility.
|
|
16
|
+
//
|
|
17
|
+
// New installs no longer add the `apply from:` line at all.
|
package/files/plugin.gradle
CHANGED