feat(telemetry): add telemetry to collect submitted model number (#9)
This commit is contained in:
Родитель
987de6caab
Коммит
0ae6a04679
|
@ -39,7 +39,7 @@ steps:
|
|||
displayName: Compile Sources
|
||||
|
||||
- script: |
|
||||
# node scripts/genAiKey.js
|
||||
node scripts/genAiKey.js
|
||||
npm install -g vsce
|
||||
vsce package
|
||||
displayName: Build VSIX Package
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"description": "Author IoT Plug and Play models, publish and manage with Model Repository",
|
||||
"version": "0.0.1",
|
||||
"publisher": "vsciot-vscode",
|
||||
"aiKey": "5b869bc6-ca93-4f24-aa87-92871a3a616e",
|
||||
"preview": true,
|
||||
"aiKey": "25cf12e8-2dc7-4556-b54e-40fb9d1dfba1",
|
||||
"icon": "logo.png",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"engines": {
|
||||
|
@ -61,7 +62,7 @@
|
|||
],
|
||||
"configuration": [
|
||||
{
|
||||
"title": "IoT Plug and Play Configuration",
|
||||
"title": "IoT Plug and Play",
|
||||
"properties": {
|
||||
"azure-digital-twins.publicRepositoryUrl": {
|
||||
"type": "string",
|
||||
|
|
|
@ -3,7 +3,7 @@ const fs = require("fs");
|
|||
const PACKAGE_JSON_FILENAME = "package.json";
|
||||
|
||||
if (process.env.BUILD_SOURCEBRANCH) {
|
||||
const IS_PROD = new RegExp(proces.env.PROD_TAG).test(process.env.BUILD_SOURCEBRANCH);
|
||||
const IS_PROD = new RegExp(process.env.PROD_TAG).test(process.env.BUILD_SOURCEBRANCH);
|
||||
if (IS_PROD) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON_FILENAME));
|
||||
packageJson.aiKey = process.env.PROD_AIKEY;
|
|
@ -8,6 +8,9 @@ export class Constants {
|
|||
public static readonly EXTENSION_NAME = "azure-digital-twins";
|
||||
public static readonly CHANNEL_NAME = "IoT Plug and Play";
|
||||
public static readonly UTF8 = "utf8";
|
||||
public static readonly BASE64 = "base64";
|
||||
public static readonly SHA256 = "sha256";
|
||||
public static readonly HEX = "hex";
|
||||
public static readonly EMPTY_STRING = "";
|
||||
public static readonly DEFAULT_SEPARATOR = ",";
|
||||
public static readonly COMPLETION_TRIGGER = '"';
|
||||
|
|
|
@ -83,7 +83,7 @@ export class NSAT {
|
|||
};
|
||||
this.telemetryClient.sendEvent("nsat.survey/userAsked");
|
||||
const button = await window.showInformationMessage(
|
||||
"Do you mind taking a quick feedback survey about the Azure IoT Edge Extension for VS Code?",
|
||||
"Do you mind taking a quick feedback survey about IoT Plug and Play Extension for VS Code?",
|
||||
take,
|
||||
remind,
|
||||
never,
|
||||
|
|
|
@ -4,29 +4,14 @@
|
|||
import * as fs from "fs";
|
||||
import * as vscode from "vscode";
|
||||
import TelemetryReporter from "vscode-extension-telemetry";
|
||||
|
||||
/**
|
||||
* Operation result of telemetry
|
||||
*/
|
||||
export enum TelemetryResult {
|
||||
Succeeded = "Succeeded",
|
||||
Failed = "Failed",
|
||||
Cancelled = "Cancelled",
|
||||
}
|
||||
|
||||
/**
|
||||
* Context of telemetry
|
||||
*/
|
||||
export interface TelemetryContext {
|
||||
start: number;
|
||||
properties: { [key: string]: string };
|
||||
measurements: { [key: string]: number };
|
||||
}
|
||||
import { TelemetryContext } from "./telemetryContext";
|
||||
|
||||
/**
|
||||
* Telemetry client
|
||||
*/
|
||||
export class TelemetryClient {
|
||||
private static readonly IS_INTERNAL = "isInternal";
|
||||
|
||||
/**
|
||||
* validate content of package json
|
||||
* @param packageJSON package json
|
||||
|
@ -50,7 +35,7 @@ export class TelemetryClient {
|
|||
private isInternal: boolean = false;
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
const packageJSON = JSON.parse(fs.readFileSync(context.asAbsolutePath("./package.json"), "utf8"));
|
||||
if (!packageJSON || TelemetryClient.validatePackageJSON(packageJSON)) {
|
||||
if (!packageJSON || !TelemetryClient.validatePackageJSON(packageJSON)) {
|
||||
return;
|
||||
}
|
||||
this.extensionId = `${packageJSON.publisher}.${packageJSON.name}`;
|
||||
|
@ -69,49 +54,14 @@ export class TelemetryClient {
|
|||
return;
|
||||
}
|
||||
if (telemetryContext) {
|
||||
telemetryContext.setProperty(TelemetryClient.IS_INTERNAL, this.isInternal.toString());
|
||||
this.client.sendTelemetryEvent(eventName, telemetryContext.properties, telemetryContext.measurements);
|
||||
} else {
|
||||
this.client.sendTelemetryEvent(eventName);
|
||||
const properties = { [TelemetryClient.IS_INTERNAL]: this.isInternal.toString() };
|
||||
this.client.sendTelemetryEvent(eventName, properties);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create telemetry context
|
||||
*/
|
||||
public createContext(): TelemetryContext {
|
||||
const context: TelemetryContext = { start: Date.now(), properties: {}, measurements: {} };
|
||||
context.properties.isInternal = this.isInternal.toString();
|
||||
context.properties.result = TelemetryResult.Succeeded;
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* set telemetry context as error
|
||||
* @param context telemetry context
|
||||
* @param error error
|
||||
*/
|
||||
public setErrorContext(context: TelemetryContext, error: Error): void {
|
||||
context.properties.result = TelemetryResult.Failed;
|
||||
context.properties.error = error.name;
|
||||
context.properties.errorMessage = error.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* set telemetry context as cancel
|
||||
* @param context telemetry context
|
||||
*/
|
||||
public setCancelContext(context: TelemetryContext): void {
|
||||
context.properties.result = TelemetryResult.Cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* close telemetry context
|
||||
* @param context telemetry context
|
||||
*/
|
||||
public closeContext(context: TelemetryContext) {
|
||||
context.measurements.duration = (Date.now() - context.start) / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* dispose telemetry client
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { UserCancelledError } from "./userCancelledError";
|
||||
|
||||
/**
|
||||
* Operation result
|
||||
*/
|
||||
enum OperationResult {
|
||||
Success = "Succeeded",
|
||||
Fail = "Failed",
|
||||
Cancel = "Cancelled",
|
||||
}
|
||||
|
||||
/**
|
||||
* Telemetry context
|
||||
*/
|
||||
export class TelemetryContext {
|
||||
/**
|
||||
* start a new context
|
||||
*/
|
||||
public static startNew(): TelemetryContext {
|
||||
return new TelemetryContext();
|
||||
}
|
||||
|
||||
public properties: { [key: string]: string };
|
||||
public measurements: { [key: string]: number };
|
||||
|
||||
private start: number;
|
||||
private constructor() {
|
||||
this.start = Date.now();
|
||||
this.properties = {};
|
||||
this.measurements = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* set property
|
||||
* @param name property name
|
||||
* @param value property value
|
||||
*/
|
||||
public setProperty(name: string, value: string): void {
|
||||
this.properties[name] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* set error
|
||||
* @param error error
|
||||
*/
|
||||
public setError(error: Error): void {
|
||||
if (error instanceof UserCancelledError) {
|
||||
this.properties.result = OperationResult.Cancel;
|
||||
} else {
|
||||
this.properties.result = OperationResult.Fail;
|
||||
this.properties.error = error.name;
|
||||
this.properties.errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* end the context
|
||||
*/
|
||||
public end(): void {
|
||||
if (!this.properties.result) {
|
||||
this.properties.result = OperationResult.Success;
|
||||
}
|
||||
this.measurements.duration = (Date.now() - this.start) / 1000;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { createHash } from "crypto";
|
||||
import * as fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import { DeviceModelManager, ModelType } from "../deviceModel/deviceModelManager";
|
||||
|
@ -143,5 +144,15 @@ export class Utility {
|
|||
return fs.readJson(filePath, { encoding: Constants.UTF8 });
|
||||
}
|
||||
|
||||
/**
|
||||
* get hash value of payload
|
||||
* @param payload payload
|
||||
*/
|
||||
public static hash(payload: string): string {
|
||||
return createHash(Constants.SHA256)
|
||||
.update(payload)
|
||||
.digest(Constants.HEX);
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ import { Command } from "./common/command";
|
|||
import { Constants } from "./common/constants";
|
||||
import { NSAT } from "./common/nsat";
|
||||
import { ProcessError } from "./common/processError";
|
||||
import { TelemetryClient, TelemetryContext } from "./common/telemetryClient";
|
||||
import { TelemetryClient } from "./common/telemetryClient";
|
||||
import { TelemetryContext } from "./common/telemetryContext";
|
||||
import { UserCancelledError } from "./common/userCancelledError";
|
||||
import { DeviceModelManager, ModelType } from "./deviceModel/deviceModelManager";
|
||||
import { DigitalTwinCompletionItemProvider } from "./intelliSense/digitalTwinCompletionItemProvider";
|
||||
|
@ -24,7 +25,12 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
const telemetryClient = new TelemetryClient(context);
|
||||
const nsat = new NSAT(Constants.NSAT_SURVEY_URL, telemetryClient);
|
||||
const deviceModelManager = new DeviceModelManager(context, outputChannel);
|
||||
const modelRepositoryManager = new ModelRepositoryManager(context, outputChannel, Constants.WEB_VIEW_PATH);
|
||||
const modelRepositoryManager = new ModelRepositoryManager(
|
||||
context,
|
||||
Constants.WEB_VIEW_PATH,
|
||||
outputChannel,
|
||||
telemetryClient,
|
||||
);
|
||||
const apiProvider = new ApiProvider(modelRepositoryManager);
|
||||
|
||||
telemetryClient.sendEvent(Constants.EXTENSION_ACTIVATED_MSG);
|
||||
|
@ -171,15 +177,15 @@ function initCommand(
|
|||
): void {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(command, async (...args: any[]) => {
|
||||
const telemetryContext: TelemetryContext = telemetryClient.createContext();
|
||||
const telemetryContext: TelemetryContext = TelemetryContext.startNew();
|
||||
telemetryClient.sendEvent(`${command}.start`);
|
||||
try {
|
||||
return await callback(...args);
|
||||
} catch (error) {
|
||||
telemetryContext.setError(error);
|
||||
if (error instanceof UserCancelledError) {
|
||||
outputChannel.warn(error.message);
|
||||
} else {
|
||||
telemetryClient.setErrorContext(telemetryContext, error);
|
||||
UI.showNotification(MessageType.Error, error.message);
|
||||
if (error instanceof ProcessError) {
|
||||
const message = `${error.message}\n${error.stack}`;
|
||||
|
@ -189,7 +195,7 @@ function initCommand(
|
|||
}
|
||||
}
|
||||
} finally {
|
||||
telemetryClient.closeContext(telemetryContext);
|
||||
telemetryContext.end();
|
||||
telemetryClient.sendEvent(`${command}.end`, telemetryContext);
|
||||
outputChannel.show();
|
||||
if (enableSurvey) {
|
||||
|
|
|
@ -7,12 +7,6 @@ import { ModelType } from "../deviceModel/deviceModelManager";
|
|||
import { GetResult, MetaModelType, SearchOptions, SearchResult } from "./modelRepositoryInterface";
|
||||
import { RepositoryInfo } from "./modelRepositoryManager";
|
||||
|
||||
const ETAG_HEADER = "etag";
|
||||
const MODEL_ID_HEADER = "x-ms-model-id";
|
||||
const MODEL_PATH = "models";
|
||||
const SEARCH_PATH = "models/search";
|
||||
const CONTENT_TYPE = "application/json";
|
||||
|
||||
/**
|
||||
* Http method type
|
||||
*/
|
||||
|
@ -42,8 +36,8 @@ export class ModelRepositoryClient {
|
|||
request(options)
|
||||
.then((response) => {
|
||||
const result: GetResult = {
|
||||
etag: response.headers[ETAG_HEADER],
|
||||
modelId: response.headers[MODEL_ID_HEADER],
|
||||
etag: response.headers[ModelRepositoryClient.ETAG_HEADER],
|
||||
modelId: response.headers["x-ms-model-id"],
|
||||
content: response.body,
|
||||
};
|
||||
return resolve(result);
|
||||
|
@ -102,7 +96,7 @@ export class ModelRepositoryClient {
|
|||
return new Promise<string>((resolve, reject) => {
|
||||
request(options)
|
||||
.then((response) => {
|
||||
const result: string = response.headers[ETAG_HEADER];
|
||||
const result: string = response.headers[ModelRepositoryClient.ETAG_HEADER];
|
||||
return resolve(result);
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -129,6 +123,8 @@ export class ModelRepositoryClient {
|
|||
});
|
||||
}
|
||||
|
||||
private static readonly ETAG_HEADER = "etag";
|
||||
|
||||
/**
|
||||
* convert to meta model type
|
||||
* @param type model type
|
||||
|
@ -152,8 +148,8 @@ export class ModelRepositoryClient {
|
|||
*/
|
||||
private static createOptions(method: HttpMethod, repoInfo: RepositoryInfo, modelId?: string): request.OptionsWithUri {
|
||||
const uri = modelId
|
||||
? `${repoInfo.hostname}/${MODEL_PATH}/${encodeURIComponent(modelId)}`
|
||||
: `${repoInfo.hostname}/${SEARCH_PATH}`;
|
||||
? `${repoInfo.hostname}/models/${encodeURIComponent(modelId)}`
|
||||
: `${repoInfo.hostname}/models/search`;
|
||||
const qs: any = { "api-version": repoInfo.apiVersion };
|
||||
if (repoInfo.repositoryId) {
|
||||
qs.repositoryId = repoInfo.repositoryId;
|
||||
|
@ -165,7 +161,7 @@ export class ModelRepositoryClient {
|
|||
qs,
|
||||
encoding: Constants.UTF8,
|
||||
json: true,
|
||||
headers: { "Authorization": accessToken, "Content-Type": CONTENT_TYPE },
|
||||
headers: { "Authorization": accessToken, "Content-Type": "application/json" },
|
||||
resolveWithFullResponse: true,
|
||||
};
|
||||
return options;
|
||||
|
|
|
@ -1,24 +1,9 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { createHmac, Hmac } from "crypto";
|
||||
import { createHmac } from "crypto";
|
||||
import { Constants } from "../common/constants";
|
||||
|
||||
const PROPERTY_SEPARATOR = ";";
|
||||
const KEY_VALUE_SEPARATOR = "=";
|
||||
const PROPERTY_COUNT = 4;
|
||||
const HOSTNAME_PROPERTY = "HostName";
|
||||
const REPOSITORY_ID_PROPERTY = "RepositoryId";
|
||||
const SHARED_ACCESS_KEY_NAME_PROPERTY = "SharedAccessKeyName";
|
||||
const SHARED_ACCESS_KEY_PROPERTY = "SharedAccessKey";
|
||||
const HOSTNAME_REGEX = new RegExp("[a-zA-Z0-9_\\-\\.]+$");
|
||||
const SHARED_ACCESS_KEY_NAME_REGEX = new RegExp("^[a-zA-Z0-9_\\-@\\.]+$");
|
||||
const BASE64 = "base64";
|
||||
const SHA256 = "sha256";
|
||||
const MILLISECONDS = 1000;
|
||||
const EXPIRY_IN_MINUTES = 30;
|
||||
const SECONDS_PER_MINUTE = 60;
|
||||
|
||||
/**
|
||||
* Model repository connection
|
||||
*/
|
||||
|
@ -32,12 +17,12 @@ export class ModelRepositoryConnection {
|
|||
throw new Error(`Connection string ${Constants.NOT_EMPTY_MSG}`);
|
||||
}
|
||||
const map: { [key: string]: string } = {};
|
||||
const properties: string[] = connectionString.split(PROPERTY_SEPARATOR);
|
||||
if (properties.length !== PROPERTY_COUNT) {
|
||||
const properties: string[] = connectionString.split(";");
|
||||
if (properties.length !== ModelRepositoryConnection.PROPERTY_COUNT) {
|
||||
throw new Error(Constants.CONNECTION_STRING_INVALID_FORMAT_MSG);
|
||||
}
|
||||
for (const property of properties) {
|
||||
const index: number = property.indexOf(KEY_VALUE_SEPARATOR);
|
||||
const index: number = property.indexOf("=");
|
||||
if (index <= 0) {
|
||||
throw new Error(Constants.CONNECTION_STRING_INVALID_FORMAT_MSG);
|
||||
}
|
||||
|
@ -50,15 +35,24 @@ export class ModelRepositoryConnection {
|
|||
}
|
||||
// validate connection
|
||||
const connection = new ModelRepositoryConnection(
|
||||
map[HOSTNAME_PROPERTY],
|
||||
map[REPOSITORY_ID_PROPERTY],
|
||||
map[SHARED_ACCESS_KEY_NAME_PROPERTY],
|
||||
map[SHARED_ACCESS_KEY_PROPERTY],
|
||||
map[ModelRepositoryConnection.HOSTNAME_PROPERTY],
|
||||
map[ModelRepositoryConnection.REPOSITORY_ID_PROPERTY],
|
||||
map[ModelRepositoryConnection.SHARED_ACCESS_KEY_NAME_PROPERTY],
|
||||
map[ModelRepositoryConnection.SHARED_ACCESS_KEY_PROPERTY],
|
||||
);
|
||||
connection.validate();
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static readonly PROPERTY_COUNT = 4;
|
||||
private static readonly HOSTNAME_PROPERTY = "HostName";
|
||||
private static readonly REPOSITORY_ID_PROPERTY = "RepositoryId";
|
||||
private static readonly SHARED_ACCESS_KEY_NAME_PROPERTY = "SharedAccessKeyName";
|
||||
private static readonly SHARED_ACCESS_KEY_PROPERTY = "SharedAccessKey";
|
||||
private static readonly HOSTNAME_REGEX = new RegExp("[a-zA-Z0-9_\\-\\.]+$");
|
||||
private static readonly SHARED_ACCESS_KEY_NAME_REGEX = new RegExp("^[a-zA-Z0-9_\\-@\\.]+$");
|
||||
private static readonly EXPIRY_IN_MINUTES = 30;
|
||||
|
||||
private readonly expiry: string;
|
||||
private constructor(
|
||||
public readonly hostName: string,
|
||||
|
@ -67,7 +61,7 @@ export class ModelRepositoryConnection {
|
|||
public readonly sharedAccessKey: string,
|
||||
) {
|
||||
const now: number = new Date().getTime();
|
||||
this.expiry = (Math.round(now / MILLISECONDS) + EXPIRY_IN_MINUTES * SECONDS_PER_MINUTE).toString();
|
||||
this.expiry = (Math.round(now / 1000) + ModelRepositoryConnection.EXPIRY_IN_MINUTES * 60).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,10 +71,12 @@ export class ModelRepositoryConnection {
|
|||
const endpoint: string = encodeURIComponent(this.hostName);
|
||||
const payload: string = [encodeURIComponent(this.repositoryId), endpoint, this.expiry].join("\n").toLowerCase();
|
||||
const signature: Buffer = Buffer.from(payload, Constants.UTF8);
|
||||
const secret: Buffer = Buffer.from(this.sharedAccessKey, BASE64);
|
||||
const hmac: Hmac = createHmac(SHA256, secret);
|
||||
hmac.update(signature);
|
||||
const hash: string = encodeURIComponent(hmac.digest(BASE64));
|
||||
const secret: Buffer = Buffer.from(this.sharedAccessKey, Constants.BASE64);
|
||||
const hash: string = encodeURIComponent(
|
||||
createHmac(Constants.SHA256, secret)
|
||||
.update(signature)
|
||||
.digest(Constants.BASE64),
|
||||
);
|
||||
return (
|
||||
"SharedAccessSignature " +
|
||||
`sr=${endpoint}&sig=${hash}&se=${this.expiry}&skn=${this.sharedAccessKeyName}&rid=${this.repositoryId}`
|
||||
|
@ -91,19 +87,28 @@ export class ModelRepositoryConnection {
|
|||
* validate model repository connection
|
||||
*/
|
||||
private validate(): void {
|
||||
if (!this.hostName || !HOSTNAME_REGEX.test(this.hostName)) {
|
||||
throw new Error(`${Constants.CONNECTION_STRING_INVALID_FORMAT_MSG} on property ${HOSTNAME_PROPERTY}`);
|
||||
if (!this.hostName || !ModelRepositoryConnection.HOSTNAME_REGEX.test(this.hostName)) {
|
||||
throw new Error(
|
||||
`${Constants.CONNECTION_STRING_INVALID_FORMAT_MSG} on property ${ModelRepositoryConnection.HOSTNAME_PROPERTY}`,
|
||||
);
|
||||
}
|
||||
if (!this.repositoryId) {
|
||||
throw new Error(`${Constants.CONNECTION_STRING_INVALID_FORMAT_MSG} on property ${REPOSITORY_ID_PROPERTY}`);
|
||||
}
|
||||
if (!this.sharedAccessKeyName || !SHARED_ACCESS_KEY_NAME_REGEX.test(this.sharedAccessKeyName)) {
|
||||
throw new Error(
|
||||
`${Constants.CONNECTION_STRING_INVALID_FORMAT_MSG} on property ${SHARED_ACCESS_KEY_NAME_PROPERTY}`,
|
||||
`${Constants.CONNECTION_STRING_INVALID_FORMAT_MSG} on property ${ModelRepositoryConnection.REPOSITORY_ID_PROPERTY}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!this.sharedAccessKeyName ||
|
||||
!ModelRepositoryConnection.SHARED_ACCESS_KEY_NAME_REGEX.test(this.sharedAccessKeyName)
|
||||
) {
|
||||
throw new Error(
|
||||
`${Constants.CONNECTION_STRING_INVALID_FORMAT_MSG} on property ${ModelRepositoryConnection.SHARED_ACCESS_KEY_NAME_PROPERTY}`,
|
||||
);
|
||||
}
|
||||
if (!this.sharedAccessKey) {
|
||||
throw new Error(`${Constants.CONNECTION_STRING_INVALID_FORMAT_MSG} on property ${SHARED_ACCESS_KEY_PROPERTY}`);
|
||||
throw new Error(
|
||||
`${Constants.CONNECTION_STRING_INVALID_FORMAT_MSG} on property ${ModelRepositoryConnection.SHARED_ACCESS_KEY_PROPERTY}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,16 @@ import * as vscode from "vscode";
|
|||
import { VSCExpress } from "vscode-express";
|
||||
import { BadRequestError } from "../common/badRequestError";
|
||||
import { ColorizedChannel } from "../common/colorizedChannel";
|
||||
import { Command } from "../common/command";
|
||||
import { Configuration } from "../common/configuration";
|
||||
import { Constants } from "../common/constants";
|
||||
import { CredentialStore } from "../common/credentialStore";
|
||||
import { ProcessError } from "../common/processError";
|
||||
import { TelemetryClient } from "../common/telemetryClient";
|
||||
import { TelemetryContext } from "../common/telemetryContext";
|
||||
import { UserCancelledError } from "../common/userCancelledError";
|
||||
import { Utility } from "../common/utility";
|
||||
import { ModelType } from "../deviceModel/deviceModelManager";
|
||||
import { DeviceModelManager, ModelType } from "../deviceModel/deviceModelManager";
|
||||
import { DigitalTwinConstants } from "../intelliSense/digitalTwinConstants";
|
||||
import { ChoiceType, MessageType, UI } from "../view/ui";
|
||||
import { UIConstants } from "../view/uiConstants";
|
||||
|
@ -139,7 +142,12 @@ export class ModelRepositoryManager {
|
|||
|
||||
private readonly express: VSCExpress;
|
||||
private readonly component: string;
|
||||
constructor(context: vscode.ExtensionContext, private readonly outputChannel: ColorizedChannel, filePath: string) {
|
||||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
filePath: string,
|
||||
private readonly outputChannel: ColorizedChannel,
|
||||
private readonly telemetryClient: TelemetryClient,
|
||||
) {
|
||||
this.express = new VSCExpress(context, filePath);
|
||||
this.component = Constants.MODEL_REPOSITORY_COMPONENT;
|
||||
}
|
||||
|
@ -197,8 +205,8 @@ export class ModelRepositoryManager {
|
|||
* submit files to model repository
|
||||
*/
|
||||
public async submitFiles(): Promise<void> {
|
||||
const files: string[] | undefined = await UI.selectModelFiles(UIConstants.SELECT_MODELS_LABEL);
|
||||
if (!files || files.length === 0) {
|
||||
const files: string[] = await UI.selectModelFiles(UIConstants.SELECT_MODELS_LABEL);
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
// check unsaved files and save
|
||||
|
@ -213,13 +221,16 @@ export class ModelRepositoryManager {
|
|||
}
|
||||
}
|
||||
|
||||
const usageData = new Map<ModelType, string[]>();
|
||||
try {
|
||||
const repoInfo: RepositoryInfo = await ModelRepositoryManager.createRepositoryInfo(false);
|
||||
await this.doSubmitLoopSilently(repoInfo, files);
|
||||
await this.doSubmitLoopSilently(repoInfo, files, usageData);
|
||||
} catch (error) {
|
||||
const operation = `Submit models to ${RepositoryType.Company}`;
|
||||
throw new ProcessError(operation, error, this.component);
|
||||
}
|
||||
// send usage data
|
||||
this.sendUsageDataOfSubmit(usageData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -414,15 +425,20 @@ export class ModelRepositoryManager {
|
|||
* submit model files silently, fault tolerant and don't throw exception
|
||||
* @param repoInfo repository info
|
||||
* @param files model file list
|
||||
* @param usageData usage data
|
||||
*/
|
||||
private async doSubmitLoopSilently(repoInfo: RepositoryInfo, files: string[]): Promise<void> {
|
||||
const option: SubmitOptions = { overwrite: false };
|
||||
private async doSubmitLoopSilently(
|
||||
repoInfo: RepositoryInfo,
|
||||
files: string[],
|
||||
usageData: Map<ModelType, string[]>,
|
||||
): Promise<void> {
|
||||
const options: SubmitOptions = { overwrite: false };
|
||||
for (const file of files) {
|
||||
const operation = `Submit file ${file}`;
|
||||
this.outputChannel.start(operation, this.component);
|
||||
|
||||
try {
|
||||
await this.doSubmitModel(repoInfo, file, option);
|
||||
await this.doSubmitModel(repoInfo, file, options, usageData);
|
||||
this.outputChannel.end(operation, this.component);
|
||||
} catch (error) {
|
||||
this.outputChannel.error(operation, this.component, error);
|
||||
|
@ -433,12 +449,19 @@ export class ModelRepositoryManager {
|
|||
/**
|
||||
* submit model file to repository
|
||||
* @param repoInfo repository info
|
||||
* @param file model file
|
||||
* @param option submit options
|
||||
* @param filePath model file path
|
||||
* @param options submit options
|
||||
* @param usageData usage data
|
||||
*/
|
||||
private async doSubmitModel(repoInfo: RepositoryInfo, file: string, option: SubmitOptions): Promise<void> {
|
||||
const content = await Utility.getJsonContent(file);
|
||||
private async doSubmitModel(
|
||||
repoInfo: RepositoryInfo,
|
||||
filePath: string,
|
||||
option: SubmitOptions,
|
||||
usageData: Map<ModelType, string[]>,
|
||||
): Promise<void> {
|
||||
const content = await Utility.getJsonContent(filePath);
|
||||
const modelId: string = content[DigitalTwinConstants.ID];
|
||||
const modelType: ModelType = DeviceModelManager.convertToModelType(content[DigitalTwinConstants.TYPE]);
|
||||
let result: GetResult | undefined;
|
||||
try {
|
||||
result = await ModelRepositoryClient.getModel(repoInfo, modelId, true);
|
||||
|
@ -467,5 +490,39 @@ export class ModelRepositoryManager {
|
|||
}
|
||||
}
|
||||
await ModelRepositoryClient.updateModel(repoInfo, modelId, content);
|
||||
|
||||
// record submitted model id
|
||||
let modelIds: string[] | undefined = usageData.get(modelType);
|
||||
if (!modelIds) {
|
||||
modelIds = [];
|
||||
usageData.set(modelType, modelIds);
|
||||
}
|
||||
modelIds.push(modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* send usage data of SubmitFiles
|
||||
* @param usageData usage data
|
||||
*/
|
||||
private sendUsageDataOfSubmit(usageData: Map<ModelType, string[]>): void {
|
||||
const telemetryContext: TelemetryContext = TelemetryContext.startNew();
|
||||
let propertyName: string = Constants.EMPTY_STRING;
|
||||
let propertyValue: string;
|
||||
for (const [key, value] of usageData) {
|
||||
switch (key) {
|
||||
case ModelType.Interface:
|
||||
propertyName = "interfaceId";
|
||||
break;
|
||||
case ModelType.CapabilityModel:
|
||||
propertyName = "capabilityModelId";
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (propertyName) {
|
||||
propertyValue = value.map((id) => Utility.hash(id)).join(Constants.DEFAULT_SEPARATOR);
|
||||
telemetryContext.setProperty(propertyName, propertyValue);
|
||||
}
|
||||
}
|
||||
this.telemetryClient.sendEvent(`${Command.SubmitFiles}.data`, telemetryContext);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -192,11 +192,11 @@ export class UI {
|
|||
* @param label label
|
||||
* @param type model type
|
||||
*/
|
||||
public static async selectModelFiles(label: string, type?: ModelType): Promise<string[] | undefined> {
|
||||
public static async selectModelFiles(label: string, type?: ModelType): Promise<string[]> {
|
||||
const fileInfos: ModelFileInfo[] = await UI.findModelFiles(type);
|
||||
if (fileInfos.length === 0) {
|
||||
UI.showNotification(MessageType.Warn, UIConstants.MODELS_NOT_FOUND_MSG);
|
||||
return undefined;
|
||||
return [];
|
||||
}
|
||||
const items: Array<QuickPickItemWithData<string>> = fileInfos.map((f) => {
|
||||
return {
|
||||
|
|
Загрузка…
Ссылка в новой задаче