Merge pull request #158 from openreplay/dev

assist and funnel hotfix v1.3.0
This commit is contained in:
Shekar Siri 2021-08-13 22:56:38 +05:30 committed by GitHub
commit 20f64d08f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1072 additions and 45 deletions

View file

@ -822,7 +822,7 @@ def get_funnel(projectId, funnelId, context):
project_id=projectId)
if data is None:
return {"errors": ["funnel not found"]}
return data
return {"data": data}
@app.route('/{projectId}/funnels/{funnelId}', methods=['POST', 'PUT'])

View file

@ -51,6 +51,7 @@ def get_live_sessions(project_id, filters=None):
SELECT {SESSION_PROJECTION_COLS}, %(project_key)s||'-'|| session_id AS peer_id
FROM public.sessions AS s
WHERE {" AND ".join(extra_constraints)}
ORDER BY start_ts DESC
LIMIT 500;""",
{"project_id": project_id,
"connected_peers": connected_peers,

View file

@ -40,22 +40,28 @@ function AssistActions({ toggleChatWindow, userId, calling, peerConnectionStatus
toast.info(`Call was rejected.`);
}
function onError() {
toast.error(`Something went wrong!`);
function onError(e) {
toast.error(e);
}
function call() {
function onCallConnect(lStream) {
setLocalStream(lStream);
setEndCall(() => callPeer(
lStream,
setIncomeStream,
onClose.bind(null, lStream),
onReject,
onError
));
}
function call() {
navigator.mediaDevices.getUserMedia({video:true, audio:true})
.then(lStream => {
setLocalStream(lStream);
setEndCall(() => callPeer(
lStream,
setIncomeStream,
onClose.bind(null, lStream),
onReject,
onError
));
}).catch(onError);
.then(onCallConnect).catch(error => { // TODO retry only if specific error
navigator.mediaDevices.getUserMedia({audio:true})
.then(onCallConnect)
.catch(onError)
});
}
const inCall = calling !== CallingState.False;

View file

@ -17,7 +17,7 @@ class EventGroupWrapper extends React.PureComponent {
}
componentDidUpdate(prevProps) {
if (prevProps.showLoadInfo !== this.props.showLoadInfo) {
if (prevProps.showLoadInfo !== this.props.showLoadInfo || prevProps.query !== this.props.query) {
this.props.mesureHeight();
}
}

View file

@ -1,9 +1,15 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { Input, Icon } from 'UI'
export default function EventSearch(props) {
const { onChange, clearSearch, value, header } = props;
const [showSearch, setShowSearch] = useState(false)
useEffect(() => {
return () => {
clearSearch()
}
}, [])
return (
<div className="flex items-center w-full">
<div className="flex flex-1 relative items-center" style={{ height: '32px' }}>

View file

@ -39,14 +39,11 @@ export default class EventsBlock extends React.PureComponent {
write = ({ target: { value, name } }) => {
const { filter } = this.state;
this.setState({ query: value })
this.props.setEventFilter({ query: value, filter })
this.props.setEventFilter({ query: value, filter })
setTimeout(() => {
this.scroller.current.scrollToRow(0);
this.scroller.current.recomputeGridSize();
this.scroller.current.recomputeRowHeights();
this.scroller.current.forceUpdateGrid();
}, 200)
this.scroller.current.scrollToRow(0);
}, 100)
}
clearSearch = () => {
@ -131,7 +128,8 @@ export default class EventsBlock extends React.PureComponent {
playing,
eventsIndex,
filteredEvents
} = this.props;
} = this.props;
const { query } = this.state;
const _events = filteredEvents || events;
const isLastEvent = index === _events.size - 1;
const isLastInGroup = isLastEvent || _events.get(index + 1).type === TYPES.LOCATION;
@ -148,7 +146,8 @@ export default class EventsBlock extends React.PureComponent {
>
{({measure, registerChild}) => (
<div style={style} ref={registerChild}>
<EventGroupWrapper
<EventGroupWrapper
query={query}
presentInSearch={eventsIndex.includes(index)}
isFirst={index==0}
mesureHeight={measure}

View file

@ -337,6 +337,8 @@ export default class MessageDistributor extends StatedScreen {
distributeMessage = (msg: TimedMessage, index: number): void => {
if ([
"mouse_move",
"mouse_click",
"create_element_node", // not a user activity, though visual change
"set_input_value",
"set_input_checked",
"set_viewport_size",

View file

@ -155,7 +155,10 @@ export default class AssistManager {
});
this.peer = peer;
peer.on('error', e => {
if (['peer-unavailable', 'network'].includes(e.type)) {
if (e.type !== 'peer-unavailable') {
console.warn("AssistManager PeerJS peer error: ", e.type, e)
}
if (['peer-unavailable', 'network', 'webrtc'].includes(e.type)) {
if (this.peer && this.connectionAttempts++ < MAX_RECONNECTION_COUNT) {
this.setStatus(ConnectionStatus.Connecting);
console.log("peerunavailable")
@ -319,7 +322,6 @@ export default class AssistManager {
return;
}
// @ts-ignore
this.md.display(false);
this.dataConnection?.close();
this.setStatus(ConnectionStatus.Disconnected);
}, 8000); // TODO: more convenient way

View file

@ -73,6 +73,6 @@ try {
? console.log(`Successfully uploaded ${sourceFiles.length} sourcemap file${sourceFiles.length > 1 ? "s" : ""} for: \n`
+ sourceFiles.join("\t\n")
)
: console.log(`No sourcemaps found in ${ args.js_dir_url }`)
: console.log(`No sourcemaps found in ${ args.sourcemap_dir_path }`)
)
.catch(e => console.error(`Sourcemap Uploader: ${e}`));

View file

@ -1,6 +1,6 @@
{
"name": "@openreplay/sourcemap-uploader",
"version": "3.0.4",
"version": "3.0.5",
"description": "NPM module to upload your JS sourcemaps files to OpenReplay",
"bin": "cli.js",
"main": "index.js",

View file

@ -1,6 +1,6 @@
{
"name": "@openreplay/tracker-assist",
"version": "3.0.0",
"version": "3.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker-assist",
"description": "Tracker plugin for screen assistance through the WebRTC",
"version": "3.0.0",
"version": "3.0.2",
"keywords": [
"WebRTC",
"assistance",

View file

@ -43,14 +43,15 @@ export default function(opts: Partial<Options> = {}) {
path: '/assist',
port: location.protocol === 'http:' && appOptions.__DISABLE_SECURE_MODE ? 80 : 443,
});
console.log(peerID)
console.log('OpenReplay tracker-assist peerID:', peerID)
peer.on('connection', function(conn) {
window.addEventListener("beforeunload", () => conn.open && conn.send("unload"));
console.log('connection')
peer.on('error', e => console.log("OpenReplay tracker-assist peer error: ", e.type, e))
console.log('OpenReplay tracker-assist: Connecting...')
conn.on('open', function() {
console.log('connection open')
console.log('OpenReplay tracker-assist: connection opened.')
// TODO: onClose
const buffer: Message[][] = [];
@ -99,7 +100,7 @@ export default function(opts: Partial<Options> = {}) {
const confirm = new Confirm(options.confirmText, options.confirmStyle);
dataConn.on('data', (data) => { // if call closed by a caller before confirm
if (data === "call_end") {
console.log('receiving callend onconfirm')
//console.log('OpenReplay tracker-assist: receiving callend onconfirm')
calling = CallingState.False;
confirm.remove();
}
@ -116,17 +117,16 @@ export default function(opts: Partial<Options> = {}) {
const mouse = new Mouse();
let callUI;
navigator.mediaDevices.getUserMedia({video:true, audio:true})
.then(lStream => {
const onCallConnect = lStream => {
const onCallEnd = () => {
console.log("on callend", call.open)
//console.log("on callend", call.open)
mouse.remove();
callUI?.remove();
lStream.getTracks().forEach(t => t.stop());
calling = CallingState.False;
}
const initiateCallEnd = () => {
console.log("callend initiated")
//console.log("callend initiated")
call.close()
notifyCallEnd();
onCallEnd();
@ -166,12 +166,12 @@ export default function(opts: Partial<Options> = {}) {
callUI.setRemoteStream(rStream);
dataConn.on('data', (data: any) => {
if (data === "call_end") {
console.log('receiving callend on call')
//console.log('receiving callend on call')
onCallEnd();
return;
}
if (data && typeof data.name === 'string') {
console.log("name",data)
//console.log("name",data)
callUI.setAssistentName(data.name);
}
if (data && typeof data.x === 'number' && typeof data.y === 'number') {
@ -179,6 +179,14 @@ export default function(opts: Partial<Options> = {}) {
}
});
});
}
navigator.mediaDevices.getUserMedia({video:true, audio:true})
.then(onCallConnect)
.catch(_ => { // TODO retry only if specific error
navigator.mediaDevices.getUserMedia({audio:true}) // in case there is no camera on device
.then(onCallConnect)
.catch(e => console.log("OpenReplay tracker-assist: cant reach media devices. ", e));
});
});
});

View file

@ -1,6 +1,6 @@
{
"name": "@openreplay/tracker",
"version": "3.1.0",
"version": "3.2.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "3.2.0",
"version": "3.2.1",
"keywords": [
"logging",
"replay"

View file

@ -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';

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 20182020 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.

View file

@ -0,0 +1,132 @@
![finder](https://medv.io/assets/finder.png)
# finder
[![npm](https://img.shields.io/npm/v/@medv/finder?color=grightgreen)](https://www.npmjs.com/package/@medv/finder)
[![Build status](https://img.shields.io/travis/antonmedv/finder)](https://travis-ci.org/antonmedv/finder)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@medv/finder?label=size)](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
<script type="module">
import {finder} from 'https://medv.io/finder/finder.js'
</script>
```
## 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
![Chrome Extension](https://user-images.githubusercontent.com/141232/36737287-4a999d84-1c0d-11e8-8a14-43bcf9baf7ca.png)
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)

View file

@ -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<Options>): string;

View file

@ -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 its not a printable ASCII character…
if (codePoint < 0x20 || codePoint > 0x7E) {
if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) {
// Its 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 {
// Its 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 theyre redundant. Note that this is only possible if the escape
// sequence isnt preceded by an odd number of backslashes.
output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
if ($1 && $1.length % 2) {
// Its not safe to remove the space, so dont.
return $0;
}
// Strip the space.
return ($1 || "") + $2;
});
if (!isIdentifier && options.wrap) {
return quote + output + quote;
}
return output;
}

View file

@ -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<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: 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<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined
}
function* combinations(stack: Node[][], path: Node[] = []): Generator<Node[]> {
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>): Path[] {
return Array.from(paths).sort((a, b) => penalty(a) - penalty(b))
}
type Scope = {
counter: number
visited: Map<string, boolean>
}
function* optimize(path: Path, input: Element, scope: Scope = {
counter: 0,
visited: new Map<string, boolean>()
}): Generator<Node[]> {
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<typeof defaultOptions> = {}) {
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 its not a printable ASCII character…
if (codePoint < 0x20 || codePoint > 0x7E) {
if (codePoint >= 0xD800 && codePoint <= 0xDBFF && counter < length) {
// Its 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 {
// Its 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 theyre redundant. Note that this is only possible if the escape
// sequence isnt preceded by an odd number of backslashes.
output = output.replace(regexExcessiveSpaces, function ($0, $1, $2) {
if ($1 && $1.length % 2) {
// Its not safe to remove the space, so dont.
return $0
}
// Strip the space.
return ($1 || "") + $2
})
if (!isIdentifier && options.wrap) {
return quote + output + quote
}
return output
}

View file

@ -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"
}

View file

@ -1,5 +1,5 @@
var sourcemapsReaderServer = require('./servers/sourcemaps-server');
var {peerRouter, peerConnection, peerDisconnect} = require('./servers/peerjs-server');
var {peerRouter, peerConnection, peerDisconnect, peerError} = require('./servers/peerjs-server');
var express = require('express');
const {ExpressPeerServer} = require('peer');
@ -28,6 +28,7 @@ const peerServer = ExpressPeerServer(server, {
});
peerServer.on('connection', peerConnection);
peerServer.on('disconnect', peerDisconnect);
peerServer.on('error', peerError);
app.use('/', peerServer);
app.enable('trust proxy');
module.exports = server;

View file

@ -42,6 +42,11 @@ const peerDisconnect = (client) => {
}
}
const peerError = (error) => {
console.error('error fired');
console.error(error);
}
peerRouter.get('/peers', function (req, res) {
console.log("looking for all available sessions");
@ -60,5 +65,6 @@ peerRouter.get('/peers/:projectKey', function (req, res) {
module.exports = {
peerRouter,
peerConnection,
peerDisconnect
peerDisconnect,
peerError
};