@theia/telemetry 1.74.0-next.68
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 +66 -0
- package/lib/browser/browser-telemetry-service.d.ts +16 -0
- package/lib/browser/browser-telemetry-service.d.ts.map +1 -0
- package/lib/browser/browser-telemetry-service.js +68 -0
- package/lib/browser/browser-telemetry-service.js.map +1 -0
- package/lib/browser/browser-telemetry-service.spec.d.ts +2 -0
- package/lib/browser/browser-telemetry-service.spec.d.ts.map +1 -0
- package/lib/browser/browser-telemetry-service.spec.js +163 -0
- package/lib/browser/browser-telemetry-service.spec.js.map +1 -0
- package/lib/browser/index.d.ts +2 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/browser/index.js +20 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/browser/telemetry-frontend-module.d.ts +4 -0
- package/lib/browser/telemetry-frontend-module.d.ts.map +1 -0
- package/lib/browser/telemetry-frontend-module.js +35 -0
- package/lib/browser/telemetry-frontend-module.js.map +1 -0
- package/lib/browser-only/browser-only-telemetry-service.d.ts +5 -0
- package/lib/browser-only/browser-only-telemetry-service.d.ts.map +1 -0
- package/lib/browser-only/browser-only-telemetry-service.js +28 -0
- package/lib/browser-only/browser-only-telemetry-service.js.map +1 -0
- package/lib/browser-only/browser-only-telemetry-service.spec.d.ts +2 -0
- package/lib/browser-only/browser-only-telemetry-service.spec.d.ts.map +1 -0
- package/lib/browser-only/browser-only-telemetry-service.spec.js +42 -0
- package/lib/browser-only/browser-only-telemetry-service.spec.js.map +1 -0
- package/lib/browser-only/telemetry-frontend-only-module.d.ts +4 -0
- package/lib/browser-only/telemetry-frontend-only-module.d.ts.map +1 -0
- package/lib/browser-only/telemetry-frontend-only-module.js +30 -0
- package/lib/browser-only/telemetry-frontend-only-module.js.map +1 -0
- package/lib/common/index.d.ts +7 -0
- package/lib/common/index.d.ts.map +1 -0
- package/lib/common/index.js +25 -0
- package/lib/common/index.js.map +1 -0
- package/lib/common/telemetry-consent-provider.d.ts +24 -0
- package/lib/common/telemetry-consent-provider.d.ts.map +1 -0
- package/lib/common/telemetry-consent-provider.js +73 -0
- package/lib/common/telemetry-consent-provider.js.map +1 -0
- package/lib/common/telemetry-consent-provider.spec.d.ts +2 -0
- package/lib/common/telemetry-consent-provider.spec.d.ts.map +1 -0
- package/lib/common/telemetry-consent-provider.spec.js +87 -0
- package/lib/common/telemetry-consent-provider.spec.js.map +1 -0
- package/lib/common/telemetry-preferences.d.ts +29 -0
- package/lib/common/telemetry-preferences.d.ts.map +1 -0
- package/lib/common/telemetry-preferences.js +77 -0
- package/lib/common/telemetry-preferences.js.map +1 -0
- package/lib/common/telemetry-preferences.spec.d.ts +2 -0
- package/lib/common/telemetry-preferences.spec.d.ts.map +1 -0
- package/lib/common/telemetry-preferences.spec.js +50 -0
- package/lib/common/telemetry-preferences.spec.js.map +1 -0
- package/lib/common/telemetry-protocol.d.ts +30 -0
- package/lib/common/telemetry-protocol.d.ts.map +1 -0
- package/lib/common/telemetry-protocol.js +78 -0
- package/lib/common/telemetry-protocol.js.map +1 -0
- package/lib/common/telemetry-service.d.ts +28 -0
- package/lib/common/telemetry-service.d.ts.map +1 -0
- package/lib/common/telemetry-service.js +65 -0
- package/lib/common/telemetry-service.js.map +1 -0
- package/lib/common/telemetry-service.spec.d.ts +2 -0
- package/lib/common/telemetry-service.spec.d.ts.map +1 -0
- package/lib/common/telemetry-service.spec.js +128 -0
- package/lib/common/telemetry-service.spec.js.map +1 -0
- package/lib/common/telemetry-topic.d.ts +9 -0
- package/lib/common/telemetry-topic.d.ts.map +1 -0
- package/lib/common/telemetry-topic.js +61 -0
- package/lib/common/telemetry-topic.js.map +1 -0
- package/lib/common/telemetry-topic.spec.d.ts +2 -0
- package/lib/common/telemetry-topic.spec.d.ts.map +1 -0
- package/lib/common/telemetry-topic.spec.js +67 -0
- package/lib/common/telemetry-topic.spec.js.map +1 -0
- package/lib/common/telemetry-types.d.ts +5 -0
- package/lib/common/telemetry-types.d.ts.map +1 -0
- package/lib/common/telemetry-types.js +21 -0
- package/lib/common/telemetry-types.js.map +1 -0
- package/lib/common/test/recording-logger.d.ts +9 -0
- package/lib/common/test/recording-logger.d.ts.map +1 -0
- package/lib/common/test/recording-logger.js +36 -0
- package/lib/common/test/recording-logger.js.map +1 -0
- package/lib/node/index.d.ts +3 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +21 -0
- package/lib/node/index.js.map +1 -0
- package/lib/node/telemetry-backend-module.d.ts +4 -0
- package/lib/node/telemetry-backend-module.d.ts.map +1 -0
- package/lib/node/telemetry-backend-module.js +37 -0
- package/lib/node/telemetry-backend-module.js.map +1 -0
- package/lib/node/telemetry-service-impl.d.ts +36 -0
- package/lib/node/telemetry-service-impl.d.ts.map +1 -0
- package/lib/node/telemetry-service-impl.js +208 -0
- package/lib/node/telemetry-service-impl.js.map +1 -0
- package/lib/node/telemetry-service-impl.spec.d.ts +2 -0
- package/lib/node/telemetry-service-impl.spec.d.ts.map +1 -0
- package/lib/node/telemetry-service-impl.spec.js +411 -0
- package/lib/node/telemetry-service-impl.spec.js.map +1 -0
- package/lib/node/telemetry-sink.d.ts +31 -0
- package/lib/node/telemetry-sink.d.ts.map +1 -0
- package/lib/node/telemetry-sink.js +21 -0
- package/lib/node/telemetry-sink.js.map +1 -0
- package/package.json +52 -0
- package/src/browser/browser-telemetry-service.spec.ts +193 -0
- package/src/browser/browser-telemetry-service.ts +69 -0
- package/src/browser/index.ts +17 -0
- package/src/browser/telemetry-frontend-module.ts +34 -0
- package/src/browser-only/browser-only-telemetry-service.spec.ts +44 -0
- package/src/browser-only/browser-only-telemetry-service.ts +23 -0
- package/src/browser-only/telemetry-frontend-only-module.ts +28 -0
- package/src/common/index.ts +22 -0
- package/src/common/telemetry-consent-provider.spec.ts +95 -0
- package/src/common/telemetry-consent-provider.ts +72 -0
- package/src/common/telemetry-preferences.spec.ts +56 -0
- package/src/common/telemetry-preferences.ts +91 -0
- package/src/common/telemetry-protocol.ts +100 -0
- package/src/common/telemetry-service.spec.ts +152 -0
- package/src/common/telemetry-service.ts +90 -0
- package/src/common/telemetry-topic.spec.ts +78 -0
- package/src/common/telemetry-topic.ts +60 -0
- package/src/common/telemetry-types.ts +21 -0
- package/src/common/test/recording-logger.ts +33 -0
- package/src/node/index.ts +18 -0
- package/src/node/telemetry-backend-module.ts +40 -0
- package/src/node/telemetry-service-impl.spec.ts +500 -0
- package/src/node/telemetry-service-impl.ts +214 -0
- package/src/node/telemetry-sink.ts +48 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 STMicroelectronics and others.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.TelemetryServiceImpl = void 0;
|
|
19
|
+
const tslib_1 = require("tslib");
|
|
20
|
+
const common_1 = require("@theia/core/lib/common");
|
|
21
|
+
const inversify_1 = require("@theia/core/shared/inversify");
|
|
22
|
+
const telemetry_consent_provider_1 = require("../common/telemetry-consent-provider");
|
|
23
|
+
const telemetry_preferences_1 = require("../common/telemetry-preferences");
|
|
24
|
+
const telemetry_protocol_1 = require("../common/telemetry-protocol");
|
|
25
|
+
const telemetry_topic_1 = require("../common/telemetry-topic");
|
|
26
|
+
const telemetry_types_1 = require("../common/telemetry-types");
|
|
27
|
+
const telemetry_sink_1 = require("./telemetry-sink");
|
|
28
|
+
const MAX_PENDING_EVENTS = 1_000;
|
|
29
|
+
let TelemetryServiceImpl = class TelemetryServiceImpl {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.readinessState = 'pending';
|
|
32
|
+
this.pendingEvents = [];
|
|
33
|
+
this.pendingQueueOverflowWarned = false;
|
|
34
|
+
}
|
|
35
|
+
init() {
|
|
36
|
+
this.preferences.onPreferenceChanged(change => {
|
|
37
|
+
if (change.preferenceName === telemetry_preferences_1.TELEMETRY_FILTERS) {
|
|
38
|
+
this.filters = undefined;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
this.preferences.ready.then(() => {
|
|
42
|
+
this.readinessState = 'ready';
|
|
43
|
+
const pendingEvents = this.pendingEvents;
|
|
44
|
+
this.pendingEvents = [];
|
|
45
|
+
this.pendingQueueOverflowWarned = false;
|
|
46
|
+
pendingEvents.forEach(event => this.doDispatch(event));
|
|
47
|
+
}, () => {
|
|
48
|
+
this.readinessState = 'failed';
|
|
49
|
+
const pendingEvents = this.pendingEvents;
|
|
50
|
+
this.pendingEvents = [];
|
|
51
|
+
this.pendingQueueOverflowWarned = false;
|
|
52
|
+
this.logger.error('Telemetry preferences failed to become ready; remote telemetry is disabled.');
|
|
53
|
+
pendingEvents.forEach(event => this.doDispatch(event));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
report(topic, data, options) {
|
|
57
|
+
this.dispatch((0, telemetry_protocol_1.createTelemetryEvent)(topic, telemetry_types_1.BACKEND_TELEMETRY_SESSION, data, options));
|
|
58
|
+
}
|
|
59
|
+
notifyEvent(event) {
|
|
60
|
+
this.dispatch(event, true);
|
|
61
|
+
}
|
|
62
|
+
async getLocalSinkInterests() {
|
|
63
|
+
return [...new Set(this.getSinks()
|
|
64
|
+
.filter(sink => sink.scope === 'local')
|
|
65
|
+
.flatMap(sink => sink.interests))];
|
|
66
|
+
}
|
|
67
|
+
async onStop() {
|
|
68
|
+
await Promise.all(this.getSinks().map(async (validatedSink) => {
|
|
69
|
+
try {
|
|
70
|
+
await validatedSink.sink.flush?.();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
this.logger.error(`Telemetry sink '${validatedSink.id}' failed while flushing.`);
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
dispatch(event, rpcOrigin = false) {
|
|
78
|
+
if (!(0, telemetry_protocol_1.isValidTelemetryEvent)(event)) {
|
|
79
|
+
this.logger.warn(`Ignoring malformed telemetry event for topic '${(0, telemetry_protocol_1.describeTelemetryEventTopic)(event)}'.`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const snapshot = (0, telemetry_protocol_1.snapshotTelemetryEvent)(rpcOrigin ? { ...event, session: `frontend/${event.session}` } : event);
|
|
83
|
+
if (this.readinessState === 'ready' || this.readinessState === 'failed') {
|
|
84
|
+
this.doDispatch(snapshot);
|
|
85
|
+
}
|
|
86
|
+
else if (this.readinessState === 'pending') {
|
|
87
|
+
if (this.pendingEvents.length >= MAX_PENDING_EVENTS) {
|
|
88
|
+
this.pendingEvents.shift();
|
|
89
|
+
if (!this.pendingQueueOverflowWarned) {
|
|
90
|
+
this.pendingQueueOverflowWarned = true;
|
|
91
|
+
this.logger.warn('Telemetry pending event queue is full; dropping oldest event.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this.pendingEvents.push(snapshot);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
doDispatch(event) {
|
|
98
|
+
const filters = this.getFilters();
|
|
99
|
+
for (const validatedSink of this.getSinks()) {
|
|
100
|
+
if (this.readinessState === 'failed' && validatedSink.scope === 'remote') {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const filterPatterns = filters.get(validatedSink.id);
|
|
104
|
+
if ((filterPatterns && !filterPatterns.some(pattern => (0, telemetry_topic_1.matchesTelemetryTopic)(pattern, event.topic)))
|
|
105
|
+
|| !validatedSink.interests.some(pattern => (0, telemetry_topic_1.matchesTelemetryTopic)(pattern, event.topic))
|
|
106
|
+
|| (validatedSink.scope === 'remote' && !(0, telemetry_consent_provider_1.isKindAllowedByLevel)(this.consentProvider.level, event.kind))) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
Promise.resolve(validatedSink.sink.handle(event)).catch(() => this.logSinkFailure(validatedSink.id, event.topic));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
this.logSinkFailure(validatedSink.id, event.topic);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
getSinks() {
|
|
118
|
+
if (this.sinks) {
|
|
119
|
+
return this.sinks;
|
|
120
|
+
}
|
|
121
|
+
const contributions = this.sinkProvider.getContributions();
|
|
122
|
+
const idCounts = new Map();
|
|
123
|
+
for (const sink of contributions) {
|
|
124
|
+
if ((0, telemetry_topic_1.isValidTelemetrySinkId)(sink.id)) {
|
|
125
|
+
idCounts.set(sink.id, (idCounts.get(sink.id) ?? 0) + 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.sinks = contributions.flatMap(sink => {
|
|
129
|
+
const id = sink.id;
|
|
130
|
+
const interests = sink.interests;
|
|
131
|
+
if (!(0, telemetry_topic_1.isValidTelemetrySinkId)(id)) {
|
|
132
|
+
this.logger.error(`Ignoring telemetry sink with invalid ID '${String(id)}'.`);
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
if (idCounts.get(id) !== 1) {
|
|
136
|
+
this.logger.error(`Ignoring duplicate telemetry sink ID '${id}'.`);
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(interests) || interests.length === 0
|
|
140
|
+
|| !interests.every(interest => (0, telemetry_topic_1.isValidTelemetryTopicPattern)(interest))) {
|
|
141
|
+
this.logger.error(`Ignoring telemetry sink '${id}' with invalid interests.`);
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const scope = sink.scope ?? 'remote';
|
|
145
|
+
if (scope !== 'local' && scope !== 'remote') {
|
|
146
|
+
this.logger.error(`Ignoring telemetry sink '${id}' with invalid scope.`);
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
return [{ sink, id, interests: Object.freeze([...interests]), scope }];
|
|
150
|
+
});
|
|
151
|
+
return this.sinks;
|
|
152
|
+
}
|
|
153
|
+
getFilters() {
|
|
154
|
+
if (this.filters) {
|
|
155
|
+
return this.filters;
|
|
156
|
+
}
|
|
157
|
+
const filters = new Map();
|
|
158
|
+
for (const [sinkId, patterns] of Object.entries(this.preferences[telemetry_preferences_1.TELEMETRY_FILTERS] ?? {})) {
|
|
159
|
+
if (!Array.isArray(patterns)) {
|
|
160
|
+
this.logger.warn(`Ignoring invalid telemetry filters for sink '${sinkId}'.`);
|
|
161
|
+
filters.set(sinkId, Object.freeze([]));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const validPatterns = patterns.filter(pattern => {
|
|
165
|
+
if ((0, telemetry_topic_1.isValidTelemetryTopicPattern)(pattern)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
this.logger.warn(`Ignoring invalid telemetry filter pattern for sink '${sinkId}'.`);
|
|
169
|
+
return false;
|
|
170
|
+
});
|
|
171
|
+
filters.set(sinkId, Object.freeze(validPatterns));
|
|
172
|
+
}
|
|
173
|
+
this.filters = filters;
|
|
174
|
+
return filters;
|
|
175
|
+
}
|
|
176
|
+
logSinkFailure(sinkId, topic) {
|
|
177
|
+
this.logger.error(`Telemetry sink '${sinkId}' failed while handling topic '${topic}'.`);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
exports.TelemetryServiceImpl = TelemetryServiceImpl;
|
|
181
|
+
tslib_1.__decorate([
|
|
182
|
+
(0, inversify_1.inject)(telemetry_consent_provider_1.TelemetryConsentProvider),
|
|
183
|
+
tslib_1.__metadata("design:type", Object)
|
|
184
|
+
], TelemetryServiceImpl.prototype, "consentProvider", void 0);
|
|
185
|
+
tslib_1.__decorate([
|
|
186
|
+
(0, inversify_1.inject)(telemetry_preferences_1.TelemetryPreferences),
|
|
187
|
+
tslib_1.__metadata("design:type", Object)
|
|
188
|
+
], TelemetryServiceImpl.prototype, "preferences", void 0);
|
|
189
|
+
tslib_1.__decorate([
|
|
190
|
+
(0, inversify_1.inject)(common_1.ContributionProvider),
|
|
191
|
+
(0, inversify_1.named)(telemetry_sink_1.TelemetrySink),
|
|
192
|
+
tslib_1.__metadata("design:type", Object)
|
|
193
|
+
], TelemetryServiceImpl.prototype, "sinkProvider", void 0);
|
|
194
|
+
tslib_1.__decorate([
|
|
195
|
+
(0, inversify_1.inject)(common_1.ILogger),
|
|
196
|
+
(0, inversify_1.named)('telemetry:TelemetryServiceImpl'),
|
|
197
|
+
tslib_1.__metadata("design:type", Object)
|
|
198
|
+
], TelemetryServiceImpl.prototype, "logger", void 0);
|
|
199
|
+
tslib_1.__decorate([
|
|
200
|
+
(0, inversify_1.postConstruct)(),
|
|
201
|
+
tslib_1.__metadata("design:type", Function),
|
|
202
|
+
tslib_1.__metadata("design:paramtypes", []),
|
|
203
|
+
tslib_1.__metadata("design:returntype", void 0)
|
|
204
|
+
], TelemetryServiceImpl.prototype, "init", null);
|
|
205
|
+
exports.TelemetryServiceImpl = TelemetryServiceImpl = tslib_1.__decorate([
|
|
206
|
+
(0, inversify_1.injectable)()
|
|
207
|
+
], TelemetryServiceImpl);
|
|
208
|
+
//# sourceMappingURL=telemetry-service-impl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-service-impl.js","sourceRoot":"","sources":["../../src/node/telemetry-service-impl.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-service-impl.spec.d.ts","sourceRoot":"","sources":["../../src/node/telemetry-service-impl.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 STMicroelectronics and others.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const chai_1 = require("chai");
|
|
19
|
+
const common_1 = require("@theia/core/lib/common");
|
|
20
|
+
const sinon = require("sinon");
|
|
21
|
+
const inversify_1 = require("@theia/core/shared/inversify");
|
|
22
|
+
const telemetry_consent_provider_1 = require("../common/telemetry-consent-provider");
|
|
23
|
+
const telemetry_preferences_1 = require("../common/telemetry-preferences");
|
|
24
|
+
const recording_logger_1 = require("../common/test/recording-logger");
|
|
25
|
+
const telemetry_service_impl_1 = require("./telemetry-service-impl");
|
|
26
|
+
const telemetry_types_1 = require("../common/telemetry-types");
|
|
27
|
+
const telemetry_sink_1 = require("./telemetry-sink");
|
|
28
|
+
function createPreferences(initialFilters, ready = Promise.resolve()) {
|
|
29
|
+
const emitter = new common_1.Emitter();
|
|
30
|
+
let filters = initialFilters;
|
|
31
|
+
return {
|
|
32
|
+
[telemetry_preferences_1.TELEMETRY_LEVEL]: 'off',
|
|
33
|
+
get [telemetry_preferences_1.TELEMETRY_FILTERS]() {
|
|
34
|
+
return filters;
|
|
35
|
+
},
|
|
36
|
+
ready,
|
|
37
|
+
onPreferenceChanged: emitter.event,
|
|
38
|
+
setFilters: (newFilters) => {
|
|
39
|
+
filters = newFilters;
|
|
40
|
+
emitter.fire({ preferenceName: telemetry_preferences_1.TELEMETRY_FILTERS, affects: () => true });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function createSink(id, interests = ['*'], scope) {
|
|
45
|
+
const events = [];
|
|
46
|
+
return {
|
|
47
|
+
id,
|
|
48
|
+
interests,
|
|
49
|
+
scope,
|
|
50
|
+
events,
|
|
51
|
+
handle: event => {
|
|
52
|
+
events.push(event);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createConsentProvider(level) {
|
|
57
|
+
return { level, onDidChangeTelemetryLevel: new common_1.Emitter().event };
|
|
58
|
+
}
|
|
59
|
+
function createServiceWithPreferences(preferences, sinks, logger = new recording_logger_1.RecordingLogger(), level = 'all') {
|
|
60
|
+
const provider = { getContributions: () => sinks };
|
|
61
|
+
const container = new inversify_1.Container();
|
|
62
|
+
container.bind(telemetry_consent_provider_1.TelemetryConsentProvider).toConstantValue(createConsentProvider(level));
|
|
63
|
+
container.bind(telemetry_preferences_1.TelemetryPreferences).toConstantValue(preferences);
|
|
64
|
+
container.bind(common_1.ContributionProvider).toConstantValue(provider).whenTargetNamed(telemetry_sink_1.TelemetrySink);
|
|
65
|
+
container.bind(common_1.ILogger).toConstantValue(logger).whenTargetNamed('telemetry:TelemetryServiceImpl');
|
|
66
|
+
container.bind(telemetry_service_impl_1.TelemetryServiceImpl).toSelf();
|
|
67
|
+
return container.get(telemetry_service_impl_1.TelemetryServiceImpl);
|
|
68
|
+
}
|
|
69
|
+
function createService(level, filters, sinks, logger = new recording_logger_1.RecordingLogger()) {
|
|
70
|
+
return createServiceWithPreferences(createPreferences(filters), sinks, logger, level);
|
|
71
|
+
}
|
|
72
|
+
async function flushDispatch() {
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
await Promise.resolve();
|
|
75
|
+
}
|
|
76
|
+
async function reportEvent(service, event = { topic: 'company/action', timestamp: 42 }) {
|
|
77
|
+
service.notifyEvent({ kind: 'usage', session: 'frontend-session', ...event });
|
|
78
|
+
await flushDispatch();
|
|
79
|
+
}
|
|
80
|
+
describe('TelemetryServiceImpl', () => {
|
|
81
|
+
let clock;
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
clock = sinon.useFakeTimers({ now: 1234 });
|
|
84
|
+
});
|
|
85
|
+
afterEach(() => clock.restore());
|
|
86
|
+
it('treats undefined filters as no configured filters', async () => {
|
|
87
|
+
const sink = createSink('company/sink');
|
|
88
|
+
const preferences = createPreferences(undefined);
|
|
89
|
+
await reportEvent(createServiceWithPreferences(preferences, [sink]));
|
|
90
|
+
(0, chai_1.expect)(sink.events).to.have.length(1);
|
|
91
|
+
});
|
|
92
|
+
it('allows missing filters and treats an empty filter as disabling the sink', async () => {
|
|
93
|
+
const allowed = createSink('company/allowed');
|
|
94
|
+
const disabled = createSink('company/disabled');
|
|
95
|
+
await reportEvent(createService('all', { 'company/disabled': [] }, [allowed, disabled]));
|
|
96
|
+
(0, chai_1.expect)(allowed.events).to.have.length(1);
|
|
97
|
+
(0, chai_1.expect)(disabled.events).to.be.empty;
|
|
98
|
+
});
|
|
99
|
+
it('gates remote sinks by level while local sinks bypass consent', async () => {
|
|
100
|
+
const remote = createSink('company/remote');
|
|
101
|
+
const local = createSink('company/local', ['*'], 'local');
|
|
102
|
+
const service = createService('error', {}, [remote, local]);
|
|
103
|
+
await reportEvent(service);
|
|
104
|
+
await reportEvent(service, { topic: 'company/error', kind: 'error', timestamp: 43 });
|
|
105
|
+
await reportEvent(service, { topic: 'company/crash', kind: 'crash', timestamp: 44 });
|
|
106
|
+
(0, chai_1.expect)(remote.events.map(event => event.kind)).to.deep.equal(['error', 'crash']);
|
|
107
|
+
(0, chai_1.expect)(local.events.map(event => event.kind)).to.deep.equal(['usage', 'error', 'crash']);
|
|
108
|
+
});
|
|
109
|
+
it('requires both the sink-specific filter and declared interest to match', async () => {
|
|
110
|
+
const first = createSink('company/first', ['company/*']);
|
|
111
|
+
const second = createSink('company/second', ['other/*']);
|
|
112
|
+
const third = createSink('company/third', ['company/*']);
|
|
113
|
+
const service = createService('all', {
|
|
114
|
+
'company/first': ['company/action'],
|
|
115
|
+
'company/second': ['*'],
|
|
116
|
+
'company/third': ['other/*']
|
|
117
|
+
}, [first, second, third]);
|
|
118
|
+
await reportEvent(service);
|
|
119
|
+
(0, chai_1.expect)(first.events).to.have.length(1);
|
|
120
|
+
(0, chai_1.expect)(second.events).to.be.empty;
|
|
121
|
+
(0, chai_1.expect)(third.events).to.be.empty;
|
|
122
|
+
});
|
|
123
|
+
it('supports exact, terminal wildcard, and global filter/interest intersections', async () => {
|
|
124
|
+
const exact = createSink('company/exact', ['company/action']);
|
|
125
|
+
const prefix = createSink('company/prefix', ['company/*']);
|
|
126
|
+
const global = createSink('company/global', ['*']);
|
|
127
|
+
const service = createService('all', {
|
|
128
|
+
'company/exact': ['company/action'],
|
|
129
|
+
'company/prefix': ['company/*'],
|
|
130
|
+
'company/global': ['*']
|
|
131
|
+
}, [exact, prefix, global]);
|
|
132
|
+
await reportEvent(service);
|
|
133
|
+
await reportEvent(service, { topic: 'company/action/deep', timestamp: 43 });
|
|
134
|
+
await reportEvent(service, { topic: 'other/action', timestamp: 44 });
|
|
135
|
+
(0, chai_1.expect)(exact.events.map(event => event.timestamp)).to.deep.equal([42]);
|
|
136
|
+
(0, chai_1.expect)(prefix.events.map(event => event.timestamp)).to.deep.equal([42, 43]);
|
|
137
|
+
(0, chai_1.expect)(global.events).to.have.length(3);
|
|
138
|
+
});
|
|
139
|
+
it('warns once per invalid filter until filters change', async () => {
|
|
140
|
+
const sink = createSink('company/sink');
|
|
141
|
+
const logger = new recording_logger_1.RecordingLogger();
|
|
142
|
+
const preferences = createPreferences({ 'company/sink': ['company/**', '*/action', 'company/action'] });
|
|
143
|
+
const service = createServiceWithPreferences(preferences, [sink], logger);
|
|
144
|
+
await reportEvent(service, { topic: 'other/action', timestamp: 42 });
|
|
145
|
+
await reportEvent(service);
|
|
146
|
+
(0, chai_1.expect)(sink.events).to.have.length(1);
|
|
147
|
+
(0, chai_1.expect)(logger.warnings).to.have.length(2);
|
|
148
|
+
preferences.setFilters({ 'company/sink': ['invalid/**', '*'] });
|
|
149
|
+
await reportEvent(service, { topic: 'other/action', timestamp: 43 });
|
|
150
|
+
(0, chai_1.expect)(sink.events).to.have.length(2);
|
|
151
|
+
(0, chai_1.expect)(logger.warnings).to.have.length(3);
|
|
152
|
+
});
|
|
153
|
+
it('rejects invalid and duplicate sink metadata', async () => {
|
|
154
|
+
const invalidId = createSink('invalid');
|
|
155
|
+
const emptyInterests = createSink('company/empty', []);
|
|
156
|
+
const invalidInterests = createSink('company/interests', ['company/**']);
|
|
157
|
+
const duplicateOne = createSink('company/duplicate');
|
|
158
|
+
const duplicateTwo = createSink('company/duplicate');
|
|
159
|
+
const valid = createSink('company/valid');
|
|
160
|
+
const logger = new recording_logger_1.RecordingLogger();
|
|
161
|
+
const service = createService('all', {
|
|
162
|
+
'company/empty': ['*'],
|
|
163
|
+
'company/interests': ['*'],
|
|
164
|
+
'company/duplicate': ['*'],
|
|
165
|
+
'company/valid': ['*']
|
|
166
|
+
}, [invalidId, emptyInterests, invalidInterests, duplicateOne, duplicateTwo, valid], logger);
|
|
167
|
+
await reportEvent(service);
|
|
168
|
+
(0, chai_1.expect)(invalidId.events).to.be.empty;
|
|
169
|
+
(0, chai_1.expect)(emptyInterests.events).to.be.empty;
|
|
170
|
+
(0, chai_1.expect)(invalidInterests.events).to.be.empty;
|
|
171
|
+
(0, chai_1.expect)(duplicateOne.events).to.be.empty;
|
|
172
|
+
(0, chai_1.expect)(duplicateTwo.events).to.be.empty;
|
|
173
|
+
(0, chai_1.expect)(valid.events).to.have.length(1);
|
|
174
|
+
(0, chai_1.expect)(logger.errors).to.have.length(5);
|
|
175
|
+
});
|
|
176
|
+
it('returns the deduplicated interests of valid local sinks', async () => {
|
|
177
|
+
const local = createSink('company/local', ['company/*', 'shared/*'], 'local');
|
|
178
|
+
const secondLocal = createSink('company/second-local', ['shared/*', 'other/action'], 'local');
|
|
179
|
+
const remote = createSink('company/remote', ['remote/*']);
|
|
180
|
+
const invalid = createSink('invalid', ['invalid/*'], 'local');
|
|
181
|
+
const logger = new recording_logger_1.RecordingLogger();
|
|
182
|
+
const service = createService('off', {}, [local, secondLocal, remote, invalid], logger);
|
|
183
|
+
(0, chai_1.expect)(await service.getLocalSinkInterests()).to.deep.equal(['company/*', 'shared/*', 'other/action']);
|
|
184
|
+
(0, chai_1.expect)(logger.errors).to.have.length(1);
|
|
185
|
+
});
|
|
186
|
+
it('flushes valid sinks on stop and contains flush failures', async () => {
|
|
187
|
+
const logger = new recording_logger_1.RecordingLogger();
|
|
188
|
+
const flushed = [];
|
|
189
|
+
const successful = createSink('company/successful');
|
|
190
|
+
successful.flush = async () => { flushed.push(successful.id); };
|
|
191
|
+
const throwing = createSink('company/throwing');
|
|
192
|
+
throwing.flush = () => { throw new Error('payload-secret'); };
|
|
193
|
+
const rejecting = createSink('company/rejecting');
|
|
194
|
+
rejecting.flush = () => Promise.reject(new Error('payload-secret'));
|
|
195
|
+
const withoutFlush = createSink('company/without-flush');
|
|
196
|
+
const invalid = createSink('invalid');
|
|
197
|
+
invalid.flush = async () => { flushed.push(invalid.id); };
|
|
198
|
+
const service = createService('all', {}, [successful, throwing, rejecting, withoutFlush, invalid], logger);
|
|
199
|
+
await service.onStop();
|
|
200
|
+
(0, chai_1.expect)(flushed).to.deep.equal(['company/successful']);
|
|
201
|
+
(0, chai_1.expect)(logger.errors).to.have.length(3);
|
|
202
|
+
(0, chai_1.expect)(logger.errors.join(' ')).not.to.contain('payload-secret');
|
|
203
|
+
});
|
|
204
|
+
it('uses a snapshot of validated sink metadata', async () => {
|
|
205
|
+
const sink = createSink('company/sink', ['company/action']);
|
|
206
|
+
const service = createService('all', { 'company/sink': ['*'] }, [sink]);
|
|
207
|
+
await reportEvent(service);
|
|
208
|
+
sink.interests = ['other/*'];
|
|
209
|
+
await reportEvent(service);
|
|
210
|
+
await reportEvent(service, { topic: 'other/action', timestamp: 43 });
|
|
211
|
+
(0, chai_1.expect)(sink.events.map(event => event.topic)).to.deep.equal(['company/action', 'company/action']);
|
|
212
|
+
});
|
|
213
|
+
it('dispatches queued events in FIFO order after preference readiness', async () => {
|
|
214
|
+
let resolveReady;
|
|
215
|
+
const ready = new Promise(resolve => resolveReady = resolve);
|
|
216
|
+
const sink = createSink('company/sink');
|
|
217
|
+
const service = createServiceWithPreferences(createPreferences({ 'company/sink': ['*'] }, ready), [sink]);
|
|
218
|
+
service.report('company/first');
|
|
219
|
+
service.report('company/second');
|
|
220
|
+
service.report('company/third');
|
|
221
|
+
(0, chai_1.expect)(sink.events).to.be.empty;
|
|
222
|
+
resolveReady();
|
|
223
|
+
await flushDispatch();
|
|
224
|
+
(0, chai_1.expect)(sink.events.map(event => event.topic)).to.deep.equal(['company/first', 'company/second', 'company/third']);
|
|
225
|
+
});
|
|
226
|
+
it('bounds queued events, discards the oldest, preserves FIFO order, and warns once', async () => {
|
|
227
|
+
let resolveReady;
|
|
228
|
+
const ready = new Promise(resolve => resolveReady = resolve);
|
|
229
|
+
const logger = new recording_logger_1.RecordingLogger();
|
|
230
|
+
const sink = createSink('company/sink');
|
|
231
|
+
const service = createServiceWithPreferences(createPreferences({}, ready), [sink], logger);
|
|
232
|
+
for (let index = 0; index < 1_002; index++) {
|
|
233
|
+
service.report(`company/${index}`);
|
|
234
|
+
}
|
|
235
|
+
(0, chai_1.expect)(logger.warnings).to.have.length(1);
|
|
236
|
+
resolveReady();
|
|
237
|
+
await flushDispatch();
|
|
238
|
+
(0, chai_1.expect)(sink.events).to.have.length(1_000);
|
|
239
|
+
(0, chai_1.expect)(sink.events.map(event => event.topic)).to.deep.equal(Array.from({ length: 1_000 }, (_, index) => `company/${index + 2}`));
|
|
240
|
+
(0, chai_1.expect)(logger.warnings).to.have.length(1);
|
|
241
|
+
});
|
|
242
|
+
it('timestamps direct reports and snapshots payloads before deferred dispatch', async () => {
|
|
243
|
+
let resolveReady;
|
|
244
|
+
const ready = new Promise(resolve => resolveReady = resolve);
|
|
245
|
+
const data = { action: 'open', durations: [1, 2], states: [true, false] };
|
|
246
|
+
const attributes = { source: 'backend', labels: ['stable'] };
|
|
247
|
+
const sink = createSink('company/sink');
|
|
248
|
+
const service = createServiceWithPreferences(createPreferences({ 'company/sink': ['*'] }, ready), [sink]);
|
|
249
|
+
service.report('company/action', data, { kind: 'error', attributes });
|
|
250
|
+
data.action = 'changed';
|
|
251
|
+
attributes.source = 'changed';
|
|
252
|
+
attributes.labels.push('changed');
|
|
253
|
+
data.durations.push(3);
|
|
254
|
+
resolveReady();
|
|
255
|
+
await flushDispatch();
|
|
256
|
+
(0, chai_1.expect)(sink.events).to.have.length(1);
|
|
257
|
+
(0, chai_1.expect)(sink.events[0].data).to.deep.equal({ action: 'open', durations: [1, 2], states: [true, false] });
|
|
258
|
+
(0, chai_1.expect)(sink.events[0].data).not.to.equal(data);
|
|
259
|
+
(0, chai_1.expect)(sink.events[0].kind).to.equal('error');
|
|
260
|
+
(0, chai_1.expect)(sink.events[0].attributes).to.deep.equal({ source: 'backend', labels: ['stable'] });
|
|
261
|
+
(0, chai_1.expect)(sink.events[0].session).to.equal(telemetry_types_1.BACKEND_TELEMETRY_SESSION);
|
|
262
|
+
(0, chai_1.expect)(Object.isFrozen(sink.events[0])).to.be.true;
|
|
263
|
+
(0, chai_1.expect)(Object.isFrozen(sink.events[0].data)).to.be.true;
|
|
264
|
+
(0, chai_1.expect)(Object.isFrozen(sink.events[0].data?.durations)).to.be.true;
|
|
265
|
+
(0, chai_1.expect)(Object.isFrozen(sink.events[0].attributes)).to.be.true;
|
|
266
|
+
(0, chai_1.expect)(Object.isFrozen(sink.events[0].attributes?.labels)).to.be.true;
|
|
267
|
+
(0, chai_1.expect)(sink.events[0].timestamp).to.equal(1234);
|
|
268
|
+
});
|
|
269
|
+
it('preserves RPC timestamps and isolates immutable payloads between sinks', async () => {
|
|
270
|
+
const data = { action: 'open', durations: [1, 2] };
|
|
271
|
+
const event = { topic: 'company/action', kind: 'error', data, attributes: { source: 'rpc', labels: ['stable'] }, session: 'rpc-session', timestamp: 987 };
|
|
272
|
+
const observed = [];
|
|
273
|
+
const mutating = {
|
|
274
|
+
id: 'company/mutating',
|
|
275
|
+
interests: ['*'],
|
|
276
|
+
handle: received => {
|
|
277
|
+
(0, chai_1.expect)(() => Object.assign(received.data, { action: 'changed' })).to.throw();
|
|
278
|
+
(0, chai_1.expect)(() => received.data.durations.push(3)).to.throw();
|
|
279
|
+
(0, chai_1.expect)(() => Object.assign(received.attributes, { source: 'changed' })).to.throw();
|
|
280
|
+
(0, chai_1.expect)(() => received.attributes.labels.push('changed')).to.throw();
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const recording = {
|
|
284
|
+
id: 'company/recording',
|
|
285
|
+
interests: ['*'],
|
|
286
|
+
handle: received => {
|
|
287
|
+
observed.push(received);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const service = createService('all', { 'company/mutating': ['*'], 'company/recording': ['*'] }, [mutating, recording]);
|
|
291
|
+
await reportEvent(service, event);
|
|
292
|
+
data.action = 'changed';
|
|
293
|
+
data.durations.push(3);
|
|
294
|
+
event.attributes.source = 'changed';
|
|
295
|
+
event.attributes.labels.push('changed');
|
|
296
|
+
(0, chai_1.expect)(observed[0].data).to.deep.equal({ action: 'open', durations: [1, 2] });
|
|
297
|
+
(0, chai_1.expect)(observed[0].kind).to.equal('error');
|
|
298
|
+
(0, chai_1.expect)(observed[0].attributes).to.deep.equal({ source: 'rpc', labels: ['stable'] });
|
|
299
|
+
(0, chai_1.expect)(observed[0].session).to.equal('frontend/rpc-session');
|
|
300
|
+
(0, chai_1.expect)(observed[0].timestamp).to.equal(987);
|
|
301
|
+
});
|
|
302
|
+
it('strips unknown top-level properties from RPC events', async () => {
|
|
303
|
+
const sink = createSink('company/sink');
|
|
304
|
+
const service = createService('all', {}, [sink]);
|
|
305
|
+
service.notifyEvent({
|
|
306
|
+
topic: 'company/action',
|
|
307
|
+
kind: 'usage',
|
|
308
|
+
session: 'frontend-session',
|
|
309
|
+
timestamp: 42,
|
|
310
|
+
unknown: 'value'
|
|
311
|
+
});
|
|
312
|
+
await flushDispatch();
|
|
313
|
+
(0, chai_1.expect)(sink.events).to.have.length(1);
|
|
314
|
+
(0, chai_1.expect)(Object.keys(sink.events[0])).to.deep.equal(['topic', 'kind', 'data', 'attributes', 'session', 'timestamp']);
|
|
315
|
+
});
|
|
316
|
+
it('drops malformed reports and RPC values without logging payload values', async () => {
|
|
317
|
+
const sink = createSink('company/sink');
|
|
318
|
+
const logger = new recording_logger_1.RecordingLogger();
|
|
319
|
+
const service = createService('all', { 'company/sink': ['*'] }, [sink], logger);
|
|
320
|
+
service.report('invalid', { secret: 'payload-secret' });
|
|
321
|
+
service.report('company/action', { mixed: [1, 'payload-secret'] });
|
|
322
|
+
service.report('company/action', undefined, { kind: 'invalid' });
|
|
323
|
+
service.report('company/action', undefined, { attributes: { nested: { secret: 'payload-secret' } } });
|
|
324
|
+
const validEnvelope = { topic: 'company/action', kind: 'usage', session: 'frontend-session', timestamp: 42 };
|
|
325
|
+
service.notifyEvent({ ...validEnvelope, data: { nested: { secret: 'payload-secret' } } });
|
|
326
|
+
service.notifyEvent({ ...validEnvelope, attributes: { nested: { secret: 'payload-secret' } } });
|
|
327
|
+
service.notifyEvent({ ...validEnvelope, kind: 'invalid' });
|
|
328
|
+
service.notifyEvent({ ...validEnvelope, session: '' });
|
|
329
|
+
service.notifyEvent({ ...validEnvelope, timestamp: Number.POSITIVE_INFINITY });
|
|
330
|
+
service.notifyEvent(undefined);
|
|
331
|
+
service.notifyEvent('payload-secret');
|
|
332
|
+
service.notifyEvent({ ...validEnvelope, topic: 42, data: 'payload-secret' });
|
|
333
|
+
service.notifyEvent({ kind: 'usage', session: 'frontend-session', data: { secret: 'payload-secret' }, timestamp: 42 });
|
|
334
|
+
await flushDispatch();
|
|
335
|
+
(0, chai_1.expect)(sink.events).to.be.empty;
|
|
336
|
+
(0, chai_1.expect)(logger.warnings).to.have.length(13);
|
|
337
|
+
(0, chai_1.expect)(logger.warnings.join(' ')).not.to.contain('payload-secret');
|
|
338
|
+
});
|
|
339
|
+
it('dispatches synchronously after preference readiness', async () => {
|
|
340
|
+
const sink = createSink('company/sink');
|
|
341
|
+
const preferences = createPreferences({ 'company/sink': ['*'] });
|
|
342
|
+
const service = createServiceWithPreferences(preferences, [sink]);
|
|
343
|
+
await preferences.ready;
|
|
344
|
+
await flushDispatch();
|
|
345
|
+
service.report('company/action');
|
|
346
|
+
(0, chai_1.expect)(sink.events).to.have.length(1);
|
|
347
|
+
});
|
|
348
|
+
it('delivers queued and subsequent events only to local sinks after preference readiness rejection and logs once', async () => {
|
|
349
|
+
let rejectReady;
|
|
350
|
+
const ready = new Promise((_resolve, reject) => rejectReady = reject);
|
|
351
|
+
const logger = new recording_logger_1.RecordingLogger();
|
|
352
|
+
const local = createSink('company/local', ['*'], 'local');
|
|
353
|
+
const remote = createSink('company/remote');
|
|
354
|
+
const preferences = createPreferences({ 'company/local': ['*'], 'company/remote': ['*'] }, ready);
|
|
355
|
+
const service = createServiceWithPreferences(preferences, [local, remote], logger);
|
|
356
|
+
service.report('company/first', { secret: 'first-secret' });
|
|
357
|
+
service.report('company/second', { secret: 'second-secret' });
|
|
358
|
+
rejectReady(new Error('readiness-secret'));
|
|
359
|
+
await flushDispatch();
|
|
360
|
+
service.report('company/third', { secret: 'third-secret' });
|
|
361
|
+
(0, chai_1.expect)(local.events.map(event => event.topic)).to.deep.equal(['company/first', 'company/second', 'company/third']);
|
|
362
|
+
(0, chai_1.expect)(remote.events).to.be.empty;
|
|
363
|
+
(0, chai_1.expect)(logger.errors).to.have.length(1);
|
|
364
|
+
(0, chai_1.expect)(logger.errors.join(' ')).not.to.contain('secret');
|
|
365
|
+
});
|
|
366
|
+
it('uses the same policy path for direct and RPC reports', async () => {
|
|
367
|
+
const sink = createSink('company/sink', ['company/*']);
|
|
368
|
+
const service = createService('all', { 'company/sink': ['company/*'] }, [sink]);
|
|
369
|
+
service.report('company/direct');
|
|
370
|
+
service.notifyEvent({ topic: 'company/rpc', kind: 'crash', session: 'rpc-session', timestamp: 99 });
|
|
371
|
+
await flushDispatch();
|
|
372
|
+
(0, chai_1.expect)(sink.events.map(event => event.topic)).to.deep.equal(['company/direct', 'company/rpc']);
|
|
373
|
+
(0, chai_1.expect)(sink.events.map(event => event.timestamp)).to.deep.equal([1234, 99]);
|
|
374
|
+
(0, chai_1.expect)(sink.events.map(event => event.kind)).to.deep.equal(['usage', 'crash']);
|
|
375
|
+
(0, chai_1.expect)(sink.events.map(event => event.session)).to.deep.equal([telemetry_types_1.BACKEND_TELEMETRY_SESSION, 'frontend/rpc-session']);
|
|
376
|
+
});
|
|
377
|
+
it('prevents RPC events from impersonating the backend session', async () => {
|
|
378
|
+
const sink = createSink('company/sink');
|
|
379
|
+
const service = createService('all', {}, [sink]);
|
|
380
|
+
service.notifyEvent({
|
|
381
|
+
topic: 'company/rpc',
|
|
382
|
+
kind: 'usage',
|
|
383
|
+
session: telemetry_types_1.BACKEND_TELEMETRY_SESSION,
|
|
384
|
+
timestamp: 99
|
|
385
|
+
});
|
|
386
|
+
await flushDispatch();
|
|
387
|
+
(0, chai_1.expect)(sink.events).to.have.length(1);
|
|
388
|
+
(0, chai_1.expect)(sink.events[0].session).to.equal(`frontend/${telemetry_types_1.BACKEND_TELEMETRY_SESSION}`);
|
|
389
|
+
(0, chai_1.expect)(sink.events[0].session).not.to.equal(telemetry_types_1.BACKEND_TELEMETRY_SESSION);
|
|
390
|
+
});
|
|
391
|
+
it('isolates void handlers, synchronous throws, asynchronous rejections, and slow sinks', async () => {
|
|
392
|
+
const logger = new recording_logger_1.RecordingLogger();
|
|
393
|
+
const successful = createSink('company/successful');
|
|
394
|
+
const throwing = {
|
|
395
|
+
id: 'company/throwing', interests: ['*'], handle: () => { throw new Error('payload-secret'); }
|
|
396
|
+
};
|
|
397
|
+
const rejecting = {
|
|
398
|
+
id: 'company/rejecting', interests: ['*'], handle: () => Promise.reject(new Error('payload-secret'))
|
|
399
|
+
};
|
|
400
|
+
const slow = {
|
|
401
|
+
id: 'company/slow', interests: ['*'], handle: () => new Promise(() => undefined)
|
|
402
|
+
};
|
|
403
|
+
const filters = Object.fromEntries([successful, throwing, rejecting, slow].map(sink => [sink.id, ['*']]));
|
|
404
|
+
await reportEvent(createService('all', filters, [throwing, slow, successful, rejecting], logger));
|
|
405
|
+
await Promise.resolve();
|
|
406
|
+
(0, chai_1.expect)(successful.events).to.have.length(1);
|
|
407
|
+
(0, chai_1.expect)(logger.errors).to.have.length(2);
|
|
408
|
+
(0, chai_1.expect)(logger.errors.join(' ')).not.to.contain('payload-secret');
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
//# sourceMappingURL=telemetry-service-impl.spec.js.map
|