diff --git a/docs/src/content/docs/about/telemetry.mdx b/docs/src/content/docs/about/telemetry.mdx index 6ce68ed8..2ae85d68 100644 --- a/docs/src/content/docs/about/telemetry.mdx +++ b/docs/src/content/docs/about/telemetry.mdx @@ -2,5 +2,75 @@ title: Telemetry --- -As of now we do not collect any telemetry data from the extension that would be sent to any external service allowing us to understand how the extension is used. We are considering adding telemetry in the future to help us improve the extension and its features. If we do so, we will ensure that it is done in a way that respects user privacy and complies with relevant regulations. -Currently we only record the number of times the extension is installed and was it installed from the Marketplace website or the embedded VS Code Marketplace. This data is used to understand the popularity of the extension and to help us prioritize features and improvements. \ No newline at end of file +The SPFx Toolkit collects basic usage telemetry to help us understand how the extension is used and improve its features. We follow the VS Code Telemetry extension authors guide and use the recommended `@vscode/extension-telemetry` package to send anonymized telemetry data to Azure Application Insights. + +## What We Track + +We collect anonymized usage data for the following actions: + +### Actions View +- Create New Project +- Add Component to Project +- Upgrade Project +- Validate Project +- Rename Project +- Increase Project Version +- Grant API Permissions +- Deploy Project +- Set Form Customizer +- Scaffold CI/CD Workflow +- View Samples Gallery +- Use @spfx in GitHub Copilot +- Validate Local Setup +- Install Dependencies + +### Tasks View +- Build Project +- Bundle Project +- Clean Project +- Deploy to Azure Storage +- Package Project +- Publish Project +- Serve Project +- Test Project +- Trust Dev Cert +- Execute Terminal Command (for NPM scripts with script name tracking) + +### App Management (Environment View) +- Add Tenant App Catalog +- Add Site App Catalog +- Remove Site App Catalog +- Deploy App +- Retract App +- Enable App +- Disable App +- Install App +- Uninstall App +- Upgrade App +- Remove App +- Copy App +- Move App +- Remove Tenant Wide Extension +- Enable Tenant Wide Extension +- Disable Tenant Wide Extension +- Update Tenant Wide Extension + +### Authentication +- Microsoft 365 login and logout actions + +## Privacy and Data Protection + +- All telemetry data is anonymized and does not contain personal information +- No code, file paths, or project-specific data is collected +- Data is used solely to understand feature usage and prioritize improvements +- We comply with relevant privacy regulations and Microsoft's privacy policies +- You can disable telemetry through VS Code's standard telemetry settings + +## How to Disable Telemetry + +To disable telemetry collection, you can use VS Code's built-in telemetry settings: +1. Open VS Code Settings (File > Preferences > Settings) +2. Search for "Telemetry" +3. Set "Telemetry: Telemetry Level" to "off" + +This will disable telemetry for all VS Code extensions that respect the standard telemetry settings, including the SharePoint Framework Toolkit. \ No newline at end of file diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 5e5f46df..1ba440e2 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -11,6 +11,7 @@ "dependencies": { "@grconrad/vscode-extension-feedback": "^1.0.0", "@pnp/cli-microsoft365-spfx-toolkit": "1.4.0", + "@vscode/extension-telemetry": "^1.2.0", "node-forge": "1.3.1", "react-markdown": "10.1.0", "rehype-raw": "7.0.0", @@ -750,6 +751,115 @@ "dev": true, "license": "MIT" }, + "node_modules/@microsoft/1ds-core-js": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.10.tgz", + "integrity": "sha512-5fSZmkGwWkH+mrIA5M1GYPZdPM+SjXwCCl2Am7VhFoVwOBJNhRnwvIpAdzw6sFjiebN/rz+/YH0NdxztGZSa9Q==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + } + }, + "node_modules/@microsoft/1ds-post-js": { + "version": "4.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.10.tgz", + "integrity": "sha512-VSLjc9cT+Y+eTiSfYltJHJCejn8oYr0E6Pq2BMhOEO7F6IyLGYIxzKKvo78ze9x+iHX7KPTATcZ+PFgjGXuNqg==", + "license": "MIT", + "dependencies": { + "@microsoft/1ds-core-js": "4.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.10.tgz", + "integrity": "sha512-iolFLz1ocWAzIQqHIEjjov3gNTPkgFQ4ArHnBcJEYoffOGWlJt6copaevS5YPI5rHzmbySsengZ8cLJJBBrXzQ==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-common": "3.3.10", + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-common": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.10.tgz", + "integrity": "sha512-RVIenPIvNgZCbjJdALvLM4rNHgAFuHI7faFzHCgnI6S2WCUNGHeXlQTs9EUUrL+n2TPp9/cd0KKMILU5VVyYiA==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.10.tgz", + "integrity": "sha512-5yKeyassZTq2l+SAO4npu6LPnbS++UD+M+Ghjm9uRzoBwD8tumFx0/F8AkSVqbniSREd+ztH/2q2foewa2RZyg==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web-basic": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.10.tgz", + "integrity": "sha512-AZib5DAT3NU0VT0nLWEwXrnoMDDgZ/5S4dso01CNU5ELNxLdg+1fvchstlVdMy4FrAnxzs8Wf/GIQNFYOVgpAw==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-channel-js": "3.3.10", + "@microsoft/applicationinsights-common": "3.3.10", + "@microsoft/applicationinsights-core-js": "3.3.10", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.4 < 2.x", + "@nevware21/ts-utils": ">= 0.11.8 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" + } + }, "node_modules/@microsoft/fast-element": { "version": "1.14.0", "resolved": "https://registry.npmjs.org/@microsoft/fast-element/-/fast-element-1.14.0.tgz", @@ -808,6 +918,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@nevware21/ts-async": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.4.tgz", + "integrity": "sha512-IBTyj29GwGlxfzXw2NPnzty+w0Adx61Eze1/lknH/XIVdxtF9UnOpk76tnrHXWa6j84a1RR9hsOcHQPFv9qJjA==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.11.6 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.12.5.tgz", + "integrity": "sha512-JPQZWPKQJjj7kAftdEZL0XDFfbMgXCGiUAZe0d7EhLC3QlXTlZdSckGqqRIQ2QNl0VTEZyZUvRBw6Ednw089Fw==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -8754,6 +8879,20 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@vscode/extension-telemetry": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-1.2.0.tgz", + "integrity": "sha512-En6dTwfy5NFzSMibvOpx/lKq2jtgWuR4++KJbi3SpQ2iT8gm+PHo9868/scocW122KDwTxl4ruxZ7i4rHmJJnQ==", + "license": "MIT", + "dependencies": { + "@microsoft/1ds-core-js": "^4.3.10", + "@microsoft/1ds-post-js": "^4.3.10", + "@microsoft/applicationinsights-web-basic": "^3.3.10" + }, + "engines": { + "vscode": "^1.75.0" + } + }, "node_modules/@vscode/test-cli": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", @@ -21136,7 +21275,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tunnel": { diff --git a/package.json b/package.json index 62c1c498..c9f13ded 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ ], "license": "MIT", "main": "./dist/extension.js", + "aiConnectionString": "InstrumentationKey=137865b8-3555-4866-a2c9-7e35ead3d578;IngestionEndpoint=https://northeurope-2.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/;ApplicationId=d253fe5b-d38c-41f5-841d-afed5bcd9557", "contributes": { "chatParticipants": [ { @@ -73,7 +74,7 @@ ] } ], - "languageModelTools": [ + "languageModelTools": [ { "name": "install_spo_app", "tags": [ @@ -471,7 +472,7 @@ } } } - }, + }, { "name": "upgrade_spfx_project", "tags": [ @@ -1305,10 +1306,11 @@ "dependencies": { "@grconrad/vscode-extension-feedback": "^1.0.0", "@pnp/cli-microsoft365-spfx-toolkit": "1.4.0", + "@vscode/extension-telemetry": "^1.2.0", "node-forge": "1.3.1", "react-markdown": "10.1.0", + "rehype-raw": "7.0.0", "remark-gfm": "4.0.1", - "use-debounce": "10.0.4", - "rehype-raw": "7.0.0" + "use-debounce": "10.0.4" } } diff --git a/src/extension.ts b/src/extension.ts index e93b8371..55049157 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,11 +17,14 @@ import { ChatTools } from './chat/tools/ChatTools'; import { SpfxAppCLIActions } from './services/actions/SpfxAppCLIActions'; import { IncreaseVersionActions } from './services/actions/IncreaseVersionActions'; import { scheduleFeedbackChecks } from '@grconrad/vscode-extension-feedback'; +import { TelemetryService } from './utils/telemetry'; const feedbackFormUrl = 'https://forms.office.com/e/ZTfqAissqt'; +let telemetryService: TelemetryService; export async function activate(context: vscode.ExtensionContext) { + const activationStartTime = Date.now(); const chatParticipant = vscode.chat.createChatParticipant(CHAT_PARTICIPANT_NAME, PromptHandlers.handle); chatParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'assets', 'images', 'parker-pnp.png'); @@ -46,6 +49,23 @@ export async function activate(context: vscode.ExtensionContext) { PnPWebview.register(); + telemetryService = TelemetryService.getInstance(); + const packageJson = context.extension?.packageJSON; + const connectionString = packageJson?.aiConnectionString; + if (connectionString) { + telemetryService.initialize(context, connectionString); + + const activationDuration = Date.now() - activationStartTime; + + telemetryService.sendEvent('Extension Activated', { + version: context.extension.packageJSON.version, + nodeVersion: process.version, + platform: process.platform + }, { + activationTimeMs: activationDuration + }); + } + const channel = vscode.window.createOutputChannel('SPFx Toolkit Extension'); scheduleFeedbackChecks( @@ -130,4 +150,9 @@ export async function activate(context: vscode.ExtensionContext) { } // this method is called when your extension is deactivated -export function deactivate() { } +export function deactivate() { + if (telemetryService) { + telemetryService.sendEvent('Extension Deactivated'); + telemetryService.dispose(); + } +} diff --git a/src/panels/CommandPanel.ts b/src/panels/CommandPanel.ts index fd4fd034..d3767f82 100644 --- a/src/panels/CommandPanel.ts +++ b/src/panels/CommandPanel.ts @@ -308,7 +308,7 @@ export class CommandPanel { actionCommands.push(new ActionTreeItem('Scaffold CI/CD Workflow', '', { name: 'rocket', custom: false }, undefined, Commands.pipeline)); actionCommands.push(new ActionTreeItem('Add new component', '', { name: 'add', custom: false }, undefined, Commands.addToProject)); actionCommands.push(new ActionTreeItem('View samples', '', { name: 'library', custom: false }, undefined, Commands.samplesGallery)); - actionCommands.push(new ActionTreeItem('Use @spfx in GitHub Copilot ', '', { name: 'copilot', custom: false }, undefined, Commands.openCopilot)); + actionCommands.push(new ActionTreeItem('Use @spfx in GitHub Copilot', '', { name: 'copilot', custom: false }, undefined, Commands.openCopilot)); } else { actionCommands.push(new ActionTreeItem('Create new project', '', { name: 'add', custom: false }, undefined, Commands.createProject)); actionCommands.push(new ActionTreeItem('View samples', '', { name: 'library', custom: false }, undefined, Commands.samplesGallery)); @@ -319,7 +319,7 @@ export class CommandPanel { actionCommands.push(new ActionTreeItem('Validate local setup', '', { name: 'verified', custom: false }, undefined, Commands.checkDependencies)); actionCommands.push(new ActionTreeItem('Install dependencies', '', { name: 'cloud-download', custom: false }, undefined, Commands.installDependencies)); - actionCommands.push(new ActionTreeItem('Use @spfx in GitHub Copilot ', '', { name: 'copilot', custom: false }, undefined, Commands.openCopilot)); + actionCommands.push(new ActionTreeItem('Use @spfx in GitHub Copilot', '', { name: 'copilot', custom: false }, undefined, Commands.openCopilot)); } window.registerTreeDataProvider('pnp-view-actions', new ActionTreeDataProvider(actionCommands)); diff --git a/src/providers/AuthProvider.ts b/src/providers/AuthProvider.ts index 415d13a3..176c14ba 100644 --- a/src/providers/AuthProvider.ts +++ b/src/providers/AuthProvider.ts @@ -12,6 +12,7 @@ import { TerminalCommandExecuter } from '../services/executeWrappers/TerminalCom import { isValidGUID } from '../utils/validateGuid'; import { CliExecuter } from '../services/executeWrappers/CliCommandExecuter'; import { EntraAppRegistration } from '../services/actions/EntraAppRegistration'; +import { TelemetryService } from '../utils/telemetry'; export class M365AuthenticationSession implements AuthenticationSession { @@ -52,10 +53,10 @@ export class AuthProvider implements AuthenticationProvider, Disposable { ); subscriptions.push( - commands.registerCommand(Commands.login, AuthProvider.signIn) + commands.registerCommand(Commands.login, TelemetryService.withTelemetry(Commands.login, AuthProvider.signIn)) ); subscriptions.push( - commands.registerCommand(Commands.logout, AuthProvider.logout) + commands.registerCommand(Commands.logout, TelemetryService.withTelemetry(Commands.logout, AuthProvider.logout)) ); } diff --git a/src/services/actions/CliActions.ts b/src/services/actions/CliActions.ts index 9ced4693..98900dde 100644 --- a/src/services/actions/CliActions.ts +++ b/src/services/actions/CliActions.ts @@ -21,6 +21,7 @@ import * as fs from 'fs'; import { ActionTreeItem } from '../../providers/ActionTreeDataProvider'; import { timezones } from '../../constants/Timezones'; import { Dependencies } from './Dependencies'; +import { TelemetryService } from '../../utils/telemetry'; export class CliActions { @@ -29,34 +30,34 @@ export class CliActions { const subscriptions: Subscription[] = Extension.getInstance().subscriptions; subscriptions.push( - commands.registerCommand(Commands.upgradeProject, CliActions.upgrade) + commands.registerCommand(Commands.upgradeProject, TelemetryService.withTelemetry(Commands.upgradeProject, CliActions.upgrade)) ); subscriptions.push( - commands.registerCommand(Commands.deployProject, CliActions.deploy) + commands.registerCommand(Commands.deployProject, TelemetryService.withTelemetry(Commands.deployProject, CliActions.deploy)) ); subscriptions.push( - commands.registerCommand(Commands.validateProject, CliActions.validateProject) + commands.registerCommand(Commands.validateProject, TelemetryService.withTelemetry(Commands.validateProject, CliActions.validateProject)) ); subscriptions.push( - commands.registerCommand(Commands.renameProject, CliActions.renameProject) + commands.registerCommand(Commands.renameProject, TelemetryService.withTelemetry(Commands.renameProject, CliActions.renameProject)) ); subscriptions.push( - commands.registerCommand(Commands.grantAPIPermissions, CliActions.grantAPIPermissions) + commands.registerCommand(Commands.grantAPIPermissions, TelemetryService.withTelemetry(Commands.grantAPIPermissions, CliActions.grantAPIPermissions)) ); subscriptions.push( - commands.registerCommand(Commands.pipeline, CliActions.showGenerateWorkflowForm) + commands.registerCommand(Commands.pipeline, TelemetryService.withTelemetry(Commands.pipeline, CliActions.showGenerateWorkflowForm)) ); subscriptions.push( - commands.registerCommand(Commands.setFormCustomizer, CliActions.setFormCustomizer) + commands.registerCommand(Commands.setFormCustomizer, TelemetryService.withTelemetry(Commands.setFormCustomizer, CliActions.setFormCustomizer)) ); subscriptions.push( - commands.registerCommand(Commands.addTenantAppCatalog, CliActions.addTenantAppCatalog) + commands.registerCommand(Commands.addTenantAppCatalog, TelemetryService.withTelemetry(Commands.addTenantAppCatalog, CliActions.addTenantAppCatalog)) ); subscriptions.push( - commands.registerCommand(Commands.addSiteAppCatalog, CliActions.addSiteAppCatalog) + commands.registerCommand(Commands.addSiteAppCatalog, TelemetryService.withTelemetry(Commands.addSiteAppCatalog, CliActions.addSiteAppCatalog)) ); subscriptions.push( - commands.registerCommand(Commands.removeSiteAppCatalog, CliActions.removeSiteAppCatalog) + commands.registerCommand(Commands.removeSiteAppCatalog, TelemetryService.withTelemetry(Commands.removeSiteAppCatalog, CliActions.removeSiteAppCatalog)) ); } diff --git a/src/services/actions/CopilotActions.ts b/src/services/actions/CopilotActions.ts index 785043e0..d9351adb 100644 --- a/src/services/actions/CopilotActions.ts +++ b/src/services/actions/CopilotActions.ts @@ -5,6 +5,7 @@ import { Extension } from '../dataType/Extension'; import { Notifications } from '../dataType/Notifications'; import { sleep } from '../../utils/sleep'; import { Logger } from '../dataType/Logger'; +import { TelemetryService } from '../../utils/telemetry'; export class CopilotActions { @@ -13,7 +14,7 @@ export class CopilotActions { const subscriptions: Subscription[] = Extension.getInstance().subscriptions; subscriptions.push( - commands.registerCommand(Commands.openCopilot, CopilotActions.openCopilot) + commands.registerCommand(Commands.openCopilot, TelemetryService.withTelemetry(Commands.openCopilot, CopilotActions.openCopilot)) ); } diff --git a/src/services/actions/Dependencies.ts b/src/services/actions/Dependencies.ts index 2ac7b39f..3bce1ae4 100644 --- a/src/services/actions/Dependencies.ts +++ b/src/services/actions/Dependencies.ts @@ -10,6 +10,7 @@ import { execSync } from 'child_process'; import { TerminalCommandExecuter } from '../executeWrappers/TerminalCommandExecuter'; import { getExtensionSettings } from '../../utils'; import { CliActions } from './CliActions'; +import { TelemetryService } from '../../utils/telemetry'; export class Dependencies { @@ -17,10 +18,10 @@ export class Dependencies { const subscriptions: Subscription[] = Extension.getInstance().subscriptions; subscriptions.push( - commands.registerCommand(Commands.checkDependencies, Dependencies.validate) + commands.registerCommand(Commands.checkDependencies, TelemetryService.withTelemetry(Commands.checkDependencies, Dependencies.validate)) ); subscriptions.push( - commands.registerCommand(Commands.installDependencies, Dependencies.install) + commands.registerCommand(Commands.installDependencies, TelemetryService.withTelemetry(Commands.installDependencies, Dependencies.install)) ); } diff --git a/src/services/actions/IncreaseVersionActions.ts b/src/services/actions/IncreaseVersionActions.ts index e6a0cca3..4fb60e01 100644 --- a/src/services/actions/IncreaseVersionActions.ts +++ b/src/services/actions/IncreaseVersionActions.ts @@ -4,6 +4,7 @@ import { Subscription } from '../../models'; import { Extension } from '../dataType/Extension'; import { Notifications } from '../dataType/Notifications'; import { increaseVersion } from '../../utils/increaseVersion'; +import { TelemetryService } from '../../utils/telemetry'; export class IncreaseVersionActions { @@ -12,7 +13,7 @@ export class IncreaseVersionActions { const subscriptions: Subscription[] = Extension.getInstance().subscriptions; subscriptions.push( - commands.registerCommand(Commands.increaseVersion, IncreaseVersionActions.increaseVersion) + commands.registerCommand(Commands.increaseVersion, TelemetryService.withTelemetry(Commands.increaseVersion, IncreaseVersionActions.increaseVersion)) ); } diff --git a/src/services/actions/Scaffolder.ts b/src/services/actions/Scaffolder.ts index 0e232905..8637cdea 100644 --- a/src/services/actions/Scaffolder.ts +++ b/src/services/actions/Scaffolder.ts @@ -15,6 +15,7 @@ import { getExtensionSettings, getPlatform } from '../../utils'; import { PnPWebview } from '../../webview/PnPWebview'; import { Executer } from '../executeWrappers/CommandExecuter'; import { M365AgentsToolkitIntegration } from '../dataType/M365AgentsToolkitIntegration'; +import { TelemetryService } from '../../utils/telemetry'; export const PROJECT_FILE = 'project.pnp'; @@ -25,10 +26,10 @@ export class Scaffolder { const subscriptions: Subscription[] = Extension.getInstance().subscriptions; subscriptions.push( - commands.registerCommand(Commands.createProject, Scaffolder.showCreateProjectForm) + commands.registerCommand(Commands.createProject, TelemetryService.withTelemetry(Commands.createProject, Scaffolder.showCreateProjectForm)) ); subscriptions.push( - commands.registerCommand(Commands.addToProject, Scaffolder.showAddProjectForm) + commands.registerCommand(Commands.addToProject, TelemetryService.withTelemetry(Commands.addToProject, Scaffolder.showAddProjectForm)) ); subscriptions.push( commands.registerCommand(Commands.createProjectCopilot, Scaffolder.createProjectCopilot) diff --git a/src/services/actions/SpfxAppCLIActions.ts b/src/services/actions/SpfxAppCLIActions.ts index b400a76c..0973f57f 100644 --- a/src/services/actions/SpfxAppCLIActions.ts +++ b/src/services/actions/SpfxAppCLIActions.ts @@ -6,6 +6,7 @@ import { ActionTreeItem } from '../../providers/ActionTreeDataProvider'; import { Notifications } from '../dataType/Notifications'; import { CliExecuter } from '../executeWrappers/CliCommandExecuter'; import { EnvironmentInformation } from '../dataType/EnvironmentInformation'; +import { TelemetryService } from '../../utils/telemetry'; export class SpfxAppCLIActions { @@ -14,66 +15,66 @@ export class SpfxAppCLIActions { const subscriptions: Subscription[] = Extension.getInstance().subscriptions; subscriptions.push( - commands.registerCommand(Commands.deployAppCatalogApp, (node: ActionTreeItem) => + commands.registerCommand(Commands.deployAppCatalogApp, TelemetryService.withTelemetry(Commands.deployAppCatalogApp, (node: ActionTreeItem) => SpfxAppCLIActions.toggleAppDeployed(node, ContextKeys.deployApp, 'deploy') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.retractAppCatalogApp, (node: ActionTreeItem) => + commands.registerCommand(Commands.retractAppCatalogApp, TelemetryService.withTelemetry(Commands.retractAppCatalogApp, (node: ActionTreeItem) => SpfxAppCLIActions.toggleAppDeployed(node, ContextKeys.retractApp, 'retract') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.removeAppCatalogApp, SpfxAppCLIActions.removeAppCatalogApp) + commands.registerCommand(Commands.removeAppCatalogApp, TelemetryService.withTelemetry(Commands.removeAppCatalogApp, SpfxAppCLIActions.removeAppCatalogApp)) ); subscriptions.push( - commands.registerCommand(Commands.enableAppCatalogApp, (node: ActionTreeItem) => + commands.registerCommand(Commands.enableAppCatalogApp, TelemetryService.withTelemetry(Commands.enableAppCatalogApp, (node: ActionTreeItem) => SpfxAppCLIActions.toggleAppEnabled(node, ContextKeys.enableApp, 'enable') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.disableAppCatalogApp, (node: ActionTreeItem) => + commands.registerCommand(Commands.disableAppCatalogApp, TelemetryService.withTelemetry(Commands.disableAppCatalogApp, (node: ActionTreeItem) => SpfxAppCLIActions.toggleAppEnabled(node, ContextKeys.disableApp, 'disable') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.installAppCatalogApp, (node: ActionTreeItem) => + commands.registerCommand(Commands.installAppCatalogApp, TelemetryService.withTelemetry(Commands.installAppCatalogApp, (node: ActionTreeItem) => SpfxAppCLIActions.toggleAppInstalled(node, ContextKeys.installApp, 'install') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.uninstallAppCatalogApp, (node: ActionTreeItem) => + commands.registerCommand(Commands.uninstallAppCatalogApp, TelemetryService.withTelemetry(Commands.uninstallAppCatalogApp, (node: ActionTreeItem) => SpfxAppCLIActions.toggleAppInstalled(node, ContextKeys.uninstallApp, 'uninstall') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.upgradeAppCatalogApp, SpfxAppCLIActions.upgradeAppCatalogApp) + commands.registerCommand(Commands.upgradeAppCatalogApp, TelemetryService.withTelemetry(Commands.upgradeAppCatalogApp, SpfxAppCLIActions.upgradeAppCatalogApp)) ); subscriptions.push( - commands.registerCommand(Commands.removeTenantWideExtension, SpfxAppCLIActions.removeTenantWideExtension) + commands.registerCommand(Commands.removeTenantWideExtension, TelemetryService.withTelemetry(Commands.removeTenantWideExtension, SpfxAppCLIActions.removeTenantWideExtension)) ); subscriptions.push( - commands.registerCommand(Commands.enableTenantWideExtension, (node: ActionTreeItem) => + commands.registerCommand(Commands.enableTenantWideExtension, TelemetryService.withTelemetry(Commands.enableTenantWideExtension, (node: ActionTreeItem) => SpfxAppCLIActions.toggleExtensionEnabled(node, ContextKeys.enableTenantWideExtension, 'enable') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.disableTenantWideExtension, (node: ActionTreeItem) => + commands.registerCommand(Commands.disableTenantWideExtension, TelemetryService.withTelemetry(Commands.disableTenantWideExtension, (node: ActionTreeItem) => SpfxAppCLIActions.toggleExtensionEnabled(node, ContextKeys.disableTenantWideExtension, 'disable') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.updateTenantWideExtension, SpfxAppCLIActions.updateTenantWideExtension) + commands.registerCommand(Commands.updateTenantWideExtension, TelemetryService.withTelemetry(Commands.updateTenantWideExtension, SpfxAppCLIActions.updateTenantWideExtension)) ); subscriptions.push( - commands.registerCommand(Commands.copyAppCatalogApp, (node: ActionTreeItem) => + commands.registerCommand(Commands.copyAppCatalogApp, TelemetryService.withTelemetry(Commands.copyAppCatalogApp, (node: ActionTreeItem) => SpfxAppCLIActions.handleAppCatalogAppTransfer(node, ContextKeys.copyApp, 'copy') - ) + )) ); subscriptions.push( - commands.registerCommand(Commands.moveAppCatalogApp, (node: ActionTreeItem) => + commands.registerCommand(Commands.moveAppCatalogApp, TelemetryService.withTelemetry(Commands.moveAppCatalogApp, (node: ActionTreeItem) => SpfxAppCLIActions.handleAppCatalogAppTransfer(node, ContextKeys.moveApp, 'move') - ) + )) ); } diff --git a/src/services/executeWrappers/TerminalCommandExecuter.ts b/src/services/executeWrappers/TerminalCommandExecuter.ts index 236b8aea..046ce016 100644 --- a/src/services/executeWrappers/TerminalCommandExecuter.ts +++ b/src/services/executeWrappers/TerminalCommandExecuter.ts @@ -9,6 +9,7 @@ import { join } from 'path'; import { ServeConfig } from '../../models/ServeConfig'; import { readFileSync } from 'fs'; import { Logger } from '../dataType/Logger'; +import { TelemetryService } from '../../utils/telemetry'; interface ShellSetting { @@ -22,34 +23,34 @@ export class TerminalCommandExecuter { public static register() { const subscriptions: Subscription[] = Extension.getInstance().subscriptions; subscriptions.push( - commands.registerCommand(Commands.serveProject, TerminalCommandExecuter.serveProject) + commands.registerCommand(Commands.serveProject, TelemetryService.withTelemetry(Commands.serveProject, TerminalCommandExecuter.serveProject)) ); subscriptions.push( - commands.registerCommand(Commands.bundleProject, TerminalCommandExecuter.bundleProject) + commands.registerCommand(Commands.bundleProject, TelemetryService.withTelemetry(Commands.bundleProject, TerminalCommandExecuter.bundleProject)) ); subscriptions.push( - commands.registerCommand(Commands.packageProject, TerminalCommandExecuter.packageProject) + commands.registerCommand(Commands.packageProject, TelemetryService.withTelemetry(Commands.packageProject, TerminalCommandExecuter.packageProject)) ); subscriptions.push( - commands.registerCommand(Commands.publishProject, TerminalCommandExecuter.publishProject) + commands.registerCommand(Commands.publishProject, TelemetryService.withTelemetry(Commands.publishProject, TerminalCommandExecuter.publishProject)) ); subscriptions.push( - commands.registerCommand(Commands.executeTerminalCommand, TerminalCommandExecuter.runCommand) + commands.registerCommand(Commands.executeTerminalCommand, TelemetryService.withTelemetry(Commands.executeTerminalCommand, TerminalCommandExecuter.runCommand)) ); subscriptions.push( - commands.registerCommand(Commands.cleanProject, TerminalCommandExecuter.cleanProject) + commands.registerCommand(Commands.cleanProject, TelemetryService.withTelemetry(Commands.cleanProject, TerminalCommandExecuter.cleanProject)) ); subscriptions.push( - commands.registerCommand(Commands.buildProject, TerminalCommandExecuter.buildProject) + commands.registerCommand(Commands.buildProject, TelemetryService.withTelemetry(Commands.buildProject, TerminalCommandExecuter.buildProject)) ); subscriptions.push( - commands.registerCommand(Commands.testProject, TerminalCommandExecuter.testProject) + commands.registerCommand(Commands.testProject, TelemetryService.withTelemetry(Commands.testProject, TerminalCommandExecuter.testProject)) ); subscriptions.push( - commands.registerCommand(Commands.trustDevCert, TerminalCommandExecuter.trustDevCert) + commands.registerCommand(Commands.trustDevCert, TelemetryService.withTelemetry(Commands.trustDevCert, TerminalCommandExecuter.trustDevCert)) ); subscriptions.push( - commands.registerCommand(Commands.deployToAzureStorage, TerminalCommandExecuter.deployToAzureStorage) + commands.registerCommand(Commands.deployToAzureStorage, TelemetryService.withTelemetry(Commands.deployToAzureStorage, TerminalCommandExecuter.deployToAzureStorage)) ); TerminalCommandExecuter.initShellPath(); @@ -256,28 +257,28 @@ export class TerminalCommandExecuter { /** * Builds the project by executing the Gulp build command. - */ + */ private static buildProject() { commands.executeCommand(Commands.executeTerminalCommand, 'gulp build'); } /** * Tests the project by executing the Gulp test command. - */ + */ private static testProject() { commands.executeCommand(Commands.executeTerminalCommand, 'gulp test'); } /** * Trusts the development certificate by executing the Gulp trust-dev-cert command. - */ + */ private static trustDevCert() { commands.executeCommand(Commands.executeTerminalCommand, 'gulp trust-dev-cert'); } /** * Deploys to Azure CDN by executing the Gulp deploy-to-azure-storage command. - */ + */ private static deployToAzureStorage() { commands.executeCommand(Commands.executeTerminalCommand, 'gulp deploy-azure-storage'); } diff --git a/src/utils/telemetry.ts b/src/utils/telemetry.ts new file mode 100644 index 00000000..6cf77e85 --- /dev/null +++ b/src/utils/telemetry.ts @@ -0,0 +1,135 @@ +import { ExtensionContext, commands, env } from 'vscode'; +import { TelemetryReporter } from '@vscode/extension-telemetry'; +import { Commands } from '../constants'; + +export const TELEMETRY_EVENTS = { + // accountTreeView + [Commands.login]: 'Login', + [Commands.logout]: 'Logout', + + // tasksTreeView + [Commands.buildProject]: 'Build Project', + [Commands.bundleProject]: 'Bundle Project', + [Commands.cleanProject]: 'Clean Project', + [Commands.deployToAzureStorage]: 'Deploy to Azure Storage', + [Commands.packageProject]: 'Package Project', + [Commands.publishProject]: 'Publish Project', + [Commands.serveProject]: 'Serve Project', + [Commands.testProject]: 'Test Project', + [Commands.trustDevCert]: 'Trust Dev Cert', + [Commands.executeTerminalCommand]: 'Execute Terminal Command', + + // actionsTreeView + [Commands.upgradeProject]: 'Upgrade Project', + [Commands.validateProject]: 'Validate Project', + [Commands.renameProject]: 'Rename Project', + [Commands.increaseVersion]: 'Increase Project Version', + [Commands.grantAPIPermissions]: 'Grant API Permissions', + [Commands.deployProject]: 'Deploy Project', + [Commands.setFormCustomizer]: 'Set Form Customizer', + [Commands.pipeline]: 'Scaffold CI/CD Workflow', + [Commands.addToProject]: 'Add Component to Project', + [Commands.samplesGallery]: 'View Samples Gallery', + [Commands.openCopilot]: 'Use @spfx in GitHub Copilot', + [Commands.createProject]: 'Create New Project', + [Commands.checkDependencies]: 'Validate Local Setup', + [Commands.installDependencies]: 'Install Dependencies', + + // environmentTreeView + [Commands.addTenantAppCatalog]: 'Add Tenant App Catalog', + [Commands.removeTenantWideExtension]: 'Remove Tenant Wide Extension', + [Commands.enableTenantWideExtension]: 'Enable Tenant Wide Extension', + [Commands.disableTenantWideExtension]: 'Disable Tenant Wide Extension', + [Commands.updateTenantWideExtension]: 'Update Tenant Wide Extension', + [Commands.copyAppCatalogApp]: 'Copy App', + [Commands.deployAppCatalogApp]: 'Deploy App', + [Commands.disableAppCatalogApp]: 'Disable App', + [Commands.enableAppCatalogApp]: 'Enable App', + [Commands.installAppCatalogApp]: 'Install App', + [Commands.moveAppCatalogApp]: 'Move App', + [Commands.removeAppCatalogApp]: 'Remove App', + [Commands.retractAppCatalogApp]: 'Retract App', + [Commands.uninstallAppCatalogApp]: 'Uninstall App', + [Commands.upgradeAppCatalogApp]: 'Upgrade App', + [Commands.addSiteAppCatalog]: 'Add Site App Catalog', + [Commands.removeSiteAppCatalog]: 'Remove Site App Catalog', +} as const; + +export class TelemetryService { + private static instance: TelemetryService; + public reporter: TelemetryReporter | undefined; + private static isInternalCall: boolean = false; + + private constructor() { } + + public static getInstance(): TelemetryService { + if (!TelemetryService.instance) { + TelemetryService.instance = new TelemetryService(); + } + + return TelemetryService.instance; + } + + public initialize(context: ExtensionContext, connectionString: string): void { + if (!this.reporter) { + this.reporter = new TelemetryReporter(connectionString); + context.subscriptions.push(this.reporter); + } + } + + public sendEvent(eventName: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }): void { + if (this.reporter && env.isTelemetryEnabled) { + this.reporter.sendTelemetryEvent(eventName, properties, measurements); + } + } + + public sendErrorEvent(eventName: string, properties?: { [key: string]: string }, measurements?: { [key: string]: number }): void { + if (this.reporter && env.isTelemetryEnabled) { + this.reporter.sendTelemetryErrorEvent(eventName, properties, measurements); + } + } + + public dispose(): void { + if (this.reporter) { + this.reporter.dispose(); + } + } + + /** + * A command handler with telemetry tracking. + * @param commandIdOrEventName The command ID to be referenced from TELEMETRY_EVENTS, or a custom event name + * @param originalCommand The original command + * @param properties Properties to include in telemetry + * @param measurements Measurements to include in telemetry + * @returns A wrapped handler that sends telemetry before executing the original handler + */ + public static withTelemetry(commandIdOrEventName: string, originalCommand: Function, properties?: Record, measurements?: Record) { + const eventName = TELEMETRY_EVENTS[commandIdOrEventName] || commandIdOrEventName; + + return async (...args: any[]) => { + const telemetryService = TelemetryService.getInstance(); + + if (telemetryService.reporter && !TelemetryService.isInternalCall) { + const telemetryProperties: Record = { ...properties }; + + // additional properties for npm scripts + if (originalCommand.name === 'runCommand' && args[0] && typeof args[0] === 'string') { + const npmCommand = args[0] as string; + if (npmCommand.startsWith('npm run ')) { + telemetryProperties.script = npmCommand.replace('npm run ', ''); + } + } + + telemetryService.sendEvent(eventName, telemetryProperties, measurements); + } + + // to avoid twice tracking triggered by clean, build, bundle, etc. which internally calls executeTerminalCommand + TelemetryService.isInternalCall = true; + try { + return await originalCommand(...args); + } finally { + TelemetryService.isInternalCall = false; + } + }; + } +} diff --git a/src/webview/PnPWebview.ts b/src/webview/PnPWebview.ts index 02053723..1478ffd1 100644 --- a/src/webview/PnPWebview.ts +++ b/src/webview/PnPWebview.ts @@ -6,6 +6,7 @@ import { Logger } from '../services/dataType/Logger'; import { Scaffolder } from '../services/actions/Scaffolder'; import { CliActions } from '../services/actions/CliActions'; import { EntraAppRegistration } from '../services/actions/EntraAppRegistration'; +import { TelemetryService } from '../utils/telemetry'; export class PnPWebview { @@ -17,7 +18,7 @@ export class PnPWebview { const subscriptions = ext.subscriptions; subscriptions.push( - commands.registerCommand(Commands.samplesGallery, () => PnPWebview.open(WebViewType.samplesGallery)) + commands.registerCommand(Commands.samplesGallery, TelemetryService.withTelemetry(Commands.samplesGallery, () => PnPWebview.open(WebViewType.samplesGallery))) ); }