From a691658ac38235593e2d799983049b09594c8834 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Fri, 26 Aug 2022 11:59:45 +0200 Subject: [PATCH] feat(tracker): return sessionHash on stop --- tracker/tracker/src/main/app/index.ts | 58 +++++++++--------------- tracker/tracker/src/main/app/session.ts | 60 +++++++++++++++++++++++-- tracker/tracker/src/main/index.ts | 15 ++++--- 3 files changed, 87 insertions(+), 46 deletions(-) diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index 5d8998146..f0a0d7248 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -13,6 +13,7 @@ import { deviceMemory, jsHeapSizeLimit } from '../modules/performance.js' import type { Options as ObserverOptions } from './observer/top_observer.js' import type { Options as SanitizerOptions } from './sanitizer.js' import type { Options as LoggerOptions } from './logger.js' +import type { Options as SessOptions } from './session.js' import type { Options as WebworkerOptions, WorkerMessageData } from '../../common/interaction.js' // TODO: Unify and clearly describe options logic @@ -49,9 +50,9 @@ enum ActivityState { type AppOptions = { revID: string node_id: string + session_reset_key: string session_token_key: string session_pageno_key: string - session_reset_key: string local_uuid_key: string ingestPoint: string resourceBaseHref: string | null // resourceHref? @@ -65,7 +66,8 @@ type AppOptions = { // @deprecated onStart?: StartCallback -} & WebworkerOptions +} & WebworkerOptions & + SessOptions export type Options = AppOptions & ObserverOptions & SanitizerOptions @@ -92,11 +94,7 @@ export default class App { private activityState: ActivityState = ActivityState.NotActive private readonly version = 'TRACKER_VERSION' // TODO: version compatability check inside each plugin. private readonly worker?: Worker - constructor( - projectKey: string, - sessionToken: string | null | undefined, - options: Partial, - ) { + constructor(projectKey: string, sessionHash: string | undefined, options: Partial) { // if (options.onStart !== undefined) { // deprecationWarn("'onStart' option", "tracker.start().then(/* handle session info */)") // } ?? maybe onStart is good @@ -129,7 +127,9 @@ export default class App { this.ticker.attach(() => this.commit()) this.debug = new Logger(this.options.__debug__) this.notify = new Logger(this.options.verbose ? LogLevel.Warnings : LogLevel.Silent) - this.session = new Session() + this.localStorage = this.options.localStorage || window.localStorage + this.sessionStorage = this.options.sessionStorage || window.sessionStorage + this.session = new Session(this, this.options) this.session.attachUpdateCallback(({ userID, metadata }) => { if (userID != null) { // TODO: nullable userID @@ -139,11 +139,9 @@ export default class App { Object.entries(metadata).forEach(([key, value]) => this.send(Metadata(key, value))) } }) - this.localStorage = this.options.localStorage || window.localStorage - this.sessionStorage = this.options.sessionStorage || window.sessionStorage - if (sessionToken != null) { - this.sessionStorage.setItem(this.options.session_token_key, sessionToken) + if (sessionHash != null) { + this.session.applySessionHash(sessionHash) } try { @@ -269,7 +267,7 @@ export default class App { return true } - private getStartInfo() { + private getTrackerInfo() { return { userUUID: this.localStorage.getItem(this.options.local_uuid_key), projectKey: this.projectKey, @@ -281,14 +279,11 @@ export default class App { getSessionInfo() { return { ...this.session.getInfo(), - ...this.getStartInfo(), + ...this.getTrackerInfo(), } } getSessionToken(): string | undefined { - const token = this.sessionStorage.getItem(this.options.session_token_key) - if (token !== null) { - return token - } + return this.session.getSessionToken() } getSessionID(): string | undefined { return this.session.getInfo().sessionID || undefined @@ -349,18 +344,10 @@ export default class App { } this.activityState = ActivityState.Starting - let pageNo = 0 - const pageNoStr = this.sessionStorage.getItem(this.options.session_pageno_key) - if (pageNoStr != null) { - pageNo = parseInt(pageNoStr) - pageNo++ - } - this.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()) - const timestamp = now() const startWorkerMsg: WorkerMessageData = { type: 'start', - pageNo, + pageNo: this.session.incPageNo(), ingestPoint: this.options.ingestPoint, timestamp, url: document.URL, @@ -387,10 +374,10 @@ export default class App { 'Content-Type': 'application/json', }, body: JSON.stringify({ - ...this.getStartInfo(), + ...this.getTrackerInfo(), timestamp, userID: this.session.getInfo().userID, - token: this.sessionStorage.getItem(this.options.session_token_key), + token: this.session.getSessionToken(), deviceMemory, jsHeapSizeLimit, reset: startOpts.forceNew || sReset !== null, @@ -429,7 +416,7 @@ export default class App { ) { return Promise.reject(`Incorrect server response: ${JSON.stringify(r)}`) } - this.sessionStorage.setItem(this.options.session_token_key, token) + this.session.setSessionToken(token) this.localStorage.setItem(this.options.local_uuid_key, userUUID) this.session.update({ sessionID, timestamp: startTimestamp || timestamp }) // TODO: no no-explicit 'any' const startWorkerMsg: WorkerMessageData = { @@ -455,8 +442,8 @@ export default class App { return SuccessfulStart(onStartInfo) }) .catch((reason) => { - this.sessionStorage.removeItem(this.options.session_token_key) this.stop() + this.session.reset() if (reason === CANCELED) { return UnsuccessfulStart(CANCELED) } @@ -482,7 +469,7 @@ export default class App { }) } } - stop(calledFromAPI = false, restarting = false): void { + stop(stopWorker = true): void { if (this.activityState !== ActivityState.NotActive) { try { this.sanitizer.clear() @@ -490,11 +477,8 @@ export default class App { this.nodes.clear() this.ticker.stop() this.stopCallbacks.forEach((cb) => cb()) - if (calledFromAPI) { - this.session.reset() - } this.notify.log('OpenReplay tracking stopped.') - if (this.worker && !restarting) { + if (this.worker && stopWorker) { this.worker.postMessage('stop') } } finally { @@ -503,7 +487,7 @@ export default class App { } } restart() { - this.stop(false, true) + this.stop(false) this.start({ forceNew: false }) } } diff --git a/tracker/tracker/src/main/app/session.ts b/tracker/tracker/src/main/app/session.ts index 110f0a60a..220500473 100644 --- a/tracker/tracker/src/main/app/session.ts +++ b/tracker/tracker/src/main/app/session.ts @@ -1,18 +1,27 @@ +import type App from './index.js' + interface SessionInfo { - sessionID: string | null + sessionID: string | undefined metadata: Record userID: string | null timestamp: number } type OnUpdateCallback = (i: Partial) => void +export type Options = { + session_token_key: string + session_pageno_key: string +} + export default class Session { private metadata: Record = {} private userID: string | null = null - private sessionID: string | null = null + private sessionID: string | undefined private readonly callbacks: OnUpdateCallback[] = [] private timestamp = 0 + constructor(private readonly app: App, private options: Options) {} + attachUpdateCallback(cb: OnUpdateCallback) { this.callbacks.push(cb) } @@ -52,6 +61,50 @@ export default class Session { this.handleUpdate({ userID }) } + private getPageNumber(): number | undefined { + const pageNoStr = this.app.sessionStorage.getItem(this.options.session_pageno_key) + if (pageNoStr == null) { + return undefined + } + return parseInt(pageNoStr) + } + + incPageNo(): number { + let pageNo = this.getPageNumber() + if (pageNo === undefined) { + pageNo = 0 + } else { + pageNo++ + } + this.app.sessionStorage.setItem(this.options.session_pageno_key, pageNo.toString()) + return pageNo + } + + getSessionToken(): string | undefined { + return this.app.sessionStorage.getItem(this.options.session_token_key) || undefined + } + setSessionToken(token: string): void { + this.app.sessionStorage.setItem(this.options.session_token_key, token) + } + + applySessionHash(hash: string) { + const [pageNoStr, token] = decodeURI(hash).split('&') + if (!pageNoStr || !token) { + return + } + this.app.sessionStorage.setItem(this.options.session_token_key, token) + this.app.sessionStorage.setItem(this.options.session_pageno_key, pageNoStr) + } + + getSessionHash(): string | undefined { + const pageNo = this.getPageNumber() + const token = this.getSessionToken() + if (pageNo === undefined || token === undefined) { + return + } + return encodeURI(String(pageNo) + '&' + token) + } + getInfo(): SessionInfo { return { sessionID: this.sessionID, @@ -62,9 +115,10 @@ export default class Session { } reset(): void { + this.app.sessionStorage.removeItem(this.options.session_token_key) this.metadata = {} this.userID = null - this.sessionID = null + this.sessionID = undefined this.timestamp = 0 } } diff --git a/tracker/tracker/src/main/index.ts b/tracker/tracker/src/main/index.ts index 53452f863..d505f46ad 100644 --- a/tracker/tracker/src/main/index.ts +++ b/tracker/tracker/src/main/index.ts @@ -37,7 +37,7 @@ export type Options = Partial< > & { projectID?: number // For the back compatibility only (deprecated) projectKey: string - sessionToken?: string + sessionHash?: string respectDoNotTrack?: boolean autoResetOnWindowOpen?: boolean // dev only @@ -70,9 +70,9 @@ function processOptions(obj: any): obj is Options { obj.projectKey = obj.projectKey.toString() } } - if (typeof obj.sessionToken !== 'string' && obj.sessionToken != null) { + if (typeof obj.sessionHash !== 'string' && obj.sessionHash != null) { console.warn( - `OpenReplay: invalid options argument type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`, + `OpenReplay: invalid 'sessionHash' option type. Please, check documentation on ${DOCS_HOST}${DOCS_SETUP}`, ) } return true @@ -110,7 +110,7 @@ export default class API { !('Blob' in window) || !('Worker' in window) ? null - : new App(options.projectKey, options.sessionToken, options)) + : new App(options.projectKey, options.sessionHash, options)) if (app !== null) { Viewport(app) CSSRules(app) @@ -184,11 +184,14 @@ export default class API { // TODO: check argument type return this.app.start(startOpts) } - stop(): void { + stop(): string | undefined { if (this.app === null) { return } - this.app.stop(true) + this.app.stop() + const sessionHash = this.app.session.getSessionHash() + this.app.session.reset() + return sessionHash } getSessionToken(): string | null | undefined {