Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
nick-delirium
cbf2ca1928
feat tracker: assist split messages, no peer option 2024-05-07 14:25:02 +02:00
nick-delirium
3cffbe18fc
9 0 14 2024-04-08 11:53:18 +02:00
nick-delirium
3902c26971 fix(tracker): fix fetch proxy .apply args check 2023-11-10 17:02:56 +01:00
nick-delirium
21f70fd3cb fix(tracker): fix resources filtering 2023-10-24 12:09:29 +02:00
nick-delirium
5f24189446 fix(tracker): 9.0.11 2023-10-19 16:03:06 +02:00
nick-delirium
d58bc39d47 fix(tracker): context dedup fix 2023-10-19 15:59:28 +02:00
nick-delirium
aca9448dc8 fix(tracker): v 9.0.10 2023-10-18 11:12:21 +02:00
nick-delirium
f8b4dc7088 fix(tracker): added excludedResourceUrls for timings 2023-10-18 11:12:07 +02:00
12 changed files with 151 additions and 43 deletions

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker-assist",
"description": "Tracker plugin for screen assistance through the WebRTC",
"version": "6.0.1",
"version": "6.0.2-beta.0",
"keywords": [
"WebRTC",
"assistance",
@ -31,7 +31,7 @@
},
"dependencies": {
"csstype": "^3.0.10",
"peerjs": "1.4.7",
"peerjs": "1.3.2",
"socket.io-client": "^4.4.1"
},
"peerDependencies": {

View file

@ -42,6 +42,7 @@ export interface Options {
config: RTCConfiguration;
serverURL: string
callUITemplate?: string;
noPeer?: boolean;
}
@ -89,6 +90,7 @@ export default class Assist {
callConfirm: {},
controlConfirm: {}, // TODO: clear options passing/merging/overwriting
recordingConfirm: {},
noPeer: false,
},
options,
)
@ -121,7 +123,13 @@ export default class Assist {
if (this.agentsConnected) {
// @ts-ignore No need in statistics messages. TODO proper filter
if (messages.length === 2 && messages[0]._id === 0 && messages[1]._id === 49) { return }
this.emit('messages', messages)
const middlePoint = Math.floor(messages.length / 2)
const msgEvents = messages.length < 10000
? [messages,]
: [messages.slice(0, middlePoint), ...messages.slice(middlePoint),]
msgEvents.forEach(batch => {
this.emit('messages', batch)
})
}
})
app.session.attachUpdateCallback(sessInfo => this.emit('UPDATE_SESSION', sessInfo))
@ -379,23 +387,25 @@ export default class Assist {
}
}
// PeerJS call (todo: use native WebRTC)
const peerOptions = {
host: this.getHost(),
path: this.getBasePrefixUrl()+'/assist',
port: location.protocol === 'http:' && this.noSecureMode ? 80 : 443,
//debug: appOptions.__debug_log ? 2 : 0, // 0 Print nothing //1 Prints only errors. / 2 Prints errors and warnings. / 3 Prints all logs.
}
if (this.options.config) {
peerOptions['config'] = this.options.config
}
if (!this.options.noPeer) {
// PeerJS call (todo: use native WebRTC)
const peerOptions = {
host: this.getHost(),
path: this.getBasePrefixUrl()+'/assist',
port: location.protocol === 'http:' && this.noSecureMode ? 80 : 443,
//debug: appOptions.__debug_log ? 2 : 0, // 0 Print nothing //1 Prints only errors. / 2 Prints errors and warnings. / 3 Prints all logs.
}
if (this.options.config) {
peerOptions['config'] = this.options.config
}
const peer = new safeCastedPeer(peerID, peerOptions) as Peer
this.peer = peer
const peer = new safeCastedPeer(peerID, peerOptions) as Peer
this.peer = peer
// @ts-ignore (peerjs typing)
peer.on('error', e => app.debug.warn('Peer error: ', e.type, e))
peer.on('disconnected', () => peer.reconnect())
// @ts-ignore
peer.on('error', e => app.debug.warn('Peer error: ', e.type, e))
peer.on('disconnected', () => peer.reconnect())
}
function updateCallerNames() {
callUI?.setAssistentName(callingAgents)
@ -453,7 +463,7 @@ export default class Assist {
}
const updateVideoFeed = ({ enabled, }) => this.emit('videofeed', { streamId: this.peer?.id, enabled, })
peer.on('call', (call) => {
this.peer?.on('call', (call) => {
app.debug.log('Incoming call from', call.peer)
let confirmAnswer: Promise<boolean>
const callingPeerIds = JSON.parse(sessionStorage.getItem(this.options.session_calling_peer_key) || '[]')

View file

@ -1,3 +1,12 @@
# 9.0.11
- new `resetTabOnWindowOpen` option to fix window.open issue with sessionStorage being inherited (replicating tabId bug), users still should use 'noopener=true' in window.open to prevent it in general...
- do not create BC channel in iframe context, add regeneration of tabid incase of duplication
# 9.0.10
- added `excludedResourceUrls` to timings options to better sanitize network data
# 9.0.9
- Fix for `{disableStringDict: true}` behavior

View file

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

View file

@ -1,6 +1,6 @@
import type Message from './messages.gen.js'
import { Timestamp, Metadata, UserID, Type as MType, TabChange, TabData } from './messages.gen.js'
import { now, adjustTimeOrigin, deprecationWarn } from '../utils.js'
import { now, adjustTimeOrigin, deprecationWarn, inIframe } from '../utils.js'
import Nodes from './nodes.js'
import Observer from './observer/top_observer.js'
import Sanitizer from './sanitizer.js'
@ -39,6 +39,7 @@ interface OnStartInfo {
sessionToken: string
userUUID: string
}
const CANCELED = 'canceled' as const
const START_ERROR = ':(' as const
type SuccessfulStart = OnStartInfo & { success: true }
@ -47,9 +48,10 @@ type UnsuccessfulStart = {
success: false
}
type RickRoll = { source: string } & (
type RickRoll = { source: string; context: string } & (
| { line: 'never-gonna-give-you-up' }
| { line: 'never-gonna-let-you-down'; token: string }
| { line: 'never-gonna-run-around-and-desert-you'; token: string }
)
const UnsuccessfulStart = (reason: string): UnsuccessfulStart => ({ reason, success: false })
@ -58,6 +60,7 @@ export type StartPromiseReturn = SuccessfulStart | UnsuccessfulStart
type StartCallback = (i: OnStartInfo) => void
type CommitCallback = (messages: Array<Message>) => void
enum ActivityState {
NotActive,
Starting,
@ -114,7 +117,8 @@ export default class App {
readonly localStorage: Storage
readonly sessionStorage: Storage
private readonly messages: Array<Message> = []
/* private */ readonly observer: Observer // non-privat for attachContextCallback
/* private */
readonly observer: Observer // non-privat for attachContextCallback
private readonly startCallbacks: Array<StartCallback> = []
private readonly stopCallbacks: Array<() => any> = []
private readonly commitCallbacks: Array<CommitCallback> = []
@ -128,13 +132,14 @@ export default class App {
private compressionThreshold = 24 * 1000
private restartAttempts = 0
private readonly bc: BroadcastChannel | null = null
private readonly contextId
public attributeSender: AttributeSender
constructor(projectKey: string, sessionToken: string | undefined, options: Partial<Options>) {
// if (options.onStart !== undefined) {
// deprecationWarn("'onStart' option", "tracker.start().then(/* handle session info */)")
// } ?? maybe onStart is good
this.contextId = Math.random().toString(36).slice(2)
this.projectKey = projectKey
this.networkOptions = options.network
this.options = Object.assign(
@ -160,7 +165,7 @@ export default class App {
)
if (!this.options.forceSingleTab && globalThis && 'BroadcastChannel' in globalThis) {
this.bc = new BroadcastChannel('rick')
this.bc = inIframe() ? null : new BroadcastChannel('rick')
}
this.revID = this.options.revID
@ -243,24 +248,45 @@ export default class App {
const thisTab = this.session.getTabId()
if (!this.session.getSessionToken() && this.bc) {
this.bc.postMessage({ line: 'never-gonna-give-you-up', source: thisTab })
}
const proto = {
// ask if there are any tabs alive
ask: 'never-gonna-give-you-up',
// yes, there are someone out there
resp: 'never-gonna-let-you-down',
// you stole someone's identity
reg: 'never-gonna-run-around-and-desert-you',
} as const
if (this.bc) {
this.bc.postMessage({
line: proto.ask,
source: thisTab,
context: this.contextId,
})
}
if (this.bc !== null) {
this.bc.onmessage = (ev: MessageEvent<RickRoll>) => {
if (ev.data.source === thisTab) return
if (ev.data.line === 'never-gonna-let-you-down') {
if (ev.data.context === this.contextId) {
return
}
if (ev.data.line === proto.resp) {
const sessionToken = ev.data.token
this.session.setSessionToken(sessionToken)
}
if (ev.data.line === 'never-gonna-give-you-up') {
if (ev.data.line === proto.reg) {
const sessionToken = ev.data.token
this.session.regenerateTabId()
this.session.setSessionToken(sessionToken)
}
if (ev.data.line === proto.ask) {
const token = this.session.getSessionToken()
if (token && this.bc) {
this.bc.postMessage({
line: 'never-gonna-let-you-down',
line: ev.data.source === thisTab ? proto.reg : proto.resp,
token,
source: thisTab,
context: this.contextId,
})
}
}
@ -284,6 +310,7 @@ export default class App {
}
private _usingOldFetchPlugin = false
send(message: Message, urgent = false): void {
if (this.activityState === ActivityState.NotActive) {
return
@ -309,6 +336,7 @@ export default class App {
this.commit()
}
}
private commit(): void {
if (this.worker && this.messages.length) {
this.messages.unshift(TabData(this.session.getTabId()))
@ -320,6 +348,7 @@ export default class App {
}
private delay = 0
timestamp(): number {
return now() + this.delay
}
@ -342,18 +371,21 @@ export default class App {
attachCommitCallback(cb: CommitCallback): void {
this.commitCallbacks.push(cb)
}
attachStartCallback(cb: StartCallback, useSafe = false): void {
if (useSafe) {
cb = this.safe(cb)
}
this.startCallbacks.push(cb)
}
attachStopCallback(cb: () => any, useSafe = false): void {
if (useSafe) {
cb = this.safe(cb)
}
this.stopCallbacks.push(cb)
}
// Use app.nodes.attachNodeListener for registered nodes instead
attachEventListener(
target: EventTarget,
@ -396,15 +428,18 @@ export default class App {
isSnippet: this.options.__is_snippet,
}
}
getSessionInfo() {
return {
...this.session.getInfo(),
...this.getTrackerInfo(),
}
}
getSessionToken(): string | undefined {
return this.session.getSessionToken()
}
getSessionID(): string | undefined {
return this.session.getInfo().sessionID || undefined
}
@ -433,9 +468,11 @@ export default class App {
getHost(): string {
return new URL(this.options.ingestPoint).host
}
getProjectKey(): string {
return this.projectKey
}
getBaseHref(): string {
if (typeof this.options.resourceBaseHref === 'string') {
return this.options.resourceBaseHref
@ -451,6 +488,7 @@ export default class App {
location.origin + location.pathname
)
}
resolveResourceURL(resourceURL: string): string {
const base = new URL(this.getBaseHref())
base.pathname += '/' + new URL(resourceURL).pathname
@ -519,7 +557,12 @@ export default class App {
const sessionToken = this.session.getSessionToken()
const isNewSession = needNewSessionID || !sessionToken
console.log('OpenReplay: starting session', needNewSessionID, sessionToken)
console.log(
'OpenReplay: starting session; need new session id?',
needNewSessionID,
'session token: ',
sessionToken,
)
return window
.fetch(this.options.ingestPoint + '/v1/web/start', {
method: 'POST',
@ -654,7 +697,7 @@ export default class App {
return new Promise((resolve) => {
setTimeout(() => {
resolve(this._start(...args))
}, 10)
}, 25)
})
} else {
return new Promise((resolve) => {
@ -663,7 +706,7 @@ export default class App {
document.removeEventListener('visibilitychange', onVisibilityChange)
setTimeout(() => {
resolve(this._start(...args))
}, 10)
}, 25)
}
}
document.addEventListener('visibilitychange', onVisibilityChange)
@ -678,6 +721,7 @@ export default class App {
getTabId() {
return this.session.getTabId()
}
stop(stopWorker = true): void {
if (this.activityState !== ActivityState.NotActive) {
try {

View file

@ -141,14 +141,18 @@ export default class Session {
return this.tabId
}
public regenerateTabId() {
const randomId = generateRandomId(12)
this.app.sessionStorage.setItem(this.options.session_tabid_key, randomId)
this.tabId = randomId
}
private createTabId() {
const localId = this.app.sessionStorage.getItem(this.options.session_tabid_key)
if (localId) {
this.tabId = localId
} else {
const randomId = generateRandomId(12)
this.app.sessionStorage.setItem(this.options.session_tabid_key, randomId)
this.tabId = randomId
this.regenerateTabId()
}
}

View file

@ -1,8 +1,10 @@
import App, { DEFAULT_INGEST_POINT } from './app/index.js'
export { default as App } from './app/index.js'
import { UserAnonymousID, CustomEvent, CustomIssue } from './app/messages.gen.js'
import * as _Messages from './app/messages.gen.js'
export const Messages = _Messages
export { SanitizeLevel } from './app/sanitizer.js'
@ -49,6 +51,7 @@ export type Options = Partial<
sessionToken?: string
respectDoNotTrack?: boolean
autoResetOnWindowOpen?: boolean
resetTabOnWindowOpen?: boolean
network?: Partial<NetworkOptions>
mouse?: Partial<MouseHandlerOptions>
flags?: {
@ -93,6 +96,7 @@ function processOptions(obj: any): obj is Options {
export default class API {
public featureFlags: FeatureFlags
private readonly app: App | null = null
constructor(private readonly options: Options) {
if (!IN_BROWSER || !processOptions(options)) {
return
@ -151,14 +155,23 @@ export default class API {
}
void this.featureFlags.reloadFlags()
})
if (options.autoResetOnWindowOpen) {
const wOpen = window.open
const wOpen = window.open
if (options.autoResetOnWindowOpen || options.resetTabOnWindowOpen) {
app.attachStartCallback(() => {
const tabId = app.getTabId()
const sessStorage = app.sessionStorage ?? window.sessionStorage
// @ts-ignore ?
window.open = function (...args) {
app.resetNextPageSession(true)
wOpen.call(window, ...args)
if (options.autoResetOnWindowOpen) {
app.resetNextPageSession(true)
}
if (options.resetTabOnWindowOpen) {
sessStorage.removeItem(options.session_tabid_key || '__openreplay_tabid')
}
const result = wOpen.call(window, ...args)
app.resetNextPageSession(false)
sessStorage.setItem(options.session_tabid_key || '__openreplay_tabid', tabId)
return result
}
})
app.attachStopCallback(() => {
@ -255,6 +268,7 @@ export default class API {
}
return this.app.getSessionToken()
}
getSessionID(): string | null | undefined {
if (this.app === null) {
return null
@ -268,6 +282,7 @@ export default class API {
}
return this.app.getTabId()
}
sessionID(): string | null | undefined {
deprecationWarn("'sessionID' method", "'getSessionID' method", '/')
return this.getSessionID()
@ -285,6 +300,7 @@ export default class API {
this.app.session.setUserID(id)
}
}
userID(id: string): void {
deprecationWarn("'userID' method", "'setUserID' method", '/')
this.setUserID(id)
@ -295,6 +311,7 @@ export default class API {
this.app.send(UserAnonymousID(id))
}
}
userAnonymousID(id: string): void {
deprecationWarn("'userAnonymousID' method", "'setUserAnonymousID' method", '/')
this.setUserAnonymousID(id)
@ -305,6 +322,7 @@ export default class API {
this.app.session.setMetadata(key, value)
}
}
metadata(key: string, value: string): void {
deprecationWarn("'metadata' method", "'setMetadata' method", '/')
this.setMetadata(key, value)

View file

@ -131,6 +131,7 @@ export class FetchProxyHandler<T extends typeof fetch> implements ProxyHandler<T
public apply(target: T, _: typeof window, argsList: [RequestInfo | URL, RequestInit]) {
const input = argsList[0]
const init = argsList[1]
if (!input) return target.apply(window, argsList)
const isORUrl =
input instanceof URL || typeof input === 'string'
@ -177,7 +178,7 @@ export class FetchProxyHandler<T extends typeof fetch> implements ProxyHandler<T
})
}
protected beforeFetch(item: NetworkMessage, input: RequestInfo, init?: RequestInit) {
protected beforeFetch(item: NetworkMessage, input: RequestInfo | string, init?: RequestInit) {
let url: URL,
method = 'GET',
requestHeader: HeadersInit = {}

View file

@ -168,6 +168,7 @@ export default function (app: App, options?: MouseHandlerOptions): void {
mouseTarget = null
selectorMap = {}
if (checkIntervalId) {
// @ts-ignore
clearInterval(checkIntervalId)
}
})

View file

@ -230,6 +230,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
return response
})
}
// @ts-ignore
context.fetch = trackFetch
/* ====== <> ====== */

View file

@ -83,6 +83,7 @@ export interface Options {
captureResourceTimings: boolean
capturePageLoadTimings: boolean
capturePageRenderTimings: boolean
excludedResourceUrls?: Array<string>
}
export default function (app: App, opts: Partial<Options>): void {
@ -91,6 +92,7 @@ export default function (app: App, opts: Partial<Options>): void {
captureResourceTimings: true,
capturePageLoadTimings: true,
capturePageRenderTimings: true,
excludedResourceUrls: [],
},
opts,
)
@ -108,6 +110,16 @@ export default function (app: App, opts: Partial<Options>): void {
if (resources !== null) {
resources[entry.name] = entry.startTime + entry.duration
}
let shouldSkip = false
options.excludedResourceUrls?.forEach((url) => {
if (entry.name.startsWith(url)) {
shouldSkip = true
return
}
})
if (shouldSkip) {
return
}
app.send(
ResourceTiming(
entry.startTime + getTimeOrigin(),

View file

@ -105,3 +105,11 @@ export function generateRandomId(len?: number) {
safeCrypto.getRandomValues(arr)
return Array.from(arr, dec2hex).join('')
}
export function inIframe() {
try {
return window.self !== window.top
} catch (e) {
return true
}
}