@xh/hoist 73.0.0-SNAPSHOT.1744206740883 → 73.0.0-SNAPSHOT.1744229935910
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 +8 -4
- package/admin/AdminUtils.ts +1 -18
- package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +3 -3
- package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -3
- package/admin/tabs/cluster/objects/DetailModel.ts +2 -3
- package/admin/tabs/cluster/objects/DetailPanel.ts +2 -3
- package/appcontainer/AppStateModel.ts +40 -7
- package/build/types/admin/AdminUtils.d.ts +0 -4
- package/build/types/appcontainer/AppStateModel.d.ts +2 -0
- package/build/types/core/XH.d.ts +9 -0
- package/build/types/format/FormatDate.d.ts +22 -1
- package/build/types/security/Types.d.ts +3 -3
- package/build/types/security/msal/MsalClient.d.ts +2 -0
- package/build/types/svc/ClientHealthService.d.ts +10 -32
- package/core/XH.ts +23 -0
- package/format/FormatDate.ts +45 -3
- package/package.json +1 -1
- package/security/Types.ts +3 -3
- package/security/msal/MsalClient.ts +11 -7
- package/svc/ClientHealthService.ts +32 -86
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## v73.0.0-SNAPSHOT - unreleased
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## v72.4.0 - 2025-04-09
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
### 🎁 New Features
|
|
8
|
+
* New methods for formatting timestamps within nested JSON objects. See `withFormattedTimestamps`
|
|
9
|
+
and `timestampReplacer` in the `@xh/hoist/format` package.
|
|
10
|
+
* `ViewManagerConfig` takes new optional key `viewMenuItemFn` to allow ViewManager implementations
|
|
11
|
+
to customize the menu items for views in the view manager menu.
|
|
9
12
|
|
|
10
13
|
### ⚙️ Technical
|
|
11
|
-
* Added
|
|
14
|
+
* Added dedicated `ClientHealthService` for managing client health report. Additional enhancements
|
|
15
|
+
to health report to include information about web sockets, idle time, and page state.
|
|
12
16
|
|
|
13
17
|
## v72.3.0 - 2025-04-08
|
|
14
18
|
|
package/admin/AdminUtils.ts
CHANGED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {XH} from '@xh/hoist/core';
|
|
8
|
-
import {
|
|
9
|
-
import {isNumber} from 'lodash';
|
|
10
|
-
import {fmtDateTimeSec} from '@xh/hoist/format';
|
|
8
|
+
import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
11
|
* Generate a standardized filename for an Admin module grid export, without datestamp.
|
|
@@ -23,18 +21,3 @@ export function exportFilename(moduleName: string): string {
|
|
|
23
21
|
export function exportFilenameWithDate(moduleName: string): () => string {
|
|
24
22
|
return () => `${XH.appCode}-${moduleName}-${LocalDate.today()}`;
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Replacer for JSON.stringify to replace timestamp properties with formatted strings.
|
|
29
|
-
*/
|
|
30
|
-
export function timestampReplacer(k: string, v: any) {
|
|
31
|
-
if (
|
|
32
|
-
(k.endsWith('Time') || k.endsWith('Date') || k.endsWith('Timestamp') || k == 'timestamp') &&
|
|
33
|
-
isNumber(v) &&
|
|
34
|
-
v > Date.now() - 25 * 365 * DAYS // heuristic to avoid catching smaller ms ranges
|
|
35
|
-
) {
|
|
36
|
-
return fmtDateTimeSec(v, {fmt: 'MMM DD HH:mm:ss.SSS'});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return v;
|
|
40
|
-
}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {exportFilename
|
|
7
|
+
import {exportFilename} from '@xh/hoist/admin/AdminUtils';
|
|
8
8
|
import * as Col from '@xh/hoist/admin/columns';
|
|
9
9
|
import {FormModel} from '@xh/hoist/cmp/form';
|
|
10
10
|
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
11
11
|
import {HoistModel, lookup, managed} from '@xh/hoist/core';
|
|
12
12
|
import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
13
13
|
import {ActivityTrackingModel} from '../ActivityTrackingModel';
|
|
14
|
-
import {fmtJson} from '@xh/hoist/format';
|
|
14
|
+
import {fmtJson, timestampReplacer} from '@xh/hoist/format';
|
|
15
15
|
|
|
16
16
|
export class ActivityDetailModel extends HoistModel {
|
|
17
17
|
@lookup(ActivityTrackingModel) activityTrackingModel: ActivityTrackingModel;
|
|
@@ -118,7 +118,7 @@ export class ActivityDetailModel extends HoistModel {
|
|
|
118
118
|
let formattedTrackData = trackData;
|
|
119
119
|
if (formattedTrackData) {
|
|
120
120
|
try {
|
|
121
|
-
formattedTrackData = fmtJson(trackData, {replacer: timestampReplacer});
|
|
121
|
+
formattedTrackData = fmtJson(trackData, {replacer: timestampReplacer()});
|
|
122
122
|
} catch (ignored) {}
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -12,8 +12,7 @@ import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
|
12
12
|
import {jsonInput} from '@xh/hoist/desktop/cmp/input';
|
|
13
13
|
import {Icon} from '@xh/hoist/icon';
|
|
14
14
|
import {isEmpty} from 'lodash';
|
|
15
|
-
import {fmtJson} from '@xh/hoist/format';
|
|
16
|
-
import {timestampReplacer} from '@xh/hoist/admin/AdminUtils';
|
|
15
|
+
import {fmtJson, timestampReplacer} from '@xh/hoist/format';
|
|
17
16
|
|
|
18
17
|
export const detailsPanel = hoistCmp.factory({
|
|
19
18
|
model: creates(DetailsModel),
|
|
@@ -59,7 +58,7 @@ const stats = hoistCmp.factory<DetailsModel>({
|
|
|
59
58
|
enableSearch: true,
|
|
60
59
|
showFullscreenButton: false,
|
|
61
60
|
editorProps: {lineNumbers: false},
|
|
62
|
-
value: fmtJson(stats, {replacer: timestampReplacer})
|
|
61
|
+
value: fmtJson(stats, {replacer: timestampReplacer()})
|
|
63
62
|
})
|
|
64
63
|
);
|
|
65
64
|
}
|
|
@@ -10,7 +10,7 @@ import {HoistModel, lookup, managed, XH} from '@xh/hoist/core';
|
|
|
10
10
|
import {StoreRecord} from '@xh/hoist/data';
|
|
11
11
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
12
12
|
import {isEmpty, isEqual, without} from 'lodash';
|
|
13
|
-
import {
|
|
13
|
+
import {withFormattedTimestamps} from '@xh/hoist/format';
|
|
14
14
|
|
|
15
15
|
export class DetailModel extends HoistModel {
|
|
16
16
|
@lookup(ClusterObjectsModel)
|
|
@@ -79,8 +79,7 @@ export class DetailModel extends HoistModel {
|
|
|
79
79
|
const gridModel = this.createGridModel(diffFields, otherFields);
|
|
80
80
|
gridModel.loadData(
|
|
81
81
|
instanceNames.map(instanceName => {
|
|
82
|
-
|
|
83
|
-
data = JSON.parse(JSON.stringify(data, timestampReplacer));
|
|
82
|
+
const data = withFormattedTimestamps(adminStatsByInstance[instanceName] ?? {});
|
|
84
83
|
return {instanceName, ...data};
|
|
85
84
|
})
|
|
86
85
|
);
|
|
@@ -12,8 +12,7 @@ import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
|
12
12
|
import {Icon} from '@xh/hoist/icon';
|
|
13
13
|
import {DetailModel} from './DetailModel';
|
|
14
14
|
import './ClusterObjects.scss';
|
|
15
|
-
import {timestampReplacer} from '@xh/hoist/
|
|
16
|
-
import {fmtJson} from '@xh/hoist/format';
|
|
15
|
+
import {fmtJson, timestampReplacer} from '@xh/hoist/format';
|
|
17
16
|
|
|
18
17
|
export const detailPanel = hoistCmp.factory({
|
|
19
18
|
model: creates(DetailModel),
|
|
@@ -44,7 +43,7 @@ export const detailPanel = hoistCmp.factory({
|
|
|
44
43
|
height: '100%',
|
|
45
44
|
showFullscreenButton: false,
|
|
46
45
|
editorProps: {lineNumbers: false},
|
|
47
|
-
value: fmtJson(selectedAdminStats, {replacer: timestampReplacer})
|
|
46
|
+
value: fmtJson(selectedAdminStats, {replacer: timestampReplacer()})
|
|
48
47
|
})
|
|
49
48
|
})
|
|
50
49
|
]
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {AppState, AppSuspendData, HoistModel, XH} from '@xh/hoist/core';
|
|
7
|
+
import {AppState, AppSuspendData, HoistModel, PlainObject, XH} from '@xh/hoist/core';
|
|
8
8
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
9
9
|
import {Timer} from '@xh/hoist/utils/async';
|
|
10
|
-
import {camelCase, isBoolean, isString, mapKeys} from 'lodash';
|
|
10
|
+
import {camelCase, isBoolean, isString, mapKeys, pick} from 'lodash';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Support for Core Hoist Application state and loading.
|
|
@@ -92,13 +92,14 @@ export class AppStateModel extends HoistModel {
|
|
|
92
92
|
timestamp: loadStarted,
|
|
93
93
|
elapsed: Date.now() - loadStarted - (timings.LOGIN_REQUIRED ?? 0),
|
|
94
94
|
data: {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
locationHref: window.location.href,
|
|
95
|
+
clientId: XH.clientId,
|
|
96
|
+
sessionId: XH.sessionId,
|
|
98
97
|
timings: mapKeys(timings, (v, k) => camelCase(k)),
|
|
99
|
-
clientHealth: XH.clientHealthService.getReport()
|
|
98
|
+
clientHealth: XH.clientHealthService.getReport(),
|
|
99
|
+
window: this.getWindowData(),
|
|
100
|
+
screen: this.getScreenData()
|
|
100
101
|
},
|
|
101
|
-
logData: ['
|
|
102
|
+
logData: ['clientId', 'sessionId'],
|
|
102
103
|
omit: !XH.appSpec.trackAppLoad
|
|
103
104
|
})
|
|
104
105
|
});
|
|
@@ -114,4 +115,36 @@ export class AppStateModel extends HoistModel {
|
|
|
114
115
|
});
|
|
115
116
|
});
|
|
116
117
|
}
|
|
118
|
+
|
|
119
|
+
private getScreenData(): PlainObject {
|
|
120
|
+
const screen = window.screen as any;
|
|
121
|
+
if (!screen) return null;
|
|
122
|
+
|
|
123
|
+
const ret: PlainObject = pick(screen, [
|
|
124
|
+
'availWidth',
|
|
125
|
+
'availHeight',
|
|
126
|
+
'width',
|
|
127
|
+
'height',
|
|
128
|
+
'colorDepth',
|
|
129
|
+
'pixelDepth',
|
|
130
|
+
'availLeft',
|
|
131
|
+
'availTop'
|
|
132
|
+
]);
|
|
133
|
+
if (screen.orientation) {
|
|
134
|
+
ret.orientation = pick(screen.orientation, ['angle', 'type']);
|
|
135
|
+
}
|
|
136
|
+
return ret;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private getWindowData(): PlainObject {
|
|
140
|
+
return pick(window, [
|
|
141
|
+
'devicePixelRatio',
|
|
142
|
+
'screenX',
|
|
143
|
+
'screenY',
|
|
144
|
+
'innerWidth',
|
|
145
|
+
'innerHeight',
|
|
146
|
+
'outerWidth',
|
|
147
|
+
'outerHeight'
|
|
148
|
+
]);
|
|
149
|
+
}
|
|
117
150
|
}
|
|
@@ -7,7 +7,3 @@ export declare function exportFilename(moduleName: string): string;
|
|
|
7
7
|
* Returned as a closure to ensure current date is evaluated at export time.
|
|
8
8
|
*/
|
|
9
9
|
export declare function exportFilenameWithDate(moduleName: string): () => string;
|
|
10
|
-
/**
|
|
11
|
-
* Replacer for JSON.stringify to replace timestamp properties with formatted strings.
|
|
12
|
-
*/
|
|
13
|
-
export declare function timestampReplacer(k: string, v: any): any;
|
package/build/types/core/XH.d.ts
CHANGED
|
@@ -21,6 +21,13 @@ export declare const MIN_HOIST_CORE_VERSION = "28.0";
|
|
|
21
21
|
* Available via import as `XH` - also installed as `window.XH` for troubleshooting purposes.
|
|
22
22
|
*/
|
|
23
23
|
export declare class XHApi {
|
|
24
|
+
/** Unique id for this loaded instance of the app. Unique for every refresh of document. */
|
|
25
|
+
clientId: string;
|
|
26
|
+
/**
|
|
27
|
+
* Unique id for this browser tab/window on this domain.
|
|
28
|
+
* Corresponds to the scope of the built-in sessionStorage object.
|
|
29
|
+
*/
|
|
30
|
+
sessionId: string;
|
|
24
31
|
/** Core implementation model hosting all application state. */
|
|
25
32
|
appContainerModel: AppContainerModel;
|
|
26
33
|
/** Provider of centralized exception handling for the app. */
|
|
@@ -405,6 +412,8 @@ export declare class XHApi {
|
|
|
405
412
|
*/
|
|
406
413
|
genId(): string;
|
|
407
414
|
private get acm();
|
|
415
|
+
private genClientId;
|
|
416
|
+
private genSessionId;
|
|
408
417
|
}
|
|
409
418
|
/** The app-wide singleton instance. */
|
|
410
419
|
export declare const XH: XHApi;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
import { DateLike } from '../core/types/Types';
|
|
2
|
+
import { DateLike, PlainObject } from '../core/types/Types';
|
|
3
3
|
import { FormatOptions } from './FormatMisc';
|
|
4
4
|
export declare const DATE_FMT = "YYYY-MM-DD", DATETIME_FMT = "YYYY-MM-DD h:mma", DATETIMESEC_FMT = "YYYY-MM-DD h:mm:ssa", TIME_FMT = "h:mma", MONTH_DAY_FMT = "MMM D";
|
|
5
5
|
/**
|
|
@@ -49,4 +49,25 @@ export interface CompactDateFormatOptions extends FormatOptions<DateLike> {
|
|
|
49
49
|
* Render dates formatted based on distance in time from current day.
|
|
50
50
|
*/
|
|
51
51
|
export declare function fmtCompactDate(v: DateLike, opts?: CompactDateFormatOptions): ReactNode;
|
|
52
|
+
export interface TimestampReplacerConfig {
|
|
53
|
+
/**
|
|
54
|
+
* Suffixes used to identify keys that may hold timestamps.
|
|
55
|
+
* Defaults to ['time', 'date', 'timestamp']
|
|
56
|
+
*/
|
|
57
|
+
suffixes?: string[];
|
|
58
|
+
/**
|
|
59
|
+
* Format for replaced timestamp.
|
|
60
|
+
* Defaults to 'MMM DD HH:mm:ss.SSS'
|
|
61
|
+
*/
|
|
62
|
+
format?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Replace timestamps in an Object with formatted strings.
|
|
66
|
+
*/
|
|
67
|
+
export declare function withFormattedTimestamps(obj: PlainObject, config?: TimestampReplacerConfig): PlainObject;
|
|
68
|
+
/**
|
|
69
|
+
* Create a replacer, suitable for JSON.stringify, that will replace timestamps with
|
|
70
|
+
* formatted strings.
|
|
71
|
+
*/
|
|
72
|
+
export declare function timestampReplacer(config?: TimestampReplacerConfig): (k: string, v: any) => any;
|
|
52
73
|
export declare const dateRenderer: (obj?: string | DateFormatOptions) => (v: DateLike) => ReactNode, dateTimeRenderer: (obj?: string | DateFormatOptions) => (v: any) => ReactNode, dateTimeSecRenderer: (obj?: string | DateFormatOptions) => (v: DateLike) => ReactNode, timeRenderer: (obj?: string | DateFormatOptions) => (v: DateLike) => ReactNode, compactDateRenderer: (obj?: CompactDateFormatOptions) => (v: DateLike) => ReactNode;
|
|
@@ -19,8 +19,8 @@ export interface TelemetryResults {
|
|
|
19
19
|
}
|
|
20
20
|
/** Aggregated telemetry results for a single type of event. */
|
|
21
21
|
export interface TelemetryEventResults {
|
|
22
|
-
firstTime:
|
|
23
|
-
lastTime:
|
|
22
|
+
firstTime: number;
|
|
23
|
+
lastTime: number;
|
|
24
24
|
successCount: number;
|
|
25
25
|
failureCount: number;
|
|
26
26
|
/** Timing info (in ms) for event instances reported with duration. */
|
|
@@ -31,7 +31,7 @@ export interface TelemetryEventResults {
|
|
|
31
31
|
worst: number;
|
|
32
32
|
};
|
|
33
33
|
lastFailure?: {
|
|
34
|
-
time:
|
|
34
|
+
time: number;
|
|
35
35
|
duration: number;
|
|
36
36
|
code: string;
|
|
37
37
|
name: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as msal from '@azure/msal-browser';
|
|
2
2
|
import { LogLevel } from '@azure/msal-browser';
|
|
3
|
+
import { PlainObject } from '@xh/hoist/core';
|
|
3
4
|
import { Token } from '@xh/hoist/security/Token';
|
|
4
5
|
import { BaseOAuthClient, BaseOAuthClientConfig } from '../BaseOAuthClient';
|
|
5
6
|
import { AccessTokenSpec, TelemetryResults, TokenMap } from '../Types';
|
|
@@ -96,6 +97,7 @@ export declare class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTo
|
|
|
96
97
|
protected fetchIdTokenAsync(useCache?: boolean): Promise<Token>;
|
|
97
98
|
protected fetchAccessTokenAsync(spec: MsalTokenSpec, useCache?: boolean): Promise<Token>;
|
|
98
99
|
protected doLogoutAsync(): Promise<void>;
|
|
100
|
+
getFormattedTelemetry(): PlainObject;
|
|
99
101
|
enableTelemetry(): void;
|
|
100
102
|
disableTelemetry(): void;
|
|
101
103
|
private loginSsoAsync;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HoistService, PlainObject } from '@xh/hoist/core';
|
|
1
|
+
import { HoistService, PageState, PlainObject } from '@xh/hoist/core';
|
|
2
2
|
/**
|
|
3
3
|
* Service for gathering data about client health.
|
|
4
4
|
*
|
|
@@ -11,9 +11,11 @@ export declare class ClientHealthService extends HoistService {
|
|
|
11
11
|
private sources;
|
|
12
12
|
initAsync(): Promise<void>;
|
|
13
13
|
/**
|
|
14
|
-
* Main
|
|
14
|
+
* Main entry point. Return a default report of client health.
|
|
15
15
|
*/
|
|
16
16
|
getReport(): ClientHealthReport;
|
|
17
|
+
/** Get report, suitable for viewing in console. **/
|
|
18
|
+
getFormattedReport(): PlainObject;
|
|
17
19
|
/**
|
|
18
20
|
* Register a new source for client health report data. No-op if background health report is
|
|
19
21
|
* not generally enabled via `xhActivityTrackingConfig.clientHealthReport.intervalMins`.
|
|
@@ -24,17 +26,18 @@ export declare class ClientHealthService extends HoistService {
|
|
|
24
26
|
addSource(key: string, callback: () => any): void;
|
|
25
27
|
/** Unregister a previously-enabled source for client health report data. */
|
|
26
28
|
removeSource(key: string): void;
|
|
27
|
-
|
|
29
|
+
getGeneral(): GeneralData;
|
|
28
30
|
getConnection(): ConnectionData;
|
|
29
31
|
getMemory(): MemoryData;
|
|
30
|
-
getScreen(): ScreenData;
|
|
31
|
-
getWindow(): WindowData;
|
|
32
32
|
getCustom(): PlainObject;
|
|
33
33
|
private sendReport;
|
|
34
34
|
}
|
|
35
|
-
export interface
|
|
35
|
+
export interface GeneralData {
|
|
36
36
|
startTime: number;
|
|
37
37
|
durationMins: number;
|
|
38
|
+
idleMins: number;
|
|
39
|
+
pageState: PageState;
|
|
40
|
+
webSocket: string;
|
|
38
41
|
}
|
|
39
42
|
export interface ConnectionData {
|
|
40
43
|
downlink: number;
|
|
@@ -48,33 +51,8 @@ export interface MemoryData {
|
|
|
48
51
|
totalJSHeapSize?: number;
|
|
49
52
|
usedJSHeapSize?: number;
|
|
50
53
|
}
|
|
51
|
-
export interface WindowData {
|
|
52
|
-
devicePixelRatio: number;
|
|
53
|
-
screenX: number;
|
|
54
|
-
screenY: number;
|
|
55
|
-
innerWidth: number;
|
|
56
|
-
innerHeight: number;
|
|
57
|
-
outerWidth: number;
|
|
58
|
-
outerHeight: number;
|
|
59
|
-
}
|
|
60
|
-
export interface ScreenData {
|
|
61
|
-
availWidth: number;
|
|
62
|
-
availHeight: number;
|
|
63
|
-
width: number;
|
|
64
|
-
height: number;
|
|
65
|
-
colorDepth: number;
|
|
66
|
-
pixelDepth: number;
|
|
67
|
-
availLeft: number;
|
|
68
|
-
availTop: number;
|
|
69
|
-
orientation?: {
|
|
70
|
-
angle: number;
|
|
71
|
-
type: string;
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
54
|
export interface ClientHealthReport {
|
|
75
|
-
|
|
55
|
+
general: GeneralData;
|
|
76
56
|
connection: ConnectionData;
|
|
77
57
|
memory: MemoryData;
|
|
78
|
-
window: WindowData;
|
|
79
|
-
screen: ScreenData;
|
|
80
58
|
}
|
package/core/XH.ts
CHANGED
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
import {installServicesAsync} from './impl/InstallServices';
|
|
66
66
|
import {instanceManager} from './impl/InstanceManager';
|
|
67
67
|
import {HoistModel, ModelSelector, RefreshContextModel} from './model';
|
|
68
|
+
import ShortUniqueId from 'short-unique-id';
|
|
68
69
|
|
|
69
70
|
export const MIN_HOIST_CORE_VERSION = '28.0';
|
|
70
71
|
|
|
@@ -85,6 +86,15 @@ declare const xhIsDevelopmentMode: boolean;
|
|
|
85
86
|
* Available via import as `XH` - also installed as `window.XH` for troubleshooting purposes.
|
|
86
87
|
*/
|
|
87
88
|
export class XHApi {
|
|
89
|
+
/** Unique id for this loaded instance of the app. Unique for every refresh of document. */
|
|
90
|
+
clientId: string = this.genClientId();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Unique id for this browser tab/window on this domain.
|
|
94
|
+
* Corresponds to the scope of the built-in sessionStorage object.
|
|
95
|
+
*/
|
|
96
|
+
sessionId: string = this.genSessionId();
|
|
97
|
+
|
|
88
98
|
//--------------------------
|
|
89
99
|
// Implementation Delegates
|
|
90
100
|
//--------------------------
|
|
@@ -796,6 +806,19 @@ export class XHApi {
|
|
|
796
806
|
private get acm(): AppContainerModel {
|
|
797
807
|
return this.appContainerModel;
|
|
798
808
|
}
|
|
809
|
+
|
|
810
|
+
private genClientId(): string {
|
|
811
|
+
return new ShortUniqueId({length: 8}).rnd();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private genSessionId(): string {
|
|
815
|
+
let ret = window.sessionStorage?.getItem('xhSessionId');
|
|
816
|
+
if (!ret) {
|
|
817
|
+
ret = new ShortUniqueId({length: 8}).rnd();
|
|
818
|
+
window.sessionStorage?.setItem('xhSessionId', ret);
|
|
819
|
+
}
|
|
820
|
+
return ret;
|
|
821
|
+
}
|
|
799
822
|
}
|
|
800
823
|
|
|
801
824
|
/** The app-wide singleton instance. */
|
package/format/FormatDate.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {isLocalDate, LocalDate} from '@xh/hoist/utils/datetime';
|
|
8
|
-
import {defaults, isString} from 'lodash';
|
|
7
|
+
import {DAYS, isLocalDate, LocalDate} from '@xh/hoist/utils/datetime';
|
|
8
|
+
import {defaults, isFinite, isString} from 'lodash';
|
|
9
9
|
import moment from 'moment';
|
|
10
10
|
import {ReactNode} from 'react';
|
|
11
|
-
import {DateLike} from '../core/types/Types';
|
|
11
|
+
import {DateLike, PlainObject} from '../core/types/Types';
|
|
12
12
|
import {fmtSpan, FormatOptions} from './FormatMisc';
|
|
13
13
|
import {createRenderer} from './FormatUtils';
|
|
14
14
|
import {saveOriginal} from './impl/Utils';
|
|
@@ -148,6 +148,48 @@ export function fmtCompactDate(v: DateLike, opts?: CompactDateFormatOptions) {
|
|
|
148
148
|
return fmtDate(v, dateOpts);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
export interface TimestampReplacerConfig {
|
|
152
|
+
/**
|
|
153
|
+
* Suffixes used to identify keys that may hold timestamps.
|
|
154
|
+
* Defaults to ['time', 'date', 'timestamp']
|
|
155
|
+
*/
|
|
156
|
+
suffixes?: string[];
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Format for replaced timestamp.
|
|
160
|
+
* Defaults to 'MMM DD HH:mm:ss.SSS'
|
|
161
|
+
*/
|
|
162
|
+
format?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Replace timestamps in an Object with formatted strings.
|
|
167
|
+
*/
|
|
168
|
+
export function withFormattedTimestamps(
|
|
169
|
+
obj: PlainObject,
|
|
170
|
+
config: TimestampReplacerConfig = {}
|
|
171
|
+
): PlainObject {
|
|
172
|
+
return JSON.parse(JSON.stringify(obj, timestampReplacer(config)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create a replacer, suitable for JSON.stringify, that will replace timestamps with
|
|
177
|
+
* formatted strings.
|
|
178
|
+
*/
|
|
179
|
+
export function timestampReplacer(
|
|
180
|
+
config: TimestampReplacerConfig = {}
|
|
181
|
+
): (k: string, v: any) => any {
|
|
182
|
+
const suffixes = config.suffixes ?? ['time', 'date', 'timestamp'],
|
|
183
|
+
fmt = 'MMM DD HH:mm:ss.SSS';
|
|
184
|
+
return (k: string, v: any) => {
|
|
185
|
+
return suffixes.some(s => k.toLowerCase().endsWith(s.toLowerCase())) &&
|
|
186
|
+
isFinite(v) &&
|
|
187
|
+
v > Date.now() - 25 * 365 * DAYS // heuristic to avoid catching smaller ms ranges
|
|
188
|
+
? fmtDateTime(v, {fmt})
|
|
189
|
+
: v;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
151
193
|
export const dateRenderer = createRenderer(fmtDate),
|
|
152
194
|
dateTimeRenderer = createRenderer(fmtDateTime),
|
|
153
195
|
dateTimeSecRenderer = createRenderer(fmtDateTimeSec),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "73.0.0-SNAPSHOT.
|
|
3
|
+
"version": "73.0.0-SNAPSHOT.1744229935910",
|
|
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
|
@@ -31,8 +31,8 @@ export interface TelemetryResults {
|
|
|
31
31
|
|
|
32
32
|
/** Aggregated telemetry results for a single type of event. */
|
|
33
33
|
export interface TelemetryEventResults {
|
|
34
|
-
firstTime:
|
|
35
|
-
lastTime:
|
|
34
|
+
firstTime: number;
|
|
35
|
+
lastTime: number;
|
|
36
36
|
successCount: number;
|
|
37
37
|
failureCount: number;
|
|
38
38
|
/** Timing info (in ms) for event instances reported with duration. */
|
|
@@ -43,7 +43,7 @@ export interface TelemetryEventResults {
|
|
|
43
43
|
worst: number;
|
|
44
44
|
};
|
|
45
45
|
lastFailure?: {
|
|
46
|
-
time:
|
|
46
|
+
time: number;
|
|
47
47
|
duration: number;
|
|
48
48
|
code: string;
|
|
49
49
|
name: string;
|
|
@@ -14,9 +14,10 @@ import {
|
|
|
14
14
|
PopupRequest,
|
|
15
15
|
SilentRequest
|
|
16
16
|
} from '@azure/msal-browser';
|
|
17
|
-
import {XH} from '@xh/hoist/core';
|
|
17
|
+
import {AppState, PlainObject, XH} from '@xh/hoist/core';
|
|
18
18
|
import {Token} from '@xh/hoist/security/Token';
|
|
19
19
|
import {logDebug, logError, logInfo, logWarn, mergeDeep, throwIf} from '@xh/hoist/utils/js';
|
|
20
|
+
import {withFormattedTimestamps} from '@xh/hoist/format';
|
|
20
21
|
import {flatMap, union, uniq} from 'lodash';
|
|
21
22
|
import {BaseOAuthClient, BaseOAuthClientConfig} from '../BaseOAuthClient';
|
|
22
23
|
import {AccessTokenSpec, TelemetryResults, TokenMap} from '../Types';
|
|
@@ -266,9 +267,13 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
266
267
|
//------------------------
|
|
267
268
|
// Telemetry
|
|
268
269
|
//------------------------
|
|
270
|
+
getFormattedTelemetry(): PlainObject {
|
|
271
|
+
return withFormattedTimestamps(this.telemetryResults);
|
|
272
|
+
}
|
|
273
|
+
|
|
269
274
|
enableTelemetry(): void {
|
|
270
275
|
if (this._telemetryCbHandle) {
|
|
271
|
-
this.logInfo('Telemetry already enabled', this.
|
|
276
|
+
this.logInfo('Telemetry already enabled', this.getFormattedTelemetry());
|
|
272
277
|
return;
|
|
273
278
|
}
|
|
274
279
|
|
|
@@ -279,7 +284,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
279
284
|
try {
|
|
280
285
|
const {events} = this.telemetryResults,
|
|
281
286
|
{name, startTimeMs, durationMs, success, errorName, errorCode} = e,
|
|
282
|
-
eTime = startTimeMs
|
|
287
|
+
eTime = startTimeMs ?? Date.now();
|
|
283
288
|
|
|
284
289
|
const eResult = (events[name] ??= {
|
|
285
290
|
firstTime: eTime,
|
|
@@ -316,10 +321,9 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
316
321
|
});
|
|
317
322
|
});
|
|
318
323
|
|
|
319
|
-
//
|
|
320
|
-
// Handle TrackService not yet initialized (common, this client likely initialized before.)
|
|
324
|
+
// Wait for clientHealthService (this client likely initialized during earlier AUTHENTICATING.)
|
|
321
325
|
this.addReaction({
|
|
322
|
-
when: () => XH.
|
|
326
|
+
when: () => XH.appState === AppState.INITIALIZING_APP,
|
|
323
327
|
run: () => XH.clientHealthService.addSource('msalClient', () => this.telemetryResults)
|
|
324
328
|
});
|
|
325
329
|
|
|
@@ -336,7 +340,7 @@ export class MsalClient extends BaseOAuthClient<MsalClientConfig, MsalTokenSpec>
|
|
|
336
340
|
this._telemetryCbHandle = null;
|
|
337
341
|
|
|
338
342
|
XH.clientHealthService.removeSource('msalClient');
|
|
339
|
-
this.logInfo('Telemetry disabled', this.
|
|
343
|
+
this.logInfo('Telemetry disabled', this.getFormattedTelemetry());
|
|
340
344
|
}
|
|
341
345
|
|
|
342
346
|
//------------------------
|