Compare commits
8 commits
main
...
tracker-9.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbf2ca1928 | ||
|
|
3cffbe18fc | ||
|
|
3902c26971 | ||
|
|
21f70fd3cb | ||
|
|
5f24189446 | ||
|
|
d58bc39d47 | ||
|
|
aca9448dc8 | ||
|
|
f8b4dc7088 |
12 changed files with 151 additions and 43 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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) || '[]')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "9.0.9",
|
||||
"version": "9.0.14",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export default function (app: App, options?: MouseHandlerOptions): void {
|
|||
mouseTarget = null
|
||||
selectorMap = {}
|
||||
if (checkIntervalId) {
|
||||
// @ts-ignore
|
||||
clearInterval(checkIntervalId)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
|||
return response
|
||||
})
|
||||
}
|
||||
// @ts-ignore
|
||||
context.fetch = trackFetch
|
||||
|
||||
/* ====== <> ====== */
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue