@xh/hoist 72.4.0 → 72.5.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 +15 -8
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +22 -14
- package/admin/tabs/cluster/instances/websocket/WebSocketColumns.ts +24 -2
- package/admin/tabs/cluster/instances/websocket/WebSocketModel.ts +76 -25
- package/admin/tabs/cluster/instances/websocket/WebSocketPanel.ts +2 -2
- package/appcontainer/AppStateModel.ts +3 -3
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +1 -1
- package/build/types/admin/tabs/cluster/instances/websocket/WebSocketColumns.d.ts +4 -1
- package/build/types/admin/tabs/cluster/instances/websocket/WebSocketModel.d.ts +5 -2
- package/build/types/core/XH.d.ts +4 -4
- package/build/types/core/types/Interfaces.d.ts +2 -2
- package/build/types/security/Types.d.ts +0 -25
- package/build/types/security/msal/MsalClient.d.ts +40 -4
- package/build/types/svc/ClientHealthService.d.ts +19 -13
- package/build/types/svc/TrackService.d.ts +5 -1
- package/build/types/svc/WebSocketService.d.ts +38 -15
- package/core/XH.ts +6 -6
- package/core/types/Interfaces.ts +2 -2
- package/data/filter/BaseFilterFieldSpec.ts +6 -2
- package/desktop/appcontainer/AboutDialog.ts +14 -0
- package/desktop/cmp/button/AppMenuButton.ts +1 -1
- package/desktop/cmp/contextmenu/ContextMenu.ts +1 -1
- package/kit/onsen/theme.scss +5 -0
- package/mobile/appcontainer/AboutDialog.scss +1 -1
- package/mobile/appcontainer/AboutDialog.ts +24 -1
- package/mobile/cmp/menu/impl/Menu.ts +2 -2
- package/package.json +1 -1
- package/security/Types.ts +0 -27
- package/security/msal/MsalClient.ts +66 -14
- package/svc/ClientHealthService.ts +35 -21
- package/svc/TrackService.ts +8 -4
- package/svc/WebSocketService.ts +74 -33
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HoistService } from '@xh/hoist/core';
|
|
1
|
+
import { HoistService, PlainObject } from '@xh/hoist/core';
|
|
2
2
|
/**
|
|
3
3
|
* Establishes and maintains a websocket connection to the Hoist server, if enabled via `AppSpec`.
|
|
4
4
|
*
|
|
@@ -27,6 +27,10 @@ export declare class WebSocketService extends HoistService {
|
|
|
27
27
|
readonly HEARTBEAT_TOPIC = "xhHeartbeat";
|
|
28
28
|
readonly REG_SUCCESS_TOPIC = "xhRegistrationSuccess";
|
|
29
29
|
readonly FORCE_APP_SUSPEND_TOPIC = "xhForceAppSuspend";
|
|
30
|
+
readonly REQ_CLIENT_HEALTH_RPT_TOPIC = "xhRequestClientHealthReport";
|
|
31
|
+
readonly METADATA_FOR_HANDSHAKE: string[];
|
|
32
|
+
/** True if WebSockets generally enabled - set statically in code via {@link AppSpec}. */
|
|
33
|
+
enabled: boolean;
|
|
30
34
|
/** Unique channel assigned by server upon successful connection. */
|
|
31
35
|
channelKey: string;
|
|
32
36
|
/** Last time a message was received, including heartbeat messages. */
|
|
@@ -35,10 +39,10 @@ export declare class WebSocketService extends HoistService {
|
|
|
35
39
|
get connected(): boolean;
|
|
36
40
|
/** Set to true to log all sent/received messages - very chatty. */
|
|
37
41
|
logMessages: boolean;
|
|
42
|
+
telemetry: WebSocketTelemetry;
|
|
38
43
|
private _timer;
|
|
39
44
|
private _socket;
|
|
40
45
|
private _subsByTopic;
|
|
41
|
-
enabled: boolean;
|
|
42
46
|
constructor();
|
|
43
47
|
initAsync(): Promise<void>;
|
|
44
48
|
/**
|
|
@@ -61,23 +65,24 @@ export declare class WebSocketService extends HoistService {
|
|
|
61
65
|
* Send a message back to the server via the connected websocket.
|
|
62
66
|
*/
|
|
63
67
|
sendMessage(message: WebSocketMessage): void;
|
|
64
|
-
connect(): void;
|
|
65
|
-
disconnect(): void;
|
|
66
|
-
heartbeatOrReconnect(): void;
|
|
67
|
-
private onServerInstanceChange;
|
|
68
68
|
shutdown(): void;
|
|
69
|
+
getFormattedTelemetry(): PlainObject;
|
|
70
|
+
private connect;
|
|
71
|
+
private disconnect;
|
|
72
|
+
private heartbeatOrReconnect;
|
|
73
|
+
private onServerInstanceChange;
|
|
69
74
|
onOpen(ev: any): void;
|
|
70
75
|
onClose(ev: any): void;
|
|
71
76
|
onError(ev: any): void;
|
|
72
|
-
onMessage(rawMsg:
|
|
73
|
-
notifySubscribers
|
|
74
|
-
getSubsForTopic
|
|
75
|
-
updateConnectedStatus
|
|
76
|
-
installChannelKey
|
|
77
|
-
updateLastMessageTime
|
|
78
|
-
buildWebSocketUrl
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
onMessage(rawMsg: MessageEvent): void;
|
|
78
|
+
private notifySubscribers;
|
|
79
|
+
private getSubsForTopic;
|
|
80
|
+
private updateConnectedStatus;
|
|
81
|
+
private installChannelKey;
|
|
82
|
+
private updateLastMessageTime;
|
|
83
|
+
private buildWebSocketUrl;
|
|
84
|
+
private maybeLogMessage;
|
|
85
|
+
private noteTelemetryEvent;
|
|
81
86
|
}
|
|
82
87
|
/**
|
|
83
88
|
* Wrapper class to encapsulate and manage a subscription to messages for a given topic + handler.
|
|
@@ -93,3 +98,21 @@ export interface WebSocketMessage {
|
|
|
93
98
|
topic: string;
|
|
94
99
|
data?: any;
|
|
95
100
|
}
|
|
101
|
+
/** Telemetry collected by this service + included in {@link ClientHealthService} reporting. */
|
|
102
|
+
export interface WebSocketTelemetry {
|
|
103
|
+
channelKey: string;
|
|
104
|
+
subscriptionCount: number;
|
|
105
|
+
events: {
|
|
106
|
+
connOpened?: WebSocketEventTelemetry;
|
|
107
|
+
connClosed?: WebSocketEventTelemetry;
|
|
108
|
+
connError?: WebSocketEventTelemetry;
|
|
109
|
+
msgReceived?: WebSocketEventTelemetry;
|
|
110
|
+
msgSent?: WebSocketEventTelemetry;
|
|
111
|
+
heartbeatReconnectAttempt?: WebSocketEventTelemetry;
|
|
112
|
+
instanceChangeReconnectAttempt?: WebSocketEventTelemetry;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export interface WebSocketEventTelemetry {
|
|
116
|
+
count: number;
|
|
117
|
+
lastTime: number;
|
|
118
|
+
}
|
package/core/XH.ts
CHANGED
|
@@ -87,13 +87,13 @@ declare const xhIsDevelopmentMode: boolean;
|
|
|
87
87
|
*/
|
|
88
88
|
export class XHApi {
|
|
89
89
|
/** Unique id for this loaded instance of the app. Unique for every refresh of document. */
|
|
90
|
-
|
|
90
|
+
loadId: string = this.genLoadId();
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Unique id for this browser tab/window on this domain.
|
|
94
94
|
* Corresponds to the scope of the built-in sessionStorage object.
|
|
95
95
|
*/
|
|
96
|
-
|
|
96
|
+
tabId: string = this.genTabId();
|
|
97
97
|
|
|
98
98
|
//--------------------------
|
|
99
99
|
// Implementation Delegates
|
|
@@ -807,15 +807,15 @@ export class XHApi {
|
|
|
807
807
|
return this.appContainerModel;
|
|
808
808
|
}
|
|
809
809
|
|
|
810
|
-
private
|
|
810
|
+
private genLoadId(): string {
|
|
811
811
|
return new ShortUniqueId({length: 8}).rnd();
|
|
812
812
|
}
|
|
813
813
|
|
|
814
|
-
private
|
|
815
|
-
let ret = window.sessionStorage?.getItem('
|
|
814
|
+
private genTabId(): string {
|
|
815
|
+
let ret = window.sessionStorage?.getItem('xhTabId');
|
|
816
816
|
if (!ret) {
|
|
817
817
|
ret = new ShortUniqueId({length: 8}).rnd();
|
|
818
|
-
window.sessionStorage?.setItem('
|
|
818
|
+
window.sessionStorage?.setItem('xhTabId', ret);
|
|
819
819
|
}
|
|
820
820
|
return ret;
|
|
821
821
|
}
|
package/core/types/Interfaces.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {RuleLike} from '@xh/hoist/data';
|
|
9
|
-
import {ReactElement, ReactNode} from 'react';
|
|
9
|
+
import {MouseEvent, ReactElement, ReactNode} from 'react';
|
|
10
10
|
import {LoadSpec} from '../load';
|
|
11
11
|
import {Intent, PlainObject, Thunkable} from './Types';
|
|
12
12
|
|
|
@@ -284,7 +284,7 @@ export interface MenuItem {
|
|
|
284
284
|
className?: string;
|
|
285
285
|
|
|
286
286
|
/** Executed when the user clicks the menu item. */
|
|
287
|
-
actionFn?: () => void;
|
|
287
|
+
actionFn?: (e: MouseEvent | PointerEvent) => void;
|
|
288
288
|
|
|
289
289
|
/** Executed before the item is shown. Use to adjust properties dynamically. */
|
|
290
290
|
prepareFn?: (me: MenuItem) => void;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {HoistBase} from '@xh/hoist/core';
|
|
8
8
|
import {Field, Store, FieldFilter, FieldType, genDisplayName, View} from '@xh/hoist/data';
|
|
9
|
-
import {isEmpty} from 'lodash';
|
|
9
|
+
import {compact, isArray, isEmpty} from 'lodash';
|
|
10
10
|
import {FieldFilterOperator} from './Types';
|
|
11
11
|
|
|
12
12
|
export interface BaseFilterFieldSpecConfig {
|
|
@@ -72,7 +72,11 @@ export abstract class BaseFilterFieldSpec extends HoistBase {
|
|
|
72
72
|
this.displayName = displayName ?? sourceField?.displayName ?? genDisplayName(field);
|
|
73
73
|
this.ops = this.parseOperators(ops);
|
|
74
74
|
this.forceSelection = forceSelection ?? false;
|
|
75
|
-
this.values = values
|
|
75
|
+
this.values = isArray(values)
|
|
76
|
+
? compact(values)
|
|
77
|
+
: this.isBoolFieldType
|
|
78
|
+
? [true, false]
|
|
79
|
+
: null;
|
|
76
80
|
this.hasExplicitValues = !isEmpty(this.values);
|
|
77
81
|
this.enableValues = this.hasExplicitValues || (enableValues ?? this.isEnumerableByDefault);
|
|
78
82
|
}
|
|
@@ -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
|
)
|
|
@@ -202,7 +202,7 @@ function parseMenuItems(items: MenuItemLike[]): ReactNode[] {
|
|
|
202
202
|
icon: item.icon,
|
|
203
203
|
intent: item.intent,
|
|
204
204
|
className: item.className,
|
|
205
|
-
onClick: actionFn ?
|
|
205
|
+
onClick: actionFn ? e => wait().then(() => actionFn(e)) : null, // do async to allow menu to close
|
|
206
206
|
disabled: item.disabled
|
|
207
207
|
};
|
|
208
208
|
|
|
@@ -70,7 +70,7 @@ function parseItems(items: MenuItemLike[]): ReactNode[] {
|
|
|
70
70
|
icon: item.icon,
|
|
71
71
|
intent: item.intent,
|
|
72
72
|
className: item.className,
|
|
73
|
-
onClick: item.actionFn ?
|
|
73
|
+
onClick: item.actionFn ? e => wait().then(() => item.actionFn(e)) : null, // do async to allow menu to close
|
|
74
74
|
popoverProps: {usePortal: true},
|
|
75
75
|
disabled: item.disabled,
|
|
76
76
|
items
|
package/kit/onsen/theme.scss
CHANGED
|
@@ -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: [
|
|
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
|
});
|
|
@@ -98,9 +98,9 @@ class LocalMenuModel extends HoistModel {
|
|
|
98
98
|
omit: hidden,
|
|
99
99
|
onTouchStart: () => (this.pressedIdx = idx),
|
|
100
100
|
onTouchEnd: () => (this.pressedIdx = null),
|
|
101
|
-
onClick:
|
|
101
|
+
onClick: e => {
|
|
102
102
|
this.pressedIdx = null;
|
|
103
|
-
if (actionFn) actionFn();
|
|
103
|
+
if (actionFn) actionFn(e);
|
|
104
104
|
onDismiss();
|
|
105
105
|
}
|
|
106
106
|
});
|
package/package.json
CHANGED
package/security/Types.ts
CHANGED
|
@@ -22,30 +22,3 @@ export interface AccessTokenSpec {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export type TokenMap = Record<string, Token>;
|
|
25
|
-
|
|
26
|
-
/** Aggregated telemetry results, produced by {@link MsalClient} when enabled via config. */
|
|
27
|
-
export interface TelemetryResults {
|
|
28
|
-
/** Stats by event type - */
|
|
29
|
-
events: Record<string, TelemetryEventResults>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Aggregated telemetry results for a single type of event. */
|
|
33
|
-
export interface TelemetryEventResults {
|
|
34
|
-
firstTime: number;
|
|
35
|
-
lastTime: number;
|
|
36
|
-
successCount: number;
|
|
37
|
-
failureCount: number;
|
|
38
|
-
/** Timing info (in ms) for event instances reported with duration. */
|
|
39
|
-
duration: {
|
|
40
|
-
count: number;
|
|
41
|
-
total: number;
|
|
42
|
-
average: number;
|
|
43
|
-
worst: number;
|
|
44
|
-
};
|
|
45
|
-
lastFailure?: {
|
|
46
|
-
time: number;
|
|
47
|
-
duration: number;
|
|
48
|
-
code: string;
|
|
49
|
-
name: string;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -20,7 +20,7 @@ import {logDebug, logError, logInfo, logWarn, mergeDeep, throwIf} from '@xh/hois
|
|
|
20
20
|
import {withFormattedTimestamps} from '@xh/hoist/format';
|
|
21
21
|
import {flatMap, union, uniq} from 'lodash';
|
|
22
22
|
import {BaseOAuthClient, BaseOAuthClientConfig} from '../BaseOAuthClient';
|
|
23
|
-
import {AccessTokenSpec,
|
|
23
|
+
import {AccessTokenSpec, TokenMap} from '../Types';
|
|
24
24
|
|
|
25
25
|
export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
|
|
26
26
|
/**
|
|
@@ -39,8 +39,7 @@ export interface MsalClientConfig extends BaseOAuthClientConfig<MsalTokenSpec> {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* True to enable support for built-in telemetry provided by this class's internal MSAL client.
|
|
42
|
-
* Captured performance events will be summarized
|
|
43
|
-
* See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/performance.md
|
|
42
|
+
* Captured performance events will be summarized as {@link MsalClientTelemetry}.
|
|
44
43
|
*/
|
|
45
44
|
enableTelemetry?: boolean;
|
|
46
45
|
|
|
@@ -115,7 +114,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
115
114
|
private initialTokenLoad: boolean;
|
|
116
115
|
|
|
117
116
|
/** Enable telemetry via `enableTelemetry` ctor config, or via {@link enableTelemetry}. */
|
|
118
|
-
|
|
117
|
+
telemetry: MsalClientTelemetry = null;
|
|
119
118
|
private _telemetryCbHandle: string = null;
|
|
120
119
|
|
|
121
120
|
constructor(config: MsalClientConfig) {
|
|
@@ -268,7 +267,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
268
267
|
// Telemetry
|
|
269
268
|
//------------------------
|
|
270
269
|
getFormattedTelemetry(): PlainObject {
|
|
271
|
-
return withFormattedTimestamps(this.
|
|
270
|
+
return withFormattedTimestamps(this.telemetry);
|
|
272
271
|
}
|
|
273
272
|
|
|
274
273
|
enableTelemetry(): void {
|
|
@@ -277,12 +276,20 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
277
276
|
return;
|
|
278
277
|
}
|
|
279
278
|
|
|
280
|
-
this.
|
|
279
|
+
this.telemetry = {
|
|
280
|
+
summary: {
|
|
281
|
+
successCount: 0,
|
|
282
|
+
failureCount: 0,
|
|
283
|
+
maxDuration: 0,
|
|
284
|
+
lastFailureTime: null
|
|
285
|
+
},
|
|
286
|
+
events: {}
|
|
287
|
+
};
|
|
281
288
|
|
|
282
289
|
this._telemetryCbHandle = this.client.addPerformanceCallback(events => {
|
|
283
290
|
events.forEach(e => {
|
|
284
291
|
try {
|
|
285
|
-
const {events} = this.
|
|
292
|
+
const {summary, events} = this.telemetry,
|
|
286
293
|
{name, startTimeMs, durationMs, success, errorName, errorCode} = e,
|
|
287
294
|
eTime = startTimeMs ?? Date.now();
|
|
288
295
|
|
|
@@ -290,15 +297,16 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
290
297
|
firstTime: eTime,
|
|
291
298
|
lastTime: eTime,
|
|
292
299
|
successCount: 0,
|
|
293
|
-
failureCount: 0
|
|
294
|
-
duration: {count: 0, total: 0, average: 0, worst: 0},
|
|
295
|
-
lastFailure: null
|
|
300
|
+
failureCount: 0
|
|
296
301
|
});
|
|
297
302
|
eResult.lastTime = eTime;
|
|
298
303
|
|
|
299
304
|
if (success) {
|
|
305
|
+
summary.successCount++;
|
|
300
306
|
eResult.successCount++;
|
|
301
307
|
} else {
|
|
308
|
+
summary.failureCount++;
|
|
309
|
+
summary.lastFailureTime = eTime;
|
|
302
310
|
eResult.failureCount++;
|
|
303
311
|
eResult.lastFailure = {
|
|
304
312
|
time: eTime,
|
|
@@ -309,11 +317,17 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
if (durationMs) {
|
|
312
|
-
const
|
|
320
|
+
const duration = (eResult.duration ??= {
|
|
321
|
+
count: 0,
|
|
322
|
+
total: 0,
|
|
323
|
+
average: 0,
|
|
324
|
+
max: 0
|
|
325
|
+
});
|
|
313
326
|
duration.count++;
|
|
314
327
|
duration.total += durationMs;
|
|
315
328
|
duration.average = Math.round(duration.total / duration.count);
|
|
316
|
-
duration.
|
|
329
|
+
duration.max = Math.max(duration.max, durationMs);
|
|
330
|
+
summary.maxDuration = Math.max(summary.maxDuration, durationMs);
|
|
317
331
|
}
|
|
318
332
|
} catch (e) {
|
|
319
333
|
this.logError(`Error processing telemetry event`, e);
|
|
@@ -324,10 +338,10 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
324
338
|
// Wait for clientHealthService (this client likely initialized during earlier AUTHENTICATING.)
|
|
325
339
|
this.addReaction({
|
|
326
340
|
when: () => XH.appState === AppState.INITIALIZING_APP,
|
|
327
|
-
run: () => XH.clientHealthService.addSource('msalClient', () => this.
|
|
341
|
+
run: () => XH.clientHealthService.addSource('msalClient', () => this.telemetry)
|
|
328
342
|
});
|
|
329
343
|
|
|
330
|
-
this.
|
|
344
|
+
this.logDebug('Telemetry enabled');
|
|
331
345
|
}
|
|
332
346
|
|
|
333
347
|
disableTelemetry(): void {
|
|
@@ -434,3 +448,41 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
434
448
|
this.logDebug('User Authenticated', account.username);
|
|
435
449
|
}
|
|
436
450
|
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Telemetry produced by this client (if enabled) + included in {@link ClientHealthService}
|
|
454
|
+
* reporting. Leverages MSAL's opt-in support for emitting performance events.
|
|
455
|
+
* See https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/performance.md
|
|
456
|
+
*/
|
|
457
|
+
interface MsalClientTelemetry {
|
|
458
|
+
/** Stats across all events */
|
|
459
|
+
summary: {
|
|
460
|
+
successCount: number;
|
|
461
|
+
failureCount: number;
|
|
462
|
+
maxDuration: number;
|
|
463
|
+
lastFailureTime: number;
|
|
464
|
+
};
|
|
465
|
+
/** Stats by event type */
|
|
466
|
+
events: Record<string, MsalEventTelemetry>;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Aggregated telemetry results for a single type of event. */
|
|
470
|
+
interface MsalEventTelemetry {
|
|
471
|
+
firstTime: number;
|
|
472
|
+
lastTime: number;
|
|
473
|
+
successCount: number;
|
|
474
|
+
failureCount: number;
|
|
475
|
+
/** Timing info (in ms) for event instances reported with duration. */
|
|
476
|
+
duration?: {
|
|
477
|
+
count: number;
|
|
478
|
+
total: number;
|
|
479
|
+
average: number;
|
|
480
|
+
max: number;
|
|
481
|
+
};
|
|
482
|
+
lastFailure?: {
|
|
483
|
+
time: number;
|
|
484
|
+
duration: number;
|
|
485
|
+
code: string;
|
|
486
|
+
name: string;
|
|
487
|
+
};
|
|
488
|
+
}
|
|
@@ -4,18 +4,20 @@
|
|
|
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
|
+
import {WebSocketTelemetry} from '@xh/hoist/svc/WebSocketService';
|
|
8
9
|
import {Timer} from '@xh/hoist/utils/async';
|
|
9
10
|
import {MINUTES} from '@xh/hoist/utils/datetime';
|
|
10
11
|
import {withFormattedTimestamps} from '@xh/hoist/format';
|
|
11
12
|
import {pick, round} from 'lodash';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Service for gathering data about client
|
|
15
|
+
* Service for gathering data about the current state and health of the client app, for submission
|
|
16
|
+
* to the server or review on the console during interactive troubleshooting.
|
|
15
17
|
*
|
|
16
|
-
* Hoist sends this data once on application load
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* Hoist sends this data once on application load and can be configured to send at regular intervals
|
|
19
|
+
* throughout a user's session via the `xhActivityTracking.clientHealthReport` app config. Reports
|
|
20
|
+
* are submitted via activity tracking for review within the Admin Console.
|
|
19
21
|
*/
|
|
20
22
|
export class ClientHealthService extends HoistService {
|
|
21
23
|
static instance: ClientHealthService;
|
|
@@ -25,33 +27,34 @@ export class ClientHealthService extends HoistService {
|
|
|
25
27
|
override async initAsync() {
|
|
26
28
|
const {clientHealthReport} = XH.trackService.conf;
|
|
27
29
|
Timer.create({
|
|
28
|
-
runFn: () => this.
|
|
30
|
+
runFn: () => this.sendReportInternal(),
|
|
29
31
|
interval: clientHealthReport.intervalMins * MINUTES,
|
|
30
32
|
delay: true
|
|
31
33
|
});
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
get enabled(): boolean {
|
|
37
|
+
return XH.trackService.enabled;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @returns a customizable report with metrics capturing client app/session state. */
|
|
37
41
|
getReport(): ClientHealthReport {
|
|
38
42
|
return {
|
|
39
43
|
general: this.getGeneral(),
|
|
40
44
|
memory: this.getMemory(),
|
|
41
45
|
connection: this.getConnection(),
|
|
46
|
+
webSockets: XH.webSocketService.telemetry,
|
|
42
47
|
...this.getCustom()
|
|
43
48
|
};
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
/**
|
|
51
|
+
/** @returns a report, formatted for easier viewing in console. **/
|
|
47
52
|
getFormattedReport(): PlainObject {
|
|
48
53
|
return withFormattedTimestamps(this.getReport());
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
52
|
-
* Register a new source for
|
|
53
|
-
* not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
|
|
54
|
-
*
|
|
57
|
+
* Register a new source for app-specific data to be sent with each report.
|
|
55
58
|
* @param key - key under which to report the data - can be used to remove this source later.
|
|
56
59
|
* @param callback - function returning serializable to include with each report.
|
|
57
60
|
*/
|
|
@@ -64,6 +67,17 @@ export class ClientHealthService extends HoistService {
|
|
|
64
67
|
this.sources.delete(key);
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Generate and submit a report to the server, via TrackService.
|
|
72
|
+
*
|
|
73
|
+
* For ad-hoc troubleshooting. Apps may also configure this service to
|
|
74
|
+
* submit on regular intervals.
|
|
75
|
+
*/
|
|
76
|
+
async sendReportAsync() {
|
|
77
|
+
this.sendReportInternal({severity: 'INFO'});
|
|
78
|
+
await XH.trackService.pushPendingAsync();
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
// -----------------------------------
|
|
68
82
|
// Generate individual report sections
|
|
69
83
|
//------------------------------------
|
|
@@ -75,8 +89,7 @@ export class ClientHealthService extends HoistService {
|
|
|
75
89
|
startTime,
|
|
76
90
|
durationMins: elapsedMins(startTime),
|
|
77
91
|
idleMins: elapsedMins(XH.lastActivityMs),
|
|
78
|
-
pageState: XH.pageState
|
|
79
|
-
webSocket: XH.webSocketService.channelKey
|
|
92
|
+
pageState: XH.pageState
|
|
80
93
|
};
|
|
81
94
|
}
|
|
82
95
|
|
|
@@ -117,19 +130,20 @@ export class ClientHealthService extends HoistService {
|
|
|
117
130
|
return ret;
|
|
118
131
|
}
|
|
119
132
|
|
|
120
|
-
|
|
133
|
+
//---------------------
|
|
121
134
|
// Implementation
|
|
122
|
-
|
|
123
|
-
private
|
|
135
|
+
//---------------------
|
|
136
|
+
private sendReportInternal(opts: Partial<TrackOptions> = {}) {
|
|
124
137
|
const {intervalMins, ...rest} = XH.trackService.conf.clientHealthReport ?? {};
|
|
125
138
|
|
|
126
139
|
XH.track({
|
|
127
140
|
category: 'App',
|
|
128
141
|
message: 'Submitted health report',
|
|
129
142
|
...rest,
|
|
143
|
+
...opts,
|
|
130
144
|
data: {
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
loadId: XH.loadId,
|
|
146
|
+
tabId: XH.tabId,
|
|
133
147
|
...this.getReport()
|
|
134
148
|
}
|
|
135
149
|
});
|
|
@@ -141,7 +155,6 @@ export interface GeneralData {
|
|
|
141
155
|
durationMins: number;
|
|
142
156
|
idleMins: number;
|
|
143
157
|
pageState: PageState;
|
|
144
|
-
webSocket: string;
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
export interface ConnectionData {
|
|
@@ -162,4 +175,5 @@ export interface ClientHealthReport {
|
|
|
162
175
|
general: GeneralData;
|
|
163
176
|
connection: ConnectionData;
|
|
164
177
|
memory: MemoryData;
|
|
178
|
+
webSockets: WebSocketTelemetry;
|
|
165
179
|
}
|
package/svc/TrackService.ts
CHANGED
|
@@ -90,10 +90,11 @@ export class TrackService extends HoistService {
|
|
|
90
90
|
this.pushPendingBuffered();
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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();
|