feat(tracker): return sessionHash on stop

This commit is contained in:
Alex Kaminskii 2022-08-26 11:59:45 +02:00
parent fd24470c1f
commit a691658ac3
3 changed files with 87 additions and 46 deletions

View file

@ -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<Options>,
) {
constructor(projectKey: string, sessionHash: string | undefined, options: Partial<Options>) {
// 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 })
}
}

View file

@ -1,18 +1,27 @@
import type App from './index.js'
interface SessionInfo {
sessionID: string | null
sessionID: string | undefined
metadata: Record<string, string>
userID: string | null
timestamp: number
}
type OnUpdateCallback = (i: Partial<SessionInfo>) => void
export type Options = {
session_token_key: string
session_pageno_key: string
}
export default class Session {
private metadata: Record<string, string> = {}
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
}
}

View file

@ -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 {