@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.
- package/README.md +149 -23
- 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 -1
- 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/BabelPluginDynatrace.js +1 -1
- package/instrumentation/DynatraceInstrumentation.js +1 -1
- package/instrumentation/jsx/CreateElement.js +5 -9
- package/instrumentation/jsx/ElementHelper.js +5 -6
- package/instrumentation/jsx/JsxDevRuntime.js +33 -31
- package/instrumentation/jsx/JsxRuntime.js +30 -30
- package/instrumentation/jsx/JsxRuntimeHelpers.js +14 -0
- package/instrumentation/jsx/components/ClassComponent.js +4 -8
- package/instrumentation/jsx/components/ComponentUtil.js +24 -25
- package/instrumentation/libs/UserInteraction.js +27 -24
- package/instrumentation/libs/react-native/RefreshControl.js +5 -9
- package/instrumentation/libs/withOnPressMonitoring.js +66 -57
- 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/DynatraceAction.js +1 -3
- package/lib/core/DynatraceInternal.js +38 -40
- package/lib/core/ErrorHandler.js +41 -21
- package/lib/core/NullAction.js +1 -3
- package/lib/core/logging/LogMessages.js +42 -0
- package/lib/core/util/JsonUtils.js +6 -0
- package/lib/features/ui-interaction/IUserInteractionEvent.js +1 -1
- package/lib/features/ui-interaction/Runtime.js +1 -1
- package/lib/features/ui-interaction/TouchMetaResolver.js +4 -3
- package/lib/next/Dynatrace.js +26 -1
- package/lib/next/events/EventPipeline.js +48 -31
- package/lib/next/events/HttpRequestEventData.js +3 -3
- package/lib/next/events/interface/HttpRequestEventDataTypes.js +2 -0
- package/lib/next/events/modifier/ModifyEventValidation.js +79 -51
- 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 +11 -6
- package/public.js +3 -1
- package/react-native-dynatrace.podspec +10 -3
- package/scripts/Config.js +22 -4
- package/scripts/DebugFlag.js +20 -0
- package/scripts/FileOperationHelper.js +15 -39
- package/scripts/Logger.js +2 -3
- package/scripts/core/InstrumentCall.js +110 -81
- package/scripts/util/CustomArgumentUtil.js +8 -6
- package/src/lib/core/interface/NativeDynatraceBridge.ts +34 -1
- 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
|
|
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
|
|
|
@@ -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
|
-
- **
|
|
1034
|
-
- **Component
|
|
1035
|
-
- **
|
|
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
|
|
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"],
|
|
1056
|
-
"ui_element.responder.id": "App/View[
|
|
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
|
-
```
|
|
1246
|
-
|
|
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
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|