Skip to content

Commit

Permalink
feat: Touch events take Regex for ignoreNames & add tests (#973)
Browse files Browse the repository at this point in the history
  • Loading branch information
jennmueng committed Jul 20, 2020
1 parent 2b5267f commit 134372c
Show file tree
Hide file tree
Showing 6 changed files with 3,087 additions and 73 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- feat: Touch events take Regex for ignoreNames & add tests #973

## 1.6.2

- fix: Don't prefix app:/// to "native" filename as well #957
Expand Down
3 changes: 3 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: ["module:metro-react-native-babel-preset"],
};
26 changes: 16 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@
"@sentry/typescript": "^5.19.0",
"@types/jest": "^25.1.4",
"@types/react-native": "^0.62.10",
"babel-jest": "^26.1.0",
"jest": "^24.9.0",
"prettier": "^2.0.5",
"react": ">=16.4.1",
"react-native": ">=0.56.0",
"replace-in-file": "^6.0.0",
"rimraf": "^3.0.0",
"ts-jest": "^24.3.0",
Expand All @@ -75,23 +78,26 @@
},
"jest": {
"collectCoverage": true,
"preset": "react-native",
"transform": {
"^.+\\.ts$": "ts-jest"
"^.+\\.(tsx)$": "<rootDir>/node_modules/react-native/jest/preprocessor.js",
"^.+\\.(ts|tsx)$": "ts-jest"
},
"moduleFileExtensions": [
"js",
"ts"
],
"testEnvironment": "node",
"testMatch": [
"**/*.test.ts"
],
"globals": {
"__DEV__": true,
"ts-jest": {
"tsConfig": "./tsconfig.json",
"diagnostics": false
}
}
},
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"testEnvironment": "node",
"testMatch": [
"**/*.test.(ts|tsx)"
]
}
}
63 changes: 42 additions & 21 deletions src/js/touchevents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ export type TouchEventBoundaryProps = {
*/
maxComponentTreeSize?: number;
/**
* Component displayName(s) to ignore when logging the touch event. This prevents unhelpful logs such as
* Component name(s) to ignore when logging the touch event. This prevents unhelpful logs such as
* "Touch event within element: View" where you still can't tell which View it occurred in.
*
* By default, only View and Text are ignored. If you pass this prop, don't forget to include them.
*/
ignoredDisplayNames?: string[];
ignoreNames?: Array<string | RegExp>;
/**
* Deprecated, use ignoreNames instead
* @deprecated
*/
ignoredDisplayNames?: Array<string | RegExp>;
};

