-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathTrackService.ts
147 lines (125 loc) · 4.84 KB
/
TrackService.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
*
* Copyright © 2025 Extremely Heavy Industries Inc.
*/
import {HoistService, PlainObject, TrackOptions, XH} from '@xh/hoist/core';
import {SECONDS} from '@xh/hoist/utils/datetime';
import {isOmitted} from '@xh/hoist/utils/impl';
import {debounced, stripTags, withDefault} from '@xh/hoist/utils/js';
import {isEmpty, isNil, isString} from 'lodash';
/**
* Primary service for tracking any activity that an application's admins want to track.
* Activities are available for viewing/querying in the Admin Console's Client Activity tab.
* Client metadata is set automatically by the server's parsing of request headers.
*/
export class TrackService extends HoistService {
static instance: TrackService;
private oncePerSessionSent = new Map();
private pending: PlainObject[] = [];
override async initAsync() {
window.addEventListener('beforeunload', () => this.pushPendingAsync());
}
get conf() {
return XH.getConf('xhActivityTrackingConfig', {
enabled: true,
maxDataLength: 2000,
maxRows: {
default: 10000,
options: [1000, 5000, 10000, 25000]
},
logData: false
});
}
get enabled(): boolean {
return this.conf.enabled === true;
}
/** Track User Activity. */
track(options: TrackOptions | string) {
// Normalize string form, msg -> message, default severity.
if (isString(options)) options = {message: options};
if (isOmitted(options)) return;
options = {
message: withDefault(options.message, (options as any).msg),
severity: withDefault(options.severity, 'INFO'),
timestamp: withDefault(options.timestamp, Date.now()),
...options
};
// Short-circuit if disabled...
if (!this.enabled) {
this.logDebug('Activity tracking disabled - activity will not be tracked.', options);
return;
}
// ...or invalid request (with warning for developer)
if (!options.message) {
this.logWarn('Required message not provided - activity will not be tracked.', options);
return;
}
// ...or if auto-refresh
if (options.loadSpec?.isAutoRefresh) return;
// ...or if unauthenticated user
if (!XH.getUsername()) return;
// ...or if already-sent once-per-session messages
if (options.oncePerSession) {
const sent = this.oncePerSessionSent,
key = options.message + '_' + (options.category ?? '');
if (sent.has(key)) return;
sent.set(key, true);
}
// Otherwise - log and for next batch,
this.logMessage(options);
this.pending.push(this.toServerJson(options));
this.pushPendingBuffered();
}
//------------------
// Implementation
//------------------
private async pushPendingAsync() {
const {pending} = this;
if (isEmpty(pending)) return;
this.pending = [];
await XH.fetchService.postJson({
url: 'xh/track',
body: {entries: pending},
params: {clientUsername: XH.getUsername()}
});
}
@debounced(10 * SECONDS)
private pushPendingBuffered() {
this.pushPendingAsync();
}
private toServerJson(options: TrackOptions): PlainObject {
const ret: PlainObject = {
msg: stripTags(options.message),
clientUsername: XH.getUsername(),
appVersion: XH.getEnv('clientVersion'),
url: window.location.href,
timestamp: Date.now()
};
if (options.category) ret.category = options.category;
if (options.correlationId) ret.correlationId = options.correlationId;
if (options.data) ret.data = options.data;
if (options.severity) ret.severity = options.severity;
if (options.logData !== undefined) ret.logData = options.logData;
if (options.elapsed !== undefined) ret.elapsed = options.elapsed;
const {maxDataLength} = this.conf;
if (ret.data?.length > maxDataLength) {
this.logWarn(
`Track log includes ${ret.data.length} chars of JSON data`,
`exceeds limit of ${maxDataLength}`,
'data will not be persisted',
options.data
);
ret.data = null;
}
return ret;
}
private logMessage(opts: TrackOptions) {
const elapsedStr = opts.elapsed != null ? `${opts.elapsed}ms` : null,
consoleMsgs = [opts.category, opts.message, opts.correlationId, elapsedStr].filter(
it => !isNil(it)
);
this.logInfo(...consoleMsgs);
}
}