@xh/hoist 73.0.0-SNAPSHOT.1744046548420 → 73.0.0-SNAPSHOT.1744112888481

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.
@@ -5,10 +5,11 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {HoistService, PlainObject, TrackOptions, XH} from '@xh/hoist/core';
8
- import {SECONDS} from '@xh/hoist/utils/datetime';
8
+ import {Timer} from '@xh/hoist/utils/async';
9
+ import {MINUTES, SECONDS} from '@xh/hoist/utils/datetime';
9
10
  import {isOmitted} from '@xh/hoist/utils/impl';
10
- import {debounced, stripTags, withDefault} from '@xh/hoist/utils/js';
11
- import {isEmpty, isNil, isString} from 'lodash';
11
+ import {debounced, getClientDeviceInfo, stripTags, withDefault} from '@xh/hoist/utils/js';
12
+ import {isEmpty, isNil, isString, round} from 'lodash';
12
13
 
13
14
  /**
14
15
  * Primary service for tracking any activity that an application's admins want to track.
@@ -18,23 +19,38 @@ import {isEmpty, isNil, isString} from 'lodash';
18
19
  export class TrackService extends HoistService {
19
20
  static instance: TrackService;
20
21
 
22
+ private clientHealthReportSources: Map<string, () => any> = new Map();
21
23
  private oncePerSessionSent = new Map();
22
24
  private pending: PlainObject[] = [];
23
25
 
24
26
  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
+
25
37
  window.addEventListener('beforeunload', () => this.pushPendingAsync());
26
38
  }
27
39
 
28
- get conf() {
29
- return XH.getConf('xhActivityTrackingConfig', {
40
+ get conf(): ActivityTrackingConfig {
41
+ const appConfig = XH.getConf('xhActivityTrackingConfig', {});
42
+ return {
43
+ clientHealthReport: {intervalMins: -1},
30
44
  enabled: true,
45
+ logData: false,
31
46
  maxDataLength: 2000,
32
47
  maxRows: {
33
48
  default: 10000,
34
49
  options: [1000, 5000, 10000, 25000]
35
50
  },
36
- logData: false
37
- });
51
+ levels: [{username: '*', category: '*', severity: 'INFO'}],
52
+ ...appConfig
53
+ };
38
54
  }
39
55
 
40
56
  get enabled(): boolean {
@@ -79,13 +95,29 @@ export class TrackService extends HoistService {
79
95
  sent.set(key, true);
80
96
  }
81
97
 
82
- // Otherwise - log and for next batch,
98
+ // Otherwise - log and queue to send with next debounced push to server.
83
99
  this.logMessage(options);
84
100
 
85
101
  this.pending.push(this.toServerJson(options));
86
102
  this.pushPendingBuffered();
87
103
  }
88
104
 
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
+
89
121
  //------------------
90
122
  // Implementation
91
123
  //------------------
@@ -144,4 +176,58 @@ export class TrackService extends HoistService {
144
176
 
145
177
  this.logInfo(...consoleMsgs);
146
178
  }
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
+ }
216
+
217
+ interface ActivityTrackingConfig {
218
+ clientHealthReport?: Partial<TrackOptions> & {
219
+ intervalMins: number;
220
+ };
221
+ enabled: boolean;
222
+ logData: boolean;
223
+ maxDataLength: number;
224
+ maxRows?: {
225
+ default: number;
226
+ options: number[];
227
+ };
228
+ levels?: Array<{
229
+ username: string | '*';
230
+ category: string | '*';
231
+ severity: 'DEBUG' | 'INFO' | 'WARN';
232
+ }>;
147
233
  }