@webex/internal-plugin-metrics 3.12.0-next.5 → 3.12.0-next.50
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/README.md +274 -0
- package/dist/automated-user.js +19 -0
- package/dist/automated-user.js.map +1 -0
- package/dist/batcher.js +3 -0
- package/dist/batcher.js.map +1 -1
- package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +635 -62
- package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
- package/dist/call-diagnostic/call-diagnostic-metrics.js +188 -78
- package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
- package/dist/call-diagnostic/call-diagnostic-metrics.util.js +5 -0
- package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -1
- package/dist/call-diagnostic/config.js +16 -3
- package/dist/call-diagnostic/config.js.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics.js +118 -5
- package/dist/metrics.js.map +1 -1
- package/dist/metrics.types.js +4 -0
- package/dist/metrics.types.js.map +1 -1
- package/dist/network-telemetry.js +610 -0
- package/dist/network-telemetry.js.map +1 -0
- package/dist/new-metrics.js +63 -17
- package/dist/new-metrics.js.map +1 -1
- package/dist/privacy-and-security-permission-enricher/constants.js +18 -0
- package/dist/privacy-and-security-permission-enricher/constants.js.map +1 -0
- package/dist/privacy-and-security-permission-enricher/index.js +143 -0
- package/dist/privacy-and-security-permission-enricher/index.js.map +1 -0
- package/dist/privacy-and-security-permission-enricher/types.js +7 -0
- package/dist/privacy-and-security-permission-enricher/types.js.map +1 -0
- package/dist/privacy-and-security-permission-enricher/utils.js +71 -0
- package/dist/privacy-and-security-permission-enricher/utils.js.map +1 -0
- package/dist/types/automated-user.d.ts +2 -0
- package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +208 -5
- package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +73 -14
- package/dist/types/call-diagnostic/config.d.ts +4 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/metrics.types.d.ts +7 -2
- package/dist/types/network-telemetry.d.ts +108 -0
- package/dist/types/new-metrics.d.ts +17 -1
- package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
- package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
- package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
- package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
- package/dist/types/unhandled-exception-telemetry/index.d.ts +60 -0
- package/dist/types/unhandled-exception-telemetry/utils.d.ts +6 -0
- package/dist/unhandled-exception-telemetry/index.js +330 -0
- package/dist/unhandled-exception-telemetry/index.js.map +1 -0
- package/dist/unhandled-exception-telemetry/utils.js +105 -0
- package/dist/unhandled-exception-telemetry/utils.js.map +1 -0
- package/package.json +12 -11
- package/src/automated-user.ts +16 -0
- package/src/batcher.js +4 -0
- package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +781 -71
- package/src/call-diagnostic/call-diagnostic-metrics.ts +119 -16
- package/src/call-diagnostic/call-diagnostic-metrics.util.ts +5 -0
- package/src/call-diagnostic/config.ts +14 -0
- package/src/config.js +8 -0
- package/src/index.ts +16 -0
- package/src/metrics.js +123 -5
- package/src/metrics.types.ts +25 -3
- package/src/network-telemetry.ts +757 -0
- package/src/new-metrics.ts +56 -6
- package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
- package/src/privacy-and-security-permission-enricher/index.ts +142 -0
- package/src/privacy-and-security-permission-enricher/types.ts +21 -0
- package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
- package/src/unhandled-exception-telemetry/index.ts +403 -0
- package/src/unhandled-exception-telemetry/utils.ts +101 -0
- package/test/unit/spec/automated-user.ts +43 -0
- package/test/unit/spec/batcher.js +43 -0
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +14 -2
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +1424 -293
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +906 -159
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +27 -0
- package/test/unit/spec/metrics.js +14 -5
- package/test/unit/spec/network-telemetry.ts +610 -0
- package/test/unit/spec/new-metrics.ts +203 -50
- package/test/unit/spec/prelogin-metrics-batcher.ts +71 -36
- package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
- package/test/unit/spec/unhandled-exception-telemetry/utils.ts +109 -0
- package/test/unit/spec/unhandled-exception-telemetry.ts +688 -0
|
@@ -460,6 +460,33 @@ describe('internal-plugin-metrics', () => {
|
|
|
460
460
|
});
|
|
461
461
|
});
|
|
462
462
|
|
|
463
|
+
it('logs a Milestone entry when event name is present', () => {
|
|
464
|
+
const logSpy = sinon.spy(webex.logger, 'log');
|
|
465
|
+
const eventName = 'client.call.initiated';
|
|
466
|
+
const eventPayload = {event: {name: eventName}};
|
|
467
|
+
|
|
468
|
+
prepareDiagnosticMetricItem(webex, {eventPayload, type: ['diagnostic-event']});
|
|
469
|
+
|
|
470
|
+
assert.isTrue(
|
|
471
|
+
logSpy.calledWith('Milestone,CallDiagnostic', eventName),
|
|
472
|
+
'expected logger.log to be called with Milestone,CallDiagnostic and the event name'
|
|
473
|
+
);
|
|
474
|
+
sinon.restore();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('does not log a Milestone entry when event name is absent', () => {
|
|
478
|
+
const logSpy = sinon.spy(webex.logger, 'log');
|
|
479
|
+
const eventPayload = {event: {}};
|
|
480
|
+
|
|
481
|
+
prepareDiagnosticMetricItem(webex, {eventPayload, type: ['diagnostic-event']});
|
|
482
|
+
|
|
483
|
+
assert.isFalse(
|
|
484
|
+
logSpy.calledWith('Milestone,CallDiagnostic', sinon.match.any),
|
|
485
|
+
'expected logger.log not to be called with Milestone,CallDiagnostic when event name is absent'
|
|
486
|
+
);
|
|
487
|
+
sinon.restore();
|
|
488
|
+
});
|
|
489
|
+
|
|
463
490
|
it('sets buildType and upgradeChannel correctly', () => {
|
|
464
491
|
const item: any = {
|
|
465
492
|
eventPayload: {
|
|
@@ -13,7 +13,7 @@ import {BrowserDetection} from '@webex/common';
|
|
|
13
13
|
const {getOSVersion} = BrowserDetection();
|
|
14
14
|
|
|
15
15
|
//@ts-ignore
|
|
16
|
-
global.window = {location: {hostname: 'whatever'}};
|
|
16
|
+
global.window = {location: {hostname: 'whatever', origin: 'https://whatever'}};
|
|
17
17
|
function promiseTick(count) {
|
|
18
18
|
let promise = Promise.resolve();
|
|
19
19
|
|
|
@@ -30,6 +30,7 @@ describe('plugin-metrics', () => {
|
|
|
30
30
|
let webex;
|
|
31
31
|
let metrics;
|
|
32
32
|
let clock;
|
|
33
|
+
let previousWindow;
|
|
33
34
|
|
|
34
35
|
const eventName = 'test_event';
|
|
35
36
|
const mockPayload = {
|
|
@@ -67,10 +68,15 @@ describe('plugin-metrics', () => {
|
|
|
67
68
|
|
|
68
69
|
beforeEach(() => {
|
|
69
70
|
clock = FakeTimers.install({now: 0});
|
|
71
|
+
previousWindow = global.window;
|
|
72
|
+
// Other test files also replace the shared browser mock. Restore the
|
|
73
|
+
// location needed to verify that app_url prefers the browser origin.
|
|
74
|
+
global.window = {location: {hostname: 'whatever', origin: 'https://whatever'}};
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
afterEach(() => {
|
|
73
78
|
clock.uninstall();
|
|
79
|
+
global.window = previousWindow;
|
|
74
80
|
});
|
|
75
81
|
|
|
76
82
|
beforeEach(() => {
|
|
@@ -107,10 +113,10 @@ describe('plugin-metrics', () => {
|
|
|
107
113
|
webex.meetings = {
|
|
108
114
|
config: {
|
|
109
115
|
metrics: {
|
|
110
|
-
clientVersion: '43.0.105'
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
116
|
+
clientVersion: '43.0.105',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
114
120
|
|
|
115
121
|
sinon.spy(webex, 'request');
|
|
116
122
|
sinon.spy(metrics, 'aliasUser');
|
|
@@ -186,6 +192,9 @@ describe('plugin-metrics', () => {
|
|
|
186
192
|
},
|
|
187
193
|
metricName: 'test',
|
|
188
194
|
tags: {
|
|
195
|
+
app_name: 'appName',
|
|
196
|
+
app_url: 'https://whatever',
|
|
197
|
+
app_version: 'appVersion',
|
|
189
198
|
appVersion: '43.0.105',
|
|
190
199
|
browser: '',
|
|
191
200
|
domain: 'whatever',
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import {assert} from '@webex/test-helper-chai';
|
|
2
|
+
import MockWebex from '@webex/test-helper-mock-webex';
|
|
3
|
+
import sinon from 'sinon';
|
|
4
|
+
|
|
5
|
+
import config from '../../../src/config';
|
|
6
|
+
import Metrics from '../../../src/metrics';
|
|
7
|
+
import {
|
|
8
|
+
createNetworkErrorMetric,
|
|
9
|
+
getOperation,
|
|
10
|
+
NETWORK_REQUEST_SUMMARY_METRIC,
|
|
11
|
+
NETWORK_TELEMETRY_INTERVAL_MS,
|
|
12
|
+
shouldTrackNetworkRequest,
|
|
13
|
+
} from '../../../src/network-telemetry';
|
|
14
|
+
|
|
15
|
+
describe('network telemetry', function () {
|
|
16
|
+
let clock: sinon.SinonFakeTimers;
|
|
17
|
+
|
|
18
|
+
function makeWebex(
|
|
19
|
+
networkTelemetryEnabled = true,
|
|
20
|
+
networkTelemetryIntervalMs = config.metrics.networkTelemetry.intervalMs
|
|
21
|
+
) {
|
|
22
|
+
const warn = sinon.stub();
|
|
23
|
+
const webex = new MockWebex({
|
|
24
|
+
children: {
|
|
25
|
+
metrics: Metrics,
|
|
26
|
+
},
|
|
27
|
+
logger: {warn},
|
|
28
|
+
config: {
|
|
29
|
+
metrics: {
|
|
30
|
+
networkTelemetry: {
|
|
31
|
+
enabled: networkTelemetryEnabled,
|
|
32
|
+
intervalMs: networkTelemetryIntervalMs,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
webex.config.metrics = {
|
|
39
|
+
...config.metrics,
|
|
40
|
+
networkTelemetry: {
|
|
41
|
+
...config.metrics.networkTelemetry,
|
|
42
|
+
enabled: networkTelemetryEnabled,
|
|
43
|
+
intervalMs: networkTelemetryIntervalMs,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {warn, webex};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeTrackingId(_value: unknown, index: number) {
|
|
51
|
+
return `tracking-${index}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
beforeEach(function () {
|
|
55
|
+
clock = sinon.useFakeTimers();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(function () {
|
|
59
|
+
clock.restore();
|
|
60
|
+
sinon.restore();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('submits the requested ten-minute aggregate shape', function () {
|
|
64
|
+
const {webex} = makeWebex();
|
|
65
|
+
const submitClientMetrics = sinon
|
|
66
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
67
|
+
.resolves();
|
|
68
|
+
const successfulOptions = {
|
|
69
|
+
headers: {trackingid: 'tracking-success'},
|
|
70
|
+
method: 'get',
|
|
71
|
+
resource: 'rooms',
|
|
72
|
+
service: 'Hydra',
|
|
73
|
+
};
|
|
74
|
+
const failedResponseOptions = {
|
|
75
|
+
headers: {trackingid: 'tracking-response'},
|
|
76
|
+
method: 'post',
|
|
77
|
+
resource: 'rooms/Y2lzY29zcGFyazovL3VzL1JPT00/messages?personId=secret',
|
|
78
|
+
service: 'Hydra',
|
|
79
|
+
};
|
|
80
|
+
const failedRequestOptions = {
|
|
81
|
+
headers: {trackingid: 'tracking-request'},
|
|
82
|
+
method: 'get',
|
|
83
|
+
resource: 'people/12345',
|
|
84
|
+
service: 'Identity',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
webex.emit('request:start', successfulOptions);
|
|
88
|
+
webex.emit('request:success', successfulOptions, {statusCode: 200});
|
|
89
|
+
webex.emit('request:start', failedResponseOptions);
|
|
90
|
+
webex.emit('request:failure', failedResponseOptions, {
|
|
91
|
+
body: {errorCode: 'SERVICE_UNAVAILABLE', message: 'Service unavailable'},
|
|
92
|
+
name: 'ServiceUnavailable',
|
|
93
|
+
statusCode: 503,
|
|
94
|
+
});
|
|
95
|
+
webex.emit('request:start', failedRequestOptions);
|
|
96
|
+
webex.emit('request:failure', failedRequestOptions, {
|
|
97
|
+
body: {code: 'ETIMEDOUT', message: 'Socket timed out'},
|
|
98
|
+
name: 'NetworkOrCORSError',
|
|
99
|
+
statusCode: 0,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
webex.emit('request:start', {service: 'metrics', resource: 'clientmetrics'});
|
|
103
|
+
webex.emit('request:success', {service: 'metrics', resource: 'clientmetrics'});
|
|
104
|
+
webex.emit('request:failure', {service: 'unifiedTelemetry'}, {statusCode: 503});
|
|
105
|
+
|
|
106
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS - 1);
|
|
107
|
+
assert.notCalled(submitClientMetrics);
|
|
108
|
+
|
|
109
|
+
clock.tick(1);
|
|
110
|
+
|
|
111
|
+
assert.calledOnceWithExactly(submitClientMetrics, NETWORK_REQUEST_SUMMARY_METRIC, {
|
|
112
|
+
type: 'operational',
|
|
113
|
+
tags: {},
|
|
114
|
+
fields: {
|
|
115
|
+
totalSendRequest: 3,
|
|
116
|
+
totalFailedRequest: 1,
|
|
117
|
+
totalRecvdResponse: 2,
|
|
118
|
+
totalFailedResponse: 1,
|
|
119
|
+
},
|
|
120
|
+
eventPayload: {
|
|
121
|
+
metricsSummary: {
|
|
122
|
+
totalSendRequest: 3,
|
|
123
|
+
totalFailedRequest: 1,
|
|
124
|
+
totalRecvdResponse: 2,
|
|
125
|
+
totalFailedResponse: 1,
|
|
126
|
+
},
|
|
127
|
+
metrics: [
|
|
128
|
+
{
|
|
129
|
+
host: 'hydra',
|
|
130
|
+
endPoint: 'rooms',
|
|
131
|
+
countSendRequest: 1,
|
|
132
|
+
countFailedRequest: 0,
|
|
133
|
+
countRecvdResponse: 1,
|
|
134
|
+
countFailedResponse: 0,
|
|
135
|
+
averageNetworkDurationMs: 0,
|
|
136
|
+
maxNetworkDurationP90Ms: 0,
|
|
137
|
+
maxNetworkDurationP99Ms: 0,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
host: 'hydra',
|
|
141
|
+
endPoint: 'rooms/:id/messages',
|
|
142
|
+
countSendRequest: 1,
|
|
143
|
+
countFailedRequest: 0,
|
|
144
|
+
countRecvdResponse: 1,
|
|
145
|
+
countFailedResponse: 1,
|
|
146
|
+
averageNetworkDurationMs: 0,
|
|
147
|
+
maxNetworkDurationP90Ms: 0,
|
|
148
|
+
maxNetworkDurationP99Ms: 0,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
host: 'identity',
|
|
152
|
+
endPoint: 'people/:id',
|
|
153
|
+
countSendRequest: 1,
|
|
154
|
+
countFailedRequest: 1,
|
|
155
|
+
countRecvdResponse: 0,
|
|
156
|
+
countFailedResponse: 0,
|
|
157
|
+
averageNetworkDurationMs: 0,
|
|
158
|
+
maxNetworkDurationP90Ms: 0,
|
|
159
|
+
maxNetworkDurationP99Ms: 0,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
errorMetrics: [
|
|
163
|
+
{
|
|
164
|
+
host: 'hydra',
|
|
165
|
+
endPoint: 'rooms/:id/messages',
|
|
166
|
+
statusCode: 503,
|
|
167
|
+
errorCode: 'SERVICE_UNAVAILABLE',
|
|
168
|
+
method: 'POST',
|
|
169
|
+
errorType: 'server_error',
|
|
170
|
+
errorMessage: 'Service unavailable',
|
|
171
|
+
trackingIds: ['tracking-response'],
|
|
172
|
+
countError: 1,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
host: 'identity',
|
|
176
|
+
endPoint: 'people/:id',
|
|
177
|
+
statusCode: 0,
|
|
178
|
+
errorCode: 'ETIMEDOUT',
|
|
179
|
+
method: 'GET',
|
|
180
|
+
errorType: 'timeout',
|
|
181
|
+
errorMessage: 'Socket timed out',
|
|
182
|
+
trackingIds: ['tracking-request'],
|
|
183
|
+
countError: 1,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
errorMetricsSummary: {
|
|
187
|
+
0: {count: 1},
|
|
188
|
+
503: {count: 1},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('aggregates identical errors and caps unique tracking IDs at ten', function () {
|
|
195
|
+
const {webex} = makeWebex();
|
|
196
|
+
const submitClientMetrics = sinon
|
|
197
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
198
|
+
.resolves();
|
|
199
|
+
|
|
200
|
+
Array.from({length: 12}, makeTrackingId).forEach(function emitFailure(trackingid) {
|
|
201
|
+
webex.emit(
|
|
202
|
+
'request:failure',
|
|
203
|
+
{
|
|
204
|
+
headers: {trackingid},
|
|
205
|
+
method: 'get',
|
|
206
|
+
resource: 'rooms',
|
|
207
|
+
service: 'hydra',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
body: {errorCode: 'NOT_FOUND', message: 'Room not found'},
|
|
211
|
+
name: 'NotFound',
|
|
212
|
+
statusCode: 404,
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS);
|
|
218
|
+
|
|
219
|
+
const telemetry = submitClientMetrics.firstCall.args[1].eventPayload;
|
|
220
|
+
|
|
221
|
+
assert.deepEqual(telemetry.metricsSummary, {
|
|
222
|
+
totalSendRequest: 0,
|
|
223
|
+
totalFailedRequest: 0,
|
|
224
|
+
totalRecvdResponse: 12,
|
|
225
|
+
totalFailedResponse: 12,
|
|
226
|
+
});
|
|
227
|
+
assert.deepEqual(telemetry.metrics, [
|
|
228
|
+
{
|
|
229
|
+
host: 'hydra',
|
|
230
|
+
endPoint: 'rooms',
|
|
231
|
+
countSendRequest: 0,
|
|
232
|
+
countFailedRequest: 0,
|
|
233
|
+
countRecvdResponse: 12,
|
|
234
|
+
countFailedResponse: 12,
|
|
235
|
+
averageNetworkDurationMs: 0,
|
|
236
|
+
maxNetworkDurationP90Ms: 0,
|
|
237
|
+
maxNetworkDurationP99Ms: 0,
|
|
238
|
+
},
|
|
239
|
+
]);
|
|
240
|
+
assert.equal(telemetry.errorMetrics[0].countError, 12);
|
|
241
|
+
assert.deepEqual(
|
|
242
|
+
telemetry.errorMetrics[0].trackingIds,
|
|
243
|
+
Array.from({length: 10}, makeTrackingId)
|
|
244
|
+
);
|
|
245
|
+
assert.deepEqual(telemetry.errorMetricsSummary, {404: {count: 12}});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('resets all aggregate sections for the next ten-minute window', function () {
|
|
249
|
+
const {webex} = makeWebex();
|
|
250
|
+
const submitClientMetrics = sinon
|
|
251
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
252
|
+
.resolves();
|
|
253
|
+
|
|
254
|
+
webex.emit('request:start', {service: 'hydra', resource: 'rooms'});
|
|
255
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS);
|
|
256
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS);
|
|
257
|
+
|
|
258
|
+
assert.calledTwice(submitClientMetrics);
|
|
259
|
+
assert.deepEqual(submitClientMetrics.secondCall.args, [
|
|
260
|
+
NETWORK_REQUEST_SUMMARY_METRIC,
|
|
261
|
+
{
|
|
262
|
+
type: 'operational',
|
|
263
|
+
tags: {},
|
|
264
|
+
fields: {
|
|
265
|
+
totalSendRequest: 0,
|
|
266
|
+
totalFailedRequest: 0,
|
|
267
|
+
totalRecvdResponse: 0,
|
|
268
|
+
totalFailedResponse: 0,
|
|
269
|
+
},
|
|
270
|
+
eventPayload: {
|
|
271
|
+
metricsSummary: {
|
|
272
|
+
totalSendRequest: 0,
|
|
273
|
+
totalFailedRequest: 0,
|
|
274
|
+
totalRecvdResponse: 0,
|
|
275
|
+
totalFailedResponse: 0,
|
|
276
|
+
},
|
|
277
|
+
metrics: [],
|
|
278
|
+
errorMetrics: [],
|
|
279
|
+
errorMetricsSummary: {},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('uses the configured interval for periodic submission', function () {
|
|
286
|
+
const {webex} = makeWebex(true, 1_000);
|
|
287
|
+
const submitClientMetrics = sinon
|
|
288
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
289
|
+
.resolves();
|
|
290
|
+
|
|
291
|
+
clock.tick(999);
|
|
292
|
+
assert.notCalled(submitClientMetrics);
|
|
293
|
+
|
|
294
|
+
clock.tick(1);
|
|
295
|
+
|
|
296
|
+
assert.calledOnce(submitClientMetrics);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('flushes the current summary immediately and keeps collecting', async function () {
|
|
300
|
+
const {webex} = makeWebex();
|
|
301
|
+
const submitClientMetrics = sinon
|
|
302
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
303
|
+
.resolves();
|
|
304
|
+
|
|
305
|
+
webex.emit('request:start', {service: 'hydra', resource: 'rooms'});
|
|
306
|
+
|
|
307
|
+
await webex.internal.metrics.flushNetworkTelemetry();
|
|
308
|
+
|
|
309
|
+
assert.calledOnce(submitClientMetrics);
|
|
310
|
+
|
|
311
|
+
webex.emit('request:start', {service: 'identity', resource: 'people'});
|
|
312
|
+
await webex.internal.metrics.flushNetworkTelemetry();
|
|
313
|
+
|
|
314
|
+
assert.calledTwice(submitClientMetrics);
|
|
315
|
+
assert.equal(submitClientMetrics.secondCall.args[1].eventPayload.metrics[0].host, 'identity');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('flushes the current summary and stops collecting during cleanup', async function () {
|
|
319
|
+
const {webex} = makeWebex();
|
|
320
|
+
const submitClientMetrics = sinon
|
|
321
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
322
|
+
.resolves();
|
|
323
|
+
|
|
324
|
+
webex.emit('request:start', {service: 'hydra', resource: 'rooms'});
|
|
325
|
+
await webex.internal.metrics.stopNetworkTelemetry();
|
|
326
|
+
webex.emit('request:start', {service: 'identity', resource: 'people'});
|
|
327
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS * 2);
|
|
328
|
+
|
|
329
|
+
assert.calledOnce(submitClientMetrics);
|
|
330
|
+
assert.deepEqual(submitClientMetrics.firstCall.args[1].eventPayload.metrics, [
|
|
331
|
+
{
|
|
332
|
+
host: 'hydra',
|
|
333
|
+
endPoint: 'rooms',
|
|
334
|
+
countSendRequest: 1,
|
|
335
|
+
countFailedRequest: 0,
|
|
336
|
+
countRecvdResponse: 0,
|
|
337
|
+
countFailedResponse: 0,
|
|
338
|
+
averageNetworkDurationMs: 0,
|
|
339
|
+
maxNetworkDurationP90Ms: 0,
|
|
340
|
+
maxNetworkDurationP99Ms: 0,
|
|
341
|
+
},
|
|
342
|
+
]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('waits for a periodic submission that is already in flight before stopping', async function () {
|
|
346
|
+
const {webex} = makeWebex();
|
|
347
|
+
let resolveSubmission: () => void = () => {};
|
|
348
|
+
const pendingSubmission = new Promise<void>((resolve) => {
|
|
349
|
+
resolveSubmission = resolve;
|
|
350
|
+
});
|
|
351
|
+
const submitClientMetrics = sinon
|
|
352
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
353
|
+
.returns(pendingSubmission);
|
|
354
|
+
|
|
355
|
+
webex.emit('request:start', {service: 'hydra', resource: 'rooms'});
|
|
356
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS);
|
|
357
|
+
|
|
358
|
+
const stopPromise = webex.internal.metrics.stopNetworkTelemetry();
|
|
359
|
+
let stopSettled = false;
|
|
360
|
+
void stopPromise.then(() => {
|
|
361
|
+
stopSettled = true;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await Promise.resolve();
|
|
365
|
+
assert.isFalse(stopSettled);
|
|
366
|
+
|
|
367
|
+
resolveSubmission();
|
|
368
|
+
await stopPromise;
|
|
369
|
+
|
|
370
|
+
assert.isTrue(stopSettled);
|
|
371
|
+
assert.calledOnce(submitClientMetrics);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('waits for a manual flush that is already in flight before stopping', async function () {
|
|
375
|
+
const {webex} = makeWebex();
|
|
376
|
+
let resolveSubmission: () => void = () => {};
|
|
377
|
+
const pendingSubmission = new Promise<void>((resolve) => {
|
|
378
|
+
resolveSubmission = resolve;
|
|
379
|
+
});
|
|
380
|
+
const submitClientMetrics = sinon
|
|
381
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
382
|
+
.returns(pendingSubmission);
|
|
383
|
+
|
|
384
|
+
webex.emit('request:start', {service: 'hydra', resource: 'rooms'});
|
|
385
|
+
const flushPromise = webex.internal.metrics.flushNetworkTelemetry();
|
|
386
|
+
const stopPromise = webex.internal.metrics.stopNetworkTelemetry();
|
|
387
|
+
let stopSettled = false;
|
|
388
|
+
void stopPromise.then(() => {
|
|
389
|
+
stopSettled = true;
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await Promise.resolve();
|
|
393
|
+
assert.isFalse(stopSettled);
|
|
394
|
+
|
|
395
|
+
resolveSubmission();
|
|
396
|
+
await Promise.all([flushPromise, stopPromise]);
|
|
397
|
+
|
|
398
|
+
assert.isTrue(stopSettled);
|
|
399
|
+
assert.calledOnce(submitClientMetrics);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('waits for all overlapping submissions before stopping', async function () {
|
|
403
|
+
const {webex} = makeWebex();
|
|
404
|
+
const resolveSubmissions: Array<() => void> = [];
|
|
405
|
+
const submitClientMetrics = sinon
|
|
406
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
407
|
+
.callsFake(
|
|
408
|
+
() =>
|
|
409
|
+
new Promise<void>((resolve) => {
|
|
410
|
+
resolveSubmissions.push(resolve);
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
webex.emit('request:start', {service: 'hydra', resource: 'rooms'});
|
|
415
|
+
const firstFlushPromise = webex.internal.metrics.flushNetworkTelemetry();
|
|
416
|
+
webex.emit('request:start', {service: 'identity', resource: 'people'});
|
|
417
|
+
const secondFlushPromise = webex.internal.metrics.flushNetworkTelemetry();
|
|
418
|
+
const stopPromise = webex.internal.metrics.stopNetworkTelemetry();
|
|
419
|
+
let stopSettled = false;
|
|
420
|
+
void stopPromise.then(() => {
|
|
421
|
+
stopSettled = true;
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await Promise.resolve();
|
|
425
|
+
assert.equal(submitClientMetrics.callCount, 2);
|
|
426
|
+
assert.isFalse(stopSettled);
|
|
427
|
+
|
|
428
|
+
resolveSubmissions[0]();
|
|
429
|
+
await Promise.resolve();
|
|
430
|
+
assert.isFalse(stopSettled);
|
|
431
|
+
|
|
432
|
+
resolveSubmissions[1]();
|
|
433
|
+
await Promise.all([firstFlushPromise, secondFlushPromise, stopPromise]);
|
|
434
|
+
|
|
435
|
+
assert.isTrue(stopSettled);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('does not initialize network telemetry unless it is enabled', function () {
|
|
439
|
+
const {webex} = makeWebex(false);
|
|
440
|
+
const submitClientMetrics = sinon.stub(webex.internal.metrics, 'submitClientMetrics');
|
|
441
|
+
|
|
442
|
+
webex.emit('request:start', {service: 'hydra', resource: 'rooms'});
|
|
443
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS);
|
|
444
|
+
|
|
445
|
+
assert.isFalse(config.metrics.networkTelemetry.enabled);
|
|
446
|
+
assert.notCalled(submitClientMetrics);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('reports average, p90, and p99 network duration for completed requests', function () {
|
|
450
|
+
const {webex} = makeWebex();
|
|
451
|
+
const submitClientMetrics = sinon
|
|
452
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
453
|
+
.resolves();
|
|
454
|
+
Array.from({length: 10}, function emitRequest(_, index) {
|
|
455
|
+
const options = {
|
|
456
|
+
service: 'hydra',
|
|
457
|
+
resource: 'rooms',
|
|
458
|
+
$timings: {networkStart: 0, networkEnd: (index + 1) * 100},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
webex.emit('request:start', options);
|
|
462
|
+
webex.emit('request:success', options, {statusCode: 200});
|
|
463
|
+
});
|
|
464
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS);
|
|
465
|
+
|
|
466
|
+
assert.deepEqual(submitClientMetrics.firstCall.args[1].eventPayload.metrics, [
|
|
467
|
+
{
|
|
468
|
+
host: 'hydra',
|
|
469
|
+
endPoint: 'rooms',
|
|
470
|
+
countSendRequest: 10,
|
|
471
|
+
countFailedRequest: 0,
|
|
472
|
+
countRecvdResponse: 10,
|
|
473
|
+
countFailedResponse: 0,
|
|
474
|
+
averageNetworkDurationMs: 550,
|
|
475
|
+
maxNetworkDurationP90Ms: 900,
|
|
476
|
+
maxNetworkDurationP99Ms: 1000,
|
|
477
|
+
},
|
|
478
|
+
]);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('contains asynchronous summary submission failures', async function () {
|
|
482
|
+
const {warn, webex} = makeWebex();
|
|
483
|
+
|
|
484
|
+
sinon
|
|
485
|
+
.stub(webex.internal.metrics, 'submitClientMetrics')
|
|
486
|
+
.rejects(new Error('metrics unavailable'));
|
|
487
|
+
|
|
488
|
+
clock.tick(NETWORK_TELEMETRY_INTERVAL_MS);
|
|
489
|
+
await Promise.resolve();
|
|
490
|
+
|
|
491
|
+
assert.calledOnceWithExactly(
|
|
492
|
+
warn,
|
|
493
|
+
'metrics: failed to submit network request summary telemetry'
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
[
|
|
498
|
+
{
|
|
499
|
+
name: 'the metrics service',
|
|
500
|
+
options: {service: 'metrics'},
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
name: 'the legacy metrics API option',
|
|
504
|
+
options: {api: 'metrics'},
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: 'the unified telemetry service',
|
|
508
|
+
options: {service: 'unifiedTelemetry'},
|
|
509
|
+
},
|
|
510
|
+
].forEach(function addExclusionTest({name, options}) {
|
|
511
|
+
it(`does not track ${name}`, function () {
|
|
512
|
+
assert.isFalse(shouldTrackNetworkRequest(options));
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
[
|
|
517
|
+
{
|
|
518
|
+
description: 'AbortError as aborted',
|
|
519
|
+
reason: {name: 'AbortError'},
|
|
520
|
+
errorType: 'aborted',
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
description: 'HTTP 408 as timeout',
|
|
524
|
+
reason: {name: 'RequestTimeout', statusCode: 408},
|
|
525
|
+
errorType: 'timeout',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
description: 'a wrapped ETIMEDOUT transport error as timeout',
|
|
529
|
+
reason: {
|
|
530
|
+
name: 'NetworkOrCORSError',
|
|
531
|
+
statusCode: 0,
|
|
532
|
+
body: {code: 'ETIMEDOUT'},
|
|
533
|
+
},
|
|
534
|
+
errorType: 'timeout',
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
description: 'elapsed request timeout metadata as timeout',
|
|
538
|
+
options: {
|
|
539
|
+
service: 'hydra',
|
|
540
|
+
timeout: 300,
|
|
541
|
+
$timings: {networkStart: 100, networkEnd: 400},
|
|
542
|
+
},
|
|
543
|
+
reason: {name: 'NetworkOrCORSError', statusCode: 0},
|
|
544
|
+
errorType: 'timeout',
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
description: 'HTTP 429 as rate_limited',
|
|
548
|
+
reason: {statusCode: 429},
|
|
549
|
+
errorType: 'rate_limited',
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
description: 'HTTP 404 as client_error',
|
|
553
|
+
reason: {statusCode: 404},
|
|
554
|
+
errorType: 'client_error',
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
description: 'HTTP 502 as server_error',
|
|
558
|
+
reason: {statusCode: 502},
|
|
559
|
+
errorType: 'server_error',
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
description: 'a status-zero transport failure as network_error',
|
|
563
|
+
reason: {name: 'NetworkOrCORSError', statusCode: 0},
|
|
564
|
+
errorType: 'network_error',
|
|
565
|
+
},
|
|
566
|
+
].forEach(function addErrorTypeTest({
|
|
567
|
+
description,
|
|
568
|
+
options = {service: 'hydra'},
|
|
569
|
+
reason,
|
|
570
|
+
errorType,
|
|
571
|
+
}) {
|
|
572
|
+
it(`classifies ${description}`, function () {
|
|
573
|
+
const errorMetric = createNetworkErrorMetric(options, reason);
|
|
574
|
+
|
|
575
|
+
assert.equal(errorMetric.errorType, errorType);
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('uses a sanitized URI host and endpoint for direct-URI requests', function () {
|
|
580
|
+
const errorMetric = createNetworkErrorMetric(
|
|
581
|
+
{
|
|
582
|
+
method: 'CUSTOM',
|
|
583
|
+
uri: 'https://user:password@customer.example.test/v1/rooms/65f81b6e-19dc-4a99-9175-59e9b34a5d42?secret=true#private',
|
|
584
|
+
},
|
|
585
|
+
{}
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
assert.equal(errorMetric.host, 'customer.example.test');
|
|
589
|
+
assert.equal(errorMetric.endPoint, 'v1/rooms/:id');
|
|
590
|
+
assert.equal(errorMetric.method, 'CUSTOM');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('uses a string response body as the error message', function () {
|
|
594
|
+
const errorMetric = createNetworkErrorMetric(
|
|
595
|
+
{service: 'hydra'},
|
|
596
|
+
{body: 'The service is unavailable'}
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
assert.equal(errorMetric.errorMessage, 'The service is unavailable');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('redacts identifiers and query parameters from resource endpoints', function () {
|
|
603
|
+
assert.equal(
|
|
604
|
+
getOperation('v1/rooms/65f81b6e-19dc-4a99-9175-59e9b34a5d42/messages?personId=secret'),
|
|
605
|
+
'v1/rooms/:id/messages'
|
|
606
|
+
);
|
|
607
|
+
assert.equal(getOperation('search/orgid/12345/entities'), 'search/orgid/:id/entities');
|
|
608
|
+
assert.equal(getOperation('reports/a%2Fb/download'), 'reports/:id/download');
|
|
609
|
+
});
|
|
610
|
+
});
|