@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.
Files changed (122) hide show
  1. package/README.md +66 -0
  2. package/lib/browser/browser-telemetry-service.d.ts +16 -0
  3. package/lib/browser/browser-telemetry-service.d.ts.map +1 -0
  4. package/lib/browser/browser-telemetry-service.js +68 -0
  5. package/lib/browser/browser-telemetry-service.js.map +1 -0
  6. package/lib/browser/browser-telemetry-service.spec.d.ts +2 -0
  7. package/lib/browser/browser-telemetry-service.spec.d.ts.map +1 -0
  8. package/lib/browser/browser-telemetry-service.spec.js +163 -0
  9. package/lib/browser/browser-telemetry-service.spec.js.map +1 -0
  10. package/lib/browser/index.d.ts +2 -0
  11. package/lib/browser/index.d.ts.map +1 -0
  12. package/lib/browser/index.js +20 -0
  13. package/lib/browser/index.js.map +1 -0
  14. package/lib/browser/telemetry-frontend-module.d.ts +4 -0
  15. package/lib/browser/telemetry-frontend-module.d.ts.map +1 -0
  16. package/lib/browser/telemetry-frontend-module.js +35 -0
  17. package/lib/browser/telemetry-frontend-module.js.map +1 -0
  18. package/lib/browser-only/browser-only-telemetry-service.d.ts +5 -0
  19. package/lib/browser-only/browser-only-telemetry-service.d.ts.map +1 -0
  20. package/lib/browser-only/browser-only-telemetry-service.js +28 -0
  21. package/lib/browser-only/browser-only-telemetry-service.js.map +1 -0
  22. package/lib/browser-only/browser-only-telemetry-service.spec.d.ts +2 -0
  23. package/lib/browser-only/browser-only-telemetry-service.spec.d.ts.map +1 -0
  24. package/lib/browser-only/browser-only-telemetry-service.spec.js +42 -0
  25. package/lib/browser-only/browser-only-telemetry-service.spec.js.map +1 -0
  26. package/lib/browser-only/telemetry-frontend-only-module.d.ts +4 -0
  27. package/lib/browser-only/telemetry-frontend-only-module.d.ts.map +1 -0
  28. package/lib/browser-only/telemetry-frontend-only-module.js +30 -0
  29. package/lib/browser-only/telemetry-frontend-only-module.js.map +1 -0
  30. package/lib/common/index.d.ts +7 -0
  31. package/lib/common/index.d.ts.map +1 -0
  32. package/lib/common/index.js +25 -0
  33. package/lib/common/index.js.map +1 -0
  34. package/lib/common/telemetry-consent-provider.d.ts +24 -0
  35. package/lib/common/telemetry-consent-provider.d.ts.map +1 -0
  36. package/lib/common/telemetry-consent-provider.js +73 -0
  37. package/lib/common/telemetry-consent-provider.js.map +1 -0
  38. package/lib/common/telemetry-consent-provider.spec.d.ts +2 -0
  39. package/lib/common/telemetry-consent-provider.spec.d.ts.map +1 -0
  40. package/lib/common/telemetry-consent-provider.spec.js +87 -0
  41. package/lib/common/telemetry-consent-provider.spec.js.map +1 -0
  42. package/lib/common/telemetry-preferences.d.ts +29 -0
  43. package/lib/common/telemetry-preferences.d.ts.map +1 -0
  44. package/lib/common/telemetry-preferences.js +77 -0
  45. package/lib/common/telemetry-preferences.js.map +1 -0
  46. package/lib/common/telemetry-preferences.spec.d.ts +2 -0
  47. package/lib/common/telemetry-preferences.spec.d.ts.map +1 -0
  48. package/lib/common/telemetry-preferences.spec.js +50 -0
  49. package/lib/common/telemetry-preferences.spec.js.map +1 -0
  50. package/lib/common/telemetry-protocol.d.ts +30 -0
  51. package/lib/common/telemetry-protocol.d.ts.map +1 -0
  52. package/lib/common/telemetry-protocol.js +78 -0
  53. package/lib/common/telemetry-protocol.js.map +1 -0
  54. package/lib/common/telemetry-service.d.ts +28 -0
  55. package/lib/common/telemetry-service.d.ts.map +1 -0
  56. package/lib/common/telemetry-service.js +65 -0
  57. package/lib/common/telemetry-service.js.map +1 -0
  58. package/lib/common/telemetry-service.spec.d.ts +2 -0
  59. package/lib/common/telemetry-service.spec.d.ts.map +1 -0
  60. package/lib/common/telemetry-service.spec.js +128 -0
  61. package/lib/common/telemetry-service.spec.js.map +1 -0
  62. package/lib/common/telemetry-topic.d.ts +9 -0
  63. package/lib/common/telemetry-topic.d.ts.map +1 -0
  64. package/lib/common/telemetry-topic.js +61 -0
  65. package/lib/common/telemetry-topic.js.map +1 -0
  66. package/lib/common/telemetry-topic.spec.d.ts +2 -0
  67. package/lib/common/telemetry-topic.spec.d.ts.map +1 -0
  68. package/lib/common/telemetry-topic.spec.js +67 -0
  69. package/lib/common/telemetry-topic.spec.js.map +1 -0
  70. package/lib/common/telemetry-types.d.ts +5 -0
  71. package/lib/common/telemetry-types.d.ts.map +1 -0
  72. package/lib/common/telemetry-types.js +21 -0
  73. package/lib/common/telemetry-types.js.map +1 -0
  74. package/lib/common/test/recording-logger.d.ts +9 -0
  75. package/lib/common/test/recording-logger.d.ts.map +1 -0
  76. package/lib/common/test/recording-logger.js +36 -0
  77. package/lib/common/test/recording-logger.js.map +1 -0
  78. package/lib/node/index.d.ts +3 -0
  79. package/lib/node/index.d.ts.map +1 -0
  80. package/lib/node/index.js +21 -0
  81. package/lib/node/index.js.map +1 -0
  82. package/lib/node/telemetry-backend-module.d.ts +4 -0
  83. package/lib/node/telemetry-backend-module.d.ts.map +1 -0
  84. package/lib/node/telemetry-backend-module.js +37 -0
  85. package/lib/node/telemetry-backend-module.js.map +1 -0
  86. package/lib/node/telemetry-service-impl.d.ts +36 -0
  87. package/lib/node/telemetry-service-impl.d.ts.map +1 -0
  88. package/lib/node/telemetry-service-impl.js +208 -0
  89. package/lib/node/telemetry-service-impl.js.map +1 -0
  90. package/lib/node/telemetry-service-impl.spec.d.ts +2 -0
  91. package/lib/node/telemetry-service-impl.spec.d.ts.map +1 -0
  92. package/lib/node/telemetry-service-impl.spec.js +411 -0
  93. package/lib/node/telemetry-service-impl.spec.js.map +1 -0
  94. package/lib/node/telemetry-sink.d.ts +31 -0
  95. package/lib/node/telemetry-sink.d.ts.map +1 -0
  96. package/lib/node/telemetry-sink.js +21 -0
  97. package/lib/node/telemetry-sink.js.map +1 -0
  98. package/package.json +52 -0
  99. package/src/browser/browser-telemetry-service.spec.ts +193 -0
  100. package/src/browser/browser-telemetry-service.ts +69 -0
  101. package/src/browser/index.ts +17 -0
  102. package/src/browser/telemetry-frontend-module.ts +34 -0
  103. package/src/browser-only/browser-only-telemetry-service.spec.ts +44 -0
  104. package/src/browser-only/browser-only-telemetry-service.ts +23 -0
  105. package/src/browser-only/telemetry-frontend-only-module.ts +28 -0
  106. package/src/common/index.ts +22 -0
  107. package/src/common/telemetry-consent-provider.spec.ts +95 -0
  108. package/src/common/telemetry-consent-provider.ts +72 -0
  109. package/src/common/telemetry-preferences.spec.ts +56 -0
  110. package/src/common/telemetry-preferences.ts +91 -0
  111. package/src/common/telemetry-protocol.ts +100 -0
  112. package/src/common/telemetry-service.spec.ts +152 -0
  113. package/src/common/telemetry-service.ts +90 -0
  114. package/src/common/telemetry-topic.spec.ts +78 -0
  115. package/src/common/telemetry-topic.ts +60 -0
  116. package/src/common/telemetry-types.ts +21 -0
  117. package/src/common/test/recording-logger.ts +33 -0
  118. package/src/node/index.ts +18 -0
  119. package/src/node/telemetry-backend-module.ts +40 -0
  120. package/src/node/telemetry-service-impl.spec.ts +500 -0
  121. package/src/node/telemetry-service-impl.ts +214 -0
  122. package/src/node/telemetry-sink.ts +48 -0
