diff --git a/tracker/tracker/CHANGELOG.md b/tracker/tracker/CHANGELOG.md index ba51f47c1..155dc8bf6 100644 --- a/tracker/tracker/CHANGELOG.md +++ b/tracker/tracker/CHANGELOG.md @@ -1,3 +1,7 @@ +# 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... + # 9.0.10 - added `excludedResourceUrls` to timings options to better sanitize network data diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index f371b6a6e..43d425bdf 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "9.0.10", + "version": "9.0.11-beta.14", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 8caec3821..6ecaae960 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -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) => void + enum ActivityState { NotActive, Starting, @@ -114,7 +117,8 @@ export default class App { readonly localStorage: Storage readonly sessionStorage: Storage private readonly messages: Array = [] - /* private */ readonly observer: Observer // non-privat for attachContextCallback + /* private */ + readonly observer: Observer // non-privat for attachContextCallback private readonly startCallbacks: Array = [] private readonly stopCallbacks: Array<() => any> = [] private readonly commitCallbacks: Array = [] @@ -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) { // 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) => { - 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 @@ -654,7 +692,7 @@ export default class App { return new Promise((resolve) => { setTimeout(() => { resolve(this._start(...args)) - }, 10) + }, 25) }) } else { return new Promise((resolve) => { @@ -663,7 +701,7 @@ export default class App { document.removeEventListener('visibilitychange', onVisibilityChange) setTimeout(() => { resolve(this._start(...args)) - }, 10) + }, 25) } } document.addEventListener('visibilitychange', onVisibilityChange) @@ -678,6 +716,7 @@ export default class App { getTabId() { return this.session.getTabId() } + stop(stopWorker = true): void { if (this.activityState !== ActivityState.NotActive) { try { diff --git a/tracker/tracker/src/main/app/session.ts b/tracker/tracker/src/main/app/session.ts index c308fdc99..656bbae71 100644 --- a/tracker/tracker/src/main/app/session.ts +++ b/tracker/tracker/src/main/app/session.ts @@ -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() } } diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index ebc15bc04..6f401075e 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -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 mouse?: Partial 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,22 @@ 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) + if (options.autoResetOnWindowOpen) { + app.resetNextPageSession(true) + } + if (options.resetTabOnWindowOpen) { + sessStorage.removeItem(options.session_tabid_key || '__openreplay_tabid') + } wOpen.call(window, ...args) app.resetNextPageSession(false) + sessStorage.setItem(options.session_tabid_key || '__openreplay_tabid', tabId) } }) app.attachStopCallback(() => { @@ -255,6 +267,7 @@ export default class API { } return this.app.getSessionToken() } + getSessionID(): string | null | undefined { if (this.app === null) { return null @@ -268,6 +281,7 @@ export default class API { } return this.app.getTabId() } + sessionID(): string | null | undefined { deprecationWarn("'sessionID' method", "'getSessionID' method", '/') return this.getSessionID() @@ -285,6 +299,7 @@ export default class API { this.app.session.setUserID(id) } } + userID(id: string): void { deprecationWarn("'userID' method", "'setUserID' method", '/') this.setUserID(id) @@ -295,6 +310,7 @@ export default class API { this.app.send(UserAnonymousID(id)) } } + userAnonymousID(id: string): void { deprecationWarn("'userAnonymousID' method", "'setUserAnonymousID' method", '/') this.setUserAnonymousID(id) @@ -305,6 +321,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) diff --git a/tracker/tracker/src/main/utils.ts b/tracker/tracker/src/main/utils.ts index 5b33503f6..ef8c8c30f 100644 --- a/tracker/tracker/src/main/utils.ts +++ b/tracker/tracker/src/main/utils.ts @@ -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 + } +}