@xh/hoist 73.0.0-SNAPSHOT.1744229935910 → 73.0.0-SNAPSHOT.1744315607129

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.
@@ -14,7 +14,14 @@ export interface AccessTokenSpec {
14
14
  export type TokenMap = Record<string, Token>;
15
15
  /** Aggregated telemetry results, produced by {@link MsalClient} when enabled via config. */
16
16
  export interface TelemetryResults {
17
- /** Stats by event type - */
17
+ /** Stats across all events */
18
+ summary: {
19
+ successCount: number;
20
+ failureCount: number;
21
+ maxDuration: number;
22
+ lastFailureTime: number;
23
+ };
24
+ /** Stats by event type */
18
25
  events: Record<string, TelemetryEventResults>;
19
26
  }
20
27
  /** Aggregated telemetry results for a single type of event. */
@@ -24,11 +31,11 @@ export interface TelemetryEventResults {
24
31
  successCount: number;
25
32
  failureCount: number;
26
33
  /** Timing info (in ms) for event instances reported with duration. */
27
- duration: {
34
+ duration?: {
28
35
  count: number;
29
36
  total: number;
30
37
  average: number;
31
- worst: number;
38
+ max: number;
32
39
  };
33
40
  lastFailure?: {
34
41
  time: number;
@@ -1,36 +1,41 @@
1
1
  import { HoistService, PageState, PlainObject } from '@xh/hoist/core';
2
2
  /**
3
- * Service for gathering data about client health.
3
+ * Service for gathering data about the current state and health of the client app, for submission
4
+ * to the server or review on the console during interactive troubleshooting.
4
5
  *
5
- * Hoist sends this data once on application load, and can be configured to send
6
- * it at regularly scheduled intervals. Configure via soft-config property
7
- * 'xhActivityTracking.clientHealthReport'.
6
+ * Hoist sends this data once on application load and can be configured to send at regular intervals
7
+ * throughout a user's session via the `xhActivityTracking.clientHealthReport` app config. Reports
8
+ * are submitted via activity tracking for review within the Admin Console.
8
9
  */
9
10
  export declare class ClientHealthService extends HoistService {
10
11
  static instance: ClientHealthService;
11
12
  private sources;
12
13
  initAsync(): Promise<void>;
13
- /**
14
- * Main entry point. Return a default report of client health.
15
- */
14
+ get enabled(): boolean;
15
+ /** @returns a customizable report with metrics capturing client app/session state. */
16
16
  getReport(): ClientHealthReport;
17
- /** Get report, suitable for viewing in console. **/
17
+ /** @returns a report, formatted for easier viewing in console. **/
18
18
  getFormattedReport(): PlainObject;
19
19
  /**
20
- * Register a new source for client health report data. No-op if background health report is
21
- * not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
22
- *
20
+ * Register a new source for app-specific data to be sent with each report.
23
21
  * @param key - key under which to report the data - can be used to remove this source later.
24
22
  * @param callback - function returning serializable to include with each report.
25
23
  */
26
24
  addSource(key: string, callback: () => any): void;
27
25
  /** Unregister a previously-enabled source for client health report data. */
28
26
  removeSource(key: string): void;
27
+ /**
28
+ * Generate and submit a report to the server, via TrackService.
29
+ *
30
+ * For ad-hoc troubleshooting. Apps may also configure this service to
31
+ * submit on regular intervals.
32
+ */
33
+ sendReportAsync(): Promise<void>;
29
34
  getGeneral(): GeneralData;
30
35
  getConnection(): ConnectionData;
31
36
  getMemory(): MemoryData;
32
37
  getCustom(): PlainObject;
33
- private sendReport;
38
+ private sendReportInternal;
34
39
  }
35
40
  export interface GeneralData {
36
41
  startTime: number;
@@ -13,7 +13,11 @@ export declare class TrackService extends HoistService {
13
13
  get enabled(): boolean;
14
14
  /** Track User Activity. */
15
15
  track(options: TrackOptions | string): void;
16
- private pushPendingAsync;
16
+ /**
17
+ * Flush the queue of pending activity tracking messages to the server.
18
+ * @internal - apps should generally allow this service to manage w/its internal debounce.
19
+ */
20
+ pushPendingAsync(): Promise<void>;
17
21
  private pushPendingBuffered;
18
22
  private toServerJson;
19
23
  private logMessage;
@@ -42,10 +42,24 @@ export const aboutDialog = hoistCmp.factory({
42
42
  item: model.getTable()
43
43
  }),
44
44
  toolbar(
45
+ button({
46
+ text: 'Send Client Health Report',
47
+ icon: Icon.health(),
48
+ omit: !XH.clientHealthService.enabled,
49
+ onClick: async () => {
50
+ try {
51
+ await XH.clientHealthService.sendReportAsync();
52
+ XH.successToast('Client health report successfully submitted.');
53
+ } catch (e) {
54
+ XH.handleException('Error sending client health report', e);
55
+ }
56
+ }
57
+ }),
45
58
  filler(),
46
59
  button({
47
60
  text: 'Close',
48
61
  intent: 'primary',
62
+ outlined: true,
49
63
  onClick: onClose
50
64
  })
51
65
  )
@@ -118,6 +118,11 @@ body.xh-app.xh-mobile {
118
118
  color: var(--xh-text-color);
119
119
  }
120
120
 
121
+ // Toast - ensure z-index exceeds dialogs
122
+ ons-toast {
123
+ z-index: 30001 !important;
124
+ }
125
+
121
126
  // Remove outlines
122
127
  .tabbar__button:focus {
123
128
  outline: none;
@@ -6,7 +6,7 @@
6
6
 
7
7
  th {
8
8
  text-align: right;
9
- vertical-align: top;
9
+ vertical-align: middle;
10
10
  white-space: nowrap;
11
11
  background-color: var(--xh-bg-alt);
12
12
  border-bottom: var(--xh-border-solid);
@@ -30,7 +30,30 @@ export const aboutDialog = hoistCmp.factory({
30
30
  className: 'xh-about-dialog',
31
31
  item: model.getTable(),
32
32
  isOpen: model.isOpen,
33
- bbar: [filler(), button({text: 'Close', onClick: () => model.hide()})]
33
+ bbar: [
34
+ button({
35
+ text: 'Send Client Health Report',
36
+ icon: Icon.health(),
37
+ omit: !XH.clientHealthService.enabled,
38
+ onClick: async () => {
39
+ try {
40
+ await XH.clientHealthService.sendReportAsync();
41
+ XH.successToast({
42
+ message: 'Client health report submitted.',
43
+ timeout: 2000
44
+ });
45
+ } catch (e) {
46
+ XH.handleException('Error sending client health report', e);
47
+ }
48
+ }
49
+ }),
50
+ filler(),
51
+ button({
52
+ text: 'Close',
53
+ outlined: true,
54
+ onClick: () => model.hide()
55
+ })
56
+ ]
34
57
  });
35
58
  }
36
59
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "73.0.0-SNAPSHOT.1744229935910",
3
+ "version": "73.0.0-SNAPSHOT.1744315607129",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
package/security/Types.ts CHANGED
@@ -25,7 +25,14 @@ export type TokenMap = Record<string, Token>;
25
25
 
26
26
  /** Aggregated telemetry results, produced by {@link MsalClient} when enabled via config. */
27
27
  export interface TelemetryResults {
28
- /** Stats by event type - */
28
+ /** Stats across all events */
29
+ summary: {
30
+ successCount: number;
31
+ failureCount: number;
32
+ maxDuration: number;
33
+ lastFailureTime: number;
34
+ };
35
+ /** Stats by event type */
29
36
  events: Record<string, TelemetryEventResults>;
30
37
  }
31
38
 
@@ -36,11 +43,11 @@ export interface TelemetryEventResults {
36
43
  successCount: number;
37
44
  failureCount: number;
38
45
  /** Timing info (in ms) for event instances reported with duration. */
39
- duration: {
46
+ duration?: {
40
47
  count: number;
41
48
  total: number;
42
49
  average: number;
43
- worst: number;
50
+ max: number;
44
51
  };
45
52
  lastFailure?: {
46
53
  time: number;
@@ -115,7 +115,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
115
115
  private initialTokenLoad: boolean;
116
116
 
117
117
  /** Enable telemetry via `enableTelemetry` ctor config, or via {@link enableTelemetry}. */
118
- telemetryResults: TelemetryResults = {events: {}};
118
+ telemetryResults: TelemetryResults = null;
119
119
  private _telemetryCbHandle: string = null;
120
120
 
121
121
  constructor(config: MsalClientConfig) {
@@ -277,12 +277,20 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
277
277
  return;
278
278
  }
279
279
 
280
- this.telemetryResults = {events: {}};
280
+ this.telemetryResults = {
281
+ summary: {
282
+ successCount: 0,
283
+ failureCount: 0,
284
+ maxDuration: 0,
285
+ lastFailureTime: null
286
+ },
287
+ events: {}
288
+ };
281
289
 
282
290
  this._telemetryCbHandle = this.client.addPerformanceCallback(events => {
283
291
  events.forEach(e => {
284
292
  try {
285
- const {events} = this.telemetryResults,
293
+ const {summary, events} = this.telemetryResults,
286
294
  {name, startTimeMs, durationMs, success, errorName, errorCode} = e,
287
295
  eTime = startTimeMs ?? Date.now();
288
296
 
@@ -290,15 +298,16 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
290
298
  firstTime: eTime,
291
299
  lastTime: eTime,
292
300
  successCount: 0,
293
- failureCount: 0,
294
- duration: {count: 0, total: 0, average: 0, worst: 0},
295
- lastFailure: null
301
+ failureCount: 0
296
302
  });
297
303
  eResult.lastTime = eTime;
298
304
 
299
305
  if (success) {
306
+ summary.successCount++;
300
307
  eResult.successCount++;
301
308
  } else {
309
+ summary.failureCount++;
310
+ summary.lastFailureTime = eTime;
302
311
  eResult.failureCount++;
303
312
  eResult.lastFailure = {
304
313
  time: eTime,
@@ -309,11 +318,17 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
309
318
  }
310
319
 
311
320
  if (durationMs) {
312
- const {duration} = eResult;
321
+ const duration = (eResult.duration ??= {
322
+ count: 0,
323
+ total: 0,
324
+ average: 0,
325
+ max: 0
326
+ });
313
327
  duration.count++;
314
328
  duration.total += durationMs;
315
329
  duration.average = Math.round(duration.total / duration.count);
316
- duration.worst = Math.max(duration.worst, durationMs);
330
+ duration.max = Math.max(duration.max, durationMs);
331
+ summary.maxDuration = Math.max(summary.maxDuration, durationMs);
317
332
  }
318
333
  } catch (e) {
319
334
  this.logError(`Error processing telemetry event`, e);
@@ -4,18 +4,19 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistService, PageState, PlainObject, XH} from '@xh/hoist/core';
7
+ import {HoistService, PageState, PlainObject, TrackOptions, XH} from '@xh/hoist/core';
8
8
  import {Timer} from '@xh/hoist/utils/async';
9
9
  import {MINUTES} from '@xh/hoist/utils/datetime';
10
10
  import {withFormattedTimestamps} from '@xh/hoist/format';
11
11
  import {pick, round} from 'lodash';
12
12
 
13
13
  /**
14
- * Service for gathering data about client health.
14
+ * Service for gathering data about the current state and health of the client app, for submission
15
+ * to the server or review on the console during interactive troubleshooting.
15
16
  *
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'.
17
+ * Hoist sends this data once on application load and can be configured to send at regular intervals
18
+ * throughout a user's session via the `xhActivityTracking.clientHealthReport` app config. Reports
19
+ * are submitted via activity tracking for review within the Admin Console.
19
20
  */
20
21
  export class ClientHealthService extends HoistService {
21
22
  static instance: ClientHealthService;
@@ -25,15 +26,17 @@ export class ClientHealthService extends HoistService {
25
26
  override async initAsync() {
26
27
  const {clientHealthReport} = XH.trackService.conf;
27
28
  Timer.create({
28
- runFn: () => this.sendReport(),
29
+ runFn: () => this.sendReportInternal(),
29
30
  interval: clientHealthReport.intervalMins * MINUTES,
30
31
  delay: true
31
32
  });
32
33
  }
33
34
 
34
- /**
35
- * Main entry point. Return a default report of client health.
36
- */
35
+ get enabled(): boolean {
36
+ return XH.trackService.enabled;
37
+ }
38
+
39
+ /** @returns a customizable report with metrics capturing client app/session state. */
37
40
  getReport(): ClientHealthReport {
38
41
  return {
39
42
  general: this.getGeneral(),
@@ -43,15 +46,13 @@ export class ClientHealthService extends HoistService {
43
46
  };
44
47
  }
45
48
 
46
- /** Get report, suitable for viewing in console. **/
49
+ /** @returns a report, formatted for easier viewing in console. **/
47
50
  getFormattedReport(): PlainObject {
48
51
  return withFormattedTimestamps(this.getReport());
49
52
  }
50
53
 
51
54
  /**
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
+ * Register a new source for app-specific data to be sent with each report.
55
56
  * @param key - key under which to report the data - can be used to remove this source later.
56
57
  * @param callback - function returning serializable to include with each report.
57
58
  */
@@ -64,6 +65,17 @@ export class ClientHealthService extends HoistService {
64
65
  this.sources.delete(key);
65
66
  }
66
67
 
68
+ /**
69
+ * Generate and submit a report to the server, via TrackService.
70
+ *
71
+ * For ad-hoc troubleshooting. Apps may also configure this service to
72
+ * submit on regular intervals.
73
+ */
74
+ async sendReportAsync() {
75
+ this.sendReportInternal({severity: 'INFO'});
76
+ await XH.trackService.pushPendingAsync();
77
+ }
78
+
67
79
  // -----------------------------------
68
80
  // Generate individual report sections
69
81
  //------------------------------------
@@ -117,16 +129,17 @@ export class ClientHealthService extends HoistService {
117
129
  return ret;
118
130
  }
119
131
 
120
- //------------------
132
+ //---------------------
121
133
  // Implementation
122
- //------------------
123
- private sendReport() {
134
+ //---------------------
135
+ private sendReportInternal(opts: Partial<TrackOptions> = {}) {
124
136
  const {intervalMins, ...rest} = XH.trackService.conf.clientHealthReport ?? {};
125
137
 
126
138
  XH.track({
127
139
  category: 'App',
128
140
  message: 'Submitted health report',
129
141
  ...rest,
142
+ ...opts,
130
143
  data: {
131
144
  clientId: XH.clientId,
132
145
  sessionId: XH.sessionId,
@@ -90,10 +90,11 @@ export class TrackService extends HoistService {
90
90
  this.pushPendingBuffered();
91
91
  }
92
92
 
93
- //------------------
94
- // Implementation
95
- //------------------
96
- private async pushPendingAsync() {
93
+ /**
94
+ * Flush the queue of pending activity tracking messages to the server.
95
+ * @internal - apps should generally allow this service to manage w/its internal debounce.
96
+ */
97
+ async pushPendingAsync() {
97
98
  const {pending} = this;
98
99
  if (isEmpty(pending)) return;
99
100
 
@@ -105,6 +106,9 @@ export class TrackService extends HoistService {
105
106
  });
106
107
  }
107
108
 
109
+ //------------------
110
+ // Implementation
111
+ //------------------
108
112
  @debounced(10 * SECONDS)
109
113
  private pushPendingBuffered() {
110
114
  this.pushPendingAsync();