Spot network refactoring (#2617)
* start refactoring network * separate network module, refactor spot network capture Signed-off-by: nick-delirium <nikita@openreplay.com> * some console refactoring, display network results in ui * detect gql error param * fix proxy ignore file, fix network tracking, fix tab tracking * some code quality improvements... * handle graphql in network lib (.2 ver), update tracker to use last version of lib * remove debug logs, change request type to gql (if its gql!) in lib, display gql in ui --------- Signed-off-by: nick-delirium <nikita@openreplay.com>
This commit is contained in:
parent
264abc986f
commit
e66423dcf4
40 changed files with 1128 additions and 684 deletions
|
|
@ -43,6 +43,8 @@ const mapSpotNetworkToEv = (ev: SpotNetworkRequest): any => {
|
||||||
return 'xhr';
|
return 'xhr';
|
||||||
case 'fetch':
|
case 'fetch':
|
||||||
return 'fetch';
|
return 'fetch';
|
||||||
|
case 'graphql':
|
||||||
|
return 'graphql';
|
||||||
case 'resource':
|
case 'resource':
|
||||||
return 'resource';
|
return 'resource';
|
||||||
default:
|
default:
|
||||||
|
|
@ -56,7 +58,7 @@ const mapSpotNetworkToEv = (ev: SpotNetworkRequest): any => {
|
||||||
})
|
})
|
||||||
const response = JSON.stringify({
|
const response = JSON.stringify({
|
||||||
headers: ev.responseHeaders,
|
headers: ev.responseHeaders,
|
||||||
body: { warn: "Chrome Manifest V3 -- No response body available in Chrome 93+" }
|
body: ev.responseBody ?? { warn: "Chrome Manifest V3 -- No response body available in Chrome 93+" }
|
||||||
})
|
})
|
||||||
return ({
|
return ({
|
||||||
...ev,
|
...ev,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export const enum ResourceType {
|
||||||
IMG = 'img',
|
IMG = 'img',
|
||||||
MEDIA = 'media',
|
MEDIA = 'media',
|
||||||
WS = 'websocket',
|
WS = 'websocket',
|
||||||
|
GRAPHQL = 'graphql',
|
||||||
OTHER = 'other',
|
OTHER = 'other',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +48,8 @@ export function getResourceType(initiator: string, url: string): ResourceType {
|
||||||
case "avi":
|
case "avi":
|
||||||
case "mp3":
|
case "mp3":
|
||||||
return ResourceType.MEDIA
|
return ResourceType.MEDIA
|
||||||
|
case "graphql":
|
||||||
|
return ResourceType.GRAPHQL
|
||||||
default:
|
default:
|
||||||
return ResourceType.OTHER
|
return ResourceType.OTHER
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
networkProxy/.gitignore
vendored
Normal file
31
networkProxy/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.output
|
||||||
|
stats.html
|
||||||
|
stats-*.json
|
||||||
|
.wxt
|
||||||
|
web-ext.config.ts
|
||||||
|
|
||||||
|
!public
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
dist/*
|
||||||
|
dist
|
||||||
19
networkProxy/LICENSE
Normal file
19
networkProxy/LICENSE
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2024 Asayer, Inc
|
||||||
|
|
||||||
|
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.
|
||||||
46
networkProxy/README.md
Normal file
46
networkProxy/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
this tiny library helps us (OpenReplay folks) to create proxy objects for fetch,
|
||||||
|
XHR and beacons for proper request tracking in @openreplay/tracker and Spot extension.
|
||||||
|
|
||||||
|
example usage:
|
||||||
|
```
|
||||||
|
import createNetworkProxy from '@openreplay/network-proxy';
|
||||||
|
|
||||||
|
const context = this;
|
||||||
|
const ignoreHeaders = ['Authorization'];
|
||||||
|
const tokenUrlMatcher = /\/auth\/token/;
|
||||||
|
function setSessionTokenHeader(setRequestHeader: (name: string, value: string) => void) {
|
||||||
|
const header = 'X-Session-Token
|
||||||
|
const sessionToken = getToken() // for exmaple, => `session #123123`;
|
||||||
|
if (sessionToken) {
|
||||||
|
setRequestHeader(header, sessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function sanitize(reqResInfo) {
|
||||||
|
if (reqResInfo.request) {
|
||||||
|
delete reqResInfo.request.body
|
||||||
|
}
|
||||||
|
return reqResInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMsg = (networkReq) => console.log(networkReq)
|
||||||
|
const isIgnoredUrl = (url) => url.includes('google.com')
|
||||||
|
|
||||||
|
// Gets current tracker request’s url and returns boolean. If present,
|
||||||
|
// sessionTokenHeader will only be applied when this function returns true.
|
||||||
|
// Default: undefined
|
||||||
|
const tokenUrlMatcher = (url) => url.includes('google.com');
|
||||||
|
|
||||||
|
// this will observe global network requests
|
||||||
|
createNetworkProxy(
|
||||||
|
context,
|
||||||
|
options.ignoreHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
(message) => app.send(message),
|
||||||
|
(url) => app.isServiceURL(url),
|
||||||
|
options.tokenUrlMatcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
// to stop it, you can save this.fetch/other apis before appliying the proxy
|
||||||
|
// and then restore them
|
||||||
|
```
|
||||||
29
networkProxy/package-lock.json
generated
Normal file
29
networkProxy/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "network-proxy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "network-proxy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||||
|
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
networkProxy/package.json
Normal file
19
networkProxy/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@openreplay/network-proxy",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"description": "this library helps us to create proxy objects for fetch, XHR and beacons for proper request tracking.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"author": "Nikita <nikita@openreplay.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { NetworkRequest } from '../../../common/messages.gen.js'
|
import NetworkMessage from './networkMessage'
|
||||||
import NetworkMessage from './networkMessage.js'
|
import { RequestState, INetworkMessage, RequestResponseData } from './types';
|
||||||
import { RequestResponseData } from './types.js'
|
import { genStringBody, getURL } from './utils'
|
||||||
import { genStringBody, getURL } from './utils.js'
|
|
||||||
|
|
||||||
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
|
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
|
||||||
const getContentType = (data?: BodyInit) => {
|
const getContentType = (data?: BodyInit) => {
|
||||||
|
|
@ -22,7 +21,7 @@ export class BeaconProxyHandler<T extends typeof navigator.sendBeacon> implement
|
||||||
private readonly ignoredHeaders: boolean | string[],
|
private readonly ignoredHeaders: boolean | string[],
|
||||||
private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
||||||
private readonly sendMessage: (item: NetworkRequest) => void,
|
private readonly sendMessage: (item: INetworkMessage) => void,
|
||||||
private readonly isServiceUrl: (url: string) => boolean,
|
private readonly isServiceUrl: (url: string) => boolean,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
@ -85,7 +84,7 @@ export default class BeaconProxy {
|
||||||
ignoredHeaders: boolean | string[],
|
ignoredHeaders: boolean | string[],
|
||||||
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
||||||
sendMessage: (item: NetworkRequest) => void,
|
sendMessage: (item: INetworkMessage) => void,
|
||||||
isServiceUrl: (url: string) => boolean,
|
isServiceUrl: (url: string) => boolean,
|
||||||
) {
|
) {
|
||||||
if (!BeaconProxy.hasSendBeacon()) {
|
if (!BeaconProxy.hasSendBeacon()) {
|
||||||
|
|
@ -5,10 +5,9 @@
|
||||||
* we can intercept the network requests
|
* we can intercept the network requests
|
||||||
* in not-so-hacky way
|
* in not-so-hacky way
|
||||||
* */
|
* */
|
||||||
import NetworkMessage, { RequestState } from './networkMessage.js'
|
import NetworkMessage from './networkMessage'
|
||||||
import { formatByteSize, genStringBody, getStringResponseByType, getURL } from './utils.js'
|
import { RequestState, INetworkMessage, RequestResponseData } from './types';
|
||||||
import { RequestResponseData } from './types.js'
|
import { formatByteSize, genStringBody, getStringResponseByType, getURL } from './utils'
|
||||||
import { NetworkRequest } from '../../../common/messages.gen.js'
|
|
||||||
|
|
||||||
export class ResponseProxyHandler<T extends Response> implements ProxyHandler<T> {
|
export class ResponseProxyHandler<T extends Response> implements ProxyHandler<T> {
|
||||||
public resp: Response
|
public resp: Response
|
||||||
|
|
@ -123,7 +122,7 @@ export class FetchProxyHandler<T extends typeof fetch> implements ProxyHandler<T
|
||||||
private readonly ignoredHeaders: boolean | string[],
|
private readonly ignoredHeaders: boolean | string[],
|
||||||
private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
||||||
private readonly sendMessage: (item: NetworkRequest) => void,
|
private readonly sendMessage: (item: INetworkMessage) => void,
|
||||||
private readonly isServiceUrl: (url: string) => boolean,
|
private readonly isServiceUrl: (url: string) => boolean,
|
||||||
private readonly tokenUrlMatcher?: (url: string) => boolean,
|
private readonly tokenUrlMatcher?: (url: string) => boolean,
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -311,7 +310,7 @@ export default class FetchProxy {
|
||||||
ignoredHeaders: boolean | string[],
|
ignoredHeaders: boolean | string[],
|
||||||
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
||||||
sendMessage: (item: NetworkRequest) => void,
|
sendMessage: (item: INetworkMessage) => void,
|
||||||
isServiceUrl: (url: string) => boolean,
|
isServiceUrl: (url: string) => boolean,
|
||||||
tokenUrlMatcher?: (url: string) => boolean,
|
tokenUrlMatcher?: (url: string) => boolean,
|
||||||
) {
|
) {
|
||||||
96
networkProxy/src/index.ts
Normal file
96
networkProxy/src/index.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import BeaconProxy from "./beaconProxy";
|
||||||
|
import FetchProxy from "./fetchProxy";
|
||||||
|
import XHRProxy from "./xhrProxy";
|
||||||
|
import { INetworkMessage, RequestResponseData } from "./types";
|
||||||
|
|
||||||
|
export {
|
||||||
|
BeaconProxy,
|
||||||
|
FetchProxy,
|
||||||
|
XHRProxy,
|
||||||
|
INetworkMessage,
|
||||||
|
RequestResponseData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWarning = (api: string) => {
|
||||||
|
const str = `Openreplay: Can't find ${api} in global context.`;
|
||||||
|
console.warn(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates network proxies for XMLHttpRequest, fetch, and sendBeacon to intercept and monitor network requests and
|
||||||
|
* responses.
|
||||||
|
*
|
||||||
|
* @param {Window | typeof globalThis} context - The global context object (e.g., window or globalThis).
|
||||||
|
* @param {boolean | string[]} ignoredHeaders - Headers to ignore from requests. If `true`, all headers are ignored; if
|
||||||
|
* an array of strings, those header names are ignored.
|
||||||
|
* @param {(cb: (name: string, value: string) => void) => void} setSessionTokenHeader - Function to set a session token
|
||||||
|
* header; accepts a callback that sets the header name and value.
|
||||||
|
* @param {(data: RequestResponseData) => RequestResponseData | null} sanitize - Function to sanitize request and
|
||||||
|
* response data; should return sanitized data or `null` to ignore the data.
|
||||||
|
* @param {(message: INetworkMessage) => void} sendMessage - Function to send network messages for further processing
|
||||||
|
* or logging.
|
||||||
|
* @param {(url: string) => boolean} isServiceUrl - Function to determine if a URL is a service URL that should be
|
||||||
|
* ignored by the proxy.
|
||||||
|
* @param {Object} [modules] - Modules to apply the proxies to.
|
||||||
|
* @param {boolean} [modules.xhr=true] - Whether to proxy XMLHttpRequest.
|
||||||
|
* @param {boolean} [modules.fetch=true] - Whether to proxy the fetch API.
|
||||||
|
* @param {boolean} [modules.beacon=true] - Whether to proxy navigator.sendBeacon.
|
||||||
|
* @param {(url: string) => boolean} [tokenUrlMatcher] - Optional function; the session token header will only be
|
||||||
|
* applied to requests matching this function.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export default function createNetworkProxy(
|
||||||
|
context: typeof globalThis,
|
||||||
|
ignoredHeaders: boolean | string[],
|
||||||
|
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
|
sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
||||||
|
sendMessage: (message: INetworkMessage) => void,
|
||||||
|
isServiceUrl: (url: string) => boolean,
|
||||||
|
modules: { xhr: boolean; fetch: boolean; beacon: boolean } = {
|
||||||
|
xhr: true,
|
||||||
|
fetch: true,
|
||||||
|
beacon: true,
|
||||||
|
},
|
||||||
|
tokenUrlMatcher?: (url: string) => boolean,
|
||||||
|
): void {
|
||||||
|
if (modules.xhr) {
|
||||||
|
if (context.XMLHttpRequest) {
|
||||||
|
context.XMLHttpRequest = XHRProxy.create(
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
tokenUrlMatcher,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
getWarning("XMLHttpRequest");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modules.fetch) {
|
||||||
|
if (context.fetch) {
|
||||||
|
context.fetch = FetchProxy.create(
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
tokenUrlMatcher,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
getWarning("fetch");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modules.beacon) {
|
||||||
|
if (context?.navigator?.sendBeacon) {
|
||||||
|
context.navigator.sendBeacon = BeaconProxy.create(
|
||||||
|
ignoredHeaders,
|
||||||
|
setSessionTokenHeader,
|
||||||
|
sanitize,
|
||||||
|
sendMessage,
|
||||||
|
isServiceUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,9 @@
|
||||||
import { NetworkRequest } from '../../app/messages.gen.js'
|
import {
|
||||||
import { RequestResponseData } from './types.js'
|
RequestResponseData,
|
||||||
import { getTimeOrigin } from '../../utils.js'
|
INetworkMessage,
|
||||||
|
httpMethod,
|
||||||
export type httpMethod =
|
RequestState,
|
||||||
// '' is a rare case of error
|
} from './types'
|
||||||
'' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'
|
|
||||||
|
|
||||||
export enum RequestState {
|
|
||||||
UNSENT = 0,
|
|
||||||
OPENED = 1,
|
|
||||||
HEADERS_RECEIVED = 2,
|
|
||||||
LOADING = 3,
|
|
||||||
DONE = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I know we're not using most of the information from this class
|
* I know we're not using most of the information from this class
|
||||||
* but it can be useful in the future if we will decide to display more stuff in our ui
|
* but it can be useful in the future if we will decide to display more stuff in our ui
|
||||||
|
|
@ -30,9 +20,9 @@ export default class NetworkMessage {
|
||||||
readyState?: RequestState = 0
|
readyState?: RequestState = 0
|
||||||
header: { [key: string]: string } = {}
|
header: { [key: string]: string } = {}
|
||||||
responseType: XMLHttpRequest['responseType'] = ''
|
responseType: XMLHttpRequest['responseType'] = ''
|
||||||
requestType: 'xhr' | 'fetch' | 'ping' | 'custom' | 'beacon'
|
requestType: 'xhr' | 'fetch' | 'ping' | 'custom' | 'beacon' | 'graphql' = 'xhr'
|
||||||
requestHeader: HeadersInit = {}
|
requestHeader: HeadersInit = {}
|
||||||
response: any
|
response: string
|
||||||
responseSize = 0 // bytes
|
responseSize = 0 // bytes
|
||||||
responseSizeText = ''
|
responseSizeText = ''
|
||||||
startTime = 0
|
startTime = 0
|
||||||
|
|
@ -47,7 +37,7 @@ export default class NetworkMessage {
|
||||||
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getMessage() {
|
getMessage(): INetworkMessage | null {
|
||||||
const { reqHs, resHs } = this.writeHeaders()
|
const { reqHs, resHs } = this.writeHeaders()
|
||||||
const request = {
|
const request = {
|
||||||
headers: reqHs,
|
headers: reqHs,
|
||||||
|
|
@ -63,19 +53,26 @@ export default class NetworkMessage {
|
||||||
response,
|
response,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!messageInfo) return
|
if (!messageInfo) return null;
|
||||||
|
|
||||||
return NetworkRequest(
|
const isGraphql = messageInfo.url.includes("/graphql");
|
||||||
this.requestType,
|
if (isGraphql && messageInfo.response.body && typeof messageInfo.response.body === 'string') {
|
||||||
messageInfo.method,
|
const isError = messageInfo.response.body.includes("errors");
|
||||||
messageInfo.url,
|
messageInfo.status = isError ? 400 : 200;
|
||||||
JSON.stringify(messageInfo.request),
|
this.requestType = 'graphql';
|
||||||
JSON.stringify(messageInfo.response),
|
}
|
||||||
messageInfo.status,
|
|
||||||
this.startTime + getTimeOrigin(),
|
return {
|
||||||
this.duration,
|
requestType: this.requestType,
|
||||||
this.responseSize,
|
method: messageInfo.method as httpMethod,
|
||||||
)
|
url: messageInfo.url,
|
||||||
|
request: JSON.stringify(messageInfo.request),
|
||||||
|
response: JSON.stringify(messageInfo.response),
|
||||||
|
status: messageInfo.status,
|
||||||
|
startTime: this.startTime,
|
||||||
|
duration: this.duration,
|
||||||
|
responseSize: this.responseSize,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
writeHeaders() {
|
writeHeaders() {
|
||||||
39
networkProxy/src/types.ts
Normal file
39
networkProxy/src/types.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
export interface RequestResponseData {
|
||||||
|
status: number
|
||||||
|
readonly method: string
|
||||||
|
url: string
|
||||||
|
request: {
|
||||||
|
body: string | null
|
||||||
|
headers: Record<string, string>
|
||||||
|
}
|
||||||
|
response: {
|
||||||
|
body: string | null
|
||||||
|
headers: Record<string, string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INetworkMessage {
|
||||||
|
requestType: 'xhr' | 'fetch' | 'ping' | 'custom' | 'beacon' | 'graphql',
|
||||||
|
method: httpMethod,
|
||||||
|
url: string,
|
||||||
|
/** stringified JSON { headers: {}, body: {} } */
|
||||||
|
request: string,
|
||||||
|
/** stringified JSON { headers: {}, body: {} } */
|
||||||
|
response: string,
|
||||||
|
status: number,
|
||||||
|
startTime: number,
|
||||||
|
duration: number,
|
||||||
|
responseSize: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type httpMethod =
|
||||||
|
// '' is a rare case of error
|
||||||
|
'' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'CONNECT' | 'OPTIONS' | 'TRACE' | 'PATCH'
|
||||||
|
|
||||||
|
export enum RequestState {
|
||||||
|
UNSENT = 0,
|
||||||
|
OPENED = 1,
|
||||||
|
HEADERS_RECEIVED = 2,
|
||||||
|
LOADING = 3,
|
||||||
|
DONE = 4,
|
||||||
|
}
|
||||||
|
|
@ -6,10 +6,9 @@
|
||||||
* in not-so-hacky way
|
* in not-so-hacky way
|
||||||
* */
|
* */
|
||||||
|
|
||||||
import NetworkMessage, { RequestState } from './networkMessage.js'
|
import NetworkMessage from './networkMessage'
|
||||||
import { genGetDataByUrl, formatByteSize, genStringBody, getStringResponseByType } from './utils.js'
|
import { RequestState, INetworkMessage, RequestResponseData } from './types';
|
||||||
import { RequestResponseData } from './types.js'
|
import { genGetDataByUrl, formatByteSize, genStringBody, getStringResponseByType } from './utils'
|
||||||
import { NetworkRequest } from '../../../common/messages.gen.js'
|
|
||||||
|
|
||||||
export class XHRProxyHandler<T extends XMLHttpRequest> implements ProxyHandler<T> {
|
export class XHRProxyHandler<T extends XMLHttpRequest> implements ProxyHandler<T> {
|
||||||
public XMLReq: XMLHttpRequest
|
public XMLReq: XMLHttpRequest
|
||||||
|
|
@ -20,7 +19,7 @@ export class XHRProxyHandler<T extends XMLHttpRequest> implements ProxyHandler<T
|
||||||
private readonly ignoredHeaders: boolean | string[],
|
private readonly ignoredHeaders: boolean | string[],
|
||||||
private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
private readonly setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
private readonly sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
||||||
private readonly sendMessage: (message: NetworkRequest) => void,
|
private readonly sendMessage: (message: INetworkMessage) => void,
|
||||||
private readonly isServiceUrl: (url: string) => boolean,
|
private readonly isServiceUrl: (url: string) => boolean,
|
||||||
private readonly tokenUrlMatcher?: (url: string) => boolean,
|
private readonly tokenUrlMatcher?: (url: string) => boolean,
|
||||||
) {
|
) {
|
||||||
|
|
@ -239,7 +238,7 @@ export default class XHRProxy {
|
||||||
ignoredHeaders: boolean | string[],
|
ignoredHeaders: boolean | string[],
|
||||||
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
||||||
sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
||||||
sendMessage: (data: NetworkRequest) => void,
|
sendMessage: (data: INetworkMessage) => void,
|
||||||
isServiceUrl: (url: string) => boolean,
|
isServiceUrl: (url: string) => boolean,
|
||||||
tokenUrlMatcher?: (url: string) => boolean,
|
tokenUrlMatcher?: (url: string) => boolean,
|
||||||
) {
|
) {
|
||||||
14
networkProxy/tsconfig.json
Normal file
14
networkProxy/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"module": "ES2022",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
BIN
spot/bun.lockb
Executable file
BIN
spot/bun.lockb
Executable file
Binary file not shown.
|
|
@ -1,13 +1,4 @@
|
||||||
import {
|
import { isTokenExpired } from "~/utils/jwt";
|
||||||
createSpotNetworkRequest,
|
|
||||||
stopTrackingNetwork,
|
|
||||||
startTrackingNetwork,
|
|
||||||
SpotNetworkRequest,
|
|
||||||
rawRequests,
|
|
||||||
} from "../utils/networkTracking";
|
|
||||||
import {
|
|
||||||
isTokenExpired
|
|
||||||
} from '../utils/jwt';
|
|
||||||
|
|
||||||
let checkBusy = false;
|
let checkBusy = false;
|
||||||
|
|
||||||
|
|
@ -65,6 +56,7 @@ export default defineBackground(() => {
|
||||||
injected: {
|
injected: {
|
||||||
from: {
|
from: {
|
||||||
bumpLogs: "ort:bump-logs",
|
bumpLogs: "ort:bump-logs",
|
||||||
|
bumpNetwork: "ort:bump-network",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
offscreen: {
|
offscreen: {
|
||||||
|
|
@ -218,7 +210,7 @@ export default defineBackground(() => {
|
||||||
setJWTToken("");
|
setJWTToken("");
|
||||||
}
|
}
|
||||||
const { jwtToken, settings } = data;
|
const { jwtToken, settings } = data;
|
||||||
const refreshUrl = `${safeApiUrl(settings.ingestPoint)}/api/spot/refresh`
|
const refreshUrl = `${safeApiUrl(settings.ingestPoint)}/api/spot/refresh`;
|
||||||
if (!isTokenExpired(jwtToken) || !jwtToken) {
|
if (!isTokenExpired(jwtToken) || !jwtToken) {
|
||||||
if (refreshInt) {
|
if (refreshInt) {
|
||||||
clearInterval(refreshInt);
|
clearInterval(refreshInt);
|
||||||
|
|
@ -305,12 +297,12 @@ export default defineBackground(() => {
|
||||||
void browser.runtime.sendMessage({
|
void browser.runtime.sendMessage({
|
||||||
type: messages.popup.to.noLogin,
|
type: messages.popup.to.noLogin,
|
||||||
});
|
});
|
||||||
checkBusy = false
|
checkBusy = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ok = await refreshToken();
|
const ok = await refreshToken();
|
||||||
if (ok) {
|
if (ok) {
|
||||||
setJWTToken(data.jwtToken)
|
setJWTToken(data.jwtToken);
|
||||||
if (!refreshInt) {
|
if (!refreshInt) {
|
||||||
refreshInt = setInterval(() => {
|
refreshInt = setInterval(() => {
|
||||||
void refreshToken();
|
void refreshToken();
|
||||||
|
|
@ -322,7 +314,7 @@ export default defineBackground(() => {
|
||||||
}, PING_INT);
|
}, PING_INT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkBusy = false
|
checkBusy = false;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
browser.runtime.onMessage.addListener((request, sender, respond) => {
|
browser.runtime.onMessage.addListener((request, sender, respond) => {
|
||||||
|
|
@ -408,6 +400,9 @@ export default defineBackground(() => {
|
||||||
settings.networkLogs,
|
settings.networkLogs,
|
||||||
settings.consoleLogs,
|
settings.consoleLogs,
|
||||||
() => recordingState.recording,
|
() => recordingState.recording,
|
||||||
|
(hook) => {
|
||||||
|
onStop = hook;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
// @ts-ignore this is false positive
|
// @ts-ignore this is false positive
|
||||||
respond(true);
|
respond(true);
|
||||||
|
|
@ -579,6 +574,10 @@ export default defineBackground(() => {
|
||||||
finalSpotObj.logs.push(...request.logs);
|
finalSpotObj.logs.push(...request.logs);
|
||||||
return "pong";
|
return "pong";
|
||||||
}
|
}
|
||||||
|
if (request.type === messages.injected.from.bumpNetwork) {
|
||||||
|
finalSpotObj.network.push(request.event);
|
||||||
|
return "pong";
|
||||||
|
}
|
||||||
if (request.type === messages.content.from.bumpClicks) {
|
if (request.type === messages.content.from.bumpClicks) {
|
||||||
finalSpotObj.clicks.push(...request.clicks);
|
finalSpotObj.clicks.push(...request.clicks);
|
||||||
return "pong";
|
return "pong";
|
||||||
|
|
@ -739,25 +738,7 @@ export default defineBackground(() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (request.type === messages.content.from.saveSpotData) {
|
if (request.type === messages.content.from.saveSpotData) {
|
||||||
stopTrackingNetwork();
|
Object.assign(finalSpotObj, request.spot);
|
||||||
const finalNetwork: SpotNetworkRequest[] = [];
|
|
||||||
const tab =
|
|
||||||
recordingState.area === "tab" ? recordingState.activeTabId : undefined;
|
|
||||||
let lastIn = 0;
|
|
||||||
try {
|
|
||||||
rawRequests.forEach((r, i) => {
|
|
||||||
lastIn = i;
|
|
||||||
const spotNetworkRequest = createSpotNetworkRequest(r, tab);
|
|
||||||
if (spotNetworkRequest) {
|
|
||||||
finalNetwork.push(spotNetworkRequest);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("cant parse network", e, rawRequests[lastIn]);
|
|
||||||
}
|
|
||||||
Object.assign(finalSpotObj, request.spot, {
|
|
||||||
network: finalNetwork,
|
|
||||||
});
|
|
||||||
return "pong";
|
return "pong";
|
||||||
}
|
}
|
||||||
if (request.type === messages.content.from.saveSpotVidChunk) {
|
if (request.type === messages.content.from.saveSpotVidChunk) {
|
||||||
|
|
@ -897,7 +878,7 @@ export default defineBackground(() => {
|
||||||
url: `${link}/view-spot/${id}`,
|
url: `${link}/view-spot/${id}`,
|
||||||
active: settings.openInNewTab,
|
active: settings.openInNewTab,
|
||||||
});
|
});
|
||||||
}, 250)
|
}, 250);
|
||||||
const blob = base64ToBlob(videoData);
|
const blob = base64ToBlob(videoData);
|
||||||
|
|
||||||
const mPromise = fetch(mobURL, {
|
const mPromise = fetch(mobURL, {
|
||||||
|
|
@ -974,7 +955,7 @@ export default defineBackground(() => {
|
||||||
|
|
||||||
async function initializeOffscreenDocument() {
|
async function initializeOffscreenDocument() {
|
||||||
const existingContexts = await browser.runtime.getContexts({
|
const existingContexts = await browser.runtime.getContexts({
|
||||||
contextTypes: ['OFFSCREEN_DOCUMENT'],
|
contextTypes: ["OFFSCREEN_DOCUMENT"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const offscreenDocument = existingContexts.find(
|
const offscreenDocument = existingContexts.find(
|
||||||
|
|
@ -997,7 +978,7 @@ export default defineBackground(() => {
|
||||||
justification: "Recording from chrome.tabCapture API",
|
justification: "Recording from chrome.tabCapture API",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('cant create new offscreen document', e)
|
console.error("cant create new offscreen document", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -1025,7 +1006,12 @@ export default defineBackground(() => {
|
||||||
if (contentArmy[sendTo]) {
|
if (contentArmy[sendTo]) {
|
||||||
await browser.tabs.sendMessage(sendTo, message);
|
await browser.tabs.sendMessage(sendTo, message);
|
||||||
} else {
|
} else {
|
||||||
console.error("Content script might not be ready in tab", sendTo);
|
console.trace(
|
||||||
|
"Content script might not be ready in tab",
|
||||||
|
sendTo,
|
||||||
|
contentArmy,
|
||||||
|
message,
|
||||||
|
);
|
||||||
await browser.tabs.sendMessage(sendTo, message);
|
await browser.tabs.sendMessage(sendTo, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1063,7 +1049,7 @@ export default defineBackground(() => {
|
||||||
withNetwork: boolean,
|
withNetwork: boolean,
|
||||||
withConsole: boolean,
|
withConsole: boolean,
|
||||||
getRecState: () => string,
|
getRecState: () => string,
|
||||||
setOnStop?: (hook: any) => void,
|
setOnStop: (hook: any) => void,
|
||||||
) {
|
) {
|
||||||
let activeTabs = await browser.tabs.query({
|
let activeTabs = await browser.tabs.query({
|
||||||
active: true,
|
active: true,
|
||||||
|
|
@ -1108,17 +1094,16 @@ export default defineBackground(() => {
|
||||||
slackChannels,
|
slackChannels,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
withConsole,
|
withConsole,
|
||||||
|
withNetwork,
|
||||||
state: "recording",
|
state: "recording",
|
||||||
// by default this is already handled by :start event
|
// by default this is already handled by :start event
|
||||||
// that triggers mount with countdown
|
// that triggers mount with countdown
|
||||||
shouldMount: false,
|
shouldMount: false,
|
||||||
};
|
};
|
||||||
void sendToActiveTab(mountMsg);
|
void sendToActiveTab(mountMsg);
|
||||||
if (withNetwork) {
|
|
||||||
startTrackingNetwork();
|
|
||||||
}
|
|
||||||
|
|
||||||
let previousTab: number | null = usedTab ?? null;
|
let previousTab: number | null = usedTab ?? null;
|
||||||
|
|
||||||
|
/** moves ui to active tab when screen recording */
|
||||||
function tabActivatedListener({ tabId }: { tabId: number }) {
|
function tabActivatedListener({ tabId }: { tabId: number }) {
|
||||||
const state = getRecState();
|
const state = getRecState();
|
||||||
if (state === REC_STATE.stopped) {
|
if (state === REC_STATE.stopped) {
|
||||||
|
|
@ -1147,14 +1132,17 @@ export default defineBackground(() => {
|
||||||
previousTab = tabId;
|
previousTab = tabId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/** moves ui to active tab when screen recording */
|
||||||
function startTabActivationListening() {
|
function startTabActivationListening() {
|
||||||
browser.tabs.onActivated.addListener(tabActivatedListener);
|
browser.tabs.onActivated.addListener(tabActivatedListener);
|
||||||
}
|
}
|
||||||
|
/** moves ui to active tab when screen recording */
|
||||||
function stopTabActivationListening() {
|
function stopTabActivationListening() {
|
||||||
browser.tabs.onActivated.removeListener(tabActivatedListener);
|
browser.tabs.onActivated.removeListener(tabActivatedListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackedTab: number | null = usedTab ?? null;
|
const trackedTab: number | null = usedTab ?? null;
|
||||||
|
/** reloads ui on currently active tab once its reloads itself */
|
||||||
function tabUpdateListener(tabId: number, changeInfo: any) {
|
function tabUpdateListener(tabId: number, changeInfo: any) {
|
||||||
const state = getRecState();
|
const state = getRecState();
|
||||||
if (state === REC_STATE.stopped) {
|
if (state === REC_STATE.stopped) {
|
||||||
|
|
@ -1186,6 +1174,7 @@ export default defineBackground(() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** discards recording if was recording single tab and its now closed */
|
||||||
function tabRemovedListener(tabId: number) {
|
function tabRemovedListener(tabId: number) {
|
||||||
if (tabId === trackedTab) {
|
if (tabId === trackedTab) {
|
||||||
void browser.runtime.sendMessage({
|
void browser.runtime.sendMessage({
|
||||||
|
|
@ -1221,12 +1210,14 @@ export default defineBackground(() => {
|
||||||
|
|
||||||
startTabListening();
|
startTabListening();
|
||||||
if (area === "desktop") {
|
if (area === "desktop") {
|
||||||
|
// if desktop, watch for tab change events
|
||||||
startTabActivationListening();
|
startTabActivationListening();
|
||||||
}
|
}
|
||||||
if (area === "tab") {
|
if (area === "tab") {
|
||||||
|
// if tab, watch for tab remove changes to discard recording
|
||||||
startRemovedListening();
|
startRemovedListening();
|
||||||
}
|
}
|
||||||
setOnStop?.(() => {
|
setOnStop(() => {
|
||||||
stopTabListening();
|
stopTabListening();
|
||||||
if (area === "desktop") {
|
if (area === "desktop") {
|
||||||
stopTabActivationListening();
|
stopTabActivationListening();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import Countdown from "@/entrypoints/content/Countdown";
|
import Countdown from "~/entrypoints/content/Countdown";
|
||||||
import "~/assets/main.css";
|
import "~/assets/main.css";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSignal, onCleanup, createEffect } from "solid-js";
|
import { createSignal, onCleanup, createEffect } from "solid-js";
|
||||||
import { STATES, formatMsToTime } from "@/entrypoints/content/utils";
|
import { STATES, formatMsToTime } from "~/entrypoints/content/utils";
|
||||||
import micOn from "@/assets/mic-on.svg";
|
import micOn from "~/assets/mic-on.svg";
|
||||||
import { createDraggable } from "@neodrag/solid";
|
import { createDraggable } from "@neodrag/solid";
|
||||||
|
|
||||||
interface IRControls {
|
interface IRControls {
|
||||||
|
|
@ -128,7 +128,7 @@ function RecordingControls({
|
||||||
handleRef.classList.remove("popupanimated");
|
handleRef.classList.remove("popupanimated");
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
const audioPerm = getAudioPerm()
|
const audioPerm = getAudioPerm();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={"rec-controls popupanimated cursor-grab"}
|
class={"rec-controls popupanimated cursor-grab"}
|
||||||
|
|
@ -202,7 +202,13 @@ function RecordingControls({
|
||||||
class={`btn btn-sm btn-circle btn-ghost tooltip tooltip-top flex items-center ${
|
class={`btn btn-sm btn-circle btn-ghost tooltip tooltip-top flex items-center ${
|
||||||
mic() ? "bg-black/20" : "bg-black"
|
mic() ? "bg-black/20" : "bg-black"
|
||||||
}`}
|
}`}
|
||||||
data-tip={audioPerm > 0 ? mic() ? "Switch Off Mic" : "Switch On Mic" : "Microphone disabled"}
|
data-tip={
|
||||||
|
audioPerm > 0
|
||||||
|
? mic()
|
||||||
|
? "Switch Off Mic"
|
||||||
|
: "Switch On Mic"
|
||||||
|
: "Microphone disabled"
|
||||||
|
}
|
||||||
onClick={audioPerm > 0 ? toggleMic : undefined}
|
onClick={audioPerm > 0 ? toggleMic : undefined}
|
||||||
>
|
>
|
||||||
{mic() ? (
|
{mic() ? (
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// noinspection SpellCheckingInspection
|
// noinspection SpellCheckingInspection
|
||||||
|
|
||||||
import { createSignal, onCleanup, createEffect } from "solid-js";
|
import { createSignal, onCleanup, createEffect } from "solid-js";
|
||||||
import { formatMsToTime } from "@/entrypoints/content/utils";
|
import { formatMsToTime } from "~/entrypoints/content/utils";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import "./dragControls.css";
|
import "./dragControls.css";
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ function SavingControls({
|
||||||
const trim =
|
const trim =
|
||||||
bounds[0] + bounds[1] === 0
|
bounds[0] + bounds[1] === 0
|
||||||
? null
|
? null
|
||||||
: [Math.floor(bounds[0] * 1000), Math.ceil(bounds[1] * 1000)]
|
: [Math.floor(bounds[0] * 1000), Math.ceil(bounds[1] * 1000)];
|
||||||
const dataObj = {
|
const dataObj = {
|
||||||
blob: videoBlob(),
|
blob: videoBlob(),
|
||||||
name: name(),
|
name: name(),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
startClickRecording,
|
startClickRecording,
|
||||||
stopClickRecording,
|
stopClickRecording,
|
||||||
} from "./eventTrackers";
|
} from "./eventTrackers";
|
||||||
import ControlsBox from "@/entrypoints/content/ControlsBox";
|
import ControlsBox from "~/entrypoints/content/ControlsBox";
|
||||||
|
|
||||||
import { convertBlobToBase64, getChromeFullVersion } from "./utils";
|
import { convertBlobToBase64, getChromeFullVersion } from "./utils";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
@ -253,20 +253,41 @@ export default defineContentScript({
|
||||||
logs: event.data.logs,
|
logs: event.data.logs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (event.data.type === "ort:bump-network") {
|
||||||
|
void chrome.runtime.sendMessage({
|
||||||
|
type: "ort:bump-network",
|
||||||
|
event: event.data.event,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function startConsoleTracking() {
|
let injected = false;
|
||||||
|
function injectScript() {
|
||||||
|
if (injected) return;
|
||||||
|
injected = true;
|
||||||
const scriptEl = document.createElement("script");
|
const scriptEl = document.createElement("script");
|
||||||
scriptEl.src = browser.runtime.getURL("/injected.js");
|
scriptEl.src = browser.runtime.getURL("/injected.js");
|
||||||
document.head.appendChild(scriptEl);
|
document.head.appendChild(scriptEl);
|
||||||
|
}
|
||||||
|
function startConsoleTracking() {
|
||||||
|
injectScript()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.postMessage({ type: "injected:start" });
|
window.postMessage({ type: "injected:c-start" });
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
function startNetworkTracking() {
|
||||||
|
injectScript()
|
||||||
|
setTimeout(() => {
|
||||||
|
window.postMessage({ type: "injected:n-start" });
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
function stopConsoleTracking() {
|
function stopConsoleTracking() {
|
||||||
window.postMessage({ type: "injected:stop" });
|
window.postMessage({ type: "injected:c-stop" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopNetworkTracking() {
|
||||||
|
window.postMessage({ type: "injected:n-stop" });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRestart() {
|
function onRestart() {
|
||||||
|
|
@ -323,7 +344,12 @@ export default defineContentScript({
|
||||||
micResponse = null;
|
micResponse = null;
|
||||||
startClickRecording();
|
startClickRecording();
|
||||||
startLocationRecording();
|
startLocationRecording();
|
||||||
startConsoleTracking();
|
if (message.withConsole) {
|
||||||
|
startConsoleTracking();
|
||||||
|
}
|
||||||
|
if (message.withNetwork) {
|
||||||
|
startNetworkTracking();
|
||||||
|
}
|
||||||
browser.runtime.sendMessage({ type: "ort:started" });
|
browser.runtime.sendMessage({ type: "ort:started" });
|
||||||
if (message.shouldMount) {
|
if (message.shouldMount) {
|
||||||
ui.mount();
|
ui.mount();
|
||||||
|
|
@ -343,6 +369,7 @@ export default defineContentScript({
|
||||||
stopClickRecording();
|
stopClickRecording();
|
||||||
stopLocationRecording();
|
stopLocationRecording();
|
||||||
stopConsoleTracking();
|
stopConsoleTracking();
|
||||||
|
stopNetworkTracking();
|
||||||
recState = "stopped";
|
recState = "stopped";
|
||||||
ui.remove();
|
ui.remove();
|
||||||
return "unmounted";
|
return "unmounted";
|
||||||
|
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
export default defineUnlistedScript(() => {
|
|
||||||
const printError =
|
|
||||||
"InstallTrigger" in window // detect Firefox
|
|
||||||
? (e) => e.message + "\n" + e.stack
|
|
||||||
: (e) => e.stack || e.message;
|
|
||||||
|
|
||||||
function printString(arg) {
|
|
||||||
if (arg === undefined) {
|
|
||||||
return "undefined";
|
|
||||||
}
|
|
||||||
if (arg === null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
if (arg instanceof Error) {
|
|
||||||
return printError(arg);
|
|
||||||
}
|
|
||||||
if (Array.isArray(arg)) {
|
|
||||||
return `Array(${arg.length})`;
|
|
||||||
}
|
|
||||||
return String(arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
function printFloat(arg) {
|
|
||||||
if (typeof arg !== "number") return "NaN";
|
|
||||||
return arg.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function printInt(arg) {
|
|
||||||
if (typeof arg !== "number") return "NaN";
|
|
||||||
return Math.floor(arg).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function printObject(arg) {
|
|
||||||
if (arg === undefined) {
|
|
||||||
return "undefined";
|
|
||||||
}
|
|
||||||
if (arg === null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
if (arg instanceof Error) {
|
|
||||||
return printError(arg);
|
|
||||||
}
|
|
||||||
if (Array.isArray(arg)) {
|
|
||||||
const length = arg.length;
|
|
||||||
const values = arg.slice(0, 10).map(printString).join(", ");
|
|
||||||
return `Array(${length})[${values}]`;
|
|
||||||
}
|
|
||||||
if (typeof arg === "object") {
|
|
||||||
const res = [];
|
|
||||||
let i = 0;
|
|
||||||
for (const k in arg) {
|
|
||||||
if (++i === 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const v = arg[k];
|
|
||||||
res.push(k + ": " + printString(v));
|
|
||||||
}
|
|
||||||
return "{" + res.join(", ") + "}";
|
|
||||||
}
|
|
||||||
return arg.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function printf(args) {
|
|
||||||
if (typeof args[0] === "string") {
|
|
||||||
args.unshift(
|
|
||||||
args.shift().replace(/%(o|s|f|d|i)/g, (s, t) => {
|
|
||||||
const arg = args.shift();
|
|
||||||
if (arg === undefined) return s;
|
|
||||||
switch (t) {
|
|
||||||
case "o":
|
|
||||||
return printObject(arg);
|
|
||||||
case "s":
|
|
||||||
return printString(arg);
|
|
||||||
case "f":
|
|
||||||
return printFloat(arg);
|
|
||||||
case "d":
|
|
||||||
case "i":
|
|
||||||
return printInt(arg);
|
|
||||||
default:
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return args.map(printObject).join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
const consoleMethods = ["log", "info", "warn", "error", "debug", "assert"];
|
|
||||||
|
|
||||||
const patchConsole = (console, ctx) => {
|
|
||||||
if (window.revokeSpotPatch || window.__or_proxy_revocable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let n = 0;
|
|
||||||
const reset = () => {
|
|
||||||
n = 0;
|
|
||||||
};
|
|
||||||
let int = setInterval(reset, 1000);
|
|
||||||
|
|
||||||
const sendConsoleLog = (level, args) => {
|
|
||||||
const msg = printf(args);
|
|
||||||
const truncated =
|
|
||||||
msg.length > 5000 ? `Truncated: ${msg.slice(0, 5000)}...` : msg;
|
|
||||||
const logs = [{ level, msg: truncated, time: Date.now() }];
|
|
||||||
window.postMessage({ type: "ort:bump-logs", logs }, "*");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handler = (level) => ({
|
|
||||||
apply: function (target, thisArg, argumentsList) {
|
|
||||||
Reflect.apply(target, ctx, argumentsList);
|
|
||||||
n = n + 1;
|
|
||||||
if (n > 10) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
sendConsoleLog(level, argumentsList); // Pass the correct level
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
window.__or_proxy_revocable = [];
|
|
||||||
consoleMethods.forEach((method) => {
|
|
||||||
if (consoleMethods.indexOf(method) === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fn = ctx.console[method];
|
|
||||||
// is there any way to preserve the original console trace?
|
|
||||||
const revProxy = Proxy.revocable(fn, handler(method));
|
|
||||||
console[method] = revProxy.proxy;
|
|
||||||
window.__or_proxy_revocable.push(revProxy);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(int);
|
|
||||||
window.__or_proxy_revocable.forEach((revocable) => {
|
|
||||||
revocable.revoke();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("message", (event) => {
|
|
||||||
if (event.data.type === "injected:start") {
|
|
||||||
if (!window.__or_revokeSpotPatch) {
|
|
||||||
window.__or_revokeSpotPatch = patchConsole(console, window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.data.type === "injected:stop") {
|
|
||||||
if (window.__or_revokeSpotPatch) {
|
|
||||||
window.__or_revokeSpotPatch();
|
|
||||||
window.__or_revokeSpotPatch = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
24
spot/entrypoints/injected.ts
Normal file
24
spot/entrypoints/injected.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { startNetwork, stopNetwork } from "~/utils/proxyNetworkTracking";
|
||||||
|
import { patchConsole } from "~/utils/consoleTracking";
|
||||||
|
|
||||||
|
export default defineUnlistedScript(() => {
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
if (event.data.type === "injected:c-start") {
|
||||||
|
if (!window.__or_revokeSpotPatch) {
|
||||||
|
window.__or_revokeSpotPatch = patchConsole(console, window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.data.type === "injected:n-start") {
|
||||||
|
startNetwork();
|
||||||
|
}
|
||||||
|
if (event.data.type === "injected:n-stop") {
|
||||||
|
stopNetwork();
|
||||||
|
}
|
||||||
|
if (event.data.type === "injected:c-stop") {
|
||||||
|
if (window.__or_revokeSpotPatch) {
|
||||||
|
window.__or_revokeSpotPatch();
|
||||||
|
window.__or_revokeSpotPatch = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import orLogo from "@/assets/orSpot.svg";
|
import orLogo from "~/assets/orSpot.svg";
|
||||||
import micOff from "@/assets/mic-off-red.svg";
|
import micOff from "~/assets/mic-off-red.svg";
|
||||||
import micOn from "@/assets/mic-on-dark.svg";
|
import micOn from "~/assets/mic-on-dark.svg";
|
||||||
import Login from "@/entrypoints/popup/Login";
|
import Login from "~/entrypoints/popup/Login";
|
||||||
import Settings from "@/entrypoints/popup/Settings";
|
import Settings from "~/entrypoints/popup/Settings";
|
||||||
import { createSignal, createEffect, onMount } from "solid-js";
|
import { createSignal, createEffect, onMount } from "solid-js";
|
||||||
import Dropdown from "@/entrypoints/popup/Dropdown";
|
import Dropdown from "~/entrypoints/popup/Dropdown";
|
||||||
import Button from "@/entrypoints/popup/Button";
|
import Button from "~/entrypoints/popup/Button";
|
||||||
import {
|
import {
|
||||||
ChevronSvg,
|
ChevronSvg,
|
||||||
RecordDesktopSvg,
|
RecordDesktopSvg,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import Button from "@/entrypoints/popup/Button";
|
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const onOpenLoginPage = async () => {
|
const onOpenLoginPage = async () => {
|
||||||
const { settings } = await chrome.storage.local.get("settings");
|
const { settings } = await chrome.storage.local.get("settings");
|
||||||
|
|
@ -11,8 +9,20 @@ function Login() {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div class={"flex flex-row gap-2"}>
|
<div class={"flex flex-row gap-2"}>
|
||||||
<button onClick={onOpenLoginPage} name={"Login"} class="btn btn-primary text-white shadow-sm text-lg w-2/4 ">Login</button>
|
<button
|
||||||
<button onClick={onOpenSignupPage} name={"Create Account"} class="btn btn-primary btn-outline bg-white shadow-sm text-lg w-2/4 ">Create Account</button>
|
onClick={onOpenLoginPage}
|
||||||
|
name={"Login"}
|
||||||
|
class="btn btn-primary text-white shadow-sm text-lg w-2/4 "
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onOpenSignupPage}
|
||||||
|
name={"Create Account"}
|
||||||
|
class="btn btn-primary btn-outline bg-white shadow-sm text-lg w-2/4 "
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSignal, onMount } from "solid-js";
|
import { createSignal, onMount } from "solid-js";
|
||||||
import orLogo from "@/assets/orSpot.svg";
|
import orLogo from "~/assets/orSpot.svg";
|
||||||
import arrowLeft from "@/assets/arrow-left.svg";
|
import arrowLeft from "~/assets/arrow-left.svg";
|
||||||
|
|
||||||
function Settings({ goBack }: { goBack: () => void }) {
|
function Settings({ goBack }: { goBack: () => void }) {
|
||||||
const [includeDevTools, setIncludeDevTools] = createSignal(true);
|
const [includeDevTools, setIncludeDevTools] = createSignal(true);
|
||||||
|
|
@ -13,7 +13,8 @@ function Settings({ goBack }: { goBack: () => void }) {
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
chrome.storage.local.get("settings", (data: any) => {
|
chrome.storage.local.get("settings", (data: any) => {
|
||||||
if (data.settings) {
|
if (data.settings) {
|
||||||
const ingest = data.settings.ingestPoint || "https://app.openreplay.com";
|
const ingest =
|
||||||
|
data.settings.ingestPoint || "https://app.openreplay.com";
|
||||||
const devToolsEnabled =
|
const devToolsEnabled =
|
||||||
data.settings.consoleLogs && data.settings.networkLogs;
|
data.settings.consoleLogs && data.settings.networkLogs;
|
||||||
setOpenInNewTab(data.settings.openInNewTab ?? false);
|
setOpenInNewTab(data.settings.openInNewTab ?? false);
|
||||||
|
|
@ -89,7 +90,8 @@ function Settings({ goBack }: { goBack: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div class={"flex flex-col"}>
|
<div class={"flex flex-col"}>
|
||||||
<div class={"flex gap-2 items-center justify-between p-4"}>
|
<div class={"flex gap-2 items-center justify-between p-4"}>
|
||||||
<button class="btn btn-xs btn-circle bg-white hover:bg-indigo-50"
|
<button
|
||||||
|
class="btn btn-xs btn-circle bg-white hover:bg-indigo-50"
|
||||||
onClick={goBack}
|
onClick={goBack}
|
||||||
>
|
>
|
||||||
<img src={arrowLeft} alt={"Go back"} />
|
<img src={arrowLeft} alt={"Go back"} />
|
||||||
|
|
@ -106,21 +108,20 @@ function Settings({ goBack }: { goBack: () => void }) {
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="p-4 border-b border-slate-300 hover:bg-indigo-50">
|
<div class="p-4 border-b border-slate-300 hover:bg-indigo-50">
|
||||||
<div class="flex flex-row justify-between items-center">
|
<div class="flex flex-row justify-between items-center">
|
||||||
<p class="font-semibold mb-1 flex items-center">
|
<p class="font-semibold mb-1 flex items-center">View Recording</p>
|
||||||
View Recording
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<label class="label cursor-pointer pr-0">
|
<label class="label cursor-pointer pr-0">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle toggle-primary toggle-sm cursor-pointer"
|
class="toggle toggle-primary toggle-sm cursor-pointer"
|
||||||
checked={openInNewTab()}
|
checked={openInNewTab()}
|
||||||
onChange={toggleOpenInNewTab}
|
onChange={toggleOpenInNewTab}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs">Take me to newly created Spot tab after saving a recording.</p>
|
<p class="text-xs">
|
||||||
|
Take me to newly created Spot tab after saving a recording.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col border-b border-slate-300 cursor-default justify-between p-4 hover:bg-indigo-50">
|
<div class="flex flex-col border-b border-slate-300 cursor-default justify-between p-4 hover:bg-indigo-50">
|
||||||
|
|
@ -140,7 +141,8 @@ function Settings({ goBack }: { goBack: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs">
|
<p class="text-xs">
|
||||||
Include console logs, network calls and other useful debugging information for developers.
|
Include console logs, network calls and other useful debugging
|
||||||
|
information for developers.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -159,7 +161,8 @@ function Settings({ goBack }: { goBack: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs">
|
<p class="text-xs">
|
||||||
Set this URL if you are self-hosting OpenReplay so it points to your instance.
|
Set this URL if you are self-hosting OpenReplay so it points to your
|
||||||
|
instance.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{showIngest() && (
|
{showIngest() && (
|
||||||
|
|
@ -191,16 +194,16 @@ function Settings({ goBack }: { goBack: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class={"flex items-center gap-2"}>
|
<div class={"flex items-center gap-2"}>
|
||||||
<span class={"text-gray-700"}>{ingest()}</span>
|
<span class={"text-gray-700"}>{ingest()}</span>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-link font-normal no-underline hover:no-underline hover:opacity-75"
|
class="btn btn-sm btn-link font-normal no-underline hover:no-underline hover:opacity-75"
|
||||||
onClick={() => toggleEditIngest(true)}
|
onClick={() => toggleEditIngest(true)}
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "wxt-starter",
|
"name": "wxt-starter",
|
||||||
"description": "manifest.json description",
|
"description": "manifest.json description",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@neodrag/solid": "^2.0.4",
|
"@neodrag/solid": "^2.0.4",
|
||||||
|
"@openreplay/network-proxy": "^1.0.3",
|
||||||
"@thedutchcoder/postcss-rem-to-px": "^0.0.2",
|
"@thedutchcoder/postcss-rem-to-px": "^0.0.2",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
|
|
@ -31,6 +32,6 @@
|
||||||
"@wxt-dev/module-solid": "^1.1.2",
|
"@wxt-dev/module-solid": "^1.1.2",
|
||||||
"daisyui": "^4.12.10",
|
"daisyui": "^4.12.10",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"wxt": "0.19.9"
|
"wxt": "0.19.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
{
|
{
|
||||||
"extends": "./.wxt/tsconfig.json",
|
"extends": "./.wxt/tsconfig.json",
|
||||||
|
"paths": {
|
||||||
|
"~": [".."],
|
||||||
|
"~/*": ["../*"],
|
||||||
|
"@@": [".."],
|
||||||
|
"@@/*": ["../*"],
|
||||||
|
"~~": [".."],
|
||||||
|
"~~/*": ["../*"]
|
||||||
|
},
|
||||||
|
"lib": ["es2022", "DOM"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"jsxImportSource": "solid-js"
|
"jsxImportSource": "solid-js"
|
||||||
|
|
|
||||||
142
spot/utils/consoleTracking.ts
Normal file
142
spot/utils/consoleTracking.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
function printString(arg) {
|
||||||
|
const printError =
|
||||||
|
"InstallTrigger" in window // detect Firefox
|
||||||
|
? (e) => e.message + "\n" + e.stack
|
||||||
|
: (e) => e.stack || e.message;
|
||||||
|
|
||||||
|
if (arg === undefined) {
|
||||||
|
return "undefined";
|
||||||
|
}
|
||||||
|
if (arg === null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
if (arg instanceof Error) {
|
||||||
|
return printError(arg);
|
||||||
|
}
|
||||||
|
if (Array.isArray(arg)) {
|
||||||
|
return `Array(${arg.length})`;
|
||||||
|
}
|
||||||
|
return String(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printFloat(arg) {
|
||||||
|
if (typeof arg !== "number") return "NaN";
|
||||||
|
return arg.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function printInt(arg) {
|
||||||
|
if (typeof arg !== "number") return "NaN";
|
||||||
|
return Math.floor(arg).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function printObject(arg) {
|
||||||
|
const printError =
|
||||||
|
"InstallTrigger" in window // detect Firefox
|
||||||
|
? (e) => e.message + "\n" + e.stack
|
||||||
|
: (e) => e.stack || e.message;
|
||||||
|
|
||||||
|
if (arg === undefined) {
|
||||||
|
return "undefined";
|
||||||
|
}
|
||||||
|
if (arg === null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
if (arg instanceof Error) {
|
||||||
|
return printError(arg);
|
||||||
|
}
|
||||||
|
if (Array.isArray(arg)) {
|
||||||
|
const length = arg.length;
|
||||||
|
const values = arg.slice(0, 10).map(printString).join(", ");
|
||||||
|
return `Array(${length})[${values}]`;
|
||||||
|
}
|
||||||
|
if (typeof arg === "object") {
|
||||||
|
const res = [];
|
||||||
|
let i = 0;
|
||||||
|
for (const k in arg) {
|
||||||
|
if (++i === 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const v = arg[k];
|
||||||
|
res.push(k + ": " + printString(v));
|
||||||
|
}
|
||||||
|
return "{" + res.join(", ") + "}";
|
||||||
|
}
|
||||||
|
return arg.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function printf(args) {
|
||||||
|
if (typeof args[0] === "string") {
|
||||||
|
args.unshift(
|
||||||
|
args.shift().replace(/%(o|s|f|d|i)/g, (s, t) => {
|
||||||
|
const arg = args.shift();
|
||||||
|
if (arg === undefined) return s;
|
||||||
|
switch (t) {
|
||||||
|
case "o":
|
||||||
|
return printObject(arg);
|
||||||
|
case "s":
|
||||||
|
return printString(arg);
|
||||||
|
case "f":
|
||||||
|
return printFloat(arg);
|
||||||
|
case "d":
|
||||||
|
case "i":
|
||||||
|
return printInt(arg);
|
||||||
|
default:
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return args.map(printObject).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const consoleMethods = ["log", "info", "warn", "error", "debug", "assert"];
|
||||||
|
|
||||||
|
export const patchConsole = (console, ctx) => {
|
||||||
|
if (window.revokeSpotPatch || window.__or_proxy_revocable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n = 0;
|
||||||
|
const reset = () => {
|
||||||
|
n = 0;
|
||||||
|
};
|
||||||
|
let int = setInterval(reset, 1000);
|
||||||
|
|
||||||
|
const sendConsoleLog = (level, args) => {
|
||||||
|
const msg = printf(args);
|
||||||
|
const truncated =
|
||||||
|
msg.length > 5000 ? `Truncated: ${msg.slice(0, 5000)}...` : msg;
|
||||||
|
const logs = [{ level, msg: truncated, time: Date.now() }];
|
||||||
|
window.postMessage({ type: "ort:bump-logs", logs }, "*");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = (level) => ({
|
||||||
|
apply: function (target, thisArg, argumentsList) {
|
||||||
|
Reflect.apply(target, ctx, argumentsList);
|
||||||
|
n = n + 1;
|
||||||
|
if (n > 10) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
sendConsoleLog(level, argumentsList); // Pass the correct level
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
window.__or_proxy_revocable = [];
|
||||||
|
consoleMethods.forEach((method) => {
|
||||||
|
if (consoleMethods.indexOf(method) === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fn = ctx.console[method];
|
||||||
|
// is there any way to preserve the original console trace?
|
||||||
|
const revProxy = Proxy.revocable(fn, handler(method));
|
||||||
|
console[method] = revProxy.proxy;
|
||||||
|
window.__or_proxy_revocable.push(revProxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(int);
|
||||||
|
window.__or_proxy_revocable.forEach((revocable) => {
|
||||||
|
revocable.revoke();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,305 +1,169 @@
|
||||||
import { WebRequest } from "webextension-polyfill";
|
// import {
|
||||||
export type TrackedRequest = {
|
// SpotNetworkRequest,
|
||||||
statusCode: number;
|
// filterBody,
|
||||||
requestHeaders: Record<string, string>;
|
// filterHeaders,
|
||||||
responseHeaders: Record<string, string>;
|
// tryFilterUrl,
|
||||||
} & (
|
// TrackedRequest,
|
||||||
| WebRequest.OnBeforeRequestDetailsType
|
// } from "./networkTrackingUtils";
|
||||||
| WebRequest.OnBeforeSendHeadersDetailsType
|
//
|
||||||
| WebRequest.OnCompletedDetailsType
|
// export const rawRequests: (TrackedRequest & {
|
||||||
| WebRequest.OnErrorOccurredDetailsType
|
// startTs: number;
|
||||||
| WebRequest.OnResponseStartedDetailsType
|
// duration: number;
|
||||||
);
|
// })[] = [];
|
||||||
|
//
|
||||||
export interface SpotNetworkRequest {
|
// export function createSpotNetworkRequestV1(
|
||||||
encodedBodySize: number;
|
// trackedRequest: TrackedRequest,
|
||||||
responseBodySize: number;
|
// trackedTab?: number,
|
||||||
duration: number;
|
// ) {
|
||||||
method: TrackedRequest["method"];
|
// if (trackedRequest.tabId === -1) {
|
||||||
type: string;
|
// return;
|
||||||
time: TrackedRequest["timeStamp"];
|
// }
|
||||||
statusCode: number;
|
// if (trackedTab && trackedTab !== trackedRequest.tabId) {
|
||||||
error?: string;
|
// return;
|
||||||
url: TrackedRequest["url"];
|
// }
|
||||||
fromCache: boolean;
|
// if (
|
||||||
body: string;
|
// ["ping", "beacon", "image", "script", "font"].includes(trackedRequest.type)
|
||||||
requestHeaders: Record<string, string>;
|
// ) {
|
||||||
responseHeaders: Record<string, string>;
|
// if (!trackedRequest.statusCode || trackedRequest.statusCode < 400) {
|
||||||
}
|
// return;
|
||||||
export const rawRequests: (TrackedRequest & {
|
// }
|
||||||
startTs: number;
|
// }
|
||||||
duration: number;
|
// const type = ["stylesheet", "script", "image", "media", "font"].includes(
|
||||||
})[] = [];
|
// trackedRequest.type,
|
||||||
|
// )
|
||||||
const sensitiveParams = new Set([
|
// ? "resource"
|
||||||
"password",
|
// : trackedRequest.type;
|
||||||
"pass",
|
//
|
||||||
"pwd",
|
// const requestHeaders = trackedRequest.requestHeaders
|
||||||
"mdp",
|
// ? filterHeaders(trackedRequest.requestHeaders)
|
||||||
"token",
|
// : {};
|
||||||
"bearer",
|
// const responseHeaders = trackedRequest.responseHeaders
|
||||||
"jwt",
|
// ? filterHeaders(trackedRequest.responseHeaders)
|
||||||
"api_key",
|
// : {};
|
||||||
"api-key",
|
//
|
||||||
"apiKey",
|
// const reqSize = trackedRequest.reqBody
|
||||||
"key",
|
// ? trackedRequest.requestSize || trackedRequest.reqBody.length
|
||||||
"secret",
|
// : 0;
|
||||||
"id",
|
//
|
||||||
"user",
|
// const status = getRequestStatus(trackedRequest);
|
||||||
"userId",
|
// let body;
|
||||||
"email",
|
// if (trackedRequest.reqBody) {
|
||||||
"ssn",
|
// try {
|
||||||
"name",
|
// body = filterBody(trackedRequest.reqBody);
|
||||||
"firstname",
|
// } catch (e) {
|
||||||
"lastname",
|
// body = "Error parsing body";
|
||||||
"birthdate",
|
// console.error(e);
|
||||||
"dob",
|
// }
|
||||||
"address",
|
// } else {
|
||||||
"zip",
|
// body = "";
|
||||||
"zipcode",
|
// }
|
||||||
"x-api-key",
|
// const request: SpotNetworkRequest = {
|
||||||
"www-authenticate",
|
// method: trackedRequest.method,
|
||||||
"x-csrf-token",
|
// type,
|
||||||
"x-requested-with",
|
// body,
|
||||||
"x-forwarded-for",
|
// responseBody: "",
|
||||||
"x-real-ip",
|
// requestHeaders,
|
||||||
"cookie",
|
// responseHeaders,
|
||||||
"authorization",
|
// time: trackedRequest.timeStamp,
|
||||||
"auth",
|
// statusCode: status,
|
||||||
"proxy-authorization",
|
// error: trackedRequest.error,
|
||||||
"set-cookie",
|
// url: tryFilterUrl(trackedRequest.url),
|
||||||
"account_key",
|
// fromCache: trackedRequest.fromCache || false,
|
||||||
]);
|
// encodedBodySize: reqSize,
|
||||||
|
// responseBodySize: trackedRequest.responseSize,
|
||||||
function filterHeaders(headers: Record<string, string>) {
|
// duration: trackedRequest.duration,
|
||||||
const filteredHeaders: Record<string, string> = {};
|
// };
|
||||||
if (Array.isArray(headers)) {
|
//
|
||||||
headers.forEach(({ name, value }) => {
|
// return request;
|
||||||
if (sensitiveParams.has(name.toLowerCase())) {
|
// }
|
||||||
filteredHeaders[name] = "******";
|
//
|
||||||
} else {
|
// function modifyOnSpot(request: TrackedRequest) {
|
||||||
filteredHeaders[name] = value;
|
// const id = request.requestId;
|
||||||
}
|
// const index = rawRequests.findIndex((r) => r.requestId === id);
|
||||||
});
|
// const ts = Date.now();
|
||||||
} else {
|
// const start = rawRequests[index]?.startTs ?? ts;
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
// rawRequests[index] = {
|
||||||
if (sensitiveParams.has(key.toLowerCase())) {
|
// ...rawRequests[index],
|
||||||
filteredHeaders[key] = "******";
|
// ...request,
|
||||||
} else {
|
// duration: ts - start,
|
||||||
filteredHeaders[key] = value;
|
// };
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
}
|
// const trackOnBefore = (
|
||||||
return filteredHeaders;
|
// details: WebRequest.OnBeforeRequestDetailsType & { reqBody: string },
|
||||||
}
|
// ) => {
|
||||||
|
// if (details.method === "POST" && details.requestBody) {
|
||||||
// JSON or form data
|
// const requestBody = details.requestBody;
|
||||||
function filterBody(body: any) {
|
// if (requestBody.formData) {
|
||||||
if (!body) {
|
// details.reqBody = JSON.stringify(requestBody.formData);
|
||||||
return body;
|
// } else if (requestBody.raw) {
|
||||||
}
|
// const raw = requestBody.raw[0]?.bytes;
|
||||||
|
// if (raw) {
|
||||||
let parsedBody;
|
// details.reqBody = new TextDecoder("utf-8").decode(raw);
|
||||||
let isJSON = false;
|
// }
|
||||||
|
// }
|
||||||
try {
|
// }
|
||||||
parsedBody = JSON.parse(body);
|
// rawRequests.push({ ...details, startTs: Date.now(), duration: 0 });
|
||||||
isJSON = true;
|
// };
|
||||||
} catch (e) {
|
// const trackOnCompleted = (details: WebRequest.OnCompletedDetailsType) => {
|
||||||
// not json
|
// modifyOnSpot(details);
|
||||||
}
|
// };
|
||||||
|
// const trackOnHeaders = (details: WebRequest.OnBeforeSendHeadersDetailsType) => {
|
||||||
if (isJSON) {
|
// modifyOnSpot(details);
|
||||||
obscureSensitiveData(parsedBody);
|
// };
|
||||||
return JSON.stringify(parsedBody);
|
// const trackOnError = (details: WebRequest.OnErrorOccurredDetailsType) => {
|
||||||
} else {
|
// modifyOnSpot(details);
|
||||||
const params = new URLSearchParams(body);
|
// };
|
||||||
for (const key of params.keys()) {
|
// export function startTrackingNetwork() {
|
||||||
if (sensitiveParams.has(key.toLowerCase())) {
|
// rawRequests.length = 0;
|
||||||
params.set(key, "******");
|
// browser.webRequest.onBeforeRequest.addListener(
|
||||||
}
|
// // @ts-ignore
|
||||||
}
|
// trackOnBefore,
|
||||||
|
// { urls: ["<all_urls>"] },
|
||||||
return params.toString();
|
// ["requestBody"],
|
||||||
}
|
// );
|
||||||
}
|
// browser.webRequest.onBeforeSendHeaders.addListener(
|
||||||
|
// trackOnHeaders,
|
||||||
function obscureSensitiveData(obj: Record<string, any> | any[]) {
|
// { urls: ["<all_urls>"] },
|
||||||
if (Array.isArray(obj)) {
|
// ["requestHeaders"],
|
||||||
obj.forEach(obscureSensitiveData);
|
// );
|
||||||
} else if (obj && typeof obj === "object") {
|
// browser.webRequest.onCompleted.addListener(
|
||||||
for (const key in obj) {
|
// trackOnCompleted,
|
||||||
if (obj.hasOwnProperty(key)) {
|
// {
|
||||||
if (sensitiveParams.has(key.toLowerCase())) {
|
// urls: ["<all_urls>"],
|
||||||
obj[key] = "******";
|
// },
|
||||||
} else if (obj[key] !== null && typeof obj[key] === "object") {
|
// ["responseHeaders"],
|
||||||
obscureSensitiveData(obj[key]);
|
// );
|
||||||
}
|
// browser.webRequest.onErrorOccurred.addListener(
|
||||||
}
|
// trackOnError,
|
||||||
}
|
// {
|
||||||
}
|
// urls: ["<all_urls>"],
|
||||||
}
|
// },
|
||||||
|
// ["extraHeaders"],
|
||||||
function tryFilterUrl(url: string) {
|
// );
|
||||||
if (!url) return "";
|
// }
|
||||||
try {
|
//
|
||||||
const urlObj = new URL(url);
|
// export function stopTrackingNetwork() {
|
||||||
if (urlObj.searchParams) {
|
// browser.webRequest.onBeforeRequest.removeListener(trackOnBefore);
|
||||||
for (const key of urlObj.searchParams.keys()) {
|
// browser.webRequest.onCompleted.removeListener(trackOnCompleted);
|
||||||
if (sensitiveParams.has(key.toLowerCase())) {
|
// browser.webRequest.onErrorOccurred.removeListener(trackOnError);
|
||||||
urlObj.searchParams.set(key, "******");
|
// }
|
||||||
}
|
//
|
||||||
}
|
// function getRequestStatus(request: any): number {
|
||||||
}
|
// if (request.statusCode) {
|
||||||
return urlObj.toString();
|
// return request.statusCode;
|
||||||
} catch (e) {
|
// }
|
||||||
return url;
|
// if (request.error) {
|
||||||
}
|
// return 0;
|
||||||
}
|
// }
|
||||||
|
// return 200;
|
||||||
export function createSpotNetworkRequest(
|
// }
|
||||||
trackedRequest: TrackedRequest,
|
//
|
||||||
trackedTab?: number,
|
// export function getFinalRequests(tabId: number): SpotNetworkRequest[] {
|
||||||
) {
|
// const finalRequests = rawRequests
|
||||||
if (trackedRequest.tabId === -1) {
|
// .map((r) => createSpotNetworkRequest(r, tabId))
|
||||||
return;
|
// .filter((r) => r !== undefined);
|
||||||
}
|
// rawRequests.length = 0;
|
||||||
if (trackedTab && trackedTab !== trackedRequest.tabId) {
|
//
|
||||||
return;
|
// return finalRequests;
|
||||||
}
|
// }
|
||||||
if (
|
|
||||||
["ping", "beacon", "image", "script", "font"].includes(trackedRequest.type)
|
|
||||||
) {
|
|
||||||
if (!trackedRequest.statusCode || trackedRequest.statusCode < 400) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const type = ["stylesheet", "script", "image", "media", "font"].includes(
|
|
||||||
trackedRequest.type,
|
|
||||||
)
|
|
||||||
? "resource"
|
|
||||||
: trackedRequest.type;
|
|
||||||
|
|
||||||
const requestHeaders = trackedRequest.requestHeaders
|
|
||||||
? filterHeaders(trackedRequest.requestHeaders)
|
|
||||||
: {};
|
|
||||||
const responseHeaders = trackedRequest.responseHeaders
|
|
||||||
? filterHeaders(trackedRequest.responseHeaders)
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const reqSize = trackedRequest.reqBody
|
|
||||||
? trackedRequest.requestSize || trackedRequest.reqBody.length
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const status = getRequestStatus(trackedRequest);
|
|
||||||
let body;
|
|
||||||
if (trackedRequest.reqBody) {
|
|
||||||
try {
|
|
||||||
body = filterBody(trackedRequest.reqBody);
|
|
||||||
} catch (e) {
|
|
||||||
body = "Error parsing body";
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body = "";
|
|
||||||
}
|
|
||||||
const request: SpotNetworkRequest = {
|
|
||||||
method: trackedRequest.method,
|
|
||||||
type,
|
|
||||||
body,
|
|
||||||
requestHeaders,
|
|
||||||
responseHeaders,
|
|
||||||
time: trackedRequest.timeStamp,
|
|
||||||
statusCode: status,
|
|
||||||
error: trackedRequest.error,
|
|
||||||
url: tryFilterUrl(trackedRequest.url),
|
|
||||||
fromCache: trackedRequest.fromCache || false,
|
|
||||||
encodedBodySize: reqSize,
|
|
||||||
responseBodySize: trackedRequest.responseSize,
|
|
||||||
duration: trackedRequest.duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
function modifyOnSpot(request: TrackedRequest) {
|
|
||||||
const id = request.requestId;
|
|
||||||
const index = rawRequests.findIndex((r) => r.requestId === id);
|
|
||||||
const ts = Date.now();
|
|
||||||
const start = rawRequests[index]?.startTs ?? ts;
|
|
||||||
rawRequests[index] = {
|
|
||||||
...rawRequests[index],
|
|
||||||
...request,
|
|
||||||
duration: ts - start,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackOnBefore = (
|
|
||||||
details: WebRequest.OnBeforeRequestDetailsType & { reqBody: string },
|
|
||||||
) => {
|
|
||||||
if (details.method === "POST" && details.requestBody) {
|
|
||||||
const requestBody = details.requestBody;
|
|
||||||
if (requestBody.formData) {
|
|
||||||
details.reqBody = JSON.stringify(requestBody.formData);
|
|
||||||
} else if (requestBody.raw) {
|
|
||||||
const raw = requestBody.raw[0]?.bytes;
|
|
||||||
if (raw) {
|
|
||||||
details.reqBody = new TextDecoder("utf-8").decode(raw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rawRequests.push({ ...details, startTs: Date.now(), duration: 0 });
|
|
||||||
};
|
|
||||||
const trackOnCompleted = (details: WebRequest.OnCompletedDetailsType) => {
|
|
||||||
modifyOnSpot(details);
|
|
||||||
};
|
|
||||||
const trackOnHeaders = (details: WebRequest.OnBeforeSendHeadersDetailsType) => {
|
|
||||||
modifyOnSpot(details);
|
|
||||||
};
|
|
||||||
const trackOnError = (details: WebRequest.OnErrorOccurredDetailsType) => {
|
|
||||||
modifyOnSpot(details);
|
|
||||||
};
|
|
||||||
export function startTrackingNetwork() {
|
|
||||||
rawRequests.length = 0;
|
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
|
||||||
// @ts-ignore
|
|
||||||
trackOnBefore,
|
|
||||||
{ urls: ["<all_urls>"] },
|
|
||||||
["requestBody"],
|
|
||||||
);
|
|
||||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
|
||||||
trackOnHeaders,
|
|
||||||
{ urls: ["<all_urls>"] },
|
|
||||||
["requestHeaders"],
|
|
||||||
);
|
|
||||||
browser.webRequest.onCompleted.addListener(
|
|
||||||
trackOnCompleted,
|
|
||||||
{
|
|
||||||
urls: ["<all_urls>"],
|
|
||||||
},
|
|
||||||
["responseHeaders"],
|
|
||||||
);
|
|
||||||
browser.webRequest.onErrorOccurred.addListener(
|
|
||||||
trackOnError,
|
|
||||||
{
|
|
||||||
urls: ["<all_urls>"],
|
|
||||||
},
|
|
||||||
["extraHeaders"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stopTrackingNetwork() {
|
|
||||||
browser.webRequest.onBeforeRequest.removeListener(trackOnBefore);
|
|
||||||
browser.webRequest.onCompleted.removeListener(trackOnCompleted);
|
|
||||||
browser.webRequest.onErrorOccurred.removeListener(trackOnError);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRequestStatus(request: any): number {
|
|
||||||
if (request.statusCode) {
|
|
||||||
return request.statusCode;
|
|
||||||
}
|
|
||||||
if (request.error) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return 200;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
168
spot/utils/networkTrackingUtils.ts
Normal file
168
spot/utils/networkTrackingUtils.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { WebRequest } from "webextension-polyfill";
|
||||||
|
export type TrackedRequest = {
|
||||||
|
statusCode: number;
|
||||||
|
requestHeaders: Record<string, string>;
|
||||||
|
responseHeaders: Record<string, string>;
|
||||||
|
} & (
|
||||||
|
| WebRequest.OnBeforeRequestDetailsType
|
||||||
|
| WebRequest.OnBeforeSendHeadersDetailsType
|
||||||
|
| WebRequest.OnCompletedDetailsType
|
||||||
|
| WebRequest.OnErrorOccurredDetailsType
|
||||||
|
| WebRequest.OnResponseStartedDetailsType
|
||||||
|
);
|
||||||
|
|
||||||
|
export function getTopWindow(): Window {
|
||||||
|
let currentWindow = window;
|
||||||
|
try {
|
||||||
|
while (currentWindow !== currentWindow.parent) {
|
||||||
|
currentWindow = currentWindow.parent;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Accessing currentWindow.parent threw an exception due to cross-origin policy
|
||||||
|
// currentWindow is the topmost accessible window
|
||||||
|
}
|
||||||
|
return currentWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpotNetworkRequest {
|
||||||
|
encodedBodySize: number;
|
||||||
|
responseBodySize: number;
|
||||||
|
duration: number;
|
||||||
|
method: TrackedRequest["method"];
|
||||||
|
type: string;
|
||||||
|
time: TrackedRequest["timeStamp"];
|
||||||
|
statusCode: number;
|
||||||
|
error?: string;
|
||||||
|
url: TrackedRequest["url"];
|
||||||
|
fromCache: boolean;
|
||||||
|
body: string;
|
||||||
|
responseBody: string;
|
||||||
|
requestHeaders: Record<string, string>;
|
||||||
|
responseHeaders: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sensitiveParams = new Set([
|
||||||
|
"password",
|
||||||
|
"pass",
|
||||||
|
"pwd",
|
||||||
|
"mdp",
|
||||||
|
"token",
|
||||||
|
"bearer",
|
||||||
|
"jwt",
|
||||||
|
"api_key",
|
||||||
|
"api-key",
|
||||||
|
"apiKey",
|
||||||
|
"key",
|
||||||
|
"secret",
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"userId",
|
||||||
|
"email",
|
||||||
|
"ssn",
|
||||||
|
"name",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"birthdate",
|
||||||
|
"dob",
|
||||||
|
"address",
|
||||||
|
"zip",
|
||||||
|
"zipcode",
|
||||||
|
"x-api-key",
|
||||||
|
"www-authenticate",
|
||||||
|
"x-csrf-token",
|
||||||
|
"x-requested-with",
|
||||||
|
"x-forwarded-for",
|
||||||
|
"x-real-ip",
|
||||||
|
"cookie",
|
||||||
|
"authorization",
|
||||||
|
"auth",
|
||||||
|
"proxy-authorization",
|
||||||
|
"set-cookie",
|
||||||
|
"account_key",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function filterHeaders(headers: Record<string, string> | { name: string; value: string }[]) {
|
||||||
|
const filteredHeaders: Record<string, string> = {};
|
||||||
|
if (Array.isArray(headers)) {
|
||||||
|
headers.forEach(({ name, value }) => {
|
||||||
|
if (sensitiveParams.has(name.toLowerCase())) {
|
||||||
|
filteredHeaders[name] = "******";
|
||||||
|
} else {
|
||||||
|
filteredHeaders[name] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
if (sensitiveParams.has(key.toLowerCase())) {
|
||||||
|
filteredHeaders[key] = "******";
|
||||||
|
} else {
|
||||||
|
filteredHeaders[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON or form data
|
||||||
|
export function filterBody(body: any): string {
|
||||||
|
if (!body) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedBody;
|
||||||
|
let isJSON = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedBody = JSON.parse(body);
|
||||||
|
isJSON = true;
|
||||||
|
} catch (e) {
|
||||||
|
// not json
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isJSON) {
|
||||||
|
obscureSensitiveData(parsedBody);
|
||||||
|
return JSON.stringify(parsedBody);
|
||||||
|
} else {
|
||||||
|
const params = new URLSearchParams(body);
|
||||||
|
for (const key of params.keys()) {
|
||||||
|
if (sensitiveParams.has(key.toLowerCase())) {
|
||||||
|
params.set(key, "******");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function obscureSensitiveData(obj: Record<string, any> | any[]) {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
obj.forEach(obscureSensitiveData);
|
||||||
|
} else if (obj && typeof obj === "object") {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (Object.hasOwn(obj, key)) {
|
||||||
|
if (sensitiveParams.has(key.toLowerCase())) {
|
||||||
|
obj[key] = "******";
|
||||||
|
} else if (obj[key] !== null && typeof obj[key] === "object") {
|
||||||
|
obscureSensitiveData(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tryFilterUrl(url: string) {
|
||||||
|
if (!url) return "";
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
if (urlObj.searchParams) {
|
||||||
|
for (const key of urlObj.searchParams.keys()) {
|
||||||
|
if (sensitiveParams.has(key.toLowerCase())) {
|
||||||
|
urlObj.searchParams.set(key, "******");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urlObj.toString();
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
spot/utils/proxyNetworkTracking.ts
Normal file
101
spot/utils/proxyNetworkTracking.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import createNetworkProxy, { INetworkMessage } from "@openreplay/network-proxy";
|
||||||
|
import {
|
||||||
|
SpotNetworkRequest,
|
||||||
|
filterBody,
|
||||||
|
filterHeaders,
|
||||||
|
tryFilterUrl,
|
||||||
|
getTopWindow,
|
||||||
|
} from "./networkTrackingUtils";
|
||||||
|
|
||||||
|
let defaultFetch: typeof fetch | undefined;
|
||||||
|
let defaultXhr: typeof XMLHttpRequest | undefined;
|
||||||
|
let defaultBeacon: typeof navigator.sendBeacon | undefined;
|
||||||
|
|
||||||
|
export function startNetwork() {
|
||||||
|
const context = getTopWindow();
|
||||||
|
defaultXhr = context.XMLHttpRequest;
|
||||||
|
defaultBeacon = context.navigator.sendBeacon;
|
||||||
|
defaultFetch = context.fetch;
|
||||||
|
createNetworkProxy(
|
||||||
|
context,
|
||||||
|
[], // headers
|
||||||
|
() => null,
|
||||||
|
(reqRes) => reqRes,
|
||||||
|
(msg) => {
|
||||||
|
const event = createSpotNetworkRequest(msg);
|
||||||
|
window.postMessage({ type: "ort:bump-network", event }, "*");
|
||||||
|
},
|
||||||
|
(url) =>
|
||||||
|
url.includes("/spot/") || url.includes(".mob?") || url.includes(".mobe?"),
|
||||||
|
{ xhr: true, fetch: true, beacon: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBody(req: { body?: string | Record<string, any> }): string {
|
||||||
|
let body;
|
||||||
|
|
||||||
|
if (req.body) {
|
||||||
|
try {
|
||||||
|
body = filterBody(req.body);
|
||||||
|
} catch (e) {
|
||||||
|
body = "Error parsing body";
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSpotNetworkRequest(
|
||||||
|
msg: INetworkMessage,
|
||||||
|
): SpotNetworkRequest {
|
||||||
|
let request: Record<string, any> = {}
|
||||||
|
let response: Record<string, any> = {};
|
||||||
|
try {
|
||||||
|
request = JSON.parse(msg.request);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing request", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
response = JSON.parse(msg.response);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing response", e);
|
||||||
|
}
|
||||||
|
const reqHeaders = request.headers ? filterHeaders(request.headers) : {};
|
||||||
|
const resHeaders = response.headers ? filterHeaders(response.headers) : {};
|
||||||
|
const responseBodySize = msg.responseSize || 0;
|
||||||
|
const reqSize = msg.request ? msg.request.length : 0;
|
||||||
|
const body = getBody(request);
|
||||||
|
const responseBody = getBody(response);
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: msg.method,
|
||||||
|
type: msg.requestType,
|
||||||
|
body,
|
||||||
|
responseBody,
|
||||||
|
requestHeaders: reqHeaders,
|
||||||
|
responseHeaders: resHeaders,
|
||||||
|
time: msg.startTime,
|
||||||
|
statusCode: msg.status || 0,
|
||||||
|
error: undefined,
|
||||||
|
url: tryFilterUrl(msg.url),
|
||||||
|
fromCache: false,
|
||||||
|
encodedBodySize: reqSize,
|
||||||
|
responseBodySize,
|
||||||
|
duration: msg.duration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopNetwork() {
|
||||||
|
if (defaultFetch) {
|
||||||
|
window.fetch = defaultFetch;
|
||||||
|
}
|
||||||
|
if (defaultXhr) {
|
||||||
|
window.XMLHttpRequest = defaultXhr;
|
||||||
|
}
|
||||||
|
if (defaultBeacon) {
|
||||||
|
window.navigator.sendBeacon = defaultBeacon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -707,6 +707,11 @@
|
||||||
proc-log "^4.0.0"
|
proc-log "^4.0.0"
|
||||||
which "^4.0.0"
|
which "^4.0.0"
|
||||||
|
|
||||||
|
"@openreplay/network-proxy@^1.0.3":
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@openreplay/network-proxy/-/network-proxy-1.0.3.tgz#82c07f0742db01456f3764bae6c0b5ff280557a9"
|
||||||
|
integrity sha512-vm/x8Ioo1BKJIyZf58tK44CgtzHA0tIwu2uZ2WdIzrBOODF+A2qV4mELC8Zdp9WqV1O7uxXGaA4J38HU3th14g==
|
||||||
|
|
||||||
"@pkgjs/parseargs@^0.11.0":
|
"@pkgjs/parseargs@^0.11.0":
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
|
resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
|
||||||
|
|
@ -5331,10 +5336,10 @@ ws@8.18.0:
|
||||||
resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz"
|
resolved "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz"
|
||||||
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
|
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
|
||||||
|
|
||||||
wxt@0.19.9:
|
wxt@0.19.10:
|
||||||
version "0.19.9"
|
version "0.19.10"
|
||||||
resolved "https://registry.yarnpkg.com/wxt/-/wxt-0.19.9.tgz#b8f7f838cab00d66f4ee22f483c49ad8f6527af8"
|
resolved "https://registry.yarnpkg.com/wxt/-/wxt-0.19.10.tgz#557d57e63ab5fcf3b026791aae3706c400b5f7cb"
|
||||||
integrity sha512-XUbF4JNyx2jTDpXwx2c/esaJcUD2Dr482C2GGenkGRMH2UnerzOIchGCtaa1hb2U8eAed7Akda0yRoMJU0uxUw==
|
integrity sha512-lX/dzAaau79SDsU7QZKUgxJUf9nBsaQXMiqsDNgGoqZO1wrRpFq0kijcN3/mNjGbXM989VJhEl7B6f3ttxKAnQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@aklinker1/rollup-plugin-visualizer" "5.12.0"
|
"@aklinker1/rollup-plugin-visualizer" "5.12.0"
|
||||||
"@types/chrome" "^0.0.269"
|
"@types/chrome" "^0.0.269"
|
||||||
|
|
@ -5350,6 +5355,7 @@ wxt@0.19.9:
|
||||||
consola "^3.2.3"
|
consola "^3.2.3"
|
||||||
defu "^6.1.4"
|
defu "^6.1.4"
|
||||||
dequal "^2.0.3"
|
dequal "^2.0.3"
|
||||||
|
dotenv "^16.4.5"
|
||||||
esbuild "^0.23.0"
|
esbuild "^0.23.0"
|
||||||
execa "^9.3.1"
|
execa "^9.3.1"
|
||||||
fast-glob "^3.3.2"
|
fast-glob "^3.3.2"
|
||||||
|
|
@ -5371,6 +5377,7 @@ wxt@0.19.9:
|
||||||
ohash "^1.1.3"
|
ohash "^1.1.3"
|
||||||
open "^10.1.0"
|
open "^10.1.0"
|
||||||
ora "^8.1.0"
|
ora "^8.1.0"
|
||||||
|
perfect-debounce "^1.0.0"
|
||||||
picocolors "^1.0.1"
|
picocolors "^1.0.1"
|
||||||
prompts "^2.4.2"
|
prompts "^2.4.2"
|
||||||
publish-browser-extension "^2.1.3"
|
publish-browser-extension "^2.1.3"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
# 14.0.8
|
||||||
|
|
||||||
|
- use separate library to handle network requests ([@openreplay/network-proxy](https://www.npmjs.com/package/@openreplay/network-proxy))
|
||||||
|
|
||||||
# 14.0.7
|
# 14.0.7
|
||||||
|
|
||||||
- check for stopping status during restarts
|
- check for stopping status during restarts
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@openreplay/tracker",
|
"name": "@openreplay/tracker",
|
||||||
"description": "The OpenReplay tracker main package",
|
"description": "The OpenReplay tracker main package",
|
||||||
"version": "14.0.7",
|
"version": "14.0.8",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"logging",
|
"logging",
|
||||||
"replay"
|
"replay"
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medv/finder": "^3.2.0",
|
"@medv/finder": "^3.2.0",
|
||||||
|
"@openreplay/network-proxy": "^1.0.2",
|
||||||
"error-stack-parser": "^2.0.6",
|
"error-stack-parser": "^2.0.6",
|
||||||
"fflate": "^0.8.2"
|
"fflate": "^0.8.2"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import FetchProxy from './fetchProxy.js'
|
|
||||||
import XHRProxy from './xhrProxy.js'
|
|
||||||
import BeaconProxy from './beaconProxy.js'
|
|
||||||
import { RequestResponseData } from './types.js'
|
|
||||||
import { NetworkRequest } from '../../../common/messages.gen.js'
|
|
||||||
|
|
||||||
const getWarning = (api: string) =>
|
|
||||||
console.warn(`Openreplay: Can't find ${api} in global context.
|
|
||||||
If you're using serverside rendering in your app, make sure that tracker is loaded dynamically, otherwise ${api} won't be tracked.`)
|
|
||||||
|
|
||||||
export default function setProxy(
|
|
||||||
context: typeof globalThis,
|
|
||||||
ignoredHeaders: boolean | string[],
|
|
||||||
setSessionTokenHeader: (cb: (name: string, value: string) => void) => void,
|
|
||||||
sanitize: (data: RequestResponseData) => RequestResponseData | null,
|
|
||||||
sendMessage: (message: NetworkRequest) => void,
|
|
||||||
isServiceUrl: (url: string) => boolean,
|
|
||||||
tokenUrlMatcher?: (url: string) => boolean,
|
|
||||||
) {
|
|
||||||
if (context.XMLHttpRequest) {
|
|
||||||
context.XMLHttpRequest = XHRProxy.create(
|
|
||||||
ignoredHeaders,
|
|
||||||
setSessionTokenHeader,
|
|
||||||
sanitize,
|
|
||||||
sendMessage,
|
|
||||||
isServiceUrl,
|
|
||||||
tokenUrlMatcher,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
getWarning('XMLHttpRequest')
|
|
||||||
}
|
|
||||||
if (context.fetch) {
|
|
||||||
context.fetch = FetchProxy.create(
|
|
||||||
ignoredHeaders,
|
|
||||||
setSessionTokenHeader,
|
|
||||||
sanitize,
|
|
||||||
sendMessage,
|
|
||||||
isServiceUrl,
|
|
||||||
tokenUrlMatcher,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
getWarning('fetch')
|
|
||||||
}
|
|
||||||
if (context?.navigator?.sendBeacon) {
|
|
||||||
context.navigator.sendBeacon = BeaconProxy.create(
|
|
||||||
ignoredHeaders,
|
|
||||||
setSessionTokenHeader,
|
|
||||||
sanitize,
|
|
||||||
sendMessage,
|
|
||||||
isServiceUrl,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
export interface RequestResponseData {
|
|
||||||
readonly status: number
|
|
||||||
readonly method: string
|
|
||||||
url: string
|
|
||||||
request: {
|
|
||||||
body: string | null
|
|
||||||
headers: Record<string, string>
|
|
||||||
}
|
|
||||||
response: {
|
|
||||||
body: string | null
|
|
||||||
headers: Record<string, string>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we only support sanitizing for json/string data because how you're gonna sanitize binary data?
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { NetworkRequest } from '../app/messages.gen.js'
|
||||||
import { getTimeOrigin } from '../utils.js'
|
import { getTimeOrigin } from '../utils.js'
|
||||||
import type { AxiosInstance } from './axiosSpy.js'
|
import type { AxiosInstance } from './axiosSpy.js'
|
||||||
import axiosSpy from './axiosSpy.js'
|
import axiosSpy from './axiosSpy.js'
|
||||||
import setProxy from './Network/index.js'
|
import createNetworkProxy from '@openreplay/network-proxy'
|
||||||
|
|
||||||
type WindowFetch = typeof window.fetch
|
type WindowFetch = typeof window.fetch
|
||||||
type XHRRequestBody = Parameters<XMLHttpRequest['send']>[0]
|
type XHRRequestBody = Parameters<XMLHttpRequest['send']>[0]
|
||||||
|
|
@ -130,13 +130,28 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
const patchWindow = (context: typeof globalThis) => {
|
const patchWindow = (context: typeof globalThis) => {
|
||||||
/* ====== modern way ====== */
|
/* ====== modern way ====== */
|
||||||
if (options.useProxy) {
|
if (options.useProxy) {
|
||||||
return setProxy(
|
return createNetworkProxy(
|
||||||
context,
|
context,
|
||||||
options.ignoreHeaders,
|
options.ignoreHeaders,
|
||||||
setSessionTokenHeader,
|
setSessionTokenHeader,
|
||||||
sanitize,
|
sanitize,
|
||||||
(message) => app.send(message),
|
(message) => {
|
||||||
|
app.send(
|
||||||
|
NetworkRequest(
|
||||||
|
message.requestType,
|
||||||
|
message.method,
|
||||||
|
message.url,
|
||||||
|
message.request,
|
||||||
|
message.response,
|
||||||
|
message.status,
|
||||||
|
message.startTime + getTimeOrigin(),
|
||||||
|
message.duration,
|
||||||
|
message.responseSize,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
(url) => app.isServiceURL(url),
|
(url) => app.isServiceURL(url),
|
||||||
|
{ xhr: true, fetch: true, beacon: true },
|
||||||
options.tokenUrlMatcher,
|
options.tokenUrlMatcher,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
"target": "es2020",
|
"target": "es2020",
|
||||||
"module": "es6",
|
"module": "es6",
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true,
|
||||||
},
|
},
|
||||||
"exclude": ["**/*.test.ts"]
|
"exclude": ["**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue