@xh/hoist 73.0.0-SNAPSHOT.1744145524716 → 73.0.0-SNAPSHOT.1744147015222

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +3 -5
  2. package/admin/AdminUtils.ts +18 -1
  3. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +3 -3
  4. package/admin/tabs/cluster/instances/BaseInstanceModel.ts +1 -29
  5. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +3 -1
  6. package/admin/tabs/cluster/objects/DetailModel.ts +5 -40
  7. package/admin/tabs/cluster/objects/DetailPanel.ts +3 -1
  8. package/appcontainer/AppContainerModel.ts +2 -0
  9. package/appcontainer/AppStateModel.ts +1 -2
  10. package/build/types/admin/AdminUtils.d.ts +4 -0
  11. package/build/types/admin/tabs/cluster/instances/BaseInstanceModel.d.ts +1 -3
  12. package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +1 -3
  13. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +0 -7
  14. package/build/types/core/XH.d.ts +2 -1
  15. package/build/types/format/FormatMisc.d.ts +3 -2
  16. package/build/types/svc/ClientHealthService.d.ts +80 -0
  17. package/build/types/svc/TrackService.d.ts +0 -12
  18. package/build/types/svc/index.d.ts +1 -0
  19. package/build/types/utils/js/index.d.ts +0 -1
  20. package/cmp/viewmanager/ViewManagerModel.ts +1 -10
  21. package/core/XH.ts +3 -1
  22. package/desktop/cmp/viewmanager/ViewMenu.ts +8 -10
  23. package/format/FormatMisc.ts +6 -4
  24. package/package.json +1 -1
  25. package/security/msal/MsalClient.ts +2 -6
  26. package/svc/ClientHealthService.ts +219 -0
  27. package/svc/TrackService.ts +3 -67
  28. package/svc/index.ts +1 -0
  29. package/tsconfig.tsbuildinfo +1 -1
  30. package/utils/js/index.ts +0 -1
  31. package/build/types/utils/js/BrowserUtils.d.ts +0 -41
  32. package/utils/js/BrowserUtils.ts +0 -103
