diff --git a/packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts b/packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts
index 5a0833342..124ba87e9 100644
--- a/packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts
+++ b/packages/core/src/rum/instrumentation/interactionTracking/DdBabelInteractionTracking.ts
@@ -155,7 +155,7 @@ class BabelInteractionTracking {
});
}
- return func(...args);
+ return func?.(...args);
};
}
}
diff --git a/packages/core/src/rum/instrumentation/interactionTracking/__tests__/DdBabelInteractionTracking.test.ts b/packages/core/src/rum/instrumentation/interactionTracking/__tests__/DdBabelInteractionTracking.test.ts
new file mode 100644
index 000000000..a5ffa9d32
--- /dev/null
+++ b/packages/core/src/rum/instrumentation/interactionTracking/__tests__/DdBabelInteractionTracking.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
+ * Copyright 2016-Present Datadog, Inc.
+ */
+
+import { RumActionType } from '../../../types';
+import { DdBabelInteractionTracking } from '../DdBabelInteractionTracking';
+
+jest.mock('../../../../specs/NativeDdSdk', () => ({
+ __esModule: true,
+ default: {
+ sendTelemetryLog: jest.fn(),
+ telemetryError: jest.fn()
+ }
+}));
+
+jest.mock('../../../../utils/time-provider/DefaultTimeProvider', () => ({
+ DefaultTimeProvider: jest.fn().mockImplementation(() => ({
+ now: jest.fn().mockReturnValue(456)
+ }))
+}));
+
+const mockTargetObject = {
+ getContent: undefined,
+ options: { useContent: true, useNamePrefix: true },
+ handlerArgs: [],
+ componentName: 'Button',
+ 'dd-action-name': [],
+ accessibilityLabel: []
+};
+
+describe('DdBabelInteractionTracking.wrapRumAction', () => {
+ it('should not crash when func is undefined', () => {
+ const wrapped = DdBabelInteractionTracking.wrapRumAction(
+ undefined as any,
+ RumActionType.TAP,
+ mockTargetObject
+ );
+
+ expect(() => wrapped()).not.toThrow();
+ expect(wrapped()).toBeUndefined();
+ });
+
+ it('should not crash when func is null', () => {
+ const wrapped = DdBabelInteractionTracking.wrapRumAction(
+ null as any,
+ RumActionType.TAP,
+ mockTargetObject
+ );
+
+ expect(() => wrapped()).not.toThrow();
+ expect(wrapped()).toBeUndefined();
+ });
+
+ it('should call func when it is defined', () => {
+ const func = jest.fn().mockReturnValue('result');
+ const wrapped = DdBabelInteractionTracking.wrapRumAction(
+ func,
+ RumActionType.TAP,
+ mockTargetObject
+ );
+
+ const result = wrapped('arg1', 'arg2');
+
+ expect(func).toHaveBeenCalledWith('arg1', 'arg2');
+ expect(result).toBe('result');
+ });
+});
diff --git a/packages/react-native-babel-plugin/src/actions/rum/tap.ts b/packages/react-native-babel-plugin/src/actions/rum/tap.ts
index 1a2224455..6e609ea92 100644
--- a/packages/react-native-babel-plugin/src/actions/rum/tap.ts
+++ b/packages/react-native-babel-plugin/src/actions/rum/tap.ts
@@ -113,9 +113,11 @@ export function handleTapAction(
expression,
getArgumentsFromParams(t, state, expression.params).callArgs
)
- : t.callExpression(expression, [
- t.spreadElement(t.identifier('args'))
- ]);
+ : t.optionalCallExpression(
+ expression,
+ [t.spreadElement(t.identifier('args'))],
+ true
+ );
}
state.hasValidTapAction = true;
@@ -296,7 +298,7 @@ function handleMemoization(
const returnExpression =
mode === 'delayed'
? callback
- : t.callExpression(callback, callArgs);
+ : t.optionalCallExpression(callback, callArgs, true);
// Update handlerArgs in argsObject to match the actual callback params
// instead of the default [...args] computed from the outer identifier
diff --git a/packages/react-native-babel-plugin/test/plugin.test.ts b/packages/react-native-babel-plugin/test/plugin.test.ts
index 2a1ca9575..ff567464e 100644
--- a/packages/react-native-babel-plugin/test/plugin.test.ts
+++ b/packages/react-native-babel-plugin/test/plugin.test.ts
@@ -91,7 +91,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Button"
- })(...args);else return func(...args);
+ })(...args);else return func?.(...args);
}
});"
`);
@@ -557,7 +557,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Pressable"
- })(...args);else return func(...args);
+ })(...args);else return func?.(...args);
}
});"
`);
@@ -592,7 +592,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Pressable"
- })(...args);else return func3(...args);
+ })(...args);else return func3?.(...args);
}
});"
`);
@@ -626,7 +626,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Pressable"
- })(...args);else return a(...args);
+ })(...args);else return a?.(...args);
}
});"
`);
@@ -827,7 +827,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Pressable"
- })(...args);else return handler(...args);
+ })(...args);else return handler?.(...args);
}
});
}"
@@ -864,7 +864,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Pressable"
- })(...args);else return handler(...args);
+ })(...args);else return handler?.(...args);
}
});
}"
@@ -901,7 +901,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Pressable"
- })(...args);else return globalThis.handler(...args);
+ })(...args);else return globalThis.handler?.(...args);
}
});
}"
@@ -942,7 +942,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Pressable"
- })(...args);else return props.onPress(...args);
+ })(...args);else return props.onPress?.(...args);
}
}));
}"
@@ -986,7 +986,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Pressable"
- })(...args);else return onPress(...args);
+ })(...args);else return onPress?.(...args);
}
}));
}"
@@ -1143,7 +1143,7 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Button"
- })(...args);else return this.handlePress(...args);
+ })(...args);else return this.handlePress?.(...args);
}
}));
}
@@ -1185,13 +1185,49 @@ describe('Babel plugin: wrap interaction handlers for RUM', () => {
},
"handlerArgs": [...args],
"componentName": "Button"
- })(...args);else return func(...args);
+ })(...args);else return func?.(...args);
}
});"
`);
});
});
+describe('Babel plugin: optional handler guard (undefined handler safety)', () => {
+ it('should generate optional chaining in fallback branch for named function reference that may be undefined', () => {
+ const input = `
+ import { Pressable, Text, TextInput as RNTextInput } from 'react-native';
+
+ function TextInput({ label, onPress }: { label: string; onPress?: () => void }) {
+ return (
+
+ {label}
+
+
+ );
+ }
+ `;
+ const output = transformCode(input);
+ // The branch when the SDK is not initialized must use optional chaining
+ expect(output).toContain('onPress?.(...args)');
+ // The branch when the SDK is initialized should pass the handler to wrapRumAction
+ expect(output).toContain('wrapRumAction(onPress,');
+ });
+
+ it('should generate optional chaining in fallback branch for Pressable with arrow function handler', () => {
+ const input = `
+ import { Pressable, Text } from 'react-native';
+ { console.log('pressed'); }}>
+ Click me
+
+ `;
+ const output = transformCode(input);
+ // Arrow functions are always defined so optional chaining is acceptable but not strictly required
+ // The key assertion is that the output compiles without crashing
+ expect(output).toBeDefined();
+ expect(output).toContain('DdBabelInteractionTracking');
+ });
+});
+
describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )', () => {
it('should wrap arrow function inside `useCallback` with parameter (gesture/native event)', () => {
const input = `
@@ -1237,7 +1273,7 @@ describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )',
console.log('Testing ', a, b, event);
setA(x => x + 1);
setB(x => x + 1);
- })(event);
+ })?.(event);
}, [a, b]);
return /*#__PURE__*/React.createElement(Pressable, {
color: "red",
@@ -1335,7 +1371,7 @@ describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )',
"componentName": "Pressable"
})();else return (() => {
console.log('Testing ');
- })();
+ })?.();
}, []);
return /*#__PURE__*/React.createElement(Pressable, {
color: "red",
@@ -1385,7 +1421,7 @@ describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )',
"componentName": "Pressable"
})();else return (() => {
console.log('Testing ');
- })();
+ })?.();
}, []);
return /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Pressable, {
color: "red",
@@ -1443,7 +1479,7 @@ describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )',
},
"handlerArgs": [test],
"componentName": "Pressable"
- })(test);else return funcN(test);
+ })(test);else return funcN?.(test);
}, [a, b]);
return /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Pressable, {
color: "red",
@@ -1498,7 +1534,7 @@ describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )',
},
"handlerArgs": [test],
"componentName": "Pressable"
- })(test);else return funcN(test);
+ })(test);else return funcN?.(test);
}, [a, b]);
return /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Pressable, {
color: "red",
@@ -1549,7 +1585,7 @@ describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )',
},
"handlerArgs": [test],
"componentName": "Pressable"
- })(test);else return funcN(test);
+ })(test);else return funcN?.(test);
}, [a, b]);
return /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Pressable, {
color: "red",
@@ -1634,7 +1670,7 @@ describe('Babel plugin: wrap interaction handlers for RUM ( with memoization )',
},
"handlerArgs": [],
"componentName": "Pressable"
- })();else return funcN();
+ })();else return funcN?.();
}, []);
return /*#__PURE__*/React.createElement(View, null, /*#__PURE__*/React.createElement(Pressable, {
color: "red",