Skip to content

Commit 32ca985

Browse files
authored
Merge pull request #199 from matthieu-crouzet/feat/set-up-telemetry
feat: set up telemetry
2 parents 8bd14eb + 14684a6 commit 32ca985

File tree

6 files changed

+651
-2
lines changed

6 files changed

+651
-2
lines changed

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Marketplace and registry for Copilot prompt bundles in VS Code.
1111
- **[Repository Installation](user-guide/repository-installation.md)** — Team-shared configurations via Git
1212
- **[Sources](user-guide/sources.md)** — Managing bundle sources
1313
- **[Profiles and Hubs](user-guide/profiles-and-hubs.md)** — Profile and Hub management
14-
- **[Configuration](user-guide/configuration.md)** — Extension settings
14+
- **[Configuration](user-guide/configuration.md)** — Extension settings and telemetry
1515
- **[Troubleshooting](user-guide/troubleshooting.md)** — Common issues
1616

1717
---

docs/user-guide/configuration.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ Access: `File → Preferences → Settings → Extensions → Prompt Registry`
1414
| `promptregistry.updateCheck.autoUpdate` | Auto-install updates | `false` |
1515
| `promptregistry.updateCheck.cacheTTL` | Cache TTL (ms) | `300000` |
1616

17+
## Telemetry
18+
19+
Telemetry respects VS Code's built-in telemetry setting. To enable or disable it:
20+
21+
1. Open **File → Preferences → Settings** (or `Cmd+,` / `Ctrl+,`)
22+
2. Search for `telemetry.telemetryLevel`
23+
3. Choose a level:
24+
25+
| Level | Effect on Prompt Registry |
26+
|-------|--------------------------|
27+
| `all` | Telemetry events are collected |
28+
| `error` | Only error events are collected |
29+
| `crash` | Telemetry is disabled |
30+
| `off` | Telemetry is disabled |
31+
32+
You can also set it in `settings.json`:
33+
34+
```json
35+
{
36+
"telemetry.telemetryLevel": "all"
37+
}
38+
```
39+
40+
Enabling telemetry helps us understand how the extension is used so we can focus on the features that matter most.
41+
1742
## Export/Import Settings
1843

1944
- **Export**: Registry Explorer toolbar → Export button

src/extension.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
import { ApmRuntimeManager } from './services/ApmRuntimeManager';
4343
import { OlafRuntimeManager } from './services/OlafRuntimeManager';
4444
import { SetupStateManager, SetupState } from './services/SetupStateManager';
45+
import { TelemetryService } from './services/TelemetryService';
4546
import { MigrationRegistry } from './services/MigrationRegistry';
4647
import { runSourceIdNormalizationMigration } from './migrations/sourceIdNormalizationMigration';
4748

