@dynatrace/react-native-plugin 2.341.1 → 2.345.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +674 -668
  2. package/android/build.gradle +16 -3
  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 -0
  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-runtime.gradle +17 -33
  12. package/files/plugin.gradle +1 -1
  13. package/instrumentation/BabelPluginDynatrace.js +1 -1
  14. package/instrumentation/libs/UserInteraction.js +21 -7
  15. package/instrumentation/libs/react-navigation/ReactNavigation.js +14 -58
  16. package/instrumentation/libs/withOnPressMonitoring.js +3 -54
  17. package/internal.d.ts +1 -0
  18. package/internal.js +3 -1
  19. package/ios/DTXScreenshotSelfMonitor.h +29 -0
  20. package/ios/DTXScreenshotSelfMonitor.mm +213 -0
  21. package/ios/DTXScreenshotSelfMonitorSwizzler.h +15 -0
  22. package/ios/DTXScreenshotSelfMonitorSwizzler.mm +68 -0
  23. package/ios/DynatraceRNBridge.h +6 -0
  24. package/ios/DynatraceRNBridge.mm +62 -0
  25. package/lib/core/Dynatrace.js +2 -0
  26. package/lib/core/configuration/ConfigurationHandler.js +0 -21
  27. package/lib/core/logging/LogMessages.js +42 -0
  28. package/lib/dynatrace-reporter.js +1 -30
  29. package/lib/dynatrace-transformer.js +6 -34
  30. package/lib/next/Dynatrace.js +26 -1
  31. package/lib/next/configuration/INativeRuntimeConfiguration.js +0 -1
  32. package/lib/next/configuration/RuntimeConfigurationObserver.js +1 -6
  33. package/lib/next/events/EventPipeline.js +48 -31
  34. package/lib/next/events/HttpRequestEventData.js +1 -1
  35. package/lib/next/events/spec/EventSpecContstants.js +1 -1
  36. package/lib/next/navigation/NavigationTracker.js +93 -0
  37. package/lib/next/navigation/useNavigationTracking.js +54 -0
  38. package/lib/next/userAction/NullUserAction.js +15 -0
  39. package/lib/next/userAction/UserActionConfiguration.js +10 -0
  40. package/lib/next/userAction/UserActionImpl.js +70 -0
  41. package/lib/next/util/TraceContextUtils.js +8 -14
  42. package/lib/next/util/Utils.js +13 -0
  43. package/package.json +10 -18
  44. package/public.js +5 -1
  45. package/react-native-dynatrace.podspec +10 -3
  46. package/scripts/Android.js +1 -20
  47. package/scripts/Config.js +17 -7
  48. package/scripts/Deprecations.js +18 -0
  49. package/scripts/Install.js +13 -0
  50. package/scripts/PathsConstants.js +0 -4
  51. package/scripts/Uninstall.js +0 -15
  52. package/scripts/core/InstrumentCall.js +14 -7
  53. package/scripts/util/DeprecationReport.js +24 -0
  54. package/src/lib/core/interface/NativeDynatraceBridge.ts +34 -1
  55. package/types.d.ts +236 -7
  56. package/instrumentation/DynatraceInstrumentation.js +0 -1
  57. package/instrumentation/libs/community/Picker.InstrInfo.js +0 -20
  58. package/instrumentation/libs/community/gesture-handler/Touchables.InstrInfo.js +0 -25
  59. package/instrumentation/libs/react-native/RefreshControl.InstrInfo.js +0 -12
  60. package/instrumentation/libs/react-native/Switch.InstrInfo.js +0 -12
  61. package/instrumentation/libs/react-native/Touchables.InstrInfo.js +0 -25
  62. package/instrumentation/parser/Babel.js +0 -60
  63. package/instrumentation/parser/ParserUtil.js +0 -11
  64. package/lib/features/ui-interaction/Config.js +0 -42
  65. package/lib/features/ui-interaction/IUserInteractionEvent.js +0 -16
  66. package/lib/features/ui-interaction/Plugin.Fragment.Test.js +0 -170
  67. package/lib/features/ui-interaction/Plugin.js +0 -289
  68. package/lib/features/ui-interaction/RootDetection.js +0 -51
  69. package/lib/features/ui-interaction/RootWrapper.js +0 -236
  70. package/lib/features/ui-interaction/Run.js +0 -38
  71. package/lib/features/ui-interaction/Runtime.js +0 -843
  72. package/lib/features/ui-interaction/TouchMetaResolver.js +0 -493
  73. package/lib/features/ui-interaction/Types.js +0 -14
  74. package/lib/metro/getSourceMapInfo.js +0 -26
  75. package/scripts/LineOffsetAnalyze.js +0 -25
  76. package/scripts/core/LineOffsetAnalyzeCall.js +0 -165
  77. package/scripts/util/SourceMapUtil.js +0 -68
  78. /package/{instrumentation/model/Reference.js → lib/next/userAction/UserAction.js} +0 -0
