diff --git a/tracker/tracker/package-lock.json b/tracker/tracker/package-lock.json
index 4af49d03f..62bc7c1b6 100644
--- a/tracker/tracker/package-lock.json
+++ b/tracker/tracker/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@openreplay/tracker",
- "version": "3.1.0",
+ "version": "3.2.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json
index 822107f58..eb760e700 100644
--- a/tracker/tracker/package.json
+++ b/tracker/tracker/package.json
@@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
- "version": "3.2.0",
+ "version": "3.2.1",
"keywords": [
"logging",
"replay"
diff --git a/tracker/tracker/src/main/modules/mouse.ts b/tracker/tracker/src/main/modules/mouse.ts
index 73e3672b9..cae32142f 100644
--- a/tracker/tracker/src/main/modules/mouse.ts
+++ b/tracker/tracker/src/main/modules/mouse.ts
@@ -1,4 +1,4 @@
-import { finder } from '@medv/finder';
+import { finder } from '../vendors/finder/finder';
import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils';
import App from '../app';
import { MouseMove, MouseClick } from '../../messages';
diff --git a/tracker/tracker/src/main/vendors/finder/LICENSE b/tracker/tracker/src/main/vendors/finder/LICENSE
new file mode 100644
index 000000000..1c1d11619
--- /dev/null
+++ b/tracker/tracker/src/main/vendors/finder/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018–2020 Anton Medvedev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/tracker/tracker/src/main/vendors/finder/README.md b/tracker/tracker/src/main/vendors/finder/README.md
new file mode 100644
index 000000000..391bfee9a
--- /dev/null
+++ b/tracker/tracker/src/main/vendors/finder/README.md
@@ -0,0 +1,132 @@
+
+
+# finder
+
+[](https://www.npmjs.com/package/@medv/finder)
+[](https://travis-ci.org/antonmedv/finder)
+[](https://bundlephobia.com/result?p=@medv/finder)
+
+> CSS Selector Generator
+
+## Features
+
+* Generates **shortest** selectors
+* **Unique** selectors per page
+* Stable and **robust** selectors
+* **2.1 kB** gzip and minify size
+
+## Install
+
+```bash
+npm install @medv/finder
+```
+
+Finder can be used via modules:
+
+```html
+
+```
+
+## Usage
+
+```js
+import {finder} from '@medv/finder'
+
+document.addEventListener('click', event => {
+ const selector = finder(event.target)
+ console.log(selector)
+})
+```
+
+## Example
+
+Example of generated selector:
+
+```css
+.blog > article:nth-child(3) .add-comment
+```
+
+## Configuration
+
+`finder` takes configuration object as second parameters. Here is example of all params with default values:
+
+```js
+const selector = finder(event.target, {
+ root: document.body,
+ className: (name) => true,
+ tagName: (name) => true,
+ attr: (name, value) => false,
+ seedMinLength: 1,
+ optimizedMinLength: 2,
+ threshold: 1000,
+ maxNumberOfTries: 10_000,
+})
+```
+
+#### `root: Element`
+
+Root of search, defaults to `document.body`.
+
+#### `idName: (name: string) => boolean`
+
+Check if this ID can be used. For example you can restrict using framework specific IDs:
+
+```js
+const selector = finder(event.target, {
+ idName: name => !name.startsWith('ember')
+})
+```
+
+#### `className: (name: string) => boolean`
+
+Check if this class name can be used. For example you can restrict using _is-*_ class names:
+
+```js
+const selector = finder(event.target, {
+ className: name => !name.startsWith('is-')
+})
+```
+
+#### `tagName: (name: string) => boolean`
+
+Check if tag name can be used, same as `className`.
+
+#### `attr: (name: string, value: string) => boolean`
+
+Check if attr name can be used.
+
+#### `seedMinLength: number`
+
+Minimum length of levels in fining selector. Starts from `1`.
+For more robust selectors give this param value around 4-5 depending on depth of you DOM tree.
+If `finder` hits `root` this param is ignored.
+
+#### `optimizedMinLength: number`
+
+Minimum length for optimising selector. Starts from `2`.
+For example selector `body > div > div > p` can be optimized to `body p`.
+
+#### `threshold: number`
+
+Max number of selectors to check before falling into `nth-child` usage.
+Checking for uniqueness of selector is very costs operation, if you have DOM tree depth of 5, with 5 classes on each level,
+that gives you more than 3k selectors to check.
+`finder` uses two step approach so it's reaching this threshold in some cases twice.
+Default `1000` is good enough in most cases.
+
+#### `maxNumberOfTries: number`
+
+Max number of tries when we do the optimization. It is a trade-off between optimization and efficiency.
+Default `10_000` is good enough in most cases.
+
+### Google Chrome Extension
+
+
+
+Generate the unique selectors in your browser by using [Chrome Extension](https://chrome.google.com/webstore/detail/get-unique-css-selector/lkfaghhbdebclkklgjhhonadomejckai)
+
+## License
+
+[MIT](LICENSE)
diff --git a/tracker/tracker/src/main/vendors/finder/finder.d.ts b/tracker/tracker/src/main/vendors/finder/finder.d.ts
new file mode 100644
index 000000000..aaff849fb
--- /dev/null
+++ b/tracker/tracker/src/main/vendors/finder/finder.d.ts
@@ -0,0 +1,12 @@
+export declare type Options = {
+ root: Element;
+ idName: (name: string) => boolean;
+ className: (name: string) => boolean;
+ tagName: (name: string) => boolean;
+ attr: (name: string, value: string) => boolean;
+ seedMinLength: number;
+ optimizedMinLength: number;
+ threshold: number;
+ maxNumberOfTries: number;
+};
+export declare function finder(input: Element, options?: Partial): string;
diff --git a/tracker/tracker/src/main/vendors/finder/finder.js b/tracker/tracker/src/main/vendors/finder/finder.js
new file mode 100644
index 000000000..0e5eab2d7
--- /dev/null
+++ b/tracker/tracker/src/main/vendors/finder/finder.js
@@ -0,0 +1,339 @@
+var Limit;
+(function (Limit) {
+ Limit[Limit["All"] = 0] = "All";
+ Limit[Limit["Two"] = 1] = "Two";
+ Limit[Limit["One"] = 2] = "One";
+})(Limit || (Limit = {}));
+let config;
+let rootDocument;
+export function finder(input, options) {
+ if (input.nodeType !== Node.ELEMENT_NODE) {
+ throw new Error(`Can't generate CSS selector for non-element node type.`);
+ }
+ if ("html" === input.tagName.toLowerCase()) {
+ return "html";
+ }
+ const defaults = {
+ root: document.body,
+ idName: (name) => true,
+ className: (name) => true,
+ tagName: (name) => true,
+ attr: (name, value) => false,
+ seedMinLength: 1,
+ optimizedMinLength: 2,
+ threshold: 1000,
+ maxNumberOfTries: 10000,
+ };
+ config = Object.assign(Object.assign({}, defaults), options);
+ rootDocument = findRootDocument(config.root, defaults);
+ let path = bottomUpSearch(input, Limit.All, () => bottomUpSearch(input, Limit.Two, () => bottomUpSearch(input, Limit.One)));
+ if (path) {
+ const optimized = sort(optimize(path, input));
+ if (optimized.length > 0) {
+ path = optimized[0];
+ }
+ return selector(path);
+ }
+ else {
+ throw new Error(`Selector was not found.`);
+ }
+}
+function findRootDocument(rootNode, defaults) {
+ if (rootNode.nodeType === Node.DOCUMENT_NODE) {
+ return rootNode;
+ }
+ if (rootNode === defaults.root) {
+ return rootNode.ownerDocument;
+ }
+ return rootNode;
+}
+function bottomUpSearch(input, limit, fallback) {
+ let path = null;
+ let stack = [];
+ let current = input;
+ let i = 0;
+ while (current && current !== config.root.parentElement) {
+ let level = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()];
+ const nth = index(current);
+ if (limit === Limit.All) {
+ if (nth) {
+ level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth)));
+ }
+ }
+ else if (limit === Limit.Two) {
+ level = level.slice(0, 1);
+ if (nth) {
+ level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth)));
+ }
+ }
+ else if (limit === Limit.One) {
+ const [node] = level = level.slice(0, 1);
+ if (nth && dispensableNth(node)) {
+ level = [nthChild(node, nth)];
+ }
+ }
+ for (let node of level) {
+ node.level = i;
+ }
+ stack.push(level);
+ if (stack.length >= config.seedMinLength) {
+ path = findUniquePath(stack, fallback);
+ if (path) {
+ break;
+ }
+ }
+ current = current.parentElement;
+ i++;
+ }
+ if (!path) {
+ path = findUniquePath(stack, fallback);
+ }
+ return path;
+}
+function findUniquePath(stack, fallback) {
+ const paths = sort(combinations(stack));
+ if (paths.length > config.threshold) {
+ return fallback ? fallback() : null;
+ }
+ for (let candidate of paths) {
+ if (unique(candidate)) {
+ return candidate;
+ }
+ }
+ return null;
+}
+function selector(path) {
+ let node = path[0];
+ let query = node.name;
+ for (let i = 1; i < path.length; i++) {
+ const level = path[i].level || 0;
+ if (node.level === level - 1) {
+ query = `${path[i].name} > ${query}`;
+ }
+ else {
+ query = `${path[i].name} ${query}`;
+ }
+ node = path[i];
+ }
+ return query;
+}
+function penalty(path) {
+ return path.map(node => node.penalty).reduce((acc, i) => acc + i, 0);
+}
+function unique(path) {
+ switch (rootDocument.querySelectorAll(selector(path)).length) {
+ case 0:
+ throw new Error(`Can't select any node with this selector: ${selector(path)}`);
+ case 1:
+ return true;
+ default:
+ return false;
+ }
+}
+function id(input) {
+ const elementId = input.getAttribute("id");
+ if (elementId && config.idName(elementId)) {
+ return {
+ name: "#" + cssesc(elementId, { isIdentifier: true }),
+ penalty: 0,
+ };
+ }
+ return null;
+}
+function attr(input) {
+ const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value));
+ return attrs.map((attr) => ({
+ name: "[" + cssesc(attr.name, { isIdentifier: true }) + "=\"" + cssesc(attr.value) + "\"]",
+ penalty: 0.5
+ }));
+}
+function classNames(input) {
+ const names = Array.from(input.classList)
+ .filter(config.className);
+ return names.map((name) => ({
+ name: "." + cssesc(name, { isIdentifier: true }),
+ penalty: 1
+ }));
+}
+function tagName(input) {
+ const name = input.tagName.toLowerCase();
+ if (config.tagName(name)) {
+ return {
+ name,
+ penalty: 2
+ };
+ }
+ return null;
+}
+function any() {
+ return {
+ name: "*",
+ penalty: 3
+ };
+}
+function index(input) {
+ const parent = input.parentNode;
+ if (!parent) {
+ return null;
+ }
+ let child = parent.firstChild;
+ if (!child) {
+ return null;
+ }
+ let i = 0;
+ while (child) {
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ i++;
+ }
+ if (child === input) {
+ break;
+ }
+ child = child.nextSibling;
+ }
+ return i;
+}
+function nthChild(node, i) {
+ return {
+ name: node.name + `:nth-child(${i})`,
+ penalty: node.penalty + 1
+ };
+}
+function dispensableNth(node) {
+ return node.name !== "html" && !node.name.startsWith("#");
+}
+function maybe(...level) {
+ const list = level.filter(notEmpty);
+ if (list.length > 0) {
+ return list;
+ }
+ return null;
+}
+function notEmpty(value) {
+ return value !== null && value !== undefined;
+}
+function* combinations(stack, path = []) {
+ if (stack.length > 0) {
+ for (let node of stack[0]) {
+ yield* combinations(stack.slice(1, stack.length), path.concat(node));
+ }
+ }
+ else {
+ yield path;
+ }
+}
+function sort(paths) {
+ return Array.from(paths).sort((a, b) => penalty(a) - penalty(b));
+}
+function* optimize(path, input, scope = {
+ counter: 0,
+ visited: new Map()
+}) {
+ if (path.length > 2 && path.length > config.optimizedMinLength) {
+ for (let i = 1; i < path.length - 1; i++) {
+ if (scope.counter > config.maxNumberOfTries) {
+ return; // Okay At least I tried!
+ }
+ scope.counter += 1;
+ const newPath = [...path];
+ newPath.splice(i, 1);
+ const newPathKey = selector(newPath);
+ if (scope.visited.has(newPathKey)) {
+ return;
+ }
+ if (unique(newPath) && same(newPath, input)) {
+ yield newPath;
+ scope.visited.set(newPathKey, true);
+ yield* optimize(newPath, input, scope);
+ }
+ }
+ }
+}
+function same(path, input) {
+ return rootDocument.querySelector(selector(path)) === input;
+}
+const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/;
+const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/;
+const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g;
+const defaultOptions = {
+ "escapeEverything": false,
+ "isIdentifier": false,
+ "quotes": "single",
+ "wrap": false
+};
+function cssesc(string, opt = {}) {
+ const options = Object.assign(Object.assign({}, defaultOptions), opt);
+ if (options.quotes != "single" && options.quotes != "double") {
+ options.quotes = "single";
+ }
+ const quote = options.quotes == "double" ? "\"" : "'";
+ const isIdentifier = options.isIdentifier;
+ const firstChar = string.charAt(0);
+ let output = "";
+ let counter = 0;
+ const length = string.length;
+ while (counter < length) {
+ const character = string.charAt(counter++);
+ let codePoint = character.charCodeAt(0);
+ let value = void 0;
+ // If it’s not a printable ASCII character…
+ if (codePoint < 0x20 || codePoint > 0x7E) {
+ if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) {
+ // It’s a high surrogate, and there is a next character.
+ const extra = string.charCodeAt(counter++);
+ if ((extra & 0xFC00) == 0xDC00) {
+ // next character is low surrogate
+ codePoint = ((codePoint & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000;
+ }
+ else {
+ // It’s an unmatched surrogate; only append this code unit, in case
+ // the next code unit is the high surrogate of a surrogate pair.
+ counter--;
+ }
+ }
+ value = "\\" + codePoint.toString(16).toUpperCase() + " ";
+ }
+ else {
+ if (options.escapeEverything) {
+ if (regexAnySingleEscape.test(character)) {
+ value = "\\" + character;
+ }
+ else {
+ value = "\\" + codePoint.toString(16).toUpperCase() + " ";
+ }
+ }
+ else if (/[\t\n\f\r\x0B]/.test(character)) {
+ value = "\\" + codePoint.toString(16).toUpperCase() + " ";
+ }
+ else if (character == "\\" || !isIdentifier && (character == "\"" && quote == character || character == "'" && quote == character) || isIdentifier && regexSingleEscape.test(character)) {
+ value = "\\" + character;
+ }
+ else {
+ value = character;
+ }
+ }
+ output += value;
+ }
+ if (isIdentifier) {
+ if (/^-[-\d]/.test(output)) {
+ output = "\\-" + output.slice(1);
+ }
+ else if (/\d/.test(firstChar)) {
+ output = "\\3" + firstChar + " " + output.slice(1);
+ }
+ }
+ // Remove spaces after `\HEX` escapes that are not followed by a hex digit,
+ // since they’re redundant. Note that this is only possible if the escape
+ // sequence isn’t preceded by an odd number of backslashes.
+ output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
+ if ($1 && $1.length % 2) {
+ // It’s not safe to remove the space, so don’t.
+ return $0;
+ }
+ // Strip the space.
+ return ($1 || "") + $2;
+ });
+ if (!isIdentifier && options.wrap) {
+ return quote + output + quote;
+ }
+ return output;
+}
diff --git a/tracker/tracker/src/main/vendors/finder/finder.ts b/tracker/tracker/src/main/vendors/finder/finder.ts
new file mode 100644
index 000000000..bb2621d75
--- /dev/null
+++ b/tracker/tracker/src/main/vendors/finder/finder.ts
@@ -0,0 +1,414 @@
+type Node = {
+ name: string
+ penalty: number
+ level?: number
+}
+
+type Path = Node[]
+
+enum Limit {
+ All,
+ Two,
+ One,
+}
+
+export type Options = {
+ root: Element
+ idName: (name: string) => boolean
+ className: (name: string) => boolean
+ tagName: (name: string) => boolean
+ attr: (name: string, value: string) => boolean
+ seedMinLength: number
+ optimizedMinLength: number
+ threshold: number
+ maxNumberOfTries: number
+}
+
+let config: Options
+
+let rootDocument: Document | Element
+export function finder(input: Element, options?: Partial) {
+ if (input.nodeType !== Node.ELEMENT_NODE) {
+ throw new Error(`Can't generate CSS selector for non-element node type.`)
+ }
+
+ if ("html" === input.tagName.toLowerCase()) {
+ return "html"
+ }
+
+ const defaults: Options = {
+ root: document.body,
+ idName: (name: string) => true,
+ className: (name: string) => true,
+ tagName: (name: string) => true,
+ attr: (name: string, value: string) => false,
+ seedMinLength: 1,
+ optimizedMinLength: 2,
+ threshold: 1000,
+ maxNumberOfTries: 10000,
+ }
+
+ config = {...defaults, ...options}
+
+ rootDocument = findRootDocument(config.root, defaults)
+
+ let path =
+ bottomUpSearch(input, Limit.All, () =>
+ bottomUpSearch(input, Limit.Two, () =>
+ bottomUpSearch(input, Limit.One)))
+
+ if (path) {
+ const optimized = sort(optimize(path, input))
+
+ if (optimized.length > 0) {
+ path = optimized[0]
+ }
+
+ return selector(path)
+ } else {
+ throw new Error(`Selector was not found.`)
+ }
+}
+
+function findRootDocument(rootNode: Element | Document, defaults: Options) {
+ if (rootNode.nodeType === Node.DOCUMENT_NODE) {
+ return rootNode
+ }
+ if (rootNode === defaults.root) {
+ return rootNode.ownerDocument as Document
+ }
+ return rootNode
+}
+
+function bottomUpSearch(input: Element, limit: Limit, fallback?: () => Path | null): Path | null {
+ let path: Path | null = null
+ let stack: Node[][] = []
+ let current: Element | null = input
+ let i = 0
+
+ while (current && current !== config.root.parentElement) {
+ let level: Node[] = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()]
+
+ const nth = index(current)
+
+ if (limit === Limit.All) {
+ if (nth) {
+ level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth)))
+ }
+ } else if (limit === Limit.Two) {
+ level = level.slice(0, 1)
+
+ if (nth) {
+ level = level.concat(level.filter(dispensableNth).map(node => nthChild(node, nth)))
+ }
+ } else if (limit === Limit.One) {
+ const [node] = level = level.slice(0, 1)
+
+ if (nth && dispensableNth(node)) {
+ level = [nthChild(node, nth)]
+ }
+ }
+
+ for (let node of level) {
+ node.level = i
+ }
+
+ stack.push(level)
+
+ if (stack.length >= config.seedMinLength) {
+ path = findUniquePath(stack, fallback)
+ if (path) {
+ break
+ }
+ }
+
+ current = current.parentElement
+ i++
+ }
+
+ if (!path) {
+ path = findUniquePath(stack, fallback)
+ }
+
+ return path
+}
+
+function findUniquePath(stack: Node[][], fallback?: () => Path | null): Path | null {
+ const paths = sort(combinations(stack))
+
+ if (paths.length > config.threshold) {
+ return fallback ? fallback() : null
+ }
+
+ for (let candidate of paths) {
+ if (unique(candidate)) {
+ return candidate
+ }
+ }
+
+ return null
+}
+
+function selector(path: Path): string {
+ let node = path[0]
+ let query = node.name
+ for (let i = 1; i < path.length; i++) {
+ const level = path[i].level || 0
+
+ if (node.level === level - 1) {
+ query = `${path[i].name} > ${query}`
+ } else {
+ query = `${path[i].name} ${query}`
+ }
+
+ node = path[i]
+ }
+ return query
+}
+
+function penalty(path: Path): number {
+ return path.map(node => node.penalty).reduce((acc, i) => acc + i, 0)
+}
+
+function unique(path: Path) {
+ switch (rootDocument.querySelectorAll(selector(path)).length) {
+ case 0:
+ throw new Error(`Can't select any node with this selector: ${selector(path)}`)
+ case 1:
+ return true
+ default:
+ return false
+ }
+}
+
+function id(input: Element): Node | null {
+ const elementId = input.getAttribute("id")
+ if (elementId && config.idName(elementId)) {
+ return {
+ name: "#" + cssesc(elementId, {isIdentifier: true}),
+ penalty: 0,
+ }
+ }
+ return null
+}
+
+function attr(input: Element): Node[] {
+ const attrs = Array.from(input.attributes).filter((attr) => config.attr(attr.name, attr.value))
+
+ return attrs.map((attr): Node => ({
+ name: "[" + cssesc(attr.name, {isIdentifier: true}) + "=\"" + cssesc(attr.value) + "\"]",
+ penalty: 0.5
+ }))
+}
+
+function classNames(input: Element): Node[] {
+ const names = Array.from(input.classList)
+ .filter(config.className)
+
+ return names.map((name): Node => ({
+ name: "." + cssesc(name, {isIdentifier: true}),
+ penalty: 1
+ }))
+}
+
+function tagName(input: Element): Node | null {
+ const name = input.tagName.toLowerCase()
+ if (config.tagName(name)) {
+ return {
+ name,
+ penalty: 2
+ }
+ }
+ return null
+}
+
+function any(): Node {
+ return {
+ name: "*",
+ penalty: 3
+ }
+}
+
+function index(input: Element): number | null {
+ const parent = input.parentNode
+ if (!parent) {
+ return null
+ }
+
+ let child = parent.firstChild
+ if (!child) {
+ return null
+ }
+
+ let i = 0
+ while (child) {
+ if (child.nodeType === Node.ELEMENT_NODE) {
+ i++
+ }
+
+ if (child === input) {
+ break
+ }
+
+ child = child.nextSibling
+ }
+
+ return i
+}
+
+function nthChild(node: Node, i: number): Node {
+ return {
+ name: node.name + `:nth-child(${i})`,
+ penalty: node.penalty + 1
+ }
+}
+
+function dispensableNth(node: Node) {
+ return node.name !== "html" && !node.name.startsWith("#")
+}
+
+function maybe(...level: (Node | null)[]): Node[] | null {
+ const list = level.filter(notEmpty)
+ if (list.length > 0) {
+ return list
+ }
+ return null
+}
+
+function notEmpty(value: T | null | undefined): value is T {
+ return value !== null && value !== undefined
+}
+
+function* combinations(stack: Node[][], path: Node[] = []): Generator {
+ if (stack.length > 0) {
+ for (let node of stack[0]) {
+ yield* combinations(stack.slice(1, stack.length), path.concat(node))
+ }
+ } else {
+ yield path
+ }
+}
+
+function sort(paths: Iterable): Path[] {
+ return Array.from(paths).sort((a, b) => penalty(a) - penalty(b))
+}
+
+type Scope = {
+ counter: number
+ visited: Map
+}
+
+function* optimize(path: Path, input: Element, scope: Scope = {
+ counter: 0,
+ visited: new Map()
+}): Generator {
+ if (path.length > 2 && path.length > config.optimizedMinLength) {
+ for (let i = 1; i < path.length - 1; i++) {
+ if (scope.counter > config.maxNumberOfTries) {
+ return // Okay At least I tried!
+ }
+ scope.counter += 1
+ const newPath = [...path]
+ newPath.splice(i, 1)
+ const newPathKey = selector(newPath)
+ if (scope.visited.has(newPathKey)) {
+ return
+ }
+ if (unique(newPath) && same(newPath, input)) {
+ yield newPath
+ scope.visited.set(newPathKey, true)
+ yield* optimize(newPath, input, scope)
+ }
+ }
+ }
+}
+
+function same(path: Path, input: Element) {
+ return rootDocument.querySelector(selector(path)) === input
+}
+
+const regexAnySingleEscape = /[ -,\.\/:-@\[-\^`\{-~]/
+const regexSingleEscape = /[ -,\.\/:-@\[\]\^`\{-~]/
+const regexExcessiveSpaces = /(^|\\+)?(\\[A-F0-9]{1,6})\x20(?![a-fA-F0-9\x20])/g
+
+const defaultOptions = {
+ "escapeEverything": false,
+ "isIdentifier": false,
+ "quotes": "single",
+ "wrap": false
+}
+
+function cssesc(string: string, opt: Partial = {}) {
+ const options = {...defaultOptions, ...opt}
+ if (options.quotes != "single" && options.quotes != "double") {
+ options.quotes = "single"
+ }
+ const quote = options.quotes == "double" ? "\"" : "'"
+ const isIdentifier = options.isIdentifier
+
+ const firstChar = string.charAt(0)
+ let output = ""
+ let counter = 0
+ const length = string.length
+ while (counter < length) {
+ const character = string.charAt(counter++)
+ let codePoint = character.charCodeAt(0)
+ let value: string | undefined = void 0
+ // If it’s not a printable ASCII character…
+ if (codePoint < 0x20 || codePoint > 0x7E) {
+ if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) {
+ // It’s a high surrogate, and there is a next character.
+ const extra = string.charCodeAt(counter++)
+ if ((extra & 0xFC00) == 0xDC00) {
+ // next character is low surrogate
+ codePoint = ((codePoint & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000
+ } else {
+ // It’s an unmatched surrogate; only append this code unit, in case
+ // the next code unit is the high surrogate of a surrogate pair.
+ counter--
+ }
+ }
+ value = "\\" + codePoint.toString(16).toUpperCase() + " "
+ } else {
+ if (options.escapeEverything) {
+ if (regexAnySingleEscape.test(character)) {
+ value = "\\" + character
+ } else {
+ value = "\\" + codePoint.toString(16).toUpperCase() + " "
+ }
+ } else if (/[\t\n\f\r\x0B]/.test(character)) {
+ value = "\\" + codePoint.toString(16).toUpperCase() + " "
+ } else if (character == "\\" || !isIdentifier && (character == "\"" && quote == character || character == "'" && quote == character) || isIdentifier && regexSingleEscape.test(character)) {
+ value = "\\" + character
+ } else {
+ value = character
+ }
+ }
+ output += value
+ }
+
+ if (isIdentifier) {
+ if (/^-[-\d]/.test(output)) {
+ output = "\\-" + output.slice(1)
+ } else if (/\d/.test(firstChar)) {
+ output = "\\3" + firstChar + " " + output.slice(1)
+ }
+ }
+
+ // Remove spaces after `\HEX` escapes that are not followed by a hex digit,
+ // since they’re redundant. Note that this is only possible if the escape
+ // sequence isn’t preceded by an odd number of backslashes.
+ output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
+ if ($1 && $1.length % 2) {
+ // It’s not safe to remove the space, so don’t.
+ return $0
+ }
+ // Strip the space.
+ return ($1 || "") + $2
+ })
+
+ if (!isIdentifier && options.wrap) {
+ return quote + output + quote
+ }
+ return output
+}
diff --git a/tracker/tracker/src/main/vendors/finder/package.json b/tracker/tracker/src/main/vendors/finder/package.json
new file mode 100644
index 000000000..685be35f4
--- /dev/null
+++ b/tracker/tracker/src/main/vendors/finder/package.json
@@ -0,0 +1,78 @@
+{
+ "_from": "@medv/finder",
+ "_id": "@medv/finder@2.0.0",
+ "_inBundle": false,
+ "_integrity": "sha512-gV4jOsGpiWNDGd8Dw7tod1Fc9Gc7StaOT4oZ/6srHRWtsHU+HYWzmkYsa3Qy/z0e9tY1WpJ9wWdBFGskfbzoug==",
+ "_location": "/@medv/finder",
+ "_phantomChildren": {},
+ "_requested": {
+ "type": "tag",
+ "registry": true,
+ "raw": "@medv/finder",
+ "name": "@medv/finder",
+ "escapedName": "@medv%2ffinder",
+ "scope": "@medv",
+ "rawSpec": "",
+ "saveSpec": null,
+ "fetchSpec": "latest"
+ },
+ "_requiredBy": [
+ "#USER",
+ "/"
+ ],
+ "_resolved": "https://registry.npmjs.org/@medv/finder/-/finder-2.0.0.tgz",
+ "_shasum": "699b7141393aa815f120b38f54f92ad212225902",
+ "_spec": "@medv/finder",
+ "_where": "/Users/shikhu/work/openreplay/tracker/tracker",
+ "author": {
+ "name": "Anton Medvedev",
+ "email": "anton@medv.io"
+ },
+ "ava": {
+ "require": [
+ "esm",
+ "./test/helpers/setup-browser-env.js"
+ ]
+ },
+ "bugs": {
+ "url": "https://github.com/antonmedv/finder/issues"
+ },
+ "bundleDependencies": false,
+ "deprecated": false,
+ "description": "CSS Selector Generator",
+ "devDependencies": {
+ "ava": "^3.8.2",
+ "babel-minify": "*",
+ "browser-env": "^3.3.0",
+ "esm": "^3.2.25",
+ "gzip-size-cli": "*",
+ "release-it": "^13.6.1",
+ "typescript": "3.9.3"
+ },
+ "files": [
+ "*.ts",
+ "*.js"
+ ],
+ "homepage": "https://github.com/antonmedv/finder",
+ "keywords": [
+ "css",
+ "selector",
+ "generator"
+ ],
+ "license": "MIT",
+ "main": "finder.js",
+ "name": "@medv/finder",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/antonmedv/finder.git"
+ },
+ "scripts": {
+ "prepare": "tsc",
+ "release": "release-it --access public",
+ "size": "minify finder.js --sourceType module | gzip-size",
+ "start": "tsc -w",
+ "test": "tsc && ava"
+ },
+ "types": "finder.d.ts",
+ "version": "2.0.0"
+}