-
Notifications
You must be signed in to change notification settings - Fork 76
Expand file tree
/
Copy pathregisterCommand.ts
More file actions
118 lines (105 loc) · 5.64 KB
/
registerCommand.ts
File metadata and controls
118 lines (105 loc) · 5.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { commands, Uri } from 'vscode';
import * as types from '../index';
import { callWithTelemetryAndErrorHandling } from './callWithTelemetryAndErrorHandling';
import { ext } from './extensionVariables';
import { addTreeItemValuesToMask } from './tree/addTreeItemValuesToMask';
import { AzExtTreeItem } from './tree/AzExtTreeItem';
import { unwrapArgs } from '@microsoft/vscode-azureresources-api';
import { parseError } from './parseError';
function isTreeElementBase(object?: unknown): object is types.TreeElementBase {
return typeof object === 'object' && object !== null && 'getTreeItem' in object;
}
// if the firstArg has a resource property, it is a ResourceGroupsItem from the resource groups extension
function isResourceGroupsItem(object?: unknown): object is types.ResourceGroupsItem {
return typeof object === 'object' && object !== null && 'resource' in object;
}
// resource has a lot of properties but for the sake of telemetry, we are interested in the id and subscriptionId
type Resource = {
id?: string;
subscription?: {
subscriptionId?: string;
};
}
export function registerCommand(commandId: string, callback: (context: types.IActionContext, ...args: unknown[]) => unknown, debounce?: number, telemetryId?: string): void {
let lastClickTime: number | undefined; /* Used for debounce */
ext.context.subscriptions.push(commands.registerCommand(commandId, async (...args: unknown[]): Promise<unknown> => {
if (debounce) { /* Only check for debounce if registered command specifies */
if (debounceCommand(debounce, lastClickTime)) {
return;
}
lastClickTime = Date.now();
}
return await callWithTelemetryAndErrorHandling(
telemetryId || commandId,
async (context: types.IActionContext) => {
if (args.length > 0) {
try {
await setTelemetryProperties(context, args);
} catch (e: unknown) {
const error = parseError(e);
// if we fail to set telemetry properties, we don't want to throw an error and prevent the command from executing
ext.outputChannel.appendLine(`registerCommand: Failed to set telemetry properties: ${e}`);
context.telemetry.properties.telemetryError = error.message;
}
}
return callback(context, ...args);
}
);
}));
}
function debounceCommand(debounce: number, lastClickTime?: number): boolean {
if (lastClickTime && lastClickTime + debounce > Date.now()) {
return true;
}
return false;
}
async function setTelemetryProperties(context: types.IActionContext, args: unknown[]): Promise<void> {
const firstArg: unknown = args[0];
if (firstArg instanceof Uri) {
context.telemetry.properties.contextValue = 'Uri';
} else if (firstArg && typeof firstArg === 'object' && 'contextValue' in firstArg && typeof firstArg.contextValue === 'string') {
context.telemetry.properties.contextValue = firstArg.contextValue;
} else if (isTreeElementBase(firstArg)) {
context.telemetry.properties.contextValue = (await firstArg.getTreeItem()).contextValue;
}
// handles items from the resource groups extension
if (isResourceGroupsItem(firstArg)) {
context.telemetry.properties.resourceId = (firstArg as { resource: Resource })?.resource?.id;
context.telemetry.properties.subscriptionId = (firstArg as { resource: Resource })?.resource?.subscription?.subscriptionId;
}
// handles items from v1 extensions
for (const arg of args) {
if (arg instanceof AzExtTreeItem) {
try {
context.telemetry.properties.resourceId = arg.id;
// it's possible that if subscription is not set on AzExtTreeItems, an error is thrown
// see https://github.com/microsoft/vscode-azuretools/blob/cc1feb3a819dd503eb59ebcc1a70051d4e9a3432/utils/src/tree/AzExtTreeItem.ts#L154
context.telemetry.properties.subscriptionId = arg.subscription.subscriptionId;
} catch (e) {
// we don't want to block execution of the command just because we can't set the telemetry properties
// see https://github.com/microsoft/vscode-azureresourcegroups/issues/1080
}
addTreeItemValuesToMask(context, arg, 'command');
}
}
// handles items from v2 extensions that are shaped like:
// id: string;
// subscription: {
// subscriptionId: string;
// }
// we don't enforce this shape so it won't work in all cases, but for ACA we mostly follow this pattern
const [node, nodes] = unwrapArgs(args);
const allNodes = [node, ...nodes ?? []];
for (const node of allNodes) {
if (node && typeof node === 'object' && 'id' in node && typeof node.id === 'string') {
context.telemetry.properties.resourceId = node.id;
if ('subscription' in node && node.subscription && typeof node.subscription === 'object' && 'subscriptionId' in node.subscription && typeof node.subscription.subscriptionId === 'string') {
context.telemetry.properties.subscriptionId = node.subscription.subscriptionId;
}
}
}
}