@@ -0,0 +1,95 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { Emitter } from '@theia/core/lib/common';
19
+ import { Container } from '@theia/core/shared/inversify';
20
+ import {
21
+ PreferenceTelemetryConsentProvider, TelemetryConsentProvider, isKindAllowedByLevel
22
+ } from './telemetry-consent-provider';
23
+ import { TELEMETRY_FILTERS, TELEMETRY_LEVEL, TelemetryPreferences } from './telemetry-preferences';
24
+ import { TelemetryLevel } from './telemetry-types';
25
+
26
+ describe('telemetry consent provider', () => {
27
+ it('maps telemetry levels to event kinds', () => {
28
+ const kinds = ['usage', 'error', 'crash'] as const;
29
+ const levels: TelemetryLevel[] = ['off', 'crash', 'error', 'all'];
30
+ expect(levels.map(level => kinds.filter(kind => isKindAllowedByLevel(level, kind)))).to.deep.equal([
31
+ [],
32
+ ['crash'],
33
+ ['error', 'crash'],
34
+ ['usage', 'error', 'crash']
35
+ ]);
36
+ });
37
+
38
+ it('applies and reports preference changes before readiness', () => {
39
+ const changes = new Emitter<never>();
40
+ const ready = new Promise<void>(() => undefined);
41
+ let level: TelemetryLevel = 'off';
42
+ const preferences = {
43
+ get [TELEMETRY_LEVEL](): TelemetryLevel {
44
+ return level;
45
+ },
46
+ [TELEMETRY_FILTERS]: {},
47
+ ready,
48
+ onPreferenceChanged: changes.event
49
+ } as unknown as TelemetryPreferences;
50
+ const container = new Container();
51
+ container.bind(TelemetryPreferences).toConstantValue(preferences);
52
+ container.bind(TelemetryConsentProvider).to(PreferenceTelemetryConsentProvider).inSingletonScope();
53
+ const provider = container.get<TelemetryConsentProvider>(TelemetryConsentProvider);
54
+ const observed: TelemetryLevel[] = [];
55
+ provider.onDidChangeTelemetryLevel(value => observed.push(value));
56
+
57
+ level = 'error';
58
+ changes.fire({ preferenceName: TELEMETRY_LEVEL } as never);
59
+
60
+ expect(provider.level).to.equal('error');
61
+ expect(observed).to.deep.equal(['error']);
62
+ });
63
+
64
+ it('starts off, then reads the preference after readiness and fires on changes', async () => {
65
+ const changes = new Emitter<never>();
66
+ let resolveReady: () => void;
67
+ const ready = new Promise<void>(resolve => resolveReady = resolve);
68
+ let level: TelemetryLevel = 'error';
69
+ const preferences = {
70
+ get [TELEMETRY_LEVEL](): TelemetryLevel {
71
+ return level;
72
+ },
73
+ [TELEMETRY_FILTERS]: {},
74
+ ready,
75
+ onPreferenceChanged: changes.event
76
+ } as unknown as TelemetryPreferences;
77
+ const container = new Container();
78
+ container.bind(TelemetryPreferences).toConstantValue(preferences);
79
+ container.bind(TelemetryConsentProvider).to(PreferenceTelemetryConsentProvider).inSingletonScope();
80
+ const provider = container.get<TelemetryConsentProvider>(TelemetryConsentProvider);
81
+ const observed: TelemetryLevel[] = [];
82
+ provider.onDidChangeTelemetryLevel(value => observed.push(value));
83
+
84
+ expect(provider.level).to.equal('off');
85
+ resolveReady!();
86
+ await preferences.ready;
87
+ await Promise.resolve();
88
+ expect(provider.level).to.equal('error');
89
+
90
+ level = 'all';
91
+ changes.fire({ preferenceName: TELEMETRY_LEVEL } as never);
92
+ expect(provider.level).to.equal('all');
93
+ expect(observed).to.deep.equal(['error', 'all']);
94
+ });
95
+ });
@@ -0,0 +1,72 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { Emitter, Event } from '@theia/core/lib/common';
18
+ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
19
+ import { TELEMETRY_LEVEL, TelemetryPreferences } from './telemetry-preferences';
20
+ import { TelemetryEventKind } from './telemetry-service';
21
+ import { TelemetryLevel } from './telemetry-types';
22
+
23
+ /** @experimental */
24
+ export const TelemetryConsentProvider = Symbol('TelemetryConsentProvider');
25
+
26
+ /** @experimental */
27
+ export interface TelemetryConsentProvider {
28
+ readonly level: TelemetryLevel;
29
+ readonly onDidChangeTelemetryLevel: Event<TelemetryLevel>;
30
+ }
31
+
32
+ /** @experimental */
33
+ export function isKindAllowedByLevel(level: TelemetryLevel, kind: TelemetryEventKind): boolean {
34
+ return level === 'all'
35
+ || level === 'error' && (kind === 'error' || kind === 'crash')
36
+ || level === 'crash' && kind === 'crash';
37
+ }
38
+
39
+ /** @experimental */
40
+ @injectable()
41
+ export class PreferenceTelemetryConsentProvider implements TelemetryConsentProvider {
42
+
43
+ protected currentLevel: TelemetryLevel = 'off';
44
+ protected readonly onDidChangeTelemetryLevelEmitter = new Emitter<TelemetryLevel>();
45
+ readonly onDidChangeTelemetryLevel = this.onDidChangeTelemetryLevelEmitter.event;
46
+
47
+ @inject(TelemetryPreferences)
48
+ protected readonly preferences: TelemetryPreferences;
49
+
50
+ @postConstruct()
51
+ protected init(): void {
52
+ this.preferences.onPreferenceChanged(change => {
53
+ if (change.preferenceName === TELEMETRY_LEVEL) {
54
+ this.setLevel(this.preferences[TELEMETRY_LEVEL]);
55
+ }
56
+ });
57
+ this.preferences.ready.then(() => {
58
+ this.setLevel(this.preferences[TELEMETRY_LEVEL]);
59
+ }, () => undefined);
60
+ }
61
+
62
+ protected setLevel(level: TelemetryLevel): void {
63
+ if (level !== this.currentLevel) {
64
+ this.currentLevel = level;
65
+ this.onDidChangeTelemetryLevelEmitter.fire(level);
66
+ }
67
+ }
68
+
69
+ get level(): TelemetryLevel {
70
+ return this.currentLevel;
71
+ }
72
+ }
@@ -0,0 +1,56 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { PreferenceScope } from '@theia/core/lib/common/preferences/preference-scope';
19
+ import {
20
+ TELEMETRY_FILTERS,
21
+ TELEMETRY_LEVEL,
22
+ TelemetryPreferenceSchema
23
+ } from './telemetry-preferences';
24
+
25
+ describe('telemetry preferences', () => {
26
+ it('defines only the level and filters preference keys', () => {
27
+ expect(Object.keys(TelemetryPreferenceSchema.properties)).to.deep.equal([TELEMETRY_LEVEL, TELEMETRY_FILTERS]);
28
+ });
29
+
30
+ it('limits telemetry preferences to user scope', () => {
31
+ expect(TelemetryPreferenceSchema.scope).to.equal(PreferenceScope.User);
32
+ });
33
+
34
+ it('uses safe defaults and describes each telemetry level', () => {
35
+ expect(TelemetryPreferenceSchema.properties[TELEMETRY_LEVEL]).to.deep.include({
36
+ type: 'string',
37
+ enum: ['off', 'crash', 'error', 'all'],
38
+ enumDescriptions: [
39
+ 'Disables all product telemetry.',
40
+ 'Sends OS level crash reports.',
41
+ 'Sends general error telemetry and crash reports.',
42
+ 'Sends usage data, errors, and crash reports.'
43
+ ],
44
+ default: 'off'
45
+ });
46
+ expect(TelemetryPreferenceSchema.properties[TELEMETRY_FILTERS].default).to.deep.equal({});
47
+ });
48
+
49
+ it('defines filters as sink IDs mapped to arrays of topic patterns', () => {
50
+ expect(TelemetryPreferenceSchema.properties[TELEMETRY_FILTERS]).to.include({ type: 'object' });
51
+ expect(TelemetryPreferenceSchema.properties[TELEMETRY_FILTERS].additionalProperties).to.deep.equal({
52
+ type: 'array',
53
+ items: { type: 'string' }
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,91 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { nls } from '@theia/core/lib/common/nls';
18
+ import { PreferenceScope } from '@theia/core/lib/common/preferences/preference-scope';
19
+ import { createPreferenceProxy, PreferenceProxy } from '@theia/core/lib/common/preferences/preference-proxy';
20
+ import { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
21
+ import { PreferenceService } from '@theia/core/lib/common/preferences/preference-service';
22
+ import { interfaces } from '@theia/core/shared/inversify';
23
+ import { TelemetryLevel } from './telemetry-types';
24
+
25
+ /** @experimental */
26
+ export const TELEMETRY_LEVEL = 'telemetry.telemetryLevel';
27
+ /** @experimental */
28
+ export const TELEMETRY_FILTERS = 'telemetry.filters';
29
+
30
+ /** @experimental */
31
+ export type TelemetryFilters = Record<string, string[]>;
32
+
33
+ /** @experimental */
34
+ export interface TelemetryConfiguration {
35
+ [TELEMETRY_LEVEL]: TelemetryLevel;
36
+ [TELEMETRY_FILTERS]: TelemetryFilters;
37
+ }
38
+
39
+ /** @experimental */
40
+ export const TelemetryPreferenceSchema: PreferenceSchema = {
41
+ scope: PreferenceScope.User,
42
+ properties: {
43
+ [TELEMETRY_LEVEL]: {
44
+ type: 'string',
45
+ enum: ['off', 'crash', 'error', 'all'],
46
+ enumDescriptions: [
47
+ nls.localizeByDefault('Disables all product telemetry.'),
48
+ nls.localizeByDefault('Sends OS level crash reports.'),
49
+ nls.localizeByDefault('Sends general error telemetry and crash reports.'),
50
+ nls.localizeByDefault('Sends usage data, errors, and crash reports.')
51
+ ],
52
+ default: 'off',
53
+ description: nls.localizeByDefault('Controls the level of telemetry.')
54
+ },
55
+ [TELEMETRY_FILTERS]: {
56
+ type: 'object',
57
+ additionalProperties: {
58
+ type: 'array',
59
+ items: {
60
+ type: 'string'
61
+ }
62
+ },
63
+ default: {},
64
+ description: nls.localize('theia/telemetry/filters',
65
+ 'Restrict the topic patterns delivered to each telemetry sink. Missing entries allow all sink interests; empty arrays disable a sink.')
66
+ }
67
+ }
68
+ };
69
+
70
+ /** @experimental */
71
+ export const TelemetryPreferenceContribution = Symbol('TelemetryPreferenceContribution');
72
+ /** @experimental */
73
+ export const TelemetryPreferences = Symbol('TelemetryPreferences');
74
+ /** @experimental */
75
+ export type TelemetryPreferences = PreferenceProxy<TelemetryConfiguration>;
76
+
77
+ /** @experimental */
78
+ export function createTelemetryPreferences(preferences: PreferenceService, schema: PreferenceSchema = TelemetryPreferenceSchema): TelemetryPreferences {
79
+ return createPreferenceProxy(preferences, schema);
80
+ }
81
+
82
+ /** @experimental */
83
+ export function bindTelemetryPreferences(bind: interfaces.Bind): void {
84
+ bind(TelemetryPreferences).toDynamicValue(ctx => {
85
+ const preferences = ctx.container.get<PreferenceService>(PreferenceService);
86
+ const contribution = ctx.container.get<PreferenceContribution>(TelemetryPreferenceContribution);
87
+ return createTelemetryPreferences(preferences, contribution.schema);
88
+ }).inSingletonScope();
89
+ bind(TelemetryPreferenceContribution).toConstantValue({ schema: TelemetryPreferenceSchema });
90
+ bind(PreferenceContribution).toService(TelemetryPreferenceContribution);
91
+ }
@@ -0,0 +1,100 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { isObject } from '@theia/core/lib/common/types';
18
+ import {
19
+ TelemetryData, TelemetryEventKind, TelemetryReportOptions, TelemetryValue, isTelemetryData, isTelemetryEventKind, snapshotTelemetryData
20
+ } from './telemetry-service';
21
+ import { isValidTelemetryTopic } from './telemetry-topic';
22
+
23
+ /** @experimental */
24
+ export interface TelemetryEvent<T extends object = Record<string, TelemetryValue>> {
25
+ readonly topic: string;
26
+ readonly kind: TelemetryEventKind;
27
+ readonly data?: TelemetryData<T>;
28
+ readonly attributes?: TelemetryData<Record<string, TelemetryValue>>;
29
+ readonly session: string;
30
+ readonly timestamp: number;
31
+ }
32
+
33
+ /** @experimental */
34
+ export function isValidTelemetryEvent(event: unknown): event is TelemetryEvent {
35
+ if (!isObject<TelemetryEvent>(event)) {
36
+ return false;
37
+ }
38
+ return isValidTelemetryTopic(event.topic)
39
+ && isTelemetryEventKind(event.kind)
40
+ && typeof event.session === 'string'
41
+ && event.session.length > 0
42
+ && typeof event.timestamp === 'number'
43
+ && Number.isFinite(event.timestamp)
44
+ && (event.data === undefined || isTelemetryData(event.data))
45
+ && (event.attributes === undefined || isTelemetryData(event.attributes));
46
+ }
47
+
48
+ /** @experimental */
49
+ export function describeTelemetryTopic(topic: unknown): string {
50
+ return typeof topic === 'string' ? topic : '<invalid>';
51
+ }
52
+
53
+ /** @experimental */
54
+ export function describeTelemetryEventTopic(event: unknown): string {
55
+ if (!isObject(event)) {
56
+ return '<invalid>';
57
+ }
58
+ return describeTelemetryTopic(event.topic);
59
+ }
60
+
61
+ /** @experimental */
62
+ export function createTelemetryEvent<T extends object>(
63
+ topic: string,
64
+ session: string,
65
+ data?: TelemetryData<T>,
66
+ options?: TelemetryReportOptions
67
+ ): TelemetryEvent<T> {
68
+ return {
69
+ topic,
70
+ kind: options?.kind ?? 'usage',
71
+ data,
72
+ attributes: options?.attributes,
73
+ session,
74
+ timestamp: Date.now()
75
+ };
76
+ }
77
+
78
+ /** @experimental */
79
+ export function snapshotTelemetryEvent(event: TelemetryEvent): TelemetryEvent {
80
+ return Object.freeze({
81
+ topic: event.topic,
82
+ kind: event.kind,
83
+ data: snapshotTelemetryData(event.data),
84
+ attributes: snapshotTelemetryData(event.attributes),
85
+ session: event.session,
86
+ timestamp: event.timestamp
87
+ });
88
+ }
89
+
90
+ /** @experimental */
91
+ export const telemetryServicePath = '/services/telemetry';
92
+
93
+ /** @experimental */
94
+ export const TelemetryRpc = Symbol('TelemetryRpc');
95
+
96
+ /** @experimental */
97
+ export interface TelemetryRpc {
98
+ notifyEvent(event: TelemetryEvent): void;
99
+ getLocalSinkInterests(): Promise<string[]>;
100
+ }
@@ -0,0 +1,152 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import URI from '@theia/core/lib/common/uri';
19
+ import { isValidTelemetryEvent } from './telemetry-protocol';
20
+ import { TelemetryData, TelemetryService, isTelemetryData, isTelemetryEventKind, snapshotTelemetryData } from './telemetry-service';
21
+
22
+ interface ConsumerPayload {
23
+ action: string;
24
+ duration: number;
25
+ successful: boolean;
26
+ tags: string[];
27
+ measurements: readonly number[];
28
+ states: boolean[];
29
+ }
30
+
31
+ const typedPayload = {
32
+ action: 'open',
33
+ duration: 12,
34
+ successful: true,
35
+ tags: ['editor', 'open'],
36
+ measurements: [4, 8] as const,
37
+ states: [true, false]
38
+ } satisfies TelemetryData<ConsumerPayload>;
39
+
40
+ const compileTimeUsage = (service: TelemetryService): void => service.report<ConsumerPayload>('consumer/action', typedPayload, {
41
+ kind: 'error',
42
+ attributes: { source: 'consumer' }
43
+ });
44
+
45
+ describe('telemetry service contract', () => {
46
+ it('supports consumer-defined payload interfaces without an index signature', () => {
47
+ expect(compileTimeUsage).to.be.a('function');
48
+ });
49
+ it('validates telemetry event kinds', () => {
50
+ expect(['usage', 'error', 'crash'].every(isTelemetryEventKind)).to.be.true;
51
+ expect(isTelemetryEventKind('invalid')).to.be.false;
52
+ });
53
+
54
+ it('validates event kind, attributes, and session', () => {
55
+ const event = {
56
+ topic: 'consumer/action',
57
+ kind: 'error' as const,
58
+ data: typedPayload,
59
+ attributes: { source: 'consumer' },
60
+ session: 'frontend-session',
61
+ timestamp: 42
62
+ };
63
+
64
+ expect(isValidTelemetryEvent(event)).to.be.true;
65
+ expect(isValidTelemetryEvent({ ...event, kind: 'invalid' })).to.be.false;
66
+ expect(isValidTelemetryEvent({ ...event, session: '' })).to.be.false;
67
+ expect(isValidTelemetryEvent({ ...event, attributes: { nested: {} } })).to.be.false;
68
+ });
69
+
70
+ it('accepts plain flat payloads with primitive telemetry values', () => {
71
+ expect(isTelemetryData(typedPayload)).to.be.true;
72
+ expect(isTelemetryData({})).to.be.true;
73
+ });
74
+
75
+ it('accepts homogeneous primitive and empty arrays unchanged', () => {
76
+ const strings = ['editor', 'open'];
77
+ const numbers = [4, 8];
78
+ const booleans = [true, false];
79
+ const empty: string[] = [];
80
+ const data = { strings, numbers, booleans, empty };
81
+
82
+ expect(isTelemetryData(data)).to.be.true;
83
+ expect(data.strings).to.equal(strings);
84
+ expect(data.numbers).to.equal(numbers);
85
+ expect(data.booleans).to.equal(booleans);
86
+ expect(data.empty).to.equal(empty);
87
+ });
88
+
89
+ it('creates immutable snapshots without transforming accepted values', () => {
90
+ const strings = ['editor', 'open'];
91
+ const data = { action: 'open', duration: 12, successful: true, strings };
92
+
93
+ const snapshot = snapshotTelemetryData(data);
94
+
95
+ expect(snapshot).to.deep.equal(data);
96
+ expect(snapshot).not.to.equal(data);
97
+ expect(snapshot?.strings).not.to.equal(strings);
98
+ expect(Object.isFrozen(snapshot)).to.be.true;
99
+ expect(Object.isFrozen(snapshot?.strings)).to.be.true;
100
+ expect(snapshotTelemetryData(undefined)).to.be.undefined;
101
+ });
102
+
103
+ it('rejects non-object payloads', () => {
104
+ for (const value of [undefined, 'value', 1, true]) {
105
+ expect(isTelemetryData(value)).to.be.false;
106
+ }
107
+ // eslint-disable-next-line no-null/no-null
108
+ expect(isTelemetryData(null)).to.be.false;
109
+ });
110
+
111
+ it('rejects top-level arrays and nested objects', () => {
112
+ expect(isTelemetryData([])).to.be.false;
113
+ expect(isTelemetryData({ value: { nested: true } })).to.be.false;
114
+ });
115
+
116
+ it('rejects mixed, nested, and sparse arrays', () => {
117
+ const sparse = new Array<string>(2);
118
+ sparse[1] = 'value';
119
+
120
+ expect(isTelemetryData({ value: ['value', 1] })).to.be.false;
121
+ expect(isTelemetryData({ value: [['nested']] })).to.be.false;
122
+ expect(isTelemetryData({ value: sparse })).to.be.false;
123
+ });
124
+
125
+ it('rejects unsupported array elements', () => {
126
+ expect(isTelemetryData({ value: [{ nested: true }] })).to.be.false;
127
+ expect(isTelemetryData({ value: ['value', undefined] })).to.be.false;
128
+ expect(isTelemetryData({ value: ['value', () => 'value'] })).to.be.false;
129
+ expect(isTelemetryData({ value: ['value', Symbol('value')] })).to.be.false;
130
+ expect(isTelemetryData({ value: new Uint8Array([1, 2]) })).to.be.false;
131
+ });
132
+
133
+ it('rejects special and binary objects', () => {
134
+ expect(isTelemetryData(new Error('failure'))).to.be.false;
135
+ expect(isTelemetryData(new Date())).to.be.false;
136
+ expect(isTelemetryData(new URI('file:///workspace'))).to.be.false;
137
+ expect(isTelemetryData(new Uint8Array([1, 2]))).to.be.false;
138
+ });
139
+
140
+ it('rejects class instances even when their properties are flat', () => {
141
+ class Payload {
142
+ value = 'value';
143
+ }
144
+ expect(isTelemetryData(new Payload())).to.be.false;
145
+ });
146
+
147
+ it('rejects unsupported values in otherwise plain payloads', () => {
148
+ expect(isTelemetryData({ value: undefined })).to.be.false;
149
+ expect(isTelemetryData({ value: () => 'value' })).to.be.false;
150
+ expect(isTelemetryData({ value: Symbol('value') })).to.be.false;
151
+ });
152
+ });
@@ -0,0 +1,90 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { deepClone, deepFreeze } from '@theia/core/lib/common/objects';
18
+ import { isObject } from '@theia/core/lib/common/types';
19
+
20
+ /** @experimental */
21
+ export type TelemetryPrimitive = string | number | boolean;
22
+ /** @experimental */
23
+ export type TelemetryValue = TelemetryPrimitive | readonly string[] | readonly number[] | readonly boolean[];
24
+
25
+ /** @experimental */
26
+ export type TelemetryData<T extends object> = {
27
+ [K in keyof T]: T[K] extends TelemetryValue ? T[K] : never;
28
+ };
29
+
30
+ function isTelemetryPrimitive(value: unknown): value is TelemetryPrimitive {
31
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
32
+ }
33
+
34
+ function isTelemetryArray(value: unknown): value is readonly TelemetryPrimitive[] {
35
+ if (!Array.isArray(value)) {
36
+ return false;
37
+ }
38
+ if (value.length === 0) {
39
+ return true;
40
+ }
41
+ if (!Object.prototype.hasOwnProperty.call(value, 0) || !isTelemetryPrimitive(value[0])) {
42
+ return false;
43
+ }
44
+ const elementType = typeof value[0];
45
+ for (let index = 1; index < value.length; index++) {
46
+ if (!Object.prototype.hasOwnProperty.call(value, index) || !isTelemetryPrimitive(value[index]) || typeof value[index] !== elementType) {
47
+ return false;
48
+ }
49
+ }
50
+ return true;
51
+ }
52
+
53
+ function isTelemetryValue(value: unknown): value is TelemetryValue {
54
+ return isTelemetryPrimitive(value) || isTelemetryArray(value);
55
+ }
56
+
57
+ /** @experimental */
58
+ export function isTelemetryData(data: unknown): data is Record<string, TelemetryValue> {
59
+ if (!isObject(data) || Array.isArray(data) || Object.getPrototypeOf(data) !== Object.prototype) {
60
+ return false;
61
+ }
62
+ return Object.values(data).every(isTelemetryValue);
63
+ }
64
+
65
+ /** @experimental */
66
+ export function snapshotTelemetryData<T extends object>(data: TelemetryData<T> | undefined): TelemetryData<T> | undefined {
67
+ return data === undefined ? undefined : deepFreeze(deepClone(data));
68
+ }
69
+
70
+ /** @experimental */
71
+ export type TelemetryEventKind = 'usage' | 'error' | 'crash';
72
+
73
+ /** @experimental */
74
+ export function isTelemetryEventKind(value: unknown): value is TelemetryEventKind {
75
+ return value === 'usage' || value === 'error' || value === 'crash';
76
+ }
77
+
78
+ /** @experimental */
79
+ export interface TelemetryReportOptions {
80
+ readonly kind?: TelemetryEventKind;
81
+ readonly attributes?: TelemetryData<Record<string, TelemetryValue>>;
82
+ }
83
+
84
+ /** @experimental */
85
+ export const TelemetryService = Symbol('TelemetryService');
86
+
87
+ /** @experimental */
88
+ export interface TelemetryService {
89
+ report<T extends object>(topic: string, data?: TelemetryData<T>, options?: TelemetryReportOptions): void;
90
+ }