Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Timing block to app loading track message #3796

Merged
merged 4 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## 69.0.0-SNAPSHOT - unreleased


### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW )
* The `INITIALIZING` AppState has been replaced with more fine-grained states (see below). This
is not expected to effect any applications.

### 🎁 New Features

* Added new AppStates `AUTHENTICATING`, `INITIALIZING_HOIST`, and `INITIALIZING_APP` to support
more granular tracking and timing of app startup lifecycle.
* Improved the default "Loaded App" activity tracking entry with more granular data on load timing.

## 68.1.0 - 2024-09-27

### 🎁 New Features
Expand Down
8 changes: 5 additions & 3 deletions appcontainer/AppContainerModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class AppContainerModel extends HoistModel {
@managed userAgentModel = new UserAgentModel();

/**
* Message shown on spinner while the application is in the INITIALIZING state.
* Message shown on spinner while the application is in a pre-running state.
* Update within `AppModel.initAsync()` to relay app-specific initialization status.
*/
@bindable initializingLoadMaskMessage: ReactNode;
Expand All @@ -108,7 +108,7 @@ export class AppContainerModel extends HoistModel {
* Main entry point. Initialize and render application code.
*/
renderApp<T extends HoistAppModel>(appSpec: AppSpec<T>) {
// Remove the pre-load exception handler installed by preflight.js
// Remove the preload exception handler installed by preflight.js
window.onerror = null;
const spinner = document.getElementById('xh-preload-spinner');
if (spinner) spinner.style.display = 'none';
Expand Down Expand Up @@ -180,6 +180,7 @@ export class AppContainerModel extends HoistModel {
await installServicesAsync([FetchService]);

// Check auth, locking out, or showing login if possible
this.setAppState('AUTHENTICATING');
XH.authModel = new this.appSpec.authModelClass();
const isAuthenticated = await XH.authModel.completeAuthAsync();
if (!isAuthenticated) {
Expand All @@ -206,6 +207,7 @@ export class AppContainerModel extends HoistModel {
*/
@action
async completeInitAsync() {
this.setAppState('INITIALIZING_HOIST');
try {
// Install identity service and confirm access
await installServicesAsync(IdentityService);
Expand All @@ -215,7 +217,6 @@ export class AppContainerModel extends HoistModel {
}

// Complete initialization process
this.setAppState('INITIALIZING');
await installServicesAsync([ConfigService, LocalStorageService]);
await installServicesAsync(TrackService);
await installServicesAsync([EnvironmentService, PrefService, JsonBlobService]);
Expand Down Expand Up @@ -272,6 +273,7 @@ export class AppContainerModel extends HoistModel {
// Delay to workaround hot-reload styling issues in dev.
await wait(XH.isDevelopmentMode ? 300 : 1);

this.setAppState('INITIALIZING_APP');
const modelClass: any = this.appSpec.modelClass;
this.appModel = modelClass.instance = new modelClass();
await this.appModel.initAsync();
Expand Down
70 changes: 32 additions & 38 deletions appcontainer/AppStateModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* Copyright © 2024 Extremely Heavy Industries Inc.
*/
import {AppState, AppSuspendData, HoistModel, XH} from '@xh/hoist/core';
import {action, makeObservable, observable, reaction} from '@xh/hoist/mobx';
import {action, makeObservable, observable} from '@xh/hoist/mobx';
import {Timer} from '@xh/hoist/utils/async';
import {getClientDeviceInfo} from '@xh/hoist/utils/js';
import {isBoolean, isString} from 'lodash';
import {camelCase, isBoolean, isString, mapKeys} from 'lodash';

/**
* Support for Core Hoist Application state and loading.
Expand All @@ -24,6 +24,10 @@ export class AppStateModel extends HoistModel {
suspendData: AppSuspendData;
accessDeniedMessage: string = 'Access Denied';

private timings: Record<AppState, number> = {} as Record<AppState, number>;
private loadStarted: number = window['_xhLoadTimestamp']; // set in index.html
private lastStateChangeTime: number = this.loadStarted;

constructor() {
super();
makeObservable(this);
Expand All @@ -33,10 +37,14 @@ export class AppStateModel extends HoistModel {

@action
setAppState(nextState: AppState) {
if (this.state !== nextState) {
this.logDebug(`AppState change`, `${this.state} → ${nextState}`);
}
if (this.state === nextState) return;

const {state, timings, lastStateChangeTime} = this,
now = Date.now();
timings[state] = (timings[state] ?? 0) + now - lastStateChangeTime;
this.lastStateChangeTime = now;
this.state = nextState;
this.logDebug(`AppState change`, `${state} → ${nextState}`);
}

suspendApp(suspendData: AppSuspendData) {
Expand Down Expand Up @@ -70,39 +78,25 @@ export class AppStateModel extends HoistModel {
// Implementation
//------------------
private trackLoad() {
let loadStarted = window['_xhLoadTimestamp'], // set in index.html
loginStarted = null,
loginElapsed = 0;

const disposer = reaction(
() => this.state,
state => {
const now = Date.now();
switch (state) {
case 'RUNNING':
XH.track({
category: 'App',
message: `Loaded ${XH.clientAppCode}`,
elapsed: now - loadStarted - loginElapsed,
data: {
appVersion: XH.appVersion,
appBuild: XH.appBuild,
locationHref: window.location.href,
...getClientDeviceInfo()
},
logData: ['appVersion', 'appBuild'],
omit: !XH.appSpec.trackAppLoad
});
disposer();
break;
case 'LOGIN_REQUIRED':
loginStarted = now;
break;
default:
if (loginStarted) loginElapsed = now - loginStarted;
}
}
);
const {timings, loadStarted} = this;
this.addReaction({
when: () => this.state === 'RUNNING',
run: () =>
XH.track({
category: 'App',
message: `Loaded ${XH.clientAppCode}`,
elapsed: Date.now() - loadStarted - (timings.LOGIN_REQUIRED ?? 0),
data: {
appVersion: XH.appVersion,
appBuild: XH.appBuild,
locationHref: window.location.href,
timings: mapKeys(timings, (v, k) => camelCase(k)),
...getClientDeviceInfo()
},
logData: ['appVersion', 'appBuild'],
omit: !XH.appSpec.trackAppLoad
})
});
}

//---------------------
Expand Down
11 changes: 8 additions & 3 deletions core/types/AppState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@
* Enumeration of possible App States
*/
export const AppState = Object.freeze({
// Main Flow
PRE_AUTH: 'PRE_AUTH',
AUTHENTICATING: 'AUTHENTICATING',
LOGIN_REQUIRED: 'LOGIN_REQUIRED',
ACCESS_DENIED: 'ACCESS_DENIED',
INITIALIZING: 'INITIALIZING',
INITIALIZING_HOIST: 'INITIALIZING_HOIST',
INITIALIZING_APP: 'INITIALIZING_APP',
RUNNING: 'RUNNING',
SUSPENDED: 'SUSPENDED',
LOAD_FAILED: 'LOAD_FAILED'

// Terminal Error States.
LOAD_FAILED: 'LOAD_FAILED',
ACCESS_DENIED: 'ACCESS_DENIED'
});

// eslint-disable-next-line
Expand Down
4 changes: 3 additions & 1 deletion desktop/appcontainer/AppContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ export const AppContainer = hoistCmp({
function viewForState({model}) {
switch (XH.appState) {
case 'PRE_AUTH':
case 'INITIALIZING':
case 'AUTHENTICATING':
case 'INITIALIZING_HOIST':
case 'INITIALIZING_APP':
return viewport(
mask({spinner: true, isDisplayed: true, message: model.initializingLoadMaskMessage})
);
Expand Down
4 changes: 3 additions & 1 deletion mobile/appcontainer/AppContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ export const AppContainer = hoistCmp({
function viewForState({model}) {
switch (XH.appState) {
case 'PRE_AUTH':
case 'INITIALIZING':
case 'AUTHENTICATING':
case 'INITIALIZING_HOIST':
case 'INITIALIZING_APP':
return viewport(
mask({spinner: true, isDisplayed: true, message: model.initializingLoadMaskMessage})
);
Expand Down
Loading