@xh/hoist 73.0.0-SNAPSHOT.1744145928224 → 73.0.0-SNAPSHOT.1744206740883

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 +9 -1
  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 +7 -0
  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 +10 -1
  21. package/core/XH.ts +3 -1
  22. package/desktop/cmp/viewmanager/ViewMenu.ts +11 -9
  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
@@ -320,11 +320,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
320
320
  // Handle TrackService not yet initialized (common, this client likely initialized before.)
321
321
  this.addReaction({
322
322
  when: () => XH.appIsRunning,
323
- run: () =>
324
- XH.trackService.addClientHealthReportSource(
325
- 'msalClient',
326
- () => this.telemetryResults
327
- )
323
+ run: () => XH.clientHealthService.addSource('msalClient', () => this.telemetryResults)
328
324
  });
329
325
 
330
326
  this.logInfo('Telemetry enabled');
@@ -339,7 +335,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
339
335
  this.client.removePerformanceCallback(this._telemetryCbHandle);
340
336
  this._telemetryCbHandle = null;
341
337
 
342
- XH.trackService.removeClientHealthReportSource('msalClient');
338
+ XH.clientHealthService.removeSource('msalClient');
343
339
  this.logInfo('Telemetry disabled', this.telemetryResults);
344
340
  }
345
341
 
@@ -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';