const touchEventStyles = StyleSheet.create({
Expand All @@ -35,7 +38,6 @@ const touchEventStyles = StyleSheet.create({
const DEFAULT_BREADCRUMB_CATEGORY = "touch";
const DEFAULT_BREADCRUMB_TYPE = "user";
const DEFAULT_MAX_COMPONENT_TREE_SIZE = 20;
const DEFAULT_IGNORED_DISPLAY_NAMES = ["View", "Text"];

/**
* Boundary to log breadcrumbs for interaction events.
Expand All @@ -45,7 +47,7 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
public static defaultProps: Partial<TouchEventBoundaryProps> = {
breadcrumbCategory: DEFAULT_BREADCRUMB_CATEGORY,
breadcrumbType: DEFAULT_BREADCRUMB_TYPE,
ignoredDisplayNames: DEFAULT_IGNORED_DISPLAY_NAMES,
ignoreNames: [],
maxComponentTreeSize: DEFAULT_MAX_COMPONENT_TREE_SIZE,
};

Expand All @@ -64,13 +66,31 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
});
};

private readonly _isNameIgnored = (name: string): boolean => {
let ignoreNames = this.props.ignoreNames || [];
if (this.props.ignoredDisplayNames) {
// This is to make it compatible with prior version.
ignoreNames = [...ignoreNames, ...this.props.ignoredDisplayNames];
}

return ignoreNames.some(
(ignoreName) =>
(typeof ignoreName === "string" && name === ignoreName) ||
(ignoreName instanceof RegExp && name.match(ignoreName))
);
};

// Originally was going to clean the names of any HOCs as well but decided that it might hinder debugging effectively. Will leave here in case
// private readonly _cleanName = (name: string): string =>
// name.replace(/.*\(/g, "").replace(/\)/g, "");

private readonly _onTouchStart = (e: any): void => {
/* tslint:disable: no-unsafe-any */
if (e._targetInst) {
let currentInst = e._targetInst;

let displayName = null;
const componentTreeNames = [];
let activeDisplayName = null;
const componentTreeNames: string[] = [];

while (
currentInst &&
Expand All @@ -87,27 +107,28 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
break;
}

if (typeof currentInst.elementType.displayName === "string") {
if (
Array.isArray(this.props.ignoredDisplayNames) &&
!this.props.ignoredDisplayNames.includes(
currentInst.elementType.displayName
)
) {
displayName = currentInst.elementType.displayName;
if (
typeof currentInst.elementType.displayName === "string" &&
!this._isNameIgnored(currentInst.elementType.displayName)
) {
const { displayName } = currentInst.elementType;
if (activeDisplayName === null) {
activeDisplayName = displayName;
}

componentTreeNames.push(currentInst.elementType.displayName);
} else if (typeof currentInst.elementType.name === "string") {
componentTreeNames.push(displayName);
} else if (
typeof currentInst.elementType.name === "string" &&
!this._isNameIgnored(currentInst.elementType.name)
) {
componentTreeNames.push(currentInst.elementType.name);
}
}

currentInst = currentInst.return;
}

if (componentTreeNames.length > 0 || displayName) {
this._logTouchEvent(componentTreeNames, displayName);
if (componentTreeNames.length > 0 || activeDisplayName) {
this._logTouchEvent(componentTreeNames, activeDisplayName);
}
}
/* tslint:enable: no-unsafe-any */
Expand Down
187 changes: 187 additions & 0 deletions test/touchevents.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import * as core from "@sentry/core";
import { Severity } from "@sentry/types";

import { TouchEventBoundary } from "../src/js/touchevents";

const addBreadcrumb = jest.spyOn(core, "addBreadcrumb");

afterEach(() => {
jest.resetAllMocks();
});

describe("TouchEventBoundary._onTouchStart", () => {
it("tree without displayName", () => {
const { defaultProps } = TouchEventBoundary;
const boundary = new TouchEventBoundary(defaultProps);

const event = {
_targetInst: {
elementType: {
name: "View",
},
return: {
elementType: {
name: "Text",
},
return: {
elementType: {
name: "CoolComponent",
},
return: {
elementType: {
name: "Screen",
},
},
},
},
},
};

// @ts-ignore
boundary._onTouchStart(event);

expect(addBreadcrumb).toBeCalledWith({
category: defaultProps.breadcrumbCategory,
data: {
componentTree: ["View", "Text", "CoolComponent", "Screen"],
},
level: Severity.Info,
message: "Touch event within component tree",
type: defaultProps.breadcrumbType,
});
});

it("displayName is displayed", () => {
const { defaultProps } = TouchEventBoundary;
const boundary = new TouchEventBoundary(defaultProps);

const event = {
_targetInst: {
elementType: {
name: "View",
},
return: {
elementType: {
name: "Text",
},
return: {
elementType: {
displayName: "Connect(View)",
},
},
},
},
};

// @ts-ignore
boundary._onTouchStart(event);

expect(addBreadcrumb).toBeCalledWith({
category: defaultProps.breadcrumbCategory,
data: {
componentTree: ["View", "Text", "Connect(View)"],
},
level: Severity.Info,
message: "Touch event within element: Connect(View)",
type: defaultProps.breadcrumbType,
});
});

it("ignoreNames", () => {
const { defaultProps } = TouchEventBoundary;
const boundary = new TouchEventBoundary({
...defaultProps,
ignoreNames: ["View", /^Connect\(/, new RegExp("^Happy\\(")],
});

const event = {
_targetInst: {
elementType: {
name: "View",
},
return: {
elementType: {
name: "Text",
},
return: {
elementType: {
displayName: "Connect(View)",
},
return: {
elementType: {
displayName: "Styled(View)",
},
return: {
elementType: {
displayName: "Happy(View)",
},
},
},
},
},
},
};

// @ts-ignore
boundary._onTouchStart(event);

expect(addBreadcrumb).toBeCalledWith({
category: defaultProps.breadcrumbCategory,
data: {
componentTree: ["Text", "Styled(View)"],
},
level: Severity.Info,
message: "Touch event within element: Styled(View)",
type: defaultProps.breadcrumbType,
});
});

it("maxComponentTreeSize", () => {
const { defaultProps } = TouchEventBoundary;
const boundary = new TouchEventBoundary({
...defaultProps,
maxComponentTreeSize: 3,
});

const event = {
_targetInst: {
elementType: {
name: "View",
},
return: {
elementType: {
name: "Text",
},
return: {
elementType: {
displayName: "Connect(View)",
},
return: {
elementType: {
displayName: "Styled(View)",
},
return: {
elementType: {
displayName: "Happy(View)",
},
},
},
},
},
},
};

// @ts-ignore
boundary._onTouchStart(event);

expect(addBreadcrumb).toBeCalledWith({
category: defaultProps.breadcrumbCategory,
data: {
componentTree: ["View", "Text", "Connect(View)"],
},
level: Severity.Info,
message: "Touch event within element: Connect(View)",
type: defaultProps.breadcrumbType,
});
});
});
Loading

0 comments on commit 134372c

Please sign in to comment.