UNPKG

@embrace-io/web-sdk

Version:
610 lines (609 loc) 24.6 kB
import { clampNumber } from "../../utils/clampNumber.js"; import { generateUUID } from "../../utils/generateUUID.js"; import { getIncrementedCount } from "../../utils/getIncrementedCount.js"; import { KEY_EMB_COLD_START, KEY_EMB_IS_FINAL_SESSION_PART, KEY_EMB_PAGE_LOAD, KEY_EMB_SDK_STARTUP_DURATION, KEY_EMB_SESSION_PART_END_REASON, KEY_EMB_SESSION_PART_ID, KEY_EMB_SESSION_PART_NUMBER, KEY_EMB_SESSION_PART_START_REASON, KEY_EMB_STATE, KEY_EMB_TYPE, KEY_EMB_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS, KEY_EMB_USER_SESSION_ID, KEY_EMB_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS, KEY_EMB_USER_SESSION_MAX_DURATION_SECONDS, KEY_EMB_USER_SESSION_NUMBER, KEY_EMB_USER_SESSION_PART_INDEX, KEY_EMB_USER_SESSION_PREVIOUS_ID, KEY_EMB_USER_SESSION_START_TS, KEY_EMB_USER_SESSION_TERMINATION_REASON, KEY_PREFIX_EMB_PROPERTIES } from "../../constants/attributes.js"; import { getVisibilityState } from "../../utils/getVisibilityState.js"; import { throttle } from "../../utils/throttle.js"; import { EmbraceExtendedSpan } from "../EmbraceTraceManager/EmbraceExtendedSpan.js"; import { DEFAULT_ACTIVITY_EVENTS, DEFAULT_ACTIVITY_THROTTLE_MS, DEFAULT_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_USER_SESSION_MAX_DURATION_SECONDS, EMBRACE_LAST_END_USER_SESSION_TS_KEY, EMBRACE_SESSION_PART_NUMBER_KEY, EMBRACE_USER_SESSION_NUMBER_KEY, EMBRACE_USER_SESSION_STATE_KEY, FINAL_SESSION_PART_END_REASONS, MAX_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS, MAX_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS, MAX_USER_SESSION_MAX_DURATION_SECONDS, MIN_USER_SESSION_MAX_DURATION_SECONDS, SESSION_PART_SPAN_NAME } from "./constants.js"; import { addActivityListeners, isTabEngaged, removeActivityListeners } from "./utils/activity.js"; import { createUserSessionState, isUserSessionExpired, readPermanentProperties, readUserSessionState, storePermanentProperties } from "./utils/state.js"; import { diag, trace } from "@opentelemetry/api"; import { ATTR_SESSION_ID, ATTR_SESSION_PREVIOUS_ID } from "@opentelemetry/semantic-conventions/incubating"; //#region src/managers/EmbraceUserSessionManager/EmbraceUserSessionManager.ts /** * Parts are engagement-gated (visible AND focused), so only one part can * be active at a time. Engagement exclusivity lets us treat storage as * a plain shared row read on engagement and written on changes. */ var EmbraceUserSessionManager = class { _state = null; _previousUserSessionId = null; _maxDurationTimeout = null; _permanentProperties = {}; _hasStoredState = false; _activeSessionPartId = null; _currentSessionPartNumber = null; _sessionPartSpan = null; _activeSessionPartCounts = null; _coldStart = true; _nextSessionPartCounts = {}; _sdkStartupDuration = 0; _sessionPartStartedListeners = []; _sessionPartEndedListeners = []; _tracer; _diag; _perf; _storage; _visibilityDoc; _limitManager; _dynamicConfigManager; _target; _activityEvents; _onActivityThrottled; _sessionPartInactivityTimer = null; constructor({ dynamicConfigManager, diag: diagParam, perf, visibilityDoc, storage, limitManager, target = window, activityThrottleMs = DEFAULT_ACTIVITY_THROTTLE_MS, activityEvents = DEFAULT_ACTIVITY_EVENTS }) { this._diag = diagParam ?? diag.createComponentLogger({ namespace: "EmbraceUserSessionManager" }); this._perf = perf; this._visibilityDoc = visibilityDoc; this._storage = storage; this._limitManager = limitManager; this._dynamicConfigManager = dynamicConfigManager; this._permanentProperties = readPermanentProperties(this._storage, this._diag); this._tracer = trace.getTracer("embrace-web-sdk-session-parts"); this._target = target; this._activityEvents = activityEvents; this._onActivityThrottled = throttle(this._onActivity, activityThrottleMs); } /** * Reads the user-session durations from remote config, clamps each to its * allowed range, and enforces inactivity <= max duration. Called once per * user-session creation; the resolved values are frozen into that session's * state. */ _resolveUserSessionDurations() { const config = this._dynamicConfigManager.getConfig(); const userSessionMaxDurationSeconds = clampNumber({ diag: this._diag, value: config.userSessionMaxDurationSeconds, defaultValue: DEFAULT_USER_SESSION_MAX_DURATION_SECONDS, min: MIN_USER_SESSION_MAX_DURATION_SECONDS, max: MAX_USER_SESSION_MAX_DURATION_SECONDS }); const userSessionInactivityTimeoutSeconds = clampNumber({ diag: this._diag, value: config.userSessionInactivityTimeoutSeconds, defaultValue: DEFAULT_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS, min: 30, max: MAX_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS }); const userSessionForegroundInactivityTimeoutSeconds = clampNumber({ diag: this._diag, value: config.userSessionForegroundInactivityTimeoutSeconds, defaultValue: DEFAULT_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS, min: 30, max: MAX_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS }); const inactivityWithinMax = userSessionInactivityTimeoutSeconds <= userSessionMaxDurationSeconds; const foregroundWithinMax = userSessionForegroundInactivityTimeoutSeconds <= userSessionMaxDurationSeconds; if (!inactivityWithinMax) this._diag.warn(`userSessionInactivityTimeoutSeconds (${userSessionInactivityTimeoutSeconds.toString()}s) exceeds userSessionMaxDurationSeconds (${userSessionMaxDurationSeconds.toString()}s); falling back to default inactivity timeout (${DEFAULT_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS.toString()}s).`); if (!foregroundWithinMax) this._diag.warn(`userSessionForegroundInactivityTimeoutSeconds (${userSessionForegroundInactivityTimeoutSeconds.toString()}s) exceeds userSessionMaxDurationSeconds (${userSessionMaxDurationSeconds.toString()}s); falling back to default foreground inactivity timeout (${DEFAULT_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS.toString()}s).`); return { userSessionMaxDurationSeconds, userSessionInactivityTimeoutSeconds: inactivityWithinMax ? userSessionInactivityTimeoutSeconds : DEFAULT_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS, userSessionForegroundInactivityTimeoutSeconds: foregroundWithinMax ? userSessionForegroundInactivityTimeoutSeconds : DEFAULT_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS }; } setTracerProvider(tracerProvider) { this._tracer = tracerProvider.getTracer("embrace-web-sdk-session-parts"); addActivityListeners({ target: this._target, visibilityDoc: this._visibilityDoc, activityEvents: this._activityEvents, onActivity: this._onActivityThrottled, onVisibilityChange: this._onVisibilityChange, onFocus: this._onFocus, onBlur: this._onBlur }); const { state, created } = this._loadOrCreateUserSessionState(this._perf.getNowMillis()); this._state = state; if (created) this._storeState(); } recordSDKStartupDuration(duration) { this._sdkStartupDuration = Math.ceil(duration); } getUserSessionId() { return this._state?.userSessionId ?? null; } getPreviousUserSessionId() { return this._state?.previousUserSessionId ?? this._previousUserSessionId; } getUserSessionStartTime() { return this._state?.userSessionStartTs ?? null; } getUserSessionAttributes() { if (!this._state) return null; const previousUserSessionId = this.getPreviousUserSessionId() ?? ""; return { [KEY_EMB_USER_SESSION_ID]: this._state.userSessionId, [ATTR_SESSION_ID]: this._state.userSessionId, [KEY_EMB_USER_SESSION_PREVIOUS_ID]: previousUserSessionId, [ATTR_SESSION_PREVIOUS_ID]: previousUserSessionId, [KEY_EMB_USER_SESSION_NUMBER]: this._state.userSessionNumber, [KEY_EMB_USER_SESSION_PART_INDEX]: this._state.userSessionPartIndex, [KEY_EMB_SESSION_PART_NUMBER]: this._currentSessionPartNumber ?? 0, [KEY_EMB_USER_SESSION_START_TS]: this._state.userSessionStartTs, [KEY_EMB_USER_SESSION_MAX_DURATION_SECONDS]: this._state.userSessionMaxDurationSeconds, [KEY_EMB_USER_SESSION_INACTIVITY_TIMEOUT_SECONDS]: this._state.userSessionInactivityTimeoutSeconds, [KEY_EMB_USER_SESSION_FOREGROUND_INACTIVITY_TIMEOUT_SECONDS]: this._state.userSessionForegroundInactivityTimeoutSeconds }; } endUserSession() { if (!this._state) { this._diag.debug("Trying to end user session, but there is no active session. This is a no-op."); return; } const now = this._perf.getNowMillis(); const lastEndRaw = this._storage.getItem(EMBRACE_LAST_END_USER_SESSION_TS_KEY); const lastEndTs = lastEndRaw === null ? NaN : Number(lastEndRaw); if (Number.isFinite(lastEndTs) && now - lastEndTs < 5e3) { this._diag.warn("endUserSession called within cooldown period, ignoring."); return; } this._rolloverUserSession("manual"); this._storage.setItem(EMBRACE_LAST_END_USER_SESSION_TS_KEY, String(now)); } getSessionPartId() { return this._activeSessionPartId; } getSessionPartSpan() { return this._sessionPartSpan; } startSessionPartInternal({ reason, timestamp }) { if (this._sessionPartSpan) { this._diag.warn(`startSessionPartInternal called while a part is active (reason: ${reason}); ignoring`); return; } if (getVisibilityState(this._visibilityDoc) === "background" || !this._visibilityDoc.hasFocus()) { this._diag.debug(`skipping session part start (reason: ${reason}) because the tab is not engaged`); return; } const activeSessionPartId = generateUUID(); const previouslyRecordedCounts = this._nextSessionPartCounts; this._beginUserSessionForPartStart(); const attributes = { ...this.getUserSessionAttributes(), [KEY_EMB_TYPE]: "ux.session_part", [KEY_EMB_STATE]: "foreground", [KEY_EMB_SESSION_PART_ID]: activeSessionPartId, [KEY_EMB_SESSION_PART_START_REASON]: reason, [KEY_EMB_COLD_START]: this._coldStart, ...previouslyRecordedCounts }; this._activeSessionPartId = activeSessionPartId; let span; try { span = new EmbraceExtendedSpan(this._tracer.startSpan(SESSION_PART_SPAN_NAME, { attributes, startTime: timestamp })); } catch (error) { this._activeSessionPartId = null; this._diag.error("Error starting session part span", error); return; } this._activeSessionPartCounts = {}; this._nextSessionPartCounts = {}; this._sessionPartSpan = span; this._startSessionPartInactivityTimer(); this._fireListeners(this._sessionPartStartedListeners, "started", { reason }); } endSessionPartInternal({ reason, userSessionEndReason, timestamp }) { if (!this._sessionPartSpan) { this._diag.debug("trying to end a session part, but there is no session part in progress. This is a no-op."); return; } this._clearSessionPartInactivityTimer(); const isFinalSessionPart = FINAL_SESSION_PART_END_REASONS.has(reason); const now = this._perf.getNowMillis(); const partEndTs = timestamp ?? now; const span = this._sessionPartSpan; try { const endAttrs = { [KEY_EMB_SESSION_PART_END_REASON]: reason, ...this._activeSessionPartCounts, ...this._limitManager.getDiagnosticCounts(), [KEY_EMB_SDK_STARTUP_DURATION]: this._sdkStartupDuration }; if (this._coldStart) endAttrs[KEY_EMB_PAGE_LOAD] = this._visibilityDoc.readyState === "complete"; if (isFinalSessionPart) { endAttrs[KEY_EMB_IS_FINAL_SESSION_PART] = 1; if (userSessionEndReason) endAttrs[KEY_EMB_USER_SESSION_TERMINATION_REASON] = userSessionEndReason; } const propertyAttrs = {}; for (const [bareKey, value] of Object.entries(this.getSessionPartProperties())) propertyAttrs[KEY_PREFIX_EMB_PROPERTIES + bareKey] = value; try { span.setAttributes({ ...propertyAttrs, ...endAttrs }); } catch (error) { this._diag.warn("Error setting end attributes on session part span; ending span without them", error); } } finally { this._fireListeners(this._sessionPartEndedListeners, "ended", void 0); try { span.end(partEndTs); } catch (error) { this._diag.warn("Error ending session part span", error); } this._sessionPartSpan = null; this._coldStart = false; this._activeSessionPartId = null; this._activeSessionPartCounts = null; this._currentSessionPartNumber = null; this._limitManager.reset(); } if (isFinalSessionPart) { this._previousUserSessionId = this._state?.userSessionId ?? null; this._state = null; this._storage.removeItem(EMBRACE_USER_SESSION_STATE_KEY); this._clearMaxDurationTimer(); } else this._continueUserSessionAfterPartEnd(now); } addBreadcrumb(name) { if (!this._sessionPartSpan) { this._diag.debug("trying to add breadcrumb, but there is no session part in progress. This is a no-op."); return; } const limitedBreadcrumb = this._limitManager.limitBreadcrumb(name); if (limitedBreadcrumb === "dropped") return; this._sessionPartSpan.addEvent("emb-breadcrumb", { message: limitedBreadcrumb.name }, this._perf.getNowMillis()); } addProperty(propertyKey, value, options) { const limitedSessionProperty = this._limitManager.limitUserSessionProperty(propertyKey, value); if (limitedSessionProperty === "dropped") return; const bareKey = limitedSessionProperty.key; const bareValue = limitedSessionProperty.value; if (options?.lifespan === "permanent") { const candidate = { ...this._permanentProperties, [bareKey]: bareValue }; if (!storePermanentProperties(this._storage, candidate)) { this._diag.warn("Storage unavailable; rejecting permanent property write."); return; } this._permanentProperties = candidate; this._removeFromUserSessionProperties(bareKey); return; } const { state } = this._loadOrCreateUserSessionState(this._perf.getNowMillis()); this._state = { ...state, userSessionProperties: { ...state.userSessionProperties, [bareKey]: bareValue } }; if (!this._storeState()) { this._state = state; this._diag.warn("Storage unavailable; rejecting user-session property write."); } } removeProperty(propertyKey) { const bareKey = this._limitManager.truncateString("session_property_key", propertyKey); this._removeFromPermanentProperties(bareKey); this._removeFromUserSessionProperties(bareKey); } /** * Bare-keyed view of properties for the active part. User-session-scoped * entries shadow permanent ones, matching the cross-scope flip rule * enforced in `addProperty`. Callers apply the wire-format prefix at * stamp time. */ getSessionPartProperties() { return { ...this._permanentProperties, ...this._state?.userSessionProperties ?? {} }; } incrSessionPartCountForKey(key) { if (!this._sessionPartSpan || !this._activeSessionPartCounts) { this._diag.debug("trying to increment a count for the active session part, but there is no session part in progress. This is a no-op."); return; } this._activeSessionPartCounts[key] = (this._activeSessionPartCounts[key] || 0) + 1; } incrNextSessionPartCountForKey(key) { this._nextSessionPartCounts[key] = (this._nextSessionPartCounts[key] || 0) + 1; } addSessionPartStartedListener(listener) { return this._addListener(this._sessionPartStartedListeners, listener); } addSessionPartEndedListener(listener) { return this._addListener(this._sessionPartEndedListeners, listener); } getSessionId() { return this.getUserSessionId(); } getPreviousSessionId() { return null; } getSessionStartTime() { return this.getUserSessionStartTime(); } endSessionSpan() { this.endUserSession(); } startSessionSpan() {} getSessionSpan() { return this.getSessionPartSpan(); } currentSessionAsReadableSpan() { return null; } addSessionStartedListener(_listener) { return () => {}; } addSessionEndedListener(_listener) { return () => {}; } /** * Returns the persisted user-session state, rotating to a fresh one if * the persisted row is missing or expired. Arms the max-duration timer * when needed. The caller is responsible for assigning the result to * `_state` and for persisting (the helper does not write to storage). */ _loadOrCreateUserSessionState(now) { let state = readUserSessionState(this._storage, this._diag); if (!state && !this._hasStoredState && this._state) state = this._state; let created = false; if (!state || isUserSessionExpired(state, now)) { if (state) this._previousUserSessionId = state.userSessionId; const userSessionNumber = getIncrementedCount(this._storage, EMBRACE_USER_SESSION_NUMBER_KEY, this._diag); if (!this._coldStart) this._dynamicConfigManager.refreshRemoteConfig(); const { userSessionMaxDurationSeconds, userSessionInactivityTimeoutSeconds, userSessionForegroundInactivityTimeoutSeconds } = this._resolveUserSessionDurations(); state = createUserSessionState({ now, previousUserSessionId: this._previousUserSessionId, userSessionMaxDurationSeconds, userSessionInactivityTimeoutSeconds, userSessionForegroundInactivityTimeoutSeconds, userSessionNumber }); created = true; } if (created || this._maxDurationTimeout === null) this._setupMaxDurationTimer(state, now); return { state, created }; } /** * Bumps the part counter and arms the max-duration timer. Relies on * `_loadOrCreateUserSessionState` to provide a non-null, non-expired state, * so `getUserSessionAttributes()` is guaranteed non-null after this. */ _beginUserSessionForPartStart() { const now = this._perf.getNowMillis(); const { state } = this._loadOrCreateUserSessionState(now); this._currentSessionPartNumber = getIncrementedCount(this._storage, EMBRACE_SESSION_PART_NUMBER_KEY, this._diag); this._state = { ...state, userSessionPartIndex: state.userSessionPartIndex + 1, inactivityDeadlineTs: null }; this._storeState(); } /** * Records the inactivity deadline on the user-session row so the next * part start can detect lazy expiry. The max-duration timer stays valid * because userSessionMaxEndTs doesn't change between parts. */ _continueUserSessionAfterPartEnd(now) { if (!this._state) { this._clearMaxDurationTimer(); return; } if (this._hasStoredState && this._storage.getItem("embrace_user_session_state") === null) { this._previousUserSessionId = this._state.userSessionId; this._state = null; this._clearMaxDurationTimer(); return; } this._state = { ...this._state, inactivityDeadlineTs: now + this._state.userSessionInactivityTimeoutSeconds * 1e3 }; this._storeState(); } /** * Ends the active part as final and starts a fresh part for the next * user session. State clearing for the engaged path is handled by * endSessionPartInternal's isFinal branch. When no part is active * (tab disengaged), the same state-clearing is performed inline so * the next engagement creates a fresh user session rather than * resuming the just-ended one. */ _rolloverUserSession(userSessionEndReason) { if (this._sessionPartSpan) { const boundaryTimestamp = this._perf.getNowMillis(); this.endSessionPartInternal({ reason: "user_session_ended", userSessionEndReason, timestamp: boundaryTimestamp }); this.startSessionPartInternal({ reason: "user_session_rollover", timestamp: boundaryTimestamp }); return; } this._previousUserSessionId = this._state?.userSessionId ?? null; this._state = null; this._storage.removeItem(EMBRACE_USER_SESSION_STATE_KEY); this._clearMaxDurationTimer(); } /** * Ends the active part and starts a new one within the same user session. * The user session is preserved while the part counters advance, so the * caller's `endReason`/`startReason` must both be non-final. Both spans share * one boundary timestamp so they tile without a gap; the caller may supply it * (for example the timestamp of the triggering event), defaulting to now. A * no-op when no part is active: there is nothing to roll over and the next * engagement starts one. */ rolloverSessionPartInternal({ endReason, startReason, timestamp }) { if (!this._sessionPartSpan) return; const boundaryTimestamp = timestamp ?? this._perf.getNowMillis(); this.endSessionPartInternal({ reason: endReason, timestamp: boundaryTimestamp }); this.startSessionPartInternal({ reason: startReason, timestamp: boundaryTimestamp }); } _setupMaxDurationTimer(state, now) { this._clearMaxDurationTimer(); const remaining = state.userSessionMaxEndTs - now; if (remaining <= 0) return; this._maxDurationTimeout = setTimeout(() => { this._maxDurationTimeout = null; this._rolloverUserSession("max_duration_reached"); }, remaining); } /** * @internal Would be private but exposed for tests to simulate a page reload */ _clearMaxDurationTimer() { if (this._maxDurationTimeout !== null) { clearTimeout(this._maxDurationTimeout); this._maxDurationTimeout = null; } } /** * Detaches browser-activity listeners and clears the part-inactivity * timer. Intended for SDK teardown and tests; in production the manager * lives for the page's lifetime. */ _shutdown() { removeActivityListeners({ target: this._target, visibilityDoc: this._visibilityDoc, activityEvents: this._activityEvents, onActivity: this._onActivityThrottled, onVisibilityChange: this._onVisibilityChange, onFocus: this._onFocus, onBlur: this._onBlur }); this._clearSessionPartInactivityTimer(); this._clearMaxDurationTimer(); } _onActivity = () => { try { if (!isTabEngaged(this._visibilityDoc)) return; if (this._activeSessionPartId === null) { this.startSessionPartInternal({ reason: "web_activity" }); return; } this._startSessionPartInactivityTimer(); } catch (e) { this._diag.warn("Error handling activity event", e); } }; _onVisibilityChange = () => { this._handleEngagementTransition("visibilitychange"); }; _onFocus = () => { this._handleEngagementTransition("focus"); }; _onBlur = () => { this._handleEngagementTransition("blur"); }; _handleEngagementTransition(source) { try { const engaged = isTabEngaged(this._visibilityDoc); const active = this._activeSessionPartId !== null; if (!engaged && active) { this._diag.debug(`tab disengaged via ${source}; ending current part`); this.endSessionPartInternal({ reason: "background" }); return; } if (engaged && !active) { this._diag.debug(`tab engaged via ${source}; starting new part`); this.startSessionPartInternal({ reason: "foreground" }); return; } if (engaged && active) this._startSessionPartInactivityTimer(); } catch (e) { this._diag.warn("Error handling engagement change", e); } } _onSessionPartInactivity = () => { this._sessionPartInactivityTimer = null; try { if (this._activeSessionPartId === null) return; this._diag.debug("inactivity timer fired; ending current part and user session"); this.endSessionPartInternal({ reason: "web_foreground_inactivity", userSessionEndReason: "inactivity" }); } catch (e) { this._diag.warn("Error handling inactivity timer", e); } }; _startSessionPartInactivityTimer() { this._clearSessionPartInactivityTimer(); if (!this._state) return; this._sessionPartInactivityTimer = setTimeout(this._onSessionPartInactivity, this._state.userSessionForegroundInactivityTimeoutSeconds * 1e3); } _clearSessionPartInactivityTimer() { if (this._sessionPartInactivityTimer !== null) { clearTimeout(this._sessionPartInactivityTimer); this._sessionPartInactivityTimer = null; } } _addListener(list, listener) { list.push(listener); return () => { const i = list.indexOf(listener); if (i !== -1) list.splice(i, 1); }; } _fireListeners(list, kind, arg) { for (const listener of list) try { listener(arg); } catch (error) { this._diag.warn(`Error while executing session part ${kind} listener`, error); } } _storeState() { if (!this._state) return false; const stored = this._storage.setItem(EMBRACE_USER_SESSION_STATE_KEY, JSON.stringify(this._state)); if (stored) this._hasStoredState = true; return stored; } _removeFromPermanentProperties(bareKey) { if (!(bareKey in this._permanentProperties)) return; const pruned = { ...this._permanentProperties }; delete pruned[bareKey]; this._permanentProperties = pruned; storePermanentProperties(this._storage, pruned); } _removeFromUserSessionProperties(bareKey) { if (!this._state || !(bareKey in this._state.userSessionProperties)) return; const pruned = { ...this._state.userSessionProperties }; delete pruned[bareKey]; this._state = { ...this._state, userSessionProperties: pruned }; this._storeState(); } }; //#endregion export { EmbraceUserSessionManager }; //# sourceMappingURL=EmbraceUserSessionManager.js.map