@@ -8,6 +8,14 @@ def isNewArchitectureEnabled() {
8
8
  return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
9
9
  }
10
10
 
11
+ // Build-time master switch for the screenshot self-monitoring (replay metrics) feature.
12
+ // Defaults to enabled; disable for a build with -PdynatraceReplayMetricsEnabled=false
13
+ // (or set dynatraceReplayMetricsEnabled=false in gradle.properties). When false, the monitor is
14
+ // never registered, regardless of the remote `replay_metrics_enabled` flag.
15
+ def isReplayMetricsBuildEnabled() {
16
+ return !project.hasProperty("dynatraceReplayMetricsEnabled") || project.dynatraceReplayMetricsEnabled == "true"
17
+ }
18
+
11
19
  buildscript {
12
20
  repositories {
13
21
  google()
@@ -21,7 +29,9 @@ buildscript {
21
29
  }
22
30
 
23
31
  apply plugin: 'com.android.library'
24
- apply plugin: 'org.jetbrains.kotlin.android'
32
+ if (!project.extensions.findByName('kotlin')) {
33
+ apply plugin: 'org.jetbrains.kotlin.android'
34
+ }
25
35
 
26
36
  if (isNewArchitectureEnabled()) {
27
37
  apply plugin: 'com.facebook.react'
@@ -43,12 +53,13 @@ android {
43
53
  }
44
54
 
45
55
  defaultConfig {
46
- minSdkVersion safeExtGet('minSdkVersion', 21)
56
+ minSdkVersion safeExtGet('minSdkVersion', 23)
47
57
  targetSdkVersion safeExtGet('targetSdkVersion', 31)
48
58
  versionCode 1
49
59
  versionName "1.0"
50
60
 
51
61
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
62
+ buildConfigField "boolean", "REPLAY_METRICS_ENABLED", isReplayMetricsBuildEnabled().toString()
52
63
  }
53
64
 
54
65
  lintOptions {
@@ -59,8 +70,10 @@ android {
59
70
  main {
60
71
  if (isNewArchitectureEnabled()) {
61
72
  java.srcDirs += ['src/new']
73
+ kotlin.srcDirs += ['src/new']
62
74
  } else {
63
75
  java.srcDirs += ['src/old']
76
+ kotlin.srcDirs += ['src/old']
64
77
  }
65
78
  }
66
79
  }
@@ -72,7 +85,7 @@ repositories {
72
85
  }
73
86
 
74
87
  dependencies {
75
- implementation 'com.dynatrace.agent:agent-android:8.341.1.1004'
88
+ implementation 'com.dynatrace.agent:agent-android:8.345.1.1003'
76
89
  implementation "com.facebook.react:react-native:${safeExtGet('reactNative', '+')}"
77
90
  }
78
91
 
@@ -1,6 +1,5 @@
1
1
  package com.dynatrace.android.agent
2
2
 
3
- import android.os.SystemClock
4
3
  import com.dynatrace.android.agent.model.AppStartMeasurement
5
4
  import com.dynatrace.android.agent.model.AppStartMeasurementType
6
5
  import com.facebook.react.bridge.Arguments
@@ -31,52 +30,31 @@ private val buffer: Queue<AppStartMeasurement> = ConcurrentLinkedQueue()
31
30
  * Setup to register all time points which we want to use to report correct app startup time.
32
31
  */
33
32
  fun setupAppStartListener() {
34
- val initTimestamp = System.currentTimeMillis() - SystemClock.elapsedRealtime();
35
33
  ReactMarker.addListener { name: ReactMarkerConstants?, _: String?, _: Int ->
36
34
  when (name) {
37
35
  ReactMarkerConstants.RELOAD -> {
38
36
  buffer.clear()
39
- buffer.add(
40
- AppStartMeasurement(
41
- AppStartMeasurementType.RELOAD.value,
42
- initTimestamp + SystemClock.elapsedRealtime()
43
- )
44
- )
37
+ buffer.add(AppStartMeasurement(AppStartMeasurementType.RELOAD.value, TimeLineProvider.getSystemTime()))
45
38
  }
46
39
 
47
- ReactMarkerConstants.CONTENT_APPEARED -> 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
  /**
@@ -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
  }
@@ -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
+ }
@@ -0,0 +1,124 @@
1
+ package com.dynatrace.android.agent
2
+
3
+ import android.app.Activity
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import android.os.SystemClock
7
+ import android.util.Log
8
+ import android.view.ViewTreeObserver
9
+ import java.lang.ref.WeakReference
10
+ import java.util.concurrent.atomic.AtomicBoolean
11
+
12
+ private const val TAG = "dtxScreenshotSelfMonitor"
13
+ private const val DEBOUNCE_MS = 500L
14
+ private const val MAX_DEBOUNCE_MS = 2000L
15
+
16
+ /**
17
+ * Self-monitoring listener that mirrors the Session Replay UI-change detection, but instead of
18
+ * capturing a screenshot it only increments the cross-platform screenshot counter via
19
+ * [HybridBridge.trackCrossPlatformScreenshot].
20
+ *
21
+ * The purpose is to estimate how many screenshots the Session Replay UI-change listener *would*
22
+ * take in production, without shipping the (not yet published) Session Replay package.
23
+ *
24
+ * It observes [ViewTreeObserver.OnDrawListener] on the current activity's decor view and debounces
25
+ * bursts of draws (a settled UI is reported once). Both [lastDrawTime] and [firstDrawTime] are only
26
+ * touched on the main thread (`onDraw` and the Handler-posted [captureRunnable]).
27
+ */
28
+ internal class UIChangeScreenshotListener : ViewTreeObserver.OnDrawListener {
29
+
30
+ private val pending = AtomicBoolean(false)
31
+ private val handler = Handler(Looper.getMainLooper())
32
+ private var registeredActivity: WeakReference<Activity>? = null
33
+
34
+ private var lastDrawTime = 0L
35
+ private var firstDrawTime = 0L
36
+
37
+ private val captureRunnable = Runnable { performCapture() }
38
+
39
+ override fun onDraw() {
40
+ val now = SystemClock.uptimeMillis()
41
+ lastDrawTime = now
42
+ if (pending.compareAndSet(false, true)) {
43
+ firstDrawTime = now
44
+ handler.postDelayed(captureRunnable, DEBOUNCE_MS)
45
+ }
46
+ }
47
+
48
+ fun registerOn(activity: Activity): Boolean {
49
+ if (registeredActivity?.get() === activity) return true
50
+
51
+ // Clean up previous registration if switching activities
52
+ unregister()
53
+
54
+ val decorView = activity.window?.decorView
55
+ if (decorView == null) {
56
+ Log.w(TAG, "Cannot register: decorView is null")
57
+ return false
58
+ }
59
+ val observer = decorView.viewTreeObserver
60
+ if (!observer.isAlive) {
61
+ Log.w(TAG, "Cannot register: ViewTreeObserver is not alive")
62
+ return false
63
+ }
64
+
65
+ observer.addOnDrawListener(this)
66
+ registeredActivity = WeakReference(activity)
67
+ Log.d(TAG, "Registered OnDrawListener on DecorView")
68
+ return true
69
+ }
70
+
71
+ fun unregister() {
72
+ val activity = registeredActivity?.get()
73
+ if (activity != null) {
74
+ try {
75
+ val decorView = activity.window?.decorView
76
+ val observer = decorView?.viewTreeObserver
77
+ if (observer != null && observer.isAlive) {
78
+ observer.removeOnDrawListener(this)
79
+ }
80
+ } catch (e: IllegalStateException) {
81
+ Log.w(TAG, "Expected cleanup issue removing OnDrawListener", e)
82
+ } catch (e: Exception) {
83
+ Log.e(TAG, "Unexpected error removing OnDrawListener", e)
84
+ }
85
+ }
86
+ registeredActivity = null
87
+ dispose()
88
+ }
89
+
90
+ private fun performCapture() {
91
+ if (registeredActivity?.get() == null) {
92
+ // Leave pending=true so onDraw won't schedule further captures
93
+ Log.w(TAG, "Activity reference lost (GC'd) — capture abandoned")
94
+ handler.removeCallbacks(captureRunnable)
95
+ return
96
+ }
97
+
98
+ val now = SystemClock.uptimeMillis()
99
+ val elapsed = now - lastDrawTime
100
+ val sinceFirst = now - firstDrawTime
101
+
102
+ // Reschedule if UI still active, unless max wait exceeded
103
+ if (elapsed < DEBOUNCE_MS && sinceFirst < MAX_DEBOUNCE_MS) {
104
+ val remaining = DEBOUNCE_MS - elapsed
105
+ Log.d(TAG, "UI still active, rescheduling in ${remaining}ms")
106
+ handler.postDelayed(captureRunnable, remaining)
107
+ return
108
+ }
109
+
110
+ pending.set(false)
111
+
112
+ try {
113
+ Log.d(TAG, "Tracking cross-platform screenshot (self-monitoring)")
114
+ HybridBridge.trackCrossPlatformScreenshot()
115
+ } catch (e: Exception) {
116
+ Log.e(TAG, "trackCrossPlatformScreenshot failed during UI change capture", e)
117
+ }
118
+ }
119
+
120
+ fun dispose() {
121
+ handler.removeCallbacks(captureRunnable)
122
+ pending.set(false)
123
+ }
124
+ }
@@ -179,6 +179,39 @@ class DynatraceRNBridge(
179
179
  impl.forwardAppStartEvent(attributes, appStartKeys)
180
180
  }
181
181
 
182
+ override fun setAutomaticUserActionDetection(enabled: Boolean) {
183
+ impl.setAutomaticUserActionDetection(enabled)
184
+ }
185
+
186
+ override fun createUserAction(
187
+ actionId: String,
188
+ name: String,
189
+ completeAutomatically: Boolean,
190
+ properties: ReadableMap?
191
+ ) {
192
+ impl.createUserAction(actionId, name, completeAutomatically, properties)
193
+ }
194
+
195
+ override fun addEventStringPropertyToUserAction(actionId: String, key: String, value: String) {
196
+ impl.addEventStringPropertyToUserAction(actionId, key, value)
197
+ }
198
+
199
+ override fun addEventDoublePropertyToUserAction(actionId: String, key: String, value: Double) {
200
+ impl.addEventDoublePropertyToUserAction(actionId, key, value)
201
+ }
202
+
203
+ override fun addEventBooleanPropertyToUserAction(actionId: String, key: String, value: Boolean) {
204
+ impl.addEventBooleanPropertyToUserAction(actionId, key, value)
205
+ }
206
+
207
+ override fun completeUserAction(actionId: String) {
208
+ impl.completeUserAction(actionId)
209
+ }
210
+
211
+ override fun setCompleteUserActionAutomatically(actionId: String, enabled: Boolean) {
212
+ impl.setCompleteUserActionAutomatically(actionId, enabled)
213
+ }
214
+
182
215
  override fun startView(name: String) {
183
216
  impl.startView(name)
184
217
  }
@@ -197,6 +197,46 @@ class DynatraceRNBridge(
197
197
  impl.forwardAppStartEvent(attributes, appStartKeys)
198
198
  }
199
199
 
200
+ @ReactMethod
201
+ fun setAutomaticUserActionDetection(enabled: Boolean) {
202
+ impl.setAutomaticUserActionDetection(enabled)
203
+ }
204
+
205
+ @ReactMethod
206
+ fun createUserAction(
207
+ actionId: String,
208
+ name: String,
209
+ completeAutomatically: Boolean,
210
+ properties: ReadableMap?
211
+ ) {
212
+ impl.createUserAction(actionId, name, completeAutomatically, properties)
213
+ }
214
+
215
+ @ReactMethod
216
+ fun addEventStringPropertyToUserAction(actionId: String, key: String, value: String) {
217
+ impl.addEventStringPropertyToUserAction(actionId, key, value)
218
+ }
219
+
220
+ @ReactMethod
221
+ fun addEventDoublePropertyToUserAction(actionId: String, key: String, value: Double) {
222
+ impl.addEventDoublePropertyToUserAction(actionId, key, value)
223
+ }
224
+
225
+ @ReactMethod
226
+ fun addEventBooleanPropertyToUserAction(actionId: String, key: String, value: Boolean) {
227
+ impl.addEventBooleanPropertyToUserAction(actionId, key, value)
228
+ }
229
+
230
+ @ReactMethod
231
+ fun completeUserAction(actionId: String) {
232
+ impl.completeUserAction(actionId)
233
+ }
234
+
235
+ @ReactMethod
236
+ fun setCompleteUserActionAutomatically(actionId: String, enabled: Boolean) {
237
+ impl.setCompleteUserActionAutomatically(actionId, enabled)
238
+ }
239
+
200
240
  @ReactMethod
201
241
  fun startView(name: String) {
202
242
  impl.startView(name)
@@ -1,33 +1,17 @@
1
- import org.gradle.process.ExecOperations
2
- import javax.inject.Inject
3
-
4
- abstract class RunLineOffsetTask extends DefaultTask {
5
- @Internal
6
- abstract DirectoryProperty getWorkingDirectory()
7
-
8
- @Inject
9
- abstract ExecOperations getExecOperations()
10
-
11
- @TaskAction
12
- void run() {
13
- execOperations.exec {
14
- workingDir workingDirectory.get().asFile
15
- if (System.getProperty('os.name').toLowerCase().contains('windows')) {
16
- commandLine 'cmd', '/c', 'npx', 'lineOffsetDynatrace'
17
- } else {
18
- commandLine 'npx', 'lineOffsetDynatrace'
19
- }
20
- }
21
- }
22
- }
23
-
24
- android.applicationVariants.all { variant ->
25
- if (variant.buildType.name == "release") {
26
- def lineOffsetTask = tasks.register("runLineOffset${variant.name.capitalize()}", RunLineOffsetTask) {
27
- workingDirectory = layout.projectDirectory
28
- }
29
- variant.mergeAssetsProvider.configure { task ->
30
- task.dependsOn(lineOffsetTask)
31
- }
32
- }
33
- }
1
+ // Intentionally left empty. DO NOT DELETE THIS FILE.
2
+ //
3
+ // Apps instrumented with an older version of this plugin have the following
4
+ // line hard-coded in their android/app/build.gradle:
5
+ //
6
+ // apply from: "../../node_modules/@dynatrace/react-native-plugin/files/plugin-runtime.gradle"
7
+ //
8
+ // That line lives in the customer's own build.gradle, so we cannot reliably
9
+ // remove it on upgrade. If this file were missing, the `apply from:` above
10
+ // would fail the Gradle build for every app that still has that line.
11
+ //
12
+ // Previously this script registered a release-build Gradle task that ran
13
+ // `npx lineOffsetDynatrace` for line-offset analysis. That is no longer needed:
14
+ // auto-instrumentation moved to a Babel plugin and capturing moved to native,
15
+ // so the script is now a no-op kept purely for backward compatibility.
16
+ //
17
+ // New installs no longer add the `apply from:` line at all.
@@ -1,4 +1,4 @@
1
1
  // TEMPLATE: plugin-gradle.template
2
2
  dependencies {
3
- classpath 'com.dynatrace.tools.android:gradle-plugin:8.341.1.1004'
3
+ classpath 'com.dynatrace.tools.android:gradle-plugin:8.345.1.1003'
4
4
  }