@xh/hoist 72.3.0 → 72.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +2 -2
- package/admin/tabs/cluster/instances/BaseInstanceModel.ts +1 -29
- package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -1
- package/admin/tabs/cluster/objects/DetailModel.ts +4 -40
- package/admin/tabs/cluster/objects/DetailPanel.ts +2 -1
- package/appcontainer/AppContainerModel.ts +2 -0
- package/appcontainer/AppStateModel.ts +40 -8
- package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +1 -3
- package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +1 -3
- package/build/types/appcontainer/AppStateModel.d.ts +2 -0
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +7 -0
- package/build/types/core/XH.d.ts +11 -1
- package/build/types/format/FormatDate.d.ts +22 -1
- package/build/types/format/FormatMisc.d.ts +3 -2
- package/build/types/security/Types.d.ts +3 -3
- package/build/types/security/msal/MsalClient.d.ts +2 -0
- package/build/types/svc/ClientHealthService.d.ts +58 -0
- package/build/types/svc/TrackService.d.ts +0 -12
- package/build/types/svc/index.d.ts +1 -0
- package/build/types/utils/js/index.d.ts +0 -1
- package/cmp/viewmanager/ViewManagerModel.ts +10 -1
- package/core/XH.ts +26 -1
- package/desktop/cmp/viewmanager/ViewMenu.ts +11 -9
- package/format/FormatDate.ts +45 -3
- package/format/FormatMisc.ts +6 -4
- package/package.json +1 -1
- package/security/Types.ts +3 -3
- package/security/msal/MsalClient.ts +13 -13
- package/svc/ClientHealthService.ts +165 -0
- package/svc/TrackService.ts +3 -67
- package/svc/index.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/js/index.ts +0 -1
- package/build/types/utils/js/BrowserUtils.d.ts +0 -41
- package/utils/js/BrowserUtils.ts +0 -103
package/core/XH.ts
CHANGED
|
@@ -28,7 +28,8 @@ import {
|
|
|
28
28
|
PrefService,
|
|
29
29
|
SessionStorageService,
|
|
30
30
|
TrackService,
|
|
31
|
-
WebSocketService
|
|
31
|
+
WebSocketService,
|
|
32
|
+
ClientHealthService
|
|
32
33
|
} from '@xh/hoist/svc';
|
|
33
34
|
import {camelCase, flatten, isString, uniqueId} from 'lodash';
|
|
34
35
|
import {Router, State} from 'router5';
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
import {installServicesAsync} from './impl/InstallServices';
|
|
65
66
|
import {instanceManager} from './impl/InstanceManager';
|
|
66
67
|
import {HoistModel, ModelSelector, RefreshContextModel} from './model';
|
|
68
|
+
import ShortUniqueId from 'short-unique-id';
|
|
67
69
|
|
|
68
70
|
export const MIN_HOIST_CORE_VERSION = '28.0';
|
|
69
71
|
|
|
@@ -84,6 +86,15 @@ declare const xhIsDevelopmentMode: boolean;
|
|
|
84
86
|
* Available via import as `XH` - also installed as `window.XH` for troubleshooting purposes.
|
|
85
87
|
*/
|
|
86
88
|
export class XHApi {
|
|
89
|
+
/** Unique id for this loaded instance of the app. Unique for every refresh of document. */
|
|
90
|
+
clientId: string = this.genClientId();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Unique id for this browser tab/window on this domain.
|
|
94
|
+
* Corresponds to the scope of the built-in sessionStorage object.
|
|
95
|
+
*/
|
|
96
|
+
sessionId: string = this.genSessionId();
|
|
97
|
+
|
|
87
98
|
//--------------------------
|
|
88
99
|
// Implementation Delegates
|
|
89
100
|
//--------------------------
|
|
@@ -131,6 +142,7 @@ export class XHApi {
|
|
|
131
142
|
alertBannerService: AlertBannerService;
|
|
132
143
|
autoRefreshService: AutoRefreshService;
|
|
133
144
|
changelogService: ChangelogService;
|
|
145
|
+
clientHealthService: ClientHealthService;
|
|
134
146
|
configService: ConfigService;
|
|
135
147
|
environmentService: EnvironmentService;
|
|
136
148
|
fetchService: FetchService;
|
|
@@ -794,6 +806,19 @@ export class XHApi {
|
|
|
794
806
|
private get acm(): AppContainerModel {
|
|
795
807
|
return this.appContainerModel;
|
|
796
808
|
}
|
|
809
|
+
|
|
810
|
+
private genClientId(): string {
|
|
811
|
+
return new ShortUniqueId({length: 8}).rnd();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private genSessionId(): string {
|
|
815
|
+
let ret = window.sessionStorage?.getItem('xhSessionId');
|
|
816
|
+
if (!ret) {
|
|
817
|
+
ret = new ShortUniqueId({length: 8}).rnd();
|
|
818
|
+
window.sessionStorage?.setItem('xhSessionId', ret);
|
|
819
|
+
}
|
|
820
|
+
return ret;
|
|
821
|
+
}
|
|
797
822
|
}
|
|
798
823
|
|
|
799
824
|
/** The app-wide singleton instance. */
|
|
@@ -12,7 +12,7 @@ import {Icon} from '@xh/hoist/icon';
|
|
|
12
12
|
import {menu, menuDivider, menuItem} from '@xh/hoist/kit/blueprint';
|
|
13
13
|
import {pluralize} from '@xh/hoist/utils/js';
|
|
14
14
|
import {Dictionary} from 'express-serve-static-core';
|
|
15
|
-
import {each, filter, groupBy, isEmpty, orderBy, some, startCase} from 'lodash';
|
|
15
|
+
import {each, filter, groupBy, isEmpty, isFunction, orderBy, some, startCase} from 'lodash';
|
|
16
16
|
import {ReactNode} from 'react';
|
|
17
17
|
import {ViewManagerLocalModel} from './ViewManagerLocalModel';
|
|
18
18
|
|
|
@@ -162,12 +162,14 @@ function viewMenuItem(view: ViewInfo, model: ViewManagerModel): ReactNode {
|
|
|
162
162
|
if (!view.isOwned && view.owner) title.push(view.owner);
|
|
163
163
|
if (view.description) title.push(view.description);
|
|
164
164
|
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
165
|
+
return isFunction(model.viewMenuItemFn)
|
|
166
|
+
? model.viewMenuItemFn(view, model)
|
|
167
|
+
: menuItem({
|
|
168
|
+
className: 'xh-view-manager__menu-item',
|
|
169
|
+
key: view.token,
|
|
170
|
+
text: view.name,
|
|
171
|
+
title: title.join(' | '),
|
|
172
|
+
icon,
|
|
173
|
+
onClick: () => model.selectViewAsync(view).catchDefault()
|
|
174
|
+
});
|
|
173
175
|
}
|
package/format/FormatDate.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {isLocalDate, LocalDate} from '@xh/hoist/utils/datetime';
|
|
8
|
-
import {defaults, isString} from 'lodash';
|
|
7
|
+
import {DAYS, isLocalDate, LocalDate} from '@xh/hoist/utils/datetime';
|
|
8
|
+
import {defaults, isFinite, isString} from 'lodash';
|
|
9
9
|
import moment from 'moment';
|
|
10
10
|
import {ReactNode} from 'react';
|
|
11
|
-
import {DateLike} from '../core/types/Types';
|
|
11
|
+
import {DateLike, PlainObject} from '../core/types/Types';
|
|
12
12
|
import {fmtSpan, FormatOptions} from './FormatMisc';
|
|
13
13
|
import {createRenderer} from './FormatUtils';
|
|
14
14
|
import {saveOriginal} from './impl/Utils';
|
|
@@ -148,6 +148,48 @@ export function fmtCompactDate(v: DateLike, opts?: CompactDateFormatOptions) {
|
|
|
148
148
|
return fmtDate(v, dateOpts);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
export interface TimestampReplacerConfig {
|
|
152
|
+
/**
|
|
153
|
+
* Suffixes used to identify keys that may hold timestamps.
|
|
154
|
+
* Defaults to ['time', 'date', 'timestamp']
|
|
155
|
+
*/
|
|
156
|
+
suffixes?: string[];
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Format for replaced timestamp.
|
|
160
|
+
* Defaults to 'MMM DD HH:mm:ss.SSS'
|
|
161
|
+
*/
|
|
162
|
+
format?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Replace timestamps in an Object with formatted strings.
|
|
167
|
+
*/
|
|
168
|
+
export function withFormattedTimestamps(
|
|
169
|
+
obj: PlainObject,
|
|
170
|
+
config: TimestampReplacerConfig = {}
|
|
171
|
+
): PlainObject {
|
|
172
|
+
return JSON.parse(JSON.stringify(obj, timestampReplacer(config)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create a replacer, suitable for JSON.stringify, that will replace timestamps with
|
|
177
|
+
* formatted strings.
|
|
178
|
+
*/
|
|
179
|
+
export function timestampReplacer(
|
|
180
|
+
config: TimestampReplacerConfig = {}
|
|
181
|
+
): (k: string, v: any) => any {
|
|
182
|
+
const suffixes = config.suffixes ?? ['time', 'date', 'timestamp'],
|
|
183
|
+
fmt = 'MMM DD HH:mm:ss.SSS';
|
|
184
|
+
return (k: string, v: any) => {
|
|
185
|
+
return suffixes.some(s => k.toLowerCase().endsWith(s.toLowerCase())) &&
|
|
186
|
+
isFinite(v) &&
|
|
187
|
+
v > Date.now() - 25 * 365 * DAYS // heuristic to avoid catching smaller ms ranges
|
|
188
|
+
? fmtDateTime(v, {fmt})
|
|
189
|
+
: v;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
151
193
|
export const dateRenderer = createRenderer(fmtDate),
|
|
152
194
|
dateTimeRenderer = createRenderer(fmtDateTime),
|
|
153
195
|
dateTimeSecRenderer = createRenderer(fmtDateTimeSec),
|
package/format/FormatMisc.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {span} from '@xh/hoist/cmp/layout';
|
|
8
|
-
import {capitalize, isNil, kebabCase, map} from 'lodash';
|
|
8
|
+
import {capitalize, isNil, isString, kebabCase, map} from 'lodash';
|
|
9
9
|
import {CSSProperties, ReactNode} from 'react';
|
|
10
|
+
import {PlainObject} from '@xh/hoist/core';
|
|
10
11
|
|
|
11
12
|
export interface FormatOptions<T = any> {
|
|
12
13
|
/** Display value for null values. */
|
|
@@ -63,11 +64,12 @@ export interface JSONFormatOptions {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
|
-
* Pretty-print a JSON string, adding line breaks and indentation.
|
|
67
|
+
* Pretty-print a JSON string or (JSON Object), adding line breaks and indentation.
|
|
67
68
|
*/
|
|
68
|
-
export function fmtJson(
|
|
69
|
+
export function fmtJson(v: string | PlainObject, opts?: JSONFormatOptions): string {
|
|
69
70
|
const {replacer = undefined, space = 2} = opts ?? {};
|
|
70
|
-
|
|
71
|
+
if (isString(v)) v = JSON.parse(v);
|
|
72
|
+
return isNil(v) ? '' : JSON.stringify(v, replacer, space);
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/**
|
package/package.json
CHANGED
package/security/Types.ts
CHANGED
|
@@ -31,8 +31,8 @@ export interface TelemetryResults {
|
|
|
31
31
|
|
|
32
32
|
/** Aggregated telemetry results for a single type of event. */
|
|
33
33
|
export interface TelemetryEventResults {
|
|
34
|
-
firstTime:
|
|
35
|
-
lastTime:
|
|
34
|
+
firstTime: number;
|
|
35
|
+
lastTime: number;
|
|
36
36
|
successCount: number;
|
|
37
37
|
failureCount: number;
|
|
38
38
|
/** Timing info (in ms) for event instances reported with duration. */
|
|
@@ -43,7 +43,7 @@ export interface TelemetryEventResults {
|
|
|
43
43
|
worst: number;
|
|
44
44
|
};
|
|
45
45
|
lastFailure?: {
|
|
46
|
-
time:
|
|
46
|
+
time: number;
|
|
47
47
|
duration: number;
|
|
48
48
|
code: string;
|
|
49
49
|
name: string;
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
PopupRequest,
|
|
15
15
|
SilentRequest
|
|
16
16
|
} from '@azure/msal-browser';
|
|
17
|
-
import {XH} from '@xh/hoist/core';
|
|
17
|
+
import {AppState, PlainObject, XH} from '@xh/hoist/core';
|
|
18
18
|
import {Token} from '@xh/hoist/security/Token';
|
|
19
19
|
import {logDebug, logError, logInfo, logWarn, mergeDeep, throwIf} from '@xh/hoist/utils/js';
|
|
20
|
+
import {withFormattedTimestamps} from '@xh/hoist/format';
|
|
20
21
|
import {flatMap, union, uniq} from 'lodash';
|
|
21
22
|
import {BaseOAuthClient, BaseOAuthClientConfig} from '../BaseOAuthClient';
|
|
22
23
|
import {AccessTokenSpec, TelemetryResults, TokenMap} from '../Types';
|
|
@@ -266,9 +267,13 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
266
267
|
//------------------------
|
|
267
268
|
// Telemetry
|
|
268
269
|
//------------------------
|
|
270
|
+
getFormattedTelemetry(): PlainObject {
|
|
271
|
+
return withFormattedTimestamps(this.telemetryResults);
|
|
272
|
+
}
|
|
273
|
+
|
|
269
274
|
enableTelemetry(): void {
|
|
270
275
|
if (this._telemetryCbHandle) {
|
|
271
|
-
this.logInfo('Telemetry already enabled', this.
|
|
276
|
+
this.logInfo('Telemetry already enabled', this.getFormattedTelemetry());
|
|
272
277
|
return;
|
|
273
278
|
}
|
|
274
279
|
|
|
@@ -279,7 +284,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
279
284
|
try {
|
|
280
285
|
const {events} = this.telemetryResults,
|
|
281
286
|
{name, startTimeMs, durationMs, success, errorName, errorCode} = e,
|
|
282
|
-
eTime = startTimeMs
|
|
287
|
+
eTime = startTimeMs ?? Date.now();
|
|
283
288
|
|
|
284
289
|
const eResult = (events[name] ??= {
|
|
285
290
|
firstTime: eTime,
|
|
@@ -316,15 +321,10 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
316
321
|
});
|
|
317
322
|
});
|
|
318
323
|
|
|
319
|
-
//
|
|
320
|
-
// Handle TrackService not yet initialized (common, this client likely initialized before.)
|
|
324
|
+
// Wait for clientHealthService (this client likely initialized during earlier AUTHENTICATING.)
|
|
321
325
|
this.addReaction({
|
|
322
|
-
when: () => XH.
|
|
323
|
-
run: () =>
|
|
324
|
-
XH.trackService.addClientHealthReportSource(
|
|
325
|
-
'msalClient',
|
|
326
|
-
() => this.telemetryResults
|
|
327
|
-
)
|
|
326
|
+
when: () => XH.appState === AppState.INITIALIZING_APP,
|
|
327
|
+
run: () => XH.clientHealthService.addSource('msalClient', () => this.telemetryResults)
|
|
328
328
|
});
|
|
329
329
|
|
|
330
330
|
this.logInfo('Telemetry enabled');
|
|
@@ -339,8 +339,8 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
339
339
|
this.client.removePerformanceCallback(this._telemetryCbHandle);
|
|
340
340
|
this._telemetryCbHandle = null;
|
|
341
341
|
|
|
342
|
-
XH.
|
|
343
|
-
this.logInfo('Telemetry disabled', this.
|
|
342
|
+
XH.clientHealthService.removeSource('msalClient');
|
|
343
|
+
this.logInfo('Telemetry disabled', this.getFormattedTelemetry());
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
//------------------------
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {HoistService, PageState, PlainObject, XH} from '@xh/hoist/core';
|
|
8
|
+
import {Timer} from '@xh/hoist/utils/async';
|
|
9
|
+
import {MINUTES} from '@xh/hoist/utils/datetime';
|
|
10
|
+
import {withFormattedTimestamps} from '@xh/hoist/format';
|
|
11
|
+
import {pick, round} from 'lodash';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Service for gathering data about client health.
|
|
15
|
+
*
|
|
16
|
+
* Hoist sends this data once on application load, and can be configured to send
|
|
17
|
+
* it at regularly scheduled intervals. Configure via soft-config property
|
|
18
|
+
* 'xhActivityTracking.clientHealthReport'.
|
|
19
|
+
*/
|
|
20
|
+
export class ClientHealthService extends HoistService {
|
|
21
|
+
static instance: ClientHealthService;
|
|
22
|
+
|
|
23
|
+
private sources: Map<string, () => any> = new Map();
|
|
24
|
+
|
|
25
|
+
override async initAsync() {
|
|
26
|
+
const {clientHealthReport} = XH.trackService.conf;
|
|
27
|
+
Timer.create({
|
|
28
|
+
runFn: () => this.sendReport(),
|
|
29
|
+
interval: clientHealthReport.intervalMins * MINUTES,
|
|
30
|
+
delay: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Main entry point. Return a default report of client health.
|
|
36
|
+
*/
|
|
37
|
+
getReport(): ClientHealthReport {
|
|
38
|
+
return {
|
|
39
|
+
general: this.getGeneral(),
|
|
40
|
+
memory: this.getMemory(),
|
|
41
|
+
connection: this.getConnection(),
|
|
42
|
+
...this.getCustom()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Get report, suitable for viewing in console. **/
|
|
47
|
+
getFormattedReport(): PlainObject {
|
|
48
|
+
return withFormattedTimestamps(this.getReport());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register a new source for client health report data. No-op if background health report is
|
|
53
|
+
* not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
|
|
54
|
+
*
|
|
55
|
+
* @param key - key under which to report the data - can be used to remove this source later.
|
|
56
|
+
* @param callback - function returning serializable to include with each report.
|
|
57
|
+
*/
|
|
58
|
+
addSource(key: string, callback: () => any) {
|
|
59
|
+
this.sources.set(key, callback);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Unregister a previously-enabled source for client health report data. */
|
|
63
|
+
removeSource(key: string) {
|
|
64
|
+
this.sources.delete(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// -----------------------------------
|
|
68
|
+
// Generate individual report sections
|
|
69
|
+
//------------------------------------
|
|
70
|
+
getGeneral(): GeneralData {
|
|
71
|
+
const startTime = XH.appContainerModel.appStateModel.loadStarted,
|
|
72
|
+
elapsedMins = (ts: number) => round((Date.now() - ts) / 60_000, 1);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
startTime,
|
|
76
|
+
durationMins: elapsedMins(startTime),
|
|
77
|
+
idleMins: elapsedMins(XH.lastActivityMs),
|
|
78
|
+
pageState: XH.pageState,
|
|
79
|
+
webSocket: XH.webSocketService.channelKey
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getConnection(): ConnectionData {
|
|
84
|
+
const nav = window.navigator as any;
|
|
85
|
+
if (!nav.connection) return null;
|
|
86
|
+
return pick(nav.connection, ['downlink', 'effectiveType', 'rtt']);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getMemory(): MemoryData {
|
|
90
|
+
const perf = window.performance as any;
|
|
91
|
+
if (!perf?.memory) return null;
|
|
92
|
+
|
|
93
|
+
const ret: MemoryData = {modelCount: XH.getModels().length};
|
|
94
|
+
['jsHeapSizeLimit', 'totalJSHeapSize', 'usedJSHeapSize'].forEach(key => {
|
|
95
|
+
const raw = perf.memory[key];
|
|
96
|
+
if (raw) ret[key] = round(raw / 1024 / 1024); // convert to MB
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const {jsHeapSizeLimit: limit, usedJSHeapSize: used} = ret;
|
|
100
|
+
if (limit && used) {
|
|
101
|
+
ret.usedPctLimit = round((used / limit) * 100);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ret;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getCustom(): PlainObject {
|
|
108
|
+
const ret = {};
|
|
109
|
+
this.sources.forEach((cb, k) => {
|
|
110
|
+
try {
|
|
111
|
+
ret[k] = cb();
|
|
112
|
+
} catch (e) {
|
|
113
|
+
ret[k] = `Error: ${e.message}`;
|
|
114
|
+
this.logWarn(`Error running client health report callback for [${k}]`, e);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return ret;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//------------------
|
|
121
|
+
// Implementation
|
|
122
|
+
//------------------
|
|
123
|
+
private sendReport() {
|
|
124
|
+
const {intervalMins, ...rest} = XH.trackService.conf.clientHealthReport ?? {};
|
|
125
|
+
|
|
126
|
+
XH.track({
|
|
127
|
+
category: 'App',
|
|
128
|
+
message: 'Submitted health report',
|
|
129
|
+
...rest,
|
|
130
|
+
data: {
|
|
131
|
+
clientId: XH.clientId,
|
|
132
|
+
sessionId: XH.sessionId,
|
|
133
|
+
...this.getReport()
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface GeneralData {
|
|
140
|
+
startTime: number;
|
|
141
|
+
durationMins: number;
|
|
142
|
+
idleMins: number;
|
|
143
|
+
pageState: PageState;
|
|
144
|
+
webSocket: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ConnectionData {
|
|
148
|
+
downlink: number;
|
|
149
|
+
effectiveType: string;
|
|
150
|
+
rtt: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface MemoryData {
|
|
154
|
+
modelCount: number;
|
|
155
|
+
usedPctLimit?: number;
|
|
156
|
+
jsHeapSizeLimit?: number;
|
|
157
|
+
totalJSHeapSize?: number;
|
|
158
|
+
usedJSHeapSize?: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ClientHealthReport {
|
|
162
|
+
general: GeneralData;
|
|
163
|
+
connection: ConnectionData;
|
|
164
|
+
memory: MemoryData;
|
|
165
|
+
}
|
package/svc/TrackService.ts
CHANGED
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {HoistService, PlainObject, TrackOptions, XH} from '@xh/hoist/core';
|
|
8
|
-
import {
|
|
9
|
-
import {MINUTES, SECONDS} from '@xh/hoist/utils/datetime';
|
|
8
|
+
import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
10
9
|
import {isOmitted} from '@xh/hoist/utils/impl';
|
|
11
|
-
import {debounced,
|
|
12
|
-
import {isEmpty, isNil, isString
|
|
10
|
+
import {debounced, stripTags, withDefault} from '@xh/hoist/utils/js';
|
|
11
|
+
import {isEmpty, isNil, isString} from 'lodash';
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Primary service for tracking any activity that an application's admins want to track.
|
|
@@ -19,21 +18,10 @@ import {isEmpty, isNil, isString, round} from 'lodash';
|
|
|
19
18
|
export class TrackService extends HoistService {
|
|
20
19
|
static instance: TrackService;
|
|
21
20
|
|
|
22
|
-
private clientHealthReportSources: Map<string, () => any> = new Map();
|
|
23
21
|
private oncePerSessionSent = new Map();
|
|
24
22
|
private pending: PlainObject[] = [];
|
|
25
23
|
|
|
26
24
|
override async initAsync() {
|
|
27
|
-
const {clientHealthReport} = this.conf;
|
|
28
|
-
if (clientHealthReport?.intervalMins > 0) {
|
|
29
|
-
Timer.create({
|
|
30
|
-
runFn: () => this.sendClientHealthReport(),
|
|
31
|
-
interval: clientHealthReport.intervalMins,
|
|
32
|
-
intervalUnits: MINUTES,
|
|
33
|
-
delay: true
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
25
|
window.addEventListener('beforeunload', () => this.pushPendingAsync());
|
|
38
26
|
}
|
|
39
27
|
|
|
@@ -102,22 +90,6 @@ export class TrackService extends HoistService {
|
|
|
102
90
|
this.pushPendingBuffered();
|
|
103
91
|
}
|
|
104
92
|
|
|
105
|
-
/**
|
|
106
|
-
* Register a new source for client health report data. No-op if background health report is
|
|
107
|
-
* not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
|
|
108
|
-
*
|
|
109
|
-
* @param key - key under which to report the data - can be used to remove this source later.
|
|
110
|
-
* @param callback - function returning serializable to include with each report.
|
|
111
|
-
*/
|
|
112
|
-
addClientHealthReportSource(key: string, callback: () => any) {
|
|
113
|
-
this.clientHealthReportSources.set(key, callback);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Unregister a previously-enabled source for client health report data. */
|
|
117
|
-
removeClientHealthReportSource(key: string) {
|
|
118
|
-
this.clientHealthReportSources.delete(key);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
93
|
//------------------
|
|
122
94
|
// Implementation
|
|
123
95
|
//------------------
|
|
@@ -176,42 +148,6 @@ export class TrackService extends HoistService {
|
|
|
176
148
|
|
|
177
149
|
this.logInfo(...consoleMsgs);
|
|
178
150
|
}
|
|
179
|
-
|
|
180
|
-
private sendClientHealthReport() {
|
|
181
|
-
const {
|
|
182
|
-
intervalMins,
|
|
183
|
-
severity: defaultSeverity,
|
|
184
|
-
...rest
|
|
185
|
-
} = this.conf.clientHealthReport ?? {},
|
|
186
|
-
{loadStarted} = XH.appContainerModel.appStateModel;
|
|
187
|
-
|
|
188
|
-
const data = {
|
|
189
|
-
session: {
|
|
190
|
-
started: loadStarted,
|
|
191
|
-
durationMins: round((Date.now() - loadStarted) / 60_000, 1)
|
|
192
|
-
},
|
|
193
|
-
...getClientDeviceInfo()
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
let severity = defaultSeverity ?? 'INFO';
|
|
197
|
-
this.clientHealthReportSources.forEach((cb, k) => {
|
|
198
|
-
try {
|
|
199
|
-
data[k] = cb();
|
|
200
|
-
if (data[k]?.severity === 'WARN') severity = 'WARN';
|
|
201
|
-
} catch (e) {
|
|
202
|
-
data[k] = `Error: ${e.message}`;
|
|
203
|
-
this.logWarn(`Error running client health report callback for [${k}]`, e);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
this.track({
|
|
208
|
-
category: 'App',
|
|
209
|
-
message: 'Submitted health report',
|
|
210
|
-
severity,
|
|
211
|
-
...rest,
|
|
212
|
-
data
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
151
|
}
|
|
216
152
|
|
|
217
153
|
interface ActivityTrackingConfig {
|
package/svc/index.ts
CHANGED
|
@@ -19,5 +19,6 @@ export * from './JsonBlobService';
|
|
|
19
19
|
export * from './PrefService';
|
|
20
20
|
export * from './TrackService';
|
|
21
21
|
export * from './WebSocketService';
|
|
22
|
+
export * from './ClientHealthService';
|
|
22
23
|
export * from './storage/LocalStorageService';
|
|
23
24
|
export * from './storage/SessionStorageService';
|