@dynatrace/react-native-plugin 2.339.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.
Files changed (62) hide show
  1. package/README.md +149 -23
  2. package/android/build.gradle +11 -2
  3. package/android/src/main/java/com/dynatrace/android/agent/DynatraceAppStartModule.kt +9 -31
  4. package/android/src/main/java/com/dynatrace/android/agent/DynatraceConfigurationModule.kt +1 -0
  5. package/android/src/main/java/com/dynatrace/android/agent/DynatraceRNBridgeImpl.kt +41 -1
  6. package/android/src/main/java/com/dynatrace/android/agent/DynatraceUtils.kt +1 -0
  7. package/android/src/main/java/com/dynatrace/android/agent/ScreenshotSelfMonitor.kt +175 -0
  8. package/android/src/main/java/com/dynatrace/android/agent/UIChangeScreenshotListener.kt +124 -0
  9. package/android/src/new/java/com/dynatrace/android/agent/DynatraceRNBridge.kt +33 -0
  10. package/android/src/old/java/com/dynatrace/android/agent/DynatraceRNBridge.kt +40 -0
  11. package/files/plugin.gradle +1 -1
  12. package/instrumentation/BabelPluginDynatrace.js +1 -1
  13. package/instrumentation/DynatraceInstrumentation.js +1 -1
  14. package/instrumentation/jsx/CreateElement.js +5 -9
  15. package/instrumentation/jsx/ElementHelper.js +5 -6
  16. package/instrumentation/jsx/JsxDevRuntime.js +33 -31
  17. package/instrumentation/jsx/JsxRuntime.js +30 -30
  18. package/instrumentation/jsx/JsxRuntimeHelpers.js +14 -0
  19. package/instrumentation/jsx/components/ClassComponent.js +4 -8
  20. package/instrumentation/jsx/components/ComponentUtil.js +24 -25
  21. package/instrumentation/libs/UserInteraction.js +27 -24
  22. package/instrumentation/libs/react-native/RefreshControl.js +5 -9
  23. package/instrumentation/libs/withOnPressMonitoring.js +66 -57
  24. package/ios/DTXScreenshotSelfMonitor.h +29 -0
  25. package/ios/DTXScreenshotSelfMonitor.mm +213 -0
  26. package/ios/DTXScreenshotSelfMonitorSwizzler.h +15 -0
  27. package/ios/DTXScreenshotSelfMonitorSwizzler.mm +68 -0
  28. package/ios/DynatraceRNBridge.h +6 -0
  29. package/ios/DynatraceRNBridge.mm +53 -0
  30. package/lib/core/Dynatrace.js +2 -0
  31. package/lib/core/DynatraceAction.js +1 -3
  32. package/lib/core/DynatraceInternal.js +38 -40
  33. package/lib/core/ErrorHandler.js +41 -21
  34. package/lib/core/NullAction.js +1 -3
  35. package/lib/core/logging/LogMessages.js +42 -0
  36. package/lib/core/util/JsonUtils.js +6 -0
  37. package/lib/features/ui-interaction/IUserInteractionEvent.js +1 -1
  38. package/lib/features/ui-interaction/Runtime.js +1 -1
  39. package/lib/features/ui-interaction/TouchMetaResolver.js +4 -3
  40. package/lib/next/Dynatrace.js +26 -1
  41. package/lib/next/events/EventPipeline.js +48 -31
  42. package/lib/next/events/HttpRequestEventData.js +3 -3
  43. package/lib/next/events/interface/HttpRequestEventDataTypes.js +2 -0
  44. package/lib/next/events/modifier/ModifyEventValidation.js +79 -51
  45. package/lib/next/events/spec/EventSpecContstants.js +1 -1
  46. package/lib/next/userAction/NullUserAction.js +15 -0
  47. package/lib/next/userAction/UserAction.js +2 -0
  48. package/lib/next/userAction/UserActionConfiguration.js +10 -0
  49. package/lib/next/userAction/UserActionImpl.js +70 -0
  50. package/lib/next/util/TraceContextUtils.js +8 -14
  51. package/lib/next/util/Utils.js +13 -0
  52. package/package.json +11 -6
  53. package/public.js +3 -1
  54. package/react-native-dynatrace.podspec +10 -3
  55. package/scripts/Config.js +22 -4
  56. package/scripts/DebugFlag.js +20 -0
  57. package/scripts/FileOperationHelper.js +15 -39
  58. package/scripts/Logger.js +2 -3
  59. package/scripts/core/InstrumentCall.js +110 -81
  60. package/scripts/util/CustomArgumentUtil.js +8 -6
  61. package/src/lib/core/interface/NativeDynatraceBridge.ts +34 -1
  62. package/types.d.ts +187 -9
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 21+
27
- * Gradle version 7.0+ ([How to update?](#updating-to-gradle-7))
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 11
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 12
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.339.1.1004
39
- * iOS Agent: 8.339.1.1011
38
+ * Android Agent: 8.343.1.1038
39
+ * iOS Agent: 8.343.1.1007
40
40
 
41
41
  ## Quick Setup
42
42
 
@@ -1030,9 +1030,9 @@ This allows you to preserve interaction analytics while avoiding exposure of sen
1030
1030
 
1031
1031
  Each captured interaction produces an event describing what the user touched and where. The event includes:
1032
1032
 
1033
- - **Element name** — the resolved label of the touched element, derived from its visible text, accessibility label, component name, or test ID.
1034
- - **Component type** — the type of the UI component that was touched (e.g. `Pressable`).
1035
- - **Element path** — a stable path through the component tree that uniquely identifies the element (e.g. `App/View[1]/Pressable[1]`).
1033
+ - **Detected name** — the resolved label of the touched element, derived from its visible text, accessibility label, component name, or test ID.
1034
+ - **Component** — the type of the UI component that was touched (e.g. `Pressable`).
1035
+ - **Component Path** — a stable path through the component tree that uniquely identifies the element (e.g. `App/View[1]/Pressable[1]`).
1036
1036
  - **Interaction type** — how the user interacted (e.g. `touch`).
1037
1037
  - **Position** — the screen coordinates where the touch occurred.
1038
1038
 
@@ -1047,17 +1047,79 @@ If masking is active for an interaction, the event still contains the same struc
1047
1047
  "characteristics.has_user_interaction": true,
1048
1048
  "ui_element.detected_name": "LoginButton",
1049
1049
  "ui_element.components": ["Pressable"],
1050
- "ui_element.id": "App/View[1]/Pressable[1]",
1050
+ "ui_element.id": "App/View/Pressable",
1051
1051
  "ui_element.name_origin": "component",
1052
- "interaction.name": "touch",
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"],
1056
- "ui_element.responder.id": "App/View[1]/Pressable[1]",
1056
+ "ui_element.responder.id": "App/View[/Pressable",
1057
1057
  "ui_element.responder.name_origin": "component"
1058
1058
  }
1059
1059
  ```
1060
1060
 
1061
+ #### Custom Names for Components
1062
+
1063
+ You can assign custom names to your components. A custom name affects both the **Component** and the **Component Path** described above. There are two ways to do this, depending on how the component is used.
1064
+
1065
+ ##### 1. As a JSX prop (`dtActionName`)
1066
+
1067
+ Use this when you render the component as JSX. Pass `dtActionName` as a prop, and it overrides the auto-detected name for that specific usage:
1068
+
1069
+ ```tsx
1070
+ <TouchableOpacity dtActionName="Checkout Button" onPress={onCheckout}>
1071
+ <Text dtActionName="Checkout Text">Buy now</Text>
1072
+ </TouchableOpacity>
1073
+ ```
1074
+
1075
+ Touching it produces:
1076
+
1077
+ ```json
1078
+ "ui_element.components": ["Checkout Text"],
1079
+ "ui_element.id": ".../Checkout Button/Checkout Text"
1080
+ ```
1081
+
1082
+ ##### 2. As a static property (`Component.dtActionName`)
1083
+
1084
+ Use this when the component is **not** rendered as JSX, so the prop approach above is not available — for example, a screen component you define once and hand to a navigator by reference. Set `dtActionName` directly on the component. This affects **every** place the component is used at once:
1085
+
1086
+ ```tsx
1087
+ import { NavigationContainer } from '@react-navigation/native';
1088
+ import { createDrawerNavigator } from '@react-navigation/drawer';
1089
+
1090
+ const Drawer = createDrawerNavigator();
1091
+
1092
+ function ProfileScreen() {
1093
+ return (/* ... */);
1094
+ }
1095
+
1096
+ // Applies to all usages of ProfileScreen below
1097
+ ProfileScreen.dtActionName = 'User Profile';
1098
+
1099
+ function App() {
1100
+ return (
1101
+ <NavigationContainer>
1102
+ <Drawer.Navigator>
1103
+ {/* Both screens reference the same component,
1104
+ so both pick up the 'User Profile' name */}
1105
+ <Drawer.Screen name="Me" component={ProfileScreen} />
1106
+ <Drawer.Screen name="Account" component={ProfileScreen} />
1107
+ </Drawer.Navigator>
1108
+ </NavigationContainer>
1109
+ );
1110
+ }
1111
+ ```
1112
+
1113
+ Touches inside one of the 2 screens now produce:
1114
+
1115
+ ```json
1116
+ "ui_element.id": ".../User Profile/..."
1117
+ ```
1118
+
1119
+ > **Note:** The static property applies to *all* uses of the component. If you need a different name per usage, render the component as JSX and use the `dtActionName` prop (option 1) instead.
1120
+ >
1121
+ > **Note:** The static property has no effect when a component is rendered as JSX. In that case the name is taken from either the `dtActionName` prop passed to it (option 1) or the name of the JSX component.
1122
+
1061
1123
  ### React Native Symbolication
1062
1124
 
1063
1125
  Dynatrace can automatically symbolicate JavaScript stack traces captured by the plugin using sourcemaps. This allows you to view human-readable file names, line numbers, and column information in your crash reports.
@@ -1098,6 +1160,7 @@ npx instrumentDynatrace [optional: config=... gradle=... plist=...]
1098
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.
1099
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.
1100
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.
1101
1164
 
1102
1165
  If `plist=...` is not provided, the plugin tries to resolve `Info.plist` automatically in this order:
1103
1166
 
@@ -1126,6 +1189,22 @@ Our scripts assumes that the usual React Native project structure is given. The
1126
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.
1127
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.
1128
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
+
1129
1208
  Examples:
1130
1209
 
1131
1210
  * React Native v0.60.1 or newer:
@@ -1242,8 +1321,9 @@ For more information regarding the differences in the react native versions, ple
1242
1321
  ## Structure of the `dynatrace.js` file
1243
1322
  The configuration is structured in the following way:
1244
1323
 
1245
- ```ts
1246
- module.exports = {
1324
+ ```js
1325
+ /** @type {import('@dynatrace/react-native-plugin').DynatraceUserConfiguration} */
1326
+ const config = {
1247
1327
  react : {
1248
1328
  // Configuration for React Native instrumentation
1249
1329
  },
@@ -1254,8 +1334,31 @@ module.exports = {
1254
1334
  // Configuration for iOS auto instrumentation
1255
1335
  }
1256
1336
  };
1337
+ module.exports = config;
1338
+ ```
1339
+
1340
+ Thanks to the `@type` annotation above, your editor can provide autocomplete and type checking for the configuration. To ensure this works, enable `checkJs` in your `tsconfig.json`:
1341
+
1342
+ ```json
1343
+ {
1344
+ "compilerOptions": {
1345
+ "checkJs": true
1346
+ }
1347
+ }
1348
+ ```
1349
+
1350
+ If your `tsconfig.json` uses an `include` array, make sure `dynatrace.config.js` is listed — TypeScript ignores files not matched by `include`, even when `checkJs` is enabled:
1351
+
1352
+ ```json
1353
+ {
1354
+ "include": ["**/*.ts", "**/*.tsx", "dynatrace.config.js"]
1355
+ }
1257
1356
  ```
1258
1357
 
1358
+
1359
+
1360
+
1361
+
1259
1362
  ### Manual Startup Counterparts
1260
1363
 
1261
1364
  Here is a list of all the counterparts for the options that can be used with a manual startup. Below in the counterparts table you will find an example configuration block for both Android and iOS.
@@ -1339,15 +1442,13 @@ Enables or disables the UI interaction (user interaction) feature. Set to `false
1339
1442
  #### Error Handler
1340
1443
 
1341
1444
  ```js
1342
- module.exports = {
1343
- // ...
1344
- react: {
1345
- errorHandler: {
1346
- enabled: true,
1347
- reportFatalErrorAsCrash: true,
1348
- },
1349
- }
1350
- };
1445
+ react: {
1446
+ errorHandler: {
1447
+ enabled: true,
1448
+ reportFatalErrorAsCrash: true,
1449
+ },
1450
+ }
1451
+
1351
1452
  ```
1352
1453
 
1353
1454
  The `enabled` property activates our Error/Crash handler which will insert our handler into your React Native application. This is true per default.
@@ -1987,6 +2088,31 @@ If you are struggling with a problem, submit a support ticket to Dynatrace (supp
1987
2088
  <br/><br/>
1988
2089
  ## Changelog
1989
2090
 
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.
2104
+
2105
+ 2.341.1
2106
+ * Updated Android (8.341.1.1004) & iOS Agent (8.341.1.1010)
2107
+ * Added support for using `dtActionName` prop to customize User Interaction component names
2108
+ * Fixed Babel instrumentation not recording start time and duration for User Interaction actions
2109
+ * Fixed `React.Fragment` and `Fragment` components being incorrectly instrumented as User Interactions
2110
+ * Fixed `config=` build argument being silently ignored when the path contains `=` characters (e.g. `config=$PWD/dynatrace.config.js`) or is an absolute path expanded from `$PWD`
2111
+ * Added TypeScript typechecking and autocomplete support in `dynatrace.config.js`
2112
+ * Decoupled Session Replay automation from the core React Native SDK on Android; renamed automation components to `ReplayEventTrigger`
2113
+ * Updated `@babel/runtime` to latest major version
2114
+ * Fixed security vulnerabilities in `ws` and `brace-expansion` dependencies
2115
+
1990
2116
  2.339.1
1991
2117
  * Updated Android (8.339.1.1004) & iOS Agent (8.339.1.1011)
1992
2118
  * Enabled User Interaction Feature by default
@@ -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', 21)
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.339.1.1004'
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 -> buffer.add(
48
- AppStartMeasurement(
49
- AppStartMeasurementType.CONTENT_APPEARED.value,
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
+ }
@@ -25,6 +25,7 @@ internal class DynatraceConfigurationModule(reactContext: ReactApplicationContex
25
25
 
26
26
  init {
27
27
  setupInternalListener()
28
+ reactApplicationContext?.let { ScreenshotSelfMonitor.initialize(it) }
28
29
  }
29
30
 
30
31
  /**
@@ -47,7 +47,6 @@ class DynatraceRNBridgeImpl(
47
47
  private var hasInternalListener: Boolean = false
48
48
  private var listenersCount: Int = 0
49
49
 
50
-
51
50
  fun getConstants(): Map<String, Any> {
52
51
  return constants;
53
52
  }
@@ -309,6 +308,47 @@ class DynatraceRNBridgeImpl(
309
308
  HybridBridge.forwardAppStartEvent(JSONObject(DynatraceUtils.toHashMap(attributes)), appStartKeys.toArrayList() as ArrayList<String>)
310
309
  }
311
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
+
312
352
  fun startView(name: String) {
313
353
  Dynatrace.startView(name)
314
354
  }
@@ -1,6 +1,7 @@
1
1
  package com.dynatrace.android.agent
2
2
 
3
3
  import android.util.Log
4
+ import com.facebook.react.bridge.Dynamic
4
5
  import com.facebook.react.bridge.ReadableArray
5
6
  import com.facebook.react.bridge.ReadableMap
6
7
  import com.facebook.react.bridge.ReadableType
@@ -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
+ }