@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
|
@@ -18,14 +18,10 @@ class DynatraceClassComponent extends react_1.Component {
|
|
|
18
18
|
if (!ConfigurationHandler_1.ConfigurationHandler.isConfigurationAvailable()) {
|
|
19
19
|
this.logger.info(LogMessages_1.LogMessage.COMPONENT_NOT_STARTED);
|
|
20
20
|
}
|
|
21
|
-
else
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
? 'Render '
|
|
26
|
-
: 'Update ';
|
|
27
|
-
this.internalAction = Dynatrace_1.Dynatrace.enterAutoAction(actionPrefix + this.wrappingName);
|
|
28
|
-
}
|
|
21
|
+
else if (ConfigurationHandler_1.ConfigurationHandler.isLifecycleUpdateEnabled() ||
|
|
22
|
+
!this.componentMounted) {
|
|
23
|
+
const actionPrefix = this.componentMounted ? 'Update ' : 'Render ';
|
|
24
|
+
this.internalAction = Dynatrace_1.Dynatrace.enterAutoAction(actionPrefix + this.wrappingName);
|
|
29
25
|
}
|
|
30
26
|
return this.props.children;
|
|
31
27
|
}
|
|
@@ -1,34 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isDynatraceIgnored = exports.isDynatraceNaming = exports.getNameFromComponent = void 0;
|
|
4
|
-
const
|
|
5
|
-
if (props
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
else if (child.type !== undefined) {
|
|
18
|
-
if (child.type.displayName !== undefined) {
|
|
19
|
-
return child.type.displayName;
|
|
20
|
-
}
|
|
21
|
-
else if (child.type._dtInfo !== undefined) {
|
|
22
|
-
return child.type._dtInfo.name;
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
return child.type.name;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
4
|
+
const getChildFromProperties = (props) => {
|
|
5
|
+
if (Array.isArray(props.children) && props.children.length === 1) {
|
|
6
|
+
return props.children[0];
|
|
7
|
+
}
|
|
8
|
+
return props.children;
|
|
9
|
+
};
|
|
10
|
+
const getNameFromChild = (child) => {
|
|
11
|
+
if (child.props !== undefined && child.props.dtActionName !== undefined) {
|
|
12
|
+
return child.props.dtActionName;
|
|
13
|
+
}
|
|
14
|
+
if (child.type === undefined) {
|
|
15
|
+
return 'Undefined Name';
|
|
28
16
|
}
|
|
29
|
-
|
|
17
|
+
if (child.type.displayName !== undefined) {
|
|
18
|
+
return child.type.displayName;
|
|
19
|
+
}
|
|
20
|
+
if (child.type._dtInfo !== undefined) {
|
|
21
|
+
return child.type._dtInfo.name;
|
|
22
|
+
}
|
|
23
|
+
return child.type.name;
|
|
24
|
+
};
|
|
25
|
+
const getNameFromComponent = (props) => {
|
|
26
|
+
if (props === undefined || props.children === undefined) {
|
|
30
27
|
return 'Undefined Name';
|
|
31
28
|
}
|
|
29
|
+
const child = getChildFromProperties(props);
|
|
30
|
+
return getNameFromChild(child);
|
|
32
31
|
};
|
|
33
32
|
exports.getNameFromComponent = getNameFromComponent;
|
|
34
33
|
const isDynatraceNaming = (properties) => typeof properties === 'object' &&
|
|
@@ -5,23 +5,25 @@ const React = require("react");
|
|
|
5
5
|
const react_native_1 = require("react-native");
|
|
6
6
|
const ConfigurationHandler_1 = require("../../lib/core/configuration/ConfigurationHandler");
|
|
7
7
|
const EventPipeline_1 = require("../../lib/next/events/EventPipeline");
|
|
8
|
+
const EventTimestamp_1 = require("../../lib/next/events/EventTimestamp");
|
|
9
|
+
const TimestampProvider_1 = require("../../lib/next/provider/TimestampProvider");
|
|
8
10
|
const TOUCH_FLUSH_DELAY = 250;
|
|
9
11
|
let pendingTouch = null;
|
|
10
12
|
const getFiberPath = (fiber) => {
|
|
11
|
-
var _a, _b, _c;
|
|
13
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
12
14
|
let anyParentMasked = false;
|
|
13
15
|
let path = [];
|
|
14
16
|
let lastDtName;
|
|
15
17
|
while (fiber != null) {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
path = [
|
|
21
|
-
lastDtName =
|
|
18
|
+
const dtNameFromComponent = (_b = (_a = fiber.type) === null || _a === void 0 ? void 0 : _a.dtActionName) !== null && _b !== void 0 ? _b : (_c = fiber.type) === null || _c === void 0 ? void 0 : _c.dtName;
|
|
19
|
+
const dtNameFromProps = (_e = (_d = fiber.memoizedProps) === null || _d === void 0 ? void 0 : _d.dtActionName) !== null && _e !== void 0 ? _e : (_f = fiber.memoizedProps) === null || _f === void 0 ? void 0 : _f.dtName;
|
|
20
|
+
const dtName = dtNameFromProps !== null && dtNameFromProps !== void 0 ? dtNameFromProps : dtNameFromComponent;
|
|
21
|
+
if (dtName && dtName !== lastDtName) {
|
|
22
|
+
path = [dtName, ...path];
|
|
23
|
+
lastDtName = dtName;
|
|
22
24
|
}
|
|
23
25
|
anyParentMasked =
|
|
24
|
-
anyParentMasked || ((
|
|
26
|
+
anyParentMasked || ((_g = fiber.memoizedProps) === null || _g === void 0 ? void 0 : _g.dtMask) == true;
|
|
25
27
|
fiber = fiber.return;
|
|
26
28
|
}
|
|
27
29
|
return { path, anyParentMasked };
|
|
@@ -58,20 +60,20 @@ const getHostComponentInfo = (event) => {
|
|
|
58
60
|
const id = path.join('/');
|
|
59
61
|
const leaf = path[path.length - 1];
|
|
60
62
|
const components = [leaf];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
let nameOrigin;
|
|
64
|
+
let detectedName;
|
|
65
|
+
if (fiber.type !== 'RCTText') {
|
|
66
|
+
nameOrigin = 'component';
|
|
67
|
+
detectedName = (_b = fiber.memoizedProps.accessibilityLabel) !== null && _b !== void 0 ? _b : leaf;
|
|
68
|
+
}
|
|
69
|
+
else if (anyParentMasked) {
|
|
70
|
+
nameOrigin = 'masked';
|
|
71
|
+
detectedName = '***';
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
nameOrigin = 'text';
|
|
75
|
+
detectedName = flatten(fiber.memoizedProps.children);
|
|
76
|
+
}
|
|
75
77
|
return { id, components, detectedName, nameOrigin };
|
|
76
78
|
};
|
|
77
79
|
const flushTouch = (responderComponentInfo) => {
|
|
@@ -80,12 +82,13 @@ const flushTouch = (responderComponentInfo) => {
|
|
|
80
82
|
const { touchedComponentInfo, positions, timeoutId } = pendingTouch;
|
|
81
83
|
clearTimeout(timeoutId);
|
|
82
84
|
pendingTouch = null;
|
|
83
|
-
const
|
|
85
|
+
const eventTimestamp = new EventTimestamp_1.EventTimestamp(TimestampProvider_1.defaultTimestampProvider);
|
|
86
|
+
const event = Object.assign(Object.assign({ 'characteristics.has_user_interaction': true, 'ui_element.detected_name': touchedComponentInfo.detectedName, 'ui_element.components': touchedComponentInfo.components, 'ui_element.id': touchedComponentInfo.id, 'interaction.type': 'touch', positions, 'ui_element.name_origin': touchedComponentInfo.nameOrigin }, (responderComponentInfo && {
|
|
84
87
|
'ui_element.responder.detected_name': responderComponentInfo.detectedName,
|
|
85
88
|
'ui_element.responder.components': responderComponentInfo.components,
|
|
86
89
|
'ui_element.responder.id': responderComponentInfo.id,
|
|
87
90
|
'ui_element.responder.name_origin': responderComponentInfo.nameOrigin,
|
|
88
|
-
}));
|
|
91
|
+
})), eventTimestamp.getEventTimeInfo());
|
|
89
92
|
EventPipeline_1.EventPipeline.insertEvent(event);
|
|
90
93
|
};
|
|
91
94
|
const onTouch = (e) => {
|
|
@@ -4,12 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
exports.RefreshControl = void 0;
|
|
5
5
|
const react_native_1 = require("react-native");
|
|
6
6
|
const Types_1 = require("../../model/Types");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
},
|
|
13
|
-
_a._dtInfo = { type: Types_1.Types.RefreshControl },
|
|
14
|
-
_a);
|
|
15
|
-
}
|
|
7
|
+
exports.RefreshControl = typeof react_native_1.RefreshControl === 'object'
|
|
8
|
+
? Object.assign({ _dtInfo: { type: Types_1.Types.RefreshControl } }, react_native_1.RefreshControl) : (_a = class RefreshControl extends react_native_1.RefreshControl {
|
|
9
|
+
},
|
|
10
|
+
_a._dtInfo = { type: Types_1.Types.RefreshControl },
|
|
11
|
+
_a);
|
|
@@ -32,11 +32,13 @@ function withOnPressMonitoring(Component, fallbackName) {
|
|
|
32
32
|
}
|
|
33
33
|
exports.withOnPressMonitoring = withOnPressMonitoring;
|
|
34
34
|
const dtGetUiBridge = () => {
|
|
35
|
+
var _a;
|
|
35
36
|
const g = globalThis;
|
|
36
|
-
return g
|
|
37
|
+
return (_a = g === null || g === void 0 ? void 0 : g.__DT_UII_BRIDGE) !== null && _a !== void 0 ? _a : null;
|
|
37
38
|
};
|
|
38
39
|
const dtBuildPressPath = (startPath, touchableName) => {
|
|
39
|
-
|
|
40
|
+
var _a;
|
|
41
|
+
const base = (_a = startPath === null || startPath === void 0 ? void 0 : startPath.toString()) !== null && _a !== void 0 ? _a : '';
|
|
40
42
|
if (!base)
|
|
41
43
|
return `Touchable(${touchableName})`;
|
|
42
44
|
const cut1 = base.lastIndexOf('/Text');
|
|
@@ -45,65 +47,72 @@ const dtBuildPressPath = (startPath, touchableName) => {
|
|
|
45
47
|
const parent = cut > 0 ? base.slice(0, cut) : base;
|
|
46
48
|
return parent + `/Touchable(${touchableName})`;
|
|
47
49
|
};
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
50
|
+
const registerPressOnBridge = (args, touchableName, isButtonLike, buttonLabel) => {
|
|
51
|
+
const bridge = dtGetUiBridge();
|
|
52
|
+
if (!bridge || typeof bridge.registerPress !== 'function') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const e = args === null || args === void 0 ? void 0 : args[0];
|
|
57
|
+
const ne = e === null || e === void 0 ? void 0 : e.nativeEvent;
|
|
58
|
+
const x = Number(ne === null || ne === void 0 ? void 0 : ne.pageX);
|
|
59
|
+
const y = Number(ne === null || ne === void 0 ? void 0 : ne.pageY);
|
|
60
|
+
const pos = Number.isFinite(x) && Number.isFinite(y) ? { x, y } : undefined;
|
|
61
|
+
const startPath = typeof bridge.getStartPath === 'function'
|
|
62
|
+
? bridge.getStartPath()
|
|
63
|
+
: null;
|
|
64
|
+
const nameForBridge = isButtonLike
|
|
65
|
+
? buttonLabel || touchableName
|
|
66
|
+
: touchableName;
|
|
67
|
+
bridge.registerPress({
|
|
68
|
+
path: dtBuildPressPath(startPath, nameForBridge),
|
|
69
|
+
elementName: nameForBridge,
|
|
70
|
+
pos,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
const errorMsg = e instanceof Error ? e.stack || e.message : String(e);
|
|
75
|
+
Logger.debug(LogMessages_1.LogMessage.REGISTER_PRESS_HOOK_FAILED, {
|
|
76
|
+
errorMsg,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const notifyPressFromEvent = (args) => {
|
|
81
|
+
if (args[0] && typeof args[0] === 'object' && 'nativeEvent' in args[0]) {
|
|
82
|
+
(0, UserInteraction_1.notifyPress)(args[0]);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const invokeOnPressWithoutConfiguration = (onPress, args) => {
|
|
86
|
+
Logger.info(LogMessages_1.LogMessage.TOUCH_NOT_STARTED);
|
|
87
|
+
onPress(...args);
|
|
88
|
+
};
|
|
89
|
+
const invokeOnPressWithAction = (onPress, args, touchableName) => {
|
|
90
|
+
const action = Dynatrace_1.Dynatrace.enterAutoAction(`Touch on ${touchableName}`);
|
|
91
|
+
let isSync = true;
|
|
92
|
+
try {
|
|
93
|
+
const returnValue = onPress(...args);
|
|
94
|
+
if (returnValue instanceof Promise) {
|
|
95
|
+
isSync = false;
|
|
96
|
+
void Promise.resolve(returnValue).finally(() => {
|
|
97
|
+
action.leaveAction();
|
|
98
|
+
});
|
|
79
99
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (isSync) {
|
|
103
|
+
action.leaveAction();
|
|
84
104
|
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const wrapOnPress = (onPress, touchableName, isButtonLike, buttonLabel) => {
|
|
108
|
+
return (...args) => {
|
|
109
|
+
registerPressOnBridge(args, touchableName, isButtonLike, buttonLabel);
|
|
110
|
+
notifyPressFromEvent(args);
|
|
85
111
|
if (!ConfigurationHandler_1.ConfigurationHandler.isConfigurationAvailable()) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
const action = Dynatrace_1.Dynatrace.enterAutoAction(`Touch on ${touchableName}`);
|
|
91
|
-
let isSync = true;
|
|
92
|
-
try {
|
|
93
|
-
const returnValue = onPress(...args);
|
|
94
|
-
if (returnValue instanceof Promise) {
|
|
95
|
-
isSync = false;
|
|
96
|
-
return Promise.resolve(returnValue).finally(() => {
|
|
97
|
-
action.leaveAction();
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
if (isSync) {
|
|
103
|
-
action.leaveAction();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
112
|
+
invokeOnPressWithoutConfiguration(onPress, args);
|
|
113
|
+
return;
|
|
106
114
|
}
|
|
115
|
+
invokeOnPressWithAction(onPress, args, touchableName);
|
|
107
116
|
};
|
|
108
117
|
};
|
|
109
118
|
exports.wrapOnPress = wrapOnPress;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DTXScreenshotSelfMonitor.h
|
|
3
|
+
//
|
|
4
|
+
// Self-monitoring counterpart of the Session Replay UI-change listener. Instead of capturing a
|
|
5
|
+
// screenshot it only increments the cross-platform screenshot counter via the agent, to estimate
|
|
6
|
+
// how many screenshots Session Replay would take in production.
|
|
7
|
+
//
|
|
8
|
+
// Monitoring is gated by the remote `replay_metrics_enabled` flag delivered through the
|
|
9
|
+
// configuration; it stays dormant until the server enables it.
|
|
10
|
+
//
|
|
11
|
+
|
|
12
|
+
#import <Foundation/Foundation.h>
|
|
13
|
+
#import "ConfigurationSubscriber.h"
|
|
14
|
+
|
|
15
|
+
// Build-time master switch for the screenshot self-monitoring (replay metrics) feature.
|
|
16
|
+
// Defaults to enabled. Disable for a build with `DYNATRACE_REPLAY_METRICS_ENABLED=0 pod install`
|
|
17
|
+
// (the podspec then injects `#define DTX_REPLAY_METRICS_ENABLED 0` via the prefix header). When 0,
|
|
18
|
+
// the monitor is never registered, regardless of the remote `replay_metrics_enabled` flag.
|
|
19
|
+
#ifndef DTX_REPLAY_METRICS_ENABLED
|
|
20
|
+
#define DTX_REPLAY_METRICS_ENABLED 1
|
|
21
|
+
#endif
|
|
22
|
+
|
|
23
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
24
|
+
|
|
25
|
+
@interface DTXScreenshotSelfMonitor : NSObject <ConfigurationSubscriber>
|
|
26
|
+
|
|
27
|
+
@end
|
|
28
|
+
|
|
29
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DTXScreenshotSelfMonitor.mm
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#import "DTXScreenshotSelfMonitor.h"
|
|
6
|
+
#import "DTXScreenshotSelfMonitorSwizzler.h"
|
|
7
|
+
#import <UIKit/UIKit.h>
|
|
8
|
+
#import <QuartzCore/QuartzCore.h>
|
|
9
|
+
|
|
10
|
+
static const double kDebounceMs = 500.0;
|
|
11
|
+
static const double kMaxDebounceMs = 2000.0;
|
|
12
|
+
|
|
13
|
+
// Remote feature-flag key in the cross-platform configuration map. Monitoring stays off unless the
|
|
14
|
+
// server delivers this flag as `true`.
|
|
15
|
+
static NSString *const kReplayMetricsConfigKey = @"replay_metrics_enabled";
|
|
16
|
+
|
|
17
|
+
// DEM-26735: replay metrics experiment sunsets on 2026-10-15T00:00:00Z; remove this class after that date.
|
|
18
|
+
static const NSTimeInterval kExperimentExpiryEpoch = 1792022400.0;
|
|
19
|
+
|
|
20
|
+
static BOOL DTXIsReplayMetricsExperimentExpired(void) {
|
|
21
|
+
return [[NSDate date] timeIntervalSince1970] >= kExperimentExpiryEpoch;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Minimal declaration of the agent's HybridBridge facade. The implementation is provided by the
|
|
25
|
+
// Dynatrace framework; we only need the screenshot-tracking entry point here.
|
|
26
|
+
//
|
|
27
|
+
// NOTE: the underlying store moved to ios-core (DynatraceCoreStatic's
|
|
28
|
+
// Assembly.shared.crossPlatformScreenshotTracker). DynatraceCoreStatic is not exposed to this plugin
|
|
29
|
+
// (it is statically merged into the Dynatrace framework and not re-exported), so we keep calling the
|
|
30
|
+
// ObjC HybridBridge facade; the agent's HybridBridge delegates to the ios-core tracker
|
|
31
|
+
// (+[HybridBridge trackCrossPlatformScreenshot] → [Assembly.shared.crossPlatformScreenshotTracker trackScreenshot]).
|
|
32
|
+
@interface HybridBridge : NSObject
|
|
33
|
+
+ (void)trackCrossPlatformScreenshot;
|
|
34
|
+
@end
|
|
35
|
+
|
|
36
|
+
@implementation DTXScreenshotSelfMonitor {
|
|
37
|
+
BOOL _started;
|
|
38
|
+
BOOL _swizzleInstalled;
|
|
39
|
+
BOOL _pending;
|
|
40
|
+
double _lastChangeTime;
|
|
41
|
+
double _firstChangeTime;
|
|
42
|
+
uint32_t _generation;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (instancetype)init {
|
|
46
|
+
self = [super init];
|
|
47
|
+
if (self) {
|
|
48
|
+
_started = NO;
|
|
49
|
+
_swizzleInstalled = NO;
|
|
50
|
+
_pending = NO;
|
|
51
|
+
_lastChangeTime = 0;
|
|
52
|
+
_firstChangeTime = 0;
|
|
53
|
+
_generation = 0;
|
|
54
|
+
}
|
|
55
|
+
return self;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
- (void)dealloc {
|
|
59
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
60
|
+
if (_swizzleInstalled) {
|
|
61
|
+
[DTXScreenshotSelfMonitorSwizzler uninstall];
|
|
62
|
+
_swizzleInstalled = NO;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#pragma mark - ConfigurationSubscriber
|
|
67
|
+
|
|
68
|
+
- (void)notifyWithConfiguration:(NSDictionary<NSString *, id> *)configuration {
|
|
69
|
+
id value = configuration[kReplayMetricsConfigKey];
|
|
70
|
+
BOOL flagEnabled = [value isKindOfClass:[NSNumber class]] && [value boolValue];
|
|
71
|
+
BOOL enabled = flagEnabled && !DTXIsReplayMetricsExperimentExpired();
|
|
72
|
+
if (flagEnabled && !enabled) {
|
|
73
|
+
NSLog(@"[dtxScreenshotSelfMonitor] config: %@=YES but experiment expired — treating as disabled", kReplayMetricsConfigKey);
|
|
74
|
+
} else {
|
|
75
|
+
NSLog(@"[dtxScreenshotSelfMonitor] config: %@=%@ → %@", kReplayMetricsConfigKey, value, enabled ? @"ENABLED" : @"DISABLED");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Configuration is delivered on a background queue; start/stop touch UIApplication and the global
|
|
79
|
+
// swizzle, so they must run on the main thread. start/stop are idempotent.
|
|
80
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
81
|
+
if (enabled) {
|
|
82
|
+
[self start];
|
|
83
|
+
} else {
|
|
84
|
+
[self stop];
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#pragma mark - Lifecycle (must be called on the main thread)
|
|
90
|
+
|
|
91
|
+
- (void)start {
|
|
92
|
+
if (_started) {
|
|
93
|
+
NSLog(@"[dtxScreenshotSelfMonitor] start ignored — already started");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
_started = YES;
|
|
97
|
+
|
|
98
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
99
|
+
selector:@selector(appDidBecomeActive)
|
|
100
|
+
name:UIApplicationDidBecomeActiveNotification
|
|
101
|
+
object:nil];
|
|
102
|
+
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
103
|
+
selector:@selector(appDidEnterBackground)
|
|
104
|
+
name:UIApplicationDidEnterBackgroundNotification
|
|
105
|
+
object:nil];
|
|
106
|
+
|
|
107
|
+
UIApplicationState appState = [UIApplication sharedApplication].applicationState;
|
|
108
|
+
NSLog(@"[dtxScreenshotSelfMonitor] start (appState=%ld, mainThread=%d)", (long)appState, (int)[NSThread isMainThread]);
|
|
109
|
+
|
|
110
|
+
// Install immediately unless we are backgrounded (avoids observing while there is nothing to do).
|
|
111
|
+
if (appState != UIApplicationStateBackground) {
|
|
112
|
+
[self installSwizzle];
|
|
113
|
+
} else {
|
|
114
|
+
NSLog(@"[dtxScreenshotSelfMonitor] start: app backgrounded — deferring swizzle install to appDidBecomeActive");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
- (void)stop {
|
|
119
|
+
if (!_started) return;
|
|
120
|
+
_started = NO;
|
|
121
|
+
NSLog(@"[dtxScreenshotSelfMonitor] stop");
|
|
122
|
+
|
|
123
|
+
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
124
|
+
[self uninstallSwizzle];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#pragma mark - Swizzle install / uninstall
|
|
128
|
+
|
|
129
|
+
- (void)installSwizzle {
|
|
130
|
+
if (_swizzleInstalled) return;
|
|
131
|
+
_swizzleInstalled = YES;
|
|
132
|
+
NSLog(@"[dtxScreenshotSelfMonitor] installing layer swizzle");
|
|
133
|
+
|
|
134
|
+
__weak __typeof__(self) weakSelf = self;
|
|
135
|
+
[DTXScreenshotSelfMonitorSwizzler installWithChangeHandler:^{
|
|
136
|
+
[weakSelf onLayerChanged];
|
|
137
|
+
}];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
- (void)uninstallSwizzle {
|
|
141
|
+
if (!_swizzleInstalled) return;
|
|
142
|
+
_swizzleInstalled = NO;
|
|
143
|
+
NSLog(@"[dtxScreenshotSelfMonitor] uninstalling layer swizzle (disarming handler)");
|
|
144
|
+
|
|
145
|
+
[DTXScreenshotSelfMonitorSwizzler uninstall];
|
|
146
|
+
_generation++;
|
|
147
|
+
_pending = NO;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#pragma mark - App lifecycle
|
|
151
|
+
|
|
152
|
+
- (void)appDidBecomeActive {
|
|
153
|
+
NSLog(@"[dtxScreenshotSelfMonitor] appDidBecomeActive");
|
|
154
|
+
[self installSwizzle];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
- (void)appDidEnterBackground {
|
|
158
|
+
NSLog(@"[dtxScreenshotSelfMonitor] appDidEnterBackground");
|
|
159
|
+
[self uninstallSwizzle];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#pragma mark - CALayer change callback
|
|
163
|
+
|
|
164
|
+
- (void)onLayerChanged {
|
|
165
|
+
double now = CACurrentMediaTime() * 1000.0;
|
|
166
|
+
_lastChangeTime = now;
|
|
167
|
+
|
|
168
|
+
if (!_pending) {
|
|
169
|
+
_pending = YES;
|
|
170
|
+
_firstChangeTime = now;
|
|
171
|
+
NSLog(@"[dtxScreenshotSelfMonitor] UI change detected — starting %.0fms debounce", kDebounceMs);
|
|
172
|
+
[self scheduleCapture];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#pragma mark - Debounce & capture
|
|
177
|
+
|
|
178
|
+
- (void)scheduleCapture {
|
|
179
|
+
uint32_t gen = _generation;
|
|
180
|
+
__weak __typeof__(self) weakSelf = self;
|
|
181
|
+
dispatch_after(
|
|
182
|
+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDebounceMs * NSEC_PER_MSEC)),
|
|
183
|
+
dispatch_get_main_queue(),
|
|
184
|
+
^{
|
|
185
|
+
__typeof__(self) strongSelf = weakSelf;
|
|
186
|
+
if (!strongSelf || gen != strongSelf->_generation) return;
|
|
187
|
+
[strongSelf performCapture];
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
- (void)performCapture {
|
|
193
|
+
double now = CACurrentMediaTime() * 1000.0;
|
|
194
|
+
double elapsed = now - _lastChangeTime;
|
|
195
|
+
double sinceFirst = now - _firstChangeTime;
|
|
196
|
+
|
|
197
|
+
if (elapsed < kDebounceMs && sinceFirst < kMaxDebounceMs) {
|
|
198
|
+
NSLog(@"[dtxScreenshotSelfMonitor] UI still active (%.0fms since last change) — rescheduling", elapsed);
|
|
199
|
+
[self scheduleCapture];
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_pending = NO;
|
|
204
|
+
|
|
205
|
+
@try {
|
|
206
|
+
NSLog(@"[dtxScreenshotSelfMonitor] capture settled — calling [HybridBridge trackCrossPlatformScreenshot]");
|
|
207
|
+
[HybridBridge trackCrossPlatformScreenshot];
|
|
208
|
+
} @catch (NSException *exception) {
|
|
209
|
+
NSLog(@"[dtxScreenshotSelfMonitor] trackCrossPlatformScreenshot threw: %@", exception.name);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DTXScreenshotSelfMonitorSwizzler.h
|
|
3
|
+
//
|
|
4
|
+
// Detects UI changes by swizzling -[CALayer layoutSublayers] and invoking a change handler on
|
|
5
|
+
// the main thread. Used by the cross-platform screenshot self-monitoring feature.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <Foundation/Foundation.h>
|
|
9
|
+
|
|
10
|
+
@interface DTXScreenshotSelfMonitorSwizzler : NSObject
|
|
11
|
+
|
|
12
|
+
+ (void)installWithChangeHandler:(void (^)(void))handler;
|
|
13
|
+
+ (void)uninstall;
|
|
14
|
+
|
|
15
|
+
@end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DTXScreenshotSelfMonitorSwizzler.mm
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
#import "DTXScreenshotSelfMonitorSwizzler.h"
|
|
6
|
+
#import <QuartzCore/QuartzCore.h>
|
|
7
|
+
#import <objc/runtime.h>
|
|
8
|
+
|
|
9
|
+
static void (^_changeHandler)(void) = nil;
|
|
10
|
+
static BOOL _installed = NO;
|
|
11
|
+
static BOOL _isHandlingChange = NO;
|
|
12
|
+
|
|
13
|
+
static IMP original_layoutSublayers_imp = NULL;
|
|
14
|
+
|
|
15
|
+
static void notifyChange(void) {
|
|
16
|
+
if (!_changeHandler) return;
|
|
17
|
+
if (![NSThread isMainThread]) return;
|
|
18
|
+
if (_isHandlingChange) return;
|
|
19
|
+
|
|
20
|
+
_isHandlingChange = YES;
|
|
21
|
+
_changeHandler();
|
|
22
|
+
_isHandlingChange = NO;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static void swizzled_layoutSublayers(id self, SEL _cmd) {
|
|
26
|
+
((void (*)(id, SEL))original_layoutSublayers_imp)(self, _cmd);
|
|
27
|
+
notifyChange();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@implementation DTXScreenshotSelfMonitorSwizzler
|
|
31
|
+
|
|
32
|
+
+ (void)installWithChangeHandler:(void (^)(void))handler {
|
|
33
|
+
_changeHandler = [handler copy];
|
|
34
|
+
|
|
35
|
+
// Swizzle exactly once for the process lifetime, then drive activity through _changeHandler.
|
|
36
|
+
//
|
|
37
|
+
// We must NOT swizzle/unswizzle on every foreground/background. -[CALayer layoutSublayers] is a
|
|
38
|
+
// shared selector that DynatraceSessionReplay also swizzles; two independent swizzlers each
|
|
39
|
+
// restoring + re-installing the IMP can interleave such that an install captures an
|
|
40
|
+
// already-swizzled IMP as its "original". swizzled_layoutSublayers() then calls back into a
|
|
41
|
+
// swizzled implementation, recursing without bound → stack overflow (EXC_BAD_ACCESS on
|
|
42
|
+
// foreground). Installing once removes all IMP churn and keeps the swizzle chain stable.
|
|
43
|
+
if (_installed) {
|
|
44
|
+
NSLog(@"[dtxScreenshotSelfMonitor] swizzler: handler re-armed (already installed)");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
_installed = YES;
|
|
48
|
+
|
|
49
|
+
Class cls = [CALayer class];
|
|
50
|
+
Method m = class_getInstanceMethod(cls, @selector(layoutSublayers));
|
|
51
|
+
IMP current = method_getImplementation(m);
|
|
52
|
+
// Defensive: never capture our own swizzled function as the original (would self-recurse).
|
|
53
|
+
if (current != (IMP)swizzled_layoutSublayers) {
|
|
54
|
+
original_layoutSublayers_imp = current;
|
|
55
|
+
}
|
|
56
|
+
method_setImplementation(m, (IMP)swizzled_layoutSublayers);
|
|
57
|
+
NSLog(@"[dtxScreenshotSelfMonitor] swizzler: installed on -[CALayer layoutSublayers] (one-time)");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
+ (void)uninstall {
|
|
61
|
+
// Intentionally does NOT restore the original IMP (see installWithChangeHandler:). Restoring is
|
|
62
|
+
// what enables the cross-swizzler recursion above, and it would also clobber any swizzler layered
|
|
63
|
+
// on top of us. We leave the one-time swizzle in place and simply stop notifying — notifyChange()
|
|
64
|
+
// no-ops while _changeHandler is nil, so there is no work and no risk when monitoring is off.
|
|
65
|
+
_changeHandler = nil;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@end
|