@@ -0,0 +1,219 @@
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, PlainObject, XH} from '@xh/hoist/core';
8
+ import {Timer} from '@xh/hoist/utils/async';
9
+ import {MINUTES} from '@xh/hoist/utils/datetime';
10
+ import {find, isPlainObject, pick, round} from 'lodash';
11
+
12
+ /**
13
+ * Service for gathering data about client health.
14
+ *
15
+ * Hoist sends this data once on application load, and can be configured to send
16
+ * it at regularly scheduled intervals. Configure via soft-config property
17
+ * 'xhActivityTracking.clientHealthReport'.
18
+ */
19
+ export class ClientHealthService extends HoistService {
20
+ static instance: ClientHealthService;
21
+
22
+ private sources: Map<string, () => any> = new Map();
23
+
24
+ override async initAsync() {
25
+ const {clientHealthReport} = XH.trackService.conf;
26
+ Timer.create({
27
+ runFn: () => this.sendReport(),
28
+ interval: clientHealthReport.intervalMins * MINUTES,
29
+ delay: true
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Main Entry report. Return a default report of client health.
35
+ */
36
+ getReport(): ClientHealthReport {
37
+ return {
38
+ session: this.getSession(),
39
+ ...this.getCustom(),
40
+ memory: this.getMemory(),
41
+ connection: this.getConnection(),
42
+ window: this.getWindow(),
43
+ screen: this.getScreen()
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Register a new source for client health report data. No-op if background health report is
49
+ * not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
50
+ *
51
+ * @param key - key under which to report the data - can be used to remove this source later.
52
+ * @param callback - function returning serializable to include with each report.
53
+ */
54
+ addSource(key: string, callback: () => any) {
55
+ this.sources.set(key, callback);
56
+ }
57
+
58
+ /** Unregister a previously-enabled source for client health report data. */
59
+ removeSource(key: string) {
60
+ this.sources.delete(key);
61
+ }
62
+
63
+ // -----------------------------------
64
+ // Generate individual report sections
65
+ //------------------------------------
66
+ getSession(): SessionData {
67
+ const {loadStarted} = XH.appContainerModel.appStateModel;
68
+ return {
69
+ startTime: loadStarted,
70
+ durationMins: round((Date.now() - loadStarted) / 60_000, 1)
71
+ };
72
+ }
73
+
74
+ getConnection(): ConnectionData {
75
+ const nav = window.navigator as any;
76
+ if (!nav.connection) return null;
77
+ return pick(nav.connection, ['downlink', 'effectiveType', 'rtt']);
78
+ }
79
+
80
+ getMemory(): MemoryData {
81
+ const perf = window.performance as any;
82
+ if (!perf?.memory) return null;
83
+
84
+ const ret: MemoryData = {modelCount: XH.getModels().length};
85
+ ['jsHeapSizeLimit', 'totalJSHeapSize', 'usedJSHeapSize'].forEach(key => {
86
+ const raw = perf.memory[key];
87
+ if (raw) ret[key] = round(raw / 1024 / 1024); // convert to MB
88
+ });
89
+
90
+ const {jsHeapSizeLimit: limit, usedJSHeapSize: used} = ret;
91
+ if (limit && used) {
92
+ ret.usedPctLimit = round((used / limit) * 100, 1);
93
+ }
94
+
95
+ return ret;
96
+ }
97
+
98
+ getScreen(): ScreenData {
99
+ const screen = window.screen as any;
100
+ if (!screen) return null;
101
+
102
+ const ret: ScreenData = pick(screen, [
103
+ 'availWidth',
104
+ 'availHeight',
105
+ 'width',
106
+ 'height',
107
+ 'colorDepth',
108
+ 'pixelDepth',
109
+ 'availLeft',
110
+ 'availTop'
111
+ ]);
112
+ if (screen.orientation) {
113
+ ret.orientation = pick(screen.orientation, ['angle', 'type']);
114
+ }
115
+ return ret;
116
+ }
117
+
118
+ getWindow(): WindowData {
119
+ return pick(window, [
120
+ 'devicePixelRatio',
121
+ 'screenX',
122
+ 'screenY',
123
+ 'innerWidth',
124
+ 'innerHeight',
125
+ 'outerWidth',
126
+ 'outerHeight'
127
+ ]);
128
+ }
129
+
130
+ getCustom(): PlainObject {
131
+ const ret = {};
132
+ this.sources.forEach((cb, k) => {
133
+ try {
134
+ ret[k] = cb();
135
+ } catch (e) {
136
+ ret[k] = `Error: ${e.message}`;
137
+ this.logWarn(`Error running client health report callback for [${k}]`, e);
138
+ }
139
+ });
140
+ return ret;
141
+ }
142
+
143
+ //------------------
144
+ // Implementation
145
+ //------------------
146
+ private sendReport() {
147
+ const {
148
+ intervalMins,
149
+ severity: defaultSeverity,
150
+ ...rest
151
+ } = XH.trackService.conf.clientHealthReport ?? {};
152
+
153
+ const rpt = this.getReport();
154
+ let severity = defaultSeverity ?? 'INFO';
155
+ if (find(rpt, (v: any) => isPlainObject(v) && v.severity === 'WARN')) {
156
+ severity = 'WARN';
157
+ }
158
+
159
+ XH.track({
160
+ category: 'App',
161
+ message: 'Submitted health report',
162
+ severity,
163
+ ...rest,
164
+ data: rpt
165
+ });
166
+ }
167
+ }
168
+
169
+ export interface SessionData {
170
+ startTime: number;
171
+ durationMins: number;
172
+ }
173
+
174
+ export interface ConnectionData {
175
+ downlink: number;
176
+ effectiveType: string;
177
+ rtt: number;
178
+ }
179
+
180
+ export interface MemoryData {
181
+ modelCount: number;
182
+ usedPctLimit?: number;
183
+ jsHeapSizeLimit?: number;
184
+ totalJSHeapSize?: number;
185
+ usedJSHeapSize?: number;
186
+ }
187
+
188
+ export interface WindowData {
189
+ devicePixelRatio: number;
190
+ screenX: number;
191
+ screenY: number;
192
+ innerWidth: number;
193
+ innerHeight: number;
194
+ outerWidth: number;
195
+ outerHeight: number;
196
+ }
197
+
198
+ export interface ScreenData {
199
+ availWidth: number;
200
+ availHeight: number;
201
+ width: number;
202
+ height: number;
203
+ colorDepth: number;
204
+ pixelDepth: number;
205
+ availLeft: number;
206
+ availTop: number;
207
+ orientation?: {
208
+ angle: number;
209
+ type: string;
210
+ };
211
+ }
212
+
213
+ export interface ClientHealthReport {
214
+ session: SessionData;
215
+ connection: ConnectionData;
216
+ memory: MemoryData;
217
+ window: WindowData;
218
+ screen: ScreenData;
219
+ }
@@ -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 {Timer} from '@xh/hoist/utils/async';
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, getClientDeviceInfo, stripTags, withDefault} from '@xh/hoist/utils/js';
12
- import {isEmpty, isNil, isString, round} from 'lodash';
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';