fix(tracker):3.5.13+:reuse metadata on internal-caused restarts

This commit is contained in:
Alex Kaminskii 2022-07-01 23:28:28 +02:00
parent c264c49d05
commit 4c6698dea3
3 changed files with 56 additions and 61 deletions

View file

@ -1,5 +1,5 @@
import type Message from "../../common/messages.js";
import { Timestamp, Metadata } from "../../common/messages.js";
import { Timestamp, Metadata, UserID } from "../../common/messages.js";
import { timestamp, deprecationWarn } from "../utils.js";
import Nodes from "./nodes.js";
import Observer from "./observer/top_observer.js";
@ -131,7 +131,15 @@ 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);
this.session = new Session();
this.session.attachUpdateCallback(({ userID, metadata }) => {
if (userID != null) { // TODO: nullable userID
this.send(new UserID(userID))
}
if (metadata != null) {
Object.entries(metadata).forEach(([key, value]) => this.send(new Metadata(key, value)))
}
})
this.localStorage = this.options.localStorage;
this.sessionStorage = this.options.sessionStorage;
@ -151,6 +159,7 @@ export default class App {
this.worker.onmessage = ({ data }: MessageEvent) => {
if (data === "failed") {
this.stop();
this._debug("worker_failed", {}) // add context (from worker)
} else if (data === "restart") {
this.stop();
this.start({ forceNew: true });
@ -161,9 +170,10 @@ export default class App {
this.worker.postMessage(null);
}
}
// TODO: keep better tactics, discard others (look https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon)
// keep better tactics, discard others?
this.attachEventListener(window, 'beforeunload', alertWorker, false);
this.attachEventListener(document.body, 'mouseleave', alertWorker, false, false);
// TODO: stop session after inactivity timeout (make configurable)
this.attachEventListener(document, 'visibilitychange', alertWorker, false);
} catch (e) {
this._debug("worker_start", e);
@ -263,7 +273,7 @@ export default class App {
userUUID: this.localStorage.getItem(this.options.local_uuid_key),
projectKey: this.projectKey,
revID: this.revID,
timestamp: timestamp(),
timestamp: timestamp(), // shouldn't it be set once?
trackerVersion: this.version,
isSnippet: this.options.__is_snippet,
}
@ -351,7 +361,14 @@ export default class App {
connAttemptCount: this.options.connAttemptCount,
connAttemptGap: this.options.connAttemptGap,
}
this.worker.postMessage(startWorkerMsg) // brings delay of 10th ms?
this.worker.postMessage(startWorkerMsg)
this.session.update({ // TODO: transparent "session" module logic AND explicit internal api for plugins.
// "updating" with old metadata in order to trigger session's UpdateCallbacks.
// (for the case of internal .start() calls, like on "restart" webworker signal or assistent connection in tracker-assist )
metadata: startOpts.metadata || this.session.getInfo().metadata,
userID: startOpts.userID,
})
const sReset = this.sessionStorage.getItem(this.options.session_reset_key);
this.sessionStorage.removeItem(this.options.session_reset_key);
@ -363,7 +380,7 @@ export default class App {
},
body: JSON.stringify({
...startInfo,
userID: startOpts.userID || this.session.getInfo().userID,
userID: this.session.getInfo().userID,
token: this.sessionStorage.getItem(this.options.session_token_key),
deviceMemory,
jsHeapSizeLimit,
@ -375,7 +392,7 @@ export default class App {
return r.json()
} else {
return r.text().then(text => text === CANCELED
? Promise.reject(CANCELED) // TODO: return {error: CANCELED} instead
? Promise.reject(CANCELED)
: Promise.reject(`Server error: ${r.status}. ${text}`)
);
}
@ -392,11 +409,7 @@ export default class App {
}
this.sessionStorage.setItem(this.options.session_token_key, token);
this.localStorage.setItem(this.options.local_uuid_key, userUUID);
this.session.update({
sessionID, // any. TODO check
...startOpts
});
this.session.update({ sessionID }) // TODO: no no-explicit 'any'
const startWorkerMsg: WorkerMessageData = {
type: "auth",
token,
@ -404,21 +417,16 @@ export default class App {
}
this.worker.postMessage(startWorkerMsg)
this.activityState = ActivityState.Active
const onStartInfo = { sessionToken: token, userUUID, sessionID };
this.startCallbacks.forEach((cb) => cb(onStartInfo));
this.startCallbacks.forEach((cb) => cb(onStartInfo)); // TODO: start as early as possible (before receiving the token)
this.observer.observe();
this.ticker.start();
if (startOpts.metadata) {
Object.entries(startOpts.metadata)
.forEach(([ key, value ]) => this.send(new Metadata(key, value)))
}
this.notify.log("OpenReplay tracking started.");
// TODO: get rid of onStart
// get rid of onStart ?
if (typeof this.options.onStart === 'function') {
this.options.onStart(onStartInfo)
}
@ -450,7 +458,7 @@ export default class App {
})
}
}
stop(): void {
stop(calledFromAPI = false): void {
if (this.activityState !== ActivityState.NotActive) {
try {
this.sanitizer.clear()
@ -458,6 +466,9 @@ 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) {
this.worker.postMessage("stop")

View file

@ -1,13 +1,6 @@
import App, { StartOptions } from "./index.js";
import { UserID, UserAnonymousID, Metadata } from "../../common/messages.js";
enum ActivityState {
NotActive,
Starting,
Active,
}
interface SessionInfo {
sessionID: string | null,
metadata: Record<string, string>,
@ -20,57 +13,42 @@ export default class Session {
private metadata: Record<string, string> = {}
private userID: string | null = null
private sessionID: string | null = null
private activityState: ActivityState = ActivityState.NotActive
private callbacks: OnUpdateCallback[] = []
constructor(private app: App) {}
attachUpdateCallback(cb: OnUpdateCallback) {
this.callbacks.push(cb)
}
private handleUpdate() {
const sessInfo: Partial<SessionInfo> = this.getInfo()
if (sessInfo.userID == null) {
delete sessInfo.userID
private handleUpdate(newInfo: Partial<SessionInfo>) {
if (newInfo.userID == null) {
delete newInfo.userID
}
if (sessInfo.sessionID == null) {
delete sessInfo.sessionID
if (newInfo.sessionID == null) {
delete newInfo.sessionID
}
this.callbacks.forEach(cb => cb(sessInfo))
this.callbacks.forEach(cb => cb(newInfo))
}
update({ userID, metadata, sessionID }: Partial<SessionInfo>) {
if (userID != null) { // TODO clear nullable/undefinable types
this._setUserID(userID)
update(newInfo: Partial<SessionInfo>) {
if (newInfo.userID !== undefined) { // TODO clear nullable/undefinable types
this.userID = newInfo.userID
}
if (metadata !== undefined) {
Object.entries(metadata).forEach(kv => this._setMetadata(...kv))
if (newInfo.metadata !== undefined) {
Object.entries(newInfo.metadata).forEach(([k,v]) => this.metadata[k] = v)
}
if (sessionID !== undefined) {
this.sessionID = sessionID
if (newInfo.sessionID !== undefined) {
this.sessionID = newInfo.sessionID
}
this.handleUpdate()
this.handleUpdate(newInfo)
}
private _setMetadata(key: string, value: string) {
this.app.send(new Metadata(key, value))
this.metadata[key] = value
}
private _setUserID(userID: string) {
this.app.send(new UserID(userID))
this.userID = userID
}
setMetadata(key: string, value: string) {
this._setMetadata(key, value)
this.handleUpdate()
this.metadata[key] = value
this.handleUpdate({ metadata: { [key]: value } })
}
setUserID(userID: string) {
this._setUserID(userID)
this.handleUpdate()
this.userID = userID
this.handleUpdate({ userID })
}
getInfo(): SessionInfo {
@ -80,4 +58,10 @@ export default class Session {
userID: this.userID,
}
}
reset(): void {
this.metadata = {}
this.userID = null
this.sessionID = null
}
}

View file

@ -166,7 +166,7 @@ export default class API {
if (this.app === null) {
return;
}
this.app.stop();
this.app.stop(true);
}
getSessionToken(): string | null | undefined {