@dynatrace/react-native-plugin 2.341.1 → 2.343.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 +37 -9
- package/android/build.gradle +11 -2
- 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.gradle +1 -1
- package/instrumentation/libs/UserInteraction.js +1 -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 +53 -0
- package/lib/core/Dynatrace.js +2 -0
- package/lib/core/logging/LogMessages.js +42 -0
- package/lib/features/ui-interaction/IUserInteractionEvent.js +1 -1
- package/lib/features/ui-interaction/Runtime.js +1 -1
- package/lib/next/Dynatrace.js +26 -1
- 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/userAction/NullUserAction.js +15 -0
- package/lib/next/userAction/UserAction.js +2 -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 +7 -3
- package/public.js +3 -1
- package/react-native-dynatrace.podspec +10 -3
- package/scripts/Config.js +17 -3
- package/scripts/core/InstrumentCall.js +12 -4
- package/src/lib/core/interface/NativeDynatraceBridge.ts +34 -1
- package/types.d.ts +29 -2
package/README.md
CHANGED
|
@@ -23,20 +23,20 @@ If you want to start using this plugin and are not a Dynatrace customer yet, hea
|
|
|
23
23
|
* React v16.8 or newer
|
|
24
24
|
* React Native v0.60 or newer
|
|
25
25
|
* For Android users:
|
|
26
|
-
* SDK version
|
|
27
|
-
* Gradle version
|
|
26
|
+
* SDK version 23+
|
|
27
|
+
* Gradle version 8.1.1+ ([How to update?](#updating-to-gradle-7))
|
|
28
28
|
* Android Gradle plugin version 7.0+
|
|
29
|
-
* Java
|
|
29
|
+
* Java 17
|
|
30
30
|
* Kotlin 2.0.21 - see [Kotlin Compatibility Note](#kotlin-compatibility-note)
|
|
31
31
|
* Jetpack Compose 1.4 - 1.10 - see [Compose Compatibility Note](#compose-compatibility-note)
|
|
32
|
-
* For iOS users: Minimum iOS
|
|
32
|
+
* For iOS users: Minimum iOS 15
|
|
33
33
|
* NodeJS 16.0.0+ since our dependencies require NodeJS 16.0.0
|
|
34
34
|
|
|
35
35
|
## Agent Versions
|
|
36
36
|
These agent versions are configured in this plugin:
|
|
37
37
|
|
|
38
|
-
* Android Agent: 8.
|
|
39
|
-
* iOS Agent: 8.
|
|
38
|
+
* Android Agent: 8.343.1.1038
|
|
39
|
+
* iOS Agent: 8.343.1.1007
|
|
40
40
|
|
|
41
41
|
## Quick Setup
|
|
42
42
|
|
|
@@ -1049,7 +1049,7 @@ If masking is active for an interaction, the event still contains the same struc
|
|
|
1049
1049
|
"ui_element.components": ["Pressable"],
|
|
1050
1050
|
"ui_element.id": "App/View/Pressable",
|
|
1051
1051
|
"ui_element.name_origin": "component",
|
|
1052
|
-
"interaction.
|
|
1052
|
+
"interaction.type": "touch",
|
|
1053
1053
|
"positions": [{ "x": 120, "y": 460 }],
|
|
1054
1054
|
"ui_element.responder.detected_name": "Pressable",
|
|
1055
1055
|
"ui_element.responder.components": ["Pressable"],
|
|
@@ -1160,6 +1160,7 @@ npx instrumentDynatrace [optional: config=... gradle=... plist=...]
|
|
|
1160
1160
|
* `gradle=C:\MyReactAndroidProject\build.gradle`: The location of the root build.gradle file. We will assume that the other gradle file resides in `/app/build.gradle`. This will add the whole agent dependencies automatically for you and will update the configuration.
|
|
1161
1161
|
* `plist=C:\MyReactIOSProject\projectName\info.plist`: Tell the script where your info.plist file is. The plist file is used for updating the configuration for the agent.
|
|
1162
1162
|
* `config=C:\SpecialFolderForDynatrace\dynatrace.config.js`: If you have not got your config file in the root folder of the React Native project but somewhere else.
|
|
1163
|
+
* `--reset`: Clear any previously-persisted custom config path and revert the Metro/Babel transformer to the default `dynatrace.config.js` lookup. Useful after switching between build flavors.
|
|
1163
1164
|
|
|
1164
1165
|
If `plist=...` is not provided, the plugin tries to resolve `Info.plist` automatically in this order:
|
|
1165
1166
|
|
|
@@ -1188,6 +1189,22 @@ Our scripts assumes that the usual React Native project structure is given. The
|
|
|
1188
1189
|
* `plist=C:\MyReactIOSProject\projectName\info.plist`: Tell the script where your info.plist file is. The plist file is used for updating the configuration for the agent.
|
|
1189
1190
|
* `config=C:\SpecialFolderForDynatrace\dynatrace.config.js`: If you have not got your config file in the root folder of the React Native project but somewhere else.
|
|
1190
1191
|
|
|
1192
|
+
### DYNATRACE_CONFIG environment variable
|
|
1193
|
+
|
|
1194
|
+
As an alternative to `config=`, you can set the `DYNATRACE_CONFIG` environment variable to specify the config file path. This is particularly useful in CI pipelines and Xcode schemes where multiple build flavors each need their own config file, because it does not require changing the instrumentation script — only the environment of the build step.
|
|
1195
|
+
|
|
1196
|
+
```
|
|
1197
|
+
DYNATRACE_CONFIG=dynatrace.config.prod.js npx instrumentDynatrace
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
**Precedence (highest to lowest):**
|
|
1201
|
+
|
|
1202
|
+
1. `config=` CLI argument — always wins
|
|
1203
|
+
2. `DYNATRACE_CONFIG` environment variable
|
|
1204
|
+
3. Default `dynatrace.config.js` in the project root
|
|
1205
|
+
|
|
1206
|
+
> **Note:** `DYNATRACE_CONFIG` applies to the **Metro/Babel transformer** as well as `npx instrumentDynatrace`. This means you can set it in your Xcode scheme's environment variables to ensure the correct config is used during iOS archive/bundling, independently of what was passed to the instrumentation step.
|
|
1207
|
+
|
|
1191
1208
|
Examples:
|
|
1192
1209
|
|
|
1193
1210
|
* React Native v0.60.1 or newer:
|
|
@@ -2071,7 +2088,19 @@ If you are struggling with a problem, submit a support ticket to Dynatrace (supp
|
|
|
2071
2088
|
<br/><br/>
|
|
2072
2089
|
## Changelog
|
|
2073
2090
|
|
|
2074
|
-
|
|
2091
|
+
2.343.1
|
|
2092
|
+
* Updated Android (8.343.1.1038) & iOS Agent (8.343.1.1007)
|
|
2093
|
+
* Added User Actions feature
|
|
2094
|
+
* Raised minimum platform requirements for Android and iOS
|
|
2095
|
+
* Android API level: 21 → 23
|
|
2096
|
+
* Java: 11 → 17
|
|
2097
|
+
* AGP: 7.0+ → 8.1.1+
|
|
2098
|
+
* iOS SDK: 12 → 15
|
|
2099
|
+
* Fixed iOS archive/bundle crash (`ENOENT: no such file or directory`) in projects that use app-specific config files (e.g. `dynatrace.config.prod.js`) instead of the default `dynatrace.config.js` at the repo root. The config resolution code was unconditionally reading the config file before `require()`, causing a hard crash in the Metro/Babel transformer instead of a graceful "config not found" fallback. Discovered via external report from a monorepo setup with multiple build flavors.
|
|
2100
|
+
* Fixed custom config path (`config=` argument to `npx instrumentDynatrace`) being lost between build steps: previously, running `npx instrumentDynatrace` without arguments in a subsequent build step (e.g. a generic iOS build phase) would silently delete the persisted custom path, causing Metro bundler to fall back to the hardcoded `dynatrace.config.js` and crash. The custom path is now preserved until explicitly overridden.
|
|
2101
|
+
* Added `DYNATRACE_CONFIG` environment variable as an alternative way to specify the config file path for the Metro/Babel transformer.
|
|
2102
|
+
* Added `npx instrumentDynatrace --reset` flag to clear a previously-persisted custom config path and revert the Metro/Babel transformer to the default `dynatrace.config.js` lookup. Useful after switching between build flavors that each use a different config file.
|
|
2103
|
+
* Fixed incorrect `start_time` on Android app start events when `3rd_gen_enabled` is false. The `EventPipeline` was suppressing the `forwardAppStartEvent` timing-correction call along with all other 3rd-gen events, causing the native Dynatrace SDK to report its own stale process-creation timestamp instead of the actual JS bundle load time.
|
|
2075
2104
|
|
|
2076
2105
|
2.341.1
|
|
2077
2106
|
* Updated Android (8.341.1.1004) & iOS Agent (8.341.1.1010)
|
|
@@ -2084,7 +2113,6 @@ In Progress
|
|
|
2084
2113
|
* Updated `@babel/runtime` to latest major version
|
|
2085
2114
|
* Fixed security vulnerabilities in `ws` and `brace-expansion` dependencies
|
|
2086
2115
|
|
|
2087
|
-
|
|
2088
2116
|
2.339.1
|
|
2089
2117
|
* Updated Android (8.339.1.1004) & iOS Agent (8.339.1.1011)
|
|
2090
2118
|
* Enabled User Interaction Feature by default
|
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()
|
|
@@ -43,12 +51,13 @@ android {
|
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
defaultConfig {
|
|
46
|
-
minSdkVersion safeExtGet('minSdkVersion',
|
|
54
|
+
minSdkVersion safeExtGet('minSdkVersion', 23)
|
|
47
55
|
targetSdkVersion safeExtGet('targetSdkVersion', 31)
|
|
48
56
|
versionCode 1
|
|
49
57
|
versionName "1.0"
|
|
50
58
|
|
|
51
59
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
60
|
+
buildConfigField "boolean", "REPLAY_METRICS_ENABLED", isReplayMetricsBuildEnabled().toString()
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
lintOptions {
|
|
@@ -72,7 +81,7 @@ repositories {
|
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
dependencies {
|
|
75
|
-
implementation 'com.dynatrace.agent:agent-android:8.
|
|
84
|
+
implementation 'com.dynatrace.agent:agent-android:8.343.1.1038'
|
|
76
85
|
implementation "com.facebook.react:react-native:${safeExtGet('reactNative', '+')}"
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -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)
|
package/files/plugin.gradle
CHANGED