@@ -87,6 +88,9 @@ export class PromptRegistryExtension {
8788
private notificationManager: NotificationManager | undefined;
8889
private autoUpdateService: AutoUpdateService | undefined;
8990

91+
// Telemetry
92+
private telemetryService: TelemetryService | undefined;
93+
9094
// Repository-level installation services
9195
private lockfileManager: LockfileManager | undefined;
9296
private repositoryActivationService: RepositoryActivationService | undefined;
@@ -160,6 +164,9 @@ export class PromptRegistryExtension {
160164
// Initialize update notification system
161165
await this.initializeUpdateSystem();
162166

167+
// Initialize telemetry service
168+
this.initializeTelemetry();
169+
163170
// Initialize repository-level installation services
164171
await this.initializeRepositoryServices();
165172

@@ -204,6 +211,9 @@ export class PromptRegistryExtension {
204211
this.disposables.forEach(disposable => disposable.dispose());
205212
this.disposables = [];
206213

214+
// Dispose telemetry event subscriptions
215+
this.telemetryService?.dispose();
216+
207217
// Dispose update scheduler
208218
this.updateScheduler?.dispose();
209219

@@ -226,6 +236,18 @@ export class PromptRegistryExtension {
226236
}
227237
}
228238

239+
/**
240+
* Initialize telemetry service and subscribe to bundle lifecycle events.
241+
*/
242+
private initializeTelemetry(): void {
243+
try {
244+
this.telemetryService = TelemetryService.getInstance();
245+
this.telemetryService.subscribeToRegistryEvents(this.registryManager);
246+
} catch (error) {
247+
this.logger.warn('Failed to initialize telemetry service (non-fatal)', error as Error);
248+
}
249+
}
250+
229251
/**
230252
* Run data migrations (idempotent).
231253
* Migrations use MigrationRegistry (globalState) to track completion.

src/services/TelemetryService.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import * as vscode from 'vscode';
2+
import { Logger } from '../utils/logger';
3+
import { InstalledBundle, Profile, RegistrySource, SourceSyncedEvent } from '../types/registry';
4+
import { RegistryManager } from './RegistryManager';
5+
6+
/**
7+
* Telemetry service that tracks bundle lifecycle events using VS Code's
8+
* built-in TelemetryLogger infrastructure.
9+
*
10+
* Uses `vscode.env.createTelemetryLogger` with a Logger-backed sender,
11+
* so VS Code automatically respects the user's telemetry preferences
12+
* (`telemetry.telemetryLevel`):
13+
* - `all` → usage + error events are sent
14+
* - `error` → only error events are sent
15+
* - `crash` / `off` → nothing is sent
16+
*
17+
* The TelemetryLogger gates calls to the sender based on `isUsageEnabled`
18+
* and `isErrorsEnabled`, so the sender itself logs unconditionally.
19+
* Currently logs events locally — no data is sent to external servers.
20+
*/
21+
export class TelemetryService {
22+
private static instance: TelemetryService;
23+
private readonly telemetryLogger: vscode.TelemetryLogger;
24+
private disposables: vscode.Disposable[] = [];
25+
26+
private constructor() {
27+
const logger = Logger.getInstance();
28+
29+
const sender: vscode.TelemetrySender = {
30+
sendEventData(eventName: string, data?: Record<string, any>): void {
31+
logger.info(`[Telemetry] ${eventName} ${JSON.stringify(data)}`);
32+
},
33+
sendErrorData(error: Error, data?: Record<string, any>): void {
34+
logger.error(`[Telemetry] ${error.message} ${JSON.stringify(data)}`);
35+
}
36+
};
37+
38+
this.telemetryLogger = vscode.env.createTelemetryLogger(sender);
39+
this.telemetryLogger.logUsage('telemetryService.started');
40+
this.disposables.push(this.telemetryLogger);
41+
}
42+
43+
public static getInstance(): TelemetryService {
44+
if (!TelemetryService.instance) {
45+
TelemetryService.instance = new TelemetryService();
46+
}
47+
return TelemetryService.instance;
48+
}
49+
50+
/**
51+
* Reset the singleton instance (for testing only).
52+
*/
53+
public static resetInstance(): void {
54+
if (TelemetryService.instance) {
55+
TelemetryService.instance.dispose();
56+
}
57+
TelemetryService.instance = undefined!;
58+
}
59+
60+
/**
61+
* Subscribe to RegistryManager bundle lifecycle events.
62+
* Subscriptions are owned by this service and cleaned up on dispose().
63+
*/
64+
public subscribeToRegistryEvents(registryManager: RegistryManager): void {
65+
this.disposables.push(
66+
// Bundle events
67+
registryManager.onBundleInstalled((bundle) => this.trackBundleEvent('bundle.installed', bundle)),
68+
registryManager.onBundleUninstalled((bundleId) => this.telemetryLogger.logUsage('bundle.uninstalled', { bundleId })),
69+
registryManager.onBundleUpdated((bundle) => this.trackBundleEvent('bundle.updated', bundle)),
70+
registryManager.onBundlesInstalled((bundles) => this.telemetryLogger.logUsage('bundles.installed', { count: bundles.length, bundleIds: bundles.map(b => b.bundleId) })),
71+
registryManager.onBundlesUninstalled((bundleIds) => this.telemetryLogger.logUsage('bundles.uninstalled', { count: bundleIds.length, bundleIds })),
72+
// Profile events
73+
registryManager.onProfileActivated((profile) => this.trackProfileEvent('profile.activated', profile)),
74+
registryManager.onProfileDeactivated((profileId) => this.telemetryLogger.logUsage('profile.deactivated', { profileId })),
75+
registryManager.onProfileCreated((profile) => this.trackProfileEvent('profile.created', profile)),
76+
registryManager.onProfileUpdated((profile) => this.trackProfileEvent('profile.updated', profile)),
77+
registryManager.onProfileDeleted((profileId) => this.telemetryLogger.logUsage('profile.deleted', { profileId })),
78+
// Source events
79+
registryManager.onSourceAdded((source) => this.trackSourceEvent('source.added', source)),
80+
registryManager.onSourceRemoved((sourceId) => this.telemetryLogger.logUsage('source.removed', { sourceId })),
81+
registryManager.onSourceUpdated((sourceId) => this.telemetryLogger.logUsage('source.updated', { sourceId })),
82+
registryManager.onSourceSynced((event) => this.trackSourceSyncedEvent('source.synced', event)),
83+
// Preference events
84+
registryManager.onAutoUpdatePreferenceChanged((event) => this.telemetryLogger.logUsage('autoUpdate.preferenceChanged', { bundleId: event.bundleId, enabled: event.enabled })),
85+
registryManager.onRepositoryBundlesChanged(() => this.telemetryLogger.logUsage('repository.bundlesChanged')),
86+
);
87+
}
88+
89+
private trackBundleEvent(eventName: string, bundle: InstalledBundle): void {
90+
this.telemetryLogger.logUsage(eventName, {
91+
bundleId: bundle.bundleId,
92+
version: bundle.version,
93+
scope: bundle.scope,
94+
sourceType: bundle.sourceType ?? 'unknown'
95+
});
96+
}
97+
98+
private trackProfileEvent(eventName: string, profile: Profile): void {
99+
this.telemetryLogger.logUsage(eventName, {
100+
profileId: profile.id,
101+
name: profile.name
102+
});
103+
}
104+
105+
private trackSourceEvent(eventName: string, source: RegistrySource): void {
106+
this.telemetryLogger.logUsage(eventName, {
107+
sourceId: source.id,
108+
type: source.type
109+
});
110+
}
111+
112+
private trackSourceSyncedEvent(eventName: string, event: SourceSyncedEvent): void {
113+
this.telemetryLogger.logUsage(eventName, {
114+
sourceId: event.sourceId,
115+
bundleCount: event.bundleCount
116+
});
117+
}
118+
119+
/**
120+
* Dispose the telemetry logger and all event subscriptions.
121+
*/
122+
public dispose(): void {
123+
this.telemetryLogger.logUsage('telemetryService.stopped');
124+
this.disposables.forEach(d => d.dispose());
125+
this.disposables = [];
126+
}
127+
}

test/mocha.setup.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,34 @@ const vscode = {
245245
sessionId: 'mock-session-id',
246246
remoteName: undefined,
247247
shell: '/bin/bash',
248-
openExternal: (uri) => Promise.resolve(true)
248+
isTelemetryEnabled: true,
249+
openExternal: (uri) => Promise.resolve(true),
250+
createTelemetryLogger: (sender, options) => {
251+
let _isUsageEnabled = true;
252+
return {
253+
get isUsageEnabled() { return _isUsageEnabled; },
254+
set isUsageEnabled(v) { _isUsageEnabled = v; },
255+
get isErrorsEnabled() { return _isUsageEnabled; },
256+
onDidChangeEnableStates: () => ({ dispose: () => {} }),
257+
logUsage: (eventName, data) => {
258+
if (_isUsageEnabled) {
259+
sender.sendEventData(eventName, data);
260+
}
261+
},
262+
logError: (eventNameOrError, data) => {
263+
if (_isUsageEnabled) {
264+
if (eventNameOrError instanceof Error) {
265+
sender.sendErrorData(eventNameOrError, data);
266+
} else {
267+
sender.sendEventData(eventNameOrError, data);
268+
}
269+
}
270+
},
271+
dispose: () => {
272+
if (sender.flush) { sender.flush(); }
273+
}
274+
};
275+
}
249276
},
250277
ConfigurationTarget: {
251278
Global: 1,

0 commit comments

Comments
 (0)